- 为 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
19 KiB
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: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 展示用)
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 结构
<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 启停切换
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 默认值(新增时)
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 格式错误')并阻止提交。
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 提交逻辑
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 的极简模式):
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 末尾追加:
export { sysAppService } from './sysAppService'
10. 路由与国际化
10.1 路由注册
在 admin-ui/src/router/modules/system.ts 的 M 常量中追加:
systemApp: 'menu.systemApp',
在 systemRoutes 数组中追加:
{ 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 后追加):
systemApp: '应用集成',
admin-ui/src/locales/en-US.ts(在 system block 内对应位置追加):
systemApp: 'App Integration',
11. 错误处理
- 统一拦截:所有 HTTP 错误由
utils/request拦截器统一提示(已存在),前端代码不再额外 try-catch 提示。 - 业务校验:表单必填、JSON 格式校验在组件内完成,失败用
ElMessage提示。 - 状态回滚:启停切换失败时,将
row.status回滚到原值。 - 删除确认:删除前
ElMessageBox.confirm二次确认。
12. 测试策略
12.1 静态检查
pnpm --filter admin-ui type-check # 0 errors
pnpm --filter admin-ui lint # 0 errors
12.2 运行时验证(手动)
- 启动 dev server:
pnpm dev:admin - 登录 后访问
/system/app(需菜单权限sys:app:query,从后端菜单加载) - 列表加载:默认加载列表数据,列展示正确
- 查询:按 name / platform / ownerType / status 过滤,验证结果正确
- 新增:点「新增应用」→ 填写各 Tab 必填项 → 提交 → 列表出现新行
- 编辑:点行内编辑 → 弹窗加载详情(4 Tab 回显)→ 修改 → 提交 → 列表更新
- 敏感字段验证:编辑时不输入 appSecret → 提交后重新打开,明文应保持不变(由后端「留空不修改」规则保证)
- JSON 字段验证:certificates / extra 输入非 JSON 文本 → 提交时被拦截并提示
- 启停:点击状态 Switch → 接口调用 → 列表状态切换;模拟失败时 row.status 回滚
- 删除:点击删除 → 二次确认 → 提交 → 行从列表消失
- 批量删除:选中多行 → 批量删除 → 全部消失
- 导出:点击导出 → 下载 CSV 文件
- 脱敏验证:在 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模式(BaseService13 行 +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)