--- name: fms-dev description: > FMS (模块/数据管理平台) 全栈开发技能。本项目后端为 Spring Boot + SqlServer 通用数据接口架构, 前端为 Vue 3 + antdv-next + Pinia。本技能描述项目约定、通用组件用法、API 模式、 数据库规范及常见开发任务的标准操作。当需要新增页面、新增后端查询/保存逻辑、创建数据库迁移脚本、 或修改 FMS 现有功能时使用此技能。 agent_created: true --- # FMS 开发技能 ## 项目概览 FMS 是一个**配置驱动的低代码数据管理平台**,核心设计理念是:后端不新增业务接口,一切数据读写走少量通用接口;前端用元数据(模块/字段/按钮/多语言)动态拼出页面。 - `fms-api/` — Spring Boot 3.x + Java 21 + Maven,端口 8088,context-path `/api` - `fms-vue/` — Vue 3 + Vite + Pinia + antdv-next + stk-table-vue,开发端口 5180 - 数据库:SQL Server,多机构独立库,按 JWT 中 orgId 路由 - 连接池:Alibaba Druid,每机构一个 ## 触发场景 - 新增/修改前端页面(数据浏览页、CRUD 页、复杂自定义页) - 新增/修改后端通用接口调用逻辑 - 创建数据库迁移脚本 - 修改模块管理(s_module / s_module_field 等元数据) - 新增文件上传/下载功能 - 排查保存或查询问题 - 任何需要理解本项目独特约定的开发任务 ## 核心铁律(必须遵守) 1. **不新增业务专用后端接口**。所有数据操作复用 `DataController` 通用端点。 2. **不新增前端 API 地址**。`services/api.js` 封装的函数是唯一入口。 3. **改动 scope 到当前任务**,不顺手重构无关代码。 4. **改完不要自己跑 build/启动服务**,除非用户明确要求。测试验证(`mvnw test`/`pnpm fmt:check`)可以。 5. **bigint 雪花 ID 必须以字符串返回前端**(JavaScript 无法安全表示 64 位整数)。 6. **表名/字段名只允许 `[A-Za-z_][A-Za-z0-9_]*`**,后端 `DbUtils` 会强校验。 7. **迁移脚本必须可重复执行**(`IF NOT EXISTS`),不修改已有业务表。 --- ## 后端架构速查 ### 通用数据端点 | 端点 | 用途 | 参数关键字段 | |------|------|-------------| | `POST /data/loaddata` | 条件查询 | `view_name`, `search_condition`, `order_by`, `search_columns` | | `POST /data/page` | 分页查询 | `view_name`, `order_by`, `page_no`, `page_size`, `search_condition`, `search_columns` | | `POST /data/saveobjt` | 跨表事务保存 | `[{table, key_field, inserts[], updates[], deletes[]}]` | | `GET /data/nextid` | 雪花 ID | `?count=N`(默认1) | | `POST /data/loaddatabysql` | 自定义 SQL | `{sql}` — **仅 SELECT/WITH 开头,禁止 DML** | | `POST /data/describe` | 表结构 | `{table}` | | `GET /data/nextcode` | 自动编号 | `?moduleId=N&count=N` | ### 保存引擎 (DataSaveService) `saveobjt` 在一个**手动 JDBC 事务**中执行,顺序:**delete → update → insert**。任一表失败触发 `connection.rollback()`。 关键行为: - 主键值 `< 0` 或 `null` → 自动分配雪花 ID - `key_field` 值从 UPDATE SET 中排除(作为 WHERE 条件) - INSERT 只写入 `writable=true` 且在数据中的列 - 要求每条 INSERT/UPDATE/DELETE 影响行数恰好为 1 ### 多机构路由 ``` JwtAuthFilter → OrgContext.setOrgId() → OrgRoutingDataSource → OrgDataSourceManager.getConnection() → DruidDataSource[orgId] → 对应 SQL Server 实例 ``` 机构数据库配置文件:`config/dbconfigs/{ORG_ID}.properties`(git-ignored)。 ### 异常与响应 | 异常类型 | code | 用途 | |---------|------|------| | `AuthenticationException` | 401 | 认证失败 | | `BusinessException` | 1000 | 业务校验失败,message 直接展示 | | `SaveObjectException` | 2001 | 保存失败,带 `{table, action, index}` | | `SQLException` / 其他 | 500 | 统一"系统异常" | 统一响应:`ApiResponse(int code, String message, T data)`,`code == 0` 表示成功。 ### 后端包结构速查 ``` cn.g3soft.fmsapi ├── config/ JwtAuthFilter, 配置属性 ├── controller/ DataController, AuthController, FileController ├── database/ 多机构路由、OrgContext(ThreadLocal) ├── exception/ 异常类 + GlobalExceptionHandler ├── service/ DataService, DataSaveService, AuthService, FileService └── utils/ DbUtils(核心), JwtUtils, ApiResponse, IdGenerator(雪花) ``` --- ## 前端架构速查 ### HTTP 层 `services/http.js`:axios 实例,baseURL `/api`,自动注入 Bearer Token,拦截 `code !== 0` 转为 Error。认证失效(401/40101)自动跳转登录。 ### API 服务层 `services/api.js` 封装全部通用接口调用——这是前端与后端交互的**唯一通道**: ```js import { loadDataApi, pageDataApi, saveObjectApi, nextIdApi, describeApi, loginApi } from '@/services/api'; ``` 所有 API 返回 `{ success, code, message, data }`,其中 `success` 为前端补充(`code === 0`)。 ### 国际化 不走 vue-i18n,翻译文本来自数据库 `b_i18n` 表。`app` store 的 `t(key, fallback)` 函数做翻译。多语言键规范:`field.{moduleCode}.{fieldCode}`、`menu.{moduleCode}`、`power.{moduleCode}.{powerCode}`。 ### 权限 `stores/permissions.js` 从 `b_user_module` / `b_user_power` / `s_module_power` 加载权限。`canAccess(moduleId)` 判断页面权限,`canPower(scopeCode, powerCode)` 判断操作权限。特殊账号 `g3soft` 为超级管理员,跳过所有权限检查。 ### 核心组件 #### FmsModuleListPage(数据列表页) **最常用的页面组件——大多数 CRUD 页面只需配置一个 data-code 即可运行。** Props 速查: | Prop | 用途 | |------|------| | `data-code` | 数据模块编码(必填),对应 `s_module.b_code` | | `fixed-search-condition` | 固定查询条件(不含 WHERE 关键字) | | `row-actions` | 行操作按钮 `[{key, label, onClick}]` | | `editable` | 可编辑模式(行内编辑+新增行) | | `row-draggable` | 行拖拽排序 | | `page-size` | 每页条数(默认 20) | | `column-extensions` | 列自定义(formatter, clickable 等) | Slots:`#actions`(工具栏按钮区)、`#actions="{ saveChanges, dirtyCount, saving }"`(可编辑模式工具栏) 暴露方法:`reload()`, `saveChanges()`, `addRow()`, `initialize()`, `resetColumns()` #### FmsTree(树形组件) Prop `treeData` 接收扁平数组,自动构建树。支持虚拟滚动、拖拽、右键菜单。自动通过 `b_i18n` 做标题翻译。 ### 路由与页面注册 1. 在 `router/routes.js` 添加路由配置,使用静态 `import()` 注册组件 2. `meta.moduleId` 指定权限检查的模块 code 3. `meta.title` 指定页面标题 ```js { path: 'my-feature', name: 'my-feature', component: () => import('@/views/my-feature/index.vue'), meta: { title: '我的功能', moduleId: 'my_feature_code' }, } ``` --- ## 后端开发模式 ### 模式 1:新增简单查询端点 在现有 Controller(通常是 `DataController` 或 `FileController` 同级)中添加方法: ```java @PostMapping("/my/query") public ApiResponse myQuery(@RequestBody Map params) throws SQLException { String viewName = ParamUtils.getRequiredString(params, "view_name"); String condition = ParamUtils.getString(params, "search_condition"); // 业务逻辑 + 调用 dataService.loadData / dbUtils.loadData return ApiResponse.success(result); } ``` ### 模式 2:新增保存端点 封装数据为 `[{table, key_field, inserts[], updates[], deletes[]}]` 格式,调用 `dataSaveService.save(requests)`。 ```java List> requests = new ArrayList<>(); Map req = new HashMap<>(); req.put("table", "my_table"); req.put("key_field", "b_id"); req.put("inserts", newList); dataSaveService.save(requests); ``` ### 模式 3:自定编号生成 调用 `dataSaveService.nextCode(moduleId, count)`,返回 `List`。 --- ## 前端开发模式(按复杂度排序) ### 模式 A:纯数据列表页(零代码) 适用于只需展示已有数据模块的页面。 ```vue ``` ### 模式 B:带行操作和自定义工具栏 ```vue ``` ### 模式 C:可编辑表格(行内编辑) ```vue ``` ### 模式 D:完全自定义页面(复杂场景) 直接使用 `loadDataApi`、`saveObjectApi`、`nextIdApi` + 原生 antdv-next 组件。需自行管理数据加载、修改追踪(使用 `utils/dataChanges.js` 的 `diffRows`/`cloneData`)、ID 分配(`utils/tempId.js` 的 `createTempId` + `allocateTemporaryIds`)。 关键工具函数: - `cloneData(obj)` — JSON 深拷贝,存储原始数据快照 - `diffRows(current, previous)` — 对比得到 `{inserts, updates, deletes}` - `allocateTemporaryIds(rowGroups)` — 负号临时 ID 替换为真实雪花 ID - `tableChange(table, changes)` — 生成 `{table, key_field, ...changes}` 结构 --- ## 数据库迁移 ### 脚本命名 `fms-api/config/migrations/NNN_description.sql`,三位数字递增。 ### 脚本模板 ```sql SET XACT_ABORT ON; BEGIN TRANSACTION; -- 描述:创建 xxx 表 IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'xxx') BEGIN CREATE TABLE [xxx] ( [b_id] BIGINT NOT NULL PRIMARY KEY, [b_name] NVARCHAR(200) NOT NULL, -- ... ); END; COMMIT; ``` 关键规则: - 使用 `XACT_ABORT ON` + 显式 `BEGIN/COMMIT TRANSACTION` - 所有 DDL 用 `IF NOT EXISTS` 包裹 - 不删除或修改已有业务表 - 表名/列名使用小写或驼峰,统一 - 大字段主键用 `BIGINT` ### 执行 使用 PowerShell `.NET SqlClient`(本机 SQL Server 兼容性),参照 `fms-api/tools/migration/` 下的工具脚本。 --- ## 代码约定细节 ### 后端 - Controller 方法声明 `throws SQLException`,返回 `ApiResponse` - Service 注入 `DataSource dataSource`(自动多机构路由)+ `DbUtils dbUtils` - 参数提取用 `ParamUtils.getRequiredString/getString/getList` - ID 生成用 `IdGenerator.nextId()` - 当前用户 ID 用 `OrgContext.getUserId()`,机构 ID 用 `OrgContext.requireOrgId()` - 业务失败抛 `BusinessException("消息")` ### 前端 - SFC 使用 `