SFT/docs/superpowers/specs/2026-07-31-personal-open-mo...

16 KiB

个人开户移动端H5 对接真实字段与人脸识别接口 设计文档

背景

docs/superpowers/specs/2026-07-27-personal-open-mobile-h5-design.md(以下简称"07-27文档")与 docs/superpowers/specs/2026-07-28-personal-open-mobile-h5-send-verify-code-api-change-design.md (以下简称"07-28文档")实现并调整了 fryl_h5 独立工程的个人开户移动端H5确认页,但当时 mobile/draft-detail 的响应字段、mobile/submit 是否需要人脸文件、"人脸对比"环节的后端接口均是 前端单方假设或完全无接口(见《缺失的后端接口.md》Part 12 第93-95条)。

最新 swagger_project_scfs_2026-07-31_09-25-57.json 显示后端已经:

  1. 正式补充了 MobileDraftDetailVo 的完整字段(不再是5个字段的占位契约)。
  2. 新增了真实的人脸识别接口 POST /api/customer-info/personal/face-recognize(凡荣e链222005), 与主项目管理后台"个人开户"表单的 src/components/FacePhotoUpload.vue/src/api/customer.js 调用的是同一个接口。
  3. mobile/submit 的接口描述明确"影像资料由后端从本地查询",请求体仍只有 {applicationNo, verifyCode}

本次设计即按新契约重新对接 H5 确认页的"信息核对"与"人脸对比"两个环节,补齐真实的人脸识别调用, 并修复一个顺带发现的 mock-server 路由顺序缺陷(否则本次新增/改造的功能无法在本地 npm run mock 环境下验证)。

范围

  • 仅改动 fryl_h5 工程内"信息核对"页字段来源、"人脸对比"步骤的接口对接、"手机验证"步骤的手机号 展示来源。
  • 不改动 mobile/submit 的请求参数结构(仍是 {applicationNo, verifyCode},人脸识别结果不透传)。
  • 不改动 send-verify-code/general 的接口地址,仅调整请求体字段取值来源(见下)。
  • 顺带修复 mock-server/index.js 的路由挂载顺序缺陷 + 新增 face-recognize 匿名 mock 路由,便于 本地联调;不改动其余 mock 业务逻辑。
  • 不涉及企业开户、贷款申请确认扫码等其它移动端确认场景。
  • 不改动管理后台侧任何页面代码(仅读取其 FacePhotoUpload.vue/customer.js 作为接口调用参照)。

接口契约变化(依据新 swagger,不再是假设)

POST /api/wallet-management/personal-opening/mobile/draft-detail

响应 MobileDraftDetailVo 真实字段:

字段 说明 备注
customerName 客户名称 不变
certificateType 证件类型(CertificateTypeEnum) 不变
certificateNumber 证件号码 不变
certificateEffectiveDate 证件生效日 不变
certificateExpiryDate 证件到期日 不变
issuingAuthority 签发机关 不变
gender 性别(GenderEnum,含 UNKNOWN) 不变
certificateAddress 证件地址 不变
occupation 职业(OccupationTypeEnum 编码) 不变
occupationNote 职业备注 字段名变化:此前假设为 jobNote
mobilePhone 手机号(明文) 字段名变化:此前假设为 mobile
mobilePhoneMask 掩码手机号(后端直接算好,如 138****1234) 新增:此前(07-28)以为后端不再提供掩码字段,现confirmed后端仍提供,前端不再需要自行计算
bankCardNumber 绑定银行卡号 不变
ethnic 民族 不变

仍缺失(遗留缺口,沿用 Part12 第93条的假设模式):channelNo(渠道编号)、bankCardName (银行卡户名)两个字段依然不存在。这两个字段原本用于 send-verify-code/general 请求体,现按以下 方案处理(已与用户确认):

  • channelNo:固定传空字符串 ''(既有假设模式的延续,不阻塞流程,等后端补充字段后再联调修正)。
  • cardName:固定传空字符串 ''(不复用 customerName,避免语义臆造)。

POST /api/customer-info/personal/face-recognize(新增,真实接口)

请求体:

{
  "fileData": "base64编码的人脸照片(字段描述写"zip"但已由主项目对接确认实际直传图片base64即可)",
  "idNo": "证件号码",
  "name": "姓名",
  "channelNo": "渠道编号(必填,H5场景无来源,固定传空字符串,见下方风险登记)"
}

