USER_CASHBACK基础分配技术服务费扣除后的可分配佣金,按商品类型和 CPS 阶梯分给买家。
把商品返现、直接邀请佣金和营销补贴收口到唯一计算引擎,统一游客预览、登录预览与订单结算,避免公式继续散落在商品、券、活动和订单模块。
userId、新人前三单加成等过渡口径。本文描述目标方案,不代表相关服务端代码和接口已经完成改造。USER_CASHBACK基础分配技术服务费扣除后的可分配佣金,按商品类型和 CPS 阶梯分给买家。
DIRECT_COMMISSION基础分配以本单 USER_CASHBACK 为基数,只支付给订单用户的有效直接邀请人。
COUPON_CASHBACK营销预算从可用券中选择本单收益最高的一张,买家专享。
REBATE_FUND营销预算以 USER_CASHBACK 为基数,从用户的返佣基金额度中核销。
ACTIVITY_CASHBACK营销预算由订单完成事件触发的佣金加成,可命中多条并叠加。
USER_CASHBACK + DIRECT_COMMISSION 受平台净佣金约束;优惠券、返佣基金和活动返现来自独立营销预算,因此买家最终到账可以超过本单佣金计算基数。| 组件 | 唯一职责 | 禁止事项 |
|---|---|---|
CommissionFacade | 统一承接游客预览、登录预览和正式结算。 | 不得自行计算任何佣金金额。 |
CommissionContextResolver | 按 calculationAt 读取商品类型、配置版本、直接关系和营销资格。 | 不得写钱包、消耗预算或复制公式。 |
CommissionCalculationEngine | 执行纯计算并返回基础分配和待核销营销明细。 | 不得访问数据库、钱包、网络或系统当前时间。 |
CommissionPostingService | 锁定订单和资源,执行预算核销、佣金记录、钱包 Pending 入账与幂等控制。 | 不得重新计算或修改引擎给出的金额。 |
CommissionFacade.preview,正式结算只调用 CommissionFacade.settle。order.userId,即可按登录用户正常结算。order.settledAt 判断,不沿用点击或预览结果。NORMAL_AFFILIATE,补全后通过同一幂等入口重试。引擎只返回金额和来源,不判断数据库中的实时剩余预算。正式结算时,CommissionPostingService 对引擎输出的营销明细执行原子核销;核销失败只移除对应营销行,不改变其他金额,也不在业务模块重新计算。
| 符号 | 含义 |
|---|---|
订单金额 | 订单佣金计价金额,minor 单位。 |
平台佣金 | 平台确认给 BeeX 的佣金,minor 单位。 |
佣金基数 | 经过佣金基数封顶后的计算基数。 |
技术服务费 | BeeX 技术服务费。 |
净佣金 | 扣除技术服务费后的可分配净佣金。 |
买家基础返现 | 买家基础返现 USER_CASHBACK。 |
直接邀请佣金 | 直接邀请人佣金 DIRECT_COMMISSION。 |
买家基础返现 + 直接邀请佣金 ≤ 净佣金。没有有效直接邀请人时 直接邀请佣金 = 0,未分配金额留在 BeeX。buyerShareBps × (10000 + directShareBps) ≤ 10000 × 10000,不满足时阻止配置发布。订单金额 > 0、平台佣金 ≥ 0,所有 bps 必须在允许区间;平台佣金 = 0 或无返佣商品直接返回零收益。perOrderCap。营销金额只受自身公式、资格、余额和总预算约束。以下均为 minor 单位:订单金额 = 1,000,000、平台佣金 = 180,000、佣金基数上限 40%、技术服务费 10%、买家分成 70%、直接邀请人按买家返现的 20%。
| 步骤 | 计算 | 结果 |
|---|---|---|
| 实际 CPS | 180,000 × 10000 ÷ 1,000,000 | 1,800 bps |
| 佣金基数 | min(180,000, 1,000,000 × 40%) | 180,000 |
| 技术服务费 | 180,000 × 10% | 18,000 |
| 净佣金 | 180,000 − 18,000 | 162,000 |
| 买家基础返现 | 162,000 × 70% | 113,400 |
| 直接邀请佣金 | 113,400 × 20% | 22,680 |
| 最优优惠券 | 固定 15,000、比例 10%=11,340、1.2 倍加成=22,680 | 22,680 |
| REBATE_FUND | 113,400 × 25% | 28,350 |
| 两条活动加成 | 113,400 × 10% + 113,400 × 5% | 17,010 |
| 买家个性化总返现 | 113,400 + 22,680 + 28,350 + 17,010 | 181,440 |
基础分配 买家基础返现 + 直接邀请佣金 = 136,080 ≤ 净佣金;买家总返现超过 佣金基数 的 1,440 来自独立营销预算,符合资金边界。
默认分成规则按 countryCode + productType + cpsTier 匹配。CPS 阶梯按 minCpsBps/maxCpsBps 匹配,每档各自携带并必填 technicalServiceFeeBps、buyerShareBps 和 directShareBps(无全局回落,缺字段的档会被拒绝发布),本身不直接产生金额。
| 商品类型 | 是否计算佣金 | 规则 |
|---|---|---|
NORMAL_AFFILIATE | 是 | 仅使用该商品类型的默认 CPS 阶梯。 |
HIGH_COMMISSION | 是 | 默认 CPS 阶梯;运营配置商品时可选商品级分成覆盖。 |
BEEX_ACTIVITY | 是 | 默认 CPS 阶梯;运营配置活动商品时可选商品级分成覆盖。 |
SHORT_VIDEO_NO_COMMISSION | 否 | 直接返回零收益。 |
LIVE_NO_COMMISSION | 否 | 直接返回零收益。 |
NORMAL_NO_COMMISSION | 否 | 直接返回零收益。 |
| 序号 | actualCpsBps 区间 | 运营展示 |
|---|---|---|
| 1 | [0, 200) | 0%–2% |
| 2 | [200, 500) | 2%–5% |
| 3 | [500, 1000) | 5%–10% |
| 4 | [1000, 1500) | 10%–15% |
| 5 | [1500, 2000) | 15%–20% |
| 6 | [2000, +∞) | 20% 及以上 |
ProductCommissionSplitOverride。它不是重新配置平台 CPS,而是在活动商品或高返佣商品配置时,选择性覆盖默认阶梯中的买家或直接邀请人分成。record ProductCommissionSplitOverride(
Integer buyerShareBps,
Integer directShareBps
) {}
BEEX_ACTIVITY > HIGH_COMMISSION > NORMAL_AFFILIATE。确定商品类型后,先命中默认阶梯,再应用有效的商品覆盖。buyerShareBps 时,directShareBps 沿用默认阶梯;反之亦然。HIGH_COMMISSION 或 BEEX_ACTIVITY,且商品覆盖记录在该时间有效时才应用。普通返佣商品不允许覆盖。| 券类型 | 公式 |
|---|---|
FIXED | coupon = fixedAmountMinor |
RATE | coupon = floor(买家基础返现 × rateBps ÷ 10000) |
MULTIPLIER | coupon = floor(买家基础返现 × (multiplierBps − 10000) ÷ 10000) |
完成时间筛出所有有效且适用本单的返现券,逐张通过统一引擎计算,选择金额最高的一张;金额相同时依次选择到期时间最早、券 ID 字典序最小的记录。预览不核销,正式结算在事务内锁定并核销。
返佣基金只支付给买家。正式结算时余额或总预算不足,本单整条 REBATE_FUND 跳过,不按剩余额度部分发放,也不影响其他佣金行。
| 维度 | 统一规则 |
|---|---|
| 触发事件 | 只接受 AFFILIATE_ORDER_DONE。 |
| 活动动作 | 只接受 GRANT_COMMISSION_BONUS。 |
| 受益人 | 固定为订单买家,不接受活动配置传入任意 beneficiary user ID。 |
| 计算基数 | 固定为 USER_CASHBACK;旧 COMMISSION 或 GMV 口径不再使用。 |
| 叠加 | 同一订单可命中多条规则,逐条计算并叠加,每条使用自己的确定性幂等键。 |
| 预算不足 | 只跳过预算不足的活动行,不部分发放,不回滚其他已满足规则。 |
NEW_USER_CASHBACK_MULT 种子逻辑并停用现有规则,历史已发记录不删除、不重算。GRANT_CASH 是独立现金奖励,不属于 ACTIVITY_CASHBACK,不得写入佣金记录或混入订单返现总额。| 模式 | 身份来源 | 返回范围 | 是否写数据 |
|---|---|---|---|
GUEST_PREVIEW | 无登录身份 | 仅 USER_CASHBACK;不查询直接关系、券、基金、活动或历史订单。 | 否 |
AUTHENTICATED_PREVIEW | 认证上下文 | 买家可得的 USER_CASHBACK + COUPON_CASHBACK + REBATE_FUND + ACTIVITY_CASHBACK。直接佣金可内部试算,但不展示给买家、不计入买家总额。 | 否 |
ORDER_SETTLEMENT | 固定使用 order.userId | 正式生成买家收益和有效直接邀请人的 DIRECT_COMMISSION。 | 单事务写佣金、钱包和预算 |
userId。无效、过期 Token 必须返回鉴权错误,不能静默降级成游客。order.userId,整单不产生任何佣金,不进入未认领池,也不允许事后补算直接佣金。| 收益类型 | 幂等键组成 |
|---|---|
USER_CASHBACK | orderId + type + buyerUserId |
DIRECT_COMMISSION | orderId + type + inviterUserId |
COUPON_CASHBACK | orderId + type + couponId |
REBATE_FUND | orderId + type + userCouponId |
ACTIVITY_CASHBACK | orderId + type + campaignRuleId |
活动预算必须维护可原子更新的 usedAmountMinor,通过“当前已用 + 本次金额不超过总预算”的条件更新抢占。禁止在结算时临时汇总历史记录后再判断,否则并发订单会超发。
退款不重新运行佣金计算引擎。Pending 冲正必须引用原佣金记录和原钱包账本;只有原规则定义为可恢复且仍能安全恢复的营销资源才退回。
CommissionCalculationResult preview(CommissionPreviewCommand command);
CommissionCalculationResult settle(String orderId);
RefundHandlingResult handleRefund(String orderId, String platformRefundId);
| 接口 / 模型 | 目标变化 |
|---|---|
POST /api/v1/order-benefits/preview | 保留统一预览入口;认证身份只来自 Token。游客只返回基础返现,登录用户返回买家个性化收益。 |
CommissionCalculationRequest | 包含 calculation mode、国家、商品类型、订单金额、平台佣金、规则版本结果、直接关系状态和已解析营销候选,不允许引擎内部查询。 |
CommissionCalculationResult | 返回计算基数、技术服务费、净佣金、实际 CPS、最终分成参数、基础收益行和待核销营销行。 |
ProductCommissionSplitOverride | buyerShareBps、directShareBps 均可为空,分别覆盖默认阶梯字段。 |
public final class CommissionCalculationEngine {
public CommissionCalculationResult calculate(ResolvedCommissionContext context) {
validate(context);
if (!context.productType().commissionable()
|| context.gmvMinor() == 0
|| context.platformCommissionMinor() == 0) {
return CommissionCalculationResult.zero(context);
}
long actualCpsBps = multiplyDivide(
context.platformCommissionMinor(), 10_000, context.gmvMinor());
CommissionSplit split = context.splitRule().resolve(
context.productType(), actualCpsBps, context.productOverride());
long commissionBaseMinor = Math.min(
context.platformCommissionMinor(),
multiplyBps(context.gmvMinor(), context.maxCommissionBaseBps()));
long technicalFeeMinor =
multiplyBps(commissionBaseMinor, split.technicalServiceFeeBps());
long netCommissionMinor = commissionBaseMinor - technicalFeeMinor;
long userCashbackMinor =
multiplyBps(netCommissionMinor, split.buyerShareBps());
long directCommissionMinor = context.directInviterEligible()
? multiplyBps(userCashbackMinor, split.directShareBps())
: 0;
if (userCashbackMinor + directCommissionMinor > netCommissionMinor) {
throw new InvalidCommissionConfigurationException(
"USER_CASHBACK + DIRECT_COMMISSION exceeds net commission");
}
CommissionLine userCashback = CommissionLine.userCashback(
context.orderId(), context.buyerUserId(), userCashbackMinor);
Optional<CommissionLine> directCommission =
directCommissionMinor == 0
? Optional.empty()
: Optional.of(CommissionLine.direct(
context.orderId(),
context.directInviterUserId(),
directCommissionMinor));
Optional<CommissionLine> coupon = context.coupons().stream()
.map(candidate -> calculateCoupon(candidate, userCashbackMinor))
.sorted(CommissionLine.bestCouponFirst())
.findFirst();
Optional<CommissionLine> rebateFund = context.rebateFund()
.map(candidate -> calculateRebateFund(candidate, userCashbackMinor));
List<CommissionLine> activities = context.activities().stream()
.map(candidate -> calculateActivity(candidate, userCashbackMinor))
.filter(line -> line.amountMinor() > 0)
.toList();
return CommissionCalculationResult.of(
actualCpsBps,
commissionBaseMinor,
technicalFeeMinor,
netCommissionMinor,
split,
userCashback,
directCommission,
coupon,
rebateFund,
activities);
}
private long multiplyBps(long amountMinor, int bps) {
return Math.multiplyExact(amountMinor, bps) / 10_000;
}
private long multiplyDivide(long value, long multiplier, long divisor) {
return Math.multiplyExact(value, multiplier) / divisor;
}
}
生产实现应继续使用项目统一的金额溢出处理;Java 17 获取列表首项使用 get(0),不能使用更高版本才提供的 getFirst()。
| 数据对象 | 新增或收口字段 | 用途 |
|---|---|---|
| 联盟点击 | platformProductId、点击时观察到的 productType | 提高订单补全成功率和链路排障能力;点击类型不作为最终结算规则。 |
| 联盟订单 | platformProductId、完成时解析的 productType、settledAt、commissionPostedAt | 支持完成时间结算、阻断补全和整单幂等。 |
| 商品运营配置 | 可空的 buyerShareBps、directShareBps 及有效期 | 承载活动商品或高返佣商品的分成覆盖。 |
| 佣金记录 | 确定性幂等键、来源类型、来源 ID、状态、金额和 releaseAt | 金额只插入一次,支持钱包关联和退款冲正。 |
| 营销预算 | budgetAmountMinor、usedAmountMinor | 条件更新,防止并发超发。 |
| 退款异常任务 | 订单、原佣金、退款原因、处理状态和审计信息 | 承接 Available/Paid 后退款的人工处理。 |
| 当前问题 | 目标处理 |
|---|---|
| 佣金公式分散在商品、订单、券、基金和活动模块。 | 全部改为调用 CommissionFacade,删除业务子模块金额计算。 |
| 部分口径按点击或归因时间取配置和邀请关系。 | 统一改为 calculationAt = order.settledAt。 |
| 直接佣金可能按净佣金而非买家返现计算。 | 统一使用 USER_CASHBACK 作为唯一基数。 |
| 新人前三单活动仍存在种子和当前文档描述。 | 删除种子调用,停用现有活动规则,保留历史奖励记录。 |
GRANT_CASH 可能被归入活动佣金。 | 与 ACTIVITY_CASHBACK 完全分离。 |
| 订单缺少平台商品 ID 或商品类型时可能降级计算。 | 阻断结算,补全成功后通过幂等入口重试。 |
| 佣金记录 upsert 可改金额,但钱包账本不会同步改写。 | 改为 insert-once,首次成功后金额冻结。 |
| 活动预算按历史记录汇总,存在并发超发窗口。 | 使用 usedAmountMinor 条件更新原子核销。 |
预览接口允许请求体传 userId。 | 登录身份固定来自认证上下文,无效 Token 不降级游客。 |
| 场景 | 期望结果 |
|---|---|
| 游客预览返佣商品 | 只返回 USER_CASHBACK,不查询或暴露任何用户权益。 |
| 携带无效 Token 预览 | 返回鉴权错误,不降级为游客。 |
| 登录用户有直接邀请人 | 买家总额不包含 DIRECT_COMMISSION;正式结算向邀请人单独入账。 |
| 商品只覆盖 buyerShareBps | 买家比例使用商品覆盖,直接比例使用商品类型默认阶梯。 |
| 商品只覆盖 directShareBps | 直接比例使用商品覆盖,买家比例使用商品类型默认阶梯。 |
| 普通返佣商品带商品覆盖 | 拒绝配置或忽略非法覆盖,始终使用默认阶梯。 |
| 没有有效直接邀请人 | DIRECT_COMMISSION=0,不把该份额追加给买家。 |
| 多张券金额相同 | 选择最早到期;仍相同则选择 ID 最小的一张。 |
| 返佣基金余额不足 | 整条 REBATE_FUND 跳过,不部分发放,基础返现照常入账。 |
| 命中三条活动,其中一条预算不足 | 发放另外两条;不足的一条不产生佣金记录。 |
| 活动动作为 GRANT_CASH | 不生成 ACTIVITY_CASHBACK。 |
| 订单完成时仍未绑定用户 | 整单不产生佣金,不支持后补领。 |
| 订单缺 platformProductId 或 productType | 阻断结算并可重试,不按普通商品兜底。 |
| 相同完成事件并发到达 | 只生成一组佣金记录和一组钱包账本,后续请求返回已有结果。 |
| Pending 阶段退款 | 自动冲正并恢复可恢复资源,佣金标记 INVALID。 |
| Available/Paid 后退款 | 不产生负钱包,创建运营异常任务。 |
| 历史已结算订单 | 切换后金额与状态保持不变。 |