实战指南

金融类项目常用工具函数库:精度、安全与性能的实践指南

深入探讨金融项目中的核心工具函数实现,涵盖金额处理、精度计算、加密安全等关键领域

2025年9月6日18 分钟939 阅读
金融工具函数精度计算安全
金融类项目常用工具函数库:精度、安全与性能的实践指南

金融类项目常用工具函数库

在金融项目开发中,精度、安全性和可靠性是首要考虑的因素。本文将系统地介绍金融项目中常用的工具函数,并深入分析其实现原理和最佳实践。

金额处理工具函数

金融项目中最常见的需求就是金额的格式化和计算。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)
}

总结

"

金融项目的工具函数不仅要考虑功能实现,更要关注精度、安全和可靠性。每一个计算误差都可能造成真实的经济损失,每一个安全漏洞都可能导致严重的后果。

本文介绍的工具函数涵盖了金融项目开发的核心场景,从金额处理到数据验证,从图表展示到风险控制。在实际项目中,建议将这些函数组织成统一的工具库,配合完善的单元测试,确保每个函数都经过充分验证。记住:在金融领域,"差不多"就是"不行"。

文章标签

# 金融# 工具函数# 精度计算# 安全
返回首页