响应 FaceRecognizeResultVo:fileNo(识别通过后的文件编号)/recode/recodeInfo/sysSerialNo/ sysDate。前端沿用主项目 FacePhotoUpload.vue 已验证的判断方式:只按 data.fileNo 是否非空 判断识别是否通过,不对 recode 做枚举分支(枚举未文档化,与主项目现状一致)。

idNo/name 取自 draft-detail 已返回的 certificateNumber/customerName(而不是身份证 OCR 结果,H5 场景本身也没有 OCR 环节)。

POST /api/wallet-management/personal-opening/mobile/submit

不变,{applicationNo, verifyCode}。接口描述"影像资料由后端从本地查询",说明后端会按证件号等信息 自行关联此前 face-recognize 调用留存的影像,前端不需要、也没有字段可以把 fileNo 传给 submit。据此,"人脸对比"步骤在 H5 侧的作用被明确为:识别通过才允许进入下一步的前置校验关卡, 识别结果本身不参与提交请求体。

页面改造

步骤1:信息核对(InfoConfirmStep.vue)

  • 手机号展示:改为直接使用 detail.mobilePhoneMask(后端已算好),不再调用任何前端掩码工具。
  • "职业备注"展示字段:detail.jobNotedetail.occupationNote
  • 其余字段展示不变。

步骤2:人脸对比(FaceCompareStep.vue,本次重点改造)

由"仅拍照留档展示,不比对"的 UI 占位,改为真实调用人脸识别接口:

  1. 通过 van-uploader:after-read 钩子拿到新选中的文件(拍照或从相册选择,capture="camera" 保留)。
  2. FileReader.readAsDataURL 转 base64(与主项目 FacePhotoUpload.vuereadAsBase64 完全 一致的实现),调用 faceRecognizeApi({ fileData, idNo, name })(idNo/name 由父组件传入, 分别取 draftDetail.certificateNumber/draftDetail.customerName;channelNo 由 api 函数内部 固定补充为 '')。
  3. 识别中:整页 van-overlay + van-loading("人脸识别中,请稍候..."),van-uploader 与"上一步" /"下一步"按钮均禁用,防止重复操作(与 SmsVerifyStep.vue 提交时的等待态规范保持一致)。
  4. 识别通过(data.fileNo 非空):showToast(data.recodeInfo || '人脸识别通过'),"下一步"按钮启用。
  5. 识别未通过(code!==200data.fileNo 为空):清空已选照片预览(强制重新拍摄,不允许提交 "识别失败的照片"进入下一步),showErrorModal(data?.recodeInfo || message || '人脸识别失败,请 重新拍摄', '人脸识别未通过'),"下一步"保持禁用。
  6. 用户删除已选照片、或重新选择新照片:识别状态重置为未通过,需重新走一次识别流程。

状态提升到父组件 PersonalOpenMobile.vue(新增 faceVerified ref,通过 v-model:verified 双向 绑定,与既有的 agreed/facePhoto 提升模式一致):用户从步骤3"上一步"退回步骤2、再次"下一步" 前进时,如果照片与识别通过状态都还保留着,不需要重新识别(组件销毁重建后用 props 里的 photo+verified 恢复本地状态,不重新触发接口调用)。

原提示文案"当前版本仅采集照片供留档展示,不进行自动人脸比对"删除,替换为真实的操作提示("请正对 手机摄像头拍摄一张清晰的人脸照片,系统将自动识别是否为本人办理")及识别结果状态提示。

步骤3:手机验证(SmsVerifyStep.vue)

  • 删除 channelNo/cardName 两个 props(不再需要从外部传入,sendVerifyCodeApi 内部固定补 '')。
  • 手机号展示改为直接使用父组件传入的 mobilePhoneMask(新 prop,替换原 mobile prop 的展示用途; 是否仍需要保留明文 mobile prop 用于调用发码接口——需要,发码接口请求体的 mobile 字段 仍必须是明文,mobilePhoneMask 只用于本步骤 UI 展示)。

