Files
rui-docs/superpowers/specs/2026-06-07-sys-app-management-design.md
vifo 4271f333b6 docs(specs): 更新已完成 spec 文档状态
- 2026-06-07-sys-app-management: 已批准,待实现 → 已实现
- 2026-06-07-multi-login-social-login: 已批准,待实现 → 已实现
- 2026-06-06-user-aggregate-query: 已批准 → 已实现

各 spec 对应的 plan 已结清/实施 commit 已落地,更新状态描述并补充实施情况指引。
2026-06-07 18:46:30 +08:00

255 lines
11 KiB
Markdown
Raw Permalink 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
> **状态**: 已实现(2026-06-07
> **作者**: AI Assistant
> **实施情况**: SysApp 实体/枚举、Mapper/Service/CRUD、Inner 接口、Feign 集成、AppCredentialsCache、降级 FallbackFactory、菜单注册均已落地,详见 git log `27fa187` 起各 commit。
---
## 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**用 `appId` 作为唯一标识**(来自请求头 `X-App-Id`
---
## 2. 核心设计原则
1. **多租户隔离**:通过 `owner_type` + `tenant_id` 双重区分
2. **凭证集中管理**:一个 `SysApp` 记录 = 一个第三方应用在本系统的接入凭证
3. **缓存优先**:高频读取走 Redis,CRUD 操作失效缓存
4. **简单文本用列 / 多证书用 JSON**:简单凭证(app_id、app_secret、app_key、aes_key)直接用 VARCHAR 列;多证书场景(如支付宝 p12 包含 private_key+public_key+证书链)用 JSON 数组存,每项含 `name/path/password`
5. **兼容现有 `AppProperties`**`AppProperties` 仍作为通用 POJO 留在 `rui-common-core`,但实际数据从 DB 加载后映射到 `AppProperties`(简单字段直接映射;`certificates` 数组由调用方单独处理)
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_key
app_id VARCHAR(200) DEFAULT NULL COMMENT '应用ID',
app_secret VARCHAR(500) DEFAULT NULL COMMENT '应用密钥',
app_key VARCHAR(200) DEFAULT NULL COMMENT '应用Key(部分平台如支付宝)',
-- 多证书:支付宝 p12 等含 private_key+public_key+证书链的复合证书
-- 每项:{name, path, password}path 存对象存储相对路径
certificates JSON DEFAULT NULL COMMENT '多证书列表(p12等复合证书)',
-- 应用自定义 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),
UNIQUE KEY uk_app_id (app_id),
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
Header: X-App-Id: wx1234567890
2. authorizationServerFilterChain 被调用
3. 从请求头 X-App-Id 取 appId
4. AppCredentialsCache.get(appId)
├─ HIT → 直接返回
└─ MISS → Feign: GET /system/inner/app/getCredentials?appId={appId}
SysAppInnerController 查 sys_app
- WHERE app_id = {appId} AND status = 1
app_id 字段已加 UNIQUE 约束)
- 找不到 → 返回 404
把 SysApp 转 AppCredentialsVO 返回
oauth2 端写 Redis: SET app:creds:{appId} = json EX 1800
5. 拿到 AppCredentialsVO → 构造 WechatApiClient(appId, appSecret) 等
6. 走完登录流程,返回 token
```
### 5.2 租户管理自己的 SysApp
```
1. 租户管理员登录管理后台
2. POST /system/app Body: SysAppSaveDTO
- owner_type=TENANT
- platform=wechat
- app_id = "wx9999999"
- tenant_id = 当前用户 tenantId
- 业务校验:uk_tenant_owner_platform + uk_app_id 唯一性
3. 写 DB
4. 删缓存:DEL app:creds:{app_id}
5. 返回成功
```
### 5.3 超管配置 PLATFORM 默认
```
1. 超管登录管理后台(tenant_id=0
2. POST /system/app
- owner_type=PLATFORM
- platform=wechat
- app_id = "wx1234567"
- tenant_id=0
3. 删缓存:DEL app:creds:wx1234567
4. 所有未自配(uk_app_id 不冲突)的租户下次登录会用 appId=wx1234567 拉这条
```
---
## 6. 缓存策略
- **Key**: `app:creds:{appId}`(用 `app_id` 唯一标识,不用 platform/tenantId 因为多租户都叫 platform=wechat 会冲突)
- **TTL**: 30 分钟(1800 秒)
- **失效时机**
- `SysApp` CRUD 写操作完成后
- `SysApp` 启/禁用操作后
- 超管强制刷新(可选接口)
- **防穿透**:缓存空对象 5 分钟(针对不存在的 appId)
- **序列化**:用 fastjson2 序列化 `AppCredentialsVO`
---
## 7. OAuth2 端改造
### 7.1 改造点
| 文件 | 改动 |
|---|---|
| `OAuth2ServerConfig` | 删 `@Bean @ConfigurationProperties` 声明 wechat/alipay AppProperties |
| 同上 | 注入 `SysAppFeign` + `AppCredentialsCache` |
| `authorizationServerFilterChain` | 从 `request.getHeader("X-App-Id")` 拿 appId,调 `appCredentialsCache.get(appId)` 拿凭证 |
### 7.2 兼容与过渡
- 旧的 `thirdparty.*` Nacos 配置可以保留一段过渡期,但不读取
- `AppProperties` 仍可作为"应用启动时的兜底",但 OAuth2 登录链路不再使用
---
## 8. 安全考虑
1. **脱敏返回**`SysAppDTO``app_secret` 返回 `******`,详情接口需要 `*:*: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