SFT/mock-server/routes/personalOpenMobile.js

143 lines
7.6 KiB
JavaScript

// 个人开户移动端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