Files
workspace/code/fms/CODEBUDDY.md
T
2026-09-27 21:59:13 +08:00

187 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"` 节点;表单的「行」也是显式节点(`type: "row"`,`edit` 为 schemaVersion 2)——行内 `sum(span) ≤ 24`,行不自动合并也不被宽度折行拆开,因此**没有 `newline` 属性**(v1 的平铺 + newline 已废弃,仅在加载时容错折行)。
- `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<String, Object>` 取参。
### 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→分组+行+`lines`)、`buildQueryRender`(query→条件)、`applyColumnSettings`(列宽/顺序回写)、`applyQueryLayout`(「保存为通用」回写默认查询布局)。运行时组件不需要感知 JSON 节点差异。
- 表单(edit)的行:JSON 是 `{type:'row', children:[字段节点...]}`;`buildEditRender` 给同一行的字段写相同的 `b_line` 并额外产出 `lines`,运行时按 `lines` / 行首标记(`index === 0 || b_line 与上一格不同` → `FormItem :line-start`)渲染。表单设计器(`design-editor/FormDesignEditor.vue`)内部同样以 `b_line` 维护行,`useFormGrid.js` 只提供行查询与 `repackLines`(超 24 格时拆行)。
- `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 结构所波及的既有测试文件要同步更新到与新实现一致;若被改动前已存在的缺失文件、旧测试或环境问题阻断,在结果中明确记录,不要为通过检查去改无关代码。