From f4761ae145bf591b77dca2c0baec2f265c4f3974 Mon Sep 17 00:00:00 2001 From: pigeon Date: Sun, 7 Jun 2026 19:32:08 +0800 Subject: [PATCH] docs(spec): add SysApp application integration management design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 rui/rui-frontend#4 工单编写设计规范 - 7 个文件变更:service 新建、index 改、router 改、locales 改、2 个 vue 新建 - 4 Tab 表单布局(基础信息/凭证信息/接口配置/高级) - 敏感字段脱敏(appSecret/appKey/aesKey) - certificates 字段 JSON 占位(等 rui/rui-framework#4 文件上传接口) 对应工单 rui/rui-frontend#4 --- .../2026-06-07-sysapp-management-design.md | 469 ++++++++++++++++++ superpowers/specs/README.md | 24 + 2 files changed, 493 insertions(+) create mode 100644 superpowers/specs/2026-06-07-sysapp-management-design.md create mode 100644 superpowers/specs/README.md diff --git a/superpowers/specs/2026-06-07-sysapp-management-design.md b/superpowers/specs/2026-06-07-sysapp-management-design.md new file mode 100644 index 0000000..d28fdb9 --- /dev/null +++ b/superpowers/specs/2026-06-07-sysapp-management-design.md @@ -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 = { + wechat: { label: '微信', type: 'success' }, + alipay: { label: '支付宝', type: 'primary' }, + stripe: { label: 'Stripe', type: 'warning' }, +} + +const ownerTypeMap: Record = { + 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 + +``` + +### 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":""}]' + 下方 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(第三方应用集成)服务 + * + *

负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。

+ * + *

后续升级路径:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加 + * `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea + * 升级为文件上传组件。

+ */ +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) diff --git a/superpowers/specs/README.md b/superpowers/specs/README.md new file mode 100644 index 0000000..2d9855f --- /dev/null +++ b/superpowers/specs/README.md @@ -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--design.md` +- 目录:`docs/superpowers/specs/` +- 配套 Plan:`docs/superpowers/plans/YYYY-MM-DD--plan.md` + +## 状态 + +- **待评审**:Spec 刚写完,等待用户/团队评审 +- **已评审**:用户已批准,可进入 Plan 阶段 +- **已实施**:对应 Plan 已执行完成 +- **已归档**:功能上线,文档归档