Преглед на файлове

member: 分销增加佣金

owen преди 1 година
родител
ревизия
c7ef8519c8
променени са 23 файла, в които са добавени 1068 реда и са изтрити 2 реда
  1. 189 0
      sql/mysql/brokerage.sql
  2. 9 0
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/ProductSkuRespDTO.java
  3. 8 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  4. 15 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  5. 30 0
      yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/brokerage/BrokerageApi.java
  6. 35 0
      yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/brokerage/dto/BrokerageAddReqDTO.java
  7. 32 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/api/brokerage/BrokerageApiImpl.java
  8. 51 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/MemberBrokerageRecordController.java
  9. 59 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/vo/MemberBrokerageRecordBaseVO.java
  10. 33 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/vo/MemberBrokerageRecordPageReqVO.java
  11. 22 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/vo/MemberBrokerageRecordRespVO.java
  12. 46 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/brokerage/record/MemberBrokerageRecordConvert.java
  13. 78 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/brokerage/record/MemberBrokerageRecordDO.java
  14. 27 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java
  15. 27 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/brokerage/record/MemberBrokerageRecordMapper.java
  16. 30 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java
  17. 40 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/brokerage/record/MemberBrokerageRecordService.java
  18. 151 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/brokerage/record/MemberBrokerageRecordServiceImpl.java
  19. 24 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java
  20. 22 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
  21. 116 0
      yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/brokerage/record/MemberBrokerageRecordServiceImplTest.java
  22. 2 1
      yudao-module-member/yudao-module-member-biz/src/test/resources/sql/clean.sql
  23. 22 1
      yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql

+ 189 - 0
sql/mysql/brokerage.sql

