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

26 KiB
Raw Blame History

个人开户移动端H5确认页 设计文档

背景

docs/superpowers/specs/2026-07-23-personal-open-confirm-modal-design.md 已实现管理后台侧"个人开户"新增表单的二次确认弹窗 + "邀请扫码办理",柜员点击后生成二维码,二维码内容为:

${VITE_H5_QRCODE_BASE_URL}${VITE_H5_QRCODE_PATH}?applicationNo=${applicationNo}

当前 VITE_H5_QRCODE_PATH=/mobile/personal-open。该设计文档明确将"扫码后跳转的移动端 H5 页面"排除在范围外。后端已提供对应的 personal-opening/mobile/draft-detailpersonal-opening/mobile/submit 接口(见《缺失的后端接口.md》第19条但前端从未实现这个 H5 页面。本次设计即补齐该页面。

范围

  • 新建一个完全独立的子工程 ./fryl_h5(独立 package.json/构建产物/部署单元,不与管理后台共用依赖或构建配置)。
  • 仅实现"个人开户"扫码核实场景一个页面流程,不涉及企业开户 confirm、贷款申请确认扫码等其他移动端确认场景。
  • 不涉及管理后台侧代码改动(二维码生成逻辑已完成,本次不改)。

技术方案

  • 技术栈Vue3 + Vite + Vant移动端 UI 组件库),纯 JavaScript与主项目一致不引入 TypeScript
  • 工程定位:面向终端客户、无需登录鉴权的匿名 H5 页面(不同于管理后台的员工登录体系),因此不引入 Pinia/auth store/refresh-token 逻辑。
  • 路由vue-router,单一业务路由 /mobile/personal-open(与二维码 VITE_H5_QRCODE_PATH 保持一致),通过 query 参数 applicationNo 定位申请单。页面内部用组件本地状态(currentStep ref值为 INFO/FACE/SMS)维护向导步骤,不为每个步骤单独设路由(避免用户直接分享/刷新中间步骤 URL 导致状态错乱)。
  • BASE_URL:不提供页面内可配置入口(用户明确决定,区别于主项目登录页齿轮图标切换的默认惯例),仅通过 .env.development/.env.productionVITE_API_BASE_URL 区分环境。

页面流程

2026-08-05 更新:原"步骤1 信息核对(含协议展示)"拆分为两个独立步骤,协议勾选不再耦合在 信息核对页面里,改为单独的"相关协议"步骤,且该步骤调用真实后端接口获取协议模板原文(不再是 占位文案)。整体变为 5 步向导

进入页面 → 调用 draft-detail 加载草稿
  ├─ 失败/无效 applicationNo → 展示错误提示页(终态,无法继续)
  └─ 成功 → 步骤1 信息核对 → 步骤2 相关协议 → 步骤3 人脸识别 → 步骤4 手机验证+提交 → 步骤5 结果页(终态)

2026-08-05 二次更新按产品要求客户在步骤2"相关协议"点击"下一步"后跳过步骤3人脸识别 直接进入步骤4手机验证+提交

进入页面 → 调用 draft-detail 加载草稿
  ├─ 失败/无效 applicationNo → 展示错误提示页(终态,无法继续)
  └─ 成功 → 步骤1 信息核对 → 步骤2 相关协议 → 步骤4 手机验证+提交 → 步骤5 结果页(终态)
             (步骤3 人脸识别:当前从向导流程跳过,代码未删除,见下方"步骤3"章节说明)

PersonalOpenMobile.vueAgreementStep@next 直接指向 currentStep = 'SMS' SmsVerifyStep@prev 直接指向 currentStep = 'AGREEMENT'FaceCompareStep.vue 组件、 faceRecognizeApi 接口封装、facePhoto/faceVerified 状态均未删除,只是不再被引用/声明, 如需恢复该环节,把上述两个事件改回指向 'FACE' 并重新引入组件即可(PersonalOpenMobile.vue 里保留了具体恢复步骤的代码注释)。

步骤1/2/4之间支持"上一步"返回步骤3当前不在流程内故不涉及步骤5结果页为终态 不支持返回上一步。页面整体视觉仍为 "每步骤单独全屏展示,点击下一步整页切换"van-nav-bar + 组件内 currentStep 切换),不是 产品截图里"顶部业务确认banner + 左侧1-5编号竖向步骤条、未激活步骤仅显示标题"的单页累积式布局 ——本次改动经与产品确认,只对齐内容/交互逻辑,不做整体视觉布局改版。