父组件 PersonalOpenMobile.vue

  • draftDetail 字段读取全部对齐新字段名:mobilemobilePhone,新增 mobilePhoneMask 透传。
  • 新增 faceVerified ref,传给 FaceCompareStepv-model:verified
  • FaceCompareStep 新增 id-no/name props(分别取 draftDetail.certificateNumber/ draftDetail.customerName)。
  • SmsVerifyStep 传参调整为 mobile(明文,draftDetail.mobilePhone)+mobile-phone-mask (draftDetail.mobilePhoneMask)+card-no(draftDetail.bankCardNumber)+id-no (draftDetail.certificateNumber),不再传 channel-no/card-name

src/utils/maskPhone.js

删除该文件(后端已直接提供掩码字段,前端不再需要自行计算),同时移除 InfoConfirmStep.vue/ SmsVerifyStep.vue 中对它的 import。

src/constants/enums.js

按新 swagger 的枚举描述校正/补全(不再是前端临时占位):

  • GENDER_LABELS 补充 UNKNOWN: '未知'
  • CERTIFICATE_TYPE_LABELS 补全 CertificateTypeEnum 全部5个值(ID_CARD/PASSPORT/OTHER/ BUSINESS_LICENSE/INDIVIDUAL_BUSINESS),避免个人客户理论上出现非身份证证件类型时展示裸编码。
  • OCCUPATION_TYPE_LABELSOccupationTypeEnum 的官方描述校正文案:SELF_EMPLOYED "个体经营人员"→"个体工商户"、FARMER "务农人员"→"农/林/牧/渔"、RETIRED "退休人员"→ "离退休人员"(其余值编码与文案不变)。

src/api/personalOpen.js

export const sendVerifyCodeApi = ({ mobile, cardNo, idNo }) =>
  request.post('/api/wallet-management/personal-opening/mobile/send-verify-code/general', {
    channelNo: '', // 契约缺口:MobileDraftDetailVo 无 channelNo,固定传空,待后端补充字段
    tradeNo: '222101',
    tradeType: '221506',
    mobile,
    cardNo,
    cardName: '', // 契约缺口:MobileDraftDetailVo 无 bankCardName,固定传空
    idNo
  })

export const faceRecognizeApi = ({ fileData, idNo, name }) =>
  request.post('/api/customer-info/personal/face-recognize', { fileData, idNo, name, channelNo: '' })

mock-server 配套修复与改造

路由挂载顺序缺陷修复(阻塞性 bug,顺带修复)