@@ -0,0 +1,189 @@
+-- 增加配置
+alter table member_point_config
+    add column brokerage_enabled bit default 1 not null comment '是否启用分佣';
+alter table member_point_config
+    add column brokerage_enabled_condition tinyint default 0 not null comment '分佣模式:0-人人分销 1-指定分销';
+alter table member_point_config
+    add column brokerage_bind_mode tinyint default 0 not null comment '分销关系绑定模式: 0-没有推广人,1-新用户';
+alter table member_point_config
+    add column brokerage_post_urls varchar(2000) null comment '分销海报图地址数组';
+alter table member_point_config
+    add column brokerage_first_percent int not null comment '一级返佣比例';
+alter table member_point_config
+    add column brokerage_second_percent int not null comment '二级返佣比例';
+alter table member_point_config
+    add column brokerage_withdraw_min_price int not null comment '用户提现最低金额';
+alter table member_point_config
+    add column brokerage_bank_names varchar(200) not null comment '提现银行(字典类型=brokerage_bank_name)';
+alter table member_point_config
+    add column brokerage_frozen_days int default 7 not null comment '佣金冻结时间(天)';
+alter table member_point_config
+    add column brokerage_withdraw_type varchar(32) default '1,2,3,4' not null comment '提现方式:1-钱包;2-银行卡;3-微信;4-支付宝';
+
+-- 用户表增加分销相关字段
+alter table member_user
+    add column brokerage_user_id bigint not null comment '推广员编号';
+alter table member_user
+    add column brokerage_bind_time datetime null comment '推广员绑定时间';
+alter table member_user
+    add column brokerage_enabled bit default 1 not null comment '是否成为推广员';
+alter table member_user
+    add column brokerage_time datetime null comment '成为分销员时间';
+alter table member_user
+    add column brokerage_price int default 0 not null comment '可用佣金';
+alter table member_user
+    add column frozen_brokerage_price int default 0 not null comment '冻结佣金';
+
+create index idx_invite_user_id on member_user (brokerage_user_id) comment '推广员编号';
+create index idx_agent on member_user (brokerage_enabled) comment '是否成为推广员';
+
+
+create table member_brokerage_record
+(
+    id            int auto_increment comment '编号'
+        primary key,
+    user_id       bigint                                                           not null comment '用户编号',
+    biz_id        varchar(64)                            default ''                not null comment '业务编号',
+    biz_type      tinyint                                default 0                 not null comment '业务类型:0-订单,1-提现',
+    title         varchar(64)                            default ''                not null comment '标题',
+    price         int                                    default 0                 not null comment '金额',
+    total_price   int                                    default 0                 not null comment '当前总佣金',
+    description   varchar(500)                           default ''                not null comment '说明',
+    status        tinyint                                default 0                 not null comment '状态:0-待结算,1-已结算,2-已取消',
+    frozen_days   int                                    default 0                 not null comment '冻结时间(天)',
+    unfreeze_time datetime                                                         null comment '解冻时间',
+    creator       varchar(64) collate utf8mb4_general_ci default ''                null comment '创建者',
+    create_time   datetime                               default CURRENT_TIMESTAMP not null comment '创建时间',
+    updater       varchar(64) collate utf8mb4_general_ci default ''                null comment '更新者',
+    update_time   datetime                               default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    deleted       bit                                    default b'0'              not null comment '是否删除',
+    tenant_id     bigint                                 default 0                 not null comment '租户编号'
+)
+    comment '佣金记录';
+
+create index idx_user_id on member_brokerage_record (user_id) comment '用户编号';
+create index idx_biz on member_brokerage_record (biz_type, biz_id) comment '业务';
+create index idx_status on member_brokerage_record (status) comment '状态';
+
+
+create table member_brokerage_withdraw
+(
+    id                  int auto_increment comment '编号'
+        primary key,
+    user_id             bigint                                                           not null comment '用户编号',
+    price               int                                    default 0                 not null comment '提现金额',
+    fee_price           int                                    default 0                 not null comment '提现手续费',
+    total_price         int                                    default 0                 not null comment '当前总佣金',
+    type                tinyint                                default 0                 not null comment '提现类型:1-钱包;2-银行卡;3-微信;4-支付宝',
+    name                varchar(64)                                                      null comment '真实姓名',
+    account_no          varchar(64)                                                      null comment '账号',
+    bank_name           varchar(100)                                                     null comment '银行名称',
+    bank_address        varchar(200)                                                     null comment '开户地址',
+    account_qr_code_url varchar(512)                                                     null comment '收款码',
+    status              tinyint(2)                             default 0                 not null comment '状态:0-审核中,10-审核通过 20-审核不通过;预留:11 - 提现成功;21-提现失败',
+    audit_reason        varchar(128)                                                     null comment '审核驳回原因',
+    audit_time          datetime                                                         null comment '审核时间',
+    remark              varchar(500)                                                     null comment '备注',
+    creator             varchar(64) collate utf8mb4_general_ci default ''                null comment '创建者',
+    create_time         datetime                               default CURRENT_TIMESTAMP not null comment '创建时间',
+    updater             varchar(64) collate utf8mb4_general_ci default ''                null comment '更新者',
+    update_time         datetime                               default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
+    deleted             bit                                    default b'0'              not null comment '是否删除',
+    tenant_id           bigint                                 default 0                 not null comment '租户编号'
+)
+    comment '佣金提现';
+
+create index idx_user_id on member_brokerage_withdraw (user_id) comment '用户编号';
+create index idx_audit_status on member_brokerage_withdraw (status) comment '状态';
+
+-- 增加字典
+insert into system_dict_type(type, name)
+values ('brokerage_enabled_condition', '分佣模式');
+insert into system_dict_data(dict_type, label, value, sort, remark)
+values ('brokerage_enabled_condition', '人人分销', 0, 0, '所有用户都可以分销'),
+       ('brokerage_enabled_condition', '指定分销', 1, 1, '仅可后台手动设置推广员');
+
+insert into system_dict_type(type, name)
+values ('brokerage_bind_mode', '分销关系绑定模式');
+insert into system_dict_data(dict_type, label, value, sort, remark)
+values ('brokerage_bind_mode', '没有推广人', 0, 0, '只要用户没有推广人,随时都可以绑定推广关系'),
+       ('brokerage_bind_mode', '新用户', 1, 1, '仅新用户注册时才能绑定推广关系');
+
+insert into system_dict_type(type, name)
+values ('brokerage_withdraw_type', '佣金提现类型');
+insert into system_dict_data(dict_type, label, value, sort)
+values ('brokerage_withdraw_type', '钱包', 1, 1),
+       ('brokerage_withdraw_type', '银行卡', 2, 2),
+       ('brokerage_withdraw_type', '微信', 3, 3),
+       ('brokerage_withdraw_type', '支付宝', 4, 4);
+
+insert into system_dict_type(type, name)
+values ('brokerage_record_biz_type', '佣金记录业务类型');
+insert into system_dict_data(dict_type, label, value, sort)
+values ('brokerage_record_biz_type', '订单返佣', 0, 0),
+       ('brokerage_record_biz_type', '申请提现', 1, 1);
+
+insert into system_dict_type(type, name)
+values ('brokerage_record_status', '佣金记录状态');
+insert into system_dict_data(dict_type, label, value, sort)
+values ('brokerage_record_status', '待结算', 0, 0),
+       ('brokerage_record_status', '已结算', 1, 1),
+       ('brokerage_record_status', '已取消', 2, 2);
+
+insert into system_dict_type(type, name)
+values ('brokerage_withdraw_status', '佣金提现状态');
+insert into system_dict_data(dict_type, label, value, sort)
+values ('brokerage_withdraw_status', '审核中', 0, 0),
+       ('brokerage_withdraw_status', '审核通过', 10, 10),
+       ('brokerage_withdraw_status', '提现成功', 11, 11),
+       ('brokerage_withdraw_status', '审核不通过', 20, 20),
+       ('brokerage_withdraw_status', '提现失败', 21, 21);
+
+insert into system_dict_type(type, name)
+values ('brokerage_bank_name', '佣金提现银行');
+insert into system_dict_data(dict_type, label, value, sort)
+values ('brokerage_bank_name', '工商银行', 0, 0),
+       ('brokerage_bank_name', '建设银行', 1, 1),
+       ('brokerage_bank_name', '农业银行', 2, 2),
+       ('brokerage_bank_name', '中国银行', 3, 3),
+       ('brokerage_bank_name', '交通银行', 4, 4),
+       ('brokerage_bank_name', '招商银行', 5, 5);
+
+-- 增加菜单:分销员管理
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name)
+VALUES ('分销员', '', 2, 7, 2262, 'brokerage', 'user', 'member/brokerage/user/index', 0, 'MemberBrokerageUser');
+-- 按钮父菜单ID
+SELECT @parentId := LAST_INSERT_ID();
+-- 按钮 SQL
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('分销员查询', 'member:brokerage-user:query', 3, 1, @parentId, '', '', '', 0);
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('分销员创建', 'member:brokerage-user:create', 3, 2, @parentId, '', '', '', 0);
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('分销员更新', 'member:brokerage-user:update', 3, 3, @parentId, '', '', '', 0);
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('分销员删除', 'member:brokerage-user:delete', 3, 4, @parentId, '', '', '', 0);
+
+-- 增加菜单:佣金记录
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name)
+VALUES ('佣金记录', '', 2, 8, 2262, 'brokerage-record', 'list', 'member/brokerage/record/index', 0,
+        'MemberBrokerageRecord');
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+-- 按钮 SQL
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('佣金记录查询', 'member:member-brokerage-record:query', 3, 1, @parentId, '', 'table', '', 0);
+
+-- 增加菜单:佣金提现
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name)
+VALUES ('佣金提现', '', 2, 9, 2262, 'brokerage-withdraw', '', 'member/brokerage/withdraw/index', 0,
+        'MemberBrokerageWithdraw');
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
+VALUES ('佣金提现查询', 'member:brokerage-withdraw:query', 3, 1, @parentId, '', '', '', 0);

