Files
medical-mall/utils/bankUtils.uts
2026-05-14 15:28:09 +08:00

296 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* bankUtils.uts
* 银行卡工具集BIN 识别、OCR 适配、银行列表、表单校验
* 所有接口均与真实 OCR 厂商解耦,需接入时只修改 recognizeBankCardFromImage 内部实现
*/
// ─────────────────────────────────────────────
// 1. 银行列表数据源
// ─────────────────────────────────────────────
export type BankItem = {
code: string // 内部 code
name: string // 银行全称
shortName: string // 简称,用于 UI 显示
logo: string // 本地 logo 路径(没有则为空串,前端显示占位)
}
export const BANK_LIST: BankItem[] = [
{ code: 'ICBC', name: '中国工商银行', shortName: '工商银行', logo: '' },
{ code: 'CCB', name: '中国建设银行', shortName: '建设银行', logo: '' },
{ code: 'ABC', name: '中国农业银行', shortName: '农业银行', logo: '' },
{ code: 'BOC', name: '中国银行', shortName: '中国银行', logo: '' },
{ code: 'COMM', name: '交通银行', shortName: '交通银行', logo: '' },
{ code: 'CMB', name: '招商银行', shortName: '招商银行', logo: '' },
{ code: 'CITIC', name: '中信银行', shortName: '中信银行', logo: '' },
{ code: 'SPDB', name: '浦发银行', shortName: '浦发银行', logo: '' },
{ code: 'CEB', name: '光大银行', shortName: '光大银行', logo: '' },
{ code: 'HXB', name: '华夏银行', shortName: '华夏银行', logo: '' },
{ code: 'CMBC', name: '民生银行', shortName: '民生银行', logo: '' },
{ code: 'PAB', name: '平安银行', shortName: '平安银行', logo: '' },
{ code: 'GDB', name: '广发银行', shortName: '广发银行', logo: '' },
{ code: 'BOBJ', name: '北京银行', shortName: '北京银行', logo: '' },
{ code: 'SHRCB', name: '上海银行', shortName: '上海银行', logo: '' },
{ code: 'PSBC', name: '中国邮政储蓄银行', shortName: '邮储银行', logo: '' },
]
// ─────────────────────────────────────────────
// 2. BIN → 银行名称映射(仅 6 位前缀)
// 来源:公开 BIN 数据,仅做参考识别用途
// ─────────────────────────────────────────────
type BinEntry = {
prefix: string
bankCode: string
}
const BIN_MAP: BinEntry[] = [
// 工商银行
{ prefix: '6212', bankCode: 'ICBC' },
{ prefix: '6216', bankCode: 'ICBC' },
{ prefix: '6217', bankCode: 'ICBC' },
{ prefix: '6222', bankCode: 'ICBC' },
// 建设银行
{ prefix: '6217', bankCode: 'CCB' },
{ prefix: '6227', bankCode: 'CCB' },
{ prefix: '6236', bankCode: 'CCB' },
// 农业银行
{ prefix: '6225', bankCode: 'ABC' },
{ prefix: '6228', bankCode: 'ABC' },
{ prefix: '9559', bankCode: 'ABC' },
// 中国银行
{ prefix: '6013', bankCode: 'BOC' },
{ prefix: '6259', bankCode: 'BOC' },
{ prefix: '6539', bankCode: 'BOC' },
// 交通银行
{ prefix: '6222', bankCode: 'COMM' },
{ prefix: '6230', bankCode: 'COMM' },
{ prefix: '6266', bankCode: 'COMM' },
// 招商银行
{ prefix: '6214', bankCode: 'CMB' },
{ prefix: '6225', bankCode: 'CMB' },
{ prefix: '6245', bankCode: 'CMB' },
// 中信银行
{ prefix: '6217', bankCode: 'CITIC' },
{ prefix: '4367', bankCode: 'CITIC' },
// 浦发银行
{ prefix: '6217', bankCode: 'SPDB' },
{ prefix: '6216', bankCode: 'SPDB' },
// 平安银行
{ prefix: '6214', bankCode: 'PAB' },
{ prefix: '6216', bankCode: 'PAB' },
// 邮储银行
{ prefix: '6210', bankCode: 'PSBC' },
{ prefix: '6221', bankCode: 'PSBC' },
{ prefix: '6230', bankCode: 'PSBC' },
]
/**
* 根据卡号前缀匹配银行(优先匹配最长前缀)
* @param cardNo 银行卡号(至少 6 位)
* @returns BankItem | null
*/
export function detectBankByBin(cardNo: string): BankItem | null {
const cleaned = cardNo.replace(/\s/g, '')
if (cleaned.length < 6) return null
const prefix6 = cleaned.substring(0, 6)
const prefix4 = cleaned.substring(0, 4)
// 优先 6 位匹配
for (let i = 0; i < BIN_MAP.length; i++) {
if (BIN_MAP[i].prefix === prefix6) {
return findBankByCode(BIN_MAP[i].bankCode)
}
}
// 降级 4 位匹配
for (let i = 0; i < BIN_MAP.length; i++) {
if (BIN_MAP[i].prefix === prefix4) {
return findBankByCode(BIN_MAP[i].bankCode)
}
}
return null
}
function findBankByCode(code: string): BankItem | null {
for (let i = 0; i < BANK_LIST.length; i++) {
if (BANK_LIST[i].code === code) return BANK_LIST[i]
}
return null
}
// ─────────────────────────────────────────────
// 3. 银行卡号格式化(每 4 位一空格,仅展示用)
// ─────────────────────────────────────────────
export function formatCardNo(raw: string): string {
const digits = raw.replace(/\D/g, '')
let result = ''
for (let i = 0; i < digits.length; i++) {
if (i > 0 && i % 4 === 0) result += ' '
result += digits[i]
}
return result
}
/** 去除格式化空格,返回纯数字 */
export function cleanCardNo(formatted: string): string {
return formatted.replace(/\s/g, '')
}
// ─────────────────────────────────────────────
// 4. 银行卡号脱敏(验证页展示用)
// ─────────────────────────────────────────────
export function maskCardNo(cardNo: string): string {
const digits = cardNo.replace(/\s/g, '')
if (digits.length < 8) return digits
const prefix = digits.substring(0, 4)
const suffix = digits.substring(digits.length - 4)
return prefix + ' **** **** ' + suffix
}
// ─────────────────────────────────────────────
// 5. Luhn 校验(可选,建议开启)
// ─────────────────────────────────────────────
export function luhnCheck(cardNo: string): boolean {
const digits = cardNo.replace(/\s/g, '')
if (digits.length < 13 || digits.length > 19) return false
let sum = 0
let alternate = false
for (let i = digits.length - 1; i >= 0; i--) {
const digitStr = digits.substring(i, i + 1)
let n = parseInt(digitStr)
if (alternate) {
n *= 2
if (n > 9) n -= 9
}
sum += n
alternate = !alternate
}
return sum % 10 === 0
}
// ─────────────────────────────────────────────
// 6. 手机号校验
// ─────────────────────────────────────────────
export function isValidPhone(phone: string): boolean {
return /^1[3-9]\d{9}$/.test(phone)
}
// ─────────────────────────────────────────────
// 7. OCR 适配层(银行卡识别)
// 【接入说明】真实接入时,在此处替换实现:
// - 腾讯云:调用 /ocr/v3/BankCardOCR
// - 阿里云:调用 /cardrecog/recognize_bank_card
// - 自有服务:调用项目后端 OCR 接口
// 注意:不要在此硬编码任何 SecretKey / AccessKey
// ─────────────────────────────────────────────
export type OCRResult = {
success: boolean
cardNo: string
bankName: string // 识别出的银行名称(可能为空)
errorMsg: string
}
/**
* 选择图片并识别银行卡
* 当前为"前端完整流程 + 后端待接入"占位实现
* @returns OCRResult
*/
export async function recognizeBankCardFromImage(): Promise<OCRResult> {
return new Promise<OCRResult>((resolve) => {
function handleSelectedImage(filePath: string): void {
if (filePath === '') {
resolve({ success: false, cardNo: '', bankName: '', errorMsg: '未选择图片' } as OCRResult)
return
}
console.log('[OCR] 已选择图片:', filePath, 'OCR 服务待接入')
resolve({
success: false,
cardNo: '',
bankName: '',
errorMsg: 'OCR 服务待接入,请手动输入卡号'
} as OCRResult)
}
uni.chooseImage({
count: 1,
sourceType: ['camera', 'album'],
success: (imgRes: ChooseImageSuccess): void => {
const filePath = (imgRes.tempFilePaths as string[])[0]
try {
handleSelectedImage(filePath)
} catch (e) {
console.error('[OCR] 识别异常:', e)
resolve({ success: false, cardNo: '', bankName: '', errorMsg: 'OCR 识别失败' } as OCRResult)
}
},
fail: () => {
resolve({ success: false, cardNo: '', bankName: '', errorMsg: '取消选择' } as OCRResult)
}
})
})
}
// ─────────────────────────────────────────────
// 8. 短信验证码适配层
// 【接入说明】项目后端接入短信服务后,替换此方法
// ─────────────────────────────────────────────
export type SmsResult = {
success: boolean
errorMsg: string
}
/**
* 发送短信验证码
* 当前为占位实现,接入阿里云/腾讯云短信后替换此处
* @param phone 11 位手机号
*/
export async function sendSmsCode(phone: string): Promise<SmsResult> {
if (!isValidPhone(phone)) {
return { success: false, errorMsg: '手机号格式不正确' } as SmsResult
}
try {
// ─── TODO: 接入真实短信服务 ────────────────────────────────
// 示例(项目后端代理):
// const res = await uni.request({
// url: '/api/sms/send',
// method: 'POST',
// data: { phone, scene: 'bind_bank_card' }
// })
// if (res.data.code === 0) return { success: true, errorMsg: '' }
// return { success: false, errorMsg: res.data.message }
// ─────────────────────────────────────────────────────────
// 占位实现:模拟发送成功,验证码请查看控制台
console.log('[SMS] 占位发送验证码到:', phone, ',模拟验证码: 123456')
return { success: true, errorMsg: '' } as SmsResult
} catch (e) {
console.error('[SMS] 发送异常:', e)
return { success: false, errorMsg: '发送失败,请稍后重试' } as SmsResult
}
}
/**
* 验证短信验证码
* 当前为占位实现:任何 6 位数字均视为通过
* @param phone 手机号
* @param code 用户输入的验证码
*/
export async function verifySmsCode(phone: string, code: string): Promise<SmsResult> {
if (code.length !== 6) {
return { success: false, errorMsg: '请输入 6 位验证码' } as SmsResult
}
try {
// ─── TODO: 接入真实验证逻辑 ────────────────────────────────
// const res = await uni.request({ ... })
// ─────────────────────────────────────────────────────────
// 占位:校验通过
console.log('[SMS] 占位验证通过, phone:', phone, 'code:', code)
return { success: true, errorMsg: '' } as SmsResult
} catch (e) {
console.error('[SMS] 验证异常:', e)
return { success: false, errorMsg: '验证失败,请重试' } as SmsResult
}
}