143 lines
7.6 KiB
JavaScript
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
|