+ 9 - 0
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/sku/dto/ProductSkuRespDTO.java

@@ -60,4 +60,13 @@ public class ProductSkuRespDTO {
      */
     private Double volume;
 
+    /**
+     * 一级分销的佣金,单位:分
+     */
+    private Integer subCommissionFirstPrice;
+    /**
+     * 二级分销的佣金,单位:分
+     */
+    private Integer subCommissionSecondPrice;
+
 }

+ 8 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java

@@ -7,11 +7,13 @@ import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
+import cn.iocoder.yudao.module.member.api.brokerage.dto.BrokerageAddReqDTO;
 import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
 import cn.iocoder.yudao.module.pay.enums.DictTypeConstants;
 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
 import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDetailRespDTO;
+import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO;
 import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordCreateReqDTO;
 import cn.iocoder.yudao.module.trade.api.order.dto.TradeOrderRespDTO;
@@ -273,4 +275,10 @@ public interface TradeOrderConvert {
 
     TradeOrderDO convert(TradeOrderRemarkReqVO reqVO);
 
+    default BrokerageAddReqDTO convert(TradeOrderItemDO item, ProductSkuRespDTO sku) {
+        return new BrokerageAddReqDTO().setBizId(String.valueOf(item.getId()))
+                .setPayPrice(item.getPayPrice()).setCount(item.getCount())
+                .setSkuFirstBrokeragePrice(sku.getSubCommissionFirstPrice())
+                .setSkuSecondBrokeragePrice(sku.getSubCommissionSecondPrice());
+    }
 }

+ 15 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -12,6 +12,8 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.module.member.api.address.AddressApi;
 import cn.iocoder.yudao.module.member.api.address.dto.AddressRespDTO;
+import cn.iocoder.yudao.module.member.api.brokerage.BrokerageApi;
+import cn.iocoder.yudao.module.member.api.brokerage.dto.BrokerageAddReqDTO;
 import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
 import cn.iocoder.yudao.module.member.api.point.MemberPointApi;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
@@ -118,6 +120,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     @Resource
     private MemberPointApi memberPointApi;
     @Resource
+    private BrokerageApi brokerageApi;
+    @Resource
     private ProductCommentApi productCommentApi;
 
     @Resource
@@ -365,6 +369,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         getSelf().addUserPointAsync(order.getUserId(), order.getPayPrice(), order.getId());
         // 增加用户经验
         getSelf().addUserExperienceAsync(order.getUserId(), order.getPayPrice(), order.getId());
+        // 增加用户佣金
+        getSelf().addBrokerageAsync(order.getUserId(), order.getId());
     }
 
     /**
@@ -742,6 +748,15 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
         memberPointApi.addPoint(userId, -refundPrice, bizType, String.valueOf(afterSaleId));
     }
 
+
+    @Async
+    protected void addBrokerageAsync(Long userId, Long orderId) {
+        List<TradeOrderItemDO> orderItems = tradeOrderItemMapper.selectListByOrderId(orderId);
+        List<BrokerageAddReqDTO> list = convertList(orderItems,
+                item -> TradeOrderConvert.INSTANCE.convert(item, productSkuApi.getSku(item.getSkuId())));
+        brokerageApi.addBrokerage(userId, list);
+    }
+
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *

+ 30 - 0
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/brokerage/BrokerageApi.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.member.api.brokerage;
+
+import cn.iocoder.yudao.module.member.api.brokerage.dto.BrokerageAddReqDTO;
+
+import java.util.List;
+
+/**
+ * 佣金 API 接口
+ *
+ * @author owen
+ */
+public interface BrokerageApi {
+
+    /**
+     * 增加佣金
+     *
+     * @param userId 会员ID
+     * @param list   请求参数列表
+     */
+    void addBrokerage(Long userId, List<BrokerageAddReqDTO> list);
+
+    /**
+     * 取消佣金
+     *
+     * @param userId 会员ID
+     * @param bizId  业务编号
+     */
+    void cancelBrokerage(Long userId, String bizId);
+
+}