步骤0加载态 / 错误态

  • 页面挂载时读取 query 中的 applicationNo,为空则直接展示"链接无效"错误态(不发请求)。
  • 调用 mobile/draft-detail 接口:
    • code === 200进入步骤1。
    • code !== 200(如申请单不存在/已过期/已提交过):展示错误提示页,用 message 原文展示,提供"关闭页面"提示文案无可操作按钮H5 场景下没有其它页面可跳转)。

步骤1信息核对2026-08-05 更新:不再包含协议展示/勾选协议已独立为步骤2

展示字段(与管理后台"个人开户申请详情"现有展示字段保持一致,全部只读):

字段 来源
客户姓名 customerName
证件类型 certificateType
证件号码 certificateNumber
证件有效期起 certificateEffectiveDate
证件失效日期 certificateExpiryDate
签发机关 issuingAuthority
证件地址 certificateAddress
性别 gender
民族 ethnic
职业 occupation+ occupationNote,当 occupation 为"其他"时展示备注)
银行卡号 bankCardNumber
手机号 mobilePhoneMask2026-07-31 更新:后端直接提供掩码字段,前端不再自行计算,见接口契约章节)

不展示证件照片本身(客户本人无需再核对自己的证件照片图像,且原始图片以文件形式存放不适合在此场景重复暴露)、不展示 applicationStatus/failReason/mainAccountNo/customerNo(这些是提交后的结果字段,核对阶段不应出现)。

点击"确认无误,下一步"进入步骤22026-08-05 更新:本步骤不再包含协议勾选/占位文案, 按钮不再有任何禁用条件点击直接进入步骤2

步骤2相关协议2026-08-05 新增,独立步骤,接入真实协议模板接口

  • 展示2份协议的"我已阅读并同意"复选框:《浙江稠州商业银行用户支付服务协议》 PAY_SERVICE_AGREEMENT)、《浙江稠州商业银行用户支付服务隐私政策》(PAY_SERVICE_PRIVACY)。 取值来源于新增的 BankAgreementFileTypeEnum该枚举共4项另外2项 CREDIT_REPORT_AUTH个人信用信息报送查询使用授权书/THIRD_PARTY_DATA_AUTH第三方数据信息 查询和使用授权书不在本页面展示范围内——与产品确认个人开户场景只需前2项
  • 点击协议标题链接时调用 GET /api/customer-info/bank-agreement/base64?fileType=xxx 拉取协议模板文件base64 编码的 PDF不能直接拼成 data:application/pdf;base64,... 赋给 iframe 的 src——真实后端联调时实测 Safari/WebKit 会拦截 iframe 对 data: URI 的 导航(表现为接口正常返回 fileContent但预览区域空白Chrome/安卓不受影响),必须把 base64 转成 Blob 再用 URL.createObjectURL() 生成 blob: URL 赋给 iframe2026-08-05 二次更新,见 AgreementStep.vuebase64ToBlobUrl 函数),组件卸载时统一 revokeObjectURL 释放内存。用 van-popup 内嵌该 iframe 展示原文;同一份协议重复点击时 使用本地缓存(缓存的是 blob: URL不重复请求/解码。
  • 两份协议均勾选后"下一步"按钮才可点击,未勾选禁用;底部提供"上一步"文字链接返回步骤1 (协议勾选状态本次不做跨步骤保留,退回再进入需重新勾选,与产品截图行为一致——截图未展示 该步骤支持"上一步",此处出于与其它步骤一致的返回体验保留了该入口,纯前端交互决策, 不影响提交参数)。
  • 点击"下一步"进入步骤3人脸对比2026-08-05 二次更新:当前从流程中跳过,实际直接进入 步骤4,见下方说明)。
  • 2026-08-06 更新两份协议都勾选后自动在《用户支付服务协议》PDF末页填入客户姓名/证件 号码/当前日期并调用通用文件上传接口拿到 fileNo"下一步"在文件生成并上传成功前保持禁用; 该 fileNo 会随最终提交一起传给后端(mobile/submit 新增 payAgreementFileNo 字段)。 详细方案PDF坐标测量方法、中文字体渲染的两个已知 bug 及规避方式、mock-server 配套改造)见 独立设计文档 docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md 接口契约缺口见《缺失的后端接口.md》Part 19。

