金融类项目常用工具函数库
在金融项目开发中,精度、安全性和可靠性是首要考虑的因素。本文将系统地介绍金融项目中常用的工具函数,并深入分析其实现原理和最佳实践。
金额处理工具函数
金融项目中最常见的需求就是金额的格式化和计算。JavaScript 的浮点数精度问题在金融场景中是致命的,我们需要特别处理。
金额格式化
typescript/** * 金额格式化 * @param amount 金额(单位:元) * @param options 格式化选项 */ interface FormatMoneyOptions { /** 货币符号 */ symbol?: string /** 小数位数 */ precision?: number /** 千分位分隔符 */ separator?: string /** 小数点符号 */ decimalSeparator?: string /** 是否显示符号 */ showSymbol?: boolean } export function formatMoney( amount: number | string, options: FormatMoneyOptions = {} ): string { const { symbol = '¥', precision = 2, separator = ',', decimalSeparator = '.', showSymbol = true } = options // 转换为数字并修正精度 const num = typeof amount === 'string' ? parseFloat(amount) : amount if (isNaN(num)) { return showSymbol ? `${symbol}0${decimalSeparator}${'0'.repeat(precision)}` : '0.00' } // 使用 toFixed 保留小数位 const [integer, decimal] = num.toFixed(precision).split('.') // 添加千分位分隔符 const formattedInteger = integer.replace(/B(?=(d{3})+(?!d))/g, separator) // 组合结果 const result = decimal ? `${formattedInteger}${decimalSeparator}${decimal}` : formattedInteger return showSymbol ? `${symbol}${result}` : result } // 使用示例 formatMoney(1234567.89) // ¥1,234,567.89 formatMoney(1234567.89, { symbol: '$' }) // $1,234,567.89 formatMoney(1234567, { precision: 0 }) // ¥1,234,567
金额转换(分转元、元转分)
typescript/** * 分转元(服务端通常以分为单位存储) */ export function centToYuan(cent: number, precision: number = 2): number { return Number((cent / 100).toFixed(precision)) } /** * 元转分(避免浮点数精度问题) */ export function yuanToCent(yuan: number | string): number { const num = typeof yuan === 'string' ? parseFloat(yuan) : yuan return Math.round(num * 100) } // 使用示例 centToYuan(123456) // 1234.56 yuanToCent(1234.56) // 123456 yuanToCent('1234.567') // 123457(四舍五入)
高精度计算工具
JavaScript 的浮点数计算存在精度问题(如 0.1 + 0.2 !== 0.3),金融项目必须解决这个问题。
typescript/** * 高精度加法 */ export function add(...numbers: number[]): number { return numbers.reduce((acc, num) => { const accStr = acc.toString() const numStr = num.toString() // 获取小数位数 const accDecimal = accStr.includes('.') ? accStr.split('.')[1].length : 0 const numDecimal = numStr.includes('.') ? numStr.split('.')[1].length : 0 const maxDecimal = Math.max(accDecimal, numDecimal) // 转换为整数计算 const multiplier = Math.pow(10, maxDecimal) return (acc * multiplier + num * multiplier) / multiplier }, 0) } /** * 高精度减法 */ export function subtract(a: number, b: number): number { const aStr = a.toString() const bStr = b.toString() const aDecimal = aStr.includes('.') ? aStr.split('.')[1].length : 0 const bDecimal = bStr.includes('.') ? bStr.split('.')[1].length : 0 const maxDecimal = Math.max(aDecimal, bDecimal) const multiplier = Math.pow(10, maxDecimal) return (a * multiplier - b * multiplier) / multiplier } /** * 高精度乘法 */ export function multiply(a: number, b: number): number { const aStr = a.toString() const bStr = b.toString() const aDecimal = aStr.includes('.') ? aStr.split('.')[1].length : 0 const bDecimal = bStr.includes('.') ? bStr.split('.')[1].length : 0 const totalDecimal = aDecimal + bDecimal const aInt = parseInt(aStr.replace('.', '')) const bInt = parseInt(bStr.replace('.', '')) return aInt * bInt / Math.pow(10, totalDecimal) } /** * 高精度除法 */ export function divide(a: number, b: number, precision: number = 2): number { if (b === 0) throw new Error('除数不能为零') const aStr = a.toString() const bStr = b.toString() const aDecimal = aStr.includes('.') ? aStr.split('.')[1].length : 0 const bDecimal = bStr.includes('.') ? bStr.split('.')[1].length : 0 const aInt = parseInt(aStr.replace('.', '')) const bInt = parseInt(bStr.replace('.', '')) const result = (aInt / bInt) * Math.pow(10, bDecimal - aDecimal) return Number(result.toFixed(precision)) } // 使用示例 add(0.1, 0.2) // 0.3 subtract(0.3, 0.1) // 0.2 multiply(0.1, 0.2) // 0.02 divide(0.3, 0.1) // 3
利率和收益计算
typescript/** * 计算单利收益 * @param principal 本金 * @param rate 年化利率(如 0.05 表示 5%) * @param days 天数 */ export function calculateSimpleInterest( principal: number, rate: number, days: number ): number { return multiply(multiply(principal, rate), days / 365) } /** * 计算复利收益 * @param principal 本金 * @param rate 年化利率 * @param periods 计息期数 */ export function calculateCompoundInterest( principal: number, rate: number, periods: number ): number { return multiply(principal, Math.pow(1 + rate, periods)) - principal } /** * 计算年化收益率 * @param profit 收益 * @param principal 本金 * @param days 持有天数 */ export function calculateAnnualizedReturn( profit: number, principal: number, days: number ): number { if (principal === 0 || days === 0) return 0 return divide(divide(profit, principal) * 365, days, 4) } // 使用示例 calculateSimpleInterest(10000, 0.05, 30) // 41.10 元 calculateCompoundInterest(10000, 0.05, 12) // 795.86 元 calculateAnnualizedReturn(500, 10000, 180) // 0.1014 (10.14%)
数据验证工具
typescript/** * 验证银行卡号(Luhn 算法) */ export function validateBankCard(cardNumber: string): boolean { const num = cardNumber.replace(/s/g, '') if (!/^d{16,19}$/.test(num)) return false let sum = 0 let isEven = false for (let i = num.length - 1; i >= 0; i--) { let digit = parseInt(num[i]) if (isEven) { digit *= 2 if (digit > 9) digit -= 9 } sum += digit isEven = !isEven } return sum % 10 === 0 } /** * 验证身份证号 */ export function validateIDCard(idCard: string): boolean { if (!/^d{17}[dXx]$/.test(idCard)) return false const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] let sum = 0 for (let i = 0; i < 17; i++) { sum += parseInt(idCard[i]) * weights[i] } const checkCode = checkCodes[sum % 11] return idCard[17].toUpperCase() === checkCode } /** * 手机号验证(中国大陆) */ export function validatePhone(phone: string): boolean { return /^1[3-9]d{9}$/.test(phone) } /** * 金额验证(支持最多两位小数) */ export function validateAmount(amount: string): boolean { return /^(0|[1-9]d*)(.d{1,2})?$/.test(amount) } // 使用示例 validateBankCard('6222021234567890123') // true/false validateIDCard('110101199003074512') // true/false validatePhone('13812345678') // true validateAmount('1234.56') // true validateAmount('1234.567') // false
时间处理工具
typescript/** * 计算两个日期之间的天数 */ export function daysBetween(start: Date, end: Date): number { const oneDay = 24 * 60 * 60 * 1000 return Math.round(Math.abs((end.getTime() - start.getTime()) / oneDay)) } /** * 判断是否为交易日(排除周末,不考虑节假日) */ export function isTradingDay(date: Date): boolean { const day = date.getDay() return day !== 0 && day !== 6 } /** * 获取下一个交易日 */ export function getNextTradingDay(date: Date): Date { const nextDay = new Date(date) nextDay.setDate(nextDay.getDate() + 1) while (!isTradingDay(nextDay)) { nextDay.setDate(nextDay.getDate() + 1) } return nextDay } /** * 格式化日期为 YYYY-MM-DD */ export function formatDate(date: Date): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } /** * 计算某月有多少天 */ export function getDaysInMonth(year: number, month: number): number { return new Date(year, month, 0).getDate() } // 使用示例 daysBetween(new Date('2025-01-01'), new Date('2025-12-31')) // 364 isTradingDay(new Date('2025-12-06')) // false (周六) formatDate(new Date()) // "2025-12-04"
数据脱敏工具
typescript/** * 手机号脱敏 */ export function maskPhone(phone: string): string { if (phone.length !== 11) return phone return phone.replace(/(d{3})d{4}(d{4})/, '$1****$2') } /** * 银行卡号脱敏 */ export function maskBankCard(cardNumber: string): string { const num = cardNumber.replace(/s/g, '') if (num.length < 8) return cardNumber const start = num.slice(0, 4) const end = num.slice(-4) const middle = '*'.repeat(num.length - 8) return `${start} ${middle} ${end}` } /** * 身份证号脱敏 */ export function maskIDCard(idCard: string): string { if (idCard.length !== 18) return idCard return idCard.replace(/(d{6})d{8}(d{4})/, '$1********$2') } /** * 姓名脱敏 */ export function maskName(name: string): string { if (name.length <= 1) return name if (name.length === 2) return name[0] + '*' return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1] } // 使用示例 maskPhone('13812345678') // 138****5678 maskBankCard('6222021234567890123') // 6222 *********** 0123 maskIDCard('110101199003074512') // 110101********4512 maskName('张三') // 张* maskName('欧阳修') // 欧*修
图表数据处理
typescript/** * 生成K线图数据 */ interface KLineData { date: string open: number close: number high: number low: number volume: number } export function generateKLineData( startDate: Date, days: number, basePrice: number = 100 ): KLineData[] { const data: KLineData[] = [] let currentPrice = basePrice for (let i = 0; i < days; i++) { const date = new Date(startDate) date.setDate(date.getDate() + i) const volatility = 0.02 // 2% 波动率 const change = (Math.random() - 0.5) * volatility * currentPrice const open = currentPrice const close = currentPrice + change const high = Math.max(open, close) * (1 + Math.random() * 0.01) const low = Math.min(open, close) * (1 - Math.random() * 0.01) const volume = Math.floor(Math.random() * 1000000) data.push({ date: formatDate(date), open: Number(open.toFixed(2)), close: Number(close.toFixed(2)), high: Number(high.toFixed(2)), low: Number(low.toFixed(2)), volume }) currentPrice = close } return data } /** * 计算移动平均线(MA) */ export function calculateMA(data: number[], period: number): number[] { const result: number[] = [] for (let i = 0; i < data.length; i++) { if (i < period - 1) { result.push(NaN) continue } const sum = data.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) result.push(Number((sum / period).toFixed(2))) } return result } /** * 数据归一化(用于图表展示) */ export function normalizeData( data: number[], min: number = 0, max: number = 100 ): number[] { const dataMin = Math.min(...data) const dataMax = Math.max(...data) const range = dataMax - dataMin if (range === 0) return data.map(() => (min + max) / 2) return data.map(value => { return Number((((value - dataMin) / range) * (max - min) + min).toFixed(2)) }) } // 使用示例 const klineData = generateKLineData(new Date('2025-01-01'), 30, 100) const prices = klineData.map(d => d.close) const ma5 = calculateMA(prices, 5) const normalizedPrices = normalizeData(prices, 0, 100)
风险控制工具
typescript/** * 计算波动率(标准差) */ export function calculateVolatility(returns: number[]): number { const mean = returns.reduce((a, b) => a + b, 0) / returns.length const squaredDiffs = returns.map(r => Math.pow(r - mean, 2)) const variance = squaredDiffs.reduce((a, b) => a + b, 0) / returns.length return Math.sqrt(variance) } /** * 计算最大回撤 */ export function calculateMaxDrawdown(prices: number[]): number { let maxPrice = prices[0] let maxDrawdown = 0 for (const price of prices) { if (price > maxPrice) { maxPrice = price } const drawdown = (maxPrice - price) / maxPrice if (drawdown > maxDrawdown) { maxDrawdown = drawdown } } return Number((maxDrawdown * 100).toFixed(2)) // 返回百分比 } /** * 计算夏普比率 * @param returns 收益率数组 * @param riskFreeRate 无风险利率 */ export function calculateSharpeRatio( returns: number[], riskFreeRate: number = 0.03 ): number { const avgReturn = returns.reduce((a, b) => a + b, 0) / returns.length const volatility = calculateVolatility(returns) if (volatility === 0) return 0 return Number(((avgReturn - riskFreeRate) / volatility).toFixed(2)) } // 使用示例 const returns = [0.05, -0.02, 0.03, 0.01, -0.01, 0.04] calculateVolatility(returns) // 0.025 calculateMaxDrawdown([100, 110, 95, 105, 90, 100]) // 18.18% calculateSharpeRatio(returns, 0.03) // 0.45
加密与安全工具
typescript/** * 简单加密(用于前端临时数据,非安全场景) */ export function simpleEncrypt(text: string, key: string): string { let result = '' for (let i = 0; i < text.length; i++) { const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length) result += String.fromCharCode(charCode) } return btoa(result) } /** * 简单解密 */ export function simpleDecrypt(encrypted: string, key: string): string { const decoded = atob(encrypted) let result = '' for (let i = 0; i < decoded.length; i++) { const charCode = decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length) result += String.fromCharCode(charCode) } return result } /** * 生成随机交易流水号 */ export function generateTransactionId(prefix: string = 'TXN'): string { const timestamp = Date.now() const random = Math.random().toString(36).substr(2, 9).toUpperCase() return `${prefix}${timestamp}${random}` } /** * 安全比较(防止时序攻击) */ export function secureCompare(a: string, b: string): boolean { if (a.length !== b.length) return false let result = 0 for (let i = 0; i < a.length; i++) { result |= a.charCodeAt(i) ^ b.charCodeAt(i) } return result === 0 } // 使用示例 const encrypted = simpleEncrypt('敏感数据', 'myKey123') const decrypted = simpleDecrypt(encrypted, 'myKey123') generateTransactionId('PAY') // PAY1733270400000ABC123XYZ
工具函数使用最佳实践
1. 精度优先原则
typescript// ❌ 错误:直接使用 JavaScript 浮点数运算 const total = 0.1 + 0.2 // 0.30000000000000004 // ✅ 正确:使用高精度计算函数 const total = add(0.1, 0.2) // 0.3
2. 服务端验证原则
typescript// ❌ 错误:仅在前端验证 function submitPayment(amount: string) { if (validateAmount(amount)) { api.pay({ amount }) } } // ✅ 正确:前后端双重验证 function submitPayment(amount: string) { // 前端验证(用户体验) if (!validateAmount(amount)) { toast.error('金额格式不正确') return } // 服务端会再次验证(安全性) api.pay({ amount }).catch(err => { if (err.code === 'INVALID_AMOUNT') { toast.error('金额验证失败') } }) }
3. 单位统一原则
typescript// ✅ 后端以分为单位,前端以元为单位 interface Transaction { amountInCent: number // 服务端数据(分) } function displayTransaction(tx: Transaction) { const amount = centToYuan(tx.amountInCent) return formatMoney(amount) }
总结
"金融项目的工具函数不仅要考虑功能实现,更要关注精度、安全和可靠性。每一个计算误差都可能造成真实的经济损失,每一个安全漏洞都可能导致严重的后果。
本文介绍的工具函数涵盖了金融项目开发的核心场景,从金额处理到数据验证,从图表展示到风险控制。在实际项目中,建议将这些函数组织成统一的工具库,配合完善的单元测试,确保每个函数都经过充分验证。记住:在金融领域,"差不多"就是"不行"。