+ 35 - 0
yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/api/brokerage/dto/BrokerageAddReqDTO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.member.api.brokerage.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 佣金 增加 Request DTO
+ * @author owen
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class BrokerageAddReqDTO {
+    /**
+     * 业务ID
+     */
+    private String bizId;
+    /**
+     * 商品支付价格
+     */
+    private Integer payPrice;
+    /**
+     * SKU 一级佣金
+     */
+    private Integer skuFirstBrokeragePrice;
+    /**
+     * SKU 二级佣金
+     */
+    private Integer skuSecondBrokeragePrice;
+    /**
+     * 购买数量
+     */
+    private Integer count;
+}

+ 32 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/api/brokerage/BrokerageApiImpl.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.member.api.brokerage;
+
+import cn.iocoder.yudao.module.member.api.brokerage.dto.BrokerageAddReqDTO;
+import cn.iocoder.yudao.module.member.service.brokerage.record.MemberBrokerageRecordService;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 佣金 API 实现类
+ *
+ * @author owen
+ */
+@Service
+@Validated
+public class BrokerageApiImpl implements BrokerageApi {
+
+    @Resource
+    private MemberBrokerageRecordService memberBrokerageRecordService;
+
+    @Override
+    public void addBrokerage(Long userId, List<BrokerageAddReqDTO> list) {
+        memberBrokerageRecordService.addBrokerage(userId, list);
+    }
+
+    @Override
+    public void cancelBrokerage(Long userId, String bizId) {
+
+    }
+}

