迁移:包名 cn.g3soft.fmsapi → cn.g3soft.erp;artifactId/name fms-api → erp-api;启动类 FmsApiApplication → ErpApiApplication;配置前缀 fms.* → erp.*;FMS_JWT_SECRET → ERP_JWT_SECRET;未带入流程引擎(service/workflow、WorkflowController*)、数据库迁移脚本(config/migrations、tools/migration)与 data/files;一并迁入 Maven Wrapper、config/dbconfigs、config/legacy-tls.security、start.cmd。
重构:拆解 utils.DbUtils(1470 行)为 common.util.SqlText + database.{MetadataReader,RowReader,RowWriter} + database.model(5 个 record);认证专用账号查询移出访问层到 service.auth.UserAccountReader;新建 common/(ApiResponse + exception 6 + util 5 + util/snowflake 9);database/ 分 org/(多机构连接)与 model/(纯数据载体);JwtUtils 与客户端信息采集簇归入 service.auth。
清理:删除迁移带入的空目录、HELP.md,及 pom 中 Initializr 留下的空 url/licenses/developers/scm;修正 OrgDataSourceFactoryTests 的陈旧断言(停留在 2026-07 的旧策略,实现已于 2026-08 反转为继续重试、不停建连线程、不 failFast)。
验证:mvn test 80/80 通过;另有 2 个 @SpringBootTest 需真实 SQL Server。本次仅提交 server/;根 .gitignore、根 .gitattributes 与 docs/ 的同步改动不在本次范围内。
server —— 后端
后端服务(Maven + Spring Boot)。现有代码是从原型 fms-api 迁入的元数据驱动引擎实现:
包名 cn.g3soft.erp,artifactId erp-api,启动类 ErpApiApplication。
构建 mvnw.cmd test;启动 start.cmd(spring-boot:run,默认 8088、context-path /api)。
迁入时未带入三项:流程引擎(
service/workflow、WorkflowController、WorkflowRuntimeController)、数据库迁移脚本(config/migrations、tools/migration)、 本地文件存储数据(data/files)。
已定的技术栈
| 项 | 选型 |
|---|---|
| 框架 | Spring Boot 4.1.0 |
| 语言 | Java 21 |
| Web | spring-boot-starter-webmvc |
| 持久层 | spring-boot-starter-jdbc(无 ORM) |
| 连接池 | Druid 1.2.18 |
| 数据库 | SQL Server(mssql-jdbc) |
| 鉴权 | JJWT 0.12.6 |
| Excel | Apache Fesod 2.0.2-incubating |
| 其他 | ip2region、ua-parser |
| 构建 | Maven |
IP 归属地
登录日志里的 IP 归属地走本地 ip2region 库,不联网。实测:加载 57ms,单次查询 14 微秒,
所以它不构成启动瓶颈(应用自身启动约 1.2~1.7s)。
部署须知:
ip2region.xdb(约 11MB)被.gitignore排除,不在仓库里,部署时必须手工放到src/main/resources/。漏放不会导致启动失败,只会让所有归属地静默变成「未知」。
内网 / 保留地址(RFC1918、回环、链路本地、IPv6 ULA)直接记「内网」,不外呼。
数据访问层(cn.g3soft.erp.database)
database 下分两块,两者零互相引用(一边管"连接怎么来",一边管"拿到连接后怎么读写"):
| 包 / 类 | 职责 |
|---|---|
database/(根) |
通用数据访问,方法都走 JDBC、无 ORM |
database.MetadataReader |
表结构内省:loadTableMetadata / getColumn / describeColumns;列名归一化也在这里 |
database.RowReader |
只读查询与分页:loadData / loadDataBySql / page;唯一持有默认数据源 |
database.RowWriter |
通用写入与条件更新语义:insert / update / delete、$original / $expr / guard、零行原因定位 |
database.model |
纯数据载体,无任何逻辑:TableMetadata / ColumnMetadata / PageResult / GuardCondition / ZeroRowCause |
database.org |
多机构数据库连接:OrgContext(请求级机构/用户上下文)、OrgDatabaseProperties / OrgDatabaseConfig*(配置与热加载)、OrgDataSourceFactory / OrgDataSourceManager / OrgRoutingDataSource(装配与路由) |
common.util.SqlText |
SQL 文本与安全校验:标识符校验与加引号、只读 SQL 校验、排序子句构建;纯静态 |
三条约定:
SqlText不依赖任何人,所以依赖方向固定为service → database → common,不成环。RowReader之外的方法都接收调用方传入的Connection(不自己开事务),事务边界留在服务层。- 认证要按
orgId取该机构连接(登录时机构上下文尚未建立),所以那条查询在service.auth.UserAccountReader里,不在访问层。
分割现状与规划
现状(迁入的原型代码,已按职责归过一轮):
cn.g3soft.erp
├── common/ # 通用返回、异常、纯静态工具
│ ├── ApiResponse.java # HTTP 响应外壳 + 全部错误码常量
│ ├── exception/ # 6 个异常与 GlobalExceptionHandler
│ └── util/ # ParamUtils / SqlText / StringUtils / TimeUtils /
│ # UuidV7Generator + snowflake/(内嵌的雪花 ID 生成器)
├── config/ # AuthProperties / FileStorageProperties / JacksonConfig / JwtAuthFilter
├── controller/ # 6 个 Controller
├── database/ # 数据访问层(见上):RowReader / RowWriter / MetadataReader + model/ + org/
└── service/
├── auth/ # 登录、会话、登录日志、JwtUtils,以及 client/(客户端信息采集)
├── data/ # 通用保存 / 删除 / 查询编排
├── export/ file/ formula/ permission/
规划(新代码按此落位,老代码后续按域重构):
cn.g3soft.erp
├── common/ # 通用返回、异常、工具 —— 已建
├── engine/ # 元数据驱动引擎(查询 / 保存 / 权限)—— 未设计;
│ # 现在的 database + service/{data,formula,permission} 是它的雏形
└── module/
├── ocean/ # 海运
├── air/ # 空运
├── container/ # 箱管
├── finance/ # 财务
├── crm/ # CRM
└── system/ # 系统管理
与前端的关系
唯一契约是 HTTP 接口。 前端不读后端源码,后端不管前端怎么调。
同仓的三条纪律(见根 README.md):
- 独立构建 ——
mvn package不触发前端构建,反之亦然 - 独立版本 —— tag 分开打(
api-v1.2.0/web-v1.2.0) - 接口契约写进
docs/并版本化