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

11 KiB
Raw Permalink Blame History

第三方应用管理(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 缓存 30minappId 作为唯一标识(来自请求头 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. 兼容现有 AppPropertiesAppProperties 仍作为通用 POJO 留在 rui-common-core,但实际数据从 DB 加载后映射到 AppProperties(简单字段直接映射;certificates 数组由调用方单独处理)
  6. 加密占位:预留 is_encrypted 字段,暂不实现 AES 加密逻辑

3. 数据库设计

3.1 表 sys_app

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. 脱敏返回SysAppDTOapp_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