# 阶段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 ``` - [ ] **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 ``` - [ ] **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 ``` - [ ] **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 ``` - [ ] **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 ``` - [ ] **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 ``` - [ ] **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` 消费处字段名一致