Spring Boot实战:五级分销定价与自动分佣系统设计与实现

一、前言

私域团购系统的核心不是商城页面,而是背后的五级定价引擎自动分佣结算系统。很多开发者第一次接触这类项目时,容易把注意力放在前端展示上,结果佣金算错、层级超限、对账不平,后期修修补补非常痛苦。

本文从实战角度,基于 Spring Boot + MyBatis-Plus + Redis + RocketMQ,讲清楚五级定价、自动分佣、三级合规的技术实现。文中数据来自公开媒体报道,仅供参考,不构成收益承诺。


二、核心表结构设计

2.1 用户身份与关系表

sql

复制代码
CREATE TABLE `user_identity` (
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `level` tinyint NOT NULL DEFAULT 1 COMMENT '身份等级 1-5',
  `parent_id` bigint DEFAULT NULL COMMENT '直接上级',
  `grandparent_id` bigint DEFAULT NULL COMMENT '上上级',
  `path` varchar(255) DEFAULT NULL COMMENT '层级路径,如 1/10/25',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`user_id`),
  KEY `idx_parent` (`parent_id`),
  KEY `idx_grand` (`grandparent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户身份与层级关系';

设计要点:只存直接上级和上上级,第三级通过查询上上级的上级获得。这样既满足三级分销,又避免无限层级查询。

2.2 商品五级价格表

sql

复制代码
CREATE TABLE `product_price` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint NOT NULL,
  `base_price` decimal(10,2) NOT NULL COMMENT '基准零售价',
  `level_1_price` decimal(10,2) NOT NULL COMMENT '团购用户价',
  `level_2_price` decimal(10,2) NOT NULL COMMENT '分销商价',
  `level_3_price` decimal(10,2) NOT NULL COMMENT '小批发价',
  `level_4_price` decimal(10,2) NOT NULL COMMENT '中批发价',
  `level_5_price` decimal(10,2) NOT NULL COMMENT '大批发价',
  `status` tinyint NOT NULL DEFAULT 1,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品五级价格';

2.3 订单表

sql

复制代码
CREATE TABLE `order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_no` varchar(64) NOT NULL,
  `user_id` bigint NOT NULL COMMENT '下单用户',
  `distributor_id` bigint NOT NULL COMMENT '归属分销商',
  `product_id` bigint NOT NULL,
  `quantity` int NOT NULL,
  `unit_price` decimal(10,2) NOT NULL COMMENT '实际成交单价',
  `total_amount` decimal(10,2) NOT NULL,
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已签收 4已完成',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `signed_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_distributor` (`distributor_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

2.4 佣金流水表

sql

复制代码
CREATE TABLE `commission_flow` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_no` varchar(64) NOT NULL,
  `user_id` bigint NOT NULL,
  `commission_type` tinyint NOT NULL COMMENT '1零售差 2层级差 3团队收益 4奖励',
  `amount` decimal(10,2) NOT NULL,
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待结算 1已结算 2已失效',
  `settled_at` datetime DEFAULT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_user_type` (`order_no`,`user_id`,`commission_type`),
  KEY `idx_user` (`user_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='佣金流水';

唯一键 uk_order_user_type 是幂等的关键,防止同一订单同一用户同一佣金类型重复入账。


三、五级定价引擎实现

3.1 价格查询服务

java

复制代码
@Service
public class PricingService {

    @Autowired
    private UserIdentityMapper userIdentityMapper;
    @Autowired
    private ProductPriceMapper productPriceMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String PRICE_CACHE_KEY = "price:product:";

    public BigDecimal getPrice(Long userId, Long productId) {
        // 1. 查用户身份
        UserIdentity identity = userIdentityMapper.selectById(userId);
        if (identity == null) {
            throw new BizException("用户身份不存在");
        }

        // 2. 查商品价格(优先走缓存)
        String cacheKey = PRICE_CACHE_KEY + productId;
        ProductPrice price = (ProductPrice) redisTemplate.opsForValue().get(cacheKey);
        if (price == null) {
            price = productPriceMapper.selectByProductId(productId);
            if (price == null) {
                throw new BizException("商品价格未配置");
            }
            redisTemplate.opsForValue().set(cacheKey, price, 10, TimeUnit.MINUTES);
        }

        // 3. 按身份等级返回对应价格
        switch (identity.getLevel()) {
            case 1: return price.getLevel1Price();
            case 2: return price.getLevel2Price();
            case 3: return price.getLevel3Price();
            case 4: return price.getLevel4Price();
            case 5: return price.getLevel5Price();
            default: throw new BizException("未知身份等级");
        }
    }
}

3.2 价格缓存更新

商品价格变更时,删除缓存:

java

复制代码
public void updateProductPrice(ProductPrice price) {
    productPriceMapper.updateById(price);
    redisTemplate.delete(PRICE_CACHE_KEY + price.getProductId());
}

四、自动分佣引擎实现

4.1 订单签收触发分佣

订单签收后,发送 MQ 消息,异步计算佣金:

java

复制代码
@Service
public class OrderService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void signOrder(String orderNo) {
        Order order = orderMapper.selectByOrderNo(orderNo);
        if (order.getStatus() != OrderStatus.SIGNED.getCode()) {
            throw new BizException("订单状态不正确");
        }
        // 发送分佣消息
        rocketMQTemplate.convertAndSend("commission-topic", orderNo);
    }
}

4.2 分佣消费者

java

复制代码
@Component
@RocketMQMessageListener(topic = "commission-topic", consumerGroup = "commission-consumer")
public class CommissionConsumer implements RocketMQListener<String> {

    @Autowired
    private CommissionService commissionService;

    @Override
    public void onMessage(String orderNo) {
        commissionService.calculateAndSave(orderNo);
    }
}

4.3 佣金计算核心逻辑

java

复制代码
@Service
public class CommissionService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserIdentityMapper userIdentityMapper;
    @Autowired
    private PricingService pricingService;
    @Autowired
    private CommissionFlowMapper commissionFlowMapper;

    @Transactional(rollbackFor = Exception.class)
    public void calculateAndSave(String orderNo) {
        Order order = orderMapper.selectByOrderNo(orderNo);
        if (order == null || order.getStatus() != OrderStatus.SIGNED.getCode()) {
            return;
        }

        Long distributorId = order.getDistributorId();
        Long productId = order.getProductId();
        Integer quantity = order.getQuantity();
        BigDecimal dealPrice = order.getUnitPrice();

        // 1. 零售差价:分销商本人
        BigDecimal distributorPrice = pricingService.getPrice(distributorId, productId);
        BigDecimal retailDiff = dealPrice.subtract(distributorPrice)
                .multiply(BigDecimal.valueOf(quantity));
        saveCommission(orderNo, distributorId, CommissionType.RETAIL, retailDiff);

        // 2. 层级差价:直接上级、上上级,最多两级
        UserIdentity distributor = userIdentityMapper.selectById(distributorId);
        Long parentId = distributor.getParentId();
        if (parentId != null) {
            BigDecimal parentPrice = pricingService.getPrice(parentId, productId);
            BigDecimal parentDiff = distributorPrice.subtract(parentPrice)
                    .multiply(BigDecimal.valueOf(quantity));
            saveCommission(orderNo, parentId, CommissionType.SPREAD, parentDiff);

            UserIdentity parent = userIdentityMapper.selectById(parentId);
            Long grandId = parent.getParentId();
            if (grandId != null) {
                BigDecimal grandPrice = pricingService.getPrice(grandId, productId);
                BigDecimal grandDiff = parentPrice.subtract(grandPrice)
                        .multiply(BigDecimal.valueOf(quantity));
                saveCommission(orderNo, grandId, CommissionType.SPREAD, grandDiff);
            }
        }

        // 3. 团队管理收益:按直属团队真实销售额计算,三级内
        // 此处省略具体实现,核心是统计真实成交订单,不统计人头
        // ...
    }

    private void saveCommission(String orderNo, Long userId, CommissionType type, BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        CommissionFlow flow = new CommissionFlow();
        flow.setOrderNo(orderNo);
        flow.setUserId(userId);
        flow.setCommissionType(type.getCode());
        flow.setAmount(amount);
        flow.setStatus(CommissionStatus.PENDING.getCode());
        try {
            commissionFlowMapper.insert(flow);
        } catch (DuplicateKeyException e) {
            // 幂等:已存在则忽略
            log.warn("佣金流水已存在,orderNo={}, userId={}, type={}", orderNo, userId, type);
        }
    }
}

4.4 幂等与并发控制

  • 数据库唯一键uk_order_user_type 保证同一订单同一用户同一类型只入账一次。

  • MQ 消费幂等 :消费者先查是否已处理,或依赖数据库唯一键捕获 DuplicateKeyException

  • 分布式锁 :如果同一订单并发触发多次,可用 Redis 锁 lock:commission:orderNo 控制。

java

复制代码
public void calculateWithLock(String orderNo) {
    String lockKey = "lock:commission:" + orderNo;
    Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
    if (Boolean.FALSE.equals(locked)) {
        return;
    }
    try {
        calculateAndSave(orderNo);
    } finally {
        redisTemplate.delete(lockKey);
    }
}

五、三级合规的技术实现

5.1 层级上限锁定

绑定上下级时校验深度:

java

复制代码
@Service
public class RelationService {

    private static final int MAX_LEVEL = 3;

    @Autowired
    private UserIdentityMapper userIdentityMapper;

    public void bindParent(Long userId, Long parentId) {
        if (userId.equals(parentId)) {
            throw new BizException("不能绑定自己");
        }
        int parentDepth = getDepth(parentId);
        if (parentDepth >= MAX_LEVEL) {
            throw new BizException("分销层级已达上限");
        }
        UserIdentity identity = new UserIdentity();
        identity.setUserId(userId);
        identity.setParentId(parentId);
        identity.setGrandparentId(getParentId(parentId));
        userIdentityMapper.updateById(identity);
    }

    private int getDepth(Long userId) {
        int depth = 1;
        Long current = userId;
        while (depth <= MAX_LEVEL) {
            UserIdentity identity = userIdentityMapper.selectById(current);
            if (identity == null || identity.getParentId() == null) {
                break;
            }
            current = identity.getParentId();
            depth++;
        }
        return depth;
    }

    private Long getParentId(Long userId) {
        UserIdentity identity = userIdentityMapper.selectById(userId);
        return identity == null ? null : identity.getParentId();
    }
}

5.2 按真实销售计酬

佣金计算只读取 order 表中状态为"已签收"的订单,不读取任何"团队人数""拉人头"数据。计酬科目仅包含:零售差价、层级差价(最多两级)、团队销售服务费(按真实销售额)、平级/培育奖励(挂钩真实销售)、团队冲量奖(后台设上限,不挂钩人头)。

5.3 全链路审计日志

sql

复制代码
CREATE TABLE `audit_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL,
  `action` varchar(64) NOT NULL COMMENT '操作类型',
  `target_id` bigint DEFAULT NULL,
  `before_data` json DEFAULT NULL,
  `after_data` json DEFAULT NULL,
  `ip` varchar(45) DEFAULT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user` (`user_id`),
  KEY `idx_action` (`action`),
  KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志';

关键操作(层级绑定、佣金结算、提现、价格变更)均写入审计日志,保留至少3年,支持按用户、时间、操作类型检索。


六、对账与异常处理

6.1 每日对账

java

复制代码
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReconciliation() {
    // 1. 统计当日已签收订单总金额
    // 2. 统计当日佣金流水总金额
    // 3. 校验订单金额 - 佣金金额 - 平台留存 = 0
    // 4. 异常记录写入对账异常表,人工介入
}

6.2 异常处理策略

  • 佣金金额为负:不生成流水,记录日志。

  • 层级关系断裂:跳过该级佣金,记录异常。

  • MQ 消费失败:重试3次,仍失败进入死信队列,人工处理。

  • 对账不平:冻结相关账户,触发告警。


七、性能优化建议

  1. 缓存:商品五级价格、用户身份信息放入 Redis,减少数据库压力。

  2. 异步结算:佣金计算走 MQ,避免阻塞订单主流程。

  3. 分库分表:订单表和佣金流水表按用户ID哈希分片。

  4. 批量插入:佣金流水批量写入,减少数据库交互。

  5. 读写分离:订单查询走从库,佣金计算走主库。


八、总结

本文从实战角度讲解了五级分销定价与自动分佣系统的核心实现:

  • 五级定价 :通过 user_identity + product_price 联动,按身份等级返回对应价格。

  • 自动分佣:订单签收后发 MQ,消费者异步计算零售差、层级差、团队收益,用唯一键和分布式锁保证幂等。

  • 三级合规:绑定层级时校验深度,佣金只读真实成交订单,关键操作写审计日志。

我们提供良久团购模式系统完整源码交付,包含五级定价、自动结算、裂变分佣、合规审计等全模块,支持二次开发。

风险提示:本文数据来自公开媒体报道,仅供参考,不构成收益承诺或投资建议。系统采购方应确保经营行为符合当地法律法规和监管要求,不得以入门费、拉人头、多级计酬等方式违规经营。具体模式配置建议咨询专业法律及合规人士。

相关推荐
爱勇宝1 小时前
《道德经》第 11 章:真正有用的,常常是你没写出来的那部分
前端·后端·产品
Json____1 小时前
从零构建在线拍卖系统:Java 全栈开发实践
java·开发语言·管理系统·it学习·wwwoop.com
我命由我123451 小时前
Android Compose 开发,使用 ConstraintLayout,但是引入的是旧的 ConstraintLayout
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
钱栈up1 小时前
mvn compile卡住半小时:定位并修复隐藏的类型不匹配编译错误
java·后端·maven
妙码生花1 小时前
golang 应用服务端部署(使用 systemd 服务)
开发语言·人工智能·后端·golang·node.js·php·gin
Zane19941 小时前
Lambda表达式没有类型,为什么还能被当成参数传来传去
java·后端
重生之小比特1 小时前
【Java SE】运算符
java·开发语言
南雨北斗1 小时前
Vue3 + ThinkPHP6 JWT 教程
后端
jsl_jsl_jsl1 小时前
MongoDB速记
后端