跳到主要内容

分销架构

概述

  1. 商家可以选择已上架的商品参与分销并设置分销返佣比例
  2. 分销等级关系绑定仅支持三级,如:用户A -> 用户B -> 用户C
  3. 分销返佣仅支持二级返佣
  4. 用户想要参与分销返佣,必须先注册成为分销商
  5. 返佣金额是按照商品销售价格乘以商家设置的返佣比例进行计算的
  6. 当分销订单售后失效(无法申请退款)时,产生的佣金才会入账变为可提现金额
  7. 售后换货或补发商品产生的二次订单不会再次产生返利金额

数据库设计

分销商信息表

表名:es_distribution

字段名名称数据类型备注
id主键IDbigint(20)
member_id会员IDbigint(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主键IDbigint(20)
member_id会员IDbigint(20)
member_name会员名称varchar(50)
member_avatar会员头像varchar(255)
agent_member_id_lv1一级(上级)分销商IDbigint(20)
agent_member_uname_lv1一级(上级)分销商名称varchar(50)
agent_member_avatar_lv1一级(上级)分销商头像varchar(255)
agent_member_id_lv2二级(上上级)分销商IDbigint(20)如果没有二级(上上级)分销商,此字段为空
agent_member_uname_lv2二级(上上级)分销商名称varchar(50)如果没有二级(上上级)分销商,此字段为空
agent_member_avatar_lv2二级(上上级)分销商头像varchar(255)如果没有二级(上上级)分销商,此字段为空
bind_time分销关系绑定时间bigint(20)

分销商品表

表名:es_distribution_goods

字段名名称数据类型备注
id主键IDbigint(20)
goods_id商品IDbigint(20)
join_flag是否参与分销int(10)1:参与,0:不参与
ratio_lv1一级佣金比例decimal(20,2)
ratio_lv2二级佣金比例decimal(20,2)

分销订单表

表名:es_distribution_order_item

字段名名称数据类型备注
id主键IDbigint(20)
order_sn订单编号varchar(50)
goods_id商品IDbigint(20)
sku_id商品skuIDbigint(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下单会员IDbigint(20)
member_name下单会员名称varchar(50)
member_avatar下单会员头像varchar(255)
agent_member_id_lv1一级(上级)分销商会员IDbigint(20)
agent_member_name_lv1一级(上级)分销商名称varchar(50)
agent_member_avatar_lv1一级(上级)分销商头像varchar(255)
agent_member_id_lv2二级(上上级)分销商会员IDbigint(20)
agent_member_name_lv2二级(上上级)分销商名称varchar(50)
agent_member_avatar_lv2二级(上上级)分销商头像varchar(255)

分销佣金表

表名:es_distribution_commission

字段名名称数据类型备注
id主键IDbigint(20)
order_sn订单编号varchar(50)
goods_id商品IDbigint(20)
sku_id商品skuIDbigint(20)
agent_member_id分销商IDbigint(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下单会员IDbigint(20)
member_name下单会员名称varchar(50)
member_avatar下单会员头像varchar(255)
goods_total_amount商品实付金额decimal(20,2)

分销动态表

表名:es_distribution_event

字段名名称数据类型备注
id主键IDbigint(20)
event_type动态类型varchar(50)COMMISSION:佣金,DISTRIBUTION:分销商,BIND:绑定关系,ACHIEVEMENT:分销业绩
member_id分销商会员IDbigint(20)
member_name分销商名称varchar(50)
member_avatar分销商头像varchar(255)
content动态内容varchar(500)
oper_type操作人类型varchar(50)USER:用户,ADMIN:管理员,SYSTEM:系统
oper_id操作人IDbigint(20)
oper_name操作人名称varchar(50)
oper_avatar操作人头像varchar(255)
create_time动态时间bigint(20)

分销佣金提现记录表

表名:es_distribution_withdraw_log

字段名名称数据类型备注
id主键IDbigint(20)
member_id提现会员IDbigint(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用户微信openIdvarchar(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审核人员IDbigint(20)
transfer_remark转账备注varchar(255)
transfer_operator转账人员名称varchar(50)
transfer_operator_id转账人员IDbigint(20)
close_remark关闭原因varchar(255)
close_operator关闭人员varchar(50)
close_operator_id关闭人员IDbigint(20)

分销佣金提现账户表

字段名名称数据类型备注
id主键IDbigint(20)
member_id提现会员IDbigint(20)
real_name真实姓名varchar(50)
account账号varchar(255)
create_time账号创建时间bigint(20)

分销商注册

注册规则

  1. 必须要先注册成为商城会员
  2. 注册时需要填写分销商名称和手机号码
  3. 已经注册过的会员不能再次注册

时序图

image-20230727145352831

重点步骤说明:

  • 步骤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, "恭喜您成为分销商");
}

分销商品

流程图

image-20230727151232016

说明:

  • 只有上架销售的商品才可以参与分销
  • 佣金比例设置分为:一级佣金比例和二级佣金比例,两种比例之和不能超过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);
}
}

