1、软件购买接入新的渠道code基于二进制的解析;2、账户明细、销售管理、软件购买列表查询和回调数据调整;

This commit is contained in:
gaoqr
2025-10-17 15:29:27 +08:00
parent e9dc372078
commit 439db39fa3
26 changed files with 1013 additions and 1083 deletions
@@ -1,15 +1,19 @@
package com.cf.imes.module.system.controller.admin.funds.balancedetails.vo;
import com.cf.imes.framework.common.pojo.PageParam;
import com.cf.imes.module.system.validation.pay.IncomeExpenseTypeEnumValid;
import com.cf.imes.module.system.validation.pay.TradeTypeEnumValid;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import static com.cf.imes.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@@ -34,10 +38,32 @@ public class BalanceDetailsPageReqVO extends PageParam {
@TradeTypeEnumValid
private Integer tradeType;
@Schema(description = "收支类型,0收入 1支出")
@IncomeExpenseTypeEnumValid
private Integer incomeExpenseType;
@Schema(description = "创建时间", example = "[2022-07-01 ,2022-07-01]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate[] createTime;
@Schema(description = "充值金额")
@DecimalMin(value = "0.01", message = "充值金额必须大于零")
@DecimalMax(value = "100000", message = "充值金额不能超过十万")
@Digits(integer = 6, fraction = 2, message = "支付金额格式不正确")
private BigDecimal rechargeAmount;
@Schema(description = "赠送金额")
@DecimalMin(value = "0", message = "赠送金额不能小于零")
@DecimalMax(value = "100000", message = "赠送金额不能超过十万")
@Digits(integer = 6, fraction = 2, message = "赠送金额格式不正确")
private BigDecimal giftAmount;
@Schema(description = "产品金额")
@DecimalMin(value = "0.01", message = "产品购买金额必须大于零")
@DecimalMax(value = "100000", message = "产品购买金额不能超过十万")
@Digits(integer = 6, fraction = 2, message = "产品购买金额格式不正确")
private BigDecimal productPrice;
@Schema(description = "组织ID")
private Long organId;
@@ -41,20 +41,20 @@ public class BalanceDetailsRespVO {
@ExcelProperty("充值金额")
private BigDecimal rechargeAmount;
@Schema(description = "充值赠送金金额")
@ExcelProperty("充值赠送金金额")
@Schema(description = "赠送金金额")
@ExcelProperty("赠送金金额")
private BigDecimal rechargeGiftAmount;
@Schema(description = "产品金额")
@ExcelProperty("产品金额")
private BigDecimal productPrice;
@Schema(description = "账户充值支付")
@ExcelProperty("账户充值支付")
@Schema(description = "账户余额支付")
@ExcelProperty("账户余额支付")
private BigDecimal balancePayAmount;
@Schema(description = "支付宝和微信支付")
@ExcelProperty("支付宝和微信支付")
@Schema(description = "扫码支付")
@ExcelProperty("扫码支付")
private BigDecimal rechargePayAmount;
@Schema(description = "账户赠送金支付")
@@ -68,4 +68,7 @@ public class BalanceDetailsRespVO {
@Schema(description = "操作人")
@ExcelProperty("操作人")
private String creator;
@Schema(description = "支付方式")
private String paymentMethod;
}
@@ -3,7 +3,6 @@ package com.cf.imes.module.system.controller.admin.funds.purchase.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.cf.imes.framework.excel.core.annotations.EnumFormat;
import com.cf.imes.framework.excel.core.convert.EnumConvert;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.ProductDurationUnitEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -42,8 +41,6 @@ public class ManagePurchaseRecordRespVO {
private Integer giftDurationUnit;
@Schema(description = "支付方式")
@ExcelProperty(value = "支付方式", converter = EnumConvert.class)
@EnumFormat(value = PayProductChannelCodeEnum.class)
private Integer paymentMethod;
@Schema(description = "订单总额")
@@ -3,14 +3,20 @@ package com.cf.imes.module.system.controller.admin.funds.purchase.vo;
import com.cf.imes.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static com.cf.imes.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static com.cf.imes.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 组织购买记录明细 Request VO")
@@ -41,7 +47,16 @@ public class PurchaseRecordPageReqVO extends PageParam {
private String creator;
@Schema(description = "订单号")
@Size(max = 64, message = "订单号长度不能超过64个字符")
private String orderNo;
@Size(max = 20, message = "订单号长度不能超过20个字符")
private String id;
@Schema(description = "有效时间", example = "[2022-07-01 01:01:01 ,2022-07-01 01:01:01]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate[] endTime;
@Schema(description = "产品金额")
@DecimalMin(value = "0.01", message = "产品购买金额必须大于零")
@DecimalMax(value = "100000", message = "产品购买金额不能超过十万")
@Digits(integer = 6, fraction = 2, message = "产品购买金额格式不正确")
private BigDecimal productPrice;
}
@@ -5,7 +5,6 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.cf.imes.framework.excel.core.annotations.EnumFormat;
import com.cf.imes.framework.excel.core.convert.EnumConvert;
import com.cf.imes.framework.jackson.core.databind.LocalDateSerializer;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.ProductDurationUnitEnum;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -59,9 +58,7 @@ public class PurchaseRecordRespVO {
private Integer giftDurationUnit;
@Schema(description = "支付方式")
@ExcelProperty(value = "支付方式", converter = EnumConvert.class)
@EnumFormat(value = PayProductChannelCodeEnum.class)
private Integer paymentMethod;
private String paymentMethod;
@Schema(description = "订单总额")
@ExcelProperty("订单总额")
@@ -71,18 +68,14 @@ public class PurchaseRecordRespVO {
@ExcelProperty("现金余额支出")
private BigDecimal accountBalanceSpent;
@Schema(description = "支付宝支出")
@ExcelProperty("支付宝支出")
private BigDecimal alipaySpent;
@Schema(description = "微信支出")
@ExcelProperty("微信支出")
private BigDecimal wechatSpent;
@Schema(description = "赠送金")
@ExcelProperty("赠送金")
private BigDecimal giftMoneySpent;
@Schema(description = "扫码支付")
@ExcelProperty("扫码支付")
private BigDecimal externalSpent;
@Schema(description = "购买时间")
@ExcelProperty("购买时间")
private LocalDateTime createTime;
@@ -31,8 +31,8 @@ public class SalesDetailRespVO {
@ExcelProperty("产品金额")
private BigDecimal totalAmount;
@Schema(description = "账户充值支付")
@ExcelProperty("账户充值支付")
@Schema(description = "扫码支付")
@ExcelProperty("扫码支付")
private BigDecimal rechargeSpent;
@Schema(description = "现金支付")
@@ -51,6 +51,9 @@ public class SalesDetailRespVO {
@ExcelProperty("组织名称")
private String organName;
@Schema(description = "支付方式")
private String paymentMethod;
@Schema(description = "交易时间")
@ExcelProperty("交易时间")
private LocalDateTime createTime;
@@ -28,6 +28,9 @@ public class SalesDetailsPageReqVO extends PageParam {
@Schema(description = "组织ID")
private Long organId;
@Schema(description = "订单号")
private Long id;
@Schema(description = "产品金额")
@DecimalMin(value = "0.01", message = "产品购买金额必须大于零")
@DecimalMax(value = "100000", message = "产品购买金额不能超过十万")
@@ -28,13 +28,13 @@ public class RechargeDetailRespVO {
@EnumFormat(value = TradeTypeEnum.class)
private Integer tradeType;
@Schema(description = "订单号")
@ExcelProperty("订单号")
private String payNo;
@Schema(description = "充值金额")
@ExcelProperty("充值金额")
private BigDecimal price;
private BigDecimal rechargeAmount;
@Schema(description = "赠送金金额")
@ExcelProperty("赠送金金额")
private BigDecimal rechargeGiftAmount;
@ExcelIgnore
private Long purchaseId;
@@ -43,8 +43,8 @@ public class RechargeDetailRespVO {
@ExcelProperty("产品金额")
private BigDecimal totalAmount;
@Schema(description = "账户充值支付")
@ExcelProperty("账户充值支付")
@Schema(description = "扫码支付")
@ExcelProperty("扫码支付")
private BigDecimal rechargeSpent;
@Schema(description = "现金支付")
@@ -66,4 +66,7 @@ public class RechargeDetailRespVO {
@Schema(description = "交易时间")
@ExcelProperty("交易时间")
private LocalDateTime createTime;
@Schema(description = "支付方式")
private String paymentMethod;
}
@@ -1,6 +1,5 @@
package com.cf.imes.module.system.controller.admin.pay.vo;
import com.cf.imes.module.system.validation.pay.PayProductChannelCodeValid;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@@ -27,7 +26,6 @@ public class PayProductOrderUnifiedReqVO {
private Long productId;
@Schema(description = "产品支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@PayProductChannelCodeValid
@NotNull(message = "产品支付渠道编码不能为空", groups = PayProductGroup.class)
private Integer productChannelCode;
@@ -0,0 +1,65 @@
package com.cf.imes.module.system.dal.dataobject.incomeandexpense;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* @author Gqr
* @since 2025/10/17 11:48
*/
@Data
public class AccountDetailsDO {
/** 收支明细ID */
private Long id;
/** 流水号 */
private String businessNo;
/** 支付订单号 */
private String orderNo;
/** 软件购买id */
private Long purchaseId;
/** 交易类型 */
private Integer tradeType;
/** 收支类型 */
private Integer incomeExpenseType;
/** 现金金额变化 */
private BigDecimal cashAmountChange;
/** 赠送金金额变化 */
private BigDecimal giftAmountChange;
/** 创建人 */
private String creator;
/** 创建时间 */
private LocalDateTime createTime;
/** 机构ID */
private Long organId;
/** 订单总金额 */
private BigDecimal totalAmount;
/** 外部支付金额 */
private BigDecimal externalSpent;
/** 账户余额支付金额 */
private BigDecimal accountBalanceSpent;
/** 赠送金支付金额 */
private BigDecimal giftMoneySpent;
/** 支付方式 */
private Integer paymentMethod;
/** 渠道编码 */
private PayChannelCodeEnum channelCode;
}
@@ -63,11 +63,6 @@ public class PayOrderDO extends BaseDO {
*/
private Long purchaseId;
/**
* 支付订单号:P + 当前id + 时间戳(yyyyMMddHHmmss
*/
private String payNo;
/**
* 交易类型
*/
@@ -1,5 +1,6 @@
package com.cf.imes.module.system.dal.dataobject.pay;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import lombok.Data;
import java.math.BigDecimal;
@@ -62,4 +63,11 @@ public class PayOrderRechargeDetailDO {
* 交易时间
*/
private LocalDateTime createTime;
/**
* 渠道编号
*/
private PayChannelCodeEnum channelCode;
private Integer payMethod;
}
@@ -44,13 +44,14 @@ public interface PurchaseRecordMapper extends BaseMapperX<PurchaseRecordDO> {
.eq(PurchaseRecordDO::getDeleted, false)
.eqIfPresent(PurchaseRecordDO::getOrganId, pageReqVO.getOrganId())
.likeIfPresent(PurchaseRecordDO::getProductName, pageReqVO.getProductName())
.likeIfPresent(PurchaseRecordDO::getOrderNo, pageReqVO.getOrderNo())
.likeIfPresent(PurchaseRecordDO::getId, pageReqVO.getId())
.eqIfPresent(PurchaseRecordDO::getPurchaseDuration, pageReqVO.getPurchaseDuration())
.betweenIfPresent(PurchaseRecordDO::getCreateTime, pageReqVO.getCreateTime())
.likeIfPresent(PurchaseRecordDO::getCreator, pageReqVO.getCreator())
.eqIfPresent(PurchaseRecordDO::getTotalAmount, pageReqVO.getProductPrice())
.betweenIfPresent(PurchaseRecordDO::getEndTime, pageReqVO.getEndTime())
.ne(PurchaseRecordDO::getInitial, true)
.orderByDesc(PurchaseRecordDO::getCreateTime));
}
/**
@@ -115,6 +116,7 @@ public interface PurchaseRecordMapper extends BaseMapperX<PurchaseRecordDO> {
*/
default PageResult<PurchaseRecordDO> getSaleDetailsPage(SalesDetailsPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<PurchaseRecordDO>()
.eqIfPresent(PurchaseRecordDO::getId, pageReqVO.getId())
.betweenIfPresent(PurchaseRecordDO::getCreateTime, pageReqVO.getCreateTime())
.eqIfPresent(PurchaseRecordDO::getTotalAmount, pageReqVO.getProductPrice())
.eqIfPresent(PurchaseRecordDO::getOrganId, pageReqVO.getOrganId())
@@ -2,16 +2,18 @@ package com.cf.imes.module.system.dal.mysql.incomeandexpense;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.module.system.controller.admin.funds.balancedetails.vo.BalanceDetailsPageReqVO;
import com.cf.imes.module.system.controller.admin.funds.manualadjust.vo.ManualAdjustAccountBalancePageReqVO;
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.AccountDetailsDO;
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.IncomeExpenseDetailsDO;
import com.cf.imes.module.system.enums.pay.AdjustTypeEnum;
import com.cf.imes.module.system.enums.pay.TradeTypeEnum;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 收支明细 Mapper
*
@@ -33,6 +35,11 @@ public interface IncomeExpenseDetailsMapper extends BaseMapperX<IncomeExpenseDet
.likeIfPresent(IncomeExpenseDetailsDO::getBusinessNo, pageReqVO.getBusinessNo())
.likeIfPresent(IncomeExpenseDetailsDO::getOrderNo, pageReqVO.getOrderNo())
.betweenIfPresent(IncomeExpenseDetailsDO::getCreateTime, pageReqVO.getCreateTime())
.eqIfPresent(IncomeExpenseDetailsDO::getIncomeExpenseType, pageReqVO.getIncomeExpenseType())
.eqIfPresent(IncomeExpenseDetailsDO::getTradeType, pageReqVO.getTradeType())
.eqIfPresent(IncomeExpenseDetailsDO::getCashAmountChange, pageReqVO.getRechargeAmount())
.eqIfPresent(IncomeExpenseDetailsDO::getGiftAmountChange, pageReqVO.getGiftAmount())
.orderByDesc(IncomeExpenseDetailsDO::getId);
Integer tradeType = pageReqVO.getTradeType();
// 如果查的是交易,需要包含手工调账
@@ -45,6 +52,15 @@ public interface IncomeExpenseDetailsMapper extends BaseMapperX<IncomeExpenseDet
}
/**
* 账户明细分页查询
*
* @param page
* @param reqVO
* @return
*/
IPage<AccountDetailsDO> getAccountDetailsPage(@Param("page") IPage<AccountDetailsDO> page, @Param("reqVO") BalanceDetailsPageReqVO reqVO);
/**
* 获取手工调账分页
*
@@ -17,20 +17,22 @@ public enum PayChannelCodeEnum {
/**
* 支付宝
*/
ALIPAY_PC(0),
ALIPAY_PC(0, "支付宝"),
/**
* 微信
*/
WECHAT_NATIVE(1),
WECHAT_NATIVE(1, "微信"),
/**
* 手工调账
*/
MANUAL_ADJUST(2);
MANUAL_ADJUST(2, "手工调账");
@EnumValue
private final Integer code;
private final String desc;
@JsonCreator
public static PayChannelCodeEnum fromType(Integer type) {
for (PayChannelCodeEnum value : values()) {
@@ -7,10 +7,6 @@ package com.cf.imes.module.system.enums.pay;
* @since 2025/8/12 17:49
*/
public class PayConstants {
public static final String PAY_ORDER_PREFIX_FORMAT = "PO%s"; // 第三方支付流水号格式
public static final String PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT = "PD%s"; // 产品购买流水号格式
public static final String PAY_PRODUCT_DETAIL_AUTO_GENERATE_MONTH_ID_FORMAT = "1759029544%d"; // 1~12月定价规则自动生成ID格式
public static final String PAY_PRODUCT_DETAIL_AUTO_GENERATE_YEAR_ID_FORMAT = "1759993429%d"; // 1年定价规则自动生成ID格式
@@ -0,0 +1,128 @@
package com.cf.imes.module.system.enums.pay;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Gqr
* @since 2025/10/16 11:58
*/
@Getter
public enum PayProductChannelCodeEnumNew {
/**
* 现金
*/
BALANCE(1, "现金"), // 二进制 0001
/**
* 赠送金
*/
GIFT(1 << 1, "赠送金"), // 二进制 0010
/**
* 支付宝
*/
ALIPAY(1 << 2, "支付宝"), // 二进制 0100
/**
* 微信
*/
WECHAT(1 << 3, "微信"); // 二进制 1000
private final int code;
private final String label;
PayProductChannelCodeEnumNew(int code, String label) {
this.code = code;
this.label = label;
}
/**
* 把多个支付方式组合成一个整数
*
* @param types
*/
public static int combine(PayProductChannelCodeEnumNew... types) {
int result = 0;
for (PayProductChannelCodeEnumNew type : types) {
result |= type.code;
}
return result;
}
/**
* 是否包含现金
*/
public static boolean hasBalance(int code) {
return (code & PayProductChannelCodeEnumNew.BALANCE.getCode()) != 0;
}
/**
* 是否只有现金
*/
public static boolean isBalanceOnly(int code) {
return code == BALANCE.code;
}
/**
* 是否是现金+赠送金
*/
public static boolean isBalanceAndGift(int code) {
return code == combine(BALANCE, GIFT);
}
/**
* 是否包含赠送金
*/
public static boolean hasGift(int code) {
return (code & PayProductChannelCodeEnumNew.GIFT.getCode()) != 0;
}
/**
* 是否只有赠送金
*/
public static boolean isGiftOnly(int code) {
return code == GIFT.code;
}
/**
* 是否包含支付宝
*/
public static boolean hasAlipay(int code) {
return (code & PayProductChannelCodeEnumNew.ALIPAY.getCode()) != 0;
}
/**
* 是否包含微信
*/
public static boolean hasWechat(int code) {
return (code & PayProductChannelCodeEnumNew.WECHAT.getCode()) != 0;
}
/**
* 获取所有包含的支付方式列表
*/
public static List<PayProductChannelCodeEnumNew> parse(int code) {
List<PayProductChannelCodeEnumNew> list = new ArrayList<>();
for (PayProductChannelCodeEnumNew type : PayProductChannelCodeEnumNew.values()) {
if ((code & type.getCode()) != 0) {
list.add(type);
}
}
return list;
}
/**
* 返回可读描述,比如“现金+微信”
*
* @param code
*/
public static String describe(int code) {
return parse(code).stream()
.map(PayProductChannelCodeEnumNew::getLabel)
.collect(Collectors.joining("+"));
}
}
@@ -2,15 +2,17 @@ package com.cf.imes.module.system.service.funds.incomeandexpense;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.module.system.controller.admin.funds.balancedetails.vo.BalanceDetailsPageReqVO;
import com.cf.imes.module.system.controller.admin.funds.balancedetails.vo.BalanceDetailsRespVO;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.AccountDetailsDO;
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.IncomeExpenseDetailsDO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.mysql.incomeandexpense.IncomeExpenseDetailsMapper;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnumNew;
import com.cf.imes.module.system.enums.pay.TradeTypeEnum;
import com.cf.imes.module.system.service.funds.purchase.PurchaseService;
import com.cf.imes.module.system.service.organ.OrganService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -29,9 +31,6 @@ public class IncomeExpenseServiceImpl implements IncomeExpenseService {
@Resource
private IncomeExpenseDetailsMapper incomeExpenseDetailsMapper;
@Resource
private PurchaseService purchaseService;
@Resource
private OrganService organService;
@@ -43,8 +42,9 @@ public class IncomeExpenseServiceImpl implements IncomeExpenseService {
@Override
public PageResult<BalanceDetailsRespVO> getBalanceDetailsPage(BalanceDetailsPageReqVO pageReqVO) {
PageResult<IncomeExpenseDetailsDO> pageResult = incomeExpenseDetailsMapper.selectDetailsPage(pageReqVO);
List<IncomeExpenseDetailsDO> pageResultList = pageResult.getList();
PageDTO<AccountDetailsDO> page = new PageDTO<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
IPage<AccountDetailsDO> pageResult = incomeExpenseDetailsMapper.getAccountDetailsPage(page, pageReqVO);
List<AccountDetailsDO> pageResultList = pageResult.getRecords();
if(CollUtil.isEmpty(pageResultList)){
return new PageResult<>();
}
@@ -54,7 +54,6 @@ public class IncomeExpenseServiceImpl implements IncomeExpenseService {
pageResultList.forEach(i -> {
BalanceDetailsRespVO respVO = new BalanceDetailsRespVO();
respVO.setBusinessNo(i.getBusinessNo());
respVO.setOrderNo(i.getOrderNo());
respVO.setCreator(i.getCreator());
respVO.setCreateTime(i.getCreateTime());
respVO.setIncomeExpenseType(i.getIncomeExpenseType());
@@ -65,23 +64,23 @@ public class IncomeExpenseServiceImpl implements IncomeExpenseService {
if (TradeTypeEnum.RECHARGE.getCode().equals(tradeType)) {
respVO.setRechargeAmount(i.getCashAmountChange());
respVO.setRechargeGiftAmount(i.getGiftAmountChange());
respVO.setPaymentMethod(i.getChannelCode().getDesc());
respVO.setOrderNo(i.getOrderNo());
} else if (TradeTypeEnum.PRODUCT.getCode().equals(tradeType)) {
Long purchaseId = i.getPurchaseId();
if (ObjectUtil.isNotNull(purchaseId)) {
// 查询对应的购买记录
PurchaseRecordDO record = purchaseService.getRecord(purchaseId);
if (ObjectUtil.isNotNull(record)) {
respVO.setProductPrice(record.getTotalAmount());
respVO.setBalancePayAmount(record.getAccountBalanceSpent());
respVO.setRechargePayAmount(record.getExternalSpent());
respVO.setGiftPayAmount(record.getGiftMoneySpent());
}
}
respVO.setProductPrice(i.getTotalAmount());
respVO.setBalancePayAmount(i.getAccountBalanceSpent());
respVO.setRechargePayAmount(i.getExternalSpent());
respVO.setGiftPayAmount(i.getGiftMoneySpent());
respVO.setPaymentMethod(PayProductChannelCodeEnumNew.describe(i.getPaymentMethod()));
respVO.setOrderNo(String.valueOf(i.getPurchaseId()));
} else if (TradeTypeEnum.MANUAL_ADJUST.getCode().equals(tradeType)) {
respVO.setRechargeAmount(i.getCashAmountChange());
respVO.setRechargeGiftAmount(i.getGiftAmountChange());
// 手工调账算到充值
respVO.setTradeType(TradeTypeEnum.RECHARGE.getCode());
respVO.setPaymentMethod(TradeTypeEnum.MANUAL_ADJUST.getDesc());
}
// 查询操作组织名
OrganizationDO organ = organService.getOrganWithDeleted(i.getOrganId());
@@ -127,7 +127,6 @@ public class ManualAdjustAccountBalanceServiceImpl implements ManualAdjustAccoun
// 新增收支明细
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.orderNo(payOrderDO.getPayNo())
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.tradeType(TradeTypeEnum.MANUAL_ADJUST.getCode())
.incomeExpenseType(incomeExpenseType)
@@ -152,7 +151,6 @@ public class ManualAdjustAccountBalanceServiceImpl implements ManualAdjustAccoun
Long payOrderId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
payOrderDO.setId(payOrderId);
payOrderDO.setPayNo(String.format(PayConstants.PAY_ORDER_PREFIX_FORMAT, payOrderId));
payOrderDO.setStatus(PayOrderStatusEnum.SUCCESS);
payOrderDO.setPrice(recharge);
payOrderDO.setSubject("手工调账");
@@ -1,5 +1,6 @@
package com.cf.imes.module.system.service.funds.purchase;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -28,7 +29,7 @@ import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.mysql.funds.purchase.PurchaseRecordMapper;
import com.cf.imes.module.system.enums.pay.InvoiceStatusEnum;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnumNew;
import com.cf.imes.module.system.enums.pay.ProductDurationUnitEnum;
import com.cf.imes.module.system.enums.pay.PurchaseRecordStatusEnum;
import com.cf.imes.module.system.enums.pay.TradeTypeEnum;
@@ -107,7 +108,12 @@ public class PurchaseServiceImpl implements PurchaseService {
if (CollUtil.isEmpty(resultList)) {
return new PageResult<>();
}
List<PurchaseRecordRespVO> respVOS = BeanUtils.toBean(resultList, PurchaseRecordRespVO.class);
List<PurchaseRecordRespVO> respVOS = new ArrayList<>();
for (PurchaseRecordDO purchaseRecordDO : resultList) {
PurchaseRecordRespVO respVO = BeanUtil.copyProperties(purchaseRecordDO, PurchaseRecordRespVO.class, "paymentMethod");
respVO.setPaymentMethod(PayProductChannelCodeEnumNew.describe(purchaseRecordDO.getPaymentMethod()));
respVOS.add(respVO);
}
respVOS.forEach(respVO -> {
OrganizationDO organ = organService.getOrganWithDeleted(respVO.getOrganId());
if (ObjectUtil.isNotNull(organ)) {
@@ -184,7 +190,7 @@ public class PurchaseServiceImpl implements PurchaseService {
.startTime(now)
// 组织创建时的套餐购买默认过期
.endTime(now.minusDays(1))
.paymentMethod(PayProductChannelCodeEnum.ORGAN_CREATE.getCode())
.paymentMethod(PayProductChannelCodeEnumNew.BALANCE.getCode())
.totalAmount(price)
.monthAverageAmount(monthAverageAmount)
.originalAmount(price)
@@ -297,6 +303,8 @@ public class PurchaseServiceImpl implements PurchaseService {
respVO.setTradeType(TradeTypeEnum.PRODUCT.getCode());
// 充值支付=支付宝/微信
respVO.setRechargeSpent(detailDO.getExternalSpent());
// 支付方式
respVO.setPaymentMethod(PayProductChannelCodeEnumNew.describe(detailDO.getPaymentMethod()));
// 查询组织名称
OrganizationDO organ = organService.getOrganWithDeleted(detailDO.getOrganId());
if (ObjectUtil.isNotNull(organ)) {
@@ -6,6 +6,7 @@ import com.cf.imes.module.system.controller.admin.funds.recharge.vo.RechargeDeta
import com.cf.imes.module.system.controller.admin.funds.recharge.vo.RechargeDetailsPageReqVO;
import com.cf.imes.module.system.controller.admin.pay.vo.PayProductOrderUnifiedReqVO;
import com.cf.imes.module.system.controller.admin.pay.vo.PayRechargeOrderReqVO;
import com.cf.imes.module.system.dal.dataobject.pay.PayOrderDO;
import java.math.BigDecimal;
import java.util.Map;
@@ -17,6 +18,14 @@ import java.util.Map;
* @since 2025/4/17 17:29
*/
public interface PayOrderService {
/**
* 根据id查询支付订单
*
* @param id
* @return
*/
PayOrderDO get(Long id);
/**
* 产品支付
*
@@ -39,6 +39,7 @@ import com.cf.imes.module.system.controller.admin.funds.products.vo.product.Prod
import com.cf.imes.module.system.dal.dataobject.funds.delay.ProductDelayRecordDO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.pay.PayOrderRechargeDetailDO;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnumNew;
import com.cf.imes.module.system.enums.pay.ProductDurationUnitEnum;
import com.cf.imes.module.system.service.funds.delay.ProductDelayService;
import com.cf.imes.module.system.service.funds.purchase.PurchaseService;
@@ -52,8 +53,6 @@ import com.cf.imes.module.system.dal.dataobject.funds.recharge.GiftMoneyDetailsD
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.IncomeExpenseDetailsDO;
import com.cf.imes.module.system.enums.pay.IncomeExpenseTypeEnum;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.PayConstants;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.TradeTypeEnum;
import com.cf.imes.module.system.enums.recharge.RechargeGiftTypeEnum;
import com.cf.imes.module.system.enums.recharge.RechargeTypeEnum;
@@ -90,6 +89,7 @@ import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.e
import static com.cf.imes.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORG_PRODUCT_PURCHASE_LOCK_ERROR;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORG_PRODUCT_PURCHASE_WAIT_PAY_ERROR;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_CHANNEL_NOT_SUPPORT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_NOTIFY_PASSBACK_TRADETYPE_NOT_SUPPORT_ERROR;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_CHANNEL_QUERY_NOT_EXIST;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_NOTIFY_DATA_PARSE_ERROR;
@@ -169,6 +169,11 @@ public class PayOrderServiceImpl implements PayOrderService {
@Resource
private SnowflakeSerialNoWorker3rd snowflakeSerialNoWorker3rd;
@Override
public PayOrderDO get(Long id) {
return payOrderMapper.selectById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayOrderRespDTO productPay(PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) {
@@ -178,6 +183,10 @@ public class PayOrderServiceImpl implements PayOrderService {
// 主动校验一次入参
ValidationUtils.validate(validator, payProductOrderUnifiedReqVO);
// 校验组合渠道
List<PayProductChannelCodeEnumNew> parse = PayProductChannelCodeEnumNew.parse(payProductOrderUnifiedReqVO.getProductChannelCode());
AssertUtils.notEmpty(parse, PAY_CHANNEL_NOT_SUPPORT);
// 校验产品和产品明细
Long productId = payProductOrderUnifiedReqVO.getProductId();
ProductsDO productsDO = productsDetailService.validateProductExist(productId);
@@ -194,7 +203,7 @@ public class PayOrderServiceImpl implements PayOrderService {
PayOrderUnifiedReqDTO payOrderUnifiedReqDTO = null;
// 获取渠道方处理器
Integer channelCode = PayProductChannelCodeEnum.isAliPay(payProductOrderUnifiedReqVO.getProductChannelCode()) ? PayChannelCodeEnum.ALIPAY_PC.getCode() : PayChannelCodeEnum.WECHAT_NATIVE.getCode();
Integer channelCode = PayProductChannelCodeEnumNew.hasAlipay(payProductOrderUnifiedReqVO.getProductChannelCode()) ? PayChannelCodeEnum.ALIPAY_PC.getCode() : PayChannelCodeEnum.WECHAT_NATIVE.getCode();
try {
// 检查机构产品是否有待支付的购买记录
List<PurchaseRecordDO> pendingPurchaseList = purchaseService.getOrgProductPendingPurchaseList(organId, productId);
@@ -212,8 +221,9 @@ public class PayOrderServiceImpl implements PayOrderService {
PreviouProductDelayRespVO previouProductDelayRespVO = productDelayService.getActiveRecordsByProductId(productId, organId);
// 根据购买类型做价格处理
PayProductProcessor payProductProcessor = new PayProductProcessor(productsDO, productsDetailDO, orgInitialPurchaseRecord, previouProductDelayRespVO, organAmount, organId, payConfig, mybatisIdProperties, snowflakeSerialNoWorker3rd);
ProductProcessorRespVO productProcessorRespVO = payProductProcessor.processPayment(PayProductChannelCodeEnum.fromType(payProductOrderUnifiedReqVO.getProductChannelCode()));
PayProductProcessorNew payProductProcessorNew = new PayProductProcessorNew(productsDO, productsDetailDO, orgInitialPurchaseRecord, previouProductDelayRespVO, organAmount, organId, payConfig, mybatisIdProperties, snowflakeSerialNoWorker3rd);
ProductProcessorRespVO productProcessorRespVO = payProductProcessorNew.processPayment(payProductOrderUnifiedReqVO.getProductChannelCode());
PayOrderDO payOrder = productProcessorRespVO.getPayOrderDO();
// 处理前置的购买记录
@@ -288,7 +298,7 @@ public class PayOrderServiceImpl implements PayOrderService {
PreviouProductDelayRespVO previouProductDelayRespVO = productDelayService.getActiveRecordsByProductId(productId, organId);
// 根据购买类型做价格处理
PayProductProcessor payProductProcessor = new PayProductProcessor(productsDO, productsDetailDO, orgInitialPurchaseRecord, previouProductDelayRespVO, null, organId, null, null, null);
PayProductProcessorNew payProductProcessor = new PayProductProcessorNew(productsDO, productsDetailDO, orgInitialPurchaseRecord, previouProductDelayRespVO, null, organId, null, null, null);
return LocalDateTimeUtil.format(payProductProcessor.getProductPayExpireTime(), FORMAT_YEAR_MONTH_DAY);
}
@@ -477,7 +487,6 @@ public class PayOrderServiceImpl implements PayOrderService {
Long organId = SecurityFrameworkUtils.getUserOrganId();
payOrderDO.setId(payOrderId);
payOrderDO.setPayNo(String.format(PayConstants.PAY_ORDER_PREFIX_FORMAT, payOrderId));
payOrderDO.setStatus(PayOrderStatusEnum.WAITING);
payOrderDO.setExpireTime(LocalDateTime.now().plus(payConfig.getPayExpireTime()));
payOrderDO.setPrice(price);
@@ -500,7 +509,6 @@ public class PayOrderServiceImpl implements PayOrderService {
String subject = payOrderDO.getSubject();
payOrderUnifiedReqDTO.setSubject(subject);
payOrderUnifiedReqDTO.setBody(subject);
payOrderUnifiedReqDTO.setOutTradeNo(payOrderDO.getPayNo());
payOrderUnifiedReqDTO.setPrice(payOrderDO.getPrice());
payOrderUnifiedReqDTO.setExpireTime(payOrderDO.getExpireTime());
payOrderUnifiedReqDTO.setNotifyUrl(String.format(alipayConfig.getNotifyUrl(), payOrderDO.getChannelCode().getCode()));
@@ -512,7 +520,7 @@ public class PayOrderServiceImpl implements PayOrderService {
public void queryPayOrderStatus(String outTradeNo, int channelCode) {
// 校验支付订单是否存在
PayOrderDO payOrderDO = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getPayNo, outTradeNo)
.eq(PayOrderDO::getId, outTradeNo)
.eq(PayOrderDO::getOrganId, validOrganId())
.eq(PayOrderDO::getDeleted, false)
);
@@ -601,7 +609,7 @@ public class PayOrderServiceImpl implements PayOrderService {
private void notifyOrderSuccess(PayOrderRespDTO notify, String outTradeNo) {
// 1. 判断 PayOrderDO 是否处于待支付
PayOrderDO order = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getPayNo, outTradeNo));
.eq(PayOrderDO::getId, outTradeNo));
Long organId = order.getOrganId();
if (ObjectUtil.isNull(order)) {
throw exception(PAY_ORDER_NOT_EXIST, outTradeNo);
@@ -653,7 +661,7 @@ public class PayOrderServiceImpl implements PayOrderService {
// 支付订单的金额
BigDecimal price = payOrderDO.getPrice();
// 订单流水号
String payNo = payOrderDO.getPayNo();
Long payOrderId = payOrderDO.getId();
// 1.更新组织资金
// 检查赠送金是否存在已经是否在有效期,有效的话计入组织充值赠送金
@@ -665,7 +673,7 @@ public class PayOrderServiceImpl implements PayOrderService {
// 2.新增收支明细
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.orderNo(payNo)
.orderNo(String.valueOf(payOrderId))
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.tradeType(TradeTypeEnum.RECHARGE.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.INCOME.getCode())
@@ -710,10 +718,11 @@ public class PayOrderServiceImpl implements PayOrderService {
// 5.如果是纯第三方支付需要新增收支明细
Integer paymentMethod = record.getPaymentMethod();
if (PayProductChannelCodeEnum.ALIPAY_PC.getCode().equals(paymentMethod) || PayProductChannelCodeEnum.WECHAT_NATIVE.getCode().equals(paymentMethod)) {
if (PayProductChannelCodeEnumNew.hasAlipay(paymentMethod) || PayProductChannelCodeEnumNew.hasWechat(paymentMethod)) {
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.orderNo(String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId))
.orderNo(String.valueOf(payOrderDO.getId()))
.purchaseId(purchaseId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
@@ -735,7 +744,7 @@ public class PayOrderServiceImpl implements PayOrderService {
private void notifyOrderClosed(PayOrderRespDTO notify, String outTradeNo) {
// 1. 查询 PayOrderDO
PayOrderDO order = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getPayNo, outTradeNo));
.eq(PayOrderDO::getId, outTradeNo));
Long organId = order.getOrganId();
if (ObjectUtil.isNull(order)) {
throw exception(PAY_ORDER_NOT_EXIST, outTradeNo);
@@ -818,7 +827,7 @@ public class PayOrderServiceImpl implements PayOrderService {
// 3、新增退回的收支记录
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.orderNo(String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId))
.purchaseId(purchaseId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.INCOME.getCode())
@@ -883,7 +892,7 @@ public class PayOrderServiceImpl implements PayOrderService {
AbstractPayHandler payHandler = payHandlerFactory.getPayHandler(channelCode.getCode());
// 2、调用三方接口,查询支付单状态,是不是已支付
PayOrderRespDTO payOrderRespDTO = payHandler.queryPayOrder(order.getPayNo());
PayOrderRespDTO payOrderRespDTO = payHandler.queryPayOrder(String.valueOf(order.getId()));
if (PayOrderStatusEnum.isSuccess(payOrderRespDTO.getStatus()) || PayOrderStatusEnum.isClosed(payOrderRespDTO.getStatus())) {
notifyOrderWithTx(payOrderRespDTO);
log.info("[expireOrder][order({}) 更新为支付关闭成功]", order.getId());
@@ -892,9 +901,9 @@ public class PayOrderServiceImpl implements PayOrderService {
// 3、系统内超期未支付状态是NOTPAY,主动关单后再次查询状态
if (PayOrderStatusEnum.WAITING.getStatus().equals(payOrderRespDTO.getStatus())) {
boolean closeOrderResult = payHandler.closeOrder(order.getPayNo());
boolean closeOrderResult = payHandler.closeOrder(String.valueOf(order.getId()));
if (closeOrderResult) {
payOrderRespDTO = payHandler.queryPayOrder(order.getPayNo());
payOrderRespDTO = payHandler.queryPayOrder(String.valueOf(order.getId()));
if (PayOrderStatusEnum.isSuccess(payOrderRespDTO.getStatus()) || PayOrderStatusEnum.isClosed(payOrderRespDTO.getStatus())) {
notifyOrderWithTx(payOrderRespDTO);
log.info("[expireOrder][order({}) 更新为支付关闭成功]", order.getId());
@@ -1,989 +0,0 @@
package com.cf.imes.module.system.service.pay;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.id.core.util.SnowflakeSerialNoWorker3rd;
import com.cf.imes.framework.pay.config.ChenfengPayConfig;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.controller.admin.funds.delay.vo.PreviouProductDelayRespVO;
import com.cf.imes.module.system.controller.admin.funds.delay.vo.ProductDelayRespVO;
import com.cf.imes.module.system.controller.admin.funds.products.vo.product.ProductProcessorRespVO;
import com.cf.imes.module.system.dal.dataobject.funds.organamount.OrganAmountDO;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDO;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDetailDO;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.IncomeExpenseDetailsDO;
import com.cf.imes.module.system.dal.dataobject.pay.PayOrderDO;
import com.cf.imes.module.system.enums.pay.IncomeExpenseTypeEnum;
import com.cf.imes.module.system.enums.pay.InvoiceStatusEnum;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.PayConstants;
import com.cf.imes.module.system.enums.pay.PayOrderStatusEnum;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.ProductDurationUnitEnum;
import com.cf.imes.module.system.enums.pay.PurchaseRecordStatusEnum;
import com.cf.imes.module.system.enums.pay.TradeTypeEnum;
import com.cf.imes.module.system.framework.snowflake.config.MybatisIdProperties;
import com.cf.imes.module.system.service.pay.dto.DateIntervalDTO;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_PRODUCT_AVAILABLE_BALANCE_INSUFFICIENT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_PRODUCT_BALANCE_INSUFFICIENT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_PRODUCT_GIFT_BALANCE_INSUFFICIENT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PRODUCTS_DETAIL_DURATION_UNIT_NOT_SUPPORT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PRODUCT_PURCHASE_ENDTIME_BEFORE_DELAYTIME_ERROR;
/**
* 产品购买类型处理器
*
* @author Gqr
* @since 2025/8/14 10:54
*/
public class PayProductProcessor {
// 当次订购的产品
private ProductsDO productsDO;
// 当次订购的定价规则
private ProductsDetailDO productsDetailDO;
// 组织产品的首次订购记录
private PurchaseRecordDO orgInitialPurchaseRecord;
// 当前组织资金信息
private OrganAmountDO organAmountDO;
// 组织id
private Long organId;
// 支付配置
private ChenfengPayConfig payConfig;
// id生成规则配置
private MybatisIdProperties mybatisIdProperties;
// id生成器
private SnowflakeSerialNoWorker3rd snowflakeSerialNoWorker3rd;
// 标记本次订购是否首次
private boolean initialized = false;
// 前序购买记录的延期记录
private List<ProductDelayRespVO> delayRespVOList;
// 前序购买记录
private PurchaseRecordDO previousPurchaseRecord;
// 本地订购产品的开始时间
private LocalDate startTime = LocalDate.now();
// 产品结束时间
private LocalDate endTime;
// 本地订购产品的月均单价
private BigDecimal monthAverageAmount = BigDecimal.ZERO;
// 原始价格
private BigDecimal originalPrice;
public PayProductProcessor(ProductsDO productsDO, ProductsDetailDO productsDetailDO, PurchaseRecordDO orgInitialPurchaseRecord, PreviouProductDelayRespVO previouProductDelayRespVO,
OrganAmountDO organAmountDO, Long organId, ChenfengPayConfig payConfig, MybatisIdProperties mybatisIdProperties, SnowflakeSerialNoWorker3rd snowflakeSerialNoWorker3rd) {
this.productsDO = productsDO;
this.productsDetailDO = productsDetailDO;
this.orgInitialPurchaseRecord = orgInitialPurchaseRecord;
if (ObjectUtil.isNotNull(previouProductDelayRespVO.getPreviouPurchaseRecord())) {
delayRespVOList = previouProductDelayRespVO.getDelayRespVOList();
previousPurchaseRecord = previouProductDelayRespVO.getPreviouPurchaseRecord();
// 上一次订购产品的结束时间进行计算
endTime = previousPurchaseRecord.getEndTime();
endTime = calculateEndTime(productsDetailDO.getDuration(), productsDetailDO.getDurationUnit());
} else {
// 首次订购正常从今天开始累加算世界时间
endTime = addTime(startTime, productsDetailDO.getDuration(), productsDetailDO.getDurationUnit());
}
this.organAmountDO = organAmountDO;
this.organId = organId;
this.payConfig = payConfig;
this.mybatisIdProperties = mybatisIdProperties;
this.snowflakeSerialNoWorker3rd = snowflakeSerialNoWorker3rd;
}
/**
* 根据产品支付类型解析出对应的信息,
*
* @param payType
*/
public ProductProcessorRespVO processPayment(PayProductChannelCodeEnum payType) {
// 首次购买记录存在则需要做计算,否则直接用定价规则的现价
boolean initialPurchaseExist = ObjectUtil.isNotNull(orgInitialPurchaseRecord);
BigDecimal price;
if (initialPurchaseExist) {
price = computePrice();
} else {
price = computeInitialProductPayAmount();
// 首次购买记录是空,那么当前就是首次订购
initialized = true;
}
// 当前组织的余额
BigDecimal balance = organAmountDO.getAmount();
// 当前组织的赠送金
BigDecimal gift = organAmountDO.getGiftAmount();
switch (payType) {
case BALANCE_ONLY:
return payByBalance(price, balance);
case GIFT_ONLY:
return payByGift(price, gift);
case ALIPAY_PC:
return payByAlipay(price);
case WECHAT_NATIVE:
return payByWechat(price);
case BALANCE_AND_GIFT:
return payByBalanceAndGift(price, gift, balance);
case BALANCE_AND_ALIPAY:
return payByBalanceAndExternal(price, balance, PayChannelCodeEnum.ALIPAY_PC);
case BALANCE_AND_WECHAT:
return payByBalanceAndExternal(price, balance, PayChannelCodeEnum.WECHAT_NATIVE);
case GIFT_AND_ALIPAY:
return payByGiftAndExternal(price, gift, PayChannelCodeEnum.ALIPAY_PC);
case GIFT_AND_WECHAT:
return payByGiftAndExternal(price, gift, PayChannelCodeEnum.WECHAT_NATIVE);
case BALANCE_AND_GIFT_AND_ALIPAY:
return payByBalanceAndGiftAndExternal(price, gift, balance, PayChannelCodeEnum.ALIPAY_PC);
case BALANCE_AND_GIFT_AND_WECHAT:
return payByBalanceAndGiftAndExternal(price, gift, balance, PayChannelCodeEnum.WECHAT_NATIVE);
default:
return null;
}
}
/**
* 获取订购产品的到期时间
*
* @return
*/
public LocalDate getProductPayExpireTime() {
return endTime;
}
/**
* 使用现金余额支付
*
* @param price
* @param balance
*/
private ProductProcessorRespVO payByBalance(BigDecimal price, BigDecimal balance) {
if (balance.compareTo(price) < 0) {
throw new ServiceException(PAY_PRODUCT_BALANCE_INSUFFICIENT);
}
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
String orderNo = String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId);
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.orderNo(orderNo)
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayProductChannelCodeEnum.BALANCE_ONLY.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.accountBalanceSpent(price)
.isInvocing(InvoiceStatusEnum.INVOICABLE.getCode())
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 新增收支明细
// 现金余额减去购买金额
BigDecimal newBalance = balance.subtract(price);
BigDecimal giftAmount = organAmountDO.getGiftAmount();
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.orderNo(orderNo)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.cashAmountChange(price)
.availableAmount(newBalance.add(giftAmount))
.amount(newBalance)
.giftAmount(giftAmount)
.build();
// 更新余额
organAmountDO.setAmount(newBalance);
organAmountDO.setGiftAmount(giftAmount);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, null, organAmountDO, delayRespVOList);
}
/**
* 使用赠送金余额支付
*
* @param price
* @param gift
*/
private ProductProcessorRespVO payByGift(BigDecimal price, BigDecimal gift) {
if (gift.compareTo(price) < 0) {
throw new ServiceException(PAY_PRODUCT_GIFT_BALANCE_INSUFFICIENT);
}
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
String orderNo = String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId);
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.orderNo(orderNo)
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayProductChannelCodeEnum.GIFT_ONLY.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.accountBalanceSpent(price)
.isInvocing(null)
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 新增收支明细
// 赠送金余额减去购买金额
BigDecimal newGift = gift.subtract(price);
BigDecimal amount = organAmountDO.getAmount();
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.orderNo(orderNo)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.giftAmountChange(price)
.availableAmount(amount.add(newGift))
.amount(amount)
.giftAmount(newGift)
.build();
// 更新余额
organAmountDO.setAmount(amount);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, null, organAmountDO, delayRespVOList);
}
/**
* 使用支付宝支付
*
* @param price
* @return
*/
private ProductProcessorRespVO payByAlipay(BigDecimal price) {
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayProductChannelCodeEnum.ALIPAY_PC.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.externalSpent(price)
.initial(initialized)
.status(PurchaseRecordStatusEnum.PENDING.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 新增第三方支付订单
PayOrderDO payOrderDO = goExternalPay(purchaseId, price, PayChannelCodeEnum.ALIPAY_PC);
return new ProductProcessorRespVO(purchaseRecordDO, null, payOrderDO, null, delayRespVOList);
}
/**
* 使用微信支付
*
* @param price
* @return
*/
private ProductProcessorRespVO payByWechat(BigDecimal price) {
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayProductChannelCodeEnum.WECHAT_NATIVE.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.externalSpent(price)
.initial(initialized)
.status(PurchaseRecordStatusEnum.PENDING.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 新增第三方支付订单
PayOrderDO payOrderDO = goExternalPay(purchaseId, price, PayChannelCodeEnum.WECHAT_NATIVE);
return new ProductProcessorRespVO(purchaseRecordDO, null, payOrderDO, null, delayRespVOList);
}
/**
* 使用现在余额+赠送金余额支付
*
* @param price
* @param gift
* @param balance
*/
private ProductProcessorRespVO payByBalanceAndGift(BigDecimal price, BigDecimal gift, BigDecimal balance) {
BigDecimal remaining = price;
BigDecimal usedGift = BigDecimal.ZERO;
BigDecimal usedBalance = BigDecimal.ZERO;
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
String orderNo = String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId);
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.orderNo(orderNo)
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayProductChannelCodeEnum.BALANCE_AND_GIFT.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.isInvocing(InvoiceStatusEnum.INVOICABLE.getCode())
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 是否只用了赠送金
boolean justPayGift = true;
// 先用赠送金
if (gift != null && gift.compareTo(BigDecimal.ZERO) > 0) {
usedGift = remaining.min(gift);
remaining = remaining.subtract(usedGift);
purchaseRecordDO.setGiftMoneySpent(usedGift);
}
// 再用余额
if (remaining.compareTo(BigDecimal.ZERO) > 0 && balance != null && balance.compareTo(BigDecimal.ZERO) > 0) {
usedBalance = remaining.min(balance);
remaining = remaining.subtract(usedBalance);
purchaseRecordDO.setAccountBalanceSpent(usedBalance);
justPayGift = false;
}
// 剩下走第三方
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
throw new ServiceException(PAY_PRODUCT_AVAILABLE_BALANCE_INSUFFICIENT);
}
// 新增收支明细
// 减去用掉的得到剩下的
BigDecimal newAmounta = balance.subtract(usedBalance);
BigDecimal newGift = gift.subtract(usedGift);
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.orderNo(orderNo)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.cashAmountChange(usedBalance)
.giftAmountChange(usedGift)
.availableAmount(newAmounta.add(newGift))
.amount(newAmounta)
.giftAmount(newGift)
.build();
// 如果只用赠送金那么不可开票
if (justPayGift) {
purchaseRecordDO.setIsInvocing(null);
}
// 更新余额
organAmountDO.setAmount(newAmounta);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, null, organAmountDO, delayRespVOList);
}
/**
* 使用现金余额+第三方
*
* @param price
* @param balance
*/
private ProductProcessorRespVO payByBalanceAndExternal(BigDecimal price, BigDecimal balance, PayChannelCodeEnum payChannelCodeEnum) {
BigDecimal remaining = price;
BigDecimal usedBalance = BigDecimal.ZERO;
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
String orderNo = String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId);
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.orderNo(orderNo)
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum) ? PayProductChannelCodeEnum.BALANCE_AND_ALIPAY.getCode() : PayProductChannelCodeEnum.BALANCE_AND_WECHAT.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 先用余额
if (balance != null && balance.compareTo(BigDecimal.ZERO) > 0) {
usedBalance = remaining.min(balance);
remaining = remaining.subtract(usedBalance);
purchaseRecordDO.setAccountBalanceSpent(usedBalance);
}
// 新增收支明细
// 减去用掉的得到剩下的
BigDecimal newAmounta = balance.subtract(usedBalance);
BigDecimal giftAmount = organAmountDO.getGiftAmount();
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.orderNo(orderNo)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.cashAmountChange(usedBalance)
.availableAmount(newAmounta.add(giftAmount))
.amount(newAmounta)
.giftAmount(giftAmount)
.build();
// 剩下走第三方
PayOrderDO payOrderDO = null;
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
payOrderDO = goExternalPay(purchaseId, remaining, payChannelCodeEnum);
purchaseRecordDO.setExternalSpent(remaining);
purchaseRecordDO.setStatus(PurchaseRecordStatusEnum.PENDING.getStatus());
}
// 更新余额
organAmountDO.setAmount(newAmounta);
organAmountDO.setGiftAmount(giftAmount);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO, delayRespVOList);
}
/**
* 使用赠送金余额+第三方
*
* @param price
* @param gift
*/
private ProductProcessorRespVO payByGiftAndExternal(BigDecimal price, BigDecimal gift, PayChannelCodeEnum payChannelCodeEnum) {
BigDecimal remaining = price;
BigDecimal usedGift = BigDecimal.ZERO;
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
String orderNo = String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId);
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.orderNo(orderNo)
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum) ? PayProductChannelCodeEnum.GIFT_AND_ALIPAY.getCode() : PayProductChannelCodeEnum.GIFT_AND_WECHAT.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 是否只用赠送金
boolean justPayGift = true;
// 先用赠送金
if (gift != null && gift.compareTo(BigDecimal.ZERO) > 0) {
usedGift = remaining.min(gift);
remaining = remaining.subtract(usedGift);
purchaseRecordDO.setGiftMoneySpent(usedGift);
}
// 新增收支明细
// 减去用掉的得到剩下的
BigDecimal newGift = gift.subtract(usedGift);
BigDecimal amount = organAmountDO.getAmount();
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.orderNo(orderNo)
.purchaseId(purchaseId)
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.giftAmountChange(usedGift)
.availableAmount(amount.add(newGift))
.amount(amount)
.giftAmount(newGift)
.build();
// 剩下走第三方
PayOrderDO payOrderDO = null;
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
payOrderDO = goExternalPay(purchaseId, remaining, payChannelCodeEnum);
purchaseRecordDO.setExternalSpent(remaining);
purchaseRecordDO.setStatus(PurchaseRecordStatusEnum.PENDING.getStatus());
justPayGift = false;
}
// 如果只用赠送金那么不可开票
if (justPayGift) {
purchaseRecordDO.setIsInvocing(null);
}
// 更新余额
organAmountDO.setAmount(amount);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO, delayRespVOList);
}
/**
* 使用现在余额+赠送金余额支付
*
* @param price
* @param gift
* @param balance
*/
private ProductProcessorRespVO payByBalanceAndGiftAndExternal(BigDecimal price, BigDecimal gift, BigDecimal balance, PayChannelCodeEnum payChannelCodeEnum) {
BigDecimal remaining = price;
BigDecimal usedGift = BigDecimal.ZERO;
BigDecimal usedBalance = BigDecimal.ZERO;
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
String orderNo = String.format(PayConstants.PAY_PRODUCT_DETAIL_BUSINESSNO_PREFIX_FORMAT, purchaseId);
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.orderNo(orderNo)
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(PayChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum) ? PayProductChannelCodeEnum.BALANCE_AND_GIFT_AND_ALIPAY.getCode() : PayProductChannelCodeEnum.BALANCE_AND_GIFT_AND_WECHAT.getCode())
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.build();
// 记录前序购买记录id
if (ObjectUtil.isNotNull(previousPurchaseRecord)) {
purchaseRecordDO.setPreviousId(previousPurchaseRecord.getId());
}
// 是否只用了赠送金
boolean justPayGift = true;
// 先用赠送金
if (gift != null && gift.compareTo(BigDecimal.ZERO) > 0) {
usedGift = remaining.min(gift);
remaining = remaining.subtract(usedGift);
purchaseRecordDO.setGiftMoneySpent(usedGift);
}
// 再用余额
if (remaining.compareTo(BigDecimal.ZERO) > 0 && balance != null && balance.compareTo(BigDecimal.ZERO) > 0) {
usedBalance = remaining.min(balance);
remaining = remaining.subtract(usedBalance);
purchaseRecordDO.setAccountBalanceSpent(usedBalance);
justPayGift = false;
}
// 新增收支明细
// 减去用掉的得到剩下的
BigDecimal newAmounta = balance.subtract(usedBalance);
BigDecimal newGift = gift.subtract(usedGift);
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.orderNo(orderNo)
.purchaseId(purchaseId)
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.cashAmountChange(usedBalance)
.giftAmountChange(usedGift)
.availableAmount(newAmounta.add(newGift))
.amount(newAmounta)
.giftAmount(newGift)
.build();
// 剩下走第三方
PayOrderDO payOrderDO = null;
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
payOrderDO = goExternalPay(purchaseId, remaining, payChannelCodeEnum);
purchaseRecordDO.setExternalSpent(remaining);
purchaseRecordDO.setStatus(PurchaseRecordStatusEnum.PENDING.getStatus());
justPayGift = false;
}
// 如果只用赠送金那么不可开票
if (justPayGift) {
purchaseRecordDO.setIsInvocing(null);
}
// 更新余额
organAmountDO.setAmount(newAmounta);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO, delayRespVOList);
}
/**
* 外部支付
*
* @param purchaseId
* @param price
* @param payChannelCodeEnum
* @return
*/
private PayOrderDO goExternalPay(Long purchaseId, BigDecimal price, PayChannelCodeEnum payChannelCodeEnum) {
PayOrderDO payOrderDO = new PayOrderDO();
Long payOrderId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
Long organId = SecurityFrameworkUtils.getUserOrganId();
payOrderDO.setId(payOrderId);
payOrderDO.setPayNo(String.format(PayConstants.PAY_ORDER_PREFIX_FORMAT, payOrderId));
payOrderDO.setStatus(PayOrderStatusEnum.WAITING);
payOrderDO.setExpireTime(LocalDateTime.now().plus(payConfig.getPayExpireTime()));
payOrderDO.setPrice(price);
payOrderDO.setSubject(productsDO.getProductName());
payOrderDO.setChannelCode(payChannelCodeEnum);
payOrderDO.setOrganId(organId);
payOrderDO.setPurchaseId(purchaseId);
payOrderDO.setTradeType(TradeTypeEnum.PRODUCT.getCode());
return payOrderDO;
}
/**
* 计算结束时间
*
* @param duration
* @param durationUnit
* @return
*/
private LocalDate calculateEndTime(Integer duration, Integer durationUnit) {
LocalDate today = LocalDate.now();
// 1、 计算延期后的实际有效期
LocalDate maxDelayEnd = endTime;
if (CollUtil.isNotEmpty(delayRespVOList)) {
// 找到延期记录中最大的 delayTime 作为最终有效期
maxDelayEnd = delayRespVOList.stream()
.map(ProductDelayRespVO::getDelayTime)
.max(LocalDate::compareTo)
.orElse(endTime);
}
// 2、 计算有效期叠加的起点
LocalDate purchaseStart;
if (today.isAfter(maxDelayEnd)) {
// 已过期 → 从今天开始累加
purchaseStart = today;
} else {
// 未过期 → 从 maxDelayEnd 的下一天开始累加
purchaseStart = maxDelayEnd;
}
// 3、加正常时间
if (ObjectUtil.isNotNull(duration) && durationUnit != null) {
endTime = addTime(purchaseStart, duration, durationUnit);
}
// 4、扣掉延期天数
long delayDays = 0L;
if (CollUtil.isNotEmpty(delayRespVOList)) {
delayDays = calculateDelayDays(startTime, delayRespVOList);
}
if (delayDays > 0) {
endTime = endTime.minusDays(delayDays);
}
if (endTime.isBefore(today)) {
throw ServiceExceptionUtil.exception(PRODUCT_PURCHASE_ENDTIME_BEFORE_DELAYTIME_ERROR, LocalDateTimeUtil.format(endTime, DatePattern.NORM_DATE_PATTERN));
}
return endTime;
}
/**
* 计算总延期天数(合并区间,避免重叠/不连续)
*/
private static long calculateDelayDays(LocalDate startTime, List<ProductDelayRespVO> delayRecords) {
// 1. 生成延期区间并打上备注
List<DateIntervalDTO> intervals = buildIntervals(startTime, delayRecords);
if (CollUtil.isEmpty(intervals)) {
return 0L;
}
// 2. 合并区间
List<DateIntervalDTO> merged = mergeIntervals(intervals);
// 3. 计算天数
return calculateTotalDays(merged);
}
/**
* 根据延期记录生成扣减区间,同时写入备注
*/
private static List<DateIntervalDTO> buildIntervals(LocalDate startTime, List<ProductDelayRespVO> delayRecords) {
List<DateIntervalDTO> intervals = new ArrayList<>();
for (ProductDelayRespVO record : delayRecords) {
LocalDate delayStart = record.getEndTime().plusDays(1);
LocalDate delayEnd = record.getDelayTime();
if (delayEnd.isBefore(delayStart)) {
record.setRemark("无扣减");
continue; // 异常数据,忽略
}
LocalDate intervalEnd;
if (!delayEnd.isAfter(startTime)) {
// 情况1:延期结束在计算结束之前 → 整段都扣
intervalEnd = delayEnd;
} else {
// 情况2:延期结束晚于计算结束 → 扣到 startTime 为止
intervalEnd = startTime;
}
long deductDays = ChronoUnit.DAYS.between(delayStart, intervalEnd) + 1;
if (deductDays > 0) {
record.setRemark("扣减" + deductDays + "");
intervals.add(new DateIntervalDTO(delayStart, intervalEnd));
} else {
record.setRemark("无扣减");
}
}
return intervals;
}
/**
* 合并区间(避免重叠/相邻)
*/
private static List<DateIntervalDTO> mergeIntervals(List<DateIntervalDTO> intervals) {
intervals.sort(Comparator.comparing(DateIntervalDTO::getStart));
List<DateIntervalDTO> merged = new ArrayList<>();
DateIntervalDTO current = intervals.get(0);
for (int i = 1; i < intervals.size(); i++) {
DateIntervalDTO next = intervals.get(i);
if (!next.getStart().isAfter(current.getEnd().plusDays(1))) {
// 重叠或相邻 → 合并
current.setEnd(current.getEnd().isAfter(next.getEnd()) ? current.getEnd() : next.getEnd());
} else {
merged.add(current);
current = next;
}
}
merged.add(current);
return merged;
}
/**
* 计算合并区间的总天数
*/
private static long calculateTotalDays(List<DateIntervalDTO> merged) {
long totalDays = 0L;
for (DateIntervalDTO interval : merged) {
totalDays += ChronoUnit.DAYS.between(interval.getStart(), interval.getEnd()) + 1;
}
return totalDays;
}
/**
* 基于时长和单位累加time
*
* @param time
* @param duration
* @param unit
* @return
*/
private static LocalDate addTime(LocalDate time, int duration, int unit) {
switch (ProductDurationUnitEnum.fromStatus(unit)) {
case YEAR:
return time.plusYears(duration);
case MONTH:
return time.plusMonths(duration);
default:
return time;
}
}
/**
* 计算有首购记录的产品价格
*
* @return
*/
private BigDecimal computePrice() {
// 月均单价
monthAverageAmount = orgInitialPurchaseRecord.getMonthAverageAmount();
// 首购的定价
BigDecimal initialPurchaseRecordTotalAmount = orgInitialPurchaseRecord.getTotalAmount();
// 定价规则的折扣
BigDecimal discount = productsDetailDO.getDiscount();
if (ObjectUtil.isNull(discount)) {
discount = BigDecimal.valueOf(100);
}
BigDecimal totalAmount;
/**
* ①:续费订购满1年:
* 原始价格 = 订购总价 * duration
* 现价 = 原始价格 * 折扣 /100,保留两位小数
* ②:不满1年按月份计算
* 原始价格 = 月均单价 * duration
* 现价 = 原始价格 * 折扣 /100,保留两位小数
*/
switch (ProductDurationUnitEnum.fromStatus(productsDetailDO.getDurationUnit())) {
case YEAR -> {
originalPrice = initialPurchaseRecordTotalAmount
.multiply(BigDecimal.valueOf(productsDetailDO.getDuration()));
totalAmount = originalPrice
.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
}
case MONTH -> {
originalPrice = monthAverageAmount
.multiply(BigDecimal.valueOf(productsDetailDO.getDuration()));
totalAmount = originalPrice
.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
}
default -> throw new ServiceException(PRODUCTS_DETAIL_DURATION_UNIT_NOT_SUPPORT);
}
return totalAmount;
}
/**
* 计算首次订购的价格
*/
private BigDecimal computeInitialProductPayAmount() {
// 订购数量
Integer duration = productsDetailDO.getDuration();
// 产品订购价格
BigDecimal orgPurchasePrice = productsDO.getPrice();
// 折扣
BigDecimal discount = productsDetailDO.getDiscount();
if (ObjectUtil.isNull(discount)) {
discount = BigDecimal.valueOf(100);
}
BigDecimal totalAmount;
switch (ProductDurationUnitEnum.fromStatus(productsDetailDO.getDurationUnit())) {
case YEAR -> {
// 原价
originalPrice = orgPurchasePrice.multiply(BigDecimal.valueOf(duration));
// 折扣价格 = 原价 * 折扣
totalAmount = originalPrice
.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
// 月均单价 = 折扣价格 / 10
monthAverageAmount = originalPrice.divide(new BigDecimal(10), 2, RoundingMode.DOWN);
}
case MONTH -> {
// 月均单价 = 订购价格 / 10
monthAverageAmount = orgPurchasePrice.divide(new BigDecimal(10), 2, RoundingMode.DOWN);
// 原价
originalPrice = monthAverageAmount.multiply(BigDecimal.valueOf(duration));
// 折扣价格 = 原价 * 折扣
totalAmount = originalPrice.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
}
default -> throw new ServiceException(PRODUCTS_DETAIL_DURATION_UNIT_NOT_SUPPORT);
}
return totalAmount;
}
}
@@ -0,0 +1,593 @@
package com.cf.imes.module.system.service.pay;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.id.core.util.SnowflakeSerialNoWorker3rd;
import com.cf.imes.framework.pay.config.ChenfengPayConfig;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.controller.admin.funds.delay.vo.PreviouProductDelayRespVO;
import com.cf.imes.module.system.controller.admin.funds.delay.vo.ProductDelayRespVO;
import com.cf.imes.module.system.controller.admin.funds.products.vo.product.ProductProcessorRespVO;
import com.cf.imes.module.system.dal.dataobject.funds.organamount.OrganAmountDO;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDO;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDetailDO;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import com.cf.imes.module.system.dal.dataobject.incomeandexpense.IncomeExpenseDetailsDO;
import com.cf.imes.module.system.dal.dataobject.pay.PayOrderDO;
import com.cf.imes.module.system.enums.pay.IncomeExpenseTypeEnum;
import com.cf.imes.module.system.enums.pay.InvoiceStatusEnum;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.PayOrderStatusEnum;
import com.cf.imes.module.system.enums.pay.PayProductChannelCodeEnumNew;
import com.cf.imes.module.system.enums.pay.ProductDurationUnitEnum;
import com.cf.imes.module.system.enums.pay.PurchaseRecordStatusEnum;
import com.cf.imes.module.system.enums.pay.TradeTypeEnum;
import com.cf.imes.module.system.framework.snowflake.config.MybatisIdProperties;
import com.cf.imes.module.system.service.pay.dto.DateIntervalDTO;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_PRODUCT_BALANCE_INSUFFICIENT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_PRODUCT_GIFT_BALANCE_INSUFFICIENT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PRODUCTS_DETAIL_DURATION_UNIT_NOT_SUPPORT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PRODUCT_PURCHASE_ENDTIME_BEFORE_DELAYTIME_ERROR;
/**
* 产品购买类型处理器
*
* @author Gqr
* @since 2025/8/14 10:54
*/
public class PayProductProcessorNew {
// 当次订购的产品
private ProductsDO productsDO;
// 当次订购的定价规则
private ProductsDetailDO productsDetailDO;
// 组织产品的首次订购记录
private PurchaseRecordDO orgInitialPurchaseRecord;
// 当前组织资金信息
private OrganAmountDO organAmountDO;
// 组织id
private Long organId;
// 支付配置
private ChenfengPayConfig payConfig;
// id生成规则配置
private MybatisIdProperties mybatisIdProperties;
// id生成器
private SnowflakeSerialNoWorker3rd snowflakeSerialNoWorker3rd;
// 标记本次订购是否首次
private boolean initialized = false;
// 前序购买记录的延期记录
private List<ProductDelayRespVO> delayRespVOList;
// 前序购买记录
private PurchaseRecordDO previousPurchaseRecord;
// 本地订购产品的开始时间
private LocalDate startTime = LocalDate.now();
// 产品结束时间
private LocalDate endTime;
// 本地订购产品的月均单价
private BigDecimal monthAverageAmount = BigDecimal.ZERO;
// 原始价格
private BigDecimal originalPrice;
// 前序购买记录的id
private Long previousPurchaseRecordId;
// 产品支付价格
private BigDecimal price;
// 组织赠送金
private BigDecimal gift;
// 组织现金
private BigDecimal balance;
// 使用赠送金
private BigDecimal usedGift;
// 使用余额
private BigDecimal usedBalance;
// 支付类型
private Integer payType;
// 当次购买记录DO
private PurchaseRecordDO purchaseRecordDO;
// 当次收支记录DO
private IncomeExpenseDetailsDO incomeExpenseDetailsDO;
// 当次支付订单DO
private PayOrderDO payOrderDO;
// 是否只用了赠送金
private boolean justPayGift = false;
public PayProductProcessorNew(ProductsDO productsDO, ProductsDetailDO productsDetailDO, PurchaseRecordDO orgInitialPurchaseRecord, PreviouProductDelayRespVO previouProductDelayRespVO,
OrganAmountDO organAmountDO, Long organId, ChenfengPayConfig payConfig, MybatisIdProperties mybatisIdProperties, SnowflakeSerialNoWorker3rd snowflakeSerialNoWorker3rd) {
this.productsDO = productsDO;
this.productsDetailDO = productsDetailDO;
this.orgInitialPurchaseRecord = orgInitialPurchaseRecord;
if (ObjectUtil.isNotNull(previouProductDelayRespVO.getPreviouPurchaseRecord())) {
delayRespVOList = previouProductDelayRespVO.getDelayRespVOList();
previousPurchaseRecord = previouProductDelayRespVO.getPreviouPurchaseRecord();
previousPurchaseRecordId = previousPurchaseRecord.getId();
// 上一次订购产品的结束时间进行计算
endTime = previousPurchaseRecord.getEndTime();
endTime = calculateEndTime(productsDetailDO.getDuration(), productsDetailDO.getDurationUnit());
} else {
// 首次订购正常从今天开始累加算世界时间
endTime = addTime(startTime, productsDetailDO.getDuration(), productsDetailDO.getDurationUnit());
}
this.organAmountDO = organAmountDO;
this.organId = organId;
this.payConfig = payConfig;
this.mybatisIdProperties = mybatisIdProperties;
this.snowflakeSerialNoWorker3rd = snowflakeSerialNoWorker3rd;
}
/**
* 根据产品支付类型解析出对应的信息,
*
* @param payType
*/
public ProductProcessorRespVO processPayment(Integer payType) {
// 首次购买记录存在则需要做计算,否则直接用定价规则的现价
boolean initialPurchaseExist = ObjectUtil.isNotNull(orgInitialPurchaseRecord);
if (initialPurchaseExist) {
price = computePrice();
} else {
price = computeInitialProductPayAmount();
// 首次购买记录是空,那么当前就是首次订购
initialized = true;
}
// 当前组织的余额
balance = organAmountDO.getAmount();
// 当前组织的赠送金
gift = organAmountDO.getGiftAmount();
this.payType = payType;
// 按序扣款
deduction();
// 根据扣款情况生成购买记录和收支记录
generatePurchaseRecordAndIncomeRecord();
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO, delayRespVOList);
}
/**
* 按顺序扣款
*/
private void deduction(){
// 有赠送金先扣赠送金
if (PayProductChannelCodeEnumNew.hasGift(payType)) {
payByGift();
if (price.compareTo(BigDecimal.ZERO) == 0) {
// 赠送金直接扣完了,开出的购买记录为不可开票
justPayGift = true;
return;
}
}
// 有现金扣现金
if(PayProductChannelCodeEnumNew.hasBalance(payType)) {
payByBalance();
if (price.compareTo(BigDecimal.ZERO) == 0) {
return;
}
}
// 有第三方支付走第三方
if (PayProductChannelCodeEnumNew.hasAlipay(payType) || PayProductChannelCodeEnumNew.hasWechat(payType)) {
payByExternal();
}
}
/**
* 赠送金支付
*/
private void payByGift() {
// 赠送金余额减去购买金额
usedGift = price.min(gift);
price = price.subtract(usedGift);
// 更新余额
organAmountDO.setAmount(organAmountDO.getAmount());
organAmountDO.setGiftAmount(gift.subtract(usedGift));
}
/**
* 余额支付
*/
private void payByBalance() {
//现金余额减去购买金额
usedBalance = price.min(balance);
price = price.subtract(usedBalance);
// 更新余额
organAmountDO.setAmount(balance.subtract(usedBalance));
organAmountDO.setGiftAmount(organAmountDO.getGiftAmount());
}
/**
* 根据扣款情况生成购买记录和收支记录
*/
private void generatePurchaseRecordAndIncomeRecord() {
// 扣款价格大于0
if (price.compareTo(BigDecimal.ZERO) > 0) {
// 只有赠送金
if (PayProductChannelCodeEnumNew.isGiftOnly(payType)) {
throw new ServiceException(PAY_PRODUCT_GIFT_BALANCE_INSUFFICIENT);
}
// 只有现金/现金+赠送金
if (PayProductChannelCodeEnumNew.isBalanceOnly(payType) || PayProductChannelCodeEnumNew.isBalanceAndGift(payType)) {
throw new ServiceException(PAY_PRODUCT_BALANCE_INSUFFICIENT);
}
}
// 扣款完成,生成记录
// 新增购买记录
long purchaseId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
purchaseRecordDO = PurchaseRecordDO.builder()
.id(purchaseId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.startTime(startTime)
.endTime(endTime)
.paymentMethod(payType)
.totalAmount(price)
.originalAmount(originalPrice)
.monthAverageAmount(monthAverageAmount)
.isInvocing(justPayGift ? null : InvoiceStatusEnum.INVOICABLE.getCode())
.initial(initialized)
.status(PurchaseRecordStatusEnum.ACTIVE.getStatus())
.previousId(previousPurchaseRecordId)
.build();
// 以上检查都通过,走的是第三方支付
if(ObjectUtil.isNotNull(payOrderDO)) {
// 第三方支付在异步回调后创建收支记录
payOrderDO.setPurchaseId(purchaseId);
if (PayProductChannelCodeEnumNew.hasAlipay(payType)) {
payOrderDO.setChannelCode(PayChannelCodeEnum.ALIPAY_PC);
} else if (PayProductChannelCodeEnumNew.hasWechat(payType)) {
payOrderDO.setChannelCode(PayChannelCodeEnum.WECHAT_NATIVE);
}
// 记录第三方支付金额
purchaseRecordDO.setExternalSpent(price);
// 购买记录为等待支付
purchaseRecordDO.setStatus(PurchaseRecordStatusEnum.PENDING.getStatus());
// 更新为不可开票,等异步回调后修改为可开票
purchaseRecordDO.setIsInvocing(null);
} else {
BigDecimal amount = organAmountDO.getAmount();
BigDecimal giftAmount = organAmountDO.getGiftAmount();
incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.businessNo(snowflakeSerialNoWorker3rd.nextSerialNo())
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getCode())
.cashAmountChange(usedBalance)
.giftAmountChange(usedGift)
.availableAmount(amount.add(giftAmount))
.amount(amount)
.giftAmount(giftAmount)
.build();
}
}
/**
* 创建第三方支付订单
*/
private void payByExternal() {
payOrderDO = new PayOrderDO();
Long payOrderId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
Long organId = SecurityFrameworkUtils.getUserOrganId();
payOrderDO.setId(payOrderId);
payOrderDO.setStatus(PayOrderStatusEnum.WAITING);
payOrderDO.setExpireTime(LocalDateTime.now().plus(payConfig.getPayExpireTime()));
payOrderDO.setPrice(price);
payOrderDO.setSubject(productsDO.getProductName());
payOrderDO.setOrganId(organId);
payOrderDO.setTradeType(TradeTypeEnum.PRODUCT.getCode());
}
/**
* 获取订购产品的到期时间
*
* @return
*/
public LocalDate getProductPayExpireTime() {
return endTime;
}
/**
* 计算结束时间
*
* @param duration
* @param durationUnit
* @return
*/
private LocalDate calculateEndTime(Integer duration, Integer durationUnit) {
LocalDate today = LocalDate.now();
// 1、 计算延期后的实际有效期
LocalDate maxDelayEnd = endTime;
if (CollUtil.isNotEmpty(delayRespVOList)) {
// 找到延期记录中最大的 delayTime 作为最终有效期
maxDelayEnd = delayRespVOList.stream()
.map(ProductDelayRespVO::getDelayTime)
.max(LocalDate::compareTo)
.orElse(endTime);
}
// 2、 计算有效期叠加的起点
LocalDate purchaseStart;
if (today.isAfter(maxDelayEnd)) {
// 已过期 → 从今天开始累加
purchaseStart = today;
} else {
// 未过期 → 从 maxDelayEnd 的下一天开始累加
purchaseStart = maxDelayEnd;
}
// 3、加正常时间
if (ObjectUtil.isNotNull(duration) && durationUnit != null) {
endTime = addTime(purchaseStart, duration, durationUnit);
}
// 4、扣掉延期天数
long delayDays = 0L;
if (CollUtil.isNotEmpty(delayRespVOList)) {
delayDays = calculateDelayDays(startTime, delayRespVOList);
}
if (delayDays > 0) {
endTime = endTime.minusDays(delayDays);
}
if (endTime.isBefore(today)) {
throw ServiceExceptionUtil.exception(PRODUCT_PURCHASE_ENDTIME_BEFORE_DELAYTIME_ERROR, LocalDateTimeUtil.format(endTime, DatePattern.NORM_DATE_PATTERN));
}
return endTime;
}
/**
* 计算总延期天数(合并区间,避免重叠/不连续)
*/
private static long calculateDelayDays(LocalDate startTime, List<ProductDelayRespVO> delayRecords) {
// 1. 生成延期区间并打上备注
List<DateIntervalDTO> intervals = buildIntervals(startTime, delayRecords);
if (CollUtil.isEmpty(intervals)) {
return 0L;
}
// 2. 合并区间
List<DateIntervalDTO> merged = mergeIntervals(intervals);
// 3. 计算天数
return calculateTotalDays(merged);
}
/**
* 根据延期记录生成扣减区间,同时写入备注
*/
private static List<DateIntervalDTO> buildIntervals(LocalDate startTime, List<ProductDelayRespVO> delayRecords) {
List<DateIntervalDTO> intervals = new ArrayList<>();
for (ProductDelayRespVO record : delayRecords) {
LocalDate delayStart = record.getEndTime().plusDays(1);
LocalDate delayEnd = record.getDelayTime();
if (delayEnd.isBefore(delayStart)) {
record.setRemark("无扣减");
continue; // 异常数据,忽略
}
LocalDate intervalEnd;
if (!delayEnd.isAfter(startTime)) {
// 情况1:延期结束在计算结束之前 → 整段都扣
intervalEnd = delayEnd;
} else {
// 情况2:延期结束晚于计算结束 → 扣到 startTime 为止
intervalEnd = startTime;
}
long deductDays = ChronoUnit.DAYS.between(delayStart, intervalEnd) + 1;
if (deductDays > 0) {
record.setRemark("扣减" + deductDays + "");
intervals.add(new DateIntervalDTO(delayStart, intervalEnd));
} else {
record.setRemark("无扣减");
}
}
return intervals;
}
/**
* 合并区间(避免重叠/相邻)
*/
private static List<DateIntervalDTO> mergeIntervals(List<DateIntervalDTO> intervals) {
intervals.sort(Comparator.comparing(DateIntervalDTO::getStart));
List<DateIntervalDTO> merged = new ArrayList<>();
DateIntervalDTO current = intervals.get(0);
for (int i = 1; i < intervals.size(); i++) {
DateIntervalDTO next = intervals.get(i);
if (!next.getStart().isAfter(current.getEnd().plusDays(1))) {
// 重叠或相邻 → 合并
current.setEnd(current.getEnd().isAfter(next.getEnd()) ? current.getEnd() : next.getEnd());
} else {
merged.add(current);
current = next;
}
}
merged.add(current);
return merged;
}
/**
* 计算合并区间的总天数
*/
private static long calculateTotalDays(List<DateIntervalDTO> merged) {
long totalDays = 0L;
for (DateIntervalDTO interval : merged) {
totalDays += ChronoUnit.DAYS.between(interval.getStart(), interval.getEnd()) + 1;
}
return totalDays;
}
/**
* 基于时长和单位累加time
*
* @param time
* @param duration
* @param unit
* @return
*/
private static LocalDate addTime(LocalDate time, int duration, int unit) {
switch (ProductDurationUnitEnum.fromStatus(unit)) {
case YEAR:
return time.plusYears(duration);
case MONTH:
return time.plusMonths(duration);
default:
return time;
}
}
/**
* 计算有首购记录的产品价格
*
* @return
*/
private BigDecimal computePrice() {
// 月均单价
monthAverageAmount = orgInitialPurchaseRecord.getMonthAverageAmount();
// 首购的定价
BigDecimal initialPurchaseRecordTotalAmount = orgInitialPurchaseRecord.getTotalAmount();
// 定价规则的折扣
BigDecimal discount = productsDetailDO.getDiscount();
if (ObjectUtil.isNull(discount)) {
discount = BigDecimal.valueOf(100);
}
BigDecimal totalAmount;
/**
* ①:续费订购满1年:
* 原始价格 = 订购总价 * duration
* 现价 = 原始价格 * 折扣 /100,保留两位小数
* ②:不满1年按月份计算
* 原始价格 = 月均单价 * duration
* 现价 = 原始价格 * 折扣 /100,保留两位小数
*/
switch (ProductDurationUnitEnum.fromStatus(productsDetailDO.getDurationUnit())) {
case YEAR -> {
originalPrice = initialPurchaseRecordTotalAmount
.multiply(BigDecimal.valueOf(productsDetailDO.getDuration()));
totalAmount = originalPrice
.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
}
case MONTH -> {
originalPrice = monthAverageAmount
.multiply(BigDecimal.valueOf(productsDetailDO.getDuration()));
totalAmount = originalPrice
.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
}
default -> throw new ServiceException(PRODUCTS_DETAIL_DURATION_UNIT_NOT_SUPPORT);
}
return totalAmount;
}
/**
* 计算首次订购的价格
*/
private BigDecimal computeInitialProductPayAmount() {
// 订购数量
Integer duration = productsDetailDO.getDuration();
// 产品订购价格
BigDecimal orgPurchasePrice = productsDO.getPrice();
// 折扣
BigDecimal discount = productsDetailDO.getDiscount();
if (ObjectUtil.isNull(discount)) {
discount = BigDecimal.valueOf(100);
}
BigDecimal totalAmount;
switch (ProductDurationUnitEnum.fromStatus(productsDetailDO.getDurationUnit())) {
case YEAR -> {
// 原价
originalPrice = orgPurchasePrice.multiply(BigDecimal.valueOf(duration));
// 折扣价格 = 原价 * 折扣
totalAmount = originalPrice
.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
// 月均单价 = 折扣价格 / 10
monthAverageAmount = originalPrice.divide(new BigDecimal(10), 2, RoundingMode.DOWN);
}
case MONTH -> {
// 月均单价 = 订购价格 / 10
monthAverageAmount = orgPurchasePrice.divide(new BigDecimal(10), 2, RoundingMode.DOWN);
// 原价
originalPrice = monthAverageAmount.multiply(BigDecimal.valueOf(duration));
// 折扣价格 = 原价 * 折扣
totalAmount = originalPrice.multiply(discount)
.divide(new BigDecimal(100), 2, RoundingMode.DOWN);
}
default -> throw new ServiceException(PRODUCTS_DETAIL_DURATION_UNIT_NOT_SUPPORT);
}
return totalAmount;
}
}
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cf.imes.module.system.dal.mysql.incomeandexpense.IncomeExpenseDetailsMapper">
<select id="getAccountDetailsPage" resultType="com.cf.imes.module.system.dal.dataobject.incomeandexpense.AccountDetailsDO">
select i.id, i.business_no, i.order_no ,i.trade_type , i.income_expense_type, i.cash_amount_change, i.gift_amount_change, i.creator, i.create_time, i.organ_id ,
p.total_amount, p.external_spent, p.account_balance_spent, p.gift_money_spent, p.payment_method, o.channel_code
from income_expense_details i
left join purchase_record p on i.purchase_id = p.id
left join pay_order o on i.order_no = o.id
where i.deleted = false
<if test="reqVO.organId != null">
AND i.organ_id = #{reqVO.organId}
</if>
<if test="reqVO.businessNo != null and reqVO.businessNo != ''">
AND i.business_no = #{reqVO.businessNo}
</if>
<if test="reqVO.orderNo != null and reqVO.orderNo != ''">
AND i.order_no = #{reqVO.orderNo}
</if>
<if test="reqVO.tradeType != null">
<choose>
<when test="reqVO.tradeType == @com.cf.imes.module.system.enums.pay.TradeTypeEnum@RECHARGE.code">
AND i.trade_type IN (
#{reqVO.tradeType},
'${@com.cf.imes.module.system.enums.pay.TradeTypeEnum@MANUAL_ADJUST.code}'
)
</when>
<otherwise>
AND i.trade_type = #{reqVO.tradeType}
</otherwise>
</choose>
</if>
<if test="reqVO.incomeExpenseType != null">
AND i.income_expense_type = #{reqVO.incomeExpenseType}
</if>
<if test="reqVO.createTime != null and reqVO.createTime.size() == 2">
AND i.create_time between #{reqVo.createTime[0]} and #{reqVo.createTime[1]}
</if>
<if test="reqVO.productPrice != null">
AND p.total_amount = #{reqVo.productPrice}
</if>
<if test="reqVO.rechargeAmount != null">
AND i.cash_amount_change = #{reqVo.rechargeAmount}
</if>
<if test="reqVO.giftAmount != null">
AND i.gift_amount_change = #{reqVo.giftAmount}
</if>
</select>
</mapper>
@@ -2,10 +2,11 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cf.imes.module.system.dal.mysql.pay.PayOrderMapper">
<select id="getRechargeDetailsPage" resultType="com.cf.imes.module.system.dal.dataobject.pay.PayOrderRechargeDetailDO">
select o.id, o.organ_id, o.pay_no, o.price, o.purchase_id, o.creator, o.create_time, p.total_amount, p.account_balance_spent, p.gift_money_spent
select o.id, o.organ_id, o.pay_no, o.price, o.purchase_id, o.channel_code, o.creator, o.create_time, p.total_amount, p.account_balance_spent, p.gift_money_spent, p.payment_method
from pay_order o
left join purchase_record p on o.purchase_id = p.id
where o.status = '${@com.cf.imes.module.system.enums.pay.PayOrderStatusEnum@SUCCESS.status}'
<if test="reqVO.organId != null">
AND o.organ_id = #{reqVO.organId}
</if>