feat 分账限制, 优化一系列分账相关的逻辑

This commit is contained in:
DaxPay
2024-05-29 18:19:10 +08:00
parent a773c520d1
commit ffa0232f20
28 changed files with 141 additions and 80 deletions

View File

@@ -14,12 +14,12 @@
- [x] 分账查询
- [x] 分账完结
- [x] 分账同步
- [ ] 保存分账同步记录
- [x] 保存分账同步记录
- [ ] SDK支持分账相关接口
- [ ] 分账回调处理
- [x] 分账通知发送功能
- [x] 分账支持手动和自动分账两种
- [ ] 金额过小不进行分账, 增加新状态, 金额小于0.01元直接忽略
- [x] 金额过小不进行分账, 增加新状态, 金额小于0.01元直接忽略
- [x] 优化签名注解和上下文初始化注解切面
- [x] 对账差异单, 数据不一致处理异常, 本地待对账订单类型记录错误
- [x] 订单超时任务注册任务错误id改为订单号

View File

@@ -45,7 +45,7 @@
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<logback-classic.version>1.2.7</logback-classic.version>
<hutool.version>5.8.25</hutool.version>
<hutool.version>5.8.27</hutool.version>
<lombok.version>1.18.30</lombok.version>
<junit.version>4.13.2</junit.version>
</properties>

View File

