PAYATHON 2026

如何动态组合 arrays 和 objects 并按 billingCycle 分组

数据小陈

结论

可以先用 map() 遍历 cyclesArray,再用 filter() 找出当前周期下的 plans,并通过 slice(0, 4) 将每个周期的 plan 数量限制在 4 条以内。

这属于数据结构转换,与 React 或 Redux Saga 无关。可以将转换逻辑写成纯函数,在提交表单或 Saga 调用 API 之前执行。

实现方式

billing cycle 是字符串,所以数组中的值需要加引号:

const cyclesArray = ["annual", "monthly", "weekly"];

const plansArray = [
  { value: 200, currency: "EUR", billingCycle: "annual" },
  { value: 220, currency: "USD", billingCycle: "monthly" },
  { value: 20, currency: "CHF", billingCycle: "monthly" },
];

下面的函数可以生成 API 所需的数据:

const buildProductPayload = (name, cycles, plans) => ({
  name,
  bCycles: cycles.map((cycle) => ({
    interval: cycle,
    plan: plans
      .filter((item) => item.billingCycle === cycle)
      .slice(0, 4)
      .map((item) => ({
        currency: {
          name: item.currency,
          amount: item.value,
        },
      })),
  })),
});

const objectToSend = buildProductPayload(
  "whatever",
  cyclesArray,
  plansArray
);

console.log(objectToSend);

生成结果如下:

{
  name: "whatever",
  bCycles: [
    {
      interval: "annual",
      plan: [
        {
          currency: {
            name: "EUR",
            amount: 200
          }
        }
      ]
    },
    {
      interval: "monthly",
      plan: [
        {
          currency: {
            name: "USD",
            amount: 220
          }
        },
        {
          currency: {
            name: "CHF",
            amount: 20
          }
        }
      ]
    },
    {
      interval: "weekly",
      plan: []
    }
  ]
}

这段转换分为四步:

  • cycles.map(...) 为每个 billing cycle 创建一个对象。
  • plans.filter(...) 筛选出 billingCycle 与当前周期相同的 plan。
  • slice(0, 4) 只保留前 4 个 plan。
  • 最后一层 map(...) 将原始 plan 转换为后端要求的结构。

不发送空周期

上面的写法会保留没有 plan 的周期,例如:

{
  interval: "weekly",
  plan: []
}

如果后端不接受空的 plan 数组,可以在生成后将其过滤掉:

const buildProductPayload = (name, cycles, plans) => ({
  name,
  bCycles: cycles
    .map((cycle) => ({
      interval: cycle,
      plan: plans
        .filter((item) => item.billingCycle === cycle)
        .slice(0, 4)
        .map((item) => ({
          currency: {
            name: item.currency,
            amount: item.value,
          },
        })),
    }))
    .filter((cycle) => cycle.plan.length > 0),
});

是否保留空周期应以后台接口约定为准,前端不应自行猜测。

数据较多时使用 reduce()

对于表单中的少量数据,map() 配合 filter() 比较容易阅读。不过,每处理一个 cycle,它都会重新遍历一次 plansArray。数据量较大时,可以先用 reduce() 分组:

const buildProductPayload = (name, cycles, plans) => {
  const groupedPlans = plans.reduce((groups, item) => {
    const cycle = item.billingCycle;

    if (!groups[cycle]) {
      groups[cycle] = [];
    }

    if (groups[cycle].length < 4) {
      groups[cycle].push({
        currency: {
          name: item.currency,
          amount: item.value,
        },
      });
    }

    return groups;
  }, {});

  return {
    name,
    bCycles: cycles.map((cycle) => ({
      interval: cycle,
      plan: groupedPlans[cycle] ?? [],
    })),
  };
};

这种写法只遍历一次 plansArray,并在分组时将每组限制为最多 4 条。

在 Redux Saga 中使用

Redux Saga 只负责组织异步请求,不负责数据分组。可以直接在 Saga 中调用同一个纯函数:

import { call, put } from "redux-saga/effects";

function* createProductSaga(action) {
  try {
    const { name, cyclesArray, plansArray } = action.payload;

    const payload = buildProductPayload(
      name,
      cyclesArray,
      plansArray
    );

    const response = yield call(api.createProduct, payload);

    yield put({
      type: "CREATE_PRODUCT_SUCCESS",
      payload: response,
    });
  } catch (error) {
    yield put({
      type: "CREATE_PRODUCT_FAILURE",
      error,
    });
  }
}

也可以在触发 action 之前完成转换。无论采用哪种方式,都不要直接修改 React state 中的原数组。上面的 map()、filter()、slice() 和 reduce() 都会返回新数据,不会破坏现有 state。

注意事项

前端使用 slice(0, 4),只能保证本次发送的数据不超过 4 条。后端仍然需要校验数量限制,否则调用方可以绕过前端,直接提交更多 plans。

提交前还可以检查以下内容:

  • billingCycle 是否存在于 cyclesArray。
  • value 是否为后端允许的数值。
  • currency 是否为有效币种。
  • 同一个周期和币种是否允许重复。
  • plans 超过 4 条时,是直接截断还是提示用户修改。

如果超过 4 条属于表单错误,就不适合用 slice(0, 4) 静默截断。可以先检查数量,并在超出限制时阻止提交:

const hasTooManyPlans = cyclesArray.some(
  (cycle) =>
    plansArray.filter(
      (item) => item.billingCycle === cycle
    ).length > 4
);

if (hasTooManyPlans) {
  throw new Error("每个 billing cycle 最多只能包含 4 个 plans");
}

这样可以防止用户填写的数据在不知情的情况下被截掉。

备注:内容仅供参考。