+ 51 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/MemberBrokerageRecordController.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.member.controller.admin.brokerage.record;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordPageReqVO;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordRespVO;
+import cn.iocoder.yudao.module.member.convert.brokerage.record.MemberBrokerageRecordConvert;
+import cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record.MemberBrokerageRecordDO;
+import cn.iocoder.yudao.module.member.service.brokerage.record.MemberBrokerageRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 佣金记录")
+@RestController
+@RequestMapping("/member/member-brokerage-record")
+@Validated
+public class MemberBrokerageRecordController {
+
+    @Resource
+    private MemberBrokerageRecordService memberBrokerageRecordService;
+
+    @GetMapping("/get")
+    @Operation(summary = "获得佣金记录")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('member:member-brokerage-record:query')")
+    public CommonResult<MemberBrokerageRecordRespVO> getMemberBrokerageRecord(@RequestParam("id") Integer id) {
+        MemberBrokerageRecordDO memberBrokerageRecord = memberBrokerageRecordService.getMemberBrokerageRecord(id);
+        return success(MemberBrokerageRecordConvert.INSTANCE.convert(memberBrokerageRecord));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得佣金记录分页")
+    @PreAuthorize("@ss.hasPermission('member:member-brokerage-record:query')")
+    public CommonResult<PageResult<MemberBrokerageRecordRespVO>> getMemberBrokerageRecordPage(@Valid MemberBrokerageRecordPageReqVO pageVO) {
+        PageResult<MemberBrokerageRecordDO> pageResult = memberBrokerageRecordService.getMemberBrokerageRecordPage(pageVO);
+        return success(MemberBrokerageRecordConvert.INSTANCE.convertPage(pageResult));
+    }
+
+}

+ 59 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/vo/MemberBrokerageRecordBaseVO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 佣金记录 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class MemberBrokerageRecordBaseVO {
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25973")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23353")
+    @NotNull(message = "业务编号不能为空")
+    private String bizId;
+
+    @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "业务类型不能为空")
+    private Integer bizType;
+
+    @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "标题不能为空")
+    private String title;
+
+    @Schema(description = "金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "28731")
+    @NotNull(message = "金额不能为空")
+    private Integer price;
+
+    @Schema(description = "当前总佣金", requiredMode = Schema.RequiredMode.REQUIRED, example = "13226")
+    @NotNull(message = "当前总佣金不能为空")
+    private Integer totalPrice;
+
+    @Schema(description = "说明", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
+    @NotNull(message = "说明不能为空")
+    private String description;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    @Schema(description = "冻结时间(天)", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "冻结时间(天)不能为空")
+    private Integer frozenDays;
+
+    @Schema(description = "解冻时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime unfreezeTime;
+
+}

+ 33 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/vo/MemberBrokerageRecordPageReqVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 佣金记录分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberBrokerageRecordPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "25973")
+    private Long userId;
+
+    @Schema(description = "业务类型", example = "1")
+    private Integer bizType;
+
+    @Schema(description = "状态", example = "1")
+    private Integer status;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 22 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/brokerage/record/vo/MemberBrokerageRecordRespVO.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 佣金记录 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MemberBrokerageRecordRespVO extends MemberBrokerageRecordBaseVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28896")
+    private Integer id;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 46 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/convert/brokerage/record/MemberBrokerageRecordConvert.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.member.convert.brokerage.record;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordRespVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record.MemberBrokerageRecordDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.enums.brokerage.BrokerageRecordBizTypeEnum;
+import cn.iocoder.yudao.module.member.enums.brokerage.BrokerageRecordStatusEnum;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 佣金记录 Convert
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberBrokerageRecordConvert {
+
+    MemberBrokerageRecordConvert INSTANCE = Mappers.getMapper(MemberBrokerageRecordConvert.class);
+
+    MemberBrokerageRecordRespVO convert(MemberBrokerageRecordDO bean);
+
+    List<MemberBrokerageRecordRespVO> convertList(List<MemberBrokerageRecordDO> list);
+
+    PageResult<MemberBrokerageRecordRespVO> convertPage(PageResult<MemberBrokerageRecordDO> page);
+
+    default MemberBrokerageRecordDO convert(MemberUserDO user, String bizId, int brokerageFrozenDays, int brokerage, LocalDateTime unfreezeTime) {
+        return new MemberBrokerageRecordDO()
+                .setUserId(user.getId())
+                .setBizType(BrokerageRecordBizTypeEnum.ORDER.getType())
+                .setBizId(bizId)
+                .setPrice(brokerage)
+                .setTotalPrice(user.getBrokeragePrice())
+                .setTitle(BrokerageRecordBizTypeEnum.ORDER.getTitle())
+                .setDescription(StrUtil.format(BrokerageRecordBizTypeEnum.ORDER.getDescription(), String.valueOf(brokerage / 100.0)))
+                .setStatus(BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus())
+                .setFrozenDays(brokerageFrozenDays)
+                .setUnfreezeTime(unfreezeTime);
+    }
+
+}

+ 78 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/brokerage/record/MemberBrokerageRecordDO.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.member.enums.brokerage.BrokerageRecordBizTypeEnum;
+import cn.iocoder.yudao.module.member.enums.brokerage.BrokerageRecordStatusEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 佣金记录 DO
+ *
+ * @author owen
+ */
+@TableName("member_brokerage_record")
+@KeySequence("member_brokerage_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MemberBrokerageRecordDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Integer id;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 业务编号
+     */
+    private String bizId;
+    /**
+     * 业务类型
+     * <p>
+     * 枚举 {@link BrokerageRecordBizTypeEnum 对应的类}
+     */
+    private Integer bizType;
+    /**
+     * 标题
+     */
+    private String title;
+    /**
+     * 金额
+     */
+    private Integer price;
+    /**
+     * 当前总佣金
+     */
+    private Integer totalPrice;
+    /**
+     * 说明
+     */
+    private String description;
+    /**
+     * 状态
+     * <p>
+     * 枚举 {@link BrokerageRecordStatusEnum 对应的类}
+     */
+    private Integer status;
+    /**
+     * 冻结时间(天)
+     */
+    private Integer frozenDays;
+    /**
+     * 解冻时间
+     */
+    private LocalDateTime unfreezeTime;
+
+}

+ 27 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/user/MemberUserDO.java

@@ -136,4 +136,31 @@ public class MemberUserDO extends TenantBaseDO {
      */
     private Long groupId;
 
+    // ========== 分销相关 ==========
+
+    /**
+     * 推广员编号
+     */
+    private Long brokerageUserId;
+    /**
+     * 推广员绑定时间
+     */
+    private LocalDateTime brokerageBindTime;
+    /**
+     * 是否成为推广员
+     */
+    private Boolean brokerageEnabled;
+    /**
+     * 成为分销员时间
+     */
+    private LocalDateTime brokerageTime;
+    /**
+     * 可用佣金
+     */
+    private Integer brokeragePrice;
+    /**
+     * 冻结佣金
+     */
+    private Integer frozenBrokeragePrice;
+
 }

+ 27 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/brokerage/record/MemberBrokerageRecordMapper.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.member.dal.mysql.brokerage.record;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record.MemberBrokerageRecordDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 佣金记录 Mapper
+ *
+ * @author owen
+ */
+@Mapper
+public interface MemberBrokerageRecordMapper extends BaseMapperX<MemberBrokerageRecordDO> {
+
+    default PageResult<MemberBrokerageRecordDO> selectPage(MemberBrokerageRecordPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MemberBrokerageRecordDO>()
+                .eqIfPresent(MemberBrokerageRecordDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(MemberBrokerageRecordDO::getBizType, reqVO.getBizType())
+                .eqIfPresent(MemberBrokerageRecordDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(MemberBrokerageRecordDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MemberBrokerageRecordDO::getId));
+    }
+
+}

+ 30 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.module.member.dal.mysql.user;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
@@ -62,4 +64,32 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
                 .apply("FIND_IN_SET({0}, tag_ids)", tagId));
     }
 
+    /**
+     * 更新用户可用佣金(增加)
+     *
+     * @param id        用户编号
+     * @param incrCount 增加佣金(正数)
+     */
+    default void updateBrokeragePriceIncr(Long id, int incrCount) {
+        Assert.isTrue(incrCount > 0);
+        LambdaUpdateWrapper<MemberUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<MemberUserDO>()
+                .setSql(" brokerage_price = brokerage_price + " + incrCount)
+                .eq(MemberUserDO::getId, id);
+        update(null, lambdaUpdateWrapper);
+    }
+
+    /**
+     * 更新用户冻结佣金(增加)
+     *
+     * @param id        用户编号
+     * @param incrCount 增加冻结佣金(正数)
+     */
+    default void updateFrozenBrokeragePriceIncr(Long id, int incrCount) {
+        Assert.isTrue(incrCount > 0);
+        LambdaUpdateWrapper<MemberUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<MemberUserDO>()
+                .setSql(" frozen_brokerage_price = frozen_brokerage_price + " + incrCount)
+                .eq(MemberUserDO::getId, id);
+        update(null, lambdaUpdateWrapper);
+    }
+
 }

