You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 21:15:41