diff --git a/docs/superpowers/plans/2026-07-09-phase3-customer-management.md b/docs/superpowers/plans/2026-07-09-phase3-customer-management.md new file mode 100644 index 0000000..34ec348 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-phase3-customer-management.md @@ -0,0 +1,1592 @@ +# 阶段3客户管理(个人客户+企业客户) 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:** 实现个人客户与企业客户的查询/新增/编辑/查看功能,对接真实后端接口(`/api/customer-info/personal/*`、`/api/enterprise-customer/*`),并提供可复用的渠道选择、身份证OCR上传、营业执照上传、个人客户选择弹窗组件。 + +**Architecture:** 沿用阶段1/2已确立的架构:Vue3 ` +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/components/ChannelSelect.vue +git commit -m "feat: 新增渠道选择器组件" +``` + +--- + +### Task 5: 身份证上传+OCR组件 `IdCardUpload.vue` + +**Files:** +- Create: `src/components/IdCardUpload.vue` + +**Interfaces:** +- Consumes: `ocrIdCardApi`, `uploadCustomerFileApi` from `@/api/customer`(Task 2) +- Produces: ``(供 Task 9 使用) + +- [ ] **Step 1: 创建组件** + +```vue + + + + + + + + + {{ label }} + + + 请先选择所属渠道 + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/components/IdCardUpload.vue +git commit -m "feat: 新增身份证上传+OCR识别组件" +``` + +--- + +### Task 6: 营业执照上传组件 `LicenseUpload.vue` + +**Files:** +- Create: `src/components/LicenseUpload.vue` + +**Interfaces:** +- Consumes: `uploadCustomerFileApi` from `@/api/customer` +- Produces: ``(供 Task 11 使用) + +- [ ] **Step 1: 创建组件** + +```vue + + + + + + + + + 上传营业执照 + + + 请先选择所属渠道 + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/components/LicenseUpload.vue +git commit -m "feat: 新增营业执照上传组件" +``` + +--- + +### Task 7: 个人客户选择弹窗 `PersonalCustomerPicker.vue` + +**Files:** +- Create: `src/components/PersonalCustomerPicker.vue` + +**Interfaces:** +- Consumes: `fetchPersonalCustomerListApi` from `@/api/customer`(Task 2)、`ProTable.vue`、`OrgTreeSelect.vue`(已有) +- Produces: 模板 ref 暴露 `show()` 方法打开弹窗;选中后 `emit('select', record)`,`record` 字段含 `id,customerCode,customerName,certificateType,certificateNumber,mobilePhone`(供 Task 9 法定代表人选择、Task 11 股东高管选择使用) + +- [ ] **Step 1: 创建组件** + +```vue + + + + + + + + + + + + + + + + + {{ index + 1 }} + + 选择 + + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/components/PersonalCustomerPicker.vue +git commit -m "feat: 新增个人客户选择弹窗组件" +``` + +--- + +### Task 8: 个人客户列表页 `PersonalList.vue` + +**Files:** +- Create: `src/views/customer/personal/PersonalList.vue` + +**Interfaces:** +- Consumes: `fetchPersonalCustomerListApi` from `@/api/customer`,`fetchOrgListAllApi` from `@/api/system`,`ProTable.vue`,`StatusTag.vue`,`OrgTreeSelect.vue` +- Produces: 路由跳转 `/customer/personal/create`、`/customer/personal/edit?id=`、`/customer/personal/detail?id=`(供 Task 9 消费) + +- [ ] **Step 1: 创建列表页** + +```vue + + + + + + + + + + + + + + + + + + + + + + 新增 + + + + + {{ index + 1 }} + + {{ orgNameMap[record.organizationId] || record.organizationId }} + + 身份证 + + + + + + 查看 + 编辑 + + + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/views/customer/personal/PersonalList.vue +git commit -m "feat: 新增个人客户列表页" +``` + +--- + +### Task 9: 个人客户表单页 `PersonalForm.vue` + +**Files:** +- Create: `src/views/customer/personal/PersonalForm.vue` + +**Interfaces:** +- Consumes: `fetchPersonalCustomerDetailApi/createPersonalCustomerApi/updatePersonalCustomerApi` (Task 2)、`ChannelSelect.vue`(Task 4)、`IdCardUpload.vue`(Task 5)、`mapOcrIdCardResult`(Task 3)、`idCardValidatorRule/phoneValidatorRule`(Task 1 / 已有) +- Produces: 无(叶子页面),路由 query `mode`/`id` + +- [ ] **Step 1: 创建表单页** + +```vue + + + + + + + + + + + + + (ocrStatus = status)" + /> + (ocrStatus = status)" + /> + + + + + + + + + + + + + + + + + + + + + 男 + 女 + 未知 + + + + + + + + + + + 证件已过期,请更换有效证件 + + + + + + + + + + + + + + + + + 提交 + 取消 + + + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/views/customer/personal/PersonalForm.vue +git commit -m "feat: 新增个人客户新增/编辑/查看表单页" +``` + +--- + +### Task 10: 企业客户列表页 `EnterpriseList.vue` + +**Files:** +- Create: `src/views/customer/enterprise/EnterpriseList.vue` + +**Interfaces:** +- Consumes: `fetchEnterpriseCustomerPageApi` from `@/api/customer`,`fetchOrgListAllApi` from `@/api/system` +- Produces: 路由跳转 `/customer/enterprise/create`、`/customer/enterprise/edit?id=`、`/customer/enterprise/detail?id=`(供 Task 11 消费) + +- [ ] **Step 1: 创建列表页** + +```vue + + + + + + + + + + + + + + + + + + + 新增 + + + + + {{ index + 1 }} + + {{ orgNameMap[record.organizationId] || record.organizationId }} + + + + + + + 查看 + 编辑 + + + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/views/customer/enterprise/EnterpriseList.vue +git commit -m "feat: 新增企业客户列表页" +``` + +--- + +### Task 11: 企业客户表单页 `EnterpriseForm.vue` + +**Files:** +- Create: `src/views/customer/enterprise/EnterpriseForm.vue` + +**Interfaces:** +- Consumes: `fetchEnterpriseCustomerDetailApi/createEnterpriseCustomerApi/updateEnterpriseCustomerApi/fetchPersonalCustomerDetailApi` (Task 2)、`ChannelSelect.vue`(Task 4)、`LicenseUpload.vue`(Task 6)、`PersonalCustomerPicker.vue`(Task 7)、`businessLicenseValidatorRule/phoneValidatorRule`(Task 1) +- Produces: 无(叶子页面) + +- [ ] **Step 1: 创建表单页** + +```vue + + + + + + + + + + + + + 编辑模式下无法读取历史营业执照图片,如需变更请重新上传 + + + + + + + + + + + + + + + + + + + + 证件已过期,请更换有效证件 + + + + + + + + + + + + + + {{ legalRepName }} + 选择 + + + + + + + {{ record.customerName }} + + + 股东 + 高管 + 受益所有人 + + + {{ record.certificateNumber }} + + 删除 + + + + 添加股东/高管 + + + 股东高管信息仅支持新增时登记,当前接口不支持查询或修改,如需变更请联系技术支持核实数据 + + + + + 提交 + 取消 + + + + + + + + + + + +``` + +- [ ] **Step 2: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 3: Commit** + +```bash +git add src/views/customer/enterprise/EnterpriseForm.vue +git commit -m "feat: 新增企业客户新增/编辑/查看表单页" +``` + +--- + +### Task 12: 路由注册 + +**Files:** +- Modify: `src/router/componentRegistry.js` +- Modify: `src/router/dynamic.js` + +**Interfaces:** +- Produces: `extraChildRoutes` 数组导出(供 `dynamic.js` 消费),使 `/customer/personal/{create,edit,detail}`、`/customer/enterprise/{create,edit,detail}` 六个非菜单路由可用 + +- [ ] **Step 1: `componentRegistry.js` 新增菜单路由映射与非菜单子路由表** + +```js +// src/router/componentRegistry.js +// routePath -> src/views 下组件相对路径(不含 .vue 扩展名)的手工映射表。 +// 权限字典由权限管理页面动态维护,前端页面组件仍需要开发时手工注册到这里才能被路由解析。 +export const routePathComponentMap = { + '/system/user': 'system/user/UserList', + '/system/role': 'system/role/RoleList', + '/system/org': 'system/org/OrgList', + '/system/permission': 'system/permission/PermissionList', + '/base-config/channel': 'base-config/channel/ChannelList', + '/base-config/manager': 'base-config/manager/ManagerList', + '/customer/personal': 'customer/personal/PersonalList', + '/customer/enterprise': 'customer/enterprise/EnterpriseList' +} + +// 新增/编辑/查看等表单页不是后端菜单节点(不出现在侧边栏),但仍需要注册为 +// MainLayout 下的可访问路由,由列表页通过 router.push 跳转进入。 +export const extraChildRoutes = [ + { path: 'customer/personal/create', name: 'customer-personal-create', component: 'customer/personal/PersonalForm' }, + { path: 'customer/personal/edit', name: 'customer-personal-edit', component: 'customer/personal/PersonalForm' }, + { path: 'customer/personal/detail', name: 'customer-personal-detail', component: 'customer/personal/PersonalForm' }, + { path: 'customer/enterprise/create', name: 'customer-enterprise-create', component: 'customer/enterprise/EnterpriseForm' }, + { path: 'customer/enterprise/edit', name: 'customer-enterprise-edit', component: 'customer/enterprise/EnterpriseForm' }, + { path: 'customer/enterprise/detail', name: 'customer-enterprise-detail', component: 'customer/enterprise/EnterpriseForm' } +] +``` + +- [ ] **Step 2: `dynamic.js` 的 `installDynamicRoutes` 追加非菜单子路由** + +将 `installDynamicRoutes` 函数改为(文件顶部 `import` 增加 `extraChildRoutes`): + +```js +import MainLayout from '@/layouts/MainLayout.vue' +import { routePathComponentMap, extraChildRoutes } from './componentRegistry' + +// ...(resolveComponent / collectFirstPath / buildRouteRecords 保持不变)... + +export function installDynamicRoutes(router, menuTree) { + const children = buildRouteRecords(menuTree) + extraChildRoutes.forEach((route) => { + children.push({ + path: route.path, + name: route.name, + component: resolveComponent(route.component) + }) + }) + router.addRoute({ + path: '/', + component: MainLayout, + redirect: collectFirstPath(menuTree) || '/403', + children + }) +} +``` + +- [ ] **Step 3: 验证构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 4: Commit** + +```bash +git add src/router/componentRegistry.js src/router/dynamic.js +git commit -m "feat: 注册客户管理列表与表单路由" +``` + +--- + +### Task 13: 缺失后端接口文档更新 + 整体验证 + +**Files:** +- Modify: `缺失的后端接口.md` + +**Interfaces:** +- 无代码接口,仅文档 + +- [ ] **Step 1: 在 `缺失的后端接口.md` 的 "Part 1:已提供接口契约速查" 追加"客户管理"一节** + +参照文件现有小节格式(表格形式,列 `接口` / `说明`),在系统管理/基础配置节之后追加: + +```markdown +### 1.5 客户管理 —— 个人客户 + +| 接口 | 说明 | +| --- | --- | +| `POST /api/customer-info/personal/list` | `{organizationIdEq,customerCodeEq,customerNameLike,certificateNumberEq,page,pageSize}`,响应 `records: IndividualCustomerListVo[]`(仅 `id,organizationId,customerName,certificateType,certificateNumber,mobilePhone,ocrStatus,customerCode`,不含性别/创建人/创建时间) | +| `POST /api/customer-info/personal/detail` | `{id}` 或 `{customerCode}`,响应 `IndividualCustomerDetailVo` | +| `POST /api/customer-info/personal/create` | `CreateIndividualCustomerBto`,响应 `data`=新建id | +| `POST /api/customer-info/personal/update` | `UpdateIndividualCustomerBto` | +| `POST /api/customer-info/personal/ocr-idcard` | `{fileBase64,channelNo,fileType('01'\|'02')}`,响应 `OcrIdCardResultVo` | +| `POST /api/customer-info/personal/upload-file` | `{fileBase64,channelNo}`,响应 `{fileNo}`,个人身份证与企业营业执照图片均调用此通用接口 | + +### 1.6 客户管理 —— 企业客户 + +| 接口 | 说明 | +| --- | --- | +| `POST /api/enterprise-customer/page` | `{page,pageSize,enterpriseName,businessLicense,mobilePhone}`,响应 `records: EnterpriseCustomerVo[]` | +| `POST /api/enterprise-customer/detail` | `{id}`,响应 `EnterpriseCustomerDetailVo` | +| `POST /api/enterprise-customer/create` | `{createEnterpriseCustomerBto,enableOcr}` | +| `POST /api/enterprise-customer/update` | `UpdateEnterpriseCustomerBto` | +``` + +在 "Part 2" 追加以下条目(接续现有编号): + +```markdown +- **个人客户/企业客户均无删除接口**,列表页暂不提供删除入口 +- **营业执照无同步OCR识别接口**,`enterprise-customer/create` 的 `enableOcr` 参数用途/时序未文档化,前端按"手动填写+提交时告知后端可异步识别"实现 +- **企业客户股东高管信息(`enterpriseRelatedPersonBtoList`)只能在新增时一次性提交**,`update`/`detail` 接口均不支持读取或修改,编辑模式下该区块前端替换为提示文案,不可编辑 +- **企业客户 `businessLicenseFileNo` 编辑/详情接口读取不到**,编辑模式无法回显已上传的营业执照图片 +- **企业客户分页查询不支持按所属机构/企业编号过滤**,仅支持企业名称/营业执照号/手机号三项 +- **个人客户 `customerCode`(客户编号)生成机制未文档化**,前端创建时不传该字段,交由后端生成 +``` + +- [ ] **Step 2: Commit 文档** + +```bash +git add "缺失的后端接口.md" +git commit -m "docs: 补充客户管理模块接口契约与能力缺口记录" +``` + +- [ ] **Step 3: 整体构建验证** + +Run: `npm run build` +Expected: 构建成功,无报错 + +- [ ] **Step 4: 启动验证** + +Run: `npm run dev`(如端口被占用则更换端口重试,不终止占用进程) +手动检查: +1. 浏览器访问登录页,登录后确认侧边栏出现"客户管理"菜单(需要后端权限管理已配置对应菜单节点,若未配置则此步骤暂时跳过,只验证直接访问路由 `/customer/personal`、`/customer/enterprise` 不报 404/白屏) +2. 个人客户列表页:查询区渲染正常,点击"新增"跳转到表单页且无控制台报错 +3. 表单页选择渠道后,点击身份证上传区域触发文件选择框(无需真实调后端验证,只需确认组件不报错) +4. 企业客户列表/表单页同上检查 +Expected: 无 Vue 报错、无 404、页面正常渲染 + +- [ ] **Step 5: 终止开发服务器** + +确认启动无误后按 `Ctrl+C` 或结束对应进程,不占用端口 + +## Self-Review 记录 + +- **Spec 覆盖检查**:设计文档第2-7节(接口契约/页面架构/交互设计/校验/业务规则/能力缺口)分别对应 Task 2、Task 8-12、Task 4-7、Task 1、Task 9/11、Task 13,无遗漏 +- **占位符扫描**:全文无 TBD/TODO,标注的"以实测为准"均附带具体的防御性实现(如 `normalizeOcrDate` 解析失败返回空串,不是空实现) +- **类型一致性**:`IdCardUpload`/`LicenseUpload` 的 `update:fileNo` 事件与 `PersonalForm`/`EnterpriseForm` 里 `v-model:file-no` 绑定的字段名(`idCardFrontFileNo`/`idCardBackFileNo`/`businessLicenseFileNo`)一致;`PersonalCustomerPicker` 的 `select` 事件 payload 字段(`id,customerCode,customerName,certificateType,certificateNumber,mobilePhone`)与 `EnterpriseForm` 消费处字段名一致