+ 40 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/brokerage/record/MemberBrokerageRecordService.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.member.service.brokerage.record;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.api.brokerage.dto.BrokerageAddReqDTO;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record.MemberBrokerageRecordDO;
+
+import java.util.List;
+
+/**
+ * 佣金记录 Service 接口
+ *
+ * @author owen
+ */
+public interface MemberBrokerageRecordService {
+
+    /**
+     * 获得佣金记录
+     *
+     * @param id 编号
+     * @return 佣金记录
+     */
+    MemberBrokerageRecordDO getMemberBrokerageRecord(Integer id);
+
+    /**
+     * 获得佣金记录分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 佣金记录分页
+     */
+    PageResult<MemberBrokerageRecordDO> getMemberBrokerageRecordPage(MemberBrokerageRecordPageReqVO pageReqVO);
+
+    /**
+     * 增加佣金
+     *
+     * @param userId 会员ID
+     * @param list   请求参数列表
+     */
+    void addBrokerage(Long userId, List<BrokerageAddReqDTO> list);
+}

+ 151 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/brokerage/record/MemberBrokerageRecordServiceImpl.java

@@ -0,0 +1,151 @@
+package cn.iocoder.yudao.module.member.service.brokerage.record;
+
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.member.api.brokerage.dto.BrokerageAddReqDTO;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordPageReqVO;
+import cn.iocoder.yudao.module.member.convert.brokerage.record.MemberBrokerageRecordConvert;
+import cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record.MemberBrokerageRecordDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.point.MemberPointConfigDO;
+import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
+import cn.iocoder.yudao.module.member.dal.mysql.brokerage.record.MemberBrokerageRecordMapper;
+import cn.iocoder.yudao.module.member.service.point.MemberPointConfigService;
+import cn.iocoder.yudao.module.member.service.user.MemberUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * 佣金记录 Service 实现类
+ *
+ * @author owen
+ */
+@Slf4j
+@Service
+@Validated
+public class MemberBrokerageRecordServiceImpl implements MemberBrokerageRecordService {
+
+    @Resource
+    private MemberBrokerageRecordMapper memberBrokerageRecordMapper;
+    @Resource
+    private MemberPointConfigService memberConfigService;
+    @Resource
+    private MemberUserService memberUserService;
+
+    @Override
+    public MemberBrokerageRecordDO getMemberBrokerageRecord(Integer id) {
+        return memberBrokerageRecordMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<MemberBrokerageRecordDO> getMemberBrokerageRecordPage(MemberBrokerageRecordPageReqVO pageReqVO) {
+        return memberBrokerageRecordMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public void addBrokerage(Long buyerId, List<BrokerageAddReqDTO> list) {
+        MemberPointConfigDO memberConfig = memberConfigService.getPointConfig();
+        // 0 未启用分销功能
+        if (memberConfig == null || !BooleanUtil.isTrue(memberConfig.getBrokerageEnabled())) {
+            log.warn("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,buyerId({})", buyerId);
+            return;
+        }
+
+        // 1.1 获得一级推广人
+        MemberUserDO firstUser = memberUserService.getBrokerageUser(buyerId);
+        if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
+            return;
+        }
+
+        // 1.2 计算一级分佣
+        addBrokerage(firstUser, list, memberConfig.getBrokerageFrozenDays(), memberConfig.getBrokerageFirstPercent(), BrokerageAddReqDTO::getSkuFirstBrokeragePrice);
+
+
+        // 2.1 获得二级推广员
+        MemberUserDO secondUser = memberUserService.getUser(firstUser.getBrokerageUserId());
+        if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
+            return;
+        }
+
+        // 2.2 计算二级分佣
+        addBrokerage(secondUser, list, memberConfig.getBrokerageFrozenDays(), memberConfig.getBrokerageSecondPercent(), BrokerageAddReqDTO::getSkuSecondBrokeragePrice);
+    }
+
+    /**
+     * 计算佣金
+     *
+     * @param payPrice          订单支付金额
+     * @param percent           商品 SKU 设置的佣金
+     * @param skuBrokeragePrice 商品的佣金
+     * @return 佣金
+     */
+    int calculateBrokerage(Integer payPrice, Integer percent, Integer skuBrokeragePrice) {
+        // 1. 优先使用商品 SKU 设置的佣金
+        if (skuBrokeragePrice != null && skuBrokeragePrice > 0) {
+            return ObjectUtil.defaultIfNull(skuBrokeragePrice, 0);
+        }
+
+        // 2. 根据订单支付金额计算佣金
+        if (payPrice != null && payPrice > 0 && percent != null && percent > 0) {
+            return NumberUtil.div(NumberUtil.mul(payPrice, percent), 100, 0, RoundingMode.DOWN).intValue();
+        }
+
+        return 0;
+    }
+
+    /**
+     * 增加用户佣金
+     *
+     * @param user                 用户
+     * @param list                 佣金增加参数列表
+     * @param brokerageFrozenDays  冻结天数
+     * @param brokeragePercent     佣金比例
+     * @param skuBrokeragePriceFun 商品 SKU 设置的佣金
+     */
+    private void addBrokerage(MemberUserDO user, List<BrokerageAddReqDTO> list, Integer brokerageFrozenDays,
+                              Integer brokeragePercent, Function<BrokerageAddReqDTO, Integer> skuBrokeragePriceFun) {
+        // 处理冻结时间
+        brokerageFrozenDays = ObjectUtil.defaultIfNull(brokerageFrozenDays, 0);
+        LocalDateTime unfreezeTime = null;
+        if (brokerageFrozenDays > 0) {
+            unfreezeTime = LocalDateTime.now().plusDays(brokerageFrozenDays);
+        }
+
+        // 计算分佣
+        int totalBrokerage = 0;
+        List<MemberBrokerageRecordDO> records = new ArrayList<>();
+        for (BrokerageAddReqDTO dto : list) {
+            int brokeragePerItem = calculateBrokerage(dto.getPayPrice(), brokeragePercent, skuBrokeragePriceFun.apply(dto));
+            if (brokeragePerItem > 0) {
+                int brokerage = brokeragePerItem * dto.getCount();
+                records.add(MemberBrokerageRecordConvert.INSTANCE.convert(user, dto.getBizId(), brokerageFrozenDays, brokerage, unfreezeTime));
+                totalBrokerage += brokerage;
+            }
+        }
+
+        if (records.isEmpty()) {
+            return;
+        }
+
+        // 保存佣金记录
+        memberBrokerageRecordMapper.insertBatch(records);
+
+        if (brokerageFrozenDays > 0) {
+            // 更新用户冻结佣金
+            memberUserService.updateUserFrozenBrokeragePrice(user.getId(), totalBrokerage);
+        } else {
+            // 更新用户可用佣金
+            memberUserService.updateUserBrokeragePrice(user.getId(), totalBrokerage);
+        }
+    }
+
+}

