Files
2026-09-27 21:59:13 +08:00

16 KiB
Raw Permalink Blame History

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/ 下)

./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)

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% 阈值

数据库脚本与迁移工具

# 执行 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 结构所波及的既有测试文件要同步更新到与新实现一致;若被改动前已存在的缺失文件、旧测试或环境问题阻断,在结果中明确记录,不要为通过检查去改无关代码。