智富ERP 平台管理使用手册
目录
1. 系统架构概述
1.1 多租户架构
智富ERP 采用多租户独立数据库架构:
┌─────────────────────────────────────────┐
│ Master 库 │
│ (erp_system) │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
│ │ tenant │ │sys_module│ │sys_open_ │ │
│ │ (租户配置)│ │(系统模块) │ │domain │ │
│ └─────────┘ └──────────┘ └───────────┘ │
└─────────────────────────────────────────┘
│ 动态数据源路由
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Tenant 1000 │ │ Tenant 1001 │ │ Tenant 1002 │
│ (erp_tenant) │ │ (独立库) │ │ (独立库) │
│ │ │ │ │ │
│ sys_user │ │ sys_user │ │ sys_user │
│ sys_role │ │ sys_role │ │ sys_role │
│ sys_menu │ │ sys_menu │ │ sys_menu │
│ ... │ │ ... │ │ ... │
└──────────────┘ └──────────────┘ └──────────────┘
- Master 库 (
erp_system):存储租户配置、系统模块定义,不存业务数据 - 租户库:每个租户拥有独立的数据库,存储用户、角色、菜单、业务数据
- 动态数据源:启动时加载所有启用的租户数据源,请求时根据上下文自动路由
1.2 租户上下文解析流程
每次请求到达时,按以下优先级确定当前租户:
1. 登录会话中的租户ID ← 最高优先级(已登录用户)
2. 请求域名匹配 ← serverName 与 request.getServerName() 比对
3. X-Tenant-Id 请求头 ← 最低优先级(外部 API 调用)
1.3 平台管理权限
所有平台管理功能要求用户同时满足:
- 平台管理员身份(
isPlatform = true) - 对应的功能权限码(如
system:tenant:add)
两者缺一不可。
2. 租户管理
路径: 平台管理 > 租户管理
2.1 业务模型
核心数据表
| 表名 | 所在库 | 作用 |
|---|---|---|
tenant |
Master | 租户基本信息与数据库连接配置 |
sys_module |
Master | 系统功能模块定义 |
sys_module_tenant |
Master | 租户-模块授权关系 |
Tenant 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
id |
Integer | 租户ID(自增主键) |
name |
String | 租户名称(唯一) |
serverName |
String | 绑定域名(用于域名自动识别租户) |
jdbcUrl |
String | 租户数据库 JDBC 连接地址 |
jdbcUsername |
String | 数据库用户名 |
jdbcPassword |
String | 数据库密码(AES 加密存储) |
isPlatform |
Boolean | 是否为平台管理租户(全局唯一) |
available |
Boolean | 启用/停用状态 |
2.2 新增租户
重要业务规则:
- 名称唯一性校验 — 租户名称不能与已有租户重复
- 平台租户唯一性 — 整个系统只能有一个平台管理租户(
isPlatform = true),创建时会检查 - 数据库连接实时验证 — 提交前会用
DriverManager.getConnection()实际测试 JDBC 连接是否可达,连接失败则拒绝创建 - 密码加密存储 — JDBC 密码使用 AES 加密后存入数据库,密钥来自配置项
smarterp.secret.key - 事务性 — 创建租户记录和动态加载数据源是两个步骤:
- 租户记录先入库(事务保证)
- 再通过
ReloadTenantEvent事件动态注册数据源 - 即使数据源加载失败,租户已创建,系统会提示"请勿重复新增"
操作流程:
填写表单 ─→ 校验名称唯一 ─→ 校验平台唯一 ─→ 验证DB连接 ─→ 加密密码 ─→ 写入DB ─→ 动态加载数据源 ─→ 完成
│
失败不阻塞,记录已保存
2.3 编辑租户
与新增的关键区别:
| 对比项 | 新增 | 编辑 |
|---|---|---|
| 名称校验 | 全局不重复 | 排除自身后不重复 |
| 平台校验 | 检查是否存在其他平台租户 | 排除自身后检查 |
| 停用限制 | 无(默认启用) | 平台租户不允许停用 |
| 密码处理 | 必填 | 仅当填写了新密码时才更新 |
| 缓存清理 | 不需要 | 清理 Tenant:id 和 Tenant:all 缓存 |
| 数据源重载 | 新增数据源 | 仅当 JDBC 配置变更时才重载 |
业务规则详解 — 停用保护:
平台管理租户(isPlatform = true)不能被停用。如果尝试将 available 设为 false,系统会抛出异常:"平台管理租户不允许停用!"
业务规则详解 — 条件重载:
仅当用户修改了 jdbcUrl、jdbcUsername 或 jdbcPassword 中任意一项时,才会触发数据源动态重载。修改名称、域名、状态等不涉及数据库连接的字段不会触发重载。
2.4 查询与详情
- 支持按租户ID(精确)、名称(模糊)、状态(精确)筛选
- 使用 PageHelper 分页
- 详情查询使用 Spring Cache(
@Cacheable("Tenant")),减少数据库压力 - 响应中对敏感字段做了保护:
jdbcUrl/jdbcUsername:响应中加密返回jdbcPassword:标记为EncryType.PASSWORD,在序列化时完全遮蔽
2.5 模块授权(点击"授权"按钮)
这是租户管理的核心子功能,决定一个租户可以使用哪些功能模块。
业务流程
进入授权页 ─→ 加载全部系统模块列表 ─→ 标注每个模块的授权状态 ─→ 用户勾选/取消模块
│
┌────────────┘
▼
点击保存 ─→ 删除旧授权 ─→ 写入新授权 ─→ 清缓存
关键业务规则
1. 授权覆盖式保存(先删后增)
保存授权时,先删除该租户所有的历史授权记录,再批量插入新的授权。这意味着:
- 未勾选的模块 = 取消授权
- 每次保存是全量替换,不是增量更新
// SysModuleTenantServiceImpl.setting()
this.remove(queryWrapper); // 删除租户所有旧授权
this.saveBatch(records); // 批量插入新授权
2. 平台模块保护
标记为 isPlatform = true 的系统模块,只能授权给平台管理租户。如果尝试将平台专属模块授权给普通租户,系统会报错:
"租户【XXX】不是平台管理租户,不允许授权【XXX】模块!"
这是为了防止普通租户访问平台管理功能(如租户管理、开放域配置等)。
3. 模块过期机制
每个授权记录都有 expireTime(过期时间)字段。查询租户可用模块时,会自动过滤已过期的授权:
// 只返回 expireTime 为空 或 未过期的模块
.filter(t -> t.getExpireTime() == null || DateUtil.now().isBefore(t.getExpireTime()))
设置 expireTime 为未来某个时间,可以实现"试用期授权"的效果。
4. 缓存与权限生效
- 授权数据缓存在
SysModuleTenant:tenantId下 - 每次修改授权后立即清除缓存
- 用户登录时从缓存/数据库加载可用模块,进而决定菜单可见性
2.6 数据源动态管理
启动时加载
应用启动时,AbstractJdbcDataSourceProvider 执行以下逻辑:
SELECT * FROM tenant WHERE available = true
│
▼
遍历每个启用的租户
│
├─→ 解密 jdbcPassword (AES)
├─→ 构造 DataSourceProperty(复制 master 配置)
├─→ 测试数据库连接
│ │
│ ├─ 成功 → 注册到 DynamicRoutingDataSource(key = 租户ID)
│ └─ 失败 → 记录警告日志,跳过该租户
│
▼
所有数据源注册完成
运行时新增/变更
通过 ReloadTenantEvent 事件机制实现:
- Controller 发布
ReloadTenantEvent ReloadTenantListener监听事件- 调用
ds.addDataSource(tenantId, property)动态注册 - 后续该租户的请求会被路由到新数据源
3. 开放域
路径: 平台管理 > 开放域
3.1 功能定位
开放域是一个 API 密钥管理体系,用于:
- 为外部系统提供 API 访问凭证
- 每个开放域绑定一个租户,代表"哪个外部系统以哪个租户的身份调用 API"
- 标记了
@OpenApi注解的接口可以免登录访问
当前状态:
@OpenApi目前仅实现"免登录访问"功能,签名校验(sign字段)的完整认证链路尚未实现。但OpenApiReqVo已预留了clientId、timestamp、nonceStr、sign等标准 HMAC 签名所需的字段。
3.2 数据模型
sys_open_domain 表
| 字段 | 类型 | 说明 |
|---|---|---|
id |
String | 主键(自增) |
name |
String | 开放域名称 |
apiSecret |
String | API 密钥(明文存储,使用时需要安全分发) |
available |
Boolean | 启用/停用 |
description |
String | 备注说明 |
tenantId |
Integer | 关联的租户ID |
3.3 CRUD 操作
| 操作 | 接口 | 权限 | 说明 |
|---|---|---|---|
| 查询列表 | GET /system/open/domain/query |
system:open-domain:config |
支持 id/name/available 筛选 |
| 查看详情 | GET /system/open/domain?id= |
同上 | 使用 Spring Cache 缓存 |
| 新增 | POST /system/open/domain |
同上 | 默认 available=true |
| 修改基本信息 | PUT /system/open/domain |
同上 | 不修改 apiSecret |
| 修改密钥 | PUT /system/open/domain/secret |
同上 | 仅修改 apiSecret |
关键设计 — 密钥与基本信息分离编辑:
更新接口 PUT /system/open/domain 不处理 apiSecret 字段,即使传了也会被忽略。修改 API 密钥必须使用专用接口 PUT /system/open/domain/secret。这样设计的原因:
- 防止密钥被误覆盖
- 允许对密钥修改做独立审计
- 密钥变更后可以触发额外的安全通知流程
3.4 @OpenApi 注解 — 免登录机制
注解定义
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface OpenApi {
boolean sign() default false; // 是否校验签名(已声明,待实现)
}
拦截器处理流程
请求到达
│
▼
TenantInterceptor ← 解析租户上下文(域名/Header/会话)
│
▼
LoginInterceptor ← 检测 @OpenApi 注解
│ │
│ ┌────┘
│ │ 有 @OpenApi → 免登录,直接放行
│ │ 无 @OpenApi → StpUtil.checkLogin() 校验登录态
│ ▼
│ PermitAllService 将 URL 加入白名单缓存
▼
SaInterceptor ← SaToken 权限校验
已标记 @OpenApi 的端点
| 端点 | 说明 |
|---|---|
GET /auth/tenant/require |
查询是否需要多租户 |
POST /auth/captcha/require |
查询是否需要验证码 |
GET /auth/captcha |
获取登录验证码 |
POST /auth/login |
用户登录 |
POST /auth/logout |
退出登录 |
GET /download/security |
安全文件下载(通过 Redis 存储的 sign token 鉴权) |
3.5 权限控制
所有开放域管理端点使用:
@HasPermission(
value = {"system:open-domain:config"},
requirePlatform = true
)
即:用户必须是平台管理员且拥有 system:open-domain:config 权限。权限校验在 AOP 切面中完成:
@HasPermission 注解
│
▼
PermissionAspect (AOP 环绕通知)
│
▼
CheckPermissionHandler.valid()
├─→ 是超级管理员? → 直接通过
├─→ requirePlatform=true 且用户非平台管理员? → 拒绝
├─→ 权限码为空且 requirePlatform=true? → 通过(仅校验平台身份)
└─→ 用户权限列表与要求权限匹配(支持通配符)→ 通过/拒绝
3.6 与租户的关系
sys_open_domain.tenantId关联到tenant.id- 创建开放域时必须指定
tenantId - 这意味着:外部系统通过开放域凭证调用 API 时,将以该租户的身份操作数据
- 多租户场景下的典型用法:为每个外部对接方创建一个开放域,绑定其专属租户
4. 在线开发
路径: 平台管理 > 在线开发
4.1 功能定位
在线开发是一个低代码配置平台,允许通过可视化配置(而非编写代码)来生成数据库表定义、多表查询视图、前端列表页面、自定义页面和下拉选择器组件。
4.2 子系统概览
在线开发
├── 数据实体 (GenDataEntity) ← 定义数据库表结构
├── 数据对象 (GenDataObj) ← 定义多表查询视图
├── 自定义列表 (GenCustomList) ← 配置前端列表页面
├── 自定义页面 (GenCustomPage) ← 编写完全自定义的页面
├── 自定义选择器 (GenCustomSelector) ← 配置弹窗选择组件
└── 编号规则 (SysGenerateCode) ← 配置单据编号生成规则(独立子系统)
4.3 完整低代码开发流程
Step 1 Step 2 Step 3 Step 4 (可选)
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│ 数据实体 │ ───→ │ 数据对象 │ ───→ │ 自定义列表 │ ───→ │ 自定义选择器 │
│ (定义表 │ │ (定义多表 │ │ (配置前端 │ │ (配置弹窗选择 │
│ 字段) │ │ 查询视图) │ │ 列表页) │ │ 组件) │
└──────────┘ └──────────┘ └──────────┘ └──────────────┘
│ │ │
│ │ ├── 列定义
│ │ ├── 搜索条件
│ │ ├── 行操作按钮
│ │ └── 工具栏按钮
│ │
│ ├── 主表 + 别名
│ ├── JOIN 关系 (关联表 + 关联字段 + JOIN类型)
│ └── 自定义 SQL 字段 (虚拟列)
│
├── 业务层属性 (名称/类型/校验/枚举/排序)
└── 数据库层属性 (列名/类型/长度/可空/默认值)
Step 1 — 定义数据实体(GenDataEntity)
数据实体是对数据库表的元数据描述。每个实体对应一张真实的数据库表。
关键业务逻辑:
双维度字段定义 — 每个字段同时维护两套属性:
- 业务层:显示名称、数据类型、视图类型、校验规则、枚举值、关联数据字典/选择器
- 数据库层:列名、SQL 类型、是否可空、默认值、列位置
事务性保存 — 实体的创建/更新在
@Transactional中完成:- 先保存实体头信息
- 再删除所有旧字段定义,批量插入新字段定义
- 任何一步失败都会回滚
级联删除 — 删除实体时同时删除其所有字段定义
分类管理 — 实体归属到分类下,分类删除时会检查是否有子实体,有则拒绝删除
Step 2 — 定义数据对象(GenDataObj)
数据对象是对多表联合查询的元数据描述,是自定义列表的数据来源。
关键业务逻辑:
主表 + 子表 JOIN 模型:
- 必须指定一个
mainTableId(主实体) - 可以添加 N 个
GenDataObjDetail(JOIN 关系) - 每个 JOIN 定义:主表字段、子表、子表字段、JOIN 类型、子表别名
- 必须指定一个
自定义 SQL 字段(虚拟列):
- 通过
GenDataObjQueryDetail定义 - 可以写原生 SQL 表达式作为查询字段
- 例如:
(SELECT COUNT(*) FROM orders WHERE orders.customer_id = c.id)作为客户实体的"订单数"虚拟列
- 通过
删除保护 — 如果数据对象被自定义列表引用,则无法删除(通过外键约束或业务校验)
Step 3 — 配置自定义列表(GenCustomList)
自定义列表是整个低代码平台最复杂的组件,一个列表由 1 个主表 + 4 个子表构成。
主表 GenCustomList 的关键配置:
| 配置项 | 说明 |
|---|---|
dataObjId |
关联的数据对象(数据来源) |
listType |
列表类型(表格/卡片) |
hasPage |
是否分页 |
treeData |
是否为树形表格 |
queryPrefixSql |
注入到 WHERE 之前的 SQL |
querySuffixSql |
注入到 WHERE 之后的 SQL |
suffixSql |
注入到 SQL 末尾(如 ORDER BY) |
allowExport |
是否允许 Excel 导出 |
四个子表的作用:
┌─────────────────────────────────────────────────┐
│ 工具栏 │
│ [新增] [批量删除] [导出Excel] [自定义按钮...] │
│ (GenCustomListToolbar) │
├─────────────────────────────────────────────────┤
│ 搜索栏 │
│ 名称: [____] 状态: [____] [查询] [重置] │
│ (GenCustomListQueryParams) │
├─────────────────────────────────────────────────┤
│ 列1 │ 列2 │ 列3 │ 操作 │
│ (GenCustom- │ (GenCus- │ (GenCus- │ [编辑] │
│ ListDetail)│ tomList- │ tomList- │ [删除] │
│ │ Detail) │ Detail) │ (GenCus- │
│ │ tomList- │
│ │ Handle- │
│ │ Column) │
└─────────────────────────────────────────────────┘
保存逻辑 — 全量替换:
创建/更新自定义列表时,采用"先删后增"策略:
@Transactional
saveOrUpdate(customList) {
this.saveOrUpdate(listHeader); // 1. 保存列表头
this.deleteAllChildren(listId); // 2. 删除旧的 4 个子表数据
this.batchInsert(details); // 3. 批量插入列定义
this.batchInsert(queryParams); // 4. 批量插入搜索条件
this.batchInsert(handleColumns); // 5. 批量插入操作按钮
this.batchInsert(toolbars); // 6. 批量插入工具栏按钮
}
这意味着每次修改都会完全覆盖旧的配置,不是增量更新。
Step 4 — 配置自定义选择器(GenCustomSelector)
自定义选择器将已有的自定义列表包装成一个弹窗选择组件,用于表单中的下拉/参照选择。
核心配置映射:
┌──────────────────────────────┐
│ 选择供应商 │ ← dialogTitle
│ [X] │
│ ┌──────────────────────┐ │
│ │ 搜索: [____] [查询] │ │ ← 来自 GenCustomList 的搜索栏
│ ├──────────────────────┤ │
│ │ 编号 │ 名称 │ 电话 │ │ ← 来自 GenCustomList 的列定义
│ │ S001 │ 张三 │ ... │ │
│ │ S002 │ 李四 │ ... │ │
│ └──────────────────────┘ │
│ [确定] [取消] │
└──────────────────────────────┘
│ │
idColumn nameColumn
(value) (display text)
idColumn+idColumnRelaId:选择后返回的值nameColumn+nameColumnRelaId:输入框中显示的文本dialogWidth:弹窗宽度placeholder:输入框占位文字
4.4 自定义页面(GenCustomPage)
与自定义列表的"配置式"不同,自定义页面采用编码式开发:
| 字段 | 用途 |
|---|---|
pageCode |
页面布局/模板代码 |
scriptCode |
业务逻辑脚本代码 |
分类支持层级树结构(parentId),可构建页面目录树。
4.5 编号规则(SysGenerateCode)
独立的子系统,用于配置各种业务单据的编号自动生成。
架构模式:策略 + 责任链
请求生成编号 (ruleId)
│
▼
从 sys_generate_code 表读取 configStr (JSON)
│
▼
GenerateCodeFactory.generate(ruleList)
│
▼
遍历 JSON 中的每个规则段
│
├─→ type=1: CurrentDateTimeHandler → 当前日期时间 (如 20260801)
├─→ type=2: CustomRandomStrHandler → 随机字符串
├─→ type=3: FlowGenerateCodeHandler → 自增流水号 (Redis INCR)
├─→ type=4: RandomIntHandler → 随机数字
├─→ type=5: SnowFlakeHandler → 雪花分布式ID
├─→ type=6: StaticStrHandler → 固定字符串
└─→ type=7: UUIDHandler → UUID
│
▼
拼接结果: "PO20260801001"
流水号规则(type=3)详细说明
最常用、最复杂的规则类型:
| 参数 | 说明 | 默认值 |
|---|---|---|
key |
计数器唯一标识(区分不同单据) | — |
len |
补零后的长度 | 10 |
step |
递增值 | 1 |
expireType |
0=每日重置, 1=按秒数重置 | 0 |
expireSeconds |
重置周期秒数 | 86400 (24h) |
分布式锁保护:
获取锁(key + 租户ID + 日期)
│
▼
Redis INCR (原子递增)
│
▼
释放锁
│
▼
左补零到目标长度 → 返回
锁 Key 格式:flow_generator_index_{ruleKey}_{tenantId}_{date}_Locker
业务单据映射
| 编号ID | 单据类型 |
|---|---|
| 200 | 采购订单 |
| 201 | 采购收货单 |
| 202 | 采购退单 |
| 203 | 销售订单 |
| 204 | 销售出库单 |
| 205 | 销售退货单 |
| 206 | 零售出库单 |
| 207 | 零售退货单 |
| 208 | 预先盘点单 |
| 209 | 盘点任务 |
| 210 | 盘点单 |
| 212 | 库存调整单 |
| 213 | 仓库调拨单 |
| 214 | 物流单 |
| 300-303 | 结算系列单据 |
| 304-307 | 客户结算系列单据 |
预览功能
POST /system/generate/code/preview — 可在线预览规则生成的编号效果,支持调试。
4.6 分类管理通用规则
所有 5 个开发模块都遵循相同的分类管理逻辑:
| 操作 | 规则 |
|---|---|
| 查询全部 | 按 code 排序,用于下拉选择器 |
| 分页查询 | 支持 code(模糊)和 name(模糊)筛选 |
| 新增 | 校验 code 唯一性,雪花ID作为主键 |
| 更新 | 校验 code 排除自身后不重复 |
| 删除 | 检查是否有子记录引用,有则拒绝删除:"该分类下存在XXX,无法删除!" |
附录:权限码速查
| 功能 | 权限码 | 平台要求 |
|---|---|---|
| 租户查询 | system:tenant:query |
平台管理员 |
| 新增租户 | system:tenant:add |
平台管理员 |
| 修改租户 | system:tenant:modify |
平台管理员 |
| 模块授权 | system:tenant:module |
平台管理员 |
| 开放域配置 | system:open-domain:config |
平台管理员 |
| 编号规则管理 | system:generate-code:manage |
平台管理员 |
| 在线开发 | dev:* |
平台管理员 |