折扣卡CPS渠道佣金比例配置功能设计
在折扣卡CPS系统运营过程中,不同联盟渠道、不同卡密批次、不同代理商需要差异化的佣金分配方案。如果佣金比例硬编码在代码中,每次调整分佣规则都需要改代码、重新发版,运营效率低且存在资金风险。本文围绕渠道佣金比例配置功能,从业务痛点、数据模型、匹配规则、权限控制、前端交互、引擎匹配逻辑、校验与风控几个方面完成方案设计,附带后端核心Java代码。
痛点分析
第一,佣金比例写死在业务代码,渠道返点调整、代理商政策变更都要走版本发布,周期长。
第二,缺少多维度优先级规则,渠道全局比例、卡批次专属比例、代理商特殊政策混在一起,规则冲突无法预判。
第三,配置缺少强校验,容易出现各级佣金总和超过渠道基础返佣,造成平台亏损。
第四,配置修改即时生效,没有灰度、变更记录,佣金异常后无法追溯是谁、什么时候修改的配置。
第五,分佣引擎匹配逻辑简单,只按渠道匹配,无法结合租户、权益卡等级做精细化分佣。
第六,禁用、过期配置没有隔离,已经下线的规则依然被引擎命中,产生错误佣金。
第七,缺少预览功能,运营人员配置完成后,无法提前测算佣金,只能等真实订单下单后验证。
整体设计思路
设计可可视化维护的分佣模板,模板支持多维度条件匹配,引擎在CPS订单到达时,按优先级顺序匹配唯一模板。支持按渠道、租户、卡密批次三个维度设置匹配条件,分配项包含平台抽成、代理商佣金、推广员佣金、用户返利。
配置变更采用“保存+版本记录”机制,每次修改都生成变更日志;配置增加生效时间、失效时间、启用状态,支持定时切换政策。新增配置预览接口,输入渠道基础佣金,实时计算各级分佣金额,提前校验是否超上限。
匹配优先级从高到低:卡密批次专属模板 > 渠道+租户模板 > 渠道全局模板,高优先级模板命中后,不再向下匹配。模板不支持叠加生效,一个订单只能命中一套分佣规则。
数据模型设计
核心表:cps_commission_template(分佣模板主表)、cps_commission_template_log(配置变更日志表)。
cps_commission_template关键字段:
id:主键ID
tenant_id:租户ID,私有化多商户隔离
channel_code:渠道编码,关联CPS渠道适配器
batch_id:卡密批次ID,为空代表不限制卡批次
template_name:模板名称,运营识别名称
agent_rate:代理商佣金比例
promoter_rate:推广员佣金比例
user_rebate_rate:用户返利比例
platform_rate:平台抽成比例
max_commission:该模板单订单佣金上限
start_time:生效时间
end_time:失效时间
enable:是否启用
remark:备注
cps_commission_template_log关键字段:
id:主键
template_id:模板ID
before_json:修改前完整配置JSON
after_json:修改后完整配置JSON
operator_id:操作人ID
operator_name:操作人名称
create_time:变更时间
配置校验规则
模板保存时后端强制校验,不通过直接拒绝保存:
- 所有比例取值范围0 ~ 1;
- 平台、代理商、推广员、用户返利比例之和不能大于1;
- 生效时间不能晚于失效时间;
- 同一租户、同一渠道、同一批次,不能存在时间重叠且同时启用的模板,避免规则冲突;
- 单订单佣金上限大于等于0。
分佣模板匹配逻辑
CPS订单进入分佣引擎后,携带上下文信息:租户ID、渠道编码、卡密batchId。引擎按照优先级依次查询可用模板,可用条件:enable=1、当前时间在start_time和end_time之间。
匹配顺序:
- 优先查询【租户+渠道+batchId】的专属模板;
- 没有命中,则查询【租户+渠道】模板;
- 仍然没有命中,查询【渠道全局】模板(batchId为null);
- 全部没有命中,该订单不生成分佣。
核心代码实现
模板实体类
@Data @TableName("cps_commission_template") public class CpsCommissionTemplate { private Long id; private String tenantId; private String channelCode; private String batchId; private String templateName; private BigDecimal agentRate; private BigDecimal promoterRate; private BigDecimal userRebateRate; private BigDecimal platformRate; private BigDecimal maxCommission; private LocalDateTime startTime; private LocalDateTime endTime; private Boolean enable; private String remark; }模板匹配服务
@Service public class CommissionTemplateServiceImpl implements CommissionTemplateService { @Autowired private CpsCommissionTemplateMapper templateMapper; @Autowired private CpsCommissionTemplateLogMapper logMapper; @Override public CpsCommissionTemplate matchTemplate(String tenantId, String channelCode, String batchId) { LocalDateTime now = LocalDateTime.now(); // 1.最高优先级:批次专属模板 CpsCommissionTemplate batchTemplate = templateMapper.selectOne( Wrappers.lambdaQuery(CpsCommissionTemplate.class) .eq(CpsCommissionTemplate::getTenantId, tenantId) .eq(CpsCommissionTemplate::getChannelCode, channelCode) .eq(CpsCommissionTemplate::getBatchId, batchId) .eq(CpsCommissionTemplate::getEnable, true) .le(CpsCommissionTemplate::getStartTime, now) .ge(CpsCommissionTemplate::getEndTime, now) ); if (batchTemplate != null) { return batchTemplate; } // 2.次优先级:租户+渠道模板 CpsCommissionTemplate tenantChannelTemplate = templateMapper.selectOne( Wrappers.lambdaQuery(CpsCommissionTemplate.class) .eq(CpsCommissionTemplate::getTenantId, tenantId) .eq(CpsCommissionTemplate::getChannelCode, channelCode) .isNull(CpsCommissionTemplate::getBatchId) .eq(CpsCommissionTemplate::getEnable, true) .le(CpsCommissionTemplate::getStartTime, now) .ge(CpsCommissionTemplate::getEndTime, now) ); if (tenantChannelTemplate != null) { return tenantChannelTemplate; } // 3.最低优先级:渠道全局模板 return templateMapper.selectOne( Wrappers.lambdaQuery(CpsCommissionTemplate.class) .eq(CpsCommissionTemplate::getTenantId, tenantId) .eq(CpsCommissionTemplate::getChannelCode, channelCode) .isNull(CpsCommissionTemplate::getBatchId) .eq(CpsCommissionTemplate::getEnable, true) .le(CpsCommissionTemplate::getStartTime, now) .ge(CpsCommissionTemplate::getEndTime, now) ); } /** * 保存模板并记录变更日志 */ @Transactional(rollbackFor = Exception.class) @Override public void saveTemplate(CpsCommissionTemplate template, String operatorId, String operatorName) { // 比例总和校验 BigDecimal total = template.getPlatformRate() .add(template.getAgentRate()) .add(template.getPromoterRate()) .add(template.getUserRebateRate()); if (total.compareTo(new BigDecimal("1")) > 0) { throw new BusinessException("佣金比例总和不能超过1"); } CpsCommissionTemplate old = null; if (template.getId() != null) { old = templateMapper.selectById(template.getId()); } templateMapper.insertOrUpdate(template); // 写入变更日志 CpsCommissionTemplateLog log = new CpsCommissionTemplateLog(); log.setTemplateId(template.getId()); log.setBeforeJson(old == null ? null : JSON.toJSONString(old)); log.setAfterJson(JSON.toJSONString(template)); log.setOperatorId(operatorId); log.setOperatorName(operatorName); logMapper.insert(log); } /** * 佣金预览测算接口 */ @Override public Map<String,BigDecimal> previewCalc(CpsCommissionTemplate template, BigDecimal baseAmount) { Map<String,BigDecimal> result = new HashMap<>(); BigDecimal agent = baseAmount.multiply(template.getAgentRate()); BigDecimal promoter = baseAmount.multiply(template.getPromoterRate()); BigDecimal user = baseAmount.multiply(template.getUserRebateRate()); BigDecimal platform = baseAmount.multiply(template.getPlatformRate()); // 佣金上限控制 BigDecimal totalCommission = agent.add(promoter).add(user).add(platform); if (template.getMaxCommission() != null && totalCommission.compareTo(template.getMaxCommission()) > 0) { BigDecimal rateScale = template.getMaxCommission().divide(totalCommission, 6, BigDecimal.ROUND_HALF_UP); agent = agent.multiply(rateScale); promoter = promoter.multiply(rateScale); user = user.multiply(rateScale); platform = platform.multiply(rateScale); } result.put("agentCommission", agent); result.put("promoterCommission", promoter); result.put("userRebate", user); result.put("platformProfit", platform); return result; } }缓存设计
模板数据使用Redis缓存,key:cps:template:match:{tenantId}:{channelCode}:{batchId}。
缓存有效期5分钟,模板新增、修改、禁用时主动清理对应缓存,避免引擎读到脏配置,减少频繁查询数据库压力。
前端交互与权限控制
管理后台页面功能:模板列表、新增模板、编辑、启用/禁用、变更日志查看、佣金预览测算。
权限控制粒度:超级管理员可以维护全部模板;租户管理员只能维护自己租户下的模板;普通运营角色只允许查看,不允许编辑、新增。
预览功能:运营录入渠道基础返佣金额,点击测算,页面直接展示平台、代理、推广员、用户各自佣金,用于政策评估。
风控与兜底规则
- 模板修改不会回溯历史订单,只对修改后新进来的CPS订单生效,历史分佣保持不变;
- 渠道联盟返佣下调后,平台需要同步调整模板,避免亏损;
- 支持设置单订单佣金上限,防止大额订单佣金超标;
- 模板变更日志永久留存,作为资金对账、纠纷核查凭证;
- 后台增加配置变更告警,高佣金模板修改后推送消息通知负责人复核。
总结
折扣卡CPS渠道佣金比例配置功能设计核心是多维度条件匹配、优先级控制、保存强校验、变更留痕、预览测算。
把佣金规则从代码抽离为可视化可配置模板,运营人员可以独立调整渠道、卡批次佣金政策,不需要开发发版;同时通过比例校验、时间区间、变更日志、缓存机制控制资金风险。该模块和分佣引擎、CPS订单链路追踪、卡密批次模块联动,支撑精细化运营,适配私有化部署、多租户、多代理商的业务场景。