Files
rui-docs/superpowers/specs/2026-06-07-sysapp-management-design.md
T
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

19 KiB
Raw Blame History

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
    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 展示用)

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"
  • propsvisible: booleanrow: any
  • emitsupdate:visiblesuccess

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 校验

  • 必填字段nameownerTypeplatform
  • 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.tsM 常量中追加:

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 运行时验证(手动)

  1. 启动 dev serverpnpm 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/uploadrui/rui-framework#4)就绪后再升级 certificates 体验。

设计评审状态: 待评审 下一步: 用户评审通过后,编写实施计划(Plan)