步骤3人脸对比2026-08-05 二次更新:按产品要求,当前从向导流程中跳过,代码保留未删除

AgreementStep步骤2点击"下一步"后直接进入步骤4手机验证不再经过本步骤 FaceCompareStep.vuefaceRecognizeApi、下方描述的识别逻辑均未删除,只是暂未被 PersonalOpenMobile.vue 引用/挂载。以下为该步骤原有设计描述,供后续恢复时参考:

  • 提供"拍照"入口(调用 Vant 的图片选择/拍照能力,accept="image/*" capture="camera"),拍照或选择后 立即调用后端真实提供的人脸识别接口 POST /api/customer-info/personal/face-recognize(与主项目 管理后台 src/components/FacePhotoUpload.vue 调用同一接口),详见 docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md
  • 识别通过(响应 data.fileNo 非空)才能点击"下一步";识别未通过则清空已选照片,强制重新拍摄。
  • 识别中整页遮罩禁止操作,识别结果不会透传给 mobile/submit(该接口描述"影像资料由后端从本地 查询"),本步骤仅作为"识别通过才能继续"的前置校验关卡。
  • 支持"上一步"返回步骤2相关协议从步骤4手机验证退回本步骤时若照片与识别通过状态仍保留 不重新触发识别。

步骤4手机验证 + 提交

  • 展示掩码手机号(直接使用 mobile/draft-detail 返回的 mobilePhoneMask不再前端自行计算见2026-07-31更新)。
  • "获取验证码"按钮:点击调用通用发码接口 send-verify-code/general(见下),成功后按钮进入 60 秒倒计时禁用态,message.success('验证码已发送')2026-08-06 更新:请求体 channelNo 取自 draft-detail 返回的 MobileDraftDetailVo.channelNo(不再固定传空字符串)、 cardName 取自 customerName(个人账户卡户名与客户姓名一致,MobileDraftDetailVo 无独立的 "银行卡户名"字段)、tradeNo 固定 "221506"tradeType 固定 "0"(此前两者取值写反且 tradeType 超长,导致真实后端报错短信类型字段长度超出,先临时改成 "1",后端确认正确 取值应为 "0",已二次修正)。真实后端联调还发现该短信类型要求 cardNo/cardName/idNo 三者必须同时非空(报错该短信类型银行卡号、户名、证件号码或者电子账户、金额必须填写 故点击"获取验证码"前新增前置校验:mobile/channelNo/cardNo/cardName/idNo 任一为空 都直接弹窗提示"请联系柜员核实"并不发起请求,避免带着必然失败的参数反复重试(见 SmsVerifyStep.vuehandleSendCode 函数)。
  • 验证码输入框6位数字
  • "确认提交"按钮:仅当验证码已输入时可点击,点击后:
    • 按钮 loading 态防重复点击,期间整页 van-overlay 遮罩禁止操作(耗时操作统一等待态,与主项目 busy/a-spin 规范同一诉求Vant 场景下用 van-overlay + van-loading 实现)。
    • 调用 mobile/submit{applicationNo, verifyCode})。2026-08-06 更新:新增 payAgreementFileNo 字段取自步骤2生成并上传的《用户支付服务协议》PDF文件编号通过 PersonalOpenMobile.vue 提升到父组件状态后作为 prop 传入本组件),详见 docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md
    • code === 200跳转步骤5展示 data.success 对应的成功/失败内容。
    • code !== 200(验证码错误等业务异常):停留在本页,用 message 原文以 Vant showFailToast/showNotify 提示错误允许用户重新输入验证码或重新获取验证码后重试不跳转步骤5。
  • 支持"上一步"返回步骤2相关协议2026-08-05 二次更新步骤3人脸对比已跳过"上一步" 不再经过它)。

步骤5结果页终态

  • 成功(data.success === true):展示成功图标 + "开户成功" + 主账户号(mainAccountNo+ 客户号(customerNo)。
  • 失败(data.success === false):展示失败图标 + "开户失败" + 失败原因(failReason)。
  • 无任何可点击的跳转/重试按钮H5 场景下没有其它页面可去,且提交已产生后端最终结果,不支持重复提交)。

