# CODEBUDDY.md This file provides guidance to CodeBuddy Code when working with code in this repository. ## 仓库结构 | 目录 | 说明 | | --- | --- | | `fms-api/` | 后端,Spring Boot 4.1 + Java 21,JDBC + Druid,无 ORM | | `fms-vue/` | 当前前端,Vue 3.5 + Vite 8 + Pinia 4 + pnpm,JS(非 TS) | | `fms-vue-old1/`、`fms-vue-old2/` | 旧版前端副本,仅作历史参考(`old2` 是权限/登录逻辑的来源),**不要改** | | `sql/` | 建表 / 视图 / 模块注册脚本,由 `RunSqlFile` 执行 | | `fms-api/tools/migration/` | 独立单文件 Java 迁移工具,`javac` 手动编译,不走 Maven | | `fms-api/config/migrations/tools/` | 早期一次性数据迁移程序(历史) | | `参考组件库/` | `antdv-next`、`shadcn` 源码,前端组件实现的参照 | | 根目录 `*.md` | 设计文档(中文):`FMS新系统核心表结构设计.md`(权威表结构)、`FMS业务表设计.md`、`开发规范.md`(AI 工作规范)、`Fms旧系统表结构.md` | 真实数据库是远端 SQL Server 2022(`118.89.70.199:1433`);旧库 `G3HY2025`,新库 `FMS`。 ## 常用命令 ### 后端(在 `fms-api/` 下) ```bash ./start.cmd # 设置 JAVA_HOME=jdk-21 / Maven 3.8.9 后 spring-boot:run ./mvnw spring-boot:run # 等价;需自行准备 JDK 21 ./mvnw test # 全部测试 ./mvnw -Dtest=DataSaveServiceTests test # 单个测试类 ./mvnw -Dtest=JwtUtilsTests#methodName test # 单个测试方法 # 连接只支持 TLS 1.0 的旧库时: mvn "-Dspring-boot.run.jvmArguments=-Djava.security.properties=./config/legacy-tls.security" spring-boot:run ``` 服务端口 `8088`,`server.servlet.context-path=/api`。 ### 前端(在 `fms-vue/` 下,用 pnpm) ```bash pnpm dev # Vite dev server,端口 5082,/api 代理到 http://127.0.0.1:8088 # 用 VITE_PROXY_TARGET 可指向另一个后端实例 pnpm build pnpm lint # oxlint . --deny-warnings pnpm lint:fix pnpm fmt # oxfmt --write . pnpm fmt:check pnpm check # lint + fmt:check + build pnpm test # vitest run pnpm test:file tests/views/login.spec.js # 单文件(也可靠目录:tests/stores) pnpm test:watch pnpm test:coverage # 只有这个会检查 60% 阈值 ``` ### 数据库脚本与迁移工具 ```bash # 执行 sql/ 下脚本(按 GO 分批,单事务,catalog 必须是 FMS) cd fms-api java -Dstdout.encoding=UTF-8 -cp "tools/migration/mssql-jdbc-13.4.0.jre11.jar" \ tools/migration/RunSqlFile.java ../sql/fms_core.sql --dry-run # 去掉 --dry-run 执行;其余工具(FieldNameSync / SchemaDumpMarkdown / OtherDataMigrate)见 # fms-api/tools/migration/README.md,编译时需 -encoding UTF-8 ``` ## 架构要点 ### 1. 元数据驱动的「SQL 核心」 系统是 SQL 驱动的内部 ERP,模块差异靠**配置数据**表达,不为单个页面复制模块专用 SQL。 核心表(定义见 `FMS新系统核心表结构设计.md`,建表见 `sql/fms_core.sql`): - `s_module` — 模块树。`b_id` 是业务编码主键(非雪花),`b_parent_id` 是权威父子关系,`b_depth`/`b_path` 是派生字段;`b_module_type` ∈ `module` / `data` / `virtual`;查询来源优先级 `b_query_sql` > `b_view_table`;保存目标 `b_save_table`。 - `s_field` — 字段业务元数据,主键 `(b_module_id, b_field)`。只存业务类型 `b_type`、名称、多语言键;**不存**数据库类型/长度/精度,需要时现场读表结构。 - `s_module_schema` — 界面配置,主键 `(b_module_id, b_schema_type)`,`b_schema_type` ∈ `view` / `edit` / `query`,内容整份存 `b_schema_json`。字段分组不再建表,是 JSON 里的 `type: "group"` 节点。 - `s_user_module_pref` — 个人层覆盖,只存相对系统默认的增量;读取顺序「个人 > 系统默认」。 - `s_autocode`、`s_relation`、`s_i18n` / `s_i18n_type`、`s_menu`、日志表。 - 权限:`s_power`(权限点)、`s_user_power`、`s_user_field_power`、`s_user_data_power`。菜单/模块/动作权限统一收在 `s_power`,字段权限和数据范围不进 `s_power`。 表结构硬约束:不使用外键、`CHECK`、触发器;树结构(模块树/菜单树/部门树)用「邻接表 + `b_depth`/`b_path` 冗余」,移动节点时同事务更新全部后代。主键类型取决于表:配置表用业务编码,业务子表用 `bigint` 雪花。 ### 2. 后端通用数据接口(`DataController`) 前端所有业务数据都走这组通用接口,没有按页面定制的接口: - `POST /api/data/loaddata` — 按表/视图 + `search_condition` + `order_by` + `search_columns` 查询 - `POST /api/data/page` — 分页(`page_no`/`page_size`),内部包一层 `COUNT_BIG(1) OVER()` 拿总数,空页回退单独 count - `POST /api/data/loaddatabysql` — 执行调用方传入的完整 `SELECT` - `POST /api/data/saveobjt` — 单个事务内保存多张表的变更请求 - `POST /api/data/nextid` / `nextcode` — 雪花 ID / 按 `s_autocode` 规则取业务编号 - `POST /api/data/describe` — 读表或视图列结构 `DbUtils`(`fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java`)是 SQL 生成与执行的唯一出口: - 标识符一律经 `SAFE_IDENTIFIER` 校验并加 `[...]`,排序字段只允许 `field [ASC|DESC]` 或 `CASE ... END`。 - `validateReadOnlySql()` 强制只读:必须以 `select`/`with` 开头,剔除字符串字面量后禁止 `;`、注释和 `insert|update|delete|merge|drop|...` 关键字。**调用方传入的 SQL 与 `b_query_sql` 同一个入口,都只允许查询**;写入必须走 `DataSaveService`。 - 通用增删改按 `ResultSetMetaData` 现场解析列,`_` 前缀派生列不参与写入。 `DataSaveService` 支持多列业务键(如 `s_field` 的 `(b_module_id, b_field)`),并按 `s_autocode` 生成业务编号。`ParamUtils` 负责从 `Map` 取参。 ### 3. 多机构数据源与鉴权 - `AuthController /auth/login` 用 `orgid + userid + password` 登录(`b_user.b_id` 即账号,密码明文保存),签发 JWT;`ActiveSessionRegistry` 保证同一账号单会话,被顶号返回 `40101`。 - `JwtAuthFilter` 校验 token + 会话后写入 `OrgContext`(ThreadLocal),异常一律以 HTTP 200 + 业务 code 返回。 - `OrgDataSourceManager` 按机构码懒加载 Druid 连接池,配置文件 `fms-api/config/dbconfigs/{ORG_ID}.properties`(该目录已 gitignore);`OrgDatabaseConfigWatcher` + 定时核对支持改配置不重启、旧池延迟关闭。**读写业务数据必须经它取连接**,不要直接用注入的默认 `DataSource`(迁移工具除外)。 ### 4. 前端运行时装配 - `src/router/routes.js` 只声明路由表与派生纯数据(`collectMenuRoutes` / `menuRouteOptions` 供菜单管理下拉用);`src/router/index.js` 只建实例 + 守卫。分离是为了避免「页面 → router/index → 守卫 → store」循环依赖,不要把两者合并。 - 路由分组节点(`/module`)只写 `path + redirect + children`、**不写 component**,子页面才能继续渲染在 `DefaultLayout` 的同一个 `router-view` 上、保持 keep-alive 层级。 - 守卫里统一 `await` 权限、菜单、语言清单三家 store 的 `load()`;三者都按登录主体(`orgId:userId`)去重、并发复用同一请求、主体切换时 `clear()`。 - `views/module/**` 是 `meta.adminOnly`,仅管理员账号 `g3soft` 可进(路由守卫拦截 + 侧栏入口只在 `AppSidebar` 渲染),且不进 `s_menu`。 - 页面缓存:`layouts/DefaultLayout.vue` 里 keep-alive 外层 key = `fullPath`(稳定),内层 key = `fullPath:refreshKey`(双击页签只重挂内层)。**每个页签一个命名包装组件**,详情页按 `cacheKeyOf(fullPath)` 生成 name;包装组件渲染不能读当前路由,`fullPath` 只能经 props 传入。`src/stores/app.js` 的 `visitedTabs` / `routeRefreshKeys` 与之联动。 ### 5. 模块列表 / 编辑的运行时(元数据渲染) 业务页面不手写表格和表单,交给通用组件按模块配置渲染: ``` s_module_schema.b_schema_json ──schemaRender.js──▶ fms-module-list / fms-module-edit 的行/分组结构 ``` - `src/components/fms-module-common/schemaRender.js` — JSON→渲染结构适配层:`loadModuleSchemas`、`buildListConfig`(view→列)、`buildEditRender`(edit→分组+行)、`buildQueryRender`(query→条件)、`applyColumnSettings`(列宽/顺序回写)、`applyQueryLayout`(「保存为通用」回写默认查询布局)。运行时组件不需要感知 JSON 节点差异。 - `src/components/fms-module-common/queryUtils.js` — 查询条件 AST → SQL 片段。常用条件与高级条件共用一套 AST(条件项自带 `join` 表达且/或),`OPERATORS` 与操作符文案是唯一定义处;`astToSql` 是唯一出口。字段编码和值在这里做 `[...]` 与 `N'...'` 转义。 - `src/components/fms-module-list/FmsModuleListPage.vue` — 通用列表页容器,props 有 `dataCode`、`fixedSearchCondition`、`rowActions`、`rowSelection`、`editable`、`rowDraggable` 等;个人查询布局偏好走同目录 `useModulePref.js`(`s_user_module_pref`)。 - `src/components/ui/` — 自研 UI 组件库,统一从 `@/components/ui` 具名导入(含命令式 `Message`、`Modal.confirm`)。 ### 6. 模块管理页 = 配置中心 `src/views/module/module-management/index.vue` 是生产上述配置数据的页面:左模块树 + 右分节面板(基础/字段/列表/表单/查询/自动编码/权限/多语言),底层就是通用 `loadDataApi` / `saveObjectApi`。它按「编排层只持有数据、各面板 `v-model` 各管各的」组织: - 每份数据成对持有 `xxx`(当前值,面板绑定)与 `xxx_org`(数据库基线),如 `maindata / maindata_org`、`fields / fields_org`、`view_schema / view_schema_org`。禁止用 `drafts`/`originals` 两个聚合对象。 - 跨面板副作用(删分组后清理引用、自动编码字段在表单中自动只读)由编排层写幂等 `watch`。 - 模块配置表用业务键/联合主键,**不调用 `nextIdApi`**,保存时也不做临时 ID → 雪花 ID 转换;`_`/`v_` 前缀列是内部派生字段,提交前剥离。 - `src/views/module/menu-management/index.vue` 是同类页面(左菜单树 + 右面板),实现时以它和模块管理为参照。 ### 7. 文件服务 `FileController` 支持 `local` 与 `aliyun-oss` 两种存储(`fms.file.storage-type`)。本地上传到 `./data/files/{orgId}/{yyyy}/{MM}/{dd}/`,静态访问 `/api/file/static/{path}`(含路径穿越防护)。前端 `src/services/fileService.js` 按 `initFileConfig()` 拿到的模式自动分流,OSS 走「共享凭证 → XHR 直传 → save-record 回调」。 ## 编码约定 `开发规范.md` 是 AI 工作规范的权威来源,重点: - **不写 placeholder / 桩代码 / 待填充 TODO**。 - **后端接口优先通用**;要新增特殊接口必须先征得用户确认。 - **改前先读**,只改完成当前需求必需的文件;不擅自回滚或顺手重构无关代码。 - **能由 SQL 完成的(关联、过滤、排序、分页、聚合、权限过滤、数据范围)在 SQL 层完成**,不要先加载大量数据再在代码里处理。 - **权限过滤以后端 SQL 为准**:前端隐藏菜单/按钮/字段只是体验,模块操作权限、字段可见/可查询/可导出权限、数据范围都必须在后端查询或写入 SQL 中落实;用户个性化配置只能调布局,不能放宽权限。 - 固定 SQL 只用于稳定明确的特殊逻辑,且保持通用,不为单个页面复制一套接口。 命名分层(`开发规范.md` 有完整示例): - 前端 JS 逻辑标识符用 `camelCase`;后端返回字段、数据库字段、与表结构一一对应的业务数据用 `snake_case`。API 封装层是边界:请求体必须保留后端原字段名,不因前端改名而改接口契约。 - 方法名动词开头、语义完整(`loadModuleConfig` / `handleSave` / `buildSaveData` / `clearModuleData`),不用 `loadMod` / `dealData`。布尔值用 `is/has/can/should` 前缀,ID 用 `xxxId`,集合用复数。 - Vue 组件文件名 PascalCase;模板中 props / 事件 / `v-model` 用 kebab-case(`:module-data`、`@save-success`、`v-model:main-data`)。 - 公共函数只在满足条件时抽取(≥2 个真实调用点 / 逻辑较复杂 / 必须保证多处规则一致);只服务某个业务目录的函数放该目录的 `utils.js`,不进全局 `src/utils`;一个业务目录原则上只有一个 `utils.js`。 - Pinia 统一用 Setup Store(`defineStore('name', () => {}, options)`),一个 store 一个领域;只有确实需要跨刷新保留的字段才配 `persist` 并显式 `pick`(见 `src/stores/app.js`)。 表单页面统一主流程与命名,不要改名或调换顺序: ``` handleSave() 保存入口(saving 用 try/finally 复位) → beforeSave() 校验/同步/准备,返回 false 终止 → buildSaveData() 组装请求,可返回单请求/请求数组/null(可 async) → 调用 saveObjectApi → afterSave() 成功后重新加载、更新基线、清草稿 ``` 编辑页加载命名:`load()` 判断新建/编辑 → `loadNew()` / `loadEdit()`;不区分时用语义明确的 `loadXxx()`。 i18n:`module/` 下的管理界面文案固定中文、不接入业务多语言;模块名/字段名/JSON 布局节点等面向客户的资源维护 `s_i18n` 键。语言清单与当前界面语言统一由 `stores/i18n.js` 管理(`s_i18n_type`),默认语言按 `s_i18n_type.b_default` 判断,不取数组第一项。 ## 测试 前端测试约定见 `fms-vue/tests/README.md`(唯一权威): - 文件一律 `*.spec.js`,与被测源码同名,**一个源码文件最多一个 spec**;`describe` 与用例名用中文。 - 页面测试 mock `@/services/api` 这一个接缝并用 `mountPage`(`tests/helpers/app.js`);store 测试必须用 `createTestPinia()`,不要手动 `createPinia()`(pinia 4 下 persist 插件会静默失效)。 - `beforeEach` 必须 `localStorage.clear()` + `sessionStorage.clear()`;异步用 `flushPromises()` + `nextTick()`。 - 覆盖率门槛 60%(lines/functions/branches/statements),只在 `pnpm test:coverage` 下生效。 后端测试在 `fms-api/src/test/java/...`,用 `@SpringBootTest` / 纯 JUnit 类,`mvnw test` 运行。 改动后的验收(`开发规范.md`「变更验收」):开发阶段以功能可用为完成标准,**不运行测试、不跑构建检查**,测试由用户统一安排(等用户明确要求再一次性执行);但先全局搜索确认无旧路径/旧导出/旧方法名残留,被改动的类名、导出名、DOM 结构所波及的既有测试文件要同步更新到与新实现一致;若被改动前已存在的缺失文件、旧测试或环境问题阻断,在结果中明确记录,不要为通过检查去改无关代码。