# 第三方应用管理(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