Files
rui-docs/superpowers/plans/2026-06-07-sysapp-management-plan.md
T

26 KiB
Raw Blame History

SysApp(第三方应用集成)管理界面实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在 admin-ui 后台实现 SysApp(第三方应用集成)管理模块,提供对微信/支付宝/Stripe 等第三方平台应用凭证信息的统一管理能力。

Architecture: 完全照搬现有 oauth2-client 模块的 CRUD 模式 —— BaseService 13 行极简继承 + RuiTable 列表页 + FormDialog 弹窗(el-tabs 4 Tab)。不引入新依赖

Tech Stack: Vue 3, TypeScript, Element Plus, Vite, Pinia


文件变更清单

# 文件 变更类型 说明
1 admin-ui/src/service/system/sysAppService.ts 新建 Service(继承 BaseService
2 admin-ui/src/service/system/index.ts 修改 追加导出 sysAppService
3 admin-ui/src/locales/zh-CN.ts 修改 systemApp: '应用集成'
4 admin-ui/src/locales/en-US.ts 修改 systemApp: 'App Integration'
5 admin-ui/src/router/modules/system.ts 修改 注册 /system/app 路由
6 admin-ui/src/views/system/app/Index.vue 新建 列表页(RuiTable
7 admin-ui/src/views/system/app/SysAppFormDialog.vue 新建 表单弹窗(4 Tab

合计:新建 3 个文件 + 修改 4 个文件 = 7 个文件


任务依赖图

Task 1 (Service) ──┬── Task 2 (Service Index)
                   │
                   ├── Task 5 (列表页) ──┐
                   │                     │
                   └── Task 6 (表单) ────┴── Task 7 (端到端验证)

Task 3 (i18n) ──┐
Task 4 (Router) ┴── Task 5 (列表页)

Task 1: 创建 SysApp Service

Files:

  • Create: admin-ui/src/service/system/sysAppService.ts

  • Step 1: 写入 Service 文件

admin-ui/src/service/system/sysAppService.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()
  • Step 2: 验证类型检查

Run:

cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/sysAppService.ts

Expected: 无错误输出

  • Step 3: Commit (commit 67d6686)
git add admin-ui/src/service/system/sysAppService.ts
git commit -m "feat(sysApp): add sysAppService extending BaseService for /system/admin/app"

Task 2: 在 Service 统一入口导出 sysAppService

Files:

  • Modify: admin-ui/src/service/system/index.ts

  • Step 1: 追加导出语句 (commit 0b4b02f)

在文件末尾追加:

export { sysAppService } from './sysAppService'
  • Step 2: 验证导入

Run:

cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/index.ts

Expected: 无错误输出

  • Step 3: Commit
git add admin-ui/src/service/system/index.ts
git commit -m "feat(sysApp): export sysAppService from system service index"

Task 3: 配置国际化(中英文)

Files:

  • Modify: admin-ui/src/locales/zh-CN.ts

  • Modify: admin-ui/src/locales/en-US.ts

  • Step 1: 在 zh-CN.ts 添加中文

定位到 systemOAuth2Client: 'OAuth2客户端', 这一行,在其后添加:

    systemApp: '应用集成',

(注意缩进:与 systemOAuth2Client 保持一致的 4 空格)

  • Step 2: 在 en-US.ts 添加英文

定位到 systemOAuth2Client: 'OAuth2 Client',(或对应位置),在其后添加:

    systemApp: 'App Integration',

(如果 en-US.ts 没有 systemOAuth2Client 这一行,则加在 system 块的合理位置,参考 systemOAuth2Client 的就近位置)

  • Step 3: 验证

Run:

grep -n "systemApp" admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts

Expected: 两个文件各有一行匹配

  • Step 4: Commit
git add admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
git commit -m "feat(sysApp): add systemApp i18n key (zh-CN: '应用集成', en-US: 'App Integration')"

Task 4: 注册路由

Files:

  • Modify: admin-ui/src/router/modules/system.ts

  • Step 1: 在 M 常量加键

定位到 systemOAuth2Client: 'menu.systemOAuth2Client', 这一行,在其后添加:

  systemApp: 'menu.systemApp',
  • Step 2: 在 systemRoutes 数组加路由

定位到 system/oauth2-client 路由条目,在其后添加:

  { path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
  • Step 3: 验证

Run:

grep -n "systemApp\|system/app" admin-ui/src/router/modules/system.ts

Expected: 至少 2 行匹配(一个 M 常量,一个 systemRoutes 数组)

  • Step 4: Commit
git add admin-ui/src/router/modules/system.ts
git commit -m "feat(sysApp): register /system/app route in system router"

Task 5: 创建列表页

Files:

  • Create: admin-ui/src/views/system/app/Index.vue

依赖Task 1Service)、Task 3i18n)、Task 4Router)必须先完成。

  • Step 1: 创建目录
mkdir -p admin-ui/src/views/system/app
  • Step 2: 写入列表页

创建 admin-ui/src/views/system/app/Index.vue,内容如下:

<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { sysAppService } from '@/service/system/sysAppService'
import type { TableColumn, PageResult, PageParams } from '@/components/RuiTable'
import SysAppFormDialog from './SysAppFormDialog.vue'

/**
 * 查询参数
 */
const query = ref({
  name: '',
  platform: '',
  ownerType: '',
  status: '',
})

/**
 * 平台枚举
 */
const platformMap: Record<string, { label: string; type: 'success' | 'primary' | '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 columns: TableColumn[] = [
  { prop: 'name', label: '应用名称', minWidth: 150 },
  {
    prop: 'platform',
    label: '平台',
    width: 100,
    align: 'center',
    slot: true,
  },
  {
    prop: 'ownerType',
    label: '所有者',
    width: 100,
    align: 'center',
    slot: true,
  },
  { prop: 'appId', label: '应用ID', minWidth: 120 },
  {
    prop: 'status',
    label: '状态',
    width: 90,
    align: 'center',
    slot: true,
  },
  {
    prop: 'createdAt',
    label: '创建时间',
    minWidth: 180,
    sortable: 'custom',
    dataType: 'dateTime',
  },
]

/**
 * 加载数据
 */
async function loadData(params: PageParams & Record<string, any>): Promise<PageResult> {
  return sysAppService.page(params)
}

/**
 * 表格组件引用
 */
const tableRef = ref<InstanceType<typeof import('@/components/RuiTable').default>>()

/**
 * 弹窗显示状态
 */
const dialogVisible = ref(false)
const currentRow = ref<any>(null)

/**
 * 新增
 */
function handleAdd() {
  currentRow.value = null
  dialogVisible.value = true
}

/**
 * 编辑
 */
function handleEdit(row: any) {
  currentRow.value = row
  dialogVisible.value = true
}

/**
 * 删除
 */
function handleDelete(row: any) {
  ElMessageBox.confirm(`确认删除应用 "${row.name}" 吗?`, '提示', {
    type: 'warning',
  }).then(async () => {
    try {
      await sysAppService.remove(row.id)
      ElMessage.success('删除成功')
      tableRef.value?.refresh()
    } catch {
      // 错误已由请求拦截器统一提示
    }
  }).catch(() => {})
}

/**
 * 状态切换
 */
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
  }
}