分销关系绑定

流程图

image-20230727160852760

重点步骤说明:

  • 步骤2:分销商品的推广链接或海报,会携带分销商的会员ID信息(以短连接形式展现)
  • 步骤3:分销商推广方式有很多种,比如说直接发送或者发布到朋友圈等等
  • 步骤4:通过分销商推广链接或海报进入商城后,前端会第一时间将链接携带的分销商信息保存起来
  • 步骤6:注册会员时,会将前端存储起来的分销商信息取出来,然后调用绑定分销关系API进行绑定操作

等级关系与返佣逻辑图

image-20230725164457773

重点代码展示

/**
* 绑定分销关系
*
* @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;
}

分销佣金

概述

  1. 分销佣金产生的条件如下:

    • 订单中包含分销商品
    • 下单人绑定了分销关系
    • 订单已付款
  2. 分销订单付款后会计算出分销商获得的佣金,此时的佣金为待入账状态,用户不可提现;只有当分销订单售后失效(无法申请退款)时,佣金状态才会变为已入账状态,此时可以对佣金进行提现操作。

    佣金状态共有3种:待入账、已入账和已退回

时序图

image-20230727173249863

重点步骤说明:

  • 步骤4-5:订单付款成功后才会发生异步消息
  • 步骤7:如果订单中不包含分销商品信息或者下单人没有绑定分销关系,则不计算分销佣金
  • 步骤9:分销订单信息中包含的是佣金总额,系统会计算出每一级的分销金额,存放在佣金明细信息中

佣金计算公式

$$ 商品分销返佣金额 = 商品售价 * 返佣比例 $$

举例说明:现有分销商品一个,售价100元,一级佣金比例10%,二级佣金比例5%。

另有分销用户绑定关系如图:

image-20230725171435192

用户购买分销商品(购买数量均为1)后,返现金额如下:

购买人用户A佣金用户B佣金用户C佣金用户D佣金
分销用户A0元0元0元0元
分销用户B100*10%=10元0元0元0元
分销用户C100*5%=5元100*10%=10元0元0元
分销用户D0元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. 提现申请提交后,申请提现的金额会暂时被冻结,等平台审核通过并转账成功后才会解冻并发放
  2. 单次提现金额最低1元,最高500元
  3. 每日可提现次数不限,但是最多提现金额不得超过20000元
  4. 分销佣金提现方式有两种:支付宝和微信。
  5. 选中提现方式为支付宝时,需要填写支付宝账号和真实姓名
  6. PC端和手机浏览器只支持支付宝提现,小程序和微信内部浏览器支付宝和微信均支持
tip

微信提现需要开通微信支付的《商家转账到零钱》产品,由于此产品开通规则中仅支持一级分销,导致产品申请开通失败。因此现Javashop系统内虽然对接了微信《商家转账到零钱》相关接口,但是无法将佣金实际提现到账。

如果有客户坚持要使用微信提现功能,需要满足以下其中之一的条件:

1、在保证系统分销结构无变化的情况下开通微信支付的《商家转账到零钱》产品

2、修改系统分销等级结构,将三级改为二级(用户A -> 用户B,此种模式在微信规则中就是一级分销),然后再去开通微信支付的《商家转账到零钱》产品

提现流程图

image-20230725171435192

  1. 当用户提交提现申请时,会扣减用户可提现金额,增加用户的冻结提现金额
  2. 当平台审核不通过时,会恢复用户可提现金额,扣减用户的冻结提现金额
  3. 当平台转账成功时,会扣减用户的冻结提现金额,增加用户的已提现金额
  4. 当平台转账失败时,会恢复用户可提现金额,扣减用户的冻结提现金额

时序图

image-20230728103855901

重点步骤说明:

  • 步骤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 略
}

注意:

  1. 当提现方式为支付宝时,必须要填写支付宝账户信息(账户和真实姓名)
  2. 当提现方式为微信时,必须要获取用户授权并得到用户的微信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);
}