15 KiB
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"节点。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()拿总数,空页回退单独 countPOST /api/data/loaddatabysql— 执行调用方传入的完整SELECTPOST /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→分组+行)、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 结构所波及的既有测试文件要同步更新到与新实现一致;若被改动前已存在的缺失文件、旧测试或环境问题阻断,在结果中明确记录,不要为通过检查去改无关代码。