/**
 * 表单操作成功回调
 */
function handleFormSuccess() {
  tableRef.value?.refresh()
}
</script>

<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应用集成列表"
    >
      <!-- 查询区域 -->
      <template #search="{ query: q, search, reset }">
        <el-form-item label="应用名称">
          <el-input v-model.trim="q.name" placeholder="请输入应用名称" clearable @keyup.enter="search" />
        </el-form-item>
        <el-form-item label="平台">
          <el-select v-model="q.platform" placeholder="全部" clearable style="width: 120px">
            <el-option v-for="(v, k) in platformMap" :key="k" :label="v.label" :value="k" />
          </el-select>
        </el-form-item>
        <el-form-item label="所有者">
          <el-select v-model="q.ownerType" placeholder="全部" clearable style="width: 120px">
            <el-option v-for="(v, k) in ownerTypeMap" :key="k" :label="v.label" :value="k" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="q.status" placeholder="全部" clearable style="width: 100px">
            <el-option label="启用" :value="1" />
            <el-option label="禁用" :value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="search">
            查询
          </el-button>
          <el-button @click="reset">
            重置
          </el-button>
        </el-form-item>
      </template>

      <!-- 工具栏左侧 -->
      <template #toolbar-left>
        <el-button type="primary" @click="handleAdd">
          新增应用
        </el-button>
      </template>

      <!-- 自定义列平台 -->
      <template #column-platform="{ row }">
        <el-tag v-if="platformMap[row.platform]" :type="platformMap[row.platform].type" size="small">
          {{ platformMap[row.platform].label }}
        </el-tag>
        <span v-else>-</span>
      </template>

      <!-- 自定义列所有者 -->
      <template #column-ownerType="{ row }">
        <el-tag v-if="ownerTypeMap[row.ownerType]" :type="ownerTypeMap[row.ownerType].type" size="small">
          {{ ownerTypeMap[row.ownerType].label }}
        </el-tag>
        <span v-else>-</span>
      </template>

      <!-- 自定义列状态 -->
      <template #column-status="{ row }">
        <el-switch
          v-model="row.status"
          :active-value="1"
          :inactive-value="0"
          @change="(val: number) => handleStatusChange(row, val)"
        />
      </template>

      <!-- 操作列 -->
      <template #action="{ row }">
        <el-button link type="primary" size="small" @click="handleEdit(row)">
          编辑
        </el-button>
        <el-button link type="danger" size="small" @click="handleDelete(row)">
          删除
        </el-button>
      </template>
    </RuiTable>

    <!-- 新增/编辑弹窗 -->
    <SysAppFormDialog
      v-model:visible="dialogVisible"
      :row="currentRow"
      @success="handleFormSuccess"
    />
  </div>
