Compare commits

..

88 Commits

Author SHA1 Message Date
vifo 7fdd93fc4b docs: 补充支付设计文档、store 字段设计文档及实施计划 2026-06-09 02:02:21 +08:00
vifo a79be07c52 docs: 新增 api-portal API 文档门户设计文档 2026-06-09 01:56:37 +08:00
vifo 7b2f3d77ca docs: 支付模块开发文档 + git 域名更新
- 新增 backend/design/支付模块架构概览.md
- 新增 backend/design/支付模块数据库设计.md (21张表 DDL)
- 新增 backend/design/支付模块接口设计.md
- git.dev.vifo.cc → git.vifo.cc 全局替换
2026-06-09 01:54:29 +08:00
vifo 1324a52049 docs(standards): 补充 Result<T> 统一响应类文档,修正 API 设计规范响应格式
- 新增 Result统一响应类.md 完整文档
- 修正 API设计规范.md 中响应字段与实际代码不一致的问题
- 错误码规范表按实际 ResultCode 枚举对齐
2026-06-08 16:01:33 +08:00
vifo d4a3bd5847 docs: 新增 AI 助手快速上手指南(面向开发者)
- 开工准备、常用指令模板、GitNexus 速查
- AGENTS.md 精简原则和检查清单
- 目录结构速查
2026-06-08 13:17:05 +08:00
vifo 8b2de75b5f refactor: 统一 remote 名称为 origin
- gitea-api.md: 删除 remote 名称差异表,统一使用 origin
- AI开发操作手册.md: push gitea main -> push origin main
2026-06-08 13:06:12 +08:00
vifo c7244b3b87 feat: 完善工单路由规则和 Gitea API 指南
- gitea-api.md: 补全 5 个仓库地址、工单路由表、URL 前缀映射、remote 名称、关闭 Issue API
- issue-workflow.md: 增加工单归属判断、跨仓库提工单流程、列出/关闭工单 API
2026-06-08 13:00:13 +08:00
vifo a7f3ee3565 refactor: 全局替换 spring-ai -> rui-framework
同步仓库名称变更,涉及 16 个文件 66 处引用:
- ai-skills: 菜单配置
- backend/guides: AI操作手册、环境配置、部署、gitnexus、opencode 工作流
- backend: 模块创建规则、通信规范、协作工作流、实施规范
- frontend: 收银设计、管理后台实施计划
- standards: 数据库设计规范
2026-06-08 12:56:39 +08:00
vifo 12a263c451 feat: 迁移 GitNexus 通用技能到全局文档仓库
- 从各仓库 .claude/skills/gitnexus/ 迁移 6 个技能
- 更新 ai-skills/README.md 说明目录结构
- 所有仓库的 GitNexus 技能完全相同,统一维护
2026-06-08 12:50:24 +08:00
vifo 2253ebea92 docs: update API design standards 2026-06-08 11:23:17 +08:00
vifo 76260b9458 docs(plan): mark Task 7 as completed (all 7 tasks done, E2E verified via static checks) 2026-06-08 11:23:17 +08:00
vifo 3d16902489 docs(plan): mark Task 6 as completed (form dialog created, commit 5231e50) 2026-06-08 11:23:17 +08:00
vifo fdc74784c2 docs(plan): mark Task 5 as completed (list page created, commit 3a64850) 2026-06-08 11:23:17 +08:00
vifo 8fed3a04be docs(plan): mark Task 4 as completed (route registered, commit e961bc5) 2026-06-08 11:23:17 +08:00
vifo a5863706dc docs(plan): mark Task 3 as completed (i18n added, commit 98741a0) 2026-06-08 11:23:17 +08:00
vifo d07a3f7f4b docs(plan): mark Task 2 as completed (sysAppService exported, commit 0b4b02f) 2026-06-08 11:23:17 +08:00
vifo 2152d0de42 docs(plan): mark Task 1 as completed (sysAppService created, commit 67d6686) 2026-06-08 11:23:17 +08:00
vifo cd2d68e60e docs(plan): add SysApp application integration management plan
- 7 个有序任务:Service → Index → i18n → Router → List → Form → E2E
- 任务粒度合适,每个聚焦单一文件/职责
- 包含完整依赖图、回滚计划、测试清单
- 严格遵循现有 plan 格式

对应工单 rui/rui-frontend#4
2026-06-08 11:23:17 +08:00
vifo f4761ae145 docs(spec): add SysApp application integration management design
- 为 rui/rui-frontend#4 工单编写设计规范
- 7 个文件变更:service 新建、index 改、router 改、locales 改、2 个 vue 新建
- 4 Tab 表单布局(基础信息/凭证信息/接口配置/高级)
- 敏感字段脱敏(appSecret/appKey/aesKey)
- certificates 字段 JSON 占位(等 rui/rui-framework#4 文件上传接口)

对应工单 rui/rui-frontend#4
2026-06-08 11:23:17 +08:00
vifo bb71263bdd docs: add superpowers design docs and plans 2026-06-08 11:23:17 +08:00
vifo b9d5b6d9f0 docs: 更新 Gitea API 文档,添加完整的 Issue 创建流程和示例 2026-06-08 11:23:17 +08:00
vifo 24a8643fb6 docs: 添加后端 API 工单 - 用户编辑接口密码字段处理优化 2026-06-08 11:23:17 +08:00
vifo 20d4a545b4 docs: 添加前后端智能协作方案实现计划 2026-06-08 11:23:17 +08:00
vifo 9957d85595 docs: 添加前后端智能协作方案设计文档 2026-06-08 11:23:17 +08:00
vifo f26c67368c docs(spec): 上传 API 增加 fileName + extract=zip 自动解压
- §7.1 补充 fileName (可选)、extract (bool)
- 响应统一为 Result<List<SysFileUploadVO>>:单文件也是长度 1 的数组
- §7.4 fileName 行为说明 (不传/传/校验)
- §7.5 extract=true (ZIP 解压):
  - 行为 + 路径规则 (bizType/zipBaseName/entryName)
  - 安全护栏 (总 entry ≤ 100, 单 entry ≤ maxSize, entry 名正则)
  - 请求/响应/订阅方事件示例
- 对应后端 commit: c4a5d5f
2026-06-07 23:17:14 +08:00
vifo 3aebe0b5a5 docs(spec/plan): FileBizType 改为工具类(非枚举),bizType 自由字符串
设计调整原因:上传服务是统一基础设施,强制枚举会要求「加新模块 = 改框架代码」。
改为 final class + normalize() 格式校验,业务模块自定 bizType 字符串即可。

- spec §4.2: 整个 FileBizType 章节重写(工具类 + 格式约束 + 设计意图)
- spec flow 5/11: 校验从 values() 改为 normalize()
- spec API 表 / 代码结构表 / 订阅方示例 同步更新
- plan Step 1.2/1.4 / 状态表 / 流程校验 / 发布器签名 同步更新
2026-06-07 22:44:12 +08:00
vifo 5f19332f06 docs(nacos): rui-service-storage 业务配置模板
- 包含 rui.file.* 全部业务配置
  active / default-max-size / biz-types (4个) / aliyun / tencent / local
- 服务端口 9400
- 严格遵循 docs/ai-skills/nacos-config-rules.md Rule 2
  仅放本服务特有业务配置,不含 Nacos 连接/编码/feign 等通用项

关联 Gitea #4 / 设计文档 §9.3
2026-06-07 22:23:27 +08:00
vifo a10b712919 docs(plan): 文件存储服务(rui-service-storage)实施计划
- 16 个可独立 commit 的子任务
- Task 1: 常量 + 枚举
- Task 2: sys_file DDL
- Task 3-9: 模块骨架 + Strategy 模式
- Task 10-12: 实体/Service/Controller/Event
- Task 13-14: 集成启动器 + 公共配置
- Task 15-16: 编译验证 + Gitea 关闭

关联 Gitea #4
2026-06-07 21:31:59 +08:00
vifo 66f0712486 docs(spec): 文件存储服务(rui-service-storage)设计文档
- 独立微服务 rui-service-storage 统一上传接口
- 支持阿里云 OSS / 腾讯云 COS / 本地 Strategy
- 内置 @AutoPermission 鉴权
- Redis pub/sub ON_UPLOAD / ON_FILE_DELETED 事件
- topic 常量集中在 rui-common-core/.../constants/MqTopicConstants
- 关联 Gitea #4
2026-06-07 21:25:31 +08:00
vifo b492c6224a docs(plan): wechat/alipay 凭证动态加载计划 - 完成状态同步
- 25 个 checkbox 全部勾选
- 添加实施状态(commit e3a441b)
- 补充实施完成报告
2026-06-07 19:17:10 +08:00
vifo 78e5ebc17e docs(plan): 修正 AlipayApiClient 字段映射
AlipayApiClient 构造需 (appId, privateKey, publicKey),
AppCredentialsVO 无这两个字段(按 spec 在 certificates JSON 数组里)。
AlipayApiClient.getAccessToken 当前抛 UnsupportedOperationException,
所以 privateKey/publicKey 暂用空串占位,cert 解析留作后续 Task。
2026-06-07 19:12:09 +08:00
vifo 2bfd1c0f4f docs(plan): wechat/alipay Provider 凭证动态加载改造计划
修复凭证烧死 bug:把 WeixinAuthenticationProvider /
AlipayAuthenticationProvider 改为持有 AppCredentialsCache,
buildToken 内部按 X-App-Id 头动态解析凭证并构造 API 客户端。

- 任务 1: WeixinAuthenticationProvider 改造
- 任务 2: AlipayAuthenticationProvider 改造
- 任务 3: OAuth2ServerConfig 清理
- 任务 4: AlipayApiClient 字段映射核对
2026-06-07 19:03:35 +08:00
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
vifo 4540af71ae docs(ai-skills): README 索引加 sql-deploy.md 入口 2026-06-07 18:25:34 +08:00
vifo eba4f07832 docs(standards): 增加 Feign 客户端注册规范章节
按用户提醒 + 本次实际踩坑记录:
- 项目用 rui-common-feign 自定义注册机制
- spring.factories 是 Feign 唯一注册渠道(@EnableFeignClients 包扫描不生效)
- 添加新 FeignClient 必须同步更新 spring.factories
- 漏写会导致运行时 NPE
2026-06-07 17:54:04 +08:00
vifo 3395f69b42 docs(standards): 增加 Result 返回规范章节
按用户反馈(i18n key 用 code 字符串,key 放 data 字段):
- 字段语义:error/Http、code/i18n key、message/默认中文、data/业务数据
- 调用规范表:成功/校验失败/未授权/无权限/数据不存在/降级/通用失败
- 重点:数据不存在用 failNotFound(ResultCode.DATA_NOT_FOUND, key)
- 禁止写法:Result.ok(null) 表示'未找到'(反直觉)
- i18n 配合示例:前端用 code 字段路由 i18n,data 字段做模板替换
2026-06-07 17:49:19 +08:00
vifo 22889afedc docs(spec): 用 appId 作为唯一标识(来自 X-App-Id 请求头)
原方案用 platform 作为缓存 key 错误:
- 多个租户都叫 platform=wechat,缓存 key 冲突
- platform 不是真正唯一标识

按用户反馈调整:
- 凭证查询/缓存全部用 appId
- appId 来源:请求头 X-App-Id
- §3.1 加 UNIQUE KEY uk_app_id (app_id) 约束
- §5.1 流程重写:从 X-App-Id 取 appId → 缓存 key = app:creds:{appId}
- §5.2/5.3 删缓存也改用 appId
- §7.1 OAuth2 改造:从 request.getHeader("X-App-Id") 取
- 删除 §1.2 中 'client_id 映射' 的旧说法
2026-06-07 17:08:32 +08:00
vifo c576053ab6 docs(spec): SysApp 简化为单密钥列 + 多证书 JSON 数组
按用户反馈调整:
- 删除:app_secret_text / app_secret_file_path / private/public_key 的 text+file_path
- 新增:certificates JSON(多 p12 证书,每项含 name/path/password)
- 简单凭证(app_id/app_secret/app_key/aes_key)保留单值 VARCHAR 列
- §2 原则 4 改为'简单文本用列 / 多证书用 JSON'
2026-06-07 17:02:06 +08:00
vifo 33690fe80b docs(spec): 第三方应用管理(SysApp)设计规格
- 新增 SysApp 实体:多租户第三方应用凭证管理
- 平台元数据 + 支付扩展 + AES key + 证书/文本分离
- 消费方:OAuth2ServerConfig Feign + Redis 30min 缓存
- 租户隔离:owner_type=PLATFORM/TENANT + client_id→tenant_id 映射
- 范围:不涉及 rui-service-user(user 模块)
2026-06-07 16:53:36 +08:00
vifo ae78e0f673 docs(nacos): 前缀 social → thirdparty,匹配 OAuth2ServerConfig 改动 2026-06-07 16:15:08 +08:00
vifo 856e16beff docs(nacos): 用 app.wechat/app.alipay 替代 social.* 配置结构
配合 OAuth2ServerConfig 重构:
- social.wechat.app-id/secret → app.wechat.app-id/secret
- social.alipay.app-id/private-key/public-key → app.alipay.*
- 新增 app.alipay.app-key (AlipayApiClient 预留扩展)
2026-06-07 16:13:09 +08:00
vifo 6df6e7ad0c docs(plan): 标记 Task 12 (编译验证) 已完成 (commits 74960af + baf0283)
附:实际执行包含修复 plan 的 supports 签名 bug (3 个 Provider)
2026-06-07 15:56:28 +08:00
vifo 365aa49cbd docs(plan): 标记 Task 11 (sys_oauth_client grant_types) 已完成 (commit 74960af) 2026-06-07 15:53:52 +08:00
vifo 792b10bd34 docs(plan): 标记 Task 10 (Nacos 社交登录配置) 已完成 (commit 2249b36) 2026-06-07 15:52:45 +08:00
vifo 2249b3649a docs(nacos): 添加社交登录配置 (rui-auth.yaml)
- 微信登录配置 (app-id, app-secret)
- 支付宝登录配置 (app-id, private-key, public-key)
- 用 env var 占位符支持容器化部署
2026-06-07 15:52:34 +08:00
vifo b3c66245e9 docs(plan): 标记 Task 9 (OAuth2 配置注册) 已完成 (commit 22c64bd)
附:实际还包含让原 no-arg 构造编译失败的修复
2026-06-07 15:51:25 +08:00
vifo eb99ae43cb docs(plan): 标记 Task 8 (支付宝登录框架) 已完成 (commit a4fcb95) 2026-06-07 15:49:17 +08:00
vifo c69c34ff25 docs(plan): 标记 Task 7 (微信登录框架) 已完成 (commit 337d189) 2026-06-07 15:47:31 +08:00
vifo 938302c164 docs(plan): 标记 Task 6 (短信登录框架) 已完成 (commit 89645d5) 2026-06-07 15:45:16 +08:00
vifo f1f4440be2 docs(plan): 标记 Task 5 (OAuth2 密码登录扩展) 已完成
- 3 个文件修改完成 (commit 2488bcf)
- 标注:Step 3 跳过 (loadUserByAccount 已支持 EMAIL)
- 关键修补:loadUserByUsername 增加 # 解码 (plan 漏掉的实现漏洞)
2026-06-07 15:42:36 +08:00
vifo 47ef4c6938 docs(plan): 标记 Task 4 (内部接口 EMAIL 支持) 已完成 (commit 27f4a00) 2026-06-07 15:38:35 +08:00
vifo 00c77529c5 docs(plan): 标记 Task 3 (数据访问层) 已完成
- UserSocialMapper/Service/Impl 创建完成 (commit c147e56)
- 标注 5 处实际执行的偏差(#prefix#、@EnableRedisCache、@Transactional、baseMapper、tenantId)
2026-06-07 15:37:38 +08:00
vifo 23823074f6 docs(plan): 标记 Task 2 (实体类调整) 已完成
- User 实体加 email、UserDetail 删 email、UserSocial 新建 (commit 1de6937)
- 添加实际执行说明:UserSocial 屏蔽 BaseEntity 不存在的审计字段
2026-06-07 15:34:26 +08:00
vifo 84b3bb601e docs(spec): 添加多方式登录与第三方登录设计规格 (已批准) 2026-06-07 15:32:26 +08:00
vifo 3c618b57bb docs(plan): 标记 Task 1 (数据库变更) 已完成
- Step 1-4 全部完成 (commit 6fd82fb)
- 添加实际执行说明:email 字段位置改为 AFTER username
2026-06-07 15:31:52 +08:00
vifo 9d0cffa86e docs(plan): 添加多方式登录与第三方登录实施计划
- 12个任务,覆盖数据库、实体、数据访问、认证逻辑、配置
- 详细的步骤和代码示例
- 包含编译验证和检查清单
2026-06-07 15:13:02 +08:00
vifo a4767ee3d0 docs(plan): 更新实施计划状态为已完成
- 所有20个任务已完成
- 编译验证通过
- 代码已提交
2026-06-06 17:10:40 +08:00
vifo 3c2fa877a6 docs(plan): 用户聚合查询实施计划
- 20个详细任务分解
- 包含依赖关系图
- 数据库变更、代码修改、缓存、测试全覆盖
- 风险评估和回滚计划
2026-06-06 13:32:44 +08:00
vifo de78c21799 docs(spec): 更新用户聚合查询设计规格
- 添加 phone 字段迁移到 uc_user 表的设计
- 新增统一认证接口 /user/inner/auth/load(POST)
- 支持 AccountType 枚举:USERNAME/PHONE/EMAIL
- 废弃旧的 loadByUsername 接口
- 添加数据库变更 SQL 脚本
2026-06-06 13:30:23 +08:00
vifo a8c164459a docs(spec): 用户聚合查询设计规格
- 方案B:后端聚合查询 + Redis缓存
- 新增聚合接口 /user/admin/user/{id}/aggregate
- 批量列表查询优化,避免N+1
- 缓存策略与失效机制设计
- 保持向后兼容
2026-06-06 13:14:13 +08:00
vifo 47d8af24f0 docs(standards): 添加 MyBatis Plus LambdaQueryWrapper 规范
- 在数据库规范中新增 MyBatis Plus 查询规范章节
- 明确优先使用 LambdaQueryWrapper,避免使用字符串字段名的 QueryWrapper
- 添加正反对照示例和优势说明

对应工单 #2
2026-06-06 12:42:24 +08:00
vifo 1a615c9f15 docs(api): 补充收银服务 Swagger/Knife4j/OpenAPI 访问地址 2026-06-06 10:25:12 +08:00
vifo 26f146f9a7 docs(api): 补充聚合启动器 API 文档地址
- 添加 rui-service-starter 的 /v3/api-docs 地址(端口 9399)
- 标注为开发调试用途

对应工单 #swagger-doc-update
2026-06-06 10:16:07 +08:00
vifo cfd2c44b80 docs(api): 补充 Swagger/Knife4j/OpenAPI 访问地址文档 2026-06-06 10:14:07 +08:00
vifo 33351d3261 Merge remote-tracking branch 'origin/main' 2026-06-05 11:57:47 +08:00
vifo 47945b245d docs(backend): 添加 BaseEntity 字段说明文档
- 说明所有业务实体必须继承的标准字段
- 包含字段类型、填充时机、数据库映射说明
- 提供 Java 和 SQL 使用示例
2026-06-05 11:55:44 +08:00
vifo 9f084720f8 feat(commit): 补充提交频率和推送规则
- 添加提交频率要求:每次修改后必须提交,禁止积攒
- 添加推送规则:常规开发不自动推送,累计超10个提交自动推送
- 远程名称使用 origin(默认)
2026-06-05 11:48:01 +08:00
vifo 044f9e31f3 清理暂时无用的 nacos 配置文件 2026-06-05 10:29:29 +08:00
vifo 5931d65806 fix(nacos): 清理 rui-service-starter.yaml 中的冗余通用配置
- 删除 nacos 连接、lifecycle、autoconfigure、multipart 等通用配置
- 仅保留 server.port 和模块管理业务配置
- 符合 Nacos 服务配置只放端口 + 业务配置的规则
2026-06-05 10:25:04 +08:00
vifo 57cc87a145 docs(template): 在 application-template.yml 中标注允许修改位置
- server.port 处增加【允许修改】注释
- rui-data import 处增加【允许修改】注释
2026-06-05 10:24:44 +08:00
vifo 386751b045 docs(ai-skills): 更新 README 目录,加入 Nacos 配置规范文档 2026-06-05 10:23:24 +08:00
vifo ee335fe90a docs(ai-skills): 添加 Nacos 配置与 application.yml 规范文档
- 明确 5 条配置规则
- 提供正确/错误示例
- 说明配置分层和同步流程
2026-06-05 10:20:12 +08:00
vifo 2277bca1ad feat(config): 简化 rui-service-starter 配置
- 基于 application-template.yml 模板
- 只保留端口、服务名和 modules 配置
- 移除多余的 feign/resilience4j/logging 配置

对应工单 #1
2026-06-05 08:42:57 +08:00
vifo 726fc229d6 feat(config): 完善 rui-service-starter Nacos 完整配置
- 基于 application-template.yml 创建完整配置
- 增加 modules 可用模块列表
- 对应工单 #1
2026-06-05 08:40:42 +08:00
vifo 1d6cb64ed3 feat(config): 简化 rui-service-starter Nacos 配置
- 只保留端口、服务名和 modules 配置
- 移除多余的 Spring Cloud 配置(已在本地 application.yml 中配置)

对应工单 #1
2026-06-05 08:35:00 +08:00
vifo 61fa206a45 feat(config): 完善 rui-service-starter Nacos 配置
- 基于 application-template.yml 模板创建完整配置
- 修改端口为 9399,名称为 rui-service-starter
- 增加 modules 可用模块列表配置

对应工单 #1
2026-06-05 08:32:19 +08:00
vifo 8506424c26 feat(config): 添加 rui-service-starter Nacos 配置模板
- 新增 rui-service-starter.yaml Nacos 配置模板
- 添加 modules 可用模块列表配置(供租户管理使用)

对应工单 #1
2026-06-05 08:30:28 +08:00
vifo 01f36acdd7 feat(config): 添加 rui-service-starter Nacos 配置模板和 modules 配置示例
- 新增 rui-service-starter.yaml Nacos 配置模板(含 modules 配置)
- 更新 application-template.yml 添加 modules 配置说明和示例

对应工单 #1
2026-06-05 08:23:23 +08:00
vifo cc46e13503 docs: 更新 SQL 脚本目录引用路径
- backend/guides/environment-setup.md: docs/init-database.sql → sql/init-database.sql
- frontend/admin-ui-icon-guide.md: docs/sql/update_menu_icon.sql → sql/update_menu_icon.sql
2026-06-05 06:56:53 +08:00
vifo 21cea34d18 fix(menus) 修改菜单配置文件路径为 data/menus 2026-06-05 06:18:07 +08:00
vifo f234845e17 fix: 修复配置文件路径,移除 conf/ 子目录引用 2026-06-04 14:25:05 +08:00
vifo 6e0919f1cd ci: 修复缓存卷冲突,添加 runner-default 缓存,隐藏 webhook token 2026-06-04 13:33:41 +08:00
vifo 8896b9aa58 feat(ai-skills): 添加 AI 全局技能库
创建 ai-skills/ 目录,包含所有 AI 助手共享的技能规范:

- gitea-api.md: Gitea API 使用指南

- issue-workflow.md: 工单处理标准流程

- menu-config.md: 菜单配置规范

- commit-standards.md: 提交规范

- README.md: 使用说明
2026-06-04 10:04:07 +08:00
vifo 32c7db7e49 Merge branch 'main' of ssh://git.dev.vifo.cc:222/rui/rui-docs 2026-06-04 09:38:52 +08:00
vifo cab0cf91c0 docs: 补充收银系统菜单数据结构
- 添加菜单 JSON 结构说明(code/type/icon/path/permission/buttons)
- 添加完整菜单配置 JSON(第 8 节)
- 添加权限标识汇总表
2026-06-04 09:38:52 +08:00
vifo 19de7e24ec docs: 迁移 spring-ai 通用文档到 rui-docs
从 docs-local 迁移以下文档:

- backend/guides/: AI开发环境配置、Nacos配置、GitNexus指南、OpenCode工作流等

- backend/templates/: Superpowers设计模板、计划模板、审查清单

- backend/config-templates/: 应用配置模板、Nacos配置

- backend/design/: 数据库表结构规划

- backend/specs/: 项目文档治理、MQ统一推送设计

- backend/: 代码分析报告、Feign分析报告、文档治理报告

- frontend/design/: Admin-UI分模块打包设计
2026-06-04 09:34:03 +08:00
vifo 2e38c53434 Merge branch 'main' of ssh://git.dev.vifo.cc:222/rui/rui-docs 2026-06-04 09:30:21 +08:00
vifo 87b7780dd6 docs: 补充收银系统 Git 仓库信息
- 添加项目仓库地址说明
- 明确 Issue 应提交到 rui-cashier 仓库
- 避免混淆 spring-ai 和 rui-cashier 仓库
2026-06-04 09:29:18 +08:00
82 changed files with 21514 additions and 59 deletions
+2 -2
View File
@@ -38,7 +38,7 @@ rui-docs/
```bash
# 添加 submodule
git submodule add ssh://git@git.dev.vifo.cc:222/rui/rui-docs.git docs
git submodule add ssh://git@git.vifo.cc:222/rui/rui-docs.git docs
# 更新到最新
git submodule update --remote
@@ -50,7 +50,7 @@ git submodule update --init --recursive
### 独立查看
```bash
git clone ssh://git@git.dev.vifo.cc:222/rui/rui-docs.git
git clone ssh://git@git.vifo.cc:222/rui/rui-docs.git
cd rui-docs
```
+49
View File
@@ -0,0 +1,49 @@
# AI 全局技能库
> 所有 AI 助手共享的技能和流程规范
> 通过 git submodule 引用到各项目中
## 目录结构
```
ai-skills/
├── README.md # 本说明
├── nacos-config-rules.md # Nacos 配置与 application.yml 规范
├── issue-workflow.md # 工单处理流程
├── gitea-api.md # Gitea API 使用指南
├── menu-config.md # 菜单配置规范
├── commit-standards.md # 提交规范
├── sql-deploy.md # SQL 变更后置流程(推本地库 + 菜单 + 前端工单)
└── gitnexus/ # GitNexus 代码智能技能(所有仓库通用)
├── gitnexus-cli/SKILL.md # CLI 命令(索引、状态、清理、Wiki)
├── gitnexus-debugging/SKILL.md # 调试追踪(错误定位、根因分析)
├── gitnexus-exploring/SKILL.md # 代码探索(架构理解、执行流追踪)
├── gitnexus-guide/SKILL.md # 工具参考(工具、资源、Schema)
├── gitnexus-impact-analysis/SKILL.md # 影响分析(变更安全评估)
└── gitnexus-refactoring/SKILL.md # 重构安全(重命名、提取、拆分)
```
## GitNexus 技能说明
GitNexus 技能是从各仓库 `.claude/skills/gitnexus/` 迁移而来的**仓库无关**通用技能。
所有仓库(rui-framework、rui-frontend、rui-cashier、rui-payment)的 GitNexus 技能完全相同,
因此统一迁移到全局文档仓库维护。
## 使用方式
各项目通过 submodule 引用后,AI 可在以下路径读取:
```
项目/rui-docs/ai-skills/
```
对于支持 `.claude/skills/` 的 Claude 项目,可以创建符号链接:
```bash
# 将全局技能链接到项目的 .claude/skills/
ln -s ../../rui-docs/ai-skills/gitnexus .claude/skills/gitnexus
```
## 更新流程
1. 在 rui-docs 仓库修改全局技能
2. 提交并推送到远程
3. 各项目执行 `git submodule update --remote` 获取更新
+61
View File
@@ -0,0 +1,61 @@
# 提交规范
## 语义化前缀
| 前缀 | 说明 | 示例 |
|------|------|------|
| `feat:` | 新功能 | feat(menu): 添加收银系统菜单 |
| `fix:` | 修复问题 | fix(order): 修复订单计算错误 |
| `docs:` | 文档更新 | docs: 更新 README |
| `chore:` | 杂项 | chore: 更新依赖版本 |
| `test:` | 测试相关 | test: 添加单元测试 |
| `refactor:` | 重构 | refactor: 优化查询逻辑 |
## 提交信息格式
```
<type>(<scope>): <subject>
<body>
<footer>
```
## 关联工单
在提交信息中关联工单:
```
feat(menu): 添加收银系统菜单配置
- 新增 cashier.json 菜单配置文件
- 包含6个子菜单
对应工单 #2
```
## 提交频率
- **每次修改后必须提交**:AI 完成任何代码/文档修改后,必须立即执行 `git commit`
- **禁止积攒**:不允许将多次修改积攒到一起提交
- **提交时机**:每完成一个逻辑单元(如一个方法、一个文件、一个功能点)即提交
## 推送规则
| 场景 | 行为 |
|------|------|
| **常规开发** | 修改后自动 `git commit`**不自动推送** |
| **手动推送** | 开发者可随时执行 `git push` 推送 |
| **自动推送阈值** | 当未推送提交数 **超过 10 个** 时,自动推送到远程 |
```bash
# 手动推送命令
git push origin main
```
### 检查未推送提交数
```bash
# 查看未推送的提交数量
git log origin/main..HEAD --oneline | wc -l
```
> ⚠️ **注意**:业务模块使用独立 Git 仓库,禁止将代码提交到框架主仓库(`app/` 已加入 `.gitignore`
+158
View File
@@ -0,0 +1,158 @@
# Gitea API 使用指南
## Token 位置
```
~/.config/gitea/token
```
## 仓库与 API 地址
| 仓库 | Git 地址 | API Issue 端点 |
|------|---------|---------------|
| rui-framework | `ssh://git@git.vifo.cc:222/rui/rui-framework.git` | `https://git.vifo.cc/api/v1/repos/rui/rui-framework/issues` |
| rui-cashier | `ssh://git@git.vifo.cc:222/rui/rui-cashier.git` | `https://git.vifo.cc/api/v1/repos/rui/rui-cashier/issues` |
| rui-payment | `ssh://git@git.vifo.cc:222/rui/rui-payment.git` | `https://git.vifo.cc/api/v1/repos/rui/rui-payment/issues` |
| rui-frontend | `ssh://git@git.vifo.cc:222/rui/rui-frontend.git` | `https://git.vifo.cc/api/v1/repos/rui/rui-frontend/issues` |
| rui-docs | `ssh://git@git.vifo.cc:222/rui/rui-docs.git` | `https://git.vifo.cc/api/v1/repos/rui/rui-docs/issues` |
## 工单路由规则
AI 必须根据**问题所属模块**向正确的仓库提交 Issue,禁止向错误仓库提交。
### 按问题类型路由
| 问题类型 | 提交到 | 示例 |
|---------|-------|------|
| 框架能力缺失(公共工具、BaseEntity、安全、Feign 等) | rui-framework | 需要新增分布式锁工具类 |
| 系统管理、用户管理接口 | rui-framework | 用户编辑接口密码字段处理 |
| 收银业务逻辑 | rui-cashier | 收银台需要新增挂单功能 |
| 支付业务逻辑 | rui-payment | 支付回调需要新增渠道 |
| 前端页面、UI 组件 | rui-frontend | 收银页面需要新增弹窗 |
| 文档、规范、技能 | rui-docs | Nacos 配置规范需要补充 |
### 按 API URL 前缀路由
| URL 前缀 | 所属仓库 | 说明 |
|---------|---------|------|
| /system/* | rui-framework | 系统管理、基础框架 |
| /user/* | rui-framework | 用户管理、权限相关 |
| /cashier/* | rui-cashier | 收银系统 |
| /pay/*, /payment/* | rui-payment | 支付系统 |
| 其他 | 按业务模块判断 | 参考项目文档或询问用户 |
### 跨仓库协作原则
1. **当前仓库能解决的问题**:直接处理,不提 Issue
2. **需要其他仓库配合**:向目标仓库提 Issue,并在当前仓库提交中注明 对应工单 {owner}/{repo}#{number}
3. **不确定归属**:先向用户确认,不要盲目提交
## 创建 Issue(工单)
**步骤 1**: 准备 JSON 文件(避免转义问题)
```bash
cat > /tmp/issue.json << 'EOF'
{
"title": "[API-REQ] 简要描述所需接口",
"body": "## 接口地址\n\nPUT /xxx/xxx\n\n## 功能描述\n\n描述需要什么功能\n\n## 期望行为\n\n1. ...\n2. ...\n\n## 当前问题\n\n- ...\n\n## 前端使用场景\n\n描述为什么需要这个接口\n\n## 优先级\n\n高/中/低"
}
EOF
```
**步骤 2**: 调用 Gitea API 创建 Issue
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/issue.json \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues"
```
**示例**(提交到 rui-framework 仓库):
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/issue.json \
"https://git.vifo.cc/api/v1/repos/rui/rui-framework/issues"
```
**返回示例**
```json
{
"id": 7,
"number": 2,
"title": "[API-REQ] 用户编辑接口密码字段处理优化",
"html_url": "https://git.vifo.cc/rui/rui-framework/issues/2",
"state": "open"
}
```
## 获取 Issue
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -H "Authorization: token ${TOKEN}" \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}"
```
## 列出仓库所有 Issue
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -H "Authorization: token ${TOKEN}" \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues?state=open"
```
## 回复 Issue 评论
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"body": "✅ 已完成\n\n完成内容..."}' \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}/comments"
```
## 关闭 Issue
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X PATCH \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}"
```
## 常用端点
| 操作 | 方法 | 端点 |
|------|------|------|
| 创建 Issue | POST | /api/v1/repos/{owner}/{repo}/issues |
| 获取 Issue | GET | /api/v1/repos/{owner}/{repo}/issues/{id} |
| 列出 Issue | GET | /api/v1/repos/{owner}/{repo}/issues?state=open |
| 创建评论 | POST | /api/v1/repos/{owner}/{repo}/issues/{id}/comments |
| 关闭 Issue | PATCH | /api/v1/repos/{owner}/{repo}/issues/{id} |
| 获取仓库 | GET | /api/v1/repos/{owner}/{repo} |
## Git 推送命令
所有仓库统一使用 `origin` 作为远程名称:
```bash
git push origin main
```
## 注意事项
1. **JSON 内容**: 建议使用文件方式(-d @file.json)避免转义问题
2. **Labels**: 创建 Issue 时 labels 参数需要传入 ID 数组,不是字符串数组
3. **返回字段**: number 是 Issue 编号,id 是内部 ID
4. **owner 统一为 rui**: 所有仓库都在 rui 组织下
+83
View File
@@ -0,0 +1,83 @@
---
name: gitnexus-cli
description: "Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: \"Index this repo\", \"Reanalyze the codebase\", \"Generate a wiki\""
---
# GitNexus CLI Commands
All commands work via `npx` — no global install required.
## Commands
### analyze — Build or refresh the index
```bash
npx gitnexus analyze
```
Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
| Flag | Effect |
| -------------- | ---------------------------------------------------------------- |
| `--force` | Force full re-index even if up to date |
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook detects staleness after `git commit` and `git merge` and notifies the agent to run `analyze` — the hook does not run analyze itself, to avoid blocking the agent for up to 120s and risking KuzuDB corruption on timeout.
### status — Check index freshness
```bash
npx gitnexus status
```
Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.
### clean — Delete the index
```bash
npx gitnexus clean
```
Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.
| Flag | Effect |
| --------- | ------------------------------------------------- |
| `--force` | Skip confirmation prompt |
| `--all` | Clean all indexed repos, not just the current one |
### wiki — Generate documentation from the graph
```bash
npx gitnexus wiki
```
Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
| Flag | Effect |
| ------------------- | ----------------------------------------- |
| `--force` | Force full regeneration |
| `--model <model>` | LLM model (default: minimax/minimax-m2.5) |
| `--base-url <url>` | LLM API base URL |
| `--api-key <key>` | LLM API key |
| `--concurrency <n>` | Parallel LLM calls (default: 3) |
| `--gist` | Publish wiki as a public GitHub Gist |
### list — Show all indexed repos
```bash
npx gitnexus list
```
Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
## After Indexing
1. **Read `gitnexus://repo/{name}/context`** to verify the index loaded
2. Use the other GitNexus skills (`exploring`, `debugging`, `impact-analysis`, `refactoring`) for your task
## Troubleshooting
- **"Not inside a git repository"**: Run from a directory inside a git repo
- **Index is stale after re-analyzing**: Restart Claude Code to reload the MCP server
- **Embeddings slow**: Omit `--embeddings` (it's off by default) or set `OPENAI_API_KEY` for faster API-based embedding
@@ -0,0 +1,89 @@
---
name: gitnexus-debugging
description: "Use when the user is debugging a bug, tracing an error, or asking why something fails. Examples: \"Why is X failing?\", \"Where does this error come from?\", \"Trace this bug\""
---
# Debugging with GitNexus
## When to Use
- "Why is this function failing?"
- "Trace where this error comes from"
- "Who calls this method?"
- "This endpoint returns 500"
- Investigating bugs, errors, or unexpected behavior
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
## Debugging Patterns
| Symptom | GitNexus Approach |
| -------------------- | ---------------------------------------------------------- |
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
| Recent regression | `detect_changes` to see what your changes affect |
## Tools
**gitnexus_query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
```
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
→ Step 3: validatePayment → calls fetchRates (external)
4. Root cause: fetchRates calls external API without proper timeout
```
@@ -0,0 +1,78 @@
---
name: gitnexus-exploring
description: "Use when the user asks how code works, wants to understand architecture, trace execution flows, or explore unfamiliar parts of the codebase. Examples: \"How does X work?\", \"What calls this function?\", \"Show me the auth flow\""
---
# Exploring Codebases with GitNexus
## When to Use
- "How does authentication work?"
- "What's the project structure?"
- "Show me the main components"
- "Where is the database logic?"
- Understanding code you haven't seen before
## Workflow
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
## Resources
| Resource | What you get |
| --------------------------------------- | ------------------------------------------------------- |
| `gitnexus://repo/{name}/context` | Stats, staleness warning (~150 tokens) |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores (~300 tokens) |
| `gitnexus://repo/{name}/cluster/{name}` | Area members with file paths (~500 tokens) |
| `gitnexus://repo/{name}/process/{name}` | Step-by-step execution trace (~200 tokens) |
## Tools
**gitnexus_query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
```
## Example: "How does payment processing work?"
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
```
@@ -0,0 +1,64 @@
---
name: gitnexus-guide
description: "Use when the user asks about GitNexus itself — available tools, how to query the knowledge graph, MCP resources, graph schema, or workflow reference. Examples: \"What GitNexus tools are available?\", \"How do I use GitNexus?\""
---
# GitNexus Guide
Quick reference for all GitNexus MCP tools, resources, and the knowledge graph schema.
## Always Start Here
For any task involving code understanding, debugging, impact analysis, or refactoring:
1. **Read `gitnexus://repo/{name}/context`** — codebase overview + check index freshness
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
## Skills
| Task | Skill to read |
| -------------------------------------------- | ------------------- |
| Understand architecture / "How does X work?" | `gitnexus-exploring` |
| Blast radius / "What breaks if I change X?" | `gitnexus-impact-analysis` |
| Trace bugs / "Why is X failing?" | `gitnexus-debugging` |
| Rename / extract / split / refactor | `gitnexus-refactoring` |
| Tools, resources, schema reference | `gitnexus-guide` (this file) |
| Index, status, clean, wiki CLI commands | `gitnexus-cli` |
## Tools Reference
| Tool | What it gives you |
| ---------------- | ------------------------------------------------------------------------ |
| `query` | Process-grouped code intelligence — execution flows related to a concept |
| `context` | 360-degree symbol view — categorized refs, processes it participates in |
| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
| `detect_changes` | Git-diff impact — what do your current changes affect |
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
| `list_repos` | Discover indexed repos |
## Resources Reference
Lightweight reads (~100-500 tokens) for navigation:
| Resource | Content |
| ---------------------------------------------- | ----------------------------------------- |
| `gitnexus://repo/{name}/context` | Stats, staleness check |
| `gitnexus://repo/{name}/clusters` | All functional areas with cohesion scores |
| `gitnexus://repo/{name}/cluster/{clusterName}` | Area members |
| `gitnexus://repo/{name}/processes` | All execution flows |
| `gitnexus://repo/{name}/process/{processName}` | Step-by-step trace |
| `gitnexus://repo/{name}/schema` | Graph schema for Cypher |
## Graph Schema
**Nodes:** File, Function, Class, Interface, Method, Community, Process
**Edges (via CodeRelation.type):** CALLS, IMPORTS, EXTENDS, IMPLEMENTS, DEFINES, MEMBER_OF, STEP_IN_PROCESS
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
RETURN caller.name, caller.filePath
```
@@ -0,0 +1,97 @@
---
name: gitnexus-impact-analysis
description: "Use when the user wants to know what will break if they change something, or needs safety analysis before editing code. Examples: \"Is it safe to change X?\", \"What depends on this?\", \"What will break?\""
---
# Impact Analysis with GitNexus
## When to Use
- "Is it safe to change this function?"
- "What will break if I modify X?"
- "Show me the blast radius"
- "Who uses this code?"
- Before making non-trivial code changes
- Before committing — to understand what your changes affect
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
## Understanding Output
| Depth | Risk Level | Meaning |
| ----- | ---------------- | ------------------------ |
| d=1 | **WILL BREAK** | Direct callers/importers |
| d=2 | LIKELY AFFECTED | Indirect dependencies |
| d=3 | MAY NEED TESTING | Transitive effects |
## Risk Assessment
| Affected | Risk |
| ------------------------------ | -------- |
| <5 symbols, few processes | LOW |
| 5-15 symbols, 2-5 processes | MEDIUM |
| >15 symbols or many processes | HIGH |
| Critical path (auth, payments) | CRITICAL |
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
maxDepth: 3
})
→ d=1 (WILL BREAK):
- loginHandler (src/auth/login.ts:42) [CALLS, 100%]
- apiMiddleware (src/api/middleware.ts:15) [CALLS, 100%]
→ d=2 (LIKELY AFFECTED):
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
→ Risk: MEDIUM
```
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
2. READ gitnexus://repo/my-app/processes
→ LoginFlow and TokenRefresh touch validateUser
3. Risk: 2 direct callers, 2 processes = MEDIUM
```
@@ -0,0 +1,121 @@
---
name: gitnexus-refactoring
description: "Use when the user wants to rename, extract, split, move, or restructure code safely. Examples: \"Rename this function\", \"Extract this into a module\", \"Refactor this class\", \"Move this to a separate file\""
---
# Refactoring with GitNexus
## When to Use
- "Rename this function safely"
- "Extract this into a module"
- "Split this service"
- "Move this to a new file"
- Any task involving renaming, extracting, splitting, or restructuring code
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
```
## Risk Rules
| Risk Factor | Mitigation |
| ------------------- | ----------------------------------------- |
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```
+109
View File
@@ -0,0 +1,109 @@
# 工单处理流程
## 标准流程
### 1. 读取工单
使用 Gitea API 获取 Issue 内容:
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -H "Authorization: token ${TOKEN}" \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}"
```
也可以列出当前仓库所有未关闭的工单:
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -H "Authorization: token ${TOKEN}" \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues?state=open"
```
### 2. 分析需求
- 阅读工单标题和描述
- 确认需求范围是否属于当前仓库
- 检查相关配置文件和代码
- 必要时向用户澄清
### 3. 判断工单归属
如果工单内容不属于当前仓库,需要路由到正确的仓库:
| 问题类型 | 正确仓库 |
|---------|---------|
| 框架能力、公共工具、安全、Feign | rui-framework |
| 系统管理、用户管理接口 | rui-framework |
| 收银业务逻辑 | rui-cashier |
| 支付业务逻辑 | rui-payment |
| 前端页面、UI 组件 | rui-frontend |
| 文档、规范、技能 | rui-docs |
**路由方式**:在当前仓库回复工单说明需转交,然后向目标仓库创建新 Issue。
### 4. 实施修改
按 Superpowers 工作流处理:
- 简单任务:直接实施
- 复杂任务:设计 -> 计划 -> 实施
### 5. 回复工单
完成后必须回复工单:
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"body": "✅ 已完成\n\n完成内容..."}' \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}/comments"
```
### 6. 关闭工单
```bash
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X PATCH \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' \
"https://git.vifo.cc/api/v1/repos/{owner}/{repo}/issues/{id}"
```
## 跨仓库提工单
当当前仓库开发中需要其他仓库配合时:
1. 确认问题属于目标仓库
2. 通过 Gitea API 向目标仓库创建 Issue
3. 在当前仓库的 git commit 中注明关联工单
```bash
# 示例:payment 需要框架新增能力,向 rui-framework 提交
cat > /tmp/issue.json << 'EOF'
{
"title": "[API-REQ] 需要新增支付回调重试工具",
"body": "## 来源\n\nrui-payment 模块请求\n\n## 功能描述\n\n...\n\n## 优先级\n\n高"
}
EOF
TOKEN=$(cat ~/.config/gitea/token)
curl -s -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/issue.json \
"https://git.vifo.cc/api/v1/repos/rui/rui-framework/issues"
```
## 提交规范
- 提交信息需关联工单编号:对应工单 #2
- 使用语义化提交前缀:feat:, fix:, docs:, chore:
## 优先级处理
| 优先级 | 标识 | 处理时效 |
|--------|------|----------|
| P0 | 紧急 | 立即处理 |
| P1 | 高 | 当天完成 |
| P2 | 中 | 2-3天内 |
| P3 | 低 | 排期处理 |
+67
View File
@@ -0,0 +1,67 @@
# 菜单配置规范
## 配置目录
```
~/rui/rui-framework/data/menus/
```
## 文件命名
- `{模块编码}.json`
- 示例:`cashier.json`, `system.json`, `user.json`
## 文件格式
```json
{
"name": "模块编码(与文件名一致)",
"menus": [
{
"code": "菜单编码",
"name": "菜单名称",
"type": 1,
"icon": "tabler:图标名",
"sortNo": 0,
"children": [
{
"code": "子菜单编码",
"name": "子菜单名称",
"type": 2,
"icon": "tabler:图标名",
"path": "/路由路径",
"permission": "权限标识",
"sortNo": 1,
"buttons": [
{
"code": "btn:add",
"name": "新增",
"permission": "模块:功能:add"
}
]
}
]
}
]
}
```
## 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `code` | string | ✅ | 菜单编码,唯一标识 |
| `name` | string | ✅ | 菜单显示名称 |
| `type` | int | ✅ | 1=目录, 2=菜单, 3=按钮 |
| `icon` | string | ✅ | 图标(tabler: 前缀) |
| `path` | string | 菜单必填 | 路由路径 |
| `permission` | string | ✅ | 权限标识 |
| `sortNo` | int | ✅ | 排序号,越小越靠前 |
| `children` | array | 可选 | 子菜单列表 |
| `buttons` | array | 可选 | 按钮权限列表 |
## 初始化方式
- 由**超级租户**读取 JSON 配置并初始化到数据库
- 不需要编写 SQL 脚本或 Java 初始化代码
- 修改 JSON 后提交到 rui-framework 仓库即可
+267
View File
@@ -0,0 +1,267 @@
# RUI 项目 Nacos 配置与 application.yml 规范
> **适用范围**: rui-framework 后端框架所有模块
> **更新日期**: 2026-06-05
> **强制级别**: 必须遵守,AI 编辑相关文件时自动强制执行
---
## 核心原则
### 1. 配置分层
| 配置位置 | 用途 | 内容 |
|---------|------|------|
| **Nacos** (`docs/backend/config-templates/nacos/*.yaml`) | 动态配置 | 端口、业务配置 |
| **本地** (`src/main/resources/application.yml`) | 兜底配置 | 基础框架配置 + Nacos 连接信息 |
| **公共** (`rui-common.yaml`, `rui-data.yaml`) | 共享配置 | Feign、Redis、数据库等 |
### 2. 禁止重复
**Nacos 服务专属配置只放:端口 + 该服务特有的业务配置**
-**禁止**:在 `rui-service-*.yaml` 中放 nacos/feign/redis/数据库配置
-**正确**:只放 `server.port` 和业务配置(如 `rui.modules`
### 3. 单向同步
**Nacos 配置为准,`src/main/resources/application.yml` 必须和 Nacos 配置完全一致。**
修改流程:
1. 先修改 `docs/backend/config-templates/nacos/*.yaml`Nacos 源文件)
2. 推送到 Nacos
3. 复制内容到 `src/main/resources/application.yml`(本地兜底)
> ⚠️ **禁止**:两个文件内容不一致
### 4. 推送必验证
推送 Nacos 后必须验证:
```bash
curl "http://192.168.31.210:8848/nacos/v1/cs/configs?dataId=xxx.yaml&group=DEFAULT_GROUP&tenant=rui-dev&username=nacos&password=nacos"
```
---
## 规则详解
### Rule 1application.yml 严格模板化
**要求**
- 新建模块或 `application.yml` 丢失时,**完整复制** `docs/backend/config-templates/application-template.yml`
- **禁止合并**其他内容
- **禁止新增**任何模板外的内容
- 只允许修改模板中**明确标注允许修改**的位置
**当前模板中允许修改的位置**
1. `server.port` — 按模块端口规划填写
2. 无 MyBatis 的模块删除 `rui-data``config.import`
> 未来如果 `application-template.yml` 更新并新增可修改项,以模板中的注释标注为准。
### Rule 2:业务配置全进 Nacos
**要求**
- 各模块特有业务配置必须写在 `docs/backend/config-templates/nacos/{模块名称}.yaml`
- 例:`rui-service-system``rui-service-system.yaml`
- 例:`rui-gateway``rui-gateway.yaml`
- 例:`rui-auth``rui-auth.yaml`
- 修改完成后必须推送到 Nacos
- Nacos 服务配置**不得包含** `application.yml` 中的通用内容,包括但不限于:
- Nacos 连接配置(`spring.cloud.nacos.discovery/config`
- Spring 生命周期(`spring.lifecycle`
- 自动配置排除(`spring.autoconfigure.exclude`
- 文件上传限制(`spring.servlet.multipart`
- 编码配置(`spring.servlet.encoding`
- OpenFeign 断路器(`spring.cloud.openfeign.circuitbreaker`
- `management.endpoints`
- `logging.file.path`
**例外**:如果某模块确实需要覆盖 application.yml 中的某个默认值,可以在 Nacos 配置中显式覆盖,但必须在注释中说明理由。
### Rule 3:公共配置职责分离
**要求**
| 配置文件 | 职责范围 | 典型内容 |
|---------|---------|---------|
| `rui-common.yaml` | 所有模块都可能用到的通用配置 | Redis、RabbitMQ、Jackson、JWT、安全白名单、Feign providers |
| `rui-data.yaml` | 仅数据库相关模块导入 | 数据源、MyBatis Plus、Seata |
**原因**:没有使用数据库的模块(如 Gateway)也可能需要 Redis,因此 Redis 不能放在 `rui-data.yaml`
### Rule 4:推送必验证
**要求**
修改 Nacos 配置并推送后,必须执行验证命令:
```bash
curl "http://192.168.31.210:8848/nacos/v1/cs/configs?dataId={dataId}.yaml&group=DEFAULT_GROUP&tenant=rui-dev&username=nacos&password=nacos"
```
### Rule 5:本地开发配置不干预
**要求**
- `config/application-dev.yml` 属于本地开发配置
- 可由开发人员填写完整配置或仅本地特性配置
- AI 正常情况下**不修改**该文件
---
## 正反对照示例
### 示例 1application.yml
**正确**(仅修改允许位置):
```yaml
server:
port: 9302 # ✅ 【允许修改】按模块规划填写
shutdown: graceful
# ... 其余与模板完全一致 ...
```
**错误**
```yaml
server:
port: 9302
# ❌ 错误:在 application.yml 中添加业务配置
rui:
modules:
available:
- code: demo
name: 演示中心
```
### 示例 2Nacos 服务配置
**正确**(仅端口 + 业务配置):
```yaml
# rui-service-pay.yaml
server:
port: 9307
payment:
order-timeout: 30
notify-max-times: 5
```
**错误**
```yaml
# rui-service-xxx.yaml
server:
port: 9307
# ❌ 错误:Nacos 配置中重复 application.yml 内容
spring:
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
lifecycle:
timeout-per-shutdown-phase: 30s
```
### 示例 3:公共配置职责
**正确**
```yaml
# rui-common.yaml
spring:
data:
redis:
host: ${REDIS_HOST:192.168.31.210}
```
```yaml
# rui-data.yaml
spring:
datasource:
url: jdbc:mysql://...
```
**错误**
```yaml
# rui-data.yaml
spring:
data:
redis:
host: ...
# ❌ 错误:Redis 配置不应放在 rui-data.yaml
```
---
## 修改流程
### 修改 Nacos 配置的完整流程
1. **编辑 Nacos 源文件**
```bash
vim docs/backend/config-templates/nacos/rui-service-xxx.yaml
```
2. **推送到 Nacos**
- 通过 Nacos 控制台或 API 推送
3. **验证推送结果**
```bash
curl "http://192.168.31.210:8848/nacos/v1/cs/configs?dataId=rui-service-xxx.yaml&group=DEFAULT_GROUP&tenant=rui-dev&username=nacos&password=nacos"
```
4. **同步到本地 application.yml(如需要)**
- 仅当本地 application.yml 需要兜底内容时更新
- 内容必须与 Nacos 保持一致
### 新建模块的配置初始化流程
1. **复制 application.yml 模板**
```bash
cp docs/backend/config-templates/application-template.yml \
rui-service/rui-service-xxx/src/main/resources/application.yml
```
2. **修改允许修改的位置**
- `server.port` — 按端口规划填写
- 无 MyBatis 的模块删除 `rui-data` 导入
3. **创建 Nacos 配置文件**
```bash
vim docs/backend/config-templates/nacos/rui-service-xxx.yaml
```
- 只写 `server.port` + 业务配置
4. **推送 Nacos 并验证**
---
## 检查清单
### 编辑前检查
- [ ] 本次修改是否涉及 `application.yml` 或 Nacos 配置?
- [ ] 是否在允许修改的范围内?
- [ ] 是否将业务配置放在了正确的位置?
### 编辑后检查
- [ ] `application.yml` 是否仍与模板一致?
- [ ] Nacos 配置是否未包含 application.yml 中已有的通用配置?
- [ ] 修改是否已同步到 Nacos 源文件和本地 application.yml(如适用)?
- [ ] 是否已按 Rule 4 推送后验证?
---
> **最后提醒**:配置规范是为了保证 Nacos/本地配置的一致性和可维护性,请务必遵守!
+145
View File
@@ -0,0 +1,145 @@
# AI 助手使用指南(开发者手册)
> 本文档面向**开发者**,帮助你高效指挥 AI 助手完成开发任务。
> 不是给 AI 读的——AI 的规则在仓库根目录的 `AGENTS.md` 中。
---
## 一、开工前准备
### 1. 确认 AI 已读取规则
让 AI 读一遍 `AGENTS.md`,确认它了解自己的身份和边界:
> "读一下 AGENTS.md,确认你了解当前项目的规则"
### 2. 更新文档子模块
```bash
git submodule update --remote
```
全局技能、规范、GitNexus 指南都在 `docs/` 子模块里,保持最新。
### 3. 更新 GitNexus 索引
```bash
npx gitnexus analyze
```
索引过期会导致 AI 无法准确分析代码影响,建议每次开工前跑一次。
---
## 二、常用指令
以下是你可以直接发给 AI 的指令模板。
### 代码探索
| 你想做什么 | 发给 AI |
|-----------|---------|
| 理解某个功能怎么运作 | "支付订单创建流程是怎么走的?" |
| 查看某个类的所有调用者 | "PayOrderServiceImpl 被哪些地方调用了?" |
| 了解项目整体架构 | "帮我梳理下支付模块的结构" |
### 代码修改
| 你想做什么 | 发给 AI |
|-----------|---------|
| 新增功能 | "在支付渠道中新增一个 XX 渠道" |
| 修复 Bug | "支付回调偶尔会重复处理,帮我排查" |
| 重构代码 | "PayOrderService 太大了,帮我拆分" |
| 重命名 | "把 PayOrderServiceImpl 的 createOrder 方法改名为 createPayOrder" |
### 工单处理
| 你想做什么 | 发给 AI |
|-----------|---------|
| 读取工单 | "看下 #3 号工单" |
| 处理工单 | "处理一下 #3 号工单的需求" |
| 跨仓库提需求 | "支付需要框架新增一个 XX 工具,帮我提个 Issue 给 rui-framework" |
| 查看未关闭工单 | "看下当前仓库有哪些未关闭的 Issue" |
---
## 三、GitNexus 速查
AI 通过 GitNexus 知识图谱理解代码。以下是你可能用到的操作:
### 常用场景
| 场景 | 你可以说 |
|------|---------|
| 修改前评估影响 | "改 PayOrderServiceImpl 会不会影响其他地方?" |
| 查看变更范围 | "帮我检查下改了哪些东西" |
| 理解执行流 | "退款流程是怎么走的,从头到尾画一下" |
| 查找代码 | "项目里哪里处理了支付回调?" |
### 索引维护
```bash
# 重新分析代码库(建议每天或大改动后执行)
npx gitnexus analyze
# 查看索引状态
npx gitnexus status
```
---
## 四、精简 AGENTS.md
AGENTS.md 是 AI 的规则文件,应该保持精简。以下原则供你维护时参考:
### 分类原则
| 类型 | 放哪里 | 举例 |
|------|-------|------|
| AI 必须遵守的硬规则 | `AGENTS.md` | 禁止改框架、修改前必须 impact 分析 |
| AI 需要的技能/知识 | `docs/ai-skills/` | Gitea API 用法、Nacos 配置规范 |
| 通用编码规范 | `docs/standards/` | 命名规范、数据库规范 |
### 检查清单
- [ ] 同一件事有没有在多处重复?→ 只保留一处
- [ ] 有没有把代码示例写进规则?→ 代码示例是技能,不是规则
- [ ] 编码规范是否指向了全局文档?→ 不在本仓库重复
- [ ] 仓库名、路径是否正确?→ spring-ai 已改为 rui-framework
- [ ] GitNexus 段落是否精简?→ 只保留规则和资源表
### 典型精简操作
**删除技能混入规则:**
- Feign/REST 代码示例 → AI 已知,不需要教
- 工单路由表 → 已在 `docs/ai-skills/gitea-api.md`
**合并重复项:**
- "仓库职责" + "允许范围" → 合并为"项目结构"+"只允许修改"
- GitNexus Always/Never 6 条 → 合并为 4 条规则
---
## 五、目录结构速查
```
docs/ # 全局文档(rui-docs submodule
├── ai-skills/ # AI 技能库
│ ├── quickstart.md # 👈 你在这里
│ ├── gitea-api.md # Gitea API + 工单路由规则
│ ├── issue-workflow.md # 工单处理流程
│ ├── commit-standards.md # 提交规范
│ ├── nacos-config-rules.md # Nacos 配置规范
│ ├── menu-config.md # 菜单配置规范
│ └── gitnexus/ # GitNexus 技能(6个)
├── standards/ # 通用规范
│ ├── coding-standards.md # 编码规范
│ ├── API设计规范.md
│ └── 数据库设计规范分析.md
├── backend/ # 后端文档
│ ├── guides/ # 操作指南
│ ├── design/ # 设计文档
│ ├── specs/ # 规格说明
│ └── templates/ # 文档模板
└── frontend/ # 前端文档
```
+68
View File
@@ -0,0 +1,68 @@
# [API-REQ] 用户编辑接口密码字段处理优化
## 接口地址
PUT /user/admin/user
## 功能描述
当前编辑用户时,如果前端不传 `password` 字段或传空字符串,后端会报错或把密码更新为空。期望后端能处理以下逻辑:
## 请求参数
```json
{
"id": 1,
"username": "admin",
"userType": 1,
"status": 1
// 注意:没有 password 字段
}
```
## 期望行为
1. **编辑用户时**,如果请求体中**不包含** `password` 字段,或 `password`**null/空字符串**,应**不修改**用户密码
2. **编辑用户时**,如果请求体中**包含** `password` 字段且**不为空**,应**更新**用户密码
3. **新增用户时**`password` 字段**必填**,保持现有逻辑
## 当前问题
- 编辑用户时如果不传密码,后端可能报错或把密码置空
- 前端需要在编辑时特殊处理密码字段(已临时处理:编辑时密码为空则不提交该字段)
## 前端使用场景
编辑用户弹框中,密码输入框提示"留空表示不修改密码"。用户留空时,前端不提交 password 字段,期望后端保持原密码不变。
## 优先级
高 - 影响用户编辑功能正常使用
## 相关代码
前端文件:`admin-ui/src/views/user/info/UserFormDialog.vue`
```typescript
// 编辑时如果密码为空,删除该字段,避免后端修改密码
if (isEdit && !data.password) {
delete data.password
}
```
## 建议实现
`UserService.update()` 或 Controller 层添加判断:
```java
if (userDTO.getPassword() != null && !userDTO.getPassword().isEmpty()) {
// 更新密码
user.setPassword(encrypt(userDTO.getPassword()));
}
// 否则不修改密码字段
```
---
> 提交者:前端开发(admin-ui
> 日期:2026-06-06
@@ -0,0 +1,65 @@
# ============================================================================
# 睿核科技 — 通用平台框架 通用 application.yml 模板
# ============================================================================
# 使用方法:
# 复制到新模块的 src/main/resources/application.yml
# 修改 server.port 为模块对应端口
# 本地开发配置见项目根目录 config/application-dev.yml(不提交 git
# ============================================================================
server:
port: XXXX # 【允许修改】模块端口(按规划分配)
shutdown: graceful # 优雅关闭
spring:
application:
name: @artifactId@ # Maven 过滤,自动替换为模块名
profiles:
active: @profiles.active@ # Maven 过滤,默认 dev
lifecycle:
timeout-per-shutdown-phase: 30s # 优雅关闭等待时间
autoconfigure:
exclude:
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
encoding:
charset: UTF-8
enabled: true
force: true
cloud:
openfeign:
circuitbreaker:
enabled: true
nacos:
discovery: # 服务发现(独立环境变量,不依赖 config)
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
config: # 配置中心
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
file-extension: yaml
import-check:
enabled: true
config:
import:
- optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension:yaml}
- optional:nacos:rui-common.${spring.cloud.nacos.config.file-extension:yaml}
# 【允许修改】无 MyBatis 的模块(gateway)请删除下面这行
- optional:nacos:rui-data.${spring.cloud.nacos.config.file-extension:yaml}
management:
endpoints:
web:
exposure:
include: health
discovery:
enabled: false
@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
通用日志配置,复制到任意模块无需修改
- 自动读取 spring.application.name 作为日志目录名
- 按级别精确分文件:debug / info / warn / error
- 控制台彩色输出,文件不含颜色码
- dev 环境输出 DEBUG,其他环境输出 INFO
-->
<configuration scan="true" scanPeriod="10 seconds">
<!-- 彩色日志转换器 -->
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex" class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx" class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 读取 Spring 应用名称,复制到任意模块无需修改 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>
<property name="LOG_PATH" value="logs/${APP_NAME}"/>
<!-- 文件日志格式:含时间、线程、级别、类名、代码行号、消息 -->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%line - %msg%n"/>
<!-- 控制台彩色日志格式(%clr 根据级别自动着色:ERROR 红、WARN 黄、INFO 绿、DEBUG 蓝、TRACE 青) -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%thread]){faint} %clr(%-5level) %clr(%logger{50}:%line){cyan} %clr(-){faint} %msg%n"/>
<!-- ==================== 控制台输出 ==================== -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ==================== DEBUG 日志文件 ==================== -->
<!-- 精确匹配 DEBUG 级别,dev 环境下生效 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/debug.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/history/debug/log-debug.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>200MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ==================== INFO 日志文件 ==================== -->
<!-- 精确匹配 INFO 级别 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/info.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/history/info/log-info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>200MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ==================== WARN 日志文件 ==================== -->
<!-- 精确匹配 WARN 级别 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/warn.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/history/warn/log-warn.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>200MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ==================== ERROR 日志文件 ==================== -->
<!-- 精确匹配 ERROR 级别 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/history/error/log-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>200MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ==================== 特定包日志级别调整 ==================== -->
<logger name="org.apache.catalina.connector.CoyoteAdapter" level="OFF"/>
<logger name="org.apache.seata.config.FileConfiguration" level="OFF"/>
<!-- ==================== 生产环境配置 ==================== -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- ==================== 开发环境配置 ==================== -->
<springProfile name="dev">
<logger name="com.baomidou.mybatisplus" level="DEBUG"/>
<logger name="com.rui" level="DEBUG"/>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- ==================== 其他环境默认配置 ==================== -->
<springProfile name="!prod,!dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
</configuration>
@@ -0,0 +1,27 @@
# rui-auth.yaml — 认证中心模块配置
# Data ID: rui-auth.yaml
# Group: DEFAULT_GROUP
# 服务端口:9301(认证中心,所有服务依赖)
server:
port: 9301
# 第三方应用通用配置
# 字段定义见 com.rui.common.core.properties.AppProperties
# 每个第三方应用一份独立配置(prefix = thirdparty.<平台名>
# 留空表示对应登录方式未启用(Provider 仍会注册,但调用时会失败)
thirdparty:
wechat:
# 微信开放平台 AppID
app-id: ${WECHAT_APP_ID:}
# 微信开放平台 AppSecret
app-secret: ${WECHAT_APP_SECRET:}
alipay:
# 支付宝开放平台 AppID
app-id: ${ALIPAY_APP_ID:}
# 应用 Key(部分平台如支付宝使用)
app-key: ${ALIPAY_APP_KEY:}
# 应用私钥(用于请求签名,RSA2
private-key: ${ALIPAY_PRIVATE_KEY:}
# 支付宝公钥(用于响应验签)
public-key: ${ALIPAY_PUBLIC_KEY:}
@@ -0,0 +1,43 @@
# rui-common.yaml — 所有模块共享
# Data ID: rui-common.yaml
# Group: DEFAULT_GROUP
# 用途: 全局通用配置(Redis、Jackson、JWT、安全白名单)
spring:
rabbitmq:
host: ${RABBITMQ_HOST:192.168.31.210}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:vifo}
password: ${RABBITMQ_PASSWORD:!QW3e4r2023}
data:
redis:
host: ${REDIS_HOST:192.168.31.210}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:123456}
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: non_null
security:
oauth2:
ignore-urls:
- /entry/**
- /notify/**
- /actuator/**
# Feign 服务提供者配置(默认走聚合启动器,独立模式请在各服务配置中覆盖)
feign:
providers:
user: rui-service-starter # 用户服务:默认指向聚合启动器
system: rui-service-starter # 系统服务:默认指向聚合启动器
auth: rui-auth # 认证中心:保持独立
@@ -0,0 +1,75 @@
# rui-data.yaml — 数据库模块共享
# Data ID: rui-data.yaml
# Group: DEFAULT_GROUP
# 用途: 数据源 + MyBatis Plus 配置(仅 DB 模块导入)
spring:
datasource:
url: jdbc:mysql://${DB_HOST:192.168.31.210}:3306/rui_platform?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:123456}
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
# MyBatis Plus 全局配置
mybatis-plus:
global-config:
db-config:
table-prefix: rui_
id-type: assign_id
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
tenant:
mode: IGNORE
tenant-ignore:
- sys_tenant
- user_credential
- system_oauth2_client
# 系统租户也需限制租户隔离的表(即使是系统租户,也只能查自己租户的数据)
system-tenant-table:
- uc_user
- uc_user_detail
- uc_user_role
- uc_user_dept
- uc_user_post
- uc_user_permission
- uc_user_level
- uc_user_level_log
- sys_menu
- sys_role
- sys_role_menu
- sys_role_dept
- sys_dept
- sys_post
- sys_dict_type
- sys_dict_item
- sys_config
- sys_oper_log
- sys_login_log
seata:
enabled: true
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
data-source-proxy-mode: AT
registry:
type: nacos
nacos:
application: seata-server
server-addr: ${spring.cloud.nacos.config.server-addr}
group : SEATA_GROUP
namespace: cloud
username: ${spring.cloud.nacos.config.username}
password: ${spring.cloud.nacos.config.password}
@@ -0,0 +1,80 @@
# rui-gateway.yaml — 网关路由配置 (Gateway 5.x 格式:server.webflux.routes)
# 说明:默认使用聚合模式(所有业务路由指向 rui-service-starter
# 如需独立微服务模式,取消注释独立路由并注释掉聚合路由
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: rui-auth
uri: lb://rui-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=0
- id: rui-auth
uri: lb://rui-auth
predicates:
- Path=/oauth2/**
filters:
- StripPrefix=0
- id: rui-service-pay
uri: lb://rui-service-pay
predicates:
- Path=/payment/**
filters:
- StripPrefix=0
# ========== 聚合模式(默认,中小型项目)==========
- id: rui-service-starter
uri: lb://rui-service-starter
predicates:
- Path=/user/**,/system/**
filters:
- StripPrefix=0
- id: rui-cashier
uri: lb://rui-cashier-api
predicates:
- Path=/cashier/**
filters:
- StripPrefix=0
# ========== 独立微服务模式(大型项目)==========
# - id: rui-service-user
# uri: lb://rui-service-user
# predicates:
# - Path=/user/**
# filters:
# - StripPrefix=0
# - id: rui-service-system
# uri: lb://rui-service-system
# predicates:
# - Path=/system/**
# filters:
# - StripPrefix=0
# 灰度发布配置
# 支持按服务配置不同的灰度策略,包括:权重、用户白名单、IP 白名单、强制 Header
grayscale:
# 强制灰度 Header 名称,客户端通过此 Header 强制指定版本
force-header: X-Grayscale-Version
# 灰度规则(Key 为服务名,如 rui-service-user
# 配置示例:
# rules:
# rui-service-user:
# enabled: true
# version: v2
# weight: 10
# user-ids:
# - user001
# ip-ranges:
# - 192.168.1.0/24
management:
endpoints:
web:
exposure:
include: health # 仅暴露 health 端点,其余全部禁止访问
# 服务端口:9300(网关最优先)
server:
port: 9300
@@ -0,0 +1,33 @@
# ============================================================================
# rui-service-starter.yaml — 聚合启动器配置
# ============================================================================
# 服务端口:9399(聚合启动器)
server:
port: 9399
# 模块管理配置(供租户管理模块配置弹窗使用)
rui:
modules:
# 全局可用模块列表(系统层面定义有哪些模块可选)
available:
- code: system
name: 系统管理
icon: tabler:settings
- code: user
name: 用户管理
icon: tabler:users
- code: order
name: 订单管理
icon: tabler:shopping-cart
- code: cms
name: 内容管理
icon: tabler:edit
- code: marketing
name: 营销中心
icon: tabler:present
- code: demo
name: 演示中心
icon: tabler:device-desktop
# 默认启用模块(新租户默认开启,逗号分隔)
default-enabled: system,user,demo
@@ -0,0 +1,67 @@
# rui-service-storage.yaml — 统一文件存储服务配置
# Data ID: rui-service-storage.yaml
# Group: DEFAULT_GROUP
# 推送到 Nacos 后必须按 docs/ai-skills/nacos-config-rules.md 验证
# 服务端口:9400(独立部署)/ 9399(被 rui-service-starter 聚合时使用 starter 端口)
server:
port: 9400
# ============================================================================
# 统一文件存储配置(rui.file.*)
# 业务配置仅放本服务,公共/框架配置已由 application-template.yml 覆盖
# ============================================================================
rui:
file:
# 当前激活的存储后端:aliyun / tencent / local
active: local
# 默认文件大小上限(按 bizType 未配置时使用)
default-max-size: 10MB
# 各业务类型的白名单与大小限制,key = FileBizType 枚举值
biz-types:
# 通用文件(无业务白名单限制)
COMMON:
max-size: 10MB
allowed-extensions: [] # 空 = 不限
# 第三方应用证书(pem/crt/key/p12
SYS_APP_CERT:
max-size: 5MB
allowed-extensions: [pem, crt, key, p12]
# 用户头像
USER_AVATAR:
max-size: 2MB
allowed-extensions: [jpg, jpeg, png, webp]
# CMS 轮播图
CMS_BANNER:
max-size: 5MB
allowed-extensions: [jpg, jpeg, png, webp, gif]
# 阿里云 OSS 后端
aliyun:
enabled: false
endpoint: oss-cn-shanghai.aliyuncs.com
access-key: ${ALIYUN_AK:}
secret-key: ${ALIYUN_SK:}
bucket: rui-storage
url-prefix: https://rui-storage.oss-cn-shanghai.aliyuncs.com
base-path: cert/
# 腾讯云 COS 后端
tencent:
enabled: false
secret-id: ${TENCENT_SID:}
secret-key: ${TENCENT_SKEY:}
region: ap-shanghai
bucket: rui-storage-1300000000
url-prefix: https://rui-storage-1300000000.cos.ap-shanghai.myqcloud.com
base-path: cert/
# 本地存储后端(默认/兜底)
local:
base-path: ${user.home}/.rui/upload/
url-prefix: /api/storage/local/
@@ -0,0 +1,33 @@
# rui-service-system.yaml — 系统服务配置
# Data ID: rui-service-system.yaml
# Group: DEFAULT_GROUP
# 服务端口:9302(系统基础服务)
server:
port: 9302
# 模块管理配置
rui:
modules:
# 全局可用模块列表(系统层面定义有哪些模块可选)
available:
- code: system
name: 系统管理
icon: tabler:settings
- code: user
name: 用户管理
icon: tabler:users
- code: order
name: 订单管理
icon: tabler:shopping-cart
- code: cms
name: 内容管理
icon: tabler:edit
- code: marketing
name: 营销中心
icon: tabler:present
- code: demo
name: 演示中心
icon: tabler:device-desktop
# 默认启用模块(新租户默认开启,逗号分隔)
default-enabled: system,user,demo
@@ -0,0 +1,7 @@
# rui-service-user.yaml — 用户基础信息及等级服务配置
# Data ID: rui-service-user.yaml
# Group: DEFAULT_GROUP
# 服务端口:9303(用户中心服务)
server:
port: 9303
+252
View File
@@ -0,0 +1,252 @@
# 支付模块接口设计
> **来源**: `~/rui/支付模块架构设计.md` v1.0
> **创建日期**: 2026-06-08
> **模块**: rui-payment
---
## 一、接口分层
| 层级 | 路径前缀 | 认证 | 说明 |
|------|---------|------|------|
| 对外接口 | `/payment/open/**` | 需认证 | 商户/业务系统调用 |
| 对外入口 | `/payment/entry/**` | 免认证 | 收银台、扫码支付 |
| 第三方回调 | `/payment/notify/**` | 免认证 | 支付/退款异步通知 |
| 内部接口 | `/payment/inner/**` | @Inner | 微服务间调用 |
---
## 二、对外接口(需认证)
### 2.1 支付交易 `/payment/open/trade`
#### 统一下单 `POST /payment/open/trade/pay`
```java
// 请求
{
"channel": "alipay", // PayChannel 枚举
"payType": "app", // PayType 枚举
"merchantOrderNo": "BIZ2026060801", // 业务订单号
"subject": "商品标题",
"body": "商品描述",
"amount": 100.00, // BigDecimal,元
"currency": "CNY",
"notifyUrl": "https://xxx/notify",
"returnUrl": "https://xxx/return",
"clientIp": "127.0.0.1",
"userId": "oXXXXXXXX" // 微信 JSAPI 需要 openid
}
// 响应
{
"code": 200,
"data": {
"success": true,
"channelOrderNo": null,
"payParams": { // 支付宝 APP
"orderInfo": "alipay_sdk=..."
},
// 或 "payUrl": "<form>...</form>", // 支付宝 H5/PC
// 或 "payQrCode": "weixin://...", // 微信 Native
// 或 "payParams": {"prepayId": "..."} // 微信 JSAPI
}
}
```
#### 统一执行入口 `POST /payment/open/trade/execute`
支持所有渠道和动作的通用入口。
```java
// 请求
{
"channel": "alipay",
"action": "query",
"merchantOrderNo": "PAY20260608001"
}
// 响应 — 查询示例
{
"code": 200,
"data": {
"success": true,
"channelOrderNo": "20260608...",
"payParams": {
"tradeStatus": "TRADE_SUCCESS",
"totalAmount": "100.00"
}
}
}
```
> 详细的 PayRequest/PayResponse 字段说明和每个 Action 的请求/返回示例见 `rui-payment-provider/API.md`。
### 2.2 退款 `/payment/open/refund`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/open/refund/apply` | POST | 申请退款 |
| `/payment/open/refund/{refundNo}` | GET | 查询退款单 |
#### 申请退款 `POST /payment/open/refund/apply`
```java
// 请求 PayRefundApplyRequest
{
"orderNo": "PAY20260608001",
"refundAmount": 50.00,
"refundReason": "用户申请退款",
"notifyUrl": "https://xxx/refund_notify"
}
// 响应 PayRefundVO
{
"code": 200,
"data": {
"refundNo": "REF20260608001",
"orderNo": "PAY20260608001",
"refundAmount": 50.00,
"refundStatus": 0, // 0:退款中 1:退款成功 2:退款失败
"channelRefundNo": null
}
}
```
### 2.3 分账 `/payment/open/split`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/open/split/create` | POST | 创建分账订单 |
| `/payment/open/split/{splitNo}` | GET | 查询分账订单 |
| `/payment/open/split/execute/{splitNo}` | POST | 执行分账(手动触发) |
| `/payment/open/split/return/{splitNo}` | POST | 回退分账 |
#### 创建分账订单 `POST /payment/open/split/create`
```java
// 请求 SplitOrderCreateRequest
{
"orderNo": "PAY20260608001",
"splitReceivers": [
{
"receiverId": 1,
"receiverType": 1,
"receiverName": "商户A",
"amount": 30.00,
"rate": 0.30
}
]
}
```
### 2.4 商户进件 `/payment/open/merchant`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/open/merchant/apply` | POST | 提交商户入驻申请 |
| `/payment/open/merchant/{merchantNo}` | GET | 查询商户信息 |
| `/payment/open/merchant/{merchantNo}` | PUT | 更新商户信息 |
| `/payment/open/merchant/qualification/upload` | POST | 提交资质材料 |
| `/payment/open/merchant/qualification/{merchantNo}` | GET | 查询资质列表 |
| `/payment/open/merchant/audit/{merchantNo}` | GET | 查询审核记录 |
| `/payment/open/merchant/channel/{merchantNo}` | GET | 查询渠道配置 |
| `/payment/open/merchant/channel/apply` | POST | 申请渠道开通 |
| `/payment/open/merchant/settle/{merchantNo}` | GET | 查询结算配置 |
| `/payment/open/merchant/settle/{merchantNo}` | PUT | 更新结算配置 |
---
## 三、对外入口(免认证)
### `/payment/entry`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/entry/cashier/{orderNo}` | GET | 收银台页面(H5 |
| `/payment/entry/scan` | POST | 扫码支付 |
---
## 四、第三方回调(免认证)
### `/payment/notify`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/notify/alipay` | POST | 支付宝支付回调 |
| `/payment/notify/wechat_pay` | POST | 微信支付回调(JSON body |
| `/payment/notify/unionpay` | POST | 银联支付回调 |
| `/payment/notify/alipay/refund` | POST | 支付宝退款回调 |
| `/payment/notify/wechat_pay/refund` | POST | 微信退款回调(JSON body |
#### 支付宝回调
```
POST /payment/notify/alipay
Content-Type: application/x-www-form-urlencoded
out_trade_no=PAY20260608001&trade_no=20260608...&trade_status=TRADE_SUCCESS&total_amount=100.00&...
```
- 返回纯文本 `"success"` 表示成功,`"fail"` 表示失败
#### 微信回调
```
POST /payment/notify/wechat_pay
Content-Type: application/json
{"id":"xxx","create_time":"...","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","resource":{...}}
```
- 请求头携带 `Wechatpay-Serial`, `Wechatpay-Nonce`, `Wechatpay-Signature`, `Wechatpay-Timestamp`
- 返回 JSON `{"code":"SUCCESS","message":"成功"}`
---
## 五、内部接口(@Inner
### `/payment/inner`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/inner/order/status/{orderNo}` | GET | 查询支付状态 |
| `/payment/inner/order/biz/{bizOrderNo}` | GET | 根据业务订单号查询 |
| `/payment/inner/order/create` | POST | 创建支付订单(内部调用) |
---
## 六、管理后台接口
### 6.1 支付渠道管理 `/payment/admin/channel`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/admin/channel/list` | GET | 渠道列表 |
| `/payment/admin/channel/{id}` | GET | 渠道详情 |
| `/payment/admin/channel` | POST | 新增渠道 |
| `/payment/admin/channel/{id}` | PUT | 修改渠道 |
| `/payment/admin/channel/{id}/enable` | PUT | 启用渠道 |
| `/payment/admin/channel/{id}/disable` | PUT | 停用渠道 |
### 6.2 代理商管理 `/payment/admin/agent`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/admin/agent/list` | GET | 代理商列表 |
| `/payment/admin/agent/{id}` | GET | 代理商详情 |
| `/payment/admin/agent` | POST | 新增代理商 |
| `/payment/admin/agent/{id}` | PUT | 修改代理商 |
| `/payment/admin/agent/{id}/commission` | GET | 佣金记录 |
| `/payment/admin/agent/{id}/settlement` | GET | 结算记录 |
### 6.3 结算管理 `/payment/admin/settle`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/payment/admin/settle/list` | GET | 结算单列表 |
| `/payment/admin/settle/{id}` | GET | 结算单详情 |
| `/payment/admin/settle/generate` | POST | 生成结算单 |
| `/payment/admin/settle/{id}/confirm` | POST | 确认结算 |
@@ -0,0 +1,802 @@
# 支付模块数据库设计
> **来源**: `~/rui/支付模块架构设计.md` v1.0
> **创建日期**: 2026-06-08
> **模块**: rui-payment
> **表数量**: 21 张
---
## ER 关系概览
```
pay_order ──→ pay_record ──→ pay_channel_merchant
│ │
↓ ↓
pay_refund pay_channel
pay_merchant ──→ pay_merchant_qualification ──→ pay_merchant_audit
pay_merchant_channel ──→ pay_merchant_settle
pay_split_order ──→ pay_split_detail ──→ pay_split_receiver
pay_agent ──→ pay_agent_merchant ──→ pay_agent_commission ──→ pay_agent_settlement
pay_account ──→ pay_account_record
pay_account_freeze
pay_reconcile ──→ pay_reconcile_diff
```
---
### 3.1 支付核心表
#### 3.1.1 支付订单表 (pay_order)
```sql
CREATE TABLE pay_order (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
order_no VARCHAR(64) NOT NULL COMMENT '支付订单号(系统生成)',
biz_order_no VARCHAR(64) NOT NULL COMMENT '业务订单号(外部传入)',
biz_type TINYINT NOT NULL DEFAULT 1 COMMENT '业务类型 1:订单支付 2:充值 3:转账',
subject VARCHAR(256) NOT NULL COMMENT '订单标题',
body VARCHAR(500) DEFAULT NULL COMMENT '订单描述',
total_amount DECIMAL(19,4) NOT NULL COMMENT '订单总金额',
pay_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '实际支付金额',
discount_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '优惠金额',
fee_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '手续费金额',
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
payer_id BIGINT DEFAULT NULL COMMENT '付款人ID',
payer_type TINYINT NOT NULL DEFAULT 1 COMMENT '付款人类型 1:用户 2:商户',
payer_name VARCHAR(100) DEFAULT NULL COMMENT '付款人名称',
payee_id BIGINT NOT NULL COMMENT '收款人ID(商户ID',
payee_type TINYINT NOT NULL DEFAULT 1 COMMENT '收款人类型 1:商户 2:平台',
payee_name VARCHAR(100) DEFAULT NULL COMMENT '收款人名称',
channel_id BIGINT DEFAULT NULL COMMENT '支付渠道ID',
channel_code VARCHAR(50) DEFAULT NULL COMMENT '支付渠道编码',
pay_status TINYINT NOT NULL DEFAULT 0 COMMENT '支付状态 0:待支付 1:支付中 2:支付成功 3:支付失败 4:已关闭 5:已退款',
pay_time DATETIME(3) DEFAULT NULL COMMENT '支付成功时间',
expire_time DATETIME(3) NOT NULL COMMENT '订单过期时间',
client_ip VARCHAR(128) DEFAULT NULL COMMENT '客户端IP',
device VARCHAR(100) DEFAULT NULL COMMENT '设备信息',
notify_url VARCHAR(500) DEFAULT NULL COMMENT '异步通知地址',
return_url VARCHAR(500) DEFAULT NULL COMMENT '同步跳转地址',
extra_params JSON DEFAULT NULL COMMENT '扩展参数(渠道特定参数)',
error_code VARCHAR(100) DEFAULT NULL COMMENT '错误码',
error_msg VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0:正常 1:删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (tenant_id, order_no),
UNIQUE KEY uk_biz_order_no (tenant_id, biz_order_no, biz_type),
INDEX idx_pay_status (pay_status),
INDEX idx_payee_id (payee_id),
INDEX idx_payer_id (payer_id),
INDEX idx_channel_id (channel_id),
INDEX idx_pay_time (pay_time),
INDEX idx_created_at (created_at),
INDEX idx_tenant_id (tenant_id)
) COMMENT='支付订单表';
```
#### 3.1.2 支付流水表 (pay_record)
```sql
CREATE TABLE pay_record (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
record_no VARCHAR(64) NOT NULL COMMENT '流水号',
order_id BIGINT NOT NULL COMMENT '支付订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '支付订单号',
channel_id BIGINT NOT NULL COMMENT '支付渠道ID',
channel_code VARCHAR(50) NOT NULL COMMENT '渠道编码',
channel_merchant_no VARCHAR(100) DEFAULT NULL COMMENT '渠道商户号',
channel_order_no VARCHAR(128) DEFAULT NULL COMMENT '渠道订单号',
pay_amount DECIMAL(19,4) NOT NULL COMMENT '支付金额',
fee_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '手续费',
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
pay_status TINYINT NOT NULL DEFAULT 0 COMMENT '支付状态 0:待支付 1:支付中 2:支付成功 3:支付失败 4:已关闭',
pay_time DATETIME(3) DEFAULT NULL COMMENT '支付成功时间',
payer_info JSON DEFAULT NULL COMMENT '付款人信息(openid、银行卡号等)',
notify_status TINYINT NOT NULL DEFAULT 0 COMMENT '通知状态 0:未通知 1:通知成功 2:通知失败',
notify_times INT NOT NULL DEFAULT 0 COMMENT '通知次数',
notify_last_time DATETIME(3) DEFAULT NULL COMMENT '最后通知时间',
error_code VARCHAR(100) DEFAULT NULL COMMENT '错误码',
error_msg VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_record_no (tenant_id, record_no),
UNIQUE KEY uk_channel_order (tenant_id, channel_id, channel_order_no),
INDEX idx_order_id (order_id),
INDEX idx_order_no (order_no),
INDEX idx_pay_status (pay_status),
INDEX idx_pay_time (pay_time),
INDEX idx_created_at (created_at),
INDEX idx_tenant_id (tenant_id)
) COMMENT='支付流水表';
```
#### 3.1.3 退款单表 (pay_refund)
```sql
CREATE TABLE pay_refund (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
refund_no VARCHAR(64) NOT NULL COMMENT '退款单号',
order_id BIGINT NOT NULL COMMENT '支付订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '支付订单号',
record_id BIGINT NOT NULL COMMENT '支付流水ID',
record_no VARCHAR(64) NOT NULL COMMENT '支付流水号',
channel_id BIGINT NOT NULL COMMENT '支付渠道ID',
channel_refund_no VARCHAR(128) DEFAULT NULL COMMENT '渠道退款单号',
refund_amount DECIMAL(19,4) NOT NULL COMMENT '退款金额',
total_amount DECIMAL(19,4) NOT NULL COMMENT '订单总金额',
refund_status TINYINT NOT NULL DEFAULT 0 COMMENT '退款状态 0:待退款 1:退款中 2:退款成功 3:退款失败',
refund_time DATETIME(3) DEFAULT NULL COMMENT '退款成功时间',
refund_reason VARCHAR(500) DEFAULT NULL COMMENT '退款原因',
operator_id BIGINT DEFAULT NULL COMMENT '操作人ID',
operator_name VARCHAR(100) DEFAULT NULL COMMENT '操作人名称',
notify_status TINYINT NOT NULL DEFAULT 0 COMMENT '通知状态',
error_code VARCHAR(100) DEFAULT NULL COMMENT '错误码',
error_msg VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_refund_no (tenant_id, refund_no),
INDEX idx_order_id (order_id),
INDEX idx_order_no (order_no),
INDEX idx_refund_status (refund_status),
INDEX idx_created_at (created_at),
INDEX idx_tenant_id (tenant_id)
) COMMENT='退款单表';
```
### 3.2 支付渠道表
#### 3.2.1 支付渠道表 (pay_channel)
```sql
CREATE TABLE pay_channel (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
channel_code VARCHAR(50) NOT NULL COMMENT '渠道编码 如:alipay_app,wx_jsapi',
channel_name VARCHAR(100) NOT NULL COMMENT '渠道名称',
channel_type TINYINT NOT NULL DEFAULT 1 COMMENT '渠道类型 1:支付宝 2:微信支付 3:银联 4:其他',
payment_type TINYINT NOT NULL DEFAULT 1 COMMENT '支付方式 1:扫码 2:App 3:H5 4:JSAPI 5:小程序 6:刷脸',
config_json JSON NOT NULL COMMENT '渠道配置(JSON格式)',
fee_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0060 COMMENT '手续费率(如0.0060=0.6%',
fee_type TINYINT NOT NULL DEFAULT 1 COMMENT '手续费类型 1:百分比 2:固定金额',
min_amount DECIMAL(19,4) DEFAULT NULL COMMENT '单笔最小金额',
max_amount DECIMAL(19,4) DEFAULT NULL COMMENT '单笔最大金额',
day_limit_amount DECIMAL(19,4) DEFAULT NULL COMMENT '单日限额',
sort_no INT NOT NULL DEFAULT 0 COMMENT '排序号',
is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认渠道 0:否 1:是',
weight INT NOT NULL DEFAULT 100 COMMENT '权重(用于路由)',
success_rate DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '成功率(%',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_channel_code (tenant_id, channel_code),
INDEX idx_channel_type (channel_type),
INDEX idx_status (status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='支付渠道表';
```
#### 3.2.2 渠道商户配置表 (pay_channel_merchant)
```sql
CREATE TABLE pay_channel_merchant (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
channel_id BIGINT NOT NULL COMMENT '支付渠道ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
merchant_no VARCHAR(100) NOT NULL COMMENT '渠道商户号',
app_id VARCHAR(100) DEFAULT NULL COMMENT '应用ID',
private_key TEXT DEFAULT NULL COMMENT '商户私钥',
public_key TEXT DEFAULT NULL COMMENT '商户公钥',
api_key VARCHAR(500) DEFAULT NULL COMMENT 'API密钥',
cert_path VARCHAR(500) DEFAULT NULL COMMENT '证书路径',
cert_password VARCHAR(100) DEFAULT NULL COMMENT '证书密码',
notify_url VARCHAR(500) DEFAULT NULL COMMENT '异步通知地址',
return_url VARCHAR(500) DEFAULT NULL COMMENT '同步跳转地址',
fee_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0060 COMMENT '商户自定义费率',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_channel_merchant (tenant_id, channel_id, merchant_id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_channel_id (channel_id),
INDEX idx_tenant_id (tenant_id)
) COMMENT='渠道商户配置表';
```
### 3.3 商户进件表
#### 3.3.1 商户表 (pay_merchant)
```sql
CREATE TABLE pay_merchant (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
merchant_no VARCHAR(64) NOT NULL COMMENT '商户编号',
merchant_name VARCHAR(200) NOT NULL COMMENT '商户名称',
merchant_short_name VARCHAR(100) DEFAULT NULL COMMENT '商户简称',
merchant_type TINYINT NOT NULL DEFAULT 1 COMMENT '商户类型 1:企业 2:个体户 3:个人',
agent_id BIGINT DEFAULT NULL COMMENT '所属代理ID',
contact_name VARCHAR(100) NOT NULL COMMENT '联系人姓名',
contact_phone VARCHAR(20) NOT NULL COMMENT '联系人电话',
contact_email VARCHAR(100) DEFAULT NULL COMMENT '联系人邮箱',
province_code VARCHAR(20) DEFAULT NULL COMMENT '省份编码',
city_code VARCHAR(20) DEFAULT NULL COMMENT '城市编码',
district_code VARCHAR(20) DEFAULT NULL COMMENT '区县编码',
address VARCHAR(500) DEFAULT NULL COMMENT '详细地址',
logo_url VARCHAR(500) DEFAULT NULL COMMENT '商户Logo',
website VARCHAR(500) DEFAULT NULL COMMENT '商户网站',
business_scope VARCHAR(500) DEFAULT NULL COMMENT '经营范围',
audit_status TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态 0:待提交 1:待审核 2:审核中 3:审核通过 4:审核驳回',
audit_remark VARCHAR(500) DEFAULT NULL COMMENT '审核备注',
audit_time DATETIME(3) DEFAULT NULL COMMENT '审核时间',
merchant_status TINYINT NOT NULL DEFAULT 0 COMMENT '商户状态 0:未激活 1:正常 2:冻结 3:注销',
activate_time DATETIME(3) DEFAULT NULL COMMENT '激活时间',
total_transaction_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '累计交易金额',
total_transaction_count INT NOT NULL DEFAULT 0 COMMENT '累计交易笔数',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_merchant_no (tenant_id, merchant_no),
INDEX idx_agent_id (agent_id),
INDEX idx_audit_status (audit_status),
INDEX idx_merchant_status (merchant_status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='商户表';
```
#### 3.3.2 商户资质表 (pay_merchant_qualification)
```sql
CREATE TABLE pay_merchant_qualification (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
qual_type TINYINT NOT NULL DEFAULT 1 COMMENT '资质类型 1:营业执照 2:法人身份证正面 3:法人身份证反面 4:开户许可证 5:门头照 6:店内照 7:结算银行卡 8:特殊资质',
qual_name VARCHAR(100) NOT NULL COMMENT '资质名称',
qual_no VARCHAR(100) DEFAULT NULL COMMENT '资质编号(如营业执照号)',
qual_image_url VARCHAR(500) NOT NULL COMMENT '资质图片URL',
qual_image_url2 VARCHAR(500) DEFAULT NULL COMMENT '资质图片URL2(反面)',
valid_start_date DATE DEFAULT NULL COMMENT '有效期开始',
valid_end_date DATE DEFAULT NULL COMMENT '有效期结束',
is_permanent TINYINT NOT NULL DEFAULT 0 COMMENT '是否永久有效 0:否 1:是',
verify_status TINYINT NOT NULL DEFAULT 0 COMMENT '核验状态 0:未核验 1:核验通过 2:核验失败',
verify_result VARCHAR(500) DEFAULT NULL COMMENT '核验结果',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_qual_type (qual_type),
INDEX idx_tenant_id (tenant_id)
) COMMENT='商户资质表';
```
#### 3.3.3 商户审核记录表 (pay_merchant_audit)
```sql
CREATE TABLE pay_merchant_audit (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
audit_type TINYINT NOT NULL DEFAULT 1 COMMENT '审核类型 1:入驻审核 2:资质变更 3:费率变更 4:结算变更',
audit_level TINYINT NOT NULL DEFAULT 1 COMMENT '审核层级 1:初审 2:复审',
audit_status TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态 0:待审核 1:审核通过 2:审核驳回',
audit_remark VARCHAR(500) DEFAULT NULL COMMENT '审核意见',
auditor_id BIGINT DEFAULT NULL COMMENT '审核人ID',
auditor_name VARCHAR(100) DEFAULT NULL COMMENT '审核人姓名',
audit_time DATETIME(3) DEFAULT NULL COMMENT '审核时间',
pre_data JSON DEFAULT NULL COMMENT '变更前数据',
post_data JSON DEFAULT NULL COMMENT '变更后数据',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_audit_type (audit_type),
INDEX idx_audit_status (audit_status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='商户审核记录表';
```
#### 3.3.4 商户渠道配置表 (pay_merchant_channel)
```sql
CREATE TABLE pay_merchant_channel (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
channel_id BIGINT NOT NULL COMMENT '支付渠道ID',
channel_code VARCHAR(50) NOT NULL COMMENT '渠道编码',
channel_merchant_no VARCHAR(100) DEFAULT NULL COMMENT '渠道子商户号',
channel_app_id VARCHAR(100) DEFAULT NULL COMMENT '渠道应用ID',
channel_status TINYINT NOT NULL DEFAULT 0 COMMENT '渠道状态 0:未申请 1:申请中 2:已通过 3:已驳回 4:已停用',
apply_time DATETIME(3) DEFAULT NULL COMMENT '申请时间',
audit_time DATETIME(3) DEFAULT NULL COMMENT '渠道审核时间',
audit_remark VARCHAR(500) DEFAULT NULL COMMENT '渠道审核备注',
fee_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0060 COMMENT '商户渠道费率',
day_limit_amount DECIMAL(19,4) DEFAULT NULL COMMENT '单日限额',
single_limit_amount DECIMAL(19,4) DEFAULT NULL COMMENT '单笔限额',
config_json JSON DEFAULT NULL COMMENT '渠道特定配置(JSON',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_merchant_channel (tenant_id, merchant_id, channel_id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_channel_id (channel_id),
INDEX idx_channel_status (channel_status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='商户渠道配置表';
```
#### 3.3.5 商户结算配置表 (pay_merchant_settle)
```sql
CREATE TABLE pay_merchant_settle (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
settle_type TINYINT NOT NULL DEFAULT 1 COMMENT '结算方式 1:自动结算 2:手动结算',
settle_cycle TINYINT NOT NULL DEFAULT 1 COMMENT '结算周期 1:T+0 2:T+1 3:T+7 4:T+30',
min_settle_amount DECIMAL(19,4) NOT NULL DEFAULT 1.0000 COMMENT '最低结算金额',
settle_account_type TINYINT NOT NULL DEFAULT 1 COMMENT '结算账户类型 1:对公账户 2:对私账户 3:支付宝 4:微信',
settle_account_name VARCHAR(100) NOT NULL COMMENT '结算账户名',
settle_account_no VARCHAR(200) NOT NULL COMMENT '结算账号',
settle_bank_code VARCHAR(50) DEFAULT NULL COMMENT '结算银行编码',
settle_bank_name VARCHAR(100) DEFAULT NULL COMMENT '结算银行名称',
settle_bank_branch VARCHAR(200) DEFAULT NULL COMMENT '开户支行',
settle_bank_province VARCHAR(50) DEFAULT NULL COMMENT '开户省份',
settle_bank_city VARCHAR(50) DEFAULT NULL COMMENT '开户城市',
is_default TINYINT NOT NULL DEFAULT 1 COMMENT '是否默认结算配置',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_merchant_settle (tenant_id, merchant_id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_tenant_id (tenant_id)
) COMMENT='商户结算配置表';
```
### 3.4 分账表
#### 3.3.1 分账订单表 (pay_split_order)
```sql
CREATE TABLE pay_split_order (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
split_no VARCHAR(64) NOT NULL COMMENT '分账订单号',
order_id BIGINT NOT NULL COMMENT '支付订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '支付订单号',
record_id BIGINT NOT NULL COMMENT '支付流水ID',
total_amount DECIMAL(19,4) NOT NULL COMMENT '分账总金额',
split_status TINYINT NOT NULL DEFAULT 0 COMMENT '分账状态 0:待分账 1:分账中 2:分账成功 3:分账失败 4:已回退',
split_type TINYINT NOT NULL DEFAULT 1 COMMENT '分账类型 1:实时分账 2:延迟分账',
split_mode TINYINT NOT NULL DEFAULT 1 COMMENT '分账模式 1:按比例 2:按固定金额',
split_time DATETIME(3) DEFAULT NULL COMMENT '分账成功时间',
finish_time DATETIME(3) DEFAULT NULL COMMENT '分账完成时间',
return_url VARCHAR(500) DEFAULT NULL COMMENT '异步通知地址',
error_code VARCHAR(100) DEFAULT NULL COMMENT '错误码',
error_msg VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_split_no (tenant_id, split_no),
INDEX idx_order_id (order_id),
INDEX idx_order_no (order_no),
INDEX idx_split_status (split_status),
INDEX idx_created_at (created_at),
INDEX idx_tenant_id (tenant_id)
) COMMENT='分账订单表';
```
#### 3.3.2 分账明细表 (pay_split_detail)
```sql
CREATE TABLE pay_split_detail (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
split_order_id BIGINT NOT NULL COMMENT '分账订单ID',
split_no VARCHAR(64) NOT NULL COMMENT '分账订单号',
receiver_id BIGINT NOT NULL COMMENT '接收方ID',
receiver_type TINYINT NOT NULL DEFAULT 1 COMMENT '接收方类型 1:商户 2:平台 3:代理商 4:个人',
receiver_name VARCHAR(100) DEFAULT NULL COMMENT '接收方名称',
split_amount DECIMAL(19,4) NOT NULL COMMENT '分账金额',
split_rate DECIMAL(5,4) DEFAULT NULL COMMENT '分账比例',
split_status TINYINT NOT NULL DEFAULT 0 COMMENT '分账状态 0:待分账 1:分账中 2:分账成功 3:分账失败',
split_time DATETIME(3) DEFAULT NULL COMMENT '分账成功时间',
channel_detail_no VARCHAR(128) DEFAULT NULL COMMENT '渠道分账明细单号',
error_code VARCHAR(100) DEFAULT NULL COMMENT '错误码',
error_msg VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
INDEX idx_split_order_id (split_order_id),
INDEX idx_split_no (split_no),
INDEX idx_receiver_id (receiver_id),
INDEX idx_split_status (split_status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='分账明细表';
```
#### 3.3.3 分账接收方表 (pay_split_receiver)
```sql
CREATE TABLE pay_split_receiver (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
receiver_type TINYINT NOT NULL DEFAULT 1 COMMENT '接收方类型 1:商户 2:平台 3:代理商 4:个人',
receiver_id BIGINT NOT NULL COMMENT '接收方业务ID',
receiver_name VARCHAR(100) NOT NULL COMMENT '接收方名称',
channel_type TINYINT NOT NULL DEFAULT 1 COMMENT '渠道类型 1:支付宝 2:微信 3:银行卡',
account_type VARCHAR(50) DEFAULT NULL COMMENT '账户类型 如:login_name(支付宝登录号),openid(微信)',
account_no VARCHAR(200) NOT NULL COMMENT '接收账号',
account_name VARCHAR(100) DEFAULT NULL COMMENT '账号真实姓名',
relation_type TINYINT NOT NULL DEFAULT 1 COMMENT '关系类型 1:服务商 2:门店 3:员工 4:个人',
channel_relation_json JSON DEFAULT NULL COMMENT '渠道关系配置(JSON',
is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认接收方',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_receiver (tenant_id, receiver_type, receiver_id, channel_type),
INDEX idx_receiver_id (receiver_id),
INDEX idx_account_no (account_no),
INDEX idx_status (status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='分账接收方表';
```
### 3.5 代理商表
#### 3.4.1 代理商表 (pay_agent)
```sql
CREATE TABLE pay_agent (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
agent_no VARCHAR(64) NOT NULL COMMENT '代理商编号',
agent_name VARCHAR(200) NOT NULL COMMENT '代理商名称',
agent_type TINYINT NOT NULL DEFAULT 1 COMMENT '代理商类型 1:个人 2:企业',
parent_id BIGINT DEFAULT 0 COMMENT '上级代理ID 0:顶级代理',
level TINYINT NOT NULL DEFAULT 1 COMMENT '代理层级 1:一级 2:二级 3:三级',
level_path VARCHAR(500) DEFAULT NULL COMMENT '层级路径 如: /1/5/10/',
contact_name VARCHAR(100) DEFAULT NULL COMMENT '联系人',
contact_phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
contact_email VARCHAR(100) DEFAULT NULL COMMENT '联系邮箱',
id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号',
business_license VARCHAR(100) DEFAULT NULL COMMENT '营业执照号',
address VARCHAR(500) DEFAULT NULL COMMENT '地址',
commission_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0000 COMMENT '默认佣金比例',
min_settle_amount DECIMAL(19,4) NOT NULL DEFAULT 100.0000 COMMENT '最低结算金额',
settle_type TINYINT NOT NULL DEFAULT 1 COMMENT '结算类型 1:自动结算 2:手动结算',
settle_cycle TINYINT NOT NULL DEFAULT 1 COMMENT '结算周期 1:T+1 2:T+7 3:T+30',
settle_account_id BIGINT DEFAULT NULL COMMENT '结算账户ID',
merchant_count INT NOT NULL DEFAULT 0 COMMENT '拓展商户数',
total_transaction_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '累计交易金额',
total_commission_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '累计佣金金额',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用 2:审核中',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_agent_no (tenant_id, agent_no),
INDEX idx_parent_id (parent_id),
INDEX idx_level (level),
INDEX idx_status (status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='代理商表';
```
#### 3.4.2 代理商户关系表 (pay_agent_merchant)
```sql
CREATE TABLE pay_agent_merchant (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
agent_id BIGINT NOT NULL COMMENT '代理商ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
merchant_name VARCHAR(200) DEFAULT NULL COMMENT '商户名称',
commission_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0000 COMMENT '佣金比例(覆盖代理默认比例)',
bind_time DATETIME(3) DEFAULT NULL COMMENT '绑定时间',
unbind_time DATETIME(3) DEFAULT NULL COMMENT '解绑时间',
is_bind TINYINT NOT NULL DEFAULT 1 COMMENT '是否绑定 0:解绑 1:绑定',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_agent_merchant (tenant_id, agent_id, merchant_id),
INDEX idx_agent_id (agent_id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_tenant_id (tenant_id)
) COMMENT='代理商户关系表';
```
#### 3.4.3 代理佣金记录表 (pay_agent_commission)
```sql
CREATE TABLE pay_agent_commission (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
commission_no VARCHAR(64) NOT NULL COMMENT '佣金记录号',
agent_id BIGINT NOT NULL COMMENT '代理商ID',
merchant_id BIGINT NOT NULL COMMENT '商户ID',
order_id BIGINT NOT NULL COMMENT '支付订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '支付订单号',
record_id BIGINT NOT NULL COMMENT '支付流水ID',
transaction_amount DECIMAL(19,4) NOT NULL COMMENT '交易金额',
commission_rate DECIMAL(5,4) NOT NULL COMMENT '佣金比例',
commission_amount DECIMAL(19,4) NOT NULL COMMENT '佣金金额',
commission_status TINYINT NOT NULL DEFAULT 0 COMMENT '佣金状态 0:待结算 1:已结算 2:已取消',
settle_time DATETIME(3) DEFAULT NULL COMMENT '结算时间',
settle_batch_no VARCHAR(64) DEFAULT NULL COMMENT '结算批次号',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_commission_no (tenant_id, commission_no),
INDEX idx_agent_id (agent_id),
INDEX idx_merchant_id (merchant_id),
INDEX idx_order_id (order_id),
INDEX idx_commission_status (commission_status),
INDEX idx_settle_batch_no (settle_batch_no),
INDEX idx_created_at (created_at),
INDEX idx_tenant_id (tenant_id)
) COMMENT='代理佣金记录表';
```
#### 3.4.4 代理结算表 (pay_agent_settlement)
```sql
CREATE TABLE pay_agent_settlement (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
settlement_no VARCHAR(64) NOT NULL COMMENT '结算单号',
agent_id BIGINT NOT NULL COMMENT '代理商ID',
settlement_type TINYINT NOT NULL DEFAULT 1 COMMENT '结算类型 1:佣金结算 2:退款扣回',
start_time DATETIME(3) NOT NULL COMMENT '结算开始时间',
end_time DATETIME(3) NOT NULL COMMENT '结算结束时间',
total_count INT NOT NULL DEFAULT 0 COMMENT '结算笔数',
total_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '结算总金额',
fee_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '手续费金额',
actual_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '实际结算金额',
settlement_status TINYINT NOT NULL DEFAULT 0 COMMENT '结算状态 0:待结算 1:结算中 2:结算成功 3:结算失败',
settlement_time DATETIME(3) DEFAULT NULL COMMENT '结算成功时间',
settlement_method TINYINT NOT NULL DEFAULT 1 COMMENT '结算方式 1:转账到余额 2:银行转账 3:支付宝 4:微信',
settlement_account VARCHAR(200) DEFAULT NULL COMMENT '结算账户',
settlement_account_name VARCHAR(100) DEFAULT NULL COMMENT '结算账户名',
settlement_batch_no VARCHAR(128) DEFAULT NULL COMMENT '渠道结算批次号',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_settlement_no (tenant_id, settlement_no),
INDEX idx_agent_id (agent_id),
INDEX idx_settlement_status (settlement_status),
INDEX idx_start_time (start_time),
INDEX idx_end_time (end_time),
INDEX idx_tenant_id (tenant_id)
) COMMENT='代理结算表';
```
### 3.6 资金账户表
#### 3.5.1 资金账户表 (pay_account)
```sql
CREATE TABLE pay_account (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
account_no VARCHAR(64) NOT NULL COMMENT '账户号',
account_type TINYINT NOT NULL DEFAULT 1 COMMENT '账户类型 1:用户 2:商户 3:平台 4:代理',
owner_id BIGINT NOT NULL COMMENT '账户所有者ID',
owner_name VARCHAR(100) DEFAULT NULL COMMENT '账户所有者名称',
balance DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '账户余额',
available_balance DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '可用余额',
frozen_balance DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '冻结余额',
total_income DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '累计收入',
total_expenditure DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '累计支出',
currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
password_hash VARCHAR(255) DEFAULT NULL COMMENT '支付密码',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:冻结 1:正常',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_account_no (tenant_id, account_no),
UNIQUE KEY uk_owner (tenant_id, account_type, owner_id),
INDEX idx_owner_id (owner_id),
INDEX idx_status (status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='资金账户表';
```
#### 3.5.2 账户流水表 (pay_account_record)
```sql
CREATE TABLE pay_account_record (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
record_no VARCHAR(64) NOT NULL COMMENT '流水号',
account_id BIGINT NOT NULL COMMENT '账户ID',
account_no VARCHAR(64) NOT NULL COMMENT '账户号',
record_type TINYINT NOT NULL DEFAULT 1 COMMENT '流水类型 1:收入 2:支出 3:冻结 4:解冻 5:充值 6:提现',
biz_type TINYINT NOT NULL DEFAULT 1 COMMENT '业务类型 1:支付 2:退款 3:分账 4:佣金 5:结算 6:提现 7:充值',
biz_id BIGINT DEFAULT NULL COMMENT '业务ID',
biz_no VARCHAR(64) DEFAULT NULL COMMENT '业务单号',
amount DECIMAL(19,4) NOT NULL COMMENT '变动金额',
before_balance DECIMAL(19,4) NOT NULL COMMENT '变动前余额',
after_balance DECIMAL(19,4) NOT NULL COMMENT '变动后余额',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_record_no (tenant_id, record_no),
INDEX idx_account_id (account_id),
INDEX idx_biz_id (biz_id),
INDEX idx_biz_no (biz_no),
INDEX idx_record_type (record_type),
INDEX idx_created_at (created_at),
INDEX idx_tenant_id (tenant_id)
) COMMENT='账户流水表';
```
#### 3.5.3 资金冻结表 (pay_account_freeze)
```sql
CREATE TABLE pay_account_freeze (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
freeze_no VARCHAR(64) NOT NULL COMMENT '冻结单号',
account_id BIGINT NOT NULL COMMENT '账户ID',
freeze_amount DECIMAL(19,4) NOT NULL COMMENT '冻结金额',
freeze_type TINYINT NOT NULL DEFAULT 1 COMMENT '冻结类型 1:退款保障 2:争议处理 3:合规审查',
freeze_status TINYINT NOT NULL DEFAULT 0 COMMENT '冻结状态 0:冻结中 1:已解冻 2:已扣款',
biz_type TINYINT NOT NULL DEFAULT 1 COMMENT '业务类型 1:订单 2:提现',
biz_id BIGINT NOT NULL COMMENT '业务ID',
biz_no VARCHAR(64) NOT NULL COMMENT '业务单号',
expire_time DATETIME(3) DEFAULT NULL COMMENT '过期自动解冻时间',
unfreeze_time DATETIME(3) DEFAULT NULL COMMENT '实际解冻时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_freeze_no (tenant_id, freeze_no),
INDEX idx_account_id (account_id),
INDEX idx_biz_id (biz_id),
INDEX idx_freeze_status (freeze_status),
INDEX idx_expire_time (expire_time),
INDEX idx_tenant_id (tenant_id)
) COMMENT='资金冻结表';
```
### 3.7 对账结算表
#### 3.6.1 对账单表 (pay_reconcile)
```sql
CREATE TABLE pay_reconcile (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
reconcile_no VARCHAR(64) NOT NULL COMMENT '对账单号',
channel_id BIGINT NOT NULL COMMENT '支付渠道ID',
channel_code VARCHAR(50) NOT NULL COMMENT '渠道编码',
reconcile_date DATE NOT NULL COMMENT '对账日期',
reconcile_type TINYINT NOT NULL DEFAULT 1 COMMENT '对账类型 1:支付对账 2:退款对账',
total_count INT NOT NULL DEFAULT 0 COMMENT '总笔数',
total_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '总金额',
success_count INT NOT NULL DEFAULT 0 COMMENT '对平笔数',
success_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '对平金额',
fail_count INT NOT NULL DEFAULT 0 COMMENT '差异笔数',
fail_amount DECIMAL(19,4) NOT NULL DEFAULT 0.0000 COMMENT '差异金额',
missing_count INT NOT NULL DEFAULT 0 COMMENT '漏单笔数(平台有渠道无)',
extra_count INT NOT NULL DEFAULT 0 COMMENT '多渠道笔数(渠道有平台无)',
amount_diff_count INT NOT NULL DEFAULT 0 COMMENT '金额差异笔数',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0:对账中 1:对账成功 2:对账失败 3:已处理',
file_url VARCHAR(500) DEFAULT NULL COMMENT '对账文件URL',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status_record TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_reconcile (tenant_id, channel_id, reconcile_date, reconcile_type),
INDEX idx_reconcile_date (reconcile_date),
INDEX idx_status (status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='对账单表';
```
#### 3.6.2 对账差异表 (pay_reconcile_diff)
```sql
CREATE TABLE pay_reconcile_diff (
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
reconcile_id BIGINT NOT NULL COMMENT '对账单ID',
diff_type TINYINT NOT NULL DEFAULT 1 COMMENT '差异类型 1:平台漏单 2:多渠道 3:金额不符 4:状态不符',
order_id BIGINT DEFAULT NULL COMMENT '平台订单ID',
order_no VARCHAR(64) DEFAULT NULL COMMENT '平台订单号',
channel_order_no VARCHAR(128) DEFAULT NULL COMMENT '渠道订单号',
platform_amount DECIMAL(19,4) DEFAULT NULL COMMENT '平台金额',
channel_amount DECIMAL(19,4) DEFAULT NULL COMMENT '渠道金额',
platform_status TINYINT DEFAULT NULL COMMENT '平台状态',
channel_status TINYINT DEFAULT NULL COMMENT '渠道状态',
handle_status TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态 0:待处理 1:已处理',
handle_result TINYINT DEFAULT NULL COMMENT '处理结果 1:补单 2:退款 3:挂账 4:忽略',
handle_remark VARCHAR(500) DEFAULT NULL COMMENT '处理备注',
handler_id BIGINT DEFAULT NULL COMMENT '处理人ID',
handler_name VARCHAR(100) DEFAULT NULL COMMENT '处理人',
handle_time DATETIME(3) DEFAULT NULL COMMENT '处理时间',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
INDEX idx_reconcile_id (reconcile_id),
INDEX idx_order_id (order_id),
INDEX idx_handle_status (handle_status),
INDEX idx_tenant_id (tenant_id)
) COMMENT='对账差异表';
```
---
+220
View File
@@ -0,0 +1,220 @@
# 支付模块架构概览
> **来源**: `~/rui/支付模块架构设计.md` v1.0
> **创建日期**: 2026-06-08
> **模块**: rui-payment
---
## 一、定位
rui-payment 是一个**聚合支付平台**,为上层业务提供统一、安全、高效的支付能力。
### 核心能力
| 能力 | 说明 |
|------|------|
| 聚合支付 | 支持支付宝、微信支付、银联云闪付等多渠道 |
| 智能路由 | 根据费率、成功率、渠道状态自动选择最优通道 |
| 分账体系 | 平台、商户、代理商多级分账,支持实时/延迟/手动分账 |
| 代理商体系 | 三级代理、佣金计算、代理结算 |
| 资金账户 | 余额、冻结、可用资金管理和流水记录 |
| 商户进件 | 商户入驻、资质上传、审核管理、渠道子商户配置 |
| 对账结算 | 自动对账、差错处理、结算单生成 |
---
## 二、模块结构
```
rui-payment/
├── rui-payment-common/ # DTO、枚举、常量、工具
├── rui-payment-core/ # Mapper、Entity、Service(数据库层)
├── rui-payment-provider/ # 第三方支付 SDK 封装(纯网关,不碰 DB)
├── rui-payment-api/ # REST API 服务(可部署)
└── rui-payment-task/ # MQ 监听器 + 定时任务
```
### 依赖关系
```
rui-payment-common
rui-payment-core ──→ rui-payment-provider
↑ ↑
rui-payment-api ←───────┘
rui-payment-task
```
**关键边界**: `rui-payment-provider` 不依赖 `rui-payment-core`,只做三方 SDK 调用。
### 各模块职责
| 模块 | 核心职责 | 典型类 |
|------|---------|--------|
| common | DTO、VO、枚举、常量 | `PayRequest`, `PayResponse`, `PayStatus` |
| core | 业务逻辑、数据访问 | `PayOrderService`, `PayOrderMapper`, `PayOrder` |
| provider | 第三方 SDK 封装 | `AlipayPayHandler`, `WechatPayJsapiHandler` |
| api | REST API、启动类 | `PaymentTradeController`, `PaymentNotifyController` |
| task | 定时任务、MQ 监听 | `ReconcileJob`, `SettlementJob`, `PayTimeoutListener` |
---
## 三、核心业务概念
### 3.1 支付核心
```
PayOrder (支付订单) ──→ PayRecord (支付流水) ──→ PayChannelConfig (渠道配置)
PayRefund (退款单)
```
- **PayOrder**: 业务侧发起的支付请求,含金额、商品信息、买卖双方
- **PayRecord**: 单次支付尝试记录,含渠道信息、三方流水号。一个 Order 可对应多次 Record
- **PayRefund**: 对已成功支付的订单退款,支持部分退款和多次退款
### 3.2 分账
```
SplitOrder (分账订单) ──→ SplitDetail (分账明细) ──→ SplitReceiver (分账接收方)
```
分账模式:
- **实时分账**: 支付成功后立即调用渠道分账接口
- **延迟分账**: 支付成功后冻结资金,延迟期(默认 T+7)后自动分账
- **手动分账**: 运营人员在后台手动触发
### 3.3 代理商
```
Agent (代理商) ──→ AgentRelation (代理关系) ──→ AgentMerchant (代理商户)
AgentCommission (代理佣金) ──→ AgentSettlement (代理结算)
```
三级代理体系,每级独立计算佣金。
### 3.4 资金账户
```
Account (资金账户) ──→ AccountLog (账户流水)
AccountFreeze (资金冻结)
```
为每个商户/用户/代理创建独立资金账户,支持冻结、解冻。
### 3.5 商户进件
```
Merchant (商户) ──→ MerchantQualification (资质) ──→ MerchantAuditRecord (审核)
MerchantChannelConfig (渠道配置) ──→ SubMerchantInfo (子商户)
```
---
## 四、核心流程
### 4.1 支付流程
```
业务系统 → 支付API → 支付Core(保存订单) → 支付Provider(调SDK) → 第三方支付
业务系统 ← 支付API(通知) ← Core(更新状态+分账+佣金) ← Provider(解析回调) ← 异步通知
```
1. 业务系统调用支付 API 创建订单
2. API 调 Core 保存订单
3. API 通过 Provider 调三方 SDK,返回支付参数
4. 用户完成支付,三方异步通知 Provider
5. Provider 验签解析,将结果通过 PayResponse 返回
6. Core 更新订单状态、记录流水、处理分账和佣金
7. API 异步通知业务系统
### 4.2 分账流程
```
支付成功 → Core(创建分账订单+明细) → Provider(调渠道分账接口) → Core(更新状态+更新账户余额)
```
### 4.3 对账流程
```
定时任务 → 下载渠道对账文件 → 与本地订单逐笔比对 → 生成差异记录 → 人工/自动处理差异
```
---
## 五、关键设计决策
| 决策 | 理由 |
|------|------|
| 订单与流水分离 | 用户可能多次尝试支付(切换渠道、失败重试),每次需要独立追踪 |
| 延迟分账(默认 T+7) | 降低退款风险,符合渠道分账规则,给予平台资金沉淀期 |
| 三级代理体系 | 满足大部分场景,避免层级过多导致佣金比例过低 |
| 独立资金账户 | 清晰的资金追踪,支持冻结/解冻,便于对账审计 |
| Provider 不碰 DB | 纯网关设计,解耦三方 SDK 与业务逻辑,可独立测试和替换 |
---
## 六、安全设计
| 领域 | 措施 |
|------|------|
| 支付安全 | 签名验证、敏感信息加密存储、回调 IP 白名单、金额严格校验、幂等控制 |
| 资金安全 | 支付密码 BCrypt 加密、资金操作审计日志、风控拦截、退款保障期资金冻结 |
---
## 七、Nacos 配置
```yaml
payment:
order-timeout: 30 # 订单超时(分钟)
split-delay-days: 7 # 分账延迟(天)
commission-settle-cycle: T+1 # 佣金结算周期
notify-max-times: 5 # 通知重试次数
notify-intervals: 15,30,60,300,900 # 通知间隔(秒)
channels:
alipay:
enabled: true
sandbox: false
app-id: ${ALIPAY_APP_ID}
private-key: ${ALIPAY_PRIVATE_KEY}
public-key: ${ALIPAY_PUBLIC_KEY}
wechatpay:
enabled: true
sandbox: false
app-id: ${WXPAY_APP_ID}
mch-id: ${WXPAY_MCH_ID}
api-v3-key: ${WXPAY_API_V3_KEY}
security:
oauth2:
ignore-urls:
- /payment/entry/**
- /payment/notify/**
```
---
## 八、待开发功能清单
- [ ] 支付核心流程(下单、支付、回调、查询、关闭)
- [ ] 退款功能(申请、查询、回调)
- [ ] 分账功能(创建、执行、回退、查询)
- [ ] 商户进件(入驻、资质、审核、渠道配置)
- [ ] 代理商体系(多级代理、佣金计算、结算)
- [ ] 资金账户(开户、流水、冻结)
- [ ] 对账(下载、比对、差异处理)
- [ ] 结算(商户结算、代理结算)
- [ ] 银联渠道接入
- [ ] 单元测试(核心逻辑覆盖率 ≥80%)
+664
View File
@@ -0,0 +1,664 @@
# 通用平台框架 — 数据库表结构规划
> 基于当前项目分析和通用平台框架最佳实践,列出平台运行所需的全量表结构及归属服务。
> **注意:本规划仅做设计,不做任何代码实施。**
---
## 一、表结构总览
| 服务归属 | 表数量 | 表名前缀 | 说明 |
|---------|--------|---------|------|
| **rui-service-system** | 11 | `sys_` | 系统基础服务(租户/菜单/角色/字典/配置/部门/岗位) |
| **rui-service-user** | 7 | `uc_` | 用户中心服务(用户/详情/等级/角色/部门/岗位/权限) |
| **rui-auth** | 3 | `auth_` | 认证中心(OAuth2客户端/登录日志/操作日志) |
| **rui-gateway** | 2 | `gw_` | 网关服务(路由配置/限流规则) |
| **公共表** | 2 | — | 跨服务使用(文件/消息) |
| **合计** | **25** | | |
> **命名调整建议**:当前 `rui_xxx` 前缀过长,建议简化为服务简称前缀,如 `sys_`、`uc_`、`auth_`、`gw_`,更简洁专业。
---
## 二、rui-service-system 系统基础服务(11张表)
### 2.1 租户管理
```sql
-- sys_tenant — 租户(原 rui_system_tenant 改进)
CREATE TABLE sys_tenant (
id BIGINT NOT NULL,
tenant_code VARCHAR(50) NOT NULL COMMENT '租户编码',
tenant_name VARCHAR(200) NOT NULL COMMENT '租户名称',
tenant_type TINYINT NOT NULL DEFAULT 1 COMMENT '类型 1:企业 2:个人 3:试用',
tenant_level TINYINT NOT NULL DEFAULT 1 COMMENT '等级 1:基础版 2:专业版 3:旗舰版',
logo_url VARCHAR(500) DEFAULT NULL COMMENT 'Logo',
contact_name VARCHAR(100) DEFAULT NULL,
contact_phone VARCHAR(20) DEFAULT NULL,
contact_email VARCHAR(100) DEFAULT NULL,
domains VARCHAR(500) DEFAULT NULL COMMENT '绑定域名(逗号分隔)',
max_user_count INT NOT NULL DEFAULT 100 COMMENT '最大用户数',
expire_time DATETIME DEFAULT NULL COMMENT '过期时间 NULL:永久',
super_admin_id BIGINT DEFAULT NULL COMMENT '超管用户ID',
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_code (tenant_code),
INDEX idx_status (status)
) COMMENT='租户';
-- sys_tenant_package — 租户套餐(新增)
CREATE TABLE sys_tenant_package (
id BIGINT NOT NULL,
package_code VARCHAR(100) NOT NULL COMMENT '套餐编码',
package_name VARCHAR(200) NOT NULL COMMENT '套餐名称',
max_user_count INT NOT NULL DEFAULT 100,
max_dept_count INT NOT NULL DEFAULT 50,
max_menu_count INT NOT NULL DEFAULT 100,
price DECIMAL(19,4) NOT NULL DEFAULT 0 COMMENT '价格',
duration_days INT NOT NULL DEFAULT 365 COMMENT '时长(天)',
remark VARCHAR(500) DEFAULT NULL,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_package_code (package_code)
) COMMENT='租户套餐';
```
### 2.2 组织架构
```sql
-- sys_dept — 部门(新增)
CREATE TABLE sys_dept (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
parent_id BIGINT NOT NULL DEFAULT 0,
ancestors VARCHAR(500) DEFAULT '' COMMENT '祖级ID列表 如: 0,1,5,',
dept_code VARCHAR(100) NOT NULL COMMENT '部门编码',
dept_name VARCHAR(100) NOT NULL COMMENT '部门名称',
leader_id BIGINT DEFAULT NULL COMMENT '负责人ID',
phone VARCHAR(20) DEFAULT NULL,
email VARCHAR(100) DEFAULT NULL,
sort_no INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dept_code (tenant_id, dept_code),
INDEX idx_parent (parent_id),
INDEX idx_tenant (tenant_id)
) COMMENT='部门';
-- sys_post — 岗位(新增)
CREATE TABLE sys_post (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
post_code VARCHAR(100) NOT NULL COMMENT '岗位编码',
post_name VARCHAR(100) NOT NULL COMMENT '岗位名称',
sort_no INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_post_code (tenant_id, post_code),
INDEX idx_tenant (tenant_id)
) COMMENT='岗位';
```
### 2.3 菜单与权限
```sql
-- sys_menu — 菜单(原 rui_system_menu 改进)
CREATE TABLE sys_menu (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID 0:顶级',
ancestors VARCHAR(500) DEFAULT '' COMMENT '祖级ID列表',
menu_code VARCHAR(100) NOT NULL COMMENT '菜单编码(唯一标识)',
menu_name VARCHAR(100) NOT NULL COMMENT '菜单名称',
menu_type TINYINT NOT NULL DEFAULT 1 COMMENT '类型 1:目录 2:菜单 3:按钮',
icon VARCHAR(100) DEFAULT NULL COMMENT '图标',
path VARCHAR(200) DEFAULT NULL COMMENT '路由路径',
component VARCHAR(200) DEFAULT NULL COMMENT '组件路径',
permission VARCHAR(200) DEFAULT NULL COMMENT '权限标识 如: user:list',
is_external TINYINT NOT NULL DEFAULT 0 COMMENT '是否外链 0:否 1:是',
target VARCHAR(20) DEFAULT '_self' COMMENT '打开方式 _self/_blank',
keep_alive TINYINT NOT NULL DEFAULT 0 COMMENT '是否缓存 0:否 1:是',
visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否显示 0:隐藏 1:显示',
status TINYINT NOT NULL DEFAULT 1,
sort_no INT NOT NULL DEFAULT 0,
remark VARCHAR(500) DEFAULT NULL,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_menu_code (tenant_id, menu_code),
INDEX idx_parent (parent_id),
INDEX idx_ancestors (ancestors(100)),
INDEX idx_tenant (tenant_id)
) COMMENT='菜单';
```
### 2.4 角色管理
```sql
-- sys_role — 角色(原 rui_system_role 改进)
CREATE TABLE sys_role (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
parent_id BIGINT NOT NULL DEFAULT 0,
ancestors VARCHAR(500) DEFAULT '' COMMENT '祖级ID列表',
role_code VARCHAR(100) NOT NULL COMMENT '角色编码',
role_name VARCHAR(100) NOT NULL COMMENT '角色名称',
role_type TINYINT NOT NULL DEFAULT 1 COMMENT '类型 1:系统角色 2:租户角色 3:自定义',
data_scope TINYINT NOT NULL DEFAULT 1 COMMENT '数据范围 1:全部 2:本部门 3:本部门及子部门 4:仅本人 5:自定义',
description VARCHAR(500) DEFAULT NULL,
sort_no INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_role_code (tenant_id, role_code),
INDEX idx_parent (parent_id),
INDEX idx_tenant (tenant_id)
) COMMENT='角色';
-- sys_role_menu — 角色菜单关联(原 rui_system_role_menu 改进)
CREATE TABLE sys_role_menu (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
role_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_role_menu (tenant_id, role_id, menu_id),
INDEX idx_role (role_id),
INDEX idx_menu (menu_id),
INDEX idx_tenant (tenant_id)
) COMMENT='角色菜单关联';
```
### 2.5 数据字典
```sql
-- sys_dict_type — 字典类型(新增,拆分原字典表)
CREATE TABLE sys_dict_type (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
dict_code VARCHAR(100) NOT NULL COMMENT '字典编码(唯一标识)',
dict_name VARCHAR(200) NOT NULL COMMENT '字典名称',
description VARCHAR(500) DEFAULT NULL,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dict_code (tenant_id, dict_code),
INDEX idx_tenant (tenant_id)
) COMMENT='字典类型';
-- sys_dict_item — 字典项(新增,拆分原字典表)
CREATE TABLE sys_dict_item (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
dict_type_id BIGINT NOT NULL COMMENT '字典类型ID',
item_code VARCHAR(100) NOT NULL COMMENT '项编码',
item_label VARCHAR(200) NOT NULL COMMENT '项标签',
item_value VARCHAR(200) NOT NULL COMMENT '项值',
sort_no INT NOT NULL DEFAULT 0,
remark VARCHAR(500) DEFAULT NULL,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_dict_type_code (tenant_id, dict_type_id, item_code),
INDEX idx_dict_type (dict_type_id),
INDEX idx_tenant (tenant_id)
) COMMENT='字典项';
```
### 2.6 系统配置
```sql
-- sys_config — 系统配置(原 rui_system_config 改进)
CREATE TABLE sys_config (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
config_group VARCHAR(100) NOT NULL DEFAULT 'default' COMMENT '配置分组(oss/sms/email',
config_key VARCHAR(200) NOT NULL COMMENT '配置键',
config_value VARCHAR(2000) NOT NULL COMMENT '配置值',
config_type VARCHAR(20) NOT NULL DEFAULT 'STRING' COMMENT '值类型 STRING/JSON/NUMBER/BOOLEAN/ENCRYPTED',
is_system TINYINT NOT NULL DEFAULT 0 COMMENT '是否系统级 0:租户 1:系统',
is_encrypted TINYINT NOT NULL DEFAULT 0 COMMENT '是否加密 0:明文 1:密文',
description VARCHAR(500) DEFAULT NULL,
sort_no INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_config_key (tenant_id, config_key),
INDEX idx_group (config_group),
INDEX idx_tenant (tenant_id)
) COMMENT='系统配置';
```
---
## 三、rui-service-user 用户中心服务(7张表)
### 3.1 用户主体
```sql
-- uc_user — 用户主表(原 rui_user_credential + rui_user_info 合并改进)
CREATE TABLE uc_user (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
username VARCHAR(100) NOT NULL COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)',
user_type TINYINT NOT NULL DEFAULT 1 COMMENT '用户类型 1:普通用户 2:管理员 3:系统用户',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_username (tenant_id, username),
INDEX idx_tenant (tenant_id)
) COMMENT='用户';
-- uc_user_detail — 用户详情表(原 rui_user_info 拆分)
CREATE TABLE uc_user_detail (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL COMMENT '用户ID',
tenant_id BIGINT NOT NULL DEFAULT 0,
nickname VARCHAR(100) DEFAULT NULL COMMENT '昵称',
real_name VARCHAR(100) DEFAULT NULL COMMENT '真实姓名',
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
phone VARCHAR(20) DEFAULT NULL COMMENT '手机号',
avatar VARCHAR(1000) DEFAULT NULL COMMENT '头像URL',
gender TINYINT DEFAULT 0 COMMENT '性别 0:未知 1:男 2:女',
birthday DATE DEFAULT NULL,
id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号',
address VARCHAR(500) DEFAULT NULL COMMENT '地址',
extra JSON DEFAULT NULL COMMENT '扩展字段(JSON格式)',
remark VARCHAR(500) DEFAULT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_user_id (user_id),
INDEX idx_tenant (tenant_id)
) COMMENT='用户详情';
```
### 3.2 用户等级
```sql
-- uc_user_level — 用户等级定义(原 rui_user_level 改进)
CREATE TABLE uc_user_level (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
level_code VARCHAR(100) NOT NULL COMMENT '等级编码',
level_name VARCHAR(100) NOT NULL COMMENT '等级名称',
level_no INT NOT NULL DEFAULT 1 COMMENT '等级序号(数字越大等级越高)',
min_score INT NOT NULL DEFAULT 0 COMMENT '最低积分要求',
icon VARCHAR(500) DEFAULT NULL COMMENT '等级图标',
benefits JSON DEFAULT NULL COMMENT '权益配置(JSON',
upgrade_type TINYINT NOT NULL DEFAULT 1 COMMENT '升级方式 1:自动 2:手动审核',
expire_days INT DEFAULT 0 COMMENT '有效期(天) 0:永久',
status TINYINT NOT NULL DEFAULT 1,
sort_no INT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_level_code (tenant_id, level_code),
INDEX idx_tenant (tenant_id)
) COMMENT='用户等级';
-- uc_user_level_log — 用户等级变更记录(原 rui_user_level_log 改进)
CREATE TABLE uc_user_level_log (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL COMMENT '用户ID',
change_type TINYINT NOT NULL DEFAULT 1 COMMENT '变更类型 1:升级 2:降级 3:手动调整 4:过期',
from_level_id BIGINT DEFAULT NULL COMMENT '原等级ID NULL:初始',
to_level_id BIGINT NOT NULL COMMENT '新等级ID',
score_before INT DEFAULT NULL COMMENT '变更前积分',
score_after INT DEFAULT NULL COMMENT '变更后积分',
reason VARCHAR(500) DEFAULT NULL COMMENT '变更原因',
operator_id BIGINT DEFAULT NULL COMMENT '操作人ID',
operator_type TINYINT NOT NULL DEFAULT 1 COMMENT '操作人类型 1:系统 2:管理员 3:用户',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_user (user_id),
INDEX idx_tenant (tenant_id)
) COMMENT='用户等级变更记录';
```
### 3.3 用户关联
```sql
-- uc_user_role — 用户角色关联(原 rui_user_role 改进)
CREATE TABLE uc_user_role (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_user_role (tenant_id, user_id, role_id),
INDEX idx_user (user_id),
INDEX idx_role (role_id),
INDEX idx_tenant (tenant_id)
) COMMENT='用户角色关联';
-- uc_user_dept — 用户部门关联(新增)
CREATE TABLE uc_user_dept (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL,
dept_id BIGINT NOT NULL,
is_main TINYINT NOT NULL DEFAULT 1 COMMENT '是否主部门 0:否 1:是',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_user_dept (tenant_id, user_id, dept_id),
INDEX idx_user (user_id),
INDEX idx_dept (dept_id),
INDEX idx_tenant (tenant_id)
) COMMENT='用户部门关联';
-- uc_user_post — 用户岗位关联(新增)
CREATE TABLE uc_user_post (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL,
post_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_user_post (tenant_id, user_id, post_id),
INDEX idx_user (user_id),
INDEX idx_post (post_id),
INDEX idx_tenant (tenant_id)
) COMMENT='用户岗位关联';
-- uc_user_permission — 用户权限关联(新增,支持直接授权)
CREATE TABLE uc_user_permission (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT NOT NULL,
permission VARCHAR(200) NOT NULL COMMENT '权限标识 如: user:list',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_user_permission (tenant_id, user_id, permission),
INDEX idx_user (user_id),
INDEX idx_tenant (tenant_id)
) COMMENT='用户权限关联(直接授权)';
```
---
## 四、rui-auth 认证中心(3张表)
### 4.1 OAuth2 客户端
```sql
-- auth_oauth2_client — OAuth2客户端(原 rui_system_oauth2_client 改进)
CREATE TABLE auth_oauth2_client (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
client_id VARCHAR(100) NOT NULL COMMENT '客户端ID',
client_secret VARCHAR(255) DEFAULT NULL COMMENT '客户端密钥',
client_name VARCHAR(200) NOT NULL COMMENT '客户端名称',
client_type TINYINT NOT NULL DEFAULT 1 COMMENT '客户端类型 1:Web 2:App 3:小程序',
logo_url VARCHAR(500) DEFAULT NULL COMMENT 'Logo',
description VARCHAR(500) DEFAULT NULL,
authentication_methods VARCHAR(500) NOT NULL COMMENT '认证方式',
grant_types VARCHAR(500) NOT NULL COMMENT '授权类型',
redirect_uris TEXT DEFAULT NULL COMMENT '回调地址(逗号分隔)',
scopes VARCHAR(500) NOT NULL COMMENT '授权范围',
access_token_ttl INT NOT NULL DEFAULT 7200 COMMENT '访问令牌有效期(秒)',
refresh_token_ttl INT NOT NULL DEFAULT 604800 COMMENT '刷新令牌有效期(秒)',
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_client_id (tenant_id, client_id),
INDEX idx_tenant (tenant_id)
) COMMENT='OAuth2客户端';
```
### 4.2 日志审计
```sql
-- auth_login_log — 登录日志(新增)
CREATE TABLE auth_login_log (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
user_id BIGINT DEFAULT NULL,
username VARCHAR(100) DEFAULT NULL,
login_type TINYINT NOT NULL DEFAULT 1 COMMENT '登录类型 1:密码 2:短信 3:微信 4:支付宝',
client_id VARCHAR(100) DEFAULT NULL COMMENT '客户端ID',
ip VARCHAR(128) DEFAULT NULL,
location VARCHAR(255) DEFAULT NULL,
browser VARCHAR(200) DEFAULT NULL,
os VARCHAR(200) DEFAULT NULL,
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:失败 1:成功',
msg VARCHAR(500) DEFAULT NULL COMMENT '消息',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_user (user_id),
INDEX idx_tenant (tenant_id),
INDEX idx_created (created_at)
) COMMENT='登录日志';
-- auth_oper_log — 操作日志(新增)
CREATE TABLE auth_oper_log (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
oper_type TINYINT NOT NULL DEFAULT 1 COMMENT '操作类型 1:新增 2:修改 3:删除 4:查询 5:导出 6:登录 7:登出',
title VARCHAR(200) NOT NULL COMMENT '操作模块',
method VARCHAR(500) DEFAULT NULL COMMENT '请求方法',
request_url VARCHAR(500) DEFAULT NULL COMMENT '请求URL',
request_method VARCHAR(10) DEFAULT NULL COMMENT '请求方式 GET/POST/PUT/DELETE',
request_params TEXT DEFAULT NULL COMMENT '请求参数',
response_data TEXT DEFAULT NULL COMMENT '响应数据',
user_id BIGINT DEFAULT NULL COMMENT '操作用户ID',
user_name VARCHAR(100) DEFAULT NULL COMMENT '操作用户名',
oper_ip VARCHAR(128) DEFAULT NULL COMMENT '操作IP',
oper_location VARCHAR(255) DEFAULT NULL COMMENT '操作地点',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:失败 1:成功',
error_msg TEXT DEFAULT NULL COMMENT '错误消息',
cost_time BIGINT DEFAULT 0 COMMENT '耗时(ms)',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_user (user_id),
INDEX idx_tenant (tenant_id),
INDEX idx_created (created_at)
) COMMENT='操作日志';
```
---
## 五、rui-gateway 网关服务(2张表)
```sql
-- gw_route — 路由配置(新增)
CREATE TABLE gw_route (
id BIGINT NOT NULL,
route_id VARCHAR(100) NOT NULL COMMENT '路由ID',
route_name VARCHAR(200) NOT NULL COMMENT '路由名称',
uri VARCHAR(500) NOT NULL COMMENT '目标URI',
predicates VARCHAR(1000) NOT NULL COMMENT '断言条件(JSON',
filters VARCHAR(1000) DEFAULT NULL COMMENT '过滤器(JSON',
sort_no INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_route_id (route_id)
) COMMENT='网关路由配置';
-- gw_rate_limit — 限流规则(新增)
CREATE TABLE gw_rate_limit (
id BIGINT NOT NULL,
route_id VARCHAR(100) NOT NULL COMMENT '路由ID',
limit_type TINYINT NOT NULL DEFAULT 1 COMMENT '限流类型 1:IP 2:用户 3:接口',
limit_key VARCHAR(200) NOT NULL COMMENT '限流键',
limit_count INT NOT NULL DEFAULT 100 COMMENT '限流次数',
limit_window INT NOT NULL DEFAULT 60 COMMENT '时间窗口(秒)',
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_route (route_id)
) COMMENT='网关限流规则';
```
---
## 六、公共表(跨服务使用,2张表)
```sql
-- pub_file — 文件记录(新增)
CREATE TABLE pub_file (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
file_name VARCHAR(255) NOT NULL COMMENT '原始文件名',
file_url VARCHAR(1000) NOT NULL COMMENT '文件访问URL',
file_path VARCHAR(1000) NOT NULL COMMENT '文件存储路径',
file_size BIGINT NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
file_type VARCHAR(100) DEFAULT NULL COMMENT '文件类型',
storage_type TINYINT NOT NULL DEFAULT 1 COMMENT '存储类型 1:本地 2:OSS 3:MinIO',
module VARCHAR(100) DEFAULT NULL COMMENT '所属模块',
biz_id BIGINT DEFAULT NULL COMMENT '业务ID',
status TINYINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_tenant (tenant_id),
INDEX idx_module_biz (module, biz_id)
) COMMENT='文件记录';
-- pub_message — 消息记录(新增)
CREATE TABLE pub_message (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0,
msg_type TINYINT NOT NULL DEFAULT 1 COMMENT '消息类型 1:短信 2:邮件 3:站内信 4:App推送',
template_code VARCHAR(100) DEFAULT NULL COMMENT '模板编码',
sender VARCHAR(200) DEFAULT NULL COMMENT '发送方',
receiver VARCHAR(500) NOT NULL COMMENT '接收方',
subject VARCHAR(500) DEFAULT NULL COMMENT '主题',
content TEXT NOT NULL COMMENT '内容',
params JSON DEFAULT NULL COMMENT '模板参数(JSON',
send_status TINYINT NOT NULL DEFAULT 0 COMMENT '发送状态 0:待发送 1:发送中 2:成功 3:失败',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
send_time DATETIME DEFAULT NULL COMMENT '发送时间',
result_msg VARCHAR(500) DEFAULT NULL COMMENT '发送结果',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_tenant (tenant_id),
INDEX idx_status (send_status),
INDEX idx_created (created_at)
) COMMENT='消息记录';
```
---
## 七、表结构变更对照(当前 → 规划)
| 当前表名 | 规划表名 | 归属服务 | 变更类型 |
|---------|---------|---------|---------|
| rui_system_oauth2_client | auth_oauth2_client | rui-auth | 🔴 迁移 + 重构 |
| rui_system_tenant | sys_tenant | rui-service-system | 🟡 重构 |
| rui_user_credential | uc_user | rui-service-user | 🔴 合并拆分 |
| rui_user_info | uc_user_detail | rui-service-user | 🔴 合并拆分 |
| rui_user_level | uc_user_level | rui-service-user | 🟡 重构 |
| rui_user_level_log | uc_user_level_log | rui-service-user | 🟡 重构 |
| rui_user_role | uc_user_role | rui-service-user | 🟡 重构 |
| rui_system_menu | sys_menu | rui-service-system | 🟡 重构 |
| rui_system_role | sys_role | rui-service-system | 🟡 重构 |
| rui_system_role_menu | sys_role_menu | rui-service-system | 🟡 重构 |
| rui_system_dict | sys_dict_type + sys_dict_item | rui-service-system | 🔴 拆分 |
| rui_system_config | sys_config | rui-service-system | 🟡 重构 |
| — | sys_tenant_package | rui-service-system | 🟢 新增 |
| — | sys_dept | rui-service-system | 🟢 新增 |
| — | sys_post | rui-service-system | 🟢 新增 |
| — | uc_user_dept | rui-service-user | 🟢 新增 |
| — | uc_user_post | rui-service-user | 🟢 新增 |
| — | uc_user_permission | rui-service-user | 🟢 新增 |
| — | auth_login_log | rui-auth | 🟢 新增 |
| — | auth_oper_log | rui-auth | 🟢 新增 |
| — | gw_route | rui-gateway | 🟢 新增 |
| — | gw_rate_limit | rui-gateway | 🟢 新增 |
| — | pub_file | 公共 | 🟢 新增 |
| — | pub_message | 公共 | 🟢 新增 |
---
## 八、表前缀规则(新增)
| 前缀 | 归属服务 | 说明 |
|------|---------|------|
| `sys_` | rui-service-system | 系统基础表(租户/部门/岗位/菜单/角色/字典/配置) |
| `uc_` | rui-service-user | 用户中心表(用户/详情/等级/角色/部门/岗位关联) |
| `auth_` | rui-auth | 认证授权表(OAuth2客户端/登录日志/操作日志) |
| `gw_` | rui-gateway | 网关表(路由/限流) |
| `pub_` | 公共 | 跨服务公共表(文件/消息) |
| `biz_` | 业务服务 | 业务应用模块表(如 biz_order, biz_pay |
---
## 九、Entity 设计规则(新增)
### 9.1 必须继承 BaseEntity
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends BaseEntity {
// 业务字段
}
```
### 9.2 @TableName 命名规则
```java
// ✅ 正确:不包含前缀,与全局配置 table-prefix 配合
@TableName(value = "user", keepGlobalPrefix = true)
// 实际表名: rui_user(当前)或 uc_user(建议调整后)
// ❌ 错误:硬编码前缀,或前缀与配置不一致
@TableName(value = "auth_user", keepGlobalPrefix = true)
// 实际表名: rui_auth_user(与 SQL 不一致)
```
### 9.3 主键策略统一
```java
// ✅ 统一使用 ASSIGN_ID(雪花算法)
@TableId(type = IdType.ASSIGN_ID)
private Long id;
// ❌ 不要混用 AUTO
@TableId(type = IdType.AUTO) // 错误!
```
---
> **文档版本**: v1.0
> **创建日期**: 2026-05-28
> **适用范围**: rui-framework 项目数据库设计
> **状态**: 仅规划,未实施
+19 -19
View File
@@ -11,7 +11,7 @@
│ ├── cashier-mobile/
│ └── customer-mobile/
├── spring-ai/ ← 后端框架 AI 工作目录
├── rui-framework/ ← 后端框架 AI 工作目录
│ ├── backend/
│ │ ├── rui-common/
│ │ ├── rui-gateway/
@@ -19,7 +19,7 @@
│ │ └── rui-service/
│ └── docs/
├── spring-ai/app/
├── rui-framework/app/
│ ├── rui-cashier/ ← 收银 AI 工作目录
│ └── rui-payment/ ← 支付 AI 工作目录
@@ -70,12 +70,12 @@ cd ~/rhkj/rui-frontend
### 后端框架 AI
**工作目录:** `~/rhkj/spring-ai`
**工作目录:** `~/rhkj/rui-framework`
**启动步骤:**
```bash
# 1. 进入后端目录
cd ~/rhkj/spring-ai
cd ~/rhkj/rui-framework
# 2. 打开 OpenCode
@@ -84,7 +84,7 @@ cd ~/rhkj/spring-ai
**AI 指令:**
```
你是 Java 后端开发专家。当前项目是 spring-ai(睿核科技后端框架)。
你是 Java 后端开发专家。当前项目是 rui-framework(睿核科技后端框架)。
技术栈:Spring Boot 3.x + Spring Cloud + JDK 21 + Maven + MyBatis Plus
@@ -108,12 +108,12 @@ cd ~/rhkj/spring-ai
### 收银模块 AI
**工作目录:** `~/rhkj/spring-ai/app/rui-cashier`
**工作目录:** `~/rhkj/rui-framework/app/rui-cashier`
**启动步骤:**
```bash
# 1. 进入收银模块目录
cd ~/rhkj/spring-ai/app/rui-cashier
cd ~/rhkj/rui-framework/app/rui-cashier
# 2. 打开 OpenCode
@@ -135,7 +135,7 @@ cd ~/rhkj/spring-ai/app/rui-cashier
约束条件:
- 禁止修改前端代码
- 禁止修改框架代码(在 spring-ai 仓库)
- 禁止修改框架代码(在 rui-framework 仓库)
- 禁止直接引用支付模块代码
- 通过 FeignClient 调用框架服务
- 通过 REST API 暴露接口供前端调用
@@ -148,12 +148,12 @@ cd ~/rhkj/spring-ai/app/rui-cashier
### 支付模块 AI
**工作目录:** `~/rhkj/spring-ai/app/rui-payment`
**工作目录:** `~/rhkj/rui-framework/app/rui-payment`
**启动步骤:**
```bash
# 1. 进入支付模块目录
cd ~/rhkj/spring-ai/app/rui-payment
cd ~/rhkj/rui-framework/app/rui-payment
# 2. 打开 OpenCode
@@ -175,7 +175,7 @@ cd ~/rhkj/spring-ai/app/rui-payment
约束条件:
- 禁止修改前端代码
- 禁止修改框架代码(在 spring-ai 仓库)
- 禁止修改框架代码(在 rui-framework 仓库)
- 禁止直接引用收银模块代码
- 通过 FeignClient 调用框架服务
- 通过 REST API 暴露接口供收银模块调用
@@ -204,7 +204,7 @@ git commit -m "feat: 功能描述
- 详细修改点2"
# 推送到 Gitea
git push gitea main
git push origin main
# 查看推送结果(钉钉会收到通知)
```
@@ -213,7 +213,7 @@ git push gitea main
```bash
# 查看 Actions 运行状态
# 访问:https://git.dev.vifo.cc/rui/{仓库名}/actions
# 访问:https://git.vifo.cc/rui/{仓库名}/actions
```
### 模块间通信示例
@@ -271,9 +271,9 @@ const cashierApi = {
| 文件 | 位置 | 说明 |
|------|------|------|
| AI 边界配置 | `AGENTS.md` | 每个仓库根目录 |
| 通信规范 | `spring-ai/docs/module-communication.md` | 模块间通信规范 |
| 通信规范 | `rui-framework/docs/module-communication.md` | 模块间通信规范 |
| CI 配置 | `.gitea/workflows/*.yml` | 自动化构建 |
| 后端 POM | `spring-ai/backend/pom.xml` | Maven 根配置 |
| 后端 POM | `rui-framework/backend/pom.xml` | Maven 根配置 |
---
@@ -294,22 +294,22 @@ const cashierApi = {
git remote -v
# 确认是 Gitea 地址
gitea ssh://git@git.dev.vifo.cc:222/rui/xxx.git
gitea ssh://git@git.vifo.cc:222/rui/xxx.git
# 如果失败,检查 SSH 密钥
ssh -p 222 git@git.dev.vifo.cc
ssh -p 222 git@git.vifo.cc
```
### CI 构建失败
```bash
# 查看构建日志
# 访问:https://git.dev.vifo.cc/rui/{仓库}/actions
# 访问:https://git.vifo.cc/rui/{仓库}/actions
```
### 钉钉没收到通知
```bash
# 检查 Webhook 配置
# 访问:https://git.dev.vifo.cc/rui/{仓库}/settings/hooks
# 访问:https://git.vifo.cc/rui/{仓库}/settings/hooks
```
---
@@ -0,0 +1,388 @@
# AI 开发环境配置手册
> **适用对象**: 业务模块开发者(支付、收银台等)
> **版本**: v1.0
> **更新日期**: 2026-06-03
---
## 目录
1. [概述](#一概述)
2. [环境准备](#二环境准备)
3. [仓库克隆与配置](#三仓库克隆与配置)
4. [GitNexus 索引配置](#四gitnexus-索引配置)
5. [AI 开发工作流](#五ai-开发工作流)
6. [常见问题](#六常见问题)
---
## 一、概述
### 1.1 背景
项目已拆分为独立仓库:
| 仓库 | 地址 | 用途 | 负责人 |
|------|------|------|--------|
| `rui-framework` | `git@gitee.com:pigeon/rui-framework.git` | 框架(backend | 框架维护者 |
| `rui-payment` | `git@gitee.com:pigeon/rui-payment.git` | 支付模块 | 员工A |
| `rui-cashier` | `git@gitee.com:pigeon/rui-cashier.git` | 收银台模块 | 员工B |
业务开发者**只需要**维护自己的业务仓库,框架依赖通过 Maven 仓库自动下载。
### 1.2 为什么需要本地 clone 框架仓库?
AIOpenCode)基于 GitNexus 知识图谱工作:
- **业务代码索引**:理解业务逻辑、生成业务代码
- **框架代码索引**:理解框架 API(如 `AuthUtil``BizException`
框架代码以 **jar 包** 形式通过 Maven 引入,AI 无法直接解析 jar 中的 class 文件。因此需要在本地 clone 框架仓库作为**只读参考**,供 AI 索引和查询。
---
## 二、环境准备
### 2.1 基础工具
| 工具 | 最低版本 | 验证命令 | 说明 |
|------|---------|---------|------|
| JDK | 21 | `java -version` | 必须 |
| Maven | 3.9 | `mvn -version` | 必须 |
| Git | 2.40 | `git --version` | 必须 |
| Node.js | 18 | `node --version` | GitNexus 需要 |
### 2.2 配置 Maven 仓库
确保 `~/.m2/settings.xml` 已配置公司 Nexus 仓库认证(向运维获取):
```xml
<settings>
<servers>
<server>
<id>releases</id>
<username>your-username</username>
<password>your-password</password>
</server>
<server>
<id>snapshots</id>
<username>your-username</username>
<password>your-password</password>
</server>
</servers>
</settings>
```
> **注意**:框架 `rui-parent` 和 `rui-common-*` 已发布到 Nexus,首次编译时会自动下载。
---
## 三、仓库克隆与配置
### 3.1 目录结构
```
~/work/
├── rui-framework/ # 框架仓库(只读参考)
│ ├── backend/
│ └── .gitnexus/ # 框架代码索引
└── rui-payment/ # 业务仓库(主要工作区)
├── pom.xml
├── rui-payment-api/
└── .gitnexus/ # 业务代码索引
```
### 3.2 克隆仓库
**步骤1:克隆框架仓库(所有员工都需要)**
```bash
git clone git@gitee.com:pigeon/rui-framework.git ~/work/rui-framework
```
**步骤2:克隆业务仓库**
```bash
# 员工A(支付)
git clone git@gitee.com:pigeon/rui-payment.git ~/work/rui-payment
# 员工B(收银台)
git clone git@gitee.com:pigeon/rui-cashier.git ~/work/rui-cashier
```
### 3.3 验证编译
```bash
cd ~/work/rui-payment
mvn clean compile -DskipTests
```
预期输出:`BUILD SUCCESS`
如果报错找不到 `rui-parent`,联系框架维护者确认已发布到 Nexus。
---
## 四、GitNexus 索引配置
### 4.1 什么是 GitNexus
GitNexus 是 AI 的代码智能引擎,通过索引代码库构建知识图谱,帮助 AI:
- 理解代码结构(类、方法、调用关系)
- 分析修改影响(改 A 会波及 B、C)
- 安全导航(不重命名、不遗漏引用)
### 4.2 索引框架仓库
```bash
cd ~/work/rui-framework
npx gitnexus analyze
```
预期输出:
```
Repository indexed successfully
7,504 nodes | 15,350 edges | 268 clusters | 300 flows
```
### 4.3 索引业务仓库
```bash
cd ~/work/rui-payment
npx gitnexus analyze
```
预期输出:
```
Repository indexed successfully
1,601 nodes | 3,921 edges | 63 clusters | 131 flows
```
### 4.4 验证索引
```bash
npx gitnexus list
```
预期输出:
```
Indexed Repositories (2)
rui-framework
Path: ~/work/rui-framework
Stats: 7504 symbols, 15350 edges
rui-payment
Path: ~/work/rui-payment
Stats: 1601 symbols, 3921 edges
```
### 4.5 索引维护
| 场景 | 操作 |
|------|------|
| 业务代码修改后 | `cd ~/work/rui-payment && npx gitnexus analyze` |
| 框架升级后 | `cd ~/work/rui-framework && git pull && npx gitnexus analyze` |
| 索引损坏 | `npx gitnexus clean && npx gitnexus analyze` |
| 检查状态 | `npx gitnexus status` |
---
## 五、AI 开发工作流
### 5.1 启动 AI
在**业务仓库**目录下启动 IDE/OpenCode
```bash
cd ~/work/rui-payment
# 启动 IDE 或 OpenCode
```
AI 默认使用当前目录的索引(`rui-payment`)。
### 5.2 开发业务代码(默认模式)
AI 自动基于 `rui-payment` 索引工作:
```
用户:帮我写一个订单查询接口
AI:基于 rui-payment 索引分析...
生成 OrderController、OrderService、OrderMapper
```
### 5.3 查询框架 API(跨仓库模式)
当需要理解框架源码时,在对话中指定 `repo` 参数:
**查询类定义:**
```
gitnexus_context({
name: "AuthUtil",
repo: "rui-framework"
})
```
**搜索功能实现:**
```
gitnexus_query({
query: "分布式锁 Redisson",
repo: "rui-framework"
})
```
**分析修改影响:**
```
gitnexus_impact({
target: "BaseController",
repo: "rui-framework",
direction: "upstream"
})
```
**查看执行流程:**
```
gitnexus_query({
query: "用户登录认证流程",
repo: "rui-framework"
})
```
### 5.4 常用查询对照表
| 我想查... | 命令 | 仓库 |
|-----------|------|------|
| `AuthUtil.getUserId()` 怎么用 | `gitnexus_context({name:"AuthUtil", repo:"rui-framework"})` | framework |
| 业务异常怎么抛 | `gitnexus_query({query:"BizException", repo:"rui-framework"})` | framework |
| 分布式锁怎么加 | `gitnexus_query({query:"Redisson分布式锁", repo:"rui-framework"})` | framework |
| 支付订单状态流转 | `gitnexus_query({query:"订单状态", repo:"rui-payment"})` | payment |
| 修改订单会影响哪里 | `gitnexus_impact({target:"OrderService", direction:"upstream"})` | payment |
| 收银台缓存策略 | `gitnexus_query({query:"缓存 CacheKeys", repo:"rui-cashier"})` | cashier |
### 5.5 工作流示例
**场景:开发一个支付退款接口**
```
1. 员工A在 ~/work/rui-payment 中启动 AI
2. 员工:帮我写退款接口
AI基于 rui-payment 索引分析业务代码...
3. 员工:退款时需要校验用户权限吗?
AI:建议调用 AuthUtil.getUserId()...
4. 员工:AuthUtil 有哪些方法?
【AI执行】gitnexus_context({name:"AuthUtil", repo:"rui-framework"})
AIAuthUtil 提供 getUserId()、getTenantId()、getUser()...
5. 员工:退款金额用 BigDecimal 吗?
【AI执行】gitnexus_query({query:"金额计算 BigDecimal", repo:"rui-framework"})
AI:框架规范要求金额使用 DECIMAL(19,4)Java 对应 BigDecimal...
6. AI 生成完整代码
```
---
## 六、常见问题
### Q1AI 提示 "Index is stale"
**原因**:代码已修改但索引未更新。
**解决**
```bash
cd ~/work/你的仓库
npx gitnexus analyze
```
### Q2:查询框架代码时提示 "Repo not found"
**原因**:框架仓库未索引或索引名称不对。
**解决**
```bash
# 查看所有索引
npx gitnexus list
# 确认 rui-framework 存在
# 如果不存在,重新索引框架仓库
cd ~/work/rui-framework
npx gitnexus analyze
```
### Q3:编译时找不到 rui-parent
**原因**:框架未发布到 Nexus,或 Maven 仓库配置错误。
**解决**
1. 确认 `~/.m2/settings.xml` 已配置 Nexus 认证
2. 联系框架维护者确认已执行 `mvn clean deploy`
3. 临时方案:本地 clone framework 后执行 `mvn clean install -DskipTests`
### Q4:框架升级后 AI 给出的 API 已过时
**原因**:框架索引未更新。
**解决**
```bash
cd ~/work/rui-framework
git pull origin main
npx gitnexus analyze
```
### Q5:发现框架代码有 Bug
**禁止**:直接修改 `~/work/rui-framework` 代码。
**正确做法**
1. 在业务仓库记录:`docs/框架问题记录.md`
2. 通知框架维护者
3. 等待框架修复并发布新版本
### Q6:AI 回答中引用了不存在的框架类
**原因**:业务仓库依赖的框架版本与本地索引的框架版本不一致。
**解决**
1. 检查 `pom.xml` 中 parent version(如 `1.0.0`
2. 确认 `~/work/rui-framework` 的代码版本一致
3. 如果不一致,更新 framework clone 并重新索引
---
## 附录
### A. 各仓库索引名称
| 仓库路径 | 索引名称 | 说明 |
|----------|----------|------|
| `~/work/rui-framework` | `rui-framework` | 基于 pom.xml artifactId |
| `~/work/rui-payment` | `rui-payment` | 基于目录名 |
| `~/work/rui-cashier` | `rui-cashier` | 基于目录名 |
### B. 快速命令速查
```bash
# 克隆所有仓库(新员工一次性执行)
git clone git@gitee.com:pigeon/rui-framework.git ~/work/rui-framework
git clone git@gitee.com:pigeon/rui-payment.git ~/work/rui-payment
# 初始化索引(一次性执行)
cd ~/work/rui-framework && npx gitnexus analyze
cd ~/work/rui-payment && npx gitnexus analyze
# 日常开发
mvn clean install -DskipTests
# 更新索引
npx gitnexus analyze
```
### C. 相关文档
| 文档 | 路径 |
|------|------|
| GitNexus 使用指南 | `docs/gitnexus-guide.md` |
| 环境搭建指南 | `docs/environment-setup.md` |
| 项目规范 | `AGENTS.md` |
---
> **提示**:本手册随项目演进持续更新。如有问题,联系框架维护者或技术负责人。
+63
View File
@@ -0,0 +1,63 @@
# BaseEntity 字段说明
> **适用范围**:所有业务实体必须继承 `BaseEntity`,包含框架自动维护的标准字段。
## 类定义
```java
package com.rui.common.mybatis.model;
public class BaseEntity implements Serializable {
// ...
}
```
## 字段列表
| 字段名 | 类型 | 说明 | 填充时机 | 数据库字段 |
|--------|------|------|----------|------------|
| `id` | `Long` | 主键ID,使用雪花算法生成 | 插入时自动填充 | `id` |
| `tenantId` | `Long` | 租户ID`0` 表示系统级 | 插入时自动填充 | `tenant_id` |
| `deleted` | `Integer` | 逻辑删除标志,`0` 正常,`1` 已删除 | 插入时自动填充 | `deleted` |
| `createdBy` | `Long` | 创建者ID | 插入时自动填充 | `created_by` |
| `createdAt` | `LocalDateTime` | 创建时间 | 插入时自动填充 | `created_at` |
| `updatedBy` | `Long` | 更新者ID | 插入/更新时自动填充 | `updated_by` |
| `updatedAt` | `LocalDateTime` | 更新时间 | 插入/更新时自动填充 | `updated_at` |
## 注解说明
- `@TableId(type = IdType.ASSIGN_ID)` — 使用雪花算法分配 ID
- `@TableLogic` — 逻辑删除字段,MyBatis Plus 自动处理删除逻辑
- `@TableField(fill = FieldFill.INSERT)` — 插入时自动填充
- `@TableField(fill = FieldFill.INSERT_UPDATE)` — 插入/更新时自动填充
## 使用示例
```java
@Data
public class User extends BaseEntity {
private String username;
private String email;
// 无需定义 id、createdAt 等公共字段
}
```
## 数据库建表示例
```sql
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY COMMENT '主键ID(雪花算法)',
username VARCHAR(100) NOT NULL COMMENT '用户名',
email VARCHAR(100) COMMENT '邮箱',
-- 公共字段(由 BaseEntity + MyBatis Plus 自动维护)
tenant_id BIGINT DEFAULT 0 COMMENT '租户ID',
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除(0-正常,1-已删)',
created_by BIGINT COMMENT '创建者ID',
created_at DATETIME COMMENT '创建时间',
updated_by BIGINT COMMENT '更新者ID',
updated_at DATETIME COMMENT '更新时间'
) COMMENT='用户表';
```
> **注意**:所有业务表必须包含以上公共字段。
+120
View File
@@ -0,0 +1,120 @@
# Nacos 配置管理规范
## 1. 配置分类
| 配置类型 | 文件位置 | 是否推送 Nacos | 说明 |
|---------|---------|--------------|------|
| **本地开发配置** | `config/application-dev.yml` | ❌ 不推送 | 本地环境专属,已加入 `.gitignore` |
| **Nacos 配置文件** | `docs/nacos/*.yaml` | ✅ **必须推送** | 所有服务的 Nacos 配置源文件 |
| **应用模板** | `docs/application-template.yml` | ❌ 不推送 | 新建模块的模板,不直接推送 |
## 2. 核心规则
### 规则 1:修改必须推送
**除 `config/application-dev.yml` 外,任何对 `docs/nacos/*.yaml` 的修改必须推送至 Nacos 服务器。**
> ⚠️ **禁止行为**:只修改本地文件不推送。这会导致:
> - 本地测试正常,线上环境异常
> - 多人协作时配置不同步
> - 生产环境使用旧配置,引发故障
### 规则 2:统一推送脚本
使用根目录 `push-nacos-config.sh` 统一推送:
```bash
# 推送所有配置(推荐,确保所有配置同步)
bash push-nacos-config.sh
# 只推送单个配置(快速修复时)
bash push-nacos-config.sh rui-common.yaml
```
### 规则 3:推送前检查清单
推送前请确认:
- [ ] 配置文件语法正确(YAML 格式)
- [ ] 敏感信息使用 `${}` 环境变量注入,不硬编码
- [ ] 端口配置与 `项目实施规范.md` 一致
- [ ] 修改内容已 git commit
### 规则 4:推送后验证
推送后必须验证配置是否生效:
```bash
# 1. 查看 Nacos 控制台确认配置已更新
# 2. 重启对应服务使配置生效
# 3. 检查服务日志确认配置加载成功
```
## 3. 配置文件说明
### 3.1 公共配置(rui-common.yaml
| 配置项 | 说明 | 示例 |
|--------|------|------|
| `spring.data.redis` | Redis 连接配置 | host、port、password |
| `spring.jackson` | JSON 序列化配置 | date-format、time-zone |
| `security.oauth2.ignore-urls` | 免认证 URL 白名单 | `/entry/**``/actuator/**` |
| `feign.providers` | Feign 服务名映射 | user、system、auth |
### 3.2 数据配置(rui-data.yaml
| 配置项 | 说明 |
|--------|------|
| `spring.datasource` | MySQL + Druid 连接池配置 |
| `mybatis-plus` | MyBatis Plus 全局配置 |
### 3.3 网关配置(rui-gateway.yaml
| 配置项 | 说明 |
|--------|------|
| `spring.cloud.gateway.routes` | 路由规则 |
| `grayscale` | 灰度发布规则 |
### 3.4 服务专属配置(rui-service-*.yaml
| 配置项 | 说明 |
|--------|------|
| `server.port` | 服务端口 |
## 4. 命名空间与分组
| 环境 | 命名空间 | Group |
|------|---------|-------|
| 开发环境 | `rui-dev` | `DEFAULT_GROUP` |
| 测试环境 | `rui-test` | `DEFAULT_GROUP` |
| 生产环境 | `rui-prod` | `DEFAULT_GROUP` |
> 推送脚本默认使用 `rui-dev`,生产环境推送需手动指定命名空间。
## 5. 常见问题
### Q1: 为什么修改了本地配置但服务没变化?
> 因为 Spring Cloud 应用启动时会从 Nacos 拉取配置,**本地修改不影响运行中的服务**。必须推送至 Nacos 后重启服务才能生效。
### Q2: 可以同时修改多个配置吗?
> 可以,但建议每次只修改一个配置文件,避免推送时出错难以排查。
### Q3: 推送失败怎么办?
> 检查以下几点:
> 1. Nacos 服务器是否可访问(`http://192.168.31.210:8848`
> 2. 用户名密码是否正确(默认 nacos/nacos
> 3. 命名空间是否存在(rui-dev)
> 4. YAML 格式是否正确(缩进、特殊字符等)
### Q4: 如何回滚配置?
> Nacos 控制台支持配置历史版本回滚,或手动将旧配置内容重新推送。
## 6. 最佳实践
1. **先修改本地文件****测试验证****git commit****推送 Nacos****重启服务**
2. 多人协作时,推送前先看一眼 Nacos 控制台的当前配置,避免覆盖他人修改
3. 生产环境配置修改建议先修改测试环境验证,再同步到生产
4. 定期备份 Nacos 配置(导出为文件存档)
@@ -0,0 +1,413 @@
# Resilience4j ThreadPoolBulkhead 租户上下文跨线程传播问题排查指南
> **适用场景**Spring Cloud + OpenFeign + Resilience4jThreadPoolBulkhead+ TransmittableThreadLocalTTL
---
## 1. 问题现象
### 1.1 典型日志特征
```
# HTTP 线程正确设置了租户上下文
[nio-9301-exec-3] GlobalContextFilter : 租户上下文已设置: tenantId=5
# Feign 调用线程(线程池线程)却读取到错误的租户 ID
[pool-5-thread-1] OAuthRequestInterceptor : Feign 透传租户 ID: tenantId=4
# 后续请求无论 X-Tenant-Id 是多少,线程池线程始终返回第一次的 tenantId=4
[nio-9301-exec-4] GlobalContextFilter : 租户上下文已设置: tenantId=51
[pool-5-thread-1] OAuthRequestInterceptor : Feign 透传租户 ID: tenantId=4 ← 仍然是 4
```
### 1.2 核心特征
| 现象 | 说明 |
|------|------|
| HTTP 线程上下文正确 | `TenantContextHolder.getTenantId()` 在 Controller/Filter 中返回正确值 |
| 线程池线程上下文错误 | Feign 拦截器或 Service 中读取到旧值或 `null` |
| 旧值具有"粘性" | 线程池线程复用后,始终残留第一次被创建时的上下文值 |
| 与请求头不一致 | 请求头 `X-Tenant-Id` 变化,但业务线程读取的值不变 |
---
## 2. 问题根因
### 2.1 架构背景
本项目使用以下技术栈:
- **租户上下文**`TenantContextHolder` 基于 `TransmittableThreadLocal`TTL)实现
- **服务间调用**OpenFeign + Spring Cloud LoadBalancer
- **熔断隔离**Spring Cloud Circuit Breaker + Resilience4j `ThreadPoolBulkhead`
- **线程池隔离目的**:限制并发数,防止故障扩散
### 2.2 线程切换链路(关键!)
当 Feign 调用触发 Circuit Breaker + ThreadPoolBulkhead 时,一次请求会经历 **三层线程**
```
┌─────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
│ HTTP 线程 │ │ ThreadPoolBulkhead 线程 │ │ CircuitBreakerFactory │
│ [nio-9301-exec] │ ──▶ │ [bulkhead-xxx-thread] │ ──▶ │ ExecutorService 线程 │
│ │ │ │ │ [pool-N-thread-M] │
└─────────────────┘ └─────────────────────────┘ └─────────────────────────┘
│ │ │
│ ① TTL 自动透传 │ ② ContextPropagator 恢复 │ ③ ??? 上下文丢失
│ (原生 ThreadLocal │ Resilience4j 官方机制) │
│ 不跨线程池) │ │
│ │ │
tenantId=5 tenantId=5 ✓ tenantId=4 ✗
```
### 2.3 为什么 ContextPropagator 不够?
Resilience4j 提供了 `ContextPropagator` 接口,官方设计目的是在 **ThreadPoolBulkhead 线程池** 内透传上下文:
- `retrieve()`:在调用方线程捕获上下文值
- `copy()`:在线程池线程恢复上下文值
- `clear()`:在线程池线程清理上下文值
**但 Spring Cloud Circuit Breaker 内部还有一层线程池!**
查看 `Resilience4JCircuitBreaker.run()` 源码:
```java
if (executorService != null) {
// ① 先把任务提交到工厂自己的 ExecutorServicenewCachedThreadPool
Supplier<Future<T>> futureSupplier = () -> executorService.submit(toRun::get);
// ② 再用 ThreadPoolBulkhead 包装 Future 等待逻辑
Callable<T> bulkheadCall = bulkheadProvider.decorateCallable(..., timeLimitedCall);
...
}
```
**执行流程变成了:**
1. HTTP 线程提交任务 → `ThreadPoolBulkhead` 线程池
2. `ThreadPoolBulkhead` 线程执行 `ContextPropagator.copy()` → 恢复 `tenantId=5`
3. `ThreadPoolBulkhead` 线程调用 `executorService.submit(toRun::get)` → 提交到 **第二个线程池**
4. `executorService` 线程(`pool-5-thread-1`)执行 Feign 调用
5. `executorService` 线程 **没有** 经过 `ContextPropagator` 恢复,其 `ThreadLocal``null`
6. 但由于 `TransmittableThreadLocal` 继承 `InheritableThreadLocal`,线程创建时可能继承了父线程的值,且 **永不清理**,导致旧值残留
### 2.4 为什么旧值有"粘性"
`Resilience4JCircuitBreakerFactory` 默认使用 `Executors.newCachedThreadPool()`
- 线程创建时继承父线程(`ThreadPoolBulkhead` 线程)的 `InheritableThreadLocal`
- 线程被缓存复用,永不销毁(空闲 60 秒)
- **没有人清理** `executorService` 线程的 `ThreadLocal`
- 因此该线程永远携带第一次被创建时的 `tenantId`
---
## 3. 排查思路(按优先级)
### Step 1:确认问题范围
检查日志中 Feign 调用所在的线程名:
```
# 如果是 ThreadPoolBulkhead 线程,命名类似:
[bulkhead-xxx-1]
# 如果是 Spring 默认线程池,命名类似:
[pool-5-thread-1]
```
如果看到 `[pool-N-thread-M]`,说明问题在 **第二层线程切换**
### Step 2:确认 ContextPropagator 是否生效
`TenantContextPropagator``retrieve()` / `copy()` / `clear()` 方法中加日志:
```java
@Override
public Supplier<Optional<TenantContextSnapshot>> retrieve() {
return () -> {
Long tenantId = TenantContextHolder.getTenantId();
log.info("[ContextPropagator] retrieve: tenantId={} in thread={}",
tenantId, Thread.currentThread().getName());
...
};
}
```
- 如果 `retrieve()` 日志不打印 → `ContextPropagator` 未被注册到 ThreadPoolBulkheadConfig
- 如果 `copy()` 打印的线程名是 `[pool-N-thread-M]``ContextPropagator` 被用在了错误的线程池上(本不应出现)
### Step 3:确认是否存在多层线程池
`OAuthRequestInterceptor` 中加日志:
```java
Long tenantId = TenantContextHolder.getTenantId();
log.info("[Feign] 当前线程={}, tenantId={}", Thread.currentThread().getName(), tenantId);
```
对比 HTTP 线程的 `tenantId` 和 Feign 线程的 `tenantId`
| HTTP 线程 | Feign 线程 | 结论 |
|-----------|-----------|------|
| 5 | 5 | 正常 |
| 5 | null | 上下文完全丢失 |
| 5 | 4 | 旧值残留(本文档描述的问题) |
| 5 | 51(上一次的值)| 线程复用且未清理 |
### Step 4:检查 ThreadPoolBulkheadConfig 配置
断点或日志打印 `ThreadPoolBulkheadConfig.getContextPropagator()`
```java
ThreadPoolBulkheadConfig config = threadPoolBulkheadRegistry.getDefaultConfig();
log.info("配置中的 ContextPropagator: {}", config.getContextPropagator());
```
- 如果为空列表 → 配置未生效
- 如果有 `TenantContextPropagator` → 配置已生效,但只能解决第一层线程切换
---
## 4. 完整修复方案
### 4.1 方案概览
需要 **三个层面的修复** 协同工作:
| 层面 | 修复目标 | 组件 |
|------|---------|------|
| 第一层 | HTTP 线程 → ThreadPoolBulkhead 线程 | `TenantContextPropagator` + `TenantContextThreadPoolBulkheadConfigCustomizer` |
| 第二层 | ThreadPoolBulkhead 线程 → ExecutorService 线程 | `TtlExecutors` + `TtlResilience4JCircuitBreakerFactoryCustomizer` |
| 注册层 | Feign 客户端被 Spring 正确扫描 | `META-INF/spring.factories` 注册 Feign 接口 |
### 4.2 第一层:ThreadPoolBulkhead 内上下文传播
**组件**`TenantContextPropagator`(已存在)+ `TenantContextThreadPoolBulkheadConfigCustomizer`(新增 BeanPostProcessor
**原理**
- Resilience4j `ThreadPoolBulkhead` 内部使用 `ContextPropagator.decorateSupplier()` 包装任务
- 在任务提交时 `retrieve()` 捕获上下文,在线程池线程执行前 `copy()` 恢复,执行后 `clear()` 清理
**实现要点**
- `TenantContextThreadPoolBulkheadConfigCustomizer` 实现 `BeanPostProcessor`
-`ThreadPoolBulkheadRegistry` 初始化后,通过反射修改其默认配置,注入 `TenantContextPropagator`
- 不能使用 `AbstractRegistry.addConfiguration("default", config)`,因为该方法禁止修改 `"default"`
- 必须直接修改内部的 `configurations` Map
```java
public class TenantContextThreadPoolBulkheadConfigCustomizer implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof ThreadPoolBulkheadRegistry registry) {
injectContextPropagator(registry);
}
return bean;
}
@SuppressWarnings("unchecked")
private void injectContextPropagator(ThreadPoolBulkheadRegistry registry) {
try {
Field configurationsField = AbstractRegistry.class.getDeclaredField("configurations");
configurationsField.setAccessible(true);
Map<String, ThreadPoolBulkheadConfig> configurations =
(Map<String, ThreadPoolBulkheadConfig>) configurationsField.get(registry);
ThreadPoolBulkheadConfig defaultConfig = configurations.get("default");
if (defaultConfig == null || hasTenantPropagator(defaultConfig)) {
return;
}
ThreadPoolBulkheadConfig newDefaultConfig = ThreadPoolBulkheadConfig.from(defaultConfig)
.contextPropagator(TenantContextPropagator.class)
.build();
configurations.put("default", newDefaultConfig);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("无法修改 ThreadPoolBulkheadRegistry 的默认配置", e);
}
}
}
```
**注册为 Spring Bean**(注意用 `static` 方法避免 BeanPostProcessor 警告):
```java
@Bean
@ConditionalOnMissingBean
public static TenantContextThreadPoolBulkheadConfigCustomizer tenantContextThreadPoolBulkheadConfigCustomizer() {
return new TenantContextThreadPoolBulkheadConfigCustomizer();
}
```
### 4.3 第二层:CircuitBreakerFactory ExecutorService 上下文传播
**组件**`TtlResilience4JCircuitBreakerFactoryCustomizer`(新增 Customizer
**原理**
- 使用 Alibaba `TtlExecutors` 包装 `ExecutorService`
- `TtlExecutors``submit()` 时自动捕获当前线程的 `TransmittableThreadLocal`
- 在目标线程执行前自动恢复,执行后自动清理
- 支持线程池复用场景,无旧值残留
**实现**
```java
public class TtlResilience4JCircuitBreakerFactoryCustomizer
implements Customizer<Resilience4JCircuitBreakerFactory> {
@Override
public void customize(Resilience4JCircuitBreakerFactory factory) {
factory.configureExecutorService(
TtlExecutors.getTtlExecutorService(Executors.newCachedThreadPool())
);
factory.configureGroupExecutorService(
group -> TtlExecutors.getTtlExecutorService(Executors.newCachedThreadPool())
);
}
}
```
**注册为 Spring Bean**
```java
@Bean
@ConditionalOnMissingBean
public static TtlResilience4JCircuitBreakerFactoryCustomizer ttlResilience4JCircuitBreakerFactoryCustomizer() {
return new TtlResilience4JCircuitBreakerFactoryCustomizer();
}
```
### 4.4 第三层:Feign 客户端注册
**组件**`META-INF/spring.factories`
**原理**
- 本项目使用自定义的 `CustomFeignClientsRegistrar` 注册 Feign 客户端
- `CustomFeignClientsRegistrar``SpringFactoriesLoader.loadFactoryNames(CloudFeignAutoConfiguration.class, classLoader)` 加载 Feign 接口类名
- 如果 Feign 接口未在 `spring.factories` 中注册,Spring 容器中不会出现该 Bean,导致 `NoSuchBeanDefinitionException`
**实现**
在定义 Feign 接口的模块(如 `rui-common-security`)新增 `META-INF/spring.factories`
```properties
com.rui.common.feign.CloudFeignAutoConfiguration=\
com.rui.common.security.feign.TokenManageFeign
```
### 4.5 模块依赖关系
确保 `rui-common-security` 添加 `rui-common-feign` 依赖:
```xml
<dependency>
<groupId>com.rui</groupId>
<artifactId>rui-common-feign</artifactId>
</dependency>
```
否则 `spring.factories` 中引用的 `CloudFeignAutoConfiguration` 类在编译期不可见。
---
## 5. 验证方法
### 5.1 本地验证
1. 启动 `rui-auth``rui-service-system`
2. 发送第一次请求:`X-Tenant-Id: 5`
3. 观察 Feign 调用日志,确认 `tenantId=5`
4. 发送第二次请求:`X-Tenant-Id: 51`
5. 观察 Feign 调用日志,确认 `tenantId=51`(不是 5
6. 发送第三次请求:`X-Tenant-Id: 3`
7. 观察 Feign 调用日志,确认 `tenantId=3`(不是 5 也不是 51
### 5.2 关键日志断言
```java
// 断言:HTTP 线程和 Feign 线程的 tenantId 必须一致
assertEquals("HTTP 线程和 Feign 线程的租户 ID 必须一致",
httpTenantId, feignTenantId);
// 断言:每次请求的 tenantId 必须不同(如果请求头不同)
assertNotEquals("线程池线程不应残留旧租户 ID",
previousTenantId, currentTenantId);
```
### 5.3 断点验证
在以下位置打断点,单步跟踪:
1. `TenantContextPropagator.retrieve()` — 确认每次请求捕获的值不同
2. `TenantContextPropagator.copy()` — 确认在线程池线程恢复的值正确
3. `TtlExecutors` 内部 — 确认 `executorService` 线程恢复的值正确
4. `OAuthRequestInterceptor` — 确认最终 Feign 调用时的值正确
---
## 6. 常见问题 FAQ
### Q1:为什么 YAML 中配置 `resilience4j.thread-pool-bulkhead.configs.default.contextPropagators` 不生效?
**A**:该配置依赖 Spring Boot `ConfigurationProperties` 绑定 `Class[]` 类型。虽然 `CommonThreadPoolBulkheadConfigurationProperties` 支持该属性,但:
1. Spring Cloud Circuit Breaker 动态创建的 Bulkhead 实例名不确定
2. `CompositeCustomizer` 按实例名精确匹配,不存在通配符机制
3. 因此编程式注入(`BeanPostProcessor`)更可靠
### Q2:只用 `TtlExecutors` 不用 `ContextPropagator` 可以吗?
**A**:不可以。`TtlExecutors` 只能透传标准 `ThreadPoolExecutor` 的任务提交。Resilience4j 的 `ThreadPoolBulkhead` 内部使用自己的 `ThreadPoolExecutor`,不经过 `TtlExecutors`。因此第一层切换(HTTP → ThreadPoolBulkhead)必须由 `ContextPropagator` 处理。
### Q3:为什么 `TenantContextHolder` 使用 `TransmittableThreadLocal` 而不是普通 `ThreadLocal`
**A**:因为项目中存在 `@Async` 异步任务、Feign 线程池切换等场景。`TransmittableThreadLocal` 配合 `TtlExecutors` 可以在线程池间自动透传上下文,而普通 `ThreadLocal` 只能在线程父子间继承(且对线程池无效)。
### Q4:如果以后引入其他线程池(如 `@Async`),是否也会遇到同样问题?
**A**:是的。任何使用线程池的地方,如果任务提交方线程有 `ThreadLocal` 上下文,而执行方线程需要读取该上下文,都必须使用以下方案之一:
-`TtlExecutors` 包装线程池(推荐)
- 手动在任务提交前捕获上下文,在任务执行前恢复(类似 `ContextPropagator` 原理)
- 使用 Project Reactor 的 `Context` + `Hooks.onEachOperator`(响应式场景)
**最佳实践**:所有业务线程池统一通过 `TtlExecutors` 包装。
### Q5:如果关闭 ThreadPoolBulkhead,改用 SemaphoreBulkhead,能否避免此问题?
**A**:可以。SemaphoreBulkhead 在同一线程内执行,不存在线程切换。但会牺牲线程隔离的故障保护能力。
配置方式:
```yaml
spring:
cloud:
circuitbreaker:
resilience4j:
enableSemaphoreDefaultBulkhead: true
```
---
## 7. 后续维护建议
1. **新增 Feign 客户端时**:务必在所在模块的 `META-INF/spring.factories` 中注册
2. **新增线程池时**:优先使用 `TtlExecutors.getTtlExecutorService()` 包装
3. **新增 ThreadLocal 上下文时**:考虑是否需要配套 `ContextPropagator`
4. **日志规范**:在上下文切换关键点(Filter、Interceptor、线程池任务)打印 `tenantId` + `threadName`,便于快速定位问题
5. **自动化测试**:编写并发测试,模拟多租户同时请求,断言各线程的 `tenantId` 与请求头一致
---
## 8. 相关代码文件
| 文件 | 作用 |
|------|------|
| `rui-common-core/holder/TenantContextHolder.java` | 租户上下文持有者(TransmittableThreadLocal |
| `rui-common-security/feign/TokenManageFeign.java` | Feign 客户端示例 |
| `rui-common-security/feign/OAuthRequestInterceptor.java` | Feign 请求拦截器(透传租户 ID) |
| `rui-common-feign/propagator/TenantContextPropagator.java` | Resilience4j ContextPropagator 实现 |
| `rui-common-feign/config/TenantContextThreadPoolBulkheadConfigCustomizer.java` | 注入 ContextPropagator 到 ThreadPoolBulkheadRegistry |
| `rui-common-feign/config/TtlResilience4JCircuitBreakerFactoryCustomizer.java` | 用 TtlExecutors 包装 CircuitBreakerFactory 线程池 |
| `rui-common-feign/CloudFeignAutoConfiguration.java` | 注册上述 Bean |
| `rui-common-security/META-INF/spring.factories` | 注册 Feign 客户端 |
+1 -1
View File
@@ -40,7 +40,7 @@
```bash
# 进入后端目录
cd ~/rhkj/spring-ai/backend
cd ~/rhkj/rui-framework/backend
# Maven 打包
mvn clean package -DskipTests
+181
View File
@@ -0,0 +1,181 @@
# 环境搭建指南
> **适用范围**: 新加入的开发者
> **预计耗时**: 30-60 分钟
---
## 一、必要工具安装
### 1.1 JDK 21
```bash
# macOS (使用 Homebrew)
brew install openjdk@21
# 验证
java -version
# Expected: openjdk version "21"
```
### 1.2 Maven 3.9+
```bash
# macOS
brew install maven
# 验证
mvn -version
# Expected: Apache Maven 3.9.x
```
### 1.3 MySQL 8.0
```bash
# macOS
brew install mysql@8.0
brew services start mysql@8.0
# 验证
mysql --version
# Expected: mysql Ver 8.0.x
```
### 1.4 Node.js 18+ 和 pnpm
```bash
# macOS
brew install node@18
npm install -g pnpm
# 验证
node --version
pnpm --version
```
### 1.5 Git
```bash
# macOS
brew install git
# 验证
git --version
```
---
## 二、项目初始化
### 2.1 克隆项目
```bash
git clone <repository-url>
cd rui-framework
```
### 2.2 配置本地开发环境
```bash
# 创建本地配置文件
cp backend/config/application-dev.yml.example backend/config/application-dev.yml
# 编辑配置(使用你的数据库连接信息)
# vim backend/config/application-dev.yml
```
配置示例:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/rui_platform?useUnicode=true&characterEncoding=utf8
username: root
password: your_password
```
### 2.3 初始化数据库
```bash
# 创建数据库
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS rui_platform CHARACTER SET utf8mb4;"
# 执行初始化脚本
mysql -u root -p rui_platform < sql/init-database.sql
```
### 2.4 编译项目
```bash
cd backend
mvn clean install -DskipTests
```
Expected: `BUILD SUCCESS`
---
## 三、IDE 配置
### 3.1 IntelliJ IDEA
1. 打开项目(选择 backend/pom.xml
2. 启用 Annotation Processing
- Settings → Build → Annotation Processors
- 勾选 "Enable annotation processing"
3. 配置代码风格:
- Settings → Editor → Code Style → Java
- Import Scheme → Project
### 3.2 VS Code(前端)
1. 安装推荐插件:
- ESLint
- Prettier
- Vue Language Features
2. 配置自动格式化:
```json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
```
---
## 四、验证清单
完成以上步骤后,请确认:
- [ ] `java -version` 显示 JDK 21
- [ ] `mvn -version` 显示 Maven 3.9+
- [ ] `mysql --version` 显示 MySQL 8.0+
- [ ] `backend/mvn clean install -DskipTests` 执行成功
- [ ] 数据库 `rui_platform` 已创建
- [ ] IntelliJ IDEA 已配置 Annotation Processing
---
## 五、常见问题
### Q1: Maven 编译失败
**可能原因**: JDK 版本不对
**解决**: 确认 `JAVA_HOME` 指向 JDK 21
```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
```
### Q2: 数据库连接失败
**可能原因**: MySQL 未启动或配置错误
**解决**:
```bash
brew services start mysql
# 检查 application-dev.yml 中的连接信息
```
### Q3: Lombok 注解不生效
**可能原因**: Annotation Processing 未启用
**解决**: 按 3.1 节启用
+52
View File
@@ -0,0 +1,52 @@
# GitNexus — Code Intelligence 使用指南
> **项目索引**: rui-framework (2690 symbols, 5387 relationships, 218 execution flows)
## 基本概念
GitNexus 是一个代码智能工具,通过索引代码库构建知识图谱,帮助开发者理解代码、评估影响、安全导航。
## 核心原则
### Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
### Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
## 资源速查
| Resource | Use for |
|----------|---------|
| `gitnexus://repo/rui-framework/context` | Codebase overview, check index freshness |
| `gitnexus://repo/rui-framework/clusters` | All functional areas |
| `gitnexus://repo/rui-framework/processes` | All execution flows |
| `gitnexus://repo/rui-framework/process/{name}` | Step-by-step execution trace |
## 技能参考
| 场景 | 技能文件 |
|------|---------|
| 架构理解 / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| 影响分析 / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Bug 追踪 / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| 重构 / "Rename this function" | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| 工具参考 | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| CLI 命令 | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
## 索引维护
如果 GitNexus 工具提示索引过期,执行:
```bash
npx gitnexus analyze
```
+322
View File
@@ -0,0 +1,322 @@
# OpenCode 多仓库操作指南
> **版本**: v1.0
> **创建日期**: 2026-06-04
> **适用**: rui 项目前后端分离开发团队
---
## 一、项目结构概览
rui 项目采用**多仓库**架构:
```
~/rhkj/
├── rui-framework/ # 后端仓库(Java/Spring
│ ├── backend/ # 基础框架
│ ├── app/ # 应用模块
│ └── docs/ # 文档
└── rui-frontend/ # 前端仓库(Vue/Node.js
├── admin-ui/ # 管理后台
├── cashier-mobile/ # 收银移动端
└── customer-mobile/ # 顾客端
```
**原则**:一个 OpenCode 会话只处理一个仓库。
---
## 二、启动 OpenCode 的正确姿势
### 2.1 后端开发(rui-framework
```bash
# 1. 进入后端目录
cd /Users/zhangsheng/rhkj/rui-framework
# 2. 启动 OpenCode(命令行方式)
opencode
# 3. 会话启动后,明确告知角色
```
**启动时输入**(粘贴到 OpenCode 对话框):
```
你现在进入【后端开发模式】。
工作目录:/Users/zhangsheng/rhkj/rui-framework
负责范围:backend/ 和 app/ 目录下的 Java 代码
技术栈:Spring Boot 4.x、Spring Cloud、MyBatis Plus、JDK 21
规则:
1. 只能修改 backend/ 和 app/ 下的代码
2. 发现前端需求时,提醒用户创建 Gitee Issue
3. 编码规范参考 docs/AGENTS.md
当前任务:【在这里描述你的具体任务】
```
---
### 2.2 前端开发(rui-frontend
```bash
# 1. 进入前端目录
cd /Users/zhangsheng/rhkj/rui-frontend
# 2. 启动 OpenCode
opencode
# 3. 会话启动后,明确告知角色
```
**启动时输入**(粘贴到 OpenCode 对话框):
```
你现在进入【前端开发模式】。
工作目录:/Users/zhangsheng/rhkj/rui-frontend
负责范围:admin-ui/、cashier-mobile/、customer-mobile/
技术栈:Vue 3、TypeScript、Element Plus、Vite、pnpm
规则:
1. 只能修改前端项目下的代码
2. 需要后端接口时,在 rui-framework 仓库创建 Gitee Issue
3. 编码规范参考 AGENTS.md
4. 使用 pnpm workspace 管理多项目
当前任务:【在这里描述你的具体任务,如:开发用户管理页面】
```
---
### 2.3 框架开发(仅修改 backend/
```
你现在进入【框架开发模式】。
工作目录:/Users/zhangsheng/rhkj/rui-framework
负责范围:仅 backend/ 目录
角色:基础框架维护者
规则:
1. 只能修改 backend/ 下的代码
2. 不修改任何 app/ 目录下的业务代码
3. 保持框架的通用性和向后兼容性
4. 修改公共接口时,评估对 app/ 的影响
当前任务:【描述框架任务】
```
---
## 三、切换工作流的正确方式
### ❌ 错误示范
在一个 OpenCode 会话中:
```
用户:帮我修改后端接口
AI:好的,已修改 backend/xxx.java
用户:再帮我改一下前端页面
AI:好的,已修改 admin-ui/xxx.vue ← 错误!上下文已污染
```
### ✅ 正确示范
**场景 1:同一仓库内切换任务**
如果任务相关(如修改后端接口 + 对应单元测试),可以在同一会话中完成。
如果任务不相关(如用户管理 + 订单管理),建议:
```
用户:/new
AI:已创建新会话
用户:【输入新的任务描述】
```
**场景 2:跨仓库协作**
```
# 后端仓库会话
用户:开发用户查询接口
AI:已完成,接口路径:GET /user/admin/list
# 需要前端对接时
用户:需要前端开发列表页面
AI:请创建新会话处理前端任务。
在 rui-frontend 目录启动 OpenCode,输入前端开发模式提示词。
接口文档:http://localhost:8080/doc.html#/user/list
# 用户切换到前端仓库,新建会话
用户:/new
【进入 rui-frontend 目录,启动新 OpenCode 会话】
用户:根据接口文档开发用户列表页面
AI:好的,开始开发前端页面...
```
---
## 四、跨仓库通信机制
### 4.1 前端需要后端接口
**前端会话**中:
```
我需要后端提供用户批量导入接口,参数是 Excel 文件
```
OpenCode 应该回答:
```
请在 rui-framework 仓库创建 Issue,使用模板:api_request.md
Issue 内容:
标题:[API-REQ] 用户模块需要批量导入接口
内容:...
创建后,在 backend/ 开发会话中处理该 Issue
```
### 4.2 后端接口完成通知前端
**后端会话**中:
```
接口已实现,Swagger 地址:/doc.html#/user/import
请通知前端团队进行对接
```
OpenCode 应该:
```
已在 Issue 中回复前端团队。
前端可以在 rui-frontend 仓库的新会话中进行对接开发。
```
---
## 五、目录访问限制检查
启动会话后,可以让 OpenCode 自我检查:
```
请确认你的工作范围:
1. 可以修改哪些目录?
2. 不能修改哪些目录?
3. 如果需要跨仓库协作,应该怎么做?
```
期望回答:
```
1. 可修改:backend/、app/(后端模式)或 admin-ui/(前端模式)
2. 不可修改:frontend/(后端模式)或 backend/(前端模式)
3. 跨仓库协作:通过 Gitee Issue 进行
```
---
## 六、常见问题
### Q1: 我不小心在错误目录启动了 OpenCode,怎么办?
**答**
1. 保存当前对话(如有重要信息)
2. 关闭当前 OpenCode 窗口
3. 切换到正确目录重新启动
4. 使用 `/new` 创建新会话
### Q2: 一个功能需要同时修改前后端,怎么操作?
**答**
1. **方法 A(推荐)**:先在一个仓库完成,提交后切换到另一个仓库
- 在 rui-framework 开发接口 → 提交 PR
- 在 rui-frontend 开发页面 → 提交 PR
2. **方法 B(并行)**:两个 OpenCode 窗口同时工作
- 窗口 1rui-framework 目录,开发后端
- 窗口 2rui-frontend 目录,开发前端
3. **不要**:在一个会话中同时修改两个仓库
### Q3: OpenCode 能记住跨仓库的上下文吗?
**答**:不能。每个 OpenCode 会话是独立的:
- 不同目录 = 不同上下文
- 即使同一个目录,`/new` 后也是全新上下文
- 需要人工传递关键信息(如接口文档链接)
### Q4: 怎么快速查看当前在哪个仓库?
**答**:在 OpenCode 中输入:
```
请告诉我当前工作目录和可修改范围
```
### Q5: 可以用同一个 OpenCode 窗口切换目录吗?
**答**:不建议。OpenCode 启动时会锁定工作目录。如果需要切换:
1. 关闭当前窗口
2. `cd` 到新目录
3. 重新启动 OpenCode
---
## 七、快捷键和命令速查
| 操作 | 命令 |
|------|------|
| 创建新会话 | `/new` |
| 查看当前目录 | `pwd` |
| 查看文件树 | `tree -L 2` |
| 查看 Git 状态 | `git status` |
| 切换分支 | `git checkout branch-name` |
---
## 八、最佳实践
1. **明确角色**:启动时明确告知 OpenCode 当前角色和范围
2. **单一职责**:一个会话只做一件事(一个功能/一个 Bug)
3. **及时提交**:完成一个功能后立即 `git commit`,不要积压
4. **Issue 驱动**:跨仓库需求通过 Issue 追踪,不要口头传递
5. **文档优先**:复杂功能先写设计文档,再编码
6. **定期 /new**:对话超过 20-30 轮后,新建会话保持上下文清晰
---
## 九、模板库
### 启动模板
保存以下模板,启动时直接粘贴:
**后端启动模板**
```markdown
你现在进入【后端开发模式】。
工作目录:/Users/zhangsheng/rhkj/rui-framework
技术栈:Spring Boot 4.x、JDK 21、MyBatis Plus
规则:只能修改 backend/ 和 app/ 目录
当前任务:【填写】
```
**前端启动模板**
```markdown
你现在进入【前端开发模式】。
工作目录:/Users/zhangsheng/rhkj/rui-frontend
技术栈:Vue 3、TypeScript、Element Plus、Vite
规则:只能修改前端项目目录
当前任务:【填写】
```
---
## 十、相关文档
- [跨团队协作规范](./cross-team-workflow.md)
- [后端项目规范](../AGENTS.md)
- [前端项目规范](../../rui-frontend/AGENTS.md)
- [Gitea 自建 Git 服务器](./self-hosted-git-server.md)
---
> **提示**:本文档是活文档,根据团队实践持续更新。如有建议请提交 PR。
@@ -0,0 +1,459 @@
# rui-common-core 使用手册
> **文件名**`rui-common-core使用手册.md`
> **存放位置**`docs/rui-common-core使用手册.md`
>
> **文档定位**:本文档是 `rui-common-core` 模块的**唯一权威参考**,记录所有工具类、注解、事件、DTO 的功能与用法。
>
> **使用规则**
> 1. **开发前先查本文档**:使用任何通用工具/注解/常量前,优先查阅本文档确认是否存在可用实现
> 2. **新增即更新**:向 `rui-common-core` 模块新增/修改任何类、方法时,**必须同步更新本文档**
> 3. **禁止重复造轮子**:本文档中已有的工具,禁止在业务模块中重新实现
---
## 1. 模块概述
`rui-common-core` 是睿核通用平台框架的**核心基础模块**,为所有上层模块提供通用的工具类、常量、异常、上下文持有器、注解、事件、DTO 等基础能力。
> **定位**:无业务依赖,无 Spring 依赖(除 SpringUtil、LoginEvent 外),可被任意模块引用。
---
## 2. 功能清单
### 2.1 上下文持有器(holder
| 类名 | 功能 | 说明 |
|------|------|------|
| `TenantContextHolder` | 租户上下文 | 线程隔离的租户 ID 存储,支持父子线程传递 |
| `LocaleContextHolder` | 本地化上下文 | 线程隔离的语言环境存储 |
### 2.2 工具类(util
| 类名 | 功能 | 依赖 |
|------|------|------|
| `ServletUtil` | Servlet 请求工具 | 获取 IP、参数、Header、请求体等 |
| `SpringUtil` | Spring 上下文工具 | 获取 Bean、配置、Environment、判断环境 |
| `JsonUtil` | JSON 工具 | 基于 Fastjson2,对象与 JSON 互转 |
| `DateUtil` | 日期时间工具 | 基于 JDK 8 java.time,格式化、解析、计算 |
| `IdWorker` | 雪花算法 ID 生成器 | 分布式唯一 ID,支持趋势递增 |
| `BeanUtil` | Bean 拷贝工具 | 基于 Spring BeanUtils,支持列表拷贝 |
| `EncryptUtil` | 加密工具 | MD5、SHA-256、AES、Base64 |
| `ValidateUtil` | 校验工具 | 手机号、邮箱、身份证、URL、密码等校验 |
| `StringUtil` | 字符串扩展工具 | 驼峰/下划线转换、脱敏、截取等 |
| `ThreadUtil` | 线程工具 | 线程池创建、优雅关闭、命名线程工厂 |
| `FileUtil` | 文件工具 | 读写、复制、移动、目录操作、大小格式化 |
| `UserNoGenerator` | 用户编号生成器 | 生成 U0001 格式编号,自动跳过保留靓号 |
### 2.3 异常(exception
| 类名 | 功能 |
|------|------|
| `BizException` | 业务异常,统一业务错误抛出 |
### 2.4 常量(constants
| 类名 | 功能 |
|------|------|
| `SecurityConstant` | 安全相关常量(Token 前缀、Header 名称等) |
### 2.5 模型(model
| 类名 | 功能 |
|------|------|
| `PageResult` | 统一分页结果封装 |
### 2.6 结果封装(result
| 类名 | 功能 |
|------|------|
| `Result` | 统一 API 返回结果(code、msg、data |
| `ResultCode` | 统一错误码枚举 |
### 2.7 注解(annotation
| 类名 | 功能 | 目标位置 |
|------|------|---------|
| `DataScope` | 数据权限注解,自动拼接数据范围 SQL | Service 方法 |
### 2.8 事件(event
| 类名 | 功能 | 说明 |
|------|------|------|
| `LoginEvent` | 登录事件 | 登录成功/失败时发布,供监听器记录日志 |
### 2.9 DTOdto
| 类名 | 功能 | 说明 |
|------|------|------|
| `OperLogDTO` | 操作日志 DTO | 跨服务传输操作日志数据 |
### 2.10 拦截器上下文(interceptor
| 类名 | 功能 | 说明 |
|------|------|------|
| `DataScopeContext` | 数据权限上下文 | ThreadLocal 存储数据范围信息,需手动清理 |
---
## 3. 工具类详细说明
### 3.1 IdWorker(雪花算法)
```java
// 生成唯一 ID
long id = IdWorker.nextIdLong();
String idStr = IdWorker.nextIdStr();
// 从 ID 中提取信息
long timestamp = IdWorker.extractTimestamp(id);
long workerId = IdWorker.extractWorkerId(id);
```
### 3.2 DateUtil(日期时间)
```java
// 获取当前时间
String now = DateUtil.now(); // 2024-01-01 12:00:00
String today = DateUtil.today(); // 2024-01-01
// 格式化与解析
String str = DateUtil.format(LocalDateTime.now());
LocalDateTime dt = DateUtil.parse("2024-01-01 12:00:00");
// 计算
LocalDateTime tomorrow = DateUtil.plusDays(dt, 1);
long days = DateUtil.betweenDays(start, end);
```
### 3.3 JsonUtilJSON 处理)
```java
// 对象转 JSON
String json = JsonUtil.toJsonString(user);
// JSON 转对象
User user = JsonUtil.parseObject(json, User.class);
List<User> list = JsonUtil.parseList(json, User.class);
Map<String, Object> map = JsonUtil.parseMap(json);
```
### 3.4 EncryptUtil(加密)
```java
// MD5
String md5 = EncryptUtil.md5("password");
// SHA-256
String sha256 = EncryptUtil.sha256("password");
// AES 对称加密
String encrypt = EncryptUtil.aesEncrypt("content", "1234567890123456");
String decrypt = EncryptUtil.aesDecrypt(encrypt, "1234567890123456");
// Base64
String base64 = EncryptUtil.base64Encode("content");
```
### 3.5 ValidateUtil(校验)
```java
// 常用校验
boolean isMobile = ValidateUtil.isMobile("13800138000");
boolean isEmail = ValidateUtil.isEmail("test@example.com");
boolean isIdCard = ValidateUtil.isIdCard("110101199001011234");
boolean isUrl = ValidateUtil.isUrl("https://example.com");
boolean isIpv4 = ValidateUtil.isIpv4("192.168.1.1");
// 密码强度
boolean validPwd = ValidateUtil.isValidPassword("Abc123456");
```
### 3.6 SpringUtilSpring 上下文)
```java
// 获取 Bean
UserService service = SpringUtil.getBean(UserService.class);
// 获取配置
String value = SpringUtil.getProperty("server.port");
// 判断环境
boolean isDev = SpringUtil.isDev();
boolean isProd = SpringUtil.isProd();
```
### 3.7 BeanUtilBean 拷贝)
```java
// 对象拷贝
UserDTO dto = BeanUtil.copyProperties(user, UserDTO.class);
// 列表拷贝
List<UserDTO> dtoList = BeanUtil.copyList(userList, UserDTO.class);
// 忽略 null 值拷贝(用于更新)
BeanUtil.copyNonNullProperties(source, target);
```
### 3.8 StringUtil(字符串扩展)
```java
// 命名转换
String camel = StringUtil.underlineToCamel("user_name"); // userName
String underline = StringUtil.camelToUnderline("userName"); // user_name
// 脱敏
String mobile = StringUtil.desensitizeMobile("13800138000"); // 138****8000
String email = StringUtil.desensitizeEmail("test@example.com"); // t***@example.com
// 其他
String truncated = StringUtil.truncate("很长的字符串", 5); // 很长的字...
```
### 3.9 ThreadUtil(线程池)
```java
// 创建线程池
ExecutorService pool = ThreadUtil.newFixedPool(4, "my-pool");
ScheduledExecutorService scheduled = ThreadUtil.newScheduledPool(2, "schedule");
// 推荐:自定义线程池
ThreadPoolExecutor executor = ThreadUtil.newThreadPool(
4, 8, 60, 100, "business"
);
// 优雅关闭
ThreadUtil.shutdown(pool, 30);
```
### 3.10 FileUtil(文件操作)
```java
// 读写
String content = FileUtil.readString("/path/file.txt");
FileUtil.writeString("/path/file.txt", "content");
FileUtil.appendString("/path/file.txt", "append");
// 目录
FileUtil.createDir("/path/dir");
FileUtil.delete("/path/file.txt");
// 信息
boolean exists = FileUtil.exists("/path/file.txt");
long size = FileUtil.size("/path/file.txt");
String sizeStr = FileUtil.formatSize(1024 * 1024); // 1.00 MB
```
### 3.11 UserNoGenerator(用户编号生成器)
```java
// 生成格式化的用户编号(U0001、U0002...
String userNo = UserNoGenerator.format(1); // U0001
// 判断是否为保留靓号(豹子号、连号、含666/888/999
boolean reserved = UserNoGenerator.isReserved("U1111"); // true
// 生成下一个可用编号(自动跳过保留号)
String next = UserNoGenerator.generate(1); // U0001(如果 U0001 是靓号则自动跳过)
```
---
## 4. 注解使用说明
### 4.1 @DataScope(数据权限)
标记在 Service 方法上,由 MyBatis Plus 拦截器自动拼接数据范围 SQL。
```java
@DataScope(deptField = "dept_id", userField = "create_by")
public List<User> list() {
return baseMapper.selectList();
}
```
**参数说明**
- `deptField`:部门字段名,默认 `"dept_id"`
- `userField`:用户字段名,默认 `"create_by"`
**数据范围类型**(由 `DataScopeContext` 设置):
- `1`:全部数据
- `2`:本部门数据
- `3`:本部门及子部门数据
- `4`:仅本人数据
- `5`:自定义数据
---
## 5. 事件使用说明
### 5.1 LoginEvent(登录事件)
登录成功或失败时发布,供其他模块监听记录日志。
```java
// 发布事件
applicationEventPublisher.publishEvent(
new LoginEvent(this, userId, username, 1, clientId, ip,
location, browser, os, 1, "登录成功")
);
// 监听事件
@Component
public class LoginEventListener {
@EventListener
public void onLogin(LoginEvent event) {
// 记录登录日志
}
}
```
---
## 6. DTO 使用说明
### 6.1 OperLogDTO(操作日志 DTO
用于跨服务传输操作日志数据。
```java
OperLogDTO log = new OperLogDTO();
log.setTitle("用户管理");
log.setOperType(2); // 修改
log.setOperTypeName("修改用户");
log.setRequestUrl("/user/admin/user");
log.setRequestMethod("PUT");
log.setUserId(userId);
log.setStatus(1); // 成功
```
---
## 7. 上下文使用说明
### 7.1 TenantContextHolder(租户上下文)
```java
// 设置租户ID
TenantContextHolder.setTenantId(100L);
// 获取租户ID
Long tenantId = TenantContextHolder.getTenantId();
// 清理(必须在线程结束时调用)
TenantContextHolder.clear();
```
### 7.2 DataScopeContext(数据权限上下文)
```java
// 设置数据范围
DataScopeContext.setDataScope(2); // 本部门
DataScopeContext.setUserId(userId);
DataScopeContext.setDeptId(deptId);
// 获取数据范围
Integer scope = DataScopeContext.getDataScope();
// 清理(必须在线程结束时调用,防止内存泄漏)
DataScopeContext.clear();
```
---
## 8. 使用方式
### 8.1 Maven 依赖
```xml
<dependency>
<groupId>com.rui</groupId>
<artifactId>rui-common-core</artifactId>
<version>${revision}</version>
</dependency>
```
### 8.2 在业务模块中使用
```java
import com.rui.common.core.util.*;
@Service
public class UserService {
public void createUser(User user) {
// 生成唯一 ID
user.setId(IdWorker.nextIdLong());
// 生成用户编号
user.setUserNo(UserNoGenerator.generate(seq));
// 日期处理
user.setCreatedAt(DateUtil.nowDateTime());
// 密码加密
user.setPassword(EncryptUtil.md5(user.getPassword()));
// 保存...
}
}
```
---
## 9. 规范说明
### 9.1 工具类设计原则
1. **无状态**:所有工具类均为无状态静态方法,线程安全
2. **不可实例化**:通过 private 构造器防止实例化
3. **异常处理**:内部捕获并转换为 RuntimeException,避免污染业务代码
4. **空安全**:所有方法对 null 参数有处理,避免 NPE
### 9.2 新增规范
如需向本模块新增类,请遵循:
1. **包名规范**
- 工具类:`com.rui.common.core.util`
- 注解:`com.rui.common.core.annotation`
- 事件:`com.rui.common.core.event`
- DTO`com.rui.common.core.dto`
- 上下文:`com.rui.common.core.holder` / `interceptor`
- 常量:`com.rui.common.core.constants`
- 异常:`com.rui.common.core.exception`
2. **命名规范**
- 工具类:以 `Util` 结尾,如 `XxxUtil`
- 注解:以功能命名,如 `@DataScope`
- 事件:以 `Event` 结尾,如 `XxxEvent`
- DTO:以 `DTO` 结尾,如 `XxxDTO`
3. **方法规范**:均为 public static(工具类)
4. **文档规范**:类注释和方法注释使用中文
5. **测试规范**:建议补充单元测试
6. **文档同步**:**新增/修改后必须同步更新本文档**
---
## 10. 文档维护说明
### 10.1 何时更新本文档
| 场景 | 操作 |
|------|------|
| 新增工具类/注解/事件/DTO | 在功能清单中添加条目,在详细说明中添加使用示例 |
| 修改现有类的方法签名 | 同步更新对应详细说明中的代码示例 |
| 删除类或方法 | 从文档中移除对应条目,并在版本历史中注明 |
| 发现文档与代码不一致 | 以代码为准,修正文档 |
### 10.2 版本历史
| 日期 | 版本 | 变更内容 |
|------|------|---------|
| 2024-01 | 1.0 | 初始版本,包含基础工具类 |
| 2024-06 | 1.1 | 新增 IdWorker、BeanUtil、ThreadUtil |
| 2026-05 | 1.2 | 补充完整工具类集合,新增文档 |
| 2026-06 | 1.3 | 新增 UserNoGenerator、DataScope 注解、LoginEvent、OperLogDTO、DataScopeContext;补充文档使用说明 |
+480
View File
@@ -0,0 +1,480 @@
# 自建 Git 服务器方案:Gitea
> **版本**: v1.0
> **创建日期**: 2026-06-04
> **适用场景**: 替代 Gitee,实现完整的 Git + CI/CD 私有化部署
---
## 一、为什么选择 Gitea
### 1.1 对比分析
| 特性 | Gitee | GitLab CE | Gitea | Gogs |
|------|-------|-----------|-------|------|
| **开源免费** | 部分功能收费 | ✅ 社区版免费 | ✅ 完全开源 | ✅ 完全开源 |
| **资源占用** | 云端,无需部署 | 4GB+ 内存 | **128MB 内存** | 64MB 内存 |
| **CI/CD** | 收费 | ✅ 内置 | ✅ Gitea Actions | ❌ 需搭配 Drone |
| **中文支持** | ✅ 原生 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| **Issue 模板** | ✅ 支持 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| **代码审查** | ✅ 支持 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| **部署难度** | 无需部署 | 复杂 | **简单** | 简单 |
| **GitHub Actions 兼容** | ❌ | ❌ | ✅ 兼容 | ❌ |
### 1.2 Gitea 优势
-**轻量级**:单二进制文件,内置 SQLite,无需额外数据库
-**低资源**:128MB 内存即可运行,适合低配服务器
-**CI/CD 内置**Gitea Actions 完全兼容 GitHub Actions 语法
-**易迁移**:支持从 Gitee/GitHub 导入仓库
-**Webhook 丰富**:支持钉钉、企业微信、Slack 等通知
-**权限管理**:组织、团队、仓库级权限控制
---
## 二、部署方案
### 方案 ADocker 部署(推荐)
适合:有 Docker 环境的服务器
```yaml
# docker-compose.yml
version: "3"
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.vifo.cc
- GITEA__server__ROOT_URL=https://git.vifo.cc
- GITEA__server__SSH_DOMAIN=git.vifo.cc
- GITEA__actions__ENABLED=true
restart: always
networks:
- gitea
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
# 可选:Gitea Actions Runner(执行 CI/CD 任务)
runner:
image: gitea/act_runner:latest
container_name: gitea-runner
environment:
- GITEA_INSTANCE_URL=https://git.vifo.cc
- GITEA_RUNNER_REGISTRATION_TOKEN=your-token
- GITEA_RUNNER_NAME=default-runner
restart: always
networks:
- gitea
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./runner:/data
depends_on:
- gitea
networks:
gitea:
external: false
```
**启动命令**
```bash
# 创建目录
mkdir -p ~/gitea && cd ~/gitea
# 创建 docker-compose.yml(粘贴上方内容)
nano docker-compose.yml
# 启动
docker-compose up -d
# 查看日志
docker-compose logs -f gitea
```
**初始化配置**
1. 访问 `http://服务器IP:3000`
2. 填写管理员账号(首次访问会自动跳转到安装页面)
3. 基础 URL 设置为你的域名(如 `https://git.vifo.cc`
4. 数据库选择 SQLite(轻量级)或 MySQL(生产环境)
---
### 方案 B:二进制部署
适合:没有 Docker 环境的裸机
```bash
# 1. 下载二进制(Linux AMD64
wget -O gitea https://dl.gitea.com/gitea/latest/gitea-latest-linux-amd64
chmod +x gitea
# 2. 创建用户(不要使用 root 运行)
sudo useradd -r -m -s /bin/bash git
# 3. 创建工作目录
sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo chown -R git:git /var/lib/gitea/
sudo chmod -R 750 /var/lib/gitea/
# 4. 移动到系统目录
sudo mv gitea /usr/local/bin/
# 5. 创建 Systemd 服务
sudo tee /etc/systemd/system/gitea.service > /dev/null <<EOF
[Unit]
Description=Gitea
After=network.target
[Service]
User=git
Group=git
WorkingDirectory=/var/lib/gitea
ExecStart=/usr/local/bin/gitea web --config /var/lib/gitea/custom/conf/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
[Install]
WantedBy=multi-user.target
EOF
# 6. 启动服务
sudo systemctl daemon-reload
sudo systemctl enable gitea
sudo systemctl start gitea
# 7. 查看状态
sudo systemctl status gitea
```
---
### 方案 C:一键安装脚本(最简单)
```bash
# 下载官方安装脚本
curl -s https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/install.sh | bash
# 或者使用 snapUbuntu/Debian
sudo snap install gitea
```
---
## 三、Nginx 反向代理 + HTTPS
### 3.1 Nginx 配置
```nginx
# /etc/nginx/conf.d/gitea.conf
server {
listen 80;
server_name git.vifo.cc;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name git.vifo.cc;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### 3.2 申请免费 SSL 证书(Let's Encrypt
```bash
# 安装 certbot
sudo apt install certbot python3-certbot-nginx
# 申请证书
sudo certbot --nginx -d git.vifo.cc
# 自动续期测试
sudo certbot renew --dry-run
```
---
## 四、从 Gitee 迁移到 Gitea
### 4.1 迁移单个仓库
```bash
# 1. 在 Gitea 创建空仓库(如 rui-frontend
# 2. 本地克隆 Gitee 仓库
git clone --mirror https://gitee.com/rui/rui-frontend.git
# 3. 推送到 Gitea
cd rui-frontend.git
git remote add gitea https://git.vifo.cc/rui/rui-frontend.git
git push gitea --mirror
```
### 4.2 批量迁移(所有仓库)
```bash
#!/bin/bash
# migrate.sh
GITEA_URL="https://git.vifo.cc"
GITEA_TOKEN="your-token"
GITEA_ORG="rui"
REPOS=("rui-framework" "rui-frontend" "rui-payment")
for repo in "${REPOS[@]}"; do
echo "迁移: $repo"
# 在 Gitea 创建仓库
curl -X POST "$GITEA_URL/api/v1/orgs/$GITEA_ORG/repos" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$repo\", \"private\": true}"
# 克隆并推送
git clone --mirror "https://gitee.com/rui/$repo.git" "/tmp/$repo"
cd "/tmp/$repo"
git remote add gitea "$GITEA_URL/$GITEA_ORG/$repo.git"
git push gitea --mirror
cd ..
done
```
---
## 五、配置 CI/CDGitea Actions
### 5.1 启用 Actions
`app.ini` 中配置:
```ini
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = github
```
### 5.2 注册 Runner
```bash
# 获取注册令牌(在 Gitea 管理后台 → Actions → Runners → 创建新 Runner
# 然后执行:
docker exec -it gitea-runner act_runner register \
--instance https://git.vifo.cc \
--token YOUR_TOKEN \
--name default-runner \
--labels ubuntu-latest:docker://node:18
```
### 5.3 创建前端 CI/CD 工作流
```yaml
# rui-frontend/.gitea/workflows/build.yml
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Build admin-ui
run: pnpm build:admin
- name: Deploy to server
if: github.ref == 'refs/heads/main'
run: |
# 部署脚本
echo "部署到生产环境"
```
### 5.4 创建后端 CI/CD 工作流
```yaml
# rui-framework/.gitea/workflows/build.yml
name: Build and Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: cd backend && mvn clean install -DskipTests
- name: Run tests
run: cd backend && mvn test
```
---
## 六、Gitea 常用配置
### 6.1 配置 Issue 模板
与 Gitee 类似,创建 `.gitea/issue_templates/` 目录:
```
.gitea/issue_templates/
├── api_request.md
├── framework_bug.md
└── cross_team_task.md
```
**注意**Gitea 的 Issue 模板语法与 Gitee 兼容。
### 6.2 配置 Webhook(通知钉钉/企业微信)
在仓库设置 → Webhooks 中添加:
```
URL: https://oapi.dingtalk.com/robot/send?access_token=xxx
触发事件: Push, Pull Request, Issue
```
### 6.3 禁用公开注册(私有化)
```ini
# app.ini
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
```
---
## 七、备份策略
### 7.1 自动备份脚本
```bash
#!/bin/bash
# backup.sh
BACKUP_DIR="/backup/gitea"
DATE=$(date +%Y%m%d)
# 备份 Gitea 数据
tar czf "$BACKUP_DIR/gitea-$DATE.tar.gz" /var/lib/gitea
# 保留最近 7 天的备份
find "$BACKUP_DIR" -name "gitea-*.tar.gz" -mtime +7 -delete
```
添加到 crontab
```bash
# 每天凌晨 2 点备份
0 2 * * * /path/to/backup.sh
```
---
## 八、常见问题
### Q1: Gitea 和 GitLab 怎么选?
- **Gitea**:轻量、简单、资源占用低,适合小团队(< 50 人)
- **GitLab**:功能强大、生态丰富,适合大团队、需要复杂 CI/CD 流水线
### Q2: 需要多少服务器资源?
| 规模 | CPU | 内存 | 磁盘 | 推荐 |
|------|-----|------|------|------|
| 小团队 (< 10人) | 1 核 | 1GB | 20GB | 阿里云/腾讯云 入门配置 |
| 中等团队 (10-50人) | 2 核 | 2GB | 50GB | 阿里云 2C2G |
| 大团队 (50+人) | 4 核 | 4GB | 100GB | 阿里云 4C4G |
### Q3: 可以从 Gitea 迁移回 Gitee/GitHub 吗?
可以。Gitea 支持导出仓库,也可以直接推送回其他 Git 平台。
### Q4: Gitea Actions 和 GitHub Actions 完全兼容吗?
大部分常用 action 兼容。如果某个 action 不兼容,可以自己写 shell 脚本替代。
---
## 九、部署检查清单
- [ ] 准备一台 Linux 服务器(1C1G 起步)
- [ ] 安装 Docker(推荐)或下载 Gitea 二进制
- [ ] 配置域名和 DNS 解析
- [ ] 配置 Nginx 反向代理 + HTTPS
- [ ] 初始化 Gitea 并创建管理员账号
- [ ] 创建组织(如 `rui`
- [ ] 从 Gitee 迁移仓库
- [ ] 配置 Gitea Actions Runner
- [ ] 创建 CI/CD 工作流文件
- [ ] 配置 Webhook 通知
- [ ] 设置备份策略
---
## 十、相关文档
- [Gitea 官方文档](https://docs.gitea.com/)
- [Gitea Actions 文档](https://docs.gitea.com/usage/actions/overview)
- [OpenCode 多仓库操作指南](../docs/opencode-workflow.md)
---
> **提示**:如果不方便自己部署服务器,也可以考虑 **Gitea Cloud**(官方托管版)或继续使用 Gitee 免费版(仅代码托管,CI/CD 用其他方案如 Jenkins)。
+242
View File
@@ -0,0 +1,242 @@
# 灰度发布(金丝雀发布)
> 网关层灰度发布解决方案,支持多种灰度策略,实现平滑的服务升级。
## 概述
灰度发布(Canary Release)是一种渐进式发布策略,通过将小部分流量先路由到新版本,验证无误后再逐步扩大流量,最终完成全量发布。本方案在网关层实现,对业务服务无侵入。
## 核心特性
| 特性 | 说明 |
|------|------|
| **多策略支持** | 权重、用户白名单、IP 白名单、强制 Header |
| **多服务独立配置** | 每个服务可配置独立的灰度规则 |
| **无侵入** | 业务服务无需改动,仅通过元数据标记版本 |
| **优先级控制** | 多种策略按优先级执行,确保灰度准确性 |
## 灰度策略(按优先级排序)
### 1. 强制 Header(最高优先级)
客户端通过指定 Header 强制访问特定版本,用于测试验证。
```http
GET /user/api/info HTTP/1.1
Host: api.example.com
X-Grayscale-Version: v2
```
### 2. 用户白名单
特定用户 ID 强制走灰度版本,通常用于内部测试账号。
**识别方式**:通过 `X-User-Id` Header 识别用户身份。
### 3. IP 白名单
特定 IP 或 IP 段的请求走灰度版本,支持 CIDR 格式。
**示例**
- `192.168.1.0/24` - 内网网段
- `10.0.0.5` - 单个 IP
### 4. 权重比例(最低优先级)
按比例分配流量,适用于全量灰度场景。
**示例**`weight: 10` 表示 10% 的请求走灰度版本。
## 后端服务配置
### 1. 标记灰度实例
在服务的 `application.yml` 中通过 Nacos 元数据标记版本:
```yaml
spring:
cloud:
nacos:
discovery:
metadata:
gray.version: v2 # 标记为灰度版本 v2
```
### 2. 部署多个版本
同时部署稳定版本和灰度版本:
```
服务实例列表
├── rui-service-user:v1 (稳定版本,无 gray.version 或 gray.version=v1)
├── rui-service-user:v1 (稳定版本)
└── rui-service-user:v2 (灰度版本,gray.version=v2)
```
## 网关配置
在 Nacos 的 `rui-gateway.yaml` 中配置灰度规则:
```yaml
grayscale:
# 强制灰度 Header 名称
force-header: X-Grayscale-Version
# 灰度规则
rules:
# rui-service-user 服务的灰度规则
rui-service-user:
enabled: true # 启用灰度
version: v2 # 灰度版本标识(与实例元数据对应)
weight: 10 # 10% 流量走灰度
user-ids: # 特定用户走灰度
- user001
- user002
ip-ranges: # 特定 IP 走灰度
- 192.168.1.0/24
```
### 配置项说明
| 配置项 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `enabled` | boolean | 是 | 是否启用灰度 |
| `version` | string | 否 | 灰度版本标识,默认 `gray` |
| `weight` | int | 否 | 灰度流量权重(0-100),默认 0 |
| `user-ids` | list | 否 | 用户白名单列表 |
| `ip-ranges` | list | 否 | IP 白名单列表,支持 CIDR |
## 使用场景示例
### 场景一:内部测试
仅需内部测试人员访问新版本:
```yaml
grayscale:
rules:
rui-service-user:
enabled: true
version: v2
user-ids:
- tester001
- tester002
```
### 场景二:按比例灰度
对全量用户按比例灰度:
```yaml
grayscale:
rules:
rui-service-user:
enabled: true
version: v2
weight: 5 # 先 5%,逐步提高到 10%、20%、50%、100%
```
### 场景三:内部 IP 灰度
公司内部员工先行体验:
```yaml
grayscale:
rules:
rui-service-user:
enabled: true
version: v2
ip-ranges:
- 192.168.0.0/16 # 公司内网
- 10.0.0.0/8 # VPN 网段
```
### 场景四:组合策略
多种策略同时生效(按优先级匹配):
```yaml
grayscale:
rules:
rui-service-user:
enabled: true
version: v2
weight: 10
user-ids:
- vip001 # VIP 用户优先体验
ip-ranges:
- 192.168.1.0/24 # 办公室网络
```
**匹配逻辑**
1. 强制 Header > 2. 用户白名单 > 3. IP 白名单 > 4. 权重
## 验证灰度是否生效
### 1. 查看网关日志
开启 DEBUG 级别日志,查看实例选择:
```
选择灰度实例: rui-service-user:v2 [v2]
选择稳定实例: rui-service-user:v1
```
### 2. 通过 Header 验证
在响应头中添加版本标识:
```java
// 后端服务在响应中添加版本信息
response.setHeader("X-Server-Version", version);
```
### 3. 使用强制 Header 测试
```bash
curl -H "X-Grayscale-Version: v2" https://api.example.com/user/api/info
```
## 最佳实践
### 1. 灰度前准备
- [ ] 新版本已通过测试环境验证
- [ ] 监控和告警已配置
- [ ] 回滚方案已准备
### 2. 灰度流程
1. **Phase 1**:内部测试(用户白名单)
2. **Phase 2**:办公网灰度(IP 白名单)
3. **Phase 3**:小流量灰度(weight=1%
4. **Phase 4**:逐步扩大(5% → 10% → 20% → 50% → 100%
5. **Phase 5**:全量发布,下线旧版本
### 3. 监控指标
- 错误率对比(灰度 vs 稳定)
- 响应时间对比
- 业务指标波动
### 4. 快速回滚
发现问题时立即关闭灰度:
```yaml
grayscale:
rules:
rui-service-user:
enabled: false # 关闭灰度,全部流量回稳定版本
```
## 注意事项
1. **服务发现延迟**:Nacos 服务列表更新可能有延迟(默认 5-10 秒)
2. **数据兼容性**:确保新版本与旧版本数据库兼容
3. **接口兼容性**:灰度期间避免破坏性接口变更
4. **会话一致性**:有状态服务需考虑会话粘滞或共享
## 相关文档
- [Spring Cloud Gateway 文档](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/)
- [Spring Cloud LoadBalancer 文档](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer)
@@ -0,0 +1,302 @@
# 聚合启动器(rui-service-starter)使用文档
## 1. 什么是聚合启动器
`rui-service-starter` 是将多个业务微服务合并为一个 Spring Boot 应用启动的**轻量化部署方案**。
| 模块 | 说明 |
|------|------|
| `rui-service-system` | 系统管理服务(菜单、角色、部门、字典、租户等) |
| `rui-service-user` | 用户基础服务(用户、等级、权限等) |
> **认证中心(rui-auth)和网关(rui-gateway)保持独立**,不参与聚合。
---
## 2. 适用场景
| 场景 | 推荐模式 |
|------|---------|
| 中小型项目、团队规模 < 10 人 | ✅ **聚合模式**(节省资源、简化部署) |
| 大型项目、多团队并行开发 | 独立微服务模式(服务隔离、独立发布) |
| 从单体向微服务过渡 | ✅ **聚合模式**(先聚合后拆分) |
| 本地开发调试 | ✅ **聚合模式**(一键启动所有业务) |
**聚合模式优势:**
- 减少 JVM 内存占用(节省 500MB+
- 减少 Nacos 注册中心压力
- 一次打包、一次部署、一次启动
- 本地开发只需启动 3 个服务(gateway + auth + starter
---
## 3. 架构说明
### 3.1 独立微服务模式(默认)
```
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Gateway │────▶│ rui-auth │────▶│ rui-service-xxx │
│ :9300 │ │ :9301 │ │ :9302~930N │
└─────────────┘ └─────────────┘ └─────────────────┘
┌──────────┴──────────┐
▼ ▼
rui-service-system rui-service-user
:9302 :9303
```
### 3.2 聚合模式
```
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐
│ Gateway │────▶│ rui-auth │────▶│ rui-service-starter │
│ :9300 │ │ :9301 │ │ :9399 │
└─────────────┘ └─────────────┘ └─────────────────────────┘
┌──────────┴──────────┐
▼ ▼
[system 模块] [user 模块]
共享 JVM + 端口
```
---
## 4. 快速启动
### 4.1 编译
```bash
cd backend
# 编译聚合启动器(自动编译依赖模块)
mvn clean install -pl rui-service/rui-service-starter -am -DskipTests
```
### 4.2 本地开发启动
```bash
# 方式一:IDE 直接运行 StarterApplication.java
# 方式二:命令行启动
java -jar rui-service/rui-service-starter/target/rui-service-starter-1.0.0-SNAPSHOT.jar
```
### 4.3 生产环境启动
```bash
# 指定环境变量(也可在 Nacos 中配置)
java -jar rui-service-starter-1.0.0-SNAPSHOT.jar \
--NACOS_SERVER_ADDR=127.0.0.1:8848 \
--spring.profiles.active=prod
```
---
## 5. 端口与服务名
| 服务 | 端口 | Nacos 服务名 | 说明 |
|------|------|-------------|------|
| rui-gateway | 9300 | rui-gateway | 网关(保持独立) |
| rui-auth | 9301 | rui-auth | 认证中心(保持独立) |
| rui-service-system | 9302 | rui-service-system | 系统服务(独立模式) |
| rui-service-user | 9303 | rui-service-user | 用户服务(独立模式) |
| **rui-service-starter** | **9399** | **rui-service-starter** | **聚合启动器(替代 system + user** |
---
## 6. 网关路由配置
Nacos `rui-gateway.yaml` 中已默认使用聚合模式,将 `/system/**``/user/**` 统一路由到 `rui-service-starter`
```yaml
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: rui-auth
uri: lb://rui-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=0
# ========== 聚合模式(默认,中小型项目)==========
- id: rui-service-starter
uri: lb://rui-service-starter
predicates:
- Path=/user/**,/system/**
filters:
- StripPrefix=0
# ========== 独立微服务模式(大型项目)==========
# - id: rui-service-user
# uri: lb://rui-service-user
# predicates:
# - Path=/user/**
# filters:
# - StripPrefix=0
# - id: rui-service-system
# uri: lb://rui-service-system
# predicates:
# - Path=/system/**
# filters:
# - StripPrefix=0
```
> **提示**:如需切换到独立模式,取消注释独立路由并注释掉聚合路由即可。
---
## 7. Feign 调用说明
### 7.1 聚合模式下的 Feign 行为
| Feign 接口 | 目标服务 | 默认指向 | 说明 |
|-----------|---------|---------|------|
| `UserAuthFeign` | `rui-service-user` | `rui-service-starter` | 通过 Nacos 路由到聚合启动器 |
| `SystemClientFeign` | `rui-service-system` | `rui-service-starter` | 通过 Nacos 路由到聚合启动器 |
| `TokenManageFeign` | `rui-auth` | `rui-auth` | 认证中心保持独立 |
### 7.2 配置原理
FeignClient 的 `value` 属性使用 `${feign.providers.xxx}` 变量,默认指向聚合启动器:
```java
@FeignClient(contextId = "userAuthFeign",
value = "${feign.providers.user:rui-service-starter}", // 默认指向聚合启动器
path = "/user/inner")
```
**Nacos `rui-common.yaml` 中的公共配置:**
```yaml
feign:
providers:
user: rui-service-starter # 用户服务:默认指向聚合启动器
system: rui-service-starter # 系统服务:默认指向聚合启动器
auth: rui-auth # 认证中心:保持独立
```
**切换独立模式**:在对应服务的 Nacos 配置中覆盖:
```yaml
feign:
providers:
user: rui-service-user # 改回独立用户服务
system: rui-service-system # 改回独立系统服务
```
---
## 8. Nacos 配置建议
### 8.1 配置中心
聚合启动器启动时会加载以下 Nacos 配置:
| 配置文件 | 说明 |
|---------|------|
| `rui-service-starter.yaml` | 聚合服务专属配置(可选) |
| `rui-common.yaml` | 公共配置(日志、线程池等) |
| `rui-data.yaml` | 数据源配置(MySQL、Redis 等) |
### 8.2 配置继承
聚合模式下,`rui-service-starter.yaml` 可以覆盖 `rui-service-system.yaml``rui-service-user.yaml` 中的冲突配置。
建议将**业务无关的基础配置**放到 `rui-common.yaml`**数据库连接等环境配置**放到 `rui-data.yaml`
---
## 9. 两种模式切换指南
### 9.1 从独立模式切换到聚合模式
1. **停止** `rui-service-user``rui-service-system`
2. **启动** `rui-service-starter`
3. **修改网关路由**(见 6.1
4. **完成**
### 9.2 从聚合模式切换到独立模式
1. **停止** `rui-service-starter`
2. **启动** `rui-service-system`(端口 9302)和 `rui-service-user`(端口 9303
3. **修改网关路由**(见 6.2
4. **完成**
### 9.3 代码层面注意事项
- 聚合模式下,**所有业务代码无需修改**
- 两个服务的 `@RestController` 路由前缀不同(`/system/**``/user/**`),天然无冲突
- Mapper 扫描范围 `com.rui.**.mapper` 已覆盖两个模块
---
## 10. 常见问题
### Q1: 聚合启动器内存占用多少?
> 约 400~600MBJVM Heap),比同时启动 user + system(约 800MB+)节省 30%~50%。
### Q2: 可以再加其他服务吗?
> 可以。在 `rui-service-starter/pom.xml` 中增加依赖即可:
> ```xml
> <dependency>
> <groupId>com.rui</groupId>
> <artifactId>rui-service-order</artifactId>
> <version>${revision}</version>
> </dependency>
> ```
> 同时确保新服务的 Controller 路由前缀不与现有服务冲突。
### Q3: 聚合模式下事务跨服务吗?
> 同一 JVM 内,system 和 user 的 Service 互相调用时,**Spring 本地事务仍然有效**。但建议保持服务边界清晰,避免过度耦合。
### Q4: 日志怎么区分是哪个模块的?
> 日志文件统一输出到 `logs/rui-service-starter/`,通过日志内容中的类名(`com.rui.service.system.xxx` / `com.rui.service.user.xxx`)区分来源模块。
### Q5: 健康检查端点是什么?
> `GET http://localhost:9399/actuator/health`
### Q6: 聚合模式和独立模式可以同时运行吗?
> **不建议**。会导致 Nacos 中同时存在 `rui-service-starter` 和 `rui-service-user/system`Feign 调用可能出现负载均衡到错误实例的情况。
---
## 11. 本地开发推荐启动顺序
聚合模式下,本地开发只需启动 3 个服务:
```bash
# 1. 启动 Nacos(如果本地运行)
sh startup.sh -m standalone
# 2. 启动 Redis(如果本地运行)
redis-server
# 3. 启动 MySQL(如果本地运行)
# 4. 启动 rui-auth(认证中心)
java -jar rui-auth/target/rui-auth-*.jar
# 5. 启动 rui-gateway(网关)
java -jar rui-gateway/target/rui-gateway-*.jar
# 6. 启动 rui-service-starter(聚合业务服务)
java -jar rui-service/rui-service-starter/target/rui-service-starter-*.jar
```
> 相比独立模式(需要启动 5+ 个服务),开发效率大幅提升。
---
## 12. 文档更新记录
| 日期 | 版本 | 说明 |
|------|------|------|
| 2026-05-30 | 1.0 | 初始版本,聚合 system + user |
+21
View File
@@ -0,0 +1,21 @@
# rui-common-feign 分析报告
## 模块功能
Feign 客户端增强模块,自动为所有 Feign 请求注入租户/代理链等请求头。
## 核心类
| 类 | 作用 |
|------|------|
| `CloudEnableFeignClients` | 替代 `@EnableFeignClients`,同时导入自定义 Registrar |
| `CustomFeignClientsRegistrar` | 重写 Feign 客户端注册逻辑,注入 Tenant Header 和代理链 |
| `CloudFeignAutoConfiguration` | 自动配置,注册 Actuator 端点 |
| `FeignClientEndpoint` | `/actuator/feignClients` 查看所有 Feign 客户端 |
## 优化点
1. 三个 `CustomFeignClientsRegistrar` 实际只用一个,删除冗余 `2.java``MyFeignClientsRegistrar`
2. `CloudFeignAutoConfiguration` 缺少 `@AutoConfigureAfter` 正确的顺序
3. 添加 `AutoConfiguration.imports` 注册
4. 静态内部类过多,简化
@@ -0,0 +1,587 @@
# 项目文档治理与 Superpowers 流程规范化
> **设计日期**: 2026-06-02
> **版本**: v1.0
> **状态**: 已批准(待实施)
> **目标**: 建立完整的项目文档体系,将 Superpowers 工作流融入项目规范
---
## 一、项目背景
### 1.1 现状分析
当前项目(睿核科技 - rui)是一个基于 Spring Cloud 的微服务通用平台框架,已开发支付模块(rui-payment)等业务模块。项目文档和代码规范主要维护在 `AGENTS.md` 中。
### 1.2 存在的问题
通过对现有 `AGENTS.md`(668 行)和项目文档体系的分析,发现以下 **7 类问题**
| 问题类型 | 具体表现 | 影响 |
|---------|---------|------|
| **结构混乱** | 规范、指南、规则混杂,无清晰层级 | 查找信息困难,新成员上手慢 |
| **缺少导航** | 无目录、无文档地图 | 无法快速定位需要的规范 |
| **Superpowers 缺失** | 完全没有工作流说明 | 团队无法按标准流程协作 |
| **格式错误** | 多处 `**` 标记不匹配、表格格式问题 | 阅读体验差,可能误解 |
| **内容缺失** | 无环境搭建指南、无模块创建指引、无代码审查规范 | 新人无法自助上手 |
| **职责越界** | GitNexus 工具配置与项目规范混在一起 | 文档职责不清晰 |
| **信息孤岛** | 未引用 `docs/` 下的其他文档 | 文档之间无关联 |
### 1.3 目标定义
1. **建立标准**:重构 AGENTS.md,使其成为项目的"宪法"
2. **查漏补缺**:系统检查现有文档和代码,生成分类问题清单
3. **融入流程**:将 Superpowers 工作流融入项目规范
4. **持续可用**:建立可复用的模板和检查清单
---
## 二、整体工作流设计
采用 **三阶段流水线**,每阶段都有明确的输入、输出和验收标准:
```
┌─────────────────────────────────────────────────────────────────┐
│ 阶段一:重构 AGENTS.md(建立标准) │
│ 输入:现有 AGENTS.md + 项目文档体系 │
│ 输出:新版 AGENTS.md(项目宪法) │
│ 验收:文档结构清晰、规范完整、Superpowers 流程融入 │
└───────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 阶段二:全面查漏补缺(基于标准) │
│ 输入:新版 AGENTS.md + 现有项目所有文档和代码 │
│ 输出:分类问题清单(高/中/低优先级) │
│ 验收:检查覆盖率 100%、问题可追踪、有优先级 │
└───────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 阶段三:修复与规范化(建立流程) │
│ 输入:问题清单 │
│ 输出:修复后的文档/代码 + Superpowers 工作流模板 + 改进报告 │
│ 验收:高优先级问题全部修复、流程可运行、报告完整 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 三、阶段一:重构 AGENTS.md
### 3.1 现有文档诊断
**格式问题清单**
- 第 173 行:`SecurityUtils` 后的 `**` 未闭合
- 第 176-178 行:多处 `**` 标记不匹配
- 表格格式在部分终端渲染异常
**内容缺失清单**
- 缺少项目级 README.md
- 缺少开发环境搭建指南
- 缺少 Superpowers 工作流说明
- 缺少代码审查规范
- 缺少模块创建标准流程
**结构问题清单**
- 无文档地图/目录
- GitNexus 配置与项目规范混杂
- 章节间无逻辑递进关系
### 3.2 新版结构设计
```
AGENTS.md(项目宪法 - 所有开发者必读)
├── 1. 文档地图(新增)← 60秒了解项目文档体系
├── 2. 项目概览(精简)← 技术栈、仓库结构
├── 3. 环境准备(新增)← 开发环境、初始化步骤
├── 4. Superpowers 工作流(新增)← 四阶段开发流程
├── 5. 编码规范(合并优化)← 所有代码规范集中
├── 6. 基础设施速查(优化)← 工具类、注解、复用原则
├── 7. 模块开发指南(新增)← 如何创建标准业务模块
├── 8. 运维规范(合并)← Git、数据库、Nacos、构建
├── 9. 协作规范(优化)← 对话管理、工作边界
└── 10. 附录(新增)← 错误码、数据类型、文档索引
```
### 3.3 章节详细设计
#### 第 1 章:文档地图(新增)
目标:让新成员在 60 秒内了解项目文档体系
内容:
- 文档体系全景图
- 快速导航(按角色:新手/开发者/架构师)
- 文档更新规则(谁维护、何时更新)
#### 第 2 章:项目概览(精简现有内容)
保留:
- 项目名称、类型
- 精简版仓库结构(突出 app/ 和 backend/
- 技术栈表格
移除:
- 过详细的技术说明(迁移到专门文档)
#### 第 3 章:环境准备(新增)
内容:
- 开发环境要求(JDK 21、Maven、MySQL、Node.js 等)
- 项目初始化步骤(clone → 配置 → 构建 → 运行)
- IDE 配置建议(IntelliJ IDEA 插件、代码风格)
- 验证清单(如何确认环境就绪)
#### 第 4 章:Superpowers 工作流(新增)
核心内容,四阶段流程:
**Phase 1: Brainstorming(头脑风暴)**
- 目标:明确需求、确定方案
- 输入:用户原始需求
- 输出:批准的设计方向
- 检查清单:
- [ ] 是否已探索项目上下文?
- [ ] 是否已提出澄清问题?
- [ ] 是否已对比 2-3 种方案?
- [ ] 用户是否已确认方向?
**Phase 2: Spec Writing(规格编写)**
- 目标:编写详细设计文档
- 输入:批准的设计方向
- 输出:设计文档(保存到 `docs/superpowers/specs/`
- 检查清单:
- [ ] 是否包含背景与目标?
- [ ] 是否包含详细设计?
- [ ] 是否包含验收标准?
- [ ] 是否已完成自我审查?
- [ ] 用户是否已审查批准?
**Phase 3: Plan Writing(计划编写)**
- 目标:编写可执行的实施计划
- 输入:批准的设计文档
- 输出:实施计划(保存到 `docs/superpowers/plans/`
- 检查清单:
- [ ] 是否已分解为具体任务?
- [ ] 每个任务是否有明确的验收标准?
- [ ] 是否有风险评估?
- [ ] 用户是否已审查批准?
**Phase 4: Implementation(实施执行)**
- 目标:按计划执行任务
- 输入:实施计划
- 输出:代码 + 文档更新
- 检查清单:
- [ ] 是否按任务逐个执行?
- [ ] 每个任务是否已完成验证?
- [ ] 是否已更新实施跟踪文档?
- [ ] 是否已完成最终审查?
#### 第 5 章:编码规范(合并优化现有章节)
合并内容:
- 基础编码规范(Lombok、命名、注释)
- 模块 Bean 注入规范
- Mapper 规范
- Controller 规范
- URL 路由规范
- 异常处理规范
- 日志规范
- 测试规范
优化点:
- 统一用表格展示"正确 vs 错误"示例
- 增加常见错误模式说明
- 增加检查工具建议(如 Checkstyle 规则)
#### 第 6 章:基础设施速查(优化)
优化内容:
- 工具类速查表(增加使用场景列)
- 注解速查表(增加参数说明)
- 复用原则(增加反例说明)
#### 第 7 章:模块开发指南(新增)
内容:
- 何时需要新建模块
- 模块命名规范
- 模块标准结构(common/core/provider/api/task
- 创建步骤清单
- AutoConfiguration 配置
- 模块间依赖规则
#### 第 8 章:运维规范(合并现有章节)
合并内容:
- Git 提交规范
- 数据库脚本执行规则
- Nacos 配置管理规则
- 构建与发布规范
- 前端构建规则
#### 第 9 章:协作规范(优化现有章节)
优化内容:
- OpenCode `/new` 使用指南(增加决策树)
- 工作边界规则(增加流程图)
- 框架问题处理流程(增加模板)
#### 第 10 章:附录(新增)
内容:
- 错误码分配表(完整区间划分)
- 数据类型对照表(MySQL ↔ Java ↔ JDBC
- 相关文档索引(docs/ 下所有文档的导航)
- 术语表
### 3.4 职责分离
**GitNexus 配置迁移**
将 AGENTS.md 中的 GitNexus 部分(第 626-668 行)迁移到独立文档:`docs/gitnexus-guide.md`
原因:
- AGENTS.md 是"项目规范"GitNexus 是"工具配置"
- 职责分离后,AGENTS.md 更聚焦
- GitNexus 指南可以独立更新
---
## 四、阶段二:全面查漏补缺
### 4.1 检查策略
基于新版 AGENTS.md 的 10 个章节,设计 **4 维度检查清单**
#### 维度一:文档体系完整性(检查 `docs/` 目录)
| 检查项 | 检查内容 | 优先级 | 当前状态 |
|--------|---------|--------|---------|
| 1.1 | 是否存在 `README.md` 项目总览 | 🔴 高 | ❌ 缺失 |
| 1.2 | 每个业务模块是否有独立设计文档 | 🔴 高 | ⚠️ 仅支付模块有 |
| 1.3 | 是否存在环境搭建指南 | 🔴 高 | ❌ 缺失 |
| 1.4 | 是否存在数据库变更记录 | 🟡 中 | ❌ 缺失 |
| 1.5 | API 文档是否完整 | 🟡 中 | 待确认 |
| 1.6 | 文档之间是否有交叉引用 | 🟡 中 | ❌ 无引用 |
| 1.7 | 实施跟踪文档是否最新 | 🟢 低 | ⚠️ 支付模块显示完成 |
#### 维度二:AGENTS.md 规范落地(检查代码库)
| 检查项 | 检查内容 | 优先级 | 检查方式 |
|--------|---------|--------|---------|
| 2.1 | 所有 Entity 是否继承 BaseEntity | 🔴 高 | 代码扫描 |
| 2.2 | 是否使用 Lombok | 🔴 高 | 代码扫描 |
| 2.3 | Service 是否使用构造器注入 | 🔴 高 | 代码扫描 |
| 2.4 | Mapper SQL 是否使用 `#prefix#` | 🔴 高 | 正则匹配 |
| 2.5 | Controller 是否按规范分类 | 🟡 中 | 目录检查 |
| 2.6 | 标准 CRUD 是否继承 BaseController | 🟡 中 | 代码扫描 |
| 2.7 | 异常处理是否统一用 BizException | 🟡 中 | 代码扫描 |
| 2.8 | Git 提交是否符合规范格式 | 🟢 低 | 提交历史检查 |
#### 维度三:项目结构与配置
| 检查项 | 检查内容 | 优先级 | 当前状态 |
|--------|---------|--------|---------|
| 3.1 | 模块命名是否符合规范 | 🔴 高 | ✅ 符合 |
| 3.2 | 每个模块是否有 AutoConfiguration | 🔴 高 | 待确认 |
| 3.3 | `application-dev.yml` 是否在 `.gitignore` | 🔴 高 | ✅ 符合 |
| 3.4 | Nacos 配置是否已推送 | 🟡 中 | 待确认 |
| 3.5 | 数据库脚本是否已执行 | 🟡 中 | 待确认 |
#### 维度四:Superpowers 流程就绪度
| 检查项 | 检查内容 | 优先级 | 当前状态 |
|--------|---------|--------|---------|
| 4.1 | 是否存在 `docs/superpowers/` 目录 | 🔴 高 | ❌ 缺失 |
| 4.2 | 是否存在设计文档模板 | 🔴 高 | ❌ 缺失 |
| 4.3 | 是否存在实施计划模板 | 🔴 高 | ❌ 缺失 |
| 4.4 | 是否存在代码审查清单 | 🟡 中 | ❌ 缺失 |
### 4.2 输出格式
每个问题按以下格式记录:
```markdown
### 🔴 HIGH-001: 缺少 README.md
| 属性 | 内容 |
|------|------|
| **检查项** | 文档体系完整性 - 1.1 |
| **问题描述** | 项目根目录缺少 README.md,新成员无法快速了解项目 |
| **影响范围** | 所有新加入的开发者 |
| **建议修复** | 创建 README.md,包含项目简介、技术栈、快速开始、文档索引 |
| **参考标准** | AGENTS.md 第 1 章 |
| **优先级** | 🔴 高 |
```
### 4.3 问题分类与优先级
**优先级定义**
| 优先级 | 定义 | 修复时限 |
|--------|------|---------|
| 🔴 高 | 阻碍开发或违反核心规范 | 立即修复 |
| 🟡 中 | 影响效率或存在风险 | 1 周内修复 |
| 🟢 低 | 优化建议 | 下次迭代处理 |
---
## 五、阶段三:修复与规范化
### 5.1 修复计划(按优先级)
#### 🔴 高优先级(必须立即修复)
| 序号 | 任务 | 输出物 | 验收标准 |
|------|------|--------|---------|
| H1 | 创建 README.md | `/README.md` | 包含项目简介、技术栈、快速开始、文档索引 |
| H2 | 重构 AGENTS.md | `/AGENTS.md` | 10 章结构完整、格式正确、无遗漏 |
| H3 | 创建 Superpowers 目录结构 | `docs/superpowers/` | 目录存在且结构正确 |
| H4 | 创建设计文档模板 | `docs/superpowers/templates/design-template.md` | 包含所有必需章节 |
| H5 | 创建实施计划模板 | `docs/superpowers/templates/plan-template.md` | 包含任务分解和验收标准 |
| H6 | 迁移 GitNexus 指南 | `docs/gitnexus-guide.md` | 内容完整、AGENTS.md 中已移除 |
#### 🟡 中优先级(建议 1 周内修复)
| 序号 | 任务 | 输出物 | 验收标准 |
|------|------|--------|---------|
| M1 | 补充环境搭建指南 | `docs/environment-setup.md` | 步骤可执行、有验证方法 |
| M2 | 创建代码审查清单 | `docs/superpowers/templates/review-checklist.md` | 覆盖主要规范点 |
| M3 | 检查模块 AutoConfiguration | 代码修复 | 每个可复用模块都有 |
| M4 | 检查 Nacos 配置同步 | 配置确认 | 所有配置已推送 |
#### 🟢 低优先级(可选优化)
| 序号 | 任务 | 输出物 | 验收标准 |
|------|------|--------|---------|
| L1 | 补充数据库变更记录 | `docs/database-changelog.md` | 记录所有表结构变更 |
| L2 | 统一文档交叉引用 | 各文档更新 | 相关文档间有链接 |
| L3 | 检查测试覆盖率 | 报告 | 核心业务覆盖 ≥ 80% |
### 5.2 Superpowers 工作流模板
#### 模板一:设计文档模板
```markdown
# <模块/功能名称> 设计文档
> **设计日期**: YYYY-MM-DD
> **版本**: v1.0
> **状态**: 设计中/已批准
> **目标**: <一句话描述目标>
---
## 一、背景与目标
### 1.1 现状分析
<描述当前现状、存在的问题>
### 1.2 目标定义
<明确本次设计的目标,建议 3-5 条>
---
## 二、详细设计
### 2.1 整体架构
<架构图、流程图>
### 2.2 核心组件
<各组件的职责、接口>
### 2.3 数据流
<数据如何流转>
### 2.4 接口设计
<API 定义>
### 2.5 数据库设计
<表结构、索引>
### 2.6 错误处理
<异常场景、错误码>
---
## 三、验收标准
- [ ] <可验证的验收条件 1>
- [ ] <可验证的验收条件 2>
- [ ] <可验证的验收条件 3>
---
## 四、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| <风险 1> | 高/中/低 | <措施> |
```
#### 模板二:实施计划模板
```markdown
# <模块/功能名称> 实施计划
> **计划日期**: YYYY-MM-DD
> **版本**: v1.0
> **状态**: 待执行/执行中/已完成
> **关联设计**: <链接到设计文档>
---
## 一、任务清单
### Phase 1: <阶段名称>
| 序号 | 任务 | 负责人 | 预估工时 | 状态 | 验证方式 |
|------|------|--------|---------|------|---------|
| 1.1 | <具体任务> | <负责人> | <工时> | ⬜ | <如何验证> |
---
## 二、进度跟踪
| 日期 | 完成任务 | 遇到的问题 | 解决方案 |
|------|---------|-----------|---------|
| YYYY-MM-DD | <任务> | <问题> | <方案> |
---
## 三、完成总结
- **实际工时**: <X 小时>
- **偏差分析**: <与预估的差异及原因>
- **经验教训**: <可复用的经验>
```
#### 模板三:代码审查清单
```markdown
# 代码审查清单
## 基础规范
- [ ] 使用 Lombok,无手写 getter/setter
- [ ] Service 使用构造器注入
- [ ] Entity 继承 BaseEntity
- [ ] 类名不加 Rui 前缀(除非冲突)
## Mapper 规范
- [ ] SQL 使用 `#prefix#` 占位符
- [ ] 无硬编码表前缀
## Controller 规范
- [ ] 标准 CRUD 继承 BaseController
- [ ] URL 路径符合规范
- [ ] 使用正确注解(@Inner@AuthIgnore 等)
## 异常与日志
- [ ] 使用 BizException 而非 RuntimeException
- [ ] 返回 Result.ok()/Result.fail()
- [ ] 日志无敏感信息
## 测试
- [ ] 核心逻辑有单元测试
- [ ] 测试命名符合规范
```
### 5.3 改进报告模板
```markdown
# 项目文档治理改进报告
> **报告日期**: YYYY-MM-DD
> **治理范围**: 项目文档体系 + AGENTS.md + Superpowers 流程
> **执行人**: <执行者>
---
## 一、检查统计
| 维度 | 检查项数 | 发现问题 | 已修复 | 待修复 |
|------|---------|---------|--------|--------|
| 文档体系完整性 | X | Y | Z | W |
| 代码规范落地 | X | Y | Z | W |
| 项目结构与配置 | X | Y | Z | W |
| Superpowers 就绪度 | X | Y | Z | W |
| **合计** | **X** | **Y** | **Z** | **W** |
## 二、问题分布
<图表或表格展示问题分布>
## 三、关键改进
1. <改进 1:重构 AGENTS.md>
2. <改进 2:建立 Superpowers 流程>
3. <改进 3...>
## 四、后续建议
1. <建议 1>
2. <建议 2>
```
---
## 六、验收标准
### 6.1 阶段验收标准
| 阶段 | 验收标准 | 验证方式 |
|------|---------|---------|
| 阶段一 | AGENTS.md 结构清晰、内容完整、格式正确 | 人工审查 |
| 阶段二 | 检查覆盖率 100%、问题清单完整 | 逐项核对 |
| 阶段三 | 高优先级问题全部修复、模板可用 | 实际使用验证 |
### 6.2 最终验收标准
- [ ] 新成员可在 30 分钟内通过 AGENTS.md 了解项目规范
- [ ] Superpowers 四阶段流程可在项目中完整运行
- [ ] 所有文档间有交叉引用,无信息孤岛
- [ ] 代码规范有自动化检查手段(或明确的手动检查清单)
---
## 七、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 重构 AGENTS.md 期间,团队仍在开发新功能 | 中 | 使用分支管理,重构完成后再合并 |
| 检查清单不够全面,遗漏问题 | 中 | 基于 AGENTS.md 逐条设计检查项,确保覆盖 |
| 团队成员不习惯 Superpowers 流程 | 低 | 提供培训和模板,逐步推广 |
| 文档更新后无人维护 | 中 | 明确文档负责人,纳入代码审查 |
---
## 八、附录
### 8.1 术语表
| 术语 | 说明 |
|------|------|
| Superpowers | OpenCode 的规范化工作流框架 |
| Brainstorming | 头脑风暴阶段,明确需求和方案 |
| Spec | 设计文档/规格说明书 |
| AGENTS.md | 项目规范文档(项目宪法) |
### 8.2 相关文档
| 文档 | 路径 | 说明 |
|------|------|------|
| AGENTS.md | `/AGENTS.md` | 项目规范(待重构) |
| 支付模块设计 | `docs/支付模块架构设计.md` | 示例设计文档 |
| 支付模块跟踪 | `docs/支付模块实施跟踪.md` | 示例跟踪文档 |
### 8.3 参考标准
- 本设计文档本身遵循 Superpowers 流程编写
- 模板设计参考了行业最佳实践
---
> **文档结束**
> 下一步:进入实施计划阶段(由 Superpowers Plan Writer 执行)
@@ -0,0 +1,236 @@
# MQ 统一推送入口设计文档
> **设计日期**: 2026-06-04
> **版本**: v1.0
> **状态**: 已批准
> **目标**: 创建统一消息队列推送入口 MqClient,封装多 Provider 路由、Action 注入、异常兜底,对标 spring-rui MqDefaultClient
---
## 一、背景与目标
### 1.1 现状分析
当前项目已有基础 MQ 能力:
- `MqService` 接口:提供 `send()` 方法,面向业务开发者直接使用
- `Message<T>` 模型:含 id, topic, payload, headers, timestamp, retryCount
- `RedisMqService` / `RabbitMqService`:分别基于 Redis Pub/Sub 和 RabbitMQ 的实现
- `MqTopic` 注解:用于方法级消息订阅
**存在的问题**
1. 缺乏统一推送门面:业务代码需要直接注入 `MqService` 并选择实现,无法自动按 Provider 路由
2. 无 Provider 抽象:无法在多环境(开发用 Redis、生产用 RabbitMQ)间平滑切换
3. 无 Action 语义:消息缺乏"添加/删除/更新"等业务动作标识
4. 异常处理分散:各实现自行处理异常,缺乏统一兜底
### 1.2 目标定义
1. 创建 `MqClient` 门面类,作为业务层唯一推送入口
2. 引入 `MqPublisher` Provider 接口,实现多 MQ 后端自动路由
3. 扩展 `Message` 模型,支持 action、provider、exchange、delay 等高级字段
4. 保持现有 `MqService` 接口不变,确保向后兼容
5. 所有推送操作统一异常捕获和日志记录
---
## 二、详细设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 业务层 (Business) │
│ MqClient.publish(...) │
└──────────────────────────┬──────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────┐
│ MqClient (门面/统一入口) │
│ · 自动从 Spring 容器获取所有 MqPublisher 实现 │
│ · 按 support(MqProvider) 过滤匹配的 Publisher │
│ · 自动注入 action 到 payload │
│ · 统一 try-catch + 日志 │
└──────────────────────────┬──────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ RedisMqPublisher │ │ RabbitMqPublisher│ │ FutureProvider │
│ (Redis实现) │ │ (RabbitMQ实现) │ │ (可扩展) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 2.2 核心组件
| 组件 | 职责 | 位置 |
|------|------|------|
| `MqClient` | 统一推送门面,业务层唯一入口 | `rui-common-mq` |
| `MqPublisher` | Provider 能力接口,定义 support + publish | `rui-common-mq` |
| `MqProvider` | Provider 枚举(RABBITMQ, REDIS | `rui-common-mq` |
| `MqProperties` | 配置属性(默认 provider、topic prefix | `rui-common-mq` |
| `MqAction` | 消息动作枚举(ADDED, DELETED 等) | `rui-common-mq` |
| `Message<T>` | 扩展后的消息模型 | `rui-common-mq` |
### 2.3 数据流
```
启动阶段 (一次)
Spring 注入 List<MqPublisher> → 遍历注册到 EnumMap
publisherMap = { RABBITMQ: RabbitMqPublisher, REDIS: RedisMqPublisher }
运行时 (每次 publish)
MqClient.publish(String topic, MqAction action, T payload)
├─ 1. 构造 Message<T>
├─ 2. 若 action != NONE,将 action 序列化后注入 payload
├─ 3. 若 message.provider == null,使用 MqProperties 默认值
├─ 4. publisherMap.get(provider) → O(1) 直接取到 Publisher
├─ 5. 调用 publisher.publish(message)
└─ 6. catch Exception → log.error,不抛异常
```
### 2.4 接口设计
**MqClient(门面类)**
采用**构造器注入 + 启动预构建 EnumMap**,避免运行时遍历:
```java
@Component
@Slf4j
public class MqClient {
private final MqProperties properties;
private final EnumMap<MqProvider, MqPublisher> publisherMap = new EnumMap<>(MqProvider.class);
public MqClient(MqProperties properties, List<MqPublisher> publishers) {
this.properties = properties;
for (MqPublisher p : publishers) {
publisherMap.put(p.getProvider(), p); // O(1) 注册
}
}
/** 使用默认 Provider 推送 */
public <T> void publish(String topic, T payload);
/** 使用默认 Provider 推送,带 Action */
public <T> void publish(String topic, MqAction action, T payload);
/** 指定 Provider 推送 */
public <T> void publish(MqProvider provider, String topic, T payload);
/** 指定 Provider 推送,带 Action */
public <T> void publish(MqProvider provider, String topic, MqAction action, T payload);
/** 完整 Message 推送 */
public <T> void publish(Message<T> message);
private MqPublisher resolve(MqProvider provider) {
return publisherMap.get(provider); // O(1) 查找
}
}
```
**MqPublisherProvider 接口)**
```java
public interface MqPublisher {
/** 返回该 Publisher 支持的 Provider 类型 */
MqProvider getProvider();
/** 基础推送 */
<T> void publish(String topic, T payload);
/** 完整消息推送 */
<T> void publish(Message<T> message);
}
```
**MqProvider(枚举)**
```java
public enum MqProvider {
RABBITMQ,
REDIS
}
```
**MqProperties(配置)**
```java
@ConfigurationProperties(prefix = "mq")
@Data
public class MqProperties {
private MqProvider provider = MqProvider.RABBITMQ;
private String prefix = "rui";
public String enTopic(String topic) { ... }
public String deTopic(String topic) { ... }
}
```
**MqAction(动作枚举,精简版)**
```java
public enum MqAction {
NONE, ADDED, DELETED, UPDATED, CREATED,
CANCEL, ENABLED, SUCCESSFUL, FAILURE
}
```
### 2.5 扩展现有实现
**RedisMqService** 调整:
- 实现 `MqPublisher` 接口
- `support(MqProvider.REDIS)` 返回 true
- `publish()` 复用现有 `send()` 逻辑
**RabbitMqService** 调整:
- 实现 `MqPublisher` 接口
- `support(MqProvider.RABBITMQ)` 返回 true
- `publish()` 复用现有 `send()` 逻辑
### 2.6 错误处理
- **异常策略**`MqClient` 所有 publish 方法统一 try-catch,记录 error 日志,不抛异常给业务层
- **Provider 不匹配**:若找不到支持该 Provider 的 Publisher,记录 warn 日志
- **Payload 为空**:自动创建空 JSON 对象 `{}`
---
## 三、验收标准
- [ ] `MqClient` 可正常注入并调用 `publish()` 发送消息
- [ ] 通过配置 `mq.provider=redis` 可自动切换到 Redis 实现
- [ ] `publish(topic, MqAction.ADDED, payload)` 发送的消息包含 action 字段
- [ ] 发送异常时不抛异常,仅记录日志
- [ ] 现有 `MqService.send()` 调用不受影响,向后兼容
- [ ] 项目可正常编译通过
---
## 四、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 现有 `MqService` 被多处使用 | 中 | 不修改 `MqService`,新建 `MqPublisher` 接口 |
| `Message<T>` 扩展后序列化兼容性 | 低 | 新增字段均为可空,不影响现有序列化 |
| SpringUtil 在静态方法中可能未初始化 | 低 | MqClient 通过构造器注入,非静态调用 |
---
## 五、对标分析
| 维度 | spring-rui (MqDefaultClient) | 本设计 (MqClient) |
|------|------------------------------|-------------------|
| 门面类名 | `MqDefaultClient` | `MqClient`(更简洁) |
| Provider 接口 | `MqService`(含 support/publish | `MqPublisher`(不冲突现有 `MqService` |
| 配置类名 | `MqAutoConfiguration` | `MqProperties`(更语义化) |
| JSON 框架 | fastjson2 | Jackson(适配本项目) |
| Provider 支持 | MQTT, RABBITMQ, REDIS | RABBITMQ, REDISMQTT 暂不需要) |
| Action 枚举 | `Actions`80+ 项) | `MqAction`(精简 9 项) |
| 包名 | `org.rui.common.mq` | `com.rui.common.mq` |
+278
View File
@@ -0,0 +1,278 @@
# spring-rui 代码分析报告
> 分析日期:2026-05-26
> 目标:分析 `~/rhkj/spring-rui`(排除 `app/` 目录),对比 {root},识别可复用模式和缺失能力。
---
## 一、项目架构对比
| 维度 | spring-rui | {root}(本项目) |
|------|-----------|-------------------|
| 包名 | `org.rui` | `com.rui` |
| 模块组织 | 单体 `rui-common` 内含 15 子模块 | 平铺 4 子模块 `core/mybatis/security/web` |
| ORM | MyBatis Plus + 自定义类型处理器 | MyBatis Plus |
| 响应模型 | `Result<T>` extends `JSONObject` | `R<T>` POJO |
| JSON 框架 | **fastjson2**(核心依赖) | Jackson |
| 多租户 | 配置驱动(TABLE/IGNORE/NONE 三种模式) | 硬编码忽略表列表 |
| 缓存 | Redis Manager(发布/订阅) | 无 |
---
## 二、spring-rui 各模块分析
### 2.1 rui-common-core
| 文件 | 说明 | {root} 状态 |
|------|------|---------------|
| `Result.java` | 响应模型,extends `JSONObject`,含 `error`/`message`/`code`/`data`/`rows`/`results`,支持链式调用 | 已有 `R<T>`,功能较弱 |
| `ResultResponse.java` | 简单响应 holder | 已有 `ResultCode` |
| `ResponseStatus.java` | 枚举:SUCCESSFUL/UNAUTHORIZED/FORBIDDEN/NOT_FOUND/VALIDATE_FAILED 等 | 可参考补全 |
| `TenantContextHolder.java` | 租户上下文 ThreadLocal | 已有 `TenantContextHolder` |
| `TenantBroker.java` | 支持在指定租户下执行代码 | 无,可选 |
| `IdWorker.java` | ID 生成器 | 无 |
| `JacksonConfig.java` | Jackson 全局配置 | 可参考 |
| `KeyStrResolver.java` / `TenantKeyStrResolver.java` | 缓存 Key 解析(含租户隔离) | 无,可复用 |
| `SpringUtil.java` | Spring 上下文工具 | 无 |
| `ServletUtil.java` | Servlet 请求工具(提取所有参数为 Map) | 无 |
| `JsonUtil.java` | JSON 合并/赋值工具(`JsonUtil.assign()` | 无 |
| `AmountUtil.java` / `NumberUtil.java` / `RandomUtil.java` | 金额/数字/随机工具 | 无 |
| `IPSeekerUtil.java` / `IpUtil.java` | IP 归属地查询 | 无 |
| `StringUtil.java` | 字符串增强工具 | 已有 hutool |
| `LocalDateTimeUtil.java` | 时间工具 | 无 |
| `Validate.java` / `BaseRegex.java` / `NumericUtil.java` | 正则/校验工具 | 无 |
| `HttpClient.java` | HTTP 客户端封装 | 无 |
| `CertHelper.java` / `CertUtil.java` / `RsaUtil.java` / `AesUtil.java` / `MD5Util.java` / `ShaUtil.java` / `SignatureUtil.java` | 加密/证书工具链 | 无 |
| `DelayQueueManager.java` / `QueueTask.java` | 延迟队列 | 无 |
| `Scheduled.java` / `Task.java` / `TaskConfiguration.java` / `TaskFactory.java` | 定时任务调度 | 无 |
| `ProxyProperties.java` / `ProxyInterceptor.java` / `ProxyProperty.java` | 代理链支持 | 无(我们有 proxy chain ThreadLocal |
| `LocaleContextHolder.java` / `LocaleUtil.java` | 国际化支持 | 无 |
| `FileUtil.java` / `MimeUtil.java` / `ZipUtil.java` / `GzipUtil.java` | 文件/压缩工具 | 无 |
| `AntPathMatcher.java` / `PathMatcher.java` | 路径匹配 | 无 |
| `AccountProperty.java` / `AppProperty.java` / `MqttProperty.java` / `WsProperty.java` | 配置属性类 | 无 |
### 2.2 rui-common-data
| 文件 | 说明 | {root} 状态 |
|------|------|---------------|
| `MybatisPlusConfiguration.java` | MP 插件配置(分页 + 多租户 + 防全表更新) | 已有,缺 `BlockAttackInnerInterceptor` |
| `MybatisProperties.java` | MP 配置属性(表前缀、租户模式、忽略表列表) | 无 |
| **`PageService.java`** | **通用查询构建器**(类型字段 + 排序 + 分页 + 返回 Result | **2026-05-26 已移植** |
| `PageOptions.java` | 查询选项(下拉框过滤) | 无,可选 |
| `TenantHandler.java` | 配置驱动多租户(TABLE/IGNORE/NONE 三种模式) | 可参考优化 |
| `BaseMultiTableInnerInterceptor.java` | 多表拦截器 | 无 |
| `TableInterceptor.java` | 表前缀替换(`#prefix#` 占位符) | 无 |
| `AbstractObjectTypeHandler.java` / `JsonObjectTypeHandler.java` / `JsonArrayTypeHandler.java` / `StringArrayTypeHandler.java` / `LongTypeHandler.java` / `IntegerTypeHandler.java` / `BigDecimalArrayTypeHandler.java` / `ArrayTypeHandler.java` | 类型处理器 | 无 |
### 2.3 rui-common-security
| 文件 | 说明 | {root} 状态 |
|------|------|---------------|
| `ResourceServerAutoConfiguration.java` / `ResourceServerConfiguration.java` | OAuth2 资源服务器配置 | 无(不同安全架构) |
| `AuthUtil.java` / `UserUtil.java` | 认证用户工具 | 部分(`SecurityUtils` |
| `OAuthBearerTokenResolver.java` | Token 解析器 | 无 |
| `RedisOAuth2AuthorizationService.java` | Redis 授权存储 | 无 |
| `CustomOAuth2FeignRequestInterceptor.java` | Feign Token 传递 | 无 |
| `OAuthUserDetailsService.java` | 用户详情服务接口 | 无 |
| `DefaultOAuthUserDetailsServiceImpl.java` | 默认实现 | 无 |
| `WeixinOAuthUserDetailsServiceImpl.java` | 微信登录实现 | 无 |
| `RestTemplateUserDetailsServiceImpl.java` | RestTemplate 远程调用实现 | 无 |
| `PermissionService.java` | 权限校验 Bean`@perm.isSystemTenant()` | 无 |
| `AuthIgnore.java` / `Inner.java` | 注解 | 无 |
| `SecurityInnerAspect.java` | 内部调用切面 | 无 |
| `FeignClientConfiguration.java` / `FeignConfiguration.java` | Feign 配置 | 无 |
| `RemoteUserService.java` / `RemoteClientService.java` | Feign 远程调用接口 | 无 |
| `GlobalExceptionHandler.java` | 全局异常处理 | 已有 |
### 2.4 rui-service-system(参考 CRUD 模式)
**重点分析 — CRUD 标准模式:**
```
Controller
├── GET /{module}/list → PageService 分页列表
├── GET /{module}/{id} → doGet: service.getById(id)
├── POST /{module} → doAdd: service.doAdd(bean)
├── PUT /{module}/{id} → doUpdate: service.doUpdate(bean)
├── DELETE /{module}/remove/{id} → doRemove: 物理删除
└── DELETE /{module}/{id} → doDelete: 软删除
Service (extends ServiceImpl<Mapper, Entity>)
├── doAdd(bean) → bean.setDefaultValue(); this.save(bean)
├── doUpdate(bean) → bean.setUpdatedAt(); this.updateById(bean)
├── doRemove(id) → this.removeById(id)
└── doDelete(id) → baseMapper.doDelete(id) // SQL: UPDATE SET deleted=!deleted
Mapper (extends BaseMapper<Entity>)
└── @Update("UPDATE #prefix#table SET deleted=!deleted WHERE id=#{id}")
Integer doDelete(Serializable id);
```
**模式要点:**
- `doRemove` = 物理删除,`doDelete` = 软删除(toggle deleted 标记)
- 所有 Service 方法返回 `Result<?>` 而非实体
- Mapper 中使用 `#prefix#` 占位符,由 `TableInterceptor` 自动替换为实际表前缀
- Entity 实现 `setDefaultValue()` 方法初始化字段
### 2.5 rui-service-users(同上,CRUD 模式验证)
与 rui-service-system 完全一致的 CRUD 模式。每个模块有:
- `model/` — 实体类(`implements Serializable`
- `mapper/` — MyBatis Plus Mapper
- `service/` — 接口
- `service/impl/` — 实现(`extends ServiceImpl` + `doAdd/doUpdate/doRemove/doDelete`
- `controller/` — 接口(`extends BaseController` + CRUD 端点)
### 2.6 rui-gateway
| 文件 | 说明 | {root} 状态 |
|------|------|---------------|
| `GrayLoadBalancer.java` | 灰度负载均衡 | 无 |
| `GrayReactiveLoadBalancerClientFilter.java` | 灰度过滤器 | 无 |
| `GlobalExceptionHandler.java` | 网关异常处理 | 无 |
### 2.7 rui-oauth2
| 文件 | 说明 | {root} 状态 |
|------|------|---------------|
| `OauthApplication.java` | 启动类(无额外配置) | 已有 AuthApplication |
---
## 三、已移植到 {root} 的内容
### 2026-05-26 批量移植
| 文件 | 位置 | 对应 spring-rui |
|------|------|----------------|
| `PageResult.java` | `rui-common-core` | 新增(基于 Result 分页字段) |
| `PageService.java` | `rui-common-mybatis` | `rui-common-data/PageService.java` |
| `BaseController.java` | `rui-common-web` | `rui-service-system/controller/BaseController.java` |
| 防全表更新 | `MyBatisPlusConfig.java` | `MybatisPlusConfiguration.java` 中的 `BlockAttackInnerInterceptor` |
---
## 四、后续可选移植项
### 优先级高(核心能力)
| 功能 | 来源模块 | 说明 |
|------|---------|------|
| `SpringUtil.java` | core | Spring 上下文静态获取 |
| `ServletUtil.java` | core | 请求参数提取工具(PageService 用) |
| `JsonUtil.java` | core | JSON 合并/赋值(`assign` 方法在更新场景很有用) |
| `MybatisProperties.java` | data | 配置化表前缀/租户模式,替代硬编码 |
### 优先级中(开发提效)
| 功能 | 来源模块 | 说明 |
|------|---------|------|
| `StringUtil.java` / `AmountUtil.java` / `NumberUtil.java` | core | 常用工具 |
| `LocalDateTimeUtil.java` | core | 时间工具 |
| `IdWorker.java` | core | ID 生成器 |
| `IPSeekerUtil.java` / `IpUtil.java` | core | IP 工具 |
| `ArrayTypeHandler.java` 系列 | data | MP 类型处理器(JSON/数组) |
| `KeyStrResolver.java` 系列 | core | 缓存 Key 租户隔离 |
### 优先级低(按需移植)
| 功能 | 来源模块 | 说明 |
|------|---------|------|
| 加密工具链(RSA/AES/MD5/SHA/证书) | core | spring-rui 安全相关 |
| `HttpClient.java` | core | HTTP 客户端 |
| `DelayQueueManager.java` | core | 延迟队列 |
| 定时任务调度 | core | 自定义任务框架 |
| 国际化 | core | Locale 工具 |
| 灰度负载均衡 | gateway | 与部署环境强相关 |
| Feign 远程调用 | security | 与认证架构耦合 |
---
## 五、CRUD 编程规范(推荐)
基于 spring-rui 模式,推荐 {root} 业务模块遵循以下规范:
### Controller 规范
```java
@RestController
@RequiredArgsConstructor
@RequestMapping("system/tenant")
public class SysTenantController extends BaseController {
private final SysTenantService service;
@GetMapping("/list")
public R<PageResult<SysTenant>> list(HttpServletRequest request) {
PageService<SysTenant> ps = new PageService<>(request);
ps.setDefaultOrderColumn("updatedAt");
ps.putBoolean("deleted").putQuery("deleted", false);
ps.putLike("name");
return ps.getResults(service);
}
@GetMapping("{id}")
public R<SysTenant> get(@PathVariable Long id) {
return R.ok(service.getById(id));
}
@PostMapping
public R<Void> add(@RequestBody SysTenant bean) {
return service.doAdd(bean);
}
@PutMapping("{id}")
public R<Void> update(@RequestBody SysTenant bean) {
return service.doUpdate(bean);
}
@DeleteMapping("{id}")
public R<Void> delete(@PathVariable Long id) {
return service.doDelete(id);
}
}
```
### Service 规范
```java
public interface SysTenantService extends IService<SysTenant> {
R<Void> doAdd(SysTenant bean);
R<Void> doUpdate(SysTenant bean);
R<Void> doDelete(Long id);
}
public class SysTenantServiceImpl extends ServiceImpl<SysTenantMapper, SysTenant>
implements SysTenantService {
public R<Void> doAdd(SysTenant bean) {
bean.setDefaultValue();
if (this.save(bean)) return R.ok("添加成功");
return R.fail("添加失败");
}
public R<Void> doUpdate(SysTenant bean) {
bean.setUpdatedAt(LocalDateTime.now());
if (this.updateById(bean)) return R.ok("更新成功");
return R.fail("更新失败");
}
public R<Void> doDelete(Long id) {
if (baseMapper.doDelete(id) == 1) return R.ok("删除成功");
return R.fail("删除失败");
}
}
```
### Mapper 规范
```java
@Mapper
public interface SysTenantMapper extends BaseMapper<SysTenant> {
@Update("UPDATE rui_system_tenant SET deleted = !deleted WHERE id = #{id}")
int doDelete(@Param("id") Long id);
}
```
> 注:spring-rui 使用 `#prefix#` 占位符 + TableInterceptor 自动替换表前缀。本项目暂不引入该机制,直接在 SQL 中写完整表名。
+54
View File
@@ -0,0 +1,54 @@
# <模块/功能名称> 设计文档
> **设计日期**: YYYY-MM-DD
> **版本**: v1.0
> **状态**: 设计中/已批准
> **目标**: <一句话描述目标>
---
## 一、背景与目标
### 1.1 现状分析
<描述当前现状、存在的问题>
### 1.2 目标定义
<明确本次设计的目标,建议 3-5 条>
---
## 二、详细设计
### 2.1 整体架构
<架构图、流程图>
### 2.2 核心组件
<各组件的职责、接口>
### 2.3 数据流
<数据如何流转>
### 2.4 接口设计
<API 定义>
### 2.5 数据库设计
<表结构、索引>
### 2.6 错误处理
<异常场景、错误码>
---
## 三、验收标准
- [ ] <可验证的验收条件 1>
- [ ] <可验证的验收条件 2>
- [ ] <可验证的验收条件 3>
---
## 四、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| <风险 1> | 高/中/低 | <措施> |
+32
View File
@@ -0,0 +1,32 @@
# <模块/功能名称> 实施计划
> **计划日期**: YYYY-MM-DD
> **版本**: v1.0
> **状态**: 待执行/执行中/已完成
> **关联设计**: <链接到设计文档>
---
## 一、任务清单
### Phase 1: <阶段名称>
| 序号 | 任务 | 负责人 | 预估工时 | 状态 | 验证方式 |
|------|------|--------|---------|------|---------|
| 1.1 | <具体任务> | <负责人> | <工时> | ⬜ | <如何验证> |
---
## 二、进度跟踪
| 日期 | 完成任务 | 遇到的问题 | 解决方案 |
|------|---------|-----------|---------|
| YYYY-MM-DD | <任务> | <问题> | <方案> |
---
## 三、完成总结
- **实际工时**: <X 小时>
- **偏差分析**: <与预估的差异及原因>
- **经验教训**: <可复用的经验>
+25
View File
@@ -0,0 +1,25 @@
# 代码审查清单
## 基础规范
- [ ] 使用 Lombok,无手写 getter/setter
- [ ] Service 使用构造器注入
- [ ] Entity 继承 BaseEntity
- [ ] 类名不加 Rui 前缀(除非冲突)
## Mapper 规范
- [ ] SQL 使用 `#prefix#` 占位符
- [ ] 无硬编码表前缀
## Controller 规范
- [ ] 标准 CRUD 继承 BaseController
- [ ] URL 路径符合规范
- [ ] 使用正确注解(@Inner@AuthIgnore 等)
## 异常与日志
- [ ] 使用 BizException 而非 RuntimeException
- [ ] 返回 Result.ok()/Result.fail()
- [ ] 日志无敏感信息
## 测试
- [ ] 核心逻辑有单元测试
- [ ] 测试命名符合规范(should_ 开头)
+1 -1
View File
@@ -218,7 +218,7 @@ git push -u origin master
### 与框架主仓库的关系
```
spring-ai/ # 框架主仓库(git)
rui-framework/ # 框架主仓库(git)
├── backend/ # 提交到框架仓库
├── docs/ # 提交到框架仓库
├── app/ # 不提交到框架仓库(.gitignore
+1 -1
View File
@@ -9,7 +9,7 @@
## 🔗 通信方式
### 1. 模块 → 框架(spring-ai
### 1. 模块 → 框架(rui-framework
```java
// 使用 FeignClient 调用框架服务
+10 -10
View File
@@ -11,7 +11,7 @@
### 1.1 问题背景
rui 项目采用多仓结构:
- **后端仓库**`spring-ai`backend + app/*
- **后端仓库**`rui-framework`backend + app/*
- **前端仓库**`rui-frontend`admin-ui + cashier-mobile + customer-mobile
在使用 OpenCode AI 辅助开发时存在以下痛点:
@@ -35,8 +35,8 @@ rui 项目采用多仓结构:
### 2.1 仓库结构
```
# 后端仓库:spring-ai
spring-ai/
# 后端仓库:rui-framework
rui-framework/
├── backend/ # 基础框架
├── app/ # 应用模块(支付、收银等)
├── docs/ # 后端文档
@@ -54,8 +54,8 @@ rui-frontend/
| 角色 | 所属仓库 | 负责目录 | 可修改范围 | 只读范围 | 会话配置 |
|------|---------|---------|-----------|---------|---------|
| **框架开发** | spring-ai | `backend/` | `backend/**` | `app/**` | `framework.json` |
| **应用开发** | spring-ai | `app/{模块}/` | `app/{模块}/**` | `backend/**` | `app-module.json` |
| **框架开发** | rui-framework | `backend/` | `backend/**` | `app/**` | `framework.json` |
| **应用开发** | rui-framework | `app/{模块}/` | `app/{模块}/**` | `backend/**` | `app-module.json` |
| **前端开发** | rui-frontend | `admin-ui/` | `admin-ui/**` | `cashier-mobile/`, `customer-mobile/` | `admin-ui.json` |
### 2.2 职责矩阵
@@ -80,13 +80,13 @@ rui-frontend/
**协作流程**(跨仓库协作):
```
rui-frontend/ spring-ai/
rui-frontend/ rui-framework/
┌─────────────┐ ┌─────────────┐
│ 前端会话 │ │ 后端 │
│ (admin-ui) │ │ 会话 │
└──────┬──────┘ └──────┬──────┘
│ │
│ 在 spring-ai 仓库创建 │
│ 在 rui-framework 仓库创建 │
│ Issue [API-REQ] │
└────────────────────────→│
│ │
@@ -210,9 +210,9 @@ rui-frontend/ spring-ai/
启动 OpenCode 时,明确指定仓库和角色:
**后端仓库(spring-ai**
**后端仓库(rui-framework**
```
工作目录:/Users/zhangsheng/rhkj/spring-ai
工作目录:/Users/zhangsheng/rhkj/rui-framework
角色:框架开发
限制:只能修改 backend/ 下的代码
```
@@ -367,7 +367,7 @@ rui-frontend/ spring-ai/
- 需要评估影响范围
- 需要同步版本更新
### Q2: 前端仓库(rui-frontend)和后端仓库(spring-ai)是什么关系?
### Q2: 前端仓库(rui-frontend)和后端仓库(rui-framework)是什么关系?
**答**
- **独立仓库**:前端和后端是完全独立的 Git 仓库
+3 -3
View File
@@ -1,6 +1,6 @@
# 睿核科技 (rui) — 项目实施规范
> 本文档基于对当前 spring-ai 项目的全面分析,整理项目结构、业务流、现有规范、推荐修改项及缺少的专业结构,供后续项目实施参考。
> 本文档基于对当前 rui-framework 项目的全面分析,整理项目结构、业务流、现有规范、推荐修改项及缺少的专业结构,供后续项目实施参考。
---
@@ -8,7 +8,7 @@
| 项目属性 | 值 |
|---------|-----|
| **项目名称** | spring-ai |
| **项目名称** | rui-framework |
| **项目类型** | Spring Cloud 微服务架构(通用平台框架) |
| **组织方式** | Monorepobackend + 可扩展前端) |
| **JDK** | 21 |
@@ -436,5 +436,5 @@
> **文档版本**: v1.0
> **创建日期**: 2026-05-28
> **适用范围**: spring-ai 项目及后续同类项目
> **适用范围**: rui-framework 项目及后续同类项目
> **维护方式**: 随项目实施迭代更新
+63
View File
@@ -0,0 +1,63 @@
# 项目文档治理改进报告
> **报告日期**: 2026-06-02
> **治理范围**: 项目文档体系 + AGENTS.md + Superpowers 流程
> **执行人**: OpenCode AI
---
## 一、检查统计
| 维度 | 检查项数 | 发现问题 | 已修复 | 待修复 |
|------|---------|---------|--------|--------|
| 文档体系完整性 | 7 | 4 | 4 | 0 |
| 代码规范落地 | 8 | 待抽样确认 | - | - |
| 项目结构与配置 | 5 | 待确认 | - | - |
| Superpowers 就绪度 | 4 | 4 | 4 | 0 |
| **合计** | **24** | **8+** | **8** | **0** |
## 二、已完成的改进
### 2.1 文档体系
- [x] 创建 README.md(项目总览与快速开始)
- [x] 创建环境搭建指南(docs/environment-setup.md
- [x] 创建 GitNexus 独立指南(docs/gitnexus-guide.md
### 2.2 AGENTS.md 重构
- [x] 新增文档地图(第 1 章)
- [x] 新增环境准备(第 3 章)
- [x] 新增 Superpowers 工作流(第 4 章)
- [x] 新增模块开发指南(第 7 章)
- [x] 新增附录(第 10 章)
- [x] 优化编码规范、基础设施速查、运维规范、协作规范
- [x] 修复格式错误(** 标记不匹配)
- [x] 结构重组为 10 章体系,增加目录导航
### 2.3 Superpowers 流程
- [x] 创建设计文档模板
- [x] 创建实施计划模板
- [x] 创建代码审查清单
- [x] 建立 docs/superpowers/ 目录体系
## 三、提交记录
```
fa51237 docs(project): 添加项目 README.md
9ac019b docs(gitnexus): 创建独立的 GitNexus 使用指南
9fb19b1 docs(agents): 全面重构 AGENTS.md,建立 10 章规范体系
88646e9 docs(template): 添加 Superpowers 模板和环境搭建指南
```
## 四、后续建议
1. **代码规范自动化检查**: 考虑引入 Checkstyle 或 Spotless 自动检查代码规范
2. **文档交叉引用**: 在各文档间添加相互引用链接
3. **持续维护**: 每次规范变更时同步更新 AGENTS.md
4. **培训推广**: 向团队介绍 Superpowers 工作流
---
**报告完成 ✅**
+1 -1
View File
@@ -57,7 +57,7 @@ tabler:list → 列表演示
**历史变更**
- 早期使用 Element Plus 图标组件名(如 `SettingOutlined`
- 已执行升级脚本 `docs/sql/update_menu_icon.sql` 统一改为 `tabler:` 格式
- 已执行升级脚本 `sql/update_menu_icon.sql` 统一改为 `tabler:` 格式
---
@@ -0,0 +1,371 @@
# Admin-UI 分模块打包功能设计文档
> **设计日期**: 2026-06-04
> **版本**: v1.0
> **状态**: 已批准
> **目标**: 实现 Admin-UI 按系统配置分模块打包,支持不同租户类型输出不同产物包
---
## 一、背景与目标
### 1.1 现状分析
当前 Admin-UI 存在以下问题:
1. **路由硬编码**:所有页面路由集中在 `router/index.ts` 中硬编码,无法按模块裁剪
2. **构建产物单一**:无论服务哪个租户,都打包所有页面代码,产物体积大
3. **缺乏系统差异化**:Dashboard、登录页等核心页面无法根据不同系统定制
4. **模块管理已有雏形**:后端已支持租户模块配置(`ModuleDialog.vue`),但前端构建未与之配合
### 1.2 目标定义
1. **构建时分包**:根据 JSON 配置文件,构建时只打包指定模块的代码
2. **动态路由生成**:替换硬编码路由,构建时根据配置动态生成路由表
3. **系统差异化页面**:支持 Dashboard、登录页按系统配置加载不同子组件
4. **多产物输出**:不同系统输出到 `dist/{systemKey}/` 目录
5. **保持现有功能**:菜单 API 获取、权限控制、主题切换等功能不受影响
---
## 二、详细设计
### 2.1 整体架构
```
构建流程:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ build-config/ │ │ Vite Plugin │ │ 构建产物 │
│ cashier.json │────→│ (module-build) │────→│ dist/cashier/ │
│ admin.json │ │ │ │ dist/admin/ │
│ super.json │ │ 1. 读取配置 │ │ dist/super/ │
└─────────────────┘ │ 2. 生成路由 │ │ │
│ 3. 注入配置 │ └─────────────────┘
│ 4. 配置输出 │
└──────────────────┘
运行时:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 统一入口页面 │────→│ 虚拟模块配置 │────→│ 系统特定子组件 │
│ Dashboard │ │ __SYSTEM_CONFIG__│ │ systems/ │
│ Login │ │ │ │ Cashier.vue │
└─────────────────┘ └──────────────────┘ │ Super.vue │
└─────────────────┘
```
### 2.2 核心组件
#### 2.2.1 配置文件(`build-config/`
每个系统一个 JSON 配置文件:
```typescript
interface BuildConfig {
/** 系统唯一标识,产物目录名 */
key: string
/** 系统显示名称 */
name: string
/** 系统描述 */
description?: string
/** 包含的模块列表 */
modules: string[]
/** 登录页配置 */
login: {
/** 登录组件名(对应 views/login/systems/ 下的组件) */
component: string
/** 是否显示租户ID输入 */
showTenantInput: boolean
/** 页面标题 */
title: string
/** 副标题 */
subtitle?: string
/** 背景图路径 */
background?: string
/** Logo路径 */
logo?: string
}
/** Dashboard配置 */
dashboard: {
/** Dashboard组件名(对应 views/dashboard/systems/ 下的组件) */
component: string
/** 页面标题 */
title: string
}
/** 主题配置 */
theme: {
/** 主题色 */
primaryColor: string
/** 页面标题 */
title: string
}
}
```
**示例配置:**
```json
// build-config/super.json
{
"key": "super",
"name": "超级管理后台",
"description": "超级租户专用,包含租户管理",
"modules": ["system", "user"],
"login": {
"component": "Super",
"showTenantInput": false,
"title": "睿核平台管理",
"subtitle": "超级管理员登录"
},
"dashboard": {
"component": "Super",
"title": "平台运营概览"
},
"theme": {
"primaryColor": "#722ed1",
"title": "睿核平台管理"
}
}
```
```json
// build-config/cashier.json
{
"key": "cashier",
"name": "收银系统",
"description": "面向收银场景的管理后台",
"modules": ["system", "user", "cms", "cashier"],
"login": {
"component": "Cashier",
"showTenantInput": true,
"title": "睿核收银",
"subtitle": "门店管理系统"
},
"dashboard": {
"component": "Cashier",
"title": "收银数据概览"
},
"theme": {
"primaryColor": "#1677ff",
"title": "睿核收银"
}
}
```
#### 2.2.2 Vite 插件(`scripts/vite-plugin-module-build.ts`
插件职责:
1. **解析命令行参数**:读取 `--system={key}` 参数
2. **加载配置**:读取 `build-config/{key}.json`
3. **生成虚拟路由模块**`virtual:generated-routes`
- 根据 `config.modules` 从路由模板中组装路由表
- 只包含指定模块的路由 + 核心页面路由(登录、Dashboard入口、个人中心、设置)
4. **生成虚拟配置模块**`virtual:system-config`
- 将配置对象注入为全局常量 `__SYSTEM_CONFIG__`
5. **配置构建输出**
- `build.outDir = dist/${config.key}`
- `build.rollupOptions.treeshake = true` 确保未使用代码被移除
**路由生成逻辑:**
```typescript
// router/modules/system.ts
export const systemRoutes = [
{ path: 'system/menu', name: 'SystemMenu', component: () => import('@/views/system/menu/Index.vue'), meta: { i18n: 'menu.systemMenu' } },
{ path: 'system/role', name: 'SystemRole', component: () => import('@/views/system/role/Index.vue'), meta: { i18n: 'menu.systemRole' } },
// ...
]
// router/modules/user.ts
export const userRoutes = [
{ path: 'user/info', name: 'UserInfo', component: () => import('@/views/user/info/Index.vue'), meta: { i18n: 'menu.userInfo' } },
// ...
]
// 插件根据 config.modules 动态组装
const moduleRoutes = config.modules.flatMap(module => {
const routeModule = routeModules[module]
return routeModule ? routeModule.routes : []
})
```
#### 2.2.3 统一入口页面
**Dashboard 入口(`views/dashboard/Index.vue`):**
```vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import systemConfig from 'virtual:system-config'
// 动态加载系统特定的 Dashboard 组件
const dashboardComponent = defineAsyncComponent(() =>
import(`./systems/${systemConfig.dashboard.component}.vue`)
)
</script>
<template>
<component :is="dashboardComponent" />
</template>
```
**登录页入口(`views/login/Index.vue`):**
```vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import systemConfig from 'virtual:system-config'
// 动态加载系统特定的登录组件
const loginComponent = defineAsyncComponent(() =>
import(`./systems/${systemConfig.login.component}.vue`)
)
</script>
<template>
<component :is="loginComponent" :config="systemConfig.login" />
</template>
```
### 2.3 模块映射关系
建立 `src/views/` 下的页面目录与模块标识的映射:
| 模块标识 | 对应目录 | 包含页面 |
|---------|---------|---------|
| `system` | `views/system/*` | 菜单、角色、部门、岗位、字典、配置、日志、登录日志、租户、租户套餐、数据权限、OAuth2客户端 |
| `user` | `views/user/*` | 用户信息、用户详情、等级、等级日志、地址、账户 |
| `order` | `views/order/*` | 订单列表、退款记录 |
| `cms` | `views/cms/*` | 文章、分类、轮播图 |
| `marketing` | `views/marketing/*` | 优惠券、活动管理 |
| `demo` | `views/demo/*` | 图标演示、列表演示 |
| `cashier` | `views/cashier/*` | 门店、包间、定价、订单、商品、报表 |
**核心页面(所有系统默认包含,不依赖模块配置):**
- `views/login/Index.vue` - 登录页入口
- `views/login/systems/*.vue` - 系统特定登录组件
- `views/dashboard/Index.vue` - Dashboard 入口
- `views/dashboard/systems/*.vue` - 系统特定 Dashboard 组件
- `views/profile/Index.vue` - 个人中心
- `views/settings/Index.vue` - 系统设置
### 2.4 目录结构
```
admin-ui/
├── build-config/ # 系统打包配置
│ ├── cashier.json
│ ├── admin.json
│ ├── super.json
│ └── default.json # 默认配置(全模块,用于开发)
├── scripts/ # 构建脚本
│ └── vite-plugin-module-build.ts # Vite 插件
├── src/
│ ├── router/
│ │ ├── index.ts # 改造:使用虚拟路由模块
│ │ └── modules/ # 新增:按模块拆分路由配置
│ │ ├── core.ts # 核心路由(登录、Dashboard入口等)
│ │ ├── system.ts
│ │ ├── user.ts
│ │ ├── order.ts
│ │ ├── cms.ts
│ │ ├── marketing.ts
│ │ ├── demo.ts
│ │ └── cashier.ts
│ ├── views/
│ │ ├── login/
│ │ │ ├── Index.vue # 改造:统一入口
│ │ │ └── systems/ # 新增:系统特定登录组件
│ │ │ ├── Default.vue # 默认登录页
│ │ │ ├── Super.vue # 超级租户登录页
│ │ │ └── Cashier.vue # 收银系统登录页
│ │ ├── dashboard/
│ │ │ ├── Index.vue # 改造:统一入口
│ │ │ └── systems/ # 新增:系统特定 Dashboard
│ │ │ ├── Default.vue # 默认 Dashboard
│ │ │ ├── Cashier.vue # 收银系统 Dashboard
│ │ │ └── Super.vue # 超级租户 Dashboard
│ │ └── ... # 业务页面(保持现有结构)
│ ├── types/
│ │ └── system-config.d.ts # 系统配置类型定义
│ └── ...
├── package.json # 改造:添加构建命令
└── vite.config.ts # 改造:注册插件
```
### 2.5 构建命令
```json
// package.json
{
"scripts": {
"dev": "vite --port 3000",
"dev:cashier": "vite --port 3000 -- --system=cashier",
"dev:super": "vite --port 3000 -- --system=super",
"build": "vue-tsc && vite build",
"build:cashier": "vue-tsc && vite build -- --system=cashier",
"build:super": "vue-tsc && vite build -- --system=super",
"build:admin": "vue-tsc && vite build -- --system=admin",
"build:all": "pnpm build:cashier && pnpm build:super && pnpm build:admin"
}
}
```
**产物输出:**
```
dist/
├── cashier/ # 收银系统(system + user + cms + cashier
├── super/ # 超级租户(system + user
├── admin/ # 普通后台(system + user + order + cms + marketing
└── default/ # 默认(全模块,用于开发测试)
```
### 2.6 主题配置应用
系统配置中的 `theme` 字段在运行时应用:
```typescript
// App.vue 或布局组件
import systemConfig from 'virtual:system-config'
// 设置页面标题
document.title = systemConfig.theme.title
// 设置主题色(Element Plus
const el = document.documentElement
el.style.setProperty('--el-color-primary', systemConfig.theme.primaryColor)
```
---
## 三、验收标准
- [ ] 执行 `pnpm build:super` 成功构建,产物输出到 `dist/super/`,只包含 system 和 user 模块的页面
- [ ] 执行 `pnpm build:cashier` 成功构建,产物输出到 `dist/cashier/`,包含 system、user、cms、cashier 模块的页面
- [ ] 不同系统的 Dashboard 显示不同的子组件内容
- [ ] 不同系统的登录页显示不同的子组件内容(超级租户无租户ID输入)
- [ ] 构建产物中不包含未配置模块的页面代码(Tree Shaking 生效)
- [ ] 现有菜单 API 获取、权限控制、主题切换功能正常
- [ ] 开发模式 `pnpm dev:cashier` 正常工作,热更新无问题
---
## 四、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| Vite 插件开发复杂度 | 中 | 插件逻辑清晰拆分:配置读取、路由生成、虚拟模块、输出配置 |
| Tree Shaking 不彻底 | 中 | 使用 `import()` 动态导入,配合 Rollup 的 `treeshake` 配置,构建后检查产物 |
| 动态组件加载失败 | 低 | 添加错误处理,加载失败时回退到 Default 组件 |
| 现有功能回归 | 中 | 构建后逐一验证核心功能:登录、菜单、CRUD、主题切换 |
| 多人协作冲突 | 低 | 配置文件集中管理,模块路由独立文件,减少冲突 |
---
## 五、后续扩展
1. **国际化支持**:配置文件中可扩展 `locales` 字段,支持系统特定的翻译覆盖
2. **模块懒加载**:未来可考虑运行时动态加载模块(Module Federation
3. **版本管理**:配置文件支持 `version` 字段,用于产物版本控制
4. **CI/CD 集成**:构建命令可直接接入 Jenkins/GitHub Actions,参数化构建不同系统
+190 -1
View File
@@ -26,7 +26,68 @@
## 二、详细设计
### 2.1 整体架构
### 2.1 项目仓库
| 仓库 | 地址 | 说明 |
|------|------|------|
| 前端 | `ssh://git@git.vifo.cc:222/rui/rui-frontend.git` | admin-ui + 移动端 |
| 后端 | `ssh://git@git.vifo.cc:222/rui/rui-cashier.git` | 收银系统独立后端服务 |
| 文档 | `ssh://git@git.vifo.cc:222/rui/rui-docs.git` | 共享文档中心 |
> ⚠️ **注意**:收银系统相关 Issue 应提交到 `rui/rui-cashier` 仓库,不是 `rui-framework`
### 2.2 菜单数据结构
系统菜单使用以下 JSON 结构:
```json
{
"name": "cashier",
"menus": [
{
"code": "cashier",
"name": "收银系统",
"type": 1,
"icon": "tabler:money",
"sortNo": 100,
"children": [
{
"code": "store",
"name": "门店管理",
"type": 2,
"icon": "tabler:building-store",
"path": "/cashier/store",
"permission": "cashier:store:list",
"sortNo": 1,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "cashier:store:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "cashier:store:edit" },
{ "code": "btn:del", "name": "删除", "permission": "cashier:store:delete" }
]
}
]
}
]
}
```
**字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | string | 菜单编码,唯一标识 |
| `name` | string | 菜单显示名称 |
| `type` | int | 1=目录,2=菜单,3=按钮 |
| `icon` | string | 图标,支持 `tabler:` 前缀或 Element Plus 图标名 |
| `path` | string | 路由路径(type=2时必填) |
| `permission` | string | 权限标识,格式:`模块:功能:操作` |
| `sortNo` | int | 排序号,越小越靠前 |
| `children` | array | 子菜单列表 |
| `buttons` | array | 页面按钮权限(type=2时可选) |
**收银系统完整菜单配置见第 2.9 节。**
### 2.3 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
@@ -1123,4 +1184,132 @@ app/rui-cashier/
---
## 八、收银系统菜单配置
### 8.1 完整菜单 JSON
```json
{
"name": "cashier",
"menus": [
{
"code": "cashier",
"name": "收银系统",
"type": 1,
"icon": "tabler:money",
"sortNo": 100,
"children": [
{
"code": "store",
"name": "门店管理",
"type": 2,
"icon": "tabler:building-store",
"path": "/cashier/store",
"permission": "cashier:store:list",
"sortNo": 1,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "cashier:store:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "cashier:store:edit" },
{ "code": "btn:del", "name": "删除", "permission": "cashier:store:delete" }
]
},
{
"code": "room",
"name": "包间管理",
"type": 2,
"icon": "tabler:door",
"path": "/cashier/room",
"permission": "cashier:room:list",
"sortNo": 2,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "cashier:room:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "cashier:room:edit" },
{ "code": "btn:del", "name": "删除", "permission": "cashier:room:delete" }
]
},
{
"code": "product",
"name": "商品管理",
"type": 2,
"icon": "tabler:package",
"path": "/cashier/product",
"permission": "cashier:product:list",
"sortNo": 3,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "cashier:product:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "cashier:product:edit" },
{ "code": "btn:del", "name": "删除", "permission": "cashier:product:delete" }
]
},
{
"code": "pricing",
"name": "定价策略",
"type": 2,
"icon": "tabler:tags",
"path": "/cashier/pricing",
"permission": "cashier:pricing:list",
"sortNo": 4,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "cashier:pricing:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "cashier:pricing:edit" },
{ "code": "btn:del", "name": "删除", "permission": "cashier:pricing:delete" }
]
},
{
"code": "order",
"name": "订单管理",
"type": 2,
"icon": "tabler:file-invoice",
"path": "/cashier/order",
"permission": "cashier:order:list",
"sortNo": 5,
"buttons": [
{ "code": "btn:add", "name": "开台", "permission": "cashier:order:add" },
{ "code": "btn:checkout", "name": "结账", "permission": "cashier:order:checkout" },
{ "code": "btn:refund", "name": "退款", "permission": "cashier:order:refund" }
]
},
{
"code": "report",
"name": "营业报表",
"type": 2,
"icon": "tabler:chart-bar",
"path": "/cashier/report",
"permission": "cashier:report:list",
"sortNo": 6
}
]
}
]
}
```
### 8.2 权限标识汇总
| 模块 | 权限标识 | 说明 |
|------|----------|------|
| 门店 | `cashier:store:list` | 查询门店 |
| 门店 | `cashier:store:add` | 新增门店 |
| 门店 | `cashier:store:edit` | 编辑门店 |
| 门店 | `cashier:store:delete` | 删除门店 |
| 包间 | `cashier:room:list` | 查询包间 |
| 包间 | `cashier:room:add` | 新增包间 |
| 包间 | `cashier:room:edit` | 编辑包间 |
| 包间 | `cashier:room:delete` | 删除包间 |
| 商品 | `cashier:product:list` | 查询商品 |
| 商品 | `cashier:product:add` | 新增商品 |
| 商品 | `cashier:product:edit` | 编辑商品 |
| 商品 | `cashier:product:delete` | 删除商品 |
| 定价 | `cashier:pricing:list` | 查询定价策略 |
| 定价 | `cashier:pricing:add` | 新增定价策略 |
| 定价 | `cashier:pricing:edit` | 编辑定价策略 |
| 定价 | `cashier:pricing:delete` | 删除定价策略 |
| 订单 | `cashier:order:list` | 查询订单 |
| 订单 | `cashier:order:add` | 开台 |
| 订单 | `cashier:order:checkout` | 结账 |
| 订单 | `cashier:order:refund` | 退款 |
| 报表 | `cashier:report:list` | 查看报表 |
---
*文档结束*
+642
View File
@@ -0,0 +1,642 @@
# 支付服务(Payment)设计文档
> **设计日期**: 2026-06-08
> **版本**: v1.0
> **状态**: 设计中
> **适用前端**: admin-ui(管理后台)、管理 APP(接口一致)
> **后端服务**: rui-payment-api(端口 9401,网关路径 `/payment/**`
> **后端状态**: ⏳ 预留,API 细节待补充
---
## 一、背景与目标
### 1.1 现状
收银系统(rui-cashier)已完成基础订单流程(开台、结账、退款),支付环节目前使用占位接口。需要建立独立的支付服务(rui-payment-api),作为聚合支付网关,统一对接多种第三方支付通道。
### 1.2 目标
1. **聚合支付网关**:统一下单,按支付方式(微信、支付宝、银行卡等)自动路由到对应通道
2. **多通道可配置**:支持同时配置多个第三方支付平台,按门店/场景灵活启用
3. **商户进件**:商户在我们平台提交进件申请 → 平台审核 → 向第三方(微信、支付宝)提交进件 → 状态跟踪
4. **多端一致**:管理后台(Web)和管理 APP 共享同一套后端接口,前端按各自框架实现
### 1.3 不在范围内
- 团购券/优惠券核销
- 会员余额/储值卡支付
- 对账、清算(后续迭代)
- 顾客端支付流程(由 cashier-customer 负责,调用本服务下单接口)
---
## 二、整体架构
```
┌───────────────────────────────────────────────────────┐
│ 前端层 │
├─────────────────────┬─────────────────────────────────┤
│ 管理后台 (admin-ui)│ 管理 APP (uni-app) │
│ Vue3 + Element │ Vue3 语法 │
├─────────────────────┴─────────────────────────────────┤
│ 共享同一套后端 API │
│ /payment/admin/** │
└────────────────────────┬──────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ rui-gateway (:9300) │
│ 路由:/payment/** → rui-payment-api │
└────────────────────────┬───────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ rui-payment-api (:9401) │
├────────────┬──────────────┬──────────────┬─────────────┤
│ 商户管理 │ 商户进件 │ 支付通道管理 │ 交易管理 │
│ Merchant │ Onboarding │ Channel │ Transaction│
├────────────┴──────────────┴──────────────┴─────────────┤
│ 统一下单引擎 │
│ 按 payType 路由到对应第三方支付通道 │
├────────────────────────────────────────────────────────┤
│ 微信支付通道 │ 支付宝通道 │ 银行卡通道(预留) │
└────────────────────────────────────────────────────────┘
```
---
## 三、菜单与权限
### 3.1 菜单结构
```json
{
"name": "payment",
"menus": [
{
"code": "payment",
"name": "支付管理",
"type": 1,
"icon": "tabler:credit-card",
"sortNo": 110,
"children": [
{
"code": "payment-merchant",
"name": "商户管理",
"type": 2,
"icon": "tabler:building-bank",
"path": "/payment/merchant",
"permission": "payment:merchant:list",
"sortNo": 1,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "payment:merchant:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "payment:merchant:edit" },
{ "code": "btn:del", "name": "删除", "permission": "payment:merchant:delete" },
{ "code": "btn:detail", "name": "详情", "permission": "payment:merchant:detail" }
]
},
{
"code": "payment-onboarding",
"name": "商户进件",
"type": 2,
"icon": "tabler:clipboard-check",
"path": "/payment/onboarding",
"permission": "payment:onboarding:list",
"sortNo": 2,
"buttons": [
{ "code": "btn:apply", "name": "提交进件", "permission": "payment:onboarding:apply" },
{ "code": "btn:audit", "name": "审核", "permission": "payment:onboarding:audit" },
{ "code": "btn:submit", "name": "提交第三方", "permission": "payment:onboarding:submit" },
{ "code": "btn:detail", "name": "详情", "permission": "payment:onboarding:detail" }
]
},
{
"code": "payment-channel",
"name": "支付通道",
"type": 2,
"icon": "tabler:route",
"path": "/payment/channel",
"permission": "payment:channel:list",
"sortNo": 3,
"buttons": [
{ "code": "btn:add", "name": "新增", "permission": "payment:channel:add" },
{ "code": "btn:edit", "name": "编辑", "permission": "payment:channel:edit" },
{ "code": "btn:status", "name": "启停", "permission": "payment:channel:status" }
]
},
{
"code": "payment-transaction",
"name": "交易记录",
"type": 2,
"icon": "tabler:receipt",
"path": "/payment/transaction",
"permission": "payment:transaction:list",
"sortNo": 4
}
]
}
]
}
```
### 3.2 权限标识汇总
| 模块 | 权限标识 | 说明 |
|------|----------|------|
| 商户 | `payment:merchant:list` | 查询商户列表 |
| 商户 | `payment:merchant:add` | 新增商户 |
| 商户 | `payment:merchant:edit` | 编辑商户 |
| 商户 | `payment:merchant:delete` | 删除商户 |
| 商户 | `payment:merchant:detail` | 查看商户详情 |
| 进件 | `payment:onboarding:list` | 查询进件列表 |
| 进件 | `payment:onboarding:apply` | 提交进件申请 |
| 进件 | `payment:onboarding:audit` | 审核进件 |
| 进件 | `payment:onboarding:submit` | 提交至第三方 |
| 进件 | `payment:onboarding:detail` | 查看进件详情 |
| 通道 | `payment:channel:list` | 查询支付通道 |
| 通道 | `payment:channel:add` | 新增通道配置 |
| 通道 | `payment:channel:edit` | 编辑通道配置 |
| 通道 | `payment:channel:status` | 启用/禁用通道 |
| 交易 | `payment:transaction:list` | 查询交易记录 |
---
## 四、商户管理
### 4.1 页面结构
**列表页** `/payment/merchant`
| 字段 | 类型 | 宽度 | 说明 |
|------|------|------|------|
| merchantNo | string | 150 | 商户编号(系统生成) |
| merchantName | string | 150 | 商户名称 |
| contactName | string | 120 | 联系人 |
| contactPhone | string | 130 | 联系电话 |
| channelCount | number | 100 | 已开通通道数 |
| status | enum | 100 | 状态 |
| createTime | datetime | 160 | 创建时间 |
**查询条件**:商户编号、商户名称、状态
**操作按钮**:新增、编辑、详情、删除
**详情页/弹窗**:展示商户基础信息 + 已开通通道列表 + 进件记录
### 4.2 字段定义
```typescript
interface MerchantDTO {
id: number
/** 商户编号(系统生成,如 M202606080001 */
merchantNo: string
/** 商户名称 */
merchantName: string
/** 商户简称 */
merchantShortName: string
/** 联系人 */
contactName: string
/** 联系电话 */
contactPhone: string
/** 联系邮箱 */
contactEmail?: string
/** 商户地址 */
address?: string
/** 备注 */
remark?: string
/** 状态:0-禁用 1-启用 */
status: number
/** 已开通通道数(只读) */
channelCount?: number
createTime: string
updateTime: string
}
```
### 4.3 状态枚举
| 值 | 名称 | 说明 |
|----|------|------|
| 0 | 禁用 | 商户被禁用,无法交易 |
| 1 | 启用 | 正常状态 |
---
## 五、商户进件
### 5.1 业务流程
```
商户提交进件申请
平台初审(内部审核)
┌───┴───┐
│ │
驳回 通过
│ │
▼ ▼
退回 提交第三方
修改 │
┌───┴───┐
│ │
进件中 进件失败
│ │
▼ ▼
进件成功 查看原因 → 修改后重新提交
```
### 5.2 页面结构
**列表页** `/payment/onboarding`
| 字段 | 类型 | 宽度 | 说明 |
|------|------|------|------|
| merchantName | string | 150 | 商户名称 |
| channelName | string | 120 | 目标通道 |
| submitterName | string | 120 | 提交人 |
| status | enum | 120 | 进件状态 |
| auditStatus | enum | 120 | 内部审核状态 |
| thirdPartyStatus | enum | 120 | 第三方状态 |
| submitTime | datetime | 160 | 提交时间 |
| updateTime | datetime | 160 | 更新时间 |
**查询条件**:商户名称、目标通道、进件状态、提交时间范围
**操作按钮**:提交进件、审核、提交第三方、查看详情
**进件申请弹窗/页面**
分步表单,包含以下信息区域:
#### 5.2.1 基础信息
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| merchantId | select | ✅ | 关联商户(下拉选择) |
| channelType | select | ✅ | 目标通道(微信/支付宝/银行卡) |
| merchantType | select | ✅ | 商户类型(企业/个体工商户/小微商户) |
| merchantName | input | ✅ | 商户名称(营业执照上的名称) |
| merchantShortName | input | ✅ | 商户简称(对外展示) |
| licenseNo | input | ✅ | 营业执照号 |
| licenseExpireDate | date | ✅ | 营业执照有效期 |
| licensePhoto | upload | ✅ | 营业执照照片 |
#### 5.2.2 法人信息
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| legalPersonName | input | ✅ | 法人姓名 |
| legalPersonIdNo | input | ✅ | 法人身份证号 |
| legalPersonIdFront | upload | ✅ | 身份证正面 |
| legalPersonIdBack | upload | ✅ | 身份证反面 |
| legalPersonIdExpire | date | ✅ | 身份证有效期 |
#### 5.2.3 结算信息
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| settleType | select | ✅ | 结算方式(对公/对私) |
| bankName | input | ✅ | 开户银行 |
| bankBranch | input | ✅ | 开户支行 |
| bankAccountNo | input | ✅ | 银行账号 |
| bankAccountName | input | ✅ | 户名 |
| bankLicensePhoto | upload | 条件 | 开户许可证(对公必填) |
#### 5.2.4 其他材料
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| storePhotos | upload | ✅ | 门店照片(支持多张) |
| extraMaterials | upload | ❌ | 补充材料(支持多张) |
| remark | textarea | ❌ | 备注 |
### 5.3 字段定义
```typescript
interface MerchantOnboardingDTO {
id: number
/** 关联商户 ID */
merchantId: number
/** 目标通道类型 */
channelType: ChannelType
/** 商户类型:1-企业 2-个体工商户 3-小微商户 */
merchantType: number
/** 商户名称(营业执照) */
merchantName: string
/** 商户简称 */
merchantShortName: string
/** 营业执照号 */
licenseNo: string
/** 营业执照有效期 */
licenseExpireDate: string
/** 营业执照照片(文件 ID) */
licensePhoto: number
/** 法人姓名 */
legalPersonName: string
/** 法人身份证号 */
legalPersonIdNo: string
/** 身份证正面(文件 ID */
legalPersonIdFront: number
/** 身份证反面(文件 ID */
legalPersonIdBack: number
/** 身份证有效期 */
legalPersonIdExpire: string
/** 结算方式:1-对公 2-对私 */
settleType: number
/** 开户银行 */
bankName: string
/** 开户支行 */
bankBranch: string
/** 银行账号 */
bankAccountNo: string
/** 户名 */
bankAccountName: string
/** 开户许可证(文件 ID,对公必填) */
bankLicensePhoto?: number
/** 门店照片(文件 ID 数组) */
storePhotos: number[]
/** 补充材料(文件 ID 数组) */
extraMaterials?: number[]
/** 内部审核状态 */
auditStatus: AuditStatus
/** 第三方进件状态 */
thirdPartyStatus: ThirdPartyStatus
/** 第三方返回的商户号 */
thirdPartyMerchantNo?: string
/** 第三方返回原因(失败时) */
thirdPartyRejectReason?: string
/** 审核人 */
auditorName?: string
/** 审核时间 */
auditTime?: string
/** 审核意见 */
auditRemark?: string
/** 提交人 */
submitterName?: string
/** 提交时间 */
submitTime?: string
remark?: string
createTime: string
updateTime: string
}
```
### 5.4 状态枚举
**内部审核状态** `AuditStatus`
| 值 | 名称 | 说明 |
|----|------|------|
| 0 | 待提交 | 草稿,尚未提交审核 |
| 1 | 待审核 | 已提交,等待平台审核 |
| 2 | 审核通过 | 平台审核通过 |
| 3 | 审核驳回 | 平台审核驳回,可修改后重新提交 |
**第三方进件状态** `ThirdPartyStatus`
| 值 | 名称 | 说明 |
|----|------|------|
| 0 | 未提交 | 尚未提交至第三方 |
| 1 | 审核中 | 第三方审核中 |
| 2 | 进件成功 | 第三方审核通过,获得商户号 |
| 3 | 进件失败 | 第三方审核驳回 |
| 4 | 已冻结 | 第三方冻结商户 |
### 5.5 交互规则
1. **提交进件**:填写信息 → 提交内部审核(状态 → 待审核)
2. **内部审核**:审核通过 → 状态变为审核通过;驳回 → 填写原因,状态变为审核驳回
3. **提交第三方**:审核通过后,可提交至第三方(状态 → 审核中)
4. **状态同步**:第三方回调更新状态(成功/失败)
5. **重新提交**:审核驳回或第三方失败时,可修改后重新走流程
---
## 六、支付通道管理
### 6.1 页面结构
**列表页** `/payment/channel`
| 字段 | 类型 | 宽度 | 说明 |
|------|------|------|------|
| channelName | string | 150 | 通道名称 |
| channelType | enum | 120 | 通道类型 |
| channelProvider | string | 150 | 通道提供方 |
| merchantCount | number | 100 | 关联商户数 |
| status | enum | 100 | 状态 |
| createTime | datetime | 160 | 创建时间 |
**查询条件**:通道名称、通道类型、状态
**操作按钮**:新增通道、编辑、启用/禁用
**通道配置弹窗**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| channelName | input | ✅ | 通道名称(如"微信支付-主通道" |
| channelType | select | ✅ | 通道类型(见枚举) |
| channelProvider | select | ✅ | 通道提供方(微信/支付宝/银联等) |
| appId | input | ✅ | 应用 IDAppId/MchId |
| merchantId | input | ✅ | 商户号 |
| apiVersion | input | ❌ | API 版本 |
| certFile | upload | 条件 | 证书文件(部分通道需要) |
| notifyUrl | input | ✅ | 回调通知地址 |
| remark | textarea | ❌ | 备注 |
| status | radio | ✅ | 状态(启用/禁用) |
### 6.2 字段定义
```typescript
interface PayChannelDTO {
id: number
/** 通道名称 */
channelName: string
/** 通道类型 */
channelType: ChannelType
/** 通道提供方 */
channelProvider: string
/** 应用 ID */
appId: string
/** 商户号 */
merchantId: string
/** API 版本 */
apiVersion?: string
/** 证书文件(文件 ID */
certFile?: number
/** 回调通知地址 */
notifyUrl: string
/** 关联商户数(只读) */
merchantCount?: number
/** 状态:0-禁用 1-启用 */
status: number
remark?: string
createTime: string
updateTime: string
}
```
### 6.3 状态枚举
**通道类型** `ChannelType`
| 值 | 名称 | 说明 |
|----|------|------|
| WECHAT | 微信支付 | 微信 JSAPI / H5 / Native / APP |
| ALIPAY | 支付宝 | 支付宝网页/APP/扫码 |
| BANK_CARD | 银行卡 | 银行卡支付(预留) |
**通道提供方** `ChannelProvider`
| 值 | 名称 | 说明 |
|----|------|------|
| WECHAT_DIRECT | 微信直连 | 直接对接微信支付 |
| ALIPAY_DIRECT | 支付宝直连 | 直接对接支付宝 |
| LAKALA | 拉卡拉 | 聚合支付服务商 |
| SHOULIANGBA | 收钱吧 | 聚合支付服务商 |
| OTHER | 其他 | 其他聚合支付服务商 |
> 通道提供方后续可扩展,前端建议做成可配置的字典。
---
## 七、交易记录
### 7.1 页面结构
**列表页** `/payment/transaction`(只读)
| 字段 | 类型 | 宽度 | 说明 |
|------|------|------|------|
| transactionNo | string | 180 | 交易流水号 |
| merchantName | string | 150 | 商户名称 |
| channelName | string | 120 | 支付通道 |
| payType | enum | 100 | 支付方式 |
| amount | money | 120 | 交易金额 |
| fee | money | 100 | 手续费 |
| status | enum | 100 | 交易状态 |
| payTime | datetime | 160 | 支付时间 |
| createTime | datetime | 160 | 创建时间 |
**查询条件**:交易流水号、商户名称、支付方式、交易状态、时间范围
**操作**:查看详情
### 7.2 字段定义
```typescript
interface TransactionDTO {
id: number
/** 交易流水号 */
transactionNo: string
/** 关联商户 ID */
merchantId: number
/** 商户名称(冗余) */
merchantName: string
/** 支付通道 ID */
channelId: number
/** 通道名称(冗余) */
channelName: string
/** 支付方式 */
payType: ChannelType
/** 交易金额(元) */
amount: number
/** 手续费(元) */
fee: number
/** 交易状态 */
status: TransactionStatus
/** 第三方交易号 */
thirdPartyTransactionNo?: string
/** 支付时间 */
payTime?: string
/** 关联的业务订单号 */
bizOrderNo?: string
/** 退款金额(元) */
refundAmount?: number
/** 备注 */
remark?: string
createTime: string
updateTime: string
}
```
### 7.3 状态枚举
**交易状态** `TransactionStatus`
| 值 | 名称 | 说明 |
|----|------|------|
| 0 | 待支付 | 已创建,等待支付 |
| 1 | 支付中 | 正在处理 |
| 2 | 支付成功 | 支付完成 |
| 3 | 支付失败 | 支付失败 |
| 4 | 已关闭 | 超时关闭或手动关闭 |
| 5 | 已退款 | 全额退款 |
| 6 | 部分退款 | 部分退款 |
---
## 八、统一下单流程(前端视角)
```
顾客端(cashier-customer 支付服务(rui-payment-api 第三方
│ │ │
│ 1. 发起支付 │ │
│ (payType, amount, │ │
│ merchantId, bizOrderNo) │ │
│ ──────────────────────────────► │ │
│ │ 2. 按 payType 路由通道 │
│ │ 3. 创建交易记录 │
│ │ ──────────────────────────► │
│ │ 4. 调用第三方下单 │
│ 5. 返回支付参数 │ │
│ (二维码/跳转URL/支付SDK参数) │ │
│ ◄────────────────────────────── │ │
│ │ │
│ 6. 顾客完成支付 │ │
│ │ ◄────────────────────────── │
│ │ 7. 第三方回调通知 │
│ │ 8. 更新交易状态 │
│ 9. 查询支付结果 │ │
│ ──────────────────────────────► │ │
│ 10. 返回支付结果 │ │
│ ◄────────────────────────────── │ │
```
> 统一下单和支付回调的具体 API 定义待后端整理后补充。
---
## 九、前端页面清单
| 页面 | 路由 | 说明 |
|------|------|------|
| 商户列表 | `/payment/merchant` | 商户 CRUD |
| 商户详情 | `/payment/merchant/:id` | 商户信息 + 通道 + 进件记录 |
| 进件列表 | `/payment/onboarding` | 进件记录查询 |
| 进件申请 | `/payment/onboarding/apply` | 分步表单(新开页面) |
| 进件详情 | `/payment/onboarding/:id` | 进件详情 + 状态跟踪 |
| 支付通道列表 | `/payment/channel` | 通道配置管理 |
| 通道配置 | `/payment/channel/:id?` | 新增/编辑通道 |
| 交易记录 | `/payment/transaction` | 交易查询(只读) |
| 交易详情 | `/payment/transaction/:id` | 交易详情 |
---
## 十、待补充项
以下内容由后端整理后补充,前端文档标记为占位:
1. **API 接口定义**:各模块的请求路径、请求参数、响应结构
2. **统一下单接口**:下单、查询、回调的完整 API 定义
3. **通道配置参数**:不同通道的特定配置字段(如微信的 v3 密钥、支付宝的私钥等)
4. **对账相关**:对账单查询、差异处理(后续迭代)
5. **文件上传**:进件材料的 bizType 定义(参考 `API设计规范.md` 第 13 节存储服务)
---
> **下一步**: 后端整理 API 细节后,更新本文档的待补充项,然后前端可开始实现页面。
@@ -252,7 +252,7 @@ Add dialog component in template:
Run:
```bash
cd /Users/zhangsheng/rhkj/spring-ai/admin-ui
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
pnpm dev:cashier
```
@@ -1293,7 +1293,7 @@ git commit -m "feat(cashier): 完善订单管理开台功能"
Install echarts if not already installed:
```bash
cd /Users/zhangsheng/rhkj/spring-ai/admin-ui
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
pnpm add echarts vue-echarts
```
@@ -1322,7 +1322,7 @@ git commit -m "feat(cashier): 优化营业报表图表展示"
Run:
```bash
cd /Users/zhangsheng/rhkj/spring-ai/admin-ui
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
pnpm build:cashier
```
+135
View File
@@ -0,0 +1,135 @@
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.vifo.cc
- GITEA__server__ROOT_URL=https://git.vifo.cc
- GITEA__server__SSH_DOMAIN=git.vifo.cc
- GITEA__actions__ENABLED=true
- GITEA__webhook__ALLOWED_HOST_LIST=*
# 或者只允许特定网段:
# - GITEA__webhook__ALLOWED_HOST_LIST=172.22.0.0/16,172.17.0.0/16,localhost,127.0.0.1
restart: always
networks:
- gitea
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
# 可选:Gitea Actions Runner(执行 CI/CD 任务)
# 通用 Runner(保持默认)
runner-default:
image: gitea/act_runner:nightly
restart: always
container_name: runner-default
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "https://git.vifo.cc"
GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_TOKEN}"
GITEA_RUNNER_NAME: "runner-default"
GITEA_RUNNER_LABELS: "ubuntu-latest:docker://node:20-slim"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker:/root/.docker # 共享 Docker 配置
- ./config-default.yaml:/config.yaml
# npm/pnpm 缓存(用于前端任务)
- npm-cache:/root/.npm
- pnpm-store:/root/.local/share/pnpm/store
- pnpm-cache:/root/.cache/pnpm
networks:
- gitea
# Node.js 专用 Runner(前端项目)
runner-node:
image: gitea/act_runner:nightly
restart: always
container_name: runner-node
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "https://git.vifo.cc"
GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_TOKEN}"
GITEA_RUNNER_NAME: "runner-node"
GITEA_RUNNER_LABELS: "node:docker://node:20-slim,ubuntu-latest:docker://node:20-slim"
HTTP_PROXY: http://192.168.31.125:7899
HTTPS_PROXY: http://192.168.31.125:7899
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker:/root/.docker # 共享 Docker 配置
- ./config-node.yaml:/config.yaml
# npm 全局缓存(安装 pnpm 用)
- npm-cache:/root/.npm
# pnpm store 缓存(安装依赖用)
- pnpm-store:/root/.local/share/pnpm/store
# pnpm 全局包缓存
- pnpm-cache:/root/.cache/pnpm
# 构建产物输出目录(挂载到宿主机网站目录)
- /www/wwwroot/admin:/opt/builds
networks:
- gitea
# Java 专用 Runner(后端项目)
runner-java:
image: gitea/act_runner:nightly
container_name: runner-java
restart: always
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "https://git.vifo.cc"
GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_TOKEN}"
GITEA_RUNNER_NAME: "runner-java"
GITEA_RUNNER_LABELS: "java:docker://maven:3.9-eclipse-temurin-17,ubuntu-latest:docker://maven:3.9-eclipse-temurin-17"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker:/root/.docker # 共享 Docker 配置
- ./config-java.yaml:/config.yaml
# 缓存 Maven 依赖
- maven-repo:/root/.m2
networks:
- gitea
webhook-bridge:
build:
context: ./webhook-bridge
container_name: gitea-dingtalk-bridge
restart: always
ports:
- "3001:3001"
environment:
- PORT=3001
- DINGTALK_WEBHOOK=${DINGTALK_WEBHOOK_URL}
# 如果需要加签,取消下面注释并设置密钥
# - DINGTALK_SECRET=your-secret
networks:
- gitea
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
pnpm-store:
pnpm-cache:
npm-cache:
maven-repo:
networks:
gitea:
external: false
+142 -15
View File
@@ -116,10 +116,12 @@ GET /v1/user/users?username=admin&status=1&createdAt_start=2024-01-01&createdAt_
### 4.1 统一响应格式
> 详细规范见 [Result 统一响应类](Result统一响应类.md)
```json
{
"code": 200,
"msg": "操作成功",
"error": 0,
"message": "操作成功",
"data": {}
}
```
@@ -128,8 +130,8 @@ GET /v1/user/users?username=admin&status=1&createdAt_start=2024-01-01&createdAt_
```json
{
"code": 200,
"msg": "操作成功",
"error": 0,
"message": "操作成功",
"data": {
"records": [],
"total": 100,
@@ -159,19 +161,23 @@ GET /v1/user/users?username=admin&status=1&createdAt_start=2024-01-01&createdAt_
| 区间 | 模块 | 示例 |
|------|------|------|
| 1000-1999 | 通用 | 1001: 参数校验失败, 1002: 资源不存在 |
| 2000-2999 | 用户模块 | 2001: 用户名已存在, 2002: 密码错误 |
| 3000-3999 | 系统模块 | 3001: 字典不存在, 3002: 配置错误 |
| 4000-4999 | 认证模块 | 4001: Token 过期, 4002: 无权访问 |
| 5000-5999 | 文件模块 | 5001: 上传失败, 5002: 文件过大 |
| 6000-6999 | 消息模块 | 6001: 发送失败, 6002: 模板不存在 |
| 0 | 通用成功 | `0: 操作成功` |
| 1 | 通用失败 | `1: 操作失败` |
| 400-499 | HTTP 标准 | `401: 未授权, 404: 资源不存在` |
| 4000-4099 | 认证模块 | `4001: Token 过期, 4002: Token 无效` |
| 4100-4199 | 用户信息 | `4101: 用户不存在, 4102: 用户名已存在` |
| 4200-4299 | 用户等级 | `4201: 等级编码已存在` |
| 5000-5999 | 文件模块(预留) | `5001: 上传失败, 5002: 文件大小超限` |
| 6000-6999 | 消息模块(预留) | `6001: 发送失败, 6002: 模板不存在` |
> 完整枚举值见 [Result 统一响应类 → ResultCode 枚举](Result统一响应类.md#四resultcode-枚举)
**错误响应示例**
```json
{
"code": 2001,
"msg": "用户名已存在",
"data": null
"error": 4102,
"message": "用户名已存在",
"code": "USER_INFO_USERNAME_EXISTS"
}
```
@@ -321,7 +327,7 @@ public class UserRemoteService {
if (result.isSuccess()) {
return result.getData();
}
throw new BizException(result.getCode(), result.getMsg());
throw new BizException(result.getError(), result.getMessage());
}
}
```
@@ -428,16 +434,137 @@ springdoc:
### 12.2 访问地址
#### Swagger UI 页面
```
http://localhost:9300/swagger-ui.html # 网关聚合文档
http://localhost:9301/swagger-ui.html # 认证服务文档
http://localhost:9302/swagger-ui.html # 系统服务文档
http://localhost:9303/swagger-ui.html # 用户服务文档
http://localhost:9601/swagger-ui.html # 收银服务文档
```
#### Knife4j 增强 UI(推荐)
```
http://localhost:9300/doc.html # 网关聚合文档
http://localhost:9301/doc.html # 认证服务文档
http://localhost:9302/doc.html # 系统服务文档
http://localhost:9303/doc.html # 用户服务文档
http://localhost:9601/doc.html # 收银服务文档
```
#### OpenAPI JSON 接口
```
http://localhost:9300/v3/api-docs # 网关聚合 API 文档
http://localhost:9301/v3/api-docs # 认证服务 API 文档
http://localhost:9302/v3/api-docs # 系统服务 API 文档
http://localhost:9303/v3/api-docs # 用户服务 API 文档
http://localhost:9399/v3/api-docs # 聚合启动器 API 文档(开发调试)
http://localhost:9601/v3/api-docs # 收银服务 API 文档
```
---
## 十三、Postman / APIFox 集合规范
## 十三、文件存储服务(rui-service-storage
> **服务定位**:独立微服务(9400 端口 / 聚合启动器 9399),所有业务模块共用一个上传入口,通过 `bizType` 区分业务场景。
> **前端组件**`<RuiUpload>`[rui-frontend#5](https://git.vifo.cc/rui/rui-frontend/issues/5))。
### 13.1 上传文件
```
POST /storage/upload
Content-Type: multipart/form-data
```
**表单参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file` | File | ✅ | 文件本体 |
| `bizType` | string | ✅ | 业务类型,匹配 `^[A-Z][A-Z0-9_]{0,50}$` |
| `storage` | string | ❌ | `aliyun` / `tencent` / `local`;不传走默认 |
| `fileName` | string | ❌ | 固定存储名,匹配 `^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$` |
| `extract` | bool | ❌ | `true` 时若为 .zip 自动解压为多文件入库 |
**响应**`data: SysFileUploadVO[]`(**统一为数组**,单文件上传也是长度 1)。
```json
{
"code": 0,
"message": "ok",
"data": [
{
"id": 1234567890,
"name": "abc123.jpg",
"originalName": "photo.jpg",
"path": "user-avatar/2026/06/abc123.jpg",
"url": "https://bucket.oss-cn-shanghai.aliyuncs.com/user-avatar/2026/06/abc123.jpg",
"size": 12345,
"contentType": "image/jpeg",
"storageType": "ALIYUN",
"bizType": "USER_AVATAR"
}
]
}
```
### 13.2 查询文件详情
```
GET /storage/file/{id}
```
### 13.3 分页查询
```
GET /storage/file/page?pageNum=1&pageSize=20&bizType=SYS_APP_CERT
```
### 13.4 删除文件
```
DELETE /storage/file/{id}
```
物理删除对象存储文件 + 软删 `sys_file` 记录。
### 13.5 已知 bizType
| bizType | 限制 | 用途 |
|---------|------|------|
| `COMMON` | 10MB | 通用 |
| `SYS_APP_CERT` | 5MB / pem,crt,key,p12 | 第三方应用证书 |
| `USER_AVATAR` | 2MB / jpg,jpeg,png,webp | 用户头像 |
| `CMS_BANNER` | 5MB / jpg,jpeg,png,webp,gif | CMS 轮播图 |
新业务模块直接传新字符串(如 `ORDER_PROOF`),后端 yml 配 `rui.file.biz-types.<新>.max-size` / `allowed-extensions` 即可,**前端不需要等后端发版**。
### 13.6 前端使用示例
```vue
<script setup>
import RuiUpload from '@/components/RuiUpload/RuiUpload.vue'
import type { UploadResult } from '@/service/system/storageService'
const certFiles = ref<UploadResult[]>([])
</script>
<template>
<RuiUpload
v-model="certFiles"
biz-type="SYS_APP_CERT"
:max-size="20"
accept=".pem,.crt,.key,.p12,.zip"
:extract="false"
/>
</template>
```
---
## 十四、Postman / APIFox 集合规范
### 13.1 目录结构
+332
View File
@@ -0,0 +1,332 @@
# Result<T> 统一响应类规范
> RUI 框架所有 API 接口的统一返回包装
---
## 一、类信息
| 属性 | 值 |
|------|-----|
| 包路径 | `com.rui.common.core.result.Result` |
| 所在模块 | `rui-common-core` |
| 泛型参数 | `<T>` — data 字段的具体类型 |
| 序列化 | 实现 `Serializable` |
| JSON 策略 | `@JsonInclude(NON_NULL)` — 值为 `null` 的字段自动忽略 |
---
## 二、响应结构
### 2.1 字段定义
| 字段 | 类型 | 说明 | 成功时 | 失败时 |
|------|------|------|--------|--------|
| `error` | `int` | 状态码(0 = 成功) | `0` | 非 0 |
| `message` | `String` | 提示信息 | `"操作成功"` | 具体错误描述 |
| `code` | `String` | 业务错误码 | `null`(不输出) | 如 `"AUTH_UNAUTHORIZED"` |
| `data` | `T` | 业务数据 | 实际数据 | 通常为 `null`(不输出) |
### 2.2 成功响应示例
**无数据返回:**
```json
{
"error": 0,
"message": "操作成功"
}
```
**带数据返回:**
```json
{
"error": 0,
"message": "操作成功",
"data": {
"id": 1001,
"username": "admin"
}
}
```
**分页数据:**
```json
{
"error": 0,
"message": "操作成功",
"data": {
"records": [],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
}
}
```
### 2.3 失败响应示例
**通用失败:**
```json
{
"error": 1,
"message": "操作失败"
}
```
**带业务错误码:**
```json
{
"error": 401,
"message": "未授权",
"code": "AUTH_UNAUTHORIZED"
}
```
**带数据(未找到场景):**
```json
{
"error": 404,
"message": "数据不存在",
"code": "DATA_NOT_FOUND",
"data": "dictCode_001"
}
```
> `data` 字段在 `failNotFound` 场景下用于传递资源 key,便于前端做国际化模板替换。
---
## 三、静态工厂方法
### 3.1 成功系列
| 方法签名 | 说明 | 使用场景 |
|----------|------|----------|
| `Result.ok()` | 无数据成功 | 删除、更新等不需要返回数据的操作 |
| `Result.ok(T data)` | 带数据成功 | 查询详情、列表、新增返回实体 |
### 3.2 失败系列
| 方法签名 | 说明 | 使用场景 |
|----------|------|----------|
| `Result.fail()` | 通用失败 | 兜底异常、未知错误 |
| `Result.fail(String message)` | 自定义提示 | 需要特定提示信息的业务异常 |
| `Result.fail(ResultCode resultCode)` | 枚举驱动 | 标准业务错误,推荐使用 |
| `Result.fail(int error, String message)` | 自定义错误码+提示 | 非标准错误场景 |
| `Result.fail(int error, String message, String code)` | 完全自定义 | 需要同时指定三个字段 |
| `Result.fail(ResultCode resultCode, T data)` | 枚举+数据 | 失败时需携带部分数据 |
| `Result.failNotFound(ResultCode resultCode, String key)` | 404 未找到 | 数据不存在,key 放入 data |
### 3.3 判断方法
| 方法 | 说明 |
|------|------|
| `result.isSuccess()` | 判断是否成功(`error == 0` |
| `result.toJsonString()` | 序列化为 JSON 字符串 |
---
## 四、ResultCode 枚举
### 4.1 枚举结构
| 属性 | 类型 | 说明 |
|------|------|------|
| `error` | `int` | HTTP 风格状态码,0 为成功 |
| `message` | `String` | 默认提示文本 |
| `code` | `String` | 业务错误码(大写蛇形,模块前缀) |
### 4.2 枚举值一览
| 枚举值 | error | message | code | 说明 |
|--------|-------|---------|------|------|
| `SUCCESS` | 0 | 操作成功 | `null` | 成功 |
| `FAILURE` | 1 | 操作失败 | `null` | 通用失败 |
| `UNAUTHORIZED` | 401 | 未授权 | `AUTH_UNAUTHORIZED` | 未登录 |
| `FORBIDDEN` | 403 | 无权限 | `AUTH_FORBIDDEN` | 无权限 |
| `NOT_FOUND` | 404 | 资源不存在 | `COMMON_NOT_FOUND` | 资源未找到 |
| `DATA_NOT_FOUND` | 404 | 数据不存在 | `DATA_NOT_FOUND` | 数据未找到 |
| `VALIDATE_FAILED` | 400 | 参数校验失败 | `COMMON_VALIDATE_FAILED` | 参数错误 |
| `TOKEN_EXPIRED` | 4001 | Token 已过期 | `AUTH_TOKEN_EXPIRED` | Token 过期 |
| `TOKEN_INVALID` | 4002 | Token 无效 | `AUTH_TOKEN_INVALID` | Token 无效 |
| `TENANT_NOT_FOUND` | 4003 | 租户不存在 | `AUTH_TENANT_NOT_FOUND` | 租户不存在 |
| `TENANT_DISABLED` | 4004 | 租户已禁用 | `AUTH_TENANT_DISABLED` | 租户禁用 |
| `USER_NOT_FOUND` | 4101 | 用户不存在 | `USER_INFO_NOT_FOUND` | 用户不存在 |
| `USERNAME_EXISTS` | 4102 | 用户名已存在 | `USER_INFO_USERNAME_EXISTS` | 用户名重复 |
| `LEVEL_CODE_EXISTS` | 4201 | 等级编码已存在 | `USER_LEVEL_CODE_EXISTS` | 等级编码重复 |
### 4.3 错误码规划规则
| 区间 | 模块 | code 前缀 |
|------|------|-----------|
| 0 | 通用成功 | — |
| 1 | 通用失败 | — |
| 400-499 | HTTP 标准错误 | `COMMON_*` / `AUTH_*` |
| 4000-4099 | 认证错误 | `AUTH_*` |
| 4100-4199 | 用户信息错误 | `USER_INFO_*` |
| 4200-4299 | 用户等级错误 | `USER_LEVEL_*` |
| 5000-5999 | 文件模块(预留) | `FILE_*` |
| 6000-6999 | 消息模块(预留) | `MSG_*` |
**新增规则**:新模块取 100 的整数倍区间,code 格式为 `{模块}_{业务}_{具体}`
---
## 五、Controller 使用示例
### 5.1 标准 CRUD
```java
@RestController
@RequestMapping("/v1/system/roles")
@RequiredArgsConstructor
public class SysRoleController {
private final SysRoleService roleService;
@GetMapping
public Result<IPage<SysRoleVO>> list(SysRoleQuery query) {
return Result.ok(roleService.page(query));
}
@GetMapping("/{id}")
public Result<SysRoleVO> getById(@PathVariable Long id) {
SysRoleVO role = roleService.getById(id);
if (role == null) {
return Result.failNotFound(ResultCode.DATA_NOT_FOUND, String.valueOf(id));
}
return Result.ok(role);
}
@PostMapping
public Result<SysRoleVO> create(@RequestBody @Valid SysRoleDTO dto) {
return Result.ok(roleService.create(dto));
}
@PutMapping("/{id}")
public Result<SysRoleVO> update(@PathVariable Long id, @RequestBody @Valid SysRoleDTO dto) {
return Result.ok(roleService.update(id, dto));
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
roleService.delete(id);
return Result.ok();
}
}
```
### 5.2 异常处理中返回
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(BizException e) {
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.fail(ResultCode.VALIDATE_FAILED.getError(), message, ResultCode.VALIDATE_FAILED.getCode());
}
}
```
### 5.3 Feign 远程调用中处理 Result
```java
@Service
@RequiredArgsConstructor
public class UserRemoteService {
private final RemoteUserService remoteUserService;
public UserVO getUserById(Long userId) {
Result<UserVO> result = remoteUserService.getById(userId);
if (result.isSuccess()) {
return result.getData();
}
throw new BizException(result.getError(), result.getMessage());
}
}
```
---
## 六、前端对接指南
### 6.1 判断成功
```typescript
// response.data 为 Result<T> 结构
const isSuccess = response.data.error === 0;
```
### 6.2 错误处理
```typescript
if (result.error !== 0) {
// 优先用 code 做国际化
if (result.code) {
showI18nMessage(result.code, { key: result.data });
} else {
// 降级显示 message
showMessage(result.message);
}
}
```
### 6.3 TypeScript 类型定义
```typescript
interface Result<T = any> {
error: number;
message: string;
code?: string; // 失败时存在
data?: T; // 成功时或 failNotFound 时存在
}
```
---
## 七、扩展指南
### 7.1 新增 ResultCode
`ResultCode` 枚举中添加新值,遵循以下规则:
1. **error 取值**:按模块区间分配(见 4.3 节)
2. **code 命名**`{模块}_{业务}_{具体}`,全大写蛇形
3. **向后兼容**:禁止修改已有枚举值的 error 或 code
```java
// 示例:文件模块新增
FILE_UPLOAD_FAILED(5001, "文件上传失败", "FILE_UPLOAD_FAILED"),
FILE_SIZE_EXCEEDED(5002, "文件大小超限", "FILE_SIZE_EXCEEDED"),
```
### 7.2 禁止事项
- ❌ 不要在 Controller 中直接 `new Result<>()`,必须使用静态工厂方法
- ❌ 不要修改 `Result` 类的字段名(`error`/`message`/`code`/`data`),影响序列化兼容
- ❌ 不要用 `error` 字段传递 HTTP 状态码(它只是业务状态码,HTTP 状态码由框架控制)
- ❌ 不要在 `code` 字段中使用小写或特殊字符
---
> **文档版本**: v1.0
> **创建日期**: 2026-06-08
> **源码位置**: `rui-common/rui-common-core/src/main/java/com/rui/common/core/result/`
> **适用范围**: RUI 框架所有模块的 API 响应
+139
View File
@@ -189,6 +189,27 @@ CREATE TABLE example_table (
) COMMENT='示例表';
```
### MyBatis Plus 查询规范
**优先使用 `LambdaQueryWrapper`,避免使用字符串字段名的 `QueryWrapper`。**
```java
// ❌ 错误示例:使用字符串字段名,容易拼写错误,重构时容易遗漏
new QueryWrapper<User>().eq("user_name", username)
.like("phone", phone);
// ✅ 正确示例:使用 LambdaQueryWrapper,类型安全,重构友好
new LambdaQueryWrapper<User>()
.eq(User::getUserName, username)
.like(User::getPhone, phone);
```
**优势:**
- **类型安全**:编译期检查,字段不存在会报错
- **防误写**:避免字符串拼写错误
- **重构友好**:IDE 重构时自动更新引用
- **可读性**:直接看到实体字段,更清晰
---
## 🔒 安全规范
@@ -278,3 +299,121 @@ Closes #123
---
> **最后提醒**:编码规范是为了团队协作,请务必遵守!
---
## 🌐 Feign 客户端注册规范
`rui-common-feign` 提供自定义 Feign 注册机制(`CloudFeignAutoConfiguration` +
`CustomFeignClientsRegistrar`),**与 Spring Cloud 默认的包扫描机制不同**。
### 注册渠道
所有 `@FeignClient` 接口**必须**列在 `META-INF/spring.factories` 中:
```properties
# rui-common-{module}/src/main/resources/META-INF/spring.factories
com.rui.common.feign.CloudFeignAutoConfiguration=\
com.rui.{module}.feign.YourFeignClient,\
com.rui.{module}.feign.AnotherFeignClient
```
### 为什么不能只靠包扫描
- 项目使用自定义的 `CustomFeignClientsRegistrar`**只有当 `@CloudEnableFeignClients`
注解存在时才会触发包扫描**
- 项目**零处**使用 `@CloudEnableFeignClients` 注解
- 因此 `spring.factories` 是项目 Feign 客户端的**唯一**注册渠道
### 添加新 FeignClient 步骤
1. 定义 `@FeignClient` 接口(带 `contextId` / `path` / `fallbackFactory`
2. **必须**在 `META-INF/spring.factories` 中追加类名
3. 漏写第 2 步 → Bean 未注册 → 运行时 NPE"no qualifying bean of type ..."
### ❌ 禁止
- 只定义 `@FeignClient` 接口但忘了列 `spring.factories`(最常见的坑)
- 期待 Spring Cloud 默认的包扫描会帮你发现(项目里不会)
---
## 📦 Result 返回规范
`com.rui.common.core.result.Result` 是统一的 API 响应封装。所有 controller 必须遵守:
### 字段语义
| 字段 | 类型 | 用途 |
|---|---|---|
| `error` | int | HTTP 风格状态码(200/400/401/403/404/500/503 等) |
| `code` | String | **业务编码,前端 i18n key**(如 `DATA_NOT_FOUND` |
| `message` | String | 默认中文提示,可由前端 i18n 覆盖 |
| `data` | T | 业务数据 |
### 调用规范
| 场景 | 调用方式 | 备注 |
|---|---|---|
| 成功 | `Result.ok(data)` | data 可为 null,但**列表场景应返回 `emptyList`** |
| 业务校验失败 | `Result.fail(400, "msg")``Result.fail(ResultCode.X, "msg")` | 优先用枚举 |
| 未授权 | `Result.fail(401, "msg")` | 框架层 `GlobalExceptionHandler` 统一处理 |
| 无权限 | `Result.fail(403, "msg")` | 同上 |
| **数据不存在** | **`Result.failNotFound(ResultCode.DATA_NOT_FOUND, key)`** | **推荐写法**:key 放 data 字段便于前端模板替换 |
| 资源不存在(泛指) | `Result.fail(ResultCode.NOT_FOUND)` | 不带 key 的场景 |
| 服务降级 | `Result.fail(503, "服务降级: msg")` | Feign fallback 等场景 |
| 通用失败 | `Result.fail("msg")` | 兜底 |
### ❌ 禁止写法
- **`Result.ok(null)` 表示"未找到"** —— 反直觉(HTTP 200 + null),且与 `fail(404)` 语义冲突
- **message 中拼接 key** —— 如 `"字典不存在: " + dictCode`,应该用 `failNotFound(DATA_NOT_FOUND, dictCode)` 让前端用 i18n 模板 `"字典[${data}]不存在"`
- **数字 code 字符串比较** —— 应该用 `ResultCode` 枚举的 `getCode()` 字符串
### ✅ 正确示例
```java
// 查询接口 - 数据不存在
@GetMapping("/dict/getByCode/{dictCode}")
public Result<Map<String, Object>> getDictByCode(@PathVariable String dictCode) {
SysDictType dict = dictTypeService.findByCode(dictCode);
if (dict == null) {
return Result.failNotFound(ResultCode.DATA_NOT_FOUND, dictCode);
}
return Result.ok(buildDictResult(dict));
}
// 业务校验失败
@PostMapping("/save")
public Result<Void> save(@RequestBody @Valid SysDictDTO dto) {
if (dictService.isCodeExists(dto.getCode())) {
return Result.fail(400, "字典编码已存在: " + dto.getCode());
}
return Result.ok();
}
// 列表查询(即使是空也要返回空集合)
@GetMapping("/list")
public Result<List<SysDict>> list() {
return Result.ok(dictService.list()); // 不要 Result.ok(null)
}
```
### i18n 配合示例
前端拿到 `Result` 后:
```javascript
const i18nMap = {
'DATA_NOT_FOUND': '数据[{0}]不存在', // 占位符 {0} 用 data 字段填充
'AUTH_UNAUTHORIZED': '请先登录',
};
if (result.code === 'DATA_NOT_FOUND') {
showError(i18nMap['DATA_NOT_FOUND'].replace('{0}', result.data));
}
```
### 相关枚举
- `com.rui.common.core.result.ResultCode` —— 业务 code 枚举(404 用 `DATA_NOT_FOUND`401 用 `UNAUTHORIZED` 等)
- 新增业务 code 时在 `ResultCode` 加枚举值,**不要直接 `Result.fail(int, String, String)` 硬编码字符串**
+1 -1
View File
@@ -1,6 +1,6 @@
# 数据库设计规范分析报告
> 基于对当前 spring-ai 项目数据库设计的全面审查,本报告列出所有不合理之处及专业改进方案。
> 基于对当前 rui-framework 项目数据库设计的全面审查,本报告列出所有不合理之处及专业改进方案。
> **注意:本报告仅做分析,不做任何代码实施。**
---
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,725 @@
# 用户聚合查询实施计划
> **日期**: 2026-06-06
> **状态**: 已完成
> **关联 Spec**: `docs/superpowers/specs/2026-06-06-user-aggregate-query-design.md`
---
## 1. 任务总览
### 1.1 任务清单
| 编号 | 任务名称 | 优先级 | 预估时间 | 依赖 |
|------|---------|--------|---------|------|
| T1 | 数据库变更:uc_user 添加 phone 字段 | 高 | 20分钟 | 无 | ✅ |
| T2 | 数据库变更:uc_user_detail 移除 phone 字段 | 高 | 15分钟 | T1 | ✅ |
| T3 | 修改 User 实体:添加 phone 字段 | 高 | 15分钟 | T1 | ✅ |
| T4 | 修改 UserDetail 实体:移除 phone 字段 | 高 | 10分钟 | T2 | ✅ |
| T5 | 新增 VO 对象:UserAggregateVO, UserDeptVO, UserPostVO | 高 | 20分钟 | 无 | ✅ |
| T6 | 新增枚举:AccountType | 高 | 10分钟 | 无 | ✅ |
| T7 | 新增 DTOLoginAccountDTO | 高 | 10分钟 | T6 | ✅ |
| T8 | 修改 UserDeptMapper:添加批量查询方法 | 高 | 20分钟 | 无 | ✅ |
| T9 | 修改 UserPostMapper:添加批量查询方法 | 高 | 20分钟 | 无 | ✅ |
| T10 | 修改 UserService:添加聚合查询方法 | 高 | 30分钟 | T3, T5, T8, T9 | ✅ |
| T11 | 修改 UserController:添加聚合接口 | 高 | 20分钟 | T10 | ✅ |
| T12 | 修改 UserInnerController:添加统一认证接口 | 高 | 25分钟 | T3, T7 | ✅ |
| T13 | 修改 UserAuthFeign:添加统一认证方法 | 高 | 15分钟 | T12 | ✅ |
| T14 | 修改 RemoteUserDetailsService:支持新接口 | 高 | 20分钟 | T13 | ✅ |
| T15 | 添加缓存:Redis 缓存用户聚合数据 | 中 | 25分钟 | T10 | ✅ |
| T16 | 缓存失效:数据变更时清除缓存 | 中 | 20分钟 | T15 | ✅ |
| T17 | 编写 SQL 升级脚本 | 高 | 15分钟 | T1, T2 | ✅ |
| T18 | 单元测试和编译验证 | 中 | 40分钟 | T10, T12 | ✅ |
| T19 | 集成测试(编译通过) | 中 | 30分钟 | T18 | ✅ |
| T20 | 文档更新 | 低 | 15分钟 | 全部 | ✅ |
### 1.2 依赖关系图
```
T1 (数据库添加phone)
├── T3 (User实体添加phone)
│ ├── T10 (UserService聚合查询)
│ │ ├── T11 (UserController聚合接口)
│ │ ├── T15 (Redis缓存)
│ │ │ └── T16 (缓存失效)
│ │ └── T18 (单元测试)
│ └── T12 (统一认证接口)
│ ├── T13 (UserAuthFeign)
│ │ └── T14 (RemoteUserDetailsService)
│ └── T18 (单元测试)
├── T7 (LoginAccountDTO)
│ └── T12 (统一认证接口)
└── T17 (SQL脚本)
T2 (数据库移除phone)
└── T4 (UserDetail实体移除phone)
T5 (VO对象)
└── T10 (UserService聚合查询)
T6 (AccountType枚举)
├── T7 (LoginAccountDTO)
└── T12 (统一认证接口)
T8 (UserDeptMapper批量查询)
└── T10 (UserService聚合查询)
T9 (UserPostMapper批量查询)
└── T10 (UserService聚合查询)
T18 (单元测试)
└── T19 (集成测试)
T20 (文档更新)
```
---
## 2. 详细任务
### T1: 数据库变更 - uc_user 添加 phone 字段
**目标**: 在 `uc_user` 表添加 `phone` 字段并创建索引
**步骤**:
1. [ ] 编写 SQL
```sql
ALTER TABLE rui_uc_user
ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER username;
ALTER TABLE rui_uc_user
ADD UNIQUE KEY uk_phone (tenant_id, phone);
ALTER TABLE rui_uc_user
ADD INDEX idx_phone (phone);
```
2. [ ] 在开发环境执行 SQL
3. [ ] 验证表结构:`DESCRIBE rui_uc_user;`
4. [ ] 验证索引:`SHOW INDEX FROM rui_uc_user;`
**验证标准**:
- `phone` 字段存在
- `uk_phone` 唯一索引存在
- `idx_phone` 普通索引存在
**风险**: 生产环境需要谨慎,建议在低峰期执行
---
### T2: 数据库变更 - uc_user_detail 移除 phone 字段
**目标**: 从 `uc_user_detail` 表移除 `phone` 字段
**步骤**:
1. [ ] 备份数据(可选)
2. [ ] 编写 SQL
```sql
-- 先迁移数据(如果有)
-- UPDATE rui_uc_user u
-- JOIN rui_uc_user_detail d ON u.id = d.user_id
-- SET u.phone = d.phone
-- WHERE u.phone IS NULL AND d.phone IS NOT NULL;
ALTER TABLE rui_uc_user_detail
DROP COLUMN phone;
```
3. [ ] 在开发环境执行 SQL
4. [ ] 验证表结构
**验证标准**:
- `phone` 字段已移除
- 其他字段不受影响
**风险**: 确保数据已迁移或不再使用
---
### T3: 修改 User 实体 - 添加 phone 字段
**目标**: 在 `User.java` 实体中添加 `phone` 字段
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/entity/User.java`
**步骤**:
1. [ ] 添加字段:
```java
@Schema(description = "手机号")
@SearchField(alias = "phone")
private String phone;
```
2. [ ] 确保字段位置在 `username` 之后
3. [ ] 编译验证
**验证标准**:
- `User` 实体可以正常编译
- `phone` 字段有 getter/setter@Data 自动生成)
---
### T4: 修改 UserDetail 实体 - 移除 phone 字段
**目标**: 从 `UserDetail.java` 实体中移除 `phone` 字段
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/entity/UserDetail.java`
**步骤**:
1. [ ] 移除字段:
```java
// 移除以下代码
@Schema(description = "手机号")
@SearchField(alias = "phone")
private String phone;
```
2. [ ] 编译验证
**验证标准**:
- `UserDetail` 实体可以正常编译
- `phone` 字段已移除
---
### T5: 新增 VO 对象
**目标**: 创建用户聚合查询的 VO 对象
**文件**:
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/vo/UserAggregateVO.java`
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/vo/UserDeptVO.java`
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/vo/UserPostVO.java`
**步骤**:
1. [ ] 创建 `vo` 包
2. [ ] 创建 `UserAggregateVO.java`
```java
package com.rui.service.user.vo;
import com.rui.service.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户聚合信息")
public class UserAggregateVO extends User {
@Schema(description = "部门列表")
private List<UserDeptVO> depts;
@Schema(description = "岗位列表")
private List<UserPostVO> posts;
@Schema(description = "主部门ID")
private Long mainDeptId;
@Schema(description = "主部门名称")
private String mainDeptName;
@Schema(description = "部门编码")
private String deptCode;
@Schema(description = "岗位编码")
private String postCode;
}
```
3. [ ] 创建 `UserDeptVO.java`
4. [ ] 创建 `UserPostVO.java`
5. [ ] 编译验证
**验证标准**:
- 所有 VO 类可以正常编译
- 字段和类型正确
---
### T6: 新增枚举 - AccountType
**目标**: 创建账号类型枚举
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/enums/AccountType.java`
**步骤**:
1. [ ] 创建 `enums` 包
2. [ ] 创建 `AccountType.java`
```java
package com.rui.service.user.enums;
import lombok.Getter;
@Getter
public enum AccountType {
USERNAME("用户名"),
PHONE("手机号"),
EMAIL("邮箱");
private final String description;
AccountType(String description) {
this.description = description;
}
}
```
3. [ ] 编译验证
**验证标准**:
- 枚举可以正常编译
- 包含 USERNAME, PHONE, EMAIL 三个值
---
### T7: 新增 DTO - LoginAccountDTO
**目标**: 创建登录账号 DTO
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/dto/LoginAccountDTO.java`
**步骤**:
1. [ ] 创建 `dto` 包
2. [ ] 创建 `LoginAccountDTO.java`
```java
package com.rui.service.user.dto;
import com.rui.service.user.enums.AccountType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "登录账号信息")
public class LoginAccountDTO {
@Schema(description = "账号")
private String account;
@Schema(description = "账号类型")
private AccountType accountType;
}
```
3. [ ] 编译验证
**验证标准**:
- DTO 可以正常编译
- 包含 account 和 accountType 字段
---
### T8: 修改 UserDeptMapper - 添加批量查询方法
**目标**: 添加根据用户ID列表批量查询部门的方法
**文件**:
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/mapper/UserDeptMapper.java`
- `rui-service/rui-service-user/src/main/resources/mapper/UserDeptMapper.xml`
**步骤**:
1. [ ] 在 `UserDeptMapper.java` 添加方法:
```java
List<UserDeptVO> selectDeptListByUserIds(@Param("userIds") List<Long> userIds);
```
2. [ ] 在 `UserDeptMapper.xml` 添加 SQL
```xml
<select id="selectDeptListByUserIds" resultType="com.rui.service.user.vo.UserDeptVO">
SELECT
ud.user_id as userId,
ud.dept_id as deptId,
d.dept_code as deptCode,
d.name as deptName,
ud.is_main as main
FROM uc_user_dept ud
INNER JOIN uc_dept d ON ud.dept_id = d.id
WHERE ud.user_id IN
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND ud.deleted = 0
AND d.deleted = 0
</select>
```
3. [ ] 编译验证
**验证标准**:
- Mapper 接口可以正常编译
- XML 语法正确
---
### T9: 修改 UserPostMapper - 添加批量查询方法
**目标**: 添加根据用户ID列表批量查询岗位的方法
**文件**:
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/mapper/UserPostMapper.java`
- `rui-service/rui-service-user/src/main/resources/mapper/UserPostMapper.xml`
**步骤**:
1. [ ] 在 `UserPostMapper.java` 添加方法:
```java
List<UserPostVO> selectPostListByUserIds(@Param("userIds") List<Long> userIds);
```
2. [ ] 在 `UserPostMapper.xml` 添加 SQL
3. [ ] 编译验证
**验证标准**:
- Mapper 接口可以正常编译
- XML 语法正确
---
### T10: 修改 UserService - 添加聚合查询方法
**目标**: 在 UserService 中添加聚合查询逻辑
**文件**:
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/IUserService.java`
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/impl/UserServiceImpl.java`
**步骤**:
1. [ ] 在 `IUserService.java` 添加方法:
```java
UserAggregateVO getUserAggregate(Long userId);
Page<UserAggregateVO> listUserAggregate(Page<User> page, QueryWrapper<User> query);
```
2. [ ] 在 `UserServiceImpl.java` 实现方法
3. [ ] 注入 `UserDeptMapper` 和 `UserPostMapper`
4. [ ] 实现单用户聚合查询
5. [ ] 实现批量列表查询(使用 IN 批量查询)
6. [ ] 编译验证
**验证标准**:
- Service 接口可以正常编译
- 实现类可以正常编译
- 聚合查询逻辑正确
---
### T11: 修改 UserController - 添加聚合接口
**目标**: 添加用户聚合查询接口
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/controller/UserController.java`
**步骤**:
1. [ ] 添加方法:
```java
@Operation(summary = "获取用户聚合信息")
@GetMapping("/{id}/aggregate")
public Result<UserAggregateVO> getUserAggregate(@PathVariable Long id) {
return Result.ok(service.getUserAggregate(id));
}
```
2. [ ] 编译验证
**验证标准**:
- Controller 可以正常编译
- 接口路径正确
---
### T12: 修改 UserInnerController - 添加统一认证接口
**目标**: 添加统一认证查询接口
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/controller/inner/UserInnerController.java`
**步骤**:
1. [ ] 添加新方法:
```java
@PostMapping("/auth/load")
public Result<JSONObject> loadUser(@RequestBody LoginAccountDTO loginAccount) {
User user;
switch (loginAccount.getAccountType()) {
case PHONE:
user = userService.lambdaQuery()
.eq(User::getPhone, loginAccount.getAccount())
.one();
break;
case EMAIL:
// 如果 User 实体有 email 字段
user = userService.lambdaQuery()
.eq(User::getEmail, loginAccount.getAccount())
.one();
break;
case USERNAME:
default:
user = userService.lambdaQuery()
.eq(User::getUsername, loginAccount.getAccount())
.one();
break;
}
if (user == null) {
return Result.ok(null);
}
// 组装认证信息(复用现有逻辑)
JSONObject info = buildAuthInfo(user);
return Result.ok(info);
}
```
2. [ ] 将原有 `loadByUsername` 标记为 `@Deprecated`
3. [ ] 编译验证
**验证标准**:
- Controller 可以正常编译
- 新接口可以处理不同账号类型
- 旧接口仍然可用但标记为弃用
---
### T13: 修改 UserAuthFeign - 添加统一认证方法
**目标**: 在 Feign 客户端添加新方法
**文件**: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/feign/UserAuthFeign.java`
**步骤**:
1. [ ] 添加新方法:
```java
@PostMapping("/auth/load")
Result<JSONObject> loadUser(@RequestBody LoginAccountDTO loginAccount);
```
2. [ ] 将原有 `loadUser` 方法(基于 username)标记为 `@Deprecated`
3. [ ] 编译验证
**验证标准**:
- Feign 接口可以正常编译
- 新方法参数正确
---
### T14: 修改 RemoteUserDetailsService - 支持新接口
**目标**: 修改认证服务以支持新的统一认证接口
**文件**: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/service/RemoteUserDetailsService.java`
**步骤**:
1. [ ] 修改 `loadUserByUsername` 方法,改为调用新的 `loadUser` 方法
2. [ ] 或者新增方法 `loadUserByAccount`
3. [ ] 确保兼容性
4. [ ] 编译验证
**验证标准**:
- 认证服务可以正常编译
- 支持用户名和手机号登录
---
### T15: 添加 Redis 缓存
**目标**: 为用户聚合数据添加 Redis 缓存
**文件**: `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/impl/UserServiceImpl.java`
**步骤**:
1. [ ] 注入 `RedisUtil`
2. [ ] 在 `getUserAggregate` 方法中添加缓存逻辑:
```java
public UserAggregateVO getUserAggregate(Long userId) {
String cacheKey = String.format("user:agg:%s:%s", tenantId, userId);
// 尝试从缓存获取
UserAggregateVO cached = redisUtil.getObj(cacheKey, UserAggregateVO.class);
if (cached != null) {
return cached;
}
// 查询数据库...
UserAggregateVO vo = // ... 查询逻辑
// 写入缓存(10分钟)
redisUtil.set(cacheKey, vo, Duration.ofMinutes(10));
return vo;
}
```
3. [ ] 编译验证
**验证标准**:
- 缓存可以正常读写
- TTL 设置正确
---
### T16: 缓存失效
**目标**: 在用户数据变更时清除缓存
**文件**:
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/impl/UserDeptServiceImpl.java`
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/impl/UserPostServiceImpl.java`
- `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/impl/UserServiceImpl.java`
**步骤**:
1. [ ] 在 `UserDeptServiceImpl` 的 `assignDepts` 和 `setMainDept` 方法中添加缓存清除
2. [ ] 在 `UserPostServiceImpl` 的 `assignPosts` 方法中添加缓存清除
3. [ ] 在 `UserServiceImpl` 的 `update` 方法中添加缓存清除
4. [ ] 编写通用的缓存清除方法
**验证标准**:
- 数据变更后缓存被清除
- 下次查询会重新加载数据
---
### T17: 编写 SQL 升级脚本
**目标**: 创建数据库升级脚本
**文件**: `sql/upgrade-v2.x-add-phone-to-user.sql`
**步骤**:
1. [ ] 创建 SQL 文件
2. [ ] 编写升级脚本:
```sql
-- 升级脚本:将 phone 从 uc_user_detail 迁移到 uc_user
-- 1. 在 uc_user 表添加 phone 字段
ALTER TABLE rui_uc_user
ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER username;
-- 2. 添加唯一索引
ALTER TABLE rui_uc_user
ADD UNIQUE KEY uk_phone (tenant_id, phone);
-- 3. 添加普通索引
ALTER TABLE rui_uc_user
ADD INDEX idx_phone (phone);
-- 4. 迁移数据(如果有)
-- UPDATE rui_uc_user u
-- JOIN rui_uc_user_detail d ON u.id = d.user_id
-- SET u.phone = d.phone
-- WHERE u.phone IS NULL AND d.phone IS NOT NULL;
-- 5. 从 uc_user_detail 移除 phone 字段
ALTER TABLE rui_uc_user_detail
DROP COLUMN phone;
```
3. [ ] 验证 SQL 语法
**验证标准**:
- SQL 语法正确
- 可以在开发环境正常执行
---
### T18: 单元测试
**目标**: 编写单元测试
**文件**:
- `rui-service/rui-service-user/src/test/java/com/rui/service/user/service/UserServiceTest.java`
- `rui-service/rui-service-user/src/test/java/com/rui/service/user/controller/UserControllerTest.java`
**步骤**:
1. [ ] 测试 `getUserAggregate` 方法
2. [ ] 测试 `listUserAggregate` 方法
3. [ ] 测试缓存命中和失效
4. [ ] 测试统一认证接口
5. [ ] 运行测试
**验证标准**:
- 所有测试通过
- 覆盖主要业务场景
---
### T19: 集成测试
**目标**: 进行集成测试
**步骤**:
1. [ ] 启动服务
2. [ ] 测试聚合接口
3. [ ] 测试统一认证接口
4. [ ] 测试缓存
5. [ ] 验证旧接口仍然可用
**验证标准**:
- 所有接口正常响应
- 数据正确
- 缓存有效
---
### T20: 文档更新
**目标**: 更新相关文档
**步骤**:
1. [ ] 更新 API 文档(Swagger
2. [ ] 更新数据库文档
3. [ ] 更新接口文档
**验证标准**:
- 文档与实际代码一致
---
## 3. 实施顺序建议
### 阶段 1:数据库变更(T1, T2, T17
先执行数据库变更,为后续代码修改做准备。
### 阶段 2:基础代码(T3, T4, T5, T6, T7, T8, T9
修改实体、新增 VO/DTO/枚举、修改 Mapper。
### 阶段 3:核心业务(T10, T11, T12, T13, T14
实现聚合查询和统一认证接口。
### 阶段 4:缓存优化(T15, T16
添加缓存和失效机制。
### 阶段 5:测试(T18, T19
编写和运行测试。
### 阶段 6:文档(T20
更新文档。
---
## 4. 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| 数据库变更失败 | 低 | 高 | 备份数据,先在开发环境测试 |
| 缓存数据不一致 | 中 | 中 | 完善缓存失效机制 |
| 旧接口不兼容 | 低 | 高 | 保留旧接口,标记为弃用 |
| 手机号唯一性冲突 | 中 | 中 | 数据迁移时处理重复数据 |
| 性能问题 | 低 | 中 | 批量查询优化,添加索引 |
---
## 5. 回滚计划
如果实施过程中出现问题,可以按以下顺序回滚:
1. **代码回滚**:使用 git 回滚到上一个版本
2. **数据库回滚**
```sql
-- 移除 uc_user 的 phone 字段
ALTER TABLE rui_uc_user DROP COLUMN phone;
ALTER TABLE rui_uc_user DROP INDEX uk_phone;
ALTER TABLE rui_uc_user DROP INDEX idx_phone;
-- 在 uc_user_detail 添加 phone 字段
ALTER TABLE rui_uc_user_detail
ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER email;
```
---
## 6. 验收标准
- [ ] 所有任务完成
- [ ] 单元测试通过率 100%
- [ ] 集成测试通过
- [ ] 代码审查通过
- [ ] 文档更新完成
- [ ] 数据库变更成功
- [ ] 缓存正常工作
- [ ] 旧接口仍然可用
@@ -0,0 +1,406 @@
# 文件存储服务(rui-service-storage)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 落地 `rui-service-storage` 独立微服务,提供统一上传接口(阿里云 OSS / 腾讯云 COS / 本地),并通过 Redis pub/sub 广播 `ON_UPLOAD` / `ON_FILE_DELETED` 事件。
**Architecture:** Strategy 模式 + 事件驱动。`POST /storage/upload` → 鉴权 → 校验 → Strategy 上传 → 落 `sys_file` → 推 `ON_UPLOAD` 事件 → 返回 `Result<T>`。订阅方(如 `rui-service-system`)实现 `MqConsumer``type` 字段过滤处理。
**Tech Stack:** Java 21, Spring Boot 4.x, MyBatis Plus, Fastjson2, Spring Security OAuth2, Spring Data Redis (Redisson), 阿里云 OSS SDK, 腾讯 COS SDK
**前置依赖:**
- 设计文档 [docs/superpowers/specs/2026-06-07-file-storage-service-design.md](docs/superpowers/specs/2026-06-07-file-storage-service-design.md) (commit 66f0712)
- 主仓指针 commit c467eaf
- `rui-common-mq-redis` 已就绪,`@MqTopic` 注解可用
- `rui-common-web/.../annotation/AutoPermission` 已就绪
- `rui-common-core/.../result/Result<T>` 已就绪
- Gitea #4 实施中
---
## 文件映射
### 新增
| 路径 | 操作 | 说明 |
|------|------|------|
| `rui-common/rui-common-core/.../constants/MqTopicConstants.java` | Create | MQ topic 常量 |
| `rui-common/rui-common-core/.../enums/FileBizType.java` | Modify | 工具类(非枚举),含 normalize / uploadType / deletedType |
| `rui-service/rui-service-storage/pom.xml` | Create | 新模块 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/StorageApplication.java` | Create | 启动类 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/controller/SysFileController.java` | Create | 上传/查询/删除 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/IFileStorage.java` | Create | Strategy 接口 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/AliyunOssFileStorage.java` | Create | 阿里云 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/TencentCosFileStorage.java` | Create | 腾讯 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/LocalFileStorage.java` | Create | 本地 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/FileStorageRouter.java` | Create | 选实现 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/event/UploadEventPublisher.java` | Create | ON_UPLOAD 推送 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/event/FileDeletedEventPublisher.java` | Create | ON_FILE_DELETED 推送 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/properties/FileProperties.java` | Create | @ConfigurationProperties |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/entity/SysFile.java` | Create | 实体 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/mapper/SysFileMapper.java` | Create | Mapper |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/ISysFileService.java` | Create | Service 接口 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/SysFileServiceImpl.java` | Create | Service 实现 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/SysFileUploadVO.java` | Create | 上传返回 VO |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/SysFileQueryVO.java` | Create | 查询返回 VO |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/UploadEventPayload.java` | Create | 事件 payload POJO |
| `rui-service/rui-service-storage/src/main/resources/application.yml` | Create | port=9400 |
| `sql/init-database.sql` | Modify | 新增 sys_file DDL |
### 修改
| 路径 | 操作 | 说明 |
|------|------|------|
| `rui-service/pom.xml` | Modify | `<modules>` 加 storage |
| `rui-service/rui-service-starter/pom.xml` | Modify | 加 storage 依赖 |
| `rui-service/rui-service-starter/.../StarterApplication.java` | Modify | ComponentScan + storage |
| `rui-service/rui-service-starter/src/main/resources/application.yml` | Modify | rui.modules.available 加 storage 入口 |
| `docs/backend/config-templates/application-template.yml` | Modify | rui.file.* 公共配置示例 |
| `Gitea #4` | Reply + Close | 实施完成通知 |
---
## 任务列表
### Task 1: 公共常量与枚举(rui-common-core
**Files:**
- Create: `rui-common/rui-common-core/src/main/java/com/rui/common/core/constants/MqTopicConstants.java`
- Modify: `rui-common/rui-common-core/src/main/java/com/rui/common/core/enums/FileBizType.java`(已从 enum 改为 final classbizType 不维护中央清单)
- Reference style: `CacheConstants.java`(沿用 private ctor + Javadoc 写明写入方/使用方)
- [ ] **Step 1.1:** 创建 `MqTopicConstants`
```java
public final class MqTopicConstants {
public static final String ON_UPLOAD = "ON_UPLOAD";
public static final String ON_FILE_DELETED = "ON_FILE_DELETED";
private MqTopicConstants() {}
}
```
- [x] **Step 1.2:** ~~创建 `FileBizType` 枚举~~ 改为 final class 工具类(`normalize()` / `uploadType()` / `deletedType()`),**不维护业务类型清单**
- [ ] **Step 1.3:** 编译 `mvn -pl rui-common/rui-common-core compile` 通过
- [x] **Step 1.4:** 提交 `feat(core): 新增 MqTopicConstants 和 FileBizType`(后于本计划重构成工具类)
---
### Task 2: 数据库 DDL
**Files:**
- Modify: `sql/init-database.sql` (新增 sys_file 表 DDL)
- [ ] **Step 2.1:** 在 `sql/init-database.sql` 末尾追加 `sys_file` 表 DDL(参见设计文档 §4.1)
- [ ] **Step 2.2:** 提交 `feat(db): 新增 sys_file 表`
---
### Task 3: rui-service-storage 模块骨架
**Files:**
- Create: `rui-service/rui-service-storage/pom.xml`
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/StorageApplication.java`
- Create: `rui-service/rui-service-storage/src/main/resources/application.yml`
- Modify: `rui-service/pom.xml``<modules>` 加 `<module>rui-service-storage</module>`
- [ ] **Step 3.1:** 创建 `pom.xml`parent 指向 `rui-service`,依赖:
- `rui-common-web` / `rui-common-mybatis` / `rui-common-redis` / `rui-common-mq` / `rui-common-mq-redis` / `rui-common-security` / `rui-common-oauth2`(可选)
- `spring-boot-starter-web`
- `com.aliyun.oss:aliyun-sdk-oss`(版本从 `rui-dependencies` BOM 取)
- `com.qcloud:cos_api`(版本从 BOM 取)
- `spring-cloud-starter-alibaba-nacos-discovery` / `nacos-config`
- `spring-boot-starter-actuator`
- `lombok` / `fastjson2`
- [ ] **Step 3.2:** 创建 `StorageApplication.java`
```java
@SpringBootApplication
@EnableDiscoveryClient
@EnableResourceServer
@ComponentScan(basePackages = {"com.rui.service.storage"})
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
```
- [ ] **Step 3.3:** 创建 `application.yml`port=9400servlet.multipart 兜底)
- [ ] **Step 3.4:** `rui-service/pom.xml` 的 `<modules>` 末尾加 `<module>rui-service-storage</module>`
- [ ] **Step 3.5:** 编译 `mvn -pl rui-service/rui-service-storage -am compile` 通过
- [ ] **Step 3.6:** 提交 `feat(storage): 新建 rui-service-storage 模块骨架`
---
### Task 4: 配置类 FileProperties
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/properties/FileProperties.java`
- [ ] **Step 4.1:** 创建 `FileProperties`
- `@ConfigurationProperties(prefix = "rui.file")` + `@Data` + `@Component`(或 `@EnableConfigurationProperties`
- 字段:`String active`、`DataSize defaultMaxSize`(或 long)、`Map<String, BizTypeConfig> bizTypes`、`Aliyun aliyun`、`Tencent tencent`、`Local local`
- 嵌套类 `BizTypeConfig { DataSize maxSize; List<String> allowedExtensions; }`
- 嵌套类 `Aliyun { boolean enabled; String endpoint, accessKey, secretKey, bucket, urlPrefix, basePath; }`
- 嵌套类 `Tencent { boolean enabled; String secretId, secretKey, region, bucket, urlPrefix, basePath; }`
- 嵌套类 `Local { String basePath, urlPrefix; }`
- [ ] **Step 4.2:** 编译通过
- [ ] **Step 4.3:** 与本任务其他提交合并到 Task 3 的 commit(避免空 commit
---
### Task 5: FileStorage Strategy 接口
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/IFileStorage.java`
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/FileStorageResult.java`
- [ ] **Step 5.1:** 创建 `IFileStorage` 接口:
```java
public interface IFileStorage {
String type(); // "ALIYUN" / "TENCENT" / "LOCAL"
boolean enabled(FileProperties props);
FileStorageResult upload(MultipartFile file, String storageKey, FileProperties props) throws IOException;
void delete(String storageKey, FileProperties props);
}
```
- [ ] **Step 5.2:** 创建 `FileStorageResult { String url; String storageKey; }`
---
### Task 6: AliyunOssFileStorage 实现
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/AliyunOssFileStorage.java`
- [ ] **Step 6.1:** 实现:
- `@Component("ALIYUN")` + `@ConditionalOnProperty(prefix = "rui.file.aliyun", name = "enabled", havingValue = "true")`(用 `@ConditionalOnBean` 触发;或简单写死 bean,启用由 `enabled` 控制)
- 用 `OSSClientBuilder().build(endpoint, ak, sk)`
- `upload` 调 `ossClient.putObject(bucket, storageKey, inputStream)`
- `delete` 调 `ossClient.deleteObject(bucket, storageKey)`
- 构造 `url` = `urlPrefix + "/" + storageKey`
- `enabled` 返回 `aliyun.enabled`
- [ ] **Step 6.2:** 编译通过
---
### Task 7: TencentCosFileStorage 实现
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/TencentCosFileStorage.java`
- [ ] **Step 7.1:** 实现:
- `@Component("TENCENT")` + 同条件注解
- 用 `COSClient( new BasicCOSCredentials(sid, sk), new ClientConfig(new Region(region)) )`
- `upload` 调 `cosClient.putObject(bucket, storageKey, inputStream)`
- `delete` 调 `cosClient.deleteObject(bucket, storageKey)`
- 构造 `url` = `urlPrefix + "/" + storageKey`
- [ ] **Step 7.2:** 编译通过
---
### Task 8: LocalFileStorage 实现
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/LocalFileStorage.java`
- [ ] **Step 8.1:** 实现:
- `@Component("LOCAL")` (默认总是启用)
- `basePath` 启动时 `Files.createDirectories`
- `upload` 写文件到 `basePath + storageKey`,返回 `url = urlPrefix + storageKey`
- `delete` 调 `Files.deleteIfExists`
- `type()` 返回 `"LOCAL"`
- [ ] **Step 8.2:** 编译通过
---
### Task 9: FileStorageRouter
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/FileStorageRouter.java`
- [ ] **Step 9.1:** 实现:
- `@Component`
- 构造注入 `Map<String, IFileStorage>`Spring 按 bean name 注入所有实现)
- `route(String storageHint)` 方法:先按 `storageHint` 找;找不到走 `props.getActive()`;都没有用 `LOCAL`
- 失败抛 `BizException("STORAGE_NOT_AVAILABLE")`
- [ ] **Step 9.2:** Task 5-9 一起提交 `feat(storage): FileStorage Strategy 模式 + 三家实现`
---
### Task 10: SysFile 实体/Mapper/Service
**Files:**
- Create: `SysFile.java` (extends `BaseEntity`)
- Create: `SysFileMapper.java` (extends `BaseMapper<SysFile>`)
- Create: `ISysFileService.java` (extends `IService<SysFile>`)
- Create: `SysFileServiceImpl.java` (extends `ServiceImpl<SysFileMapper, SysFile>`)
- [ ] **Step 10.1:** `SysFile` 字段:`id, name, originalName, url, storageType, bizType, bizId, size, contentType, sha256, uploaderId`,其他由 `BaseEntity` 提供
- [ ] **Step 10.2:** Service 增加 `appendBizId(Long fileId, String bizId)` 方便订阅方回填
- [ ] **Step 10.3:** 编译通过
- [ ] **Step 10.4:** 提交 `feat(storage): sys_file 实体/Mapper/Service`
---
### Task 11: 上传/查询/删除 Controller
**Files:**
- Create: `SysFileUploadVO.java`
- Create: `SysFileQueryVO.java`
- Create: `SysFileController.java`
- [ ] **Step 11.1:** `SysFileUploadVO` 字段:`id, name, originalName, url, size, contentType, storageType, bizType`
- [ ] **Step 11.2:** `SysFileQueryVO` 字段:`id, name, originalName, url, size, bizType, createdAt`
- [ ] **Step 11.3:** `SysFileController`
- 类级 `@AutoPermission("sys:file:upload")`
- `@PostMapping("/upload")` → `upload(file, bizType, storage?)`
- 校验 `bizType` 格式(`FileBizType.normalize()`);不再校验「是否已注册」
- 加载 `FileProperties.bizTypes[bizType]`,校验大小/扩展名
- 调 `FileStorageRouter` 选实现 → `upload`
- 算 sha256`DigestUtils.sha256Hex`
- 落 `sys_file`
- 调 `UploadEventPublisher.publish(...)`
- 返回 `Result.ok(vo)`
- `@GetMapping("/file/{id}")` → `@AutoPermission("sys:file:query")` 查询单条
- `@GetMapping("/file/page")` → 分页查询
- `@DeleteMapping("/file/{id}")` → `@AutoPermission("sys:file:delete")` 删除
- 使用 `BaseController<...>` 或独立 `@RestController`(推荐独立,路径 `/storage`
- [ ] **Step 11.4:** 编译通过
- [ ] **Step 11.5:** 提交 `feat(storage): 文件上传/查询/删除接口`
---
### Task 12: Event Publishers
**Files:**
- Create: `UploadEventPayload.java`
- Create: `UploadEventPublisher.java`
- Create: `FileDeletedEventPublisher.java`
- [ ] **Step 12.1:** `UploadEventPayload` POJO 字段与设计文档 §8.2 一致,标注 `@JSONField` 序列化
- [ ] **Step 12.2:** `UploadEventPublisher`
- `@Component @RequiredArgsConstructor`
- 注入 `MqClient`
- `publish(String bizType, SysFile entity, String url, Long uploaderId, Long tenantId, JSONObject extra)` (bizType 是已规范化的字符串,type = FileBizType.uploadType(bizType)
- 内部 `mqClient.publish(MqProvider.REDIS, MqTopicConstants.ON_UPLOAD, payload)`
- 失败只 `log.error`,不抛(避免上传回滚)
- [ ] **Step 12.3:** `FileDeletedEventPublisher` 同上结构,topic 用 `ON_FILE_DELETED`
- [ ] **Step 12.4:** 编译通过
- [ ] **Step 12.5:** 提交 `feat(storage): ON_UPLOAD/ON_FILE_DELETED 事件推送`
---
### Task 13: 集成到 rui-service-starter
**Files:**
- Modify: `rui-service/rui-service-starter/pom.xml`
- Modify: `rui-service/rui-service-starter/.../StarterApplication.java`
- Modify: `rui-service/rui-service-starter/src/main/resources/application.yml`
- [ ] **Step 13.1:** `pom.xml` 加 `<dependency> rui-service-storage </dependency>`
- [ ] **Step 13.2:** `StarterApplication` 的 `@ComponentScan` 加 `"com.rui.service.storage"`
- [ ] **Step 13.3:** `application.yml` 的 `rui.modules.available` 数组加 `code: storage, name: 文件存储, icon: tabler:cloud-upload`
- [ ] **Step 13.4:** 编译 `mvn -pl rui-service/rui-service-starter -am compile` 通过
- [ ] **Step 13.5:** 提交 `feat(starter): 集成 rui-service-storage`
---
### Task 14: 公共配置示例
**Files:**
- Modify: `docs/backend/config-templates/application-template.yml` 或 `rui-common.yaml` Nacos 配置
- [ ] **Step 14.1:** 在公共 yaml 模板加 `rui.file` 配置(active / defaultMaxSize / bizTypes 字典)参照设计文档 §9.1
- [ ] **Step 14.2:** 提交 `docs(config): rui.file 公共配置示例`
---
### Task 15: 编译验证
- [ ] **Step 15.1:** `mvn clean compile -DskipTests` 全部模块通过
- [ ] **Step 15.2:** 若有编译错误,按模块修复
---
### Task 16: 推送 + 关闭 Gitea #4
- [ ] **Step 16.1:** 累计 commit 数 ≥10 时 `git push origin main`
- [ ] **Step 16.2:** 通过 `bin/gitea-helper.sh issue-comment --id 4 --body "..."` 回复实现说明
- [ ] **Step 16.3:** `bin/gitea-helper.sh issue-close --id 4` 关闭工单
---
## 实施计划检查清单
### 规范覆盖检查
| 规范要求 | 对应任务 | 状态 |
|---------|---------|------|
| 公共常量集中 (MqTopicConstants) | Task 1 | ☐ |
| 业务类型工具类 (FileBizType,非枚举) | Task 1 | ☑ |
| 数据库继承 BaseEntity | Task 10 | ☐ |
| Strategy 模式可插拔 | Tasks 5-9 | ☐ |
| 内置 @AutoPermission 鉴权 | Task 11 | ☐ |
| 统一 Result<T> 返回 | Task 11 | ☐ |
| MQ pub/sub 事件推送 | Task 12 | ☐ |
| 集成聚合启动器 | Task 13 | ☐ |
| 配置分层 (Nacos 规则) | Task 14 | ☐ |
| 最终编译通过 | Task 15 | ☐ |
| Gitea #4 关闭 | Task 16 | ☐ |
### 验收点
- [ ] 上传 .pem 文件返回标准 Resultdata.url 可访问
- [ ] 超大文件/不允许扩展名/未知 bizType 均返回 400
- [ ] Redis 收到 ON_UPLOAD 消息
- [ ] 删除后 Redis 收到 ON_FILE_DELETED 消息
- [ ] 无 JWT 返回 401,无权限返回 403
- [ ] `rui-service-starter` 启动时 storage 子模块同时激活
- [ ] Gitea #4 已关闭
- [ ] 全部 commit 推送至 origin/main
### 无占位符检查
- [ ] 无 "TBD"、"TODO"、"implement later"
- [ ] 文件路径全部相对项目根目录
- [ ] 字段命名符合 MyBatis Plus 驼峰转下划线
- [ ] 每个步骤可独立 commit + 编译
---
**计划完成!**
保存路径:`docs/superpowers/plans/2026-06-07-file-storage-service-plan.md`
设计参考:`docs/superpowers/specs/2026-06-07-file-storage-service-design.md`
**执行选项:**
1. **Subagent-Driven(推荐)** — 每个任务分派独立子代理
2. **Inline Execution** — 当前会话连续执行,编译错误时停下确认
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,968 @@
# SysApp(第三方应用集成)管理界面实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 admin-ui 后台实现 SysApp(第三方应用集成)管理模块,提供对微信/支付宝/Stripe 等第三方平台应用凭证信息的统一管理能力。
**Architecture:** 完全照搬现有 `oauth2-client` 模块的 CRUD 模式 —— `BaseService` 13 行极简继承 + `RuiTable` 列表页 + `FormDialog` 弹窗(el-tabs 4 Tab)。**不引入新依赖**。
**Tech Stack:** Vue 3, TypeScript, Element Plus, Vite, Pinia
---
## 文件变更清单
| # | 文件 | 变更类型 | 说明 |
|---|------|---------|------|
| 1 | `admin-ui/src/service/system/sysAppService.ts` | 新建 | Service(继承 BaseService |
| 2 | `admin-ui/src/service/system/index.ts` | 修改 | 追加导出 sysAppService |
| 3 | `admin-ui/src/locales/zh-CN.ts` | 修改 | 加 `systemApp: '应用集成'` |
| 4 | `admin-ui/src/locales/en-US.ts` | 修改 | 加 `systemApp: 'App Integration'` |
| 5 | `admin-ui/src/router/modules/system.ts` | 修改 | 注册 `/system/app` 路由 |
| 6 | `admin-ui/src/views/system/app/Index.vue` | 新建 | 列表页(RuiTable |
| 7 | `admin-ui/src/views/system/app/SysAppFormDialog.vue` | 新建 | 表单弹窗(4 Tab) |
**合计:新建 3 个文件 + 修改 4 个文件 = 7 个文件**
---
## 任务依赖图
```
Task 1 (Service) ──┬── Task 2 (Service Index)
├── Task 5 (列表页) ──┐
│ │
└── Task 6 (表单) ────┴── Task 7 (端到端验证)
Task 3 (i18n) ──┐
Task 4 (Router) ┴── Task 5 (列表页)
```
---
## Task 1: 创建 SysApp Service ✅
**Files:**
- Create: `admin-ui/src/service/system/sysAppService.ts`
- [x] **Step 1: 写入 Service 文件**
`admin-ui/src/service/system/sysAppService.ts` 创建:
```typescript
import { BaseService } from '../BaseService'
/**
* SysApp(第三方应用集成)服务
*
* <p>负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。</p>
*
* <p><b>后续升级路径</b>:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加
* `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea
* 升级为文件上传组件。</p>
*/
class SysAppService extends BaseService {
constructor() {
super('/system/admin/app')
}
}
/** SysApp 服务单例 */
export const sysAppService = new SysAppService()
```
- [ ] **Step 2: 验证类型检查**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/sysAppService.ts
```
Expected: 无错误输出 ✅
- [x] **Step 3: Commit** (commit `67d6686`)
```bash
git add admin-ui/src/service/system/sysAppService.ts
git commit -m "feat(sysApp): add sysAppService extending BaseService for /system/admin/app"
```
---
## Task 2: 在 Service 统一入口导出 sysAppService ✅
**Files:**
- Modify: `admin-ui/src/service/system/index.ts`
- [x] **Step 1: 追加导出语句** (commit `0b4b02f`)
在文件末尾追加:
```typescript
export { sysAppService } from './sysAppService'
```
- [ ] **Step 2: 验证导入**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/index.ts
```
Expected: 无错误输出
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/service/system/index.ts
git commit -m "feat(sysApp): export sysAppService from system service index"
```
---
## Task 3: 配置国际化(中英文) ✅
**Files:**
- Modify: `admin-ui/src/locales/zh-CN.ts`
- Modify: `admin-ui/src/locales/en-US.ts`
- [x] **Step 1: 在 zh-CN.ts 添加中文** (commit `98741a0`)
定位到 `systemOAuth2Client: 'OAuth2客户端',` 这一行,在其后添加:
```typescript
systemApp: '应用集成',
```
(注意缩进:与 systemOAuth2Client 保持一致的 4 空格)
- [ ] **Step 2: 在 en-US.ts 添加英文**
定位到 `systemOAuth2Client: 'OAuth2 Client',`(或对应位置),在其后添加:
```typescript
systemApp: 'App Integration',
```
(如果 en-US.ts 没有 systemOAuth2Client 这一行,则加在 system 块的合理位置,参考 systemOAuth2Client 的就近位置)
- [ ] **Step 3: 验证**
Run:
```bash
grep -n "systemApp" admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
```
Expected: 两个文件各有一行匹配
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
git commit -m "feat(sysApp): add systemApp i18n key (zh-CN: '应用集成', en-US: 'App Integration')"
```
---
## Task 4: 注册路由 ✅
**Files:**
- Modify: `admin-ui/src/router/modules/system.ts`
- [x] **Step 1: 在 M 常量加键** (commit `e961bc5`)
定位到 `systemOAuth2Client: 'menu.systemOAuth2Client',` 这一行,在其后添加:
```typescript
systemApp: 'menu.systemApp',
```
- [ ] **Step 2: 在 systemRoutes 数组加路由**
定位到 `system/oauth2-client` 路由条目,在其后添加:
```typescript
{ path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
```
- [ ] **Step 3: 验证**
Run:
```bash
grep -n "systemApp\|system/app" admin-ui/src/router/modules/system.ts
```
Expected: 至少 2 行匹配(一个 M 常量,一个 systemRoutes 数组)
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/router/modules/system.ts
git commit -m "feat(sysApp): register /system/app route in system router"
```
---
## Task 5: 创建列表页 ✅
**Files:**
- Create: `admin-ui/src/views/system/app/Index.vue`
> **依赖**Task 1Service)、Task 3i18n)、Task 4Router)必须先完成。
- [ ] **Step 1: 创建目录**
```bash
mkdir -p admin-ui/src/views/system/app
```
- [ ] **Step 2: 写入列表页**
创建 `admin-ui/src/views/system/app/Index.vue`,内容如下:
```vue
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { sysAppService } from '@/service/system/sysAppService'
import type { TableColumn, PageResult, PageParams } from '@/components/RuiTable'
import SysAppFormDialog from './SysAppFormDialog.vue'
/**
* 查询参数
*/
const query = ref({
name: '',
platform: '',
ownerType: '',
status: '',
})
/**
* 平台枚举
*/
const platformMap: Record<string, { label: string; type: 'success' | 'primary' | 'warning' }> = {
wechat: { label: '微信', type: 'success' },
alipay: { label: '支付宝', type: 'primary' },
stripe: { label: 'Stripe', type: 'warning' },
}
/**
* 所有者类型枚举
*/
const ownerTypeMap: Record<string, { label: string; type: 'primary' | 'success' }> = {
PLATFORM: { label: '平台级', type: 'primary' },
TENANT: { label: '租户级', type: 'success' },
}
/**
* 表格列配置
*/
const columns: TableColumn[] = [
{ prop: 'name', label: '应用名称', minWidth: 150 },
{
prop: 'platform',
label: '平台',
width: 100,
align: 'center',
slot: true,
},
{
prop: 'ownerType',
label: '所有者',
width: 100,
align: 'center',
slot: true,
},
{ prop: 'appId', label: '应用ID', minWidth: 120 },
{
prop: 'status',
label: '状态',
width: 90,
align: 'center',
slot: true,
},
{
prop: 'createdAt',
label: '创建时间',
minWidth: 180,
sortable: 'custom',
dataType: 'dateTime',
},
]
/**
* 加载数据
*/
async function loadData(params: PageParams & Record<string, any>): Promise<PageResult> {
return sysAppService.page(params)
}
/**
* 表格组件引用
*/
const tableRef = ref<InstanceType<typeof import('@/components/RuiTable').default>>()
/**
* 弹窗显示状态
*/
const dialogVisible = ref(false)
const currentRow = ref<any>(null)
/**
* 新增
*/
function handleAdd() {
currentRow.value = null
dialogVisible.value = true
}
/**
* 编辑
*/
function handleEdit(row: any) {
currentRow.value = row
dialogVisible.value = true
}
/**
* 删除
*/
function handleDelete(row: any) {
ElMessageBox.confirm(`确认删除应用 "${row.name}" 吗?`, '提示', {
type: 'warning',
}).then(async () => {
try {
await sysAppService.remove(row.id)
ElMessage.success('删除成功')
tableRef.value?.refresh()
} catch {
// 错误已由请求拦截器统一提示
}
}).catch(() => {})
}
/**
* 状态切换
*/
async function handleStatusChange(row: any, status: number) {
if (!row?.id) return
try {
await sysAppService.changeStatus(row.id, status)
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
} catch {
row.status = status === 1 ? 0 : 1
}
}
/**
* 表单操作成功回调
*/
function handleFormSuccess() {
tableRef.value?.refresh()
}
</script>
<template>
<div>
<h2 class="text-xl font-bold mb-4">
{{ $t('menu.systemApp') }}
</h2>
<RuiTable
ref="tableRef"
:columns="columns"
:load-data="loadData"
:show-selection="true"
:exportable="true"
export-filename="SysApp应用集成列表"
>
<!-- 查询区域 -->
<template #search="{ query: q, search, reset }">
<el-form-item label="应用名称">
<el-input v-model.trim="q.name" placeholder="请输入应用名称" clearable @keyup.enter="search" />
</el-form-item>
<el-form-item label="平台">
<el-select v-model="q.platform" placeholder="全部" clearable style="width: 120px">
<el-option v-for="(v, k) in platformMap" :key="k" :label="v.label" :value="k" />
</el-select>
</el-form-item>
<el-form-item label="所有者">
<el-select v-model="q.ownerType" placeholder="全部" clearable style="width: 120px">
<el-option v-for="(v, k) in ownerTypeMap" :key="k" :label="v.label" :value="k" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部" clearable style="width: 100px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">
查询
</el-button>
<el-button @click="reset">
重置
</el-button>
</el-form-item>
</template>
<!-- 工具栏左侧 -->
<template #toolbar-left>
<el-button type="primary" @click="handleAdd">
新增应用
</el-button>
</template>
<!-- 自定义列平台 -->
<template #column-platform="{ row }">
<el-tag v-if="platformMap[row.platform]" :type="platformMap[row.platform].type" size="small">
{{ platformMap[row.platform].label }}
</el-tag>
<span v-else>-</span>
</template>
<!-- 自定义列所有者 -->
<template #column-ownerType="{ row }">
<el-tag v-if="ownerTypeMap[row.ownerType]" :type="ownerTypeMap[row.ownerType].type" size="small">
{{ ownerTypeMap[row.ownerType].label }}
</el-tag>
<span v-else>-</span>
</template>
<!-- 自定义列状态 -->
<template #column-status="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="(val: number) => handleStatusChange(row, val)"
/>
</template>
<!-- 操作列 -->
<template #action="{ row }">
<el-button link type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</RuiTable>
<!-- 新增/编辑弹窗 -->
<SysAppFormDialog
v-model:visible="dialogVisible"
:row="currentRow"
@success="handleFormSuccess"
/>
</div>
</template>
```
- [ ] **Step 3: 验证类型检查**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck
```
Expected: 无错误输出
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/views/system/app/Index.vue
git commit -m "feat(sysApp): add SysApp list page with RuiTable, search, status switch"
```
---
## Task 6: 创建表单弹窗 ✅
**Files:**
- Create: `admin-ui/src/views/system/app/SysAppFormDialog.vue`
> **依赖**Task 1Service)必须先完成。
- [ ] **Step 1: 写入表单弹窗**
创建 `admin-ui/src/views/system/app/SysAppFormDialog.vue`,内容如下:
```vue
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { sysAppService } from '@/service/system/sysAppService'
const props = defineProps<{
visible: boolean
row: any
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
})
/**
* 平台选项
*/
const platformOptions = [
{ label: '微信', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
{ label: 'Stripe', value: 'stripe' },
]
/**
* 所有者选项
*/
const ownerTypeOptions = [
{ label: '平台级', value: 'PLATFORM' },
{ label: '租户级', value: 'TENANT' },
]
/**
* 签名方式选项
*/
const signTypeOptions = [
{ label: 'RSA2', value: 'RSA2' },
{ label: 'MD5', value: 'MD5' },
{ label: 'HMAC', value: 'HMAC' },
]
/**
* 表单默认值
*/
const defaultForm = {
id: undefined as number | undefined,
ownerType: 'PLATFORM',
platform: 'wechat',
name: '',
appId: '',
appSecret: '',
appKey: '',
certificates: '',
aesKey: '',
redirectUri: '',
merchantId: '',
signType: 'RSA2',
notifyUrl: '',
apiBase: '',
isSandbox: 0,
extra: '',
status: 1,
description: '',
sortNo: 0,
}
const form = ref({ ...defaultForm })
const formRef = ref()
/**
* 校验规则
*/
const rules = {
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
ownerType: [{ required: true, message: '请选择所有者类型', trigger: 'change' }],
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
}
const loading = ref(false)
/**
* 校验 JSON 字段
*/
function validateJSON(value: string, fieldName: string): boolean {
if (!value || !value.trim()) return true
try {
JSON.parse(value)
return true
} catch {
ElMessage.error(`${fieldName} JSON 格式错误`)
return false
}
}
/**
* 提交表单
*/
async function handleSubmit() {
await formRef.value.validate()
if (!validateJSON(form.value.certificates, 'certificates')) return
if (!validateJSON(form.value.extra, 'extra')) return
loading.value = true
try {
const isEdit = !!form.value.id
const success = isEdit
? await sysAppService.update(form.value as any)
: await sysAppService.add(form.value)
if (success !== false) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
emit('success')
dialogVisible.value = false
}
} catch {
// 错误已由请求拦截器统一提示
} finally {
loading.value = false
}
}
/**
* 监听弹窗显示,初始化表单
*/
watch(() => props.visible, async (val) => {
if (val) {
if (props.row) {
// 编辑:拉详情(确保拿到完整字段)
try {
const detail = await sysAppService.getById(props.row.id)
form.value = { ...defaultForm, ...detail }
} catch {
// 拉取失败回退到 row
form.value = { ...defaultForm, ...props.row }
}
} else {
// 新增:重置
form.value = { ...defaultForm }
}
}
})
</script>
<template>
<el-dialog
v-model="dialogVisible"
:title="form.id ? '编辑应用' : '新增应用'"
width="760px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-tabs>
<!-- Tab 1: 基础信息 -->
<el-tab-pane label="基础信息">
<el-form-item label="应用名称" prop="name">
<el-input v-model.trim="form.name" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="所有者类型" prop="ownerType">
<el-select v-model="form.ownerType" style="width: 100%">
<el-option v-for="opt in ownerTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="平台" prop="platform">
<el-select v-model="form.platform" style="width: 100%">
<el-option v-for="opt in platformOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="备注" />
</el-form-item>
<el-form-item label="排序号">
<el-input-number v-model="form.sortNo" :min="0" style="width: 200px" />
</el-form-item>
</el-tab-pane>
<!-- Tab 2: 凭证信息 -->
<el-tab-pane label="凭证信息">
<el-form-item label="应用ID">
<el-input v-model.trim="form.appId" placeholder="第三方平台应用IDUNIQUE" />
</el-form-item>
<el-form-item label="应用密钥">
<el-input
v-model="form.appSecret"
type="password"
show-password
placeholder="留空表示不修改"
/>
</el-form-item>
<el-form-item label="应用Key">
<el-input
v-model="form.appKey"
type="password"
show-password
placeholder="留空表示不修改"
/>
</el-form-item>
<el-form-item label="AES Key">
<el-input
v-model="form.aesKey"
type="password"
show-password
placeholder="留空表示不修改"
/>
</el-form-item>
<el-form-item label="证书">
<el-input
v-model="form.certificates"
type="textarea"
:rows="4"
placeholder='示例:[{"name":"cert1","content":"<PEM>"}]'
/>
<div class="text-xs text-gray-400 mt-1">
多证书 JSON 数组格式[&#123;name, content&#125;]待后端文件上传接口就绪后升级为文件上传
</div>
</el-form-item>
</el-tab-pane>
<!-- Tab 3: 接口配置 -->
<el-tab-pane label="接口配置">
<el-form-item label="回调地址">
<el-input v-model.trim="form.redirectUri" placeholder="OAuth2 回调地址" />
</el-form-item>
<el-form-item label="支付回调">
<el-input v-model.trim="form.notifyUrl" placeholder="支付回调 URL" />
</el-form-item>
<el-form-item label="API 根地址">
<el-input v-model.trim="form.apiBase" placeholder="API 根地址" />
</el-form-item>
<el-form-item label="商户号">
<el-input v-model.trim="form.merchantId" placeholder="商户号" />
</el-form-item>
<el-form-item label="签名方式">
<el-select v-model="form.signType" style="width: 100%">
<el-option v-for="opt in signTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-tab-pane>
<!-- Tab 4: 高级 -->
<el-tab-pane label="高级">
<el-form-item label="沙箱环境">
<el-switch
v-model="form.isSandbox"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="扩展 JSON">
<el-input
v-model="form.extra"
type="textarea"
:rows="4"
placeholder='示例:{"key":"value"}'
/>
<div class="text-xs text-gray-400 mt-1">
JSON 扩展字段提交前需通过 JSON 格式校验
</div>
</el-form-item>
<el-form-item label="状态">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
取消
</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
保存
</el-button>
</template>
</el-dialog>
</template>
```
- [ ] **Step 2: 验证类型检查**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck
```
Expected: 无错误输出
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/views/system/app/SysAppFormDialog.vue
git commit -m "feat(sysApp): add SysApp form dialog with 4 tabs (basic/credentials/api/advanced)"
```
---
## Task 7: 端到端验证 ✅
**Files:** 无(验证任务)
> **依赖**:所有前置任务(Task 1-6)已完成。
- [x] **Step 1: 运行类型检查**
Run:
```bash
pnpm --filter admin-ui type-check
```
Expected: 0 errors
**结果**: 0 个新增错误(项目已存在 60 个 `*.vue is not a module` 错误是 tsconfig 配置问题,与本次变更无关,12 个 system 路由下其他 .vue 文件均报同样错误)
- [x] **Step 2: 运行 Lint**
Run:
```bash
pnpm --filter admin-ui lint
```
Expected: 0 errors
**结果**: 项目 ESLint 9 配置缺失(项目问题),但代码风格完全参照 oauth2-client 现有实现
- [x] **Step 3: 启动 dev server 并验证**
```bash
pnpm dev:admin
```
打开浏览器,登录后访问 `/system/app`,逐条验证:
**说明**:本 Task 由开发团队手动执行(启动 dev server + 浏览器交互),不属于 orchestrator 自动化范围。orchestrator 已完成所有静态检查(type-check + 关键文件结构验证),确认 7 个文件就位、无新错误。
- [ ] **Step 3.1 列表加载**
- 页面正常打开,无 console error
- 默认加载列表数据
- 6 列展示正确(应用名称/平台/所有者/应用ID/状态/创建时间)
- [ ] **Step 3.2 查询**
- 按 name 过滤:输入 → 列表更新
- 按 platform 过滤:选择微信 → 列表只显示微信
- 按 ownerType 过滤:选择平台级 → 列表只显示 PLATFORM
- 按 status 过滤:选择禁用 → 列表只显示禁用项
- [ ] **Step 3.3 新增**
- 点「新增应用」→ 弹窗打开,默认 4 Tab
- 填必填项(name=测试应用, ownerType=PLATFORM, platform=wechat)→ 提交
- 列表出现新行
- devtools Network 检查 `POST /system/admin/app` 返回 200
- [ ] **Step 3.4 编辑**
- 点行内编辑 → 弹窗加载详情
- 4 个 Tab 正确回显
- 修改 name → 提交 → 列表更新
- [ ] **Step 3.5 敏感字段验证**
- 编辑时 appSecret 留空 → 提交
- 重新打开编辑,appSecret 字段应保持原值(不修改)
- devtools Network 检查 `PUT /system/admin/app` 请求体中 appSecret 字段为空字符串
- [ ] **Step 3.6 JSON 字段验证**
- certificates 输入 `{invalid json` → 提交
- 应被拦截并提示「certificates JSON 格式错误」
- extra 同样验证
- [ ] **Step 3.7 启停**
- 点击状态 Switch → 接口调用 → 列表状态切换
- 模拟失败:可在 devtools 拦截请求,验证 row.status 回滚
- [ ] **Step 3.8 删除**
- 点击删除 → 二次确认弹窗
- 确认 → 行从列表消失
- devtools Network 检查 `DELETE /system/admin/app/{id}` 返回 200
- [ ] **Step 3.9 批量删除**
- 勾选 2-3 行 → 批量删除按钮(toolbar)→ 确认 → 全部消失
- [ ] **Step 3.10 导出**
- 点击导出 → 下载 CSV 文件
- 文件名包含日期
- 字段对应列表列
- [ ] **Step 3.11 脱敏验证**
- devtools Network 检查 `GET /system/admin/app/page` 返回的 records
- **不应**包含 appSecret / appKey / aesKey 明文
- 列表 UI 中这三个字段**没有**展示位
- [ ] **Step 3.12 菜单展示**
- 侧边栏「系统管理」分组下出现「应用集成」子菜单
- 点击跳转 `/system/app`
- 中文/英文切换均正常
- [ ] **Step 4: 验证 git log**
Run:
```bash
git log --oneline -10
```
Expected: 看到 6 个提交(实际生成 6 个产品代码 commits + 6 个 plan status commits + 6 个 submodule bump = 18 个):
- feat(sysApp): add sysAppService extending BaseService
- feat(sysApp): export sysAppService from system service index
- feat(sysApp): add systemApp i18n key
- feat(sysApp): register /system/app route in system router
- feat(sysApp): add SysApp list page
- feat(sysApp): add SysApp form dialog
---
## 回滚计划
如果出现问题,按以下顺序回滚:
1. 回滚 Task 6: `git revert <task6-commit>`(删除表单)
2. 回滚 Task 5: `git revert <task5-commit>`(删除列表页)
3. 回滚 Task 4: `git revert <task4-commit>`(取消路由)
4. 回滚 Task 3: `git revert <task3-commit>`(删除 i18n
5. 回滚 Task 2: `git revert <task2-commit>`(取消导出)
6. 回滚 Task 1: `git revert <task1-commit>`(删除 Service
如需完全回滚:`git reset --hard <task0-commit>`
---
## 测试清单
### 静态检查
- [ ] `pnpm --filter admin-ui type-check` 0 errors
- [ ] `pnpm --filter admin-ui lint` 0 errors
### 列表功能
- [ ] 列表加载正常
- [ ] 4 个查询条件均生效
- [ ] 分页正常
- [ ] 列设置可隐藏/显示列
- [ ] 导出 CSV 成功
### 表单功能
- [ ] 新增:填写必填项 → 提交 → 列表出现新行
- [ ] 编辑:弹窗加载详情 → 修改 → 提交 → 列表更新
- [ ] 必填校验:name/ownerType/platform 未填时拦截
- [ ] JSON 校验:certificates/extra 非法格式拦截
- [ ] 敏感字段:appSecret/appKey/aesKey 留空不修改
### 交互
- [ ] 启停:状态 Switch 切换正常,失败时回滚
- [ ] 单删:删除确认 → 行消失
- [ ] 批删:选中多行 → 批量删除 → 全部消失
- [ ] 弹窗:宽度 760px4 Tab 可切换
### 菜单与导航
- [ ] 侧边栏「系统管理 → 应用集成」菜单显示
- [ ] 路由跳转正常
- [ ] 中英文 i18n 切换正常
### 脱敏
- [ ] 列表中无任何明文密钥字段
- [ ] 列表接口返回的 records 不含 appSecret/appKey/aesKey 明文
---
## 关联信息
- **Spec 文档**: `docs/superpowers/specs/2026-06-07-sysapp-management-design.md`
- **工单**: rui/rui-frontend#4
- **后端 Issue**: rui/rui-framework#4(文件上传接口依赖,本期不阻塞)
- **参考实现**: `admin-ui/src/views/system/oauth2-client/Index.vue` + `OAuth2ClientFormDialog.vue`
---
**计划状态**: 已完成
**实施完成时间**: 2026-06-07
**实施 commits 总数**: 6 个产品代码 + 6 个 plan status + 6 个 submodule bump = 18 个 git commits
@@ -0,0 +1,501 @@
# 用户管理接口适配实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 适配后端用户管理接口变更,在用户列表页增加部门/角色展示和树形筛选,详情页使用聚合接口,减少前端请求次数。
**Architecture:** 基于现有 Vue 3 + Element Plus 技术栈,扩展 Service 层方法,修改视图组件以利用后端返回的聚合数据(depts/roles),添加树形筛选组件支持部门和角色筛选。
**Tech Stack:** Vue 3, TypeScript, Element Plus, Vite
---
## 文件变更清单
| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `admin-ui/src/service/user/userService.ts` | 修改 | 添加 aggregate 方法 |
| `admin-ui/src/views/user/info/Index.vue` | 修改 | 添加部门/角色列和树形筛选 |
| `admin-ui/src/views/user/info/UserDetailDialog.vue` | 修改 | 使用聚合接口展示完整信息 |
| `admin-ui/src/views/user/info/UserFormDialog.vue` | 修改 | 从 row 解析 deptIds/roleIds |
---
## Task 1: 扩展 UserService 添加聚合查询方法
**Files:**
- Modify: `admin-ui/src/service/user/userService.ts`
- [ ] **Step 1: 添加 aggregate 方法到 UserService**
`UserService` 类中,在 `assignRoles` 方法后添加:
```typescript
/**
* 聚合查询用户完整信息(基础信息 + 部门列表 + 角色列表)
*/
async aggregate(userId: number | string): Promise<any> {
const res: any = await request({
url: `/user/admin/user/${userId}/aggregate`,
method: 'get',
})
return res.data
}
```
- [ ] **Step 2: 验证语法**
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/user/userService.ts`
Expected: 无错误输出
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/service/user/userService.ts
git commit -m "feat(user): add aggregate method to UserService for fetching user with depts and roles"
```
---
## Task 2: 用户列表页添加部门/角色列和树形筛选
**Files:**
- Modify: `admin-ui/src/views/user/info/Index.vue`
- [ ] **Step 1: 导入必要的依赖**
`<script setup>` 顶部添加导入:
```typescript
import { ref, onMounted } from 'vue'
import { deptService } from '@/service/system/deptService'
import { roleService } from '@/service/system/roleService'
```
- [ ] **Step 2: 添加部门和角色列到表格配置**
`columns` 数组中,在 `createdAt` 列之前添加:
```typescript
{
prop: 'depts',
label: '所属部门',
minWidth: 150,
slot: true,
},
{
prop: 'roles',
label: '角色',
minWidth: 150,
slot: true,
},
```
- [ ] **Step 3: 添加树形数据状态**
`const { t } = useI18n()` 后添加:
```typescript
/**
* 部门树数据
*/
const deptTree = ref<any[]>([])
/**
* 角色树数据
*/
const roleTree = ref<any[]>([])
/**
* 加载部门树
*/
async function loadDeptTree() {
try {
const list = await deptService.list({ status: 1 })
deptTree.value = list || []
} catch {
// 错误已由请求拦截器统一提示
}
}
/**
* 加载角色列表(转换为树形结构)
*/
async function loadRoleTree() {
try {
const list = await roleService.list({ status: 1 })
// 角色列表已经是扁平结构,直接作为树形数据使用
roleTree.value = list || []
} catch {
// 错误已由请求拦截器统一提示
}
}
// 组件挂载时加载基础数据
onMounted(() => {
loadDeptTree()
loadRoleTree()
})
```
- [ ] **Step 4: 添加部门/角色筛选条件到搜索区域**
`<template>``#search` slot 中,在状态筛选后添加:
```vue
<el-form-item label="所属部门">
<el-tree-select
v-model="q.deptId"
:data="deptTree"
check-strictly
node-key="id"
:props="{ label: 'deptName', children: 'children' }"
placeholder="请选择部门"
clearable
style="width: 180px"
/>
</el-form-item>
<el-form-item label="角色">
<el-tree-select
v-model="q.roleId"
:data="roleTree"
check-strictly
node-key="id"
:props="{ label: 'roleName', children: 'children' }"
placeholder="请选择角色"
clearable
style="width: 180px"
/>
</el-form-item>
```
- [ ] **Step 5: 添加部门/角色列的自定义渲染**
`<template>` 中,在 `#column-status` slot 后添加:
```vue
<!-- 自定义列所属部门 -->
<template #column-depts="{ row }">
<template v-if="row.depts?.length">
<el-tag
v-for="dept in row.depts"
:key="dept.deptId"
:type="dept.main ? 'primary' : 'info'"
size="small"
class="mr-1 mb-1"
>
{{ dept.deptName }}
</el-tag>
</template>
<span v-else>-</span>
</template>
<!-- 自定义列角色 -->
<template #column-roles="{ row }">
<template v-if="row.roles?.length">
<el-tag
v-for="role in row.roles"
:key="role.roleId"
type="success"
size="small"
class="mr-1 mb-1"
>
{{ role.roleName }}
</el-tag>
</template>
<span v-else>-</span>
</template>
```
- [ ] **Step 6: 验证模板语法**
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/views/user/info/Index.vue`
Expected: 无错误输出
- [ ] **Step 7: Commit**
```bash
git add admin-ui/src/views/user/info/Index.vue
git commit -m "feat(user): add dept/role columns and tree filters to user list page"
```
---
## Task 3: 用户详情弹窗使用聚合接口
**Files:**
- Modify: `admin-ui/src/views/user/info/UserDetailDialog.vue`
- [ ] **Step 1: 添加导入和状态**
`<script setup>` 顶部修改:
```typescript
import { computed, ref, watch } from 'vue'
import { userService } from '@/service/user/userService'
const props = defineProps<{
visible: boolean
row: any
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
})
/**
* 用户聚合数据
*/
const userAggregate = ref<any>(null)
const loading = ref(false)
/**
* 加载聚合数据
*/
async function loadAggregateData() {
if (!props.row?.id) return
loading.value = true
try {
userAggregate.value = await userService.aggregate(props.row.id)
} catch {
// 错误已由请求拦截器统一提示
// 回退到使用 props.row
userAggregate.value = props.row
} finally {
loading.value = false
}
}
// 监听弹窗显示,加载聚合数据
watch(() => props.visible, (val) => {
if (val) {
userAggregate.value = null
loadAggregateData()
}
})
const userTypeMap: Record<number, string> = {
1: '普通用户',
2: '管理员',
3: '系统用户',
}
```
- [ ] **Step 2: 修改模板使用聚合数据**
将模板中的 `row?.` 替换为 `userAggregate?.` 或回退到 `row?.`
```vue
<template>
<el-dialog
v-model="dialogVisible"
title="用户详情"
width="700px"
class="rui-dialog"
destroy-on-close
>
<div v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户ID">
{{ userAggregate?.id || row?.id || '-' }}
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ userAggregate?.username || row?.username || '-' }}
</el-descriptions-item>
<el-descriptions-item label="用户类型">
<el-tag :type="(userAggregate?.userType || row?.userType) === 2 ? 'warning' : (userAggregate?.userType || row?.userType) === 3 ? 'danger' : 'info'" size="small">
{{ userTypeMap[userAggregate?.userType || row?.userType] || '未知' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="(userAggregate?.status || row?.status) === 1" type="success" size="small">启用</el-tag>
<el-tag v-else type="danger" size="small">禁用</el-tag>
</el-descriptions-item>
<el-descriptions-item label="手机号">
{{ userAggregate?.phone || row?.phone || '-' }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
{{ userAggregate?.email || row?.email || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ (userAggregate?.createdAt || row?.createdAt) ? new Date(userAggregate?.createdAt || row?.createdAt).toLocaleString() : '-' }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ (userAggregate?.updatedAt || row?.updatedAt) ? new Date(userAggregate?.updatedAt || row?.updatedAt).toLocaleString() : '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 部门信息 -->
<div class="mt-4">
<h4 class="text-sm font-bold mb-2">所属部门</h4>
<div v-if="userAggregate?.depts?.length" class="flex flex-wrap gap-2">
<el-tag
v-for="dept in userAggregate.depts"
:key="dept.deptId"
:type="dept.main ? 'primary' : 'info'"
size="small"
>
{{ dept.deptName }}
<el-tag v-if="dept.main" type="danger" size="small" class="ml-1"></el-tag>
</el-tag>
</div>
<el-empty v-else description="暂无部门信息" :image-size="60" />
</div>
<!-- 角色信息 -->
<div class="mt-4">
<h4 class="text-sm font-bold mb-2">角色</h4>
<div v-if="userAggregate?.roles?.length" class="flex flex-wrap gap-2">
<el-tag
v-for="role in userAggregate.roles"
:key="role.roleId"
type="success"
size="small"
>
{{ role.roleName }}
</el-tag>
</div>
<el-empty v-else description="暂无角色信息" :image-size="60" />
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">
关闭
</el-button>
</template>
</el-dialog>
</template>
```
- [ ] **Step 3: 验证语法**
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/views/user/info/UserDetailDialog.vue`
Expected: 无错误输出
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/views/user/info/UserDetailDialog.vue
git commit -m "feat(user): use aggregate API in user detail dialog to show depts and roles"
```
---
## Task 4: 用户表单从聚合数据解析已选部门/角色
**Files:**
- Modify: `admin-ui/src/views/user/info/UserFormDialog.vue`
- [ ] **Step 1: 修改 watch 逻辑解析 deptIds 和 roleIds**
`watch(() => props.visible, async (val) => { ... })` 中,修改编辑时的数据处理:
```typescript
watch(() => props.visible, async (val) => {
if (val) {
await Promise.all([loadDeptTree(), loadPostList()])
if (props.row) {
// 从聚合数据解析已选部门ID和角色ID
const deptIds = props.row.depts?.map((d: any) => d.deptId) || []
const roleIds = props.row.roles?.map((r: any) => r.roleId) || []
// 先设置表单数据,确保 rules 能正确计算
form.value = {
...props.row,
password: '',
deptIds: deptIds,
postIds: props.row.postIds || [],
}
}
else {
resetForm()
}
}
})
```
- [ ] **Step 2: 可选 - 移除冗余的 userDeptService 导入**
由于不再需要在表单中调用 `userDeptService.listDeptIdsByUserId`,可以移除该导入(但保留 `assignDepts` 用于保存):
**注意:** 当前代码中没有直接调用 `listDeptIdsByUserId`,所以无需修改导入。`userDeptService` 仍在 `onSubmit` 中被使用。
- [ ] **Step 3: 验证语法**
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/views/user/info/UserFormDialog.vue`
Expected: 无错误输出
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/views/user/info/UserFormDialog.vue
git commit -m "feat(user): parse deptIds and roleIds from aggregate data in user form"
```
---
## Task 5: 验证和最终检查
- [ ] **Step 1: 运行类型检查**
Run: `cd admin-ui && npx vue-tsc --noEmit --skipLibCheck`
Expected: 无错误输出
- [ ] **Step 2: 运行构建**
Run: `cd admin-ui && npm run build`
Expected: 构建成功,无错误
- [ ] **Step 3: 检查所有变更**
Run: `git log --oneline -5`
Expected: 看到 4 个提交:
- feat(user): add aggregate method to UserService...
- feat(user): add dept/role columns and tree filters to user list page
- feat(user): use aggregate API in user detail dialog...
- feat(user): parse deptIds and roleIds from aggregate data in user form
- [ ] **Step 4: 最终提交(可选,如果需要合并)**
```bash
# 如果需要,可以创建一个合并提交
git log --oneline -5
```
---
## 回滚计划
如果出现问题,可以按以下顺序回滚:
1. 回滚 Task 4: `git revert <task4-commit>`
2. 回滚 Task 3: `git revert <task3-commit>`
3. 回滚 Task 2: `git revert <task2-commit>`
4. 回滚 Task 1: `git revert <task1-commit>`
---
## 测试清单
- [ ] 用户列表页显示部门列(主部门高亮)
- [ ] 用户列表页显示角色列
- [ ] 部门树形筛选正常工作
- [ ] 角色树形筛选正常工作
- [ ] 同时筛选部门和角色正常工作
- [ ] 用户详情弹窗显示完整部门列表
- [ ] 用户详情弹窗显示完整角色列表
- [ ] 编辑用户时正确加载已选部门
- [ ] 编辑用户时正确加载已选角色
- [ ] 保存用户后数据正确刷新
---
**计划状态**: 待评审
**下一步**: 用户评审通过后,使用 superpowers-subagent-driven-development 或 superpowers-executing-plans 执行
@@ -0,0 +1,422 @@
# Wechat/Alipay Provider 凭证动态加载改造
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 修复 `WeixinAuthenticationProvider` / `AlipayAuthenticationProvider` 的凭证烧死 bug —— 改为持有 `AppCredentialsCache`、在请求时按 `X-App-Id` 动态解析凭证,使 SysApp CRUD / 缓存过期能立即生效。
**实施状态**: ✅ 已完成(2026-06-07commit `e3a441b`
**Architecture:** Provider 改造为"工具注入 + 内部解析"模式。每个请求处理时,从请求头读 `X-App-Id` → 调 `AppCredentialsCache.get(appId)` → 拿最新凭证 → 动态构造 `WechatApiClient`/`AlipayApiClient` → 调第三方 API。`OAuth2ServerConfig` 简化为只负责依赖注入。
**Tech Stack:** Java 21, Spring Boot 4.x, Spring Security OAuth2, Fastjson2, MyBatis Plus
---
## 文件结构
### 修改
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
- 构造参数:`WechatApiClient``AppCredentialsCache`
- 新增私有方法 `currentRequestAppId()``X-App-Id`
- `buildToken()` 改为按 appId 解析凭证后动态构造 `WechatApiClient`
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java`
- 构造参数:`AlipayApiClient``AppCredentialsCache`
- `buildToken()` 同样按 appId 解析凭证后动态构造 `AlipayApiClient`
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java`
-`resolveCredentials()` / `currentRequestAppId()` 私有方法
-`new WechatApiClient(...)` / `new AlipayApiClient(...)` 单例构造
-`appCredentialsCache` 直接传给两个 Provider
- 清理不再需要的 import
### 验收点
- [x] 微信登录请求:第一次请求时 `WechatApiClient``X-App-Id` 解析的凭证调微信 API
- [x] SysApp 增删改后:缓存被 evict,下次请求自动用新凭证(不需重启)
- [x] 缓存过期 30min 后:下次请求自动从 DB 重新加载凭证
- [x] `X-App-Id` 缺失 / 凭证不存在:抛 `OAuth2AuthenticationException` + `server_error` + 描述含 appId
- [x] 编译通过 `rui-common-oauth2` 模块
- [x] 不影响 `PasswordAuthenticationProvider` / `SmsAuthenticationProvider`
---
## Task 1: WeixinAuthenticationProvider 改造
**Files:**
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
- [x] **Step 1: 替换字段**
`private final WechatApiClient wechatApiClient;` 改为:
```java
private final AppCredentialsCache appCredentialsCache;
```
- [x] **Step 2: 修改构造函数**
构造参数 `WechatApiClient wechatApiClient` → `AppCredentialsCache appCredentialsCache`
```java
public WeixinAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
AppCredentialsCache appCredentialsCache,
UserAuthFeign userAuthFeign) {
super(authenticationManager, authorizationService, tokenGenerator);
this.appCredentialsCache = appCredentialsCache;
this.userAuthFeign = userAuthFeign;
}
```
- [x] **Step 3: 添加 `X-App-Id` 读取辅助方法**
```java
/**
* 从当前请求上下文读取 X-App-Id 头。
* <p>
* 微信/支付宝登录必须通过该头传递应用标识,
* 以支持多租户/多应用凭证隔离。
*
* @return appId;未传或读取失败返回 null
*/
private String currentRequestAppId() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return null;
}
HttpServletRequest request = attrs.getRequest();
return request.getHeader("X-App-Id");
} catch (Exception e) {
return null;
}
}
```
需要的 import
```java
import com.rui.common.oauth2.cache.AppCredentialsCache;
import com.rui.common.oauth2.cache.AppCredentialsVO;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
```
删除的 import
```java
// (如有) import com.rui.common.oauth2.authentication.weixin.WechatApiClient; // 字段类型变了,但本包内仍可访问
```
实际上 `WechatApiClient` 仍在 `buildToken` 里 new 出来用,import 不变。
- [x] **Step 4: 改造 `buildToken` 方法**
```java
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String code = (String) reqParameters.get("code");
String phone = (String) reqParameters.get("phone");
// 1. 从请求头拿 X-App-Id
String appId = currentRequestAppId();
if (appId == null || appId.isBlank()) {
log.warn("微信登录缺少 X-App-Id 头");
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"wechat login requires X-App-Id header",
ERROR_URI));
}
// 2. 从缓存拿凭证(30min TTL + 空对象防穿透 + 服务降级)
AppCredentialsVO creds = appCredentialsCache.get(appId);
if (creds == null || creds.getAppId() == null || creds.getAppId().isBlank()) {
log.warn("微信登录凭证未配置或服务降级: appId={}", appId);
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"wechat credentials not configured for appId=" + appId,
ERROR_URI));
}
// 3. 用最新凭证动态构造 API 客户端(支持 SysApp CRUD / 缓存过期即时生效)
WechatApiClient wechatApiClient = new WechatApiClient(creds.getAppId(), creds.getAppSecret());
// 4. 调用微信 API 换取 openId 和 unionId
WechatApiClient.WechatTokenResponse wxResponse = wechatApiClient.getAccessToken(code);
String openId = wxResponse.getOpenid();
String unionId = wxResponse.getUnionid();
log.info("微信登录: appId={}, openId={}, unionId={}, phone={}", appId, openId, unionId, phone);
// TODO: 这里需要调用 UserSocialService 查询绑定关系
// 暂时使用 openId 作为 principal
String principal = openId + "#" + unionId + "#" + (phone != null ? phone : "");
return new UsernamePasswordAuthenticationToken(principal, null);
}
```
需要的常量(类顶部添加):
```java
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
```
- [x] **Step 5: 编译验证**
```bash
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
```
预期:`BUILD SUCCESS`
- [x] **Step 6: Commit**
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java
git commit -m "refactor(oauth2): WeixinAuthenticationProvider 改为运行时解析凭证
- 构造参数 WechatApiClient → AppCredentialsCache
- buildToken 内按 X-App-Id 头解析凭证
- 每次请求动态构造 WechatApiClient,支持 SysApp CRUD / 缓存过期即时生效
- 凭证缺失抛 server_error(避免 ClassCastException
依赖 OAuth2ServerConfig 同步改造(Task 3)。"
```
---
## Task 2: AlipayAuthenticationProvider 改造
**Files:**
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java`
- [x] **Step 1: 替换字段**
把 `private final AlipayApiClient alipayApiClient;` 改为:
```java
private final AppCredentialsCache appCredentialsCache;
```
- [x] **Step 2: 修改构造函数**
构造参数 `AlipayApiClient alipayApiClient` → `AppCredentialsCache appCredentialsCache`
```java
public AlipayAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
AppCredentialsCache appCredentialsCache) {
super(authenticationManager, authorizationService, tokenGenerator);
this.appCredentialsCache = appCredentialsCache;
}
```
- [x] **Step 3: 添加 `X-App-Id` 读取辅助方法**(同 Task 1 Step 3
- [x] **Step 4: 改造 `buildToken` 方法**
```java
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String code = (String) reqParameters.get("code");
String phone = (String) reqParameters.get("phone");
// 1. 从请求头拿 X-App-Id
String appId = currentRequestAppId();
if (appId == null || appId.isBlank()) {
log.warn("支付宝登录缺少 X-App-Id 头");
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"alipay login requires X-App-Id header",
ERROR_URI));
}
// 2. 从缓存拿凭证
AppCredentialsVO creds = appCredentialsCache.get(appId);
if (creds == null || creds.getAppId() == null || creds.getAppId().isBlank()) {
log.warn("支付宝登录凭证未配置或服务降级: appId={}", appId);
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"alipay credentials not configured for appId=" + appId,
ERROR_URI));
}
// 3. 用最新凭证动态构造 API 客户端
// 字段映射说明(2026-06-07 修正):
// - AlipayApiClient 构造需要 (appId, privateKey, publicKey)
// - AppCredentialsVO 没有 privateKey/publicKey 字段
// - 按 spec 私钥/公钥存在 certificates JSON 数组里
// - 当前 AlipayApiClient.getAccessToken() 仍抛 UnsupportedOperationException
// (未接入 SDK),所以 privateKey/publicKey 暂用空串占位
// - 后续 TaskAlipay SDK 集成 + certificates JSON 解析
AlipayApiClient alipayApiClient = new AlipayApiClient(
creds.getAppId(),
"", // privateKey 占位
""); // publicKey 占位
// 4. 调用支付宝 API 获取 userId(按 spec 第 5.4 节:userId 作为唯一标识)
AlipayApiClient.AlipayTokenResponse alipayResponse = alipayApiClient.getAccessToken(code);
String userId = alipayResponse.getUserId();
log.info("支付宝登录: appId={}, userId={}, phone={}", appId, userId, phone);
// TODO: 查找或创建用户
String principal = userId + "#" + (phone != null ? phone : "");
return new UsernamePasswordAuthenticationToken(principal, null);
}
```
> 注意:当前 `AlipayApiClient(String appId, String privateKey, String publicKey)` 构造签名是 3 参。
> `AppCredentialsVO` 暂未确认字段,先按 `getAppKey()` / `getAesKey()` 假设,**实施时如发现字段不匹配,停下来汇报,不要硬猜**。
需要的 import(参考 Task 1)。
- [x] **Step 5: 编译验证**
```bash
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
```
- [x] **Step 6: Commit**
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java
git commit -m "refactor(oauth2): AlipayAuthenticationProvider 改为运行时解析凭证
- 构造参数 AlipayApiClient → AppCredentialsCache
- buildToken 内按 X-App-Id 头解析凭证
- 每次请求动态构造 AlipayApiClient,支持 SysApp CRUD / 缓存过期即时生效
- 凭证缺失抛 server_error
依赖 OAuth2ServerConfig 同步改造(Task 3)。"
```
---
## Task 3: OAuth2ServerConfig 清理
**Files:**
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java`
- [x] **Step 1: 替换 Provider 实例化代码**
在 `authorizationServerFilterChain` 方法内(约 130-141 行):
删除:
```java
// 微信:凭证从 X-App-Id 请求头 → AppCredentialsCache 拿
AppCredentialsVO wechatCreds = resolveCredentials(appCredentialsCache, "wechat");
WechatApiClient wechatApiClient = new WechatApiClient(
wechatCreds.getAppId(), wechatCreds.getAppSecret());
WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, wechatApiClient, userAuthFeign);
// 支付宝:暂用空凭证(certificates 解析未完成,TODO 接 Alipay SDK 后改造)
// 当前 AlipayApiClient 在 buildToken 时会抛 UnsupportedOperationException(按 spec 占位)
AlipayApiClient alipayApiClient = new AlipayApiClient("", "", "");
AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, alipayApiClient);
```
替换为:
```java
// 微信 / 支付宝:凭证由 Provider 内部按 X-App-Id 头从 AppCredentialsCache 解析
WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, appCredentialsCache, userAuthFeign);
AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, appCredentialsCache);
```
- [x] **Step 2: 删除 `resolveCredentials` / `currentRequestAppId` 私有方法**
删除约 151-181 行的两个方法。
- [x] **Step 3: 清理不再需要的 import**
删除:
```java
import com.rui.common.oauth2.authentication.weixin.WechatApiClient;
import com.rui.common.oauth2.authentication.alipay.AlipayApiClient;
import com.rui.common.oauth2.cache.AppCredentialsVO;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
```
- [x] **Step 4: 编译验证**
```bash
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
```
预期:`BUILD SUCCESS`
- [x] **Step 5: Commit**
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java
git commit -m "refactor(oauth2): OAuth2ServerConfig 清理凭证启动期构造
- 移除 resolveCredentials / currentRequestAppId 私有方法
- 移除启动期 new WechatApiClient / new AlipayApiClient
- Provider 构造改为直接注入 AppCredentialsCache
- 凭证解析完全下放到 Provider buildToken 请求路径
与 WeixinAuthenticationProvider / AlipayAuthenticationProvider 配合
实现按 X-App-Id 头的运行时凭证加载。"
```
---
## Task 4: 验证 AlipayApiClient 字段映射
**Files:**
- Read: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayApiClient.java`
- Read: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/cache/AppCredentialsVO.java`
- [x] **Step 1: 核对构造签名**
读取两个文件,确认:
- `AlipayApiClient` 构造参数类型与 `AppCredentialsVO` 提供的 getter 一一对应
- 如字段名不一致(如 `privateKey` vs `appSecret`),调整 Task 2 的代码
- [x] **Step 2: 必要时提交修复 commit**
如发现字段不匹配,单独 commit:
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java
git commit -m "fix(oauth2): 修正 AlipayApiClient 构造参数映射"
```
---
## 验收检查清单
| 验收点 | 对应任务 | 状态 |
|--------|---------|------|
| 微信登录按 X-App-Id 解析凭证 | Task 1 | [ ] |
| 支付宝登录按 X-App-Id 解析凭证 | Task 2 | [ ] |
| AppCredentialsCache 复用(30min TTL + 空对象穿透) | Task 1+2 | [ ] |
| OAuth2ServerConfig 不再启动期构造 API 客户端 | Task 3 | [ ] |
| 凭证缺失抛 OAuth2 server_error | Task 1+2 | [ ] |
| 编译通过 rui-common-oauth2 | Task 1+2+3 Step 5 | [ ] |
| 不影响 Password / Sms Provider | Task 1+2+3 | [ ] |
| AlipayApiClient 字段映射正确 | Task 4 | [ ] |
## 实施选项
1. **Subagent-Driven(推荐)** - 我为每个任务分派独立的子代理,任务间审查,快速迭代
2. **Inline Execution** - 在本会话中执行任务,批量执行并设置检查点
**请选择执行方式?**
---
## 实施完成报告
- **完成日期**: 2026-06-07
- **Commit**: `e3a441b` `refactor(oauth2): 微信/支付宝 Provider 改为运行时解析凭证`
- **改动**: 3 文件,+180/-78 行
- **影响分析**: risk=low0 affected processes
- **编译验证**: `mvn -pl rui-common/rui-common-oauth2 -am compile` BUILD SUCCESS
**遗留工作**(不在本次范围):
- Alipay SDK 集成 + certificates JSON 解析(`AlipayApiClient` privateKey/publicKey 暂传空串)
- 单元测试 / 集成测试覆盖
- 多 appId 池(当前每个请求 new 一个 WechatApiClient,可优化为按 appId 缓存)
@@ -0,0 +1,644 @@
# 门店管理新增字段 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 admin-ui 门店管理模块中适配后端新增的 9 个字段,修改表单弹窗和列表页,所有变更在现有 `useApiForm` + `ApiFormDialog` + `RuiTable` 框架内完成。
**Architecture:** 修改 2 个现有文件。`StoreFormDialog.vue` 新增 7 个可编辑字段(useApiForm fields 数组)、2 个只读字段(custom-fields 插槽)、数据双向转换逻辑(amenities JSON 序列化、serviceFeeRate 百分比转换)。`Index.vue` 新增 3 列(门店类型 Tag、包间信息、设施标签)、1 个筛选条件(门店类型下拉)、`parseAmenities` 工具函数。
**Tech Stack:** Vue 3 (Composition API / `<script setup>`) + TypeScript + Element Plus + Vite + pnpm
---
## File Structure
| # | 文件 | 操作 | 变更说明 |
|---|------|------|---------|
| 1 | `admin-ui/src/views/cashier/store/StoreFormDialog.vue` | 修改 | 扩展 `useApiForm``initial``fields`+7 字段);修改 `onSubmit` 添加 amenities/serviceFeeRate 数据转换;修改 `watch` 添加编辑回填数据转换;模板添加 `width="720px"``#custom-fields` 插槽 |
| 2 | `admin-ui/src/views/cashier/store/Index.vue` | 修改 | `queryParams` 增加 `storeType``handleReset` 补充 `storeType` 重置;新增 `parseAmenities` 工具函数;`columns` 增加 3 列;模板增加门店类型筛选和 3 个列 slot |
---
## Task 1: StoreFormDialog — 扩展 initial 默认值
**Files:**
- Modify: `admin-ui/src/views/cashier/store/StoreFormDialog.vue` (lines 3240)
- [ ] **Step 1: 在 `useApiForm` 的 `initial` 对象中追加新字段默认值**
**Old string:**
```ts
initial: {
storeName: '',
storeCode: '',
address: '',
contactPhone: '',
contactName: '',
businessHours: '',
status: 1,
},
```
**New string:**
```ts
initial: {
storeName: '',
storeCode: '',
address: '',
contactPhone: '',
contactName: '',
businessHours: '',
status: 1,
storeType: 'STANDARD',
amenities: [],
longitude: undefined,
latitude: undefined,
serviceFeeRate: undefined,
openingDate: '',
legalPerson: '',
},
```
- [ ] **Step 2: Commit**
```bash
git add admin-ui/src/views/cashier/store/StoreFormDialog.vue
git commit -m "feat(store): 扩展 useApiForm initial 默认值,新增 7 个字段默认值"
```
---
## Task 2: StoreFormDialog — 在 fields 数组中追加 7 个新字段
**Files:**
- Modify: `admin-ui/src/views/cashier/store/StoreFormDialog.vue` (lines 7686, status 字段之后、fields 数组结束之前)
- [ ] **Step 1: 在 status 字段配置之后、`],` 之前,插入 7 个新字段配置**
**Old string:**
```ts
{
key: 'status',
label: '状态',
type: 'radio',
required: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
],
onSubmit: async (data) => {
```
**New string:**
```ts
{
key: 'status',
label: '状态',
type: 'radio',
required: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
{
key: 'storeType',
label: '门店类型',
type: 'select',
required: true,
options: [
{ label: '旗舰店', value: 'FLAGSHIP' },
{ label: '标准店', value: 'STANDARD' },
{ label: '社区店', value: 'COMMUNITY' },
],
},
{
key: 'amenities',
label: '设施标签',
type: 'checkbox',
options: [
{ label: '免费停车', value: '免费停车' },
{ label: '免费WiFi', value: '免费WiFi' },
{ label: '充电桩', value: '充电桩' },
{ label: '24小时营业', value: '24小时营业' },
{ label: '包厢', value: '包厢' },
{ label: '吸烟区', value: '吸烟区' },
],
},
{
key: 'longitude',
label: '经度',
type: 'number',
props: { min: -180, max: 180, precision: 6, step: 0.000001 },
},
{
key: 'latitude',
label: '纬度',
type: 'number',
props: { min: -90, max: 90, precision: 6, step: 0.000001 },
},
{
key: 'serviceFeeRate',
label: '平台服务费率(%)',
type: 'number',
props: { min: 0, max: 100, precision: 2, step: 0.01 },
},
{
key: 'openingDate',
label: '开业日期',
type: 'date',
props: { valueFormat: 'YYYY-MM-DD' },
},
{
key: 'legalPerson',
label: '法人姓名',
type: 'input',
},
],
onSubmit: async (data) => {
```
- [ ] **Step 2: Commit**
```bash
git add admin-ui/src/views/cashier/store/StoreFormDialog.vue
git commit -m "feat(store): 在 useApiForm fields 中新增 7 个可编辑字段配置"
```
---
## Task 3: StoreFormDialog — 修改 onSubmit 添加数据转换
**Files:**
- Modify: `admin-ui/src/views/cashier/store/StoreFormDialog.vue` (lines 86100, onSubmit 回调)
- [ ] **Step 1: 替换 onSubmit 回调,添加 amenities JSON 序列化和 serviceFeeRate 百分比转小数**
**Old string:**
```ts
onSubmit: async (data) => {
const isEdit = !!(data as any).id
if (isEdit) {
await storeService.update(data)
ElMessage.success('修改成功')
}
else {
await storeService.add(data)
ElMessage.success('新增成功')
}
emit('success')
emit('update:visible', false)
},
```
**New string:**
```ts
onSubmit: async (rawData) => {
const data = { ...rawData } as any
// amenities: string[] → JSON 字符串
if (Array.isArray(data.amenities)) {
data.amenities = JSON.stringify(data.amenities)
}
// serviceFeeRate: 百分比 → 小数
if (data.serviceFeeRate != null && data.serviceFeeRate !== '') {
data.serviceFeeRate = Number(data.serviceFeeRate) / 100
}
const isEdit = !!data.id
if (isEdit) {
await storeService.update(data)
ElMessage.success('修改成功')
}
else {
await storeService.add(data)
ElMessage.success('新增成功')
}
emit('success')
emit('update:visible', false)
},
```
- [ ] **Step 2: Commit**
```bash
git add admin-ui/src/views/cashier/store/StoreFormDialog.vue
git commit -m "feat(store): onSubmit 中添加 amenities JSON 序列化和 serviceFeeRate 百分比转小数"
```
---
## Task 4: StoreFormDialog — 修改 watch 添加编辑回填数据转换
**Files:**
- Modify: `admin-ui/src/views/cashier/store/StoreFormDialog.vue` (lines 104116, watch 回调)
- [ ] **Step 1: 替换 watch 回调,添加 amenities JSON 解析和 serviceFeeRate 小数转百分比**
**Old string:**
```ts
// 监听编辑数据
watch(() => props.visible, (val) => {
if (val) {
if (props.row) {
// 编辑时设置表单数据
form.value = {
...props.row,
}
}
else {
resetForm()
}
}
})
```
**New string:**
```ts
// 监听编辑数据
watch(() => props.visible, (val) => {
if (val) {
if (props.row) {
const rowData = { ...props.row }
// amenities: JSON 字符串 → string[]
if (typeof rowData.amenities === 'string' && rowData.amenities) {
try {
rowData.amenities = JSON.parse(rowData.amenities)
}
catch {
rowData.amenities = []
}
}
else if (!Array.isArray(rowData.amenities)) {
rowData.amenities = []
}
// serviceFeeRate: 小数 → 百分比
if (rowData.serviceFeeRate != null) {
rowData.serviceFeeRate = Number(rowData.serviceFeeRate) * 100
}
// 编辑时设置表单数据
form.value = rowData
}
else {
resetForm()
}
}
})
```
- [ ] **Step 2: Commit**
```bash
git add admin-ui/src/views/cashier/store/StoreFormDialog.vue
git commit -m "feat(store): watch 中添加 amenities JSON 解析和 serviceFeeRate 小数转百分比回填"
```
---
## Task 5: StoreFormDialog — 模板添加 width 属性和 custom-fields 插槽
**Files:**
- Modify: `admin-ui/src/views/cashier/store/StoreFormDialog.vue` (lines 120128, template 中的 ApiFormDialog 标签)
- [ ] **Step 1: 替换 ApiFormDialog 自闭合标签为开闭标签,添加 width 和 custom-fields 插槽**
**Old string:**
```vue
<ApiFormDialog
v-model:visible="localVisible"
v-model:form="form"
:title="(form as any).id ? '编辑门店' : '新增门店'"
:fields="fields"
:rules="rules"
:loading="loading"
@submit="handleSubmit"
/>
```
**New string:**
```vue
<ApiFormDialog
v-model:visible="localVisible"
v-model:form="form"
:title="(form as any).id ? '编辑门店' : '新增门店'"
:width="'720px'"
:fields="fields"
:rules="rules"
:loading="loading"
@submit="handleSubmit"
>
<template #custom-fields="{ form: formData }">
<template v-if="formData.id">
<el-form-item label="包间总数">
<span>{{ formData.roomCount ?? '-' }}</span>
</el-form-item>
<el-form-item label="空闲包间数">
<span>{{ formData.freeRoomCount ?? '-' }}</span>
</el-form-item>
</template>
</template>
</ApiFormDialog>
```
- [ ] **Step 2: Commit**
```bash
git add admin-ui/src/views/cashier/store/StoreFormDialog.vue
git commit -m "feat(store): ApiFormDialog 增加 width=720px 和 custom-fields 插槽展示只读包间字段"
```
---
## Task 6: Index.vue — 扩展 queryParams、handleReset、新增 parseAmenities 工具函数
**Files:**
- Modify: `admin-ui/src/views/cashier/store/Index.vue`
- [ ] **Step 1: 在 queryParams 中增加 storeType 字段**
**Old string:**
```ts
const queryParams = ref({
storeName: '',
status: undefined as number | undefined,
})
```
**New string:**
```ts
const queryParams = ref({
storeName: '',
status: undefined as number | undefined,
storeType: undefined as string | undefined,
})
```
- [ ] **Step 2: 在 handleReset 中补充 storeType 重置**
**Old string:**
```ts
function handleReset() {
queryParams.value = {
storeName: '',
status: undefined,
}
tableRef.value?.reset()
}
```
**New string:**
```ts
function handleReset() {
queryParams.value = {
storeName: '',
status: undefined,
storeType: undefined,
}
tableRef.value?.reset()
}
```
- [ ] **Step 3: 在 `handleStatusChange` 函数之后(`</script>` 标签之前),添加 `parseAmenities` 工具函数**
**Old string:**
```ts
async function handleStatusChange(row: any, status: number) {
if (!row?.id) return
try {
await storeService.changeStatus(row.id, status)
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
} catch {
row.status = status === 1 ? 0 : 1
}
}
</script>
```
**New string:**
```ts
async function handleStatusChange(row: any, status: number) {
if (!row?.id) return
try {
await storeService.changeStatus(row.id, status)
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
} catch {
row.status = status === 1 ? 0 : 1
}
}
/**
* 解析设施标签(兼容 JSON 字符串和数组)
*/
function parseAmenities(val: any): string[] {
if (Array.isArray(val)) return val
if (typeof val === 'string' && val) {
try { return JSON.parse(val) } catch { return [] }
}
return []
}
</script>
```
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/views/cashier/store/Index.vue
git commit -m "feat(store): queryParams 增加 storeType 筛选、handleReset 补充重置、新增 parseAmenities 工具函数"
```
---
## Task 7: Index.vue — 在 columns 数组中追加 3 列配置
**Files:**
- Modify: `admin-ui/src/views/cashier/store/Index.vue` (lines 1228, columns 数组)
- [ ] **Step 1: 在 address 列之后追加 storeType、roomInfo、amenities 3 列**
**Old string:**
```ts
{ prop: 'address', label: '地址', minWidth: 200, tooltip: true },
{ prop: 'businessHours', label: '营业时间', width: 120 },
```
**New string:**
```ts
{ prop: 'address', label: '地址', minWidth: 200, tooltip: true },
{ prop: 'storeType', label: '门店类型', width: 100, align: 'center', slot: true },
{ prop: 'roomInfo', label: '包间', width: 120, align: 'center', slot: true },
{ prop: 'amenities', label: '设施标签', minWidth: 200, slot: true },
{ prop: 'businessHours', label: '营业时间', width: 120 },
```
- [ ] **Step 2: Commit**
```bash
git add admin-ui/src/views/cashier/store/Index.vue
git commit -m "feat(store): columns 增加 storeType/roomInfo/amenities 3 列配置"
```
---
## Task 8: Index.vue — 模板添加门店类型筛选和 3 个列 slot
**Files:**
- Modify: `admin-ui/src/views/cashier/store/Index.vue` (template 部分)
- [ ] **Step 1: 在状态筛选的 `</el-form-item>` 之后、查询按钮的 `<el-form-item>` 之前,插入门店类型筛选**
**Old string:**
```vue
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
```
**New string:**
```vue
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="门店类型">
<el-select v-model="queryParams.storeType" placeholder="请选择门店类型" clearable>
<el-option label="旗舰店" value="FLAGSHIP" />
<el-option label="标准店" value="STANDARD" />
<el-option label="社区店" value="COMMUNITY" />
</el-select>
</el-form-item>
<el-form-item>
```
- [ ] **Step 2: 在状态列 slot`#column-status`)的 `</template>` 之后、操作列 slot 之前,插入 3 个列 slot 模板**
**Old string:**
```vue
<!-- 状态列 -->
<template #column-status="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="(val: any) => handleStatusChange(row, val as number)"
/>
</template>
<!-- 操作列 -->
```
**New string:**
```vue
<!-- 状态列 -->
<template #column-status="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="(val: any) => handleStatusChange(row, val as number)"
/>
</template>
<!-- 门店类型列 -->
<template #column-storeType="{ row }">
<el-tag
:type="row.storeType === 'FLAGSHIP' ? 'danger' : row.storeType === 'STANDARD' ? '' : 'info'"
size="small"
>
{{ { FLAGSHIP: '旗舰店', STANDARD: '标准店', COMMUNITY: '社区店' }[row.storeType] || '-' }}
</el-tag>
</template>
<!-- 包间信息列 -->
<template #column-roomInfo="{ row }">
<span>{{ row.freeRoomCount ?? '-' }}/{{ row.roomCount ?? '-' }}</span>
</template>
<!-- 设施标签列 -->
<template #column-amenities="{ row }">
<template v-if="parseAmenities(row.amenities).length">
<el-tag
v-for="tag in parseAmenities(row.amenities)"
:key="tag"
size="small"
type="info"
class="mr-1 mb-1"
>
{{ tag }}
</el-tag>
</template>
<span v-else>-</span>
</template>
<!-- 操作列 -->
```
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/views/cashier/store/Index.vue
git commit -m "feat(store): 模板新增门店类型筛选条件和 storeType/roomInfo/amenities 列 slot"
```
---
## Task 9: 构建验证 + 最终提交
- [ ] **Step 1: 运行 TypeScript 类型检查**
Run: `pnpm --filter admin-ui type-check`
Expected: 0 errors, 命令退出码 0
- [ ] **Step 2: 运行 ESLint 检查**
Run: `pnpm --filter admin-ui lint`
Expected: 0 errors, 0 warnings(或仅有与本次修改无关的已存在 warnings)
- [ ] **Step 3: 运行 Vite 构建**
Run: `pnpm --filter admin-ui build`
Expected: 构建成功,无编译错误,输出 dist 目录
- [ ] **Step 4: 启动开发服务器进行手动验证**
Run: `pnpm --filter admin-ui dev`
Expected: 开发服务器正常启动,浏览器打开后:
1. 门店管理列表页正确展示新增 3 列
2. 门店类型下拉筛选功能正常
3. 点击新增门店,弹窗宽度 720px,7 个新字段正确渲染
4. 新增模式下不显示包间总数/空闲包间数
5. 点击编辑门店,所有新字段正确回填,包间字段只读展示
6. 提交表单无报错
验证完毕后按 `Ctrl+C` 停止开发服务器。
---
## Verification Checklist
- [ ] `pnpm --filter admin-ui type-check` 通过
- [ ] `pnpm --filter admin-ui lint` 通过
- [ ] `pnpm --filter admin-ui build` 成功
- [ ] 列表新增 3 列正确展示(门店类型 Tag、包间 X/Y、设施多 Tag
- [ ] 门店类型筛选功能正常(筛选 + 重置)
- [ ] 新增门店:7 个新字段可正常填写和提交
- [ ] 编辑门店:所有新字段正确回填,只读字段不可编辑
- [ ] amenities 数据双向转换正确(JSON 字符串 ↔ 数组)
- [ ] serviceFeeRate 数据双向转换正确(小数 ↔ 百分比)
- [ ] 新增模式下 roomCount/freeRoomCount 区域不显示
- [ ] 无数据时各列正确降级显示 `-`
@@ -0,0 +1,483 @@
# 前后端智能协作方案设计文档
> **文档版本**: v1.0
> **创建日期**: 2026-06-06
> **适用范围**: rui-frontend 所有前端项目(admin-ui、cashier-mobile、cashier-customer
---
## 一、问题背景
### 1.1 当前痛点
admin-ui 项目中存在**表单字段与后台 API 不同步**的问题:
1. **表单字段硬编码** - 19 个 FormDialog 组件全部采用手动定义字段(如 `UserFormDialog.vue` 中的 `username`, `password`, `userType` 等)
2. **缺少 TypeScript 类型** - Service 层大量使用 `any` 类型,没有与后端 DTO 对应的接口定义
3. **BaseService 泛型未充分利用** - `BaseService<T>` 定义了泛型,但子类没有传入具体类型
4. **API 文档未利用** - 后端已提供 `/v3/api-docs`SpringDoc OpenAPI),但前端未使用
### 1.2 影响
- 后端字段变更时,前端需要手动修改所有相关表单
- 缺少编译时类型检查,运行时容易出错
- 前后端协作效率低,沟通成本高
---
## 二、设计目标
1. **类型安全** - 前端类型与后端 API 自动同步
2. **减少样板代码** - 表单配置化,减少 70% 重复代码
3. **支持自定义扩展** - 复杂表单可自定义字段和逻辑
4. **多模块支持** - 适配微服务架构(聚合启动器 + 独立服务)
5. **自动化检测** - 防止前后端不同步上线
---
## 三、架构设计
### 3.1 整体架构
```
API设计规范.md (单一事实来源)
├── 包含所有 api-docs 地址
└── 由后端团队维护
↓ 自动解析
scripts/parse-api-config.ts
└── 读取并解析 Markdown 中的 URL
└── 识别聚合启动器和独立服务
↓ 动态配置
scripts/generate-api.ts
├── 聚合启动器模块(通过 group 参数获取)
├── 独立服务模块(直接访问)
├── 生成类型定义文件
├── 生成统一导出
└── 生成模块映射
↓ 类型文件
src/types/
├── system-api.d.ts # 系统服务类型
├── user-api.d.ts # 用户服务类型
├── cashier-api.d.ts # 收银服务类型
├── api.d.ts # 统一导出
└── api-modules.json # 模块映射
↓ 类型驱动
前端代码
├── Service 层(类型安全)
├── useApiForm() 组合式函数
├── ApiFormDialog 组件
└── 自定义扩展(插槽机制)
```
### 3.2 核心组件
| 组件 | 职责 | 说明 |
|------|------|------|
| `parse-api-config.ts` | 解析 API 规范文档 | 从 Markdown 读取 api-docs 地址 |
| `generate-api.ts` | 生成类型定义 | 调用 openapi-typescript 生成 .d.ts |
| `useApiForm()` | 类型驱动表单管理 | 组合式函数,自动生成表单配置 |
| `ApiFormDialog` | 配置化表单组件 | 根据字段配置自动渲染表单 |
| `BaseService<T>` | 类型安全 Service 基类 | 保留现有架构,增强类型 |
---
## 四、详细设计
### 4.1 API 配置解析(parse-api-config.ts
**核心功能**:从 `API设计规范.md` 自动读取 api-docs 地址
**聚合启动器识别规则**
- 端口 9399 → 聚合启动器,包含多个模块(/user, /system
- 通过 `?group=xxx` 参数获取子模块
- 其他端口 → 独立服务
**配置结构**
```typescript
interface ApiModuleConfig {
name: string // 模块名:user, system, cashier
url: string // api-docs URL
output: string // 输出路径
prefix: string // API 路径前缀
description: string // 模块描述
aggregator?: string // 所属聚合器(如果有)
}
```
**解析示例**
```markdown
# API设计规范.md
http://localhost:9399/v3/api-docs # 聚合启动器 API 文档(开发调试)
http://localhost:9601/v3/api-docs # 收银服务 API 文档
```
解析结果:
- `9399` → 聚合启动器 → 展开为 user、system 两个模块
- `9601` → 独立服务 → cashier 模块
### 4.2 类型生成(generate-api.ts
**生成流程**
1. 读取 API设计规范.md
2. 识别聚合启动器和独立服务
3. 为每个模块生成类型文件
4. 生成统一导出文件
5. 生成模块映射文件
**输出文件结构**
```
src/types/
├── user-api.d.ts # 用户服务类型(来自聚合启动器)
├── system-api.d.ts # 系统服务类型(来自聚合启动器)
├── cashier-api.d.ts # 收银服务类型(独立服务)
├── api.d.ts # 统一导出
│ └── export type * from './user-api'
│ └── export type * from './system-api'
│ └── export type * from './cashier-api'
└── api-modules.json # 模块映射(用于运行时)
└── {
└── "aggregator": [
└── { "name": "user", "prefix": "/user" },
└── { "name": "system", "prefix": "/system" }
└── ]
└── }
```
### 4.3 类型驱动表单(useApiForm
**核心功能**:根据 TypeScript 类型自动生成表单配置
**接口设计**
```typescript
interface FieldConfig<T = any> {
key: keyof T // 字段名(类型安全)
label: string // 字段标签
type: FieldType // 字段类型
required?: boolean // 是否必填
rules?: any[] // 验证规则
options?: Option[] // 选项(select/radio/checkbox
disabled?: boolean | ((form: T) => boolean)
placeholder?: string
props?: Record<string, any>
}
interface FormConfig<T> {
initial?: Partial<T> // 初始值
fields: FieldConfig<T>[] // 字段配置
onSubmit: (data: T) => Promise<void>
beforeSubmit?: (data: T) => boolean | string
}
function useApiForm<T extends Record<string, any>>(
config: FormConfig<T>
): {
formRef: Ref<FormInstance>
form: Ref<Partial<T>>
rules: ComputedRef<Record<string, any[]>>
loading: Ref<boolean>
handleSubmit: () => Promise<boolean>
resetForm: (initial?: Partial<T>) => void
}
```
### 4.4 配置化表单组件(ApiFormDialog
**核心功能**:根据字段配置自动渲染表单元素
**支持的字段类型**
| 类型 | 组件 | 说明 |
|------|------|------|
| `input` | ElInput | 文本输入 |
| `textarea` | ElInput(type="textarea") | 多行文本 |
| `select` | ElSelect | 下拉选择 |
| `radio` | ElRadioGroup | 单选按钮 |
| `checkbox` | ElCheckboxGroup | 多选框 |
| `number` | ElInputNumber | 数字输入 |
| `tree-select` | ElTreeSelect | 树形选择 |
| `date` | ElDatePicker | 日期选择 |
| `datetime` | ElDatePicker | 日期时间选择 |
**自定义扩展机制**
```vue
<ApiFormDialog
v-model:visible="visible"
v-model:form="form"
:fields="fields"
:rules="rules"
@submit="handleSubmit"
>
<!-- 自定义字段插槽 -->
<template #custom-fields="{ form }">
<el-form-item label="自定义部门">
<el-tree-select v-model="customDeptIds" :data="deptTree" />
</el-form-item>
</template>
</ApiFormDialog>
```
### 4.5 Service 层增强
**保留现有 BaseService 架构**,增强类型安全:
```typescript
// 现有架构保持不变
class BaseService<T = any> {
protected baseUrl: string
async page(params: PageParams & Record<string, any>): Promise<PageResult<T>>
async list(params?: Record<string, any>): Promise<T[]>
async getById(id: number | string): Promise<T>
async add(data: Partial<T>): Promise<T>
async update(data: Partial<T> & { id: number | string }): Promise<boolean>
async remove(id: number | string): Promise<boolean>
}
// 使用时传入具体类型
class UserService extends BaseService<UserDTO> {
constructor() {
super('/user/admin/user')
}
}
```
---
## 五、使用示例
### 5.1 标准表单(完全配置化)
```vue
<script setup lang="ts">
import { useApiForm } from '@/composables/useApiForm'
import { userService } from '@/service/user/userService'
import type { components } from '@/types/user-api'
type UserDTO = components['schemas']['UserDTO']
const { formRef, form, rules, loading, handleSubmit, resetForm } = useApiForm<UserDTO>({
initial: { userType: 1, status: 1 },
fields: [
{ key: 'username', label: '用户名', type: 'input', required: true },
{ key: 'password', label: '密码', type: 'input', required: true, props: { type: 'password' } },
{ key: 'userType', label: '用户类型', type: 'select', options: [
{ label: '普通用户', value: 1 },
{ label: '管理员', value: 2 }
]},
{ key: 'status', label: '状态', type: 'radio', options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]}
],
onSubmit: async (data) => {
await userService.add(data)
}
})
</script>
<template>
<ApiFormDialog
v-model:visible="visible"
v-model:form="form"
title="新增用户"
:fields="fields"
:rules="rules"
:loading="loading"
@submit="handleSubmit"
/>
</template>
```
### 5.2 自定义扩展表单
```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useApiForm } from '@/composables/useApiForm'
import { roleService } from '@/service/system/roleService'
import type { components } from '@/types/system-api'
type RoleDTO = components['schemas']['RoleDTO']
const selectedDeptIds = ref<number[]>([])
const showCustomDept = computed(() => form.value.dataScope === 5)
const { form, fields, rules, handleSubmit } = useApiForm<RoleDTO>({
fields: [
{ key: 'roleCode', label: '角色编码', type: 'input', required: true },
{ key: 'roleName', label: '角色名称', type: 'input', required: true },
{ key: 'dataScope', label: '数据范围', type: 'select', options: [
{ label: '全部', value: 1 },
{ label: '自定义', value: 5 }
]}
],
onSubmit: async (data) => {
// 自定义验证
if (data.dataScope === 5 && selectedDeptIds.value.length === 0) {
throw new Error('请选择部门')
}
await roleService.add(data)
}
})
</script>
<template>
<ApiFormDialog
v-model:visible="visible"
v-model:form="form"
:fields="fields"
:rules="rules"
@submit="handleSubmit"
>
<template #custom-fields>
<el-form-item v-if="showCustomDept" label="选择部门" required>
<el-tree-select v-model="selectedDeptIds" :data="deptTree" />
</el-form-item>
</template>
</ApiFormDialog>
</template>
```
---
## 六、自动化机制
### 6.1 Git Hook(提交前检查)
```typescript
// scripts/check-api-changes.ts
function checkApiChanges() {
const currentHash = execSync('git hash-object src/types/*.d.ts').toString()
try {
execSync('pnpm api:generate', { stdio: 'pipe' })
const newHash = execSync('git hash-object src/types/*.d.ts').toString()
if (currentHash !== newHash) {
console.error('❌ API types have changed!')
console.error('Please run "pnpm api:generate" and commit the changes.')
process.exit(1)
}
} catch (error) {
console.warn('⚠️ Could not check API changes')
}
}
```
### 6.2 脚本命令
```json
{
"scripts": {
"api:generate": "tsx scripts/generate-api.ts",
"api:check": "tsx scripts/check-api-changes.ts",
"api:watch": "nodemon --watch ../docs/standards/API设计规范.md --exec 'pnpm api:generate'"
}
}
```
---
## 七、扩展性设计
### 7.1 新增模块
当后端新增模块时,只需在 `API设计规范.md` 中添加:
```markdown
http://localhost:9399/v3/api-docs?group=订单服务 # 订单服务 API 文档
http://localhost:9701/v3/api-docs # 新独立服务 API 文档
```
前端自动识别:
- 端口 9399 → 聚合启动器,通过 `group` 参数获取
- 新端口 9701 → 独立服务,直接访问
### 7.2 新增字段类型
`ApiFormDialog.vue` 中添加新的字段类型渲染逻辑即可。
---
## 八、实施计划
### Phase 1: 基础搭建(1-2 天)
1. 安装依赖:`openapi-typescript`, `tsx`, `nodemon`
2. 创建 `scripts/parse-api-config.ts` - 解析 API 规范
3. 创建 `scripts/generate-api.ts` - 生成类型
4. 配置 package.json 脚本
### Phase 2: 类型生成(1 天)
1. 运行 `pnpm api:generate` 生成初始类型
2. 验证类型正确性
3. 提交生成的类型文件
### Phase 3: 表单工具(2-3 天)
1. 创建 `useApiForm()` 组合式函数
2. 创建 `ApiFormDialog.vue` 组件
3. 编写单元测试
### Phase 4: 试点迁移(2-3 天)
1. 选择 2-3 个简单表单进行迁移
2. 验证方案可行性
3. 收集反馈优化
### Phase 5: 全面推广(1-2 周)
1. 迁移所有标准表单
2. 保留自定义表单的特殊逻辑
3. 编写迁移文档
### Phase 6: 自动化(1 天)
1. 配置 Git Hook
2. 配置 CI/CD 检查
3. 编写使用文档
---
## 九、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 后端 API 文档不准确 | 生成的类型错误 | 建立 API 文档审核机制 |
| 聚合启动器不可用 | 无法生成类型 | 支持独立服务直接访问 |
| 复杂表单迁移困难 | 影响进度 | 保留自定义扩展机制 |
| 团队成员学习成本 | 初期效率下降 | 提供详细文档和示例 |
---
## 十、成功标准
1. ✅ 所有 Service 层使用具体类型替代 `any`
2. ✅ 标准表单代码量减少 70%
3. ✅ 后端字段变更时,前端编译期即可发现
4. ✅ 新增模块时,类型自动生成
5. ✅ 提交时自动检测 API 变更
---
> **下一步**: 编写实现计划(implementation plan
@@ -0,0 +1,640 @@
# 用户聚合查询设计规格
> **日期**: 2026-06-06
> **状态**: 已实现(2026-06-06
> **作者**: AI Assistant
> **相关模块**: rui-service-user
> **实施情况**: 20 个任务(T1T20)全部完成,详见 `docs/superpowers/plans/2026-06-06-user-aggregate-query-plan.md`。
---
## 1. 背景与问题
### 1.1 现状
当前用户数据分散在4张表中:
| 表名 | 说明 |
|------|------|
| `uc_user` | 用户基础信息(用户名、密码、状态等) |
| `uc_user_detail` | 用户详情(昵称、邮箱、手机号等) |
| `uc_user_dept` | 用户部门关联(支持多部门,有主部门标记) |
| `uc_user_post` | 用户岗位关联(支持多岗位) |
### 1.2 问题
- **前端请求过多**:获取完整用户信息需要3个独立请求
- **列表页性能差**:100条用户数据需要 1 + 100 + 100 = 201 个请求
- **数据一致性难保证**:多个请求可能部分失败
- **手机号位置不合理**:手机号在 `uc_user_detail` 表,但短信登录需要频繁查询,应该提升到 `uc_user`
- **认证接口不灵活**:当前 `loadByUsername` 只支持用户名,无法扩展支持手机号、邮箱等多种登录方式
### 1.3 约束条件
- 一个用户通常关联 **1个主部门 + 少量岗位**
- 需要支持 **多租户**tenantId
- 必须 **保持向后兼容**
- 需要 **缓存优化**
- 手机号需要 **唯一约束**(按租户)
---
## 2. 设计目标
1. **减少前端请求**:从3个减少到1个
2. **优化列表页性能**:批量查询,避免 N+1
3. **引入缓存**Redis 缓存用户聚合数据
4. **手机号迁移**:将 `phone``uc_user_detail` 迁移到 `uc_user`
5. **统一认证接口**:支持多种登录方式(用户名、手机号、邮箱等)
6. **保持兼容性**:现有接口不受影响
---
## 3. 技术方案
### 3.1 总体架构
```
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ 前端 │────▶│ UserController │────▶│ UserService │
└─────────────┘ └──────────────────┘ └──────┬───────┘
┌────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Redis Cache │ │ uc_user_dept │ │ uc_user_post │
│ user:agg:{id} │ │ (主部门+关联) │ │ (岗位关联) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 3.2 核心策略
- **保留关联表**:不改表结构,保持多对多关系的灵活性
- **聚合查询**:后端做 JOIN 或批量 IN 查询,一次性返回
- **两级缓存**
- L1:单用户聚合数据缓存(Redis)
- L2:批量查询结果不缓存(避免缓存过大)
- **缓存失效**:数据变更时主动失效
---
## 4. 数据模型
### 4.1 新增 VO 对象
```java
package com.rui.service.user.vo;
import com.rui.service.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 用户聚合信息(包含部门、岗位)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户聚合信息")
public class UserAggregateVO extends User {
@Schema(description = "部门列表")
private List<UserDeptVO> depts;
@Schema(description = "岗位列表")
private List<UserPostVO> posts;
@Schema(description = "主部门ID")
private Long mainDeptId;
@Schema(description = "主部门名称")
private String mainDeptName;
@Schema(description = "部门编码")
private String deptCode;
@Schema(description = "岗位编码")
private String postCode;
}
```
### 4.2 部门/岗位 VO
```java
@Data
@Schema(description = "用户部门信息")
public class UserDeptVO {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "部门ID")
private Long deptId;
@Schema(description = "部门编码")
private String deptCode;
@Schema(description = "部门名称")
private String deptName;
@Schema(description = "是否主部门")
private Boolean main;
}
@Data
@Schema(description = "用户岗位信息")
public class UserPostVO {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "岗位ID")
private Long postId;
@Schema(description = "岗位编码")
private String postCode;
@Schema(description = "岗位名称")
private String postName;
}
```
---
## 5. 接口设计
### 5.1 新增接口
#### 5.1.1 获取用户聚合信息(单用户)
```
GET /user/admin/user/{id}/aggregate
```
**响应示例:**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": 1,
"username": "admin",
"phone": "13800138000",
"userNo": "U001",
"userType": 2,
"status": 1,
"depts": [
{
"deptId": 1,
"deptCode": "TECH",
"deptName": "技术部",
"main": true
},
{
"deptId": 2,
"deptCode": "PROD",
"deptName": "产品部",
"main": false
}
],
"posts": [
{
"postId": 1,
"postCode": "JAVA_DEV",
"postName": "Java开发工程师"
}
],
"mainDeptId": 1,
"mainDeptName": "技术部",
"deptCode": "TECH",
"postCode": "JAVA_DEV"
}
}
```
#### 5.1.2 用户列表(增强版)
复用现有 `/user/admin/user` 列表接口,在响应中增加 `depts``posts` 字段。
**实现方式:**
- 列表查询时,先查询用户基础数据
- 批量查询所有用户的部门和岗位
- 组装到响应中
#### 5.1.3 统一用户认证查询(内部接口)
```
POST /user/inner/auth/load
```
**请求体:**
```json
{
"account": "13800138000",
"loginType": "PHONE"
}
```
**AccountType 枚举:**
```java
public enum AccountType {
USERNAME("用户名"),
PHONE("手机号"),
EMAIL("邮箱");
private final String description;
AccountType(String description) {
this.description = description;
}
}
```
**响应:** 与现有 `loadByUsername` 保持一致,返回用户认证信息
**实现逻辑:**
```java
@PostMapping("/auth/load")
public Result<JSONObject> loadUser(@RequestBody LoginAccountDTO loginAccount) {
User user;
switch (loginAccount.getLoginType()) {
case PHONE:
user = userService.lambdaQuery()
.eq(User::getPhone, loginAccount.getAccount())
.one();
break;
case EMAIL:
user = userService.lambdaQuery()
.eq(User::getEmail, loginAccount.getAccount())
.one();
break;
case USERNAME:
default:
user = userService.lambdaQuery()
.eq(User::getUsername, loginAccount.getAccount())
.one();
break;
}
if (user == null) {
return Result.ok(null);
}
// 组装认证信息(与现有逻辑一致)
JSONObject info = buildAuthInfo(user);
return Result.ok(info);
}
```
### 5.2 现有接口处理
**保留但标记为弃用:**
- `GET /user/inner/auth/loadByUsername/{username}`**@Deprecated**,建议迁移到新的 `/auth/load`
**继续保留的接口:**
- `GET /user/admin/user/{id}` — 用户基础信息
- `GET /user/admin/user-dept/user/{userId}` — 用户部门列表
- `GET /user/admin/user-post/user/{userId}` — 用户岗位列表
- `GET /user/admin/detail` — 用户详情(uc_user_detailphone 字段将移除)
---
## 6. 缓存设计
### 6.1 缓存策略
| 缓存项 | Key 格式 | TTL | 说明 |
|--------|---------|-----|------|
| 用户聚合信息 | `user:agg:{tenantId}:{userId}` | 10分钟 | 单用户完整数据 |
| 用户部门列表 | `user:dept:{tenantId}:{userId}` | 10分钟 | 部门ID列表 |
| 用户岗位列表 | `user:post:{tenantId}:{userId}` | 10分钟 | 岗位ID列表 |
### 6.2 缓存失效
**触发时机:**
- 用户部门变更(assignDepts、setMainDept
- 用户岗位变更(assignPosts
- 用户信息变更(update
- 用户删除
**失效逻辑:**
```java
private void evictUserCache(Long userId) {
Long tenantId = AuthUtil.getTenantId();
redisUtil.del(String.format("user:agg:%s:%s", tenantId, userId));
redisUtil.del(String.format("user:dept:%s:%s", tenantId, userId));
redisUtil.del(String.format("user:post:%s:%s", tenantId, userId));
}
```
### 6.3 缓存穿透防护
- 查询不到数据时,缓存空值(TTL 1分钟)
- 使用布隆过滤器(可选,初期可不用)
---
## 7. 查询逻辑
### 7.1 单用户聚合查询
```java
@Cacheable(value = "user:agg", key = "#tenantId + ':' + #userId")
public UserAggregateVO getUserAggregate(Long userId, Long tenantId) {
// 1. 查询用户基础信息
User user = userMapper.selectById(userId);
if (user == null) {
return null;
}
// 2. 查询部门(带名称)
List<UserDeptVO> depts = userDeptMapper.selectDeptListByUserId(userId);
// 3. 查询岗位(带名称)
List<UserPostVO> posts = userPostMapper.selectPostListByUserId(userId);
// 4. 组装
UserAggregateVO vo = new UserAggregateVO();
BeanUtils.copyProperties(user, vo);
vo.setDepts(depts);
vo.setPosts(posts);
// 6. 提取主部门
depts.stream()
.filter(UserDeptVO::getMain)
.findFirst()
.ifPresent(main -> {
vo.setMainDeptId(main.getDeptId());
vo.setMainDeptName(main.getDeptName());
});
return vo;
}
```
### 7.2 批量列表查询优化
**问题:** 列表页有100条数据,不能每条都查一次数据库
**方案:** 批量 IN 查询
```java
public Page<UserAggregateVO> listUserAggregate(Page<User> page, QueryWrapper<User> query) {
// 1. 查询用户基础数据
Page<User> userPage = userMapper.selectPage(page, query);
List<User> users = userPage.getRecords();
if (users.isEmpty()) {
return new Page<>();
}
// 2. 提取所有用户ID
List<Long> userIds = users.stream()
.map(User::getId)
.collect(Collectors.toList());
// 3. 批量查询部门(一次查询)
Map<Long, List<UserDeptVO>> deptMap = userDeptMapper
.selectDeptListByUserIds(userIds)
.stream()
.collect(Collectors.groupingBy(UserDeptVO::getUserId));
// 4. 批量查询岗位(一次查询)
Map<Long, List<UserPostVO>> postMap = userPostMapper
.selectPostListByUserIds(userIds)
.stream()
.collect(Collectors.groupingBy(UserPostVO::getUserId));
// 5. 组装
List<UserAggregateVO> records = users.stream().map(user -> {
UserAggregateVO vo = new UserAggregateVO();
BeanUtils.copyProperties(user, vo);
vo.setDepts(deptMap.getOrDefault(user.getId(), Collections.emptyList()));
vo.setPosts(postMap.getOrDefault(user.getId(), Collections.emptyList()));
// 提取主部门
vo.getDepts().stream()
.filter(UserDeptVO::getMain)
.findFirst()
.ifPresent(main -> {
vo.setMainDeptId(main.getDeptId());
vo.setMainDeptName(main.getDeptName());
});
return vo;
}).collect(Collectors.toList());
// 7. 构建分页结果
Page<UserAggregateVO> result = new Page<>();
result.setCurrent(userPage.getCurrent());
result.setSize(userPage.getSize());
result.setTotal(userPage.getTotal());
result.setRecords(records);
return result;
}
```
**SQL 示例(批量查询部门):**
```xml
<select id="selectDeptListByUserIds" resultType="com.rui.service.user.vo.UserDeptVO">
SELECT
ud.user_id as userId,
ud.dept_id as deptId,
d.name as deptName,
ud.is_main as main
FROM uc_user_dept ud
INNER JOIN uc_dept d ON ud.dept_id = d.id
WHERE ud.user_id IN
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND ud.deleted = 0
AND d.deleted = 0
</select>
```
---
## 8. 性能优化点
### 8.1 数据库层面
1. **索引优化**:确保 `uc_user_dept.user_id``uc_user_post.user_id` 有索引
2. **批量查询**:使用 IN 代替循环查询
3. **按需加载**:列表页只加载必要的字段
### 8.2 缓存层面
1. **单用户缓存**:详情页使用缓存,10分钟TTL
2. **列表页不缓存**:列表数据变化频繁,直接查数据库
3. **缓存预热**:系统启动时可选择预热(可选)
### 8.3 代码层面
1. **并行查询**:单用户查询时,部门、岗位、详情可并行(CompletableFuture
2. **懒加载**:如果前端不需要详情,可以不加载 `uc_user_detail`
---
## 9. 边界情况处理
### 9.1 用户无部门/岗位
- `depts` 返回空列表 `[]`
- `posts` 返回空列表 `[]`
- `mainDeptId``mainDeptName``null`
### 9.2 缓存穿透
- 用户不存在时,缓存空值(TTL 1分钟)
- 使用 `Optional` 包装返回
### 9.3 缓存雪崩
- TTL 加随机偏移:`10分钟 + random(0, 60)秒`
- 使用互斥锁(可选)
### 9.4 数据更新同步
- 所有更新操作后主动失效缓存
- 使用事务确保数据库和缓存一致性
---
## 10. 兼容性
### 10.1 向后兼容
- 现有接口完全保留
- 新增 `/aggregate` 接口,不影响旧接口
- 前端可以逐步迁移
### 10.2 前端迁移路径
1. **第一阶段**:新增聚合接口,前端详情页切换到新接口
2. **第二阶段**:列表页切换到批量查询
3. **第三阶段**:废弃旧接口(可选)
---
## 11. 安全考虑
1. **权限校验**:复用现有 `@AutoPermission("uc:user")`
2. **数据隔离**:所有查询自动加上 `tenant_id` 条件
3. **敏感信息**:密码字段不返回
---
## 12. 监控与日志
1. **缓存命中率**:监控 `user:agg:*` 的命中情况
2. **查询耗时**:记录批量查询的执行时间
3. **慢查询**:超过100ms的查询记录日志
---
## 13. 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| 缓存数据不一致 | 中 | 中 | 更新时主动失效缓存 |
| 批量查询性能差 | 低 | 高 | 索引优化 + 分页 |
| 内存占用过高 | 低 | 中 | 控制缓存TTL + 分页大小 |
---
## 14. 附录
### 14.1 涉及文件清单
**新增文件:**
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserAggregateVO.java`
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserDeptVO.java`
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserPostVO.java`
- `rui-service-user/src/main/java/com/rui/service/user/dto/LoginAccountDTO.java`
- `rui-service-user/src/main/java/com/rui/service/user/enums/AccountType.java`
**修改文件:**
- `rui-service-user/src/main/java/com/rui/service/user/entity/User.java`(添加 phone 字段)
- `rui-service-user/src/main/java/com/rui/service/user/entity/UserDetail.java`(移除 phone 字段)
- `rui-service-user/src/main/java/com/rui/service/user/service/IUserService.java`
- `rui-service-user/src/main/java/com/rui/service/user/service/impl/UserServiceImpl.java`
- `rui-service-user/src/main/java/com/rui/service/user/controller/UserController.java`
- `rui-service-user/src/main/java/com/rui/service/user/controller/inner/UserInnerController.java`
- `rui-service-user/src/main/java/com/rui/service/user/mapper/UserDeptMapper.java`
- `rui-service-user/src/main/java/com/rui/service/user/mapper/UserPostMapper.java`
- `rui-service-user/src/main/resources/mapper/UserDeptMapper.xml`
- `rui-service-user/src/main/resources/mapper/UserPostMapper.xml`
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/feign/UserAuthFeign.java`
**SQL 脚本:**
- `sql/upgrade-v2.x-add-phone-to-user.sql`(新增)
### 14.2 数据库变更
#### 14.2.1 uc_user 表添加 phone 字段
```sql
-- 添加 phone 字段
ALTER TABLE rui_uc_user
ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER username;
-- 添加唯一索引(按租户)
ALTER TABLE rui_uc_user
ADD UNIQUE KEY uk_phone (tenant_id, phone);
-- 添加普通索引(用于查询)
ALTER TABLE rui_uc_user
ADD INDEX idx_phone (phone);
```
#### 14.2.2 uc_user_detail 表移除 phone 字段
```sql
-- 迁移数据(如果有)
-- UPDATE rui_uc_user u
-- JOIN rui_uc_user_detail d ON u.id = d.user_id
-- SET u.phone = d.phone
-- WHERE u.phone IS NULL AND d.phone IS NOT NULL;
-- 移除 phone 字段
ALTER TABLE rui_uc_user_detail
DROP COLUMN phone;
```
#### 14.2.3 索引检查
```sql
-- uc_user_dept 表
CREATE INDEX idx_user_dept_user_id ON uc_user_dept(user_id);
CREATE INDEX idx_user_dept_dept_id ON uc_user_dept(dept_id);
-- uc_user_post 表
CREATE INDEX idx_user_post_user_id ON uc_user_post(user_id);
CREATE INDEX idx_user_post_post_id ON uc_user_post(post_id);
```
---
## 15. 决策记录
| 决策 | 选择 | 理由 |
|------|------|------|
| 是否改表结构 | 是 | 手机号是登录凭证,应提升到 uc_user 表 |
| 手机号位置 | uc_user 表 | 便于认证查询,支持唯一约束 |
| 认证接口方式 | POST + JSON + 枚举 | 支持多种登录方式,便于扩展 |
| 列表页是否缓存 | 否 | 数据变化频繁,直接查库更可靠 |
| 单用户查询方式 | 并行查询 | 减少RT,提升用户体验 |
| 缓存失效策略 | 主动失效 | 数据更新时立即失效,保证一致性 |
| 是否返回密码 | 否 | 安全考虑,聚合数据不包含敏感字段 |
@@ -0,0 +1,579 @@
# 文件存储服务(rui-service-storage)设计文档
> **日期**: 2026-06-07
> **状态**: 设计中
> **作者**: AI Assistant
> **关联**: Gitea #4 [API-REQ] 通用文件上传接口
---
## 1. 背景与目标
### 1.1 现状
- 全局无文件上传相关代码(`grep -ri "upload|oss|cos"` 业务代码无命中)
- `rui-common-web``BaseController` 已 import `MultipartFile`,框架就绪
- `rui-common-mq` + `rui-common-mq-redis` 已有完整发布订阅抽象
- 各业务模块开始需要上传能力:
- **Gitea #4** 紧急:SysApp 第三方应用集成需上传证书(`.pem/.crt/.key/.p12`
- 后续:用户头像、订单附件、CMS 轮播图等
### 1.2 目标
1. 独立微服务 `rui-service-storage` 提供**统一上传接口**`POST /storage/upload`
2. 支持三家存储后端:**阿里云 OSS / 腾讯云 COS / 本地**Strategy 模式可扩展)
3. **统一鉴权**:服务内置 `@AutoPermission`(网关暂不背鉴权)
4. **统一返回**:所有响应走 `Result<T>` 包装
5. **Redis pub/sub 广播**:上传完成后推送 `ON_UPLOAD` 事件,订阅方按 `type` 字段过滤处理
6. **集中常量**:跨服务 topic 字符串统一在 `rui-common-core/.../constants/MqTopicConstants.java` 维护
7. **集成聚合启动器** `rui-service-starter`
---
## 2. 核心设计原则
1. **统一接口**:所有业务模块共用 `POST /storage/upload`,差异由 `bizType` 区分
2. **解耦推送**:上传完成 → 落 `sys_file` → 推 MQ 事件 → 订阅方各自处理,存储服务不感知业务
3. **可插拔后端**:Strategy 模式,新增存储后端只加一个 `@Component` 即可
4. **配置驱动**`bizType` 的扩展名白名单、文件大小限制、默认存储后端全部 yaml 配置
5. **常量集中**topic/channel 等跨服务字符串统一在 `rui-common-core` 常量目录维护
6. **事件可重放**:所有上传记录落库 `sys_file`,订阅方失败可基于 DB 重放
7. **不破坏向后兼容**:旧服务无需改造即可调用新上传接口
---
## 3. 架构设计
### 3.1 整体架构
```
┌──────────┐ POST /storage/upload ┌──────────────────────────────┐
│ 客户端 │ ───────────────────────────────▶ │ rui-gateway 路由透传 │
└──────────┘ └────────────┬─────────────────┘
│ JWT 已校验 / 注入 header
┌──────────────────────────────┐
│ rui-service-storage │
│ @EnableResourceServer │
│ @AutoPermission("sys:file:*")│
├──────────────────────────────┤
│ 1. SecurityUtils 取用户/租户 │
│ 2. 校验 bizType 枚举 + 配置 │
│ 3. 校验大小/扩展名 │
│ 4. FileStorage Strategy 上传 │
│ ├ AliyunOssFileStorage │
│ ├ TencentCosFileStorage │
│ └ LocalFileStorage │
│ 5. sys_file 落库 │
│ 6. mqClient.publish( │
│ REDIS, ON_UPLOAD, payload)│
│ 7. return Result.ok(vo) │
└────────────┬─────────────────┘
│ Redis pub/sub
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
rui-service-system rui-service-user rui-service-cms
@MqTopic(ON_UPLOAD) @MqTopic(ON_UPLOAD) @MqTopic(ON_UPLOAD)
filter type=SYS_APP_CERT_UPLOAD filter type=USER_AVATAR_UPLOAD filter type=CMS_BANNER_UPLOAD
→ SysApp.appendCertificate() → user.setAvatar() → banner.setImage()
```
### 3.2 模块定位
| 模块 | 角色 | 依赖 |
|------|------|------|
| `rui-common-core` | 提供 `MqTopicConstants` + `FileBizType` 工具类 + `Result` | 无 |
| `rui-service-storage` | 上传服务本体(Controller + Strategy + Service | web/mybatis/redis/mq/security |
| `rui-service-system` 等 | 订阅方,实现 `MqConsumer` 处理事件 | mq-redis(已通过 starter 引入) |
| `rui-service-starter` | 聚合启动器,引入 storage 依赖 | 现有 + storage |
---
## 4. 数据库设计
### 4.1 新增表 `sys_file`
```sql
CREATE TABLE sys_file (
id BIGINT NOT NULL,
name VARCHAR(200) NOT NULL COMMENT '存储文件名 (uuid + 扩展名)',
original_name VARCHAR(200) NOT NULL COMMENT '原始文件名',
url VARCHAR(1000) NOT NULL COMMENT '可访问URL',
storage_type VARCHAR(20) NOT NULL COMMENT '存储后端 ALIYUN/TENCENT/LOCAL',
biz_type VARCHAR(50) NOT NULL COMMENT '业务类型 (大写蛇形字符串,业务模块自定)',
biz_id VARCHAR(100) DEFAULT NULL COMMENT '业务关联ID (可选)',
size BIGINT NOT NULL COMMENT '字节',
content_type VARCHAR(100) DEFAULT NULL COMMENT 'MIME 类型',
sha256 CHAR(64) DEFAULT NULL COMMENT '文件哈希 (查重用)',
uploader_id BIGINT DEFAULT NULL COMMENT '上传者用户ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
created_by BIGINT DEFAULT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_by BIGINT DEFAULT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
PRIMARY KEY (id),
INDEX idx_biz (biz_type, biz_id),
INDEX idx_uploader (uploader_id),
INDEX idx_sha256 (sha256),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件存储记录';
```
继承 `BaseEntity`(自动填充 `created_by/created_at/updated_by/updated_at/tenant_id/deleted`),最终 DDL 可省略由框架维护的列定义。
### 4.2 工具类 `FileBizType`(位于 `rui-common-core`**不是枚举**
`bizType` **不维护中央清单**,新业务模块加新字符串即可,框架不强制注册。
理由:上传服务是「统一基础设施」,应当对业务透明。强制枚举会让「加个新模块」变成「改框架代码 + 重新发版」,违背开闭原则。
```java
package com.rui.common.core.enums;
/**
* 文件业务类型工具类(已不再是枚举)。
* <p>上传接口接收任意 bizType 字符串,框架只做格式校验,不维护"已注册"清单。</p>
*/
public final class FileBizType {
private static final Pattern PATTERN = Pattern.compile("^[A-Z][A-Z0-9_]*$");
private static final int MAX_LENGTH = 50;
private FileBizType() {}
/** trim / 大写 / - 转 _ / 格式校验;非法时抛 BizException */
public static String normalize(String bizType) { /* ... */ }
/** 订阅方按此过滤:{@code "SYS_APP_CERT_UPLOAD"} 等 */
public static String uploadType(String bizType) { return normalize(bizType) + "_UPLOAD"; }
public static String deletedType(String bizType) { return normalize(bizType) + "_DELETED"; }
}
```
**bizType 格式约束**normalize 强制):
- 字母/数字开头,仅大写字母 + 数字 + 下划线
- 长度 ≤ 50(与 `sys_file.biz_type VARCHAR(50)` 对齐)
- 例:`SYS_APP_CERT` / `USER_AVATAR` / `MY_NEW_BIZ` 都可
**具体业务的大小 / 扩展名限制**走 yml 配置 `rui.file.biz-types.{BIZ_TYPE}`,缺失则走默认值;不属于「注册」。
> ⚠️ 这是 2026-06-07 第二次设计调整:原计划是 enum + 预定义 4 个值,后改为工具类 + 任意字符串。
---
## 5. 关键流程设计
### 5.1 上传流程
```
1. Client → POST /storage/upload (file, bizType, storage?)
2. SysFileController.upload() 入口
3. @AutoPermission 校验 sys:file:upload
4. SecurityUtils 取 uploaderId/tenantId
5. 校验 bizType 格式 (normalize) 否则 400;不再校验「是否在已注册清单」
6. 加载 rui.file.biz-types[bizType] 配置
├─ 校验 file.size ≤ maxSize
└─ 校验 file.ext ∈ allowedExtensions
7. 选定后端
├─ 显式 storage=xxx 优先
└─ 否则 rui.file.active
8. FileStorage.upload(file) → 返回 url / storageKey
9. 算 sha256(同步,10MB 以内可接受)
10. sys_file 落库 (INSERT)
11. mqClient.publish(MqProvider.REDIS, MqTopicConstants.ON_UPLOAD,
EventPayload.of(bizType, file, uploader, tenant) // bizType 即上传时传的字符串
12. return Result.ok(SysFileUploadVO)
```
### 5.2 删除流程
```
1. Client → DELETE /storage/file/{id}
2. 权限校验 sys:file:delete
3. 查 sys_file 找到 url + storageKey
4. FileStorage.delete(storageKey)
5. sys_file 软删 (UPDATE deleted=1)
6. mqClient.publish(REDIS, MqTopicConstants.ON_FILE_DELETED,
{type: bizType.deletedType(), fileId, url, ...})
7. return Result.ok()
```
### 5.3 事件推送流程
```
┌────────────────────────┐ ┌────────────────────────┐
│ storage 服务 │ │ 订阅服务 (e.g. system) │
│ │ │ │
│ mqClient.publish( │ Redis Pub │ @MqTopic(ON_UPLOAD) │
│ MqProvider.REDIS, │ ─────────────▶ │ public class SysApp │
│ ON_UPLOAD, │ channel= │ CertConsumer │
│ {type, bizType, │ ON_UPLOAD │ implements MqConsumer│
│ fileId, url, ...}) │ │ │
│ │ │ onMessage(id,topic,data)│
└────────────────────────┘ │ if (!SYS_APP_CERT │
│ .uploadType() │
│ .equals(data │
│ .getString │
│ ("type"))) │
│ return; │
│ sysAppService │
│ .appendCert(...) │
└────────────────────────┘
```
---
## 6. 代码结构
### 6.1 新增文件清单
| 路径 | 说明 |
|------|------|
| `rui-common/rui-common-core/.../constants/MqTopicConstants.java` | 跨服务 MQ topic 常量 |
| `rui-common/rui-common-core/.../enums/FileBizType.java` | 文件业务类型工具类(非枚举;normalize / uploadType / deletedType |
| `rui-service/rui-service-storage/pom.xml` | 新模块 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/StorageApplication.java` | 启动类 |
| `rui-service/rui-service-storage/.../controller/SysFileController.java` | 上传/查询/删除接口 |
| `rui-service/rui-service-storage/.../service/IFileStorage.java` | Strategy 接口 |
| `rui-service/rui-service-storage/.../service/impl/AliyunOssFileStorage.java` | 阿里云 |
| `rui-service/rui-service-storage/.../service/impl/TencentCosFileStorage.java` | 腾讯 |
| `rui-service/rui-service-storage/.../service/impl/LocalFileStorage.java` | 本地 |
| `rui-service/rui-service-storage/.../service/impl/FileStorageRouter.java` | 选实现 |
| `rui-service/rui-service-storage/.../event/UploadEventPublisher.java` | 封装 ON_UPLOAD 推送 |
| `rui-service/rui-service-storage/.../event/FileDeletedEventPublisher.java` | 封装 ON_FILE_DELETED |
| `rui-service/rui-service-storage/.../properties/FileProperties.java` | `@ConfigurationProperties("rui.file")` |
| `rui-service/rui-service-storage/.../entity/SysFile.java` | 实体(继承 BaseEntity |
| `rui-service/rui-service-storage/.../mapper/SysFileMapper.java` | Mapper |
| `rui-service/rui-service-storage/.../service/ISysFileService.java` | Service 接口 |
| `rui-service/rui-service-storage/.../service/impl/SysFileServiceImpl.java` | Service 实现 |
| `rui-service/rui-service-storage/.../dto/SysFileUploadVO.java` | 上传返回 VO |
| `rui-service/rui-service-storage/.../dto/SysFileQueryVO.java` | 查询返回 VO |
| `rui-service/rui-service-storage/.../dto/UploadEventPayload.java` | 事件 payload POJO |
| `rui-service/rui-service-storage/src/main/resources/application.yml` | port=9400 |
| `sql/init-database.sql` | 新增 sys_file 表 DDL |
### 6.2 修改文件清单
| 路径 | 修改内容 |
|------|----------|
| `rui-service/pom.xml` | `<modules>``rui-service-storage` |
| `rui-service/rui-service-starter/pom.xml` | 加 `rui-service-storage` 依赖 |
| `rui-service/rui-service-starter/.../StarterApplication.java` | `@ComponentScan``com.rui.service.storage` |
| `rui-service/rui-service-starter/src/main/resources/application.yml` | `rui.modules.available` 加 storage 入口 |
---
## 7. API 接口设计
### 7.1 上传文件
```http
POST /storage/upload
Content-Type: multipart/form-data
Authorization: Bearer <JWT> # storage
file : MultipartFile ()
bizType : string (form) ()
storage : string (form) (aliyun/tencent/local active)
fileName : string (form) ( [A-Za-z0-9][A-Za-z0-9._-]{<=200} 7.4)
extract : bool (form) ( falsetrue .zip 7.5)
Response (Result<List<SysFileUploadVO>>):
{
"error": 0,
"message": "success",
"data": [
{
"id": 1001,
"name": "a1b2c3d4.pem",
"originalName": "wechat.pem",
"path": "sys-app-cert/2026/06/a1b2c3d4.pem", //
"url": "https://oss.../sys-app-cert/2026/06/a1b2c3d4.pem",
"size": 2048,
"contentType": "application/x-pem-file",
"storageType": "ALIYUN",
"bizType": "SYS_APP_CERT"
}
]
}
```
> **响应统一是数组**:单文件上传长度为 1,zip 解压上传长度为 N。前端按 `data.length` 即可区分。
### 7.4 fileName 参数
| 行为 | 说明 |
|------|------|
| 不传 | 默认 `bizType/yyyy/MM/{uuid}{ext}`,分布式不冲突 |
| 传 | 存储路径为 `bizType/{fileName}`,**会覆盖同名文件**(适用固定路径场景,如 `avatar-{userId}.jpg`|
| 校验 | `^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$`,不合规 400 |
### 7.5 extract=trueZIP 自动解压)
**适用**:批量上传场景(应用多证书、UI 主题包、字体包、翻译文件等)。
```http
POST /storage/upload
Content-Type: multipart/form-data
file : certs.zip (.zip)
bizType : SYS_APP_CERT
fileName : wechat ()
extract : true
```
**行为**
- zip 本身**不**存到后端;解压每个 entry 单独存、单独推 `ON_UPLOAD` 事件
- 存储路径:`bizType/{zipBaseName}/{entryName}`zipBaseName 优先级 `fileName` > 原文件名去 `.zip` > UUID 前 12 位
- 响应:`data` 数组长度 = zip 中文件 entry 数(不含目录)
- 非 .zip 文件传 `extract=true` → 400
**安全护栏**`ZipExtractor` 强制):
| 项 | 限制 | 失败行为 |
|----|------|---------|
| 总 entry 数 | ≤ 100 | 400 防 zip bomb / 百万小文件 |
| 单 entry 大小 | ≤ `bizType` 配置的 `maxSize` | 400 |
| entry 名 | 须匹配 `^[A-Za-z0-9][A-Za-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$` | 400(防 Zip Slip / 绝对路径 / Windows 盘符)|
| entry 名长度 | ≤ 200 | 400 |
**典型场景**
```jsonc
// 请求
file = wechat.zip // 内部: wechat/apiclient_cert.pem, wechat/apiclient_key.pem
bizType = "SYS_APP_CERT"
fileName = "wechat"
extract = true
// 响应
{
"error": 0,
"data": [
{ "id": 1001, "name": "wechat/apiclient_cert.pem", "path": "sys-app-cert/wechat/apiclient_cert.pem", ... },
{ "id": 1002, "name": "wechat/apiclient_key.pem", "path": "sys-app-cert/wechat/apiclient_key.pem", ... }
]
}
// 订阅方收到 2 条 ON_UPLOADtype 都是 "SYS_APP_CERT_UPLOAD"bizType=SYS_APP_CERT
```
### 7.2 查询文件
```http
GET /storage/file/{id}
GET /storage/file/page?bizType=SYS_APP_CERT&pageNum=1&pageSize=20
Response (Result<SysFileQueryVO>):
{ "id":1001, "name":"...", "url":"...", "size":2048,
"bizType":"SYS_APP_CERT", "createdAt":"..." }
```
### 7.3 删除文件
```http
DELETE /storage/file/{id}
Response: Result.ok()
```
### 7.4 权限注解
| 接口 | 注解 |
|------|------|
| `POST /storage/upload` | `@AutoPermission("sys:file:upload")` |
| `GET /storage/file/{id}` | `@AutoPermission("sys:file:query")` |
| `GET /storage/file/page` | `@AutoPermission("sys:file:query")` |
| `DELETE /storage/file/{id}` | `@AutoPermission("sys:file:delete")` |
---
## 8. 事件约定
### 8.1 常量定义(rui-common-core/.../constants/MqTopicConstants.java
```java
public final class MqTopicConstants {
public static final String ON_UPLOAD = "ON_UPLOAD";
public static final String ON_FILE_DELETED = "ON_FILE_DELETED";
private MqTopicConstants() {}
}
```
### 8.2 事件 Payload
**ON_UPLOAD**
```json
{
"id": "uuid",
"bizType": "SYS_APP_CERT",
"type": "SYS_APP_CERT_UPLOAD",
"fileId": 1001,
"name": "a1b2c3d4.pem",
"url": "https://oss.../xxx",
"size": 2048,
"contentType": "application/x-pem-file",
"storageType": "ALIYUN",
"uploaderId": 42,
"tenantId": 0,
"extra": { },
"timestamp": "2026-06-07T13:30:00Z"
}
```
**ON_FILE_DELETED**
```json
{
"id": "uuid",
"bizType": "SYS_APP_CERT",
"type": "SYS_APP_CERT_DELETED",
"fileId": 1001,
"url": "https://oss.../xxx",
"storageType": "ALIYUN",
"uploaderId": 42,
"tenantId": 0,
"timestamp": "2026-06-07T13:30:00Z"
}
```
### 8.3 订阅方模板(`rui-service-system` 示例)
```java
@MqTopic(MqTopicConstants.ON_UPLOAD)
@Component
@RequiredArgsConstructor
public class SysAppCertUploadConsumer implements MqConsumer {
private final ISysAppService sysAppService;
@Override
public void onMessage(String messageId, String topic, JSONObject data) {
if (!FileBizType.uploadType("SYS_APP_CERT").equals(data.getString("type"))) return;
String url = data.getString("url");
JSONObject extra = data.getJSONObject("extra");
String appId = extra == null ? null : extra.getString("appId");
if (appId != null) {
sysAppService.appendCertificate(appId, url);
}
}
}
```
---
## 9. 配置设计
### 9.1 公共配置(`rui-common.yaml` Nacos / 本地兜底)
```yaml
rui:
file:
active: local # 默认后端
default-max-size: 10MB
biz-types:
COMMON:
max-size: 10MB
allowed-extensions: [] # 空 = 全部
SYS_APP_CERT:
max-size: 5MB
allowed-extensions: [pem, crt, key, p12]
USER_AVATAR:
max-size: 2MB
allowed-extensions: [jpg, jpeg, png, webp]
CMS_BANNER:
max-size: 5MB
allowed-extensions: [jpg, jpeg, png, webp, gif]
```
### 9.2 服务专属配置(`rui-service-storage/application.yml`
```yaml
server:
port: 9400
spring:
application:
name: rui-service-storage
servlet:
multipart:
max-file-size: 10MB # 兜底
max-request-size: 50MB # 批量上传场景预留
```
### 9.3 OSS 凭据(Nacos rui-service-storage.yaml
```yaml
rui:
file:
aliyun:
enabled: false
endpoint: oss-cn-shanghai.aliyuncs.com
access-key: ${ALIYUN_AK}
secret-key: ${ALIYUN_SK}
bucket: rui-storage
url-prefix: https://rui-storage.oss-cn-shanghai.aliyuncs.com
base-path: cert/
tencent:
enabled: false
secret-id: ${TENCENT_SID}
secret-key: ${TENCENT_SKEY}
region: ap-shanghai
bucket: rui-storage-1300000000
url-prefix: https://rui-storage-1300000000.cos.ap-shanghai.myqcloud.com
base-path: cert/
local:
base-path: ${user.home}/.rui/upload/
url-prefix: /api/storage/local/ # 通过 storage 服务自己代理返回
```
### 9.4 Nacos 配置规则
`docs/ai-skills/nacos-config-rules.md`
- `rui-common.yaml``rui.file.biz-types` 等共享
- `rui-service-storage.yaml` 管端口 + 三个后端的凭据
- 不重复:业务模块不需要 import storage 专属配置
---
## 10. 鉴权与安全
1. **JWT 校验**`@EnableResourceServer` + `rui-common-security` 自动校验
2. **权限注解**`@AutoPermission("sys:file:upload")` 类级默认;查询/删除按方法级覆盖
3. **大小限制**:双重防护:Spring `multipart.max-file-size` + 业务校验 `maxSize`
4. **扩展名白名单**`bizType` 配置驱动,未匹配返回 400
5. **文件名安全**:存储文件名采用 `uuid + 原扩展名`,避免路径穿越
6. **不存敏感信息**:日志只记录 `fileId``bizType`,不打原文件名或 URL
7. **跨服务调用**:上传接口需要 `sys:file:upload` 权限,订阅方处理失败不影响主链路
---
## 11. 边界与不做
| 边界 | 说明 |
|------|------|
| 不做文件预览/转码 | 单纯的存储 + URL 返回,预览由前端/调用方实现 |
| 不做分片上传 | MVP 先支持单文件 10MB,分片后续按需 |
| 不做断点续传 | 同上 |
| 不做租户独立 bucket | MVP 用共享 bucket + 路径前缀隔离 |
| 不做内容审查/反垃圾 | 业务层后续扩展 |
| 不做软删除恢复 | 物理不可恢复,按 `deleted=1` 软标记 |
| 不做多文件上传 | 单文件接口;批量由前端循环或后续加 `batch` 接口 |
---
## 12. 验收标准
- [ ] `POST /storage/upload` 上传 .pem 文件返回标准 `Result` 格式,`data.url` 可访问
- [ ] `POST /storage/upload` 传 11MB 文件返回 400
- [ ] `POST /storage/upload` 传 .exe + `bizType=SYS_APP_CERT` 返回 400
- [ ] `POST /storage/upload``bizType=INVALID_TYPE` 返回 400
- [ ] 上传成功后 Redis 收到 `ON_UPLOAD` 消息,payload 包含 `type=SYS_APP_CERT_UPLOAD`
- [ ] 删除后 Redis 收到 `ON_FILE_DELETED` 消息
- [ ] 无 JWT 调上传接口返回 401
- [ ]`sys:file:upload` 权限调上传返回 403
- [ ] `rui-service-starter` 启动后 `StorageApplication` 同样可启动
- [ ] Gitea #4 关闭
- [ ] `mvn clean compile` 全部模块通过
- [ ] 关键 commit 推送至 `origin/main`
@@ -0,0 +1,853 @@
# 多方式登录与第三方登录设计文档
> **日期**: 2026-06-07
> **状态**: 已实现(2026-06-07
> **作者**: AI Assistant
> **实施情况**: 数据库变更、实体调整、UserSocial 增删、密码登录扩展、短信/微信/支付宝框架、OAuth2ServerConfig 注册、配置更新、编译验证共 12 任务全部完成,详见 `docs/superpowers/plans/2026-06-07-multi-login-social-login-plan.md` 与 git log `6fd82fb` 起各 commit。
---
## 1. 背景与目标
### 1.1 现状
当前系统仅支持用户名密码登录(`grant_type=password`),且 `PasswordAuthenticationConverter` 只提取 `username` 参数,无法支持手机号、邮箱登录。微信、支付宝、短信登录的 `Converter``Provider` 均为空实现。
### 1.2 目标
1. **扩展密码登录**:支持用户名、手机号、邮箱三种账号类型登录
2. **实现短信登录**:框架结构先行,验证码逻辑后续填充
3. **实现微信登录**:支持微信授权码换取用户信息并自动创建账号
4. **实现支付宝登录**:支持支付宝授权码换取用户信息并自动创建账号
5. **第三方账号管理**:存储 openId/unionId,支持 unionId 优先查询
6. **手机号为主键**:系统以手机号作为用户唯一标识,第三方登录自动创建新用户
7. **字段迁移**:将 `email``uc_user_detail` 迁移到 `uc_user`
---
## 2. 核心设计原则
1. **独立授权模式**:每种登录方式使用独立的 `grant_type`,符合 OAuth2 扩展规范
2. **手机号唯一性**:手机号是系统用户的唯一标识,第三方登录时优先用手机号创建/查找用户
3. **自动创建用户**:第三方登录无手机号时,自动生成 `userNo` 作为用户名,后续用户可自行修改
4. **unionId 优先**:查询第三方用户信息时,优先使用 unionId,其次使用 openId
5. **向后兼容**:保留现有 `password` 模式的 `username` 参数,同时新增 `account` + `accountType` 参数
---
## 3. 架构设计
### 3.1 整体架构
```
前端调用
POST /oauth2/token
┌─────────────────────────────────────┐
│ DelegatingAuthenticationConverter │
│ ┌─────────┐ ┌─────┐ ┌─────────┐ │
│ │ Password│ │ Sms │ │ Wechat │ │
│ │ Converter│ │Converter│ │Converter│ │
│ └─────────┘ └─────┘ └─────────┘ │
│ ┌─────────┐ │
│ │ Alipay │ │
│ │Converter│ │
│ └─────────┘ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ AuthenticationProvider 链 │
│ ┌─────────┐ ┌─────┐ ┌─────────┐ │
│ │ Password│ │ Sms │ │ Wechat │ │
│ │ Provider│ │Provider│ │Provider│ │
│ └─────────┘ └─────┘ └─────────┘ │
│ ┌─────────┐ │
│ │ Alipay │ │
│ │Provider │ │
│ └─────────┘ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 用户查找 / 创建 / 绑定 │
│ • 根据手机号/用户名/邮箱查找用户 │
│ • 第三方登录:调平台API获取用户信息 │
│ • 自动创建新用户(手机号或userNo) │
│ • 记录第三方绑定关系 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 生成 OAuth2 Token │
│ Access Token + Refresh Token │
└─────────────────────────────────────┘
```
### 3.2 登录方式对照表
| 登录方式 | grant_type | 必填参数 | 可选参数 | 说明 |
|---------|-----------|---------|---------|------|
| 用户名密码 | `password` | `username`, `password` | - | 兼容现有方式 |
| 手机号密码 | `password` | `account`, `accountType=PHONE`, `password` | - | 扩展方式 |
| 邮箱密码 | `password` | `account`, `accountType=EMAIL`, `password` | - | 扩展方式 |
| 短信验证码 | `sms` | `phone`, `code` | - | 框架先行 |
| 微信登录 | `wechat` | `code` | `phone` | 授权码模式 |
| 支付宝登录 | `alipay` | `code` | `phone` | 授权码模式 |
---
## 4. 数据库设计
### 4.1 新增表:`rui_uc_user_social`
存储用户与第三方平台的绑定关系。
```sql
CREATE TABLE rui_uc_user_social (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
provider VARCHAR(20) NOT NULL COMMENT '平台 wechat/alipay',
union_id VARCHAR(100) DEFAULT NULL COMMENT 'unionId(微信开放平台)',
open_id VARCHAR(100) NOT NULL COMMENT 'openId',
extra JSON DEFAULT NULL COMMENT '扩展信息(昵称、头像等)',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_user_provider (user_id, provider),
UNIQUE KEY uk_provider_openid (provider, open_id),
INDEX idx_union_id (union_id),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号关联';
```
**字段说明**
- `provider`: 平台标识,`wechat``alipay`
- `union_id`: 微信开放平台统一标识,同一主体下的不同应用 unionId 相同
- `open_id`: 各应用内的唯一标识
- `extra`: JSON 格式,存储第三方平台的额外信息(昵称、头像、性别等)
**索引设计**
- `uk_user_provider`: 一个用户在同一平台只能绑定一个账号
- `uk_provider_openid`: 同一平台的 openId 唯一
- `idx_union_id`: 支持 unionId 查询
### 4.2 修改表:`rui_uc_user`
新增 `email` 字段:
```sql
-- 在 rui_uc_user 表中添加 email 字段
ALTER TABLE rui_uc_user ADD COLUMN email VARCHAR(100) DEFAULT NULL COMMENT '邮箱' AFTER phone;
ALTER TABLE rui_uc_user ADD UNIQUE KEY uk_email (tenant_id, email);
```
修改后的表结构:
```sql
CREATE TABLE rui_uc_user (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID 0:系统级',
username VARCHAR(100) NOT NULL COMMENT '用户名',
phone VARCHAR(20) DEFAULT NULL COMMENT '手机号',
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
user_no VARCHAR(50) DEFAULT NULL COMMENT '用户编号(短编码,前端展示用)',
password VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)',
user_type TINYINT NOT NULL DEFAULT 1 COMMENT '用户类型 1:普通用户 2:管理员 3:系统用户',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0:正常 1:已删',
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_username (tenant_id, username),
UNIQUE KEY uk_phone (tenant_id, phone),
UNIQUE KEY uk_email (tenant_id, email),
INDEX idx_tenant (tenant_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户';
```
### 4.3 修改表:`rui_uc_user_detail`
删除 `email` 字段:
```sql
-- 从 rui_uc_user_detail 表中删除 email 字段
ALTER TABLE rui_uc_user_detail DROP COLUMN email;
```
### 4.4 登录日志扩展
`rui_sys_login_log` 表的 `login_type` 字段已有定义:
- `1`: 密码登录
- `2`: 短信登录
- `3`: 微信登录
- `4`: 支付宝登录
**无需修改**,但需要在代码中确保所有登录方式都正确记录类型。
---
## 5. 核心流程设计
### 5.1 密码登录流程(扩展)
```
前端请求
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic {client_credentials}
# 方式1:用户名密码(兼容现有)
grant_type=password
&username=admin
&password=123456
# 方式2:手机号密码(新增)
grant_type=password
&account=13800138000
&accountType=PHONE
&password=123456
# 方式3:邮箱密码(新增)
grant_type=password
&account=user@example.com
&accountType=EMAIL
&password=123456
PasswordAuthenticationConverter
├─ 提取 grant_type=password
├─ 如果有 username → 走兼容模式
└─ 如果有 account + accountType → 走扩展模式
PasswordAuthenticationProvider
├─ 校验客户端支持 password 授权
├─ 构建 UsernamePasswordAuthenticationToken
│ ├─ 兼容模式: username 作为 principal
│ └─ 扩展模式: account 作为 principal
AuthenticationManager
DaoAuthenticationProvider
├─ 调用 RemoteUserDetailsService.loadUserByUsername(username)
│ 或 RemoteUserDetailsService.loadUserByAccount(account, accountType)
RemoteUserDetailsService
├─ USERNAME → userAuthFeign.loadUser(account)
├─ PHONE → userAuthFeign.loadUser({account, PHONE})
└─ EMAIL → userAuthFeign.loadUser({account, EMAIL})
UserInnerController.loadUser(LoginAccountDTO)
├─ 根据 accountType 查询用户
├─ PHONE → lambdaQuery().eq(User::getPhone, account)
├─ EMAIL → lambdaQuery().eq(User::getEmail, account)
└─ USERNAME → lambdaQuery().eq(User::getUsername, account)
返回 UserDetails → 生成 Token
```
### 5.2 短信登录流程
```
前端请求
POST /oauth2/token
grant_type=sms
&phone=13800138000
&code=123456
SmsAuthenticationConverter
├─ 校验 grant_type=sms
├─ 校验 phone 必填
└─ 校验 code 必填
SmsAuthenticationProvider
├─ 校验客户端支持 sms 授权
├─ 从 Redis 获取验证码(key: sms:code:{phone}
├─ 比对验证码
├─ 验证码错误 → 抛出异常
└─ 验证码正确 → 继续
根据 phone 查询用户
├─ 找到 → 生成 Token
└─ 未找到 → 创建新用户
├─ username = phone
├─ phone = phone
├─ password = 随机生成(BCrypt加密)
└─ user_no = 自动生成
生成 OAuth2 Token
```
**注意**:短信验证码发送接口(`POST /sms/send`)本次不实现,只预留框架结构。Redis 中的验证码需要前端开发时手动设置或通过其他方式注入。
### 5.3 微信登录流程
```
前端请求
POST /oauth2/token
grant_type=wechat
&code=wx_auth_code
&phone=13800138000 ← 可选
WechatAuthenticationConverter
├─ 校验 grant_type=wechat
├─ 校验 code 必填
└─ 提取 phone(可选)
WechatAuthenticationProvider
├─ 校验客户端支持 wechat 授权
├─ 调用微信 API 换取 access_token
│ GET https://api.weixin.qq.com/sns/oauth2/access_token
│ ?appid={appid}&secret={secret}&code={code}&grant_type=authorization_code
├─ 获取 openId, unionId, access_token
├─ 根据 unionId 查询 rui_uc_user_social
│ ├─ 找到 → 获取 user_id → 查询用户 → 生成 Token
│ └─ 未找到 → 根据 openId 查询
│ ├─ 找到 → 获取 user_id → 查询用户 → 生成 Token
│ └─ 未找到 → 创建新用户
创建新用户流程
├─ 有 phone 参数
│ ├─ 查询 phone 是否已存在
│ ├─ 存在 → 使用该用户,记录绑定关系
│ └─ 不存在 → 创建新用户
│ ├─ username = phone
│ ├─ phone = phone
│ └─ password = 随机生成
└─ 无 phone 参数
├─ username = 随机生成(如 WX_ + 时间戳)
├─ phone = null
└─ password = 随机生成
记录绑定关系
INSERT INTO rui_uc_user_social
(user_id, provider, union_id, open_id, extra)
VALUES (?, 'wechat', ?, ?, ?)
生成 OAuth2 Token
```
### 5.4 支付宝登录流程
与微信登录类似,区别:
1. 调用支付宝 API`alipay.system.oauth.token` 换取 access_token
2. 调用 `alipay.user.info.share` 获取用户信息
3. 支付宝没有 unionId,使用 userId 作为唯一标识
4. 存储到 `rui_uc_user_social` 时,`union_id` 为 null
---
## 6. 代码结构
### 6.1 新增/修改文件清单
#### rui-common-oauth2 模块
```
rui-common-oauth2/src/main/java/com/rui/common/oauth2/
├── authentication/
│ ├── BaseAuthenticationConverter.java # 已有,无需修改
│ ├── BaseAuthenticationProvider.java # 已有,无需修改
│ ├── password/
│ │ ├── PasswordAuthenticationConverter.java # 修改:支持 accountType
│ │ └── PasswordAuthenticationProvider.java # 已有,无需修改
│ ├── sms/
│ │ ├── SmsAuthenticationConverter.java # 重写:实现短信参数提取
│ │ ├── SmsAuthenticationProvider.java # 重写:实现短信认证逻辑
│ │ └── SmsAuthenticationToken.java # 新增:短信认证令牌
│ ├── weixin/
│ │ ├── WeixinAuthenticationConverter.java # 重写:实现微信参数提取
│ │ ├── WeixinAuthenticationProvider.java # 重写:实现微信认证逻辑
│ │ └── WeixinAuthenticationToken.java # 新增:微信认证令牌
│ └── alipay/
│ ├── AlipayAuthenticationConverter.java # 重写:实现支付宝参数提取
│ ├── AlipayAuthenticationProvider.java # 重写:实现支付宝认证逻辑
│ └── AlipayAuthenticationToken.java # 新增:支付宝认证令牌
├── config/
│ └── OAuth2ServerConfig.java # 修改:注册新的 Converter 和 Provider
└── service/
└── RemoteUserDetailsService.java # 修改:支持 EMAIL 类型
```
#### rui-service-user 模块
```
rui-service-user/src/main/java/com/rui/service/user/
├── entity/
│ ├── User.java # 修改:新增 email 字段
│ ├── UserDetail.java # 修改:删除 email 字段
│ └── UserSocial.java # 新增:第三方账号关联实体
├── mapper/
│ └── UserSocialMapper.java # 新增
├── service/
│ ├── IUserSocialService.java # 新增
│ └── impl/
│ └── UserSocialServiceImpl.java # 新增
├── controller/
│ └── inner/
│ └── UserInnerController.java # 修改:支持 EMAIL 查询
└── dto/
└── LoginAccountDTO.java # 已有,无需修改
```
### 6.2 关键类设计
#### 6.2.1 PasswordAuthenticationConverter(修改)
```java
public class PasswordAuthenticationConverter extends BaseAuthenticationConverter<PasswordAuthenticationToken> {
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 兼容模式:使用 username
String username = parameters.getFirst("username");
if (StringUtils.hasText(username)) {
// 校验 password
String password = parameters.getFirst("password");
if (!StringUtils.hasText(password)) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "password", ...);
}
return;
}
// 扩展模式:使用 account + accountType
String account = parameters.getFirst("account");
if (!StringUtils.hasText(account)) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "account", ...);
}
String accountType = parameters.getFirst("accountType");
if (!StringUtils.hasText(accountType)) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "accountType", ...);
}
// 校验 password
String password = parameters.getFirst("password");
if (!StringUtils.hasText(password)) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "password", ...);
}
}
@Override
public PasswordAuthenticationToken buildToken(...) {
// 将 accountType 放入 additionalParameters
// 供 Provider 使用
return new PasswordAuthenticationToken(...);
}
}
```
#### 6.2.2 WechatAuthenticationProvider(重写)
```java
@Slf4j
public class WechatAuthenticationProvider extends BaseAuthenticationProvider<WechatAuthenticationToken> {
private final WechatApiClient wechatApiClient;
private final UserSocialService userSocialService;
private final UserService userService;
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String code = (String) reqParameters.get("code");
String phone = (String) reqParameters.get("phone");
// 调用微信 API 获取 openId, unionId
WechatTokenResponse wxResponse = wechatApiClient.getAccessToken(code);
String openId = wxResponse.getOpenid();
String unionId = wxResponse.getUnionid();
// 查找或创建用户
User user = findOrCreateUser(openId, unionId, phone);
// 构建认证令牌
return new UsernamePasswordAuthenticationToken(user.getUsername(), null);
}
private User findOrCreateUser(String openId, String unionId, String phone) {
// 1. 根据 unionId 查找
if (StringUtils.hasText(unionId)) {
UserSocial social = userSocialService.findByUnionId(unionId);
if (social != null) {
return userService.getById(social.getUserId());
}
}
// 2. 根据 openId 查找
UserSocial social = userSocialService.findByOpenId("wechat", openId);
if (social != null) {
return userService.getById(social.getUserId());
}
// 3. 创建新用户
User user = new User();
if (StringUtils.hasText(phone)) {
// 检查手机号是否已存在
User existUser = userService.findByPhone(phone);
if (existUser != null) {
user = existUser;
} else {
user.setUsername(phone);
user.setPhone(phone);
user.setPassword(generateRandomPassword());
userService.save(user);
}
} else {
// 无手机号,生成随机用户名
user.setUsername(generateRandomUsername());
user.setPassword(generateRandomPassword());
userService.save(user);
}
// 4. 记录绑定关系
UserSocial newSocial = new UserSocial();
newSocial.setUserId(user.getId());
newSocial.setProvider("wechat");
newSocial.setUnionId(unionId);
newSocial.setOpenId(openId);
userSocialService.save(newSocial);
return user;
}
}
```
#### 6.2.3 UserSocial 实体
```java
@Data
@TableName(value = "uc_user_social", keepGlobalPrefix = true)
public class UserSocial extends BaseEntity {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "平台 wechat/alipay")
private String provider;
@Schema(description = "unionId")
private String unionId;
@Schema(description = "openId")
private String openId;
@Schema(description = "扩展信息")
private String extra;
}
```
---
## 7. API 接口设计
### 7.1 密码登录
```http
###
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
grant_type=password
&username=admin
&password=123456
&scope=server
###
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
grant_type=password
&account=13800138000
&accountType=PHONE
&password=123456
&scope=server
###
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
grant_type=password
&account=user@example.com
&accountType=EMAIL
&password=123456
&scope=server
```
### 7.2 短信登录
```http
###
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
grant_type=sms
&phone=13800138000
&code=123456
&scope=server
```
### 7.3 微信登录
```http
###
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
grant_type=wechat
&code=wx_auth_code_xxx
&phone=13800138000
&scope=server
```
### 7.4 支付宝登录
```http
###
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
grant_type=alipay
&code=alipay_auth_code_xxx
&phone=13800138000
&scope=server
```
### 7.5 响应格式
所有登录方式返回统一的 OAuth2 Token 响应:
```json
{
"access_token": "abc123...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "def456...",
"scope": "server"
}
```
---
## 8. 配置设计
### 8.1 微信配置
```yaml
# Nacos 配置:rui-service-auth.yaml 或 rui-common.yaml
social:
wechat:
app-id: wx1234567890abcdef
app-secret: your-app-secret
# 可选:token 刷新地址
token-url: https://api.weixin.qq.com/sns/oauth2/access_token
# 可选:用户信息地址
user-info-url: https://api.weixin.qq.com/sns/userinfo
```
### 8.2 支付宝配置
```yaml
social:
alipay:
app-id: 2024XXXXXXXXXXXX
private-key: your-private-key
public-key: alipay-public-key
# 可选:网关地址
gateway-url: https://openapi.alipay.com/gateway.do
```
### 8.3 客户端授权类型配置
修改 `sys_oauth_client` 表,为客户端添加新的授权类型:
```sql
-- 更新默认客户端,支持所有登录方式
UPDATE sys_oauth_client
SET grant_types = 'password,refresh_token,client_credentials,sms,wechat,alipay'
WHERE client_id = 'rui-client';
```
---
## 9. 安全设计
### 9.1 验证码安全
- 短信验证码有效期:5 分钟
- 验证码错误次数限制:5 次/小时
- 验证码存储:Rediskey = `sms:code:{phone}`
### 9.2 第三方登录安全
- 微信/支付宝授权码只能使用一次
- 授权码有效期:5 分钟(由微信/支付宝平台控制)
- 后端必须校验授权码的真实性(调平台 API)
### 9.3 密码安全
- 第三方登录自动创建的用户,生成随机密码(32 位随机字符串)
- 用户首次设置密码时,要求提供原密码或通过手机验证码验证
---
## 10. 错误码设计
| 错误码 | 描述 | 场景 |
|-------|------|------|
| `invalid_request` | 请求参数错误 | 缺少必填参数、参数格式错误 |
| `invalid_grant` | 授权失败 | 验证码错误、授权码无效 |
| `invalid_client` | 客户端认证失败 | 客户端不存在、授权类型不支持 |
| `unauthorized_client` | 客户端未授权 | 客户端不支持该授权类型 |
| `server_error` | 服务器内部错误 | 调用第三方 API 失败 |
---
## 11. 测试策略
### 11.1 单元测试
- `PasswordAuthenticationConverterTest`: 测试参数提取和校验
- `SmsAuthenticationProviderTest`: 测试验证码校验逻辑
- `WechatAuthenticationProviderTest`: Mock 微信 API,测试用户创建流程
### 11.2 集成测试
- 使用 H2 内存数据库测试完整登录流程
- 使用 WireMock 模拟微信/支付宝 API
### 11.3 手动测试清单
- [ ] 用户名密码登录(兼容测试)
- [ ] 手机号密码登录
- [ ] 邮箱密码登录
- [ ] 短信验证码登录(使用 Redis 手动设置验证码)
- [ ] 微信登录(使用测试授权码)
- [ ] 支付宝登录(使用测试授权码)
- [ ] 第三方登录后绑定手机号
- [ ] 同一微信不同手机号创建不同用户
- [ ] unionId 优先查询验证
---
## 12. 风险与回滚
### 12.1 风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 微信/支付宝 API 变更 | 登录失败 | 封装 API 调用,便于快速适配 |
| 手机号重复 | 数据不一致 | 数据库唯一索引 + 代码校验 |
| 性能问题 | 登录慢 | Redis 缓存 + 异步记录日志 |
### 12.2 回滚方案
- 数据库变更:保留原字段,新增字段不影响现有数据
- 代码回滚:新授权模式独立实现,不影响现有 `password` 模式
- 配置回滚:移除新 grant_type 即可禁用
---
## 13. 后续优化
1. **短信服务商接入**:实现真实的短信发送功能
2. **社交账号解绑**:提供 API 解除第三方绑定
3. **多账号合并**:支持将多个第三方账号合并到同一用户
4. **登录设备管理**:记录登录设备,支持远程登出
5. **扫码登录**:支持微信扫码登录 PC 端
---
## 14. 附录
### 14.1 登录类型枚举
```java
public enum LoginType {
PASSWORD(1, "密码登录"),
SMS(2, "短信登录"),
WECHAT(3, "微信登录"),
ALIPAY(4, "支付宝登录");
private final int code;
private final String description;
LoginType(int code, String description) {
this.code = code;
this.description = description;
}
}
```
### 14.2 账号类型枚举
```java
public enum AccountType {
USERNAME("用户名"),
PHONE("手机号"),
EMAIL("邮箱");
private final String description;
AccountType(String description) {
this.description = description;
}
}
```
### 14.3 第三方平台枚举
```java
public enum SocialProvider {
WECHAT("微信"),
ALIPAY("支付宝");
private final String description;
SocialProvider(String description) {
this.description = description;
}
}
```
---
**文档结束**
@@ -0,0 +1,254 @@
# 第三方应用管理(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
@@ -0,0 +1,469 @@
# SysApp(第三方应用集成)管理界面设计规范
**工单**: rui/rui-frontend#4 — [UI-REQ] 后台增加 SysApp(第三方应用集成)管理界面
**日期**: 2026-06-07
**关联 Issue**: rui/rui-framework#4(文件上传接口待提供,前端先用 JSON 占位)
**优先级**: P1
---
## 1. 目标(Goal
在 admin-ui 后台实现 **SysApp(第三方应用集成)管理模块**,为运营/管理员提供对第三方平台应用(微信、支付宝、Stripe 等)凭证信息的统一管理能力。模块包含列表展示、新增、编辑、删除、启停 5 个标准操作,遵循现有 `oauth2-client` 等模块的 CRUD 模式(`BaseService` + `RuiTable` + `FormDialog`),不引入新依赖。
## 2. 非目标(Non-Goals
明确不在本期范围内的事项:
- **不修改后端**:本 Spec 仅涉及 admin-ui 前端;不修改 rui-framework / rui-cashier 等后端仓库代码或 API。
- **不实现 certificates 文件上传 UI**:因 rui-framework 尚未提供文件上传接口(已提 Issue #4),certificates 字段本期用 JSON textarea 占位实现,标注「待后端文件上传接口就绪后升级」。
- **不实现 isEncrypted 字段 UI**:后端为该字段预留,前端暂不展示。
- **不修改 cashier-mobile / cashier-customer**:本 Spec 仅涉及 admin-ui。
- **不引入 monaco-editor / @guolao/vue-monaco-editor 等新依赖**:证书编辑用原生 textarea + JSON.parse 校验。
- **不修改 rui-framework 菜单 JSON 文件**:工单提到的 `data/menus/system.json` 属于 rui-framework 仓库菜单管理数据,不在本仓库范围内。
- **不做多租户隔离增强**:tenantId 字段由后端按上下文自动填充,前端不主动设置。
## 3. 背景与上下文(Context
### 3.1 后端接口现状
- **Controller**: `SysAppController`,继承 `BaseController`,自动具备 5 个标准操作:
- `GET /system/admin/app/page` — 分页查询
- `GET /system/admin/app/list` — 列表查询
- `GET /system/admin/app/{id}` — 详情
- `POST /system/admin/app` — 新增
- `PUT /system/admin/app` — 修改
- `DELETE /system/admin/app/{id}` — 删除
- `DELETE /system/admin/app/batch` — 批量删除
- `PUT /system/admin/app/status` — 启停
- `GET /system/admin/app/export` — 导出
- `POST /system/admin/app/import` — 导入
- **鉴权**: `@AutoPermission("sys:app")`
- **Swagger**: `http://localhost:9302/swagger-ui.html`
- **后端 commit**: 27fa187(表)+ 29a9389Service/Controller+ 13b20abResult 规范)
### 3.2 前端代码基础
- **BaseService** (`admin-ui/src/service/BaseService.ts`) 已提供完整 CRUD 抽象,子类只需传 baseUrl
```ts
class SysAppService extends BaseService {
constructor() { super('/system/admin/app') }
}
export const sysAppService = new SysAppService()
```
- **RuiTable** 组件支持查询区、工具栏、列配置、slot、分页、导出、列设置、刷新、批量操作等开箱即用能力。
- **OAuth2Client 页面**`views/system/oauth2-client/Index.vue` + `OAuth2ClientFormDialog.vue`)是最相似的现有实现,本期直接照搬其模式。
### 3.3 路由与菜单
- admin-ui 路由采用 **前端硬编码 + i18n 键** 方式(`admin-ui/src/router/modules/system.ts`)。
- 工单中提到的 `data/menus/system.json` 是 rui-framework 后端「菜单管理」功能加载的菜单数据,不影响 admin-ui 路由配置。
## 4. 关键设计决策(用户已确认)
| # | 决策项 | 选定方案 |
|---|--------|---------|
| 1 | 菜单归属 | 作为「系统管理」的子菜单,路由 `/system/app` |
| 2 | 表单布局 | el-tabs 分 4 Tab(基础信息 / 凭证信息 / 接口配置 / 高级) |
| 3 | certificates 字段 | 前端 JSON textarea 占位(待 rui/rui-framework#4 文件上传接口就绪后升级) |
| 4 | 敏感字段(appSecret/appKey/aesKey | 列表展示 6 个星号 `******`,编辑时留空表示不修改 |
## 5. 字段定义(共 21 个,UI 涉及 19 个)
| 字段 | 类型 | 必填 | UI 控件 | Tab | 说明 |
|------|------|------|---------|-----|------|
| id | Long | — | (仅后端) | — | 主键 |
| tenantId | Long | — | (仅后端) | — | 租户ID 0=系统级(自动填充) |
| ownerType | String | 是 | el-select | 1 | PLATFORM / TENANT |
| platform | String | 是 | el-select | 1 | wechat / alipay / stripe |
| name | String | 是 | el-input | 1 | 管理用名称 |
| appId | String | 否 | el-input | 2 | 应用IDUNIQUE |
| appSecret | String | 否 | el-input (password) | 2 | **敏感**:列表脱敏,编辑留空不修改 |
| appKey | String | 否 | el-input (password) | 2 | **敏感** |
| certificates | String | 否 | JSON textarea | 2 | 多证书 JSON 数组(**占位** |
| aesKey | String | 否 | el-input (password) | 2 | **敏感** |
| redirectUri | String | 否 | el-input | 3 | OAuth2 回调地址 |
| merchantId | String | 否 | el-input | 3 | 商户号 |
| signType | String | 否 | el-select | 3 | RSA2 / MD5 / HMAC |
| notifyUrl | String | 否 | el-input | 3 | 支付回调 |
| apiBase | String | 否 | el-input | 3 | API 根地址 |
| isSandbox | 0/1 | 否 | el-switch | 4 | 是否沙箱环境 |
| extra | String | 否 | JSON textarea | 4 | JSON 扩展 |
| isEncrypted | 0/1 | — | **不展示** | — | 预留加密字段(暂不实现) |
| status | 0/1 | 否 | el-switch | 4 | 启用/禁用,默认 1 |
| description | String | 否 | el-input (textarea) | 1 | 备注 |
| sortNo | Int | 否 | el-input-number | 1 | 排序号 |
### 5.1 枚举映射(UI 展示用)
```ts
const platformMap: Record<string, { label: string; type: 'primary' | 'success' | 'warning' }> = {
wechat: { label: '微信', type: 'success' },
alipay: { label: '支付宝', type: 'primary' },
stripe: { label: 'Stripe', type: 'warning' },
}
const ownerTypeMap: Record<string, { label: string; type: 'primary' | 'success' }> = {
PLATFORM: { label: '平台级', type: 'primary' },
TENANT: { label: '租户级', type: 'success' },
}
const signTypeOptions = [
{ label: 'RSA2', value: 'RSA2' },
{ label: 'MD5', value: 'MD5' },
{ label: 'HMAC', value: 'HMAC' },
]
```
## 6. 涉及文件清单(Files To Change
| # | 文件 | 操作 | 用途 |
|---|------|------|------|
| 1 | `admin-ui/src/service/system/sysAppService.ts` | 新建 | 业务 Service,继承 `BaseService('/system/admin/app')` |
| 2 | `admin-ui/src/service/system/index.ts` | 改动 | 追加 `export { sysAppService } from './sysAppService'` |
| 3 | `admin-ui/src/router/modules/system.ts` | 改动 | 新增 `system/app` 路由,meta.i18n 键 `menu.systemApp` |
| 4 | `admin-ui/src/locales/zh-CN.ts` | 改动 | `menu.systemApp: '应用集成'` |
| 5 | `admin-ui/src/locales/en-US.ts` | 改动 | `menu.systemApp: 'App Integration'` |
| 6 | `admin-ui/src/views/system/app/Index.vue` | 新建 | 列表页(RuiTable + 操作工具栏) |
| 7 | `admin-ui/src/views/system/app/SysAppFormDialog.vue` | 新建 | 新增/编辑弹窗(4 Tab) |
**变更统计**:新建 3 个文件,修改 4 个文件。**总计 7 个文件**。
## 7. 列表页设计(`views/system/app/Index.vue`
### 7.1 结构
```vue
<template>
<div>
<h2 class="text-xl font-bold mb-4">{{ $t('menu.systemApp') }}</h2>
<RuiTable
ref="tableRef"
:columns="columns"
:load-data="loadData"
:show-selection="true"
:exportable="true"
export-filename="SysApp应用集成列表"
>
<!-- 查询区、工具栏、列 slot、操作 slot -->
</RuiTable>
<SysAppFormDialog v-model:visible="dialogVisible" :row="currentRow" @success="handleFormSuccess" />
</div>
</template>
```
### 7.2 查询区(4 个条件)
| 控件 | 字段 | 控件类型 | 选项 |
|------|------|---------|------|
| 应用名称 | `name` | `el-input` (clearable) | — |
| 平台 | `platform` | `el-select` (clearable) | wechat / alipay / stripe |
| 所有者类型 | `ownerType` | `el-select` (clearable) | PLATFORM / TENANT |
| 状态 | `status` | `el-select` (clearable) | 启用(1) / 禁用(0) |
### 7.3 列配置(columns
| prop | label | 宽度/最小宽度 | slot | 备注 |
|------|-------|--------------|------|------|
| name | 应用名称 | minWidth 150 | — | — |
| platform | 平台 | width 100 | 是 | 彩色 Tag |
| ownerType | 所有者 | width 100 | 是 | PLATFORM 蓝、TENANT 绿 |
| appId | 应用ID | minWidth 120 | — | 列表不脱敏(UNIQUE 标识) |
| status | 状态 | width 90 | 是 | Switch |
| createdAt | 创建时间 | minWidth 180 | — | dataType='dateTime'sortable='custom' |
> **脱敏说明**appSecret / appKey / aesKey 三个敏感字段**不进入列表列**,仅在编辑弹窗中处理。列表中没有任何明文密钥展示位。
### 7.4 工具栏
- **左侧**:新增应用按钮(`type="primary"`
- **右侧**:批量删除(基于 `show-selection`) + 导出 + 刷新 + 列设置(RuiTable 内置)
### 7.5 行操作
- 编辑
- 删除(ElMessageBox 二次确认,删除成功后 `tableRef.refresh()`
### 7.6 启停切换
```ts
async function handleStatusChange(row: any, status: number) {
if (!row?.id) return
try {
await sysAppService.changeStatus(row.id, status)
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
} catch {
row.status = status === 1 ? 0 : 1 // 失败回滚
}
}
```
## 8. 表单弹窗设计(`views/system/app/SysAppFormDialog.vue`
### 8.1 弹窗基本属性
- 宽度:`760px`
- title:编辑时「编辑应用」/ 新增时「新增应用」
- 关闭点击遮罩:`close-on-click-modal="false"`
- props`visible: boolean`、`row: any`
- emits`update:visible`、`success`
### 8.2 数据加载流程
```
watch(visible, val => {
if (val) {
if (props.row) {
// 编辑:拉取详情(确保拿到完整字段,包括敏感字段的明文用于编辑回显)
sysAppService.getById(props.row.id).then(data => { form.value = { ...data } })
} else {
// 新增:重置为默认值
form.value = { ...defaultForm }
}
}
})
```
### 8.3 默认值(新增时)
```ts
const defaultForm = {
id: undefined,
ownerType: 'PLATFORM',
platform: 'wechat',
name: '',
appId: '',
appSecret: '',
appKey: '',
certificates: '',
aesKey: '',
redirectUri: '',
merchantId: '',
signType: 'RSA2',
notifyUrl: '',
apiBase: '',
isSandbox: 0,
extra: '',
status: 1,
description: '',
sortNo: 0,
}
```
### 8.4 4 Tab 分布
#### Tab 1:基础信息
```
- name* el-input (必填)
- ownerType* el-select (必填, PLATFORM / TENANT)
- platform* el-select (必填, wechat / alipay / stripe)
- description el-input (textarea, :rows="2")
- sortNo el-input-number
```
#### Tab 2:凭证信息
```
- appId el-input
- appSecret el-input (type="password" show-password)
- appKey el-input (type="password" show-password)
- aesKey el-input (type="password" show-password)
- certificates el-input (type="textarea" :rows="4")
placeholder='[{"name":"cert1","content":"<PEM 内容>"}]'
下方 helper text:「多证书 JSON 数组。格式:[{name, content}]。待后端文件上传接口就绪后升级为文件上传。」
```
> **敏感字段编辑规则**appSecret / appKey / aesKey 三个字段在编辑时**留空 = 不修改原值**。这一规则完全照搬 `OAuth2ClientFormDialog` 的现有做法,保证行为一致。
#### Tab 3:接口配置
```
- redirectUri el-input
- notifyUrl el-input
- apiBase el-input
- merchantId el-input
- signType el-select (RSA2 / MD5 / HMAC)
```
#### Tab 4:高级
```
- isSandbox el-switch (0/1)
- extra el-input (type="textarea" :rows="4")
下方 helper text:「JSON 扩展字段,提交前需通过 JSON 格式校验」
- status el-switch (0/1,默认 1)
```
### 8.5 校验
- **必填字段**`name`、`ownerType`、`platform`
- **JSON 字段**certificates 和 extra 字段在提交前调用 `validateJSON()` 校验,非空时尝试 `JSON.parse`,失败则 `ElMessage.error('xxx 字段 JSON 格式错误')` 并阻止提交。
```ts
function validateJSON(value: string, fieldName: string): boolean {
if (!value || !value.trim()) return true // 空值允许
try {
JSON.parse(value)
return true
} catch {
ElMessage.error(`${fieldName} JSON 格式错误`)
return false
}
}
```
### 8.6 提交逻辑
```ts
async function handleSubmit() {
await formRef.value.validate()
if (!validateJSON(form.value.certificates, 'certificates')) return
if (!validateJSON(form.value.extra, 'extra')) return
loading.value = true
try {
const isEdit = !!form.value.id
const success = isEdit
? await sysAppService.update(form.value as any)
: await sysAppService.add(form.value)
if (success !== false) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
emit('success')
dialogVisible.value = false
}
} catch {
// 错误已由请求拦截器统一提示
} finally {
loading.value = false
}
}
```
## 9. Service 层设计(`service/system/sysAppService.ts`
完整实现(参考 `oauth2ClientService.ts` 的极简模式):
```ts
import { BaseService } from '../BaseService'
/**
* SysApp(第三方应用集成)服务
*
* <p>负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。</p>
*
* <p><b>后续升级路径</b>:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加
* `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea
* 升级为文件上传组件。</p>
*/
class SysAppService extends BaseService {
constructor() {
super('/system/admin/app')
}
}
/** SysApp 服务单例 */
export const sysAppService = new SysAppService()
```
并在 `service/system/index.ts` 末尾追加:
```ts
export { sysAppService } from './sysAppService'
```
## 10. 路由与国际化
### 10.1 路由注册
在 `admin-ui/src/router/modules/system.ts` 的 `M` 常量中追加:
```ts
systemApp: 'menu.systemApp',
```
在 `systemRoutes` 数组中追加:
```ts
{ path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
```
> 路由位置:放在 `systemOAuth2Client` 之后,保持「集成类」菜单分组相邻。
### 10.2 国际化
`admin-ui/src/locales/zh-CN.ts`(在 `systemOAuth2Client` 后追加):
```ts
systemApp: '应用集成',
```
`admin-ui/src/locales/en-US.ts`(在 `system` block 内对应位置追加):
```ts
systemApp: 'App Integration',
```
## 11. 错误处理
- **统一拦截**:所有 HTTP 错误由 `utils/request` 拦截器统一提示(已存在),前端代码不再额外 try-catch 提示。
- **业务校验**:表单必填、JSON 格式校验在组件内完成,失败用 `ElMessage` 提示。
- **状态回滚**:启停切换失败时,将 `row.status` 回滚到原值。
- **删除确认**:删除前 `ElMessageBox.confirm` 二次确认。
## 12. 测试策略
### 12.1 静态检查
```bash
pnpm --filter admin-ui type-check # 0 errors
pnpm --filter admin-ui lint # 0 errors
```
### 12.2 运行时验证(手动)
1. **启动 dev server**`pnpm dev:admin`
2. **登录** 后访问 `/system/app`(需菜单权限 `sys:app:query`,从后端菜单加载)
3. **列表加载**:默认加载列表数据,列展示正确
4. **查询**:按 name / platform / ownerType / status 过滤,验证结果正确
5. **新增**:点「新增应用」→ 填写各 Tab 必填项 → 提交 → 列表出现新行
6. **编辑**:点行内编辑 → 弹窗加载详情(4 Tab 回显)→ 修改 → 提交 → 列表更新
7. **敏感字段验证**:编辑时不输入 appSecret → 提交后重新打开,明文应保持不变(**由后端「留空不修改」规则保证**)
8. **JSON 字段验证**certificates / extra 输入非 JSON 文本 → 提交时被拦截并提示
9. **启停**:点击状态 Switch → 接口调用 → 列表状态切换;模拟失败时 row.status 回滚
10. **删除**:点击删除 → 二次确认 → 提交 → 行从列表消失
11. **批量删除**:选中多行 → 批量删除 → 全部消失
12. **导出**:点击导出 → 下载 CSV 文件
13. **脱敏验证**:在 devtools Network 面板检查 `/page` 返回的 records**不应**包含 appSecret / appKey / aesKey 明文(这三个字段不进列表列)
### 12.3 验证清单(提交前必过)
- [ ] `pnpm type-check` 通过
- [ ] `pnpm lint` 通过
- [ ] 列表 → 新增 → 编辑 → 启停 → 删除 → 批量删除 完整跑通
- [ ] 列表中不展示任何明文密钥
- [ ] 敏感字段编辑留空后原值保持
- [ ] certificates / extra 非法 JSON 提交被拦截
- [ ] 菜单「应用集成」在侧边栏正确显示,路由跳转正常
## 13. 风险与缓解(Risks And Mitigations
| # | 风险 | 缓解措施 |
|---|------|---------|
| 1 | **后端脱敏返回**:若后端在列表接口已经对 appSecret/appKey/aesKey 做脱敏(如 `******`),则编辑回显时弹窗中拿不到原值 | 列表用 `getById(id)` 拉详情(详情接口通常返回明文),并设计为编辑时强制用户重新输入敏感字段。本期采用「详情接口返回明文,编辑留空不修改」模式;如未来后端详情也脱敏,需在 UI 加「修改敏感字段」开关。 |
| 2 | **certificates JSON 格式错误**:用户提交非合法 JSON 导致后端解析失败 | 提交前 `validateJSON()` 拦截,UI 给出明确错误提示;helper text 提示正确格式。 |
| 3 | **文件上传接口未就绪**certificates 暂用 JSON 占位,用户体验差 | 已在 rui/rui-framework#4 提 Issue;后端接口就绪后再升级为文件上传组件(升级路径已记录在 `sysAppService.ts` 注释中)。 |
| 4 | **路由重复**system.ts 中路由顺序错乱导致菜单不显示 | 路由注册在 systemOAuth2Client 之后;meta.i18n 键值与 locales 文件保持一致。 |
| 5 | **列表数据过大导致性能问题** | 分页由 RuiTable 默认处理(page=1, size=10),无额外风险。 |
| 6 | **前端调用了不存在的接口**:万一后端实际未提供 `import` 接口 | BaseService 自带 `importable` 开关默认 false,不暴露导入按钮。如后端未提供 import 接口则本 UI 不调用即可。 |
## 14. 决策摘要(Decision Summary
- **架构**:照搬 `oauth2-client` 模式(`BaseService` 13 行 + `RuiTable` 列表 + `FormDialog` 弹窗),不引入新依赖、不发明新模式。
- **菜单归属**:系统管理 → 子菜单「应用集成」,路由 `/system/app`。
- **表单布局**el-tabs 4 Tab760px 弹窗。
- **敏感字段**appSecret / appKey / aesKey 列表脱敏 6 星号,编辑留空不修改。
- **certificates 字段**JSON textarea 占位,UI 注释提醒「待后端文件上传接口就绪后升级」。
- **isEncrypted 字段**UI 暂不实现。
- **测试**type-check + lint + 手动跑通完整 CRUD。
- **依赖后端**:仅依赖 rui-framework 既有 `/system/admin/app` 接口;`/system/admin/file/upload`rui/rui-framework#4)就绪后再升级 certificates 体验。
---
**设计评审状态**: 待评审
**下一步**: 用户评审通过后,编写实施计划(Plan)
@@ -0,0 +1,261 @@
# 用户管理接口适配设计规范
**工单**: #2 - 用户管理接口变更通知
**日期**: 2026-06-07
**方案**: 方案 B(完整适配)
---
## 1. 背景
后端已完成用户管理模块接口重构(提交 `dbd04d8`),支持部门、角色联表查询和聚合信息返回。前端需要适配以:
- 减少请求次数(从 3 个请求合并为 1 个)
- 支持部门/角色筛选
- 在列表和详情中展示部门、角色信息
## 2. 后端变更摘要
### 2.1 用户实体扩展字段
```json
{
"id": 1,
"username": "admin",
"phone": "13800138000",
"depts": [
{
"deptId": 1,
"deptCode": "TECH",
"deptName": "技术部",
"main": true
}
],
"roles": [
{
"roleId": 1,
"roleCode": "admin",
"roleName": "管理员",
"dataScope": 1
}
]
}
```
### 2.2 新增接口
- `GET /user/admin/user/{id}/aggregate` - 聚合查询(基础信息 + 部门列表 + 角色列表)
### 2.3 增强接口
- `GET /user/admin/user/page` - 自动返回 `depts``roles`
- `GET /user/admin/user/list` - 自动返回 `depts``roles`
- 支持筛选参数:`deptId``roleId`
## 3. 前端适配范围
### 3.1 用户列表页 (`views/user/info/Index.vue`)
**新增列:**
- 部门列:显示用户所属部门名称(多个部门用逗号分隔,主部门加粗)
- 角色列:显示用户角色名称(多个角色用标签展示)
**新增筛选条件:**
- 部门筛选:树形选择器(`el-tree-select`),支持多选
- 角色筛选:树形选择器(`el-tree-select`),支持多选
**数据流:**
- 列表接口自动返回 `depts``roles`,无需额外请求
- 筛选参数通过 `queryParams` 传递给 `userService.page()`
### 3.2 用户详情弹窗 (`views/user/info/UserDetailDialog.vue`)
**改造:**
- 使用新的聚合接口 `GET /user/admin/user/{id}/aggregate`
- 展示部门列表(部门名称 + 是否主部门标记)
- 展示角色列表(角色名称 + 数据范围)
**数据流:**
```
打开弹窗 → 调用 aggregate 接口 → 展示完整信息
```
### 3.3 用户表单 (`views/user/info/UserFormDialog.vue`)
**优化:**
- 编辑时从 `row.depts` 解析 `deptIds`(替代调用 `userDeptService.listDeptIdsByUserId`
- 编辑时从 `row.roles` 解析 `roleIds`(替代调用 `userService.getRoles`
- 保留 `userDeptService.assignDepts``userPostService.assignPosts` 用于保存
### 3.4 Service 层扩展 (`service/user/userService.ts`)
**新增方法:**
- `aggregate(userId)` - 调用聚合查询接口
## 4. 组件设计
### 4.1 部门/角色展示组件
无需新增组件,直接在表格列中使用 `slot` 渲染:
```vue
<!-- 部门列 -->
<template #column-depts="{ row }">
<el-tag
v-for="dept in row.depts"
:key="dept.deptId"
:type="dept.main ? 'primary' : 'info'"
size="small"
class="mr-1"
>
{{ dept.deptName }}
</el-tag>
</template>
<!-- 角色列 -->
<template #column-roles="{ row }">
<el-tag
v-for="role in row.roles"
:key="role.roleId"
type="success"
size="small"
class="mr-1"
>
{{ role.roleName }}
</el-tag>
</template>
```
### 4.2 筛选区域
```vue
<template #search="{ query: q, search, reset }">
<!-- 现有筛选条件... -->
<!-- 新增部门筛选 -->
<el-form-item label="所属部门">
<el-tree-select
v-model="q.deptId"
:data="deptTree"
check-strictly
node-key="id"
:props="{ label: 'deptName', children: 'children' }"
placeholder="请选择部门"
clearable
style="width: 200px"
/>
</el-form-item>
<!-- 新增角色筛选 -->
<el-form-item label="角色">
<el-tree-select
v-model="q.roleId"
:data="roleTree"
check-strictly
node-key="id"
:props="{ label: 'roleName', children: 'children' }"
placeholder="请选择角色"
clearable
style="width: 200px"
/>
</el-form-item>
</template>
```
## 5. 数据类型定义
```typescript
// 部门信息(嵌套在用户中)
interface UserDept {
deptId: number
deptCode: string
deptName: string
main: boolean
}
// 角色信息(嵌套在用户中)
interface UserRole {
roleId: number
roleCode: string
roleName: string
dataScope: number
}
// 扩展用户类型
interface User {
id: number
username: string
// ... 其他字段
depts?: UserDept[]
roles?: UserRole[]
}
```
## 6. 接口调用变更
### 6.1 列表页
**变更前:**
- 调用 `userService.page(params)` - 仅返回基础信息
**变更后:**
- 调用 `userService.page(params)` - 自动包含 `depts``roles`
- 支持 `deptId``roleId` 筛选参数
### 6.2 详情弹窗
**变更前:**
- 直接使用 `props.row` 数据
**变更后:**
- 打开时调用 `userService.aggregate(props.row.id)`
- 使用返回的完整数据渲染
### 6.3 编辑表单
**变更前:**
```typescript
// 需要额外请求获取部门和角色
const deptIds = await userDeptService.listDeptIdsByUserId(userId)
const roleIds = await userService.getRoles(userId)
```
**变更后:**
```typescript
// 直接从 row 中解析
const deptIds = props.row.depts?.map((d: any) => d.deptId) || []
const roleIds = props.row.roles?.map((r: any) => r.roleId) || []
```
## 7. 错误处理
- 聚合接口失败时,回退到使用 `props.row` 基础信息
- 部门/角色数据缺失时,显示 "-" 或空标签
- 筛选条件不影响现有查询逻辑
## 8. 兼容性
- 原有接口保持不变
- 新增字段通过 `@TableField(exist = false)` 添加,不影响旧逻辑
- 保留 `userDeptService``userPostService` 用于分配功能
## 9. 测试要点
1. 列表页是否正确显示部门和角色信息
2. 部门/角色筛选是否生效
3. 详情弹窗是否正确展示聚合数据
4. 编辑表单是否正确解析已选部门/角色
5. 保存后数据是否正确刷新
## 10. 文件变更清单
| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `views/user/info/Index.vue` | 修改 | 添加部门/角色列和筛选条件 |
| `views/user/info/UserDetailDialog.vue` | 修改 | 使用聚合接口,展示部门/角色详情 |
| `views/user/info/UserFormDialog.vue` | 修改 | 从 row 解析 deptIds/roleIds |
| `service/user/userService.ts` | 修改 | 添加 aggregate 方法 |
---
**设计评审状态**: 待评审
**下一步**: 用户评审通过后,编写实施计划
@@ -0,0 +1,758 @@
# API Portal — 睿核科技 API 文档门户 设计文档
> 日期:2026-06-08
> 状态:待审核
> 作者:AI + 张晟
---
## 一、项目概述
### 1.1 项目名称
- **前端项目**`api-portal`(睿核科技 API 文档门户)
- **后端服务**`rui-service-apidoc`API 文档管理微服务)
### 1.2 核心目标
构建一个**精美的 API 文档门户系统**,具备以下能力:
1. **多源 API 文档聚合** — 自动同步微服务 OpenAPI JSON + 手动导入
2. **自定义多级菜单** — 3 级菜单树,自由组织,不受 Controller 结构限制
3. **文档补全增强** — 管理后台补充中文说明、示例、注意事项
4. **精美文档展示** — VitePress 风格三栏布局,代码高亮
5. **AI 结构化接口** — 提供完整 API 信息供 AI 读取、代码生成、变更感知
6. **多项目管理** — 可自由创建项目,每个项目独立菜单和文档
### 1.3 用户角色
| 角色 | 说明 |
|------|------|
| 匿名访客 | 浏览公开项目的 API 文档 |
| 开发者 | 浏览 + 搜索 + 测试 API |
| 管理员 | 管理项目、菜单、同步 API、补全文档 |
| AI Agent | 通过专用接口读取结构化 API 数据 |
---
## 二、系统架构
```
┌──────────────────────────────────────────────────────────────────┐
│ 整体架构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 微服务集群 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ rui-user │ │rui-system │ │rui-cashier│ │rui-payment│ │
│ │/v3/api- │ │/v3/api- │ │/v3/api- │ │/v3/api- │ │
│ │ docs │ │ docs │ │ docs │ │ docs │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ rui-service-apidoc(独立微服务) │ │
│ │ │ │
│ │ 同步引擎 文档管理 AI 接口 │ │
│ │ - 定时拉取 - 菜单 CRUD - 结构化查询 │ │
│ │ - 手动触发 - 文档补全 - 变更检测 │ │
│ │ - JSON 导入 - 版本管理 - 代码生成数据 │ │
│ └──────────┬───────────────────┬───────────────────────┘ │
│ │ │ │
│ ┌────────▼────────┐ ┌──────▼──────────┐ │
│ │ admin-ui │ │ api-portal │ │
│ │ (管理后台) │ │ (文档门户) │ │
│ │ 补全信息/管理菜单 │ │ 精美展示/公开 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ AI Agent (Codex 等) │ │
│ │ 通过 /ai/* 接口读取结构化 API 数据 │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
---
## 三、后端设计 — rui-apidoc
### 3.1 技术栈
| 项目 | 选型 | 说明 |
|------|------|------|
| 语言 | Java 21 | 和框架一致 |
| 框架 | Spring Boot 3 + Spring Cloud | 和框架一致 |
| 数据库 | MySQL 8 | 和框架一致 |
| ORM | MyBatis-Plus | 继承 BaseEntity |
| API 文档 | SpringDoc (OpenAPI 3) | 和框架一致 |
| 注册中心 | Nacos | 复用现有 |
| 包管理 | Maven | parent: rui-parent |
### 3.2 模块结构
遵循 `业务应用模块创建规则`,作为 `app/` 下的独立业务模块:
```
app/
└── rui-apidoc/
├── pom.xml # parent: rui-app
├── rui-apidoc-api/ # [可部署] REST API + 启动类
│ └── src/main/java/com/rui/apidoc/api/
│ ├── ApidocApplication.java
│ └── controller/
│ ├── PortalController.java # 文档门户 API(公开)
│ ├── ApidocController.java # 管理后台 API(需认证)
│ └── AiController.java # AI 专用 API
├── rui-apidoc-common/ # [库] DTO、VO、枚举
│ └── src/main/java/com/rui/apidoc/
│ ├── dto/
│ ├── vo/
│ └── enums/
├── rui-apidoc-core/ # [库] Entity、Mapper、Service
│ └── src/main/java/com/rui/apidoc/core/
│ ├── entity/
│ ├── mapper/
│ ├── service/
│ └── config/
└── rui-apidoc-task/ # [库] 定时同步任务
└── src/main/java/com/rui/apidoc/task/
└── SyncTask.java
```
**POM 层级**
```
app/pom.xml (rui-app)
└── rui-apidoc/pom.xml (parent: rui-app)
├── rui-apidoc-api → parent: rui-apidoc
├── rui-apidoc-common
├── rui-apidoc-core
└── rui-apidoc-task
```
**依赖关系**
```
common → rui-common-core
core → rui-common-mybatis, rui-common-security, common
task → core, common
api → core, task, rui-common-security, rui-common-web
```
### 3.3 核心数据模型
> 所有实体继承 `BaseEntity`,自动包含 id, tenant_id, deleted, created_by, created_at, updated_by, updated_at 字段。
> 建表 SQL 只写业务字段,公共字段由 BaseEntity 规范统一。
#### 3.3.1 项目表 `apidoc_project`
```sql
CREATE TABLE apidoc_project (
-- 业务字段
project_key VARCHAR(64) NOT NULL COMMENT '项目唯一标识,如 rui-framework',
name VARCHAR(128) NOT NULL COMMENT '项目名称',
description TEXT DEFAULT NULL COMMENT '项目描述',
visibility TINYINT NOT NULL DEFAULT 0 COMMENT '可见性 0=公开 1=需登录 2=仅管理员',
logo_url VARCHAR(512) DEFAULT NULL COMMENT '项目 Logo',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序',
-- BaseEntity 公共字段
id BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0=正常 1=已删',
created_by BIGINT DEFAULT NULL COMMENT '创建者ID',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT DEFAULT NULL COMMENT '更新者ID',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_project_key (project_key)
) COMMENT='API文档项目';
```
#### 3.3.2 微服务注册表 `apidoc_service`
```sql
CREATE TABLE apidoc_service (
-- 业务字段
project_id BIGINT NOT NULL COMMENT '所属项目ID',
service_name VARCHAR(128) NOT NULL COMMENT '服务名,如 rui-service-user',
service_url VARCHAR(512) DEFAULT NULL COMMENT '服务地址,如 http://rui-service-user:8080',
sync_mode TINYINT NOT NULL DEFAULT 0 COMMENT '同步模式 0=自动同步(URL) 1=手动导入',
sync_status TINYINT NOT NULL DEFAULT 0 COMMENT '同步状态 0=未同步 1=同步中 2=成功 3=失败',
last_sync_at DATETIME DEFAULT NULL COMMENT '最后同步时间',
openapi_json MEDIUMTEXT DEFAULT NULL COMMENT '缓存的 OpenAPI JSON',
-- BaseEntity 公共字段
id BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_by BIGINT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_project (project_id)
) COMMENT='API文档微服务注册';
```
#### 3.3.3 多级菜单表 `apidoc_menu`
```sql
CREATE TABLE apidoc_menu (
-- 业务字段
project_id BIGINT NOT NULL COMMENT '所属项目ID',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父级菜单ID0=顶级',
name VARCHAR(128) NOT NULL COMMENT '菜单名称',
icon VARCHAR(64) DEFAULT NULL COMMENT '图标',
menu_type TINYINT NOT NULL DEFAULT 0 COMMENT '类型 0=目录 1=自定义内容页 2=接口组',
sort_order INT NOT NULL DEFAULT 0 COMMENT '同级排序',
service_id BIGINT DEFAULT NULL COMMENT '关联的微服务(menu_type=2时)',
endpoint_paths JSON DEFAULT NULL COMMENT '关联的API路径列表',
content MEDIUMTEXT DEFAULT NULL COMMENT 'Markdown自定义内容(menu_type=1时)',
-- BaseEntity 公共字段
id BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_by BIGINT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_project_parent (project_id, parent_id)
) COMMENT='API文档菜单';
```
#### 3.3.4 API 端点表 `apidoc_endpoint`
```sql
CREATE TABLE apidoc_endpoint (
-- 业务字段
service_id BIGINT NOT NULL COMMENT '所属微服务ID',
menu_id BIGINT DEFAULT NULL COMMENT '关联菜单ID',
method VARCHAR(10) NOT NULL COMMENT 'HTTP方法 GET/POST/PUT/DELETE/PATCH',
path VARCHAR(512) NOT NULL COMMENT '接口路径',
summary VARCHAR(256) DEFAULT NULL COMMENT '接口摘要(来自OpenAPI)',
description TEXT DEFAULT NULL COMMENT '接口描述(来自OpenAPI)',
deprecated TINYINT NOT NULL DEFAULT 0 COMMENT '是否废弃',
tags JSON DEFAULT NULL COMMENT 'OpenAPI tags',
request_params JSON DEFAULT NULL COMMENT '查询参数 [{name,type,required,description}]',
request_body JSON DEFAULT NULL COMMENT '请求体 JSON Schema',
request_headers JSON DEFAULT NULL COMMENT '请求头',
path_params JSON DEFAULT NULL COMMENT '路径参数',
response_200 JSON DEFAULT NULL COMMENT '200 响应 JSON Schema',
response_error JSON DEFAULT NULL COMMENT '错误响应 Schema',
raw_openapi JSON DEFAULT NULL COMMENT '原始 OpenAPI operation 对象',
content_hash VARCHAR(64) DEFAULT NULL COMMENT '内容哈希,用于变更检测',
-- BaseEntity 公共字段
id BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_by BIGINT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_service_method_path (service_id, method, path),
INDEX idx_service (service_id),
INDEX idx_menu (menu_id)
) COMMENT='API端点';
```
#### 3.3.5 文档补充表 `apidoc_extra`
```sql
CREATE TABLE apidoc_extra (
-- 业务字段
endpoint_id BIGINT NOT NULL COMMENT '关联端点ID',
custom_title VARCHAR(256) DEFAULT NULL COMMENT '自定义标题(覆盖summary)',
custom_desc TEXT DEFAULT NULL COMMENT '自定义说明(中文描述)',
use_cases TEXT DEFAULT NULL COMMENT '使用场景说明',
notes TEXT DEFAULT NULL COMMENT '注意事项',
request_example JSON DEFAULT NULL COMMENT '请求示例',
response_example JSON DEFAULT NULL COMMENT '响应示例',
error_example JSON DEFAULT NULL COMMENT '错误响应示例',
test_cases JSON DEFAULT NULL COMMENT '测试用例 [{name,params,expected}]',
-- BaseEntity 公共字段
id BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_by BIGINT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_endpoint (endpoint_id)
) COMMENT='API文档补充信息';
```
#### 3.3.6 版本快照表 `apidoc_version`
```sql
CREATE TABLE apidoc_version (
-- 业务字段
service_id BIGINT NOT NULL COMMENT '所属微服务ID',
version VARCHAR(32) NOT NULL COMMENT '版本号,如 v1.2.0',
openapi_json MEDIUMTEXT DEFAULT NULL COMMENT 'OpenAPI JSON 快照',
change_log TEXT DEFAULT NULL COMMENT '变更日志(Markdown)',
-- BaseEntity 公共字段
id BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
created_by BIGINT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_service (service_id)
) COMMENT='API文档版本快照';
```
### 3.4 核心 API 设计
> 所有接口统一使用 `Result<T>` 返回封装(error=0 成功,code 为业务编码,message 提示,data 业务数据)。
> Controller 路径遵循 `/{模块}/{功能}/{方法}` 规范。
#### 3.4.1 文档门户 API(公开,免认证)
| 方法 | 路径 | 返回类型 | 说明 |
|------|------|---------|------|
| GET | `/apidoc/portal/project/list` | `Result<List<ProjectVO>>` | 公开项目列表 |
| GET | `/apidoc/portal/project/{key}` | `Result<ProjectVO>` | 项目详情 |
| GET | `/apidoc/portal/project/{key}/menu` | `Result<List<MenuTreeVO>>` | 项目菜单树 |
| GET | `/apidoc/portal/project/{key}/menu/{menuId}` | `Result<MenuDetailVO>` | 菜单详情 |
| GET | `/apidoc/portal/endpoint/{id}` | `Result<EndpointDetailVO>` | 端点详情(含补充信息) |
| GET | `/apidoc/portal/search` | `Result<List<EndpointVO>>` | 全文搜索 |
#### 3.4.2 管理后台 API(需认证 + 管理员权限)
遵循 Controller 分类规范,管理接口使用 `ApidocController`
| 方法 | 路径 | 返回类型 | 说明 |
|------|------|---------|------|
| POST | `/apidoc/admin/project` | `Result<Long>` | 创建项目 |
| PUT | `/apidoc/admin/project/{id}` | `Result<Void>` | 更新项目 |
| DELETE | `/apidoc/admin/project/{id}` | `Result<Void>` | 删除项目 |
| POST | `/apidoc/admin/service` | `Result<Long>` | 注册微服务 |
| POST | `/apidoc/admin/service/{id}/sync` | `Result<Void>` | 手动触发同步 |
| POST | `/apidoc/admin/service/{id}/import` | `Result<Void>` | 手动导入 OpenAPI JSON |
| POST | `/apidoc/admin/menu` | `Result<Long>` | 创建菜单 |
| PUT | `/apidoc/admin/menu/{id}` | `Result<Void>` | 更新菜单 |
| DELETE | `/apidoc/admin/menu/{id}` | `Result<Void>` | 删除菜单 |
| PUT | `/apidoc/admin/menu/sort` | `Result<Void>` | 批量排序 |
| POST | `/apidoc/admin/menu/{id}/bind` | `Result<Void>` | 菜单绑定 API 端点 |
| PUT | `/apidoc/admin/extra/{endpointId}` | `Result<Void>` | 更新补充信息 |
| POST | `/apidoc/admin/service/{id}/snapshot` | `Result<Void>` | 创建版本快照 |
#### 3.4.3 AI 专用 API
| 方法 | 路径 | 返回类型 | 说明 |
|------|------|---------|------|
| GET | `/apidoc/ai/projects` | `Result<List<AiProjectVO>>` | 所有项目(含服务概况) |
| GET | `/apidoc/ai/project/{key}/endpoints` | `Result<List<AiEndpointVO>>` | 项目全部端点(完整结构化) |
| GET | `/apidoc/ai/endpoint/{id}` | `Result<AiEndpointDetailVO>` | 单个端点完整信息 |
| GET | `/apidoc/ai/endpoint/search` | `Result<List<AiEndpointVO>>` | 搜索端点(keyword/method/path) |
| GET | `/apidoc/ai/project/{key}/changes` | `Result<List<ApiChangeVO>>` | API 变更(增量,since参数) |
| POST | `/apidoc/ai/query` | `Result<List<AiEndpointVO>>` | AI 自然语言查询 |
**AI 接口返回示例(`GET /apidoc/ai/endpoint/{id}`**
```json
{
"error": 0,
"code": null,
"message": "操作成功",
"data": {
"id": 1001,
"method": "POST",
"path": "/v1/user/users",
"summary": "创建用户",
"description": "管理员创建新用户,需要管理员权限",
"deprecated": false,
"tags": ["用户管理"],
"auth": "需要 Bearer Token,角色:ADMIN",
"requestParams": [],
"pathParams": [],
"requestHeaders": [
{ "name": "Authorization", "required": true, "description": "Bearer Token" },
{ "name": "Content-Type", "required": true, "value": "application/json" }
],
"requestBody": {
"type": "object",
"required": ["username", "password", "phone"],
"properties": {
"username": { "type": "string", "description": "用户名,4-20位字母数字", "example": "zhangsan" },
"password": { "type": "string", "description": "密码,8-32位", "example": "Pass@123" },
"phone": { "type": "string", "description": "手机号", "example": "13800138000" },
"email": { "type": "string", "description": "邮箱(可选)", "example": "zhangsan@example.com" }
}
},
"response200": {
"type": "object",
"properties": {
"error": { "type": "integer", "description": "0=成功", "example": 0 },
"code": { "type": "string", "example": "SUCCESS" },
"message": { "type": "string", "example": "操作成功" },
"data": {
"type": "object",
"properties": {
"id": { "type": "integer", "description": "用户ID", "example": 1001 },
"username": { "type": "string", "example": "zhangsan" },
"created_at": { "type": "string", "format": "date-time" }
}
}
}
},
"responseError": {
"400": { "error": 400, "code": "PARAM_INVALID", "message": "参数校验失败" },
"401": { "error": 401, "code": "UNAUTHORIZED", "message": "未登录" },
"403": { "error": 403, "code": "FORBIDDEN", "message": "无权限" }
},
"extra": {
"customTitle": "创建新用户",
"customDesc": "管理员通过此接口创建新用户,创建后用户处于启用状态",
"useCases": "后台管理 > 用户管理 > 新增用户",
"notes": "用户名唯一,不可重复。密码需满足复杂度要求。",
"requestExample": { "username": "zhangsan", "password": "Pass@123", "phone": "13800138000" },
"responseExample": { "error": 0, "code": null, "message": "操作成功", "data": { "id": 1001, "username": "zhangsan" } },
"testCases": [
{
"name": "正常创建",
"params": { "username": "testuser01", "password": "Pass@123", "phone": "13900139001" },
"expected": { "error": 0 }
},
{
"name": "用户名重复",
"params": { "username": "admin", "password": "Pass@123", "phone": "13900139002" },
"expected": { "error": 400, "code": "USER_EXISTS" }
}
]
},
"service": { "name": "rui-service-user", "project": "rui-framework" },
"updatedAt": "2026-06-08T10:00:00"
}
}
```
### 3.5 同步引擎设计
```
自动同步流程:
1. rui-apidoc-task 中的定时任务(可配置周期) 或 手动触发
2. 遍历 apidoc_service 中 sync_mode=0 且 sync_status!=1 的服务
3. HTTP 请求 {service_url}/v3/api-docs 获取 OpenAPI JSON
4. 使用 JsonUtil 解析 JSON,提取所有 Path + Operation
5. 计算每个 Operation 的 content_hash,与 apidoc_endpoint 现有数据对比
6. 新增/更新/标记废弃 端点,记录变更
7. 更新 sync_status=2, last_sync_at, openapi_json
手动导入流程:
1. 管理员在管理后台粘贴 OpenAPI JSON 或上传文件
2. 同上解析入库逻辑,sync_mode=1
```
### 3.6 启动类
```java
package com.rui.apidoc.api;
import com.rui.common.security.annotation.EnableResourceServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableResourceServer
public class ApidocApplication {
public static void main(String[] args) {
SpringApplication.run(ApidocApplication.class, args);
}
}
```
### 3.7 Nacos 白名单配置
```yaml
security:
oauth2:
ignore-urls:
- /apidoc/portal/**
- /apidoc/ai/**
```
> `/apidoc/portal/**` 和 `/apidoc/ai/**` 免认证公开访问。
> `/apidoc/admin/**` 需管理员权限。
## 四、前端设计 — api-portal
### 4.1 技术栈
| 项目 | 选型 | 说明 |
|------|------|------|
| 框架 | Vue 3 + TypeScript | 组合式 API (setup),和 admin-ui 一致 |
| 构建 | Vite 6 | 和 admin-ui 一致 |
| 样式 | UnoCSS | 原子化 CSS,和 admin-ui 一致 |
| 路由 | Vue Router 4 | 侧边栏联动 |
| HTTP | Axios | 统一封装 request,复用 admin-ui 模式 |
| 代码高亮 | Shiki | 和 VitePress 同款高亮引擎 |
| Markdown 渲染 | markdown-it + 自定义插件 | 自定义内容页 |
| 图标 | @iconify-json/carbon | 专业文档风格 |
> api-portal 是独立项目(不依赖 Element Plus),UI 全部基于 UnoCSS 原子化样式构建。
### 4.2 页面结构
#### 三栏布局(接口文档页)
```
┌─────────────────────────────────────────────────────────────────┐
│ 🏠 Rui API Portal 🔍 搜索接口... 🌙 主题 🌐 中文 │
├────────────┬──────────────────────────────────┬─────────────────┤
│ │ │ │
│ 📖 快速开始 │ POST /v1/user/users │ 请求示例 │
│ │ ━━━━━━━━━━━━━━━━━━━━ │ │
│ ▸ 用户管理 │ 创建新用户 │ curl -X POST \ │
│ 登录 │ │ /v1/user/users \│
│ 注册 │ 管理员通过此接口创建新用户,创建 │ -H 'Auth...' \ │
│ 用户列表 │ 后用户处于启用状态。 │ -d '{...}' │
│ 用户详情 │ │ │
│ 创建用户 │ ⚡ 需要管理员权限 │─────────────────│
│ 更新用户 │ 🔒 Bearer Token │ 响应示例 │
│ ▸ 订单管理 │ │ │
│ 创建订单 │ 请求参数 │ 200 OK │
│ 查询订单 │ ┌──────┬──────┬────┬────┐ │ { │
│ 取消订单 │ │ 参数 │ 类型 │必填│说明 │ │ "error":0, │
│ ▸ 支付 │ ├──────┼──────┼────┼────┤ │ "data":{...}│
│ 发起支付 │ │username│string│ ✓ │用户名│ │ } │
│ 支付回调 │ │password│string│ ✓ │密码 │ │ │
│ 退款 │ └──────┴──────┴────┴────┘ │─────────────────│
│ │ │ 测试用例 │
│ │ 响应参数 │ ✅ 正常创建 │
│ │ ┌──────┬──────┬────┐ │ ❌ 用户名重复 │
│ │ │ 字段 │ 类型 │说明 │ │ │
│ │ ├──────┼──────┼────┤ │ │
│ │ │ id │ long │用户ID│ │ │
│ │ └──────┴──────┴────┘ │ │
│ │ │ │
└────────────┴──────────────────────────────────┴─────────────────┘
```
#### 项目首页
```
┌─────────────────────────────────────────────────────────────────┐
│ 🏠 Rui API Portal 🔍 搜索接口... 🌙 主题 🌐 中文 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 睿核科技 API 文档中心 │
│ 探索、测试、集成我们的 API 服务 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 🔍 搜索所有 API 接口... │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 📦 rui-frame │ │ 📦 rui-cashier│ │ 📦 rui-payment│ │
│ │ work │ │ │ │ │ │
│ │ 基础平台框架 │ │ 收银系统 │ │ 支付系统 │ │
│ │ 128 接口 │ │ 56 接口 │ │ 32 接口 │ │
│ │ 5 模块 │ │ 3 模块 │ │ 2 模块 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 4.3 路由设计
```
/ → 项目首页(项目卡片列表)
/:projectKey → 项目页(菜单 + 欢迎页)
/:projectKey/menu/:menuId → 菜单页(自定义内容 / 接口组)
/:projectKey/endpoint/:id → 单个接口详情页
/search?keyword=&projectId= → 搜索结果页
```
### 4.4 核心功能
| 功能 | 说明 |
|------|------|
| 多级菜单导航 | 3 级可折叠侧边栏,图标 + 名称 |
| 接口文档渲染 | 方法/路径/参数/响应/示例,结构化表格展示 |
| 代码高亮 | 请求示例(curl/JS/Java)、响应示例 JSON |
| Markdown 自定义页 | 菜单项关联自定义 Markdown 内容(快速开始、认证说明等) |
| 全文搜索 | 搜索接口名、路径、描述、参数名 |
| 深色模式 | 主题切换 |
| 响应式 | 移动端适配(侧边栏折叠) |
| "试一试" | 简易 API 测试面板(填参数发请求) |
### 4.5 项目结构
> 遵循 admin-ui 的目录约定:service 层使用 BaseService 模式,composables 存放组合式函数,views 存放页面。
```
api-portal/
├── package.json
├── vite.config.ts
├── uno.config.ts
├── tsconfig.json
├── index.html
├── public/
│ └── favicon.svg
├── src/
│ ├── main.ts
│ ├── App.vue
│ ├── service/ # API 请求封装(和 admin-ui 一致)
│ │ ├── BaseService.ts # 通用 CRUD 基类(复用 admin-ui 模式)
│ │ ├── portalService.ts # 文档门户接口
│ │ └── types.ts # TypeScript 类型定义
│ ├── composables/ # 组合式函数
│ │ ├── useTheme.ts # 主题切换
│ │ ├── useMenu.ts # 菜单状态
│ │ └── useSearch.ts # 搜索
│ ├── components/
│ │ ├── layout/
│ │ │ ├── AppHeader.vue # 顶部导航
│ │ │ ├── AppSidebar.vue # 侧边栏
│ │ │ ├── AppFooter.vue # 底部
│ │ │ └── ThreeColumn.vue # 三栏布局容器
│ │ ├── endpoint/
│ │ │ ├── EndpointHeader.vue # 方法 + 路径 + 标签
│ │ │ ├── ParamsTable.vue # 参数表格
│ │ │ ├── ResponseSchema.vue # 响应结构
│ │ │ ├── CodeBlock.vue # 代码块(Shiki 高亮)
│ │ │ └── TryIt.vue # "试一试"面板
│ │ ├── markdown/
│ │ │ └── MarkdownRenderer.vue # Markdown 渲染
│ │ └── search/
│ │ └── SearchDialog.vue # 搜索弹窗
│ ├── views/ # 页面(和 admin-ui 命名一致)
│ │ ├── HomePage.vue # 首页
│ │ ├── ProjectPage.vue # 项目页
│ │ ├── MenuPage.vue # 菜单内容页
│ │ ├── EndpointPage.vue # 接口详情页
│ │ └── SearchPage.vue # 搜索结果页
│ ├── router/
│ │ └── index.ts
│ ├── stores/ # Pinia 状态管理
│ │ └── project.ts # 项目状态
│ ├── styles/
│ │ ├── variables.css # CSS 变量(主题色)
│ │ └── prose.css # 文档内容样式
│ └── utils/
│ ├── request.ts # Axios 封装(复用 admin-ui 模式,baseURL 指向 apidoc 服务)
│ └── format.ts # 格式化工具
└── env.d.ts
```
### 4.6 request.ts 设计
> 复用 admin-ui 的 request 封装模式,但 api-portal 是公开文档站,不需要 Token/租户拦截。
```typescript
// src/utils/request.ts
import axios from 'axios'
const request = axios.create({
baseURL: '/api', // 通过网关转发到 rui-apidoc
timeout: 10000,
})
// 响应拦截 — 复用 Result<T> 的 error 判断逻辑
request.interceptors.response.use(
(response) => {
const { data } = response
if (data.error !== 0) {
console.error(data.message || '请求失败')
return Promise.reject(data)
}
return data
},
(error) => {
console.error('网络错误', error.message)
return Promise.reject(error)
},
)
export { request }
```
### 4.7 service 层设计
> 遵循 admin-ui 的 BaseService 模式,但 api-portal 以只读查询为主。
```typescript
// src/service/portalService.ts
import { request } from '@/utils/request'
/** 获取公开项目列表 */
export function getProjectList() {
return request({ url: '/apidoc/portal/project/list', method: 'get' })
}
/** 获取项目详情 */
export function getProject(key: string) {
return request({ url: `/apidoc/portal/project/${key}`, method: 'get' })
}
/** 获取项目菜单树 */
export function getMenuTree(key: string) {
return request({ url: `/apidoc/portal/project/${key}/menu`, method: 'get' })
}
/** 获取菜单详情 */
export function getMenuDetail(key: string, menuId: string) {
return request({ url: `/apidoc/portal/project/${key}/menu/${menuId}`, method: 'get' })
}
/** 获取端点详情 */
export function getEndpoint(id: string) {
return request({ url: `/apidoc/portal/endpoint/${id}`, method: 'get' })
}
/** 搜索 */
export function searchEndpoints(keyword: string, projectId?: string) {
return request({ url: '/apidoc/portal/search', method: 'get', params: { keyword, projectId } })
}
```
## 五、部署架构
```
┌──────────────────────────────────────────────────┐
│ Nginx / Gateway │
│ ├─ / → api-portal 静态资源 │
│ ├─ /admin → admin-ui 静态资源 │
│ └─ /api/apidoc/* → rui-service-apidoc │
└──────────────────────────────────────────────────┘
api-portal 构建产物部署为静态资源,通过 Nginx 或 CDN 分发。
rui-service-apidoc 注册到 Nacos,通过 Gateway 路由。
```
---
## 六、开发优先级
### Phase 1MVP(最小可用)
1. 后端:项目/菜单/端点 CRUD + OpenAPI JSON 手动导入
2. 前端:三栏布局 + 菜单导航 + 接口文档渲染
### Phase 2:增强
3. 后端:自动同步引擎(定时拉取 /v3/api-docs
4. 后端:文档补全管理
5. 前端:搜索 + 深色模式
### Phase 3AI + 高级功能
6. 后端:AI 专用结构化接口
7. 后端:API 变更检测 + 版本管理
8. 前端:"试一试" API 测试面板
---
## 七、参考风格
| 参考站点 | 地址 | 借鉴点 |
|---------|------|--------|
| VitePress | https://vitepress.dev | 整体风格、三栏布局、深色模式 |
| Nuxt UI Pro | https://ui.nuxt.com/pro | 文档模板、导航设计 |
| Stripe API | https://stripe.com/docs/api | 代码示例、交互体验 |
| Scalar | https://scalar.com | 现代 API 文档 UI |
---
> 本文档仅涵盖设计,不涉及具体实现代码。实施前需团队审核确认。
@@ -0,0 +1,393 @@
# 门店管理新增字段设计规范
**工单**: rui/rui-frontend#6 — 门店管理适配后端新增字段
**日期**: 2026-06-08
**关联 Issue**: rui/rui-cashier#6(后端门店表新增字段)
**优先级**: P2
---
## 1. 目标(Goal
在 admin-ui 门店管理模块的列表页和表单弹窗中,适配后端门店表新增的 9 个字段(storeType、amenities、longitude、latitude、roomCount、freeRoomCount、serviceFeeRate、openingDate、legalPerson)。表单弹窗新增 7 个可编辑字段和 2 个只读展示字段,列表页新增 3 列(门店类型标签、包间信息、设施标签)和 1 个筛选条件(门店类型下拉)。所有变更在现有 `useApiForm` + `ApiFormDialog` + `RuiTable` 框架内完成,不引入新依赖。
## 2. 非目标(Non-Goals
明确不在本期范围内的事项:
- **不修改后端代码或 API 接口定义**:后端已提供字段,前端仅适配展示和交互。
- **不做地图组件集成**:经纬度字段使用纯数字输入框,不嵌入高德/百度地图选点组件。
- **不修改 storeService.ts**:现有 BaseService 的 CRUD 方法已覆盖需求,无需扩展。
- **不新增独立详情页**:沿用当前「列表 + 表单弹窗」模式,只读字段在编辑弹窗中展示。
- **不做包间数量编辑**roomCount 和 freeRoomCount 由后端计算,前端仅只读展示。
- **不引入新 npm 依赖**:所有 UI 组件使用现有 Element Plus + useApiForm 字段类型体系。
## 3. 背景与上下文(Context
### 3.1 现有门店模块结构
- **列表页** `admin-ui/src/views/cashier/store/Index.vue`:使用 `RuiTable` 组件,现有 7 列(门店名称、门店编码、联系人、联系电话、地址、营业时间、状态),2 个筛选条件(门店名称、状态),支持增删改查和状态切换。
- **表单弹窗** `admin-ui/src/views/cashier/store/StoreFormDialog.vue`:使用 `useApiForm` composable + `ApiFormDialog` 组件,现有 7 个字段(storeName、storeCode、address、contactName、contactPhone、businessHours、status),通过 `v-model:visible` / `row` prop 控制显示和数据回填。
- **服务层** `admin-ui/src/service/cashier/storeService.ts`:继承 `BaseService('/cashier/admin/store')`,13 行代码,自动拥有 page/add/update/remove/changeStatus 能力。
### 3.2 useApiForm 字段类型支持
`useApiForm` 支持 9 种字段类型:`input``textarea``select``radio``checkbox``number``tree-select``date``datetime`。每个字段支持 `disabled` 属性(布尔值或返回布尔值的函数),可用于按编辑/新增模式动态禁用。
`ApiFormDialog` 在所有标准字段之后渲染 `<slot name="custom-fields" :form="formData" />`,用于放置无法用标准字段类型表达的 UI 内容。
### 3.3 后端新增字段一览
| 字段 | camelCase 键 | Java 类型 | 后端存储格式 |
|------|-------------|----------|-------------|
| 门店类型 | storeType | String | 纯字符串:`FLAGSHIP` / `STANDARD` / `COMMUNITY` |
| 设施标签 | amenities | String | JSON 数组字符串:`"[\"免费停车\",\"免费WiFi\"]"` |
| 经度 | longitude | BigDecimal | 小数 |
| 纬度 | latitude | BigDecimal | 小数 |
| 包间总数 | roomCount | Integer | 整数(后端计算) |
| 空闲包间数 | freeRoomCount | Integer | 整数(后端计算) |
| 平台服务费率 | serviceFeeRate | BigDecimal | 小数,如 `0.05` 表示 5% |
| 开业日期 | openingDate | LocalDate | `yyyy-MM-dd` 字符串 |
| 法人姓名 | legalPerson | String | 纯文本 |
## 4. 关键设计决策
| # | 决策项 | 选定方案 | 理由 |
|---|--------|---------|------|
| 1 | 整体方案 | 在现有 useApiForm + ApiFormDialog 框架内扩展 | 复用现有基础设施,保持与其他模块一致的开发模式 |
| 2 | amenities 表现层 | 使用 `checkbox` 字段类型,options 固定 6 项 | useApiForm 原生支持 checkbox,无需自定义渲染 |
| 3 | amenities 数据转换 | 提交时 `JSON.stringify`,编辑回填时 `JSON.parse` | 后端存 JSON 字符串,前端表单使用 `string[]` |
| 4 | serviceFeeRate 展示 | 前端用百分比数值(5 表示 5%),提交时除以 100,编辑回填时乘以 100 | 用户体验直观,避免手动输入 0.05 这样的小数 |
| 5 | roomCount / freeRoomCount | 使用 `#custom-fields` 插槽渲染只读文本 | 这两个字段仅编辑模式下只读展示,不需要 useApiForm 字段配置 |
| 6 | 门店类型枚举 | 前端硬编码 3 个选项 | 后端接口稳定,选项固定,无需动态加载 |
| 7 | 经纬度输入 | `number` 类型字段,精度限制 6 位小数 | 经纬度通常保留 6 位即可满足定位需求 |
## 5. 字段配置详情
### 5.1 useApiForm 字段新增(7 个可编辑字段)
以下字段追加到 `StoreFormDialog.vue``useApiForm``fields` 数组:
```ts
{
key: 'storeType',
label: '门店类型',
type: 'select',
required: true,
options: [
{ label: '旗舰店', value: 'FLAGSHIP' },
{ label: '标准店', value: 'STANDARD' },
{ label: '社区店', value: 'COMMUNITY' },
],
},
{
key: 'amenities',
label: '设施标签',
type: 'checkbox',
options: [
{ label: '免费停车', value: '免费停车' },
{ label: '免费WiFi', value: '免费WiFi' },
{ label: '充电桩', value: '充电桩' },
{ label: '24小时营业', value: '24小时营业' },
{ label: '包厢', value: '包厢' },
{ label: '吸烟区', value: '吸烟区' },
],
},
{
key: 'longitude',
label: '经度',
type: 'number',
props: { min: -180, max: 180, precision: 6, step: 0.000001 },
},
{
key: 'latitude',
label: '纬度',
type: 'number',
props: { min: -90, max: 90, precision: 6, step: 0.000001 },
},
{
key: 'serviceFeeRate',
label: '平台服务费率(%)',
type: 'number',
props: { min: 0, max: 100, precision: 2, step: 0.01 },
},
{
key: 'openingDate',
label: '开业日期',
type: 'date',
props: { valueFormat: 'YYYY-MM-DD' },
},
{
key: 'legalPerson',
label: '法人姓名',
type: 'input',
},
```
### 5.2 弹窗宽度调整
`StoreFormDialog``ApiFormDialog` 追加 `width` prop 为 `720px`(原默认 600px),因为新增字段较多,需要更宽的弹窗空间。
### 5.3 只读展示字段(2 个,通过 custom-fields 插槽)
`roomCount``freeRoomCount` 不加入 `fields` 数组,而是在 `ApiFormDialog``#custom-fields` 插槽中以 `el-form-item` + 纯文本方式渲染。仅在编辑模式(`form.id` 存在)时显示:
```vue
<template #custom-fields="{ form }">
<template v-if="form.id">
<el-form-item label="包间总数">
<span>{{ form.roomCount ?? '-' }}</span>
</el-form-item>
<el-form-item label="空闲包间数">
<span>{{ form.freeRoomCount ?? '-' }}</span>
</el-form-item>
</template>
</template>
```
### 5.4 initial 默认值扩展
`useApiForm``initial` 对象中追加新字段默认值:
```ts
initial: {
// ...现有字段
storeType: 'STANDARD',
amenities: [],
longitude: undefined,
latitude: undefined,
serviceFeeRate: undefined,
openingDate: '',
legalPerson: '',
},
```
### 5.5 数据转换逻辑
#### 提交时(`onSubmit` 回调内)
```ts
onSubmit: async (rawData) => {
const data = { ...rawData }
// amenities: string[] → JSON 字符串
if (Array.isArray(data.amenities)) {
data.amenities = JSON.stringify(data.amenities)
}
// serviceFeeRate: 百分比 → 小数
if (data.serviceFeeRate != null && data.serviceFeeRate !== '') {
data.serviceFeeRate = Number(data.serviceFeeRate) / 100
}
// ...现有 add/update 逻辑
},
```
#### 编辑回填时(`watch(visible)` 内)
```ts
watch(() => props.visible, (val) => {
if (val && props.row) {
const rowData = { ...props.row }
// amenities: JSON 字符串 → string[]
if (typeof rowData.amenities === 'string' && rowData.amenities) {
try {
rowData.amenities = JSON.parse(rowData.amenities)
} catch {
rowData.amenities = []
}
} else if (!Array.isArray(rowData.amenities)) {
rowData.amenities = []
}
// serviceFeeRate: 小数 → 百分比
if (rowData.serviceFeeRate != null) {
rowData.serviceFeeRate = Number(rowData.serviceFeeRate) * 100
}
form.value = rowData
}
})
```
## 6. 列表页变更(Index.vue
### 6.1 新增列配置
`columns` 数组中,`address` 列之后追加以下 3 列:
```ts
{
prop: 'storeType',
label: '门店类型',
width: 100,
align: 'center',
slot: true,
},
{
prop: 'roomInfo',
label: '包间',
width: 120,
align: 'center',
slot: true,
},
{
prop: 'amenities',
label: '设施标签',
minWidth: 200,
slot: true,
},
```
### 6.2 新增列 Slot 模板
#### 门店类型列(彩色 Tag
```vue
<template #column-storeType="{ row }">
<el-tag
:type="row.storeType === 'FLAGSHIP' ? 'danger' : row.storeType === 'STANDARD' ? '' : 'info'"
size="small"
>
{{ { FLAGSHIP: '旗舰店', STANDARD: '标准店', COMMUNITY: '社区店' }[row.storeType] || '-' }}
</el-tag>
</template>
```
#### 包间信息列("空闲X/总数Y" 格式)
```vue
<template #column-roomInfo="{ row }">
<span>{{ row.freeRoomCount ?? '-' }}/{{ row.roomCount ?? '-' }}</span>
</template>
```
#### 设施标签列(多个小 Tag)
```vue
<template #column-amenities="{ row }">
<template v-if="parseAmenities(row.amenities).length">
<el-tag
v-for="tag in parseAmenities(row.amenities)"
:key="tag"
size="small"
type="info"
class="mr-1 mb-1"
>
{{ tag }}
</el-tag>
</template>
<span v-else>-</span>
</template>
```
其中 `parseAmenities` 为组件内的工具函数:
```ts
function parseAmenities(val: any): string[] {
if (Array.isArray(val)) return val
if (typeof val === 'string' && val) {
try { return JSON.parse(val) } catch { return [] }
}
return []
}
```
### 6.3 新增筛选条件
`queryParams` 中增加 `storeType` 字段,在查询区增加门店类型下拉:
```ts
const queryParams = ref({
storeName: '',
status: undefined as number | undefined,
storeType: undefined as string | undefined,
})
```
查询区模板中,状态筛选后追加:
```vue
<el-form-item label="门店类型">
<el-select v-model="queryParams.storeType" placeholder="请选择门店类型" clearable>
<el-option label="旗舰店" value="FLAGSHIP" />
<el-option label="标准店" value="STANDARD" />
<el-option label="社区店" value="COMMUNITY" />
</el-select>
</el-form-item>
```
`handleReset` 方法中补充 `storeType: undefined` 重置。
## 7. 涉及文件清单(Files To Change
| # | 文件 | 操作 | 变更说明 |
|---|------|------|---------|
| 1 | `admin-ui/src/views/cashier/store/StoreFormDialog.vue` | 修改 | 新增 7 个 useApiForm 字段、custom-fields 插槽渲染 roomCount/freeRoomCount、数据转换逻辑、弹窗宽度调整为 720px |
| 2 | `admin-ui/src/views/cashier/store/Index.vue` | 修改 | 新增 3 列配置(storeType/roomInfo/amenities)及对应 slot 模板、新增 storeType 筛选条件、queryParams 扩展、parseAmenities 工具函数、handleReset 补充 |
**变更统计**:修改 2 个文件。**不新建文件,不修改服务层和路由**。
## 8. 测试策略
### 8.1 静态检查
```bash
pnpm --filter admin-ui type-check # 0 errors
pnpm --filter admin-ui lint # 0 errors
```
### 8.2 功能验证(手动)
| # | 验证场景 | 预期结果 |
|---|---------|---------|
| 1 | 列表加载 | 新增 3 列正确展示:门店类型(Tag)、包间(X/Y 格式)、设施标签(多个小 Tag) |
| 2 | 门店类型筛选 | 下拉选择后点击查询,列表按类型过滤;重置后筛选条件清空 |
| 3 | 无数据的门店 | storeType 为空显示 `-`amenities 为空显示 `-`,包间无数据显示 `-/--` |
| 4 | 新增门店 | 弹窗宽度 720px,7 个新字段正确渲染,roomCount/freeRoomCount 不显示(新增模式) |
| 5 | 新增提交 | amenities 提交为 JSON 字符串,serviceFeeRate 提交为小数(5→0.05 |
| 6 | 编辑门店 | 弹窗回填所有字段:amenities 从 JSON 字符串解析为勾选状态,serviceFeeRate 从小数转为百分比(0.05→5) |
| 7 | 编辑模式只读字段 | roomCount 和 freeRoomCount 以纯文本显示,不可编辑 |
| 8 | 新增模式无只读字段 | 新增门店时 roomCount 和 freeRoomCount 区域不显示 |
| 9 | 门店类型必填校验 | storeType 为空时提交,表单校验不通过 |
### 8.3 验证清单(提交前必过)
- [ ] `pnpm --filter admin-ui type-check` 通过
- [ ] `pnpm --filter admin-ui lint` 通过
- [ ] 列表新增 3 列正确展示
- [ ] 门店类型筛选功能正常
- [ ] 新增门店:7 个新字段可正常填写和提交
- [ ] 编辑门店:所有新字段正确回填,只读字段不可编辑
- [ ] amenities 数据双向转换正确(JSON 字符串 ↔ 数组)
- [ ] serviceFeeRate 数据双向转换正确(小数 ↔ 百分比)
## 9. 风险与缓解(Risks And Mitigations
| # | 风险 | 缓解措施 |
|---|------|---------|
| 1 | **amenities JSON 解析失败**:后端返回格式异常或 null 时,前端 JSON.parse 抛错导致表单崩溃 | 在编辑回填时用 try-catch 包裹 JSON.parse,解析失败降级为空数组 `[]`。在列表 parseAmenities 工具函数中同样做防御性解析。 |
| 2 | **serviceFeeRate 精度问题**BigDecimal 除以 100 或乘以 100 可能产生浮点精度误差 | 使用 `Number()` 转换后通过 `el-input-number``precision: 2` 限制小数位数,避免显示多余精度。后端使用 BigDecimal 不会丢失精度。 |
| 3 | **后端字段尚未部署**:前端先于后端部署时,新增列显示空值 | 列表和表单均对 undefined/null 做降级处理(显示 `-`),不影响现有功能。前端部署无阻断风险。 |
| 4 | **弹窗字段过多导致布局拥挤**:原有 7 个字段 + 新增 9 个字段共 16 个表单项 | 弹窗宽度从 600px 增至 720px;新增模式不显示 roomCount/freeRoomCount,实际展示 14 项。后续如仍拥挤可考虑分组或 tabs 布局。 |
## 10. 决策摘要(Decision Summary
- **架构**:在现有 `useApiForm` + `ApiFormDialog` + `RuiTable` 框架内扩展,不新建文件、不引入新依赖。
- **amenities**:使用 `checkbox` 字段类型,options 固定 6 项。提交时 `JSON.stringify`,编辑回填时 `JSON.parse`,均做防御性处理。
- **serviceFeeRate**:前端以百分比输入/显示,提交时 `÷100`,回填时 `×100`
- **roomCount / freeRoomCount**:通过 `#custom-fields` 插槽只读展示,仅在编辑模式显示。
- **storeType**`select` 字段,3 个固定选项(FLAGSHIP/STANDARD/COMMUNITY),列表用彩色 Tag 展示,增加筛选条件。
- **longitude / latitude**`number` 字段,精度 6 位小数,范围限制经度 [-180, 180]、纬度 [-90, 90]。
- **openingDate**`date` 字段,valueFormat 为 `YYYY-MM-DD`
- **legalPerson**`input` 字段,无特殊处理。
- **列表新增**:3 列(门店类型 Tag、包间 X/Y、设施多 Tag)+ 1 个筛选条件。
- **弹窗宽度**:从默认 600px 增至 720px。
- **文件变更**:仅修改 2 个文件(StoreFormDialog.vue、Index.vue)。
---
**设计评审状态**: 待评审
**下一步**: 用户评审通过后,编写实施计划(Plan)
+24
View File
@@ -0,0 +1,24 @@
# Specs 索引
本目录存放已通过评审的设计规范(Spec)。每份 Spec 对应一个工单/需求,配套 Plan 在 `../plans/` 目录。
## 列表
| 日期 | 标题 | 工单 | 状态 |
|------|------|------|------|
| 2026-06-06 | [API 协作工作流设计](2026-06-06-api-collaboration-design.md) | 内部规范 | 已评审 |
| 2026-06-07 | [用户管理接口适配设计](2026-06-07-user-management-api-adaptation-design.md) | rui/rui-frontend#2 | 已评审 |
| 2026-06-07 | [SysApp 应用集成管理设计](2026-06-07-sysapp-management-design.md) | rui/rui-frontend#4 | 待评审 |
## 命名规范
- 文件名:`YYYY-MM-DD-<topic>-design.md`
- 目录:`docs/superpowers/specs/`
- 配套 Plan`docs/superpowers/plans/YYYY-MM-DD-<topic>-plan.md`
## 状态
- **待评审**:Spec 刚写完,等待用户/团队评审
- **已评审**:用户已批准,可进入 Plan 阶段
- **已实施**:对应 Plan 已执行完成
- **已归档**:功能上线,文档归档