分销架构
概述
- 商家可以选择已上架的商品参与分销并设置分销返佣比例
- 分销等级关系绑定仅支持三级,如:用户A -> 用户B -> 用户C
- 分销返佣仅支持二级返佣
- 用户想要参与分销返佣,必须先注册成为分销商
- 返佣金额是按照商品销售价格乘以商家设置的返佣比例进行计算的
- 当分销订单售后失效(无法申请退款)时,产生的佣金才会入账变为可提现金额
- 售后换货或补发商品产生的二次订单不会再次产生返利金额
数据库设计
分销商信息表
表名:es_distribution
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
member_id | 会员ID | bigint(20) | |
uname | 会员名称 | varchar(20) | |
name | 分销商姓名 | varchar(20) | 用户注册分销商时必须填写 |
phone | 分销商电话号码 | varchar(20) | 用户注册分销商时必须填写 |
avatar | 分销商头像 | varchar(255) | |
total_income | 可提现佣金 | decimal(20,2) | |
frozen_income | 提现冻结佣金 | decimal(20,2) | |
received_income | 已提现佣金 | decimal(20,2) | |
child_team_count_lv1 | 一级(下级)团队人数 | int(10) | |
child_team_count_lv2 | 二级(下下级)团队人数 | int(10) | |
child_order_money | 团队分销总金额 | decimal(20,2) | 以分销订单实际支付金额为准 |
child_order_count | 团队分销总订单数 | int(10) | 分销订单如果包含多个商品,订单数量也只+1 |
state | 状态 | int(10) | 1:正常,0:禁用 |
create_time | 分销商注册时间 | bigint(20) |
分销关系表
表名:es_distribution_relation
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
member_id | 会员ID | bigint(20) | |
member_name | 会员名称 | varchar(50) | |
member_avatar | 会员头像 | varchar(255) | |
agent_member_id_lv1 | 一级(上级)分销商ID | bigint(20) | |
agent_member_uname_lv1 | 一级(上级)分销商名称 | varchar(50) | |
agent_member_avatar_lv1 | 一级(上级)分销商头像 | varchar(255) | |
agent_member_id_lv2 | 二级(上上级)分销商ID | bigint(20) | 如果没有二级(上上级)分销商,此字段为空 |
agent_member_uname_lv2 | 二级(上上级)分销商名称 | varchar(50) | 如果没有二级(上上级)分销商,此字段为空 |
agent_member_avatar_lv2 | 二级(上上级)分销商头像 | varchar(255) | 如果没有二级(上上级)分销商,此字段为空 |
bind_time | 分销关系绑定时间 | bigint(20) |
分销商品表
表名:es_distribution_goods
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
goods_id | 商品ID | bigint(20) | |
join_flag | 是否参与分销 | int(10) | 1:参与,0:不参与 |
ratio_lv1 | 一级佣金比例 | decimal(20,2) | |
ratio_lv2 | 二级佣金比例 | decimal(20,2) |
分销订单表
表名:es_distribution_order_item
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
order_sn | 订单编号 | varchar(50) | |
goods_id | 商品ID | bigint(20) | |
sku_id | 商品skuID | bigint(20) | |
goods_num | 商品购买数量 | int(10) | |
goods_total_amount | 订单商品实付金额 | decimal(20,2) | |
commission_total | 分佣总金额 | decimal(20,2) | |
commission_status | 分佣状态 | varchar(20) | WAIT:待入账,CONFIRM:已入账,RETURN:已退回 |
create_time | 创建时间 | bigint(20) | |
return_commission_total | 已退回分佣总金额 | decimal(20,2) | |
member_id | 下单会员ID | bigint(20) | |
member_name | 下单会员名称 | varchar(50) | |
member_avatar | 下单会员头像 | varchar(255) | |
agent_member_id_lv1 | 一级(上级)分销商会员ID | bigint(20) | |
agent_member_name_lv1 | 一级(上级)分销商名称 | varchar(50) | |
agent_member_avatar_lv1 | 一级(上级)分销商头像 | varchar(255) | |
agent_member_id_lv2 | 二级(上上级)分销商会员ID | bigint(20) | |
agent_member_name_lv2 | 二级(上上级)分销商名称 | varchar(50) | |
agent_member_avatar_lv2 | 二级(上上级)分销商头像 | varchar(255) |
分销佣金表
表名:es_distribution_commission
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
order_sn | 订单编号 | varchar(50) | |
goods_id | 商品ID | bigint(20) | |
sku_id | 商品skuID | bigint(20) | |
agent_member_id | 分销商ID | bigint(20) | |
agent_member_name | 分销商名称 | varchar(50) | |
agent_member_avatar | 分销商头像 | varchar(255) | |
commission_level | 分佣等级 | int(10) | 一级、二级 |
grade_rebate | 分销比例 | decimal(20,2) | |
commission | 分佣金额 | decimal(20,2) | |
commission_status | 分佣状态 | varchar(20) | WAIT:待入账,CONFIRM:已入账,RETURN:已退回 |
create_time | 分佣时间 | bigint(20) | |
return_time | 退回时间 | bigint(20) | |
return_commission | 已退回分佣金额 | decimal(20,2) | |
goods_num | 商品购买数量 | int(10) | |
member_id | 下单会员ID | bigint(20) | |
member_name | 下单会员名称 | varchar(50) | |
member_avatar | 下单会员头像 | varchar(255) | |
goods_total_amount | 商品实付金额 | decimal(20,2) |
分销动态表
表名:es_distribution_event
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
event_type | 动态类型 | varchar(50) | COMMISSION:佣金,DISTRIBUTION:分销商,BIND:绑定关系,ACHIEVEMENT:分销业绩 |
member_id | 分销商会员ID | bigint(20) | |
member_name | 分销商名称 | varchar(50) | |
member_avatar | 分销商头像 | varchar(255) | |
content | 动态内容 | varchar(500) | |
oper_type | 操作人类型 | varchar(50) | USER:用户,ADMIN:管理员,SYSTEM:系统 |
oper_id | 操作人ID | bigint(20) | |
oper_name | 操作人名称 | varchar(50) | |
oper_avatar | 操作人头像 | varchar(255) | |
create_time | 动态时间 | bigint(20) |
分销佣金提现记录表
表名:es_distribution_withdraw_log
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
member_id | 提现会员ID | bigint(20) | |
member_name | 提现会员名称 | varchar(50) | |
state | 提现状态 | varchar(20) | WAIT_AUDIT:待审核,TRANSFERRING:转账中,AUDIT_PASS:待转账,AUDIT_FAIL:审核未通过,FINISHED:已完成,CLOSED:已关闭,TRANSFER_FAILED:提现失败 |
way | 提现方式 | varchar(20) | ALIPAY:支付宝,WECHAT:微信 |
price | 提现金额 | decimal(20,2) | |
withdraw_sn | 提现单号 | varchar(50) | |
wechat_out_batch_no | 商户系统内部的商家批次单号 | varchar(100) | 只有提现方式way=WECHAT(微信)时有值 |
open_id | 用户微信openId | varchar(100) | 只有提现方式way=WECHAT(微信)时有值 |
transfer_sn | 第三方转账单号 | varchar(100) | 微信批次单号或支付宝转账订单号 |
real_name | 收款方真实姓名 | varchar(50) | 只有提现方式way=ALIPAY(支付宝)时有值 |
account_info | 支付宝账号信息 | varchar(100) | 只有提现方式way=ALIPAY(支付宝)时有值 |
create_time | 申请时间 | bigint(20) | |
complete_time | 完成时间 | bigint(20) | |
fail_code | 提现失败编码 | varchar(100) | |
fail_reason | 提现失败原因 | varchar(255) | |
client_type | 申请提现客户端类型 | varchar(50) | PC:电脑浏览器,H5:手机浏览器,WECHAT_H5:微信内部浏览器,MINI:小程序,APP:手机应用程序 |
audit_remark | 审核备注 | varchar(255) | |
audit_operator | 审核人员名称 | varchar(50) | |
audit_operator_id | 审核人员ID | bigint(20) | |
transfer_remark | 转账备注 | varchar(255) | |
transfer_operator | 转账人员名称 | varchar(50) | |
transfer_operator_id | 转账人员ID | bigint(20) | |
close_remark | 关闭原因 | varchar(255) | |
close_operator | 关闭人员 | varchar(50) | |
close_operator_id | 关闭人员ID | bigint(20) |
分销佣金提现账户表
字段名 | 名称 | 数据类型 | 备注 |
---|---|---|---|
id | 主键ID | bigint(20) | |
member_id | 提现会员ID | bigint(20) | |
real_name | 真实姓名 | varchar(50) | |
account | 账号 | varchar(255) | |
create_time | 账号创建时间 | bigint(20) |
分销商注册
注册规则
- 必须要先注册成为商城会员
- 注册时需要填写分销商名称和手机号码
- 已经注册过的会员不能再次注册
时序图
重点步骤说明:
- 步骤1:调用注册分销商API时,必须要上传分销商名称和手机号这两个参数
- 步骤4:根据会员ID查询分销商表中是否已经存在了此会员数据,如果存在证明已经注册过,不存在证明没注册过
重点代码展示
/**
* 用户注册成为分销商
*
* @param name 姓名
* @param phone 电话
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void distributionRegister(String name, String phone) {
//获取登录用户信息
Buyer buyer = UserContext.getBuyer();
//获取会员详细信息
Member member = this.memberClient.getModel(buyer.getUid());
if (member == null) {
throw new ServiceException(DistributionErrorCode.E1000.code(), "未查询到当前登录的会员相关信息,不可注册");
}
//验证当前会员是否已经注册了分销商
Distribution distribution = this.getByMemberId(buyer.getUid());
if (distribution != null) {
throw new ServiceException(DistributionErrorCode.E1000.code(), "当前会员已经注册了分销商,不可重复注册");
}
//构建分销商信息
distribution = new Distribution();
distribution.setMemberId(buyer.getUid());
distribution.setUname(buyer.getUsername());
distribution.setName(name);
distribution.setPhone(phone);
distribution.setState(1);
distribution.setCreateTime(DateUtil.getDateline());
distribution.setAvatar(member.getFace());
this.distributionMapper.insert(distribution);
//新增分销动态信息 - 注册分销商动态
this.distributionEventManager.addDistributionEvent(buyer.getUid(), buyer.getUid(), EventTypeEnum.DISTRIBUTION, OperateTypeEnum.USER, "恭喜您成为分销商");
}
分销商品
流程图
说明:
- 只有上架销售的商品才可以参与分销
- 佣金比例设置分为:一级佣金比例和二级佣金比例,两种比例之和不能超过100%
重点代码展示
/**
* 设置商品的分销返佣比例
*
* @param distributionGoods 分销商品信息
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void addDistributionGoodsRebate(DistributionGoods distributionGoods) {
//查询商品是否已有分销返佣比例的设置信息,如果有则修改,没有就新增
DistributionGoods goodsDO = this.getByGoodsId(distributionGoods.getGoodsId());
//如果为空则新增数据
if (goodsDO == null) {
distributionGoodsMapper.insert(distributionGoods);
} else {
//更新数据
distributionGoods.setId(goodsDO.getId());
distributionGoodsMapper.updateById(distributionGoods);
}
}
分销关系绑定
流程图
重点步骤说明:
- 步骤2:分销商品的推广链接或海报,会携带分销商的会员ID信息(以短连接形式展现)
- 步骤3:分销商推广方式有很多种,比如说直接发送或者发布到朋友圈等等
- 步骤4:通过分销商推广链接或海报进入商城后,前端会第一时间将链接携带的分销商信息保存起来
- 步骤6:注册会员时,会将前端存储起来的分销商信息取出来,然后调用绑定分销关系API进行绑定操作
等级关系与返佣逻辑图
重点代码展示
/**
* 绑定分销关系
*
* @param parentId 上级分销商ID
* @return true | false
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Boolean bindDistributionRelation(Long parentId) {
//获取当前登录的会员信息
Buyer buyer = UserContext.getBuyer();
if (buyer.getUid().equals(parentId)) {
throw new ServiceException(DistributionErrorCode.E1000.code(), "分销关系绑定异常");
}
//获取当前登录的会员详细信息
Member member = this.memberClient.getModel(buyer.getUid());
if (member == null) {
throw new ServiceException(DistributionErrorCode.E1000.code(), "未查询到当前登录的会员信息,无法进行分销关系绑定");
}
//获取上级分销商信息
Distribution parentDistribution = distributionManager.getByMemberId(parentId);
if (parentDistribution == null) {
throw new ServiceException(DistributionErrorCode.E1000.code(), "未查询到上级分销商信息,无法进行分销关系绑定");
}
//查询当前登录会员是否已经存在分销绑定关系,如果存在则不允许再次绑定
DistributionRelation relation = this.getRelation(buyer.getUid());
if (relation != null) {
throw new ServiceException(DistributionErrorCode.E1000.code(), "当前会员已绑定分销商,不可进行二次绑定");
}
//绑定当前用户的分销关系
relation = new DistributionRelation();
relation.setMemberId(member.getMemberId());
relation.setMemberName(member.getUname());
relation.setMemberAvatar(member.getFace());
//设置上级分销商信息
relation.setAgentMemberIdLv1(parentId);
relation.setAgentMemberUnameLv1(parentDistribution.getName());
relation.setAgentMemberAvatarLv1(parentDistribution.getAvatar());
//查询上级分销商分销绑定关系的信息
DistributionRelation parentRelation = this.getRelation(parentId);
//如果上级分销商有绑定关系,需要取上级分销商的上级信息来设置当前用户的二级分销商信息
if (parentRelation != null && parentRelation.getAgentMemberIdLv1() != null) {
relation.setAgentMemberIdLv2(parentRelation.getAgentMemberIdLv1());
relation.setAgentMemberUnameLv2(parentRelation.getAgentMemberUnameLv1());
relation.setAgentMemberAvatarLv2(parentRelation.getAgentMemberAvatarLv1());
}
//设置分销关系绑定时间
relation.setBindTime(DateUtil.getDateline());
//分销关系信息入库
int result = this.distributionRelationMapper.insert(relation);
//入库成功,新增其它分销信息
if (result == 1) {
//增加各级团队人数
this.distributionManager.increaseTeamCount(relation.getAgentMemberIdLv1(), relation.getAgentMemberIdLv2());
//新增分销动态信息 - 绑定分销关系动态
this.distributionEventManager.addDistributionEvent(parentId, buyer.getUid(), EventTypeEnum.BIND, OperateTypeEnum.USER, "您已成为用户【" + buyer.getUsername() + "】的推荐人");
}
return result == 1;
}
分销佣金
概述
分销佣金产生的条件如下:
- 订单中包含分销商品
- 下单人绑定了分销关系
- 订单已付款
分销订单付款后会计算出分销商获得的佣金,此时的佣金为待入账状态,用户不可提现;只有当分销订单售后失效(无法申请退款)时,佣金状态才会变为已入账状态,此时可以对佣金进行提现操作。
佣金状态共有3种:待入账、已入账和已退回
时序图
重点步骤说明:
- 步骤4-5:订单付款成功后才会发生异步消息
- 步骤7:如果订单中不包含分销商品信息或者下单人没有绑定分销关系,则不计算分销佣金
- 步骤9:分销订单信息中包含的是佣金总额,系统会计算出每一级的分销金额,存放在佣金明细信息中
佣金计算公式
$$ 商品分销返佣金额 = 商品售价 * 返佣比例 $$
举例说明:现有分销商品一个,售价100元,一级佣金比例10%,二级佣金比例5%。
另有分销用户绑定关系如图:
用户购买分销商品(购买数量均为1)后,返现金额如下:
购买人 | 用户A佣金 | 用户B佣金 | 用户C佣金 | 用户D佣金 |
---|---|---|---|---|
分销用户A | 0元 | 0元 | 0元 | 0元 |
分销用户B | 100*10%=10元 | 0元 | 0元 | 0元 |
分销用户C | 100*5%=5元 | 100*10%=10元 | 0元 | 0元 |
分销用户D | 0元 | 100*5%=5元 | 100*10%=10元 | 0元 |
重点代码展示
新增分销订单业务方法
/**
* 新增分销订单数据
* 在分销商品订单付款后调用
*
* @param order 订单信息
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void addDistributionOrder(OrderDO order) {
//换货订单或者补发商品订单不产生返利金额
if (OrderTypeEnum.CHANGE.name().equals(order.getOrderType())
|| OrderTypeEnum.SUPPLY_AGAIN.name().equals(order.getOrderType())) {
return;
}
//查询订单中开启了分销返利的商品信息(此处是将订单中开启分销返利的商品进行拆分,每个商品都拆分成独立的对象)
List<DistributionOrderGoodsVO> goodsList = distributionGoodsManager.listOrderDistributionGoods(order.getSn());
//如果为null或者为空,则表示没有参与分销返利的商品,直接跳过分佣计算
if (goodsList.size() == 0) {
return;
}
//查询购买人的分销绑定关系信息
DistributionRelation relation = this.distributionRelationManager.getRelation(order.getMemberId());
//没有分销绑定关系,则直接跳过分佣计算
if (relation == null) {
return;
}
for (DistributionOrderGoodsVO orderGoods : goodsList) {
//分佣总金额
Double commissionTotal = 0D;
//如果下单会员存在一级(上级)分销关系,则计算一级(上级)分销返佣金额
if (relation.getAgentMemberIdLv1() != null) {
//一级分佣金额计算
DistributionCommission CommissionLv1 = distributionCommissionManager.addCommission(orderGoods, relation.getAgentMemberIdLv1(), 1, order.getMemberId());
commissionTotal = CurrencyUtil.add(commissionTotal, CommissionLv1.getCommission());
//新增分销商的分销订单金额
this.distributionManager.addDistributionOrderAmount(relation.getAgentMemberIdLv1(), orderGoods.getSubtotal());
}
if (relation.getAgentMemberIdLv2() != null) {
//二级分佣金额计算
DistributionCommission CommissionLv2 = distributionCommissionManager.addCommission(orderGoods, relation.getAgentMemberIdLv2(), 2, order.getMemberId());
commissionTotal = CurrencyUtil.add(commissionTotal, CommissionLv2.getCommission());
//新增分销商的分销订单金额
this.distributionManager.addDistributionOrderAmount(relation.getAgentMemberIdLv2(), orderGoods.getSubtotal());
}
//构建分销订单信息
DistributionOrderItem distributionOrderItem = new DistributionOrderItem();
//订单编号
distributionOrderItem.setOrderSn(orderGoods.getOrderSn());
//skuID
distributionOrderItem.setSkuId(orderGoods.getSkuId());
//商品ID
distributionOrderItem.setGoodsId(orderGoods.getGoodsId());
//商品购买数量
distributionOrderItem.setGoodsNum(orderGoods.getNum());
//订单商品结算金额
distributionOrderItem.setGoodsTotalAmount(orderGoods.getSubtotal());
//分佣总金额
distributionOrderItem.setCommissionTotal(commissionTotal);
//分佣状态
distributionOrderItem.setCommissionStatus(CommissionTypeEnum.WAIT.name());
//分佣时间
distributionOrderItem.setCreateTime(DateUtil.getDateline());
//下单会员ID
distributionOrderItem.setMemberId(order.getMemberId());
//下单会员名称
distributionOrderItem.setMemberName(relation.getMemberName());
//下单会员头像
distributionOrderItem.setMemberAvatar(relation.getMemberAvatar());
//一级(上级)分销商ID
distributionOrderItem.setAgentMemberIdLv1(relation.getAgentMemberIdLv1());
//一级(上级)分销商名称
distributionOrderItem.setAgentMemberNameLv1(relation.getAgentMemberUnameLv1());
//一级(上级)分销商头像
distributionOrderItem.setAgentMemberAvatarLv1(relation.getAgentMemberAvatarLv1());
//二级(上上级)分销商ID
distributionOrderItem.setAgentMemberIdLv2(relation.getAgentMemberIdLv2());
//二级(上上级)分销商名称
distributionOrderItem.setAgentMemberNameLv2(relation.getAgentMemberUnameLv2());
//二级(上上级)分销商头像
distributionOrderItem.setAgentMemberAvatarLv2(relation.getAgentMemberAvatarLv2());
//分销订单入库
distributionOrderItemMapper.insert(distributionOrderItem);
}
//如果下单会员存在一级(上级)分销关系,则增加一级分销商的分销订单数量
if (relation.getAgentMemberIdLv1() != null) {
//新增分销商的分销订单数量
this.distributionManager.addDistributionOrderNum(relation.getAgentMemberIdLv1(), 1);
}
//如果下单会员存在二级(上上级)分销关系,则增加二级分销商的分销订单数量
if (relation.getAgentMemberIdLv2() != null) {
//新增分销商的分销订单数量
this.distributionManager.addDistributionOrderNum(relation.getAgentMemberIdLv2(), 1);
}
}
新增佣金明细业务方法
/**
* 新增分销佣金信息
*
* @param orderGoods 订单分销返利商品信息
* @param agentMemberId 获得佣金的分销商会员ID
* @param level 分佣级别(1:一级(上级),2:二级(上上级))
* @param memberId 下单会员ID
* @return 分销佣金信息
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public DistributionCommission addCommission(DistributionOrderGoodsVO orderGoods, Long agentMemberId, Integer level, Long memberId) {
//分销佣金信息
DistributionCommission commissionDO = new DistributionCommission();
//获取分销商信息
Distribution distribution = this.distributionManager.getByMemberId(agentMemberId);
//设置获得佣金的分销商信息
commissionDO.setAgentMemberId(distribution.getMemberId());
commissionDO.setAgentMemberName(distribution.getName());
commissionDO.setAgentMemberAvatar(distribution.getAvatar());
//获取下单会员信息
Member member = this.memberClient.getModel(memberId);
//设置下单会员信息
commissionDO.setMemberId(member.getMemberId());
commissionDO.setMemberName(member.getUname());
commissionDO.setMemberAvatar(member.getFace());
//设置分销商品信息
commissionDO.setOrderSn(orderGoods.getOrderSn());
commissionDO.setGoodsId(orderGoods.getGoodsId());
commissionDO.setSkuId(orderGoods.getSkuId());
commissionDO.setGoodsNum(orderGoods.getNum());
commissionDO.setReturnCommission(0.00);
commissionDO.setGoodsTotalAmount(orderGoods.getSubtotal());
//设置佣金状态为:待入账
commissionDO.setCommissionStatus(CommissionTypeEnum.WAIT.name());
//设置佣金等级
commissionDO.setCommissionLevel(level);
//设置获得佣金的时间
commissionDO.setCreateTime(DateUtil.getDateline());
//获取分销商品的返佣比例
Double gradeRebate = 0D;
switch (level) {
case 1:
gradeRebate = orderGoods.getRatioLv1();
break;
case 2:
gradeRebate = orderGoods.getRatioLv2();
break;
default:
break;
}
//设置返佣比例
commissionDO.setGradeRebate(gradeRebate);
//计算佣金金额 = 订单商品实付总金额 * 返佣比例
Double commission = CurrencyUtil.mul(orderGoods.getSubtotal(), CurrencyUtil.div(gradeRebate, 100));
//设置佣金金额
commissionDO.setCommission(commission);
//分销佣金信息入库
distributionCommissionMapper.insert(commissionDO);
//新增分销动态信息 - 佣金动态
this.distributionEventManager.addDistributionEvent(agentMemberId, memberId, EventTypeEnum.COMMISSION, OperateTypeEnum.USER,
"编号为【" + orderGoods.getOrderSn() + "】的订单用户已支付成功, 您有" + commission + "元佣金待入账");
//新增分销动态信息 - 业绩动态
this.distributionEventManager.addDistributionEvent(agentMemberId, memberId, EventTypeEnum.COMMISSION, OperateTypeEnum.USER,
"用户【" + member.getUname() + "】购买了商品【" + orderGoods.getName() + "】,订单编号为【" + orderGoods.getOrderSn() + "】,分销业绩 +" + commission + "元,分销订单 +1");
return commissionDO;
}
分销佣金状态变更
佣金状态共有3种:待入账、已入账和已退回
每种状态变化的时机如下:
待入账 | 已入账 | 已退回 |
---|---|---|
分销订单付款产生分销佣金时 | 分销订单售后失效(无法进行退款)时 | 分销订单申请售后退款完成时 |
重点说明:
订单售后失效是通过系统每小时定时任务自动处理的,而是否可以设置为售后失效是根据平台管理端 -> 设置 -> 系统参数 -> 系统设置 -> 订单设置中,设置的订单售后自动失效时间来判定的
如果用户申请的是部分佣金售后退款,那么佣金状态不会变成已退回
例1:一条分销订单中包含A和B两种分销商品,购买数量是各1件,其中A申请了售后退款,完成后分销订单的佣金状态不会变为已退回
例2:一条分销订单中只包含分销商品A,但是购买数量是2件,其中1件申请了退款,完成后分销订单的佣金状态不会变为已退回
佣金提现
概述
- 提现申请提交后,申请提现的金额会暂时被冻结,等平台审核通过并转账成功后才会解冻并发放
- 单次提现金额最低1元,最高500元
- 每日可提现次数不限,但是最多提现金额不得超过20000元
- 分销佣金提现方式有两种:支付宝和微信。
- 选中提现方式为支付宝时,需要填写支付宝账号和真实姓名
- PC端和手机浏览器只支持支付宝提现,小程序和微信内部浏览器支付宝和微信均支持
tip
提现流程图
- 当用户提交提现申请时,会扣减用户可提现金额,增加用户的冻结提现金额
- 当平台审核不通过时,会恢复用户可提现金额,扣减用户的冻结提现金额
- 当平台转账成功时,会扣减用户的冻结提现金额,增加用户的已提现金额
- 当平台转账失败时,会恢复用户可提现金额,扣减用户的冻结提现金额
时序图
重点步骤说明:
- 步骤3-4:只有状态为待审核的提现申请可以进行审核操作
- 步骤5-6:只有状态为待转账的提现申请可以进行转账操作
- 步骤8:提现方式如果是支付宝,则调用支付宝单笔转账接口;如果是微信,则调用微信商家转账到零钱接口
- 步骤11-13:只有当转账接口返回的转账状态是转账处理中时才会触发查询操作
重点代码展示
提现申请业务方法
/**
* 分销佣金申请提现
*
* @param withdrawApply 申请提现参数
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void withdrawApply(WithdrawApplyDTO withdrawApply) {
//校验申请佣金提现参数信息
this.checkApplyParams(withdrawApply);
//获取当前申请提现的会员信息
Buyer buyer = UserContext.getBuyer();
//获取分销商信息
Distribution distribution = this.distributionManager.getByMemberId(buyer.getUid());
//新增提现记录
DistributionWithdrawLog log = new DistributionWithdrawLog();
BeanUtil.copyProperties(withdrawApply, log);
//设置提现分销商会员信息
log.setMemberId(distribution.getMemberId());
log.setMemberName(distribution.getName());
//设置申请提现时间
log.setCreateTime(DateUtil.getDateline());
//设置提现申请状态-默认为待审核状态
log.setState(WithdrawStatusEnum.WAIT_AUDIT.value());
//设置提现申请单号
String withdrawSn = "WS" + snCreator.create(SubCode.WITHDRAW);
log.setWithdrawSn(withdrawSn);
//提现记录信息入库
this.distributionWithdrawLogMapper.insert(log);
//如果提现方式为支付宝,需要当前用户的支付宝提现账户是否已存在,如果不存在则新增到账户信息中
if (WithdrawWayEnum.ALIPAY.value().equals(withdrawApply.getWay())) {
this.addAccount(log.getMemberId(), log.getRealName(), log.getAccountInfo());
}
//扣减分销商可提现即可,增加冻结提现金额
List<DistributionWithdrawLog> logList = new ArrayList<>();
logList.add(log);
this.distributionManager.updateWithdrawAmount(logList, 0);
}
WithdrawApplyDTO.java实体参数内容如下:
public class WithdrawApplyDTO {
/**
* 提现方式
* @see WithdrawWayEnum
*/
@Schema(name = "way", description = "提现方式", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = "ALIPAY,WECHAT")
@NotEmpty(message = "提现方式不能为空")
private String way;
/**
* 提现金额
*/
@Schema(name = "price", description = "提现金额", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "提现金额不能为空")
@AmountRange(max = 500, min = 1, message = "单次提现金额范围:1元 ~ 500元")
private Double price;
/**
* 客户端类型
* @see ClientTypeEnum
*/
@Schema(name = "client_type", description = "客户端类型", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = "PC,H5,WECHAT_H5,APP,MINI")
@NotEmpty(message = "客户端类型不能为空")
private String clientType;
/**
* 收款方真实姓名
* 当提现方式way=ALIPAY(支付宝)时此值必填
*/
@Schema(name = "real_name", description = "收款方真实姓名")
private String realName;
/**
* 支付宝账号信息
* 当提现方式way=ALIPAY(支付宝)时此值必填
*/
@Schema(name = "account_info", description = "支付宝账号信息")
private String accountInfo;
/**
* 用户openId
* 当提现方式way=WECHAT(微信)时此值必填
*/
@Schema(name = "open_id", description = "用户openId")
private String openId;
//get&set 略
}
注意:
- 当提现方式为支付宝时,必须要填写支付宝账户信息(账户和真实姓名)
- 当提现方式为微信时,必须要获取用户授权并得到用户的微信openId
审核提现申请业务方法
/**
* 批量审核提现申请
*
* @param withdrawAuditDTO 审核参数
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchAudit(WithdrawAuditDTO withdrawAuditDTO) {
//校验审核提现申请参数信息
this.checkAuditParams(withdrawAuditDTO);
//获取当前管理员信息
Admin admin = AdminUserContext.getAdmin();
//修改提现记录信息
new LambdaUpdateChainWrapper<>(distributionWithdrawLogMapper)
//设置状态
.set(DistributionWithdrawLog::getState, withdrawAuditDTO.getAuditStatus())
//设置审核操作人
.set(DistributionWithdrawLog::getAuditOperator, admin.getUsername())
//设置审核操作人ID
.set(DistributionWithdrawLog::getAuditOperatorId, admin.getUid())
//设置审核备注
.set(StringUtil.notEmpty(withdrawAuditDTO.getRemark()), DistributionWithdrawLog::getAuditRemark, withdrawAuditDTO.getRemark())
//根据提现记录主键ID进行修改
.in(DistributionWithdrawLog::getId, Arrays.asList(withdrawAuditDTO.getIds()))
//修改
.update();
//如果是审核未通过,需要发送提现申请审核不通过消息
if (WithdrawStatusEnum.AUDIT_FAIL.value().equals(withdrawAuditDTO.getAuditStatus())) {
DistributionWithdrawMsg msg = new DistributionWithdrawMsg(withdrawAuditDTO.getIds(), DistributionWithdrawMsg.AUDIT_FAIL);
this.messageSender.send(msg);
}
}
提现申请转账业务方法
/**
* 提现申请单条转账
* @param id 主键ID
* @param remark 转账备注
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void singleTransfer(Long id, String remark) {
//根据主键ID获取提现申请信息
DistributionWithdrawLog log = this.getById(id);
if (log == null) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "提现记录不存在");
}
if (!WithdrawStatusEnum.AUDIT_PASS.name().equals(log.getState())) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "只有状态为审核通过的提现申请可以进行转账操作");
}
//获取当前管理员信息
Admin admin = AdminUserContext.getAdmin();
//判断提现方式,不同的提现方式调用不同的接口进行转账
if (WithdrawWayEnum.WECHAT.name().equals(log.getWay())) {
//构建微信转账参数
List<DistributionWithdrawLog> logList = new ArrayList<>();
logList.add(log);
WechatTransferParam param = this.buildWechatTransferParam(logList);
//设置申请提现客户端类型
param.setClientType(ClientTypeEnum.valueOf(log.getClientType()));
//调用微信批量转账接口
Map map = this.merchantTransferClient.wechatBatchTransfer(param);
//判断转账结果
String status = map.get("status").toString();
if (WithdrawStatusEnum.TRANSFER_FAILED.name().equals(status)) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "调用微信商户转账到零钱接口失败,失败原因:" + map.get("error_message"));
}
//设置转账批次编号
log.setWechatOutBatchNo(param.getOutBatchNo());
} else if (WithdrawWayEnum.ALIPAY.name().equals(log.getWay())) {
//构建支付宝转账参数
AliPayTransferParam param = new AliPayTransferParam();
param.setOutBizNo(log.getWithdrawSn());
param.setTransAmount(log.getPrice());
param.setAccount(log.getAccountInfo());
param.setRealName(log.getRealName());
param.setClientType(ClientTypeEnum.valueOf(log.getClientType()));
//调用支付宝转账接口
Map map = this.merchantTransferClient.alipayTransfer(param);
//判断转账结果
String status = map.get("status").toString();
if (WithdrawStatusEnum.TRANSFER_FAILED.name().equals(status)) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "调用支付宝商户转账到支付宝账户接口失败,失败原因:" + map.get("fail_reason"));
}
//设置支付宝转账订单号
log.setTransferSn(map.get("transfer_sn").toString());
}
//设置其他信息
log.setState(WithdrawStatusEnum.TRANSFERRING.value());
log.setTransferOperator(admin.getUsername());
log.setTransferOperatorId(admin.getUid());
log.setTransferRemark(remark);
//修改提现申请信息
this.distributionWithdrawLogMapper.updateById(log);
//发送转账操作成功消息(操作成功不代表转账已成功)
DistributionWithdrawMsg msg = new DistributionWithdrawMsg(new Long[]{id}, DistributionWithdrawMsg.TRANSFER_OPERATE_SUCCESS, WithdrawWayEnum.valueOf(log.getWay()));
this.messageSender.send(msg);
}
转账分为支付宝转账和微信转账,要根据提现方式来判断具体调用哪一种接口
支付宝单笔转账接口对接代码如下:
/**
* 支付宝商户转账
*
* @param param 转账参数
* @return
*/
public Map transfer(AliPayTransferParam param) {
//返回结果
Map result = new HashMap();
try {
//根据客户端类型创建AlipayClient实例
AlipayClient alipayClient = this.buildClient(param.getClientType());
//设置请求参数
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
//构建参数信息
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
//设置转账单号
model.setOutBizNo(param.getOutBizNo());
//设置业务场景
model.setBizScene(DIRECT_TRANSFER);
//设置收款人信息
Participant payeeInfo = new Participant();
//设置收款方的标识类型
payeeInfo.setIdentityType(IDENTITY_TYPE);
//设置收款人支付宝账号
payeeInfo.setIdentity(param.getAccount());
//设置收款人真实姓名
payeeInfo.setName(param.getRealName());
model.setPayeeInfo(payeeInfo);
//设置转账金额
model.setTransAmount(param.getTransAmount().toString());
//设置产品码
model.setProductCode(TRANS_ACCOUNT_NO_PWD);
//设置转账业务的标题
model.setOrderTitle("分销佣金转账");
//设置请求参数
request.setBizModel(model);
//调用支付宝单笔转账接口
AlipayFundTransUniTransferResponse response = alipayClient.certificateExecute(request);
logger.info("调用支付宝单笔转账接口返回结果:" + response.getBody());
//判断接口是否调用成功
if (response.isSuccess()) {
//设置支付宝转账单号
result.put("transfer_sn", response.getOrderId());
//获取转账状态
String status = response.getStatus();
/**
* 转账状态只要不为FAIL,都将状态设置为转账中(TRANSFERRING),
* 后面利用查询接口去查询转账是否成功
*/
if (FAIL.equals(status)) {
result.put("status", WithdrawStatusEnum.TRANSFER_FAILED.name());
result.put("fail_reason", response.getSubMsg());
this.logger.error("调用支付宝单笔转账接口失败,失败信息为:" + "code["+response.getCode()+"],msg["+response.getMsg()+"]," +
"sub_code["+response.getSubCode()+"],sub_msg["+response.getSubMsg()+"]");
} else {
result.put("status", WithdrawStatusEnum.TRANSFERRING.name());
}
} else {
result.put("status", WithdrawStatusEnum.TRANSFER_FAILED.name());
result.put("fail_reason", response.getSubMsg());
this.logger.error("调用支付宝单笔转账接口失败,失败信息为:" + "code["+response.getCode()+"],msg["+response.getMsg()+"]," +
"sub_code["+response.getSubCode()+"],sub_msg["+response.getSubMsg()+"]");
}
return result;
} catch (Exception e) {
logger.error("调用支付宝单笔转账接口报错:", e);
result.put("fail_reason", e.getMessage());
}
result.put("status", WithdrawStatusEnum.TRANSFER_FAILED.name());
return result;
}
微信商家转账到零钱接口对接代码如下:
/**
* 微信商户转账
*
* @param param 转账参数
* @return
*/
public Map transfer(WechatTransferParam param) {
//返回结果
Map result = new HashMap();
try {
//根据支付客户端类型来获取微信支付相关配置信息
WechatPayConfig wechatPayConfig = this.getWechatPayConfig(param.getClientType());
//初始化商户配置
RSAConfig config = this.buildWechatPayRSAConfig(wechatPayConfig);
//初始化服务
TransferBatchService service = new TransferBatchService.Builder().config(config).build();
//设置请求参数
InitiateBatchTransferRequest request = new InitiateBatchTransferRequest();
//设置应用ID
request.setAppid(wechatPayConfig.getAppId());
//设置商家批次单号
request.setOutBatchNo(param.getOutBatchNo());
//设置批次名称
request.setBatchName("分销佣金提现");
//设置批次备注
request.setBatchRemark("分销佣金提现");
//设置转账总金额(以分为单位)
Double totalAmount = param.getTotalAmount();
//由于接口参数规则,需要把金额转换为以分为单位的整数
Long totalFen = StringUtil.toLong(CurrencyUtil.toFen(totalAmount), 0);
request.setTotalAmount(totalFen);
//设置转账总笔数(默认单笔转账)
request.setTotalNum(param.getTotalNum());
//设置转账明细信息
List<TransferDetailInput> transferDetailList = new ArrayList<>();
for (WechatTransferItem wechatTransferItem : param.getItemList()) {
TransferDetailInput input = new TransferDetailInput();
//设置收款用户openid
input.setOpenid(wechatTransferItem.getOpenId());
//设置商家明细单号
input.setOutDetailNo(wechatTransferItem.getOutDetailNo());
//设置转账金额(由于接口参数规则,需要把金额转换为以分为单位的整数)
Long transferAmount = StringUtil.toLong(CurrencyUtil.toFen(wechatTransferItem.getTransferAmount()), 0);
input.setTransferAmount(transferAmount);
//设置转账备注
input.setTransferRemark("分销佣金提现");
transferDetailList.add(input);
}
request.setTransferDetailList(transferDetailList);
//设置转账场景ID(1002代表分销返佣,需要设置为默认场景)
request.setTransferSceneId("1002");
//调用微信发起商家转账接口
InitiateBatchTransferResponse response = service.initiateBatchTransfer(request);
logger.info("调用微信商家转账到零钱接口结果为:" + response.toString());
//设置返回结果信息
result.put("status", WithdrawStatusEnum.TRANSFERRING.name());
result.put("out_batch_no", response.getOutBatchNo());
result.put("batch_id", response.getBatchId());
return result;
} catch (HttpException e) {
// 发送HTTP请求失败
this.logger.error("调用微信商家转账到零钱接口错误", e);
result.put("error_message", e.getMessage());
} catch (ServiceException e) {
// 服务返回状态小于200或大于等于300,例如500
this.logger.error("调用微信商家转账到零钱接口错误", e);
result.put("error_message", e.getMessage());
} catch (MalformedMessageException e) {
// 服务返回成功,返回体类型不合法,或者解析返回体失败
this.logger.error("调用微信商家转账到零钱接口错误", e);
result.put("error_message", e.getMessage());
} catch (Exception e) {
this.logger.error("调用微信商家转账到零钱接口错误", e);
result.put("error_message", e.getMessage());
}
result.put("status", WithdrawStatusEnum.TRANSFER_FAILED.name());
return result;
}
关闭提现申请业务方法
当平台发现提现申请有问题无法进行转账操作时,可以进关闭操作
tip
只有状态为待转账的提现申请可以被关闭,并且关闭原因必须填写
/**
* 批量关闭提现申请
*
* @param withdrawAuditDTO 关闭参数
*/
@Override
@Transactional(value = "distributionTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchClose(WithdrawAuditDTO withdrawAuditDTO) {
//获取要关闭的提现申请主键ID集合
Long[] ids = withdrawAuditDTO.getIds();
if (ids == null || ids.length == 0) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "请选择要关闭的提现申请记录");
}
//关闭原因必须填写
if (StringUtil.isEmpty(withdrawAuditDTO.getRemark())) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "请填写关闭原因");
}
if (MIN_LENGTH > withdrawAuditDTO.getRemark().length() || MAX_LENGTH < withdrawAuditDTO.getRemark().length()) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "关闭原因需要输入2-200个字符");
}
//获取要关闭的提现申请信息集合
List<DistributionWithdrawLog> logList = this.listByIds(Arrays.asList(ids), WithdrawStatusEnum.AUDIT_PASS.value(), null);
if (ids.length != logList.size()) {
throw new DistributionException(DistributionErrorCode.E1006.code(), "只有状态为待转账的提现申请可以进行关闭操作,请仔细核对后再进行关闭操作");
}
//获取当前管理员信息
Admin admin = AdminUserContext.getAdmin();
//修改提现记录信息
new LambdaUpdateChainWrapper<>(distributionWithdrawLogMapper)
//设置状态为已关闭
.set(DistributionWithdrawLog::getState, WithdrawStatusEnum.CLOSED.value())
//设置转账操作人
.set(DistributionWithdrawLog::getCloseOperator, admin.getUsername())
//设置转账操作人ID
.set(DistributionWithdrawLog::getCloseOperatorId, admin.getUid())
//设置转账备注
.set(StringUtil.notEmpty(withdrawAuditDTO.getRemark()), DistributionWithdrawLog::getCloseRemark, withdrawAuditDTO.getRemark())
//根据提现记录主键ID进行修改
.in(DistributionWithdrawLog::getId, Arrays.asList(withdrawAuditDTO.getIds()))
//修改
.update();
//发送提现申请关闭消息
DistributionWithdrawMsg msg = new DistributionWithdrawMsg(withdrawAuditDTO.getIds(), DistributionWithdrawMsg.CLOSED);
this.messageSender.send(msg);
}