1
3 天以前 70ecc717c746eef0c4503a19031ada582f5e94d1
Merge remote-tracking branch 'origin/devlop-phyz' into devlop-phyz
70个文件已添加
20个文件已修改
6085 ■■■■■ 已修改文件
pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/config/setting.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/MyMenu.jsx 136 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/TabsBar.jsx 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/index.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/basicInfo/loc/InitModal.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/PageDrawer.jsx 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuCreate.jsx 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuEdit.jsx 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuList.jsx 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuPanel.jsx 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/matnrRoleMenu/index.jsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderModal.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderPanel.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/role/AssignPermissions_matnr.jsx 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/role/RoleList.jsx 167 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/work/stockTransfer/stockTransferList.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/pom.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/annotation/OperationLog.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/aspect/LogAspect.java 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/ApiSecurityConfig.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/WebMvcConfig.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/AuthController.java 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/TaskController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsErpController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsRcsController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/ApiAuthExampleController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/AppAuthExampleController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/FieldMappingExampleController.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/JsonReplaceExampleController.java 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/TokenAuthExampleController.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/phyz/ERPController.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/phyz/MESController.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/ApiFunctionController.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/ApiMapController.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/AppController.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/AppAuthParam.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/ApiForeignLog.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/ApiFunction.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/ApiMap.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/App.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/constant/Constants.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/dto/CommonResponse.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/CheckOrder.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/CheckResult.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Customer.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/InventoryDetails.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/InventoryQueryCondition.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/InventorySummary.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/MatPreparationOrder.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/MatPreparationOrderItem.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Material.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Order.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/OrderItem.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Pallet.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/SimpleProductionTask.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Station.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Supplier.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Task.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/TaskResult.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Warehouse.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/ApiForeignLogMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/ApiFunctionMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/ApiMapMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/AppMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/filter/AppIdAuthenticationFilter.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/service/AppAuthService.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/utils/AuthUtils.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/utils/TokenUtils.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/ApiForeignLogService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/ApiFunctionService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/ApiMapService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/AppService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/ApiForeignLogServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/ApiFunctionServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/ApiMapServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/AppServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/utils/JsonReplaceTest.java 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/utils/ParamsMapUtils.java 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/logback-spring.xml 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MatnrGroup.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/MatnrRoleMenuController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/MatnrRoleMenu.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/MatnrRoleMenuMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/MatnrRoleMenuService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/MatnrRoleMenuServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/matnrRoleMenu.sql 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/system/MatnrRoleMenuMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -38,6 +38,11 @@
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.21</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
rsf-admin/src/config/setting.js
@@ -25,7 +25,7 @@
export const ABORT_SIGNAL = false;
export const DEFAULT_PAGE_SIZE = 25;
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_START_PAGE = 1;
rsf-admin/src/layout/MyMenu.jsx
@@ -84,75 +84,75 @@
  // 在 MyMenu 组件的 generateMenu 函数中,确保 MenuItemLink 也左对齐
  const generateMenu = (permissions) => {
    return permissions.map((node) => {
        if (node.children) {
            const selected = isSelected(node.component) || hasSelectedChild(node);
            return (
                <SubMenu
                    key={node.id}
                    handleToggle={() => handleToggle(node.route)}
                    isOpen={state[node.route]}
                    name={node.name}
                    dense={dense}
                    icon={getIcon(node.icon)}
                    isSelected={selected}
                >
                    {generateMenu(node.children)}
                </SubMenu>
            );
        } else {
            if (node.component) {
                const selected = isSelected(node.component);
                // 在 generateMenu 函数中的 MenuItemLink 部分
                return (
                    <MenuItemLink
                        key={node.id}
                        to={node.component}
                        state={{ _scrollToTop: true }}
                        primaryText={translate(node.name)}
                        leftIcon={getIcon(node.icon)}
                        dense={dense}
                        sx={{
                            backgroundColor: selected ? 'rgba(25, 118, 210, 0.08) !important' : 'transparent',
                            color: selected ? '#1976d2 !important' : 'text.secondary',
                            '&:hover': {
                                backgroundColor: selected ? 'rgba(25, 118, 210, 0.12) !important' : 'rgba(0, 0, 0, 0.04)',
                            },
                            borderLeft: 'none',
                            borderRadius: '4px',
                            margin: '2px 8px',
                            width: 'calc(100% - 16px)',
                            transition: 'all 0.2s ease-in-out',
                            // 缩小整体间距
                            padding: '6px 8px', // 减少内边距
                            minHeight: '36px', // 稍微减小高度
                            '& .RaMenuItemLink-icon': {
                                color: selected ? '#1976d2 !important' : 'text.secondary',
                                minWidth: '32px !important', // 缩小图标区域宽度
                                marginRight: '4px', // 缩小图标和文字间距
                                display: 'flex',
                                alignItems: 'center',
                                justifyContent: 'center', // 图标居中显示
                            },
                            fontWeight: selected ? 600 : 400,
                            // 确保文字内容左对齐
                            '& .MuiListItemText-root': {
                                margin: 0,
                                '& .MuiTypography-root': {
                                    textAlign: 'left',
                                    justifyContent: 'flex-start',
                                    fontSize: '0.875rem', // 稍微减小字体大小
                                    lineHeight: '1.3',
                                }
                            },
                        }}
                    />
                );
            }
      if (node.children) {
        const selected = isSelected(node.component) || hasSelectedChild(node);
        return (
          <SubMenu
            key={node.id}
            handleToggle={() => handleToggle(node.route)}
            isOpen={state[node.route]}
            name={node.name}
            dense={dense}
            icon={getIcon(node.icon)}
            isSelected={selected}
          >
            {generateMenu(node.children)}
          </SubMenu>
        );
      } else {
        if (node.component) {
          const selected = isSelected(node.component);
          // 在 generateMenu 函数中的 MenuItemLink 部分
          return (
            <MenuItemLink
              key={node.id}
              to={node.component}
              state={{ _scrollToTop: true }}
              primaryText={translate(node.name)}
              leftIcon={getIcon(node.icon)}
              dense={dense}
              sx={{
                backgroundColor: selected ? 'rgba(25, 118, 210, 0.08) !important' : 'transparent',
                color: selected ? '#1976d2 !important' : 'text.secondary',
                '&:hover': {
                  backgroundColor: selected ? 'rgba(25, 118, 210, 0.12) !important' : 'rgba(0, 0, 0, 0.04)',
                },
                borderLeft: 'none',
                borderRadius: '4px',
                margin: '2px 8px',
                width: 'calc(100% - 16px)',
                transition: 'all 0.2s ease-in-out',
                // 缩小整体间距
                padding: '6px 8px', // 减少内边距
                minHeight: '36px', // 稍微减小高度
                '& .RaMenuItemLink-icon': {
                  color: selected ? '#1976d2 !important' : 'text.secondary',
                  minWidth: '32px !important', // 缩小图标区域宽度
                  marginRight: '4px', // 缩小图标和文字间距
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center', // 图标居中显示
                },
                fontWeight: selected ? 600 : 400,
                // 确保文字内容左对齐
                '& .MuiListItemText-root': {
                  margin: 0,
                  '& .MuiTypography-root': {
                    textAlign: 'left',
                    justifyContent: 'flex-start',
                    fontSize: '0.875rem', // 稍微减小字体大小
                    lineHeight: '1.3',
                  }
                },
              }}
            />
          );
        }
      }
    });
  };
rsf-admin/src/layout/TabsBar.jsx
@@ -297,19 +297,24 @@
                                <span>{getTabLabel(tab)}</span>
                                {tab.closable && (
                                    <Tooltip title="关闭">
                                        <IconButton
                                            size="small"
                                        <Box
                                            component="span"
                                            onClick={(e) => handleCloseTab(e, tab.path)}
                                            sx={{
                                                display: 'inline-flex',
                                                alignItems: 'center',
                                                justifyContent: 'center',
                                                p: 0.25,
                                                ml: 0.5,
                                                borderRadius: '50%',
                                                cursor: 'pointer',
                                                '&:hover': {
                                                    backgroundColor: 'rgba(0, 0, 0, 0.1)',
                                                },
                                            }}
                                        >
                                            <CloseIcon sx={{ fontSize: 14 }} />
                                        </IconButton>
                                        </Box>
                                    </Tooltip>
                                )}
                            </Box>
rsf-admin/src/layout/index.jsx
@@ -31,7 +31,8 @@
      <Box sx={{
        position: 'fixed',
        top: 48,
        left: sidebarWidth,
        // left: 0,
        left: sidebarWidth + 5,
        right: 0,
        zIndex: 1100,
        transition: (theme) =>
rsf-admin/src/page/basicInfo/loc/InitModal.jsx
@@ -232,7 +232,7 @@
                                    onChange={(e) => handleChange(+e.target.value, 'channel')}
                                    size="small"
                                    type="number"
                                    validate={[required()]}
                                    // validate={[required()]}
                                />
                            </Grid>
                            <Grid item xs={4}>
@@ -243,7 +243,7 @@
                                    onChange={(e) => handleChange(+e.target.value, 'startChannel')}
                                    size="small"
                                    type="number"
                                    validate={[required()]}
                                    // validate={[required()]}
                                />
                            </Grid>
                        </Grid>
rsf-admin/src/page/components/PageDrawer.jsx
@@ -29,10 +29,15 @@
            open={!!drawerVal}
            anchor="right"
            onClose={handleClose}
            sx={{ zIndex: 100 }}
            sx={{
                zIndex: 100,
                '& .MuiDrawer-paper': {
                    top: '86px', // AppBar(50px) + TabsBar(36px)
                }
            }}
        >
            {!!drawerVal && (
                <Box pt={5} width={{ xs: '100vW', sm: width }} height={'calc(100vh - 200px);'} mt={{ xs: 2, sm: 1 }}>
                <Box pt={2} width={{ xs: '100vW', sm: width }} mt={{ xs: 2, sm: 1 }}>
                    <Stack direction="row" p={2}>
                        <Typography variant="h6" flex="1">
                            {title}
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuCreate.jsx
New file
@@ -0,0 +1,125 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    BooleanInput,
    DateInput,
    SaveButton,
    SelectInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    Toolbar,
    required,
    useDataProvider,
    useNotify,
    Form,
    useCreateController,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Stack,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "../components/DialogCloseButton";
import StatusSelectInput from "../components/StatusSelectInput";
import MemoInput from "../components/MemoInput";
const MatnrRoleMenuCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async (data) => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <>
            <CreateBase
                record={{}}
                transform={(data) => {
                    return data;
                }}
                mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
            >
                <Dialog
                    open={open}
                    onClose={handleClose}
                    aria-labelledby="form-dialog-title"
                    fullWidth
                    disableRestoreFocus
                    maxWidth="md"   // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
                >
                    <Form>
                        <DialogTitle id="form-dialog-title" sx={{
                            position: 'sticky',
                            top: 0,
                            backgroundColor: 'background.paper',
                            zIndex: 1000
                        }}
                        >
                            {translate('create.title')}
                            <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={6} display="flex" gap={1}>
                                    <NumberInput
                                        label="table.field.matnrRoleMenu.roleId"
                                        source="roleId"
                                        autoFocus
                                        validate={required()}
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                        label="table.field.matnrRoleMenu.menuId"
                                        source="menuId"
                                        validate={required()}
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <StatusSelectInput />
                                </Grid>
                                <Grid item xs={12} display="flex" gap={1}>
                                    <Stack direction="column" spacing={1} width={'100%'}>
                                        <MemoInput />
                                    </Stack>
                                </Grid>
                            </Grid>
                        </DialogContent>
                        <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                            <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}  >
                                <SaveButton />
                            </Toolbar>
                        </DialogActions>
                    </Form>
                </Dialog>
            </CreateBase>
        </>
    )
}
export default MatnrRoleMenuCreate;
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuEdit.jsx
New file
@@ -0,0 +1,97 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    Edit,
    SimpleForm,
    FormDataConsumer,
    useTranslate,
    TextInput,
    NumberInput,
    BooleanInput,
    DateInput,
    SelectInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    SaveButton,
    Toolbar,
    Labeled,
    NumberField,
    required,
    useRecordContext,
    DeleteButton,
} from 'react-admin';
import { useWatch, useFormContext } from "react-hook-form";
import { Stack, Grid, Box, Typography } from '@mui/material';
import * as Common from '@/utils/common';
import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
import EditBaseAside from "../components/EditBaseAside";
import CustomerTopToolBar from "../components/EditTopToolBar";
import MemoInput from "../components/MemoInput";
import StatusSelectInput from "../components/StatusSelectInput";
const FormToolbar = () => {
    const { getValues } = useFormContext();
    return (
        <Toolbar sx={{ justifyContent: 'space-between' }}>
            <SaveButton />
            <DeleteButton mutationMode="optimistic" />
        </Toolbar>
    )
}
const MatnrRoleMenuEdit = () => {
    const translate = useTranslate();
    return (
        <Edit
            redirect="list"
            mutationMode={EDIT_MODE}
            actions={<CustomerTopToolBar />}
            aside={<EditBaseAside />}
        >
            <SimpleForm
                shouldUnregister
                warnWhenUnsavedChanges
                toolbar={<FormToolbar />}
                mode="onTouched"
                defaultValues={{}}
            // validate={(values) => { }}
            >
                <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                    <Grid item xs={12} md={8}>
                        <Typography variant="h6" gutterBottom>
                            {translate('common.edit.title.main')}
                        </Typography>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                                label="table.field.matnrRoleMenu.roleId"
                                source="roleId"
                                autoFocus
                                validate={required()}
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                                label="table.field.matnrRoleMenu.menuId"
                                source="menuId"
                                validate={required()}
                            />
                        </Stack>
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <Typography variant="h6" gutterBottom>
                            {translate('common.edit.title.common')}
                        </Typography>
                        <StatusSelectInput />
                        <Box mt="2em" />
                        <MemoInput />
                    </Grid>
                </Grid>
            </SimpleForm>
        </Edit >
    )
}
export default MatnrRoleMenuEdit;
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuList.jsx
New file
@@ -0,0 +1,154 @@
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,
} from 'react-admin';
import { Box, Typography, Card, Stack } from '@mui/material';
import { styled } from '@mui/material/styles';
import MatnrRoleMenuCreate from "./MatnrRoleMenuCreate";
import MatnrRoleMenuPanel from "./MatnrRoleMenuPanel";
import EmptyData from "../components/EmptyData";
import MyCreateButton from "../components/MyCreateButton";
import MyExportButton from '../components/MyExportButton';
import PageDrawer from "../components/PageDrawer";
import MyField from "../components/MyField";
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
        height: '.9em'
    },
    '& .RaDatagrid-row': {
        cursor: 'auto'
    },
    '& .column-name': {
    },
    '& .opt': {
        width: 200
    },
}));
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <NumberInput source="roleId" label="table.field.matnrRoleMenu.roleId" />,
    <NumberInput source="menuId" label="table.field.matnrRoleMenu.menuId" />,
    <TextInput label="common.field.memo" source="memo" />,
    <SelectInput
        label="common.field.status"
        source="status"
        choices={[
            { id: '1', name: 'common.enums.statusTrue' },
            { id: '0', name: 'common.enums.statusFalse' },
        ]}
        resettable
    />,
]
const MatnrRoleMenuList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    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.matnrRoleMenu"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
                sort={{ field: "create_time", order: "desc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='matnrRoleMenu' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey='matnrRoleMenu'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => false}
                    expand={() => <MatnrRoleMenuPanel />}
                    expandSingle={true}
                    omit={['id', 'createTime', 'createBy', 'memo']}
                >
                    <NumberField source="id" />
                    <NumberField source="roleId" label="table.field.matnrRoleMenu.roleId" />
                    <NumberField source="menuId" label="table.field.matnrRoleMenu.menuId" />
                    <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
                    <DateField source="updateTime" label="common.field.updateTime" showTime />
                    <ReferenceField source="createBy" label="common.field.createBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
                    <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">
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                </StyledDatagrid>
            </List>
            <MatnrRoleMenuCreate
                open={createDialog}
                setOpen={setCreateDialog}
            />
            <PageDrawer
                title='MatnrRoleMenu Detail'
                drawerVal={drawerVal}
                setDrawerVal={setDrawerVal}
            >
            </PageDrawer>
        </Box>
    )
}
export default MatnrRoleMenuList;
rsf-admin/src/page/matnrRoleMenu/MatnrRoleMenuPanel.jsx
New file
@@ -0,0 +1,63 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import { Box, Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
import {
    useTranslate,
    useRecordContext,
} from 'react-admin';
import PanelTypography from "../components/PanelTypography";
import * as Common from '@/utils/common'
const MatnrRoleMenuPanel = () => {
    const record = useRecordContext();
    if (!record) return null;
    const translate = useTranslate();
    return (
        <>
            <Card sx={{ width: { xs: 300, sm: 500, md: 600, lg: 800 }, margin: 'auto' }}>
                <CardContent>
                    <Grid container spacing={2}>
                        <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'space-between' }}>
                            <Typography variant="h6" gutterBottom align="left" sx={{
                                maxWidth: { xs: '100px', sm: '180px', md: '260px', lg: '360px' },
                                whiteSpace: 'nowrap',
                                overflow: 'hidden',
                                textOverflow: 'ellipsis',
                            }}>
                                {Common.camelToPascalWithSpaces(translate('table.field.matnrRoleMenu.id'))}: {record.id}
                            </Typography>
                            {/*  inherit, primary, secondary, textPrimary, textSecondary, error */}
                            <Typography variant="h6" gutterBottom align="right" >
                                ID: {record.id}
                            </Typography>
                        </Grid>
                    </Grid>
                    <Grid container spacing={2}>
                        <Grid item xs={12} container alignContent="flex-end">
                            <Typography variant="caption" color="textSecondary" sx={{ wordWrap: 'break-word', wordBreak: 'break-all' }}>
                                {Common.camelToPascalWithSpaces(translate('common.field.memo'))}:{record.memo}
                            </Typography>
                        </Grid>
                    </Grid>
                    <Box height={20}>&nbsp;</Box>
                    <Grid container spacing={2}>
                        <Grid item xs={6}>
                            <PanelTypography
                                title="table.field.matnrRoleMenu.roleId"
                                property={record.roleId}
                            />
                        </Grid>
                        <Grid item xs={6}>
                            <PanelTypography
                                title="table.field.matnrRoleMenu.menuId"
                                property={record.menuId}
                            />
                        </Grid>
                    </Grid>
                </CardContent>
            </Card >
        </>
    );
};
export default MatnrRoleMenuPanel;
rsf-admin/src/page/matnrRoleMenu/index.jsx
New file
@@ -0,0 +1,18 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    ListGuesser,
    EditGuesser,
    ShowGuesser,
} from "react-admin";
import MatnrRoleMenuList from "./MatnrRoleMenuList";
import MatnrRoleMenuEdit from "./MatnrRoleMenuEdit";
export default {
    list: MatnrRoleMenuList,
    edit: MatnrRoleMenuEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => {
        return `${record.id}`
    }
};
rsf-admin/src/page/orders/asnOrder/AsnOrderModal.jsx
@@ -706,7 +706,7 @@
                        },
                    },
                }}
                pageSizeOptions={[15, 25, 50, 100]}
                pageSizeOptions={[10, 20, 50, 100]}
                editMode="row"
                checkboxSelection
                onRowSelectionModelChange={handleSelectionChange}
rsf-admin/src/page/orders/asnOrder/AsnOrderPanel.jsx
@@ -175,7 +175,7 @@
                        },
                    },
                }}
                pageSizeOptions={[15, 25, 50]}
                pageSizeOptions={[10, 20, 50]}
            />
        </Box >
