Compare commits
88 Commits
0666c3ec7b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fdd93fc4b | |||
| a79be07c52 | |||
| 7b2f3d77ca | |||
| 1324a52049 | |||
| d4a3bd5847 | |||
| 8b2de75b5f | |||
| c7244b3b87 | |||
| a7f3ee3565 | |||
| 12a263c451 | |||
| 2253ebea92 | |||
| 76260b9458 | |||
| 3d16902489 | |||
| fdc74784c2 | |||
| 8fed3a04be | |||
| a5863706dc | |||
| d07a3f7f4b | |||
| 2152d0de42 | |||
| cd2d68e60e | |||
| f4761ae145 | |||
| bb71263bdd | |||
| b9d5b6d9f0 | |||
| 24a8643fb6 | |||
| 20d4a545b4 | |||
| 9957d85595 | |||
| f26c67368c | |||
| 3aebe0b5a5 | |||
| 5f19332f06 | |||
| a10b712919 | |||
| 66f0712486 | |||
| b492c6224a | |||
| 78e5ebc17e | |||
| 2bfd1c0f4f | |||
| 4271f333b6 | |||
| 4540af71ae | |||
| eba4f07832 | |||
| 3395f69b42 | |||
| 22889afedc | |||
| c576053ab6 | |||
| 33690fe80b | |||
| ae78e0f673 | |||
| 856e16beff | |||
| 6df6e7ad0c | |||
| 365aa49cbd | |||
| 792b10bd34 | |||
| 2249b3649a | |||
| b3c66245e9 | |||
| eb99ae43cb | |||
| c69c34ff25 | |||
| 938302c164 | |||
| f1f4440be2 | |||
| 47ef4c6938 | |||
| 00c77529c5 | |||
| 23823074f6 | |||
| 84b3bb601e | |||
| 3c618b57bb | |||
| 9d0cffa86e | |||
| a4767ee3d0 | |||
| 3c2fa877a6 | |||
| de78c21799 | |||
| a8c164459a | |||
| 47d8af24f0 | |||
| 1a615c9f15 | |||
| 26f146f9a7 | |||
| cfd2c44b80 | |||
| 33351d3261 | |||
| 47945b245d | |||
| 9f084720f8 | |||
| 044f9e31f3 | |||
| 5931d65806 | |||
| 57cc87a145 | |||
| 386751b045 | |||
| ee335fe90a | |||
| 2277bca1ad | |||
| 726fc229d6 | |||
| 1d6cb64ed3 | |||
| 61fa206a45 | |||
| 8506424c26 | |||
| 01f36acdd7 | |||
| cc46e13503 | |||
| 21cea34d18 | |||
| f234845e17 | |||
| 6e0919f1cd | |||
| 8896b9aa58 | |||
| 32c7db7e49 | |||
| cab0cf91c0 | |||
| 19de7e24ec | |||
| 2e38c53434 | |||
| 87b7780dd6 |
@@ -38,7 +38,7 @@ rui-docs/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 添加 submodule
|
# 添加 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
|
git submodule update --remote
|
||||||
@@ -50,7 +50,7 @@ git submodule update --init --recursive
|
|||||||
### 独立查看
|
### 独立查看
|
||||||
|
|
||||||
```bash
|
```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
|
cd rui-docs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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` 获取更新
|
||||||
@@ -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`)
|
||||||
@@ -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 组织下
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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 | 低 | 排期处理 |
|
||||||
@@ -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 仓库即可
|
||||||
@@ -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 1:application.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 正常情况下**不修改**该文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 正反对照示例
|
||||||
|
|
||||||
|
### 示例 1:application.yml
|
||||||
|
|
||||||
|
**正确**(仅修改允许位置):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 9302 # ✅ 【允许修改】按模块规划填写
|
||||||
|
shutdown: graceful
|
||||||
|
# ... 其余与模板完全一致 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 9302
|
||||||
|
|
||||||
|
# ❌ 错误:在 application.yml 中添加业务配置
|
||||||
|
rui:
|
||||||
|
modules:
|
||||||
|
available:
|
||||||
|
- code: demo
|
||||||
|
name: 演示中心
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2:Nacos 服务配置
|
||||||
|
|
||||||
|
**正确**(仅端口 + 业务配置):
|
||||||
|
|
||||||
|
```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/本地配置的一致性和可维护性,请务必遵守!
|
||||||
@@ -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/ # 前端文档
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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='对账差异表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
@@ -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%)
|
||||||
@@ -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
@@ -11,7 +11,7 @@
|
|||||||
│ ├── cashier-mobile/
|
│ ├── cashier-mobile/
|
||||||
│ └── customer-mobile/
|
│ └── customer-mobile/
|
||||||
│
|
│
|
||||||
├── spring-ai/ ← 后端框架 AI 工作目录
|
├── rui-framework/ ← 后端框架 AI 工作目录
|
||||||
│ ├── backend/
|
│ ├── backend/
|
||||||
│ │ ├── rui-common/
|
│ │ ├── rui-common/
|
||||||
│ │ ├── rui-gateway/
|
│ │ ├── rui-gateway/
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
│ │ └── rui-service/
|
│ │ └── rui-service/
|
||||||
│ └── docs/
|
│ └── docs/
|
||||||
│
|
│
|
||||||
├── spring-ai/app/
|
├── rui-framework/app/
|
||||||
│ ├── rui-cashier/ ← 收银 AI 工作目录
|
│ ├── rui-cashier/ ← 收银 AI 工作目录
|
||||||
│ └── rui-payment/ ← 支付 AI 工作目录
|
│ └── rui-payment/ ← 支付 AI 工作目录
|
||||||
│
|
│
|
||||||
@@ -70,12 +70,12 @@ cd ~/rhkj/rui-frontend
|
|||||||
|
|
||||||
### 后端框架 AI
|
### 后端框架 AI
|
||||||
|
|
||||||
**工作目录:** `~/rhkj/spring-ai`
|
**工作目录:** `~/rhkj/rui-framework`
|
||||||
|
|
||||||
**启动步骤:**
|
**启动步骤:**
|
||||||
```bash
|
```bash
|
||||||
# 1. 进入后端目录
|
# 1. 进入后端目录
|
||||||
cd ~/rhkj/spring-ai
|
cd ~/rhkj/rui-framework
|
||||||
|
|
||||||
# 2. 打开 OpenCode
|
# 2. 打开 OpenCode
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ cd ~/rhkj/spring-ai
|
|||||||
|
|
||||||
**AI 指令:**
|
**AI 指令:**
|
||||||
```
|
```
|
||||||
你是 Java 后端开发专家。当前项目是 spring-ai(睿核科技后端框架)。
|
你是 Java 后端开发专家。当前项目是 rui-framework(睿核科技后端框架)。
|
||||||
|
|
||||||
技术栈:Spring Boot 3.x + Spring Cloud + JDK 21 + Maven + MyBatis Plus
|
技术栈:Spring Boot 3.x + Spring Cloud + JDK 21 + Maven + MyBatis Plus
|
||||||
|
|
||||||
@@ -108,12 +108,12 @@ cd ~/rhkj/spring-ai
|
|||||||
|
|
||||||
### 收银模块 AI
|
### 收银模块 AI
|
||||||
|
|
||||||
**工作目录:** `~/rhkj/spring-ai/app/rui-cashier`
|
**工作目录:** `~/rhkj/rui-framework/app/rui-cashier`
|
||||||
|
|
||||||
**启动步骤:**
|
**启动步骤:**
|
||||||
```bash
|
```bash
|
||||||
# 1. 进入收银模块目录
|
# 1. 进入收银模块目录
|
||||||
cd ~/rhkj/spring-ai/app/rui-cashier
|
cd ~/rhkj/rui-framework/app/rui-cashier
|
||||||
|
|
||||||
# 2. 打开 OpenCode
|
# 2. 打开 OpenCode
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ cd ~/rhkj/spring-ai/app/rui-cashier
|
|||||||
|
|
||||||
约束条件:
|
约束条件:
|
||||||
- 禁止修改前端代码
|
- 禁止修改前端代码
|
||||||
- 禁止修改框架代码(在 spring-ai 仓库)
|
- 禁止修改框架代码(在 rui-framework 仓库)
|
||||||
- 禁止直接引用支付模块代码
|
- 禁止直接引用支付模块代码
|
||||||
- 通过 FeignClient 调用框架服务
|
- 通过 FeignClient 调用框架服务
|
||||||
- 通过 REST API 暴露接口供前端调用
|
- 通过 REST API 暴露接口供前端调用
|
||||||
@@ -148,12 +148,12 @@ cd ~/rhkj/spring-ai/app/rui-cashier
|
|||||||
|
|
||||||
### 支付模块 AI
|
### 支付模块 AI
|
||||||
|
|
||||||
**工作目录:** `~/rhkj/spring-ai/app/rui-payment`
|
**工作目录:** `~/rhkj/rui-framework/app/rui-payment`
|
||||||
|
|
||||||
**启动步骤:**
|
**启动步骤:**
|
||||||
```bash
|
```bash
|
||||||
# 1. 进入支付模块目录
|
# 1. 进入支付模块目录
|
||||||
cd ~/rhkj/spring-ai/app/rui-payment
|
cd ~/rhkj/rui-framework/app/rui-payment
|
||||||
|
|
||||||
# 2. 打开 OpenCode
|
# 2. 打开 OpenCode
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ cd ~/rhkj/spring-ai/app/rui-payment
|
|||||||
|
|
||||||
约束条件:
|
约束条件:
|
||||||
- 禁止修改前端代码
|
- 禁止修改前端代码
|
||||||
- 禁止修改框架代码(在 spring-ai 仓库)
|
- 禁止修改框架代码(在 rui-framework 仓库)
|
||||||
- 禁止直接引用收银模块代码
|
- 禁止直接引用收银模块代码
|
||||||
- 通过 FeignClient 调用框架服务
|
- 通过 FeignClient 调用框架服务
|
||||||
- 通过 REST API 暴露接口供收银模块调用
|
- 通过 REST API 暴露接口供收银模块调用
|
||||||
@@ -204,7 +204,7 @@ git commit -m "feat: 功能描述
|
|||||||
- 详细修改点2"
|
- 详细修改点2"
|
||||||
|
|
||||||
# 推送到 Gitea
|
# 推送到 Gitea
|
||||||
git push gitea main
|
git push origin main
|
||||||
|
|
||||||
# 查看推送结果(钉钉会收到通知)
|
# 查看推送结果(钉钉会收到通知)
|
||||||
```
|
```
|
||||||
@@ -213,7 +213,7 @@ git push gitea main
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看 Actions 运行状态
|
# 查看 Actions 运行状态
|
||||||
# 访问:https://git.dev.vifo.cc/rui/{仓库名}/actions
|
# 访问:https://git.vifo.cc/rui/{仓库名}/actions
|
||||||
```
|
```
|
||||||
|
|
||||||
### 模块间通信示例
|
### 模块间通信示例
|
||||||
@@ -271,9 +271,9 @@ const cashierApi = {
|
|||||||
| 文件 | 位置 | 说明 |
|
| 文件 | 位置 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| AI 边界配置 | `AGENTS.md` | 每个仓库根目录 |
|
| AI 边界配置 | `AGENTS.md` | 每个仓库根目录 |
|
||||||
| 通信规范 | `spring-ai/docs/module-communication.md` | 模块间通信规范 |
|
| 通信规范 | `rui-framework/docs/module-communication.md` | 模块间通信规范 |
|
||||||
| CI 配置 | `.gitea/workflows/*.yml` | 自动化构建 |
|
| 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
|
git remote -v
|
||||||
|
|
||||||
# 确认是 Gitea 地址
|
# 确认是 Gitea 地址
|
||||||
gitea ssh://git@git.dev.vifo.cc:222/rui/xxx.git
|
gitea ssh://git@git.vifo.cc:222/rui/xxx.git
|
||||||
|
|
||||||
# 如果失败,检查 SSH 密钥
|
# 如果失败,检查 SSH 密钥
|
||||||
ssh -p 222 git@git.dev.vifo.cc
|
ssh -p 222 git@git.vifo.cc
|
||||||
```
|
```
|
||||||
|
|
||||||
### CI 构建失败
|
### CI 构建失败
|
||||||
```bash
|
```bash
|
||||||
# 查看构建日志
|
# 查看构建日志
|
||||||
# 访问:https://git.dev.vifo.cc/rui/{仓库}/actions
|
# 访问:https://git.vifo.cc/rui/{仓库}/actions
|
||||||
```
|
```
|
||||||
|
|
||||||
### 钉钉没收到通知
|
### 钉钉没收到通知
|
||||||
```bash
|
```bash
|
||||||
# 检查 Webhook 配置
|
# 检查 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 框架仓库?
|
||||||
|
|
||||||
|
AI(OpenCode)基于 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"})
|
||||||
|
AI:AuthUtil 提供 getUserId()、getTenantId()、getUser()...
|
||||||
|
|
||||||
|
5. 员工:退款金额用 BigDecimal 吗?
|
||||||
|
【AI执行】gitnexus_query({query:"金额计算 BigDecimal", repo:"rui-framework"})
|
||||||
|
AI:框架规范要求金额使用 DECIMAL(19,4),Java 对应 BigDecimal...
|
||||||
|
|
||||||
|
6. AI 生成完整代码
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、常见问题
|
||||||
|
|
||||||
|
### Q1:AI 提示 "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` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **提示**:本手册随项目演进持续更新。如有问题,联系框架维护者或技术负责人。
|
||||||
@@ -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='用户表';
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:所有业务表必须包含以上公共字段。
|
||||||
@@ -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 + Resilience4j(ThreadPoolBulkhead)+ TransmittableThreadLocal(TTL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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) {
|
||||||
|
// ① 先把任务提交到工厂自己的 ExecutorService(newCachedThreadPool)
|
||||||
|
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 客户端 |
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进入后端目录
|
# 进入后端目录
|
||||||
cd ~/rhkj/spring-ai/backend
|
cd ~/rhkj/rui-framework/backend
|
||||||
|
|
||||||
# Maven 打包
|
# Maven 打包
|
||||||
mvn clean package -DskipTests
|
mvn clean package -DskipTests
|
||||||
|
|||||||
@@ -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 节启用
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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 窗口同时工作
|
||||||
|
- 窗口 1:rui-framework 目录,开发后端
|
||||||
|
- 窗口 2:rui-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 DTO(dto)
|
||||||
|
|
||||||
|
| 类名 | 功能 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `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 JsonUtil(JSON 处理)
|
||||||
|
|
||||||
|
```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 SpringUtil(Spring 上下文)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 获取 Bean
|
||||||
|
UserService service = SpringUtil.getBean(UserService.class);
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
String value = SpringUtil.getProperty("server.port");
|
||||||
|
|
||||||
|
// 判断环境
|
||||||
|
boolean isDev = SpringUtil.isDev();
|
||||||
|
boolean isProd = SpringUtil.isProd();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 BeanUtil(Bean 拷贝)
|
||||||
|
|
||||||
|
```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;补充文档使用说明 |
|
||||||
@@ -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 等通知
|
||||||
|
- ✅ **权限管理**:组织、团队、仓库级权限控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、部署方案
|
||||||
|
|
||||||
|
### 方案 A:Docker 部署(推荐)
|
||||||
|
|
||||||
|
适合:有 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
|
||||||
|
|
||||||
|
# 或者使用 snap(Ubuntu/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/CD(Gitea 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)。
|
||||||
@@ -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~600MB(JVM 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 |
|
||||||
@@ -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) 查找
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**MqPublisher(Provider 接口)**
|
||||||
|
|
||||||
|
```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, REDIS(MQTT 暂不需要) |
|
||||||
|
| Action 枚举 | `Actions`(80+ 项) | `MqAction`(精简 9 项) |
|
||||||
|
| 包名 | `org.rui.common.mq` | `com.rui.common.mq` |
|
||||||
@@ -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 中写完整表名。
|
||||||
@@ -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> | 高/中/低 | <措施> |
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# <模块/功能名称> 实施计划
|
||||||
|
|
||||||
|
> **计划日期**: YYYY-MM-DD
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **状态**: 待执行/执行中/已完成
|
||||||
|
> **关联设计**: <链接到设计文档>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、任务清单
|
||||||
|
|
||||||
|
### Phase 1: <阶段名称>
|
||||||
|
|
||||||
|
| 序号 | 任务 | 负责人 | 预估工时 | 状态 | 验证方式 |
|
||||||
|
|------|------|--------|---------|------|---------|
|
||||||
|
| 1.1 | <具体任务> | <负责人> | <工时> | ⬜ | <如何验证> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、进度跟踪
|
||||||
|
|
||||||
|
| 日期 | 完成任务 | 遇到的问题 | 解决方案 |
|
||||||
|
|------|---------|-----------|---------|
|
||||||
|
| YYYY-MM-DD | <任务> | <问题> | <方案> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、完成总结
|
||||||
|
|
||||||
|
- **实际工时**: <X 小时>
|
||||||
|
- **偏差分析**: <与预估的差异及原因>
|
||||||
|
- **经验教训**: <可复用的经验>
|
||||||
@@ -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_ 开头)
|
||||||
@@ -218,7 +218,7 @@ git push -u origin master
|
|||||||
### 与框架主仓库的关系
|
### 与框架主仓库的关系
|
||||||
|
|
||||||
```
|
```
|
||||||
spring-ai/ # 框架主仓库(git)
|
rui-framework/ # 框架主仓库(git)
|
||||||
├── backend/ # 提交到框架仓库
|
├── backend/ # 提交到框架仓库
|
||||||
├── docs/ # 提交到框架仓库
|
├── docs/ # 提交到框架仓库
|
||||||
├── app/ # 不提交到框架仓库(.gitignore)
|
├── app/ # 不提交到框架仓库(.gitignore)
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
## 🔗 通信方式
|
## 🔗 通信方式
|
||||||
|
|
||||||
### 1. 模块 → 框架(spring-ai)
|
### 1. 模块 → 框架(rui-framework)
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// 使用 FeignClient 调用框架服务
|
// 使用 FeignClient 调用框架服务
|
||||||
|
|||||||
+10
-10
@@ -11,7 +11,7 @@
|
|||||||
### 1.1 问题背景
|
### 1.1 问题背景
|
||||||
|
|
||||||
rui 项目采用多仓结构:
|
rui 项目采用多仓结构:
|
||||||
- **后端仓库**:`spring-ai`(backend + app/*)
|
- **后端仓库**:`rui-framework`(backend + app/*)
|
||||||
- **前端仓库**:`rui-frontend`(admin-ui + cashier-mobile + customer-mobile)
|
- **前端仓库**:`rui-frontend`(admin-ui + cashier-mobile + customer-mobile)
|
||||||
|
|
||||||
在使用 OpenCode AI 辅助开发时存在以下痛点:
|
在使用 OpenCode AI 辅助开发时存在以下痛点:
|
||||||
@@ -35,8 +35,8 @@ rui 项目采用多仓结构:
|
|||||||
### 2.1 仓库结构
|
### 2.1 仓库结构
|
||||||
|
|
||||||
```
|
```
|
||||||
# 后端仓库:spring-ai
|
# 后端仓库:rui-framework
|
||||||
spring-ai/
|
rui-framework/
|
||||||
├── backend/ # 基础框架
|
├── backend/ # 基础框架
|
||||||
├── app/ # 应用模块(支付、收银等)
|
├── app/ # 应用模块(支付、收银等)
|
||||||
├── docs/ # 后端文档
|
├── docs/ # 后端文档
|
||||||
@@ -54,8 +54,8 @@ rui-frontend/
|
|||||||
|
|
||||||
| 角色 | 所属仓库 | 负责目录 | 可修改范围 | 只读范围 | 会话配置 |
|
| 角色 | 所属仓库 | 负责目录 | 可修改范围 | 只读范围 | 会话配置 |
|
||||||
|------|---------|---------|-----------|---------|---------|
|
|------|---------|---------|-----------|---------|---------|
|
||||||
| **框架开发** | spring-ai | `backend/` | `backend/**` | `app/**` | `framework.json` |
|
| **框架开发** | rui-framework | `backend/` | `backend/**` | `app/**` | `framework.json` |
|
||||||
| **应用开发** | spring-ai | `app/{模块}/` | `app/{模块}/**` | `backend/**` | `app-module.json` |
|
| **应用开发** | rui-framework | `app/{模块}/` | `app/{模块}/**` | `backend/**` | `app-module.json` |
|
||||||
| **前端开发** | rui-frontend | `admin-ui/` | `admin-ui/**` | `cashier-mobile/`, `customer-mobile/` | `admin-ui.json` |
|
| **前端开发** | rui-frontend | `admin-ui/` | `admin-ui/**` | `cashier-mobile/`, `customer-mobile/` | `admin-ui.json` |
|
||||||
|
|
||||||
### 2.2 职责矩阵
|
### 2.2 职责矩阵
|
||||||
@@ -80,13 +80,13 @@ rui-frontend/
|
|||||||
**协作流程**(跨仓库协作):
|
**协作流程**(跨仓库协作):
|
||||||
|
|
||||||
```
|
```
|
||||||
rui-frontend/ spring-ai/
|
rui-frontend/ rui-framework/
|
||||||
┌─────────────┐ ┌─────────────┐
|
┌─────────────┐ ┌─────────────┐
|
||||||
│ 前端会话 │ │ 后端 │
|
│ 前端会话 │ │ 后端 │
|
||||||
│ (admin-ui) │ │ 会话 │
|
│ (admin-ui) │ │ 会话 │
|
||||||
└──────┬──────┘ └──────┬──────┘
|
└──────┬──────┘ └──────┬──────┘
|
||||||
│ │
|
│ │
|
||||||
│ 在 spring-ai 仓库创建 │
|
│ 在 rui-framework 仓库创建 │
|
||||||
│ Issue [API-REQ] │
|
│ Issue [API-REQ] │
|
||||||
└────────────────────────→│
|
└────────────────────────→│
|
||||||
│ │
|
│ │
|
||||||
@@ -210,9 +210,9 @@ rui-frontend/ spring-ai/
|
|||||||
|
|
||||||
启动 OpenCode 时,明确指定仓库和角色:
|
启动 OpenCode 时,明确指定仓库和角色:
|
||||||
|
|
||||||
**后端仓库(spring-ai)**:
|
**后端仓库(rui-framework)**:
|
||||||
```
|
```
|
||||||
工作目录:/Users/zhangsheng/rhkj/spring-ai
|
工作目录:/Users/zhangsheng/rhkj/rui-framework
|
||||||
角色:框架开发
|
角色:框架开发
|
||||||
限制:只能修改 backend/ 下的代码
|
限制:只能修改 backend/ 下的代码
|
||||||
```
|
```
|
||||||
@@ -367,7 +367,7 @@ rui-frontend/ spring-ai/
|
|||||||
- 需要评估影响范围
|
- 需要评估影响范围
|
||||||
- 需要同步版本更新
|
- 需要同步版本更新
|
||||||
|
|
||||||
### Q2: 前端仓库(rui-frontend)和后端仓库(spring-ai)是什么关系?
|
### Q2: 前端仓库(rui-frontend)和后端仓库(rui-framework)是什么关系?
|
||||||
|
|
||||||
**答**:
|
**答**:
|
||||||
- **独立仓库**:前端和后端是完全独立的 Git 仓库
|
- **独立仓库**:前端和后端是完全独立的 Git 仓库
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# 睿核科技 (rui) — 项目实施规范
|
# 睿核科技 (rui) — 项目实施规范
|
||||||
|
|
||||||
> 本文档基于对当前 spring-ai 项目的全面分析,整理项目结构、业务流、现有规范、推荐修改项及缺少的专业结构,供后续项目实施参考。
|
> 本文档基于对当前 rui-framework 项目的全面分析,整理项目结构、业务流、现有规范、推荐修改项及缺少的专业结构,供后续项目实施参考。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
| 项目属性 | 值 |
|
| 项目属性 | 值 |
|
||||||
|---------|-----|
|
|---------|-----|
|
||||||
| **项目名称** | spring-ai |
|
| **项目名称** | rui-framework |
|
||||||
| **项目类型** | Spring Cloud 微服务架构(通用平台框架) |
|
| **项目类型** | Spring Cloud 微服务架构(通用平台框架) |
|
||||||
| **组织方式** | Monorepo(backend + 可扩展前端) |
|
| **组织方式** | Monorepo(backend + 可扩展前端) |
|
||||||
| **JDK** | 21 |
|
| **JDK** | 21 |
|
||||||
@@ -436,5 +436,5 @@
|
|||||||
|
|
||||||
> **文档版本**: v1.0
|
> **文档版本**: v1.0
|
||||||
> **创建日期**: 2026-05-28
|
> **创建日期**: 2026-05-28
|
||||||
> **适用范围**: spring-ai 项目及后续同类项目
|
> **适用范围**: rui-framework 项目及后续同类项目
|
||||||
> **维护方式**: 随项目实施迭代更新
|
> **维护方式**: 随项目实施迭代更新
|
||||||
|
|||||||
@@ -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 工作流
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**报告完成 ✅**
|
||||||
@@ -57,7 +57,7 @@ tabler:list → 列表演示
|
|||||||
|
|
||||||
**历史变更**:
|
**历史变更**:
|
||||||
- 早期使用 Element Plus 图标组件名(如 `SettingOutlined`)
|
- 早期使用 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,参数化构建不同系统
|
||||||
@@ -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` | 查看报表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*文档结束*
|
*文档结束*
|
||||||
|
|||||||
@@ -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 | ✅ | 应用 ID(AppId/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:
|
Run:
|
||||||
```bash
|
```bash
|
||||||
cd /Users/zhangsheng/rhkj/spring-ai/admin-ui
|
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
|
||||||
pnpm dev:cashier
|
pnpm dev:cashier
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1293,7 +1293,7 @@ git commit -m "feat(cashier): 完善订单管理开台功能"
|
|||||||
|
|
||||||
Install echarts if not already installed:
|
Install echarts if not already installed:
|
||||||
```bash
|
```bash
|
||||||
cd /Users/zhangsheng/rhkj/spring-ai/admin-ui
|
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
|
||||||
pnpm add echarts vue-echarts
|
pnpm add echarts vue-echarts
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1322,7 +1322,7 @@ git commit -m "feat(cashier): 优化营业报表图表展示"
|
|||||||
|
|
||||||
Run:
|
Run:
|
||||||
```bash
|
```bash
|
||||||
cd /Users/zhangsheng/rhkj/spring-ai/admin-ui
|
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
|
||||||
pnpm build:cashier
|
pnpm build:cashier
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -116,10 +116,12 @@ GET /v1/user/users?username=admin&status=1&createdAt_start=2024-01-01&createdAt_
|
|||||||
|
|
||||||
### 4.1 统一响应格式
|
### 4.1 统一响应格式
|
||||||
|
|
||||||
|
> 详细规范见 [Result 统一响应类](Result统一响应类.md)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 200,
|
"error": 0,
|
||||||
"msg": "操作成功",
|
"message": "操作成功",
|
||||||
"data": {}
|
"data": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -128,8 +130,8 @@ GET /v1/user/users?username=admin&status=1&createdAt_start=2024-01-01&createdAt_
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 200,
|
"error": 0,
|
||||||
"msg": "操作成功",
|
"message": "操作成功",
|
||||||
"data": {
|
"data": {
|
||||||
"records": [],
|
"records": [],
|
||||||
"total": 100,
|
"total": 100,
|
||||||
@@ -159,19 +161,23 @@ GET /v1/user/users?username=admin&status=1&createdAt_start=2024-01-01&createdAt_
|
|||||||
|
|
||||||
| 区间 | 模块 | 示例 |
|
| 区间 | 模块 | 示例 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 1000-1999 | 通用 | 1001: 参数校验失败, 1002: 资源不存在 |
|
| 0 | 通用成功 | `0: 操作成功` |
|
||||||
| 2000-2999 | 用户模块 | 2001: 用户名已存在, 2002: 密码错误 |
|
| 1 | 通用失败 | `1: 操作失败` |
|
||||||
| 3000-3999 | 系统模块 | 3001: 字典不存在, 3002: 配置错误 |
|
| 400-499 | HTTP 标准 | `401: 未授权, 404: 资源不存在` |
|
||||||
| 4000-4999 | 认证模块 | 4001: Token 过期, 4002: 无权访问 |
|
| 4000-4099 | 认证模块 | `4001: Token 已过期, 4002: Token 无效` |
|
||||||
| 5000-5999 | 文件模块 | 5001: 上传失败, 5002: 文件过大 |
|
| 4100-4199 | 用户信息 | `4101: 用户不存在, 4102: 用户名已存在` |
|
||||||
| 6000-6999 | 消息模块 | 6001: 发送失败, 6002: 模板不存在 |
|
| 4200-4299 | 用户等级 | `4201: 等级编码已存在` |
|
||||||
|
| 5000-5999 | 文件模块(预留) | `5001: 上传失败, 5002: 文件大小超限` |
|
||||||
|
| 6000-6999 | 消息模块(预留) | `6001: 发送失败, 6002: 模板不存在` |
|
||||||
|
|
||||||
|
> 完整枚举值见 [Result 统一响应类 → ResultCode 枚举](Result统一响应类.md#四resultcode-枚举)
|
||||||
|
|
||||||
**错误响应示例**:
|
**错误响应示例**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 2001,
|
"error": 4102,
|
||||||
"msg": "用户名已存在",
|
"message": "用户名已存在",
|
||||||
"data": null
|
"code": "USER_INFO_USERNAME_EXISTS"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -321,7 +327,7 @@ public class UserRemoteService {
|
|||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
return result.getData();
|
return result.getData();
|
||||||
}
|
}
|
||||||
throw new BizException(result.getCode(), result.getMsg());
|
throw new BizException(result.getError(), result.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -428,16 +434,137 @@ springdoc:
|
|||||||
|
|
||||||
### 12.2 访问地址
|
### 12.2 访问地址
|
||||||
|
|
||||||
|
#### Swagger UI 页面
|
||||||
|
|
||||||
```
|
```
|
||||||
http://localhost:9300/swagger-ui.html # 网关聚合文档
|
http://localhost:9300/swagger-ui.html # 网关聚合文档
|
||||||
http://localhost:9301/swagger-ui.html # 认证服务文档
|
http://localhost:9301/swagger-ui.html # 认证服务文档
|
||||||
http://localhost:9302/swagger-ui.html # 系统服务文档
|
http://localhost:9302/swagger-ui.html # 系统服务文档
|
||||||
http://localhost:9303/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 目录结构
|
### 13.1 目录结构
|
||||||
|
|
||||||
|
|||||||
@@ -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 响应
|
||||||
@@ -189,6 +189,27 @@ CREATE TABLE example_table (
|
|||||||
) COMMENT='示例表';
|
) 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,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 | 新增 DTO:LoginAccountDTO | 高 | 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 class,bizType 不维护中央清单)
|
||||||
|
- 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=9400,servlet.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 文件返回标准 Result,data.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 1(Service)、Task 3(i18n)、Task 4(Router)必须先完成。
|
||||||
|
|
||||||
|
- [ ] **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 1(Service)必须先完成。
|
||||||
|
|
||||||
|
- [ ] **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="第三方平台应用ID(UNIQUE)" />
|
||||||
|
</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 数组。格式:[{name, content}]。待后端文件上传接口就绪后升级为文件上传。
|
||||||
|
</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 切换正常,失败时回滚
|
||||||
|
- [ ] 单删:删除确认 → 行消失
|
||||||
|
- [ ] 批删:选中多行 → 批量删除 → 全部消失
|
||||||
|
- [ ] 弹窗:宽度 760px,4 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-07,commit `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 暂用空串占位
|
||||||
|
// - 后续 Task:Alipay 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=low,0 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 32–40)
|
||||||
|
|
||||||
|
- [ ] **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 76–86, 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 86–100, 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 104–116, 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 120–128, 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 12–28, 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 个任务(T1–T20)全部完成,详见 `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_detail,phone 字段将移除)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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) (可选,默认 false;true 时若文件是 .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=true(ZIP 自动解压)
|
||||||
|
|
||||||
|
**适用**:批量上传场景(应用多证书、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_UPLOAD,type 都是 "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 次/小时
|
||||||
|
- 验证码存储:Redis,key = `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(表)+ 29a9389(Service/Controller)+ 13b20ab(Result 规范)
|
||||||
|
|
||||||
|
### 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 | 应用ID(UNIQUE) |
|
||||||
|
| 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 Tab,760px 弹窗。
|
||||||
|
- **敏感字段**: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 '父级菜单ID,0=顶级',
|
||||||
|
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 1:MVP(最小可用)
|
||||||
|
|
||||||
|
1. 后端:项目/菜单/端点 CRUD + OpenAPI JSON 手动导入
|
||||||
|
2. 前端:三栏布局 + 菜单导航 + 接口文档渲染
|
||||||
|
|
||||||
|
### Phase 2:增强
|
||||||
|
|
||||||
|
3. 后端:自动同步引擎(定时拉取 /v3/api-docs)
|
||||||
|
4. 后端:文档补全管理
|
||||||
|
5. 前端:搜索 + 深色模式
|
||||||
|
|
||||||
|
### Phase 3:AI + 高级功能
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -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 已执行完成
|
||||||
|
- **已归档**:功能上线,文档归档
|
||||||
Reference in New Issue
Block a user