# 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` 库**,不联网。实测:加载 5~7ms,单次查询 1~4 微秒, 所以它不构成启动瓶颈(应用自身启动约 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 校验、排序子句构建;纯静态 | 三条约定: 1. `SqlText` 不依赖任何人,所以依赖方向固定为 `service → database → common`,不成环。 2. `RowReader` 之外的方法都接收调用方传入的 `Connection`(不自己开事务),事务边界留在服务层。 3. 认证要按 `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`): 1. **独立构建** —— `mvn package` 不触发前端构建,反之亦然 2. **独立版本** —— tag 分开打(`api-v1.2.0` / `web-v1.2.0`) 3. **接口契约写进 `docs/`** 并版本化