20 KiB
阶段3设计:客户管理(个人客户 + 企业客户)
- 状态: 用户已授权自主推进(批量决策已确认,不再逐项等待审批),自查通过后直接进入 writing-plans
- 上级文档:
2026-07-08-fryl-frontend-overall-design.md(总体架构,阶段3=客户管理) - 支撑材料:
2026-07-08-module-requirements-mapping.md第5节(2.5.1/2.5.2 字段与业务规则)、凡荣e链前端业务需求说明书.md2.5.1/2.5.2 原文(约1094-1234行)、swagger_project_scfs_2026-07-09_14-15-23.json(/api/customer-info/personal/*、/api/enterprise-customer/*) - 本文档产出物: ①个人客户管理页面(列表+新增/编辑+详情);②企业客户管理页面(列表+新增/编辑+详情,含股东高管子表单);③通用"文件上传+OCR"组件;④个人客户选择弹窗组件(供企业客户表单及后续阶段复用);⑤
渠道选择器组件;⑥validators.js补充证件号正则;⑦缺失的后端接口.md新增"客户管理"一节 - 已确认的跨阶段批量决策(与本阶段相关部分):企业客户表单增加"法定代表人"选择字段;OCR/上传相关接口所需的"渠道编号(channelNo)"通过表单内"所属渠道"选择框获取(数据源为阶段2核心企业列表)
1. 范围
包含:
- 个人客户:查询列表、新增、编辑、查看详情、(复用已有批量删除模式的)删除
- 企业客户:查询列表、新增、编辑、查看详情、删除,含股东/高管/受益所有人子表单(选择已登记个人客户后字段快照回填)
- 身份证 OCR 识别(个人客户)、通用文件上传(个人客户身份证正反面、企业客户营业执照图片)
- 渠道选择器组件(复用阶段2核心企业数据源)
- 个人客户选择弹窗组件(企业客户"法定代表人"及"股东高管信息"选人使用)
不包含(YAGNI/超出后端能力,已决策不问用户,列入下文"能力缺口"或直接排除):
- 原型页面按钮"关联用户"——customer_info 相关接口无对应能力,本阶段不做
- 原型页面按钮"钱包开立"/"贷款申请"——属于阶段4/5的入口,由阶段4/5各自的"选择客户"流程承接,客户列表本身不重复暴露跳转入口,避免在阶段4/5落地前出现指向空路由的按钮
- 查询条件"资料完整""客户状态"——个人客户
FindIndividualCustomerListQto、企业客户page请求体均无对应字段,客户实体本身也无"状态"属性,不做 - 列表列"性别""创建人""创建时间"(个人客户)——
IndividualCustomerListVo未返回这些字段,不展示(详情页仍展示"创建时间",因为IndividualCustomerDetailVo有该字段) - 企业客户股东高管信息表单不展示"职务/持股比例/国籍/是否股东/联系电话/邮箱"——需求文档 2.5.2 的股东高管信息字段表只要求"证件类型/证件号码/证件生效日/证件到期日"4项(均为选中个人客户后只读回显),这些扩展字段属于阶段4企业开户"法人代表/实际控制人/受益人信息"的需求(2.6.x),两者共用同一后端
EnterpriseRelatedPersonBto结构,但字段取用范围按各自需求文档取舍,不在客户管理阶段引入
2. 接口契约速查
2.1 个人客户(/api/customer-info/personal/*)
| 接口 | Body | 说明 |
|---|---|---|
POST .../list |
{organizationIdEq, customerCodeEq, customerNameLike, certificateNumberEq, page, pageSize} |
响应 {total,page,pageSize,records: IndividualCustomerListVo[]};records 字段:id,organizationId,customerName,certificateType,certificateNumber,mobilePhone,ocrStatus,customerCode |
POST .../detail |
{id} 或 {customerCode} |
响应 IndividualCustomerDetailVo(比列表多:birthDate,gender,ethnicity,certificateEffectiveDate,certificateExpiryDate,issuingAuthority,certificateAddress,idCardFrontFileNo,idCardBackFileNo,occupation,creatorId,createTime,updateTime) |
POST .../create |
CreateIndividualCustomerBto(见下) |
响应 data = 新建 id(int64) |
POST .../update |
UpdateIndividualCustomerBto(同 Create 字段 + id) |
响应 data = boolean |
POST .../ocr-idcard |
{fileBase64, channelNo, fileType('01'人像面|'02'国徽面)}(均必填) |
响应 OcrIdCardResultVo{name,sex,nation,birth,idcard,address,authority,validDate},用于新增页"上传后自动识别回显" |
POST .../upload-file |
{fileBase64, channelNo}(均必填) |
响应 FileUploadResultVo{fileNo},通用上传,个人客户身份证正/反面、企业客户营业执照图片均调用此接口 |
CreateIndividualCustomerBto 字段:organizationId,customerName,certificateType(固定ID_CARD),certificateNumber,birthDate,gender(MALE/FEMALE/UNKNOWN),ethnicity,certificateEffectiveDate,certificateExpiryDate,issuingAuthority,mobilePhone,certificateAddress,idCardFrontFileNo,idCardBackFileNo,occupation,ocrStatus(PENDING/PROCESSING/SUCCESS/FAILED),customerCode。customerCode/ocrStatus 由前端在提交时按规则自动生成/设置(见 4.3),不作为用户可编辑输入项。
删除接口:未在 swagger 中检索到 /api/customer-info/personal/delete,个人客户暂无删除接口(见第7节能力缺口),本阶段列表页删除按钮先隐藏(不展示批量删除工具栏中的删除按钮),仅保留查询/新增/编辑/查看。
2.2 企业客户(/api/enterprise-customer/*)
| 接口 | Body | 说明 |
|---|---|---|
POST .../page |
{page,pageSize,enterpriseName,businessLicense,mobilePhone} |
响应 records: EnterpriseCustomerVo[]:id,organizationId,enterpriseName,businessLicense,certificateType,certificateNumber,certificateEffectiveDate,certificateExpiryDate,mobilePhone,certificateAddress,businessScope,enterpriseCode,ocrStatus,createTime |
POST .../detail |
{id} |
响应 EnterpriseCustomerDetailVo(在 Vo 基础上另有 legalRepresentativeId,无 businessLicenseFileNo——文件编号只用于写入,查看详情不需要回显图片本身,只需状态) |
POST .../create |
{createEnterpriseCustomerBto: CreateEnterpriseCustomerBto, enableOcr: boolean} |
响应 data 无固定类型说明,前端按"提交成功即视为创建成功"处理,不依赖返回值内容 |
POST .../update |
UpdateEnterpriseCustomerBto(同 Create 字段 + id) |
响应 data 无固定类型说明,同上 |
CreateEnterpriseCustomerBto 字段:organizationId,enterpriseName,businessLicense,certificateType(固定BUSINESS_LICENSE),certificateNumber,certificateEffectiveDate,certificateExpiryDate,mobilePhone,certificateAddress,businessScope,businessLicenseFileNo,enterpriseCode,legalRepresentativeId,ocrStatus,enterpriseRelatedPersonBtoList: EnterpriseRelatedPersonBto[]。
EnterpriseRelatedPersonBto(本阶段仅使用其中 5 个字段,其余字段见第1节"不包含"说明):enterpriseId,individualId,relatedType(SHAREHOLDER股东/EXECUTIVE高管/BENEFICIARY_OWNER受益所有人),certificateType,certificateNumber,certificateEffectiveDate,certificateExpiryDate。
删除接口:同样未检索到 /api/enterprise-customer/delete,企业客户暂无删除接口,处理方式同 2.1。
2.3 枚举取值(均已从 swagger 摘取,非编造)
CertificateTypeEnum:ID_CARD身份证 /PASSPORT护照 /OTHER其他 /BUSINESS_LICENSE营业执照GenderEnum:MALE男 /FEMALE女 /UNKNOWN未知OcrStatusEnum:PENDING待识别 /PROCESSING识别中 /SUCCESS识别成功 /FAILED识别失败RelatedPersonTypeEnum:SHAREHOLDER股东 /EXECUTIVE高管 /BENEFICIARY_OWNER受益所有人
3. 页面架构
沿用阶段2确立的目录风格,新增:
src/views/customer/
personal/
PersonalList.vue # 列表页(ProTable模式)
PersonalForm.vue # 新增/编辑/查看(独立路由页,三态由query区分)
enterprise/
EnterpriseList.vue
EnterpriseForm.vue
src/components/
ChannelSelect.vue # 渠道(核心企业)下拉选择器,跨模块复用
PersonalCustomerPicker.vue # 个人客户选择弹窗,跨模块复用
IdCardUpload.vue # 身份证正/反面上传+OCR回显,个人客户专用
LicenseUpload.vue # 营业执照上传(无OCR同步接口,仅上传+预览),企业客户专用
src/api/customer.js
新增/编辑改为独立路由页而非 <a-modal>(与阶段1/2的弹窗模式不同,是本阶段的必要调整):个人客户表单含身份证双面上传+OCR异步识别+9个必填字段;企业客户表单含营业执照上传+9个基础字段+法定代表人选择+股东高管子表单(可增删多行)。字段量与交互复杂度已超出弹窗合理承载范围,弹窗会导致内容被压缩、滚动区域嵌套滚动等体验问题。列表页仍是 ProTable 单文件模式并复用阶段2的批量删除交互组件(见第2节,本阶段实际不启用删除按钮,但保留 ProTable 默认能力不做特殊阉割,便于后端补齐删除接口后无需改动即可启用)。
路由:componentRegistry.js 新增映射:
'/customer/personal': 'customer/personal/PersonalList'
'/customer/personal/create': 'customer/personal/PersonalForm'
'/customer/personal/edit': 'customer/personal/PersonalForm'
'/customer/personal/detail': 'customer/personal/PersonalForm'
'/customer/enterprise': 'customer/enterprise/EnterpriseList'
'/customer/enterprise/create': 'customer/enterprise/EnterpriseForm'
'/customer/enterprise/edit': 'customer/enterprise/EnterpriseForm'
'/customer/enterprise/detail': 'customer/enterprise/EnterpriseForm'
PersonalForm.vue/EnterpriseForm.vue 通过路由 query ?mode=create|edit|detail&id=xxx 区分三态(detail 模式下所有表单项 disabled,不展示上传/OCR操作按钮)。菜单节点(客户管理 > 个人客户/企业客户)需要在后端权限管理里配置,子路由(create/edit/detail)不建菜单节点,只在 componentRegistry.js 注册以支持动态路由 resolve,列表页内部用 router.push 跳转(此模式与阶段1/2"菜单项=路由=componentRegistry一一对应"略有不同,是本阶段为支撑独立表单页新增的必要例外,需要在 dynamic.js 确认:非菜单节点的路由是否也能被 buildRouteRecords/resolveComponent 处理——技术方案:创建/编辑/详情三个路由不通过后端菜单树生成,而是在 router/index.js 里作为 MainLayout 的静态 children 直接注册(懒加载 resolveComponent 复用同一套 import.meta.glob),不依赖 componentRegistry.js 的菜单映射表,避免破坏"菜单驱动路由"的既有约定;仅列表页两个路由继续走菜单驱动。
4. 关键交互设计
4.1 渠道选择器 ChannelSelect.vue
- Props:
v-model:value,内部onMounted调fetchCoreEnterprisePageApi({page:1, pageSize:200})拉取全部核心企业,下拉选项label=enterpriseName, value=enterpriseCode(渠道编号取核心企业的enterpriseCode,与阶段2批量决策一致) - 用于个人客户表单的"上传身份证/OCR识别"操作前置校验:未选择渠道时禁用上传按钮,并提示"请先选择所属渠道"
- 企业客户表单同理用于营业执照上传前置校验
- 该选择结果不随表单提交持久化到客户实体(customer-info/personal 与 enterprise-customer 的 Create/Update Bto 均无 channelNo 字段),仅作为调用 OCR/上传接口的临时参数,页面卸载/切换后不保留
4.2 IdCardUpload.vue(身份证上传+OCR)
- Props:
side('front'|'back')、channelNo、v-model:fileNo、v-model:ocrResult(仅 side='front' 时输出,身份证反面'02国徽面'不含个人信息字段,只需上传不需识别結果回填,但仍调用同一ocr-idcard接口传fileType='02'以获得authority/validDate等国徽面才有的信息用于回显"签发机关"/"证件有效期") - 内部用
<a-upload :before-upload>拦截,FileReader.readAsDataURL转 base64,依次调用ocrIdCardApi({fileBase64, channelNo, fileType})与uploadFileApi({fileBase64, channelNo})(两次调用,分别拿到识别结果与文件编号;若只需其一亦可,但需求要求"文件必须持久化到凡荣e链"且"字段需OCR回显",两者都需要) - 识别成功(
code===200且返回字段非空):emit('update:ocrResult', data),父组件据此回填表单字段,并将本地ocrStatus置为SUCCESS - 识别失败(接口报错或返回字段为空):
message.warning('识别失败,请手动填写'),ocrStatus置为FAILED,表单字段保持可编辑,不阻断继续手动填写和提交 - 上传中禁用重复点击(
uploading状态 + antdloading效果),符合全局交互规则"异步操作展示 loading 防止重复点击"
4.3 个人客户 customerCode/ocrStatus 生成规则
customerCode(客户编号):后端 schema 未标注自动生成机制,且创建响应只返回id不返回编号。为避免前端编造编号规则造成与后端实际生成逻辑冲突,新增表单不提供"客户编号"输入框,提交create时不传customerCode字段(交由后端生成,若后端要求必填但未生成,列入第7节能力缺口待核实,前端按"选填不传"实现,后端报错则在实施阶段据实际报错信息调整,不阻塞设计)ocrStatus:由IdCardUpload组件识别结果驱动(见4.2),不提供用户手动选择该字段的 UI
4.4 企业客户表单关键交互
- 基础信息区:企业名称/证件号(营业执照号)/证件生效日/证件到期日/手机号码/证件地址/经营范围均为可编辑输入框(非只读,因为无同步OCR接口,详见第7节能力缺口,采用"手动填写为主"方案);证件类型固定展示"营业执照"文本,不可编辑
- 营业执照上传:
LicenseUpload.vue仅调用uploadFileApi拿到businessLicenseFileNo,不调用任何 OCR 接口(不存在);上传成功后仅做图片预览,不做字段回显 - 提交创建时固定传
enableOcr: true(告知后端可尝试异步识别,不影响前端已填字段,即便后端异步识别有结果也只影响该客户记录的ocrStatus字段,不会覆盖前端已提交的其它字段——这是基于当前 swagger 无法验证的合理假设,已登记入第7节,后续若后端行为不同以实测调整) - "法定代表人"选择:点击"选择"按钮打开
PersonalCustomerPicker,选中后legalRepresentativeId赋值,同时展示所选人的customerName(选择器组件内部把选中行的展示字段一并 emit 出来,便于表单展示,不需要额外调用 detail 接口) - "股东高管信息"列表:表格形式,可"添加一行"→ 每行点击"选择个人客户"打开
PersonalCustomerPicker→ 选中后该行individualId及只读展示的certificateType/certificateNumber/certificateEffectiveDate/certificateExpiryDate(来自选中行的个人客户列表数据,若列表数据缺失证件生效日字段则追加一次detail查询补全,因为IndividualCustomerListVo不含certificateEffectiveDate——需要在实施时确认并处理该字段缺口)→ 用户再选择该行"关联人类型"(股东/高管/受益所有人,下拉,必选)→ 可"删除该行";提交时逐行组装enterpriseRelatedPersonBtoList,enterpriseId在新增场景下留空(后端按当前企业客户上下文关联,若后端要求必填由后端报错驱动调整),编辑场景下回填当前企业客户id
4.5 PersonalCustomerPicker.vue(个人客户选择弹窗)
<a-modal>内嵌一个只读用途的迷你ProTable(复用现有组件,rowSelection设为单选模式或直接用行点击选中,不需要批量删除工具栏,#actionsslot 留空)- 搜索条件:所属机构、客户编号、客户名称(与需求文档"索引要素:所属机构、客户编号、客户名称"一致)
- 列表来源:
fetchPersonalCustomerListApi,选中一行后emit('select', record)并关闭弹窗
5. 表单校验补充(src/utils/validators.js)
新增:
ID_CARD_REGEX = /^\d{17}[\dXx]$/(18位身份证号,末位允许X,不做校验码算术级校验,与手机号正则的"够用不过度设计"风格一致)+idCardValidatorRule()BUSINESS_LICENSE_REGEX = /^[0-9A-Z]{15,20}$/(统一社会信用代码/营业执照号,15-20位数字大写字母组合,覆盖新旧两种编码位数)+businessLicenseValidatorRule()
两者均遵循现有 xxxValidatorRule() 返回 {validator: (_rule, value) => Promise} 的写法。
6. 业务规则映射
| 需求规则 | 落地方式 |
|---|---|
| 同一证件号在同一机构下不可重复登记 | 前端不做预校验查重请求(无对应查重接口),提交后依赖后端 create 接口返回的 code!==200 + message(如"该证件号已存在")直接展示,遵循全局约定"不做具体错误码分支,统一展示 message" |
| 手机号11位数字校验 | 复用现有 phoneValidatorRule() |
| OCR识别失败允许手动修正并提示 | 见4.2 IdCardUpload 失败分支 |
| 证件到期日早于当前日期提示"证件已过期" | 表单 certificateExpiryDate 字段值变化时前端本地比较 dayjs(value).isBefore(dayjs()),提示但不阻断提交(需求原文仅要求"提示",未要求强制阻断) |
| 仅能查看/操作所属机构及下级机构数据 | 依赖后端按当前用户 organizationId 做数据权限过滤(阶段1鉴权体系已处理,前端"所属机构"筛选框仍展示机构树供有权限跨机构查看的用户使用,不额外加前端侧过滤) |
| 股东/受益所有人须为已登记且证件有效的个人客户 | 前端在 PersonalCustomerPicker 列表查询时不做"证件有效"额外过滤(后端列表接口无此过滤参数),该规则视为后端 create 接口的服务端校验职责,前端仅展示报错 message |
7. 已识别的后端能力缺口(登记入 缺失的后端接口.md)
- 个人客户、企业客户均无删除接口(swagger 中未找到
/api/customer-info/personal/delete或/api/enterprise-customer/delete)——列表页暂不提供删除入口,待后端补充后再启用批量删除工具栏 - 营业执照无同步OCR识别接口——只有个人身份证
ocr-idcard接口,企业客户创建时的enableOcr参数用途/时序(同步/异步、是否覆盖已填字段)缺乏文档说明,前端按"手动填写为主+提交时告知后端可异步识别"实现,行为以实测为准 - 列表查询筛选能力收窄——原型/需求文档描述的"资料完整""客户状态"查询项在真实接口中不存在(客户实体本身也无状态属<E68081>),不实现
- 个人客户列表接口字段收窄——
IndividualCustomerListVo不返回性别/创建人/创建时间,列表列相应精简 customerCode(个人客户编号)生成机制不明确——创建接口不返回编号,创建请求体是否需要前端传值也未标注必填,前端按"不传,交由后端生成"实现- 股东高管子表单证件生效日字段来源不确定——
IndividualCustomerListVo缺少certificateEffectiveDate,选择个人客户后可能需要额外一次detail查询才能补全该字段用于企业客户关联人快照,增加一次网络请求,建议后端后续在个人客户列表接口中补充该字段 - 无"关联用户"相关能力——原型按钮无对应后端接口,本阶段不实现
8. 自查(占位符/矛盾/歧义/范围)
- 已检查全文无 "TBD"/"待定" 类占位符;第4.4/7节中标注的"假设,以实测为准"属于基于现有 swagger 文档信息不足的合理工程假设,已明确标注,不是遗留占位符
- 范围与总体设计文档阶段3定义(客户管理=个人客户+企业客户)一致,未扩展至钱包/授信模块内容
- 第3节路由方案(非菜单驱动的表单子路由)与阶段1"菜单驱动路由"约定存在差异,已在文中说明差异原因与技术方案,不是矛盾遗漏
- 与批量决策结果核对:企业客户表单已包含"法定代表人"选择字段(4.4节);渠道编号来源已采用表单内"所属渠道"选择框(4.1节),与批量决策结论一致,无冲突
设计自查通过,直接进入 writing-plans 生成实施计划。