rsf-admin/src/page/system/role/AssignPermissions_matnr.jsx
New file
@@ -0,0 +1,380 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import {
    useTranslate,
    useNotify,
    TextInput
} from 'react-admin';
import { Box, Button, Card, Stack, CardContent, Skeleton, TextField } from '@mui/material';
import { SimpleTreeView, TreeItem, RichTreeView, useTreeViewApiRef } from '@mui/x-tree-view';
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request'
const DEFAULT_EXPAND_ALL = true;
const AssignPermissionsMatnr = (props) => {
    const { role, originMenuIds, setDrawerVal, closeCallback, authType } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const [loading, setLoading] = useState(false);
    const [treeData, setTreeData] = useState([]);
    const [selectedItems, setSelectedItems] = useState([]);
    const [expandedItems, setExpandedItems] = useState([]);
    const [parmas, setParmas] = useState({ condition: '', authType: authType });
    const toggledItemRef = useRef({});
    const apiRef = useTreeViewApiRef();
    useEffect(() => {
        reload()
    }, [role, originMenuIds])
    const reload = () => {
        setSelectedItems(originMenuIds.map(item => item + ""));
        const transformTree = (treeData) => {
            return treeData.map(data => {
                return {
                    id: data.id + '',
                    label: data.type === 0 ? translate(data.name || data.code) : data.name || data.code,
                    type: data.type,
                    children: (data.children && data.children.length > 0 ? transformTree(data.children) : null)
                }
            })
        }
        const http = async () => {
            const res = await request.post('/menuMatnrGroup/tree', parmas);
            if (res?.data?.code === 200) {
                const transformData = transformTree(res.data.data);
                setTreeData(transformData);
                if (DEFAULT_EXPAND_ALL) {
                    setExpandedItems(getAllItemsWithChildrenItemIds(transformData));
                }
            } else {
                notify(res.data.msg, { type: 'error' });
            }
            setLoading(false);
        }
        setLoading(true);
        setTimeout(() => {
            http();
        }, 200);
    }
    const getAllItemItemIds = () => {
        const ids = [];
        const registerItemId = (item) => {
            ids.push(item.id);
            item.children?.forEach(registerItemId);
        };
        treeData.forEach(registerItemId);
        return ids;
    };
    const getAllItemsWithChildrenItemIds = (treeDataHandle) => {
        const itemIds = [];
        const registerItemId = (item) => {
            if (item.children?.length) {
                itemIds.push(item.id);
                item.children.forEach(registerItemId);
            }
        };
        if (treeDataHandle) {
            treeDataHandle.forEach(registerItemId);
        } else {
            treeData.forEach(registerItemId);
        }
        return itemIds;
    };
    const handleSelectedItemsChange = (event, newSelectedItems) => {
        const itemsToSelect = [];
        const itemsToUnSelect = {};
        Object.entries(toggledItemRef.current).forEach(([itemId, isSelected]) => {
            const item = apiRef.current.getItem(itemId);
            if (isSelected) {
                itemsToSelect.push(...getItemDescendantsIds(item));
                const parentIds = getParentIds(treeData, itemId);
                itemsToSelect.push(...parentIds);
            } else {
                // 取消子节点
                const treeNode = checkoutTreeNode(treeData, itemId);
                if (treeNode?.children && treeNode?.children.length > 0) {
                    const allChildren = getItemDescendantsIds(treeNode);
                    const childrenSet = new Set(allChildren);
                    newSelectedItems = newSelectedItems.filter(item => !childrenSet.has(item));
                }
                // 取消父节点
                const removeParentIfAllSiblingsDeselected = (itemId, newSelectedItems, treeData) => {
                    let updatedSelectedItems = [...newSelectedItems];
                    let currentId = itemId;
                    while (true) {
                        const parentId = getParentId(treeData, currentId);
                        if (!parentId) break;
                        const siblings = getChildrenIds(treeData, parentId);
                        const allSiblingsDeselected = siblings.every(siblingId => !updatedSelectedItems.includes(siblingId));
                        if (allSiblingsDeselected) {
                            updatedSelectedItems = updatedSelectedItems.filter(id => id !== parentId);
                            currentId = parentId;
                        } else {
                            break;
                        }
                    }
                    return updatedSelectedItems;
                };
                newSelectedItems = removeParentIfAllSiblingsDeselected(itemId, newSelectedItems, treeData);
            }
        });
        const newSelectedItemsWithChildren = Array.from(
            new Set(
                [...newSelectedItems, ...itemsToSelect].filter(
                    (itemId) => !itemsToUnSelect[itemId],
                ),
            ),
        );
        setSelectedItems(newSelectedItemsWithChildren);
        toggledItemRef.current = {};
    };
    const handleSave = (event) => {
        request.post('/roleMatnr/scope/update', {
            id: role.id,
            menuIds: {
                checked: selectedItems,
                halfChecked: []
            }
        }).then(res => {
            if (res?.data.code === 200) {
                setDrawerVal(null);
                if (closeCallback) {
                    closeCallback();
                }
                notify(res.data.msg, { type: 'info', messageArgs: { _: res.data.msg } })
            } else {
                notify(res.data.msg, { type: 'error' })
            }
        })
    }
    const search = (e) => {
        const value = e.target.value;
        setParmas({
            ...parmas,
            condition: value
        })
        reload()
    }
    return (
        <>
            <Card sx={{
                ml: 1,
                mr: 1,
                height: 'calc(100vh - 140px)',
                overflowY: 'auto'
            }}>
                <CardContent sx={{
                    overflow: 'auto',
                    display: 'flex',
                    flexDirection: 'column',
                    justifyContent: 'space-between'
                }}>
                    <Box>
                        <Box mb={1} sx={{
                            display: 'flex',
                            justifyContent: 'space-between'
                        }}>
                            <Button onClick={() => {
                                setSelectedItems((oldSelected) =>
                                    oldSelected.length === 0 ? getAllItemItemIds() : [],
                                );
                            }}>
                                {selectedItems.length === 0 ? translate('ra.action.select_all') : translate('ra.action.unselect')}
                            </Button>
                            <Button onClick={() => {
                                setExpandedItems((oldExpanded) =>
                                    oldExpanded.length === 0 ? getAllItemsWithChildrenItemIds() : [],
                                );
                            }}>
                                {expandedItems.length === 0 ? translate('common.action.expandAll') : translate('common.action.collapseAll')}
                            </Button>
                        </Box>
                        <Box sx={{
                            display: 'flex',
                            justifyContent: 'space-between',
                            alignItems: 'center'
                        }}>
                            <TextField sx={{ width: '200px' }} label="搜索菜单" variant="outlined" value={parmas.condition} onChange={(e) => search(e)} />
                            <Button startIcon={<SaveIcon />} size="small" variant="contained" onClick={handleSave} sx={{ height: '40px' }}>
                                {translate('ra.action.save')}
                            </Button>
                        </Box>
                        <Box sx={{
                            minWidth: 290,
                            overflow: 'auto',
                            marginTop: '10px',
                            padding: 1,
                            borderBottom: '1px solid background.paper',
                            borderRadius: '4px',
                            boxShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',
                            backgroundColor: 'background.paper',
                        }}>
                            {loading ? (
                                <SkeletonBox />
                            ) : (
                                <RichTreeView
                                    multiSelect
                                    checkboxSelection
                                    apiRef={apiRef}
                                    items={treeData}
                                    selectedItems={selectedItems}
                                    onSelectedItemsChange={handleSelectedItemsChange}
                                    onItemSelectionToggle={(event, itemId, isSelected) => {
                                        toggledItemRef.current[itemId] = isSelected;
                                    }}
                                    expandedItems={expandedItems}
                                    onExpandedItemsChange={(event, itemIds) => {
                                        setExpandedItems(itemIds);
                                    }}
                                />
                            )}
                        </Box>
                    </Box>
                </CardContent>
            </Card>
        </>
    )
}
const checkoutTreeNode = (treeData, targetId) => {
    let result = null;
    const checkout = (node) => {
        if (node.id === targetId) {
            result = node;
            return true;
        } else {
            if (node.children) {
                for (const child of node.children) {
                    if (checkout(child)) {
                        return true;
                    }
                }
            }
        }
        return false;
    };
    treeData.forEach(item => {
        if (checkout(item)) {
            return;
        }
    });
    return result;
};
const getItemDescendantsIds = (item) => {
    const ids = [];
    item.children?.forEach((child) => {
        ids.push(child.id);
        ids.push(...getItemDescendantsIds(child));
    });
    return ids;
}
const getParentIds = (tree, targetId) => {
    let parentIds = [];
    const searchTree = (node, path = []) => {
        if (node.id === targetId) {
            parentIds = [...path];
            return true;
        }
        if (node.children) {
            for (const child of node.children) {
                if (searchTree(child, [...path, node.id])) {
                    return true;
                }
            }
        }
        return false;
    };
    tree.forEach(item => {
        searchTree(item);
    })
    return parentIds;
};
const getParentId = (tree, targetId) => {
    let parentId = null;
    const searchTree = (node) => {
        if (node.children) {
            for (const child of node.children) {
                if (child.id === targetId) {
                    parentId = node.id;
                    return true;
                }
                if (searchTree(child)) {
                    return true;
                }
            }
        }
        return false;
    };
    tree.forEach(item => {
        if (searchTree(item)) {
            return parentId;
        }
    });
    return parentId;
};
const getChildrenIds = (tree, targetId) => {
    let childrenIds = [];
    const searchTree = (node) => {
        if (node.id === targetId && node.children) {
            childrenIds = node.children.map(child => child.id);
        } else if (node.children) {
            for (const child of node.children) {
                searchTree(child);
            }
        }
    };
    tree.forEach(item => {
        searchTree(item);
    });
    return childrenIds;
};
const SkeletonBox = () => {
    return (
        <Stack spacing={1}>
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
        </Stack>
    )
}
export default AssignPermissionsMatnr;
rsf-admin/src/page/system/role/RoleList.jsx
@@ -30,7 +30,8 @@
    useNotify,
    Button,
} from 'react-admin';
import { Box, Card, Stack } from '@mui/material';
import { Box, Card, Stack, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import SecurityIcon from '@mui/icons-material/Security';
import { styled } from '@mui/material/styles';
import RoleCreate from "./RoleCreate";
import RolePanel from "./RolePanel";
@@ -43,9 +44,12 @@
import * as Common from '@/utils/common';
import AssignPermissions from "./AssignPermissions";
import AssignPermissionsPda from "./AssignPermissions_pda";
import AssignPermissionsMatnr from "./AssignPermissions_matnr";
import request from '@/utils/request';
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd';
import AdUnitsIcon from '@mui/icons-material/AdUnits';
import ArticleIcon from '@mui/icons-material/Article';
import { margin } from "@mui/system";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -83,12 +87,14 @@
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const [drawerValPda, setDrawerValPda] = useState(false);
    const [drawerValMatnr, setDrawerValMatnr] = useState(false);
    const [menuIds, setMenuIds] = useState([]);
    const [authType, setAuthType] = useState(0)
    const assign = (record) => {
        setDrawerValPda(false);
        request('/role/scope/list', {
            method: 'GET',
            params: {
@@ -106,6 +112,7 @@
    }
    const assignPda = (record) => {
        setDrawerVal(false);
        request('/rolePda/scope/list', {
            method: 'GET',
            params: {
@@ -122,6 +129,24 @@
        });
    }
    const assignMatnr = (record) => {
        setDrawerVal(false);
        request('/roleMatnr/scope/list', {
            method: 'GET',
            params: {
                roleId: record.id
            }
        }).then((res) => {
            if (res?.data?.code === 200) {
                const { data: menuIds } = res.data;
                setMenuIds(menuIds || []);
                setDrawerValMatnr(!!drawerValMatnr && drawerValMatnr === record ? null : record);
            } else {
                notify(res.data.msg, { type: 'error' });
            }
        });
    }
    return (
        <Box display="flex">
            <List
@@ -131,7 +156,7 @@
                        theme.transitions.create(['all'], {
                            duration: theme.transitions.duration.enteringScreen,
                        }),
                    marginRight: (!!drawerVal || !!drawerValPda) ? `${PAGE_DRAWER_WIDTH}px` : 0,
                    marginRight: (!!drawerVal || !!drawerValPda || !!drawerValMatnr) ? `${PAGE_DRAWER_WIDTH}px` : 0,
                }}
                title={"menu.role"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
@@ -167,9 +192,12 @@
                    <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">
                        <ScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assign={assign} auType={0} setAuthType={setAuthType} label="网页权限&nbsp;&nbsp;&nbsp;" />
                        <PdaScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assignPda={assignPda} auType={1} setAuthType={setAuthType} label="PDA权限&nbsp;&nbsp;&nbsp;" />
                        <ScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assign={assign} auType={2} setAuthType={setAuthType} label="仓库权限&nbsp;" />
                        <PermissionMenuButton
                            assign={assign}
                            assignPda={assignPda}
                            assignMatnr={assignMatnr}
                            setAuthType={setAuthType}
                        />
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
@@ -215,46 +243,109 @@
                    authType={authType}
                />
            </PageDrawer>
            <PageDrawer
                drawerVal={drawerValMatnr}
                setDrawerVal={setDrawerValMatnr}
                title={!!drawerValMatnr ? `Scope by ${drawerValMatnr.code || drawerValMatnr.name}` : 'Role Detail'}
                closeCallback={() => {
                    setMenuIds([]);
                }}
            >
                <AssignPermissionsMatnr
                    role={drawerValMatnr}
                    originMenuIds={menuIds}
                    setDrawerVal={setDrawerValMatnr}
                    closeCallback={() => {
                        setMenuIds([]);
                    }}
                    authType={authType}
                />
            </PageDrawer>
        </Box>
    )
}
const ScopeButton = (props) => {
const PermissionMenuButton = ({ assign, assignPda, assignMatnr, setAuthType }) => {
    const record = useRecordContext();
    const { assign, auType, setAuthType, label, ...rest } = props;
    return (
        <Button
            variant="text"
            color="primary"
            startIcon={<AssignmentIndIcon />}
            label={label}
            onClick={(event) => {
                setAuthType(auType);
                event.stopPropagation();
                assign(record);
            }}
            {...rest}
        />
    )
}
    const [anchorEl, setAnchorEl] = useState(null);
    const open = Boolean(anchorEl);
const PdaScopeButton = (props) => {
    const record = useRecordContext();
    const { assignPda, auType, setAuthType, label, ...rest } = props;
    const handleClick = (event) => {
        event.stopPropagation();
        setAnchorEl(event.currentTarget);
    };
    const handleClose = (event) => {
        if (event) event.stopPropagation();
        setAnchorEl(null);
    };
    const handleWebPermission = (event) => {
        event.stopPropagation();
        setAuthType(0);
        assign(record);
        handleClose();
    };
    const handlePdaPermission = (event) => {
        event.stopPropagation();
        setAuthType(1);
        assignPda(record);
        handleClose();
    };
    const handleMatnrPermission = (event) => {
        event.stopPropagation();
        setAuthType(2);
        assignMatnr(record);
        handleClose();
    };
    return (
        <Button
            variant="text"
            color="primary"
            startIcon={<AdUnitsIcon />}
            label={label}
            onClick={(event) => {
                setAuthType(auType);
                event.stopPropagation();
                assignPda(record);
            }}
            {...rest}
        />
    )
        <>
            <Button
                variant="text"
                color="primary"
                startIcon={<SecurityIcon />}
                label="权限"
                onClick={handleClick}
                sx={{ marginLeft: '2px', padding: '1px', fontSize: '.75rem' }}
            />
            <Menu
                anchorEl={anchorEl}
                open={open}
                onClose={handleClose}
                onClick={(e) => e.stopPropagation()}
                anchorOrigin={{
                    vertical: 'bottom',
                    horizontal: 'left',
                }}
                transformOrigin={{
                    vertical: 'top',
                    horizontal: 'left',
                }}
            >
                <MenuItem onClick={handleWebPermission}>
                    <ListItemIcon>
                        <AssignmentIndIcon fontSize="small" />
                    </ListItemIcon>
                    <ListItemText>网页权限</ListItemText>
                </MenuItem>
                <MenuItem onClick={handlePdaPermission}>
                    <ListItemIcon>
                        <AdUnitsIcon fontSize="small" />
                    </ListItemIcon>
                    <ListItemText>PDA权限</ListItemText>
                </MenuItem>
                <MenuItem onClick={handleMatnrPermission}>
                    <ListItemIcon>
                        <ArticleIcon fontSize="small" />
                    </ListItemIcon>
                    <ListItemText>物料权限</ListItemText>
                </MenuItem>
            </Menu>
        </>
    );
}
export default RoleList;
rsf-admin/src/page/work/stockTransfer/stockTransferList.jsx
@@ -387,7 +387,7 @@
                        },
                    },
                }}
                pageSizeOptions={[10, 25, 50, 100]}
                pageSizeOptions={[10, 20, 50, 100]}
                editMode="row"                
                onRowSelectionModelChange={handleSelectionChange}
                selectionModel={selectedRows}
rsf-open-api/pom.xml
@@ -21,6 +21,24 @@
            <artifactId>rsf-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!-- JWT依赖 -->
        <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>
    </dependencies>
    <build>
        <finalName>rsf-open-api</finalName>
rsf-open-api/src/main/java/com/vincent/rsf/openApi/annotation/OperationLog.java
New file
@@ -0,0 +1,42 @@
package com.vincent.rsf.openApi.annotation;
import java.lang.annotation.*;
/**
 * 操作日志记录注解
 *
 * @author vincent
 * @since 2020-03-21 17:03:08
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
    /**
     * 操作功能
     */
    String value() default "";
    /**
     * 操作模块
     */
    String module() default "";
    /**
     * 备注
     */
    String comments() default "";
    /**
     * 是否记录请求参数
     */
    boolean param() default true;
    /**
     * 是否记录返回结果
     */
    boolean result() default true;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/aspect/LogAspect.java
New file
@@ -0,0 +1,264 @@
package com.vincent.rsf.openApi.aspect;
import com.alibaba.fastjson.JSON;
import com.vincent.rsf.common.utils.Utils;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.openApi.annotation.OperationLog;
import com.vincent.rsf.openApi.entity.app.ApiForeignLog;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.service.ApiForeignLogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.rmi.NoSuchObjectException;
import java.util.*;
/**
 * Created by Administrator on 2019-07-09.
 */
