SFT/docs/superpowers/specs/2026-07-09-phase3-customer-...

21 KiB
Raw Blame History

阶段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链前端业务需求说明书.md 2.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),customerCodecustomerCode/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,内部 onMountedfetchCoreEnterprisePageApi({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')、channelNov-model:fileNov-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 状态 + antd loading 效果),符合全局交互规则"异步操作展示 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 接口)
  • "股东高管信息"列表(仅新增模式展示,编辑/详情模式见第7节能力缺口8的替代提示文案):表格形式,可"添加一行"→ 每行点击"选择个人客户"打开 PersonalCustomerPicker → 选中后该行 individualId 及只读展示的 certificateType/certificateNumber/certificateEffectiveDate/certificateExpiryDate(来自选中行的个人客户列表数据,若列表数据缺失证件生效日字段则追加一次 detail 查询补全,因为 IndividualCustomerListVo 不含 certificateEffectiveDate)→ 用户再选择该行"关联人类型"(股东/高管/受益所有人,下拉,必选)→ 可"删除该行";提交时逐行组装 enterpriseRelatedPersonBtoList,enterpriseId 留空(后端按当前企业客户上下文关联,若后端要求必填由后端报错驱动调整)

4.5 PersonalCustomerPicker.vue(个人客户选择弹窗)

  • <a-modal> 内嵌一个只读用途的迷你 ProTable(复用现有组件,rowSelection 设为单选模式或直接用行点击选中,不需要批量删除工具栏,#actions slot 留空)
  • 搜索条件:所属机构、客户编号、客户名称(与需求文档"索引要素:所属机构、客户编号、客户名称"一致)
  • 列表来源: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)

  1. 个人客户、企业客户均无删除接口(swagger 中未找到 /api/customer-info/personal/delete/api/enterprise-customer/delete)——列表页暂不提供删除入口,待后端补充后再启用批量删除工具栏
  2. 营业执照无同步OCR识别接口——只有个人身份证 ocr-idcard 接口,企业客户创建时的 enableOcr 参数用途/时序(同步/异步、是否覆盖已填字段)缺乏文档说明,前端按"手动填写为主+提交时告知后端可异步识别"实现,行为以实测为准
  3. 列表查询筛选能力收窄——原型/需求文档描述的"资料完整""客户状态"查询项在真实接口中不存在(客户实体本身也无状态属<E68081>),不实现
  4. 个人客户列表接口字段收窄——IndividualCustomerListVo 不返回性别/创建人/创建时间,列表列相应精简
  5. customerCode(个人客户编号)生成机制不明确——创建接口不返回编号,创建请求体是否需要前端传值也未标注必填,前端按"不传,交由后端生成"实现
  6. 股东高管子表单证件生效日字段来源不确定——IndividualCustomerListVo 缺少 certificateEffectiveDate,选择个人客户后可能需要额外一次 detail 查询才能补全该字段用于企业客户关联人快照,增加一次网络请求,建议后端后续在个人客户列表接口中补充该字段
  7. 无"关联用户"相关能力——原型按钮无对应后端接口,本阶段不实现
  8. 企业客户股东高管信息只能在新增时一次性提交,编辑/详情接口均不支持读取或修改——UpdateEnterpriseCustomerBtoEnterpriseCustomerDetailVo 均不含 enterpriseRelatedPersonBtoList 字段(仅 CreateEnterpriseCustomerBto 有)。据此调整4.4节交互:"股东高管信息"子表单仅在新增(create)模式下展示且可编辑;编辑(edit)/查看(detail)模式下该区块替换为一行提示文字"股东高管信息仅支持新增时登记,当前接口不支持查询或修改,如需变更请联系技术支持核实数据",不渲染表格与"添加"按钮。同理,企业客户 businessLicenseFileNo(营业执照文件编号)编辑/详情接口也读取不到(Detail Vo 无此字段),编辑模式下"重新上传营业执照"仍可操作(调用同一 LicenseUpload 拿新 fileNo 提交 update),但不会展示已上传的历史图片(因为读不到)
  9. 企业客户分页查询接口 page 不支持按"所属机构""客户编号(企业编号)"过滤,仅支持 enterpriseName/businessLicense/mobilePhone 三项,比需求文档原型描述的查询区(所属机构/客户编号/客户名称/证件号码)少两项,按"后端能力收窄即接受"的既定原则(与阶段1/2迁移文档处理方式一致),查询表单只提供后端实际支持的3个字段

8. 自查(占位符/矛盾/歧义/范围)

  • 已检查全文无 "TBD"/"待定" 类占位符;第4.4/7节中标注的"假设,以实测为准"属于基于现有 swagger 文档信息不足的合理工程假设,已明确标注,不是遗留占位符
  • 范围与总体设计文档阶段3定义(客户管理=个人客户+企业客户)一致,未扩展至钱包/授信模块内容
  • 第3节路由方案(非菜单驱动的表单子路由)与阶段1"菜单驱动路由"约定存在差异,已在文中说明差异原因与技术方案,不是矛盾遗漏
  • 与批量决策结果核对:企业客户表单已包含"法定代表人"选择字段(4.4节);渠道编号来源已采用表单内"所属渠道"选择框(4.1节),与批量决策结论一致,无冲突

设计自查通过,直接进入 writing-plans 生成实施计划。