docs(spec): 第三方应用管理(SysApp)设计规格
- 新增 SysApp 实体:多租户第三方应用凭证管理 - 平台元数据 + 支付扩展 + AES key + 证书/文本分离 - 消费方:OAuth2ServerConfig Feign + Redis 30min 缓存 - 租户隔离:owner_type=PLATFORM/TENANT + client_id→tenant_id 映射 - 范围:不涉及 rui-service-user(user 模块)
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# 第三方应用管理(SysApp)设计文档
|
||||
|
||||
> **日期**: 2026-06-07
|
||||
> **状态**: 已批准,待实现
|
||||
> **作者**: AI Assistant
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状
|
||||
|
||||
`rui-common-core` 中已有 `AppProperties` 通用第三方应用 POJO(含 appId/appSecret/appKey/privateKey/publicKey/redirectUri)。
|
||||
`rui-common-oauth2` 通过 `@Bean + @ConfigurationProperties` 读取 `thirdparty.wechat.*` / `thirdparty.alipay.*` 配置,固定为系统级配置。
|
||||
|
||||
**问题**:
|
||||
- 普通租户无法管理自己的第三方应用凭证
|
||||
- 配置硬编码在 Nacos,修改需要运维介入
|
||||
- 字段不够用(缺支付平台字段、AES key、证书文件等)
|
||||
- 无法区分"平台默认配置"和"租户自配"
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
1. 在 `rui-service-system` 增加 `SysApp` 实体 + CRUD + 内部接口
|
||||
2. 支持多租户:每条记录区分 `owner_type=PLATFORM`(平台默认)或 `TENANT`(租户自配)
|
||||
3. 凭证字段完整:覆盖社交登录 + 第三方支付 + AES 对称加密 + 证书文件
|
||||
4. `OAuth2ServerConfig` 改为运行时从 `SysApp` 拉凭证,Redis 缓存 30min
|
||||
5. 租户隔离按 `client_id → tenant_id` 映射
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心设计原则
|
||||
|
||||
1. **多租户隔离**:通过 `owner_type` + `tenant_id` 双重区分
|
||||
2. **凭证集中管理**:一个 `SysApp` 记录 = 一个第三方应用在本系统的接入凭证
|
||||
3. **缓存优先**:高频读取走 Redis,CRUD 操作失效缓存
|
||||
4. **证书与文本分离**:每个凭证字段分 `_text` 和 `_file_path` 两个列,使用方按业务实际选用(写入时只填其中一个)
|
||||
5. **兼容现有 `AppProperties`**:`AppProperties` 仍作为通用 POJO 留在 `rui-common-core`,但实际数据从 DB 加载后映射到 `AppProperties`
|
||||
6. **加密占位**:预留 `is_encrypted` 字段,暂不实现 AES 加密逻辑
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据库设计
|
||||
|
||||
### 3.1 表 `sys_app`
|
||||
|
||||
```sql
|
||||
CREATE TABLE sys_app (
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID 0:系统级',
|
||||
owner_type VARCHAR(20) NOT NULL COMMENT '所有者类型 PLATFORM/TENANT',
|
||||
platform VARCHAR(50) NOT NULL COMMENT '平台编码 wechat/alipay/stripe',
|
||||
name VARCHAR(100) NOT NULL COMMENT '管理用名称',
|
||||
-- 凭证:app_id / app_secret(证书/文本二选一)
|
||||
app_id VARCHAR(200) DEFAULT NULL COMMENT '应用ID',
|
||||
app_secret_text VARCHAR(500) DEFAULT NULL COMMENT '文本型应用密钥',
|
||||
app_secret_file_path VARCHAR(500) DEFAULT NULL COMMENT '证书型应用密钥路径',
|
||||
-- 凭证:app_key
|
||||
app_key VARCHAR(200) DEFAULT NULL COMMENT '应用Key(部分平台如支付宝)',
|
||||
-- 凭证:private_key(证书/文本二选一)
|
||||
private_key_text TEXT DEFAULT NULL COMMENT '文本型私钥',
|
||||
private_key_file_path VARCHAR(500) DEFAULT NULL COMMENT '证书型私钥路径',
|
||||
-- 凭证:public_key(证书/文本二选一)
|
||||
public_key_text TEXT DEFAULT NULL COMMENT '文本型公钥',
|
||||
public_key_file_path VARCHAR(500) DEFAULT NULL COMMENT '证书型公钥路径',
|
||||
-- 应用自定义 AES key
|
||||
aes_key VARCHAR(100) DEFAULT NULL COMMENT '应用AES对称密钥(16/24/32字节)',
|
||||
-- 通用
|
||||
redirect_uri VARCHAR(500) DEFAULT NULL COMMENT 'OAuth2授权回调地址',
|
||||
-- 支付平台专用
|
||||
merchant_id VARCHAR(100) DEFAULT NULL COMMENT '商户号',
|
||||
sign_type VARCHAR(20) DEFAULT NULL COMMENT '签名方式 RSA2/MD5/HMAC',
|
||||
notify_url VARCHAR(500) DEFAULT NULL COMMENT '支付回调URL',
|
||||
api_base VARCHAR(500) DEFAULT NULL COMMENT 'API根地址',
|
||||
is_sandbox TINYINT NOT NULL DEFAULT 0 COMMENT '是否沙箱环境 0:否 1:是',
|
||||
extra JSON DEFAULT NULL COMMENT '扩展字段',
|
||||
-- 状态与审计
|
||||
is_encrypted TINYINT NOT NULL DEFAULT 0 COMMENT '预留:是否加密 0:否 1:是(暂不实现)',
|
||||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
|
||||
description VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
sort_no INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
created_by BIGINT DEFAULT NULL COMMENT '创建者ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT DEFAULT NULL COMMENT '更新者ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_tenant_owner_platform (tenant_id, owner_type, platform, status),
|
||||
INDEX idx_tenant (tenant_id),
|
||||
INDEX idx_platform (platform),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方应用集成';
|
||||
```
|
||||
|
||||
> 注:`status` 参与唯一约束,避免"软删除"后还能插同 key。逻辑删除在 `BaseEntity` 里有 `deleted` 字段。
|
||||
|
||||
### 3.2 枚举 `SysAppOwnerType`
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|---|
|
||||
| `PLATFORM` | 平台默认(`tenant_id=0`),所有租户可继承使用 |
|
||||
| `TENANT` | 租户自配,覆盖对应 PLATFORM 默认 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Java 包结构
|
||||
|
||||
```
|
||||
com.rui.service.system/
|
||||
├── entity/
|
||||
│ ├── SysApp.java
|
||||
│ └── SysAppOwnerType.java # 枚举 PLATFORM/TENANT
|
||||
├── mapper/
|
||||
│ └── SysAppMapper.java
|
||||
├── service/
|
||||
│ ├── ISysAppService.java
|
||||
│ └── impl/SysAppServiceImpl.java
|
||||
├── dto/
|
||||
│ ├── SysAppDTO.java # 详情响应(app_secret 脱敏为 ******)
|
||||
│ ├── SysAppSaveDTO.java # 创建/更新
|
||||
│ └── SysAppQueryDTO.java # 分页查询
|
||||
├── vo/
|
||||
│ └── AppCredentialsVO.java # 给 oauth2 用的精简视图(仅凭证字段)
|
||||
└── controller/
|
||||
├── SysAppController.java # /system/app/** (管理后台 CRUD)
|
||||
└── inner/
|
||||
└── SysAppInnerController.java # /system/inner/app/** (oauth2 Feign 调用)
|
||||
```
|
||||
|
||||
`rui-common-oauth2` 增加:
|
||||
```
|
||||
com.rui.common.oauth2.feign/
|
||||
└── SysAppFeign.java # @FeignClient 调用 system
|
||||
com.rui.common.oauth2.cache/
|
||||
└── AppCredentialsCache.java # Redis 包装,30min TTL
|
||||
```
|
||||
|
||||
`rui-common-core` 保持 `AppProperties` 不变(POJO 通用载体)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键流程
|
||||
|
||||
### 5.1 OAuth2 登录运行时获取凭证
|
||||
|
||||
```
|
||||
1. POST /oauth2/token?grant_type=wechat&code=xxx
|
||||
↓
|
||||
2. authorizationServerFilterChain 被调用
|
||||
↓
|
||||
3. 从 OAuth2ClientAuthenticationToken 取 clientId
|
||||
↓
|
||||
4. 查 rui_auth_oauth2_client 表 → 取 tenantId
|
||||
↓
|
||||
5. AppCredentialsCache.get(platform=wechat, tenantId)
|
||||
├─ HIT → 直接返回
|
||||
└─ MISS → Feign: GET /system/inner/app/getCredentials?platform=wechat&tenantId={tenantId}
|
||||
↓
|
||||
SysAppInnerController 查 sys_app:
|
||||
- 优先 owner_type=TENANT AND tenant_id={tid} AND status=1
|
||||
- 缺省 owner_type=PLATFORM AND tenant_id=0 AND status=1
|
||||
- 都找不到 → 返回 404(throw new UsernameNotFoundException 或 BizException)
|
||||
↓
|
||||
把 SysApp 转 AppCredentialsVO 返回
|
||||
↓
|
||||
oauth2 端写 Redis: SET app:creds:{platform}:{tenantId} = json EX 1800
|
||||
↓
|
||||
6. 拿到 AppCredentialsVO → 构造 WechatApiClient(appId, appSecret) 等
|
||||
↓
|
||||
7. 走完登录流程,返回 token
|
||||
```
|
||||
|
||||
### 5.2 租户管理自己的 SysApp
|
||||
|
||||
```
|
||||
1. 租户管理员登录管理后台
|
||||
2. POST /system/app (Body: SysAppSaveDTO)
|
||||
- owner_type=TENANT
|
||||
- platform=wechat
|
||||
- tenant_id = 当前用户 tenantId
|
||||
- 业务校验:uk_tenant_owner_platform 唯一性
|
||||
3. 写 DB
|
||||
4. 删缓存:DEL app:creds:wechat:{tenantId}
|
||||
5. 返回成功
|
||||
```
|
||||
|
||||
### 5.3 超管配置 PLATFORM 默认
|
||||
|
||||
```
|
||||
1. 超管登录管理后台(tenant_id=0)
|
||||
2. POST /system/app
|
||||
- owner_type=PLATFORM
|
||||
- platform=wechat
|
||||
- tenant_id=0
|
||||
3. 删缓存:DEL app:creds:wechat:0
|
||||
4. 所有未自配的租户下次登录都会拉到这条
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 缓存策略
|
||||
|
||||
- **Key**: `app:creds:{platform}:{tenantId}`(tenantId=0 表示 PLATFORM 默认)
|
||||
- **TTL**: 30 分钟(1800 秒)
|
||||
- **失效时机**:
|
||||
- `SysApp` CRUD 写操作完成后
|
||||
- `SysApp` 启/禁用操作后
|
||||
- 超管强制刷新(可选接口)
|
||||
- **防穿透**:缓存空对象 5 分钟(针对不存在的 tenantId)
|
||||
- **序列化**:用 fastjson2 序列化 `AppCredentialsVO`
|
||||
|
||||
---
|
||||
|
||||
## 7. OAuth2 端改造
|
||||
|
||||
### 7.1 改造点
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `OAuth2ServerConfig` | 删 `@Bean @ConfigurationProperties` 声明 wechat/alipay AppProperties |
|
||||
| 同上 | 注入 `SysAppFeign` + `AppCredentialsCache` |
|
||||
| `authorizationServerFilterChain` | 根据 grant_type 动态选 platform,调用 `appCredentialsCache.get(platform, tenantId)` 拿凭证 |
|
||||
|
||||
### 7.2 兼容与过渡
|
||||
|
||||
- 旧的 `thirdparty.*` Nacos 配置可以保留一段过渡期,但不读取
|
||||
- `AppProperties` 仍可作为"应用启动时的兜底",但 OAuth2 登录链路不再使用
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考虑
|
||||
|
||||
1. **脱敏返回**:`SysAppDTO` 中 `app_secret_text` 返回 `******`,详情接口需要 `*:*:app:detail` 权限
|
||||
2. **审计日志**:所有 CRUD 写操作记录操作人
|
||||
3. **加密占位**:`is_encrypted` 字段保留,后续接入 AES 时只改 service 层
|
||||
4. **租户越权防护**:CRUD 接口根据当前用户 tenantId 过滤;非超管只能操作 `tenant_id=current` 的记录
|
||||
|
||||
---
|
||||
|
||||
## 9. 边界与不做
|
||||
|
||||
- **不做**:AES 加密实现(仅预留 `is_encrypted` 字段)
|
||||
- **不做**:SysApp 导入导出
|
||||
- **不做**:SysApp 版本控制/变更历史(仅靠 `updated_by/at`)
|
||||
- **不做**:多语言 i18n(所有界面文案中文)
|
||||
- **不涉及**:`rui-service-user` 模块(user 表/用户登录管理不在本次范围)
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
1. 超管能创建 `owner_type=PLATFORM` 的 wechat 默认配置
|
||||
2. 租户能创建 `owner_type=TENANT` 的 wechat 自配(覆盖默认)
|
||||
3. CRUD 接口都通;`uk_tenant_owner_platform` 唯一约束生效
|
||||
4. `SysAppInnerController.getCredentials` 能被 oauth2 Feign 调用
|
||||
5. OAuth2 登录时凭证从缓存读,CRUD 后缓存被清
|
||||
6. 不存在的 tenantId 第二次调用不会再打 DB(空对象缓存)
|
||||
7. 编译通过 21 个模块 BUILD SUCCESS
|
||||
Reference in New Issue
Block a user