+ 24 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java

@@ -166,4 +166,28 @@ public interface MemberUserService {
      * @param point  积分数量
      */
     void updateUserPoint(Long userId, Integer point);
+
+    /**
+     * 获得用户的推广人
+     *
+     * @param id 用户编号
+     * @return 用户的推广人
+     */
+    MemberUserDO getBrokerageUser(Long id);
+
+    /**
+     * 增加用户佣金
+     *
+     * @param id             用户编号
+     * @param brokeragePrice 用户可用佣金
+     */
+    void updateUserBrokeragePrice(Long id, int brokeragePrice);
+
+    /**
+     * 增加用户佣金
+     *
+     * @param id                   用户编号
+     * @param frozenBrokeragePrice 用户冻结佣金
+     */
+    void updateUserFrozenBrokeragePrice(Long id, int frozenBrokeragePrice);
 }

+ 22 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java

@@ -30,6 +30,7 @@ import javax.validation.Valid;
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@@ -259,4 +260,25 @@ public class MemberUserServiceImpl implements MemberUserService {
         memberUserMapper.updateById(new MemberUserDO().setId(userId).setPoint(point));
     }
 
+    @Override
+    public MemberUserDO getBrokerageUser(Long id) {
+        return Optional.ofNullable(id)
+                .map(this::getUser)
+                .map(MemberUserDO::getBrokerageUserId)
+                .map(this::getUser)
+                .orElse(null);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateUserBrokeragePrice(Long id, int brokeragePrice) {
+        memberUserMapper.updateBrokeragePriceIncr(id, brokeragePrice);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateUserFrozenBrokeragePrice(Long id, int frozenBrokeragePrice) {
+        memberUserMapper.updateFrozenBrokeragePriceIncr(id, frozenBrokeragePrice);
+    }
+
 }

+ 116 - 0
yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/brokerage/record/MemberBrokerageRecordServiceImplTest.java

@@ -0,0 +1,116 @@
+package cn.iocoder.yudao.module.member.service.brokerage.record;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.member.controller.admin.brokerage.record.vo.MemberBrokerageRecordPageReqVO;
+import cn.iocoder.yudao.module.member.dal.dataobject.brokerage.record.MemberBrokerageRecordDO;
+import cn.iocoder.yudao.module.member.dal.mysql.brokerage.record.MemberBrokerageRecordMapper;
+import cn.iocoder.yudao.module.member.service.point.MemberPointConfigService;
+import cn.iocoder.yudao.module.member.service.user.MemberUserService;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.math.RoundingMode;
+
+import static cn.hutool.core.util.RandomUtil.randomEle;
+import static cn.hutool.core.util.RandomUtil.randomInt;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomInteger;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * {@link MemberBrokerageRecordServiceImpl} 的单元测试类
+ *
+ * @author owen
+ */
+@Import(MemberBrokerageRecordServiceImpl.class)
+public class MemberBrokerageRecordServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private MemberBrokerageRecordServiceImpl memberBrokerageRecordService;
+    @Resource
+    private MemberBrokerageRecordMapper memberBrokerageRecordMapper;
+
+    @MockBean
+    private MemberPointConfigService memberPointConfigService;
+    @MockBean
+    private MemberUserService memberUserService;
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetMemberBrokerageRecordPage() {
+        // mock 数据
+        MemberBrokerageRecordDO dbMemberBrokerageRecord = randomPojo(MemberBrokerageRecordDO.class, o -> { // 等会查询到
+            o.setUserId(null);
+            o.setBizType(null);
+            o.setStatus(null);
+            o.setCreateTime(null);
+        });
+        memberBrokerageRecordMapper.insert(dbMemberBrokerageRecord);
+        // 测试 userId 不匹配
+        memberBrokerageRecordMapper.insert(cloneIgnoreId(dbMemberBrokerageRecord, o -> o.setUserId(null)));
+        // 测试 bizType 不匹配
+        memberBrokerageRecordMapper.insert(cloneIgnoreId(dbMemberBrokerageRecord, o -> o.setBizType(null)));
+        // 测试 status 不匹配
+        memberBrokerageRecordMapper.insert(cloneIgnoreId(dbMemberBrokerageRecord, o -> o.setStatus(null)));
+        // 测试 createTime 不匹配
+        memberBrokerageRecordMapper.insert(cloneIgnoreId(dbMemberBrokerageRecord, o -> o.setCreateTime(null)));
+        // 准备参数
+        MemberBrokerageRecordPageReqVO reqVO = new MemberBrokerageRecordPageReqVO();
+        reqVO.setUserId(null);
+        reqVO.setBizType(null);
+        reqVO.setStatus(null);
+        reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+        // 调用
+        PageResult<MemberBrokerageRecordDO> pageResult = memberBrokerageRecordService.getMemberBrokerageRecordPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbMemberBrokerageRecord, pageResult.getList().get(0));
+    }
+
+    @Test
+    public void testCalculateBrokerage_useSkuBrokeragePrice() {
+        // mock 数据
+        Integer payPrice = randomInteger();
+        Integer percent = randomInt(1, 101);
+        Integer skuBrokeragePrice = randomInt();
+        // 调用
+        int brokerage = memberBrokerageRecordService.calculateBrokerage(payPrice, percent, skuBrokeragePrice);
+        // 断言
+        assertEquals(brokerage, skuBrokeragePrice);
+    }
+
+    @Test
+    public void testCalculateBrokerage_usePercent() {
+        // mock 数据
+        Integer payPrice = randomInteger();
+        Integer percent = randomInt(1, 101);
+        Integer skuBrokeragePrice = randomEle(new Integer[]{0, null});
+        System.out.println("skuBrokeragePrice=" + skuBrokeragePrice);
+        // 调用
+        int brokerage = memberBrokerageRecordService.calculateBrokerage(payPrice, percent, skuBrokeragePrice);
+        // 断言
+        assertEquals(brokerage, NumberUtil.div(NumberUtil.mul(payPrice, percent), 100, 0, RoundingMode.DOWN).intValue());
+    }
+
+    @Test
+    public void testCalculateBrokerage_equalsZero() {
+        // mock 数据
+        Integer payPrice = null;
+        Integer percent = null;
+        Integer skuBrokeragePrice = null;
+        // 调用
+        int brokerage = memberBrokerageRecordService.calculateBrokerage(payPrice, percent, skuBrokeragePrice);
+        // 断言
+        assertEquals(brokerage, 0);
+    }
+}