mock-server/routes/customer.js/wallet.js 均在文件顶部用 router.use(requireAuth)(不带路径) 挂鉴权中间件。Express 的这种写法会拦截该 router 匹配到的任意路径,而不仅是该文件里定义的具体 路由。mock-server/index.js 里这两个 router 都以 app.use('/api', xxxRouter) 挂载,且顺序早于 personalOpenMobileRouter(同样挂载在 /api 前缀下)。结果是:任何 /api/* 请求都会先进入 customerRouter,被其内部的 requireAuth 拦截返回401(除非携带合法 token),H5 匿名接口永远 拿不到执行机会。已实测复现:本地 npm run mock 环境下访问 /api/wallet-management/personal-opening/mobile/draft-detail 直接返回401。

修复方式:调整 index.js 挂载顺序,把不需要鉴权的 router(personalOpenMobileRouter + 新增的 faceRecognizeRouter)挪到 customerRouter/walletRouter 之前挂载。

新增 mock-server/routes/faceRecognize.js(不带 requireAuth)

POST /customer-info/personal/face-recognize
  • 校验 fileData/idNo/name 非空(channelNo 允许为空字符串,不强制校验,呼应契约缺口), 缺失则 sendFail
  • 校验通过则返回 { fileNo, recode: '0000', recodeInfo: '识别通过', sysSerialNo, sysDate }, fileNoFACE${nextId('file')} 生成,足够支撑前端"按 fileNo 是否非空判断通过"的逻辑。
  • 不模拟"人脸不符"等业务失败场景(与主项目现状一致,无可参照的真实失败样例)。

放在独立、不挂 requireAuth 的文件里,同时服务于管理后台(已登录,不受影响)和 H5(匿名)两种 调用方,弥补此前主项目 FacePhotoUpload.vue 一直没有对应 mock、无法本地联调的缺口。

mock-server/routes/personalOpenMobile.js 字段同步

draft-detail mock 响应同步新字段名:

  • mobilemobilePhone
  • 新增 mobilePhoneMask(mock 内自行按"前3后4"算法计算)
  • jobNoteoccupationNote
  • 移除 channelNo/bankCardName(不再从 application 对象读取,契约缺口不在 mock 层面"填平", 与真实后端行为保持一致,便于本地测试提前暴露前端对这两个字段留空的兼容性)

send-verify-code/generalsubmit 两个路由内部逻辑不变(仍以 mobilePhone 明文作为验证码 key)。

数据流(更新后)

PersonalOpenMobile.vue (onMounted)
  → fetchDraftDetailApi(applicationNo)
  → draftDetail = { customerName, certificateType, certificateNumber, certificateEffectiveDate,
                     certificateExpiryDate, issuingAuthority, gender, certificateAddress,
                     occupation, occupationNote, mobilePhone, mobilePhoneMask, bankCardNumber, ethnic }

步骤1 InfoConfirmStep: 展示 mobilePhoneMask(不再自算);职业备注读 occupationNote

步骤2 FaceCompareStep(props: idNo=certificateNumber, name=customerName):
  拍照/选图 → readAsBase64 → faceRecognizeApi({fileData, idNo, name})
            → 成功(fileNo非空) → emit update:verified=true → "下一步"可点
            → 失败 → 清空预览,emit update:verified=false → 强制重拍

步骤3 SmsVerifyStep(props: mobile=mobilePhone, mobilePhoneMask, cardNo=bankCardNumber, idNo=certificateNumber):
  展示 mobilePhoneMask
  "获取验证码" → sendVerifyCodeApi({ mobile, cardNo, idNo })
                  (channelNo=''/cardName''/tradeNo/tradeType 由 api 函数内部固定补充)
  "确认提交"  → submitApplicationApi(applicationNo, verifyCode)  // 不变,不含人脸文件编号

错误处理

与现有全局规则一致:

  • code !== 200:request.js 响应拦截器统一 showErrorModal 提示,不新增特殊分支。
  • 人脸识别的"code===200data.fileNo 为空"这种业务软失败,全局拦截器不会弹窗(因为 code===200),FaceCompareStep.vue 内部单独调用 showErrorModal 兜底提示,与主项目 FacePhotoUpload.vue 的既有处理方式保持一致(即便存在与全局拦截器"双弹窗"的可能,也是本仓库 已有的既定模式,本次不额外处理去重,不属于本次任务范围)。
  • 网络异常:同现有拦截器统一提示。

已知风险 / 遗留缺口(需后端后续确认)

  1. channelNo 缺口:face-recognize 接口把 channelNo 标记为必填,但 H5 场景(draft-detail 无此字段、二维码 URL 也不携带渠道信息)只能固定传空字符串。若后端 @Valid 校验真的强制拒绝空 channelNo,人脸识别会始终失败,阻塞整个 H5 流程。需要后端确认该接口对 H5 匿名场景下 channelNo 为空的容忍度,或在 MobileDraftDetailVo 补充该字段
  2. bankCardName 缺口:send-verify-code/generalcardName 固定传空字符串,若凡荣e链短信 接口对该字段有强校验,可能导致发送验证码失败。需要后端确认该字段是否允许为空。
  3. 匿名访问 face-recognize 的可行性未经真实后端验证:该接口目前仅由已登录的管理后台调用, 本次 H5 侧调用不带 Authorization 头(与其余 H5 匿名接口一致的约定)。由于本地无法连接真实 后端环境验证,无法确认 Spring Security 是否已将该接口路径纳入匿名白名单;若未纳入,H5 场景下 该接口会返回401,需要后端配合调整安全配置。

以上3点已同步登记到《缺失的后端接口.md》Part 12。

范围边界(本次不做)

  • 不新增"人脸识别失败原因"的细分枚举分支处理(recode 取值未文档化,与主项目现状一致)。
  • 不做人脸照片压缩/裁剪等图像处理。
  • 不修改管理后台侧 FacePhotoUpload.vue/src/api/customer.js 任何代码(仅作为接口调用参照)。
  • 不解决"全局拦截器 + 组件内部"双弹窗的既有小问题(超出本次范围)。