Files
workspace/code/g3soft-libs/docs/content/backend/architecture.md
T
2026-10-13 22:32:50 +08:00

182 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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,不入版本库。