182 lines
8.9 KiB
Markdown
182 lines
8.9 KiB
Markdown
---
|
||
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,不入版本库。
|