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

对应工单 rui/rui-frontend#4
2026-06-08 11:23:17 +08:00

470 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SysApp(第三方应用集成)管理界面设计规范
**工单**: rui/rui-frontend#4 — [UI-REQ] 后台增加 SysApp(第三方应用集成)管理界面
**日期**: 2026-06-07
**关联 Issue**: rui/rui-framework#4(文件上传接口待提供,前端先用 JSON 占位)
**优先级**: P1
---
## 1. 目标(Goal
在 admin-ui 后台实现 **SysApp(第三方应用集成)管理模块**,为运营/管理员提供对第三方平台应用(微信、支付宝、Stripe 等)凭证信息的统一管理能力。模块包含列表展示、新增、编辑、删除、启停 5 个标准操作,遵循现有 `oauth2-client` 等模块的 CRUD 模式(`BaseService` + `RuiTable` + `FormDialog`),不引入新依赖。
## 2. 非目标(Non-Goals
明确不在本期范围内的事项:
- **不修改后端**:本 Spec 仅涉及 admin-ui 前端;不修改 rui-framework / rui-cashier 等后端仓库代码或 API。
- **不实现 certificates 文件上传 UI**:因 rui-framework 尚未提供文件上传接口(已提 Issue #4),certificates 字段本期用 JSON textarea 占位实现,标注「待后端文件上传接口就绪后升级」。
- **不实现 isEncrypted 字段 UI**:后端为该字段预留,前端暂不展示。
- **不修改 cashier-mobile / cashier-customer**:本 Spec 仅涉及 admin-ui。
- **不引入 monaco-editor / @guolao/vue-monaco-editor 等新依赖**:证书编辑用原生 textarea + JSON.parse 校验。
- **不修改 rui-framework 菜单 JSON 文件**:工单提到的 `data/menus/system.json` 属于 rui-framework 仓库菜单管理数据,不在本仓库范围内。
- **不做多租户隔离增强**:tenantId 字段由后端按上下文自动填充,前端不主动设置。
## 3. 背景与上下文(Context
### 3.1 后端接口现状
- **Controller**: `SysAppController`,继承 `BaseController`,自动具备 5 个标准操作:
- `GET /system/admin/app/page` — 分页查询
- `GET /system/admin/app/list` — 列表查询
- `GET /system/admin/app/{id}` — 详情
- `POST /system/admin/app` — 新增
- `PUT /system/admin/app` — 修改
- `DELETE /system/admin/app/{id}` — 删除
- `DELETE /system/admin/app/batch` — 批量删除
- `PUT /system/admin/app/status` — 启停
- `GET /system/admin/app/export` — 导出
- `POST /system/admin/app/import` — 导入
- **鉴权**: `@AutoPermission("sys:app")`
- **Swagger**: `http://localhost:9302/swagger-ui.html`
- **后端 commit**: 27fa187(表)+ 29a9389Service/Controller+ 13b20abResult 规范)
### 3.2 前端代码基础
- **BaseService** (`admin-ui/src/service/BaseService.ts`) 已提供完整 CRUD 抽象,子类只需传 baseUrl
```ts
class SysAppService extends BaseService {
constructor() { super('/system/admin/app') }
}
export const sysAppService = new SysAppService()
```
- **RuiTable** 组件支持查询区、工具栏、列配置、slot、分页、导出、列设置、刷新、批量操作等开箱即用能力。
- **OAuth2Client 页面**`views/system/oauth2-client/Index.vue` + `OAuth2ClientFormDialog.vue`)是最相似的现有实现,本期直接照搬其模式。
### 3.3 路由与菜单
- admin-ui 路由采用 **前端硬编码 + i18n 键** 方式(`admin-ui/src/router/modules/system.ts`)。
- 工单中提到的 `data/menus/system.json` 是 rui-framework 后端「菜单管理」功能加载的菜单数据,不影响 admin-ui 路由配置。
## 4. 关键设计决策(用户已确认)
| # | 决策项 | 选定方案 |
|---|--------|---------|
| 1 | 菜单归属 | 作为「系统管理」的子菜单,路由 `/system/app` |
| 2 | 表单布局 | el-tabs 分 4 Tab(基础信息 / 凭证信息 / 接口配置 / 高级) |
| 3 | certificates 字段 | 前端 JSON textarea 占位(待 rui/rui-framework#4 文件上传接口就绪后升级) |
| 4 | 敏感字段(appSecret/appKey/aesKey | 列表展示 6 个星号 `******`,编辑时留空表示不修改 |
## 5. 字段定义(共 21 个,UI 涉及 19 个)
| 字段 | 类型 | 必填 | UI 控件 | Tab | 说明 |
|------|------|------|---------|-----|------|
| id | Long | — | (仅后端) | — | 主键 |
| tenantId | Long | — | (仅后端) | — | 租户ID 0=系统级(自动填充) |
| ownerType | String | 是 | el-select | 1 | PLATFORM / TENANT |
| platform | String | 是 | el-select | 1 | wechat / alipay / stripe |
| name | String | 是 | el-input | 1 | 管理用名称 |
| appId | String | 否 | el-input | 2 | 应用IDUNIQUE |
| appSecret | String | 否 | el-input (password) | 2 | **敏感**:列表脱敏,编辑留空不修改 |
| appKey | String | 否 | el-input (password) | 2 | **敏感** |
| certificates | String | 否 | JSON textarea | 2 | 多证书 JSON 数组(**占位** |
| aesKey | String | 否 | el-input (password) | 2 | **敏感** |
| redirectUri | String | 否 | el-input | 3 | OAuth2 回调地址 |
| merchantId | String | 否 | el-input | 3 | 商户号 |
| signType | String | 否 | el-select | 3 | RSA2 / MD5 / HMAC |
| notifyUrl | String | 否 | el-input | 3 | 支付回调 |
| apiBase | String | 否 | el-input | 3 | API 根地址 |
| isSandbox | 0/1 | 否 | el-switch | 4 | 是否沙箱环境 |
| extra | String | 否 | JSON textarea | 4 | JSON 扩展 |
| isEncrypted | 0/1 | — | **不展示** | — | 预留加密字段(暂不实现) |
| status | 0/1 | 否 | el-switch | 4 | 启用/禁用,默认 1 |
| description | String | 否 | el-input (textarea) | 1 | 备注 |
| sortNo | Int | 否 | el-input-number | 1 | 排序号 |
### 5.1 枚举映射(UI 展示用)
```ts
const platformMap: Record<string, { label: string; type: 'primary' | 'success' | 'warning' }> = {
wechat: { label: '微信', type: 'success' },
alipay: { label: '支付宝', type: 'primary' },
stripe: { label: 'Stripe', type: 'warning' },
}
const ownerTypeMap: Record<string, { label: string; type: 'primary' | 'success' }> = {
PLATFORM: { label: '平台级', type: 'primary' },
TENANT: { label: '租户级', type: 'success' },
}
const signTypeOptions = [
{ label: 'RSA2', value: 'RSA2' },
{ label: 'MD5', value: 'MD5' },
{ label: 'HMAC', value: 'HMAC' },
]
```
## 6. 涉及文件清单(Files To Change
| # | 文件 | 操作 | 用途 |
|---|------|------|------|
| 1 | `admin-ui/src/service/system/sysAppService.ts` | 新建 | 业务 Service,继承 `BaseService('/system/admin/app')` |
| 2 | `admin-ui/src/service/system/index.ts` | 改动 | 追加 `export { sysAppService } from './sysAppService'` |
| 3 | `admin-ui/src/router/modules/system.ts` | 改动 | 新增 `system/app` 路由,meta.i18n 键 `menu.systemApp` |
| 4 | `admin-ui/src/locales/zh-CN.ts` | 改动 | `menu.systemApp: '应用集成'` |
| 5 | `admin-ui/src/locales/en-US.ts` | 改动 | `menu.systemApp: 'App Integration'` |
| 6 | `admin-ui/src/views/system/app/Index.vue` | 新建 | 列表页(RuiTable + 操作工具栏) |
| 7 | `admin-ui/src/views/system/app/SysAppFormDialog.vue` | 新建 | 新增/编辑弹窗(4 Tab) |
**变更统计**:新建 3 个文件,修改 4 个文件。**总计 7 个文件**。
## 7. 列表页设计(`views/system/app/Index.vue`
### 7.1 结构
```vue
<template>
<div>
<h2 class="text-xl font-bold mb-4">{{ $t('menu.systemApp') }}</h2>
<RuiTable
ref="tableRef"
:columns="columns"
:load-data="loadData"
:show-selection="true"
:exportable="true"
export-filename="SysApp应用集成列表"
>
<!-- 查询区、工具栏、列 slot、操作 slot -->
</RuiTable>
<SysAppFormDialog v-model:visible="dialogVisible" :row="currentRow" @success="handleFormSuccess" />
</div>
</template>
```
### 7.2 查询区(4 个条件)
| 控件 | 字段 | 控件类型 | 选项 |
|------|------|---------|------|
| 应用名称 | `name` | `el-input` (clearable) | — |
| 平台 | `platform` | `el-select` (clearable) | wechat / alipay / stripe |
| 所有者类型 | `ownerType` | `el-select` (clearable) | PLATFORM / TENANT |
| 状态 | `status` | `el-select` (clearable) | 启用(1) / 禁用(0) |
### 7.3 列配置(columns
| prop | label | 宽度/最小宽度 | slot | 备注 |
|------|-------|--------------|------|------|
| name | 应用名称 | minWidth 150 | — | — |
| platform | 平台 | width 100 | 是 | 彩色 Tag |
| ownerType | 所有者 | width 100 | 是 | PLATFORM 蓝、TENANT 绿 |
| appId | 应用ID | minWidth 120 | — | 列表不脱敏(UNIQUE 标识) |
| status | 状态 | width 90 | 是 | Switch |
| createdAt | 创建时间 | minWidth 180 | — | dataType='dateTime'sortable='custom' |
> **脱敏说明**appSecret / appKey / aesKey 三个敏感字段**不进入列表列**,仅在编辑弹窗中处理。列表中没有任何明文密钥展示位。
### 7.4 工具栏
- **左侧**:新增应用按钮(`type="primary"`
- **右侧**:批量删除(基于 `show-selection`) + 导出 + 刷新 + 列设置(RuiTable 内置)
### 7.5 行操作
- 编辑
- 删除(ElMessageBox 二次确认,删除成功后 `tableRef.refresh()`
### 7.6 启停切换
```ts
async function handleStatusChange(row: any, status: number) {
if (!row?.id) return
try {
await sysAppService.changeStatus(row.id, status)
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
} catch {
row.status = status === 1 ? 0 : 1 // 失败回滚
}
}
```
## 8. 表单弹窗设计(`views/system/app/SysAppFormDialog.vue`
### 8.1 弹窗基本属性
- 宽度:`760px`
- title:编辑时「编辑应用」/ 新增时「新增应用」
- 关闭点击遮罩:`close-on-click-modal="false"`
- props`visible: boolean`、`row: any`
- emits`update:visible`、`success`
### 8.2 数据加载流程
```
watch(visible, val => {
if (val) {
if (props.row) {
// 编辑:拉取详情(确保拿到完整字段,包括敏感字段的明文用于编辑回显)
sysAppService.getById(props.row.id).then(data => { form.value = { ...data } })
} else {
// 新增:重置为默认值
form.value = { ...defaultForm }
}
}
})
```
### 8.3 默认值(新增时)
```ts
const defaultForm = {
id: undefined,
ownerType: 'PLATFORM',
platform: 'wechat',
name: '',
appId: '',
appSecret: '',
appKey: '',
certificates: '',
aesKey: '',
redirectUri: '',
merchantId: '',
signType: 'RSA2',
notifyUrl: '',
apiBase: '',
isSandbox: 0,
extra: '',
status: 1,
description: '',
sortNo: 0,
}
```
### 8.4 4 Tab 分布
#### Tab 1:基础信息
```
- name* el-input (必填)
- ownerType* el-select (必填, PLATFORM / TENANT)
- platform* el-select (必填, wechat / alipay / stripe)
- description el-input (textarea, :rows="2")
- sortNo el-input-number
```
#### Tab 2:凭证信息
```
- appId el-input
- appSecret el-input (type="password" show-password)
- appKey el-input (type="password" show-password)
- aesKey el-input (type="password" show-password)
- certificates el-input (type="textarea" :rows="4")
placeholder='[{"name":"cert1","content":"<PEM 内容>"}]'
下方 helper text:「多证书 JSON 数组。格式:[{name, content}]。待后端文件上传接口就绪后升级为文件上传。」
```
> **敏感字段编辑规则**appSecret / appKey / aesKey 三个字段在编辑时**留空 = 不修改原值**。这一规则完全照搬 `OAuth2ClientFormDialog` 的现有做法,保证行为一致。
#### Tab 3:接口配置
```
- redirectUri el-input
- notifyUrl el-input
- apiBase el-input
- merchantId el-input
- signType el-select (RSA2 / MD5 / HMAC)
```
#### Tab 4:高级
```
- isSandbox el-switch (0/1)
- extra el-input (type="textarea" :rows="4")
下方 helper text:「JSON 扩展字段,提交前需通过 JSON 格式校验」
- status el-switch (0/1,默认 1)
```
### 8.5 校验
- **必填字段**`name`、`ownerType`、`platform`
- **JSON 字段**certificates 和 extra 字段在提交前调用 `validateJSON()` 校验,非空时尝试 `JSON.parse`,失败则 `ElMessage.error('xxx 字段 JSON 格式错误')` 并阻止提交。
```ts
function validateJSON(value: string, fieldName: string): boolean {
if (!value || !value.trim()) return true // 空值允许
try {
JSON.parse(value)
return true
} catch {
ElMessage.error(`${fieldName} JSON 格式错误`)
return false
}
}
```
### 8.6 提交逻辑
```ts
async function handleSubmit() {
await formRef.value.validate()
if (!validateJSON(form.value.certificates, 'certificates')) return
if (!validateJSON(form.value.extra, 'extra')) return
loading.value = true
try {
const isEdit = !!form.value.id
const success = isEdit
? await sysAppService.update(form.value as any)
: await sysAppService.add(form.value)
if (success !== false) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
emit('success')
dialogVisible.value = false
}
} catch {
// 错误已由请求拦截器统一提示
} finally {
loading.value = false
}
}
```
## 9. Service 层设计(`service/system/sysAppService.ts`
完整实现(参考 `oauth2ClientService.ts` 的极简模式):
```ts
import { BaseService } from '../BaseService'
/**
* SysApp(第三方应用集成)服务
*
* <p>负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。</p>
*
* <p><b>后续升级路径</b>:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加
* `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea
* 升级为文件上传组件。</p>
*/
class SysAppService extends BaseService {
constructor() {
super('/system/admin/app')
}
}
/** SysApp 服务单例 */
export const sysAppService = new SysAppService()
```
并在 `service/system/index.ts` 末尾追加:
```ts
export { sysAppService } from './sysAppService'
```
## 10. 路由与国际化
### 10.1 路由注册
在 `admin-ui/src/router/modules/system.ts` 的 `M` 常量中追加:
```ts
systemApp: 'menu.systemApp',
```
在 `systemRoutes` 数组中追加:
```ts
{ path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
```
> 路由位置:放在 `systemOAuth2Client` 之后,保持「集成类」菜单分组相邻。
### 10.2 国际化
`admin-ui/src/locales/zh-CN.ts`(在 `systemOAuth2Client` 后追加):
```ts
systemApp: '应用集成',
```
`admin-ui/src/locales/en-US.ts`(在 `system` block 内对应位置追加):
```ts
systemApp: 'App Integration',
```
## 11. 错误处理
- **统一拦截**:所有 HTTP 错误由 `utils/request` 拦截器统一提示(已存在),前端代码不再额外 try-catch 提示。
- **业务校验**:表单必填、JSON 格式校验在组件内完成,失败用 `ElMessage` 提示。
- **状态回滚**:启停切换失败时,将 `row.status` 回滚到原值。
- **删除确认**:删除前 `ElMessageBox.confirm` 二次确认。
## 12. 测试策略
### 12.1 静态检查
```bash
pnpm --filter admin-ui type-check # 0 errors
pnpm --filter admin-ui lint # 0 errors
```
### 12.2 运行时验证(手动)
1. **启动 dev server**`pnpm dev:admin`
2. **登录** 后访问 `/system/app`(需菜单权限 `sys:app:query`,从后端菜单加载)
3. **列表加载**:默认加载列表数据,列展示正确
4. **查询**:按 name / platform / ownerType / status 过滤,验证结果正确
5. **新增**:点「新增应用」→ 填写各 Tab 必填项 → 提交 → 列表出现新行
6. **编辑**:点行内编辑 → 弹窗加载详情(4 Tab 回显)→ 修改 → 提交 → 列表更新
7. **敏感字段验证**:编辑时不输入 appSecret → 提交后重新打开,明文应保持不变(**由后端「留空不修改」规则保证**)
8. **JSON 字段验证**certificates / extra 输入非 JSON 文本 → 提交时被拦截并提示
9. **启停**:点击状态 Switch → 接口调用 → 列表状态切换;模拟失败时 row.status 回滚
10. **删除**:点击删除 → 二次确认 → 提交 → 行从列表消失
11. **批量删除**:选中多行 → 批量删除 → 全部消失
12. **导出**:点击导出 → 下载 CSV 文件
13. **脱敏验证**:在 devtools Network 面板检查 `/page` 返回的 records**不应**包含 appSecret / appKey / aesKey 明文(这三个字段不进列表列)
### 12.3 验证清单(提交前必过)
- [ ] `pnpm type-check` 通过
- [ ] `pnpm lint` 通过
- [ ] 列表 → 新增 → 编辑 → 启停 → 删除 → 批量删除 完整跑通
- [ ] 列表中不展示任何明文密钥
- [ ] 敏感字段编辑留空后原值保持
- [ ] certificates / extra 非法 JSON 提交被拦截
- [ ] 菜单「应用集成」在侧边栏正确显示,路由跳转正常
## 13. 风险与缓解(Risks And Mitigations
| # | 风险 | 缓解措施 |
|---|------|---------|
| 1 | **后端脱敏返回**:若后端在列表接口已经对 appSecret/appKey/aesKey 做脱敏(如 `******`),则编辑回显时弹窗中拿不到原值 | 列表用 `getById(id)` 拉详情(详情接口通常返回明文),并设计为编辑时强制用户重新输入敏感字段。本期采用「详情接口返回明文,编辑留空不修改」模式;如未来后端详情也脱敏,需在 UI 加「修改敏感字段」开关。 |
| 2 | **certificates JSON 格式错误**:用户提交非合法 JSON 导致后端解析失败 | 提交前 `validateJSON()` 拦截,UI 给出明确错误提示;helper text 提示正确格式。 |
| 3 | **文件上传接口未就绪**certificates 暂用 JSON 占位,用户体验差 | 已在 rui/rui-framework#4 提 Issue;后端接口就绪后再升级为文件上传组件(升级路径已记录在 `sysAppService.ts` 注释中)。 |
| 4 | **路由重复**system.ts 中路由顺序错乱导致菜单不显示 | 路由注册在 systemOAuth2Client 之后;meta.i18n 键值与 locales 文件保持一致。 |
| 5 | **列表数据过大导致性能问题** | 分页由 RuiTable 默认处理(page=1, size=10),无额外风险。 |
| 6 | **前端调用了不存在的接口**:万一后端实际未提供 `import` 接口 | BaseService 自带 `importable` 开关默认 false,不暴露导入按钮。如后端未提供 import 接口则本 UI 不调用即可。 |
## 14. 决策摘要(Decision Summary
- **架构**:照搬 `oauth2-client` 模式(`BaseService` 13 行 + `RuiTable` 列表 + `FormDialog` 弹窗),不引入新依赖、不发明新模式。
- **菜单归属**:系统管理 → 子菜单「应用集成」,路由 `/system/app`。
- **表单布局**el-tabs 4 Tab760px 弹窗。
- **敏感字段**appSecret / appKey / aesKey 列表脱敏 6 星号,编辑留空不修改。
- **certificates 字段**JSON textarea 占位,UI 注释提醒「待后端文件上传接口就绪后升级」。
- **isEncrypted 字段**UI 暂不实现。
- **测试**type-check + lint + 手动跑通完整 CRUD。
- **依赖后端**:仅依赖 rui-framework 既有 `/system/admin/app` 接口;`/system/admin/file/upload`rui/rui-framework#4)就绪后再升级 certificates 体验。
---
**设计评审状态**: 待评审
**下一步**: 用户评审通过后,编写实施计划(Plan)