chen.lin
2 天以前 9140aee230de0ef41de9682a9353fbd372e2bcaa
云仓WMS接口
57个文件已添加
52个文件已修改
4214 ■■■■ 已修改文件
rsf-admin/src/config/MyDataProvider.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/MyMenu.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderItemLogList.jsx 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogCreate.jsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogEdit.jsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogList.jsx 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogListBase.jsx 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogPanel.jsx 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogShow.jsx 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/outStockOrderLog/OutStockOrderLogList.jsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/histories/outStockOrderLog/index.jsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/openApiApp/OpenApiAppCreate.jsx 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/openApiApp/OpenApiAppEdit.jsx 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/openApiApp/OpenApiAppList.jsx 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/openApiApp/index.jsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/OpenApi.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/ApiSecurityConfig.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/CryptoConfig.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/CustomErrorAttributes.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/FeignConfig.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/GlobalExceptionHandler.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/WebMvcConfig.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/AuthController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsErpController.java 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/AppController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/App.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/constant/Constants.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/constant/WmsConstant.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/dto/CommonResponse.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/dto/ResultData.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/ErpMatnrParms.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/ErpOpParams.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/GetTokenParam.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/WmsOrderItemParam.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/erp/ErpReportFeignClient.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/wms/WmsServerFeignClient.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/wms/fallback/WmsServerFeignClientFallback.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/wms/fallback/WmsServerFeignClientFallbackFactory.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/AppMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/filter/AppIdAuthenticationFilter.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/service/AppAuthService.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/utils/TokenUtils.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/AppService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/TokenService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/WmsErpService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/AppServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/TokenServiceImpl.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/WmsErpServiceImpl.java 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-dev.yml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-prod.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/pom.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ServerBoot.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RemotesInfoProperties.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/CloudWmsMockController.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/ErpQueryController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/BaseMatParms.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/FlexibleDateDeserializer.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InOutResultReportParam.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryAdjustReportParam.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryDetailsParam.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventorySummaryParam.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncOrderParams.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/fallback/CloudWmsErpFeignClientFallback.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/fallback/CloudWmsErpFeignClientFallbackFactory.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/CloudWmsReportService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/ReceiveMsgService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/CloudWmsReportServiceImpl.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/ScheduleTriggerController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/CheckOrderParams.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/AsnOrderItemLog.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/CloudWmsNotifyLog.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WkOrderItem.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/excel/AsnOrderTemplate.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/excel/TransferTemplate.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/AsnOrderItemMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/AsnOrderMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/CloudWmsNotifyLogMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/AsnOrderLogSchedule.java 112 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/CloudWmsNotifySchedule.java 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/CloudWmsNotifyLogService.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/CloudWmsNotifyLogServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/ReviseLogServiceImpl.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/GlobalConfigCode.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/OpenApiAppController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/OpenApiApp.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/OpenApiAppMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/OpenApiAppService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/OpenApiAppServiceImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-dev.yml 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/manager/AsnOrderItemMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/manager/AsnOrderMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/test/java/com/vincent/rsf/server/common/security/SecurityDemoControllerTest.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/cloud_wms_notify_config.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/man_cloud_wms_notify_log.sql 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/open_api_app.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/open_api_app_menu.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/out_stock_order_log_menu.sql 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/config/MyDataProvider.js
@@ -1,14 +1,17 @@
import request from "../utils/request";
import * as Common from "../utils/common";
// 出库历史单与入库历史单共用 asnOrderLog 接口,仅前端 resource 不同
const getApiResource = (resource) => (resource === "outStockOrderLog" ? "asnOrderLog" : resource);
const MyDataProvider = {
  // *** https://marmelab.com/react-admin/DataProviderWriting.html ***
  // get a list of records based on sort, filter, and pagination
  getList: async (resource, params) => {
    // console.log("getList", resource, params);
    const apiResource = getApiResource(resource);
    const _params = Common.integrateParams(params);
    const res = await request.post(resource + "/page", _params);
    const res = await request.post(apiResource + "/page", _params);
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -21,8 +24,8 @@
  // get a single record by id
  getOne: async (resource, params) => {
    // console.log("getOne", resource, params);
    const res = await request.get(resource + "/" + params.id);
    const apiResource = getApiResource(resource);
    const res = await request.get(apiResource + "/" + params.id);
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -37,11 +40,11 @@
  // get a list of records based on an array of ids
  getMany: async (resource, params) => {
    if (resource === "user") {
      await new Promise((r) => setTimeout(r, 1000));
    }
    const res = await request.post(resource + "/many/" + params.ids);
    const apiResource = getApiResource(resource);
    const res = await request.post(apiResource + "/many/" + params.ids);
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -63,7 +66,8 @@
  // create a record
  create: async (resource, params) => {
    const res = await request.post(resource + "/save", params?.data);
    const apiResource = getApiResource(resource);
    const res = await request.post(apiResource + "/save", params?.data);
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -77,7 +81,8 @@
  // update a record based on a patch
  update: async (resource, params) => {
    const res = await request.post(resource + "/update", {
    const apiResource = getApiResource(resource);
    const res = await request.post(apiResource + "/update", {
      id: params.id,
      ...params.data,
    });
@@ -92,9 +97,9 @@
  // update a list of records based on an array of ids and a common patch
  updateMany: async (resource, params) => {
    console.log("updateMany", resource, params);
    const apiResource = getApiResource(resource);
    const res = await request.post(
      resource + "/update/many",
      apiResource + "/update/many",
      params.ids.map((id) => ({ id, ...params.data })),
    );
    const { code, msg, data } = res.data;
@@ -108,8 +113,8 @@
  // delete a record by id
  delete: async (resource, params) => {
    console.log("delete", resource, params);
    const res = await request.post(resource + "/remove/" + [params.id]);
    const apiResource = getApiResource(resource);
    const res = await request.post(apiResource + "/remove/" + [params.id]);
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -123,8 +128,8 @@
  // delete a list of records based on an array of ids
  deleteMany: async (resource, params) => {
    console.log("deleteMany", resource, params);
    const res = await request.post(resource + "/remove/" + params?.ids);
    const apiResource = getApiResource(resource);
    const res = await request.post(apiResource + "/remove/" + params?.ids);
    const { code, msg, data } = res.data;
    if (code === 200) {
      return Promise.resolve({
@@ -136,9 +141,10 @@
  // export excel from all data
  export: async (resource, params) => {
    const apiResource = getApiResource(resource);
    const _params = Common.integrateParams(params);
    try {
      const res = await request.post(`${resource}/export`, _params, {
      const res = await request.post(`${apiResource}/export`, _params, {
        responseType: "blob",
      });
      return res;
rsf-admin/src/i18n/en.js
@@ -174,6 +174,7 @@
        asnOrder: 'AsnOrder',
        asnOrderItem: 'AsnOrderItem',
        asnOrderLog: 'asnOrderLog',
        outStockOrderLog: 'Outbound Order Log',
        asnOrderItemLog: 'asnOrderItemLog',
        purchase: 'Purchase',
        purchaseItem: 'PurchaseItem',
rsf-admin/src/i18n/zh.js
@@ -3,6 +3,9 @@
const customChineseMessages = {
    ...chineseMessages,
    hello: '你好世界',
    resources: {
        config: { name: '配置参数' },
    },
    common: {
        response: {
            success: "操作成功",
@@ -172,9 +175,10 @@
        companys: '往来企业',
        serialRuleItem: '编码规则子表',
        serialRule: '编码规则',
        asnOrder: '收货通知单',
        asnOrder: '入库通知单',
        asnOrderItem: '收货明细',
        asnOrderLog: '收货历史单',
        asnOrderLog: '入库历史单',
        outStockOrderLog: '出库历史单',
        asnOrderItemLog: '收货历史明细',
        purchase: 'PO单',
        purchaseItem: 'PO单明细',
@@ -202,7 +206,7 @@
        logs: '日志',
        permissions: '权限管理',
        delivery: 'DO单',
        outStock: '出库单',
        outStock: '出库通知单',
        outStockItem: '出库单明细',
        inStockPoces: '入库管理',
        outStockPoces: '出库管理',
@@ -647,7 +651,7 @@
                maxWeight: "最大重量",
            },
            asnOrder: {
                code: "ASN单号",
                code: "WMS单号",
                poCode: "PO编码",
                poId: "PO标识",
                type: "单据类型",
@@ -1389,6 +1393,14 @@
        selectWave: '波次规则',
    },
    ra: {
        boolean: {
            true: '正常',
            false: '禁用',
        },
        message: {
            delete_title: '删除 %{name} #%{id}',
            delete_content: '您确定要删除此项吗?',
        },
        action: {
            search: '搜索',
            add_filter: '过滤条件',
rsf-admin/src/layout/MyMenu.jsx
@@ -81,7 +81,7 @@
      } else {
        if (node.component) {
          // RCS测试:指向独立页路由,可单独打开一整页(无侧边栏/标签栏)
          const to = node.component === 'rcsTest' ? '/rcsTest-page' : node.component;
          const to = node.component === 'rcsTest' ? '/rcsTest-page' : `/${node.component}`;
          return (
            <MenuItemLink
              key={node.id}
rsf-admin/src/page/ResourceContent.js
@@ -38,6 +38,7 @@
import waitPakin from "./waitPakin";
import waitPakinLog from "./histories/waitPakinLog";
import asnOrderLog from "./histories/asnOrderLog";
import outStockOrderLog from "./histories/outStockOrderLog";
import task from "./task";
import taskLog from "./histories/taskLog";
import stock from "./orders/stock";
@@ -65,6 +66,7 @@
import inStatisticItem from './statistics/inStockItem';
import statisticCount from './statistics/stockStatisticNum';
import rcsTest from './rcsTest';
import openApiApp from './system/openApiApp';
const ResourceContent = (node) => {
  switch (node.component) {
@@ -120,6 +122,8 @@
      return asnOrder;
    case "asnOrderLog":
      return asnOrderLog;
    case "outStockOrderLog":
      return outStockOrderLog;
    case "purchase":
      return purchase;
    case "fields":
@@ -190,6 +194,8 @@
      return statisticCount;
    case "rcsTest":
      return rcsTest;
    case "openApiApp":
      return openApiApp;
    default:
      return {
        list: ListGuesser,
rsf-admin/src/page/histories/asnOrderLog/AsnOrderItemLogList.jsx
@@ -86,12 +86,12 @@
    <TextInput source="trackCode" label="table.field.asnOrderItemLog.trackCode" />,
    <TextInput source="barcode" label="table.field.asnOrderItemLog.barcode" />,
    <TextInput source="packName" label="table.field.asnOrderItemLog.packName" />,
    <SelectInput source="ntyStatus" label="table.field.asnOrderItemLog.ntyStatus"
        choices={[
            { id: 0, name: ' 未上报' },
            { id: 1, name: ' 已上报' },
        ]}
    />,
    // <SelectInput source="ntyStatus" label="table.field.asnOrderItemLog.ntyStatus"
    //     choices={[
    //         { id: 0, name: ' 未上报' },
    //         { id: 1, name: ' 已上报' },
    //     ]}
    // />,
    <TextInput label="common.field.memo" source="memo" />,
    <SelectInput
@@ -105,11 +105,16 @@
    />,
]
const AsnOrderItemLogList = () => {
/**
 * @param {Object} props
 * @param {number} [props.logId] - 入库历史单主键,传入时只显示该单的明细(用于详情页)
 */
const AsnOrderItemLogList = ({ logId: logIdProp }) => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const recodeId = useGetRecordId();
    const recordId = useGetRecordId();
    const logId = logIdProp != null ? logIdProp : recordId;
    return (
        <Box display="flex">
@@ -126,7 +131,7 @@
                title={"menu.asnOrderItemLog"}
                empty={false}
                filters={filters}
                filter={{ logId: recodeId }}
                filter={{ logId }}
                sort={{ field: "create_time", order: "desc" }}
                actions={(
                    <TopToolbar>
@@ -163,7 +168,7 @@
                    <TextField source="qrcode" label="table.field.asnOrderItemLog.qrcode" />
                    <TextField source="trackCode" label="table.field.asnOrderItemLog.trackCode" />
                    <TextField source="packName" label="table.field.asnOrderItemLog.packName" />
                    <TextField source="ntyStatus$" label="table.field.asnOrderItemLog.ntyStatus" sortable={false} />
                    {/*<TextField source="ntyStatus$" label="table.field.asnOrderItemLog.ntyStatus" sortable={false} />*/}
                    <TextField source="updateBy$" label="common.field.updateBy" />
                    <TextField source="createBy$" label="common.field.createBy" />
                    <DateField source="createTime" label="common.field.createTime" showTime />
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogCreate.jsx
@@ -158,6 +158,7 @@
                                        ]}
                                    />
                                </Grid>
                                {/* 质检上报状态
                                <Grid item xs={6} display="flex" gap={1}>
                                    <SelectInput
                                        label="table.field.asnOrderLog.ntyStatus"
@@ -169,6 +170,7 @@
                                        ]}
                                    />
                                </Grid>
                                */}
                                <Grid item xs={6} display="flex" gap={1}>
                                    <StatusSelectInput />
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogEdit.jsx
@@ -126,6 +126,7 @@
                                    readOnly
                                    source="arrTime"
                                />
                                {/* 质检上报状态
                                <SelectInput
                                    label="table.field.asnOrderLog.ntyStatus"
                                    source="ntyStatus"
@@ -137,6 +138,7 @@
                                    ]}
                                    validate={required()}
                                />
                                */}
                            </Stack>
                        </Grid>
                    </Grid>
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogList.jsx
@@ -1,210 +1,7 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { useNavigate } from 'react-router-dom';
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    CreateButton,
    ExportButton,
    BulkDeleteButton,
    WrapperField,
    useRecordContext,
    useTranslate,
    useNotify,
    useListContext,
    FunctionField,
    TextField,
    NumberField,
    DateField,
    BooleanField,
    ReferenceField,
    TextInput,
    DateTimeInput,
    DateInput,
    SelectInput,
    NumberInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    DeleteButton,
    Button,
    useRecordSelection,
    useRefresh,
} from 'react-admin';
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import DictionarySelect from "../../components/DictionarySelect";
import MyCreateButton from "../../components/MyCreateButton";
import MyExportButton from '../../components/MyExportButton';
import { Box, Typography, Card, Stack } from '@mui/material';
import ConfirmButton from '../../components/ConfirmButton';
import PageDrawer from "../../components/PageDrawer";
import AsnOrderLogCreate from "./AsnOrderLogCreate";
import CachedIcon from '@mui/icons-material/Cached';
import EmptyData from "../../components/EmptyData";
import AsnOrderLogPanel from "./AsnOrderLogPanel";
import { styled } from '@mui/material/styles';
import * as Common from '@/utils/common';
import request from '@/utils/request';
import React from "react";
import AsnOrderLogListBase from "./AsnOrderLogListBase";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
        height: '.9em'
    },
    '& .RaDatagrid-row': {
        cursor: 'auto'
    },
    '& .column-name': {
    },
    '& .opt': {
        width: 150
    },
    '& .MuiTableCell-root': {
        whiteSpace: 'nowrap',
        overflow: 'visible',
        textOverflow: 'unset'
    }
}));
const AsnOrderLogList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const dicts = JSON.parse(localStorage.getItem('sys_dicts'))?.filter(dict => (dict.dictTypeCode == 'sys_order_type')) || [];
    const filters = [
        <SearchInput source="condition" alwaysOn />,
        <TextInput source="code" label="table.field.asnOrderLog.code" />,
        <TextInput source="poCode" label="table.field.asnOrderLog.poCode" />,
        <NumberInput source="poId" label="table.field.asnOrderLog.poId" />,
        // <TextInput source="type" label="table.field.asnOrderLog.type" />,
        // <TextInput source="wkType" label="table.field.asnOrderLog.wkType" />,
        <NumberInput source="anfme" label="table.field.asnOrderLog.anfme" />,
        <NumberInput source="qty" label="table.field.asnOrderLog.qty" />,
        <TextInput source="logisNo" label="table.field.asnOrderLog.logisNo" />,
        <DateInput source="arrTime" label="table.field.asnOrderLog.arrTime" />,
        // <SelectInput source="ntyStatus" label="table.field.asnOrderLog.ntyStatus"
        //     choices={[
        //         { id: 0, name: ' 未上报' },
        //         { id: 1, name: ' 已上报' },
        //         { id: 2, name: ' 部分上报' },
        //     ]}
        // />,
        <AutocompleteInput
            choices={dicts}
            optionText="label"
            label="table.field.asnOrder.type"
            source="type"
            // defaultValue="in"
            optionValue="value"
            parse={v => v}
            alwaysOn
        />,
        <ReferenceInput source="wkType" reference="dictData" filter={{ dictTypeCode: 'sys_business_type', group: "1" }} label="table.field.asnOrder.wkType" alwaysOn>
            <AutocompleteInput label="table.field.asnOrder.wkType" optionValue="value" />
        </ReferenceInput>,
        <DictionarySelect
            label='table.field.asnOrder.exceStatus'
            name="exceStatus"
            group="1"
            dictTypeCode="sys_asn_exce_status"
            alwaysOn
        />,
    ]
    return (
        <Box display="flex">
            <List
                sx={{
                    flexGrow: 1,
                    transition: (theme) =>
                        theme.transitions.create(['all'], {
                            duration: theme.transitions.duration.enteringScreen,
                        }),
                    marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                }}
                title={"menu.asnOrderLog"}
                empty={false}
                filters={filters}
                sort={{ field: "create_time", order: "desc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <SelectColumnsButton preferenceKey='asnOrderLog' />
                        {/* <MyExportButton /> */}
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey='asnOrderLog'
                    bulkActionButtons={false}
                    rowClick={'edit'}
                    expand={false}
                    expandSingle={true}
                    omit={['id', 'createTime', 'createBy', 'memo', 'logisNo', 'poId', 'rleStatus$', 'statusBool', 'createBy$']}
                >
                    <NumberField source="id" />
                    <TextField source="code" label="table.field.asnOrderLog.code" />
                    <TextField source="poCode" label="table.field.asnOrderLog.poCode" />
                    <NumberField source="poId" label="table.field.asnOrderLog.poId" />
                    <TextField source="type$" label="table.field.asnOrderLog.type" />
                    <TextField source="wkType$" label="table.field.asnOrderLog.wkType" />
                    <NumberField source="anfme" label="table.field.asnOrderLog.anfme" />
                    <NumberField source="qty" label="table.field.asnOrderLog.qty" />
                    <TextField source="logisNo" label="table.field.asnOrderLog.logisNo" />
                    <DateField source="arrTime" label="table.field.asnOrderLog.arrTime" showTime />
                    <TextField source="rleStatus$" label="table.field.asnOrderLog.rleStatus" sortable={false} />
                    <TextField source="ntyStatus$" label="table.field.asnOrderLog.ntyStatus" sortable={false} />
                    <TextField source="updateBy$" label="common.field.updateBy" />
                    <DateField source="updateTime" label="common.field.updateTime" showTime />
                    <TextField source="createBy$" label="common.field.createBy" />
                    <DateField source="createTime" label="common.field.createTime" showTime />
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <ContinueButton />
                    </WrapperField>
                </StyledDatagrid>
            </List>
            <AsnOrderLogCreate
                open={createDialog}
                setOpen={setCreateDialog}
            />
            <PageDrawer
                title='AsnOrderLog Detail'
                drawerVal={drawerVal}
                setDrawerVal={setDrawerVal}
            >
            </PageDrawer>
        </Box>
    )
/** 入库历史单列表:固定 type=in,请求后端 asnOrderLog 接口 */
export default function AsnOrderLogList() {
    return <AsnOrderLogListBase typeFilter="in" listTitle="menu.asnOrderLog" />;
}
export default AsnOrderLogList;
const ContinueButton = () => {
    const refresh = useRefresh();
    const record = useRecordContext();
    const notify = useNotify();
    const continueReceipt = async () => {
        const { data: { code, data, msg } } = await request.post(`/asnOrderLog/continue/${record.id}`);
        if (code === 200) {
            notify(msg);
        } else {
            notify(msg);
        }
        refresh();
    }
    return (
        record.type == 'in' ? <ConfirmButton label={"toolbar.continue"} startIcon={<CachedIcon />} onConfirm={continueReceipt} /> : <></>
    )
}
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogListBase.jsx
New file
@@ -0,0 +1,123 @@
import React, { useState } from "react";
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    ShowButton,
    FilterButton,
    WrapperField,
    TextField,
    NumberField,
    DateField,
    BooleanField,
    TextInput,
    DateInput,
    NumberInput,
    ReferenceInput,
    AutocompleteInput,
} from 'react-admin';
import { PAGE_DRAWER_WIDTH, DEFAULT_PAGE_SIZE } from '@/config/setting';
import DictionarySelect from "../../components/DictionarySelect";
import { Box } from '@mui/material';
import PageDrawer from "../../components/PageDrawer";
import AsnOrderLogCreate from "./AsnOrderLogCreate";
import { styled } from '@mui/material/styles';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': { height: '.9em' },
    '& .RaDatagrid-row': { cursor: 'auto' },
    '& .opt': { width: 150 },
    '& .MuiTableCell-root': { whiteSpace: 'nowrap', overflow: 'visible', textOverflow: 'unset' }
}));
/**
 * 入库/出库历史单列表共用骨架,仅 type 与标题由外部传入。
 * 入库历史单:typeFilter="in", listTitle="menu.asnOrderLog"
 * 出库历史单:typeFilter="out", listTitle="menu.outStockOrderLog"
 * 后端接口均为 asnOrderLog(出库由 dataProvider 映射),参数 filter.type 不同。
 */
export default function AsnOrderLogListBase({ typeFilter, listTitle }) {
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const filters = [
        <SearchInput source="condition" alwaysOn />,
        <TextInput source="code" label="table.field.asnOrderLog.code" />,
        <TextInput source="poCode" label="table.field.asnOrderLog.poCode" />,
        <NumberInput source="poId" label="table.field.asnOrderLog.poId" />,
        <NumberInput source="anfme" label="table.field.asnOrderLog.anfme" />,
        <NumberInput source="qty" label="table.field.asnOrderLog.qty" />,
        <TextInput source="logisNo" label="table.field.asnOrderLog.logisNo" />,
        <DateInput source="arrTime" label="table.field.asnOrderLog.arrTime" />,
        <ReferenceInput source="wkType" reference="dictData" filter={{ dictTypeCode: 'sys_business_type', group: "1" }} label="table.field.asnOrder.wkType" alwaysOn>
            <AutocompleteInput label="table.field.asnOrder.wkType" optionValue="value" />
        </ReferenceInput>,
        <DictionarySelect
            label='table.field.asnOrder.exceStatus'
            name="exceStatus"
            group="1"
            dictTypeCode="sys_asn_exce_status"
            alwaysOn
        />,
    ];
    return (
        <Box display="flex">
            <List
                key={`orderLog-list-${typeFilter}`}
                sx={{
                    flexGrow: 1,
                    transition: (theme) => theme.transitions.create(['all'], { duration: theme.transitions.duration.enteringScreen }),
                    marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                }}
                title={listTitle}
                empty={false}
                filters={filters}
                filterDefaultValues={{ type: typeFilter }}
                filter={{ type: typeFilter }}
                sort={{ field: "create_time", order: "desc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <SelectColumnsButton preferenceKey='asnOrderLog' />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey='asnOrderLog'
                    bulkActionButtons={false}
                    rowClick={false}
                    expand={false}
                    expandSingle={true}
                    omit={['id', 'createTime', 'createBy', 'memo', 'logisNo', 'poId', 'rleStatus$', 'statusBool', 'createBy$']}
                >
                    <NumberField source="id" />
                    <TextField source="code" label="table.field.asnOrderLog.code" />
                    <TextField source="poCode" label="table.field.asnOrderLog.poCode" />
                    <NumberField source="poId" label="table.field.asnOrderLog.poId" />
                    <TextField source="type$" label="table.field.asnOrderLog.type" />
                    <TextField source="wkType$" label="table.field.asnOrderLog.wkType" />
                    <NumberField source="anfme" label="table.field.asnOrderLog.anfme" />
                    <NumberField source="qty" label="table.field.asnOrderLog.qty" />
                    <TextField source="logisNo" label="table.field.asnOrderLog.logisNo" />
                    <DateField source="arrTime" label="table.field.asnOrderLog.arrTime" showTime />
                    <TextField source="rleStatus$" label="table.field.asnOrderLog.rleStatus" sortable={false} />
                    <TextField source="updateBy$" label="common.field.updateBy" />
                    <DateField source="updateTime" label="common.field.updateTime" showTime />
                    <TextField source="createBy$" label="common.field.createBy" />
                    <DateField source="createTime" label="common.field.createTime" showTime />
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <ShowButton label="toolbar.detail" />
                    </WrapperField>
                </StyledDatagrid>
            </List>
            <AsnOrderLogCreate open={createDialog} setOpen={setCreateDialog} />
            <PageDrawer title='AsnOrderLog Detail' drawerVal={drawerVal} setDrawerVal={setDrawerVal} />
        </Box>
    );
}
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogPanel.jsx
@@ -104,10 +104,12 @@
            field: 'packName',
            headerName: translate('table.field.asnOrderItemLog.packName')
        },
        /* 质检上报状态
        {
            field: 'ntyStatus$',
            headerName: translate('table.field.asnOrderItemLog.ntyStatus')
        }]
        }
        */]
    const maktxChange = (value) => {
        setMaktx(value)
rsf-admin/src/page/histories/asnOrderLog/AsnOrderLogShow.jsx
@@ -1,12 +1,27 @@
import { BooleanField, DateField, NumberField, ReferenceField, Show, SimpleShowLayout, TextField ,DateInput,
    SelectInput,required,useTranslate,
    useRecordContext,} from 'react-admin';
    import { Stack, Grid, Box, Typography, Card } from '@mui/material';
    import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
    import EditBaseAside from "../../components/EditBaseAside";
    import CustomerTopToolBar from "../../components/EditTopToolBar";
    import AsnOrderItemLogList from "./AsnOrderItemLogList"
import { Stack, Grid, Box, Typography, Card } from '@mui/material';
import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
import EditBaseAside from "../../components/EditBaseAside";
import CustomerTopToolBar from "../../components/EditTopToolBar";
import AsnOrderItemLogList from "./AsnOrderItemLogList";
const AsnOrderLogDetailWithItems = () => {
    const record = useRecordContext();
    const translate = useTranslate();
    if (!record?.id) return null;
    return (
        <>
            <Grid item xs={24} md={16} sx={{ marginTop: '1em', width: '100%' }}>
                <Typography variant="h6" gutterBottom>
                    {translate('common.edit.title.common')}
                </Typography>
            </Grid>
            <AsnOrderItemLogList logId={record.id} />
        </>
    );
};
const Aa = () =>{
    const translate = useTranslate();
@@ -22,7 +37,6 @@
            <SimpleShowLayout
            shouldUnregister
            warnWhenUnsavedChanges
            mode="onTouched"
            defaultValues={{}}
            >
@@ -113,28 +127,23 @@
                                    <DateField source="arrTime" label="type" showTime/>
                                </Box>
                            </Grid>
                            <Grid item  display="flex" gap={1} minWidth={150}>
                            {/* 质检上报状态
                            <Grid item  display="flex" gap={1} minWidth={150}>
                                <Box flexGrow={1}>
                                    <Typography variant="body2" sx={{fontSize: 20}}>
                                        {translate('table.field.asnOrderLog.ntyStatus')}
                                    </Typography>
                                    <TextField source="ntyStatus$" label="type"/>
                                </Box>
                            </Grid>
                            </Grid>
                            */}
                        </Stack>
                    </Grid>
                </Grid>
                <AsnOrderLogDetailWithItems />
            </SimpleShowLayout>
        </Show>
        <Grid item xs={24} md={16} sx={{ marginTop: '1em' }}>
                <Typography variant="h6" gutterBottom >
                    {translate('common.edit.title.common')}
                </Typography>
            </Grid>
            <AsnOrderItemLogList />
        </>
       ); 
}
rsf-admin/src/page/histories/outStockOrderLog/OutStockOrderLogList.jsx
New file
@@ -0,0 +1,7 @@
import React from "react";
import AsnOrderLogListBase from "../asnOrderLog/AsnOrderLogListBase";
/** 出库历史单列表:固定 type=out,请求后端 asnOrderLog 接口(dataProvider 映射 outStockOrderLog→asnOrderLog) */
export default function OutStockOrderLogList() {
    return <AsnOrderLogListBase typeFilter="out" listTitle="menu.outStockOrderLog" />;
}
rsf-admin/src/page/histories/outStockOrderLog/index.jsx
New file
@@ -0,0 +1,11 @@
import React from "react";
import OutStockOrderLogList from "./OutStockOrderLogList";
import AsnOrderLogEdit from "../asnOrderLog/AsnOrderLogEdit";
import AsnorderlogShow from "../asnOrderLog/AsnOrderLogShow";
export default {
    list: OutStockOrderLogList,
    edit: AsnOrderLogEdit,
    show: AsnorderlogShow,
    recordRepresentation: (record) => `${record.id}`,
};
rsf-admin/src/page/system/openApiApp/OpenApiAppCreate.jsx
New file
@@ -0,0 +1,91 @@
import React from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    Form,
    required,
    useNotify,
} from 'react-admin';
import { Dialog, DialogContent, DialogTitle, Grid, Box } from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
const OpenApiAppCreate = (props) => {
    const { open, setOpen } = props;
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") setOpen(false);
    };
    const handleSuccess = () => {
        setOpen(false);
        notify('新增成功');
    };
    const handleError = (error) => {
        notify(error?.message || '新增失败', { type: 'error' });
    };
    return (
        <CreateBase
            resource="openApiApp"
            record={{}}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog
                open={open}
                onClose={handleClose}
                fullWidth
                disableRestoreFocus
                maxWidth="sm"
            >
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        新增应用
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={12}>
                                <TextInput label="应用ID" source="id" validate={required()} fullWidth />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput label="应用密钥" source="screct" validate={required()} fullWidth />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput label="应用名称" source="name" fullWidth />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput label="应用URL" source="url" fullWidth />
                            </Grid>
                            <Grid item xs={12}>
                                <SelectInput
                                    label="启用状态"
                                    source="enable"
                                    choices={[
                                        { id: 1, name: '启用' },
                                        { id: 0, name: '未启用' },
                                    ]}
                                    defaultValue={1}
                                    fullWidth
                                />
                            </Grid>
                        </Grid>
                    </DialogContent>
                    <Box sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000, p: 2, display: 'flex', justifyContent: 'flex-end' }}>
                        <Toolbar><SaveButton /></Toolbar>
                    </Box>
                </Form>
            </Dialog>
        </CreateBase>
    );
};
export default OpenApiAppCreate;
rsf-admin/src/page/system/openApiApp/OpenApiAppEdit.jsx
New file
@@ -0,0 +1,61 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    SaveButton,
    SelectInput,
    Toolbar,
    required,
    DeleteButton,
} from 'react-admin';
import { Grid, Stack } from '@mui/material';
import { EDIT_MODE } from '@/config/setting';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
const FormToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton mutationMode="optimistic" />
    </Toolbar>
);
const OpenApiAppEdit = () => {
    return (
        <Edit
            resource="openApiApp"
            redirect="list"
            mutationMode={EDIT_MODE}
            actions={<CustomerTopToolBar />}
            aside={<EditBaseAside />}
        >
            <SimpleForm
                toolbar={<FormToolbar />}
                defaultValues={{ enable: 1 }}
            >
                <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                    <Grid item xs={12} md={8}>
                        <Stack spacing={2}>
                            <TextInput label="应用ID" source="id" validate={required()} disabled />
                            <TextInput label="应用密钥" source="screct" validate={required()} fullWidth />
                            <TextInput label="应用名称" source="name" fullWidth />
                            <TextInput label="应用URL" source="url" fullWidth />
                            <SelectInput
                                label="启用状态"
                                source="enable"
                                choices={[
                                    { id: 1, name: '启用' },
                                    { id: 0, name: '未启用' },
                                ]}
                                fullWidth
                            />
                        </Stack>
                    </Grid>
                </Grid>
            </SimpleForm>
        </Edit>
    );
};
export default OpenApiAppEdit;
rsf-admin/src/page/system/openApiApp/OpenApiAppList.jsx
New file
@@ -0,0 +1,84 @@
import React, { useState } from "react";
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextField,
    TextInput,
    FunctionField,
    SelectInput,
    WrapperField,
    DeleteButton,
} from 'react-admin';
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import OpenApiAppCreate from "./OpenApiAppCreate";
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
const StyledDatagrid = styled(DatagridConfigurable)(() => ({
    '& .RaDatagrid-row': { cursor: 'auto' },
    '& .opt': { width: 140 },
}));
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <TextInput source="id" label="应用ID" />,
    <TextInput source="name" label="应用名称" />,
    <SelectInput
        label="启用状态"
        source="enable"
        choices={[
            { id: 1, name: '启用' },
            { id: 0, name: '未启用' },
        ]}
    />,
];
const OpenApiAppList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <Box display="flex">
            <List
                title="应用管理"
                empty={<EmptyData onClick={() => setCreateDialog(true)} />}
                filters={filters}
                sort={{ field: "id", order: "asc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => setCreateDialog(true)} />
                        <SelectColumnsButton preferenceKey="openApiApp" />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey="openApiApp"
                    bulkActionButtons={() => <DeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={false}
                    omit={['tenantId']}
                >
                    <TextField source="id" label="应用ID" />
                    <TextField source="name" label="应用名称" />
                    <TextField source="screct" label="应用密钥" />
                    <TextField source="url" label="应用URL" />
                    <FunctionField source="enable" label="启用" render={(r) => (r.enable === 1 ? '启用' : '未启用')} />
                    <WrapperField cellClassName="opt" label="操作">
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                </StyledDatagrid>
            </List>
            <OpenApiAppCreate open={createDialog} setOpen={setCreateDialog} />
        </Box>
    );
};
export default OpenApiAppList;
rsf-admin/src/page/system/openApiApp/index.jsx
New file
@@ -0,0 +1,11 @@
import React from "react";
import { ShowGuesser } from "react-admin";
import OpenApiAppList from "./OpenApiAppList";
import OpenApiAppEdit from "./OpenApiAppEdit";
export default {
    list: OpenApiAppList,
    edit: OpenApiAppEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => record?.name || record?.id || '',
};
rsf-open-api/pom.xml
@@ -21,6 +21,39 @@
            <artifactId>rsf-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!-- OpenFeign:转发调用立库 WMS 接口 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 熔断器:Feign 调用失败时触发 Fallback,在 Feign 内统一返回错误响应 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
        </dependency>
        <!-- JWT:Token 生成与校验(/erp 认证) -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <!-- BCrypt:getToken 时校验 appSecret 用哈希比对 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-crypto</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>rsf-open-api</finalName>
rsf-open-api/src/main/java/com/vincent/rsf/openApi/OpenApi.java
@@ -4,8 +4,10 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class })
@EnableFeignClients(basePackages = "com.vincent.rsf.openApi.feign")
public class OpenApi {
    public static void main(String[] args) {
        SpringApplication.run(OpenApi.class, args);
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/ApiSecurityConfig.java
New file
@@ -0,0 +1,28 @@
package com.vincent.rsf.openApi.config;
import com.vincent.rsf.openApi.security.filter.AppIdAuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
 * API 安全配置 认证过滤器
 */
@Configuration
public class ApiSecurityConfig {
    @Resource
    private AppIdAuthenticationFilter appIdAuthenticationFilter;
    @Bean
    public FilterRegistrationBean<AppIdAuthenticationFilter> apiAuthenticationFilter() {
        FilterRegistrationBean<AppIdAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(appIdAuthenticationFilter);
        registrationBean.addUrlPatterns("/api/*", "/erp/*", "/cloudwms/*", "/mes/*", "/agv/*");
        registrationBean.setName("apiAuthenticationFilter");
        registrationBean.setOrder(1);
        return registrationBean;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/CryptoConfig.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.openApi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class CryptoConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/CustomErrorAttributes.java
New file
@@ -0,0 +1,36 @@
package com.vincent.rsf.openApi.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
/**
 * 自定义错误属性:响应体中不返回 path(仅打印日志);保证业务异常信息写入 message 返回给前端。
 */
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
    private static final Logger log = LoggerFactory.getLogger(CustomErrorAttributes.class);
    private static final String KEY_PATH = "path";
    private static final String KEY_MESSAGE = "message";
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> attrs = super.getErrorAttributes(webRequest, options);
        Object path = attrs.remove(KEY_PATH);
        if (path != null) {
            log.warn("Error path: {}", path);
        }
        // 保证业务异常信息返回给前端(如 CoolException)
        Throwable error = getError(webRequest);
        if (error != null && error.getMessage() != null && !error.getMessage().isEmpty()) {
            attrs.put(KEY_MESSAGE, error.getMessage());
        }
        return attrs;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/FeignConfig.java
New file
@@ -0,0 +1,18 @@
package com.vincent.rsf.openApi.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * Feign 请求配置,与原有 RestTemplate 转发保持一致的请求头
 */
@Configuration
public class FeignConfig {
    @Bean
    public RequestInterceptor wmsApiVersionInterceptor() {
        return (RequestTemplate template) -> template.header("api-version", "v2.0");
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/GlobalExceptionHandler.java
New file
@@ -0,0 +1,38 @@
package com.vincent.rsf.openApi.config;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.dto.ResultData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
 * 全局异常处理,返回值符合 8.2.3:code、msg、data(含 result:SUCCESS/FAIL)。
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    @ExceptionHandler(CoolException.class)
    public ResponseEntity<CommonResponse> handleCoolException(CoolException e) {
        log.warn("业务异常: {}", e.getMessage());
        CommonResponse r = CommonResponse.error(e.getMessage());
        return ResponseEntity.status(HttpStatus.OK).body(r);
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<CommonResponse> handleException(Exception e) {
        log.error("系统异常", e);
        String msg = e.getMessage() != null ? e.getMessage() : "系统异常";
        CommonResponse r = new CommonResponse();
        r.setCode(500);
        r.setMsg(msg);
        r.setData(ResultData.fail());
        return ResponseEntity.status(HttpStatus.OK).body(r);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/WebMvcConfig.java
@@ -27,7 +27,7 @@
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getAsyncHandlerInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**","/erp/**", "/v2/**","/v3/**","/doc.html/**", "/swagger-ui.html/**");
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/cloudwms/**", "/erp/**", "/v2/**", "/v3/**", "/doc.html/**", "/swagger-ui.html/**");
    }
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/AuthController.java
New file
@@ -0,0 +1,37 @@
package com.vincent.rsf.openApi.controller;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.openApi.entity.constant.Constants;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.params.GetTokenParam;
import com.vincent.rsf.openApi.security.service.AppAuthService;
import com.vincent.rsf.openApi.security.utils.TokenUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Api(tags = "应用认证管理")
public class AuthController {
    @Autowired
    private AppAuthService appAuthService;
    @ApiOperation("获取App认证Token")
    @PostMapping("/getToken")
    public CommonResponse getToken(@RequestBody GetTokenParam param) {
        String appId = param != null ? param.getAppId() : null;
        String appSecret = param != null ? param.getAppSecret() : null;
        if (Cools.isEmpty(appId, appSecret)) {
            return CommonResponse.error("AppId和AppSecret不能为空");
        }
        if (!appAuthService.validateApp(appId, appSecret)) {
            return CommonResponse.error("AppId或AppSecret无效");
        }
        String token = Constants.TOKEN_PREFIX + TokenUtils.generateToken(appId, appSecret);
        return CommonResponse.ok().setMsg("获取Token成功").setData(token);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsErpController.java
@@ -17,7 +17,7 @@
import java.util.Objects;
@RestController
@RequestMapping("/erp")
@RequestMapping({"/erp","/cloudwms"})
@Api("ERP接口对接")
public class WmsErpController {
@@ -41,50 +41,37 @@
    /**
     * 订单修改
     * @param params
     * @return
     * 入/出库通知单下发(对接协议 8.3):新增/修改/取消。operateType 1新增 2修改 3取消,不传或 1/2 时有则更新、无则新增;3 时按取消。
     * @param params 单据参数(含 orderNo、orderItems、operateType 等,见对接文档)
     * @return 操作结果
     */
    @ApiOperation("单据修改")
    @PostMapping("/order/upadte")
    public CommonResponse modifyOrderDtel(@RequestBody ErpOpParams params) {
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
        }
        return wmsErpService.updateOrderDetl(params);
    }
    /**
     * 订单新增
     * @param params
     * @return
     */
    @ApiOperation("新增单据")
    @ApiOperation("新增单据(兼容修改、取消)")
    @PostMapping("/order/add")
    public CommonResponse orderAdd(@RequestBody ErpOpParams params) {
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
        }
        return wmsErpService.updateOrderDetl(params);
        return wmsErpService.addOrUpdateOrder(params);
    }
    /**
     * 删除订单
     * @param params
     * @return
     * 取消订单/取消单据。与 /order/add 传 operateType=3 的取消逻辑一致,均转发立库 sync/orders/delete。
     * @param params 至少包含 orderNo,可选 orderItems
     */
    @ApiOperation("删除订单")
    @PostMapping("/order/del")
    public CommonResponse orderDel(@RequestBody ErpOpParams params) {
    @ApiOperation("取消订单")
    @PostMapping({"/order/cancel", "/order/del"})
    public CommonResponse orderCancel(@RequestBody ErpOpParams params) {
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
        }
        return wmsErpService.orderDel(params);
        return wmsErpService.orderCancel(params);
    }
    @ApiOperation("基础物料信息更新")
    /**
     * 物料基础信息同步(对接协议 8.2)
     * 支持 operateType:1新增 2修改 3禁用 4启用;请求体支持协议字段 matNr/makTx 与 matnr/maktx。
     */
    @ApiOperation("基础物料信息同步(支持 operateType、matNr/makTx)")
    @PostMapping("/mat/sync/auth/v1")
    public CommonResponse syncMatnrs(@RequestBody ErpMatnrParms parms) {
        if (Objects.isNull(parms)) {
@@ -94,19 +81,37 @@
    }
    @ApiOperation("订单信息上报")
    @PostMapping("/report/order")
    public CommonResponse reportOrders(@RequestBody ReportParams params) {
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
        }
        return wmsErpService.reportOrders(params);
//    @ApiOperation("订单信息上报")
//    @PostMapping("/report/order")
//    public CommonResponse reportOrders(@RequestBody ReportParams params) {
//        if (Objects.isNull(params)) {
//            throw new CoolException("参数不能为空!!");
//        }
//        return wmsErpService.reportOrders(params);
//    }
//
//    @ApiOperation("盘点差异修改")
//    @PostMapping("/check/locitem/update")
//    public CommonResponse reportCheck(@RequestBody ReportParams params) {
//        return wmsErpService.reportCheck(params);
//    }
    @ApiOperation("库位信息查询")
    @PostMapping("/query/locs/detls")
    public CommonResponse queryLocsDetls(@RequestBody Map<String, Object> params) {
        return wmsErpService.queryLocsDetls(params);
    }
    @ApiOperation("盘点差异修改")
    @PostMapping("/check/locitem/update")
    public CommonResponse reportCheck(@RequestBody ReportParams params) {
        return wmsErpService.reportCheck(params);
    @ApiOperation("库存明细查询(对接协议8.4)")
    @PostMapping("/inventory/details")
    public CommonResponse inventoryDetails(@RequestBody(required = false) Map<String, Object> params) {
        return wmsErpService.inventoryDetails(params);
    }
    @ApiOperation("库存汇总查询(对接协议8.5)")
    @PostMapping("/inventory/summary")
    public CommonResponse inventorySummary(@RequestBody(required = false) Map<String, Object> params) {
        return wmsErpService.inventorySummary(params);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/AppController.java
New file
@@ -0,0 +1,79 @@
package com.vincent.rsf.openApi.controller.platform;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.openApi.entity.app.App;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.service.AppService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/app")
@Api(tags = "应用管理")
public class AppController {
    @Autowired
    private AppService appService;
    @ApiOperation("分页查询应用列表")
    @GetMapping("/page")
    public CommonResponse page(@RequestParam(defaultValue = "1") Integer current,
                               @RequestParam(defaultValue = "10") Integer size) {
        Page<App> page = appService.page(new Page<>(current, size));
        return CommonResponse.ok().setData(page);
    }
    @ApiOperation("查询所有应用")
    @GetMapping("/list")
    public CommonResponse list() {
        List<App> list = appService.list();
        return CommonResponse.ok().setData(list);
    }
    @ApiOperation("根据ID查询应用")
    @GetMapping("/{id}")
    public CommonResponse getById(@PathVariable String id) {
        App app = appService.getById(id);
        return CommonResponse.ok().setData(app);
    }
    @ApiOperation("新增应用")
    @PostMapping
    public CommonResponse save(@RequestBody App app) {
        if (appService.save(app)) {
            return CommonResponse.ok().setMsg("新增成功");
        }
        return CommonResponse.error("新增失败");
    }
    @ApiOperation("更新应用")
    @PutMapping
    public CommonResponse update(@RequestBody App app) {
        if (appService.updateById(app)) {
            return CommonResponse.ok().setMsg("更新成功");
        }
        return CommonResponse.error("更新失败");
    }
    @ApiOperation("删除应用")
    @DeleteMapping("/{id}")
    public CommonResponse delete(@PathVariable String id) {
        if (appService.removeById(id)) {
            return CommonResponse.ok().setMsg("删除成功");
        }
        return CommonResponse.error("删除失败");
    }
    @ApiOperation("批量删除应用")
    @DeleteMapping("/batch")
    public CommonResponse deleteBatch(@RequestBody List<String> ids) {
        if (appService.removeByIds(ids)) {
            return CommonResponse.ok().setMsg("批量删除成功");
        }
        return CommonResponse.error("批量删除失败");
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/App.java
New file
@@ -0,0 +1,34 @@
package com.vincent.rsf.openApi.entity.app;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("open_api_app")
public class App implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("appId")
    @TableId(value = "id")
    private String id;
    @ApiModelProperty("appSecret")
    private String screct;
    @ApiModelProperty("appName")
    private String name;
    @ApiModelProperty("appUrl")
    private String url;
    @ApiModelProperty("是否启用 0未启用 1启用")
    private Integer enable;
    @ApiModelProperty("租户id")
    private Long tenantId;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/constant/Constants.java
@@ -91,6 +91,21 @@
    public static final String TOKEN_TYPE = "Bearer";
    /**
     * Authorization 头中的 Token 前缀
     */
    public static final String TOKEN_PREFIX = "Bearer ";
    /**
     * HTTP 头 Authorization 的 key(小写,请求头不区分大小写)
     */
    public static final String HEADER_AUTHORIZATION = "Authorization";
    /**
     * 请求属性:认证通过后写入的 appId
     */
    public static final String REQUEST_ATTR_APP_ID = "appId";
    /**
     * 库存出库
     */
    public static final String TASK_TYPE_OUT_STOCK = "outStock";
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/constant/WmsConstant.java
@@ -13,7 +13,7 @@
    //订单信息修改/添加
    public static String MODIFY_ORDER_DETLS = "/rsf-server/order/sync/orders/update";
    //删除单据信息
    // 取消单据(立库侧 /order/sync/orders/delete 实现取消逻辑)
    public static String ORDER_DEL = "/rsf-server/order/sync/orders/delete";
    //获取出入库流水
@@ -25,6 +25,48 @@
    //订单完成回写
    public static String REPORT_ORDER_CALLBACK = "/C3Api?SysCode=WMS";
    //库位信息查询(云仓只调 open-api,由 open-api 转发立库)
    public static String QUERY_LOCS_DETLS = "/rsf-server/erp/query/locs/detls";
    //调拨单信息查询
    public static String QUERY_TRANSFER = "/rsf-server/erp/query/transfer";
    //物料分类列表查询
    public static String QUERY_MATNR_GROUP = "/rsf-server/erp/query/matnr/group";
    // ========== 单据同步(立库提供,云仓调 open-api 转发) ==========
    /** 采购单同步 */
    public static String ORDER_SYNC_PURCHASE = "/rsf-server/order/sync/purchase";
    /** 出库通知单(DO单)同步 */
    public static String ORDER_SYNC_DELIVERY = "/rsf-server/order/sync/delivery";
    /** 收货通知单/单据同步 */
    public static String ORDER_SYNC_CHECKS = "/rsf-server/order/sync/checks";
    /** 调拨单同步 */
    public static String ORDER_SYNC_TRANSFERS = "/rsf-server/order/sync/transfers";
    /** 库存调整单同步 */
    public static String ORDER_SYNC_REVISES = "/rsf-server/order/sync/revises";
    /** 质检单上报 */
    public static String ORDER_SYNC_QLY_INSPECT = "/rsf-server/order/sync/qlyInspect";
    /** 盘点差异单同步 */
    public static String ORDER_SYNC_CHECK_RESULT = "/rsf-server/order/sync/check/result";
    // ========== 基础数据同步(立库提供) ==========
    /** 基础物料信息同步(批量) */
    public static String BASE_SYNC_MATNRS = "/rsf-server/base/sync/base/matnrs";
    /** 库位信息同步 */
    public static String BASE_SYNC_LOCS = "/rsf-server/base/sync/locs";
    /** 物料分组信息同步 */
    public static String BASE_SYNC_MAT_GROUPS = "/rsf-server/base/sync/matGroups";
    /** 库区数据同步 */
    public static String BASE_SYNC_WAREHOUSE_AREAS = "/rsf-server/base/sync/warehouse/areas";
    /** 仓库数据同步 */
    public static String BASE_SYNC_WAREHOUSE = "/rsf-server/base/sync/warehouse";
    /** 企业信息同步 */
    public static String BASE_SYNC_COMPANIES = "/rsf-server/base/sync/companies";
    /** 对接协议 8.4 库存明细查询 */
    public static String INVENTORY_DETAILS = "/rsf-server/erp/inventory/details";
    /** 对接协议 8.5 库存汇总查询 */
    public static String INVENTORY_SUMMARY = "/rsf-server/erp/inventory/summary";
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/dto/CommonResponse.java
@@ -25,4 +25,24 @@
    @ApiModelProperty("响应结果")
    private Object data;
    public static CommonResponse ok() {
        CommonResponse r = new CommonResponse();
        r.setCode(200);
        r.setMsg("操作成功");
        return r;
    }
    /** 8.2.3 格式:成功且 data 仅含 result */
    public static CommonResponse okWithResult() {
        return ok().setData(ResultData.success());
    }
    public static CommonResponse error(String msg) {
        CommonResponse r = new CommonResponse();
        r.setCode(500);
        r.setMsg(msg != null ? msg : "操作失败");
        r.setData(ResultData.fail());
        return r;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/dto/ResultData.java
New file
@@ -0,0 +1,31 @@
package com.vincent.rsf.openApi.entity.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 8.2.3 返回值 data 数据模型:执行结果 SUCCESS/FAIL
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "ResultData", description = "执行结果")
public class ResultData {
    @ApiModelProperty(value = "执行结果:SUCCESS 成功;FAIL 失败", example = "SUCCESS")
    private String result;
    public static final String SUCCESS = "SUCCESS";
    public static final String FAIL = "FAIL";
    public static ResultData success() {
        return new ResultData(SUCCESS);
    }
    public static ResultData fail() {
        return new ResultData(FAIL);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/ErpMatnrParms.java
@@ -1,20 +1,29 @@
package com.vincent.rsf.openApi.entity.params;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 物料基础信息同步入参(对接协议 8.2)
 * 协议字段 matNr/makTx 与 matnr/maktx 均可接收。
 */
@Data
@Accessors(chain = true)
public class ErpMatnrParms {
    @ApiModelProperty("物料名称")
    private String maktx;
    @ApiModelProperty(value = "操作类型:1新增 2修改 3禁用 4启用", example = "1")
    private Integer operateType;
    @ApiModelProperty("物料编码*")
    @ApiModelProperty(value = "物料编码*(协议字段 matNr 同义)")
    @JsonAlias("matNr")
    private String matnr;
    @ApiModelProperty(value = "物料名称(协议字段 makTx 同义)")
    @JsonAlias("makTx")
    private String maktx;
    @ApiModelProperty("物料分组")
    private String groupName;
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/ErpOpParams.java
@@ -1,6 +1,6 @@
package com.vincent.rsf.openApi.entity.params;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -8,30 +8,44 @@
import java.util.List;
/**
 * 入/出库通知单下发(对接协议 8.3)请求参数。
 * 以 8.3 文档字段为主,其他旧字段尽量不用。
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "ErpOpParams", description = "ERP操作请求参数")
@ApiModel(value = "ErpOpParams", description = "8.3 入/出库通知单下发参数")
public class ErpOpParams {
    /**
     * 单号
     */
    @ApiModelProperty("订单号")
    @ApiModelProperty(value = "订单编码", required = true)
    private String orderNo;
    @ApiModelProperty("业务类型")
    @ApiModelProperty(value = "单据内码,唯一标识,若没有可补充订单编码", required = true)
    private String orderInternalCode;
    @ApiModelProperty(value = "订单类型:1 出库单;2 入库单;3 调拨单", required = true)
    private Integer orderType;
    @ApiModelProperty(value = "业务类型,如:采购入库单、销售出库单、调拨申请单等", required = true)
    private String wkType;
    @ApiModelProperty("订单类型")
    private String type;
    @ApiModelProperty(value = "业务日期,时间戳精确到秒", required = true)
    private Long businessTime;
    @ApiModelProperty("数量")
    private Double anfme;
    @ApiModelProperty(value = "创建日期,时间戳精确到秒", required = true)
    private Long createTime;
    @ApiModelProperty("执行状态")
    private Short exceStatus;
    @ApiModelProperty("订单明细")
    @ApiModelProperty(value = "订单明细", required = true)
    private List<WmsOrderItemParam> orderItems;
    @ApiModelProperty("入/出库接驳站点,需要则补充")
    private String stationId;
    @ApiModelProperty("操作类型:1 新增(默认);2 修改;3 取消")
    private Integer operateType;
    /** 兼容旧字段:与 orderInternalCode 二选一 */
    @JsonAlias("orderId")
    @ApiModelProperty(hidden = true)
    private Long orderId;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/GetTokenParam.java
New file
@@ -0,0 +1,21 @@
package com.vincent.rsf.openApi.entity.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 对接协议 8.1 获取Token 请求参数
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "GetTokenParam", description = "获取Token")
public class GetTokenParam {
    @ApiModelProperty(value = "应用编码,立库WMS线下分配", required = true)
    private String appId;
    @ApiModelProperty(value = "应用秘钥,立库WMS线下分配", required = true)
    private String appSecret;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/WmsOrderItemParam.java
@@ -1,36 +1,33 @@
package com.vincent.rsf.openApi.entity.params;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 8.3 订单明细,以文档字段为主。
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "WmsOrderItemParam", description = "订单明细参数")
@ApiModel(value = "WmsOrderItemParam", description = "8.3 订单明细")
public class WmsOrderItemParam {
    @ApiModelProperty("物料编码")
    private String matnr;
    @ApiModelProperty(value = "行内码,唯一标识", required = true)
    private String lineId;
    @ApiModelProperty(value = "物料编码", required = true)
    @JsonAlias("matnr")
    private String matNr;
    @ApiModelProperty("物料名称")
    private String maktx;
    @JsonAlias("maktx")
    private String makTx;
    @ApiModelProperty("客单号")
    private String platOrderCode;
    @ApiModelProperty("平台标识(行号)")
    private String platItemId;
    @ApiModelProperty("工单号")
    private String platWorkCode;
    @ApiModelProperty("项目号")
    private String projectCode;
    @ApiModelProperty("现金票号")
    private String crushNo;
    @ApiModelProperty(value = "数量,若有小数默认保留2位", required = true)
    @JsonAlias("qty")
    private String anfme;
    @ApiModelProperty("规格")
    private String spec;
@@ -38,19 +35,21 @@
    @ApiModelProperty("型号")
    private String model;
    @ApiModelProperty("数量")
    private Double anfme;
    @ApiModelProperty("库存单位")
    @ApiModelProperty("单位")
    private String unit;
    @ApiModelProperty("批次")
    @ApiModelProperty("批次号")
    private String batch;
    @ApiModelProperty("已收数量")
    private Double qty;
    @ApiModelProperty("托盘码,出库单时可指定该托盘出库")
    private String palletId;
    @ApiModelProperty("条形码")
    private String barcode;
    @ApiModelProperty("计划跟踪号")
    private String planNo;
    @ApiModelProperty("建议入库仓库")
    private String targetWareHouseId;
    @ApiModelProperty("建议出库仓库")
    private String sourceWareHouseId;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/erp/ErpReportFeignClient.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.openApi.feign.erp;
import com.vincent.rsf.openApi.entity.params.ReportParams;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
/**
 * 调用 ERP/云仓 上报接口的 OpenFeign 客户端(订单完成回写、盘点差异单修改)。
 * 可选方式,当前仍使用 HttpEntity(RestTemplate);启用时在 WmsErpServiceImpl 中切换。
 */
@FeignClient(
    name = "erp-report",
    url = "${platform.erp.host:http://127.0.0.1}:${platform.erp.port:8080}"
)
public interface ErpReportFeignClient {
    /** 订单完成回写 / 盘点差异单修改:同一 ERP 回调地址 */
    @PostMapping(value = "/C3Api?SysCode=WMS", consumes = MediaType.APPLICATION_JSON_VALUE)
    Map<String, Object> report(@RequestBody ReportParams params);
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/wms/WmsServerFeignClient.java
New file
@@ -0,0 +1,55 @@
package com.vincent.rsf.openApi.feign.wms;
import com.vincent.rsf.openApi.entity.params.ErpMatnrParms;
import com.vincent.rsf.openApi.entity.params.ErpOpParams;
import com.vincent.rsf.openApi.feign.wms.fallback.WmsServerFeignClientFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
import java.util.Map;
/**
 * WMS Server(立库)Feign 客户端
 * 用于 open-api 转发调用 rsf-server 的接口,云仓只调 open-api,由本 Feign 转发至立库。
 *
 * url 从 application 中读取 platform.wms.host 与 platform.wms.port;
 * 同机部署时可配为本地地址,分开部署时配 server 实际地址。
 */
@FeignClient(
    name = "wms-server",
    url = "${platform.wms.host:http://127.0.0.1}:${platform.wms.port:8086}",
    path = "",
    fallbackFactory = WmsServerFeignClientFallbackFactory.class
)
public interface WmsServerFeignClient {
    /** 订单信息及明细查询 */
    @PostMapping("/rsf-server/erp/query/order")
    Map<String, Object> queryOrderAndDetls(@RequestBody ErpOpParams params);
    /** 订单信息修改/添加 */
    @PostMapping("/rsf-server/order/sync/orders/update")
    Map<String, Object> updateOrderDetls(@RequestBody List<Map<String, Object>> body);
    /** 删除/取消单据(服务端接收 List&lt;SyncOrderParams&gt;) */
    @PostMapping("/rsf-server/order/sync/orders/delete")
    Map<String, Object> orderDel(@RequestBody List<Map<String, Object>> body);
    /** 物料信息同步 */
    @PostMapping("/rsf-server/base/mat/sync/auth/v1")
    Map<String, Object> syncMatnrs(@RequestBody ErpMatnrParms params);
    /** 库位信息查询 */
    @PostMapping("/rsf-server/erp/query/locs/detls")
    Map<String, Object> queryLocsDetls(@RequestBody Map<String, Object> params);
    /** 库存明细查询(对接协议 8.4) */
    @PostMapping("/rsf-server/erp/inventory/details")
    Map<String, Object> inventoryDetails(@RequestBody Map<String, Object> params);
    /** 库存汇总查询(对接协议 8.5) */
    @PostMapping("/rsf-server/erp/inventory/summary")
    Map<String, Object> inventorySummary(@RequestBody Map<String, Object> params);
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/wms/fallback/WmsServerFeignClientFallback.java
New file
@@ -0,0 +1,130 @@
package com.vincent.rsf.openApi.feign.wms.fallback;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.openApi.entity.params.ErpMatnrParms;
import com.vincent.rsf.openApi.entity.params.ErpOpParams;
import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
 * WMS Server Feign 客户端降级处理,在 Feign 内统一返回错误响应(不抛异常)。
 * 由 WmsServerFeignClientFallbackFactory 创建并传入异常 cause。
 */
@Slf4j
@Component
public class WmsServerFeignClientFallback implements WmsServerFeignClient {
    /** 触发降级时的异常,由 FallbackFactory 传入;无 cause 时为 null */
    private final Throwable cause;
    public WmsServerFeignClientFallback() {
        this.cause = null;
    }
    public WmsServerFeignClientFallback(Throwable cause) {
        this.cause = cause;
    }
    private Map<String, Object> errorResponse() {
        return R.error(filterErrorMessage(cause));
    }
    /**
     * 过滤错误消息中的URL,只保留错误类型
     * @param throwable 异常对象(可选)
     * @return 过滤后的完整错误消息(包含"查询失败:"前缀)
     */
    public static String filterErrorMessage(Throwable throwable) {
        if (throwable == null) {
            return "查询失败:服务调用失败,请稍后重试";
        }
        return filterErrorMessage(throwable.getMessage());
    }
    /**
     * 过滤错误消息中的URL,只保留错误类型
     * @param errorMessage 错误消息字符串(可选)
     * @return 过滤后的完整错误消息(包含"查询失败:"前缀)
     */
    public static String filterErrorMessage(String errorMessage) {
        if (errorMessage == null || errorMessage.isEmpty()) {
            return "查询失败:未知错误";
        }
        String filteredMessage = errorMessage;
        // 如果包含"executing",说明是HTTP请求错误,去掉URL部分
        if (filteredMessage.contains("executing")) {
            int executingIndex = filteredMessage.indexOf("executing");
            if (executingIndex > 0) {
                // 提取"executing"之前的部分(如"Read timed out")
                filteredMessage = filteredMessage.substring(0, executingIndex).trim();
            } else {
                // 如果"executing"在开头,使用默认错误消息
                filteredMessage = "请求超时";
            }
        }
        // 如果包含"http://"或"https://",也尝试去掉URL部分
        else if (filteredMessage.contains("http://") || filteredMessage.contains("https://")) {
            // 使用正则表达式去掉URL
            filteredMessage = filteredMessage.replaceAll("https?://[^\\s]+", "").trim();
            if (filteredMessage.isEmpty()) {
                filteredMessage = "请求失败";
            }
        }
        // 如果过滤后的消息为空,使用默认错误消息
        if (filteredMessage.isEmpty()) {
            filteredMessage = "未知错误";
        }
        // 返回包含"查询失败:"前缀的完整错误消息
        return "查询失败:" + filteredMessage;
    }
    @Override
    public Map<String, Object> queryOrderAndDetls(ErpOpParams params) {
        log.error("调用立库WMS Server订单信息查询接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> updateOrderDetls(List<Map<String, Object>> body) {
        log.error("调用立库WMS Server订单修改接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> orderDel(List<Map<String, Object>> body) {
        log.error("调用立库WMS Server取消单据接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> syncMatnrs(ErpMatnrParms params) {
        log.error("调用立库WMS Server物料信息同步接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> queryLocsDetls(Map<String, Object> params) {
        log.error("调用立库WMS Server库位信息查询接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> inventoryDetails(Map<String, Object> params) {
        log.error("调用立库WMS Server库存明细查询接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> inventorySummary(Map<String, Object> params) {
        log.error("调用立库WMS Server库存汇总查询接口失败,触发降级", cause);
        return errorResponse();
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/feign/wms/fallback/WmsServerFeignClientFallbackFactory.java
New file
@@ -0,0 +1,17 @@
package com.vincent.rsf.openApi.feign.wms.fallback;
import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
 * Feign 调用失败时创建带异常信息的 Fallback,在 Feign 内统一返回错误响应。
 */
@Component
public class WmsServerFeignClientFallbackFactory implements FallbackFactory<WmsServerFeignClient> {
    @Override
    public WmsServerFeignClient create(Throwable cause) {
        return new WmsServerFeignClientFallback(cause);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/AppMapper.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.App;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AppMapper extends BaseMapper<App> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/filter/AppIdAuthenticationFilter.java
New file
@@ -0,0 +1,92 @@
package com.vincent.rsf.openApi.security.filter;
import com.vincent.rsf.openApi.entity.constant.Constants;
import com.vincent.rsf.openApi.security.service.AppAuthService;
import com.vincent.rsf.openApi.security.utils.TokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * AppId/Token 认证过滤器
 */
@Slf4j
@Component
@Order(1)
public class AppIdAuthenticationFilter extends OncePerRequestFilter {
    @Resource
    private AppAuthService appAuthService;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        if (isAuthRequest(requestURI)) {
            filterChain.doFilter(request, response);
            return;
        }
        String authHeader = request.getHeader(Constants.HEADER_AUTHORIZATION);
        if (authHeader != null) {
            String token = TokenUtils.extractTokenFromHeader(authHeader);
            if (token != null && TokenUtils.validateTokenTime(token)) {
                String tokenAppId = TokenUtils.getAppIdFromToken(token);
                String tokenAppSecret = TokenUtils.getSecretFromToken(token);
                if (!StringUtils.hasText(tokenAppId) || !StringUtils.hasText(tokenAppSecret)
                        || !appAuthService.validateApp(tokenAppId, tokenAppSecret)) {
                    log.warn("Token验证失败");
                    sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token");
                    return;
                }
                request.setAttribute(Constants.REQUEST_ATTR_APP_ID, tokenAppId);
            } else {
                log.warn("Token验证失败或缺失");
                sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token");
                return;
            }
        } else {
            log.warn("缺少Token认证信息");
            sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token");
            return;
        }
        filterChain.doFilter(request, response);
    }
    private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException {
        response.setStatus(code);
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write("{\"code\": " + code + ", \"msg\": \"" + message + "\", \"data\": null}");
        writer.flush();
    }
    private boolean isAuthRequest(String requestURI) {
        return requestURI != null && requestURI.contains("/getToken");
    }
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return requestURI == null
                || requestURI.contains("/auth/")
                || requestURI.contains("/public/")
                || requestURI.contains("/doc.html")
                || requestURI.contains("/swagger")
                || requestURI.contains("/webjars")
                || requestURI.contains("/v2/api-docs")
                || requestURI.contains("/v3/api-docs");
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/service/AppAuthService.java
New file
@@ -0,0 +1,58 @@
package com.vincent.rsf.openApi.security.service;
import com.vincent.rsf.openApi.entity.app.App;
import com.vincent.rsf.openApi.service.AppService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class AppAuthService {
    @Resource
    private AppService appService;
    @Resource
    private PasswordEncoder passwordEncoder;
    public boolean validateApp(String appId, String appSecret) {
        if (appId == null || appSecret == null) {
            return false;
        }
        try {
            App app = appService.getById(appId);
            if (app == null) {
                return false;
            }
            if (app.getEnable() != null && app.getEnable() != 1) {
                return false;
            }
            String stored = app.getScrect();
            if (stored == null) {
                return false;
            }
            // 存的是 BCrypt 哈希则用 matches,否则兼容明文
            if (stored.startsWith("$2")) {
                return passwordEncoder.matches(appSecret, stored);
            }
            return appSecret.equals(stored);
        } catch (Exception e) {
            log.error("validateApp异常 appId={}", appId, e);
            return false;
        }
    }
    public App getAppInfo(String appId) {
        if (appId == null) {
            return null;
        }
        try {
            return appService.getById(appId);
        } catch (Exception e) {
            log.error("getAppInfo失败 appId={}", appId, e);
            return null;
        }
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/utils/TokenUtils.java
New file
@@ -0,0 +1,87 @@
package com.vincent.rsf.openApi.security.utils;
import com.vincent.rsf.openApi.entity.constant.Constants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * JWT Token 工具类
 */
public class TokenUtils {
    private static final Logger log = LoggerFactory.getLogger(TokenUtils.class);
    private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long TOKEN_EXPIRATION = 60 * 60 * 1000L;
    public static String generateToken(Map<String, Object> claims) {
        long now = System.currentTimeMillis();
        Date expiration = new Date(now + TOKEN_EXPIRATION);
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(expiration)
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .compact();
    }
    public static String generateToken(String appId, String appSecret) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("appId", appId);
        claims.put("appSecret", appSecret);
        claims.put("created", System.currentTimeMillis());
        return generateToken(claims);
    }
    public static Claims parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            log.error("解析Token失败: {}", e.getMessage());
            return null;
        }
    }
    public static boolean validateTokenTime(String token) {
        try {
            Claims claims = parseToken(token);
            if (claims == null) {
                return false;
            }
            Date expiration = claims.getExpiration();
            return expiration != null && expiration.after(new Date());
        } catch (JwtException e) {
            log.error("验证Token失败: {}", e.getMessage());
            return false;
        }
    }
    public static String getAppIdFromToken(String token) {
        Claims claims = parseToken(token);
        return claims != null ? (String) claims.get("appId") : null;
    }
    public static String getSecretFromToken(String token) {
        Claims claims = parseToken(token);
        return claims != null ? (String) claims.get("appSecret") : null;
    }
    public static String extractTokenFromHeader(String authHeader) {
        if (authHeader != null && authHeader.startsWith(Constants.TOKEN_PREFIX)) {
            return authHeader.substring(Constants.TOKEN_PREFIX.length()).trim();
        }
        return null;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/AppService.java
New file
@@ -0,0 +1,7 @@
package com.vincent.rsf.openApi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.openApi.entity.app.App;
public interface AppService extends IService<App> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/TokenService.java
New file
@@ -0,0 +1,20 @@
package com.vincent.rsf.openApi.service;
/**
 * 对接协议 8.1 Token 签发与校验
 */
public interface TokenService {
    /**
     * 校验 appId+appSecret 并签发 token,有效期 1 小时
     * @param appId 应用编码
     * @param appSecret 应用秘钥
     * @return token 字符串,失败返回 null
     */
    String issueToken(String appId, String appSecret);
    /**
     * 校验 token 是否有效
     */
    boolean validateToken(String token);
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/WmsErpService.java
@@ -11,13 +11,31 @@
    CommonResponse getOrderInfo(ErpOpParams params);
    CommonResponse updateOrderDetl(ErpOpParams params);
    /** 新增单据(兼容修改):入/出库通知单下发,有则更新、无则新增 */
    CommonResponse addOrUpdateOrder(ErpOpParams params);
    CommonResponse orderDel(ErpOpParams params);
    /** 取消订单/取消单据:符合取消条件时执行取消逻辑 */
    CommonResponse orderCancel(ErpOpParams params);
    CommonResponse syncMatnrs(ErpMatnrParms parms);
    CommonResponse reportOrders(ReportParams params);
    CommonResponse reportCheck(ReportParams params);
    /** 库位信息查询(转发立库) */
    CommonResponse queryLocsDetls(Map<String, Object> params);
    /**
     * 通用转发:将云仓请求原样转发至立库 WMS(接口提供方:立库)
     * @param wmsPath 立库路径常量,见 WmsConstant
     * @param body 请求体,可为 List 或 Map/对象,null 时按空 body 转发
     */
    CommonResponse forwardToWms(String wmsPath, Object body);
    /** 对接协议 8.4 库存明细查询 */
    CommonResponse inventoryDetails(Map<String, Object> params);
    /** 对接协议 8.5 库存汇总查询 */
    CommonResponse inventorySummary(Map<String, Object> params);
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/AppServiceImpl.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.openApi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.openApi.entity.app.App;
import com.vincent.rsf.openApi.mapper.AppMapper;
import com.vincent.rsf.openApi.service.AppService;
import org.springframework.stereotype.Service;
@Service
public class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/TokenServiceImpl.java
New file
@@ -0,0 +1,56 @@
package com.vincent.rsf.openApi.service.impl;
import com.vincent.rsf.openApi.security.service.AppAuthService;
import com.vincent.rsf.openApi.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class TokenServiceImpl implements TokenService {
    private static final long EXPIRE_MS = 60 * 60 * 1000L;
    @Autowired
    private AppAuthService appAuthService;
    private final Map<String, Long> tokenExpire = new ConcurrentHashMap<>();
    @Override
    public String issueToken(String appId, String appSecret) {
        if (!StringUtils.hasText(appId) || !StringUtils.hasText(appSecret)) {
            return null;
        }
        if (!appAuthService.validateApp(appId, appSecret)) {
            return null;
        }
        String token = UUID.randomUUID().toString().replace("-", "");
        tokenExpire.put(token, System.currentTimeMillis() + EXPIRE_MS);
        evictExpired();
        return token;
    }
    @Override
    public boolean validateToken(String token) {
        if (!StringUtils.hasText(token)) {
            return false;
        }
        Long expire = tokenExpire.get(token);
        if (expire == null || System.currentTimeMillis() > expire) {
            tokenExpire.remove(token);
            return false;
        }
        return true;
    }
    private void evictExpired() {
        long now = System.currentTimeMillis();
        tokenExpire.entrySet().removeIf(e -> e.getValue() < now);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/WmsErpServiceImpl.java
@@ -11,10 +11,13 @@
import com.vincent.rsf.openApi.entity.constant.WmsConstant;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.dto.ErpCommonResponse;
import com.vincent.rsf.openApi.entity.dto.ResultData;
import com.vincent.rsf.openApi.entity.dto.OrderDto;
import com.vincent.rsf.openApi.entity.params.ErpMatnrParms;
import com.vincent.rsf.openApi.entity.params.ErpOpParams;
import com.vincent.rsf.openApi.entity.params.ReportParams;
import com.vincent.rsf.openApi.entity.params.WmsOrderItemParam;
import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient;
import com.vincent.rsf.openApi.service.WmsErpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +29,7 @@
import org.springframework.web.client.RestTemplate;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service("WmsErpService")
@@ -40,6 +44,47 @@
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private WmsServerFeignClient wmsServerFeignClient;
    /**
     * 可选:改用 OpenFeign 调用 ERP 上报(订单完成回写、盘点差异修改)时启用。
     * 1)增加 import:com.vincent.rsf.openApi.feign.erp.ErpReportFeignClient;
     * 2)取消下面两行注释,注入 ErpReportFeignClient;
     * 3)在 reportOrders、reportCheck 中注释掉本方法内整段 HttpEntity/restTemplate 请求,改为使用下面注释中的 erpReportFeignClient.report(params) 及响应解析逻辑。
     */
    // @Autowired
    // private ErpReportFeignClient erpReportFeignClient;
    /**
     * 将 Feign 返回的 Map(或 R)转为 CommonResponse,符合 8.2.3:code、msg、data(含 result)。
     * 无法解析或非成功(code!=200)时直接 throw CoolException,不返回错误体。
     */
    private CommonResponse mapToCommonResponse(Map<String, Object> map) {
        if (map == null) {
            throw new CoolException("请求失败");
        }
        Object c = map.get("code");
        int code = c instanceof Number ? ((Number) c).intValue() : 500;
        if (code != 200) {
            String msg = map.get("msg") != null ? map.get("msg").toString() : "请求失败";
            throw new CoolException(msg);
        }
        CommonResponse r = new CommonResponse();
        r.setCode(200);
        r.setMsg(map.get("msg") != null ? map.get("msg").toString() : "操作成功");
        Object rawData = map.get("data");
        if (rawData == null) {
            r.setData(ResultData.success());
        } else {
            Map<String, Object> dataModel = new LinkedHashMap<>();
            dataModel.put("result", ResultData.SUCCESS);
            dataModel.put("data", rawData);
            r.setData(dataModel);
        }
        return r;
    }
    /**
     * 获取订单明细
     *
@@ -51,131 +96,131 @@
        if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) {
            throw new CoolException("订单号不能为空!!");
        }
        /**WMS基础配置链接*/
        String rcsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.QUERY_ORDER_AND_DETLS;
        log.info("查询订单信息及状态: {}, 请求参数: {}", rcsUrl, JSONObject.toJSONString(params));
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        HttpEntity httpEntity = new HttpEntity(params, headers);
        ResponseEntity<String> exchange = restTemplate.exchange(rcsUrl, HttpMethod.POST, httpEntity, String.class);
        log.info("查询响应结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
                    JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData()));
                    OrderDto dto = new OrderDto();
                    dto.setOrderNo(object.getString("code"))
                            .setAnfme(object.getDouble("anfme"))
                            .setType(object.getString("type"))
                            .setWkType(object.getString("wkType"))
                            .setQty(object.getDouble("qty"))
                            .setPoCode(object.getString("poCode"))
                            .setExceStatus(object.getShort("exceStatus"))
                            .setWorkQty(object.getDouble("workQty"));
                    result.setData(dto);
                    return result;
                } else {
                    return result;
//                    throw new CoolException("查询失败!!");
                }
            } catch (JsonProcessingException e) {
                throw new CoolException(e.getMessage());
        log.info("查询订单信息及状态,请求参数: {}", JSONObject.toJSONString(params));
        Map<String, Object> res = wmsServerFeignClient.queryOrderAndDetls(params);
        CommonResponse result = mapToCommonResponse(res);
        if (result.getCode() == 200 && result.getData() instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> dataModel = (Map<String, Object>) result.getData();
            Object inner = dataModel.get("data");
            if (inner != null) {
                JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(inner));
                OrderDto dto = new OrderDto();
                dto.setOrderNo(object.getString("code"))
                        .setAnfme(object.getDouble("anfme"))
                        .setType(object.getString("type"))
                        .setWkType(object.getString("wkType"))
                        .setQty(object.getDouble("qty"))
                        .setPoCode(object.getString("poCode"))
                        .setExceStatus(object.getShort("exceStatus"))
                        .setWorkQty(object.getDouble("workQty"));
                Map<String, Object> wrap = new LinkedHashMap<>();
                wrap.put("result", ResultData.SUCCESS);
                wrap.put("data", dto);
                result.setData(wrap);
            }
        }
        return result;
    }
    /**
     * 新增单据(兼容修改、取消):8.3 入/出库通知单下发。operateType=3 时按取消处理。
     * 以 8.3 文档字段为主,转发立库时映射为服务端 SyncOrderParams 字段。
     */
    @Override
    public CommonResponse addOrUpdateOrder(ErpOpParams params) {
        if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) {
            throw new CoolException("订单号不能为空!!");
        }
        if (Integer.valueOf(3).equals(params.getOperateType())) {
            log.info("order/add 收到 operateType=3,走统一取消逻辑: {}", params.getOrderNo());
            return doCancel(params);
        }
        Map<String, Object> mapParams = toServerOrderMap(params);
        List<Map<String, Object>> maps = Collections.singletonList(mapParams);
        log.info("新增/修改单据,请求参数: {}", JSONArray.toJSONString(maps));
        Map<String, Object> res = wmsServerFeignClient.updateOrderDetls(maps);
        CommonResponse r = mapToCommonResponse(res);
        // 8.3.3:data 仅含 result,不返回业务载荷
        r.setData(ResultData.success());
        return r;
    }
    /** 8.3 参数转为立库 SyncOrderParams 结构(orderNo/type/wkType/anfme/arrTime/orderItems 等) */
    private Map<String, Object> toServerOrderMap(ErpOpParams params) {
        Map<String, Object> m = new HashMap<>();
        m.put("orderNo", params.getOrderNo());
        m.put("wkType", params.getWkType());
        m.put("type", params.getOrderType() != null ? String.valueOf(params.getOrderType()) : null);
        m.put("orderId", params.getOrderId());
        double anfmeSum = 0;
        if (params.getOrderItems() != null) {
            List<Map<String, Object>> items = params.getOrderItems().stream()
                    .map(this::toServerOrderItemMap)
                    .collect(Collectors.toList());
            m.put("orderItems", items);
            for (WmsOrderItemParam item : params.getOrderItems()) {
                anfmeSum += parseAnfme(item.getAnfme());
            }
        } else {
            m.put("orderItems", Collections.emptyList());
        }
        m.put("anfme", anfmeSum);
        if (params.getBusinessTime() != null) {
            m.put("arrTime", new Date(params.getBusinessTime() * 1000));
        } else if (params.getCreateTime() != null) {
            m.put("arrTime", new Date(params.getCreateTime() * 1000));
        }
        return m;
    }
    private Map<String, Object> toServerOrderItemMap(WmsOrderItemParam item) {
        Map<String, Object> m = new HashMap<>();
        m.put("matnr", item.getMatNr());
        m.put("maktx", item.getMakTx());
        m.put("platItemId", item.getLineId());
        m.put("anfme", parseAnfme(item.getAnfme()));
        m.put("spec", item.getSpec());
        m.put("model", item.getModel());
        m.put("unit", item.getUnit());
        m.put("batch", item.getBatch());
        return m;
    }
    private static double parseAnfme(String anfme) {
        if (anfme == null || anfme.trim().isEmpty()) {
            return 0;
        }
        try {
            return Double.parseDouble(anfme.trim());
        } catch (NumberFormatException e) {
            return 0;
        }
    }
    /**
     * 订单修改
     *
     * @param params
     * @return
     * 取消订单/取消单据。与 /order/add(operateType=3)共用同一套取消逻辑,转发立库 sync/orders/delete。
     */
    @Override
    public CommonResponse updateOrderDetl(ErpOpParams params) {
    public CommonResponse orderCancel(ErpOpParams params) {
        if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) {
            throw new CoolException("订单号不能为空!!");
        }
        /**WMS基础配置链接*/
        String wmsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.MODIFY_ORDER_DETLS;
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        List<Map<String, Object>> maps = new ArrayList<>();
        Map<String, Object> mapParams = new HashMap<>();
        mapParams.put("orderNo", params.getOrderNo());
        mapParams.put("anfme", params.getAnfme());
        mapParams.put("type", params.getType());
        mapParams.put("wkType", params.getWkType());
        mapParams.put("exceStatus", params.getExceStatus());
        mapParams.put("orderItems", params.getOrderItems());
        maps.add(mapParams);
        log.info("修改订单信息及状态: {}, 请求参数: {}", wmsUrl, JSONArray.toJSONString(maps));
        HttpEntity<List<Map<String, Object>>> httpEntity = new HttpEntity<>(maps, headers);
        ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity, String.class);
        log.info("订单修改返回结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
//                    JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData()));
                    return result;
                } else {
                    return result;
//                    throw new CoolException("查询失败!!");
                }
            } catch (JsonProcessingException e) {
                throw new CoolException(e.getMessage());
            }
        }
        return doCancel(params);
    }
    /**
     * 删除单据
     *
     * @param params
     * @return
     */
    @Override
    public CommonResponse orderDel(ErpOpParams params) {
        if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) {
            throw new CoolException("订单号不能为空!!");
        }
        /**WMS基础配置链接*/
        String rcsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.ORDER_DEL;
        log.info("查询订单信息及状态: {}, 请求参数: {}", rcsUrl, JSONObject.toJSONString(params));
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        HttpEntity httpEntity = new HttpEntity(params, headers);
        ResponseEntity<String> exchange = restTemplate.exchange(rcsUrl, HttpMethod.POST, httpEntity, String.class);
        log.info("查询响应结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("查询失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
                    return result;
                } else {
                    throw new CoolException("查询失败!!");
                }
            } catch (JsonProcessingException e) {
                throw new CoolException(e.getMessage());
            }
        }
    /** 统一取消逻辑:/order/add(operateType=3) 与 /order/cancel、/order/del 均走此方法;8.3.3 data 仅含 result */
    private CommonResponse doCancel(ErpOpParams params) {
        log.info("取消单据,请求参数: {}", JSONObject.toJSONString(params));
        Map<String, Object> one = new HashMap<>();
        one.put("orderNo", params.getOrderNo());
        one.put("orderItems", params.getOrderItems() != null ? params.getOrderItems().stream()
                .map(this::toServerOrderItemMap)
                .collect(Collectors.toList()) : Collections.emptyList());
        Map<String, Object> res = wmsServerFeignClient.orderDel(Collections.singletonList(one));
        CommonResponse r = mapToCommonResponse(res);
        r.setData(ResultData.success());
        return r;
    }
    /**
@@ -192,31 +237,9 @@
        if (Objects.isNull(params.getMaktx())) {
            throw new CoolException("物料名称不能为空!!");
        }
        /**WMS基础配置链接*/
        String rcsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.UPDATE_MATNR_INFO;
        log.info("物料修改:{}, 请求参数: {}", rcsUrl, JSONObject.toJSONString(params));
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        HttpEntity httpEntity = new HttpEntity(params, headers);
        ResponseEntity<String> exchange = restTemplate.exchange(rcsUrl, HttpMethod.POST, httpEntity, String.class);
        log.info("修改结果: {}", exchange);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("修改失败!!");
        } else {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
            try {
                CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class);
                if (result.getCode() == 200) {
                    return result;
                } else {
                    throw new CoolException("修改失败!!");
                }
            } catch (JsonProcessingException e) {
                throw new CoolException(e.getMessage());
            }
        }
        log.info("物料修改,请求参数: {}", JSONObject.toJSONString(params));
        Map<String, Object> res = wmsServerFeignClient.syncMatnrs(params);
        return mapToCommonResponse(res);
    }
    /**
@@ -252,6 +275,13 @@
                throw new CoolException("上传失败!!");
            }
        }
        // Map<String, Object> res = erpReportFeignClient.report(params);
        // if (res == null) throw new CoolException("上传失败!!");
        // Object c = res.get("code"); int code = c instanceof Number ? ((Number) c).intValue() : 500;
        // if (code != 200) throw new CoolException("上传失败!!");
        // CommonResponse commonResponse = new CommonResponse();
        // commonResponse.setCode(200).setMsg(String.valueOf(res.get("msg"))).setData(res.get("data"));
        // return commonResponse;
    }
    /**
@@ -287,6 +317,79 @@
                throw new CoolException("修改失败!!");
            }
        }
        // Map<String, Object> res = erpReportFeignClient.report(params);
        // if (res == null) throw new CoolException("修改失败!!");
        // Object c = res.get("code"); int code = c instanceof Number ? ((Number) c).intValue() : 500;
        // if (code != 200) throw new CoolException("修改失败!!");
        // CommonResponse commonResponse = new CommonResponse();
        // commonResponse.setCode(200).setMsg(String.valueOf(res.get("msg"))).setData(res.get("data"));
        // return commonResponse;
    }
    @Override
    public CommonResponse queryLocsDetls(Map<String, Object> params) {
        Map<String, Object> p = params == null ? new HashMap<>() : params;
        log.info("库位信息查询,请求参数: {}", JSONObject.toJSONString(p));
        return mapToCommonResponse(wmsServerFeignClient.queryLocsDetls(p));
    }
    /** 8.4 库存明细查询:返回值 data 为对象数组,不包 result 外层 */
    @Override
    public CommonResponse inventoryDetails(Map<String, Object> params) {
        Map<String, Object> p = params == null ? new HashMap<>() : params;
        log.info("库存明细查询,请求参数: {}", JSONObject.toJSONString(p));
        CommonResponse r = mapToCommonResponse(wmsServerFeignClient.inventoryDetails(p));
        unwrapDataToArray(r);
        return r;
    }
    /** 8.5 库存汇总查询:返回值 data 为对象数组,不包 result 外层 */
    @Override
    public CommonResponse inventorySummary(Map<String, Object> params) {
        Map<String, Object> p = params == null ? new HashMap<>() : params;
        log.info("库存汇总查询,请求参数: {}", JSONObject.toJSONString(p));
        CommonResponse r = mapToCommonResponse(wmsServerFeignClient.inventorySummary(p));
        unwrapDataToArray(r);
        return r;
    }
    /** 8.4/8.5 规范:data 为对象数组,将 { result, data: array } 改为 data = array */
    private void unwrapDataToArray(CommonResponse r) {
        if (r.getData() instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> dataModel = (Map<String, Object>) r.getData();
            Object inner = dataModel.get("data");
            if (inner != null) {
                r.setData(inner);
            }
        }
    }
    @Override
    public CommonResponse forwardToWms(String wmsPath, Object body) {
        String url = wmsApi.getHost() + ":" + wmsApi.getPort() + wmsPath;
        Object payload = body != null ? body : new HashMap<String, Object>();
        log.info("转发请求: {}, 请求体长度: {}", url, payload instanceof List ? ((List<?>) payload).size() : 1);
        return postToWms(url, payload);
    }
    /** 统一转发并解析为 CommonResponse */
    private CommonResponse postToWms(String url, Object body) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        headers.add("api-version", "v2.0");
        HttpEntity<Object> httpEntity = new HttpEntity<>(body, headers);
        ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
        if (Objects.isNull(exchange.getBody())) {
            throw new CoolException("请求失败!!");
        }
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty);
        try {
            return objectMapper.readValue(exchange.getBody(), CommonResponse.class);
        } catch (JsonProcessingException e) {
            throw new CoolException("解析响应失败:" + e.getMessage());
        }
    }
}
rsf-open-api/src/main/resources/application-dev.yml
@@ -5,6 +5,10 @@
spring:
  application:
    name: @pom.artifactId@
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true   # Feign 调用失败时走 Fallback,在 Feign 内统一返回错误
  mvc:
    static-path-pattern: /**
    path match:
@@ -12,6 +16,7 @@
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#    url: jdbc:mysql://127.0.0.1:3306/jdxajwms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 12345
    type: com.alibaba.druid.pool.DruidDataSource
@@ -64,6 +69,6 @@
    port: 8086
  erp:
    #链接
    host: http://www.itsdg.cn
    host: http://127.0.0.1
    #端口
    port: 3741
rsf-open-api/src/main/resources/application-prod.yml
@@ -77,5 +77,5 @@
    host: http://127.0.0.1
    port: 8085
  erp:
    host: http://www.itsdg.cn
    host: http://127.0.0.1
    port: 3741
rsf-open-api/src/main/resources/application.yml
@@ -15,8 +15,8 @@
    :banner: false
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
      logic-delete-value: 1     #删除状态
      logic-not-delete-value: 0 #正常状态
logging:
  file:
rsf-server/pom.xml
@@ -33,6 +33,25 @@
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 熔断器:Feign 调用失败时触发 Fallback,在 Feign 内统一返回错误响应 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
rsf-server/src/main/java/com/vincent/rsf/server/ServerBoot.java
@@ -2,10 +2,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.vincent.rsf.server", "com.vincent.rsf.common"})
@EnableFeignClients(basePackages = "com.vincent.rsf.server.api.feign")
public class ServerBoot {
    public static void main(String[] args) {
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RemotesInfoProperties.java
@@ -30,14 +30,23 @@
     */
    private String prePath;
    /**
     * 云仓地址
     */
    private String baseUrl;
    @Data
    @Configuration
    @ConfigurationProperties(prefix = "platform.erp.api")
    public class ApiInfo {
        /**
         * 一键上报质检接口
         */
        /** 一键上报质检接口 */
        private String notifyInspect;
        /** 9.1 入/出库结果上报(立库侧请求云仓) */
        private String inOutResultPath = "/api/report/inOutResult";
        /** 9.2 库存调整主动上报(立库侧请求云仓) */
        private String inventoryAdjustPath = "/api/report/inventoryAdjust";
        /** 物料基础信息同步(立库侧请求云仓) */
        private String matSyncPath = "/api/mat/sync";
    }
    @Data
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/CloudWmsMockController.java
New file
@@ -0,0 +1,67 @@
package com.vincent.rsf.server.api.controller;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
 * 云仓WMS 模拟接口(对接协议 9.1、9.2、物料同步)。
 * 云仓未提供真实 URL 时,可将 platform.erp.base-url 指向本机该服务(如 http://127.0.0.1:8086/rsf-server),
 * 立库上报请求会打到本接口并返回模拟成功。
 */
@Slf4j
@RestController
@RequestMapping("/api")
@Api(value = "云仓模拟接口", tags = "云仓模拟(无真实云仓URL时使用)")
public class CloudWmsMockController {
    private static Map<String, Object> successResponse() {
        Map<String, Object> data = new HashMap<>();
        data.put("result", "SUCCESS");
        Map<String, Object> map = new HashMap<>();
        map.put("code", 200);
        map.put("msg", "");
        map.put("data", data);
        return map;
    }
    /** 9.1 入/出库结果上报 - 模拟 */
    @ApiOperation("入/出库结果上报(模拟)")
    @PostMapping(value = "/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Map<String, Object> mockInOutResult(@RequestBody InOutResultReportParam body) {
        log.info("云仓模拟-入/出库结果上报,orderNo={},locId={},matNr={}",
                body != null ? body.getOrderNo() : null,
                body != null ? body.getLocId() : null,
                body != null ? body.getMatNr() : null);
        return successResponse();
    }
    /** 9.2 库存调整主动上报 - 模拟 */
    @ApiOperation("库存调整主动上报(模拟)")
    @PostMapping(value = "/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Map<String, Object> mockInventoryAdjust(@RequestBody InventoryAdjustReportParam body) {
        log.info("云仓模拟-库存调整上报,changeType={},wareHouseId={},matNr={}",
                body != null ? body.getChangeType() : null,
                body != null ? body.getWareHouseId() : null,
                body != null ? body.getMatNr() : null);
        return successResponse();
    }
    /** 物料基础信息同步 - 模拟 */
    @ApiOperation("物料同步(模拟)")
    @PostMapping(value = "/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Map<String, Object> mockMatSync(@RequestBody Object body) {
        log.info("云仓模拟-物料同步,body={}", body != null ? body.toString() : null);
        return successResponse();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/ErpQueryController.java
@@ -104,4 +104,22 @@
        return receiveMsgService.queryTransfer(queryParams);
    }
    /**
     * 对接协议 8.4 库存明细查询
     */
    @PostMapping("/inventory/details")
    @ApiOperation(value = "库存明细查询")
    public R inventoryDetails(@RequestBody(required = false) InventoryDetailsParam param) {
        return receiveMsgService.inventoryDetails(param != null ? param : new InventoryDetailsParam());
    }
    /**
     * 对接协议 8.5 库存汇总查询
     */
    @PostMapping("/inventory/summary")
    @ApiOperation(value = "库存汇总查询")
    public R inventorySummary(@RequestBody(required = false) InventorySummaryParam param) {
        return receiveMsgService.inventorySummary(param != null ? param : new InventorySummaryParam());
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/BaseMatParms.java
@@ -1,20 +1,29 @@
package com.vincent.rsf.server.api.controller.erp.params;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.annotations.ApiModelProperty;
import lombok.experimental.Accessors;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 物料基础信息同步入参(对接协议 8.2)
 * 协议字段 matNr/makTx 与 matnr/maktx 均可接收。
 */
@Data
@Accessors(chain = true)
public class BaseMatParms {
    @ApiModelProperty("物料名称")
    private String maktx;
    @ApiModelProperty(value = "操作类型:1新增 2修改 3禁用 4启用", example = "1")
    private Integer operateType;
    @ApiModelProperty("物料编码*")
    @ApiModelProperty(value = "物料编码*(协议字段 matNr 同义)")
    @JsonAlias("matNr")
    private String matnr;
    @ApiModelProperty(value = "物料名称(协议字段 makTx 同义)")
    @JsonAlias("makTx")
    private String maktx;
    @ApiModelProperty("物料分组")
    private String groupName;
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/FlexibleDateDeserializer.java
New file
@@ -0,0 +1,76 @@
package com.vincent.rsf.server.api.controller.erp.params;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
 * 支持时间戳(秒/毫秒)与多种字符串格式的 Date 反序列化。
 * 字符串支持:ISO-8601(如 2024-03-03T08:00:00.000+00:00)、yyyy-MM-dd HH:mm:ss,精确到秒。
 */
public class FlexibleDateDeserializer extends JsonDeserializer<Date> {
    private static final long TIMESTAMP_MS_THRESHOLD = 10_000_000_000L; // 约 1970-04-26 起为毫秒
    private static final String PATTERN_SECONDS = "yyyy-MM-dd HH:mm:ss";
    private static final TimeZone DEFAULT_TZ = TimeZone.getTimeZone("GMT+8");
    @Override
    public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonToken t = p.getCurrentToken();
        if (t == JsonToken.VALUE_NUMBER_INT || t == JsonToken.VALUE_NUMBER_FLOAT) {
            long v = p.getLongValue();
            long ms = v < TIMESTAMP_MS_THRESHOLD ? v * 1000 : v;
            return new Date(ms);
        }
        if (t == JsonToken.VALUE_STRING) {
                String s = p.getText().trim();
                if (s.isEmpty()) {
                    return null;
                }
                // 1) 尝试纯数字字符串(秒或毫秒)
                try {
                    long v = Long.parseLong(s);
                    long ms = v < TIMESTAMP_MS_THRESHOLD ? v * 1000 : v;
                    return new Date(ms);
                } catch (NumberFormatException ignored) {
                }
                // 2) ISO-8601(含 T 和时区)
                if (s.contains("T")) {
                    try {
                        return java.util.Date.from(java.time.Instant.parse(s));
                    } catch (Exception ignored) {
                    }
                }
                // 3) yyyy-MM-dd HH:mm:ss
                try {
                    SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_SECONDS);
                    sdf.setTimeZone(DEFAULT_TZ);
                    sdf.setLenient(false);
                    return sdf.parse(s);
                } catch (Exception ignored) {
                }
                // 4) 仅日期 yyyy-MM-dd
                if (s.length() == 10 && s.charAt(4) == '-' && s.charAt(7) == '-') {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                        sdf.setTimeZone(DEFAULT_TZ);
                        sdf.setLenient(false);
                        return sdf.parse(s);
                    } catch (Exception ignored) {
                    }
                }
                throw new IOException("Cannot parse date: " + s + ", support: timestamp(seconds/ms), ISO-8601, yyyy-MM-dd HH:mm:ss");
        }
        if (t == JsonToken.VALUE_NULL) {
            return null;
        }
        return (Date) ctxt.handleUnexpectedToken(Date.class, p);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InOutResultReportParam.java
New file
@@ -0,0 +1,42 @@
package com.vincent.rsf.server.api.controller.erp.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 对接协议 9.1 入/出库结果上报 - 请求体(立库WMS 直接请求 云仓WMS)
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "InOutResultReportParam", description = "入/出库结果上报")
public class InOutResultReportParam {
    @ApiModelProperty(value = "订单编码", required = true)
    private String orderNo;
    @ApiModelProperty("计划跟踪号")
    private String planNo;
    @ApiModelProperty("行内码")
    private String lineId;
    @ApiModelProperty(value = "仓库编码", required = true)
    private String wareHouseId;
    @ApiModelProperty(value = "库位号", required = true)
    private String locId;
    @ApiModelProperty(value = "物料编码", required = true)
    private String matNr;
    @ApiModelProperty(value = "本次出/入数量", required = true)
    private String qty;
    @ApiModelProperty("托盘号")
    private String palletId;
    @ApiModelProperty("批次")
    private String batch;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryAdjustReportParam.java
New file
@@ -0,0 +1,37 @@
package com.vincent.rsf.server.api.controller.erp.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * 对接协议 9.2 库存调整主动上报 - 请求体
 * 立库侧调用云仓通知:立库WMS 主动调整库存后向云仓WMS 上报。
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "InventoryAdjustReportParam", description = "库存调整主动上报")
public class InventoryAdjustReportParam {
    @ApiModelProperty(value = "调整类型:1 入库;2 出库;3 移库", required = true)
    private Integer changeType;
    @ApiModelProperty(value = "仓库编码", required = true)
    private String wareHouseId;
    @ApiModelProperty("源库位号")
    private String sourceLocId;
    @ApiModelProperty("目标库位号(移库时有)")
    private String targetLocId;
    @ApiModelProperty(value = "物料编码", required = true)
    private String matNr;
    @ApiModelProperty(value = "调整数量", required = true)
    private String qty;
    @ApiModelProperty("托盘号")
    private String palletId;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryDetailsParam.java
New file
@@ -0,0 +1,38 @@
package com.vincent.rsf.server.api.controller.erp.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
 * 对接协议 8.4 库存明细查询 请求参数
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "InventoryDetailsParam", description = "库存明细查询")
public class InventoryDetailsParam implements Serializable {
    @ApiModelProperty("仓库编码")
    private String wareHouseId;
    @ApiModelProperty("库位编码")
    private String locId;
    @ApiModelProperty("物料编码")
    private String matNr;
    @ApiModelProperty("订单号/工单号")
    private String orderNo;
    @ApiModelProperty("计划跟踪号")
    private String planNo;
    @ApiModelProperty("批次号")
    private String batch;
    @ApiModelProperty("物料组")
    private String matGroup;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventorySummaryParam.java
New file
@@ -0,0 +1,23 @@
package com.vincent.rsf.server.api.controller.erp.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
 * 对接协议 8.5 库存汇总查询 请求参数
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "InventorySummaryParam", description = "库存汇总查询")
public class InventorySummaryParam implements Serializable {
    @ApiModelProperty("仓库编码")
    private String wareHouseId;
    @ApiModelProperty("物料编码,多个以英文逗号分隔")
    private String matNr;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncOrderParams.java
@@ -1,6 +1,7 @@
package com.vincent.rsf.server.api.controller.erp.params;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -36,8 +37,9 @@
    @ApiModelProperty("数量")
    private Double anfme;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:ss:mm")
    @JsonFormat(pattern = "yyyy-MM-dd HH:ss:mm")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JsonDeserialize(using = FlexibleDateDeserializer.class)
    private Date arrTime;
    @ApiModelProperty("单据明细信息")
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/CloudWmsErpFeignClient.java
New file
@@ -0,0 +1,35 @@
package com.vincent.rsf.server.api.feign;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import com.vincent.rsf.server.api.feign.fallback.CloudWmsErpFeignClientFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
/**
 * 立库侧通过 OpenFeign 调用云仓WMS:入/出库结果上报(9.1)、库存调整上报(9.2)、物料同步。
 * 使用 platform.erp.base-url 作为根地址;失败时走 Fallback,统一返回错误响应(不抛异常)。
 */
@FeignClient(
    name = "cloudWmsErp",
    url = "${platform.erp.base-url:http://127.0.0.1:8080}",
    fallbackFactory = CloudWmsErpFeignClientFallbackFactory.class
)
public interface CloudWmsErpFeignClient {
    /** 9.1 入/出库结果上报 */
    @PostMapping(value = "/api/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE)
    Map<String, Object> reportInOutResult(@RequestBody InOutResultReportParam body);
    /** 9.2 库存调整主动上报 */
    @PostMapping(value = "/api/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE)
    Map<String, Object> reportInventoryAdjust(@RequestBody InventoryAdjustReportParam body);
    /** 物料基础信息同步 */
    @PostMapping(value = "/api/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE)
    Map<String, Object> syncMatnrs(@RequestBody Object body);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/fallback/CloudWmsErpFeignClientFallback.java
New file
@@ -0,0 +1,95 @@
package com.vincent.rsf.server.api.feign.fallback;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
 * 云仓WMS Feign 客户端降级处理,在 Feign 内统一返回错误响应(不抛异常)。
 * 由 CloudWmsErpFeignClientFallbackFactory 创建并传入异常 cause。
 */
@Slf4j
@Component
public class CloudWmsErpFeignClientFallback implements CloudWmsErpFeignClient {
    private final Throwable cause;
    public CloudWmsErpFeignClientFallback() {
        this.cause = null;
    }
    public CloudWmsErpFeignClientFallback(Throwable cause) {
        this.cause = cause;
    }
    private Map<String, Object> errorResponse() {
        return resultMap(500, filterErrorMessage(cause), dataFail());
    }
    private static Map<String, Object> dataFail() {
        Map<String, Object> data = new HashMap<>();
        data.put("result", "FAIL");
        return data;
    }
    private static Map<String, Object> resultMap(int code, String msg, Map<String, Object> data) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", code);
        map.put("msg", msg);
        map.put("data", data);
        return map;
    }
    /**
     * 过滤错误消息中的 URL,只保留错误类型
     */
    public static String filterErrorMessage(Throwable throwable) {
        if (throwable == null) {
            return "请求失败:服务调用失败,请稍后重试";
        }
        return filterErrorMessage(throwable.getMessage());
    }
    public static String filterErrorMessage(String errorMessage) {
        if (errorMessage == null || errorMessage.isEmpty()) {
            return "请求失败:未知错误";
        }
        String filteredMessage = errorMessage;
        if (filteredMessage.contains("executing")) {
            int i = filteredMessage.indexOf("executing");
            filteredMessage = i > 0 ? filteredMessage.substring(0, i).trim() : "请求超时";
        } else if (filteredMessage.contains("http://") || filteredMessage.contains("https://")) {
            filteredMessage = filteredMessage.replaceAll("https?://[^\\s]+", "").trim();
            if (filteredMessage.isEmpty()) {
                filteredMessage = "请求失败";
            }
        }
        if (filteredMessage.isEmpty()) {
            filteredMessage = "未知错误";
        }
        return "请求失败:" + filteredMessage;
    }
    @Override
    public Map<String, Object> reportInOutResult(InOutResultReportParam body) {
        log.error("调用云仓WMS 入/出库结果上报接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam body) {
        log.error("调用云仓WMS 库存调整上报接口失败,触发降级", cause);
        return errorResponse();
    }
    @Override
    public Map<String, Object> syncMatnrs(Object body) {
        log.error("调用云仓WMS 物料同步接口失败,触发降级", cause);
        return errorResponse();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/feign/fallback/CloudWmsErpFeignClientFallbackFactory.java
New file
@@ -0,0 +1,17 @@
package com.vincent.rsf.server.api.feign.fallback;
import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
 * Feign 调用云仓失败时创建带异常信息的 Fallback,在 Feign 内统一返回错误响应。
 */
@Component
public class CloudWmsErpFeignClientFallbackFactory implements FallbackFactory<CloudWmsErpFeignClient> {
    @Override
    public CloudWmsErpFeignClient create(Throwable cause) {
        return new CloudWmsErpFeignClientFallback(cause);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/CloudWmsReportService.java
New file
@@ -0,0 +1,33 @@
package com.vincent.rsf.server.api.service;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import java.util.Map;
/**
 * 立库侧请求云仓WMS:上报、物料同步等
 */
public interface CloudWmsReportService {
    /**
     * 物料基础信息同步(立库侧请求云仓)
     * @param body 物料数据,可为单条或列表,具体结构以云仓接口为准
     * @return 云仓返回结构 Map:code, msg, data
     */
    Map<String, Object> syncMatnrsToCloud(Object body);
    /**
     * 9.1 入/出库结果上报
     * @param param 上报参数
     * @return 云仓返回结构 Map:code, msg, data(data.result 为 SUCCESS/FAIL)
     */
    Map<String, Object> reportInOutResult(InOutResultReportParam param);
    /**
     * 9.2 库存调整主动上报(立库侧调用云仓通知)
     * @param param 上报参数
     * @return 云仓返回结构 Map:code, msg, data(data.result 为 SUCCESS/FAIL)
     */
    Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam param);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/ReceiveMsgService.java
@@ -152,4 +152,14 @@
     * @return
     */
    R matUpdate(BaseMatParms baseMatParms);
    /**
     * 对接协议 8.4 库存明细查询
     */
    R inventoryDetails(InventoryDetailsParam param);
    /**
     * 对接协议 8.5 库存汇总查询
     */
    R inventorySummary(InventorySummaryParam param);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/CloudWmsReportServiceImpl.java
New file
@@ -0,0 +1,99 @@
package com.vincent.rsf.server.api.service.impl;
import com.vincent.rsf.server.api.config.RemotesInfoProperties;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient;
import com.vincent.rsf.server.api.service.CloudWmsReportService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
 * 立库侧请求云仓:入/出库结果上报(9.1)、库存调整主动上报(9.2)、物料基础信息同步。
 * 使用 OpenFeign 调用;可选 HttpEntity(RestTemplate) 方式已注释保留。
 */
@Slf4j
@Service
public class CloudWmsReportServiceImpl implements CloudWmsReportService {
    @Autowired
    private RemotesInfoProperties erpApi;
    @Autowired
    private RemotesInfoProperties.ApiInfo erpApiInfo;
    @Autowired
    private CloudWmsErpFeignClient cloudWmsErpFeignClient;
    /**
     * 可选:改用 HttpEntity(RestTemplate) 调用云仓时启用。
     */
    // @Autowired
    // private RestTemplate restTemplate;
    @Override
    public Map<String, Object> syncMatnrsToCloud(Object body) {
        if (!isCloudWmsConfigured()) {
            log.warn("ErpApi(云仓WMS) 未配置 host,跳过物料基础信息同步");
            return stubSuccess("云仓地址未配置,未实际同步");
        }
        return cloudWmsErpFeignClient.syncMatnrs(body != null ? body : new HashMap<>());
    }
    @Override
    public Map<String, Object> reportInOutResult(InOutResultReportParam param) {
        if (param == null) {
            return resultMap(400, "参数不能为空", null);
        }
        if (!isCloudWmsConfigured()) {
            log.warn("ErpApi(云仓WMS) 未配置 host,跳过 9.1 入/出库结果上报,订单:{}", param.getOrderNo());
            return stubSuccess("云仓地址未配置,未实际上报");
        }
        return cloudWmsErpFeignClient.reportInOutResult(param);
    }
    @Override
    public Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam param) {
        if (param == null) {
            return resultMap(400, "参数不能为空", null);
        }
        if (!isCloudWmsConfigured()) {
            log.warn("ErpApi(云仓WMS) 未配置 host,跳过 9.2 库存调整上报,物料:{}", param.getMatNr());
            return stubSuccess("云仓地址未配置,未实际上报");
        }
        return cloudWmsErpFeignClient.reportInventoryAdjust(param);
    }
    private boolean isCloudWmsConfigured() {
        String host = erpApi.getHost();
        return host != null && !host.trim().isEmpty();
    }
    private Map<String, Object> stubSuccess(String msg) {
        Map<String, Object> data = new HashMap<>();
        data.put("result", "SUCCESS");
        return resultMap(200, msg, data);
    }
    private Map<String, Object> resultMap(int code, String msg, Map<String, Object> data) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", code);
        map.put("msg", msg);
        map.put("data", data);
        return map;
    }
    // ========== 可选:HttpEntity(RestTemplate) 方式(当前未使用) ==========
    // 启用步骤:1)取消上方 restTemplate 的 @Autowired 注入;
    // 2)取消下面整段注释,恢复 buildUrl、postToCloudWms、parseResponse 方法及 OBJECT_MAPPER;
    // 3)在 syncMatnrsToCloud/reportInOutResult/reportInventoryAdjust 中改为:String url = buildUrl(erpApiInfo.getXxxPath()); if (url == null) return stubSuccess(...); return postToCloudWms(url, body);
    //
    // private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    // private String buildUrl(String path) { ... }
    // private Map<String, Object> postToCloudWms(String url, Object body) { HttpHeaders headers = ...; HttpEntity<Object> entity = new HttpEntity<>(body, headers); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); return parseResponse(response.getBody()); }
    // private Map<String, Object> parseResponse(String json) { ... }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java
@@ -579,7 +579,7 @@
                || !Objects.isNull(fieldIndex) || !Cools.isEmpty(matnrCode) || !Cools.isEmpty(asnCode);
        
        if (!hasValidCondition) {
            throw new CoolException("请至少输入一个查询条件:物料编码、ASN单号、跟踪码、批次或票号");
            throw new CoolException("请至少输入一个查询条件:物料编码、WMS单号、批号");/*、跟踪码、批次或票号*/
        }
        
        // 如果扫描物料编码且ASN单号为空,直接从物料信息表获取,不查询收货区
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java
@@ -9,6 +9,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.controller.erp.params.*;
@@ -101,6 +102,8 @@
    private DictDataService dictDataService;
    @Autowired
    private DictTypeService dictTypeService;
    @Autowired
    private LocItemService locItemService;
    /**
@@ -773,9 +776,8 @@
    }
    /**
     * 基础物料信息变更
     * @param baseMatParms
     * @return
     * 基础物料信息变更(对接协议 8.2)
     * operateType:1新增 2修改 3禁用 4启用;不传或 1/2 时按有则更新、无则新增。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -783,6 +785,21 @@
        if (StringUtils.isBlank(baseMatParms.getMatnr())) {
            throw new CoolException("物料编码不能为空!!");
        }
        Integer operateType = baseMatParms.getOperateType();
        // 3 禁用 / 4 启用:仅更新状态(status 1 正常 0 冻结)
        if (Integer.valueOf(3).equals(operateType) || Integer.valueOf(4).equals(operateType)) {
            Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, baseMatParms.getMatnr()));
            if (matnr == null) {
                throw new CoolException("物料不存在,无法执行禁用/启用!!");
            }
            int status = Integer.valueOf(4).equals(operateType) ? 1 : 0; // 4 启用=1 正常,3 禁用=0 冻结
            matnr.setStatus(status);
            if (!matnrService.updateById(matnr)) {
                throw new CoolException(operateType == 4 ? "物料启用失败!!" : "物料禁用失败!!");
            }
            return R.ok();
        }
        // 1 新增 / 2 修改 / 不传:有则更新、无则新增
        Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, baseMatParms.getMatnr()));
        if (Objects.isNull(matnr)) {
            Matnr matnr1 = new Matnr();
@@ -819,4 +836,118 @@
        return R.ok();
    }
    @Override
    public R inventoryDetails(InventoryDetailsParam param) {
        LambdaQueryWrapper<LocItem> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LocItem::getDeleted, 0);
        if (!Cools.isEmpty(param.getLocId())) {
            wrapper.eq(LocItem::getLocCode, param.getLocId());
        }
        if (!Cools.isEmpty(param.getMatNr())) {
            wrapper.eq(LocItem::getMatnrCode, param.getMatNr());
        }
        if (!Cools.isEmpty(param.getBatch())) {
            wrapper.eq(LocItem::getBatch, param.getBatch());
        }
        if (!Cools.isEmpty(param.getOrderNo())) {
            wrapper.and(w -> w.eq(LocItem::getPlatOrderCode, param.getOrderNo()).or().eq(LocItem::getPlatWorkCode, param.getOrderNo()));
        }
        if (!Cools.isEmpty(param.getPlanNo())) {
            wrapper.eq(LocItem::getPlatWorkCode, param.getPlanNo());
        }
        if (!Cools.isEmpty(param.getWareHouseId())) {
            Warehouse wh = warehouseService.getOne(new LambdaQueryWrapper<Warehouse>().eq(Warehouse::getCode, param.getWareHouseId()));
            if (wh != null) {
                List<Loc> locs = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getWarehouseId, wh.getId()));
                if (!locs.isEmpty()) {
                    wrapper.in(LocItem::getLocId, locs.stream().map(Loc::getId).collect(Collectors.toList()));
                } else {
                    return R.ok().add(Collections.emptyList());
                }
            } else {
                return R.ok().add(Collections.emptyList());
            }
        }
        List<LocItem> list = locItemService.list(wrapper);
        List<Map<String, Object>> result = new ArrayList<>();
        for (LocItem item : list) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("locId", item.getLocCode());
            Loc loc = locService.getById(item.getLocId());
            if (loc != null && loc.getWarehouseId() != null) {
                Warehouse w = warehouseService.getById(loc.getWarehouseId());
                row.put("wareHouseId", w != null ? w.getCode() : null);
                row.put("wareHouseName", w != null ? w.getName() : null);
            } else {
                row.put("wareHouseId", null);
                row.put("wareHouseName", null);
            }
            row.put("palletId", item.getTrackCode());
            row.put("matNr", item.getMatnrCode());
            row.put("makTx", item.getMaktx());
            row.put("anfme", item.getAnfme() != null ? item.getAnfme() : 0);
            row.put("unit", item.getUnit());
            row.put("status", item.getStatus() != null ? item.getStatus() : 1);
            row.put("orderType", item.getWkType());
            row.put("orderNo", item.getPlatOrderCode());
            row.put("planNo", item.getPlatWorkCode());
            row.put("batch", item.getBatch());
            result.add(row);
        }
        return R.ok().add(result);
    }
    @Override
    public R inventorySummary(InventorySummaryParam param) {
        LambdaQueryWrapper<LocItem> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LocItem::getDeleted, 0).select(LocItem::getLocId, LocItem::getMatnrCode, LocItem::getMaktx, LocItem::getAnfme, LocItem::getUnit);
        if (!Cools.isEmpty(param.getWareHouseId())) {
            Warehouse wh = warehouseService.getOne(new LambdaQueryWrapper<Warehouse>().eq(Warehouse::getCode, param.getWareHouseId()));
            if (wh != null) {
                List<Loc> locs = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getWarehouseId, wh.getId()));
                if (!locs.isEmpty()) {
                    wrapper.in(LocItem::getLocId, locs.stream().map(Loc::getId).collect(Collectors.toList()));
                } else {
                    return R.ok().add(Collections.emptyList());
                }
            } else {
                return R.ok().add(Collections.emptyList());
            }
        }
        if (!Cools.isEmpty(param.getMatNr())) {
            List<String> matNrs = Arrays.asList(param.getMatNr().split(","));
            wrapper.in(LocItem::getMatnrCode, matNrs.stream().map(String::trim).collect(Collectors.toList()));
        }
        List<LocItem> list = locItemService.list(wrapper);
        Map<String, Map<String, Object>> sumMap = new LinkedHashMap<>();
        for (LocItem item : list) {
            Loc loc = locService.getById(item.getLocId());
            String whId = null;
            String whName = null;
            if (loc != null && loc.getWarehouseId() != null) {
                Warehouse w = warehouseService.getById(loc.getWarehouseId());
                whId = w != null ? w.getCode() : null;
                whName = w != null ? w.getName() : null;
            }
            String key = (whId != null ? whId : "") + "|" + (item.getMatnrCode() != null ? item.getMatnrCode() : "");
            final String finalWhId = whId;
            final String finalWhName = whName;
            sumMap.compute(key, (k, v) -> {
                if (v == null) {
                    v = new LinkedHashMap<>();
                    v.put("wareHouseId", finalWhId);
                    v.put("wareHouseName", finalWhName);
                    v.put("matNr", item.getMatnrCode());
                    v.put("matTx", item.getMaktx());
                    v.put("anfme", (item.getAnfme() != null ? item.getAnfme() : 0));
                    v.put("unit", item.getUnit());
                } else {
                    v.put("anfme", ((Number) v.get("anfme")).doubleValue() + (item.getAnfme() != null ? item.getAnfme() : 0));
                }
                return v;
            });
        }
        return R.ok().add(new ArrayList<>(sumMap.values()));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/ScheduleTriggerController.java
New file
@@ -0,0 +1,78 @@
package com.vincent.rsf.server.manager.controller;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.manager.schedules.AsnOrderLogSchedule;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
/**
 * 定时任务手动触发接口,仅允许本机请求(127.0.0.1 / ::1)。
 */
@Slf4j
@RestController
@RequestMapping("/schedule")
@Api(value = "定时任务触发", tags = "定时任务手动触发(仅本地)")
public class ScheduleTriggerController {
    private static final List<String> LOCALHOST_IPS = Arrays.asList("127.0.0.1", "0:0:0:0:0:0:0:1", "::1");
    private final AsnOrderLogSchedule asnOrderLogSchedule;
    public ScheduleTriggerController(AsnOrderLogSchedule asnOrderLogSchedule) {
        this.asnOrderLogSchedule = asnOrderLogSchedule;
    }
    private static boolean isLocalRequest(HttpServletRequest request) {
        String remote = request.getRemoteAddr();
        if (remote != null && LOCALHOST_IPS.contains(remote)) {
            return true;
        }
        String forwarded = request.getHeader("X-Forwarded-For");
        if (forwarded != null && !forwarded.isEmpty()) {
            remote = forwarded.split(",")[0].trim();
        }
        return remote != null && LOCALHOST_IPS.contains(remote);
    }
    @ApiOperation("手动执行入库转历史(InStockToLog),仅允许本地请求")
    @PostMapping("/trigger/inStockToLog")
    public ResponseEntity<R> triggerInStockToLog(HttpServletRequest request) {
        if (!isLocalRequest(request)) {
            log.warn("拒绝非本地请求触发 InStockToLog,remote={}", request.getRemoteAddr());
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(R.error("仅允许本地请求"));
        }
        try {
            asnOrderLogSchedule.InStockToLog();
            return ResponseEntity.ok(R.ok("执行完成"));
        } catch (Exception e) {
            log.error("InStockToLog 执行失败", e);
            return ResponseEntity.ok(R.error(e.getMessage()));
        }
    }
    @ApiOperation("手动执行物理删除上上个月之前已逻辑删除的原单及明细,仅允许本地请求")
    @PostMapping("/trigger/physicalDeleteLogicDeletedOrders")
    public ResponseEntity<R> triggerPhysicalDeleteLogicDeletedOrders(HttpServletRequest request) {
        if (!isLocalRequest(request)) {
            log.warn("拒绝非本地请求触发 physicalDeleteLogicDeletedOrders,remote={}", request.getRemoteAddr());
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(R.error("仅允许本地请求"));
        }
        try {
            asnOrderLogSchedule.physicalDeleteLogicDeletedOrders();
            return ResponseEntity.ok(R.ok("执行完成"));
        } catch (Exception e) {
            log.error("physicalDeleteLogicDeletedOrders 执行失败", e);
            return ResponseEntity.ok(R.error(e.getMessage()));
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/CheckOrderParams.java
@@ -18,7 +18,7 @@
    @ApiModelProperty(value= "秀点单ID")
    private Long orderId;
    @ApiModelProperty(value= "ASN单号")
    @ApiModelProperty(value= "WMS单号")
    private String orderCode;
    @ApiModelProperty(value= "物料标识")
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/AsnOrderItemLog.java
@@ -50,9 +50,9 @@
    private Long asnId;
    /**
     * ASN单号
     * WMS单号
     */
    @ApiModelProperty(value= "ASN单号")
    @ApiModelProperty(value= "WMS单号")
    private String asnCode;
    /**
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/CloudWmsNotifyLog.java
New file
@@ -0,0 +1,88 @@
package com.vincent.rsf.server.manager.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/** 云仓上报待办记录 */
@Data
@Accessors(chain = true)
@TableName("man_cloud_wms_notify_log")
@ApiModel(value = "CloudWmsNotifyLog", description = "云仓上报待办记录")
public class CloudWmsNotifyLog implements Serializable {
    private static final long serialVersionUID = 1L;
    public static final String REPORT_TYPE_IN_OUT_RESULT = "IN_OUT_RESULT";
    public static final String REPORT_TYPE_INVENTORY_ADJUST = "INVENTORY_ADJUST";
    /** 通知状态:待通知 */
    public static final int NOTIFY_STATUS_PENDING = 0;
    /** 通知状态:已成功 */
    public static final int NOTIFY_STATUS_SUCCESS = 1;
    /** 通知状态:失败(含超过重试次数) */
    public static final int NOTIFY_STATUS_FAIL = 2;
    @ApiModelProperty("主键")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("上报类型:IN_OUT_RESULT-入出库结果,INVENTORY_ADJUST-库存调整")
    private String reportType;
    @ApiModelProperty("请求体 JSON")
    private String requestBody;
    @ApiModelProperty("是否已通知到云仓:0 待通知 1 成功 2 失败")
    private Integer notifyStatus;
    @ApiModelProperty("已通知次数(重试累计)")
    private Integer retryCount;
    @ApiModelProperty("最大重试次数")
    private Integer maxRetryCount;
    @ApiModelProperty("重试间隔秒数")
    private Integer retryIntervalSeconds;
    @ApiModelProperty("最近一次请求体(重试时可能与原 requestBody 一致)")
    private String lastRequestBody;
    @ApiModelProperty("最近一次返回结果 JSON")
    private String lastResponseBody;
    @ApiModelProperty("最近一次请求时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date lastNotifyTime;
    @ApiModelProperty("业务关联(如 taskId、reviseLogId,便于排查)")
    private String bizRef;
    @ApiModelProperty("租户")
    private Integer tenantId;
    @ApiModelProperty("是否删除 0 否 1 是")
    @TableLogic
    private Integer deleted;
    @ApiModelProperty("创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    @ApiModelProperty("更新时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WkOrderItem.java
@@ -75,9 +75,9 @@
    private Double workQty;
    /**
     * ASN单号
     * WMS单号
     */
    @ApiModelProperty(value= "ASN单号")
    @ApiModelProperty(value= "WMS单号")
    private String orderCode;
    /**
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/excel/AsnOrderTemplate.java
@@ -28,8 +28,8 @@
    /**
     * 编号
     */
    @Excel(name = "*ASN单号")
    @ApiModelProperty(value = "*ASN单号")
    @Excel(name = "*WMS单号")
    @ApiModelProperty(value = "*WMS单号")
    @ExcelComment(value = "code", example = "ASN5945272236")
    private String code;
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/excel/TransferTemplate.java
@@ -8,8 +8,8 @@
    /**
     * 编号
     */
    @Excel(name = "*ASN单号")
    @ApiModelProperty(value = "*ASN单号")
    @Excel(name = "*WMS单号")
    @ApiModelProperty(value = "*WMS单号")
    @ExcelComment(value = "code", example = "ASN5945272236")
    private String code;
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/AsnOrderItemMapper.java
@@ -12,6 +12,7 @@
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Mapper
@@ -22,4 +23,7 @@
    WkOrderItem resultById(@Param(Constants.WRAPPER) LambdaQueryWrapper<WkOrderItem> buildWrapper);
    /** 按订单 id 物理删除已逻辑删除的明细 */
    int physicalDeleteByOrderIds(@Param("orderIds") List<Long> orderIds);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/AsnOrderMapper.java
@@ -11,6 +11,7 @@
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.Date;
import java.util.List;
@Mapper
@@ -20,4 +21,10 @@
    DashboardDto getDashbord(@Param("type") String type, @Param("taskType") String taskType);
    List<StockTransItemDto> getStockTrand(@Param(Constants.WRAPPER) LambdaQueryWrapper<StockStatistic> queryWrapper);
    /** 查询在指定时间之前被逻辑删除的订单 id(用于物理清理,补删历史) */
    List<Long> selectLogicDeletedOrderIdsBefore(@Param("before") Date before);
    /** 按 id 物理删除已逻辑删除的订单 */
    int physicalDeleteByIds(@Param("ids") List<Long> ids);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/CloudWmsNotifyLogMapper.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.server.manager.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface CloudWmsNotifyLogMapper extends BaseMapper<CloudWmsNotifyLog> {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/AsnOrderLogSchedule.java
@@ -10,6 +10,8 @@
import com.vincent.rsf.server.common.utils.FieldsUtils;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.*;
import com.vincent.rsf.server.manager.mapper.AsnOrderItemMapper;
import com.vincent.rsf.server.manager.mapper.AsnOrderMapper;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.service.impl.StockItemServiceImpl;
import com.vincent.rsf.server.manager.service.impl.StockServiceImpl;
@@ -23,6 +25,8 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
@@ -56,6 +60,10 @@
    @Autowired
    private ReportMsgService reportMsgService;
    @Autowired
    private AsnOrderMapper asnOrderMapper;
    @Autowired
    private AsnOrderItemMapper asnOrderItemMapper;
    /**
     * @param
@@ -64,7 +72,7 @@
     * @description 删除已完成订单加入Log表
     * @time 2025/3/19 19:09
     */
    @Scheduled(cron = "0 0 2 1 * ?")
    @Scheduled(cron = "0 0 5 * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void InStockToLog() {
        List<WkOrder> wkOrders = asnOrderService.list(new LambdaQueryWrapper<WkOrder>()
@@ -89,7 +97,7 @@
     * @description 出库单完成后,状态修改
     * @time 2025/6/16 08:35
     */
    @Scheduled(cron = "0/15 * * * * ?  ")
    @Scheduled(cron = "0/25 * * * * ?  ")
//    @Scheduled(cron = "0 0 2 1 * ?")
    @Transactional(rollbackFor = Exception.class)
    public void outStockComplete() {
@@ -144,8 +152,9 @@
            }
//            if (order.getType().equals(OrderType.ORDER_OUT.type) &&  order.getReportOnce() >= 4) {
                AsnOrderLog one = asnOrderLogService.getOne(new LambdaQueryWrapper<AsnOrderLog>().eq(AsnOrderLog::getCode, order.getCode()), false);
                AsnOrderLog orderLog;
                if (Objects.isNull(one)) {
                    AsnOrderLog orderLog = new AsnOrderLog();
                    orderLog = new AsnOrderLog();
                    if (type.equals(OrderType.ORDER_OUT.type)) {
                        order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val);
                        order.setQty(order.getWorkQty());
@@ -153,28 +162,41 @@
                    BeanUtils.copyProperties(order, orderLog);
                    orderLog.setId(null);
                    orderLog.setAsnId(order.getId());
                    if (!asnOrderLogService.save(orderLog)) {
                        throw new CoolException("主单历史档添加失败!!");
                    }
                    List<AsnOrderItemLog> logs = new ArrayList<>();
                    List<WkOrderItem> items = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>()
                            .eq(WkOrderItem::getOrderId, order.getId()));
                    items.forEach(item -> {
                        AsnOrderItemLog itemLog = new AsnOrderItemLog();
                        BeanUtils.copyProperties(item, itemLog);
                        itemLog.setAsnItemId(itemLog.getId())
                                .setId(null)
                                .setMatnrId(item.getMatnrId())
                                .setLogId(orderLog.getId())
                                .setAsnId(item.getOrderId());
                        logs.add(itemLog);
                    });
                    if (!asnOrderItemLogService.saveBatch(logs)) {
                        throw new CoolException("单据明细历史档保存失败!!");
                } else {
                    if (type.equals(OrderType.ORDER_OUT.type)) {
                        order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val);
                        order.setQty(order.getWorkQty());
                    }
                    long existingLogId = one.getId();
                    BeanUtils.copyProperties(order, one);
                    one.setId(existingLogId);
                    one.setAsnId(order.getId());
                    if (!asnOrderLogService.updateById(one)) {
                        throw new CoolException("主单历史档更新失败!!");
                    }
                    orderLog = one;
                    asnOrderItemLogService.remove(new LambdaQueryWrapper<AsnOrderItemLog>().eq(AsnOrderItemLog::getLogId, existingLogId));
                }
                List<AsnOrderItemLog> logs = new ArrayList<>();
                List<WkOrderItem> items = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>()
                        .eq(WkOrderItem::getOrderId, order.getId()));
                items.forEach(item -> {
                    AsnOrderItemLog itemLog = new AsnOrderItemLog();
                    BeanUtils.copyProperties(item, itemLog);
                    itemLog.setAsnItemId(item.getId())
                            .setId(null)
                            .setMatnrId(item.getMatnrId())
                            .setLogId(orderLog.getId())
                            .setAsnId(item.getOrderId());
                    logs.add(itemLog);
                });
                if (!asnOrderItemLogService.saveBatch(logs)) {
                    throw new CoolException("单据明细历史档保存失败!!");
                }
                    //更新PO/DO单执行状态
                    if (type.equals(OrderType.ORDER_IN.type)) {
@@ -200,7 +222,8 @@
                                    .set(Transfer::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val))) {
                                throw new CoolException("调拔单状态修改失败!!");
                            }
                            return;
                            removeOriginalOrderAndItems(order);
                            continue;
                        } else {
                            if (!Objects.isNull(order.getPoId())) {
                                purchaseService.update(new LambdaUpdateWrapper<Purchase>()
@@ -215,7 +238,8 @@
                                throw new CoolException("单据状态更新失败!!");
                            }
                            //如果为调拔单据保留
                            return;
                            removeOriginalOrderAndItems(order);
                            continue;
                        } else {
                            if (!Objects.isNull(order.getPoId())) {
                                deliveryService.update(new LambdaUpdateWrapper<Delivery>()
@@ -224,16 +248,38 @@
                            }
                        }
                    }
                }
//                if (!asnOrderItemService.remove(new LambdaQueryWrapper<WkOrderItem>()
//                        .eq(WkOrderItem::getOrderId, order.getId()))) {
//                    throw new CoolException("原单据明细删除失败!!");
//                }
//                if (!this.asnOrderService.removeById(order.getId())) {
//                    throw new CoolException("原单据删除失败!!");
//                }
//            }
                    // 转入历史后删除原单及明细
                    removeOriginalOrderAndItems(order);
        }
    }
    /** 删除原入库/出库通知单及明细(转入历史后调用) */
    private void removeOriginalOrderAndItems(WkOrder order) {
        if (!asnOrderItemService.remove(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()))) {
            throw new CoolException("原单据明细删除失败!!");
        }
        if (!asnOrderService.removeById(order.getId())) {
            throw new CoolException("原单据删除失败!!");
        }
    }
    /** 每月1号凌晨执行:物理删除上上个月之前已被逻辑删除的入库/出库通知单及明细 */
    @Scheduled(cron = "0 0 0 1 * ?")
    @Transactional(rollbackFor = Exception.class)
    public void physicalDeleteLogicDeletedOrders() {
        LocalDate startOfTwoMonthsAgo = LocalDate.now().minusMonths(2).withDayOfMonth(1);
        Date before = Date.from(startOfTwoMonthsAgo.atStartOfDay(ZoneId.systemDefault()).toInstant());
        List<Long> ids = asnOrderMapper.selectLogicDeletedOrderIdsBefore(before);
        if (ids == null || ids.isEmpty()) {
            return;
        }
        final int batchSize = 500;
        for (int i = 0; i < ids.size(); i += batchSize) {
            int to = Math.min(i + batchSize, ids.size());
            List<Long> batch = ids.subList(i, to);
            asnOrderItemMapper.physicalDeleteByOrderIds(batch);
            asnOrderMapper.physicalDeleteByIds(batch);
        }
        log.info("物理删除上上个月之前已逻辑删除的原单及明细,订单数:{}", ids.size());
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/CloudWmsNotifySchedule.java
New file
@@ -0,0 +1,123 @@
package com.vincent.rsf.server.manager.schedules;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import com.vincent.rsf.server.api.service.CloudWmsReportService;
import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog;
import com.vincent.rsf.server.manager.service.CloudWmsNotifyLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
/** 云仓上报定时任务 */
@Slf4j
@Component
public class CloudWmsNotifySchedule {
    private static final int BATCH_LIMIT = 50;
    @Autowired
    private CloudWmsNotifyLogService cloudWmsNotifyLogService;
    @Autowired
    private CloudWmsReportService cloudWmsReportService;
    @Autowired
    private ObjectMapper objectMapper;
    @Scheduled(cron = "0/30 * * * * ?")
    public void syncCloudWmsNotify() {
        List<CloudWmsNotifyLog> pending = cloudWmsNotifyLogService.listPending(BATCH_LIMIT, 999);
        if (pending.isEmpty()) {
            return;
        }
        long nowMs = System.currentTimeMillis();
        for (CloudWmsNotifyLog logRecord : pending) {
            try {
                Integer maxRetry = logRecord.getMaxRetryCount();
                Integer intervalSeconds = logRecord.getRetryIntervalSeconds();
                if (maxRetry == null || intervalSeconds == null || intervalSeconds <= 0) {
                    continue;
                }
                if (logRecord.getRetryCount() != null && logRecord.getRetryCount() >= maxRetry) {
                    continue;
                }
                if (logRecord.getLastNotifyTime() != null) {
                    long elapsed = (nowMs - logRecord.getLastNotifyTime().getTime()) / 1000;
                    if (elapsed < intervalSeconds) {
                        continue;
                    }
                }
                processOne(logRecord);
            } catch (Exception e) {
                log.warn("云仓上报定时任务处理单条异常,id={},bizRef={}:{}", logRecord.getId(), logRecord.getBizRef(), e.getMessage());
            }
        }
    }
    private void processOne(CloudWmsNotifyLog logRecord) {
        String reportType = logRecord.getReportType();
        String requestBody = logRecord.getRequestBody();
        Date now = new Date();
        int nextRetry = (logRecord.getRetryCount() == null ? 0 : logRecord.getRetryCount()) + 1;
        int effectiveMaxRetry = logRecord.getMaxRetryCount();
        try {
            if (cloudWmsNotifyLogService.getReportTypeInOutResult().equals(reportType)) {
                InOutResultReportParam param = objectMapper.readValue(requestBody, InOutResultReportParam.class);
                Map<String, Object> res = cloudWmsReportService.reportInOutResult(param);
                updateAfterNotify(logRecord, requestBody, res, nextRetry, now, effectiveMaxRetry);
            } else if (cloudWmsNotifyLogService.getReportTypeInventoryAdjust().equals(reportType)) {
                InventoryAdjustReportParam param = objectMapper.readValue(requestBody, InventoryAdjustReportParam.class);
                Map<String, Object> res = cloudWmsReportService.reportInventoryAdjust(param);
                updateAfterNotify(logRecord, requestBody, res, nextRetry, now, effectiveMaxRetry);
            } else {
                log.warn("未知上报类型,id={},reportType={}", logRecord.getId(), reportType);
                return;
            }
        } catch (JsonProcessingException e) {
            log.warn("云仓上报请求体反序列化失败,id={}:{}", logRecord.getId(), e.getMessage());
            setFailResult(logRecord, requestBody, "反序列化失败: " + e.getMessage(), nextRetry, now, effectiveMaxRetry);
        } catch (Exception e) {
            log.warn("云仓上报请求失败,id={},bizRef={}:{}", logRecord.getId(), logRecord.getBizRef(), e.getMessage());
            setFailResult(logRecord, requestBody, "请求异常: " + e.getMessage(), nextRetry, now, effectiveMaxRetry);
        }
    }
    private void updateAfterNotify(CloudWmsNotifyLog logRecord, String requestBody, Map<String, Object> res, int nextRetry, Date now, int effectiveMaxRetry) {
        String responseJson;
        try {
            responseJson = res != null ? objectMapper.writeValueAsString(res) : "null";
        } catch (JsonProcessingException e) {
            responseJson = String.valueOf(res);
        }
        Object codeObj = res != null ? res.get("code") : null;
        boolean success = Integer.valueOf(200).equals(codeObj);
        int status = success ? cloudWmsNotifyLogService.getNotifyStatusSuccess() : cloudWmsNotifyLogService.getNotifyStatusPending();
        if (!success && nextRetry >= effectiveMaxRetry) {
            status = cloudWmsNotifyLogService.getNotifyStatusFail();
        }
        logRecord.setLastRequestBody(requestBody);
        logRecord.setLastResponseBody(responseJson);
        logRecord.setLastNotifyTime(now);
        logRecord.setRetryCount(nextRetry);
        logRecord.setNotifyStatus(status);
        logRecord.setUpdateTime(now);
        cloudWmsNotifyLogService.updateById(logRecord);
    }
    private void setFailResult(CloudWmsNotifyLog logRecord, String requestBody, String errorMsg, int nextRetry, Date now, int effectiveMaxRetry) {
        logRecord.setLastRequestBody(requestBody);
        logRecord.setLastResponseBody(errorMsg);
        logRecord.setLastNotifyTime(now);
        logRecord.setRetryCount(nextRetry);
        logRecord.setNotifyStatus(nextRetry >= effectiveMaxRetry ? cloudWmsNotifyLogService.getNotifyStatusFail() : cloudWmsNotifyLogService.getNotifyStatusPending());
        logRecord.setUpdateTime(now);
        cloudWmsNotifyLogService.updateById(logRecord);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java
@@ -648,6 +648,9 @@
                                        if (!Boolean.parseBoolean(allowChang.getVal())) {
                                            if (order.getAnfme().compareTo(order.getQty()) == 0) {
                                                order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val);
                                                if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) {
                                                    order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0);
                                                }
                                                if (!asnOrderService.updateById(order)) {
                                                    logger.error("出库单更新状态失败。订单ID:{},订单编码:{}", order.getId(), order.getCode());
                                                }
@@ -655,6 +658,9 @@
                                        } else {
                                            if (order.getAnfme().compareTo(order.getQty()) <= 0) {
                                                order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val);
                                                if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) {
                                                    order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0);
                                                }
                                                if (!asnOrderService.updateById(order)) {
                                                    logger.error("出库单更新状态失败。订单ID:{},订单编码:{}", order.getId(), order.getCode());
                                                }
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/CloudWmsNotifyLogService.java
New file
@@ -0,0 +1,29 @@
package com.vincent.rsf.server.manager.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog;
import java.util.List;
/** 云仓上报待办 */
public interface CloudWmsNotifyLogService extends IService<CloudWmsNotifyLog> {
    List<CloudWmsNotifyLog> listPending(int limit, int maxRetry);
    void fillFromConfig(CloudWmsNotifyLog log);
    /** 上报类型:入出库结果(系统配置优先,缺省 IN_OUT_RESULT) */
    String getReportTypeInOutResult();
    /** 上报类型:库存调整(系统配置优先,缺省 INVENTORY_ADJUST) */
    String getReportTypeInventoryAdjust();
    /** 通知状态:待通知(系统配置优先,缺省 0) */
    int getNotifyStatusPending();
    /** 通知状态:已成功(系统配置优先,缺省 1) */
    int getNotifyStatusSuccess();
    /** 通知状态:失败(系统配置优先,缺省 2) */
    int getNotifyStatusFail();
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderServiceImpl.java
@@ -510,30 +510,38 @@
//            throw new CoolException("收货数量不能为零!!");
//        }
        WkOrder order = this.getById(asrder.getId());
        AsnOrderLog orderLog = new AsnOrderLog();
//        order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val);
        BeanUtils.copyProperties(order, orderLog);
        orderLog.setId(null);
        orderLog.setAsnId(order.getId());
//        if (!this.saveOrUpdate(order)) {
//            throw new CoolException("状态修改失败!!");
//        }
//        orderLog.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_CLOSE.val);
        if (!asnOrderLogService.save(orderLog)) {
            throw new CoolException("主单历史档添加失败!!");
        AsnOrderLog one = asnOrderLogService.getOne(new LambdaQueryWrapper<AsnOrderLog>().eq(AsnOrderLog::getCode, order.getCode()), false);
        AsnOrderLog orderLog;
        if (Objects.isNull(one)) {
            orderLog = new AsnOrderLog();
            BeanUtils.copyProperties(order, orderLog);
            orderLog.setId(null);
            orderLog.setAsnId(order.getId());
            if (!asnOrderLogService.save(orderLog)) {
                throw new CoolException("主单历史档添加失败!!");
            }
        } else {
            long existingLogId = one.getId();
            BeanUtils.copyProperties(order, one);
            one.setId(existingLogId);
            one.setAsnId(order.getId());
            if (!asnOrderLogService.updateById(one)) {
                throw new CoolException("主单历史档更新失败!!");
            }
            orderLog = one;
            asnOrderItemLogService.remove(new LambdaQueryWrapper<AsnOrderItemLog>().eq(AsnOrderItemLog::getLogId, existingLogId));
        }
        List<AsnOrderItemLog> logs = new ArrayList<>();
        List<WkOrderItem> items = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()));
        items.forEach(item -> {
            AsnOrderItemLog itemLog = new AsnOrderItemLog();
            BeanUtils.copyProperties(item, itemLog);
            itemLog.setAsnItemId(itemLog.getId())
            itemLog.setAsnItemId(item.getId())
                    .setId(null)
                    .setLogId(orderLog.getId())
                    .setAsnId(item.getOrderId());
            logs.add(itemLog);
        });
        if (!asnOrderItemLogService.saveBatch(logs)) {
            throw new CoolException("通知单明细历史档保存失败!!");
        }
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/CloudWmsNotifyLogServiceImpl.java
New file
@@ -0,0 +1,90 @@
package com.vincent.rsf.server.manager.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog;
import com.vincent.rsf.server.manager.mapper.CloudWmsNotifyLogMapper;
import com.vincent.rsf.server.manager.service.CloudWmsNotifyLogService;
import com.vincent.rsf.server.system.constant.GlobalConfigCode;
import com.vincent.rsf.server.system.entity.Config;
import com.vincent.rsf.server.system.service.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CloudWmsNotifyLogServiceImpl extends ServiceImpl<CloudWmsNotifyLogMapper, CloudWmsNotifyLog> implements CloudWmsNotifyLogService {
    @Autowired
    private ConfigService configService;
    @Override
    public List<CloudWmsNotifyLog> listPending(int limit, int maxRetry) {
        Page<CloudWmsNotifyLog> page = new Page<>(1, Math.max(1, limit));
        LambdaQueryWrapper<CloudWmsNotifyLog> wrapper = new LambdaQueryWrapper<CloudWmsNotifyLog>()
                .eq(CloudWmsNotifyLog::getNotifyStatus, getNotifyStatusPending())
                .lt(CloudWmsNotifyLog::getRetryCount, maxRetry)
                .orderByAsc(CloudWmsNotifyLog::getId);
        return page(page, wrapper).getRecords();
    }
    @Override
    public String getReportTypeInOutResult() {
        return getConfigString(GlobalConfigCode.CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT, CloudWmsNotifyLog.REPORT_TYPE_IN_OUT_RESULT);
    }
    @Override
    public String getReportTypeInventoryAdjust() {
        return getConfigString(GlobalConfigCode.CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST, CloudWmsNotifyLog.REPORT_TYPE_INVENTORY_ADJUST);
    }
    @Override
    public int getNotifyStatusPending() {
        return getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_STATUS_PENDING, CloudWmsNotifyLog.NOTIFY_STATUS_PENDING);
    }
    @Override
    public int getNotifyStatusSuccess() {
        return getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_STATUS_SUCCESS, CloudWmsNotifyLog.NOTIFY_STATUS_SUCCESS);
    }
    @Override
    public int getNotifyStatusFail() {
        return getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_STATUS_FAIL, CloudWmsNotifyLog.NOTIFY_STATUS_FAIL);
    }
    private String getConfigString(String flag, String defaultVal) {
        Config c = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, flag).last("LIMIT 1"));
        if (c != null && c.getVal() != null && !c.getVal().isEmpty()) {
            return c.getVal().trim();
        }
        return defaultVal;
    }
    private int getConfigInt(String flag, int defaultVal) {
        Integer v = getConfigInt(flag);
        return v != null ? v : defaultVal;
    }
    @Override
    public void fillFromConfig(CloudWmsNotifyLog log) {
        Integer maxRetry = getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_MAX_RETRY);
        Integer interval = getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_RETRY_INTERVAL_SECONDS);
        log.setMaxRetryCount(maxRetry);
        log.setRetryIntervalSeconds(interval);
    }
    /** 返回 null 表示未配置或解析失败 */
    private Integer getConfigInt(String flag) {
        try {
            Config c = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, flag).last("LIMIT 1"));
            if (c != null && c.getVal() != null && !c.getVal().isEmpty()) {
                return Integer.parseInt(c.getVal().trim());
            }
        } catch (Exception ignored) {
        }
        return null;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java
@@ -1066,6 +1066,9 @@
            return R.error("出库单不存在!!");
        }
        order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val);
        if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) {
            order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0);
        }
        if (!this.updateById(order)) {
            throw new CoolException("完成出库单失败!!");
        }
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/ReviseLogServiceImpl.java
@@ -4,6 +4,8 @@
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam;
import com.vincent.rsf.server.manager.controller.params.ReviseLogParams;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.AsnExceStatus;
@@ -13,6 +15,7 @@
import com.vincent.rsf.server.manager.mapper.ReviseLogMapper;
import com.vincent.rsf.server.manager.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -27,6 +30,7 @@
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service("reviseLogService")
public class ReviseLogServiceImpl extends ServiceImpl<ReviseLogMapper, ReviseLog> implements ReviseLogService {
@@ -50,6 +54,15 @@
    @Autowired
    private OutStockItemService outStockItemService;
    @Autowired
    private WarehouseService warehouseService;
    @Autowired
    private CloudWmsNotifyLogService cloudWmsNotifyLogService;
    @Autowired
    private ObjectMapper objectMapper;
    /**
     * 库存调整单明细添加
@@ -203,6 +216,7 @@
            // 删除原库位的库存明细(如果存在)
            locItemService.remove(new LambdaQueryWrapper<LocItem>().eq(LocItem::getLocId, loc.getId()));
            final Loc sourceLoc = loc;
            Loc finalLoc = loc;
            reviseItems.forEach(logItem -> {
                LocItem locDetl = new LocItem();
@@ -220,6 +234,39 @@
                if (!locItemService.save(locDetl)) {
                    throw new CoolException("库存明细保存失败!!");
                }
                // 9.2 库存调整主动上报待办
                try {
                    String wareHouseId = null;
                    if (finalLoc.getWarehouseId() != null) {
                        Warehouse wh = warehouseService.getById(finalLoc.getWarehouseId());
                        if (wh != null) {
                            wareHouseId = wh.getCode();
                        }
                    }
                    if (wareHouseId != null && logItem.getMatnrCode() != null && sourceLoc != null) {
                        InventoryAdjustReportParam param = new InventoryAdjustReportParam()
                                .setChangeType(3) // 3 移库
                                .setWareHouseId(wareHouseId)
                                .setSourceLocId(sourceLoc.getCode())
                                .setTargetLocId(finalLoc.getCode())
                                .setMatNr(logItem.getMatnrCode())
                                .setQty(logItem.getReviseQty() != null ? String.valueOf(logItem.getReviseQty()) : "0");
                        String requestBody = objectMapper.writeValueAsString(param);
                        Date now = new Date();
                        CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog()
                                .setReportType(cloudWmsNotifyLogService.getReportTypeInventoryAdjust())
                                .setRequestBody(requestBody)
                                .setNotifyStatus(cloudWmsNotifyLogService.getNotifyStatusPending())
                                .setRetryCount(0)
                                .setBizRef("reviseId=" + revise.getId() + ",reviseLogItemId=" + logItem.getId())
                                .setCreateTime(now)
                                .setUpdateTime(now);
                        cloudWmsNotifyLogService.fillFromConfig(notifyLog);
                        cloudWmsNotifyLogService.save(notifyLog);
                    }
                } catch (Exception e) {
                    log.warn("库存调整上报待办落库失败(不影响库存保存),matNr={}:{}", logItem.getMatnrCode(), e.getMessage());
                }
                // 为库存调整产生的库存创建对应的WkOrderItem
                // 遍历所有未完成的出库单,检查是否需要这些物料
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -4,12 +4,14 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.DateUtils;
import com.vincent.rsf.server.api.config.RemotesInfoProperties;
import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam;
import com.vincent.rsf.server.api.controller.erp.params.TaskInParam;
import com.vincent.rsf.server.api.entity.CommonResponse;
import com.vincent.rsf.server.api.entity.constant.RcsConstant;
@@ -26,6 +28,7 @@
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.utils.LocUtils;
import com.vincent.rsf.server.manager.controller.params.GenerateTaskParams;
import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.mapper.TaskMapper;
import com.vincent.rsf.server.manager.service.*;
@@ -124,6 +127,10 @@
    private RestTemplate restTemplate;
    @Autowired
    private RemotesInfoProperties.RcsApi rcsApi;
    @Autowired
    private CloudWmsNotifyLogService cloudWmsNotifyLogService;
    @Autowired
    private WarehouseService warehouseService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -1627,6 +1634,8 @@
                .set(Task::getTaskStatus, TaskStsType.WAVE_SEED.id))) {
            throw new CoolException("库存状态更新失败!!");
        }
        // 9.1 入/出库结果上报:出库完成后通知云仓
        reportInOutResultToCloud(task, loc, taskItems, null, false);
//        if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type) || task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
//            if (!this.update(new LambdaUpdateWrapper<Task>()
@@ -2240,6 +2249,8 @@
        if (!this.update(new LambdaUpdateWrapper<Task>().eq(Task::getId, task.getId()).set(Task::getTaskStatus, TaskStsType.UPDATED_IN.id))) {
            throw new CoolException("任务状态修改失败!!");
        }
        // 9.1 入/出库结果上报:入库完成后通知云仓
        reportInOutResultToCloud(task, loc, taskItems, pkinItemIds, true);
    }
    /**
@@ -2408,4 +2419,69 @@
            }
        }
    }
    /**
     * 9.1 入/出库结果上报待办
     * @param isInbound true 入库完成,false 出库完成
     * @param pkinItemIds 入库时组托明细 ID 集合,用于查 asnCode 作为 orderNo;出库时传 null,用 taskItem.platOrderCode
     */
    private void reportInOutResultToCloud(Task task, Loc loc, List<TaskItem> taskItems, Set<Long> pkinItemIds, boolean isInbound) {
        try {
            String locId = isInbound ? task.getTargLoc() : task.getOrgLoc();
            String wareHouseId = null;
            if (loc.getWarehouseId() != null) {
                Warehouse wh = warehouseService.getById(loc.getWarehouseId());
                if (wh != null) {
                    wareHouseId = wh.getCode();
                }
            }
            if (wareHouseId == null) {
                log.warn("入/出库结果上报待办跳过:仓库编码为空,taskId={}", task.getId());
                return;
            }
            Map<Long, String> sourceToOrderNo = new HashMap<>();
            if (isInbound && pkinItemIds != null && !pkinItemIds.isEmpty()) {
                List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().in(WaitPakinItem::getId, pkinItemIds));
                for (WaitPakinItem p : pakinItems) {
                    if (p.getAsnCode() != null) {
                        sourceToOrderNo.put(p.getId(), p.getAsnCode());
                    }
                }
            }
            ObjectMapper om = new ObjectMapper();
            Date now = new Date();
            for (TaskItem item : taskItems) {
                String orderNo = isInbound ? sourceToOrderNo.get(item.getSource()) : (item.getPlatOrderCode() != null ? item.getPlatOrderCode() : item.getPlatWorkCode());
                if (orderNo == null || item.getMatnrCode() == null) {
                    continue;
                }
                InOutResultReportParam param = new InOutResultReportParam()
                        .setOrderNo(orderNo)
                        .setPlanNo(item.getPlatWorkCode())
                        .setLineId(item.getPlatItemId())
                        .setWareHouseId(wareHouseId)
                        .setLocId(locId)
                        .setMatNr(item.getMatnrCode())
                        .setQty(item.getAnfme() != null ? String.valueOf(item.getAnfme()) : "0")
                        .setBatch(item.getBatch());
                try {
                    String requestBody = om.writeValueAsString(param);
                    CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog()
                            .setReportType(cloudWmsNotifyLogService.getReportTypeInOutResult())
                            .setRequestBody(requestBody)
                            .setNotifyStatus(cloudWmsNotifyLogService.getNotifyStatusPending())
                            .setRetryCount(0)
                            .setBizRef("taskId=" + task.getId() + ",orderNo=" + orderNo)
                            .setCreateTime(now)
                            .setUpdateTime(now);
                    cloudWmsNotifyLogService.fillFromConfig(notifyLog);
                    cloudWmsNotifyLogService.save(notifyLog);
                } catch (JsonProcessingException e) {
                    log.warn("入/出库结果上报待办落库失败(不影响主流程),taskId={},orderNo={}:{}", task.getId(), orderNo, e.getMessage());
                }
            }
        } catch (Exception e) {
            log.warn("入/出库结果上报待办失败,taskId={},isInbound={}:{}", task.getId(), isInbound, e.getMessage());
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/GlobalConfigCode.java
@@ -27,4 +27,19 @@
    public final static String ALLOW_PUB_TASK = "AllowPubTask";
    /** 云仓上报最大重试次数 */
    public final static String CLOUD_WMS_NOTIFY_MAX_RETRY = "CLOUD_WMS_NOTIFY_MAX_RETRY";
    /** 云仓上报重试间隔秒数 */
    public final static String CLOUD_WMS_NOTIFY_RETRY_INTERVAL_SECONDS = "CLOUD_WMS_NOTIFY_RETRY_INTERVAL_SECONDS";
    /** 云仓上报类型:入出库结果 */
    public final static String CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT = "CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT";
    /** 云仓上报类型:库存调整 */
    public final static String CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST = "CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST";
    /** 云仓通知状态:待通知 */
    public final static String CLOUD_WMS_NOTIFY_STATUS_PENDING = "CLOUD_WMS_NOTIFY_STATUS_PENDING";
    /** 云仓通知状态:已成功 */
    public final static String CLOUD_WMS_NOTIFY_STATUS_SUCCESS = "CLOUD_WMS_NOTIFY_STATUS_SUCCESS";
    /** 云仓通知状态:失败 */
    public final static String CLOUD_WMS_NOTIFY_STATUS_FAIL = "CLOUD_WMS_NOTIFY_STATUS_FAIL";
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/OpenApiAppController.java
New file
@@ -0,0 +1,78 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.system.entity.OpenApiApp;
import com.vincent.rsf.server.system.service.OpenApiAppService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Map;
@RestController
public class OpenApiAppController extends BaseController {
    @Autowired
    private OpenApiAppService openApiAppService;
    @PreAuthorize("hasAuthority('system:openApiApp:list')")
    @PostMapping("/openApiApp/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<OpenApiApp, BaseParam> pageParam = new PageParam<>(baseParam, OpenApiApp.class);
        LambdaQueryWrapper<OpenApiApp> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(OpenApiApp::getId);
        Page<OpenApiApp> page = openApiAppService.page(pageParam, wrapper);
        return R.ok().add(page);
    }
    @PreAuthorize("hasAuthority('system:openApiApp:list')")
    @PostMapping("/openApiApp/list")
    public R list(@RequestBody Map<String, Object> map) {
        return R.ok().add(openApiAppService.list(new LambdaQueryWrapper<OpenApiApp>().orderByDesc(OpenApiApp::getId)));
    }
    @PreAuthorize("hasAuthority('system:openApiApp:list')")
    @PostMapping("/openApiApp/many/{ids}")
    public R many(@PathVariable String[] ids) {
        return R.ok().add(openApiAppService.listByIds(Arrays.asList(ids)));
    }
    @PreAuthorize("hasAuthority('system:openApiApp:list')")
    @GetMapping("/openApiApp/{id}")
    public R get(@PathVariable String id) {
        return R.ok().add(openApiAppService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:openApiApp:save')")
    @PostMapping("/openApiApp/save")
    public R save(@RequestBody OpenApiApp app) {
        if (openApiAppService.save(app)) {
            return R.ok("Save Success").add(app);
        }
        return R.error("Save Fail");
    }
    @PreAuthorize("hasAuthority('system:openApiApp:update')")
    @PostMapping("/openApiApp/update")
    public R update(@RequestBody OpenApiApp app) {
        if (openApiAppService.updateById(app)) {
            return R.ok("Update Success").add(app);
        }
        return R.error("Update Fail");
    }
    @PreAuthorize("hasAuthority('system:openApiApp:remove')")
    @PostMapping("/openApiApp/remove/{ids}")
    public R remove(@PathVariable String[] ids) {
        if (openApiAppService.removeByIds(Arrays.asList(ids))) {
            return R.ok("Remove Success");
        }
        return R.error("Remove Fail");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/OpenApiApp.java
New file
@@ -0,0 +1,34 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("open_api_app")
public class OpenApiApp implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("应用ID")
    @TableId(value = "id")
    private String id;
    @ApiModelProperty("应用密钥")
    private String screct;
    @ApiModelProperty("应用名称")
    private String name;
    @ApiModelProperty("应用URL")
    private String url;
    @ApiModelProperty("是否启用 0未启用 1启用")
    private Integer enable;
    @ApiModelProperty("租户id")
    private Long tenantId;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/OpenApiAppMapper.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.OpenApiApp;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface OpenApiAppMapper extends BaseMapper<OpenApiApp> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/OpenApiAppService.java
New file
@@ -0,0 +1,7 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.OpenApiApp;
public interface OpenApiAppService extends IService<OpenApiApp> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/OpenApiAppServiceImpl.java
New file
@@ -0,0 +1,41 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.OpenApiApp;
import com.vincent.rsf.server.system.mapper.OpenApiAppMapper;
import com.vincent.rsf.server.system.service.OpenApiAppService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OpenApiAppServiceImpl extends ServiceImpl<OpenApiAppMapper, OpenApiApp> implements OpenApiAppService {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Override
    public boolean save(OpenApiApp entity) {
        encodeScrectIfPlain(entity);
        return super.save(entity);
    }
    @Override
    public boolean updateById(OpenApiApp entity) {
        encodeScrectIfPlain(entity);
        return super.updateById(entity);
    }
    /** 若为明文则改为 BCrypt 再存库,便于 getToken 时用 BCrypt 校验 */
    private void encodeScrectIfPlain(OpenApiApp entity) {
        if (entity == null) {
            return;
        }
        String s = entity.getScrect();
        if (s == null || s.isEmpty() || s.startsWith("$2")) {
            return;
        }
        entity.setScrect(passwordEncoder.encode(s));
    }
}
rsf-server/src/main/resources/application-dev.yml
@@ -5,6 +5,10 @@
spring:
  application:
    name: @pom.artifactId@
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true   # Feign 调用失败时走 Fallback,在 Feign 内统一返回错误响应
  mvc:
    static-path-pattern: /**
    path match:
@@ -13,6 +17,7 @@
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#    url: jdbc:mysql://127.0.0.1:3306/jdxajwms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    password: 12345
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
@@ -75,17 +80,20 @@
    #端口号
    port: 8080
    #接品链接前缀
    pre-path: rsf-server
    #接口明细
    pre-path: ""
    # Feign 调用云仓时的根地址。云仓未提供 URL 时可用本机模拟:http://127.0.0.1:8086/rsf-server(本服务提供的 CloudWmsMockController)
    base-url: http://127.0.0.1:8086/rsf-server
    #接口明细(立库侧请求云仓时使用的路径)
    api:
      #质检上报接口
      notify-inspect: /report/inspect
      in-out-result-path: /api/report/inOutResult
      inventory-adjust-path: /api/report/inventoryAdjust
      mat-sync-path: /api/mat/sync
  rcs:
    #链接
    host: http://10.10.10.200
    #端口
    port: 8088
#仓库功能参数配置
stock:
  #是否允许打印货物标签, 默认允许打印,也可由供应商提供标签
rsf-server/src/main/resources/mapper/manager/AsnOrderItemMapper.xml
@@ -134,4 +134,11 @@
            ) t
            ${ew.customSqlSegment}
    </select>
    <delete id="physicalDeleteByOrderIds">
        DELETE FROM man_asn_order_item
        WHERE deleted = 1
        AND order_id IN
        <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach>
    </delete>
</mapper>
rsf-server/src/main/resources/mapper/manager/AsnOrderMapper.xml
@@ -30,6 +30,19 @@
                GROUP BY
                    `day_time`, task_type
            ) t
        ${ew.customSqlSegment}
            ${ew.customSqlSegment}
    </select>
    <select id="selectLogicDeletedOrderIdsBefore" resultType="long">
        SELECT id FROM man_asn_order
        WHERE deleted = 1
          AND update_time &lt; #{before}
    </select>
    <delete id="physicalDeleteByIds">
        DELETE FROM man_asn_order
        WHERE deleted = 1
        AND id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
    </delete>
</mapper>
rsf-server/src/test/java/com/vincent/rsf/server/common/security/SecurityDemoControllerTest.java
New file
@@ -0,0 +1,29 @@
package com.vincent.rsf.server.common.security;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
 * 字符串加密与验证。
 *
 */
class SecurityDemoControllerTest {
    @Test
    void encryptAndVerify() {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String raw = "wms001";
        // 加密
        String encoded = encoder.encode(raw);
        System.out.println("密文: " + encoded);
        // 验证:原文与加密结果匹配
        boolean ok = encoder.matches(raw, encoded);
        System.out.println("加密后验证: " + ok);
        // 验证:错误原文不匹配
        boolean bad = encoder.matches("wrong", encoded);
        System.out.println("错误原文验证: " + bad);
    }
}
version/db/cloud_wms_notify_config.sql
New file
@@ -0,0 +1,7 @@
INSERT INTO `sys_config` (`uuid`, `name`, `flag`, `type`, `val`, `content`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
VALUES
(UPPER(UUID()), '云仓上报类型-入出库结果', 'CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT', 3, 'IN_OUT_RESULT', '入出库结果上报类型标识', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.report_type 对应'),
(UPPER(UUID()), '云仓上报类型-库存调整', 'CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST', 3, 'INVENTORY_ADJUST', '库存调整上报类型标识', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.report_type 对应'),
(UPPER(UUID()), '云仓通知状态-待通知', 'CLOUD_WMS_NOTIFY_STATUS_PENDING', 2, '0', '待通知', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.notify_status 对应'),
(UPPER(UUID()), '云仓通知状态-已成功', 'CLOUD_WMS_NOTIFY_STATUS_SUCCESS', 2, '1', '已成功', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.notify_status 对应'),
(UPPER(UUID()), '云仓通知状态-失败', 'CLOUD_WMS_NOTIFY_STATUS_FAIL', 2, '2', '失败(含超过重试次数)', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.notify_status 对应');
version/db/man_cloud_wms_notify_log.sql
New file
@@ -0,0 +1,25 @@
-- 云仓上报待办表:业务事务内只落库,由定时任务异步请求云仓并更新通知结果
-- 9.1 入出库结果上报、9.2 库存调整主动上报
SET NAMES utf8mb4;
DROP TABLE IF EXISTS `man_cloud_wms_notify_log`;
CREATE TABLE `man_cloud_wms_notify_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `report_type` varchar(32) NOT NULL COMMENT '上报类型:IN_OUT_RESULT-入出库结果,INVENTORY_ADJUST-库存调整',
  `request_body` text COMMENT '请求体JSON(与协议一致,供定时任务重放)',
  `notify_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否已通知到云仓:0待通知 1成功 2失败',
  `retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '已通知次数(重试累计)',
  `max_retry_count` int(11) DEFAULT NULL COMMENT '最大重试次数(为空则用系统配置)',
  `retry_interval_seconds` int(11) DEFAULT NULL COMMENT '重试频率/间隔秒数(为空则用系统配置)',
  `last_request_body` text COMMENT '最近一次请求体',
  `last_response_body` text COMMENT '最近一次返回结果JSON',
  `last_notify_time` datetime DEFAULT NULL COMMENT '最近一次请求时间',
  `biz_ref` varchar(255) DEFAULT NULL COMMENT '业务关联(如taskId、reviseLogId)',
  `tenant_id` int(11) DEFAULT NULL COMMENT '租户',
  `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 0否 1是',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_notify_status_retry` (`notify_status`, `retry_count`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='云仓上报待办记录';
version/db/open_api_app.sql
New file
@@ -0,0 +1,16 @@
-- 8.1 Token 鉴权:应用表,getToken 时用 appId+appSecret 在此表校验
SET NAMES utf8mb4;
DROP TABLE IF EXISTS `open_api_app`;
CREATE TABLE `open_api_app` (
  `id` varchar(64) NOT NULL COMMENT 'appId',
  `screct` varchar(255) NOT NULL COMMENT 'appSecret',
  `name` varchar(128) DEFAULT NULL COMMENT '应用名称',
  `url` varchar(512) DEFAULT NULL COMMENT '应用URL',
  `enable` tinyint(4) NOT NULL DEFAULT '1' COMMENT '是否启用 0未启用 1启用',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开放接口应用';
-- 示例数据(可选,用于云仓对接)
-- INSERT INTO `open_api_app` (`id`, `screct`, `name`, `url`, `enable`, `tenant_id`) VALUES ('cloud_wms', 'your_secret', '云仓WMS', NULL, 1, NULL);
version/db/open_api_app_menu.sql
New file
@@ -0,0 +1,7 @@
-- 应用管理菜单(开放接口 Token 鉴权应用,需在 sys_menu 已有数据之后执行)
-- 父菜单 id=47,子菜单 48-51;parent_id=1 表示挂在「系统」下
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (47, '应用管理', 1, 'menu.system', '1', 'menu.system', '/system/openApiApp', 'openApiApp', NULL, NULL, 0, NULL, 'Apps', 11, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (48, 'Query App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (49, 'Create App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:save', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (50, 'Update App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:update', NULL, 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (51, 'Delete App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:remove', NULL, 3, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL);
version/db/out_stock_order_log_menu.sql
New file
@@ -0,0 +1,10 @@
-- 出库历史单菜单:与「入库历史单」同级,共用 asnOrderLog 接口,仅前端 resource=outStockOrderLog、固定 type=out
-- 执行前需已存在 component='asnOrderLog' 的菜单,否则本 INSERT 不会插入任何行
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 52, 'menu.outStockOrderLog', m.parent_id, m.parent_name, CONCAT(IFNULL(m.path,''), ',52'), 'menu.outStockOrderLog', '/histories/outStockOrderLog', 'outStockOrderLog', NULL, NULL, 0, NULL, 'Outbox', 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM `sys_menu` m WHERE m.component = 'asnOrderLog' LIMIT 1;
-- 出库历史单列表权限(与入库历史单共用 manager:asnOrderLog:list,有该权限即可访问两个菜单)
INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 53, 'Query OutStockOrderLog', 52, NULL, CONCAT(IFNULL(m.path,''), ',53'), NULL, NULL, NULL, NULL, NULL, 1, 'manager:asnOrderLog:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL
FROM `sys_menu` m WHERE m.id = 52 LIMIT 1;