接口契约

已有明确契约(来自 swagger无需假设2026-07-31 更新为完整真实字段

POST /api/wallet-management/personal-opening/mobile/draft-detail 请求:{ applicationNo } 响应 dataMobileDraftDetailVocustomerName/channelNo2026-08-06 新增字段 send-verify-code/general 依赖它)/certificateType/certificateNumber/ certificateEffectiveDate/certificateExpiryDate/issuingAuthority/gender/certificateAddress/ occupation/occupationNote/mobilePhone(明文)/mobilePhoneMask(后端直接提供的掩码值)/ bankCardNumber/ethnic

POST /api/wallet-management/personal-opening/mobile/send-verify-code/general 2026-08-06 补全契约SendVerifyCodeGeneralParamVo 请求:{ channelNo, tradeNo(固定"221506"), tradeType(固定"0"), mobile, cardNo, cardName, idNo } cardNo/cardName/idNo 三者必须同时非空,否则后端报错该短信类型银行卡号、户名、证件 号码或者电子账户、金额必须填写 响应:{ code, message, data: null }

POST /api/customer-info/personal/face-recognize2026-07-31 新增,真实接口) 请求:{ fileData(人脸照片base64), idNo, name, channelNo } 响应 dataFaceRecognizeResultVofileNo/recode/recodeInfo/sysSerialNo/sysDate

POST /api/wallet-management/personal-opening/mobile/submit 请求:{ applicationNo, verifyCode } 响应 dataSubmitApplicationResultVosuccess/mainAccountNo/customerNo/failReason

