# 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)