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:
2026-06-07 16:53:36 +08:00
parent ae78e0f673
commit 33690fe80b
@@ -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
- 都找不到 → 返回 404throw 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