Files
rui-docs/superpowers/specs/2026-06-07-sys-app-management-design.md
T
vifo 33690fe80b docs(spec): 第三方应用管理(SysApp)设计规格
- 新增 SysApp 实体:多租户第三方应用凭证管理
- 平台元数据 + 支付扩展 + AES key + 证书/文本分离
- 消费方:OAuth2ServerConfig Feign + Redis 30min 缓存
- 租户隔离:owner_type=PLATFORM/TENANT + client_id→tenant_id 映射
- 范围:不涉及 rui-service-user(user 模块)
2026-06-07 16:53:36 +08:00

258 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第三方应用管理(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