@Component
@Aspect
@Slf4j
@Order(2)
public class LogAspect {
    // 参数、返回结果、错误信息等最大保存长度
    private static final int MAX_LENGTH = 1000;
    // 用于记录请求耗时
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();
    @Resource
    private ApiForeignLogService apiForeignLogService;
    public LogAspect() {
    }
    /**
     * 切入点
     * 匹配controller包及其所有子包下的所有类的所有方法
     */
    @Pointcut("execution(* com.vincent.rsf.openApi.controller..*.*(..))")
    public void controllerPc() {
    }
    /**
     * 环绕通知
     * @param pjp ProceedingJoinPoint
     * @return 方法结果
     */
    @Around("controllerPc()")
    public Object around(ProceedingJoinPoint pjp) {
        String methodName = pjp.getSignature().getName();
        try {
            ServletRequestAttributes attributes = Optional.ofNullable((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                    .orElseThrow(() -> new NoSuchObjectException("前置通知中获取的 ServletRequestAttributes 对象为空"));
            HttpServletRequest request = attributes.getRequest();
//            if("getToken".equals(methodName) || "getToken".equals(methodName))
//                return pjp.proceed();
            // 前置通知
            log.info("------【前置通知】------");
            // 记录请求内容
            log.info("浏览器输入的网址:{}", request.getRequestURL().toString());
            log.info("HTTP_METHOD:{}", request.getMethod());
            log.info("IP:{}", request.getRemoteAddr());
            log.info("执行的业务方法名:{}", pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName());
            log.info("业务方法获得的参数:{}", Arrays.toString(pjp.getArgs()));
            Object result = pjp.proceed();
            // 后置通知
            log.info("------【后置通知】------");
            log.info("{}方法的返回值:{}", pjp.getSignature().getName(), result);
            saveLog1(pjp, result, null);
            return result;
        } catch (Throwable e) {
            // 异常通知
            log.error("------【异常通知】------");
            log.error("{}方法异常,参数:{},异常:", pjp.getSignature().getName(), Arrays.toString(pjp.getArgs()), e);
            saveLog1(pjp, null, (Exception) e);
            return CommonResponse.error("服务器处理数据异常");
        } finally {
            // 最终通知
            if(!methodName.isEmpty() && !"getRegistered".equals(methodName)){
                log.info("------【最终通知】------");
                log.info("{}方法执行结束", pjp.getSignature().getName());
            }
        }
    }
    private void saveLog1(JoinPoint joinPoint, Object result, Exception e) {
        ApiForeignLog record = new ApiForeignLog();
        Long endTime = startTime.get();
        record.setCreateTime(new Date());
        // 记录操作耗时
        if (endTime != null) {
            record.setSpendTime((int) (System.currentTimeMillis() - endTime));
        }
        record.setTimestamp(String.valueOf(endTime));
//        // 记录当前登录用户id、租户id
//        User user = getLoginUser();
//        if (user != null) {
//            record.setUserId(user.getId());
//            record.setTenantId(user.getTenantId());
//        }
        // 记录请求地址、请求方式、ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (attributes == null ? null : attributes.getRequest());
        if (request != null) {
            record.setUrl(request.getRequestURI());
//            record.setClientIp(IpTools.gainRealIp(request));
        }
        // 记录异常信息
        if (e != null) {
            record.setResult(0);
            record.setErr(Utils.sub(e.toString(), MAX_LENGTH));
        } else {
            record.setResult(1);
        }
//        // 记录操作功能
//        record.setNamespace(desc);
//        // 记录备注
//        if (!Cools.isEmpty(ol.comments())) {
//            record.setMemo(ol.comments());
//        }
        // 记录请求参数
        record.setRequest(Utils.sub(Arrays.toString(joinPoint.getArgs()), MAX_LENGTH));
        record.setResponse(Utils.sub(JSON.toJSONString(result), MAX_LENGTH));
        apiForeignLogService.saveAsync(record);
    }
    /**
     * 保存操作记录
     */
    private void saveLog(JoinPoint joinPoint, Object result, Exception e) {
        // 记录模块名、操作功能、请求方法、请求参数、返回结果
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (null == method) {
            return;
        }
        OperationLog ol = method.getAnnotation(OperationLog.class);
        if (null == ol) {
            return;
        }
        String desc = getDescription(method, ol);
        if (Cools.isEmpty(desc)) {
            return;
        }
        ApiForeignLog record = new ApiForeignLog();
        Long endTime = startTime.get();
        record.setCreateTime(new Date());
        // 记录操作耗时
        if (endTime != null) {
            record.setSpendTime((int) (System.currentTimeMillis() - endTime));
        }
        record.setTimestamp(String.valueOf(endTime));
//        // 记录当前登录用户id、租户id
//        User user = getLoginUser();
//        if (user != null) {
//            record.setUserId(user.getId());
//            record.setTenantId(user.getTenantId());
//        }
        // 记录请求地址、请求方式、ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (attributes == null ? null : attributes.getRequest());
        if (request != null) {
            record.setUrl(request.getRequestURI());
//            record.setClientIp(IpTools.gainRealIp(request));
        }
        // 记录异常信息
        if (e != null) {
            record.setResult(0);
            record.setErr(Utils.sub(e.toString(), MAX_LENGTH));
        } else {
            record.setResult(1);
        }
        // 记录操作功能
        record.setNamespace(desc);
        // 记录备注
        if (!Cools.isEmpty(ol.comments())) {
            record.setMemo(ol.comments());
        }
        // 记录请求参数
        if (ol.param() && request != null) {
            record.setRequest(Utils.sub(getParams(joinPoint, request), MAX_LENGTH));
        }
        // 记录请求结果
        if (ol.result() && result != null) {
            record.setResponse(Utils.sub(JSON.toJSONString(result), MAX_LENGTH));
        }
        apiForeignLogService.saveAsync(record);
    }
    /**
     * 获取操作功能
     *
     * @param method Method
     * @param ol     OperationLog
     * @return String
     */
    private String getDescription(Method method, OperationLog ol) {
        if (!Cools.isEmpty(ol.value())) {
            return ol.value();
        }
        return null;
    }
    /**
     * 获取请求参数
     *
     * @param joinPoint JoinPoint
     * @param request   HttpServletRequest
     * @return String
     */
    private String getParams(JoinPoint joinPoint, HttpServletRequest request) {
        String params;
        Map<String, String> paramsMap = new HashMap<>();
        Map<String, String[]> map = Collections.unmodifiableMap(request.getParameterMap());
        for (Map.Entry<String, String[]> entry : map.entrySet()) {
            paramsMap.put(entry.getKey(), Utils.join(entry.getValue(), ","));
        }
        if (paramsMap.keySet().size() > 0) {
            params = JSON.toJSONString(paramsMap);
        } else {
            StringBuilder sb = new StringBuilder();
            for (Object arg : joinPoint.getArgs()) {
                if (null == arg
                        || arg instanceof MultipartFile
                        || arg instanceof HttpServletRequest
                        || arg instanceof HttpServletResponse) {
                    continue;
                }
                sb.append(JSON.toJSONString(arg)).append(" ");
            }
            params = sb.toString();
        }
        return params;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/ApiSecurityConfig.java
New file
@@ -0,0 +1,36 @@
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安全配置类
 * 用于注册API认证过滤器
 */
@Configuration
public class ApiSecurityConfig {
    @Resource
    private AppIdAuthenticationFilter appIdAuthenticationFilter;
    /**
     * 注册API认证过滤器(支持AppId/AppSecret和Token认证)
     *
     * @return 过滤器注册Bean
     */
    @Bean
    public FilterRegistrationBean<AppIdAuthenticationFilter> apiAuthenticationFilter() {
        FilterRegistrationBean<AppIdAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(appIdAuthenticationFilter);
        registrationBean.addUrlPatterns("/api/*", "/erp/*", "/mes/*", "/agv/*"); // 拦截API请求、ERP请求、MES请求、管理AGV任务请求
        registrationBean.setName("apiAuthenticationFilter");
        registrationBean.setOrder(1); // 设置过滤器优先级
        return registrationBean;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/config/WebMvcConfig.java
@@ -2,7 +2,9 @@
import com.vincent.rsf.openApi.entity.constant.Constants;
import com.vincent.rsf.openApi.security.filter.AppIdAuthenticationFilter;
import com.vincent.rsf.openApi.utils.Http;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
@@ -23,36 +25,15 @@
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private AppIdAuthenticationFilter appIdAuthenticationFilter;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getAsyncHandlerInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**","/erp/**", "/v2/**","/v3/**","/doc.html/**", "/swagger-ui.html/**");
    }
    @Bean
    public AsyncHandlerInterceptor getAsyncHandlerInterceptor() {
        return new AsyncHandlerInterceptor(){
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                Http.cors(response);
                return true;
            }
        };
    }
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
    @Override
    public void addCorsMappings(CorsRegistry registry) {
@@ -66,13 +47,14 @@
    }
    public static void cors(HttpServletResponse response){
        // 跨域设置
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Expose-Headers", Constants.TOKEN_HEADER_NAME);
    @Bean
    public AsyncHandlerInterceptor getAsyncHandlerInterceptor() {
        return new AsyncHandlerInterceptor(){
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                Http.cors(response);
                return true;
            }
        };
    }
}
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/AuthController.java
New file
@@ -0,0 +1,174 @@
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.AppAuthParam;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
 * App认证控制器
 *
 * 提供AppId和AppSecret的登录认证功能
 *
 * @author vincent
 * @since 2026-01-05
 */
@RestController
//@RequestMapping("/auth")
@Api(tags = "应用认证管理")
@Slf4j
public class AuthController {
    // 开启模拟数据
    @Value("${foreign.api.data.simulated}")
    public static String SIMULATED_DATA_ENABLE = "1";
    @Resource
    private AppAuthService appAuthService;
    /**
     * 获取App认证Token
     *
     * @param param 应用ID和应用密钥
     * @return 认证Token
     */
    @ApiOperation("获取App认证Token")
    @PostMapping("/getToken")
    public CommonResponse getToken(@RequestBody AppAuthParam param) {
        String appId = param.getAppId();
        String appSecret = param.getAppSecret();
        if (Cools.isEmpty(appId, appSecret)) {
            return CommonResponse.error("AppId和AppSecret不能为空");
        }
        boolean isValid = appAuthService.validateApp(appId, appSecret);
        if (isValid) {
            String token = Constants.TOKEN_PREFIX + TokenUtils.generateToken(appId, appSecret);  //appAuthService.generateAppToken(appId, appSecret);
            return CommonResponse.ok()
                    .setMsg("获取Token成功")
                    .setData(token);
        } else {
            return CommonResponse.error("AppId或AppSecret无效");
        }
    }
//    /**
//     * 验证Token的接口
//     *
//     * @param token 要验证的Token
//     * @return Token验证结果
//     */
//    @PostMapping("/validateToken")
//    public Map<String, Object> validateToken(@RequestHeader(name = "authorization") String token) {
//        log.info("验证Token: {}", token.substring(0, Math.min(10, token.length())) + "...");
//
//        boolean isValid = TokenUtils.validateToken(token);
//
//        Map<String, Object> response = new HashMap<>();
//        response.put("code", "200");
//        response.put("message", isValid ? "Token有效" : "Token无效");
//        response.put("data", Map.of(
//                "valid", isValid,
//                "appId", isValid ? TokenUtils.getAppIdFromToken(token) : null,
//                "userId", isValid ? TokenUtils.getUserIdFromToken(token) : null
//        ));
//        response.put("success", isValid);
//
//        return response;
//    }
//    /**
//     * AppId和AppSecret登录认证
//     *
//     * @param param 认证参数
//     * @return 认证结果
//     */
//    @ApiOperation("AppId和AppSecret登录认证")
//    @PostMapping("/login")
//    public CommonResponse login(@RequestBody AppAuthParam param) {
//        String appId = param.getAppId();
//        String appSecret = param.getAppSecret();
//
//        if (Cools.isEmpty(appId, appSecret)) {
//            return CommonResponse.error("AppId和AppSecret不能为空");
//        }
//
//        boolean isValid = appAuthService.validateApp(appId, appSecret);
//        if (isValid) {
//            // 生成Token
//            String token = appAuthService.generateAppToken(appId, appSecret);
//            return CommonResponse.ok()
//                    .setMsg("登录成功")
//                    .setData(token);
//        } else {
//            return CommonResponse.error("AppId或AppSecret无效");
//        }
//    }
//
//
//
//    /**
//     * 验证App认证
//     *
//     * @param request HTTP请求
//     * @return 验证结果
//     */
//    @ApiOperation("验证App认证")
//    @PostMapping("/validate")
//    public CommonResponse validate(HttpServletRequest request) {
//        String appId = request.getHeader(Constants.HEADER_APP_ID);
//        String appSecret = request.getHeader(Constants.HEADER_APP_SECRET);
//
//        if (Cools.isEmpty(appId, appSecret)) {
//            return CommonResponse.error("缺少AppId或AppSecret");
//        }
//
//        boolean isValid = appAuthService.validateApp(appId, appSecret);
//        if (isValid) {
//            return CommonResponse.ok()
//                    .setMsg("验证成功")
//                    .setData(appAuthService.getAppInfo(appId));
//        } else {
//            return CommonResponse.error("验证失败");
//        }
//    }
//
//    /**
//     * 获取当前认证的App信息
//     *
//     * @param request HTTP请求
//     * @return App信息
//     */
//    @ApiOperation("获取当前认证的App信息")
//    @GetMapping("/info")
//    public CommonResponse getAppInfo(HttpServletRequest request) {
//        String appId = (String) request.getAttribute("APP_ID");
//        if (appId == null) {
//            appId = request.getHeader(Constants.HEADER_APP_ID);
//        }
//
//        if (appId == null) {
//            return CommonResponse.error("未找到AppId");
//        }
//
//        var appInfo = appAuthService.getAppInfo(appId);
//        if (appInfo != null) {
//            return CommonResponse.ok()
//                    .setMsg("获取App信息成功")
//                    .setData(appInfo);
//        } else {
//            return CommonResponse.error("未找到App信息");
//        }
//    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/TaskController.java
New file
@@ -0,0 +1,59 @@
package com.vincent.rsf.openApi.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.phyz.Task;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
import static com.vincent.rsf.openApi.controller.AuthController.SIMULATED_DATA_ENABLE;
import static com.vincent.rsf.openApi.controller.phyz.ERPController.paramsFormat;
@RestController
@Api("任务管理接口")
@Slf4j
public class TaskController {
    @ApiOperation("点对点创建AGV搬运任务")
    @PostMapping("/agv/transTask/add")
    public CommonResponse addAgvTask(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Task> tasks = JSON.parseArray(params.toJSONString(), Task.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("点对点取消AGV搬运任务")
    @PostMapping("/agv/transTask/cancel")
    public CommonResponse cancelAgvTask(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Task> tasks = JSON.parseArray(params.toJSONString(), Task.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
}
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("/erp1")
@Api("ERP接口对接")
public class WmsErpController {
@@ -46,7 +46,7 @@
     * @return
     */
    @ApiOperation("单据修改")
    @PostMapping("/order/upadte")
    @PostMapping("/order/update")
    public CommonResponse modifyOrderDtel(@RequestBody ErpOpParams params) {
        if (Objects.isNull(params)) {
            throw new CoolException("参数不能为空!!");
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsRcsController.java
@@ -3,6 +3,7 @@
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.dto.SyncLocsDto;
import com.vincent.rsf.openApi.entity.params.ExMsgCallbackParams;
import com.vincent.rsf.openApi.entity.params.RcsPubTaskParams;
import com.vincent.rsf.openApi.entity.params.SyncRcsLocsParam;
@@ -15,6 +16,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -74,11 +76,12 @@
     */
    @ApiOperation("RCS库位信息同步")
    @PostMapping("/sync/locs")
    public R syncLocsToWms(@RequestBody SyncRcsLocsParam params) {
    public CommonResponse syncLocsToWms(@RequestBody SyncRcsLocsParam params) {
         if (Objects.isNull(params)) {
             return R.error("参数不能为空!!");
             throw new CoolException("参数不能为空!!");
         }
         return R.ok().add(wmsRcsService.syncLocs(params));
         List<SyncLocsDto> result = wmsRcsService.syncLocs(params);
         return CommonResponse.ok(result);
    }
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/ApiAuthExampleController.java
New file
@@ -0,0 +1,96 @@
package com.vincent.rsf.openApi.controller.example;
import com.vincent.rsf.openApi.entity.constant.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
 * API认证示例控制器
 * 演示如何使用统一的认证机制(支持AppId/AppSecret和Token)
 */
@RestController
@RequestMapping("/api/example/auth")
public class ApiAuthExampleController {
    private static final Logger log = LoggerFactory.getLogger(ApiAuthExampleController.class);
    /**
     * 获取受保护的数据 - 支持AppId/AppSecret或Token认证
     *
     * @param request HTTP请求
     * @return 受保护的数据
     */
    @GetMapping("/protected-data")
    public Map<String, Object> getProtectedData(HttpServletRequest request) {
        // 从请求属性中获取认证信息(由AppIdAuthenticationFilter设置)
        String appId = (String) request.getAttribute(Constants.REQUEST_ATTR_APP_ID);
        String userId = (String) request.getAttribute(Constants.REQUEST_ATTR_USER_ID);
        log.info("访问受保护接口,AppId: {}, UserId: {}", appId, userId);
        Map<String, Object> response = new HashMap<>();
        response.put("code", "200");
        response.put("message", "访问成功");
        response.put("data", Map.of(
            "appId", appId,
            "userId", userId,
            "protectedInfo", "这是受保护的数据",
            "authType", userId != null ? "Token" : "AppId/AppSecret",
            "timestamp", System.currentTimeMillis()
        ));
        response.put("success", true);
        return response;
    }
    /**
     * 获取认证信息 - 支持AppId/AppSecret或Token认证
     *
     * @param request HTTP请求
     * @return 认证信息
     */
    @GetMapping("/auth-info")
    public Map<String, Object> getAuthInfo(HttpServletRequest request) {
        // 从请求属性中获取认证信息
        String appId = (String) request.getAttribute(Constants.REQUEST_ATTR_APP_ID);
        String userId = (String) request.getAttribute(Constants.REQUEST_ATTR_USER_ID);
        log.info("获取认证信息,AppId: {}, UserId: {}", appId, userId);
        Map<String, Object> response = new HashMap<>();
        response.put("code", "200");
        response.put("message", "获取认证信息成功");
        response.put("data", Map.of(
            "appId", appId,
            "userId", userId,
            "authType", userId != null ? "Token" : "AppId/AppSecret",
            "authenticated", appId != null
        ));
        response.put("success", true);
        return response;
    }
    /**
     * 测试接口 - 不需要认证
     *
     * @return 测试数据
     */
    @GetMapping("/public-test")
    public Map<String, Object> getPublicTest() {
        Map<String, Object> response = new HashMap<>();
        response.put("code", "200");
        response.put("message", "公开接口访问成功");
        response.put("data", Map.of(
            "info", "这是一个不需要认证的公开接口",
            "timestamp", System.currentTimeMillis()
        ));
        response.put("success", true);
        return response;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/AppAuthExampleController.java
New file
@@ -0,0 +1,98 @@
package com.vincent.rsf.openApi.controller.example;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.security.utils.AuthUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
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;
/**
 * App认证使用示例控制器
 *
 * 演示如何在控制器中使用AppId认证
 *
 * @author vincent
 * @since 2026-01-05
 */
@RestController
@RequestMapping("/example/auth")
@Api(tags = "App认证使用示例")
public class AppAuthExampleController {
    /**
     * 需要App认证的接口示例
     *
     * @param request HTTP请求
     * @return 响应结果
     */
    @ApiOperation("需要App认证的接口示例")
    @GetMapping("/protected")
    public CommonResponse protectedEndpoint(HttpServletRequest request) {
        // 获取认证的AppId
        String appId = AuthUtils.getAppId(request);
        // 检查是否已认证
        if (appId == null) {
            return CommonResponse.error("未通过App认证");
        }
        return CommonResponse.ok()
                .setMsg("访问成功")
                .setData("认证的AppId: " + appId);
    }
    /**
     * 获取当前认证的App信息
     *
     * @param request HTTP请求
     * @return App信息
     */
    @ApiOperation("获取当前认证的App信息")
    @GetMapping("/app-info")
    public CommonResponse getAppInfo(HttpServletRequest request) {
        String appId = AuthUtils.getAppId(request);
        if (appId == null) {
            return CommonResponse.error("未通过App认证");
        }
        return CommonResponse.ok()
                .setMsg("获取App信息成功")
                .setData("当前AppId: " + appId);
    }
    /**
     * 无需认证的公开接口
     *
     * @return 响应结果
     */
    @ApiOperation("无需认证的公开接口")
    @GetMapping("/public")
    public CommonResponse publicEndpoint() {
        return CommonResponse.ok()
                .setMsg("公开接口访问成功")
                .setData("任何人都可以访问此接口");
    }
    /**
     * 检查认证状态
     *
     * @param request HTTP请求
     * @return 认证状态
     */
    @ApiOperation("检查认证状态")
    @PostMapping("/check-auth")
    public CommonResponse checkAuth(HttpServletRequest request) {
        boolean isAuthenticated = AuthUtils.isAuthenticated(request);
        String appId = AuthUtils.getAppId(request);
        return CommonResponse.ok()
                .setMsg("认证检查完成")
                .setData("isAuthenticated: " + isAuthenticated + ", appId: " + appId);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/FieldMappingExampleController.java
New file
@@ -0,0 +1,195 @@
package com.vincent.rsf.openApi.controller.example;
import com.alibaba.fastjson.JSONObject;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
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;
/**
 * 接口字段映射使用示例
 *
 * 展示如何在实际业务中使用 FuncMap 进行字段映射
 *
 * @author vincent
 * @since 2026-01-04
 */
@Slf4j
@RestController
@RequestMapping("/example/mapping")
@Api(tags = "字段映射使用示例")
public class FieldMappingExampleController {
    /**
     * 示例1:ERP订单接收 - 字段映射
     *
     * 场景:接收ERP系统的订单数据,字段名与WMS不一致,需要进行映射
     *
     * ERP字段 -> WMS字段:
     * orderNumber -> code
     * orderQty -> qty
     * orderAmount -> anfme
     */
    @ApiOperation("示例:ERP订单接收")
    @PostMapping("/erp/order/receive")
    public CommonResponse erpOrderReceive(@RequestBody JSONObject requestData) {
        log.info("接收到ERP订单数据(映射前):{}", requestData);
        // 执行字段映射
        String appId = "ERP_SYSTEM";
        String funcId = "ORDER_SYNC";
        JSONObject mappedData = ParamsMapUtils.apiMaps(appId, funcId, requestData);
        log.info("字段映射后的数据:{}", mappedData);
        // 这里可以继续调用WMS内部的业务逻辑处理映射后的数据
        // orderService.createOrder(mappedData);
        return CommonResponse.ok()
                .setMsg("订单接收成功")
                .setData(mappedData);
    }
    /**
     * 示例2:物料信息同步 - 字段映射
     *
     * 场景:接收ERP系统的物料数据
     *
     * ERP字段 -> WMS字段:
     * materialCode -> matnr
     * materialName -> maktx
     * materialSpec -> spec
     */
    @ApiOperation("示例:物料信息同步")
    @PostMapping("/erp/material/sync")
    public CommonResponse materialSync(@RequestBody JSONObject requestData) {
        log.info("接收到物料数据(映射前):{}", requestData);
        // 执行字段映射
        String appId = "ERP_SYSTEM";
        String funcId = "MATNR_SYNC";
        JSONObject mappedData = ParamsMapUtils.apiMaps(appId, funcId, requestData);
        log.info("字段映射后的数据:{}", mappedData);
        return CommonResponse.ok()
                .setMsg("物料同步成功")
                .setData(mappedData);
    }
    /**
     * 示例3:WCS任务创建 - 字段映射
     *
     * 场景:接收WCS系统的任务创建请求
     *
     * WCS字段 -> WMS字段:
     * containerCode -> barcode
     * stationCode -> sourceStaNo
     * taskType -> ioType
     */
    @ApiOperation("示例:WCS任务创建")
    @PostMapping("/wcs/task/create")
    public CommonResponse wcsTaskCreate(@RequestBody JSONObject requestData) {
        log.info("接收到WCS任务数据(映射前):{}", requestData);
        // 执行字段映射
        String appId = "WCS_SYSTEM";
        String funcId = "TASK_CREATE";
        JSONObject mappedData = ParamsMapUtils.apiMaps(appId, funcId, requestData);
        log.info("字段映射后的数据:{}", mappedData);
        return CommonResponse.ok()
                .setMsg("任务创建成功")
                .setData(mappedData);
    }
    /**
     * 示例4:批量数据映射
     *
     * 场景:批量接收订单明细,需要对每条明细进行字段映射
     */
    @ApiOperation("示例:批量订单明细映射")
    @PostMapping("/erp/order/batch")
    public CommonResponse batchOrderMapping(@RequestBody JSONObject requestData) {
        log.info("接收到批量订单数据");
        String appId = "ERP_SYSTEM";
        String funcId = "ORDER_SYNC";
        // 映射订单头
        JSONObject orderHeader = requestData.getJSONObject("header");
        JSONObject mappedHeader = ParamsMapUtils.apiMaps(appId, funcId, orderHeader);
        // 映射订单明细列表
        com.alibaba.fastjson.JSONArray items = requestData.getJSONArray("items");
        com.alibaba.fastjson.JSONArray mappedItems = new com.alibaba.fastjson.JSONArray();
        if (items != null) {
            for (int i = 0; i < items.size(); i++) {
                JSONObject item = items.getJSONObject(i);
                JSONObject mappedItem = ParamsMapUtils.apiMaps(appId, funcId, item);
                mappedItems.add(mappedItem);
            }
        }
        // 组装结果
        JSONObject result = new JSONObject();
        result.put("header", mappedHeader);
        result.put("items", mappedItems);
        log.info("批量映射完成,共处理 {} 条明细", mappedItems.size());
        return CommonResponse.ok()
                .setMsg("批量处理成功")
                .setData(result);
    }
    /**
     * 示例5:条件映射
     *
     * 场景:根据不同的应用来源,使用不同的映射规则
     */
    @ApiOperation("示例:条件映射")
    @PostMapping("/dynamic/mapping")
    public CommonResponse dynamicMapping(@RequestBody JSONObject requestData) {
        // 从请求中获取应用标识
        String source = requestData.getString("source");
        String funcId = requestData.getString("function");
        // 移除元数据字段
        requestData.remove("source");
        requestData.remove("function");
        log.info("动态映射 - 来源:{},功能:{}", source, funcId);
        // 根据来源选择不同的映射规则
        String appId;
        switch (source) {
            case "ERP":
                appId = "ERP_SYSTEM";
                break;
            case "WCS":
                appId = "WCS_SYSTEM";
                break;
            case "MES":
                appId = "MES_SYSTEM";
                break;
            default:
                return CommonResponse.error("未知的数据源:" + source);
        }
        // 执行映射
        JSONObject mappedData = ParamsMapUtils.apiMaps(appId, funcId, requestData);
        return CommonResponse.ok()
                .setMsg("动态映射成功")
                .setData(mappedData);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/JsonReplaceExampleController.java
New file
@@ -0,0 +1,326 @@
package com.vincent.rsf.openApi.controller.example;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
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;
/**
 * JSON属性名递归替换使用示例
 *
 * 演示如何使用 FuncMap 递归遍历并替换JSON所有层级的属性名称
 *
 * @author vincent
 * @since 2026-01-04
 */
@Slf4j
@RestController
@RequestMapping("/example/json-replace")
@Api(tags = "JSON属性递归替换示例")
public class JsonReplaceExampleController {
    /**
     * 示例1:简单对象的属性替换
     *
     * 输入:
     * {
     *   "orderNumber": "PO001",
     *   "orderQty": 100,
     *   "orderAmount": 5000.00
     * }
     *
     * 输出:
     * {
     *   "code": "PO001",
     *   "qty": 100,
     *   "anfme": 5000.00
     * }
     */
    @ApiOperation("示例1:简单对象属性替换")
    @PostMapping("/simple-replace")
    public CommonResponse simpleReplace(@RequestBody JSONObject data) {
        log.info("原始数据:{}", data);
        // 定义映射规则
        Map<String, String> mappingRules = new HashMap<>();
        mappingRules.put("orderNumber", "code");
        mappingRules.put("orderQty", "qty");
        mappingRules.put("orderAmount", "anfme");
        // 执行替换
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, mappingRules);
        log.info("替换后数据:{}", result);
        return CommonResponse.ok()
                .setMsg("替换成功")
                .setData(result);
    }
    /**
     * 示例2:嵌套对象的深度替换
     *
     * 输入:
     * {
     *   "orderNumber": "PO001",
     *   "customer": {
     *     "customerName": "张三",
     *     "customerPhone": "13800138000",
     *     "address": {
     *       "cityName": "北京",
     *       "streetName": "朝阳路"
     *     }
     *   }
     * }
     *
     * 输出:
     * {
     *   "code": "PO001",
     *   "customer": {
     *     "name": "张三",
     *     "phone": "13800138000",
     *     "address": {
     *       "city": "北京",
     *       "street": "朝阳路"
     *     }
     *   }
     * }
     */
    @ApiOperation("示例2:嵌套对象深度替换")
    @PostMapping("/nested-replace")
    public CommonResponse nestedReplace(@RequestBody JSONObject data) {
        log.info("原始嵌套数据:{}", data);
        // 定义映射规则(适用于所有层级)
        Map<String, String> mappingRules = new HashMap<>();
        mappingRules.put("orderNumber", "code");
        mappingRules.put("customerName", "name");
        mappingRules.put("customerPhone", "phone");
        mappingRules.put("cityName", "city");
        mappingRules.put("streetName", "street");
        // 递归替换所有层级
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, mappingRules);
        log.info("替换后嵌套数据:{}", result);
        return CommonResponse.ok()
                .setMsg("嵌套替换成功")
                .setData(result);
    }
    /**
     * 示例3:数组对象的批量替换
     *
     * 输入:
     * {
     *   "orderNumber": "PO001",
     *   "items": [
     *     {
     *       "materialCode": "MAT001",
     *       "materialName": "物料A",
     *       "itemQty": 10
     *     },
     *     {
     *       "materialCode": "MAT002",
     *       "materialName": "物料B",
     *       "itemQty": 20
     *     }
     *   ]
     * }
     *
     * 输出:
     * {
     *   "code": "PO001",
     *   "items": [
     *     {
     *       "matnr": "MAT001",
     *       "maktx": "物料A",
     *       "qty": 10
     *     },
     *     {
     *       "matnr": "MAT002",
     *       "maktx": "物料B",
     *       "qty": 20
     *     }
     *   ]
     * }
     */
    @ApiOperation("示例3:数组对象批量替换")
    @PostMapping("/array-replace")
    public CommonResponse arrayReplace(@RequestBody JSONObject data) {
        log.info("原始数组数据:{}", data);
        // 定义映射规则
        Map<String, String> mappingRules = new HashMap<>();
        mappingRules.put("orderNumber", "code");
        mappingRules.put("materialCode", "matnr");
        mappingRules.put("materialName", "maktx");
        mappingRules.put("itemQty", "qty");
        // 递归替换(包括数组中的所有对象)
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, mappingRules);
        log.info("替换后数组数据:{}", result);
        return CommonResponse.ok()
                .setMsg("数组替换成功")
                .setData(result);
    }
    /**
     * 示例4:复杂结构的完整替换
     *
     * 输入包含:嵌套对象 + 数组 + 多层嵌套
     */
    @ApiOperation("示例4:复杂结构完整替换")
    @PostMapping("/complex-replace")
    public CommonResponse complexReplace(@RequestBody JSONObject data) {
        log.info("原始复杂数据:{}", data);
        // 定义完整的映射规则
        Map<String, String> mappingRules = new HashMap<>();
        // 订单字段
        mappingRules.put("orderNumber", "code");
        mappingRules.put("orderType", "type");
        mappingRules.put("orderQty", "qty");
        mappingRules.put("orderAmount", "anfme");
        mappingRules.put("orderStatus", "exceStatus");
        // 客户字段
        mappingRules.put("customerName", "custName");
        mappingRules.put("customerCode", "custCode");
        // 物料字段
        mappingRules.put("materialCode", "matnr");
        mappingRules.put("materialName", "maktx");
        mappingRules.put("materialSpec", "spec");
        mappingRules.put("materialUnit", "meins");
        // 其他字段
        mappingRules.put("warehouseCode", "whCode");
        mappingRules.put("locationCode", "locCode");
        // 执行递归替换
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, mappingRules);
        log.info("替换后复杂数据:{}", result);
        return CommonResponse.ok()
                .setMsg("复杂替换成功")
                .setData(result);
    }
    /**
     * 示例5:使用appId和funcId自动映射
     *
     * 这个示例展示如何结合数据库配置自动进行字段映射
     */
    @ApiOperation("示例5:使用配置自动映射")
    @PostMapping("/auto-replace")
    public CommonResponse autoReplace(@RequestBody JSONObject request) {
        String appId = request.getString("appId");
        String funcId = request.getString("funcId");
        JSONObject data = request.getJSONObject("data");
        log.info("自动映射 - appId:{}, funcId:{}, 数据:{}", appId, funcId, data);
        // 使用 FuncMap 的 apiMaps 方法,它会自动从数据库加载映射规则并递归替换
        JSONObject result = ParamsMapUtils.apiMaps(appId, funcId, data);
        log.info("自动映射后数据:{}", result);
        return CommonResponse.ok()
                .setMsg("自动映射成功")
                .setData(result);
    }
    /**
     * 示例6:直接替换JSON数组
     */
    @ApiOperation("示例6:直接替换JSON数组")
    @PostMapping("/array-direct-replace")
    public CommonResponse arrayDirectReplace(@RequestBody JSONArray array) {
        log.info("原始JSON数组:{}", array);
        // 定义映射规则
        Map<String, String> mappingRules = new HashMap<>();
        mappingRules.put("materialCode", "matnr");
        mappingRules.put("materialName", "maktx");
        mappingRules.put("stockQty", "qty");
        mappingRules.put("warehouseCode", "whCode");
        // 直接替换数组
        JSONArray result = ParamsMapUtils.replaceJsonArrayKeys(array, mappingRules);
        log.info("替换后JSON数组:{}", result);
        return CommonResponse.ok()
                .setMsg("数组直接替换成功")
                .setData(result);
    }
    /**
     * 测试用例:生成示例数据
     */
    @ApiOperation("生成测试数据")
    @PostMapping("/generate-test-data")
    public CommonResponse generateTestData() {
        // 创建复杂的测试数据
        JSONObject testData = new JSONObject();
        testData.put("orderNumber", "PO20260104001");
        testData.put("orderType", "PURCHASE");
        testData.put("orderQty", 500);
        testData.put("orderAmount", 25000.00);
        testData.put("orderStatus", "PENDING");
        // 客户信息
        JSONObject customer = new JSONObject();
        customer.put("customerCode", "CUST001");
        customer.put("customerName", "北京科技有限公司");
        JSONObject address = new JSONObject();
        address.put("cityName", "北京");
        address.put("districtName", "朝阳区");
        address.put("streetName", "建国路88号");
        customer.put("address", address);
        testData.put("customer", customer);
        // 订单明细
        JSONArray items = new JSONArray();
        for (int i = 1; i <= 3; i++) {
            JSONObject item = new JSONObject();
            item.put("itemNo", i);
            item.put("materialCode", "MAT00" + i);
            item.put("materialName", "测试物料" + i);
            item.put("materialSpec", "规格" + i);
            item.put("materialUnit", "EA");
            item.put("itemQty", 100 + i * 50);
            item.put("itemAmount", 5000.00 + i * 1000);
            // 仓库信息
            JSONObject warehouse = new JSONObject();
            warehouse.put("warehouseCode", "WH0" + i);
            warehouse.put("locationCode", "LOC-A-0" + i);
            item.put("warehouse", warehouse);
            items.add(item);
        }
        testData.put("items", items);
        return CommonResponse.ok()
                .setMsg("测试数据生成成功")
                .setData(testData);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/example/TokenAuthExampleController.java
New file
@@ -0,0 +1,182 @@
package com.vincent.rsf.openApi.controller.example;
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.AppAuthParam;
import com.vincent.rsf.openApi.security.service.AppAuthService;
import com.vincent.rsf.openApi.security.utils.TokenUtils;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
 * Token认证示例控制器
 * 演示如何使用JWT Token进行接口保护
 */
@RestController
@RequestMapping("/api/example/token")
public class TokenAuthExampleController {
    private static final Logger log = LoggerFactory.getLogger(TokenAuthExampleController.class);
    @Resource
    private AppAuthService appAuthService;
    /**
     * 获取受保护的数据 - 需要有效的Token
     *
     * @param request HTTP请求
     * @return 受保护的数据
     */
    @GetMapping("/protected-data")
    public Map<String, Object> getProtectedData(HttpServletRequest request) {
        // 从请求属性中获取认证信息(由TokenAuthenticationFilter设置)
        String appId = (String) request.getAttribute(Constants.REQUEST_ATTR_APP_ID);
        String userId = (String) request.getAttribute(Constants.REQUEST_ATTR_USER_ID);
        log.info("访问受保护接口,AppId: {}, UserId: {}", appId, userId);
        Map<String, Object> response = new HashMap<>();
        response.put("code", "200");
        response.put("message", "访问成功");
        response.put("data", Map.of(
            "appId", appId,
            "userId", userId,
            "protectedInfo", "这是受保护的数据",
            "timestamp", System.currentTimeMillis()
        ));
        response.put("success", true);
        return response;
    }
    /**
     * 获取用户信息 - 需要有效的Token
     *
     * @param request HTTP请求
     * @return 用户信息
     */
    @GetMapping("/user-info")
    public Map<String, Object> getUserInfo(HttpServletRequest request) {
        // 从请求属性中获取认证信息
        String appId = (String) request.getAttribute(Constants.REQUEST_ATTR_APP_ID);
        String userId = (String) request.getAttribute(Constants.REQUEST_ATTR_USER_ID);
        log.info("获取用户信息,AppId: {}, UserId: {}", appId, userId);
        Map<String, Object> response = new HashMap<>();
        response.put("code", "200");
        response.put("message", "获取用户信息成功");
        response.put("data", Map.of(
            "appId", appId,
            "userId", userId,
            "userName", "用户" + (userId != null ? userId : "未知"),
            "role", "USER",
            "permissions", new String[]{"read", "write"}
        ));
        response.put("success", true);
        return response;
    }
    /**
     * 手动生成Token的示例接口
     * 注意:在实际应用中,这个接口通常需要其他形式的认证
     *
     * @param appId 应用ID
     * @param userId 用户ID
     * @return 包含Token的响应
     */
    @PostMapping("/generate-token")
    public Map<String, Object> generateToken(@RequestParam String appId, @RequestParam(required = false) String userId) {
        log.info("生成Token,AppId: {}, UserId: {}", appId, userId);
        try {
            // 生成Token
            String token = TokenUtils.generateToken(appId, userId);
            Map<String, Object> response = new HashMap<>();
            response.put("code", "200");
            response.put("message", "Token生成成功");
            response.put("data", Map.of(
                "token", token,
                "appId", appId,
                "userId", userId,
                "expiresIn", 24 * 60 * 60 // 24小时过期
            ));
            response.put("success", true);
            return response;
        } catch (Exception e) {
            log.error("生成Token失败", e);
            Map<String, Object> response = new HashMap<>();
            response.put("code", "500");
            response.put("message", "生成Token失败: " + e.getMessage());
            response.put("data", null);
            response.put("success", false);
            return response;
        }
    }
    /**
     * 获取App认证Token
     *
     * @param param 应用ID和应用密钥
     * @return 认证Token
     */
    @ApiOperation("获取App认证Token")
    @PostMapping("/getToken")
    public CommonResponse getToken(@RequestBody AppAuthParam param) {
        String appId = param.getAppId();
        String appSecret = param.getAppSecret();
        if (Cools.isEmpty(appId, appSecret)) {
            return CommonResponse.error("AppId和AppSecret不能为空");
        }
        boolean isValid = appAuthService.validateApp(appId, appSecret);
        if (isValid) {
            String token = appAuthService.generateAppToken(appId, appSecret);
            return CommonResponse.ok()
                    .setMsg("获取Token成功")
                    .setData(token);
        } else {
            return CommonResponse.error("AppId或AppSecret无效");
        }
    }
//    /**
//     * 验证Token的接口
//     *
//     * @param token 要验证的Token
//     * @return Token验证结果
//     */
//    @PostMapping("/validateToken")
//    public Map<String, Object> validateToken(@RequestParam String token) {
//        log.info("验证Token: {}", token.substring(0, Math.min(10, token.length())) + "...");
//
//        boolean isValid = TokenUtils.validateToken(token);
//
//        Map<String, Object> response = new HashMap<>();
//        response.put("code", "200");
//        response.put("message", isValid ? "Token有效" : "Token无效");
//        response.put("data", Map.of(
//                "valid", isValid,
//                "appId", isValid ? TokenUtils.getAppIdFromToken(token) : null,
//                "userId", isValid ? TokenUtils.getUserIdFromToken(token) : null
//        ));
//        response.put("success", isValid);
//
//        return response;
//    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/phyz/ERPController.java
New file
@@ -0,0 +1,284 @@
package com.vincent.rsf.openApi.controller.phyz;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.phyz.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
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.List;
import java.util.Objects;
import static com.vincent.rsf.openApi.controller.AuthController.SIMULATED_DATA_ENABLE;
@RestController
@RequestMapping("/erp")
@Api("银座新工厂(五期)ERP接口")
@Slf4j
public class ERPController {
    @ApiOperation("仓库信息同步")
    @PostMapping("/wareHouse/sync")
    public CommonResponse syncWareHouse(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Warehouse> warehouseList = JSON.parseArray(params.toJSONString(), Warehouse.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("物料信息同步")
    @PostMapping("/mat/sync")
    public CommonResponse syncMaterial(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Material> materialList = JSON.parseArray(params.toJSONString(), Material.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("客户信息同步")
    @PostMapping("/customer/sync")
    public CommonResponse syncCustomer(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Customer> customerList = JSON.parseArray(params.toJSONString(), Customer.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("供应商信息同步")
    @PostMapping("/supplier/sync")
    public CommonResponse syncSupplier(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Supplier> supplierList = JSON.parseArray(params.toJSONString(), Supplier.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("入/出库任务通知单")
    @PostMapping("/order/add")
    public CommonResponse addOrder(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Order> orderList = JSON.parseArray(params.toJSONString(), Order.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("入/出库任务通知单取消")
    @PostMapping("/order/cancel")
    public CommonResponse cancelOrder(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Order> orderList = JSON.parseArray(params.toJSONString(), Order.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("库存查询明细")
    @PostMapping("/inventory/details")
    public CommonResponse queryInventoryDetails(@RequestBody JSONObject params) {
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            String x = "[\n" +
                    "  {\n" +
                    "    \"locId\": \"LOC-A-01-01\",\n" +
                    "    \"wareHouseId\": \"WH001\",\n" +
                    "    \"wareHouseName\": \"原料仓库\",\n" +
                    "    \"palletId\": \"PALLET001\",\n" +
                    "    \"matNr\": \"MAT10001\",\n" +
                    "    \"makTx\": \"钢材Q235\",\n" +
                    "    \"spec\": \"国标GB/T700-2006\",\n" +
                    "    \"anfme\": 10.5,\n" +
                    "    \"unit\": \"吨\",\n" +
                    "    \"status\": \"可用\",\n" +
                    "    \"orderType\": 1,\n" +
                    "    \"orderNo\": \"Order202698921\",\n" +
                    "    \"prepareType\": 1,\n" +
                    "    \"planNo\": \"PLAN202601060001\",\n" +
                    "    \"batch\": \"BATCH20260106001\",\n" +
                    "    \"stockOrgId\": \"ORG001\"\n" +
                    "  },\n" +
                    "  {\n" +
                    "    \"locId\": \"LOC-B-02-03\",\n" +
                    "    \"wareHouseId\": \"WH002\",\n" +
                    "    \"wareHouseName\": \"成品仓库\",\n" +
                    "    \"palletId\": \"PALLET002\",\n" +
                    "    \"matNr\": \"MAT20001\",\n" +
                    "    \"makTx\": \"电机组件\",\n" +
                    "    \"spec\": \"380V 50Hz\",\n" +
                    "    \"anfme\": 50,\n" +
                    "    \"unit\": \"台\",\n" +
                    "    \"status\": \"可用\",\n" +
                    "    \"orderType\": \"1\",\n" +
                    "    \"orderNo\": \"SO202601060001\",\n" +
                    "    \"prepareType\": 1,\n" +
                    "    \"planNo\": \"PLAN202601060002\",\n" +
                    "    \"batch\": \"BATCH20260106002\",\n" +
                    "    \"stockOrgId\": \"ORG001\"\n" +
                    "  }\n" +
                    "]";
            return CommonResponse.ok(JSONArray.parseArray(x, InventoryDetails.class));
        }
        InventoryQueryCondition condition = JSON.parseObject(params.toJSONString(), InventoryQueryCondition.class);
        // 数据处理,转发server
        List<InventoryDetails> inventoryDetails = Lists.newArrayList();
        return new CommonResponse().setCode(200).setData(inventoryDetails);
    }
    @ApiOperation("库存查询汇总")
    @PostMapping("/inventory/summary")
    public CommonResponse queryInventorySummary(@RequestBody JSONObject params) {
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            String s = "{\n" +
                    "  \"code\": 200,\n" +
                    "  \"msg\": \"操作成功\",\n" +
                    "  \"data\": [\n" +
                    "    {\n" +
                    "      \"wareHouseId\": \"WH001\",\n" +
                    "      \"wareHouseName\": \"原料仓库\",\n" +
                    "      \"matNr\": \"MAT10001\",\n" +
                    "      \"makTx\": \"钢材Q235\",\n" +
                    "      \"spec\": \"国标GB/T700-2006\",\n" +
                    "      \"anfme\": 10.5,\n" +
                    "      \"unit\": \"吨\",\n" +
                    "      \"stockOrgId\": \"ORG001\",\n" +
                    "      \"batch\": \"BATCH20260106001\",\n" +
                    "      \"planNo\": \"Plan20260106006\"\n" +
                    "    },\n" +
                    "    {\n" +
                    "      \"wareHouseId\": \"WH001\",\n" +
                    "      \"wareHouseName\": \"原料仓库\",\n" +
                    "      \"matNr\": \"MAT10002\",\n" +
                    "      \"makTx\": \"铝型材6061\",\n" +
                    "      \"spec\": \"国标GB/T3190-2008\",\n" +
                    "      \"anfme\": 20.3,\n" +
                    "      \"unit\": \"吨\",\n" +
                    "      \"stockOrgId\": \"ORG001\",\n" +
                    "      \"batch\": \"BATCH20260106002\",\n" +
                    "      \"planNo\": \"Plan20260106005\"\n" +
                    "    },\n" +
                    "    {\n" +
                    "      \"wareHouseId\": \"WH002\",\n" +
                    "      \"wareHouseName\": \"成品仓库\",\n" +
                    "      \"matNr\": \"MAT30001\",\n" +
                    "      \"makTx\": \"电机成品\",\n" +
                    "      \"spec\": \"380V 50Hz 15KW\",\n" +
                    "      \"anfme\": 100,\n" +
                    "      \"unit\": \"台\",\n" +
                    "      \"stockOrgId\": \"ORG001\",\n" +
                    "      \"batch\": \"BATCH20260106003\",\n" +
                    "      \"planNo\": \"Plan20260106004\"\n" +
                    "    }\n" +
                    "  ]\n" +
                    "}";
            return JSONObject.parseObject(s, CommonResponse.class);
        }
        InventoryQueryCondition condition = JSON.parseObject(params.toJSONString(), InventoryQueryCondition.class);
        // 数据处理,转发server
        List<InventorySummary> inventorySummaries = Lists.newArrayList();
        return new CommonResponse().setCode(200).setData(inventorySummaries);
    }
    @ApiOperation("盘点结果确认")
    @PostMapping("/check/confirm")
    public CommonResponse checkConfirm(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        CheckOrder checkResult = JSON.parseObject(params.toJSONString(), CheckOrder.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    /**
     * 兼容JSONObject和JSONArray格式数据
     *
     * @param data json格式参数
     * @return JSONArray格式数据
     */
    public static JSONArray paramsFormat(Object data) {
        if (Objects.isNull(data)) {
            return new JSONArray();
        }
        try {
            String jsonStr = JSON.toJSONString(data);
            if (jsonStr.startsWith("[")) {
                return JSON.parseArray(jsonStr);
            } else if (jsonStr.startsWith("{")) {
                JSONArray params = new JSONArray();
                params.add(JSON.parseObject(jsonStr));
                return params;
            }
        } catch (Exception e) {
            // 解析失败,返回空数组
            log.error("转换参数为json格式错误", e);
        }
        // 默认返回空数组
        return new JSONArray();
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/phyz/MESController.java
New file
@@ -0,0 +1,94 @@
package com.vincent.rsf.openApi.controller.phyz;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.entity.phyz.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.List;
import java.util.Objects;
import static com.vincent.rsf.openApi.controller.AuthController.SIMULATED_DATA_ENABLE;
import static com.vincent.rsf.openApi.controller.phyz.ERPController.paramsFormat;
@RestController
@RequestMapping("/mes")
@Api("银座新工厂(五期)MES接口")
public class MESController {
    @ApiOperation("备料通知")
    @PostMapping("/callMaterial")
    public CommonResponse callMaterial(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("0")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<MatPreparationOrder> orders = JSON.parseArray(params.toJSONString(), MatPreparationOrder.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("简单生产领料、退料AGV任务")
    @PostMapping("/transTask/simpleProduction")
    public CommonResponse simpleProductionTask(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("0")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<SimpleProductionTask> tasks = JSON.parseArray(params.toJSONString(), SimpleProductionTask.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("空托出库")
    @PostMapping("/emptyTray/callOut")
    public CommonResponse callOutEmptyTray(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Task> tasks = JSON.parseArray(params.toJSONString(), Task.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
    @ApiOperation("空托入库")
    @PostMapping("/emptyTray/backIn")
    public CommonResponse emptyTrayBackIn(@RequestBody Object objParams) {
        if (Objects.isNull(objParams)) {
            throw new CoolException("参数不能为空!!");
        }
        // 返回模拟数据
        if (SIMULATED_DATA_ENABLE.equals("1")) {
            return CommonResponse.ok();
        }
        JSONArray params = paramsFormat(objParams);
        List<Task> tasks = JSON.parseArray(params.toJSONString(), Task.class);
        // 数据处理,转发server
        return CommonResponse.ok();
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/ApiFunctionController.java
New file
@@ -0,0 +1,100 @@
package com.vincent.rsf.openApi.controller.platform;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.openApi.entity.app.ApiFunction;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.service.ApiFunctionService;
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;
/**
 * ApiFunction管理Controller
 *
 * @author vincent
 * @since 2026-01-04
 */
@RestController
@RequestMapping("/api/function")
@Api(tags = "接口功能管理")
public class ApiFunctionController {
    @Autowired
    private ApiFunctionService apiFunctionService;
    @ApiOperation("分页查询接口功能列表")
    @GetMapping("/page")
    public CommonResponse page(@RequestParam(defaultValue = "1") Integer current,
                                @RequestParam(defaultValue = "10") Integer size) {
        Page<ApiFunction> page = apiFunctionService.page(new Page<>(current, size));
        return CommonResponse.ok().setData(page);
    }
    @ApiOperation("查询所有接口功能")
    @GetMapping("/list")
    public CommonResponse list() {
        List<ApiFunction> list = apiFunctionService.list();
        return CommonResponse.ok().setData(list);
    }
    @ApiOperation("根据ID查询接口功能")
    @GetMapping("/{id}")
    public CommonResponse getById(@PathVariable String id) {
        ApiFunction function = apiFunctionService.getById(id);
        return CommonResponse.ok().setData(function);
    }
    @ApiOperation("新增接口功能")
    @PostMapping
    public CommonResponse save(@RequestBody ApiFunction function) {
        boolean result = apiFunctionService.save(function);
        if (result) {
            apiFunctionService.refreshCache();
            return CommonResponse.ok().setMsg("新增成功");
        }
        return CommonResponse.error("新增失败");
    }
    @ApiOperation("更新接口功能")
    @PutMapping
    public CommonResponse update(@RequestBody ApiFunction function) {
        boolean result = apiFunctionService.updateById(function);
        if (result) {
            apiFunctionService.refreshCache();
            return CommonResponse.ok().setMsg("更新成功");
        }
        return CommonResponse.error("更新失败");
    }
    @ApiOperation("删除接口功能")
    @DeleteMapping("/{id}")
    public CommonResponse delete(@PathVariable String id) {
        boolean result = apiFunctionService.removeById(id);
        if (result) {
            apiFunctionService.refreshCache();
            return CommonResponse.ok().setMsg("删除成功");
        }
        return CommonResponse.error("删除失败");
    }
    @ApiOperation("批量删除接口功能")
    @DeleteMapping("/batch")
    public CommonResponse deleteBatch(@RequestBody List<String> ids) {
        boolean result = apiFunctionService.removeByIds(ids);
        if (result) {
            apiFunctionService.refreshCache();
            return CommonResponse.ok().setMsg("批量删除成功");
        }
        return CommonResponse.error("批量删除失败");
    }
    @ApiOperation("刷新功能缓存")
    @PostMapping("/refresh")
    public CommonResponse refresh() {
        apiFunctionService.refreshCache();
        return CommonResponse.ok().setMsg("缓存刷新成功");
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/ApiMapController.java
New file
@@ -0,0 +1,138 @@
package com.vincent.rsf.openApi.controller.platform;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.openApi.entity.app.ApiMap;
import com.vincent.rsf.openApi.entity.dto.CommonResponse;
import com.vincent.rsf.openApi.service.ApiMapService;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
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;
/**
 * ApiMap管理Controller
 *
 * @author vincent
 * @since 2026-01-04
 */
@RestController
@RequestMapping("/api/map")
@Api(tags = "字段映射管理")
public class ApiMapController {
    @Autowired
    private ApiMapService apiMapService;
    @ApiOperation("分页查询字段映射列表")
    @GetMapping("/page")
    public CommonResponse page(@RequestParam(defaultValue = "1") Integer current,
                                @RequestParam(defaultValue = "10") Integer size,
                                @RequestParam(required = false) String appId,
                                @RequestParam(required = false) String funcId) {
        LambdaQueryWrapper<ApiMap> wrapper = new LambdaQueryWrapper<>();
        if (appId != null && !appId.isEmpty()) {
            wrapper.eq(ApiMap::getAppId, appId);
        }
        if (funcId != null && !funcId.isEmpty()) {
            wrapper.eq(ApiMap::getFuncId, funcId);
        }
        Page<ApiMap> page = apiMapService.page(new Page<>(current, size), wrapper);
        return CommonResponse.ok().setData(page);
    }
    @ApiOperation("查询所有字段映射")
    @GetMapping("/list")
    public CommonResponse list(@RequestParam(required = false) String appId,
                                @RequestParam(required = false) String funcId) {
        LambdaQueryWrapper<ApiMap> wrapper = new LambdaQueryWrapper<>();
        if (appId != null && !appId.isEmpty()) {
            wrapper.eq(ApiMap::getAppId, appId);
        }
        if (funcId != null && !funcId.isEmpty()) {
            wrapper.eq(ApiMap::getFuncId, funcId);
        }
        List<ApiMap> list = apiMapService.list(wrapper);
        return CommonResponse.ok().setData(list);
    }
    @ApiOperation("根据ID查询字段映射")
    @GetMapping("/{id}")
    public CommonResponse getById(@PathVariable Integer id) {
        ApiMap map = apiMapService.getById(id);
        return CommonResponse.ok().setData(map);
    }
    @ApiOperation("新增字段映射")
    @PostMapping
    public CommonResponse save(@RequestBody ApiMap map) {
        boolean result = apiMapService.save(map);
        if (result) {
            apiMapService.refreshCache();
            return CommonResponse.ok().setMsg("新增成功");
        }
        return CommonResponse.error("新增失败");
    }
    @ApiOperation("批量新增字段映射")
    @PostMapping("/batch")
    public CommonResponse saveBatch(@RequestBody List<ApiMap> maps) {
        boolean result = apiMapService.saveBatch(maps);
        if (result) {
            apiMapService.refreshCache();
            return CommonResponse.ok().setMsg("批量新增成功");
        }
        return CommonResponse.error("批量新增失败");
    }
    @ApiOperation("更新字段映射")
    @PutMapping
    public CommonResponse update(@RequestBody ApiMap map) {
        boolean result = apiMapService.updateById(map);
        if (result) {
            apiMapService.refreshCache();
            return CommonResponse.ok().setMsg("更新成功");
        }
        return CommonResponse.error("更新失败");
    }
    @ApiOperation("删除字段映射")
    @DeleteMapping("/{id}")
    public CommonResponse delete(@PathVariable Integer id) {
        boolean result = apiMapService.removeById(id);
        if (result) {
            apiMapService.refreshCache();
            return CommonResponse.ok().setMsg("删除成功");
        }
        return CommonResponse.error("删除失败");
    }
    @ApiOperation("批量删除字段映射")
    @DeleteMapping("/batch")
    public CommonResponse deleteBatch(@RequestBody List<Integer> ids) {
        boolean result = apiMapService.removeByIds(ids);
        if (result) {
            apiMapService.refreshCache();
            return CommonResponse.ok().setMsg("批量删除成功");
        }
        return CommonResponse.error("批量删除失败");
    }
    @ApiOperation("刷新映射缓存")
    @PostMapping("/refresh")
    public CommonResponse refresh() {
        apiMapService.refreshCache();
        return CommonResponse.ok().setMsg("缓存刷新成功");
    }
//    @ApiOperation("刷新所有缓存")
//    @PostMapping("/refresh/all")
//    public CommonResponse refreshAll() {
//        new ParamsMapUtils().refreshAll();
//        return CommonResponse.ok().setMsg("所有缓存刷新成功");
//    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/platform/AppController.java
New file
@@ -0,0 +1,100 @@
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;
/**
 * App管理Controller
 *
 * @author vincent
 * @since 2026-01-04
 */
@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) {
        boolean result = appService.save(app);
        if (result) {
            appService.refreshCache();
            return CommonResponse.ok().setMsg("新增成功");
        }
        return CommonResponse.error("新增失败");
    }
    @ApiOperation("更新应用")
    @PutMapping
    public CommonResponse update(@RequestBody App app) {
        boolean result = appService.updateById(app);
        if (result) {
            appService.refreshCache();
            return CommonResponse.ok().setMsg("更新成功");
        }
        return CommonResponse.error("更新失败");
    }
    @ApiOperation("删除应用")
    @DeleteMapping("/{id}")
    public CommonResponse delete(@PathVariable String id) {
        boolean result = appService.removeById(id);
        if (result) {
            appService.refreshCache();
            return CommonResponse.ok().setMsg("删除成功");
        }
        return CommonResponse.error("删除失败");
    }
    @ApiOperation("批量删除应用")
    @DeleteMapping("/batch")
    public CommonResponse deleteBatch(@RequestBody List<String> ids) {
        boolean result = appService.removeByIds(ids);
        if (result) {
            appService.refreshCache();
            return CommonResponse.ok().setMsg("批量删除成功");
        }
        return CommonResponse.error("批量删除失败");
    }
    @ApiOperation("刷新应用缓存")
    @PostMapping("/refresh")
    public CommonResponse refresh() {
        appService.refreshCache();
        return CommonResponse.ok().setMsg("缓存刷新成功");
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/AppAuthParam.java
New file
@@ -0,0 +1,26 @@
package com.vincent.rsf.openApi.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * App认证参数
 *
 * 用于AppId和AppSecret的认证请求
 *
 * @author vincent
 * @since 2026-01-05
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "AppAuthParam", description = "App认证参数")
public class AppAuthParam {
    @ApiModelProperty(value = "应用ID", required = true)
    private String appId;
    @ApiModelProperty(value = "应用密钥", required = true)
    private String appSecret;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/ApiForeignLog.java
New file
@@ -0,0 +1,136 @@
package com.vincent.rsf.openApi.entity.app;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@TableName("open_api_foreign_log")
public class ApiForeignLog implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    /**
     * ID
     */
    @ApiModelProperty(value= "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 名称空间
     */
    @ApiModelProperty(value= "名称空间")
    private String namespace;
    /**
     * 接口地址
     */
    @ApiModelProperty(value= "接口地址")
    private String url;
    /**
     * 平台密钥
     */
    @ApiModelProperty(value= "平台密钥")
    private String appkey;
    /**
     * 时间戳
     */
    @ApiModelProperty(value= "时间戳")
    private String timestamp;
    /**
     * 客户端IP
     */
    @ApiModelProperty(value= "客户端IP")
    private String clientIp;
    /**
     * 请求内容
     */
    @ApiModelProperty(value= "请求内容")
    private String request;
    /**
     * 响应内容
     */
    @ApiModelProperty(value= "响应内容")
    private String response;
    /**
     * 消耗时间
     */
    @ApiModelProperty(value= "消耗时间")
    private Integer spendTime;
    /**
     * 异常内容
     */
    @ApiModelProperty(value= "异常内容")
    private String err;
    /**
     * 结果 1: 成功  0: 失败
     */
    @ApiModelProperty(value= "结果 1: 成功  0: 失败  ")
    private Integer result;
    /**
     * 用户
     */
    @ApiModelProperty(value= "用户")
    private Long userId;
    /**
     * 所属机构
     */
    @ApiModelProperty(value= "所属机构")
    private Long tenantId;
    /**
     * 添加时间
     */
    @ApiModelProperty(value= "添加时间")
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date createTime;
    /**
     * 备注
     */
    @ApiModelProperty(value= "备注")
    private String memo;
    public String getResult$(){
        if (null == this.result){ return null; }
        switch (this.result){
            case 1:
                return "成功";
            case 0:
                return "失败";
            default:
                return String.valueOf(this.result);
        }
    }
    public String getCreateTime$(){
        if (Cools.isEmpty(this.createTime)){
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/ApiFunction.java
New file
@@ -0,0 +1,48 @@
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.Serial;
import java.io.Serializable;
@Data
@TableName("open_api_function")
public class ApiFunction implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    /**
     * functionId
     */
    @ApiModelProperty(value= "functionId")
    @TableId(value = "id")
    private String id;
    /**
     * functionName
     */
    @ApiModelProperty(value = "functionName")
    private String name;
    /**
     * functionType,1 WMS接收;2 WMS发送;
     */
    @ApiModelProperty(value = "functionType")
    private Integer type;
    /**
     * functionUrl,对应rsf-server的controller接口相对路径
     */
    @ApiModelProperty(value = "functionUrl")
    private String url;
    /**
     * 租户id
     */
    @ApiModelProperty(value = "租户id")
    private Long tenant_id;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/ApiMap.java
New file
@@ -0,0 +1,78 @@
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.Serial;
import java.io.Serializable;
@Data
@TableName("open_api_attribute_map")
public class ApiMap implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    /**
     * id
     */
    @ApiModelProperty(value= "id")
    @TableId(value = "id")
    private Integer id;
    /**
     * appId
     */
    @ApiModelProperty(value= "appId")
    private String appId;
    /**
     * func_id
     */
    @ApiModelProperty(value= "func_id")
    private String funcId;
    /**
     * func_type
     */
    @ApiModelProperty(value= "func_type")
    private String funcType;
    /**
     * source_attribute
     */
    @ApiModelProperty(value = "source_attribute")
    private String sourceAttribute;
//    /**
//     * source_type,源属性类型,Integer、String、Double、JSONObject、JSONArray
//     */
//    @ApiModelProperty(value = "source_type")
//    private String sourceType;
    /**
     * target_attribute
     */
    @ApiModelProperty(value = "target_attribute")
    private String targetAttribute;
//    /**
//     * target_type,目标属性类型,Integer、String、Double、JSONObject、JSONArray
//     */
//    @ApiModelProperty(value = "target_type")
//    private String targetType;
    /**
     * 是否启用,0 未启用;1 启用;
     */
    @ApiModelProperty(value = "是否启用,0 未启用;1 启用;")
    private Integer enable;
    /**
     * 租户id
     */
    @ApiModelProperty(value = "租户id")
    private Long tenant_id;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/app/App.java
New file
@@ -0,0 +1,54 @@
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.Serial;
import java.io.Serializable;
@Data
@TableName("open_api_app")
public class App implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    /**
     * appId
     */
    @ApiModelProperty(value= "appId")
    @TableId(value = "id")
    private String id;
    /**
     * appScrect
     */
    @ApiModelProperty(value = "appScrect")
    private String screct;
    /**
     * appName
     */
    @ApiModelProperty(value = "appName")
    private String name;
    /**
     * appUrl
     */
    @ApiModelProperty(value = "appUrl")
    private String url;
    /**
     * 是否启用,0 未启用;1 启用;
     */
    @ApiModelProperty(value = "是否启用,0 未启用;1 启用;")
    private Integer enable;
    /**
     * 租户id
     */
    @ApiModelProperty(value = "租户id")
    private Long tenant_id;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/constant/Constants.java
@@ -28,7 +28,7 @@
    /**
     * 无权限错误码
     */
    public static final int UNAUTHORIZED_CODE = 403;
    public static final String UNAUTHORIZED_CODE = "403";
    /**
     * 无权限提示信息
@@ -38,7 +38,7 @@
    /**
     * 未认证错误码
     */
    public static final int UNAUTHENTICATED_CODE = 401;
    public static final String UNAUTHENTICATED_CODE = "401";
    /**
     * 未认证提示信息
@@ -131,4 +131,12 @@
     */
    public static final Integer TASK_SORT_MIN_VALUE =  0;
    // AppId认证相关常量
    public static final String HEADER_APP_ID = "appId"; //X-App-Id
    public static final String HEADER_APP_SECRET = "appSecret"; //X-App-Secret
    public static final String HEADER_AUTHORIZATION = "authorization";  //Authorization
    public static final String TOKEN_PREFIX = "Zoneyung";
    public static final String REQUEST_ATTR_APP_ID = "appId"; //request_app_id
    public static final String REQUEST_ATTR_USER_ID = "appSecret"; //request_user_id
    public static final String REQUEST_ATTR_APP_INFO = "APP_INFO";
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/dto/CommonResponse.java
@@ -1,5 +1,6 @@
package com.vincent.rsf.openApi.entity.dto;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -25,4 +26,35 @@
    @ApiModelProperty("响应结果")
    private Object data;
    /**
     * 成功响应
     */
    public static CommonResponse ok() {
        CommonResponse response = new CommonResponse();
        response.setCode(200);
        response.setMsg("操作成功");
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("result", "SUCCESS");
        response.setData(jsonObject);
        return response;
    }
    public static CommonResponse ok(Object data) {
        CommonResponse response = new CommonResponse();
        response.setCode(200);
        response.setMsg("操作成功");
        response.setData(data);
        return response;
    }
    /**
     * 失败响应
     */
    public static CommonResponse error(String msg) {
        CommonResponse response = new CommonResponse();
        response.setCode(500);
        response.setMsg(msg);
        return response;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/CheckOrder.java
New file
@@ -0,0 +1,35 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "CheckOrder", description = "盘点单")
public class CheckOrder {
    // 盘点单号,唯一标识
    @NotNull
    @JsonProperty("orderNo")
    private String orderNo;
    // 盘点单名称
    @JsonProperty("orderName")
    private String orderName;
    // 确认时间,秒级时间戳
    private Long checkTime;
    // 单据类型,需ERP确定上报时需要使用哪个类型
    private String orderTypeId;
    // 资产组织
    private String assetOrgId;
    // 盘点方案,需ERP确定上报时使用哪个方案
    private String inventSchemeId;
    // 确认结果
    private List<CheckResult> checkItems;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/CheckResult.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "CheckResult", description = "盘点结果")
public class CheckResult {
    // 仓库编码
    private String wareHouseId;
    // 物料编码
    private String matNr;
    // 库存数量
    private Double qty;
    // 确认后库存数量,实际库存以WMS盘点为准,只保存不做库存数量调整
    private Double checkQty;
    // 是否解冻,0 解冻;1 不解冻;
    private Integer defrost;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Customer.java
New file
@@ -0,0 +1,39 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Customer", description = "客户信息同步")
public class Customer {
    // 客户编码,唯一标识
    @NotNull
    @JsonProperty("customerId")
    private String customerId;
    // 客户名称
    @JsonProperty("customerName")
    private String customerName;
    // 客户昵称
    private String customerNickName;
    // 客户分组,国内,国外
    private String customerGroup;
    // 联系人
    private String contact;
    // 联系电话
    private String telephone;
    // 邮箱
    private String email;
    // 地址
    private String address;
    // 操作类型,1 新增;2 修改;3禁用;4 反禁用;
    @JsonProperty("operateType")
    private Integer operateType;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/InventoryDetails.java
New file
@@ -0,0 +1,47 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "InventoryDetails", description = "库存明细")
public class InventoryDetails {
    // 库位编码
    private String locId;
    // 仓库编码
    private String wareHouseId;
    // 仓库名称
    private String wareHouseName;
    // 托盘码,如果一个托盘上备了2个工单号,则分为2条
    private String palletId;
    // 物料编码
    private String matNr;
    // 物料名称
    private String makTx;
    // 规格
    private String spec;
    // 数量,小数点最长6位
    private Double anfme;
    // 单位
    private String unit;
    // 库存状态,待ERP补充,示例:可用;冻结;
    private String status;
    // 绑定的订单类型,1 出库单;2 入库单;3 备料单;未绑定时为空
    private Integer orderType;
    // 订单号、备料单号,未绑定时为空
    private String orderNo;
    // 备料类型,为备料单时有,1 正常领料;2 生产补料;
    private Integer prepareType;
    // 计划跟踪号
    private String planNo;
    // 批次号
    private String batch;
    // 库存组织
    private String stockOrgId;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/InventoryQueryCondition.java
New file
@@ -0,0 +1,28 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "InventoryQueryCondition", description = "库存查询条件")
public class InventoryQueryCondition {
    // 仓库编码
    private String wareHouseId;
    // 物料编码
    private String matNr;
    // 物料组
    private String matGroup;
    // 库位编码
    private String locId;
    // 订单号/工单号/MES工单号
    private String orderNo;
    // 数计划跟踪号
    private String planNo;
    // 批次号
    private String batch;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/InventorySummary.java
New file
@@ -0,0 +1,34 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "InventorySummary", description = "库存汇总")
public class InventorySummary {
    // 仓库编码
    private String wareHouseId;
    // 仓库名称
    private String wareHouseName;
    // 物料编码
    private String matNr;
    // 物料名称
    private String makTx;
    // 规格
    private String spec;
    // 数量,小数点最长6位
    private Double anfme;
    // 单位
    private String unit;
    // 库存组织
    private String stockOrgId;
    // 批号
    private String batch;
    // 计划跟踪号
    private String planNo;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/MatPreparationOrder.java
New file
@@ -0,0 +1,30 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "MatPreparationOrder", description = "备料单")
public class MatPreparationOrder {
    // 备料单号,唯一标识
    @NotNull
    @JsonProperty("orderNo")
    private String orderNo;
    // 备料单创建时间,秒级时间戳
    @JsonProperty("orderTime")
    private Long orderTime;
    // 备料类型,为备料单时有,1 正常领料;2 生产补料;
    @JsonProperty("prepareType")
    private Integer prepareType;
    // 备料详情
    private List<MatPreparationOrderItem> orderItems;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/MatPreparationOrderItem.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "MatPreparationOrderItem", description = "备料单明细")
public class MatPreparationOrderItem {
    // 物料编码
    @NotNull
    @JsonProperty("matNr")
    private String matNr;
    // 备料数量
    @JsonProperty("anfme")
    private Double anfme;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Material.java
New file
@@ -0,0 +1,55 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Material", description = "物料信息同步")
public class Material {
    // 物料编码,唯一标识
    @NotNull
    @JsonProperty("matNr")
    private String matNr;
    // 物料名称
    @JsonProperty("makTx")
    private String makTx;
    // 物料分组编码
    @JsonProperty("groupId")
    private String groupId;
    // 物料分组
    @JsonProperty("groupName")
    private String groupName;
    // 型号
    private String model;
    // 重量
    private Double weight;
    // 颜色
    private String color;
    // 尺寸
    private String size;
    // 规格
    private String spec;
    // 描述
    private String describe;
    // 单位
    private String unit;
    // 基本单位,银座有双单位,如长度和重量,可能需要转换
    private String baseUnit;
    // 使用组织编码
    private String useOrgId;
    // 使用组织名称
    private String useOrgName;
    // 物料属性,外购等
    private String erpClsID;
    // 操作类型,1 新增;2 修改;3禁用;4 反禁用;
    @JsonProperty("operateType")
    private Integer operateType;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Order.java
New file
@@ -0,0 +1,78 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Order", description = "入/出库通知单")
public class Order {
    // 入/出库订单号,唯一标识
    @NotNull
    @JsonProperty("orderNo")
    private String orderNo;
    // 单据内码,唯一标识
    @JsonProperty("orderInternalCode")
    private String orderInternalCode;
    // 业务类型,示例:
    // 入库:收料通知单(PUR_ReceiveBill)、采购入库单(STK_InStock)、退料申请单(PUR_MRAPP)、采购退料单(PUR_MRB)、
    //   退货通知单(SAL_RETURNNOTICE)、销售退货单(SAL_RETURNSTOCK)、生产退料单(PRD_ReturnMtrl)、生产入库单(PRD_INSTOCK)/生产汇报单(PRD_MORPT)、
    //   其他入库单(STK_MISCELLANEOUS)
    // 出库:发货通知单(SAL_DELIVERYNOTICE)、销售出库单(SAL_OUTSTOCK)、出库申请单(STK_OutStockApply)、生产领料单(PRD_PickMtrl)、
    //   生产补料单(PRD_FeedMtrl)、其他出库单(STK_MisDelivery)
    // 调拨:调拨申请单(STK_TRANSFERAPPLY)、直接调拨单(STK_TransferDirect)
    private String wkType;
    // 订单类型,1 出库单;2 入库单;3 调拨单;
    private String type;
    // 创建日期,时间戳,精确到秒
    private Long createTime;
    // 业务日期,对账使用,时间戳,精确到秒
    private Long businessTime;
    // 库存方向
    private String stockDirect;
    // 订单明细
    private List<OrderItem> orderItems;
    // 出入库接驳站点,出库时将物料出库后运输至该站点,入库时从该站点将物料运回库中
    private String stationId;
    // 客户编码
    private String customerId;
    // 客户名称
    private String customerName;
    // 供应商编码
    private String supplierId;
    // 供应商名称
    private String supplierName;
    // 收料/发货组织
    private String stockOrgId;
    // 收料/发货组织名称
    private String stockOrgName;
    // 采购组织
    private String purchaseOrgId;
    // 采购组织名称
    private String purchaseOrgName;
    // 采购员
    private String purchaseUserId;
    // 采购员名称
    private String purchaseUserName;
    // 生产组织
    private String prdOrgId;
    // 生产组织名称
    private String prdOrgName;
    // 销售组织
    private String saleOrgId;
    // 销售组织名称
    private String saleOrgName;
    // 销售员
    private String saleUserId;
    // 销售员名称
    private String saleUserName;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/OrderItem.java
New file
@@ -0,0 +1,65 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "OrderItem", description = "入/出库通知单明细")
public class OrderItem {
    // 计划跟踪号
    @NotNull
    @JsonProperty("planNo")
    private String planNo;
    // 行内码,唯一标识
    @JsonProperty("lineId")
    private String lineId;
    // 物料编码,唯一标识
    @NotNull
    @JsonProperty("matNr")
    private String matNr;
    // 物料名称
    @JsonProperty("makTx")
    private String makTx;
    // 规格
    private String spec;
    // 型号
    private String model;
    // 数量
    private Double anfme;
    // 批号
    private String batch;
    // 单位
    private String unit;
    // 基本单位
    private String baseUnitId;
    // 计价单位
    private String priceUnitId;
    // 建议目标仓库
    private String palletId;
    // 调出仓
    private String targetWareHouseId;
    // 业务日期,对账使用,时间戳,精确到秒
    private String sourceWareHouseId;
    // 入库类型
    private String inStockType;
    // 货主类型
    private String ownerTypeId;
    // 货主
    private String ownerId;
    // 货主名称
    private String ownerName;
    // 保管者类型
    private String keeperTypeId;
    // 保管者
    private String keeperId;
    // 保管者名称
    private String keeperName;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Pallet.java
New file
@@ -0,0 +1,36 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Pallet", description = "托盘信息")
public class Pallet {
    // 托盘条码
    @NotNull
    @JsonProperty("BarCode")
    private String barCode;
    // 托盘编码
    @JsonProperty("PalletCode")
    private String palletCode;
    // 托盘名称
    @JsonProperty("PalletName")
    private String palletName;
    // 托盘类型编码
    @JsonProperty("PalletTypeCode")
    private String palletTypeCode;
    // 托盘类型
    @JsonProperty("PalletTypeName")
    private String palletTypeName;
    // 创建人
    @JsonProperty("CreatedBy")
    private String createdBy;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/SimpleProductionTask.java
New file
@@ -0,0 +1,14 @@
package com.vincent.rsf.openApi.entity.phyz;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class SimpleProductionTask extends Task {
    // 领料内容
    private String matText;
    // 领料区域,线下创建于MES同步
    private String regionId;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Station.java
New file
@@ -0,0 +1,39 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Station", description = "站点信息")
public class Station {
    // 接驳口编码
    @NotNull
    @JsonProperty("ConnPortCode")
    private String connPortCode;
    // 接驳口名称
    @JsonProperty("ConnPortName")
    private String connPortName;
    // 车间编码
    @JsonProperty("WorkshopCode")
    private String workshopCode;
    // 车间
    @JsonProperty("WorkshopName")
    private String workshopName;
    // 仓库编码
    @JsonProperty("ProductionLineCode")
    private String productionLineCode;
    // 仓库
    @JsonProperty("ProductionLineName")
    private String productionLineName;
    // 创建人
    @JsonProperty("CreatedBy")
    private String createdBy;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Supplier.java
New file
@@ -0,0 +1,39 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Supplier", description = "供应商信息同步")
public class Supplier {
    // 供应商编码,唯一标识
    @NotNull
    @JsonProperty("supplierId")
    private String supplierId;
    // 供应商名称
    @JsonProperty("supplierName")
    private String supplierName;
    // 供应商昵称
    private String supplierNickName;
    // 供应商分组,国内,国外
    private String supplierGroup;
    // 联系人
    private String contact;
    // 联系电话
    private String telephone;
    // 邮箱
    private String email;
    // 地址
    private String address;
    // 操作类型,1 新增;2 修改;3禁用;4 反禁用;
    @JsonProperty("operateType")
    private Integer operateType;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Task.java
New file
@@ -0,0 +1,36 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Task", description = "工作任务")
public class Task {
    // 任务号,唯一标识
    @NotNull
    @JsonProperty("taskNo")
    private String taskNo;
    // 任务名称
    @JsonProperty("taskName")
    private String taskName;
    // 任务类型,1 入库;2 出库;3 转移;
    private Integer taskType;
    // 任务优先级,范围:1-100;数字越大,优先级越高,默认10;
    private Integer initPriority;
    // 起始站点编号
    private String startStationId;
    // 目标站点编号
    private String endStationId;
    // 托盘编号
    private String palletId;
    // 托盘类型,枚举类型:1 托盘;2 料架;3 料箱;等枚举类型待后续对接
    private String palletType;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/TaskResult.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "TaskResult", description = "工作任务结果")
public class TaskResult {
    // 任务号,唯一标识
    @NotNull
    @JsonProperty("TaskNo")
    private String taskNo;
    // 状态
    @JsonProperty("Status")
    private String status;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/phyz/Warehouse.java
New file
@@ -0,0 +1,33 @@
package com.vincent.rsf.openApi.entity.phyz;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Accessors(chain = true)
@ApiModel(value = "Warehouse", description = "仓库信息同步")
public class Warehouse {
    // 仓库编码,唯一标识
    @NotNull
    @JsonProperty("wareHouseId")
    private String wareHouseId;
    // 仓库名称
    @JsonProperty("wareHouseName")
    private String wareHouseName;
    // 仓库位置
    private String address;
    // 使用组织编码
    private String useOrgId;
    // 使用组织名称
    private String useOrgName;
    // 操作类型,1 新增;2 修改;3禁用;4 反禁用;
    @JsonProperty("operateType")
    private Integer operateType;
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/ApiForeignLogMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.ApiForeignLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApiForeignLogMapper extends BaseMapper<ApiForeignLog> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/ApiFunctionMapper.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.ApiFunction;
import org.apache.ibatis.annotations.Mapper;
/**
 * ApiFunction Mapper
 *
 * @author vincent
 * @since 2026-01-04
 */
@Mapper
public interface ApiFunctionMapper extends BaseMapper<ApiFunction> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/ApiMapMapper.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.openApi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.openApi.entity.app.ApiMap;
import org.apache.ibatis.annotations.Mapper;
/**
 * ApiMap Mapper
 *
 * @author vincent
 * @since 2026-01-04
 */
@Mapper
public interface ApiMapMapper extends BaseMapper<ApiMap> {
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/mapper/AppMapper.java
New file
@@ -0,0 +1,15 @@
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;
/**
 * App Mapper
 *
 * @author vincent
 * @since 2026-01-04
 */
@Mapper
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,121 @@
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.apache.tika.utils.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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和AppSecret认证过滤器
 *
 * 用于验证请求头中的AppId和AppSecret
 *
 * @author vincent
 * @since 2026-01-05
 */
@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();
        // 检查是否为认证请求(如获取token)
        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)) {
                // Token时间认证成功,认证AppId和AppSecret
                String tokenAppId = TokenUtils.getAppIdFromToken(token);
                String tokenAppSecret = TokenUtils.getSecretFromToken(token);
                if (StringUtils.isBlank(tokenAppId) || StringUtils.isBlank(tokenAppSecret)
                        || !appAuthService.validateApp(tokenAppId, tokenAppSecret)) {
                    log.warn("Token验证失败");
                    sendErrorResponse(response, Integer.parseInt(Constants.UNAUTHENTICATED_CODE), "认证失败,请提供有效的Token");
                    return;
                } else {
                    request.setAttribute(Constants.REQUEST_ATTR_APP_ID, tokenAppId);
                }
            } else {
                log.warn("Token验证失败或缺失");
                sendErrorResponse(response, Integer.parseInt(Constants.UNAUTHENTICATED_CODE), "认证失败,请提供有效的Token");
                return;
            }
        } else {
            log.warn("缺少Token认证信息");
            sendErrorResponse(response, Integer.parseInt(Constants.UNAUTHENTICATED_CODE), "认证失败,请提供有效的Token");
            return;
        }
        filterChain.doFilter(request, response);
    }
    /**
     * 发送错误响应
     *
     * @param response HTTP响应
     * @param code 错误码
     * @param message 错误消息
     * @throws IOException
     */
    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();
    }
    /**
     * 检查是否为认证请求(不需要认证的请求)
     *
     * @param requestURI 请求URI
     * @return 是否为认证请求
     */
    private boolean isAuthRequest(String requestURI) {
        return requestURI.contains("/getToken");
//               || requestURI.contains("/auth/validate") ||
//               requestURI.contains("/auth/login");
    }
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String requestURI = request.getRequestURI();
        // 不过滤认证相关请求和公开接口
        return 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,112 @@
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.stereotype.Service;
import javax.annotation.Resource;
/**
 * App认证服务
 *
 * 提供AppId和AppSecret的验证功能
 *
 * @author vincent
 * @since 2026-01-05
 */
@Slf4j
@Service
public class AppAuthService {
    @Resource
    private AppService appService;
    /**
     * 验证AppId和AppSecret
     *
     * @param appId 应用ID
     * @param appSecret 应用密钥
     * @return 验证结果
     */
    public boolean validateApp(String appId, String appSecret) {
        log.debug("验证AppId: {}, AppSecret: ****", appId);
        if (appId == null || appSecret == null) {
            log.warn("AppId或AppSecret为空");
            return false;
        }
        try {
            // 从数据库查询应用信息
            App app = appService.getById(appId);
            if (app == null) {
                log.warn("未找到应用: {}", appId);
                return false;
            }
            // 检查应用是否启用
            if (app.getEnable() != 1) {
                log.warn("应用未启用: {}", appId);
                return false;
            }
            // 验证密钥
            boolean isValid = appSecret.equals(app.getScrect());
            if (!isValid) {
                log.warn("AppSecret验证失败: AppId={}", appId);
            } else {
                log.info("AppId认证成功: AppId={}", appId);
            }
            return isValid;
        } catch (Exception e) {
            log.error("验证AppId和AppSecret时发生异常", e);
            return false;
        }
    }
    public boolean validateApp(String appId) {
        App app = appService.getById(appId);
        return app != null;
    }
    /**
     * 获取应用信息
     *
     * @param appId 应用ID
     * @return 应用信息
     */
    public App getAppInfo(String appId) {
        if (appId == null) {
            return null;
        }
        try {
            return appService.getById(appId);
        } catch (Exception e) {
            log.error("获取应用信息失败: {}", appId, e);
            return null;
        }
    }
    /**
     * 生成AppToken(可选功能)
     *
     * @param appId 应用ID
     * @param appSecret 应用密钥
     * @return 生成的Token
     */
    public String generateAppToken(String appId, String appSecret) {
        // 这里可以实现基于AppId和AppSecret的Token生成逻辑
        // 例如使用JWT生成Token
        if (validateApp(appId, appSecret)) {
            // 生成Token的逻辑
            long timestamp = System.currentTimeMillis();
            String tokenData = appId + ":" + timestamp;
            // TODO:这里可以使用更安全的加密算法
            return java.util.Base64.getEncoder().encodeToString(tokenData.getBytes());
        }
        return null;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/utils/AuthUtils.java
New file
@@ -0,0 +1,54 @@
package com.vincent.rsf.openApi.security.utils;
import com.vincent.rsf.openApi.entity.constant.Constants;
import com.vincent.rsf.openApi.entity.app.App;
import javax.servlet.http.HttpServletRequest;
/**
 * 认证工具类
 *
 * 提供认证相关的通用功能
 *
 * @author vincent
 * @since 2026-01-05
 */
public class AuthUtils {
    /**
     * 从请求中获取AppId
     *
     * @param request HTTP请求
     * @return AppId
     */
    public static String getAppId(HttpServletRequest request) {
        // 优先从请求属性中获取(认证过滤器设置的)
        String appId = (String) request.getAttribute(Constants.REQUEST_ATTR_APP_ID);
        if (appId != null) {
            return appId;
        }
        // 从请求头获取
        return request.getHeader(Constants.HEADER_APP_ID);
    }
    /**
     * 从请求中获取App信息
     *
     * @param request HTTP请求
     * @return App信息
     */
    public static App getAppInfo(HttpServletRequest request) {
        return (App) request.getAttribute(Constants.REQUEST_ATTR_APP_INFO);
    }
    /**
     * 检查请求是否已通过App认证
     *
     * @param request HTTP请求
     * @return 是否已认证
     */
    public static boolean isAuthenticated(HttpServletRequest request) {
        return getAppId(request) != null;
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/security/utils/TokenUtils.java
New file
@@ -0,0 +1,161 @@
package com.vincent.rsf.openApi.security.utils;
import com.vincent.rsf.openApi.entity.app.App;
import com.vincent.rsf.openApi.entity.constant.Constants;
import com.vincent.rsf.openApi.service.AppService;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.apache.tika.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;
/**
 * JWT Token工具类
 * 用于生成和验证JWT Token
 */
public class TokenUtils {
    private static final Logger log = LoggerFactory.getLogger(TokenUtils.class);
    // 使用一个安全的密钥,实际应用中应该从配置文件读取
    private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    // Token过期时间,默认1小时
    private static final long TOKEN_EXPIRATION = 60 * 60 * 1000L; // 24小时
    @Resource
    private AppService appService;
    /**
     * 生成JWT Token
     *
     * @param claims Token中包含的声明信息
     * @return 生成的Token字符串
     */
    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();
    }
    /**
     * 生成带AppId的Token
     *
     * @param appId 应用ID
     * @param appSecret 应用秘钥
     * @return 生成的Token字符串
     */
    public static String generateToken(String appId, String appSecret) {
        Map<String, Object> claims = Map.of(
            "appId", appId,
            "appSecret", appSecret,
            "created", System.currentTimeMillis()
        );
        return generateToken(claims);
    }
    /**
     * 解析Token获取声明信息
     *
     * @param token Token字符串
     * @return 声明信息
     */
    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;
        }
    }
    /**
     * 验证Token时间是否有效
     *
     * @param token Token字符串
     * @return 时间是否有效
     */
    public static boolean validateTokenTime(String token) {
        try {
            Claims claims = parseToken(token);
            if (claims == null) {
                return false;
            }
            // 检查Token是否过期
            Date expiration = claims.getExpiration();
            return expiration != null && expiration.after(new Date());
        } catch (JwtException e) {
            log.error("验证Token失败: {}", e.getMessage());
            return false;
        }
    }
    /**
     * 从Token中获取AppId
     *
     * @param token Token字符串
     * @return AppId
     */
    public static String getAppIdFromToken(String token) {
        Claims claims = parseToken(token);
        if (claims != null) {
            return (String) claims.get("appId");
        }
        return null;
    }
    /**
     * 从Token中获取appSecret
     *
     * @param token Token字符串
     * @return appSecret
     */
    public static String getSecretFromToken(String token) {
        Claims claims = parseToken(token);
        if (claims != null) {
            return (String) claims.get("appSecret");
        }
        return null;
    }
//    /**
//     * 从Token中获取UserId
//     *
//     * @param token Token字符串
//     * @return UserId
//     */
//    public static String getUserIdFromToken(String token) {
//        Claims claims = parseToken(token);
//        if (claims != null) {
//            return (String) claims.get("userId");
//        }
//        return null;
//    }
    /**
     * 从Authorization头中提取Token
     *
     * @param authHeader Authorization头内容
     * @return Token字符串(不包含Bearer前缀)
     */
    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/ApiForeignLogService.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.openApi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.openApi.entity.app.ApiForeignLog;
public interface ApiForeignLogService extends IService<ApiForeignLog> {
    void saveAsync(ApiForeignLog log);
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/ApiFunctionService.java
New file
@@ -0,0 +1,18 @@
package com.vincent.rsf.openApi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.openApi.entity.app.ApiFunction;
/**
 * ApiFunction Service
 *
 * @author vincent
 * @since 2026-01-04
 */
public interface ApiFunctionService extends IService<ApiFunction> {
    /**
     * 刷新功能缓存
     */
    void refreshCache();
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/ApiMapService.java
New file
@@ -0,0 +1,18 @@
package com.vincent.rsf.openApi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.openApi.entity.app.ApiMap;
/**
 * ApiMap Service
 *
 * @author vincent
 * @since 2026-01-04
 */
public interface ApiMapService extends IService<ApiMap> {
    /**
     * 刷新映射缓存
     */
    void refreshCache();
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/AppService.java
New file
@@ -0,0 +1,18 @@
package com.vincent.rsf.openApi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.openApi.entity.app.App;
/**
 * App Service
 *
 * @author vincent
 * @since 2026-01-04
 */
public interface AppService extends IService<App> {
    /**
     * 刷新应用缓存
     */
    void refreshCache();
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/ApiForeignLogServiceImpl.java
New file
@@ -0,0 +1,18 @@
package com.vincent.rsf.openApi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.openApi.entity.app.ApiForeignLog;
import com.vincent.rsf.openApi.mapper.ApiForeignLogMapper;
import com.vincent.rsf.openApi.service.ApiForeignLogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class ApiForeignLogServiceImpl extends ServiceImpl<ApiForeignLogMapper, ApiForeignLog> implements ApiForeignLogService {
    @Async
    @Override
    public void saveAsync(ApiForeignLog log) {
        baseMapper.insert(log);
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/ApiFunctionServiceImpl.java
New file
@@ -0,0 +1,30 @@
package com.vincent.rsf.openApi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.openApi.entity.app.ApiFunction;
import com.vincent.rsf.openApi.mapper.ApiFunctionMapper;
import com.vincent.rsf.openApi.service.ApiFunctionService;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * ApiFunction Service Implementation
 *
 * @author vincent
 * @since 2026-01-04
 */
@Slf4j
@Service
public class ApiFunctionServiceImpl extends ServiceImpl<ApiFunctionMapper, ApiFunction> implements ApiFunctionService {
    @Override
    public void refreshCache() {
        log.info("开始刷新接口功能缓存...");
        List<ApiFunction> functions = this.list();
        ParamsMapUtils.FUNCTIONS = functions;
        log.info("接口功能缓存刷新完成,共加载 {} 个功能", functions.size());
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/ApiMapServiceImpl.java
New file
@@ -0,0 +1,32 @@
package com.vincent.rsf.openApi.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.openApi.entity.app.ApiMap;
import com.vincent.rsf.openApi.mapper.ApiMapMapper;
import com.vincent.rsf.openApi.service.ApiMapService;
import com.vincent.rsf.openApi.utils.ParamsMapUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * ApiMap Service Implementation
 *
 * @author vincent
 * @since 2026-01-04
 */
@Slf4j
@Service
public class ApiMapServiceImpl extends ServiceImpl<ApiMapMapper, ApiMap> implements ApiMapService {
    @Override
    public void refreshCache() {
        log.info("开始刷新字段映射缓存...");
        List<ApiMap> maps = this.list(new LambdaQueryWrapper<ApiMap>()
                .eq(ApiMap::getEnable, 1));
        ParamsMapUtils.ATTRIBUTE_MAPS = maps;
        log.info("字段映射缓存刷新完成,共加载 {} 条映射规则", maps.size());
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/AppServiceImpl.java
New file
@@ -0,0 +1,32 @@
package com.vincent.rsf.openApi.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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 com.vincent.rsf.openApi.utils.ParamsMapUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * App Service Implementation
 *
 * @author vincent
 * @since 2026-01-04
 */
@Slf4j
@Service
public class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService {
    @Override
    public void refreshCache() {
        log.info("开始刷新应用缓存...");
        List<App> apps = this.list(new LambdaQueryWrapper<App>()
                .eq(App::getEnable, 1));
        ParamsMapUtils.APPS = apps;
        log.info("应用缓存刷新完成,共加载 {} 个应用", apps.size());
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/utils/JsonReplaceTest.java
New file
@@ -0,0 +1,192 @@
package com.vincent.rsf.openApi.utils;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
 * JSON属性名递归替换测试类
 *
 * 演示如何使用 FuncMap 进行JSON属性名的递归替换
 *
 * @author vincent
 * @since 2026-01-04
 */
public class JsonReplaceTest {
    public static void main(String[] args) {
        System.out.println("========== JSON属性名递归替换测试 ==========\n");
        // 测试1:简单对象
        testSimpleObject();
        // 测试2:嵌套对象
        testNestedObject();
        // 测试3:包含数组
        testWithArray();
        // 测试4:复杂结构
        testComplexStructure();
    }
    /**
     * 测试1:简单对象属性替换
     */
    private static void testSimpleObject() {
        System.out.println("【测试1:简单对象】");
        // 构造测试数据
        JSONObject data = new JSONObject();
        data.put("orderNumber", "PO001");
        data.put("orderQty", 100);
        data.put("orderAmount", 5000.00);
        System.out.println("原始数据:" + data.toJSONString());
        // 定义映射规则
        Map<String, String> rules = new HashMap<>();
        rules.put("orderNumber", "code");
        rules.put("orderQty", "qty");
        rules.put("orderAmount", "anfme");
        // 执行替换
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, rules);
        System.out.println("替换结果:" + result.toJSONString());
        System.out.println();
    }
    /**
     * 测试2:嵌套对象深度替换
     */
    private static void testNestedObject() {
        System.out.println("【测试2:嵌套对象】");
        // 构造嵌套数据
        JSONObject data = new JSONObject();
        data.put("orderNumber", "PO002");
        JSONObject customer = new JSONObject();
        customer.put("customerName", "张三");
        customer.put("customerPhone", "13800138000");
        JSONObject address = new JSONObject();
        address.put("cityName", "北京");
        address.put("streetName", "朝阳路88号");
        customer.put("address", address);
        data.put("customer", customer);
        System.out.println("原始数据:" + data.toJSONString());
        // 定义映射规则(会应用到所有层级)
        Map<String, String> rules = new HashMap<>();
        rules.put("orderNumber", "code");
        rules.put("customerName", "name");
        rules.put("customerPhone", "phone");
        rules.put("cityName", "city");
        rules.put("streetName", "street");
        // 执行递归替换
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, rules);
        System.out.println("替换结果:" + result.toJSONString());
        System.out.println();
    }
    /**
     * 测试3:包含数组的替换
     */
    private static void testWithArray() {
        System.out.println("【测试3:包含数组】");
        // 构造包含数组的数据
        JSONObject data = new JSONObject();
        data.put("orderNumber", "PO003");
        JSONArray items = new JSONArray();
        for (int i = 1; i <= 3; i++) {
            JSONObject item = new JSONObject();
            item.put("materialCode", "MAT00" + i);
            item.put("materialName", "物料" + i);
            item.put("itemQty", 10 * i);
            items.add(item);
        }
        data.put("items", items);
        System.out.println("原始数据:" + data.toJSONString());
        // 定义映射规则
        Map<String, String> rules = new HashMap<>();
        rules.put("orderNumber", "code");
        rules.put("materialCode", "matnr");
        rules.put("materialName", "maktx");
        rules.put("itemQty", "qty");
        // 执行替换(包括数组中的所有对象)
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, rules);
        System.out.println("替换结果:" + result.toJSONString());
        System.out.println();
    }
    /**
     * 测试4:复杂结构(嵌套+数组+多层)
     */
    private static void testComplexStructure() {
        System.out.println("【测试4:复杂结构】");
        // 构造复杂结构
        JSONObject data = new JSONObject();
        data.put("orderNumber", "PO004");
        data.put("orderStatus", "PENDING");
        // 客户信息
        JSONObject customer = new JSONObject();
        customer.put("customerCode", "CUST001");
        customer.put("customerName", "北京公司");
        data.put("customer", customer);
        // 订单明细数组
        JSONArray items = new JSONArray();
        for (int i = 1; i <= 2; i++) {
            JSONObject item = new JSONObject();
            item.put("itemNo", i);
            item.put("materialCode", "MAT00" + i);
            item.put("materialName", "物料" + i);
            item.put("itemQty", 50 + i * 10);
            // 仓库信息(嵌套在明细中)
            JSONObject warehouse = new JSONObject();
            warehouse.put("warehouseCode", "WH0" + i);
            warehouse.put("locationCode", "LOC-A-0" + i);
            item.put("warehouse", warehouse);
            items.add(item);
        }
        data.put("items", items);
        System.out.println("原始数据:" + data.toJSONString());
        // 定义完整的映射规则
        Map<String, String> rules = new HashMap<>();
        rules.put("orderNumber", "code");
        rules.put("orderStatus", "exceStatus");
        rules.put("customerCode", "custCode");
        rules.put("customerName", "custName");
        rules.put("materialCode", "matnr");
        rules.put("materialName", "maktx");
        rules.put("itemQty", "qty");
        rules.put("warehouseCode", "whCode");
        rules.put("locationCode", "locCode");
        // 执行递归替换
        JSONObject result = ParamsMapUtils.replaceJsonKeys(data, rules);
        System.out.println("替换结果:" + result.toJSONString());
        System.out.println();
    }
}
rsf-open-api/src/main/java/com/vincent/rsf/openApi/utils/ParamsMapUtils.java
New file
@@ -0,0 +1,273 @@
package com.vincent.rsf.openApi.utils;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.vincent.rsf.openApi.entity.app.ApiFunction;
import com.vincent.rsf.openApi.entity.app.ApiMap;
import com.vincent.rsf.openApi.entity.app.App;
import com.vincent.rsf.openApi.service.ApiMapService;
import com.vincent.rsf.openApi.service.ApiFunctionService;
import com.vincent.rsf.openApi.service.AppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.utils.StringUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * 接口字段动态映射工具类
 * 支持从数据库加载映射配置,并提供字段转换功能
 *
 * @author vincent
 * @since 2026-01-04
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ParamsMapUtils {
    private final AppService appService;
    private final ApiFunctionService functionService;
    private final ApiMapService mapService;
    // 缓存到内存
    public static List<App> APPS = new ArrayList<>();
    public static List<ApiFunction> FUNCTIONS = new ArrayList<>();
    public static List<ApiMap> ATTRIBUTE_MAPS = new ArrayList<>();
    /**
     * 应用完全启动后自动加载配置
     * 使用 ApplicationReadyEvent 确保 Spring 容器完全初始化
     */
    @EventListener(ApplicationReadyEvent.class)
    public void init() {
        log.info("=============== 开始加载接口映射配置 ===============");
        try {
            appService.refreshCache();
            functionService.refreshCache();
            mapService.refreshCache();
            log.info("接口映射配置加载完成 - 应用数:{}, 功能数:{}, 映射规则数:{}",
                    APPS.size(), FUNCTIONS.size(), ATTRIBUTE_MAPS.size());
        } catch (Exception e) {
            log.error("接口映射配置加载失败", e);
        }
    }
    /**
     * 手动刷新所有缓存
     */
    public void refreshAll() {
        log.info("手动刷新所有映射缓存...");
        appService.refreshCache();
        functionService.refreshCache();
        mapService.refreshCache();
    }
    /**
     * 执行字段映射转换
     *
     * @param appId 应用ID
     * @param funcId 功能ID
     * @param params 原始参数
     * @return 转换后的参数
     */
    public static JSONObject apiMaps(String appId, String funcId, JSONObject params) {
        if (params == null || params.isEmpty()) {
            return params;
        }
        // 1、获取映射表
        List<ApiMap> maps = ATTRIBUTE_MAPS.stream()
                .filter(map -> map.getAppId().equals(appId) && map.getFuncId().equals(funcId))
                .toList();
        if (maps.isEmpty()) {
            log.debug("未找到映射配置 - appId:{}, funcId:{}", appId, funcId);
            return params;
        }
        // 2、构建映射规则Map
        Map<String, String> mappingRules = new HashMap<>();
        for (ApiMap map : maps) {
            String sourceAttribute = map.getSourceAttribute();
            String targetAttribute = map.getTargetAttribute();
            if (!StringUtils.isBlank(sourceAttribute) && !StringUtils.isBlank(targetAttribute)) {
                mappingRules.put(sourceAttribute, targetAttribute);
            }
        }
        // 3、递归替换所有层级的属性名称
        JSONObject result = replaceKeysRecursive(params, mappingRules);
        return result;
    }
    /**
     * 递归替换JSON所有层级的属性名称
     * 支持嵌套对象和数组的深度遍历
     *
     * @param json 原始JSON对象
     * @param mappingRules 映射规则 (源字段名 -> 目标字段名)
     * @return 替换后的JSON对象
     */
    public static JSONObject replaceKeysRecursive(JSONObject json, Map<String, String> mappingRules) {
        if (json == null || json.isEmpty()) {
            return json;
        }
        JSONObject result = new JSONObject();
        for (String key : json.keySet()) {
            Object value = json.get(key);
            // 确定新的键名(如果有映射规则则使用映射后的名称)
            String newKey = mappingRules.getOrDefault(key, key);
            if (value instanceof JSONObject) {
                // 递归处理嵌套对象
                JSONObject nestedResult = replaceKeysRecursive((JSONObject) value, mappingRules);
                result.put(newKey, nestedResult);
                log.debug("替换对象字段: {} -> {}", key, newKey);
            } else if (value instanceof JSONArray) {
                // 递归处理数组
                JSONArray arrayResult = replaceKeysInArray((JSONArray) value, mappingRules);
                result.put(newKey, arrayResult);
                log.debug("替换数组字段: {} -> {}", key, newKey);
            } else {
                // 普通值直接赋值
                Object convertedValue = convertValue(value, newKey);
                result.put(newKey, convertedValue);
                if (!key.equals(newKey)) {
                    log.debug("替换字段: {} -> {} (值: {})", key, newKey, convertedValue);
                }
            }
        }
        return result;
    }
    /**
     * 递归处理JSON数组中的所有元素
     *
     * @param array 原始JSON数组
     * @param mappingRules 映射规则
     * @return 处理后的JSON数组
     */
    private static JSONArray replaceKeysInArray(JSONArray array, Map<String, String> mappingRules) {
        if (array == null || array.isEmpty()) {
            return array;
        }
        JSONArray result = new JSONArray();
        for (int i = 0; i < array.size(); i++) {
            Object element = array.get(i);
            if (element instanceof JSONObject) {
                // 数组元素是对象,递归处理
                JSONObject replacedObject = replaceKeysRecursive((JSONObject) element, mappingRules);
                result.add(replacedObject);
            } else if (element instanceof JSONArray) {
                // 数组元素是数组,递归处理
                JSONArray replacedArray = replaceKeysInArray((JSONArray) element, mappingRules);
                result.add(replacedArray);
            } else {
                // 基本类型,直接添加
                result.add(element);
            }
        }
        return result;
    }
    /**
     * 通用的JSON属性名替换方法(对外提供)
     * 可以直接传入映射规则进行替换
     *
     * @param json 原始JSON对象
     * @param mappingRules 映射规则 Map<源字段名, 目标字段名>
     * @return 替换后的JSON对象
     */
    public static JSONObject replaceJsonKeys(JSONObject json, Map<String, String> mappingRules) {
        return replaceKeysRecursive(json, mappingRules);
    }
    /**
     * 通用的JSON数组属性名替换方法(对外提供)
     *
     * @param array 原始JSON数组
     * @param mappingRules 映射规则 Map<源字段名, 目标字段名>
     * @return 替换后的JSON数组
     */
    public static JSONArray replaceJsonArrayKeys(JSONArray array, Map<String, String> mappingRules) {
        return replaceKeysInArray(array, mappingRules);
    }
    /**
     * 值类型转换(可扩展)
     *
     * @param value 原始值
     * @param targetField 目标字段名
     * @return 转换后的值
     */
    private static Object convertValue(Object value, String targetField) {
        if (value == null) {
            return null;
        }
        // 这里可以根据需要添加更多类型转换逻辑
        // 例如:日期格式转换、数字精度转换等
        // 示例:如果字段名包含amount、price等,转换为BigDecimal
        String lowerField = targetField.toLowerCase();
        if ((lowerField.contains("amount") || lowerField.contains("price")
                || lowerField.contains("qty")) && value instanceof String) {
            try {
                return new BigDecimal(value.toString());
            } catch (Exception e) {
                log.warn("数字转换失败: {} -> {}", value, targetField);
                return value;
            }
        }
        return value;
    }
    /**
     * 获取应用信息
     *
     * @param appId 应用ID
     * @return 应用信息
     */
    public static App getApp(String appId) {
        return APPS.stream()
                .filter(app -> app.getId().equals(appId))
                .findFirst()
                .orElse(null);
    }
    /**
     * 获取功能信息
     *
     * @param funcId 功能ID
     * @return 功能信息
     */
    public static ApiFunction getFunction(String funcId) {
        return FUNCTIONS.stream()
                .filter(func -> func.getId().equals(funcId))
                .findFirst()
                .orElse(null);
    }
}
rsf-open-api/src/main/resources/logback-spring.xml
New file
@@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="10 seconds">
    <!--<include resource="org/springframework/boot/logging/logback/base.xml" />-->
    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被添加到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <!--    <property name="log.path" value="./emp-log"/>-->
    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 配置属性 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!--输出到控制台的appender-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--日志级别过滤器-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!--日志过滤级别-->
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!--log输出文件路径-->
    <springProperty scope="context" name="log.path" source="logging.file.path"/>
    <!--日志文件路径属性-->
    <property name="logback.logdir" value="${log.path}"/>
    <!-- level为 DEBUG 日志 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${logback.logdir}/log_debug.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 指定日志记录器的拆分归档策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <fileNamePattern>${logback.logdir}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!--日志级过滤规则-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--日志过滤级别-->
            <level>debug</level>
            <!--超过过滤级别的策略-->
            <onMatch>ACCEPT</onMatch>
            <!--未超过过滤级别的策略-->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--  level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${logback.logdir}/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 指定日志记录器的拆分归档策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${logback.logdir}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!--日志级过滤规则-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--日志过滤级别-->
            <level>info</level>
            <!--超过过滤级别的策略-->
            <onMatch>ACCEPT</onMatch>
            <!--未超过过滤级别的策略-->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--  level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${logback.logdir}/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 指定日志记录器的拆分归档策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logback.logdir}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!--日志级过滤规则-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--日志过滤级别-->
            <level>warn</level>
            <!--超过过滤级别的策略-->
            <onMatch>ACCEPT</onMatch>
            <!--未超过过滤级别的策略-->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--  level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${logback.logdir}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!--指定日志记录器的拆分归档策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logback.logdir}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!--日志级过滤规则-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--日志过滤级别-->
            <level>ERROR</level>
            <!--超过过滤级别的策略-->
            <onMatch>ACCEPT</onMatch>
            <!--未超过过滤级别的策略-->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
        以及指定<appender>。<logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
              如果未设置此属性,那么当前logger将会继承上级的级别。
        addtivity:是否向上级logger传递打印信息。默认是true。
    -->
    <!--<logger name="org.springframework.web" level="info"/>-->
    <!--<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>-->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
     -->
    <!--
        root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
        不能设置为INHERITED或者同义词NULL。默认是DEBUG
        可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->
    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <logger name="com.lg.emp.controller" level="error"/>
    </springProfile>
    <!--root logger 配置    -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="DEBUG_FILE"/>
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="WARN_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>
    <!--生产环境:输出到文件-->
    <!--<springProfile name="pro">-->
    <!--<root level="info">-->
    <!--<appender-ref ref="CONSOLE" />-->
    <!--<appender-ref ref="DEBUG_FILE" />-->
    <!--<appender-ref ref="INFO_FILE" />-->
    <!--<appender-ref ref="ERROR_FILE" />-->
    <!--<appender-ref ref="WARN_FILE" />-->
    <!--</root>-->
    <!--</springProfile>-->
</configuration>
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
@@ -22,9 +22,9 @@
//        generator.username="sa";
//        generator.password="Zoneyung@zy56$";
        generator.table = "sys_pda_role_menu";
        generator.tableDesc = "PDA权限";
        generator.packagePath = "com.vincent.rsf.server.manager";
        generator.table = "sys_matnr_role_menu";
        generator.tableDesc = "物料权限";
        generator.packagePath = "com.vincent.rsf.server.system";
        generator.build();
    }
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
@@ -54,6 +54,7 @@
                        "sys_menu",
                        "sys_pda_role_menu",
                        "sys_menu_pda",
                        "sys_matnr_role_menu",
                        "man_loc_type_rela",
                        "man_qly_inspect_result",
                        "view_stock_manage",
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MatnrGroup.java
@@ -60,6 +60,8 @@
    @ApiModelProperty(value= "上级分类ID")
    private Long parentId;
    private Integer sort;
    /**
     * 状态 1: 正常  0: 冻结  
     */
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/MatnrRoleMenuController.java
New file
@@ -0,0 +1,68 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.common.utils.Utils;
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.common.annotation.OperationLog;
import com.vincent.rsf.server.manager.entity.MatnrGroup;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.vincent.rsf.server.manager.service.MatnrGroupService;
import com.vincent.rsf.server.system.controller.param.RoleScopeParam;
import com.vincent.rsf.server.system.entity.MatnrRoleMenu;
import com.vincent.rsf.server.system.entity.PdaRoleMenu;
import com.vincent.rsf.server.system.service.MatnrRoleMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class MatnrRoleMenuController extends BaseController {
    @Autowired
    private MatnrRoleMenuService matnrRoleMenuService;
    @Autowired
    private MatnrGroupService matnrGroupService;
    @GetMapping("/roleMatnr/scope/list")
    public R scopeList(@RequestParam Long roleId) {
        return R.ok().add(matnrRoleMenuService.listStrictlyMenuByRoleId(roleId));
    }
    @PostMapping("/menuMatnrGroup/tree")
    public R tree(@RequestBody Map<String, Object> map) {
        List<MatnrGroup> menuList = matnrGroupService.list(new LambdaQueryWrapper<MatnrGroup>().orderByAsc(MatnrGroup::getSort));
        List<MatnrGroup> treeData = Utils.toTreeData(menuList, 0L, MatnrGroup::getParentId, MatnrGroup::getId,
                MatnrGroup::setChildren);
        if (!Cools.isEmpty(map.get("condition"))) {
            Utils.treeRemove(treeData, String.valueOf(map.get("condition")), MatnrGroup::getName, MatnrGroup::getChildren);
            Utils.treeRemove(treeData, String.valueOf(map.get("condition")), MatnrGroup::getName, MatnrGroup::getChildren);
        }
        return R.ok().add(treeData);
    }
    @PreAuthorize("hasAuthority('system:role:update')")
    @OperationLog("Assign Permissions")
    @PostMapping("/roleMatnr/scope/update")
    @Transactional
    public R scopeUpdate(@RequestBody RoleScopeParam param) {
        Long roleId = param.getId();
        List<Long> menuIds = new ArrayList<>(param.getMenuIds().getChecked());
        menuIds.addAll(param.getMenuIds().getHalfChecked());
        matnrRoleMenuService.remove(new LambdaQueryWrapper<MatnrRoleMenu>().eq(MatnrRoleMenu::getRoleId, roleId));
        for (Long menuId : menuIds) {
            if (!matnrRoleMenuService.save(new MatnrRoleMenu(roleId, menuId))) {
                throw new CoolException("Internal Server Error!");
            }
        }
        return R.ok("Assign Success");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/MatnrRoleMenu.java
New file
@@ -0,0 +1,50 @@
package com.vincent.rsf.server.system.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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.system.service.UserService;
import com.vincent.rsf.server.system.entity.User;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("sys_matnr_role_menu")
public class MatnrRoleMenu implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value= "")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value= "")
    private Long roleId;
    @ApiModelProperty(value= "")
    private Long menuId;
    public MatnrRoleMenu() {}
    public MatnrRoleMenu(Long roleId,Long menuId) {
        this.roleId = roleId;
        this.menuId = menuId;
    }
//    MatnrRoleMenu matnrRoleMenu = new MatnrRoleMenu(
//            null,    // [非空]
//            null    // [非空]
//    );
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/MatnrRoleMenuMapper.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.server.system.mapper;
import com.vincent.rsf.server.system.entity.MatnrRoleMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface MatnrRoleMenuMapper extends BaseMapper<MatnrRoleMenu> {
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/MatnrRoleMenuService.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.MatnrRoleMenu;
import java.util.List;
public interface MatnrRoleMenuService extends IService<MatnrRoleMenu> {
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/MatnrRoleMenuServiceImpl.java
New file
@@ -0,0 +1,17 @@
package com.vincent.rsf.server.system.service.impl;
import com.vincent.rsf.server.system.mapper.MatnrRoleMenuMapper;
import com.vincent.rsf.server.system.entity.MatnrRoleMenu;
import com.vincent.rsf.server.system.service.MatnrRoleMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("matnrRoleMenuService")
public class MatnrRoleMenuServiceImpl extends ServiceImpl<MatnrRoleMenuMapper, MatnrRoleMenu> implements MatnrRoleMenuService {
    @Override
    public List<Long> listStrictlyMenuByRoleId(Long roleId) {
        return baseMapper.listStrictlyMenuByRoleId(roleId);
    }
}
rsf-server/src/main/java/matnrRoleMenu.sql
New file
@@ -0,0 +1,23 @@
-- save matnrRoleMenu record
-- mysql
insert into `sys_menu` ( `name`, `parent_id`, `route`, `component`, `type`, `sort`, `tenant_id`, `status`) values ( 'menu.matnrRoleMenu', '0', '/system/matnrRoleMenu', 'matnrRoleMenu', '0' , '0', '1' , '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Query 物料权限', '', '1', 'system:matnrRoleMenu:list', '0', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Create 物料权限', '', '1', 'system:matnrRoleMenu:save', '1', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Update 物料权限', '', '1', 'system:matnrRoleMenu:update', '2', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Delete 物料权限', '', '1', 'system:matnrRoleMenu:remove', '3', '1', '1');
-- locale menu name
matnrRoleMenu: 'MatnrRoleMenu',
-- locale field
matnrRoleMenu: {
    roleId: "roleId",
    menuId: "menuId",
},
-- ResourceContent
import matnrRoleMenu from './matnrRoleMenu';
case 'matnrRoleMenu':
    return matnrRoleMenu;
rsf-server/src/main/resources/mapper/system/MatnrRoleMenuMapper.xml
New file
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vincent.rsf.server.system.mapper.MatnrRoleMenuMapper">
    <select id="listStrictlyMenuByRoleId" resultType="java.lang.Long">
        select sm.id
        from man_matnr_group sm
        left join sys_matnr_role_menu srm on sm.id = srm.menu_id
        where 1=1
        and sm.deleted = 0
        and srm.role_id = #{roleId}
        <!--
        and sm.id not in (
            select sm.parent_id
            from sys_menu sm
            inner join sys_role_menu srm on sm.id = srm.menu_id
            and srm.role_id = #{roleId}
        )
        -->
        order by sm.sort
    </select>
</mapper>