// 个人开户移动端H5确认页(fryl_h5 独立工程)配套 mock 路由。 // 与 wallet.js 的区别:本文件**不挂载 requireAuth**,因为这些接口面向扫码后的未登录终端客户, // 属于匿名接口(H5 页面无法获得管理后台登录态 token)。详见: // - 设计文档 docs/superpowers/specs/2026-07-31-personal-open-mobile-h5-face-recognize-and-field-sync-design.md // - 契约缺口登记 《缺失的后端接口.md》 Part 12 import express from 'express' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { personalOpenApplications, walletAccounts, balances } from '../db/seedWallet.js' import { nextId } from '../utils/id.js' import { sendOk, sendFail } from '../utils/response.js' import { genSmsCode, verifySmsCode } from '../state.js' const router = express.Router() const __dirname = path.dirname(fileURLToPath(import.meta.url)) // 保留前3位+后4位,中间4个*,与后端 PersonalOpeningMobileController.maskMobilePhone 参考实现一致 function maskMobilePhone(phone) { if (!phone || phone.length < 8) return phone || '' return `${phone.slice(0, 3)}****${phone.slice(-4)}` } router.post('/wallet-management/personal-opening/mobile/draft-detail', (req, res) => { const { applicationNo } = req.body || {} const application = personalOpenApplications.find((a) => a.applicationNo === applicationNo) if (!application) return sendFail(res, '申请单不存在或已失效,请联系柜员重新生成二维码', 404) sendOk(res, { customerName: application.customerName, // 2026-08-06 新增:真实后端 MobileDraftDetailVo 已补充 channelNo 字段(见 swagger_project_scfs_ // 2026-08-05_15-15-22.json),前端 send-verify-code/general 需要用它,不再是契约缺口 channelNo: application.channelNo, certificateType: application.certificateType, certificateNumber: application.certificateNumber, certificateEffectiveDate: application.certificateEffectiveDate, certificateExpiryDate: application.certificateExpiryDate, issuingAuthority: application.issuingAuthority, certificateAddress: application.certificateAddress, gender: application.gender, ethnic: application.ethnic, occupation: application.occupation, occupationNote: application.jobNote, bankCardNumber: application.bankCardNumber, // 明文手机号,前端持有用于调用发码/人脸识别接口;mobilePhoneMask 由后端(此处为mock)算好, // 前端页面展示时直接使用,不再自行计算 mobilePhone: application.mobilePhone, mobilePhoneMask: maskMobilePhone(application.mobilePhone) }) }) // 通用发码接口(与申请单解耦),按 mobile 作为验证码 key,见 // docs/superpowers/specs/2026-07-28-personal-open-mobile-h5-send-verify-code-api-change-design.md router.post('/wallet-management/personal-opening/mobile/send-verify-code/general', (req, res) => { const { mobile } = req.body || {} if (!mobile) return sendFail(res, '手机号不能为空', 400) genSmsCode('WALLET_PERSONAL_OPEN_MOBILE', mobile) sendOk(res, null, '验证码已发送') }) router.post('/wallet-management/personal-opening/mobile/submit', (req, res) => { const { applicationNo, verifyCode, payAgreementFileNo } = req.body || {} const application = personalOpenApplications.find((a) => a.applicationNo === applicationNo) if (!application) return sendFail(res, '申请单不存在或已失效,请联系柜员重新生成二维码', 404) if (!verifyCode || !verifySmsCode('WALLET_PERSONAL_OPEN_MOBILE', application.mobilePhone, verifyCode)) { return sendFail(res, '验证码错误或已过期', 400) } // 2026-08-06 新增:payAgreementFileNo 为前端在《用户支付服务协议》PDF末页填入客户姓名/证件 // 号码/签署日期后,调用 upload-file 上传得到的文件编号,mock 仅留存到申请单记录以便调试排查, // 不做真实的开户资料归档(真实后端行为未知,契约缺口见《缺失的后端接口.md》Part 16)。 if (payAgreementFileNo) application.payAgreementFileNo = payAgreementFileNo // 幂等:若已开立(如重复提交/已通过其它渠道确认),直接返回既有结果,不重复开户 if (application.applicationStatus === 'OPENED') { return sendOk(res, { success: true, mainAccountNo: application.mainAccountNo, customerNo: application.customerNo }) } const id = nextId('walletAccount') const seq = nextId('walletAccountSeq') const newAccountNo = `622${String(seq).padStart(13, '0')}` walletAccounts.push({ id, customerId: application.customerId, customerType: 'PERSONAL', accountNo: newAccountNo, accountType: 'A1', accountName: application.customerName, accountRelation: '0', customerNo: `CN${String(application.customerId).padStart(8, '0')}`, accountStatus: 'NORMAL', mainAccountNo: newAccountNo, openDate: new Date().toISOString().slice(0, 19).replace('T', ' ') }) balances[newAccountNo] = '0.00' application.applicationStatus = 'OPENED' application.mainAccountNo = newAccountNo application.customerNo = `CN${String(application.customerId).padStart(8, '0')}` application.updatedAt = new Date().toISOString().slice(0, 19).replace('T', ' ') sendOk(res, { success: true, mainAccountNo: application.mainAccountNo, customerNo: application.customerNo }) }) // fileType -> 本地PDF文件(assets/bank-agreements/),对应 BankAgreementFileTypeEnum。 // 2026-08-06 更新:PAY_SERVICE_AGREEMENT/PAY_SERVICE_PRIVACY 已替换为银行提供的正式模板原件 // (此前是纯占位文本PDF),因为"相关协议"步骤新增的"勾选后自动填充PDF签名页并上传"功能 // (见 AgreementStep.vue/utils/agreementPdfFill.js)依赖模板末页真实存在的"用户:/证件号码:/ // 时间: 年 月 日"空白区块及其精确坐标,占位文本PDF没有这个区块,无法用于本地联调该功能。 // CREDIT_REPORT_AUTH/THIRD_PARTY_DATA_AUTH 当前 fryl_h5"相关协议"步骤不展示,复用前两者的 // 占位文件即可(枚举本身有4项,mock 一并补全避免其它场景对接缺口,这两项不涉及填充/上传)。 const BANK_AGREEMENT_FILES = { PAY_SERVICE_AGREEMENT: { fileName: '浙江稠州商业银行用户支付服务协议.pdf', path: path.join(__dirname, '../assets/bank-agreements/pay_service_agreement.pdf') }, PAY_SERVICE_PRIVACY: { fileName: '浙江稠州商业银行用户支付服务隐私政策.pdf', path: path.join(__dirname, '../assets/bank-agreements/pay_service_privacy.pdf') }, CREDIT_REPORT_AUTH: { fileName: '个人信用信息报送查询使用授权书.pdf', path: path.join(__dirname, '../assets/bank-agreements/pay_service_agreement.pdf') }, THIRD_PARTY_DATA_AUTH: { fileName: '第三方数据信息查询和使用授权书.pdf', path: path.join(__dirname, '../assets/bank-agreements/pay_service_privacy.pdf') } } // GET /api/customer-info/bank-agreement/base64?fileType=xxx —— 2026-08-05 新增。 // 注意:必须像本文件其它路由一样不挂 requireAuth,且挂载在 mock-server/index.js 里 // customerRouter 等带 router.use(requireAuth) 的路由之前,否则会被误拦截返回401 // (见 mock-server/index.js 顶部注释与本文件顶部注释说明的挂载顺序缺陷)。 router.get('/customer-info/bank-agreement/base64', (req, res) => { const { fileType } = req.query || {} const file = BANK_AGREEMENT_FILES[fileType] if (!file) return sendFail(res, '不支持的协议文件类型', 400) try { const fileContent = fs.readFileSync(file.path).toString('base64') sendOk(res, { fileType, fileName: file.fileName, fileContent }) } catch { sendFail(res, '协议模板文件读取失败', 500) } }) export default router