</template>
  • Step 3: 验证类型检查

Run:

cd admin-ui && npx vue-tsc --noEmit --skipLibCheck

Expected: 无错误输出

  • Step 4: Commit
git add admin-ui/src/views/system/app/Index.vue
git commit -m "feat(sysApp): add SysApp list page with RuiTable, search, status switch"

Task 6: 创建表单弹窗

Files:

  • Create: admin-ui/src/views/system/app/SysAppFormDialog.vue

依赖Task 1Service)必须先完成。

  • Step 1: 写入表单弹窗

创建 admin-ui/src/views/system/app/SysAppFormDialog.vue,内容如下:

<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { sysAppService } from '@/service/system/sysAppService'

const props = defineProps<{
  visible: boolean
  row: any
}>()

const emit = defineEmits<{
  (e: 'update:visible', value: boolean): void
  (e: 'success'): void
}>()

const dialogVisible = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val),
})

/**
 * 平台选项
 */
const platformOptions = [
  { label: '微信', value: 'wechat' },
  { label: '支付宝', value: 'alipay' },
  { label: 'Stripe', value: 'stripe' },
]

/**
 * 所有者选项
 */
const ownerTypeOptions = [
  { label: '平台级', value: 'PLATFORM' },
  { label: '租户级', value: 'TENANT' },
]

/**
 * 签名方式选项
 */
const signTypeOptions = [
  { label: 'RSA2', value: 'RSA2' },
  { label: 'MD5', value: 'MD5' },
  { label: 'HMAC', value: 'HMAC' },
]

/**
 * 表单默认值
 */
const defaultForm = {
  id: undefined as number | 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,
}

const form = ref({ ...defaultForm })
const formRef = ref()

/**
 * 校验规则
 */
const rules = {
  name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
  ownerType: [{ required: true, message: '请选择所有者类型', trigger: 'change' }],
  platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
}

const loading = ref(false)

/**
 * 校验 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
  }
}

/**
 * 提交表单
 */
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
  }
}

/**
 * 监听弹窗显示,初始化表单
 */
watch(() => props.visible, async (val) => {
  if (val) {
    if (props.row) {
      // 编辑:拉详情(确保拿到完整字段)
      try {
        const detail = await sysAppService.getById(props.row.id)
        form.value = { ...defaultForm, ...detail }
      } catch {
        // 拉取失败回退到 row
        form.value = { ...defaultForm, ...props.row }
      }
    } else {
      // 新增:重置
      form.value = { ...defaultForm }
    }
  }
})
</script>

