JavaScript实现无Mutation的多人AA账单收付款匹配方法
多人账单分摊转账匹配实现
核心逻辑说明
- 无数据突变要求:所有计算过程不修改原始用户数组、初始拆分的应收方/应付款方数组,每一步状态更新都生成新的对象/数组
- 匹配规则:采用双端递归匹配,每次从待付款列表、待收款列表各取首位用户,计算两者的最小待结算金额生成转账记录,再将抵消后仍有余额的用户放回对应列表,递归处理直到两边列表清空
- 实付金额刚好等于人均份额的用户直接跳过,不生成转账记录
完整实现代码
const users = [ {id:1, username:"John One", amount:50}, {id:2, username:"John Two", amount:75}, {id:3, username:"John Three", amount:100}, {id:4, username:"John Four", amount:125}, {id:5, username:"John Five", amount:150}, ] const total = users.reduce((acc, user) => acc + user.amount, 0); const calculate = () => { const perPersonShare = total / users.length; // 拆分应收/应付方,不修改原始user数据 const separateUsers = () => { const initialLenders = users.filter(user => user.amount > perPersonShare); const initialOwers = users.filter(user => user.amount < perPersonShare); const lenders = initialLenders.map(lender => ({ ...lender, receiveAmount: lender.amount - perPersonShare })) const owers = initialOwers.map(ower => ({ ...ower, payAmount: perPersonShare - ower.amount })) return {owers, lenders} } const {lenders: initLenders, owers: initOwers} = separateUsers(); // 递归匹配转账关系,全程生成新数组/新对象,无突变 const matchTransfer = (remainingOwers, remainingLenders, records = []) => { // 递归终止条件:两边都没有待结算用户 if (remainingOwers.length === 0 && remainingLenders.length === 0) { return records; } const currentOwer = remainingOwers[0]; const currentLender = remainingLenders[0]; const settleAmount = Math.min(currentOwer.payAmount, currentLender.receiveAmount); // 生成当前转账记录 const newRecord = { username: currentOwer.username, hasToPay: settleAmount, to: currentLender.username } // 计算结算后双方剩余金额 const newOwerRemaining = currentOwer.payAmount - settleAmount; const newLenderRemaining = currentLender.receiveAmount - settleAmount; // 更新待付款列表:如果当前付款人还有未付清金额则保留,否则移除 const updatedOwers = newOwerRemaining > 0 ? [{...currentOwer, payAmount: newOwerRemaining}, ...remainingOwers.slice(1)] : remainingOwers.slice(1); // 更新待收款列表:如果当前收款人还有未收齐金额则保留,否则移除 const updatedLenders = newLenderRemaining > 0 ? [{...currentLender, receiveAmount: newLenderRemaining}, ...remainingLenders.slice(1)] : remainingLenders.slice(1); // 递归处理下一轮匹配 return matchTransfer(updatedOwers, updatedLenders, [...records, newRecord]); } return matchTransfer(initOwers, initLenders); } const result = calculate();
运行结果
执行代码后输出结果和预期完全一致:
[
{username:"John One", hasToPay:25, to:"John Four"},
{username:"John One", hasToPay:25, to:"John Five"},
{username:"John Two", hasToPay:25, to:"John Five"}
]
实现特性
- 所有用户对象通过展开运算符生成拷贝,没有直接修改原始对象属性
- 每一轮递归的待结算列表、转账记录数组都是新生成的数组,不存在对原数组的
push、splice等突变操作 - 逻辑可扩展,支持任意人数、任意金额差的分摊场景,只要总支付金额和总应付金额相等就能生成正确的转账记录
内容的提问来源于stack exchange,提问作者Johan
相关产品推荐
相关产品推荐

