diff --git a/cf-framework/cf-spring-boot-starter-biz-id/src/main/java/com/cf/imes/framework/id/core/util/SnowflakeIdWorker3rd.java b/cf-framework/cf-spring-boot-starter-biz-id/src/main/java/com/cf/imes/framework/id/core/util/SnowflakeIdWorker3rd.java index 517948856..add3a73f8 100644 --- a/cf-framework/cf-spring-boot-starter-biz-id/src/main/java/com/cf/imes/framework/id/core/util/SnowflakeIdWorker3rd.java +++ b/cf-framework/cf-spring-boot-starter-biz-id/src/main/java/com/cf/imes/framework/id/core/util/SnowflakeIdWorker3rd.java @@ -21,7 +21,7 @@ import java.time.LocalDate; public class SnowflakeIdWorker3rd { /** 初始时间 (2024-01-01: 1704038400) */ // private final int twepoch = 28400640;// 1704038400000L/1000/60; - private final int twepoch = 1704038400; + private final int twepoch = 28400640; /** 序列在id中占位数 */ private final long sequenceBits = 7L; /** 时间截向左移7bit */ @@ -122,8 +122,8 @@ public class SnowflakeIdWorker3rd { * @return 当前时间(分钟) */ protected int timeGen() { - // String timestamp = String.valueOf(System.currentTimeMillis() / 1000 / 60); - String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000 / 60); +// String timestamp = String.valueOf(System.currentTimeMillis() / 1000); return Integer.valueOf(timestamp); } diff --git a/cf-framework/cf-spring-boot-starter-biz-pay/src/main/java/com/cf/imes/framework/pay/config/ChenfengWechatPayConfig.java b/cf-framework/cf-spring-boot-starter-biz-pay/src/main/java/com/cf/imes/framework/pay/config/ChenfengWechatPayConfig.java index 88a8b0e3d..01df2fbab 100644 --- a/cf-framework/cf-spring-boot-starter-biz-pay/src/main/java/com/cf/imes/framework/pay/config/ChenfengWechatPayConfig.java +++ b/cf-framework/cf-spring-boot-starter-biz-pay/src/main/java/com/cf/imes/framework/pay/config/ChenfengWechatPayConfig.java @@ -1,9 +1,14 @@ package com.cf.imes.framework.pay.config; +import com.cf.imes.framework.common.util.encrypt.AesUtils; import com.github.binarywang.wxpay.config.WxPayConfig; import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; +import java.nio.charset.StandardCharsets; + /** * @author Gqr * @since 2025/4/15 16:52 @@ -11,4 +16,44 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "chenfeng.pay.wechat") @Data public class ChenfengWechatPayConfig extends WxPayConfig { + @Value("${chenfeng.encrypt.publicKey:}") + private String publicKey; + + @Override + public void setAppId(String appId) { + if (StringUtils.isNotEmpty(publicKey)) { + super.setAppId(AesUtils.decrypt(appId, publicKey)); + } else { + super.setAppId(appId); + } + } + + @Override + public void setMchId(String mchId) { + if (StringUtils.isNotEmpty(publicKey)) { + super.setMchId(AesUtils.decrypt(mchId, publicKey)); + } else { + super.setMchId(mchId); + } + } + + @Override + public void setMchKey(String mchKey) { + if (StringUtils.isNotEmpty(publicKey)) { + super.setMchKey(AesUtils.decrypt(mchKey, publicKey)); + } else { + super.setMchKey(mchKey); + } + } + + @Override + public void setKeyContent(byte[] keyContent) { + if (StringUtils.isNotEmpty(publicKey)) { + String encrypted = new String(keyContent, StandardCharsets.UTF_8); + String decrypted = AesUtils.decrypt(encrypted, publicKey); + super.setKeyContent(decrypted.getBytes(StandardCharsets.UTF_8)); + } else { + super.setKeyContent(keyContent); + } + } } diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java index 4d853e604..0868a3041 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java @@ -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, "当前产品不存在"); diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/products/DurationUnitEnum.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/products/DurationUnitEnum.java index 4af5feb30..b96a34a4b 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/products/DurationUnitEnum.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/products/DurationUnitEnum.java @@ -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); + } } diff --git a/cf-module-system/cf-module-system-biz/pom.xml b/cf-module-system/cf-module-system-biz/pom.xml index 234fad088..da102c821 100644 --- a/cf-module-system/cf-module-system-biz/pom.xml +++ b/cf-module-system/cf-module-system-biz/pom.xml @@ -49,6 +49,10 @@ com.cf.imes cf-spring-boot-starter-biz-ip + + com.cf.imes + cf-spring-boot-starter-biz-id + @@ -244,6 +248,10 @@ com.cf.imes cf-spring-boot-starter-biz-ip + + com.cf.imes + cf-spring-boot-starter-biz-id + com.cf.imes cf-spring-boot-starter-biz-operatelog diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganAmountRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganAmountRespVO.java index 68bfcb788..136396944 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganAmountRespVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganAmountRespVO.java @@ -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; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganPayAmountRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganPayAmountRespVO.java new file mode 100644 index 000000000..b1bd3d2e2 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganPayAmountRespVO.java @@ -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; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganRechargeAmountRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganRechargeAmountRespVO.java index f6b9936c2..19820c7ed 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganRechargeAmountRespVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/organamount/vo/OrganRechargeAmountRespVO.java @@ -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 { diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/products/vo/product/ProductProcessorRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/products/vo/product/ProductProcessorRespVO.java new file mode 100644 index 000000000..238b45178 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/products/vo/product/ProductProcessorRespVO.java @@ -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; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/PurchaseService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/PurchaseService.java new file mode 100644 index 000000000..c8c8048d3 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/PurchaseService.java @@ -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); +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/PurchaseServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/PurchaseServiceImpl.java new file mode 100644 index 000000000..4c422721f --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/PurchaseServiceImpl.java @@ -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() + .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() + .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; + } +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/vo/OrgEarliestPurchaseProductRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/vo/OrgEarliestPurchaseProductRespVO.java new file mode 100644 index 000000000..18380bd05 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/funds/purchase/vo/OrgEarliestPurchaseProductRespVO.java @@ -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; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/PayController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/PayController.java index 70be38d9f..1e963666d 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/PayController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/PayController.java @@ -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 initiatePay(@RequestBody PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) { -// payOrderService.productPay(payProductOrderUnifiedReqVO); -// return success(null); -// } + @PostMapping("/product") + @Operation(summary = "创建产品支付订单") + public CommonResult initiatePay(@RequestBody PayProductOrderUnifiedReqVO payProductOrderUnifiedReqVO) { + PayOrderRespDTO payOrderRespDTO = payOrderService.productPay(payProductOrderUnifiedReqVO); + return success(payOrderRespDTO); + } @GetMapping("/order/{channelCode}/{outTradeNo}") @Operation(summary = "查询支付订单") diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/vo/PayProductOrderUnifiedReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/vo/PayProductOrderUnifiedReqVO.java index cd9e49a2a..59bbb5681 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/vo/PayProductOrderUnifiedReqVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/pay/vo/PayProductOrderUnifiedReqVO.java @@ -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; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/funds/purchase/PurchaseRecordDO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/funds/purchase/PurchaseRecordDO.java new file mode 100644 index 000000000..4fda9730f --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/funds/purchase/PurchaseRecordDO.java @@ -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; + + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/incomeandexpense/IncomeExpenseDetailsDO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/incomeandexpense/IncomeExpenseDetailsDO.java index 02bfda3f3..ee6f532d8 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/incomeandexpense/IncomeExpenseDetailsDO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/dataobject/incomeandexpense/IncomeExpenseDetailsDO.java @@ -78,7 +78,7 @@ public class IncomeExpenseDetailsDO extends BaseDO { /** * 支出/入账赠送金金额 */ - private BigDecimal bonusAmountChange; + private BigDecimal giftAmountChange; /** diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/funds/purchase/PurchaseRecordMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/funds/purchase/PurchaseRecordMapper.java new file mode 100644 index 000000000..e998401e7 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/funds/purchase/PurchaseRecordMapper.java @@ -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 { +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/InvoiceStatusEnum.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/InvoiceStatusEnum.java new file mode 100644 index 000000000..0be96ca46 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/InvoiceStatusEnum.java @@ -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; + } + + + + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayConstants.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayConstants.java index 0656d674f..7aadead3b 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayConstants.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayConstants.java @@ -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 } \ No newline at end of file diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayOrderStatusEnum.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayOrderStatusEnum.java index 4f35cdc3a..d5623e017 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayOrderStatusEnum.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayOrderStatusEnum.java @@ -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()); } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayProductChannelCodeEnum.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayProductChannelCodeEnum.java index 753a0a7df..ec739f8ab 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayProductChannelCodeEnum.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/enums/pay/PayProductChannelCodeEnum.java @@ -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); + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/package-info.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/package-info.java deleted file mode 100644 index ac3e1ae74..000000000 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 属于 system 模块的 framework 封装 - * - * @author 晨丰科技 - */ -package com.cf.imes.module.system.framework; diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/snowflake/config/MybatisIdProperties.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/snowflake/config/MybatisIdProperties.java new file mode 100644 index 000000000..031630baf --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/snowflake/config/MybatisIdProperties.java @@ -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; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/snowflake/package-info.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/snowflake/package-info.java new file mode 100644 index 000000000..f234a3964 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/snowflake/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 system 模块的 framework.snowflake 封装 + * + * @author 晨丰科技 + */ +package com.cf.imes.module.system.framework.snowflake; diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/job/pay/PayOrderExpireJob.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/job/pay/PayOrderExpireJob.java new file mode 100644 index 000000000..abdea2a0d --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/job/pay/PayOrderExpireJob.java @@ -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); + } + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/advertisement/AdvertisementServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/advertisement/AdvertisementServiceImpl.java index 32995e280..e81719392 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/advertisement/AdvertisementServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/advertisement/AdvertisementServiceImpl.java @@ -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) { diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountService.java index c8614c82d..4f2b9b9b0 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountService.java @@ -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); diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountServiceImpl.java index 8ccd83361..79983cb6b 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/organamount/OrganAmountServiceImpl.java @@ -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() + .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().eq(OrganAmountDO::getOrganId, organId)); + BigDecimal newAmount = organAmountDO.getAmount().add(balance); + BigDecimal newGift = organAmountDO.getGiftAmount().add(gift); + + int updated = organAmountMapper.update(new LambdaUpdateWrapper() + .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().eq(OrganAmountDO::getOrganId, organId)); + newAmount = organAmountDO.getAmount().add(balance); + newGift = organAmountDO.getGiftAmount().add(gift); + + updated = organAmountMapper.update(new LambdaUpdateWrapper() + .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; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailService.java index 64f091af7..85e2dd5d7 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailService.java @@ -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); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailServiceImpl.java index aa94b24a4..d6dafa83d 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/products/ProductsDetailServiceImpl.java @@ -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() .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; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/recharge/RechargeActiveServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/recharge/RechargeActiveServiceImpl.java index defea1374..7014e2a1a 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/recharge/RechargeActiveServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/funds/recharge/RechargeActiveServiceImpl.java @@ -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() .eq(GiftMoneyDetailsDO::getDeleted, false) diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderService.java index 0bbf8ce29..166e821ac 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderService.java @@ -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 params, String body); + + /** + * 处理过期订单 + * + * @return + */ + int expirePayOrder(); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderServiceImpl.java index 7eda3460c..f3a4e67cd 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayOrderServiceImpl.java @@ -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 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 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 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 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 expireOrders = payOrderMapper.selectList(new LambdaQueryWrapper() + .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; + } + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayProductProcessor.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayProductProcessor.java new file mode 100644 index 000000000..663edf8f2 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/PayProductProcessor.java @@ -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; + } + } + +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/factory/impl/wechat/WechatNativePayHandler.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/factory/impl/wechat/WechatNativePayHandler.java index 9f45ee3f4..f0ea7f8ba 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/factory/impl/wechat/WechatNativePayHandler.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/pay/factory/impl/wechat/WechatNativePayHandler.java @@ -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 params, String body) { - return null; + public PayOrderRespDTO doParseOrderNotify(Map 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); } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/SnowflakeIdWorker3rd.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/SnowflakeIdWorker3rd.java deleted file mode 100644 index 6ab065221..000000000 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/util/SnowflakeIdWorker3rd.java +++ /dev/null @@ -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()); - } - } -} \ No newline at end of file diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/application-local.yaml b/cf-module-system/cf-module-system-biz/src/main/resources/application-local.yaml index c1d7b6a57..fa79e5dd7 100644 --- a/cf-module-system/cf-module-system-biz/src/main/resources/application-local.yaml +++ b/cf-module-system/cf-module-system-biz/src/main/resources/application-local.yaml @@ -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 diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml b/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml index 45b589dc4..38dd24293 100644 --- a/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml +++ b/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml @@ -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 数据库