<template>
  <el-dialog
    v-model="dialogVisible"
    :title="form.id ? '编辑应用' : '新增应用'"
    width="760px"
    :close-on-click-modal="false"
  >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
      <el-tabs>
        <!-- Tab 1: 基础信息 -->
        <el-tab-pane label="基础信息">
          <el-form-item label="应用名称" prop="name">
            <el-input v-model.trim="form.name" placeholder="请输入应用名称" />
          </el-form-item>
          <el-form-item label="所有者类型" prop="ownerType">
            <el-select v-model="form.ownerType" style="width: 100%">
              <el-option v-for="opt in ownerTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
            </el-select>
          </el-form-item>
          <el-form-item label="平台" prop="platform">
            <el-select v-model="form.platform" style="width: 100%">
              <el-option v-for="opt in platformOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
            </el-select>
          </el-form-item>
          <el-form-item label="备注">
            <el-input v-model="form.description" type="textarea" :rows="2" placeholder="备注" />
          </el-form-item>
          <el-form-item label="排序号">
            <el-input-number v-model="form.sortNo" :min="0" style="width: 200px" />
          </el-form-item>
        </el-tab-pane>

        <!-- Tab 2: 凭证信息 -->
        <el-tab-pane label="凭证信息">
          <el-form-item label="应用ID">
            <el-input v-model.trim="form.appId" placeholder="第三方平台应用IDUNIQUE" />
          </el-form-item>
          <el-form-item label="应用密钥">
            <el-input
              v-model="form.appSecret"
              type="password"
              show-password
              placeholder="留空表示不修改"
            />
          </el-form-item>
          <el-form-item label="应用Key">
            <el-input
              v-model="form.appKey"
              type="password"
              show-password
              placeholder="留空表示不修改"
            />
          </el-form-item>
          <el-form-item label="AES Key">
            <el-input
              v-model="form.aesKey"
              type="password"
              show-password
              placeholder="留空表示不修改"
            />
          </el-form-item>
          <el-form-item label="证书">
            <el-input
              v-model="form.certificates"
              type="textarea"
              :rows="4"
              placeholder='示例:[{"name":"cert1","content":"<PEM>"}]'
            />
            <div class="text-xs text-gray-400 mt-1">
              多证书 JSON 数组格式[&#123;name, content&#125;]待后端文件上传接口就绪后升级为文件上传
            </div>
          </el-form-item>
        </el-tab-pane>

        <!-- Tab 3: 接口配置 -->
        <el-tab-pane label="接口配置">
          <el-form-item label="回调地址">
            <el-input v-model.trim="form.redirectUri" placeholder="OAuth2 回调地址" />
          </el-form-item>
          <el-form-item label="支付回调">
            <el-input v-model.trim="form.notifyUrl" placeholder="支付回调 URL" />
          </el-form-item>
          <el-form-item label="API 根地址">
            <el-input v-model.trim="form.apiBase" placeholder="API 根地址" />
          </el-form-item>
          <el-form-item label="商户号">
            <el-input v-model.trim="form.merchantId" placeholder="商户号" />
          </el-form-item>
          <el-form-item label="签名方式">
            <el-select v-model="form.signType" style="width: 100%">
              <el-option v-for="opt in signTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
            </el-select>
          </el-form-item>
        </el-tab-pane>

        <!-- Tab 4: 高级 -->
        <el-tab-pane label="高级">
          <el-form-item label="沙箱环境">
            <el-switch
              v-model="form.isSandbox"
              :active-value="1"
              :inactive-value="0"
            />
          </el-form-item>
          <el-form-item label="扩展 JSON">
            <el-input
              v-model="form.extra"
              type="textarea"
              :rows="4"
              placeholder='示例:{"key":"value"}'
            />
            <div class="text-xs text-gray-400 mt-1">
              JSON 扩展字段提交前需通过 JSON 格式校验
            </div>
          </el-form-item>
          <el-form-item label="状态">
            <el-switch
              v-model="form.status"
              :active-value="1"
              :inactive-value="0"
              active-text="启用"
              inactive-text="禁用"
            />
          </el-form-item>
        </el-tab-pane>
      </el-tabs>
    </el-form>
    <template #footer>
      <el-button @click="dialogVisible = false">
        取消
      </el-button>
      <el-button type="primary" :loading="loading" @click="handleSubmit">
        保存
      </el-button>
    </template>
  </el-dialog>
</template>
  • Step 2: 验证类型检查

Run:

cd admin-ui && npx vue-tsc --noEmit --skipLibCheck

Expected: 无错误输出

  • Step 3: Commit
git add admin-ui/src/views/system/app/SysAppFormDialog.vue
git commit -m "feat(sysApp): add SysApp form dialog with 4 tabs (basic/credentials/api/advanced)"

Task 7: 端到端验证

Files: 无(验证任务)

依赖:所有前置任务(Task 1-6)已完成。

  • Step 1: 运行类型检查

Run:

pnpm --filter admin-ui type-check

Expected: 0 errors

  • Step 2: 运行 Lint

Run:

pnpm --filter admin-ui lint

Expected: 0 errors

  • Step 3: 启动 dev server 并验证
pnpm dev:admin

打开浏览器,登录后访问 /system/app,逐条验证:

  • Step 3.1 列表加载

    • 页面正常打开,无 console error
    • 默认加载列表数据
    • 6 列展示正确(应用名称/平台/所有者/应用ID/状态/创建时间)
  • Step 3.2 查询

    • 按 name 过滤:输入 → 列表更新
    • 按 platform 过滤:选择微信 → 列表只显示微信
    • 按 ownerType 过滤:选择平台级 → 列表只显示 PLATFORM
    • 按 status 过滤:选择禁用 → 列表只显示禁用项
  • Step 3.3 新增

    • 点「新增应用」→ 弹窗打开,默认 4 Tab
    • 填必填项(name=测试应用, ownerType=PLATFORM, platform=wechat)→ 提交
    • 列表出现新行
    • devtools Network 检查 POST /system/admin/app 返回 200
  • Step 3.4 编辑

    • 点行内编辑 → 弹窗加载详情
    • 4 个 Tab 正确回显
    • 修改 name → 提交 → 列表更新
  • Step 3.5 敏感字段验证

    • 编辑时 appSecret 留空 → 提交
    • 重新打开编辑,appSecret 字段应保持原值(不修改)
    • devtools Network 检查 PUT /system/admin/app 请求体中 appSecret 字段为空字符串
  • Step 3.6 JSON 字段验证

    • certificates 输入 {invalid json → 提交
    • 应被拦截并提示「certificates JSON 格式错误」
    • extra 同样验证
  • Step 3.7 启停

    • 点击状态 Switch → 接口调用 → 列表状态切换
    • 模拟失败:可在 devtools 拦截请求,验证 row.status 回滚
  • Step 3.8 删除

    • 点击删除 → 二次确认弹窗
    • 确认 → 行从列表消失
    • devtools Network 检查 DELETE /system/admin/app/{id} 返回 200
  • Step 3.9 批量删除

    • 勾选 2-3 行 → 批量删除按钮(toolbar)→ 确认 → 全部消失
  • Step 3.10 导出

    • 点击导出 → 下载 CSV 文件
    • 文件名包含日期
    • 字段对应列表列
  • Step 3.11 脱敏验证

    • devtools Network 检查 GET /system/admin/app/page 返回的 records
    • 不应包含 appSecret / appKey / aesKey 明文
    • 列表 UI 中这三个字段没有展示位
  • Step 3.12 菜单展示

    • 侧边栏「系统管理」分组下出现「应用集成」子菜单
    • 点击跳转 /system/app
    • 中文/英文切换均正常
  • Step 4: 验证 git log

Run:

git log --oneline -10

Expected: 看到 6 个提交:

  • feat(sysApp): add sysAppService extending BaseService
  • feat(sysApp): export sysAppService from system service index
  • feat(sysApp): add systemApp i18n key
  • feat(sysApp): register /system/app route in system router
  • feat(sysApp): add SysApp list page
  • feat(sysApp): add SysApp form dialog

回滚计划

如果出现问题,按以下顺序回滚:

  1. 回滚 Task 6: git revert <task6-commit>(删除表单)
  2. 回滚 Task 5: git revert <task5-commit>(删除列表页)
  3. 回滚 Task 4: git revert <task4-commit>(取消路由)
  4. 回滚 Task 3: git revert <task3-commit>(删除 i18n
  5. 回滚 Task 2: git revert <task2-commit>(取消导出)
  6. 回滚 Task 1: git revert <task1-commit>(删除 Service

如需完全回滚:git reset --hard <task0-commit>


测试清单

静态检查

  • pnpm --filter admin-ui type-check 0 errors
  • pnpm --filter admin-ui lint 0 errors

列表功能

  • 列表加载正常
  • 4 个查询条件均生效
  • 分页正常
  • 列设置可隐藏/显示列
  • 导出 CSV 成功

表单功能

  • 新增:填写必填项 → 提交 → 列表出现新行
  • 编辑:弹窗加载详情 → 修改 → 提交 → 列表更新
  • 必填校验:name/ownerType/platform 未填时拦截
  • JSON 校验:certificates/extra 非法格式拦截
  • 敏感字段:appSecret/appKey/aesKey 留空不修改

交互

  • 启停:状态 Switch 切换正常,失败时回滚
  • 单删:删除确认 → 行消失
  • 批删:选中多行 → 批量删除 → 全部消失
  • 弹窗:宽度 760px,4 Tab 可切换

菜单与导航

  • 侧边栏「系统管理 → 应用集成」菜单显示
  • 路由跳转正常
  • 中英文 i18n 切换正常

脱敏

  • 列表中无任何明文密钥字段
  • 列表接口返回的 records 不含 appSecret/appKey/aesKey 明文

关联信息

  • Spec 文档: docs/superpowers/specs/2026-06-07-sysapp-management-design.md
  • 工单: rui/rui-frontend#4
  • 后端 Issue: rui/rui-framework#4(文件上传接口依赖,本期不阻塞)
  • 参考实现: admin-ui/src/views/system/oauth2-client/Index.vue + OAuth2ClientFormDialog.vue

计划状态: 待评审
下一步: 用户评审通过后,使用 superpowers-subagent-driven-developmentsuperpowers-executing-plans 执行