+ 2 - 1
yudao-module-member/yudao-module-member-biz/src/test/resources/sql/clean.sql

@@ -2,4 +2,5 @@ DELETE FROM "member_user";
 DELETE FROM "member_address";
 DELETE FROM "member_tag";
 DELETE FROM "member_level";
-DELETE FROM "member_group";
+DELETE FROM "member_group";
+DELETE FROM "member_brokerage_record";

+ 22 - 1
yudao-module-member/yudao-module-member-biz/src/test/resources/sql/create_tables.sql

@@ -89,4 +89,25 @@ CREATE TABLE IF NOT EXISTS "member_group"
     "deleted"     bit      NOT NULL DEFAULT FALSE,
     "tenant_id"      bigint   not null default '0',
     PRIMARY KEY ("id")
-) COMMENT '用户分组';
+) COMMENT '用户分组';
+CREATE TABLE IF NOT EXISTS "member_brokerage_record"
+(
+    "id"            int      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"       bigint   NOT NULL,
+    "biz_id"        varchar  NOT NULL,
+    "biz_type"      varchar  NOT NULL,
+    "title"         varchar  NOT NULL,
+    "price"         int      NOT NULL,
+    "total_price"   int      NOT NULL,
+    "description"   varchar  NOT NULL,
+    "status"        varchar  NOT NULL,
+    "frozen_days"   int      NOT NULL,
+    "unfreeze_time" varchar,
+    "creator"       varchar           DEFAULT '',
+    "create_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"       varchar           DEFAULT '',
+    "update_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"       bit      NOT NULL DEFAULT FALSE,
+    "tenant_id"      bigint   not null default '0',
+    PRIMARY KEY ("id")
+) COMMENT '佣金记录';