From 33690fe80bf032cd29816d04fc1eb725adca36c5 Mon Sep 17 00:00:00 2001 From: pigeon Date: Sun, 7 Jun 2026 16:53:36 +0800 Subject: [PATCH] =?UTF-8?q?docs(spec):=20=E7=AC=AC=E4=B8=89=E6=96=B9?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=AE=A1=E7=90=86=EF=BC=88SysApp=EF=BC=89?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=A7=84=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SysApp 实体:多租户第三方应用凭证管理 - 平台元数据 + 支付扩展 + AES key + 证书/文本分离 - 消费方:OAuth2ServerConfig Feign + Redis 30min 缓存 - 租户隔离:owner_type=PLATFORM/TENANT + client_id→tenant_id 映射 - 范围:不涉及 rui-service-user(user 模块) --- .../2026-06-07-sys-app-management-design.md | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 superpowers/specs/2026-06-07-sys-app-management-design.md diff --git a/superpowers/specs/2026-06-07-sys-app-management-design.md b/superpowers/specs/2026-06-07-sys-app-management-design.md new file mode 100644 index 0000000..8966c1c --- /dev/null +++ b/superpowers/specs/2026-06-07-sys-app-management-design.md @@ -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