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 显示后端已经:
- 正式补充了
MobileDraftDetailVo的完整字段(不再是5个字段的占位契约)。 - 新增了真实的人脸识别接口
POST /api/customer-info/personal/face-recognize(凡荣e链222005), 与主项目管理后台"个人开户"表单的src/components/FacePhotoUpload.vue/src/api/customer.js调用的是同一个接口。 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.jobNote→detail.occupationNote。 - 其余字段展示不变。
步骤2:人脸对比(FaceCompareStep.vue,本次重点改造)
由"仅拍照留档展示,不比对"的 UI 占位,改为真实调用人脸识别接口:
- 通过
van-uploader的:after-read钩子拿到新选中的文件(拍照或从相册选择,capture="camera"保留)。 - 用
FileReader.readAsDataURL转 base64(与主项目FacePhotoUpload.vue的readAsBase64完全 一致的实现),调用faceRecognizeApi({ fileData, idNo, name })(idNo/name由父组件传入, 分别取draftDetail.certificateNumber/draftDetail.customerName;channelNo由 api 函数内部 固定补充为'')。 - 识别中:整页
van-overlay+van-loading("人脸识别中,请稍候..."),van-uploader与"上一步" /"下一步"按钮均禁用,防止重复操作(与SmsVerifyStep.vue提交时的等待态规范保持一致)。 - 识别通过(
data.fileNo非空):showToast(data.recodeInfo || '人脸识别通过'),"下一步"按钮启用。 - 识别未通过(
code!==200或data.fileNo为空):清空已选照片预览(强制重新拍摄,不允许提交 "识别失败的照片"进入下一步),showErrorModal(data?.recodeInfo || message || '人脸识别失败,请 重新拍摄', '人脸识别未通过'),"下一步"保持禁用。 - 用户删除已选照片、或重新选择新照片:识别状态重置为未通过,需重新走一次识别流程。
状态提升到父组件 PersonalOpenMobile.vue(新增 faceVerified ref,通过 v-model:verified 双向
绑定,与既有的 agreed/facePhoto 提升模式一致):用户从步骤3"上一步"退回步骤2、再次"下一步"
前进时,如果照片与识别通过状态都还保留着,不需要重新识别(组件销毁重建后用 props 里的
photo+verified 恢复本地状态,不重新触发接口调用)。
原提示文案"当前版本仅采集照片供留档展示,不进行自动人脸比对"删除,替换为真实的操作提示("请正对 手机摄像头拍摄一张清晰的人脸照片,系统将自动识别是否为本人办理")及识别结果状态提示。
步骤3:手机验证(SmsVerifyStep.vue)
- 删除
channelNo/cardName两个 props(不再需要从外部传入,sendVerifyCodeApi内部固定补'')。 - 手机号展示改为直接使用父组件传入的
mobilePhoneMask(新 prop,替换原mobileprop 的展示用途; 是否仍需要保留明文mobileprop 用于调用发码接口——需要,发码接口请求体的mobile字段 仍必须是明文,mobilePhoneMask只用于本步骤 UI 展示)。
父组件 PersonalOpenMobile.vue
draftDetail字段读取全部对齐新字段名:mobile→mobilePhone,新增mobilePhoneMask透传。- 新增
faceVerifiedref,传给FaceCompareStep做v-model:verified。 - 给
FaceCompareStep新增id-no/nameprops(分别取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_LABELS按OccupationTypeEnum的官方描述校正文案: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 },fileNo用FACE${nextId('file')}生成,足够支撑前端"按fileNo是否非空判断通过"的逻辑。 - 不模拟"人脸不符"等业务失败场景(与主项目现状一致,无可参照的真实失败样例)。
放在独立、不挂 requireAuth 的文件里,同时服务于管理后台(已登录,不受影响)和 H5(匿名)两种
调用方,弥补此前主项目 FacePhotoUpload.vue 一直没有对应 mock、无法本地联调的缺口。
mock-server/routes/personalOpenMobile.js 字段同步
draft-detail mock 响应同步新字段名:
mobile→mobilePhone- 新增
mobilePhoneMask(mock 内自行按"前3后4"算法计算) jobNote→occupationNote- 移除
channelNo/bankCardName(不再从application对象读取,契约缺口不在 mock 层面"填平", 与真实后端行为保持一致,便于本地测试提前暴露前端对这两个字段留空的兼容性)
send-verify-code/general 与 submit 两个路由内部逻辑不变(仍以 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===200但data.fileNo为空"这种业务软失败,全局拦截器不会弹窗(因为code===200),FaceCompareStep.vue内部单独调用showErrorModal兜底提示,与主项目FacePhotoUpload.vue的既有处理方式保持一致(即便存在与全局拦截器"双弹窗"的可能,也是本仓库 已有的既定模式,本次不额外处理去重,不属于本次任务范围)。 - 网络异常:同现有拦截器统一提示。
已知风险 / 遗留缺口(需后端后续确认)
channelNo缺口:face-recognize接口把channelNo标记为必填,但 H5 场景(draft-detail无此字段、二维码 URL 也不携带渠道信息)只能固定传空字符串。若后端@Valid校验真的强制拒绝空channelNo,人脸识别会始终失败,阻塞整个 H5 流程。需要后端确认该接口对 H5 匿名场景下channelNo为空的容忍度,或在MobileDraftDetailVo补充该字段。bankCardName缺口:send-verify-code/general的cardName固定传空字符串,若凡荣e链短信 接口对该字段有强校验,可能导致发送验证码失败。需要后端确认该字段是否允许为空。- 匿名访问
face-recognize的可行性未经真实后端验证:该接口目前仅由已登录的管理后台调用, 本次 H5 侧调用不带Authorization头(与其余 H5 匿名接口一致的约定)。由于本地无法连接真实 后端环境验证,无法确认 Spring Security 是否已将该接口路径纳入匿名白名单;若未纳入,H5 场景下 该接口会返回401,需要后端配合调整安全配置。
以上3点已同步登记到《缺失的后端接口.md》Part 12。
范围边界(本次不做)
- 不新增"人脸识别失败原因"的细分枚举分支处理(
recode取值未文档化,与主项目现状一致)。 - 不做人脸照片压缩/裁剪等图像处理。
- 不修改管理后台侧
FacePhotoUpload.vue/src/api/customer.js任何代码(仅作为接口调用参照)。 - 不解决"全局拦截器 + 组件内部"双弹窗的既有小问题(超出本次范围)。