1、新增组织购买产品接口、混合支付和异步回调;2、新增微信支付和回调基础代码;3、组织余额新增有效时长内容;4、新增过期支付单处理类;

This commit is contained in:
gaoqr
2025-08-19 17:34:07 +08:00
parent df1915989a
commit 8229f086dd
38 changed files with 1635 additions and 118 deletions
@@ -434,6 +434,7 @@ public class ErrorCodeConstants {
public static final ErrorCode RECHARGE_ACTIVE_NAME_IS_EXIST = new ErrorCode(1_002_043_022, "当前赠送金活动名称已存在,请修改");
public static final ErrorCode IMAGE_UPLOAD_ERROR = new ErrorCode(1_002_043_023, "图片上传失败,请重试或重新选择图片");
public static final ErrorCode ORGAN_RECHARGE_AMOUNT_UPDATE_ERROR = new ErrorCode(1_002_043_024, "组织充值失败,请检查组织资金数据");
public static final ErrorCode ORGAN_PRODUCT_PAY_AMOUNT_CHANGE_ERROR = new ErrorCode(1_002_043_025, "发起产品支付失败,组织资金已发生改变,请刷新或稍后再发起支付");
//=========== 支付相关 1-002-048-000 ============
public static final ErrorCode PAY_CHANNEL_NOT_SUPPORT = new ErrorCode(1_002_048_000, "暂不支持的支付渠道,请使用其他支付渠道");
@@ -446,6 +447,9 @@ public class ErrorCodeConstants {
public static final ErrorCode PAY_RECHARGE_LOCK_EXISTS_ERROR = new ErrorCode(1_002_048_007, "组织下存在待支付的充值订单,请稍后再试");
public static final ErrorCode PAY_NOTIFY_PASSBACK_NOT_EXISTS_ERROR = new ErrorCode(1_002_048_008, "支付回调缺少passback_params,请检查支付渠道");
public static final ErrorCode PAY_NOTIFY_PASSBACK_TRADETYPE_NOT_SUPPORT_ERROR = new ErrorCode(1_002_048_009, "不支持的支付回调tradeType:【{}】,请检查支付渠道");
public static final ErrorCode PAY_PRODUCT_BALANCE_INSUFFICIENT = new ErrorCode(1_002_048_010, "现金余额不足,请选择其他支付方式!");
public static final ErrorCode PAY_PRODUCT_GIFT_BALANCE_INSUFFICIENT = new ErrorCode(1_002_048_010, "赠送金余额不足,请选择其他支付方式!");
public static final ErrorCode PAY_PRODUCT_AVAILABLE_BALANCE_INSUFFICIENT = new ErrorCode(1_002_048_010, "可用金(现金余额+赠送金)余额不足,请选择其他支付方式!");
//=========== 产品相关 1-002-045-000 ============
public static final ErrorCode PRODUCTS_NO_EXIST = new ErrorCode(1_002_045_001, "当前产品不存在");
@@ -48,4 +48,10 @@ public enum DurationUnitEnum implements IntArrayValuable {
return Objects.equals(DAY.name, name);
}
public static DurationUnitEnum fromStatus(Integer status) {
return Arrays.stream(values())
.filter(bean -> Objects.equals(bean.status, status))
.findFirst()
.orElse(null);
}
}
@@ -49,6 +49,10 @@
<groupId>com.cf.imes</groupId>
<artifactId>cf-spring-boot-starter-biz-ip</artifactId>
</dependency>
<dependency>
<groupId>com.cf.imes</groupId>
<artifactId>cf-spring-boot-starter-biz-id</artifactId>
</dependency>
<!-- Web 相关 -->
@@ -244,6 +248,10 @@
<groupId>com.cf.imes</groupId>
<artifactId>cf-spring-boot-starter-biz-ip</artifactId>
</include>
<include>
<groupId>com.cf.imes</groupId>
<artifactId>cf-spring-boot-starter-biz-id</artifactId>
</include>
<include>
<groupId>com.cf.imes</groupId>
<artifactId>cf-spring-boot-starter-biz-operatelog</artifactId>
@@ -21,5 +21,20 @@ public class OrganAmountRespVO {
@Schema(description = "产品有效期")
private String productEfficientDate;
/**
* 正常时间,不加赠送时间
*/
@Schema(description = "产品时长")
private String duration;
@Schema(description = "产品时长单位")
private Integer durationUnit;
@Schema(description = "赠送时长")
private String giftDuration;
@Schema(description = "赠送时长单位")
private Integer giftDurationUnit;
}
@@ -0,0 +1,29 @@
package com.cf.imes.module.system.controller.admin.funds.organamount.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author Gqr
* @since 2025/8/14 15:20
*/
@Data
@AllArgsConstructor
public class OrganPayAmountRespVO {
/**
* 可用余额
*/
private BigDecimal availableAmount;
/**
* 现金余额
*/
private BigDecimal amount;
/**
* 赠送金余额
*/
private BigDecimal giftAmount;
}
@@ -1,6 +1,5 @@
package com.cf.imes.module.system.controller.admin.funds.organamount.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -10,7 +9,6 @@ import java.math.BigDecimal;
* @author Gqr
* @since 2025/8/13 16:46
*/
@Schema(description = "管理后台 - 组织充值后余额信息 Response VO")
@Data
@AllArgsConstructor
public class OrganRechargeAmountRespVO {
@@ -0,0 +1,36 @@
package com.cf.imes.module.system.controller.admin.funds.products.vo.product;
import com.cf.imes.module.system.dal.dataobject.funds.organamount.OrganAmountDO;
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 lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author Gqr
* @since 2025/8/14 15:22
*/
@Data
@AllArgsConstructor
public class ProductProcessorRespVO {
/**
* 购买记录
*/
private PurchaseRecordDO purchaseRecordDO;
/**
* 收支明细
*/
private IncomeExpenseDetailsDO incomeExpenseDetailsDO;
/**
* 外部支付信息
*/
private PayOrderDO payOrderDO;
/**
* 组织资金
*/
private OrganAmountDO organAmountDO;
}
@@ -0,0 +1,40 @@
package com.cf.imes.module.system.controller.admin.funds.purchase;
import com.cf.imes.module.system.controller.admin.funds.purchase.vo.OrgEarliestPurchaseProductRespVO;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
/**
* @author 软件购买接口
*/
public interface PurchaseService {
/**
* 查询购买记录
*
* @param id
* @return
*/
PurchaseRecordDO getRecord(Long id);
/**
* 创建购买记录
*
* @param purchaseRecordDO
*/
void createRecord(PurchaseRecordDO purchaseRecordDO);
/**
* 更新购买记录为可开票
*
* @param purchaseRecordId
* @param organId
*/
void updateRecordInvoicable(Long purchaseRecordId, Long organId);
/**
* 查询组织下最早购买产品定价规则
*
* @param organId
* @return
*/
OrgEarliestPurchaseProductRespVO getOrgEarliestProductDetail(Long organId);
}
@@ -0,0 +1,61 @@
package com.cf.imes.module.system.controller.admin.funds.purchase;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cf.imes.module.system.controller.admin.funds.purchase.vo.OrgEarliestPurchaseProductRespVO;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import com.cf.imes.module.system.dal.mysql.funds.products.ProductsDetailMapper;
import com.cf.imes.module.system.dal.mysql.funds.purchase.PurchaseRecordMapper;
import com.cf.imes.module.system.enums.pay.InvoiceStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* @author 软件购买接口 实现类
*/
@Slf4j
@Service
public class PurchaseServiceImpl implements PurchaseService {
@Resource
private PurchaseRecordMapper purchaseRecordMapper;
@Resource
private ProductsDetailMapper productsDetailMapper;
@Override
public PurchaseRecordDO getRecord(Long id) {
return purchaseRecordMapper.selectById(id);
}
@Override
public void createRecord(PurchaseRecordDO purchaseRecordDO) {
purchaseRecordMapper.insert(purchaseRecordDO);
}
@Override
public void updateRecordInvoicable(Long purchaseRecordId, Long organId) {
purchaseRecordMapper.update(new LambdaUpdateWrapper<PurchaseRecordDO>()
.eq(PurchaseRecordDO::getId, purchaseRecordId)
.eq(PurchaseRecordDO::getOrganId, organId)
.set(PurchaseRecordDO::getIsInvocing, InvoiceStatusEnum.INVOICABLE.getStatus()));
}
@Override
public OrgEarliestPurchaseProductRespVO getOrgEarliestProductDetail(Long organId) {
// 查询购买记录中还有效的最早一条
PurchaseRecordDO purchaseRecordDO = purchaseRecordMapper.selectOne(new LambdaQueryWrapper<PurchaseRecordDO>()
.eq(PurchaseRecordDO::getOrganId, organId)
.gt(PurchaseRecordDO::getEndTime, LocalDateTime.now())
.orderByAsc(PurchaseRecordDO::getCreateTime)
.last("limit 1"));
if (ObjectUtil.isNotNull(purchaseRecordDO)) {
return new OrgEarliestPurchaseProductRespVO(purchaseRecordDO, productsDetailMapper.selectById(purchaseRecordDO.getProductDetailsId()));
}
return null;
}
}
@@ -0,0 +1,26 @@
package com.cf.imes.module.system.controller.admin.funds.purchase.vo;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDetailDO;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 查询组织下最早购买产品记录和关联定价规则 response vo
*
* @author Gqr
* @since 2025/8/19 17:10
*/
@Data
@AllArgsConstructor
public class OrgEarliestPurchaseProductRespVO {
/**
* 购买记录
*/
private PurchaseRecordDO purchaseRecordDO;
/**
* 产品定价规则
*/
private ProductsDetailDO productsDetailDO;
}
@@ -2,6 +2,7 @@ package com.cf.imes.module.system.controller.admin.pay;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
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.service.pay.PayOrderService;
import io.swagger.v3.oas.annotations.Operation;
@@ -44,12 +45,12 @@ public class PayController {
return success(payOrderRespDTO);
}
// @PostMapping("/product")
// @Operation(summary = "创建产品支付订单")
// public CommonResult<PayOrderRespDTO> initiatePay(@RequestBody PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) {
// payOrderService.productPay(payProductOrderUnifiedReqVO);
// return success(null);
// }
@PostMapping("/product")
@Operation(summary = "创建产品支付订单")
public CommonResult<PayOrderRespDTO> initiatePay(@RequestBody PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) {
PayOrderRespDTO payOrderRespDTO = payOrderService.productPay(payProductOrderUnifiedReqVO);
return success(payOrderRespDTO);
}
@GetMapping("/order/{channelCode}/{outTradeNo}")
@Operation(summary = "查询支付订单")
@@ -20,11 +20,14 @@ public class PayProductOrderUnifiedReqVO {
* 产品定价规则编号
*/
@Schema(description = "产品定价规则编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotEmpty(message = "产品定价规则编号不能为空")
@NotNull(message = "产品定价规则编号不能为空")
private Long productDetailsId;
@Schema(description = "支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@Schema(description = "产品支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@PayProductChannelCodeValid
@NotNull(message = "支付渠道编码不能为空")
private Integer channelCode;
@NotNull(message = "产品支付渠道编码不能为空")
private Integer productChannelCode;
@Schema(description = "机构编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long organId;
}
@@ -0,0 +1,167 @@
package com.cf.imes.module.system.dal.dataobject.funds.purchase;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cf.imes.framework.mybatis.core.dataobject.BaseDO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* @author 购买记录表
*/
@TableName("purchase_record")
@KeySequence("purchase_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PurchaseRecordDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 组织ID
*/
private Long organId;
// /**
// * 用户ID-发票申请人ID
// */
// private Long userId;
//
//
// /**
// * 用户姓名
// */
// private Long userName;
/**
* 产品ID
*/
private Long productId;
/**
* 产品明细ID
*/
private Long productDetailsId;
/**
* 产品名称
*/
private String productName;
/**
* 购买时长
*/
private String purchaseDuration;
/**
* 购买时长单位
*/
private Integer purchaseDurationUnit;
/**
* 赠送时长
*/
private String giftDuration;
/**
* 赠送时间单位
*/
private Integer giftDurationUnit;
/**
* 产品开始时间
*/
private LocalDateTime startTime;
/**
* 产品结束时间
*/
private LocalDateTime endTime;
/**
* 支付方式
*/
private Integer paymentMethod;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 现金余额支出
*/
private BigDecimal accountBalanceSpent;
/**
* 支付宝支出
*/
private BigDecimal alipaySpent;
/**
* 微信支出
*/
private BigDecimal wechatSpent;
/**
* 赠送金
*/
private BigDecimal giftMoneySpent;
/**
* 货币类型
*/
private Integer currency;
/**
* 货币单位
*/
private String currencyUnit;
/**
* 是否开票
*/
private Integer isInvocing;
}
@@ -78,7 +78,7 @@ public class IncomeExpenseDetailsDO extends BaseDO {
/**
* 支出/入账赠送金金额
*/
private BigDecimal bonusAmountChange;
private BigDecimal giftAmountChange;
/**
@@ -0,0 +1,12 @@
package com.cf.imes.module.system.dal.mysql.funds.purchase;
import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX;
import com.cf.imes.module.system.dal.dataobject.funds.purchase.PurchaseRecordDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 购买记录 Mapper
*/
@Mapper
public interface PurchaseRecordMapper extends BaseMapperX<PurchaseRecordDO> {
}
@@ -0,0 +1,63 @@
package com.cf.imes.module.system.enums.pay;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 发票状态枚举类型
*/
@Getter
@AllArgsConstructor
public enum InvoiceStatusEnum {
/**
* 可开票
*/
INVOICABLE(0, "可开票"),
/**
* 待开票
*/
PENDINGINVOICING(1, "待开票"),
/**
* 已开票
*/
INVOICINGSUCCESSFUL(2,"已开票"),
/**
* 开票失败
*/
INVOICINGFAILED(3,"开票失败");
/**
* 状态
*/
private final Integer status;
/**
* 名称
*/
private final String name;
public static InvoiceStatusEnum fromType(Integer status) {
if (status == null) {
return null;
}
for (InvoiceStatusEnum value : values()) {
if (ObjectUtil.equal(value.getStatus(), status)) {
return value;
}
}
return null;
}
}
@@ -2,10 +2,16 @@ package com.cf.imes.module.system.enums.pay;
/**
* 支付常量
*
* @author Gqr
* @since 2025/8/12 17:49
*/
public class PayConstants {
public static final String PAY_PASSBACK_PARAMS_TRADE_TYPE_KEY = "tradeType"; //支付passback 交易类型key
public static final String PAY_PASSBACK_PARAMS_KEY = "passback_params";// 回调参数passback key
public static final String PAY_ORDER_PREFIX_FORMAT = "P%dO%s%s";
public static final String PAY_PASSBACK_PARAMS_PURCHASE_RECORD_ID_KEY = "purchaseRecordId"; //支付passback 购买记录key
}
@@ -61,6 +61,6 @@ public enum PayOrderStatusEnum {
* @return 是否支付关闭
*/
public static boolean isClosed(Integer status) {
return Objects.equals(status, SUCCESS.getStatus()) || Objects.equals(status, FAIL.getStatus());
return Objects.equals(status, FAIL.getStatus());
}
}
@@ -14,34 +14,62 @@ import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PayProductChannelCodeEnum {
/**
* 账户余额
*/
BALANCE_ONLY(0),
/**
* 赠送金
*/
GIFT_ONLY(1),
/**
* 支付宝
*/
ALIPAY_PC(0),
ALIPAY_PC(2),
/**
* 微信
*/
WECHAT_NATIVE(1),
WECHAT_NATIVE(3),
/**
* 账户余额
* 现金+赠送金
*/
BALANCE_ONLY(2),
BALANCE_AND_GIFT(4),
/**
* 账户余额+支付宝
* 现金+支付宝
*/
BALANCE_AND_ALIPAY(3),
BALANCE_AND_ALIPAY(5),
/**
* 账户余额+微信
* 现金+微信
*/
BALANCE_AND_WECHAT(4);
BALANCE_AND_WECHAT(6),
/**
* 赠送金+支付宝
*/
GIFT_AND_ALIPAY(7),
/**
* 赠送金+微信
*/
GIFT_AND_WECHAT(8),
/**
* 现金+赠送金+支付宝
*/
BALANCE_AND_GIFT_AND_ALIPAY(9),
/**
* 现金+赠送金+微信
*/
BALANCE_AND_GIFT_AND_WECHAT(10);
@EnumValue
private final Integer code;
@JsonCreator
public static PayProductChannelCodeEnum fromType(Integer type) {
for (PayProductChannelCodeEnum value : values()) {
if (value.getCode().equals(type)) {
@@ -50,4 +78,14 @@ public enum PayProductChannelCodeEnum {
}
return null;
}
/**
* 产品购买渠道是否包含支付宝支付方式
*
* @param code
* @return
*/
public static boolean isAliPay(Integer code) {
return code.equals(ALIPAY_PC.code) || code.equals(BALANCE_AND_ALIPAY.code) || code.equals(GIFT_AND_ALIPAY.code) || code.equals(BALANCE_AND_GIFT_AND_ALIPAY.code);
}
}
@@ -1,6 +0,0 @@
/**
* 属于 system 模块的 framework 封装
*
* @author 晨丰科技
*/
package com.cf.imes.module.system.framework;
@@ -0,0 +1,19 @@
package com.cf.imes.module.system.framework.snowflake.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* mybatis id策略配置类
*
* @author Gqr
* @since 2025/8/18 17:33
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "mybatis-plus.global-config.db-config")
public class MybatisIdProperties {
private long workerId;
private long datacenterId;
}
@@ -0,0 +1,6 @@
/**
* 属于 system 模块的 framework.snowflake 封装
*
* @author 晨丰科技
*/
package com.cf.imes.module.system.framework.snowflake;
@@ -0,0 +1,31 @@
package com.cf.imes.module.system.job.pay;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.cf.imes.module.system.service.pay.PayOrderService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 支付订单的过期 Job
*
* 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
*
* @author
*/
@Component
@Slf4j
public class PayOrderExpireJob {
@Resource
private PayOrderService payOrderService;
// @Scheduled(cron = "0 0/1 * * * ?")
public String execute() {
int count = payOrderService.expirePayOrder();
log.info("[execute][支付过期 ({}) 个]", count);
return CharSequenceUtil.format("支付过期 ({}) 个",count);
}
}
@@ -5,6 +5,7 @@ import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.Assert.AssertUtils;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.id.core.util.SnowflakeIdWorker3rd;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.module.system.controller.admin.funds.advertisement.vo.*;
import com.cf.imes.module.system.dal.dataobject.funds.advertisement.AdvertisementDO;
@@ -13,7 +14,6 @@ import com.cf.imes.module.system.dal.mysql.funds.advertisement.AdvertisementTask
import com.cf.imes.module.system.enums.advertisement.AdvertisementStatusEnum;
import com.cf.imes.module.system.mq.producer.advertisement.AdvertisementProducer;
import com.cf.imes.module.system.service.dict.DictDataService;
import com.cf.imes.module.system.util.SnowflakeIdWorker3rd;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
@@ -86,7 +86,7 @@ public class AdvertisementServiceImpl implements AdvertisementService{
AssertUtils.empty(aDo,ADVERTISEMENT_NAME_IS_EXIST);
Long id = idWorker.nextId();
long id = idWorker.nextId();
String filePath = "";
if(createReqVO.getAdImageFile() != null) {
@@ -21,7 +21,7 @@ public interface OrganAmountService {
OrganAmountDO validOrganAmount(Long organId);
/**
* 更新机构余额
* 机构充值
*
* @param organId
* @param amount 充值金额
@@ -29,6 +29,23 @@ public interface OrganAmountService {
*/
OrganRechargeAmountRespVO recharge(Long organId, BigDecimal amount);
/**
* 组织购买产品
*
* @param organAmountDO
* @return
*/
void productPay(OrganAmountDO organAmountDO);
/**
* 产品外部支付失败更新余额/赠送金
*
* @param organId
* @param balance
* @param gift
*/
OrganRechargeAmountRespVO refundProductOrganAmountBalanceAndGift(Long organId, BigDecimal balance, BigDecimal gift);
OrganAmountRespVO getOrganAmount(Long organId);
@@ -1,15 +1,23 @@
package com.cf.imes.module.system.service.funds.organamount;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.util.Assert.AssertUtils;
import com.cf.imes.framework.common.util.date.DateUtils;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.module.executor.api.funds.ExecutorFundsApi;
import com.cf.imes.module.system.controller.admin.funds.organamount.vo.AllOrganAmountRespVO;
import com.cf.imes.module.system.controller.admin.funds.organamount.vo.OrganAmountRespVO;
import com.cf.imes.module.system.controller.admin.funds.organamount.vo.OrganRechargeAmountRespVO;
import com.cf.imes.module.system.controller.admin.funds.purchase.PurchaseService;
import com.cf.imes.module.system.controller.admin.funds.purchase.vo.OrgEarliestPurchaseProductRespVO;
import com.cf.imes.module.system.dal.dataobject.funds.organamount.OrganAmountDO;
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.mysql.funds.organamount.OrganAmountMapper;
import com.cf.imes.module.system.service.organ.OrganService;
import lombok.extern.slf4j.Slf4j;
@@ -19,8 +27,10 @@ import jakarta.annotation.Resource;
import java.math.BigDecimal;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_AMOUNT_NO_EXIST;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_PRODUCT_PAY_AMOUNT_CHANGE_ERROR;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_RECHARGE_AMOUNT_UPDATE_ERROR;
/**
@@ -37,10 +47,11 @@ public class OrganAmountServiceImpl implements OrganAmountService {
@Resource
private OrganService organService;
@Resource
private ExecutorFundsApi executorFundsApi;
@Resource
private PurchaseService purchaseService;
@Override
public OrganAmountDO validOrganAmount(Long organId) {
@@ -85,6 +96,54 @@ public class OrganAmountServiceImpl implements OrganAmountService {
}
}
@Override
public void productPay(OrganAmountDO organAmountDO) {
int updated = organAmountMapper.update(new LambdaUpdateWrapper<OrganAmountDO>()
.eq(OrganAmountDO::getOrganId, organAmountDO.getOrganId())
.eq(OrganAmountDO::getVersion, organAmountDO.getVersion())
.set(OrganAmountDO::getAmount, organAmountDO.getAmount())
.set(OrganAmountDO::getGiftAmount, organAmountDO.getGiftAmount())
.set(OrganAmountDO::getVersion, organAmountDO.getVersion() + 1));
if (updated == 0) {
throw new ServiceException(ORGAN_PRODUCT_PAY_AMOUNT_CHANGE_ERROR);
}
}
@Override
public OrganRechargeAmountRespVO refundProductOrganAmountBalanceAndGift(Long organId, BigDecimal balance, BigDecimal gift) {
OrganAmountDO organAmountDO = organAmountMapper.selectOne(new LambdaQueryWrapper<OrganAmountDO>().eq(OrganAmountDO::getOrganId, organId));
BigDecimal newAmount = organAmountDO.getAmount().add(balance);
BigDecimal newGift = organAmountDO.getGiftAmount().add(gift);
int updated = organAmountMapper.update(new LambdaUpdateWrapper<OrganAmountDO>()
.eq(OrganAmountDO::getOrganId, organId)
.eq(OrganAmountDO::getVersion, organAmountDO.getVersion())
.set(OrganAmountDO::getAmount, newAmount)
.set(OrganAmountDO::getGiftAmount, newGift)
.set(OrganAmountDO::getVersion, organAmountDO.getVersion() + 1));
// 1. 首次更新失败,认为出现并发,再查一次再更新
if (updated == 0) {
organAmountDO = organAmountMapper.selectOne(new LambdaQueryWrapper<OrganAmountDO>().eq(OrganAmountDO::getOrganId, organId));
newAmount = organAmountDO.getAmount().add(balance);
newGift = organAmountDO.getGiftAmount().add(gift);
updated = organAmountMapper.update(new LambdaUpdateWrapper<OrganAmountDO>()
.eq(OrganAmountDO::getOrganId, organId)
.eq(OrganAmountDO::getVersion, organAmountDO.getVersion())
.set(OrganAmountDO::getAmount, newAmount)
.set(OrganAmountDO::getGiftAmount, newGift)
.set(OrganAmountDO::getVersion, organAmountDO.getVersion() + 1));
// 2. 第二次更新失败,资金表可能不存在
if (updated == 0) {
throw exception(ORGAN_RECHARGE_AMOUNT_UPDATE_ERROR);
} else {
return new OrganRechargeAmountRespVO(newAmount.add(newGift), newAmount, newGift);
}
} else {
return new OrganRechargeAmountRespVO(newAmount.add(newGift), newAmount, newGift);
}
}
@Override
public OrganAmountRespVO getOrganAmount(Long organId) {
@@ -94,6 +153,20 @@ public class OrganAmountServiceImpl implements OrganAmountService {
OrganAmountRespVO organAmountRespVO = BeanUtils.toBean(organAmountDO, OrganAmountRespVO.class);
// 查询组织最早还有效的产品购买记录
OrgEarliestPurchaseProductRespVO orgEarliestProductDetail = purchaseService.getOrgEarliestProductDetail(organId);
if (ObjectUtil.isNotNull(orgEarliestProductDetail)) {
// 购买记录
PurchaseRecordDO purchaseRecordDO = orgEarliestProductDetail.getPurchaseRecordDO();
// 购买记录关联的定价规则
ProductsDetailDO productsDetailDO = orgEarliestProductDetail.getProductsDetailDO();
organAmountRespVO.setProductEfficientDate(LocalDateTimeUtil.format(purchaseRecordDO.getEndTime(), NORM_DATETIME_FORMATTER));
organAmountRespVO.setDuration(productsDetailDO.getDuration());
organAmountRespVO.setDurationUnit(productsDetailDO.getDurationUnit());
organAmountRespVO.setGiftDuration(productsDetailDO.getGiftDuration());
organAmountRespVO.setGiftDurationUnit(productsDetailDO.getGiftDurationUnit());
}
return organAmountRespVO;
}
@@ -3,6 +3,7 @@ package com.cf.imes.module.system.service.funds.products;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.module.system.controller.admin.funds.products.vo.productDetails.ProductDetailsPageReqVO;
import com.cf.imes.module.system.controller.admin.funds.products.vo.productDetails.ProductDetailsSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDO;
import com.cf.imes.module.system.dal.dataobject.funds.products.ProductsDetailDO;
import jakarta.validation.Valid;
@@ -65,5 +66,5 @@ public interface ProductsDetailService {
*
* @param productId
*/
void validateProductExist(Long productId);
ProductsDO validateProductExist(Long productId);
}
@@ -170,7 +170,7 @@ public class ProductsDetailServiceImpl implements ProductsDetailService {
* @param productId
*/
@Override
public void validateProductExist(Long productId) {
public ProductsDO validateProductExist(Long productId) {
ProductsDO productsDO = productsMapper.selectOne(new LambdaQueryWrapperX<ProductsDO>()
.eq(ProductsDO::getId, productId)
.eq(ProductsDO::getDeleted, false));
@@ -180,6 +180,8 @@ public class ProductsDetailServiceImpl implements ProductsDetailService {
if (productsDO.getStatus().equals(ProductStatusEnum.PRODUCT_UPDATES.getStatus())) {
throw exception(PRODUCTS_DETAIL_NO_UPDATE);
}
return productsDO;
}
@@ -3,12 +3,12 @@ package com.cf.imes.module.system.service.funds.recharge;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.Assert.AssertUtils;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.id.core.util.SnowflakeIdWorker3rd;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.module.system.controller.admin.funds.recharge.vo.RechargePageReqVO;
import com.cf.imes.module.system.controller.admin.funds.recharge.vo.RechargeSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.funds.recharge.GiftMoneyDetailsDO;
import com.cf.imes.module.system.dal.mysql.funds.recharge.GiftMoneyDetailsMapper;
import com.cf.imes.module.system.util.SnowflakeIdWorker3rd;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -36,7 +36,7 @@ public class RechargeActiveServiceImpl implements RechargeActiveService{
@Override
public Long create(RechargeSaveReqVO createReqVO) {
Long id = idWorker.nextId();
long id = idWorker.nextId();
GiftMoneyDetailsDO giftMoneyDetailsDO = giftMoneyDetailsMapper.selectOne(new LambdaQueryWrapperX<GiftMoneyDetailsDO>()
.eq(GiftMoneyDetailsDO::getDeleted, false)
@@ -18,7 +18,7 @@ public interface PayOrderService {
*
* @param payProductOrderUnifiedReqVO
*/
void productPay(PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO);
PayOrderRespDTO productPay(PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO);
/**
* 账户充值支付
@@ -44,4 +44,11 @@ public interface PayOrderService {
* @return
*/
void doOrderNotify(Integer channelCode, Map<String, String> params, String body);
/**
* 处理过期订单
*
* @return
*/
int expirePayOrder();
}
@@ -15,6 +15,7 @@ import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.util.Assert.AssertUtils;
import com.cf.imes.framework.common.util.json.JsonUtils;
import com.cf.imes.framework.common.util.validation.ValidationUtils;
import com.cf.imes.framework.id.core.util.SnowflakeIdWorker3rd;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.pay.config.ChenfengAlipayConfig;
import com.cf.imes.framework.pay.config.ChenfengPayConfig;
@@ -23,16 +24,22 @@ import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.controller.admin.funds.organamount.vo.OrganAmountRespVO;
import com.cf.imes.module.system.controller.admin.funds.organamount.vo.OrganRechargeAmountRespVO;
import com.cf.imes.module.system.controller.admin.funds.products.vo.product.ProductProcessorRespVO;
import com.cf.imes.module.system.controller.admin.funds.purchase.PurchaseService;
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.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.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.framework.snowflake.config.MybatisIdProperties;
import com.cf.imes.module.system.service.funds.incomeandexpense.IncomeExpenseService;
import com.cf.imes.module.system.service.funds.organamount.OrganAmountService;
import com.cf.imes.module.system.service.funds.products.ProductsDetailService;
@@ -52,6 +59,7 @@ import jakarta.validation.Validator;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -77,6 +85,9 @@ import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_RECHARGE_OR
@Slf4j
public class PayOrderServiceImpl implements PayOrderService {
// 外部payNo正则
private static final Pattern pattern = Pattern.compile("P(\\d+)O");
@Resource
private PayHandlerFactory payHandlerFactory;
@@ -86,8 +97,6 @@ public class PayOrderServiceImpl implements PayOrderService {
@Resource
private Validator validator;
private static final String PAY_ORDER_PREFIX_FORMAT = "P%dO%s%s";
@Resource
private ChenfengAlipayConfig alipayConfig;
@@ -103,18 +112,78 @@ public class PayOrderServiceImpl implements PayOrderService {
@Resource
private IncomeExpenseService incomeExpenseService;
@Resource
private PurchaseService purchaseService;
@Resource
private SnowflakeIdWorker3rd snowflakeIdWorker3rd;
@Resource
private MybatisIdProperties mybatisIdProperties;
@Override
public void productPay(PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) {
@Transactional(rollbackFor = Exception.class)
public PayOrderRespDTO productPay(PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) {
Long organId = payProductOrderUnifiedReqVO.getOrganId();
organId = ObjectUtil.isNull(organId) ? SecurityFrameworkUtils.getUserOrganId() : organId;
// 主动校验一次入参
ValidationUtils.validate(validator, payProductOrderUnifiedReqVO);
// 校验产品和产品明细
ProductsDetailDO productsDetailDO = productsDetailService.validateProductDetailsExists(payProductOrderUnifiedReqVO.getProductDetailsId());
productsDetailService.validateProductExist(productsDetailDO.getProductId());
ProductsDO productsDO = productsDetailService.validateProductExist(productsDetailDO.getProductId());
// 查询组织余额信息
OrganAmountRespVO organAmount = organAmountService.getOrganAmount(SecurityFrameworkUtils.getUserOrganId());
OrganAmountDO organAmount = organAmountService.validOrganAmount(organId);
// 根据购买类型做价格处理
PayProductProcessor payProductProcessor = new PayProductProcessor(productsDO, productsDetailDO, organAmount, organId, snowflakeIdWorker3rd, payConfig, mybatisIdProperties);
ProductProcessorRespVO productProcessorRespVO = payProductProcessor.processPayment(PayProductChannelCodeEnum.fromType(payProductOrderUnifiedReqVO.getProductChannelCode()));
// 记录入库,如果有外部支付发起请求二维码
// 购买记录入库
purchaseService.createRecord(productProcessorRespVO.getPurchaseRecordDO());
// 收支明细入库
IncomeExpenseDetailsDO incomeExpenseDetailsDO = productProcessorRespVO.getIncomeExpenseDetailsDO();
if (ObjectUtil.isNotNull(incomeExpenseDetailsDO)) {
incomeExpenseService.createDetail(incomeExpenseDetailsDO);
}
// 更新组织资金,纯第三方支付不需要更新余额
OrganAmountDO finalOrganAmountDO = productProcessorRespVO.getOrganAmountDO();
if (ObjectUtil.isNotNull(finalOrganAmountDO)) {
organAmountService.productPay(finalOrganAmountDO);
}
// 如果有外部支付,发起请求
PayOrderDO payOrder = productProcessorRespVO.getPayOrderDO();
if(ObjectUtil.isNotNull(payOrder)) {
payOrderMapper.insert(payOrder);
// 拼接充值个性化参数
Map<String, Object> passbackParams = new HashMap<>();
passbackParams.put(PayConstants.PAY_PASSBACK_PARAMS_TRADE_TYPE_KEY, TradeTypeEnum.PRODUCT.getCode());// 交易类型
passbackParams.put(PayConstants.PAY_PASSBACK_PARAMS_PURCHASE_RECORD_ID_KEY, productProcessorRespVO.getPurchaseRecordDO().getId());// 购买记录id,用于更新开票状态
// 发起渠道方支付
PayOrderUnifiedReqDTO payOrderUnifiedReqDTO = null;
Integer channelCode = PayProductChannelCodeEnum.isAliPay(payProductOrderUnifiedReqVO.getProductChannelCode()) ? PayChannelCodeEnum.ALIPAY_PC.getCode() : PayChannelCodeEnum.WECHAT_NATIVE.getCode();
try {
// 获取渠道方处理器
AbstractPayHandler payHandler = payHandlerFactory.getPayHandler(channelCode);
payOrderUnifiedReqDTO = createPayOrderUnifiedReqDTO(payOrder, passbackParams);
return payHandler.unifiedOrder(payOrderUnifiedReqDTO);
} catch (Throwable e) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
channelCode, JSON.toJSONString(payOrderUnifiedReqDTO), e);
throw new PayException(PAY_ORDER_SUBMIT_CHANNEL_ERROR);
}
}
return null;
}
@Override
@@ -164,11 +233,11 @@ public class PayOrderServiceImpl implements PayOrderService {
private PayOrderDO createPayOrder(BigDecimal price, String subject, Integer channelCode) {
PayOrderDO payOrderDO = new PayOrderDO();
Long payOrderId = IdUtil.getSnowflake(0, 0).nextId();
Long payOrderId = IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId();
Long organId = SecurityFrameworkUtils.getUserOrganId();
payOrderDO.setId(payOrderId);
payOrderDO.setPayNo(String.format(PAY_ORDER_PREFIX_FORMAT, organId, payOrderId, DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN)));
payOrderDO.setPayNo(String.format(PayConstants.PAY_ORDER_PREFIX_FORMAT, organId, payOrderId, DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN)));
payOrderDO.setStatus(PayOrderStatusEnum.WAITING);
payOrderDO.setExpireTime(LocalDateTime.now().plus(payConfig.getPayExpireTime()));
payOrderDO.setPrice(price);
@@ -276,7 +345,6 @@ public class PayOrderServiceImpl implements PayOrderService {
* @return
*/
private Long getOrganIdFromPayNo(String outTradeNo) {
Pattern pattern = Pattern.compile("P(\\d+)O");
Matcher matcher = pattern.matcher(outTradeNo);
if (matcher.find()) {
@@ -333,6 +401,7 @@ public class PayOrderServiceImpl implements PayOrderService {
notifyRechargeOrderSuccess(order, organId);
break;
case PRODUCT:
notifyProductPayOrderSuccess(organId, passbackParams);
break;
default:
throw ServiceExceptionUtil.exception(PAY_NOTIFY_PASSBACK_TRADETYPE_NOT_SUPPORT_ERROR, passbackObj.toString());
@@ -341,6 +410,7 @@ public class PayOrderServiceImpl implements PayOrderService {
/**
* 充值成功回调
*
* @param payOrderDO
* @param organId
*/
@@ -369,6 +439,19 @@ public class PayOrderServiceImpl implements PayOrderService {
incomeExpenseService.createDetail(incomeExpenseDetailsDO);
}
/**
* 产品购买成功回调
* @param organId
* @param passbackParams
*/
private void notifyProductPayOrderSuccess(Long organId, Map<String, Object> passbackParams) {
// 1、更新购买记录的开票状态
Object purchaseRecordIdObj = passbackParams.get(PayConstants.PAY_PASSBACK_PARAMS_PURCHASE_RECORD_ID_KEY);
if (ObjectUtil.isNotNull(purchaseRecordIdObj)) {
purchaseService.updateRecordInvoicable(NumberUtil.parseLong(purchaseRecordIdObj.toString()), organId);
}
}
/**
* 支付失败回调处理
*
@@ -413,6 +496,64 @@ public class PayOrderServiceImpl implements PayOrderService {
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.debug("[notifyOrderClosed][order({}) 更新为支付关闭]", order.getId());
// 3. 根据交易类型做对应回调处理
Map<String, Object> passbackParams = notify.getPassbackParams();
Object passbackObj = passbackParams.get(PayConstants.PAY_PASSBACK_PARAMS_TRADE_TYPE_KEY);
AssertUtils.notEmpty(passbackObj, PAY_NOTIFY_PASSBACK_NOT_EXISTS_ERROR);
switch (TradeTypeEnum.fromCode(NumberUtil.parseInt(passbackObj.toString()))) {
case RECHARGE:
notifyRechargeOrderFail(order, organId);
break;
case PRODUCT:
notifyProductPayOrderFail(organId, passbackParams);
break;
default:
throw ServiceExceptionUtil.exception(PAY_NOTIFY_PASSBACK_TRADETYPE_NOT_SUPPORT_ERROR, passbackObj.toString());
}
}
/**
* 充值成功回调
*
* @param payOrderDO
* @param organId
*/
private void notifyRechargeOrderFail(PayOrderDO payOrderDO, Long organId) {
// todo 充值成功回调
}
/**
* 产品购买失败回调
*
* @param organId
* @param passbackParams
*/
private void notifyProductPayOrderFail(Long organId, Map<String, Object> passbackParams) {
Object purchaseRecordIdObj = passbackParams.get(PayConstants.PAY_PASSBACK_PARAMS_PURCHASE_RECORD_ID_KEY);
if (ObjectUtil.isNotNull(purchaseRecordIdObj)) {
PurchaseRecordDO record = purchaseService.getRecord(NumberUtil.parseLong(purchaseRecordIdObj.toString()));
// 1、混合支付失败,退回扣除的现金和赠送金
BigDecimal accountBalanceSpent = record.getAccountBalanceSpent();
BigDecimal giftMoneySpent = record.getGiftMoneySpent();
OrganRechargeAmountRespVO organRefundProductRespVO = organAmountService.refundProductOrganAmountBalanceAndGift(organId, accountBalanceSpent, giftMoneySpent);
// 2、新增退回的收支记录
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.INCOME.getType())
.cashAmountChange(accountBalanceSpent)
.giftAmountChange(giftMoneySpent)
.availableAmount(organRefundProductRespVO.getAvailableAmount())
.amount(organRefundProductRespVO.getAmount())
.giftAmount(organRefundProductRespVO.getGiftAmount())
.build();
incomeExpenseDetailsDO.setCreator(record.getCreator());
incomeExpenseDetailsDO.setUpdater(record.getUpdater());
incomeExpenseService.createDetail(incomeExpenseDetailsDO);
}
}
/**
@@ -428,4 +569,45 @@ public class PayOrderServiceImpl implements PayOrderService {
}
return organId;
}
@Override
public int expirePayOrder() {
// 1、查询过期的待支付订单
List<PayOrderDO> expireOrders = payOrderMapper.selectList(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getStatus, PayOrderStatusEnum.WAITING.getStatus())
.lt(PayOrderDO::getExpireTime, LocalDateTime.now()));
// 2、遍历处理
int count = 0;
for (PayOrderDO order : expireOrders) {
count += expireOrder(order) ? 1 : 0;
}
return count;
}
/**
* 同步单个支付单
*
* @param order 支付单
* @return 是否已过期
*/
private boolean expireOrder(PayOrderDO order) {
try {
// 1、获取支付渠道
PayChannelCodeEnum channelCode = order.getChannelCode();
AbstractPayHandler payHandler = payHandlerFactory.getPayHandler(channelCode.getCode());
// 2、调用三方接口,查询支付单状态,是不是已支付
PayOrderRespDTO payOrderRespDTO = payHandler.queryPayOrder(order.getPayNo());
if (PayOrderStatusEnum.isSuccess(payOrderRespDTO.getStatus())) {
notifyOrder(payOrderRespDTO);
return false;
}
log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
return true;
} catch (Throwable e) {
log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e);
return false;
}
}
}
@@ -0,0 +1,627 @@
package com.cf.imes.module.system.service.pay;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.id.core.util.SnowflakeIdWorker3rd;
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.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.TradeTypeEnum;
import com.cf.imes.module.system.enums.products.DurationUnitEnum;
import com.cf.imes.module.system.framework.snowflake.config.MybatisIdProperties;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
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;
/**
* 产品购买类型处理器
*
* @author Gqr
* @since 2025/8/14 10:54
*/
public class PayProductProcessor {
private ProductsDO productsDO;
private ProductsDetailDO productsDetailDO;
private OrganAmountDO organAmountDO;
private Long organId;
private SnowflakeIdWorker3rd snowflakeIdWorker3rd;
private ChenfengPayConfig payConfig;
private MybatisIdProperties mybatisIdProperties;
public PayProductProcessor(ProductsDO productsDO, ProductsDetailDO productsDetailDO, OrganAmountDO organAmountDO,
Long organId, SnowflakeIdWorker3rd snowflakeIdWorker3rd, ChenfengPayConfig payConfig,
MybatisIdProperties mybatisIdProperties) {
this.productsDO = productsDO;
this.productsDetailDO = productsDetailDO;
this.organAmountDO = organAmountDO;
this.organId = organId;
this.snowflakeIdWorker3rd = snowflakeIdWorker3rd;
this.payConfig = payConfig;
this.mybatisIdProperties = mybatisIdProperties;
}
/**
* 根据产品支付类型解析出对应的信息,
*
* @param payType
*/
public ProductProcessorRespVO processPayment(PayProductChannelCodeEnum payType) {
// 需要支付的价格
BigDecimal price = productsDetailDO.getCurrentPrice();
// 当前组织的余额
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;
}
}
/**
* 使用现金余额支付
*
* @param price
* @param balance
*/
private ProductProcessorRespVO payByBalance(BigDecimal price, BigDecimal balance) {
if (balance.compareTo(price) < 0) {
throw new ServiceException(PAY_PRODUCT_BALANCE_INSUFFICIENT);
}
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(PayProductChannelCodeEnum.BALANCE_ONLY.getCode())
.totalAmount(price)
.accountBalanceSpent(price)
.isInvocing(InvoiceStatusEnum.INVOICABLE.getStatus())
.build();
purchaseRecordDO.setEndTime(calculateEndTime(purchaseRecordDO.getStartTime(),
purchaseRecordDO.getPurchaseDuration(), purchaseRecordDO.getPurchaseDurationUnit(),
purchaseRecordDO.getGiftDuration(), purchaseRecordDO.getGiftDurationUnit()));
// 新增收支明细
// 现金余额减去购买金额
BigDecimal newBalance = balance.subtract(price);
BigDecimal giftAmount = organAmountDO.getGiftAmount();
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getType())
.cashAmountChange(price)
.availableAmount(newBalance.add(giftAmount))
.amount(newBalance)
.giftAmount(giftAmount)
.build();
// 更新余额
organAmountDO.setAmount(newBalance);
organAmountDO.setGiftAmount(giftAmount);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, null, organAmountDO);
}
/**
* 使用赠送金余额支付
*
* @param price
* @param gift
*/
private ProductProcessorRespVO payByGift(BigDecimal price, BigDecimal gift) {
if (gift.compareTo(price) < 0) {
throw new ServiceException(PAY_PRODUCT_GIFT_BALANCE_INSUFFICIENT);
}
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(PayProductChannelCodeEnum.BALANCE_ONLY.getCode())
.totalAmount(price)
.accountBalanceSpent(price)
.isInvocing(InvoiceStatusEnum.INVOICABLE.getStatus())
.build();
purchaseRecordDO.setEndTime(calculateEndTime(purchaseRecordDO.getStartTime(),
purchaseRecordDO.getPurchaseDuration(), purchaseRecordDO.getPurchaseDurationUnit(),
purchaseRecordDO.getGiftDuration(), purchaseRecordDO.getGiftDurationUnit()));
// 新增收支明细
// 赠送金余额减去购买金额
BigDecimal newGift = gift.subtract(price);
BigDecimal amount = organAmountDO.getAmount();
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getType())
.giftAmountChange(price)
.availableAmount(amount.add(newGift))
.amount(amount)
.giftAmount(newGift)
.build();
// 更新余额
organAmountDO.setAmount(amount);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, null, organAmountDO);
}
/**
* 使用支付宝支付
*
* @param price
* @return
*/
private ProductProcessorRespVO payByAlipay(BigDecimal price) {
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(PayProductChannelCodeEnum.BALANCE_ONLY.getCode())
.totalAmount(price)
.alipaySpent(price)
.build();
purchaseRecordDO.setEndTime(calculateEndTime(purchaseRecordDO.getStartTime(),
purchaseRecordDO.getPurchaseDuration(), purchaseRecordDO.getPurchaseDurationUnit(),
purchaseRecordDO.getGiftDuration(), purchaseRecordDO.getGiftDurationUnit()));
// 新增第三方支付订单
PayOrderDO payOrderDO = goExternalPay(price, PayChannelCodeEnum.ALIPAY_PC);
return new ProductProcessorRespVO(purchaseRecordDO, null, payOrderDO, null);
}
/**
* 使用微信支付
*
* @param price
* @return
*/
private ProductProcessorRespVO payByWechat(BigDecimal price) {
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(PayProductChannelCodeEnum.BALANCE_ONLY.getCode())
.totalAmount(price)
.wechatSpent(price)
.build();
purchaseRecordDO.setEndTime(calculateEndTime(purchaseRecordDO.getStartTime(),
purchaseRecordDO.getPurchaseDuration(), purchaseRecordDO.getPurchaseDurationUnit(),
purchaseRecordDO.getGiftDuration(), purchaseRecordDO.getGiftDurationUnit()));
// 新增第三方支付订单
PayOrderDO payOrderDO = goExternalPay(price, PayChannelCodeEnum.WECHAT_NATIVE);
return new ProductProcessorRespVO(purchaseRecordDO, null, payOrderDO, null);
}
/**
* 使用现在余额+赠送金余额支付
*
* @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;
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(PayProductChannelCodeEnum.BALANCE_AND_GIFT.getCode())
.totalAmount(price)
.isInvocing(InvoiceStatusEnum.INVOICABLE.getStatus())
.build();
// 先用赠送金
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);
}
// 剩下走第三方
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)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getType())
.cashAmountChange(usedBalance)
.giftAmountChange(usedGift)
.availableAmount(newAmounta.add(newGift))
.amount(newAmounta)
.giftAmount(newGift)
.build();
// 更新余额
organAmountDO.setAmount(newAmounta);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, null, organAmountDO);
}
/**
* 使用现金余额+第三方
*
* @param price
* @param balance
*/
private ProductProcessorRespVO payByBalanceAndExternal(BigDecimal price, BigDecimal balance, PayChannelCodeEnum payChannelCodeEnum) {
BigDecimal remaining = price;
BigDecimal usedBalance = BigDecimal.ZERO;
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(payChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum) ? PayProductChannelCodeEnum.BALANCE_AND_GIFT_AND_ALIPAY.getCode() : PayProductChannelCodeEnum.BALANCE_AND_GIFT_AND_WECHAT.getCode())
.totalAmount(price)
.build();
// 先用余额
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)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getType())
.cashAmountChange(usedBalance)
.availableAmount(newAmounta.add(giftAmount))
.amount(newAmounta)
.giftAmount(giftAmount)
.build();
// 剩下走第三方
PayOrderDO payOrderDO = null;
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
payOrderDO = goExternalPay(remaining, payChannelCodeEnum);
// 记录第三方支付支出
if (PayChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum)) {
purchaseRecordDO.setAlipaySpent(remaining);
} else {
purchaseRecordDO.setWechatSpent(remaining);
}
}
// 更新余额
organAmountDO.setAmount(newAmounta);
organAmountDO.setGiftAmount(giftAmount);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO);
}
/**
* 使用赠送金余额+第三方
*
* @param price
* @param gift
*/
private ProductProcessorRespVO payByGiftAndExternal(BigDecimal price, BigDecimal gift, PayChannelCodeEnum payChannelCodeEnum) {
BigDecimal remaining = price;
BigDecimal usedGift = BigDecimal.ZERO;
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(payChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum) ? PayProductChannelCodeEnum.GIFT_AND_ALIPAY.getCode() : PayProductChannelCodeEnum.GIFT_AND_WECHAT.getCode())
.totalAmount(price)
.build();
// 先用余额
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)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getType())
.giftAmountChange(usedGift)
.availableAmount(amount.add(newGift))
.amount(amount)
.giftAmount(newGift)
.build();
// 剩下走第三方
PayOrderDO payOrderDO = null;
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
goExternalPay(remaining, payChannelCodeEnum);
// 记录第三方支付支出
if (PayChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum)) {
purchaseRecordDO.setAlipaySpent(remaining);
} else {
purchaseRecordDO.setWechatSpent(remaining);
}
}
// 更新余额
organAmountDO.setAmount(amount);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO);
}
/**
* 使用现在余额+赠送金余额支付
*
* @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;
// 新增购买记录
PurchaseRecordDO purchaseRecordDO = PurchaseRecordDO.builder()
.id(IdUtil.getSnowflake(mybatisIdProperties.getWorkerId(), mybatisIdProperties.getDatacenterId()).nextId())
.organId(organId)
.productId(productsDO.getId())
.productName(productsDO.getProductName())
.productDetailsId(productsDetailDO.getId())
.purchaseDuration(productsDetailDO.getDuration())
.purchaseDurationUnit(productsDetailDO.getDurationUnit())
.giftDuration(productsDetailDO.getGiftDuration())
.giftDurationUnit(productsDetailDO.getGiftDurationUnit())
.startTime(LocalDateTime.now())
.paymentMethod(PayProductChannelCodeEnum.BALANCE_AND_GIFT.getCode())
.totalAmount(price)
.build();
// 先用赠送金
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);
}
// 新增收支明细
// 减去用掉的得到剩下的
BigDecimal newAmounta = balance.subtract(usedBalance);
BigDecimal newGift = gift.subtract(usedGift);
IncomeExpenseDetailsDO incomeExpenseDetailsDO = IncomeExpenseDetailsDO.builder()
.organId(organId)
.purchaseId(purchaseRecordDO.getId())
.tradeType(TradeTypeEnum.PRODUCT.getCode())
.incomeExpenseType(IncomeExpenseTypeEnum.EXPENSES.getType())
.cashAmountChange(usedBalance)
.giftAmountChange(usedGift)
.availableAmount(newAmounta.add(newGift))
.amount(newAmounta)
.giftAmount(newGift)
.build();
// 剩下走第三方
PayOrderDO payOrderDO = null;
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
payOrderDO = goExternalPay(remaining, payChannelCodeEnum);
// 记录第三方支付支出
if (PayChannelCodeEnum.ALIPAY_PC.equals(payChannelCodeEnum)) {
purchaseRecordDO.setAlipaySpent(remaining);
} else {
purchaseRecordDO.setWechatSpent(remaining);
}
}
// 更新余额
organAmountDO.setAmount(newAmounta);
organAmountDO.setGiftAmount(newGift);
return new ProductProcessorRespVO(purchaseRecordDO, incomeExpenseDetailsDO, payOrderDO, organAmountDO);
}
/**
* 外部支付
*
* @param price
* @param payChannelCodeEnum
* @return
*/
private PayOrderDO goExternalPay(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, organId, payOrderId, DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN)));
payOrderDO.setStatus(PayOrderStatusEnum.WAITING);
payOrderDO.setExpireTime(LocalDateTime.now().plus(payConfig.getPayExpireTime()));
payOrderDO.setPrice(price);
payOrderDO.setSubject(productsDO.getProductName());
payOrderDO.setChannelCode(payChannelCodeEnum);
payOrderDO.setOrganId(organId);
return payOrderDO;
}
/**
* 计算结束时间
*
* @param startTime
* @param duration
* @param durationUnit
* @param giftDuration
* @param giftDurationUnit
* @return
*/
private static LocalDateTime calculateEndTime(LocalDateTime startTime, String duration, Integer durationUnit,
String giftDuration, Integer giftDurationUnit) {
// 加正常时间
if (StringUtils.isNotEmpty(duration) && durationUnit != null) {
startTime = addTime(startTime, Integer.parseInt(duration), durationUnit);
}
// 加赠送时间
if (StringUtils.isNotEmpty(giftDuration) && giftDurationUnit != null) {
startTime = addTime(startTime, Integer.parseInt(giftDuration), giftDurationUnit);
}
return startTime;
}
/**
* 基于时长和单位累加time
*
* @param time
* @param duration
* @param unit
* @return
*/
private static LocalDateTime addTime(LocalDateTime time, int duration, int unit) {
switch (DurationUnitEnum.fromStatus(unit)) {
case YEAR:
return time.plusYears(duration);
case MONTH:
return time.plusMonths(duration);
case DAY:
return time.plusDays(duration);
default:
return time;
}
}
}
@@ -1,12 +1,31 @@
package com.cf.imes.module.system.service.pay.factory.impl.wechat;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.TemporalAccessorUtil;
import com.alibaba.fastjson.JSON;
import com.cf.imes.framework.common.util.json.JsonUtils;
import com.cf.imes.framework.pay.config.ChenfengWechatPayConfig;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.cf.imes.module.system.enums.pay.PayOrderStatusEnum;
import com.cf.imes.module.system.service.pay.factory.impl.AbstractPayHandler;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;
/**
* 微信native支付处理器
@@ -18,9 +37,32 @@ import java.util.Map;
@Slf4j
public class WechatNativePayHandler extends AbstractPayHandler {
@Resource
private ChenfengWechatPayConfig chenfengWechatPayConfig;
@Override
public PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
return null;
public PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(TemporalAccessorUtil.format(reqDTO.getExpireTime().atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
.attach(JSON.toJSONString(reqDTO.getPassbackParams()))
.build();
WxPayService client = new WxPayServiceImpl();
client.setConfig(chenfengWechatPayConfig);
// 执行请求
WxPayAppOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), JsonUtils.toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
@Override
@@ -29,7 +71,20 @@ public class WechatNativePayHandler extends AbstractPayHandler {
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
return null;
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
// 1. 解析回调
WxPayService client = new WxPayServiceImpl();
client.setConfig(chenfengWechatPayConfig);
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusEnum.SUCCESS.getStatus() : PayOrderStatusEnum.FAIL.getStatus();
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), body);
}
static LocalDateTime parseDateV2(String time) {
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
}
}
@@ -1,63 +0,0 @@
package com.cf.imes.module.system.util;
import org.springframework.stereotype.Component;
@Component
public class SnowflakeIdWorker3rd {
/** 初始时间 (2024-01-01 毫秒数: 1704038400000L) */
private final long twepoch = 1704038400000L;
/** 序列号占位数 */
private final long sequenceBits = 12L;
/** 时间戳左移位数 */
private final long timestampLeftShift = 22L; // 12位序列号 + 10位预留
/** 序列号掩码 */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 当前序列号 */
private long sequence = 0L;
/** 上次生成ID的时间戳 */
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,无法生成ID。回拨时间:" + (lastTimestamp - timestamp) + "ms");
}
// 同一毫秒内生成序列号
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// 组合时间戳和序列号生成ID
return ((timestamp - twepoch) << timestampLeftShift) | sequence;
}
/** 等待至下一毫秒 */
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/** 获取当前毫秒时间 */
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowflakeIdWorker3rd idWorker = new SnowflakeIdWorker3rd();
for (int i = 0; i < 10; i++) {
System.out.println(idWorker.nextId());
}
}
}
@@ -291,6 +291,12 @@ chenfeng:
privateKey: H/XEvo3RBcn9PFtuV6R94kMDyLkhAyVP0TrY6LNZPDKSpt6m47265Q5wZJzlcZ2FO1ChVaqd4SNQQuQiSkTwzS3dlHtJuZO4SDzSzwDfCQCgAbGkMNjJyMue9WOjtumxC19vDZVQvmQHP+iFis/psw5MMppp+GwtyVGm64kwhKAlUvbSOJWN38H3z0i67KdcRC07tdoD5NTh5vWQOljRK52ISHlxssaK8+oC9rTSsI6mvkNL1OH/MktfLfmWEfZTX8tuSIrLkWikU9NFfXjD+IWrkC7qRIxdDeWHwCHTU86KSkYteRkMaltLmvWXBYycieM18iRIf2QvT6/gymkT8qosvderqKS0wz5gCIq9LVEZVdYsNKcXAZvN56lVlbb3zsHIFCtZBTfwSHJ5xy3r4Xn3kor8QqeCMk+ACZoxTaORxJ/phPEltCgIEVhAXCMdRmpJKd11NsjAHAP0P633MOhl/i2fzeagJWhV1wk1S8N0m4hfvchn//JEjxsqU90S3fuQokvj7KnDfXUsu9RgOAySAyZbkuMM7q/s7ZPNd0y41WmBoK3gVesu3qKfNzeap9vRD/N8ccyiRG1HwhH3NJhJ8Uo158KNFlJydFsq1Lllm0MsokeJEwEQuVUX07RfM9sPbdFfluXLTP1Xi817lKO8duIqwTmHcesv9Zlb2DX9prMtuTWB/CokboGhXh1L1gAtPXMCIHcjNpz+ACIOhxdQEP5hW9zRWnhF6LMc9lReUPnkKj7Lx08FLTsX8O00gvzX7plR8UyrMJc7a7MMRjN98L1XBKDzHg3gonIJXdGJxJP1j0elyILjr6DUDB8H4fYVmndm9FcuCMTXx84poIK39BAey5PJPvFTm1l8l6vw3gXjajffLhgdqCqn60nwG20464FvZusdMzmx/7s5HPB5wGfCNQZ0J+Q5FctITR92juHTUe6BsBbo7ZlGkWBlJByGy0kw4m81jCFzQpymTmpWaxCi56ysHnMXRnbeMyPe7v/6pHvNqwMLOBJzKBZi72I3o0gEuBnxR87F4r1GAVljlmSthxYFqWe4Jt9zG+dnUrscWJ+/pvGaRz40exLl8ahywPNX63sm9uXqt96PAqosOB1Il9vwY31xa+OeSOp4gT5thKKbbUtTPQIu92vp82HdFCTLLwDYOa3NQPrvxZjWH8cR6sYM7D1AUizzHNc6WfiU1YutDOHQVev6U9RKqwsZKgj94ySa1bH+24CiglXojLyAQ1uU99Hkth3JwCX2hPpCyNHJ34mhsMoe0XZZRrpzmtxRBkhVHtyF44pVekCH/SU3qZkeNuk5ETk+13GpZ7vhbmE9dBpLR4LsiPEfGJHqNH8D624cEKbcXcAL4tWfYiBqQAHlqnnd3dLHLawaAbXGr6xM0/hXVT/Z9Cb1Yh//pPtGnuHPZVCKSzdL14NLDlhJ3IDDYcmDW8pjbCRXNsfi75eZgNJwju2A6+I7j3Ovl0ma8kHrA73SAy2e8ldx25t7kvuw3s/kcYLCuGo9d4Fq0RpTk3YzF7XlevoHxrfXjxf/tGoZCrttURFjaWVtNnOmo2He894V3mSpl3IuaPwvo2VrPPlSFQ3aNkK0/Et2Gmt7OWGRHWgzPKWH378zMxPMo5cYcktmKXvOBvGyrMrHbKEl1lMMM/cAZh6pMdtcqU5J3kCKUbickAkf10KgULzqenPku+7rFB34Uv0Emw5s+l42bW9pcia58FyY+3km6Fl3dkPe2nm7YRvWSfisCAyCi/CHqxHecgchCParCJ9EcGoMADQfzkfsJInIuOT5c8IQXYzwqpSpHMk9n5JjqcyfCNPWEO1/YQOYTExkeDp4SENYIf6TZxvPQCb5cOnYzZcct2YNSrPzKahmzoTwpYmTxxMYuunFlWmyineV/Y+1sGoQ/Lzk+M83uf3UCHG9Pbsp5gFj9t21U95Yukw4Qs45fG8bVPPYfU97K/DrWDd4tfroFUyResqAP2HsXKNd0k+59rpBzEg2TrJH0XlU9zBdU0Oe93HSC/0TggZrKtd717uQXKqo5hJ65lV0JqbZInl92qK2t6BqZE/sKj4hMdeHnAebxROfLOojLFtJB5se6Dn9XnG+sf/cExSMmJqgDIGsOyGt7aA1dV0tDbzgio2V6jjOE2U9kx4luHuaTd2aWJ0oIAuWo6v2U3kMcLH8z+KqtnWAgGS0mEgpr7iz27w=
alipayPublicKey: KbUGE7zsPBKWPx0/zMcAdLRldKrcgy2LRiOrqOBm7y4iPJLXTYtXFJT15p2JMQiyOM8mqiYQR0OEDwfrKxDgHXLtIut/e0EAk9bZwZoLrqVe0pN1huseRC38zEglbWil3yz+tZfHZuAaEq2EzvWJltX/tRKOzNBqAT9l+UvnqNedgCFB9MY2EErqJjbn/at3gYCkV2idt31ghF0z8SEQ5h9ztNYtmTvk8c8ipi0/hIjX8XryN6mlOvGQfyZ4rWrBWBuZ6iD/SyuThwxKmFYU4dhHbHVpBrva5nJWU43CP7RVaJm649nNJnce+Ppj2Tv8+ugSLe5poZWo2zmGe3Xn5WmN6OTZyRI1pnmn2/hGxtOMmTZJX7Gh0pI4v3wmU0gOd/gbRlqlvN+u12T92nyH8MQrHdA7UXuttAtolYUa/Rli/20qHX69QtOtvfZk8mNvDSj9dDvODseGcrFHm07jyfxpaIkQDeN75cQ3NwNgDQEW8urQszaU1IJ7hc+20tt0Z3td4+7MfiVPpGfVxsGE66gwLgpB8wsxEhyBTaBr7osMLqOv
notifyUrl: RIEeHCoPHaAryuVN6mEYskBSl+EPvhsxRpjIbuIE7f7jABVxm46bLc01WQUe4Fn9NPYyNTv0xCxM68iRrmFDjAUjQ/hkIVNG+yD/kNsjcYCWa2gtOQPrvWZzDA==
# wechat:
# payBaseUrl: https://api.mch.weixin.qq.com/xdc/apiv2sandbox
# appId: wxd0b9c0b5d0b9c0b5
# mchId: 1725093395
# mchKey: Cf100261028910026102891002610289
justauth:
enabled: true
@@ -69,6 +69,8 @@ mybatis-plus:
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
global-config:
db-config:
worker-id: 0
datacenter-id: 0
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库