GET /api/customer-info/bank-agreement/base642026-08-05 新增,真实接口) 请求query 参数 fileType(枚举 BankAgreementFileTypeEnum,本页面只用到 PAY_SERVICE_AGREEMENT/PAY_SERVICE_PRIVACY 两项见步骤2说明 响应 dataBankAgreementFileVofileType/fileName/fileContentbase64 编码的 PDF

仍未解决的契约缺口(详见《缺失的后端接口.md》Part 12 第97条、Part 15 第97/99条

  1. channelNo/bankCardName 两个字段仍不存在——2026-08-06 更新 MobileDraftDetailVo 已补充 channelNo 字段,send-verify-code/generalchannelNo 已从 draft-detail 响应取值,不再是契约缺口;cardName(银行卡户名)仍无独立字段,用 customerName 代替(个人账户场景卡户名与客户姓名一致)。face-recognizechannelNo 因该步骤当前已从流程跳过见步骤3说明暂不受影响固定传空字符串的写法保留在 faceRecognizeApi 里未改动。
  2. 匿名访问 face-recognize 的可行性未经真实后端环境验证:该接口此前只被已登录的管理后台 调用H5 侧本次调用不带 Authorization,本地无法验证真实后端是否已将其纳入匿名白名单。

详见 docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md 的"已知风险/遗留缺口"章节。

请求头规则

  • 不携带 Authorization这些接口面向未登录的终端客户H5 页面无法获得管理后台的登录态 token属于匿名接口face-recognize 是否真的允许匿名访问尚待后端确认见上方契约缺口第2点
  • 仍携带通用 trace headerAppno/Serialno/Transdate/Transtradetime):沿用主项目 src/utils/traceHeaders.js 的实现逻辑(原样迁移一份到 fryl_h5,无需改动)。
  • 不携带 Channelno 请求头:二维码 URL 中不包含渠道信息(?applicationNo=xxx),页面无渠道上下文,按现有规则("channelNo 如果页面没有渠道信息,则不需要传值")不传该 header。注意:这里指的是 trace header 里的 Channelno,与请求体中的 channelNo 参数是两个独立概念,互不影响。
  • 响应体统一 {code, message, data} 判定成功用 code === 200,与主项目一致。

工程结构(fryl_h5

fryl_h5/
├── index.html
├── package.json          # 独立依赖vue、vue-router、vant、axios、dayjs、vite
├── vite.config.js         # 参照主项目结构,@ 指向 ./srcserver.port 另设(如 5174,避免与主项目 5173 冲突)
├── .env.development       # VITE_API_BASE_URL=http://localhost:8888(与主项目 mock-server 共用端口)
├── .env.production        # VITE_API_BASE_URL=(留空,构建时按实际环境注入或走同源部署)
└── src/
    ├── main.js
    ├── App.vue
    ├── router/index.js               # 单路由 /mobile/personal-open
    ├── api/
    │   ├── request.js                 # 精简版 axios 实例baseURL + trace header注入 + 统一错误提示,无 auth/refresh 逻辑
    │   └── personalOpen.js            # fetchDraftDetailApi / sendVerifyCodeApi / faceRecognizeApi / submitApplicationApi / fetchBankAgreementApi(2026-08-05新增) / uploadFileApi(2026-08-06新增)
    ├── constants/
    │   └── enums.js                   # 含 BANK_AGREEMENT_FILE_TYPE_LABELS(2026-08-05新增)
    ├── utils/
    │   └── traceHeaders.js            # 从主项目原样迁移
    └── views/
        └── PersonalOpenMobile.vue     # 页面主组件,内部拆 5 个子组件2026-08-05 由4个拆为5个
            ├── steps/InfoConfirmStep.vue
            ├── steps/AgreementStep.vue    # 2026-08-05 新增:相关协议,调用 fetchBankAgreementApi
            ├── steps/FaceCompareStep.vue
            ├── steps/SmsVerifyStep.vue
            └── steps/ResultStep.vue

2026-08-06 更新:新增 src/utils/agreementPdfFill.js(用 pdf-lib 在《用户支付服务协议》 PDF末页填入客户姓名/证件号码/当前日期;姓名用 Canvas 渲染成小图片再嵌入,不嵌入自定义中文 字体——最初的字体嵌入方案已推翻,原因及推翻过程见 docs/superpowers/specs/2026-08-06-agreement-pdf-fill-and-upload-design.md),新增依赖 pdf-lib

mock-server 配套改造(供本地联调,2026-07-31 更新:新增人脸识别路由 + 路由挂载顺序修复,详见

docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md

mock-server/routes/personalOpenMobile.js 实现三个路由,新增 mock-server/routes/faceRecognize.js 实现人脸识别路由(不带 requireAuth管理后台与H5共用mock-server/state.js 复用现有 genSmsCode/verifySmsCode2026-07-28 更新:发码接口改为通用 /general 路径且不再携带 applicationNo,验证码 key 统一改用手机号):

  • POST /wallet-management/personal-opening/mobile/draft-detail:按 applicationNo 查找 personalOpenApplications,不存在返回 sendFail('申请单不存在', 404);存在则返回真实 swagger 字段,mobilePhone 直接返回 application.mobilePhone 明文,mobilePhoneMask 由 mock 自行按"前3后4"算法计算返回(后端真实接口已确认直接提供该字段,前端不再自行计算),occupationNoteapplication.jobNote种子数据字段名未变仅响应体字段名对齐真实swagger2026-08-06 更新:新增返回 channelNo(取 application.channelNo,种子数据已有此字段),真实 swagger 已补充该字段,不再是契约缺口;bankCardName 仍无对应字段,前端改用 customerName 代替。
  • POST /customer-info/personal/face-recognize:校验 fileData/idNo/name 非空,缺失返回 sendFail;校验通过返回 {fileNo, recode:'0000', recodeInfo:'识别通过', sysSerialNo, sysDate},不模拟"人脸不符"等业务失败场景(无可参照的真实失败样例,与主项目现状一致)。
  • POST /wallet-management/personal-opening/mobile/send-verify-code/general:不再依赖 applicationNo(新接口本身与申请单解耦),直接以请求体 mobile 校验非空后调用 genSmsCode('WALLET_PERSONAL_OPEN_MOBILE', mobile)(以手机号作为 key固定码 123456 同时生效(与现有短信验证码 mock 规则一致)。
  • POST /wallet-management/personal-opening/mobile/submit:申请单不存在返回 sendFail('申请单不存在', 404)(与其余 mock 路由一致,code!==200,前端停留本页提示,不进入结果页——现实中该场景仅发生于极端并发下的过期链接,重试大概率仍失败,但由前端统一按"停留本页"规则处理,不单独特殊化);校验 verifySmsCode('WALLET_PERSONAL_OPEN_MOBILE', application.mobilePhone, verifyCode) 失败返回 sendFail('验证码错误或已过期', 400)key 与发码环节的 mobile 保持一致);两者均为 code!==200,触发前端"停留本页"分支。验证码校验通过后执行与现有 query-progress 相同的"生成主账户"逻辑(若尚未开立),返回 code===200 + {success: true, mainAccountNo, customerNo},前端据此跳转结果页展示成功。data.success === false 的业务失败分支如核心银行侧开户失败mock 暂不模拟(无对应真实失败场景可参照),仅保留前端结果页对该字段的展示逻辑,供后端真实接口返回失败时兜底展示。
  • GET /customer-info/bank-agreement/base642026-08-05 新增):按 fileType 查 mock 内置的 文件名->本地占位 PDF 映射表(mock-server/assets/bank-agreements/*.pdf,占位内容非正式条款 文本,仅用于验证前端 iframe 预览 PDF 的渲染效果),读取文件并转 base64 返回 {fileType, fileName, fileContent}fileType 不在枚举内返回 sendFail('不支持的协议文件类型', 400)。 同样不带 requireAuth,且路由定义在本文件内(personalOpenMobileRouter 本身已在 customerRouter 等之前挂载,见下方"路由挂载顺序缺陷"说明),无需额外调整挂载顺序。

路由挂载顺序缺陷2026-07-31 修复)mock-server/index.jscustomerRouter/walletRouter/ userRouter/roleRouter/baseConfigRouter 等均以 app.use('/api', xxxRouter) 形式挂载且内部 用 router.use(requireAuth)(不带路径)拦截鉴权,会拦截"该 router 挂载前缀下的任意路径"而不仅是 自身定义的路由。此前 personalOpenMobileRouter 挂载顺序在它们之后,导致本地 mock 环境下 H5 匿名 接口全部被误拦截返回401已实测复现。已将 personalOpenMobileRouter/faceRecognizeRouter 调整 到这些 router 之前挂载,详见《缺失的后端接口.md》Part 15 第100条。

此改动仅为本地离线联调用,真实后端接口到位后无需删除(管理后台侧 mock 路由同样长期保留,供后续演示环境使用)。

环境变量

fryl_h5/.env.development

VITE_API_BASE_URL=http://localhost:8888

fryl_h5/.env.production

VITE_API_BASE_URL=

(留空表示构建产物部署时与后端网关同源,或由运维在部署时通过其它机制配置;不提供页面内配置入口,符合本次用户决策。)

2026-08-01 更新:本地联调真实后端遇 CORS 的临时规避方案——与根目录管理后台完全一致的做法: 在 fryl_h5/.env.development.local(已被 .gitignore*.local 规则忽略,不会提交)中配置 DEV_API_PROXY_TARGET=真实后端地址,同时把该文件里的 VITE_API_BASE_URL 设为空字符串, fryl_h5/vite.config.js 会把浏览器发往同源 /api 的请求在 Node 端转发给真实后端(changeOrigin

  • 请求/响应体打印日志,与主项目 vite.config.js 的代理配置逐条对应,仅端口号随本工程固定为 5174。未配置 DEV_API_PROXY_TARGET 时行为不变(直接请求 VITE_API_BASE_URL 指向的地址, 默认走本地 mock server。仅用于本地 npm run dev 调试,不影响生产构建逻辑。

依赖清单(新增)

vuevue-routervantaxiosdayjstraceHeaders.js 依赖)、@vitejs/plugin-vuedevDependenciesvitedevDependencies。均为独立 fryl_h5/package.json,与根目录 package.json 完全隔离。

范围边界(本次不做)

  • 不实现真实人脸活体检测/比对逻辑后端无对应接口仅做拍照UI占位——2026-07-31 更新 后端已提供真实人脸识别接口,已改为真实调用,见 docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md
  • 不实现协议正式文本/链接(占位文案,后续产品补充后再替换,不涉及提交参数变更)。—— 2026-08-05 更新:后端已提供 bank-agreement/base64 真实接口,"相关协议"步骤已改为调用 真实接口展示协议原文PDF不再是占位文案。
  • 不修改管理后台侧任何代码(二维码生成逻辑已完成)。
  • 不做 H5 页面内 BASE_URL 可配置入口。
  • 不支持结果页之后的"重新提交"/跳转其它页面。
  • 不引入 TypeScript、不引入状态管理库Pinia 等,本页面状态足够简单,用组件内 ref 管理即可)。