智富ERP 平台管理使用手册


目录

  1. 系统架构概述
  2. 租户管理
  3. 开放域
  4. 在线开发

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 新增租户

重要业务规则:

  1. 名称唯一性校验 — 租户名称不能与已有租户重复
  2. 平台租户唯一性 — 整个系统只能有一个平台管理租户(isPlatform = true),创建时会检查
  3. 数据库连接实时验证 — 提交前会用 DriverManager.getConnection() 实际测试 JDBC 连接是否可达,连接失败则拒绝创建
  4. 密码加密存储 — JDBC 密码使用 AES 加密后存入数据库,密钥来自配置项 smarterp.secret.key
  5. 事务性 — 创建租户记录和动态加载数据源是两个步骤:
    • 租户记录先入库(事务保证)
    • 再通过 ReloadTenantEvent 事件动态注册数据源
    • 即使数据源加载失败,租户已创建,系统会提示"请勿重复新增"

操作流程:

填写表单 ─→ 校验名称唯一 ─→ 校验平台唯一 ─→ 验证DB连接 ─→ 加密密码 ─→ 写入DB ─→ 动态加载数据源 ─→ 完成
                                                                              │
                                                                       失败不阻塞,记录已保存

2.3 编辑租户

与新增的关键区别:

对比项 新增 编辑
名称校验 全局不重复 排除自身后不重复
平台校验 检查是否存在其他平台租户 排除自身后检查
停用限制 无(默认启用) 平台租户不允许停用
密码处理 必填 仅当填写了新密码时才更新
缓存清理 不需要 清理 Tenant:idTenant:all 缓存
数据源重载 新增数据源 仅当 JDBC 配置变更时才重载

业务规则详解 — 停用保护: 平台管理租户(isPlatform = true)不能被停用。如果尝试将 available 设为 false,系统会抛出异常:"平台管理租户不允许停用!"

业务规则详解 — 条件重载: 仅当用户修改了 jdbcUrljdbcUsernamejdbcPassword任意一项时,才会触发数据源动态重载。修改名称、域名、状态等不涉及数据库连接的字段不会触发重载。

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 事件机制实现:

  1. Controller 发布 ReloadTenantEvent
  2. ReloadTenantListener 监听事件
  3. 调用 ds.addDataSource(tenantId, property) 动态注册
  4. 后续该租户的请求会被路由到新数据源

3. 开放域

路径: 平台管理 > 开放域

3.1 功能定位

开放域是一个 API 密钥管理体系,用于:

  • 为外部系统提供 API 访问凭证
  • 每个开放域绑定一个租户,代表"哪个外部系统以哪个租户的身份调用 API"
  • 标记了 @OpenApi 注解的接口可以免登录访问

当前状态: @OpenApi 目前仅实现"免登录访问"功能,签名校验(sign 字段)的完整认证链路尚未实现。但 OpenApiReqVo 已预留了 clientIdtimestampnonceStrsign 等标准 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)

数据实体是对数据库表的元数据描述。每个实体对应一张真实的数据库表。

关键业务逻辑:

  1. 双维度字段定义 — 每个字段同时维护两套属性:

    • 业务层:显示名称、数据类型、视图类型、校验规则、枚举值、关联数据字典/选择器
    • 数据库层:列名、SQL 类型、是否可空、默认值、列位置
  2. 事务性保存 — 实体的创建/更新在 @Transactional 中完成:

    • 先保存实体头信息
    • 删除所有旧字段定义批量插入新字段定义
    • 任何一步失败都会回滚
  3. 级联删除 — 删除实体时同时删除其所有字段定义

  4. 分类管理 — 实体归属到分类下,分类删除时会检查是否有子实体,有则拒绝删除

Step 2 — 定义数据对象(GenDataObj)

数据对象是对多表联合查询的元数据描述,是自定义列表的数据来源。

关键业务逻辑:

  1. 主表 + 子表 JOIN 模型

    • 必须指定一个 mainTableId(主实体)
    • 可以添加 N 个 GenDataObjDetail(JOIN 关系)
    • 每个 JOIN 定义:主表字段、子表、子表字段、JOIN 类型、子表别名
  2. 自定义 SQL 字段(虚拟列)

    • 通过 GenDataObjQueryDetail 定义
    • 可以写原生 SQL 表达式作为查询字段
    • 例如:(SELECT COUNT(*) FROM orders WHERE orders.customer_id = c.id) 作为客户实体的"订单数"虚拟列
  3. 删除保护 — 如果数据对象被自定义列表引用,则无法删除(通过外键约束或业务校验)

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:* 平台管理员