@@ -18,9 +18,6 @@ public class AllocReceiverModel {
/** 分账接收方编号, 需要保证唯一 */
private String receiverNo;
/** 账号别名 */
private String name;
/**
* 所属通道
* @see PayChannelEnum

View File

@@ -25,9 +25,6 @@ public class AllocReceiverAddParam extends DaxPayRequest<AllocReceiverAddModel>
/** 接收者编号 */
private String receiverNo;
/** 账号别名 */
private String name;
/**
* 所属通道
* @see PayChannelEnum

View File

@@ -44,7 +44,6 @@ public class AllocationReceiverTest {
param.setRelationType("user");
param.setRelationName("测试");
param.setReceiverNo("123456");
param.setName("测试");
param.setReceiverType("user");
param.setReceiverAccount("123456");
DaxPayResult<AllocReceiverAddModel> execute = DaxPayKit.execute(param);

View File

@@ -15,7 +15,7 @@ import cn.daxpay.single.service.core.order.pay.service.PayOrderQueryService;
import cn.daxpay.single.service.core.payment.allocation.service.AllocationService;
import cn.daxpay.single.service.core.payment.close.service.PayCloseService;
import cn.daxpay.single.service.core.payment.sync.service.PaySyncService;
import cn.daxpay.single.service.dto.order.pay.PayOrderDetailDto;
import cn.daxpay.single.service.dto.order.pay.PayOrderInfoDto;
import cn.daxpay.single.service.dto.order.pay.PayOrderDto;
import cn.daxpay.single.service.dto.order.pay.PayOrderExtraDto;
import cn.daxpay.single.service.param.order.PayOrderQuery;
@@ -63,11 +63,11 @@ public class PayOrderController {
@Operation(summary = "查询订单详情")
@GetMapping("/findByOrderNo")
public ResResult<PayOrderDetailDto> findByOrderNo(String orderNo){
public ResResult<PayOrderInfoDto> findByOrderNo(String orderNo){
PayOrderDto order = queryService.findByOrderNo(orderNo)
.map(PayOrder::toDto)
.orElseThrow(() -> new DataNotExistException("支付订单不存在"));
PayOrderDetailDto detailDto=new PayOrderDetailDto();
PayOrderInfoDto detailDto=new PayOrderInfoDto();
detailDto.setPayOrder(order);
detailDto.setPayOrderExtra(payOrderExtraService.findById(order.getId()));
return Res.ok(detailDto);

View File

@@ -15,6 +15,8 @@ public enum AllocDetailResultEnum {
PENDING("pending", "待分账"),
SUCCESS("success", "分账成功"),
FAIL("fail", "分账失败"),
/** 金额为0时不进行分账 */
IGNORE("ignore", "忽略分账"),
;
private final String code;

View File

@@ -17,6 +17,8 @@ public enum AllocOrderStatusEnum {
ALLOCATION_FAILED("allocation_failed", "分账失败"),
FINISH("finish", "完结"),
FINISH_FAILED("finish_failed", "完结失败"),
/** 忽略, 如果金额为0, 不进行分账 */
IGNORE("ignore", "忽略"),
;
final String code;

View File

@@ -9,6 +9,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
/**
* 分账接收者添加参数
* @author xxm
@@ -21,17 +23,15 @@ import lombok.experimental.Accessors;
public class AllocReceiverAddParam extends PaymentCommonParam {
@Schema(description = "接收者编号, 需要保证唯一")
@NotEmpty(message = "接收者编号必填")
private String receiverNo;
/** 账号别名 */
@Schema(description = "账号别名")
private String name;
/**
* 所属通道
* @see PayChannelEnum
*/
@Schema(description = "所属通道")
@NotEmpty(message = "所属通道必填")
private String channel;
/**
@@ -39,10 +39,12 @@ public class AllocReceiverAddParam extends PaymentCommonParam {
* @see AllocReceiverTypeEnum
*/
@Schema(description = "分账接收方类型")
@NotEmpty(message = "分账接收方类型必填")
private String receiverType;
/** 接收方账号 */
@Schema(description = "接收方账号")
@NotEmpty(message = "接收方账号必填")
private String receiverAccount;
/** 接收方姓名 */
@@ -54,6 +56,7 @@ public class AllocReceiverAddParam extends PaymentCommonParam {
* @see AllocRelationTypeEnum
*/
@Schema(description = "分账关系类型")
@NotEmpty(message = "分账关系类型必填")
private String relationType;
/** 关系名称 关系类型为自定义是填写 */

View File

@@ -4,6 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
/**
* 分账接收方列表
* @author xxm
@@ -16,9 +19,12 @@ public class AllocReceiverParam {
/** 分账接收方编号 */
@Schema(description = "分账接收方编号")
@NotEmpty(message = "分账接收方编号必填")
private String receiverNo;
/** 分账金额 */
@Schema(description = "分账金额")
@NotEmpty(message = "分账金额必填")
@Min(value = 1,message = "分账金额至少为0.01元")
private Integer amount;
}

View File

@@ -6,6 +6,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
/**
* 分账接收者删除参数
* @author xxm
@@ -18,6 +20,7 @@ import lombok.experimental.Accessors;
public class AllocReceiverRemoveParam extends PaymentCommonParam {
@Schema(description = "接收者编号")
@NotEmpty(message = "接收者编号必填")
private String receiverNo;
}

View File

@@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
@@ -43,6 +44,7 @@ public class AllocationParam extends PaymentCommonParam {
/** 分账接收方列表 */
@Schema(description = "分账接收方列表")
@Valid
private List<AllocReceiverParam> receivers;
/** 是否不启用异步通知 */

View File

@@ -6,6 +6,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
/**
* 查询分账接收者参数
* @author xxm
@@ -19,5 +21,6 @@ public class QueryAllocReceiverParam extends PaymentCommonParam {
/** 所属通道 */
@Schema(description = "所属通道")
@NotEmpty(message = "所属通道必填")
private String channel;
}

View File

@@ -20,9 +20,6 @@ public class AllocReceiverResult {
/** 分账接收方编号, 需要保证唯一 */
private String receiverNo;
/** 账号别名 */
private String name;
/**
* 所属通道
* @see PayChannelEnum

View File

@@ -3,7 +3,7 @@ package cn.daxpay.single.service.annotation;
import java.lang.annotation.*;
/**
* 支付签名标识
* 支付校验签名标识
* 支付方法至少有一个参数,并且需要签名支付参数需要放在第一位
* 返回对象必须为 ResResult<T extends PaymentCommonResult> 格式
* @author xxm

View File

@@ -58,15 +58,17 @@ public class WeChatPayAllocationService {
}
String finalDescription = description;
orderDetails.sort(Comparator.comparing(MpIdEntity::getId));
List<ReceiverModel> list = orderDetails.stream().map(o->{
AllocReceiverTypeEnum receiverTypeEnum = AllocReceiverTypeEnum.findByCode(o.getReceiverType());
return ReceiverModel.builder()
.type(receiverTypeEnum.getOutCode())
.account(o.getReceiverAccount())
.amount(o.getAmount())
.description(finalDescription)
.build();
}).collect(Collectors.toList());
List<ReceiverModel> list = orderDetails.stream()
.filter(o-> Objects.equals(o.getResult(), AllocDetailResultEnum.PENDING.getCode()))
.map(o->{
AllocReceiverTypeEnum receiverTypeEnum = AllocReceiverTypeEnum.findByCode(o.getReceiverType());
return ReceiverModel.builder()
.type(receiverTypeEnum.getOutCode())
.account(o.getReceiverAccount())
.amount(o.getAmount())
.description(finalDescription)
.build();
}).collect(Collectors.toList());
Map<String, String> params = ProfitSharingModel.builder()
.mch_id(config.getWxMchId())

View File

@@ -40,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -104,6 +105,14 @@ public class AllocationOrderService {
return allocationOrderDetailManager.findById(id).map(AllocationOrderDetail::toDto).orElseThrow(() -> new DataNotExistException("分账订单明细不存在"));
}
/**
* 查询扩展订单信息
*/
public AllocationOrderExtraDto findExtraById(Long id) {
return allocationOrderExtraManager.findById(id).map(AllocationOrderExtra::toDto)
.orElseThrow(() -> new DataNotExistException("未找到"));
}
/**
* 生成分账订单, 根据分账组创建
@@ -113,11 +122,15 @@ public class AllocationOrderService {
// 订单明细
List<AllocationOrderDetail> details = receiversByGroups.stream()
.map(o -> {
// 计算分账金额, 小数分直接舍弃, 防止分账金额超过上限
// 计算分账金额, 小数分直接舍弃, 防止分账金额超过上限
Integer rate = o.getRate();
Integer amount = payOrder.getAmount() * rate / 10000;
AllocationOrderDetail detail = new AllocationOrderDetail();
detail.setReceiverNo(o.getReceiverNo())
// 等同于 payOrder.getAmount() * rate / 10000;
int amount = BigDecimal.valueOf(payOrder.getAmount())
.multiply(BigDecimal.valueOf(rate))
.divide(BigDecimal.valueOf(10000), 0, RoundingMode.DOWN).intValue();
AllocationOrderDetail detail = new AllocationOrderDetail()
.setReceiverNo(o.getReceiverNo())
.setReceiverId(o.getId())
.setAmount(amount)
.setResult(AllocDetailResultEnum.PENDING.getCode())
@@ -125,6 +138,12 @@ public class AllocationOrderService {
.setReceiverType(o.getReceiverType())
.setReceiverName(o.getReceiverName())
.setReceiverAccount(o.getReceiverAccount());
// 如果金额为0, 设置为分账失败, 不参与分账
if (amount == 0){
detail.setResult(AllocDetailResultEnum.IGNORE.getCode())
.setErrorMsg("分账比例有误或金额太小, 无法进行分账")
.setFinishTime(LocalDateTime.now());
}
return detail;
})
.collect(Collectors.toList());
@@ -186,7 +205,6 @@ public class AllocationOrderService {
long allocId = IdUtil.getSnowflakeNextId();
// 分账明细设置ID
details.forEach(o -> o.setAllocationId(allocId));
// 求分账的总额
Integer sumAmount = details.stream()
.map(AllocationOrderDetail::getAmount)
@@ -204,6 +222,14 @@ public class AllocationOrderService {
.setDescription(param.getDescription())
.setStatus(AllocOrderStatusEnum.ALLOCATION_PROCESSING.getCode())
.setAmount(sumAmount);
// 如果分账订单金额为0, 设置为忽略状态
if (sumAmount == 0){
allocationOrder.setStatus(AllocOrderStatusEnum.IGNORE.getCode())
.setFinishTime(LocalDateTime.now())
.setResult(AllocOrderStatusEnum.ALLOCATION_FAILED.getCode())
.setErrorMsg("分账比例有误或金额太小, 无法进行分账");
}
allocationOrder.setId(allocId);
// 分账订单扩展
NoticeLocal noticeInfo = PaymentContextLocal.get().getNoticeInfo();
@@ -220,13 +246,5 @@ public class AllocationOrderService {
allocationOrderManager.save(allocationOrder);
return new OrderAndDetail().setOrder(allocationOrder).setDetails(details);
}
/**
* 查询扩展订单信息
*/
public AllocationOrderExtraDto findExtraById(Long id) {
return allocationOrderExtraManager.findById(id).map(AllocationOrderExtra::toDto)
.orElseThrow(() -> new DataNotExistException("未找到"));
}
}

View File

@@ -33,10 +33,6 @@ public class AllocationReceiver extends MpBaseEntity implements EntityBaseFuncti
@DbColumn(comment = "分账接收方编号")
private String receiverNo;
/** 账号别名 */
@DbColumn(comment = "账号别名")
private String name;
/**
* 所属通道
* @see PayChannelEnum

View File

@@ -78,7 +78,6 @@ public class AllocationGroupService {
AllocationReceiver receiver = receiverMap.get(o.getReceiverId());
return new AllocationGroupReceiverResult()
.setId(o.getId())
.setName(receiver.getName())
.setReceiverId(receiver.getId())
.setReceiverNo(receiver.getReceiverNo())
.setReceiverAccount(receiver.getReceiverAccount())

View File

@@ -97,30 +97,24 @@ public class AllocationService {
throw new RepetitiveOperationException("分账发起处理中,请勿重复操作");
}
try {
// 建分账单和明细并保存, 同时更新支付订单状态 使用事务
OrderAndDetail orderAndDetail;
// 判断是否传输了分账接收方列表
if (CollUtil.isNotEmpty(param.getReceivers())) {
orderAndDetail = allocationOrderService.createAndUpdate(param, payOrder);
} else if (Objects.nonNull(param.getGroupNo())){
// 指定分账组
AllocationGroup allocationGroup = groupManager.findByGroupNo(param.getGroupNo()).orElseThrow(() -> new DataNotExistException("未查询到分账组"));
List<AllocationGroupReceiverResult> receiversByGroups = allocationGroupService.findReceiversByGroups(allocationGroup.getId());
orderAndDetail = allocationOrderService.createAndUpdate(param ,payOrder, receiversByGroups);
} else {
// 默认分账组
AllocationGroup allocationGroup = groupManager.findDefaultGroup(payOrder.getChannel()).orElseThrow(() -> new PayFailureException("未查询到默认分账组"));
List<AllocationGroupReceiverResult> receiversByGroups = allocationGroupService.findReceiversByGroups(allocationGroup.getId());
orderAndDetail = allocationOrderService.createAndUpdate(param ,payOrder, receiversByGroups);
}
// 创建分账策略并初始化
AbsAllocationStrategy allocationStrategy = AllocationFactory.create(payOrder.getChannel());
// 建分账订单相关信息
OrderAndDetail orderAndDetail = this.createAlloc(param, payOrder);
// 检查是否需要进行分账
AllocationOrder order = orderAndDetail.getOrder();
List<AllocationOrderDetail> details = orderAndDetail.getDetails();
// 无需进行分账,
if (Objects.equals(order.getStatus(),AllocOrderStatusEnum.IGNORE.getCode())){
return new AllocationResult()
.setAllocationNo(order.getAllocationNo())
.setStatus(order.getStatus());
}
// 创建分账策略并初始化
AbsAllocationStrategy allocationStrategy = AllocationFactory.create(payOrder.getChannel());
allocationStrategy.initParam(order, details);
// 分账预处理
allocationStrategy.doBeforeHandler();
try {
// 分账预处理
allocationStrategy.doBeforeHandler();
// 分账处理
allocationStrategy.allocation();
// 执行中
@@ -276,4 +270,27 @@ public class AllocationService {
result.setDetails(details);
return result;
}
/**
* 构建分账订单相关信息
*/
private OrderAndDetail createAlloc(AllocationParam param, PayOrder payOrder){
// 创建分账单和明细并保存, 同时更新支付订单状态 使用事务
OrderAndDetail orderAndDetail;
// 判断是否传输了分账接收方列表
if (CollUtil.isNotEmpty(param.getReceivers())) {
orderAndDetail = allocationOrderService.createAndUpdate(param, payOrder);
} else if (Objects.nonNull(param.getGroupNo())){
// 指定分账组
AllocationGroup allocationGroup = groupManager.findByGroupNo(param.getGroupNo()).orElseThrow(() -> new DataNotExistException("未查询到分账组"));
List<AllocationGroupReceiverResult> receiversByGroups = allocationGroupService.findReceiversByGroups(allocationGroup.getId());
orderAndDetail = allocationOrderService.createAndUpdate(param ,payOrder, receiversByGroups);
} else {
// 默认分账组
AllocationGroup allocationGroup = groupManager.findDefaultGroup(payOrder.getChannel()).orElseThrow(() -> new PayFailureException("未查询到默认分账组"));
List<AllocationGroupReceiverResult> receiversByGroups = allocationGroupService.findReceiversByGroups(allocationGroup.getId());
orderAndDetail = allocationOrderService.createAndUpdate(param ,payOrder, receiversByGroups);
}
return orderAndDetail;
}
}

