--- title: 后端架构 description: g3soft-erp 后端的实际结构:技术栈、包组织、多机构数据源路由、通用数据接口、权限与工作流 --- # 后端架构 本文描述 `g3soft-erp/server` **当前的实现**(不是规划稿)。所有内容以代码为准,涉及取舍的地方都写明原因。 ## 1. 技术栈 | 项 | 选型 | |---|---| | 框架 | Spring Boot 4.1.0 / Java 21 | | 数据访问 | `spring-boot-starter-jdbc` + **纯手写 SQL**,**无 ORM** | | 连接池 | Druid | | 数据库 | SQL Server(2022) | | 鉴权 | JJWT | | 导出 | Apache Fesod + POI | | 参数校验 | 自研 `ParamUtils` / `RequestParams` | **为什么不用 ORM**:业务查询大量依赖视图、动态条件、聚合与按机构分库,ORM 的映射层在这些场景反而成为阻碍。查询与写入都直接落到 SQL。 ## 2. 包组织 ``` cn.g3soft.erp ├── common/ 通用设施 │ ├── ApiResponse 统一响应包体 │ ├── exception/ 业务异常 + 全局异常处理 │ └── util/ 参数、SQL 文本、时间、UUIDv7、雪花 ID ├── config/ 过滤器与配置属性 │ ├── JwtAuthFilter JWT 鉴权 │ ├── RequestLoggingFilter 访问日志(MDC) │ └── *Properties 按功能分组的配置绑定 ├── controller/ 接口层(6 个控制器) ├── database/ 数据访问基础设施 │ ├── MetadataReader 表结构内省 │ ├── RowReader 查询 │ ├── RowWriter 写入 │ ├── model/ 元数据模型 │ └── org/ 多机构数据源路由 └── service/ 业务服务 ├── auth/ 登录、JWT、会话、登录日志 ├── data/ 通用数据查询 / 保存 / 删除 ├── export/ 导出 ├── file/ 附件 ├── formula/ 公式引擎 ├── permission/ 权限判定 └── workflow/ 工作流引擎 ``` 分层约定:**controller 只做参数接收与响应包装**,业务逻辑一律在 service;SQL 相关的底层能力集中在 `database/`,由各 service 复用。 ## 3. 多机构数据源路由 一套部署服务多个机构,每个机构一个独立数据库。 ```text 请求 → JwtAuthFilter 解析 JWT,写入 OrgContext(ThreadLocal: orgId / userId) → 业务代码注入 @Primary 的 OrgRoutingDataSource → 按 OrgContext.requireOrgId() 路由到该机构的连接池 ``` | 组件 | 职责 | |---|---| | `OrgContext` | `ThreadLocal` 保存当前请求的机构码与用户编码 | | `OrgRoutingDataSource` | `@Primary` 数据源,每次取连接时按机构路由 | | `OrgDataSourceManager` | 按机构懒加载并缓存连接池 | | `OrgDatabaseConfigLoader` | 读取 `config/dbconfigs/{机构码}.properties` | | `OrgDatabaseConfigWatcher` | 监听配置目录,改动后热切换连接池 | **几个刻意的做法**: - **不依赖 Spring 的事务抽象**:全项目不使用 `@Transactional`,事务边界由 service 手动控制(`setAutoCommit(false)` → `commit` / `rollback`)。 - **连接池配置热更新**:配置文件变更后重建连接池,旧池延迟关闭,保证在途请求有时间归还连接。 - **建池即验活**:`initial-size: 1`,配置指向不可达的库时快速失败并保留旧池,不会把整个机构打挂。 - **后台任务要自己设上下文**:无请求上下文的定时任务必须逐个机构设置 `OrgContext` 再取连接,用完清理。 ## 4. 通用数据接口 除工作流外,业务数据的读写统一走 `/data/**`,不为每个模块写专用 CRUD。 | 接口 | 作用 | |---|---| | `POST /data/describe` | 探测表/视图的列结构 | | `POST /data/loaddata` | 按视图 + 条件查询列表 | | `POST /data/loaddatabysql` | 执行只读 SQL | | `POST /data/page` | 分页查询(返回 `rows` / `total` / `page_no` / `page_size`) | | `POST /data/saveobjt` | 通用保存(批量,主子表一次提交) | | `POST /data/delete` | 通用删除 | | `POST /data/nextid` | 取雪花 ID | | `POST /data/nextcode` | 取自动编码 | **共同约定**: - 表名、视图名、列名**先过标识符校验**(只允许 `[A-Za-z_][A-Za-z0-9_]*`,可带 `dbo.` 限定),不做字符串拼接。 - `search_condition` 是**原样拼进 `WHERE` 的布尔表达式片段**,整条 SQL 之后过只读校验(禁分号、禁注释、禁写关键字)。 - 返回的行是普通 Map,键是**库里的原始列名**。 - **审计列由服务端统一填**,前端不能指定:`b_created_by` / `b_created_at` / `b_updated_by` / `b_updated_at` 由保存层写入,不建触发器。 **保存的并发保护**:`b_row_version` 参与乐观锁;影响行数为 0 时区分「记录不存在」与「版本冲突」,分别返回可识别的错误,前端据此提示刷新。 ## 5. 权限模型 前端隐藏菜单只是体验,**真正生效的过滤必须在后端拼进 SQL**。`SqlPermissionService` 是这一层的唯一实现,分三层: | 层 | 数据来源 | 语义 | |---|---|---| | 动作权限 | `s_power` + `s_user_power` | **白名单**:没有对应权限点、或未授权、或任一侧停用即拒绝 | | 字段权限 | `s_user_field_power` | `b_view` / `b_export`,无记录按模块默认(可看可导出) | | 数据范围 | `s_user_data_power` | 按操作精确匹配优先,多条规则 OR,整体再与其它条件 AND | 动作权限编码为 `action.{模块}.{动作}`;数据范围的判定字段取自**模块配置** `s_module.b_scope_field`(记录归属人字段),而不是授权行——同一个模块的所有用户必须用同一字段判范围。 ## 6. 工作流 工作流是独立子系统,不与通用数据接口混用:**业务单据的状态只能由流程服务推进**,业务表的状态列由流程投影写入。 ```text 业务模块 → 流程绑定 → 已发布版本 → 节点与连线 / 审批人规则 业务提交 → 流程实例 → 节点运行 → 待办任务 → 动作 / 审计 / 通知 ``` | 接口组 | 前缀 | 职责 | |---|---|---| | 配置 | `/workflow/**` | 定义、版本(草稿/发布/停用/复制)、绑定 | | 运行 | `/workflow/runtime/**` | 送审、审批动作、抢占、转派、加签、催办、撤回、取消、待办/已办/进度/抄送 | 实现上的关键口径: - 一次动作的全部副作用在**同一事务**内完成:锁定 → 校验 → 写动作 → 更新任务/节点 → 推进或结束 → 投影业务状态 → 写 outbox → 提交。 - **锁序统一为「实例优先」**,避免实例与任务互锁造成的死锁。 - **幂等**:送审按 `b_idempotency_key`,审批按 `wf_action.b_request_id`,各由一条筛选唯一索引兜底。 - 审批人解析为空时实例进入 `error` 而非跳过节点。 - **超时只标记任务、不改流程**:到期任务置 `expired` 并从待办移除,实例保持运行,由管理员加签/转派处理。 ## 7. 鉴权与会话 ```text POST /auth/login → 校验账号 → 签发 JWT(含机构码、用户编码)→ 登记活动会话 后续请求 → JwtAuthFilter 校验签名与会话有效性 → 写入 OrgContext 与日志 MDC ``` - 过滤器只放行 `OPTIONS` 与 `/auth/login`,其余请求一律校验。 - **会话登记**:`ActiveSessionRegistry` 记录活动会话,可在登录时判定是否顶掉旧会话。 - 登录日志独立记录(含客户端信息与 IP 归属地),便于审计。 ## 8. 公式引擎 `service/formula` 是一套独立的表达式引擎:词法 → 语法 → AST → 求值,配合依赖图与配置服务。 - **依赖分析**:`FormulaDeps` / `FormulaGraph` 解析字段间依赖,支持检测循环引用。 - **校验与配置分离**:`FormulaValidator` 负责保存期校验,`FormulaConfigService` 负责读取与缓存。 - 求值结果与错误都走统一类型,便于前端展示。 ## 9. 附件 `POST /file/**` 提供两类存储模式,由 `erp.file.storage-type` 配置: | 模式 | 流程 | |---|---| | `local` | 后端写盘 + 落库,静态资源走 `/file/static/**` | | `aliyun-oss` | 前后端取号 → 凭证直传 OSS → 回调落库 | 配置前缀为 `erp.file.*`(`storage-dir` / `url-prefix` / `g3oss-url`)。文件记录表使用 `bf_` 业务域前缀。 ## 10. 配置 配置按功能分组,统一挂在 `erp.*` 下: ```yaml erp: auth: jwt-secret / jwt-expiration database: config-dir / refresh-check-interval / pool.* file: storage-type / local.* / oss.* log: slow-request-ms / max-param-bytes / exclude-param-paths workflow: expire-sweep-interval ``` **机构数据库配置独立存放**:`server/config/dbconfigs/{机构码}.properties`,含连接串、账号与驱动。该目录已 gitignore,不入版本库。