diff --git a/docs/superpowers/plans/2026-10-15-mp-webview-crm-contract.md b/docs/superpowers/plans/2026-10-15-mp-webview-crm-contract.md new file mode 100644 index 000000000..f086051fb --- /dev/null +++ b/docs/superpowers/plans/2026-10-15-mp-webview-crm-contract.md @@ -0,0 +1,681 @@ +# 小程序 WebView · CRM 合同 Implementation Plan + +> **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:** 在小程序工作台开放「CRM管理 → 合同管理 / 合同审批」,通过既有 `mp-webview` 壳打开 `web-admin` `/#/m/crm/contract*`,提供管理闭环(列表/详情/增改删/提交审批/复制/作废·恢复/上传扫描件)与审批闭环(待审/已审 + 单条与批量通过驳回),不含调价函、打印/PDF、导出、执行、续签。 + +**Architecture:** 对齐报价 WebView(ticket → exchange → `MpWebviewShell`)。新建 `views/m/crm/contract/*`,样式 `mp-webview.scss`。业务 API 复用 `@/api/crm/contract`。桌面合同表单增加 `variant="page"`。列表支持多选批量。菜单 SQL 幂等写入 `MINI_CRM_CONTRACT*`,按 Web `crm:contract:list` / `crm:contract:approve` 分别补绑。 + +**Tech Stack:** Vue 3 · Vue Router hash · Element Plus · Pinia · uni-app web-view · MySQL · Node test(`pnpm exec tsx --test`) + +**Spec:** `docs/superpowers/specs/2026-10-15-mp-webview-crm-contract-design.md` + +--- + +## 文件结构 + +| 路径 | 职责 | +|------|------| +| `web-admin/src/utils/mp-crm-contract-card.ts` | Tab/查询/卡片纯函数 | +| `web-admin/src/utils/mp-crm-contract-card.test.ts` | 单测 | +| `web-admin/src/utils/mp-webview.ts` | `MP_CRM_CONTRACT_LIST_PATH`、`MP_CRM_CONTRACT_APPROVAL_PATH` | +| `web-admin/src/views/m/crm/contract/MpContractList.vue` | 共享列表(含多选批量) | +| `web-admin/src/views/m/crm/contract/index.vue` | 管理壳 | +| `web-admin/src/views/m/crm/contract/approval.vue` | 审批壳 | +| `web-admin/src/views/m/crm/contract/detail.vue` | 详情 + 动作 | +| `web-admin/src/views/m/crm/contract/form.vue` | 表单路由壳 | +| `web-admin/src/views/crm/contract/components/ContractFormDialog.vue` | `variant=page` | +| `web-admin/src/router/routes/staticRoutes.ts` | `/m/crm/contract*` | +| `miniprogram/config/workbench-menus.js` | 两项入口 | +| `miniprogram/config/api.js` | `WORKBENCH_ROUTE_MAP` | +| `docs/sql/migrations/2026-10-15-mini-crm-contract-webview-menus.sql` | 菜单 + 补绑 | +| `docs/web-admin-mp-webview-modules.md` | 合同对照行 | + +**明确不改:** mp-webview 鉴权协议、后端合同业务语义、桌面合同默认 dialog 行为、调价函。 + +**查询语义:** + +| mode | view | 请求 | +|------|------|------| +| manage | `all` | `excludeStatus=invalid` | +| manage | `approving` | `status=approving` | +| manage | `approved` | `status=approved` | +| approve | `submitted` | `pendingApprovalUserId=currentUserId` | +| approve | `approved` | `approvedByUserId=currentUserId` | + +分页:`pageNum` / `pageSize`。 + +--- + +### Task 1: 列表卡片纯函数 + 单测 + +**Files:** +- Create: `web-admin/src/utils/mp-crm-contract-card.ts` +- Create: `web-admin/src/utils/mp-crm-contract-card.test.ts` + +- [ ] **Step 1: 写失败单测** + +```ts +// web-admin/src/utils/mp-crm-contract-card.test.ts +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { + MP_CONTRACT_MANAGE_TABS, + MP_CONTRACT_APPROVE_TABS, + buildContractListQuery, + buildContractCard, + formatContractMoney +} from './mp-crm-contract-card' + +describe('tabs', () => { + it('manage all/approving/approved; approve submitted/approved', () => { + assert.deepEqual( + MP_CONTRACT_MANAGE_TABS.map((x) => x.value), + ['all', 'approving', 'approved'] + ) + assert.deepEqual( + MP_CONTRACT_APPROVE_TABS.map((x) => x.value), + ['submitted', 'approved'] + ) + }) +}) + +describe('buildContractListQuery', () => { + it('manage all excludes invalid and trims keyword', () => { + assert.deepEqual( + buildContractListQuery({ + mode: 'manage', + view: 'all', + pageNum: 1, + pageSize: 20, + keyword: ' 合同 ', + currentUserId: 'u1' + }), + { pageNum: 1, pageSize: 20, keyword: '合同', excludeStatus: 'invalid' } + ) + }) + + it('manage approving sets status only', () => { + const q = buildContractListQuery({ + mode: 'manage', + view: 'approving', + pageNum: 1, + pageSize: 20, + keyword: '', + currentUserId: 'u1' + }) + assert.equal(q.status, 'approving') + assert.equal('pendingApprovalUserId' in q, false) + }) + + it('manage approved sets status approved', () => { + const q = buildContractListQuery({ + mode: 'manage', + view: 'approved', + pageNum: 1, + pageSize: 20, + keyword: '', + currentUserId: 'u1' + }) + assert.equal(q.status, 'approved') + assert.equal('approvedByUserId' in q, false) + }) + + it('approve submitted uses pendingApprovalUserId', () => { + const q = buildContractListQuery({ + mode: 'approve', + view: 'submitted', + pageNum: 1, + pageSize: 20, + keyword: '', + currentUserId: 'u9' + }) + assert.equal(q.pendingApprovalUserId, 'u9') + }) + + it('approve approved uses approvedByUserId', () => { + const q = buildContractListQuery({ + mode: 'approve', + view: 'approved', + pageNum: 1, + pageSize: 20, + keyword: 'x', + currentUserId: 'u9' + }) + assert.equal(q.approvedByUserId, 'u9') + assert.equal(q.keyword, 'x') + }) +}) + +describe('buildContractCard', () => { + it('maps fields', () => { + const card = buildContractCard({ + id: 'c1', + contractNo: 'HT001', + contractTitle: '硫酸销售合同', + customerName: '甲公司', + amount: 1000, + freight: 50, + barrelDeposit: 20, + status: 'draft', + signDate: '2026-10-01' + }) + assert.equal(card.id, 'c1') + assert.equal(card.contractNo, 'HT001') + assert.equal(card.amountText, '¥1,070.00') + assert.equal(card.status, 'draft') + }) +}) + +describe('formatContractMoney', () => { + it('formats and handles empty', () => { + assert.equal(formatContractMoney(1000), '¥1,000.00') + assert.equal(formatContractMoney(undefined), '—') + }) +}) +``` + +- [ ] **Step 2: 跑测确认失败** + +```bash +cd web-admin && pnpm exec tsx --test src/utils/mp-crm-contract-card.test.ts +``` + +Expected: FAIL / module not found + +- [ ] **Step 3: 实现纯函数** + +```ts +// web-admin/src/utils/mp-crm-contract-card.ts +export type MpContractMode = 'manage' | 'approve' +export type MpContractManageView = 'all' | 'approving' | 'approved' +export type MpContractApproveView = 'submitted' | 'approved' +export type MpContractView = MpContractManageView | MpContractApproveView + +export const MP_CONTRACT_MANAGE_TABS: Array<{ value: MpContractManageView; label: string }> = [ + { value: 'all', label: '全部' }, + { value: 'approving', label: '待审批' }, + { value: 'approved', label: '已审批' } +] + +export const MP_CONTRACT_APPROVE_TABS: Array<{ value: MpContractApproveView; label: string }> = [ + { value: 'submitted', label: '待审批' }, + { value: 'approved', label: '已审批' } +] + +export function formatContractMoney(amount?: number | null): string { + if (amount === undefined || amount === null || Number.isNaN(Number(amount))) return '—' + return `¥${Number(amount).toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })}` +} + +export function buildContractListQuery(input: { + mode: MpContractMode + view: MpContractView + pageNum: number + pageSize: number + keyword: string + currentUserId: string +}): Record { + const params: Record = { + pageNum: input.pageNum, + pageSize: input.pageSize + } + const kw = String(input.keyword || '').trim() + if (kw) params.keyword = kw + + if (input.mode === 'manage') { + if (input.view === 'all') params.excludeStatus = 'invalid' + else if (input.view === 'approving') params.status = 'approving' + else params.status = 'approved' + return params + } + + if (input.view === 'submitted') { + params.pendingApprovalUserId = input.currentUserId + } else { + params.approvedByUserId = input.currentUserId + } + return params +} + +export function buildContractCard(row: { + id: string | number + contractNo?: string + contractTitle?: string + customerName?: string + amount?: number + freight?: number + barrelDeposit?: number + status?: string + signDate?: string +}) { + const total = + (Number(row.amount) || 0) + (Number(row.freight) || 0) + (Number(row.barrelDeposit) || 0) + return { + id: String(row.id), + contractNo: row.contractNo || '—', + contractTitle: row.contractTitle || '—', + customerName: row.customerName || '—', + amountText: formatContractMoney(total), + status: row.status || '', + signDate: row.signDate || '—' + } +} +``` + +状态 Tag 在页面用 `dictStore` `crm_contract_status`。 + +- [ ] **Step 4: 跑测确认通过** + +```bash +cd web-admin && pnpm exec tsx --test src/utils/mp-crm-contract-card.test.ts +``` + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/utils/mp-crm-contract-card.ts web-admin/src/utils/mp-crm-contract-card.test.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp CRM contract card query helpers. + +EOF +)" +``` + +--- + +### Task 2: 菜单 SQL + +**Files:** +- Create: `docs/sql/migrations/2026-10-15-mini-crm-contract-webview-menus.sql` + +- [ ] **Step 1: 写入幂等 SQL**(结构对齐报价脚本) + +```sql +-- 小程序:CRM管理 → 合同管理 / 合同审批(WebView) +-- list 随 CRM3000000000000000000000000020 (crm:contract:list) +-- approve 随 CRM3000000000000000000000000021 (crm:contract:approve) +SET NAMES utf8mb4; + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_CRM_ROOT', 'CRM管理', '1', 'M', '/mini/crm', NULL, NULL, + 1, '1', '1', 'shanxia2.0', 2, '小程序CRM管理', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_ROOT' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_CRM_CONTRACT', '合同管理', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL, + 'mini:crm:contract:list', + 1, '1', '1', 'shanxia2.0', 4, '小程序 WebView CRM 合同管理', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_CONTRACT' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_CRM_CONTRACT_APPROVAL', '合同审批', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL, + 'mini:crm:contract:approve', + 1, '1', '1', 'shanxia2.0', 5, '小程序 WebView CRM 合同审批', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_CONTRACT_APPROVAL' AND `del_flag` = 0 +); + +INSERT INTO `sys_role_menu` ( + `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + MD5(CONCAT(r.id, '|', m.menu_id, '|mini-crm-contract-webview|v1')), + r.id, + m.menu_id, + IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'), + 0, + '小程序CRM合同管理:随 Web crm:contract:list 补绑', + 'system', '', NOW(), NULL, NOW(), 0 +FROM `sys_role` r +JOIN `sys_role_menu` web + ON web.role_id = r.id + AND web.menu_id = 'CRM3000000000000000000000000020' + AND web.del_flag = 0 +JOIN ( + SELECT '1' AS menu_id UNION ALL + SELECT 'MINI_CRM_ROOT' UNION ALL + SELECT 'MINI_CRM_CONTRACT' +) m +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_menu` x + WHERE x.role_id = r.id AND x.menu_id = m.menu_id AND x.del_flag = 0 + ); + +INSERT INTO `sys_role_menu` ( + `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + MD5(CONCAT(r.id, '|', m.menu_id, '|mini-crm-contract-approval-webview|v1')), + r.id, + m.menu_id, + IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'), + 0, + '小程序CRM合同审批:随 Web crm:contract:approve 补绑', + 'system', '', NOW(), NULL, NOW(), 0 +FROM `sys_role` r +JOIN `sys_role_menu` web + ON web.role_id = r.id + AND web.menu_id = 'CRM3000000000000000000000000021' + AND web.del_flag = 0 +JOIN ( + SELECT '1' AS menu_id UNION ALL + SELECT 'MINI_CRM_ROOT' UNION ALL + SELECT 'MINI_CRM_CONTRACT_APPROVAL' +) m +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_menu` x + WHERE x.role_id = r.id AND x.menu_id = m.menu_id AND x.del_flag = 0 + ); +``` + +- [ ] **Step 2: Commit(本 Task 不连库执行)** + +```bash +git add docs/sql/migrations/2026-10-15-mini-crm-contract-webview-menus.sql +git commit -m "$(cat <<'EOF' +docs(sql): add miniprogram CRM contract webview menus and role bind. + +EOF +)" +``` + +--- + +### Task 3: 静态路由 + 路径常量 + 空壳 + +**Files:** +- Modify: `web-admin/src/utils/mp-webview.ts` +- Modify: `web-admin/src/router/routes/staticRoutes.ts` +- Create shells under `web-admin/src/views/m/crm/contract/` + +- [ ] **Step 1: 常量** + +```ts +export const MP_CRM_CONTRACT_LIST_PATH = '/m/crm/contract' +export const MP_CRM_CONTRACT_APPROVAL_PATH = '/m/crm/contract/approval' +``` + +- [ ] **Step 2: 路由(catch-all 前;顺序固定)** + +```ts + { + path: '/m/crm/contract', + name: 'MpCrmContractList', + component: () => import('@views/m/crm/contract/index.vue'), + meta: { title: '合同管理', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/contract/approval', + name: 'MpCrmContractApproval', + component: () => import('@views/m/crm/contract/approval.vue'), + meta: { title: '合同审批', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/contract/new', + name: 'MpCrmContractNew', + component: () => import('@views/m/crm/contract/form.vue'), + meta: { title: '新增合同', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/contract/:id/edit', + name: 'MpCrmContractEdit', + component: () => import('@views/m/crm/contract/form.vue'), + meta: { title: '编辑合同', isHideTab: true, isMpWebview: true } + }, + { + path: '/m/crm/contract/:id', + name: 'MpCrmContractDetail', + component: () => import('@views/m/crm/contract/detail.vue'), + meta: { title: '合同详情', isHideTab: true, isMpWebview: true } + }, +``` + +- [ ] **Step 3: 最小壳 vue**(`MpWebviewShell` + title;同报价 Task 3) + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): register /m/crm/contract static routes. + +EOF +)" +``` + +--- + +### Task 4: 共享列表 + 多选批量 + +**Files:** +- Create: `web-admin/src/views/m/crm/contract/MpContractList.vue` +- Replace: `index.vue` / `approval.vue` 薄壳 + +对照:`views/m/crm/quote/MpQuoteList.vue`,并增加多选。 + +- [ ] **Step 1: 实现 `MpContractList.vue`** + +1. Prop `mode: 'manage' | 'approve'` +2. `@ready` 后 `listContract(buildContractListQuery(...))`;解析 `list|rows` + `total` +3. `resolveCurrentUserId` 从 `MpQuoteList` / `ContractListPage` 复制 +4. Tab + keyword + 卡片(`buildContractCard` + dict 状态 Tag) +5. **多选**:卡片左侧 checkbox;`selectedIds`;底栏「已选 N」 +6. manage:Speed Dial 新增;批量「提交审批」(打开 Sheet,仅草稿 id);可选批量作废(逐条 `invalidContract`,跳过不可作废) +7. approve:批量通过/驳回(Sheet;驳回意见必填);对每条解析 pending approval id(对齐桌面:`listContractApproval` 找当前用户 pending),调用 `approvePass` / `approveReject`(打开 `api/crm/contract/approval.ts` 核对字段) +8. 详情:`/m/crm/contract/${id}?from=${mode}` +9. `goToMiniWorkbench()`;`mp-webview.scss`;403→无权限 +10. **禁止**:导出、打印、执行、续签、facet + +批量提交 Sheet:`getContractApproverSetting` 预填 + `fetchUserOptions`;`batchSubmitApproval({ ids, approverIds, remark })`。 + +- [ ] **Step 2: 薄壳** + +```vue + +``` + +approval:`mode="approve"`。 + +- [ ] **Step 3: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp webview CRM contract list pages. + +EOF +)" +``` + +--- + +### Task 5: 详情页动作 + +**Files:** +- Create/Modify: `web-admin/src/views/m/crm/contract/detail.vue` + +对照:`views/m/crm/quote/detail.vue` + 桌面 `ContractDetailDialog` 动作集合(排除执行/续签/打印)。 + +- [ ] **Step 1: 加载与展示** + +`getContract` + 明细 + `listContractApproval`;分区展示;扫描件 URL 可预览。 + +- [ ] **Step 2: 管理底栏** + +| 条件 | 按钮 | +|------|------| +| draft | 提交审批、编辑、删除(`canDeleteByCreator`)、作废、复制、上传扫描件 | +| 可作废状态(draft/approving/approved/executing 等,对齐桌面) | 作废、复制、上传;可编辑则编辑 | +| invalid | 作废恢复、复制 | +| 永不出现 | 执行、续签、打印、审批通过驳回 | + +动作 API:`submitApproval`、`invalidContract`、`restoreContract`、`delContract`、`uploadFile`/`uploadAttachments`(对照 `UploadDialog.vue`)、复制 → `/new?copyFrom=`。 + +- [ ] **Step 3: 审批底栏** + +待我审 → 通过/驳回 Sheet → `approvePass`/`approveReject`。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): add mp webview CRM contract detail actions. + +EOF +)" +``` + +--- + +### Task 6: 表单 `variant="page"` + +**Files:** +- Modify: `web-admin/src/views/crm/contract/components/ContractFormDialog.vue`(及内部 Panel 若有) +- Create/Modify: `web-admin/src/views/m/crm/contract/form.vue` + +对照报价 `QuoteFormDialog` page 模式与 `views/m/crm/quote/form.vue`。 + +- [ ] **Step 1: 默认 `variant=dialog`;page 模式无 el-dialog、单列布局、sticky 底栏(取消/保存)** +- [ ] **Step 2: form.vue 支持 add / edit / copyFrom** +- [ ] **Step 3: 验证全字段保存** +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): add page variant for contract form in mp webview. + +EOF +)" +``` + +--- + +### Task 7: 工作台入口 + +**Files:** +- Modify: `miniprogram/config/workbench-menus.js` +- Modify: `miniprogram/config/api.js` + +- [ ] **Step 1: CRM menus 增加两项** + +```js +{ + name: '合同管理', + enabled: true, + code: 'crm-contract-webview', + icon: '📄', + perms: 'mini:crm:contract:list' +}, +{ + name: '合同审批', + enabled: true, + code: 'crm-contract-approval-webview', + icon: '✅', + perms: 'mini:crm:contract:approve' +} +``` + +`desc` 更新含「合同」。**不要**加调价函。 + +- [ ] **Step 2: WORKBENCH_ROUTE_MAP** + +```js +'crm-contract-webview': + PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/contract'), +'crm-contract-approval-webview': + PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/contract/approval') +``` + +- [ ] **Step 3: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(miniprogram): enable CRM contract webview workbench entries. + +EOF +)" +``` + +--- + +### Task 8: 文档 + 回归清单 + +**Files:** +- Modify: `docs/web-admin-mp-webview-modules.md` + +- [ ] **Step 1: 增加 CRM 合同对照** + +```markdown +## CRM 合同对照 + +- 管理 `/m/crm/contract`,审批 `/m/crm/contract/approval`,详情 `/:id`,表单 `/new` `/:id/edit` +- 工作台 code `crm-contract-webview` / `crm-contract-approval-webview` +- perms `mini:crm:contract:list` / `mini:crm:contract:approve` +- 支持单条与批量提交/审批;含复制、作废/恢复、上传扫描件 +- 不做:调价函、打印/PDF、导出、执行、续签 +``` + +文首「已落地」补上合同。 + +- [ ] **Step 2: 回归(spec §10);测试库执行 Task 2 SQL** + +- [ ] **Step 3: Commit** + +```bash +git commit -m "$(cat <<'EOF' +docs: document CRM contract as mp webview module. + +EOF +)" +``` + +--- + +## Spec 覆盖自检 + +| Spec 项 | Task | +|---------|------| +| 嵌套 H5 | 3–7 | +| 管理闭环 B + 批量 | 4, 5 | +| 全字段表单 | 6 | +| 不做调价函/打印/导出/执行/续签 | 4–8 禁止 | +| 菜单 + 补绑 | 2 | +| 工作台 | 7 | +| 文档 | 8 | + +--- + +## 执行说明 + +当前分支 V1.5 可直接实现(与报价同模式)。每 Task 完成后 commit。后端无新接口。测试库执行菜单 SQL。