8.9 KiB
title, description
| 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. 多机构数据源路由
一套部署服务多个机构,每个机构一个独立数据库。
请求 → 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. 工作流
工作流是独立子系统,不与通用数据接口混用:业务单据的状态只能由流程服务推进,业务表的状态列由流程投影写入。
业务模块 → 流程绑定 → 已发布版本 → 节点与连线 / 审批人规则
业务提交 → 流程实例 → 节点运行 → 待办任务 → 动作 / 审计 / 通知
| 接口组 | 前缀 | 职责 |
|---|---|---|
| 配置 | /workflow/** |
定义、版本(草稿/发布/停用/复制)、绑定 |
| 运行 | /workflow/runtime/** |
送审、审批动作、抢占、转派、加签、催办、撤回、取消、待办/已办/进度/抄送 |
实现上的关键口径:
- 一次动作的全部副作用在同一事务内完成:锁定 → 校验 → 写动作 → 更新任务/节点 → 推进或结束 → 投影业务状态 → 写 outbox → 提交。
- 锁序统一为「实例优先」,避免实例与任务互锁造成的死锁。
- 幂等:送审按
b_idempotency_key,审批按wf_action.b_request_id,各由一条筛选唯一索引兜底。 - 审批人解析为空时实例进入
error而非跳过节点。 - 超时只标记任务、不改流程:到期任务置
expired并从待办移除,实例保持运行,由管理员加签/转派处理。
7. 鉴权与会话
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.* 下:
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,不入版本库。