View File

@@ -66,6 +66,12 @@ public class AllocationSyncService {
allocationOrder = allocationOrderManager.findByAllocationNo(param.getBizAllocationNo())
.orElseThrow(() -> new DataNotExistException("分账单不存在"));
}
// 如果类型为忽略, 不进行同步处理
if (Objects.equals(allocationOrder.getStatus(), AllocOrderStatusEnum.IGNORE.getCode())){
return new AllocSyncResult();
}
// 调用同步逻辑
this.sync(allocationOrder);
return new AllocSyncResult();
}
@@ -85,6 +91,7 @@ public class AllocationSyncService {
allocationStrategy.initParam(allocationOrder, detailList);
// 分账完结预处理
allocationStrategy.doBeforeHandler();
// 执行同步操作, 分账明细的状态变更会在这个里面
AllocRemoteSyncResult allocRemoteSyncResult = allocationStrategy.doSync();
// 保存分账同步记录
this.saveRecord(allocationOrder, allocRemoteSyncResult,null,null);

View File

@@ -50,7 +50,7 @@ public class AliPayAllocationStrategy extends AbsAllocationStrategy {
this.aliPayConfig = aliPayConfigService.getConfig();
// 判断是否支持分账
if (Objects.equals(aliPayConfig.getAllocation(),false)){
throw new PayFailureException("微信支付配置不支持分账");
throw new PayFailureException("支付宝支付配置不支持分账");
}
aliPayConfigService.initConfig(this.aliPayConfig);
}

View File

@@ -51,6 +51,10 @@ public class WeChatPayAllocationStrategy extends AbsAllocationStrategy {
if (Objects.equals(weChatPayConfig.getAllocation(),false)){
throw new PayFailureException("微信支付配置不支持分账");
}
// 如果分账金额为0, 不发起分账
if (getAllocationOrder().getAmount() == 0){
throw new PayFailureException("微信订单的分账比例不正确或订单金额太小, 无法进行分账");
}
}
/**

View File

@@ -26,9 +26,6 @@ public class AllocationGroupReceiverResult {
@Schema(description = "接收方编号")
private String receiverNo;
@Schema(description = "接收方账号别名")
private String name;
@Schema(description = "分账比例(万分之多少)")
private Integer rate;

View File

@@ -0,0 +1,16 @@
package cn.daxpay.single.service.dto.order.allocation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
*
* @author xxm
* @since 2024/5/29
*/
@Data
@Accessors(chain = true)
@Schema(title = "")
public class AllocationOrderInfoDto {
}

View File

@@ -10,7 +10,7 @@ import lombok.Data;
*/
@Data
@Schema(title = "支付订单和扩展信息")
public class PayOrderDetailDto {
public class PayOrderInfoDto {
@Schema(description = "支付订单")
private PayOrderDto payOrder;
@Schema(description = "支付订单扩展信息")

View File

@@ -16,10 +16,6 @@ import lombok.Setter;
@Setter
public class AllocationReceiverQuery {
@QueryParam(type = QueryParam.CompareTypeEnum.LIKE)
@Schema(description = "账号别名")
private String name;
@QueryParam(type = QueryParam.CompareTypeEnum.LIKE)
@Schema(description = "接收方编号")
private String receiverNo;

View File

@@ -25,7 +25,7 @@ public class AllocationAutoStartTask implements Job {
private final AllocationService allocationService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
public void execute(JobExecutionContext jobExecutionContext) {
for (PayOrder payOrder : payOrderManager.findAutoAllocation()) {
AllocationParam param = new AllocationParam();
param.setBizAllocationNo(OrderNoGenerateUtil.allocation());
@@ -35,8 +35,6 @@ public class AllocationAutoStartTask implements Job {
log.warn("自动分账失败, 支付订单号: {}", payOrder.getOrderNo());
log.warn("自动分账失败:{}", e.getMessage());
}
}
}
}