4271f333b6
- 2026-06-07-sys-app-management: 已批准,待实现 → 已实现 - 2026-06-07-multi-login-social-login: 已批准,待实现 → 已实现 - 2026-06-06-user-aggregate-query: 已批准 → 已实现 各 spec 对应的 plan 已结清/实施 commit 已落地,更新状态描述并补充实施情况指引。
255 lines
11 KiB
Markdown
255 lines
11 KiB
Markdown
# 第三方应用管理(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
|