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

8.9 KiB
Raw Blame History

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,不入版本库。