优化支付模块的代码,增加微信扫码支付的功能
This commit is contained in:
parent
623cdd23b0
commit
cac17af0fe
@ -11,24 +11,31 @@ public enum PayChannelEnum {
|
||||
/**
|
||||
* 微信支付
|
||||
*/
|
||||
WECHAT_JSAPI("WECHAT_JSAPI", "微信支付"),
|
||||
WECHAT_JSAPI("WECHAT_JSAPI", "微信JSAPI支付", "WECHAT"),
|
||||
|
||||
/**
|
||||
* 支付宝支付
|
||||
* 微信扫码支付
|
||||
*/
|
||||
ALIPAY("ALIPAY", "支付宝支付"),
|
||||
WECHAT_NATIVE("WECHAT_NATIVE", "微信扫码支付", "WECHAT"),
|
||||
|
||||
/**
|
||||
* 学豆支付
|
||||
* 支付宝扫码支付
|
||||
*/
|
||||
COIN("COIN", "学豆支付");
|
||||
ALIPAY_QR("ALIPAY_QR", "支付宝扫码支付", "ALIPAY"),
|
||||
|
||||
/**
|
||||
* 钱包支付
|
||||
*/
|
||||
COIN("COIN", "钱包支付", "COIN");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
private final String type;
|
||||
|
||||
PayChannelEnum(String code, String description) {
|
||||
PayChannelEnum(String code, String description,String type) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -75,4 +75,9 @@ public interface CommonConstant {
|
||||
* 启用
|
||||
*/
|
||||
Integer ENABLE = 1;
|
||||
|
||||
/**
|
||||
* 微信openId
|
||||
*/
|
||||
String WX_OPEN_ID = "openId";
|
||||
}
|
||||
@ -57,7 +57,9 @@
|
||||
|
||||
<!-- 支付集成 -->
|
||||
<wechatpay-apiv3.version>0.2.17</wechatpay-apiv3.version>
|
||||
<weixin-java.version>4.7.5.B</weixin-java.version>
|
||||
<weixin-java.version>4.8.1.B</weixin-java.version>
|
||||
<alipay-sdk-java.version>4.35.79.ALL</alipay-sdk-java.version>
|
||||
<com.github.binarywang.version>4.8.1.B</com.github.binarywang.version>
|
||||
<!-- 编解码 -->
|
||||
<protobuf.version>4.30.2</protobuf.version>
|
||||
|
||||
@ -84,7 +86,7 @@
|
||||
|
||||
<jackson.version>2.17.2</jackson.version>
|
||||
<xxl-job.version>3.3.1</xxl-job.version>
|
||||
<com.github.binarywang.version>4.7.5.B</com.github.binarywang.version>
|
||||
|
||||
|
||||
<jetcache.version>2.7.7</jetcache.version>
|
||||
<kryo.version>4.0.3</kryo.version>
|
||||
@ -298,6 +300,18 @@
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>${alipay-sdk-java.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.dongliu</groupId>
|
||||
<artifactId>apk-parser</artifactId>
|
||||
|
||||
@ -171,10 +171,10 @@ public class AppOrderServiceImpl implements IAppOrderService {
|
||||
settlementRespVO.setItems(goodRespList);
|
||||
settlementRespVO.setTotalCount(totalCount);
|
||||
if (PayChannelEnum.COIN.getCode().equalsIgnoreCase(orderSettlementReq.getChannelCode())) {
|
||||
settlementRespVO.setTotalPrice(totalPrices);
|
||||
} else {
|
||||
CoinToAmountRespDTO coinToAmountRespDTO = accountServiceApi.convertCoinToAmount(String.valueOf(totalPrices));
|
||||
settlementRespVO.setTotalPrice(new BigDecimal(coinToAmountRespDTO.getAmount()));
|
||||
} else {
|
||||
settlementRespVO.setTotalPrice(totalPrices);
|
||||
}
|
||||
log.info("订单结算完成,用户使用[{}]支付的总价格: {}, 总数量: {}", orderSettlementReq.getChannelCode(), totalPrices, totalCount);
|
||||
return settlementRespVO;
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
package com.seer.teach.pay.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.seer.teach.common.entity.BaseEntity;
|
||||
import java.io.Serializable;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@ -22,8 +19,6 @@ import lombok.Setter;
|
||||
@TableName("pay_channel")
|
||||
public class PayChannelEntity extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 渠道编码(WECHAT,ALIPAY)
|
||||
*/
|
||||
@ -42,6 +37,18 @@ public class PayChannelEntity extends BaseEntity {
|
||||
@TableField("`status`")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* appId
|
||||
*/
|
||||
@TableField("app_id")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 回调域名
|
||||
*/
|
||||
@TableField("notify_domain")
|
||||
private String notifyDomain;
|
||||
|
||||
/**
|
||||
* 支付配置(JSON格式)
|
||||
*/
|
||||
|
||||
@ -5,6 +5,7 @@ import com.seer.teach.common.ResultBean;
|
||||
import com.seer.teach.common.annotation.LogPrint;
|
||||
import com.seer.teach.pay.admin.pay.controller.req.AdminPayChannelSaveReq;
|
||||
import com.seer.teach.pay.admin.pay.controller.req.AdminPayChannelUpdateReq;
|
||||
import com.seer.teach.pay.admin.pay.controller.resp.PayChannelCodeResp;
|
||||
import com.seer.teach.pay.admin.pay.controller.resp.PayChannelResp;
|
||||
import com.seer.teach.pay.admin.pay.service.AdminPayChannelService;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@ -25,6 +26,13 @@ public class AdminPayChannelController {
|
||||
|
||||
private final AdminPayChannelService payChannelService;
|
||||
|
||||
@GetMapping("/code/list")
|
||||
@Operation(summary = "获取支付渠道编码列表")
|
||||
@LogPrint
|
||||
@SaCheckPermission("admin:pay:channel:list")
|
||||
public ResultBean<List<PayChannelCodeResp>> getPayChannelCodeList(){
|
||||
return ResultBean.success(payChannelService.getPayChannelCodeList());
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取支付渠道列表")
|
||||
|
||||
@ -32,6 +32,20 @@ public class AdminPayChannelSaveReq {
|
||||
@Schema(description = "是否启用(0-禁用,1-启用)", example = "1")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@NotBlank(message = "应用ID不能为空")
|
||||
@Schema(description = "应用ID", example = "wx1234567890")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 回调域名
|
||||
*/
|
||||
@NotBlank(message = "回调域名不能为空")
|
||||
@Schema(description = "回调域名", example = "https://www.seer.com")
|
||||
private String notifyDomain;
|
||||
|
||||
/**
|
||||
* 支付配置(JSON格式)
|
||||
*/
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.seer.teach.pay.admin.pay.controller.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
@ -33,6 +34,20 @@ public class AdminPayChannelUpdateReq {
|
||||
@Schema(description = "支付配置(JSON对象)")
|
||||
private Map<String, String> config;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@NotBlank(message = "应用ID不能为空")
|
||||
@Schema(description = "应用ID", example = "wx1234567890")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 回调域名
|
||||
*/
|
||||
@NotBlank(message = "回调域名不能为空")
|
||||
@Schema(description = "回调域名", example = "https://www.seer.com")
|
||||
private String notifyDomain;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package com.seer.teach.pay.admin.pay.controller.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
@Schema(description = "支付渠道编码响应类")
|
||||
public class PayChannelCodeResp {
|
||||
|
||||
@Schema(description = "支付渠道编码")
|
||||
private String channelCode;
|
||||
|
||||
@Schema(description = "支付渠道名称")
|
||||
private String channelName;
|
||||
|
||||
@Schema(description = "支付渠道类型")
|
||||
private String type;
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.seer.teach.pay.admin.pay.controller.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@ -27,6 +26,18 @@ public class PayChannelResp {
|
||||
|
||||
@Schema(description = "支付渠道状态,0:禁用, 1:启用")
|
||||
private Integer status;
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@Schema(description = "应用ID", example = "wx1234567890")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 回调域名
|
||||
*/
|
||||
@Schema(description = "回调域名", example = "https://www.seer.com")
|
||||
private String notifyDomain;
|
||||
|
||||
|
||||
@Schema(description = "支付渠道配置")
|
||||
private Map<String, String> payInfo;
|
||||
|
||||
@ -4,10 +4,11 @@ package com.seer.teach.pay.admin.pay.service;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.utils.AESUtils;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.utils.AssertUtils;
|
||||
import com.seer.teach.pay.admin.pay.controller.req.AdminPayChannelSaveReq;
|
||||
import com.seer.teach.pay.admin.pay.controller.req.AdminPayChannelUpdateReq;
|
||||
import com.seer.teach.pay.admin.pay.controller.resp.PayChannelCodeResp;
|
||||
import com.seer.teach.pay.admin.pay.controller.resp.PayChannelResp;
|
||||
import com.seer.teach.pay.admin.pay.convert.PayChannelConvert;
|
||||
import com.seer.teach.pay.admin.pay.util.PayConfigEncryptUtils;
|
||||
@ -17,9 +18,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@ -43,9 +44,7 @@ public class AdminPayChannelService {
|
||||
* @param params
|
||||
*/
|
||||
public void addPayChannel(AdminPayChannelSaveReq params) {
|
||||
PayChannelEntity one = payChannelService.getOne(new LambdaQueryWrapper<PayChannelEntity>()
|
||||
.eq(PayChannelEntity::getChannelCode, params.getChannelCode())
|
||||
);
|
||||
PayChannelEntity one = payChannelService.getOne(new LambdaQueryWrapper<PayChannelEntity>().eq(PayChannelEntity::getChannelCode, params.getChannelCode()));
|
||||
AssertUtils.isNull(one, ResultCodeEnum.PAY_CHANNEL_ALREADY_EXISTS);
|
||||
// 获取配置信息并加密
|
||||
Map<String, String> encryptedConfig = PayConfigEncryptUtils.encryptConfig(params.getConfig());
|
||||
@ -105,4 +104,12 @@ public class AdminPayChannelService {
|
||||
resp.setPayInfo(decrypted);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付渠道编码列表
|
||||
* @return
|
||||
*/
|
||||
public List<PayChannelCodeResp> getPayChannelCodeList() {
|
||||
return Arrays.stream(PayChannelEnum.values()).map( payChannelEnum -> new PayChannelCodeResp(payChannelEnum.getCode(), payChannelEnum.getDescription(), payChannelEnum.getType()) ).toList();
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,8 @@ spring:
|
||||
active: dev
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ant_path_matcher
|
||||
flyway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
locations: classpath:db/mysql
|
||||
baseline-on-migrate: true
|
||||
clean-disable: true
|
||||
@ -20,6 +17,7 @@ spring:
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml
|
||||
- optional:nacos:shared-database.yaml
|
||||
- optional:nacos:shared-redis.yaml
|
||||
- optional:nacos:shared-sa-token.yaml
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
|
||||
@ -4,6 +4,8 @@ CREATE TABLE `pay_channel` (
|
||||
`channel_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '渠道编码(WECHAT,ALIPAY)',
|
||||
`channel_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '渠道名称(微信、支付宝)',
|
||||
`status` int NULL DEFAULT NULL COMMENT '是否启用(0-禁用,1-启用)',
|
||||
`app_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT 'appId',
|
||||
`notify_domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NULL DEFAULT NULL COMMENT '回调域名,如:https://api.example.com',
|
||||
`config_json` JSON NOT NULL COMMENT '支付配置(JSON格式)',
|
||||
`sort` int NULL DEFAULT NULL COMMENT '排序',
|
||||
`deleted` tinyint(1) NOT NULL default 0 COMMENT '逻辑删除',
|
||||
|
||||
@ -56,6 +56,24 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.35.79.ALL</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
package com.seer.teach.pay.app.client;
|
||||
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.exception.CommonException;
|
||||
import com.seer.teach.common.utils.AssertUtils;
|
||||
import com.seer.teach.pay.app.client.convert.PayClientConvert;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderCallBackRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
@ -11,12 +9,7 @@ import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderExtensionEntity;
|
||||
import com.seer.teach.pay.service.IPayOrderExtensionService;
|
||||
import com.seer.teach.pay.service.IPayOrderService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -28,33 +21,21 @@ import java.util.Optional;
|
||||
@Slf4j
|
||||
public abstract class AbstractPayClient implements PayClient {
|
||||
|
||||
@Autowired
|
||||
protected IPayOrderService payOrderService;
|
||||
protected final String channelCode;
|
||||
|
||||
@Autowired
|
||||
protected IPayOrderExtensionService payOrderExtensionService;
|
||||
protected final String config;
|
||||
|
||||
|
||||
public AbstractPayClient(String channelCode, String config) {
|
||||
this.channelCode = channelCode;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderRespDTO payOrder(PayOrderReqDTO payOrderReqDTO) {
|
||||
log.info("开始支付订单,渠道:{},支付订单号:{}", getChannel(), payOrderReqDTO.getPayOrderSn());
|
||||
|
||||
Object extrasRequest = getPrepayRequestParam(payOrderReqDTO);
|
||||
|
||||
PayOrderRespDTO payOrderRespDTO = doPayOrder(payOrderReqDTO, extrasRequest);
|
||||
|
||||
// 查找或创建支付订单扩展信息
|
||||
Optional<PayOrderExtensionEntity> extension = payOrderExtensionService.getOneByOrderId(payOrderReqDTO.getParOrderId());
|
||||
|
||||
// 如果扩展信息不存在,则创建
|
||||
if (extension.isPresent()) {
|
||||
log.info("支付订单扩展信息不存在,创建新的扩展信息");
|
||||
updatePayOrderExtension(extension.get(), payOrderRespDTO);
|
||||
} else {
|
||||
PayOrderExtensionEntity orderExtension = createPayOrderExtension(payOrderReqDTO);
|
||||
boolean extensionSaved = payOrderExtensionService.save(orderExtension);
|
||||
log.info("保存支付订单扩展信息结果:{}", extensionSaved);
|
||||
}
|
||||
PayOrderRespDTO payOrderRespDTO = doPayOrder(payOrderReqDTO);
|
||||
|
||||
log.info("支付订单创建成功,订单号:{}", payOrderReqDTO.getParOrderId());
|
||||
return payOrderRespDTO;
|
||||
@ -63,66 +44,26 @@ public abstract class AbstractPayClient implements PayClient {
|
||||
@Override
|
||||
public PayOrderRespDTO handlePayCallback(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
log.info("开始处理支付回调,渠道:{}", getChannel());
|
||||
// 解析支付回调
|
||||
PayOrderCallBackRespDTO payOrderCallBackRespDTO = parseOrderNotify(headers, params, body);
|
||||
|
||||
PayOrderEntity payOrder = payOrderService.getOrderByOrderSn(payOrderCallBackRespDTO.getPayOrderSn());
|
||||
AssertUtils.notNull(payOrder, ResultCodeEnum.PAY_ORDER_IS_NOT_EXIST);
|
||||
|
||||
if (PayStatusEnum.SUCCESS.getCode() == payOrderCallBackRespDTO.getPayStatus().getCode()) {
|
||||
log.info("该笔支付已处理,订单号:{}", payOrderCallBackRespDTO.getPayOrderSn());
|
||||
throw new CommonException(ResultCodeEnum.PAY_ORDER_IS_PROCESSED);
|
||||
}
|
||||
return PayClientConvert.INSTANCE.convert(payOrderCallBackRespDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付订单扩展信息
|
||||
*/
|
||||
private PayOrderExtensionEntity createPayOrderExtension(PayOrderReqDTO payOrderReqDTO) {
|
||||
PayOrderExtensionEntity extension = new PayOrderExtensionEntity();
|
||||
extension.setOrderId(payOrderReqDTO.getParOrderId());
|
||||
extension.setMerchantOrderSn(payOrderReqDTO.getMerchantOrderSn());
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付订单扩展信息
|
||||
*/
|
||||
private void updatePayOrderExtension(PayOrderExtensionEntity extension, PayOrderRespDTO payOrderRespDTO) {
|
||||
if (payOrderRespDTO.getPrepayId() != null || payOrderRespDTO.getPayParams() != null) {
|
||||
// 将 payParams 对象转换为 JSON 字符串存储
|
||||
String channelExtras = null;
|
||||
if (payOrderRespDTO.getPayParams() != null) {
|
||||
try {
|
||||
channelExtras = cn.hutool.json.JSONUtil.toJsonStr(payOrderRespDTO.getPayParams());
|
||||
} catch (Exception e) {
|
||||
channelExtras = payOrderRespDTO.getPayParams().toString();
|
||||
}
|
||||
}
|
||||
extension.setChannelExtras(channelExtras);
|
||||
payOrderExtensionService.updateById(extension);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract PayOrderCallBackRespDTO parseOrderNotify(Map<String, String> headers, Map<String, String> params, String body);
|
||||
|
||||
protected abstract Object getPrepayRequestParam(PayOrderReqDTO payOrderReq);
|
||||
|
||||
/**
|
||||
* 具体支付渠道实现创建预支付订单
|
||||
*/
|
||||
protected abstract PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrderReqDTO , Object extrasRequest);
|
||||
protected abstract PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrderReqDTO);
|
||||
|
||||
/**
|
||||
* 具体支付渠道实现查询支付状态
|
||||
*/
|
||||
protected abstract PayOrderEntity doQueryPayStatus(String orderSn);
|
||||
protected abstract PayOrderRespDTO doQueryPayStatus(String orderSn);
|
||||
|
||||
/**
|
||||
* 具体支付渠道实现关闭支付订单
|
||||
*/
|
||||
protected abstract boolean doClosePayOrder(String orderSn);
|
||||
protected abstract String doClosePayOrder(String orderSn);
|
||||
|
||||
|
||||
protected abstract RefundSubmitRespDTO doRefundOrder(PayRefundReqDTO payRefundReqDTO);
|
||||
@ -136,12 +77,12 @@ public abstract class AbstractPayClient implements PayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderEntity queryPayStatus(String orderSn) {
|
||||
public PayOrderRespDTO queryPayStatus(String orderSn) {
|
||||
return doQueryPayStatus(orderSn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean closePayOrder(String orderSn) {
|
||||
public String closePayOrder(String orderSn) {
|
||||
return doClosePayOrder(orderSn);
|
||||
}
|
||||
|
||||
|
||||
@ -2,12 +2,10 @@ package com.seer.teach.pay.app.client;
|
||||
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.app.pay.controller.req.OrderPayReq;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -21,9 +19,8 @@ public interface PayClient {
|
||||
/**
|
||||
* 初始化支付客户端
|
||||
*
|
||||
* @param configJson 配置信息
|
||||
*/
|
||||
void init(String configJson);
|
||||
void init();
|
||||
|
||||
/**
|
||||
* 获取支付渠道编码
|
||||
@ -43,7 +40,7 @@ public interface PayClient {
|
||||
* @param orderSn 订单号
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderEntity queryPayStatus(String orderSn);
|
||||
PayOrderRespDTO queryPayStatus(String orderSn);
|
||||
|
||||
/**
|
||||
* 处理支付回调
|
||||
@ -59,7 +56,7 @@ public interface PayClient {
|
||||
* @param orderSn 订单号
|
||||
* @return 是否关闭成功
|
||||
*/
|
||||
boolean closePayOrder(String orderSn);
|
||||
String closePayOrder(String orderSn);
|
||||
|
||||
/**
|
||||
* 判断是否是异步回调
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
package com.seer.teach.pay.app.client;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.pay.app.client.alipay.AbstractAlipayPayClient;
|
||||
import com.seer.teach.pay.app.client.alipay.AlipayQrPayClient;
|
||||
import com.seer.teach.pay.app.client.coin.CoinPayClient;
|
||||
import com.seer.teach.pay.app.client.wechat.WechatJsApiPayClient;
|
||||
import com.seer.teach.pay.app.client.wechat.WechatNativePayClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 支付策略工厂,根据支付渠道编码获取对应的支付策略
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class PayClientFactory implements InitializingBean {
|
||||
|
||||
private static final Map<PayChannelEnum, PayClient> CLIENT_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<PayChannelEnum, Class<? extends PayClient>> clientClass = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据支付渠道编码获取支付策略
|
||||
*
|
||||
* @param channel 支付渠道编码(如:WECHAT、ALIPAY)
|
||||
* @return 支付策略
|
||||
*/
|
||||
public PayClient getPayClient(PayChannelEnum channel, String configJson) {
|
||||
PayClient client = CLIENT_MAP.get(channel);
|
||||
if (Objects.isNull(client)) {
|
||||
Class<? extends PayClient> clazz = clientClass.get(channel);
|
||||
client = ReflectUtil.newInstance(clazz, configJson);
|
||||
client.init();
|
||||
CLIENT_MAP.put(channel, client);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
clientClass.put(PayChannelEnum.ALIPAY_QR, AlipayQrPayClient.class);
|
||||
|
||||
clientClass.put(PayChannelEnum.WECHAT_NATIVE, WechatNativePayClient.class);
|
||||
clientClass.put(PayChannelEnum.WECHAT_JSAPI, WechatJsApiPayClient.class);
|
||||
|
||||
clientClass.put(PayChannelEnum.COIN, CoinPayClient.class);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
package com.seer.teach.pay.app.client.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayConfig;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradeRefundModel;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.internal.util.AntCertificationUtil;
|
||||
import com.alipay.api.internal.util.codec.Base64;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
import com.alipay.api.response.AlipayTradeRefundResponse;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.pay.app.client.AbstractPayClient;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderCallBackRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||
|
||||
/**
|
||||
* 支付宝支付策略实现
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractAlipayPayClient extends AbstractPayClient {
|
||||
|
||||
protected DefaultAlipayClient client;
|
||||
|
||||
public AbstractAlipayPayClient(String channelCode, String configJson) {
|
||||
super(channelCode, configJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
AlipayConfig alipayConfig = JSONUtil.toBean(config, AlipayConfig.class);
|
||||
try {
|
||||
this.client = new DefaultAlipayClient(alipayConfig);
|
||||
} catch (AlipayApiException e) {
|
||||
log.error("支付宝初始化失败", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncCallback() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RefundSubmitRespDTO doRefundOrder(PayRefundReqDTO payRefundReqDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RefundNotificationRespDTO> parseRefundNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundSubmitRespDTO doQueryRefundStatus(String refundSn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderCallBackRespDTO parseOrderNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
|
||||
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||
PayStatusEnum status = parseStatus(bodyObj.get("trade_status"));
|
||||
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
|
||||
if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
|
||||
status = PayStatusEnum.WECHAT_REFUND_SUCCESS;
|
||||
}
|
||||
PayOrderCallBackRespDTO payOrderCallBackRespDTO = new PayOrderCallBackRespDTO();
|
||||
payOrderCallBackRespDTO.setPayStatus(status);
|
||||
payOrderCallBackRespDTO.setPayOrderSn(bodyObj.get("out_trade_no"));
|
||||
payOrderCallBackRespDTO.setPayOrderId(MapUtil.getInt(bodyObj, "trade_no"));
|
||||
payOrderCallBackRespDTO.setPaySuccessTime(parseTime(bodyObj.get("gmt_payment")));
|
||||
payOrderCallBackRespDTO.setResponse(body);
|
||||
return payOrderCallBackRespDTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doQueryPayStatus(String orderSn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doClosePayOrder(String orderSn) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static PayStatusEnum parseStatus(String tradeStatus) {
|
||||
if(Objects.equals("WAIT_BUYER_PAY", tradeStatus)){
|
||||
return PayStatusEnum.PENDING;
|
||||
}
|
||||
if(Objects.equals("TRADE_FINISHED", tradeStatus) || Objects.equals("TRADE_SUCCESS", tradeStatus)){
|
||||
return PayStatusEnum.SUCCESS;
|
||||
}
|
||||
if(Objects.equals("TRADE_CLOSED", tradeStatus)){
|
||||
return PayStatusEnum.WECHAT_REFUND_CLOSED;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String formatAmount(Integer amount) {
|
||||
return String.valueOf(amount / 100.0);
|
||||
}
|
||||
|
||||
protected String formatTime(LocalDateTime time) {
|
||||
return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
protected LocalDateTime parseTime(String str) {
|
||||
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
package com.seer.teach.pay.app.client.alipay;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.utils.MoneyUtil;
|
||||
import com.seer.teach.pay.app.client.AbstractPayClient;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderCallBackRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderExtensionEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 支付宝支付策略实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AlipayPayClient extends AbstractPayClient {
|
||||
|
||||
|
||||
public AlipayPayClient() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付渠道的编码
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public PayChannelEnum getChannel() {
|
||||
return PayChannelEnum.ALIPAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncCallback() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RefundSubmitRespDTO doRefundOrder(PayRefundReqDTO payRefundReqDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RefundNotificationRespDTO> parseRefundNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundSubmitRespDTO doQueryRefundStatus(String refundSn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void init(String configJson) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderCallBackRespDTO parseOrderNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getPrepayRequestParam(PayOrderReqDTO payOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrder, Object extrasRequest) {
|
||||
// TODO: 实现支付宝预支付逻辑
|
||||
// 1. 调用支付宝SDK创建预支付订单
|
||||
// 2. 获取支付宝返回的支付参数
|
||||
PayOrderRespDTO response = new PayOrderRespDTO();
|
||||
response.setChannelCode(payOrder.getChannelCode());
|
||||
response.setStatus(PayStatusEnum.PENDING.getCode());
|
||||
// 支付宝通常返回支付字符串或二维码
|
||||
Map<String, Object> payParams = new HashMap<>();
|
||||
payParams.put("payString", "支付宝支付参数字符串");
|
||||
response.setPayParams(payParams); // 设置为对象格式
|
||||
response.setPrepayId(null); // 支付宝没有 prepayId 概念
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderEntity doQueryPayStatus(String orderSn) {
|
||||
log.info("查询支付宝支付状态,订单号:{}", orderSn);
|
||||
|
||||
PayOrderEntity payOrder = payOrderService.getOne(
|
||||
new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn)
|
||||
.eq(PayOrderEntity::getChannelCode, getChannel())
|
||||
);
|
||||
|
||||
if (payOrder == null) {
|
||||
log.warn("支付订单不存在,订单号:{}", orderSn);
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: 调用支付宝查询订单接口获取最新状态
|
||||
|
||||
return payOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doClosePayOrder(String orderSn) {
|
||||
log.info("关闭支付宝支付订单,订单号:{}", orderSn);
|
||||
|
||||
try {
|
||||
PayOrderEntity payOrder = payOrderService.getOne(
|
||||
new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn)
|
||||
.eq(PayOrderEntity::getChannelCode, getChannel())
|
||||
);
|
||||
|
||||
if (payOrder == null) {
|
||||
log.warn("支付订单不存在,订单号:{}", orderSn);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新订单状态为已取消
|
||||
boolean updateResult = payOrderService.update(
|
||||
new LambdaUpdateWrapper<PayOrderEntity>()
|
||||
.set(PayOrderEntity::getStatus, PayStatusEnum.CANCEL.getCode())
|
||||
.eq(PayOrderEntity::getId, payOrder.getId())
|
||||
);
|
||||
|
||||
// 更新扩展表状态
|
||||
if (updateResult) {
|
||||
payOrderExtensionService.update(
|
||||
new LambdaUpdateWrapper<PayOrderExtensionEntity>()
|
||||
.eq(PayOrderExtensionEntity::getOrderId, payOrder.getId())
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: 调用支付宝关闭订单接口
|
||||
|
||||
log.info("关闭支付宝支付订单结果:{},订单号:{}", updateResult, orderSn);
|
||||
return updateResult;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("关闭支付宝支付订单失败,订单号:{}", orderSn, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.seer.teach.pay.app.client.alipay;
|
||||
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.domain.AlipayTradePrecreateModel;
|
||||
import com.alipay.api.request.AlipayTradePrecreateRequest;
|
||||
import com.alipay.api.response.AlipayTradePrecreateResponse;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class AlipayQrPayClient extends AbstractAlipayPayClient{
|
||||
|
||||
|
||||
public AlipayQrPayClient(String configJson) {
|
||||
super(PayChannelEnum.ALIPAY_QR.getCode(), configJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrderReqDTO) {
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
model.setOutTradeNo(payOrderReqDTO.getPayOrderSn());
|
||||
model.setSubject(payOrderReqDTO.getDescription());
|
||||
model.setTotalAmount(formatAmount(payOrderReqDTO.getTotalAmount()));
|
||||
model.setProductCode("FACE_TO_FACE_PAYMENT");
|
||||
|
||||
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
|
||||
request.setBizModel(model);
|
||||
request.setNotifyUrl(payOrderReqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(payOrderReqDTO.getReturnUrl());
|
||||
|
||||
try {
|
||||
AlipayTradePrecreateResponse response = client.execute(request);
|
||||
|
||||
PayOrderRespDTO payOrderRespDTO = new PayOrderRespDTO();
|
||||
payOrderRespDTO.setChannelCode(channelCode);
|
||||
payOrderRespDTO.setPayOrderSn(payOrderReqDTO.getPayOrderSn());
|
||||
payOrderRespDTO.setQrCodeUrl(response.getQrCode());
|
||||
payOrderRespDTO.setStatus(response.isSuccess() ? 0 : 2);
|
||||
payOrderRespDTO.setResponse(response.getBody());
|
||||
return payOrderRespDTO;
|
||||
} catch (AlipayApiException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayChannelEnum getChannel() {
|
||||
return PayChannelEnum.ALIPAY_QR;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package com.seer.teach.pay.app.client.coin;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.TradeTypeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
@ -18,30 +17,39 @@ import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.entity.PayCoinAccountEntity;
|
||||
import com.seer.teach.pay.entity.PayCoinAccountTradeLogEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderExtensionEntity;
|
||||
import com.seer.teach.pay.service.IPayCoinAccountService;
|
||||
import com.seer.teach.pay.service.IPayCoinAccountTradeLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 学豆支付策略实现
|
||||
* 钱包支付实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CoinPayClient extends AbstractPayClient {
|
||||
|
||||
private final IPayCoinAccountService payConAccountService;
|
||||
private IPayCoinAccountService payConAccountService;
|
||||
|
||||
private final IPayCoinAccountTradeLogService userCoinAccountTradeLogService;
|
||||
private IPayCoinAccountTradeLogService userCoinAccountTradeLogService;
|
||||
|
||||
public CoinPayClient(String config) {
|
||||
super(PayChannelEnum.COIN.getCode(), config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
if(Objects.isNull(payConAccountService)){
|
||||
payConAccountService = SpringUtil.getBean(IPayCoinAccountService.class);
|
||||
}
|
||||
if(Objects.isNull(userCoinAccountTradeLogService)){
|
||||
userCoinAccountTradeLogService = SpringUtil.getBean(IPayCoinAccountTradeLogService.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付渠道的编码
|
||||
@ -61,11 +69,11 @@ public class CoinPayClient extends AbstractPayClient {
|
||||
@Override
|
||||
protected RefundSubmitRespDTO doRefundOrder(PayRefundReqDTO payRefundReqDTO) {
|
||||
Integer userId = payRefundReqDTO.getUserId();
|
||||
Long refundAmount = payRefundReqDTO.getRefundPrice();
|
||||
Integer refundAmount = payRefundReqDTO.getRefundPrice();
|
||||
BigDecimal refundPrice = BigDecimal.valueOf(refundAmount);
|
||||
PayCoinAccountEntity beforeAccount = payConAccountService.getByUserId(userId);
|
||||
|
||||
// 增加用户学豆余额
|
||||
// 增加用户余额
|
||||
boolean addBalanceResult = payConAccountService.addBalance(userId, beforeAccount.getId(), refundPrice);
|
||||
log.info("确认退款的增加余额结果:{}", addBalanceResult);
|
||||
|
||||
@ -107,31 +115,25 @@ public class CoinPayClient extends AbstractPayClient {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void init(String configJson) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderCallBackRespDTO parseOrderNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getPrepayRequestParam(PayOrderReqDTO payOrderReqDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrder, Object extrasRequest) {
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrder) {
|
||||
try {
|
||||
log.info("开始创建学豆支付订单,订单号:{},用户ID:{},支付学豆:{},",
|
||||
log.info("开始创建支付订单,订单号:{},用户ID:{},支付:{},",
|
||||
payOrder.getPayOrderSn(), payOrder.getUserId(), payOrder.getTotalCoin());
|
||||
|
||||
Integer userId = payOrder.getUserId();
|
||||
BigDecimal totalCoin = payOrder.getTotalCoin();
|
||||
|
||||
// 获取用户学豆账户
|
||||
// 获取用户账户
|
||||
PayCoinAccountEntity coinAccount = payConAccountService.getByUserId(userId);
|
||||
AssertUtils.notNull(coinAccount, ResultCodeEnum.USER_NOT_FOUND);
|
||||
|
||||
@ -140,20 +142,20 @@ public class CoinPayClient extends AbstractPayClient {
|
||||
throw new CommonException(ResultCodeEnum.USER_COIN_LOCK_ERROR);
|
||||
}
|
||||
|
||||
// 检查学豆余额是否充足
|
||||
// 检查余额是否充足
|
||||
BigDecimal availableBalance = coinAccount.getAvailableBalance();
|
||||
if (availableBalance == null || availableBalance.compareTo(totalCoin) < 0) {
|
||||
log.warn("学豆余额不足,用户ID:{},可用余额:{},需要支付:{}", userId, availableBalance, totalCoin);
|
||||
log.warn("余额不足,用户ID:{},可用余额:{},需要支付:{}", userId, availableBalance, totalCoin);
|
||||
throw new CommonException(ResultCodeEnum.USER_NOT_ENOUGH_COIN,
|
||||
String.format("学豆余额不足,可用学豆余额:%s,需要支付学豆:%s", availableBalance, totalCoin));
|
||||
String.format("余额不足,可用余额:%s,需要支付:%s", availableBalance, totalCoin));
|
||||
}
|
||||
|
||||
// 扣减学豆余额
|
||||
// 扣减余额
|
||||
boolean deductResult = payConAccountService.deductBalance(userId, coinAccount.getId(), totalCoin);
|
||||
if (!deductResult) {
|
||||
log.error("扣减学豆余额失败,用户ID:{},账户ID:{},扣减学豆:{}",
|
||||
log.error("扣减余额失败,用户ID:{},账户ID:{},扣减:{}",
|
||||
userId, coinAccount.getId(), totalCoin);
|
||||
throw new CommonException(ResultCodeEnum.USER_COIN_PAY_ERROR, "学豆扣减失败");
|
||||
throw new CommonException(ResultCodeEnum.USER_COIN_PAY_ERROR, "扣减失败");
|
||||
}else {
|
||||
// 添加用户账户流水
|
||||
PayCoinAccountEntity afterAccount = payConAccountService.getByUserId(userId);
|
||||
@ -174,70 +176,35 @@ public class CoinPayClient extends AbstractPayClient {
|
||||
response.setStatus(PayStatusEnum.SUCCESS.getCode());
|
||||
response.setPayOrderSn(payOrder.getPayOrderSn());
|
||||
response.setPaySuccessTime(LocalDateTime.now());
|
||||
log.info("学豆支付订单完成,订单号:{},支付学豆:{}", payOrder.getPayOrderSn(), payOrder.getTotalCoin());
|
||||
log.info("支付订单完成,订单号:{},支付:{}", payOrder.getPayOrderSn(), payOrder.getTotalCoin());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("创建学豆支付订单失败,订单号:{},支付学豆:{}", payOrder.getPayOrderSn(), payOrder.getTotalCoin(), e);
|
||||
log.error("创建支付订单失败,订单号:{},支付:{}", payOrder.getPayOrderSn(), payOrder.getTotalCoin(), e);
|
||||
throw new CommonException(ResultCodeEnum.USER_COIN_PAY_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询学豆支付状态
|
||||
* 查询支付状态
|
||||
*
|
||||
* @param orderSn 订单号
|
||||
* @return 支付订单
|
||||
*/
|
||||
@Override
|
||||
protected PayOrderEntity doQueryPayStatus(String orderSn) {
|
||||
log.info("查询学豆支付状态,订单号:{}", orderSn);
|
||||
PayOrderEntity payOrder = payOrderService.getOne(
|
||||
new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn)
|
||||
.eq(PayOrderEntity::getChannelCode, getChannel()));
|
||||
AssertUtils.notNull(payOrder, ResultCodeEnum.ORDER_NOT_FOUND);
|
||||
return payOrder;
|
||||
protected PayOrderRespDTO doQueryPayStatus(String orderSn) {
|
||||
log.info("查询支付状态,订单号:{}", orderSn);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭学豆支付订单
|
||||
* 关闭支付订单
|
||||
*
|
||||
* @param orderSn 订单号
|
||||
* @return 关闭结果
|
||||
*/
|
||||
@Override
|
||||
protected boolean doClosePayOrder(String orderSn) {
|
||||
try {
|
||||
log.info("关闭学豆支付订单,订单号:{}", orderSn);
|
||||
// 查询订单
|
||||
PayOrderEntity payOrder = payOrderService.getOne(
|
||||
new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn)
|
||||
.eq(PayOrderEntity::getChannelCode, getChannel()));
|
||||
AssertUtils.notNull(payOrder, ResultCodeEnum.ORDER_NOT_FOUND);
|
||||
|
||||
// 学豆支付订单是待支付状态才能关闭
|
||||
if (PayStatusEnum.PENDING.getCode() != payOrder.getStatus()) {
|
||||
log.warn("学豆支付订单状态不允许关闭,订单号:{},当前状态:{}", orderSn, payOrder.getStatus());
|
||||
return false;
|
||||
}
|
||||
// 更新订单状态为已取消
|
||||
boolean updateResult = payOrderService.update(
|
||||
new LambdaUpdateWrapper<PayOrderEntity>()
|
||||
.set(PayOrderEntity::getStatus, PayStatusEnum.CANCEL.getCode())
|
||||
.eq(PayOrderEntity::getId, payOrder.getId()));
|
||||
// 更新扩展表
|
||||
if (updateResult) {
|
||||
payOrderExtensionService.update(
|
||||
new LambdaUpdateWrapper<PayOrderExtensionEntity>()
|
||||
.eq(PayOrderExtensionEntity::getOrderId, payOrder.getId()));
|
||||
}
|
||||
log.info("关闭学豆支付订单结果:{},订单号:{}", updateResult, orderSn);
|
||||
return updateResult;
|
||||
} catch (Exception e) {
|
||||
log.error("关闭学豆支付订单失败,订单号:{}", orderSn, e);
|
||||
return false;
|
||||
}
|
||||
protected String doClosePayOrder(String orderSn) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@ -50,11 +50,14 @@ public interface PayClientConvert {
|
||||
* @param payReq
|
||||
* @return PayOrderReqDTO
|
||||
*/
|
||||
@Mapping(source = "payOrderEntity.subject", target = "description")
|
||||
@Mapping(source = "payOrderEntity.subject", target = "subject")
|
||||
@Mapping(source = "payReq.channelCode", target = "channelCode")
|
||||
@Mapping(source = "payOrderEntity.merchantOrderSn", target = "merchantOrderSn")
|
||||
@Mapping(source = "payOrderEntity.id", target = "parOrderId")
|
||||
@Mapping(source = "payOrderEntity.orderSn", target = "payOrderSn")
|
||||
@Mapping(source = "payOrderEntity.body", target = "description")
|
||||
@Mapping(source = "payOrderEntity.totalAmount", target = "totalAmount")
|
||||
@Mapping(source = "payOrderEntity.expireTime", target = "expireTime")
|
||||
PayOrderReqDTO convert2PayOrderReqDTO(PayOrderEntity payOrderEntity,OrderPayReq payReq);
|
||||
|
||||
}
|
||||
@ -42,4 +42,13 @@ public class PayOrderCallBackRespDTO {
|
||||
* 订单来源
|
||||
*/
|
||||
private Integer orderSource;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
package com.seer.teach.pay.app.client.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付订单的请求信息
|
||||
@ -15,6 +19,11 @@ public class PayOrderReqDTO {
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 用户IP
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 支付订单SN
|
||||
*/
|
||||
@ -43,7 +52,12 @@ public class PayOrderReqDTO {
|
||||
/**
|
||||
* 实际支付订单总金额,单位为分,整型,必须大于0
|
||||
*/
|
||||
private Long totalAmount;
|
||||
private Integer totalAmount;
|
||||
|
||||
/**
|
||||
* 商品
|
||||
*/
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 【商品描述】商品信息描述
|
||||
@ -54,4 +68,21 @@ public class PayOrderReqDTO {
|
||||
* 货币类型 说明:CNY:人民币
|
||||
*/
|
||||
private String currency;
|
||||
|
||||
/**
|
||||
* 支付过期时间
|
||||
*/
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 支付结果的 notify 回调地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 支付结果的 return 回调地址,支付宝使用
|
||||
*/
|
||||
private String returnUrl;
|
||||
|
||||
private Map<String, String> channelExtras = new HashMap<>();
|
||||
}
|
||||
|
||||
@ -15,9 +15,6 @@ public class PayOrderRespDTO {
|
||||
@Schema(description = "支付渠道编码")
|
||||
private String channelCode;
|
||||
|
||||
|
||||
|
||||
/******** jsAPI 预支付参数 *************/
|
||||
@Schema(description = "支付参数(对象格式)")
|
||||
private Object payParams;
|
||||
|
||||
@ -25,9 +22,6 @@ public class PayOrderRespDTO {
|
||||
private String prepayId;
|
||||
|
||||
|
||||
|
||||
/******** 回调参数 *************/
|
||||
|
||||
@Schema(description = "支付状态(0-待支付,1-支付成功,2-支付失败,3-申请退款,4-取消支付)")
|
||||
private Integer status;
|
||||
|
||||
@ -44,4 +38,7 @@ public class PayOrderRespDTO {
|
||||
private LocalDateTime paySuccessTime;
|
||||
|
||||
private String response;
|
||||
|
||||
@Schema(description = "二维码地址")
|
||||
private String qrCodeUrl;
|
||||
}
|
||||
|
||||
@ -33,12 +33,12 @@ public class PayRefundReqDTO {
|
||||
*
|
||||
* 目前微信支付在退款的时候,必须传递该字段
|
||||
*/
|
||||
private Long payPrice;
|
||||
private Integer payPrice;
|
||||
|
||||
/**
|
||||
* 退款金额,单位:分
|
||||
*/
|
||||
private Long refundPrice;
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
package com.seer.teach.pay.app.client.factory;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.utils.AssertUtils;
|
||||
import com.seer.teach.pay.app.client.PayClient;
|
||||
import com.seer.teach.pay.entity.PayChannelEntity;
|
||||
import com.seer.teach.pay.service.IPayChannelService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 支付策略工厂,根据支付渠道编码获取对应的支付策略
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PayClientFactory {
|
||||
|
||||
private final List<PayClient> payStrategies;
|
||||
|
||||
private final IPayChannelService payChannelService;
|
||||
|
||||
private static final Map<PayChannelEnum, PayClient> STRATEGY_MAP = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
for (PayClient strategy : payStrategies) {
|
||||
STRATEGY_MAP.put(strategy.getChannel(), strategy);
|
||||
log.info("注册支付渠道:{} -> {}", strategy.getChannel(), strategy.getClass().getSimpleName());
|
||||
}
|
||||
List<PayChannelEntity> channels = payChannelService.list(new LambdaUpdateWrapper<>(PayChannelEntity.class).eq(PayChannelEntity::getStatus, 1));
|
||||
AssertUtils.notEmpty(channels, ResultCodeEnum.PAY_CHANNEL_IS_NOT_EXIST);
|
||||
Integer count = 0;
|
||||
for (PayChannelEntity channel : channels) {
|
||||
PayClient strategy = STRATEGY_MAP.get(PayChannelEnum.fromCode(channel.getChannelCode()));
|
||||
if(Objects.nonNull(strategy) && StringUtils.hasText(channel.getConfigJson())){
|
||||
strategy.init(channel.getConfigJson());
|
||||
log.info("支付渠道:{} -> {}", channel.getChannelCode(), strategy.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
log.info("支付策略工厂初始化完成,共注册{}个策略", count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付渠道编码获取支付策略
|
||||
* @param channel 支付渠道编码(如:WECHAT、ALIPAY)
|
||||
* @return 支付策略
|
||||
*/
|
||||
public PayClient getPayClient(PayChannelEnum channel) {
|
||||
PayClient strategy = STRATEGY_MAP.get(channel);
|
||||
AssertUtils.notNull(strategy, ResultCodeEnum.PAY_CHANNEL_NOT_SUPPORTED);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
public PayClient getPayClient(String channelCode) {
|
||||
PayChannelEnum channel = PayChannelEnum.fromCode(channelCode);
|
||||
AssertUtils.notNull(channel, ResultCodeEnum.PAY_CHANNEL_IS_NOT_EXIST);
|
||||
PayClient strategy = STRATEGY_MAP.get(channel);
|
||||
AssertUtils.notNull(strategy, ResultCodeEnum.PAY_CHANNEL_NOT_SUPPORTED);
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,346 @@
|
||||
package com.seer.teach.pay.app.client.wechat;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayOrderQueryV3Request;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayRefundQueryV3Request;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayOrderCloseResult;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.enums.pay.RefundStatusEnum;
|
||||
import com.seer.teach.common.exception.CommonException;
|
||||
import com.seer.teach.common.utils.DateUtils;
|
||||
import com.seer.teach.pay.app.client.AbstractPayClient;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderCallBackRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.app.util.ConfigDecryptUtil;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractWxPayClient extends AbstractPayClient {
|
||||
|
||||
|
||||
protected WxPayService client;
|
||||
protected String notifyUrl;
|
||||
|
||||
protected String refundNotifyUrl;
|
||||
|
||||
public AbstractWxPayClient(String channelCode, String configJson) {
|
||||
super(channelCode, configJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
* @param tradeType
|
||||
*/
|
||||
protected void doInit(String tradeType) {
|
||||
WxPayConfig wxPayConfig = new WxPayConfig();
|
||||
wxPayConfig.setTradeType(tradeType);
|
||||
JSONObject channelConfig = JSONUtil.parseObj(config);
|
||||
// 从渠道配置获取必要参数,并进行解密处理
|
||||
String appId = channelConfig.getStr("appId");
|
||||
String mchId = channelConfig.getStr("mchId");
|
||||
String apiV3Key = channelConfig.getStr("apiV3Key");
|
||||
String publicKeyId = channelConfig.getStr("publicKeyId");
|
||||
String merchantSerialNumber = channelConfig.getStr("merchantSerialNumber");
|
||||
String privateKeyPem = channelConfig.getStr("privateKeyPem");
|
||||
String publicKeyPem = channelConfig.getStr("publicKeyPem");
|
||||
this.notifyUrl = channelConfig.getStr("jsPayNotifyUrl");
|
||||
this.refundNotifyUrl = channelConfig.getStr("jsRefundNotifyUrl");
|
||||
// 使用安全解密方法处理敏感配置参数
|
||||
apiV3Key = ConfigDecryptUtil.safeDecrypt(apiV3Key);
|
||||
publicKeyId = ConfigDecryptUtil.safeDecrypt(publicKeyId);
|
||||
merchantSerialNumber = ConfigDecryptUtil.safeDecrypt(merchantSerialNumber);
|
||||
// 对PEM格式证书使用特殊处理
|
||||
privateKeyPem = cleanPemContent(ConfigDecryptUtil.safeDecrypt(privateKeyPem, true));
|
||||
publicKeyPem = cleanPemContent(ConfigDecryptUtil.safeDecrypt(publicKeyPem, true));
|
||||
|
||||
wxPayConfig.setAppId(ConfigDecryptUtil.safeDecrypt(appId));
|
||||
wxPayConfig.setMchId(ConfigDecryptUtil.safeDecrypt(mchId));
|
||||
wxPayConfig.setApiV3Key(ConfigDecryptUtil.safeDecrypt(apiV3Key));
|
||||
wxPayConfig.setCertSerialNo(merchantSerialNumber);
|
||||
|
||||
wxPayConfig.setPublicKeyId(ConfigDecryptUtil.safeDecrypt(publicKeyId));
|
||||
wxPayConfig.setPublicKeyString(publicKeyPem);
|
||||
|
||||
wxPayConfig.setPrivateCertString(privateKeyPem);
|
||||
|
||||
wxPayConfig.setNotifyUrl(notifyUrl);
|
||||
wxPayConfig.setRefundNotifyUrl(refundNotifyUrl);
|
||||
|
||||
client = new WxPayServiceImpl();
|
||||
client.setConfig(wxPayConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderCallBackRespDTO parseOrderNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
SignatureHeader signatureHeader = getRequestHeader(headers);
|
||||
TransferBillsNotifyResult response = null;
|
||||
try {
|
||||
response = client.getTransferService().parseTransferBillsNotifyResult(body, signatureHeader);
|
||||
|
||||
TransferBillsNotifyResult.DecryptNotifyResult result = response.getResult();
|
||||
|
||||
PayOrderCallBackRespDTO payOrderCallBackRespDTO = new PayOrderCallBackRespDTO();
|
||||
payOrderCallBackRespDTO.setPayOrderSn(result.getOutBillNo());
|
||||
payOrderCallBackRespDTO.setPayStatus(PayStatusEnum.fromWxOrderCode(result.getState()));
|
||||
payOrderCallBackRespDTO.setPaySuccessTime(DateUtils.parseStringToLocalDateTime(result.getUpdateTime()));
|
||||
payOrderCallBackRespDTO.setTransactionId(result.getTransferBillNo());
|
||||
payOrderCallBackRespDTO.setChannelErrorCode(result.getState());
|
||||
payOrderCallBackRespDTO.setChannelErrorMsg(result.getFailReason());
|
||||
payOrderCallBackRespDTO.setResponse(JSONUtil.toJsonStr(result));
|
||||
return payOrderCallBackRespDTO;
|
||||
} catch (WxPayException e) {
|
||||
log.error("微信回调失败", e);
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doQueryPayStatus(String orderSn) {
|
||||
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
|
||||
.setOutTradeNo(orderSn);
|
||||
try {
|
||||
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
|
||||
PayOrderRespDTO payOrderRespDTO = new PayOrderRespDTO();
|
||||
payOrderRespDTO.setChannelCode(channelCode);
|
||||
payOrderRespDTO.setPayOrderSn(orderSn);
|
||||
payOrderRespDTO.setStatus(parseStatus(response.getTradeState()));
|
||||
payOrderRespDTO.setPaySuccessTime(parseDateV3(response.getSuccessTime()));
|
||||
payOrderRespDTO.setTransactionId(response.getTransactionId());
|
||||
payOrderRespDTO.setResponse(JSONUtil.toJsonStr(response));
|
||||
return payOrderRespDTO;
|
||||
} catch (WxPayException e) {
|
||||
log.error("微信查询订单失败", e);
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RefundSubmitRespDTO doRefundOrder(PayRefundReqDTO reqDTO) {
|
||||
WxPayRefundV3Request request = new WxPayRefundV3Request()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
|
||||
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
|
||||
.setReason(reqDTO.getReason())
|
||||
.setNotifyUrl(this.refundNotifyUrl);
|
||||
try {
|
||||
WxPayRefundV3Result response = client.refundV3(request);
|
||||
|
||||
RefundSubmitRespDTO refundSubmitRespDTO = new RefundSubmitRespDTO();
|
||||
refundSubmitRespDTO.setRefundId(response.getRefundId());
|
||||
refundSubmitRespDTO.setTransactionId(response.getTransactionId());
|
||||
refundSubmitRespDTO.setPayOrderSn(reqDTO.getOutTradeNo());
|
||||
refundSubmitRespDTO.setRefundOrderSn(reqDTO.getOutRefundNo());
|
||||
refundSubmitRespDTO.setMerchantRefundSn(response.getOutRefundNo());
|
||||
refundSubmitRespDTO.setSuccessTime(parseDateV3(response.getSuccessTime()));
|
||||
refundSubmitRespDTO.setStatus(PayStatusEnum.SUCCESS.getCode());
|
||||
refundSubmitRespDTO.setRefundResJson(JSONUtil.toJsonStr(response));
|
||||
|
||||
String status = response.getStatus();
|
||||
if (status.equals("PROCESSING")) {
|
||||
refundSubmitRespDTO.setStatus(RefundStatusEnum.PENDING_WECHAT_REFUND.getCode());
|
||||
} else if (status.equals("SUCCESS")) {
|
||||
refundSubmitRespDTO.setStatus(RefundStatusEnum.REFUND_SUCCESS.getCode());
|
||||
} else {
|
||||
refundSubmitRespDTO.setStatus(RefundStatusEnum.WECHAT_REFUND_FAILED.getCode());
|
||||
}
|
||||
|
||||
return refundSubmitRespDTO;
|
||||
} catch (WxPayException e) {
|
||||
throw new CommonException(ResultCodeEnum.REFUND_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<RefundNotificationRespDTO> parseRefundNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
SignatureHeader signatureHeader = getRequestHeader(headers);
|
||||
try {
|
||||
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader);
|
||||
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
|
||||
|
||||
RefundNotificationRespDTO refundNotificationRespDTO = new RefundNotificationRespDTO();
|
||||
refundNotificationRespDTO.setRefundId(result.getRefundId());
|
||||
refundNotificationRespDTO.setTransactionId(result.getTransactionId());
|
||||
refundNotificationRespDTO.setSuccessTime(parseDateV3(result.getSuccessTime()));
|
||||
refundNotificationRespDTO.setRefundStatus(RefundStatusEnum.fromWxCode(result.getRefundStatus()));
|
||||
|
||||
return Optional.of(refundNotificationRespDTO);
|
||||
} catch (WxPayException e) {
|
||||
throw new CommonException(ResultCodeEnum.REFUND_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefundSubmitRespDTO doQueryRefundStatus(String refundSn) {
|
||||
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
|
||||
request.setOutRefundNo(refundSn);
|
||||
try {
|
||||
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
|
||||
|
||||
RefundStatusEnum refundStatusEnum = RefundStatusEnum.fromWxCode(response.getStatus());
|
||||
|
||||
RefundSubmitRespDTO refundSubmitRespDTO = new RefundSubmitRespDTO();
|
||||
refundSubmitRespDTO.setRefundId(response.getRefundId());
|
||||
refundSubmitRespDTO.setTransactionId(response.getTransactionId());
|
||||
refundSubmitRespDTO.setPayOrderSn(response.getOutTradeNo());
|
||||
refundSubmitRespDTO.setRefundOrderSn(refundSn);
|
||||
refundSubmitRespDTO.setMerchantRefundSn(response.getOutRefundNo());
|
||||
refundSubmitRespDTO.setSuccessTime(parseDateV3(response.getSuccessTime()));
|
||||
refundSubmitRespDTO.setStatus(refundStatusEnum.getCode());
|
||||
refundSubmitRespDTO.setRefundResJson(JSONUtil.toJsonStr(response));
|
||||
return refundSubmitRespDTO;
|
||||
} catch (WxPayException e) {
|
||||
throw new CommonException(ResultCodeEnum.REFUND_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doClosePayOrder(String orderSn) {
|
||||
try {
|
||||
WxPayOrderCloseResult closeResult = client.closeOrder(orderSn);
|
||||
return closeResult.getResultMsg();
|
||||
} catch (WxPayException e) {
|
||||
log.error("微信关闭订单失败", e);
|
||||
throw new CommonException(ResultCodeEnum.REFUND_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
protected WxPayUnifiedOrderV3Request buildPayRequestV3(PayOrderReqDTO reqDTO) {
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||
request.setOutTradeNo(reqDTO.getPayOrderSn());
|
||||
request.setDescription(reqDTO.getDescription());
|
||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getTotalAmount())); // 单位分
|
||||
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(getNotifyUrl());
|
||||
return request;
|
||||
}
|
||||
|
||||
protected String formatDateV3(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
|
||||
protected LocalDateTime parseDateV3(String time) {
|
||||
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
|
||||
private SignatureHeader getRequestHeader(Map<String, String> headers) {
|
||||
return SignatureHeader.builder()
|
||||
.signature(getHeaderValue(headers, "Wechatpay-Signature", "wechatpay-signature"))
|
||||
.nonce(getHeaderValue(headers, "Wechatpay-Nonce", "wechatpay-nonce"))
|
||||
.serial(getHeaderValue(headers, "Wechatpay-Serial", "wechatpay-serial"))
|
||||
.timeStamp(getHeaderValue(headers, "Wechatpay-Timestamp", "wechatpay-timestamp"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getHeaderValue(Map<String, String> headers, String capitalizedKey, String lowercaseKey) {
|
||||
String value = headers.get(capitalizedKey);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
return headers.get(lowercaseKey);
|
||||
}
|
||||
|
||||
|
||||
protected String getNotifyUrl() {
|
||||
return notifyUrl;
|
||||
}
|
||||
|
||||
protected static Integer parseStatus(String tradeState) {
|
||||
switch (tradeState) {
|
||||
case "NOTPAY":
|
||||
case "USERPAYING":
|
||||
return PayStatusEnum.PENDING.getCode();
|
||||
case "SUCCESS":
|
||||
return PayStatusEnum.SUCCESS.getCode();
|
||||
case "REFUND":
|
||||
return PayStatusEnum.WECHAT_REFUND_SUCCESS.getCode();
|
||||
case "CLOSED":
|
||||
case "REVOKED":
|
||||
case "PAYERROR":
|
||||
return PayStatusEnum.PAY_FAILED.getCode();
|
||||
default:
|
||||
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理PEM内容,确保符合微信SDK要求
|
||||
*
|
||||
* @param pemContent PEM内容
|
||||
* @return 清理后的PEM内容
|
||||
*/
|
||||
protected String cleanPemContent(String pemContent) {
|
||||
if (pemContent == null || pemContent.isEmpty()) {
|
||||
return pemContent;
|
||||
}
|
||||
|
||||
try {
|
||||
// 移除所有反斜杠
|
||||
pemContent = pemContent.replace("\\", "");
|
||||
|
||||
// 确保正确的换行符
|
||||
pemContent = pemContent.replace("\\n", "\n")
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\r", "\n");
|
||||
|
||||
// 移除多余的空白字符,但要确保BEGIN和END标记格式正确
|
||||
pemContent = pemContent.trim();
|
||||
|
||||
// 确保BEGIN和END标记格式正确(包含正确的空格)
|
||||
pemContent = pemContent.replace("-----BEGINPRIVATEKEY-----", "-----BEGIN PRIVATE KEY-----")
|
||||
.replace("-----ENDPRIVATEKEY-----", "-----END PRIVATE KEY-----")
|
||||
.replace("-----BEGINPUBLICKEY-----", "-----BEGIN PUBLIC KEY-----")
|
||||
.replace("-----ENDPUBLICKEY-----", "-----END PUBLIC KEY-----");
|
||||
|
||||
// 确保BEGIN和END标记独立成行
|
||||
if (pemContent.contains("-----BEGIN")) {
|
||||
// 先标准化标记格式
|
||||
pemContent = pemContent.replaceAll("-----BEGIN PRIVATE KEY-----", "\n-----BEGIN PRIVATE KEY-----\n")
|
||||
.replaceAll("-----END PRIVATE KEY-----", "\n-----END PRIVATE KEY-----\n")
|
||||
.replaceAll("-----BEGIN PUBLIC KEY-----", "\n-----BEGIN PUBLIC KEY-----\n")
|
||||
.replaceAll("-----END PUBLIC KEY-----", "\n-----END PUBLIC KEY-----\n")
|
||||
.replaceAll("\n+", "\n") // 将多个连续换行符合并为一个
|
||||
.trim();
|
||||
|
||||
// 确保标记前后有换行符
|
||||
pemContent = "\n" + pemContent + "\n";
|
||||
}
|
||||
|
||||
log.debug("PEM内容清理完成,长度: {}", pemContent.length());
|
||||
return pemContent;
|
||||
} catch (Exception e) {
|
||||
log.warn("PEM内容清理异常: {}", e.getMessage());
|
||||
return pemContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,48 +1,35 @@
|
||||
package com.seer.teach.pay.app.client.wechat;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.utils.DateUtils;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.seer.teach.common.constants.CommonConstant;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.exception.CommonException;
|
||||
import com.seer.teach.common.utils.AssertUtils;
|
||||
import com.seer.teach.common.utils.MoneyUtil;
|
||||
import com.seer.teach.pay.app.client.AbstractPayClient;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.app.client.wechat.core.WechatPlayClient;
|
||||
import com.seer.teach.pay.app.client.wechat.core.resp.WechatPrepayTransactionResp;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderCallBackRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderExtensionEntity;
|
||||
import com.seer.teach.user.api.UserInfoServiceApi;
|
||||
import com.seer.teach.user.api.dto.UserAuthDTO;
|
||||
import com.wechat.pay.java.service.payments.model.Transaction;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 微信支付策略实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WechatJsApiPayClient extends AbstractPayClient {
|
||||
public class WechatJsApiPayClient extends AbstractWxPayClient {
|
||||
|
||||
private WechatPlayClient wechatPlayClient;
|
||||
public WechatJsApiPayClient(String configJson){
|
||||
super(PayChannelEnum.WECHAT_JSAPI.getCode(), configJson);
|
||||
}
|
||||
|
||||
private final UserInfoServiceApi userInfoServiceApi;
|
||||
@Override
|
||||
public void init() {
|
||||
super.doInit(WxPayConstants.TradeType.JSAPI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付渠道的编码
|
||||
@ -60,75 +47,55 @@ public class WechatJsApiPayClient extends AbstractPayClient {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String configJson) {
|
||||
wechatPlayClient = new WechatPlayClient(configJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getPrepayRequestParam(PayOrderReqDTO payOrder) {
|
||||
UserAuthDTO userAuthDTO = userInfoServiceApi.getUserAuthByUserIdAndAppId(payOrder.getUserId(), wechatPlayClient.getAppId());
|
||||
return userAuthDTO.getOpenId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付订单
|
||||
*
|
||||
* @param payOrder 支付订单实体
|
||||
* @param reqDTO 支付订单实体
|
||||
* @return 创建的支付订单结果
|
||||
*/
|
||||
@Override
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrder, Object extrasRequest) {
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO reqDTO) {
|
||||
try {
|
||||
// 调用微信预支付接口时传入配置
|
||||
log.info("准备调用微信预支付接口");
|
||||
WechatPrepayTransactionResp wechatResp = wechatPlayClient.prepayPay(payOrder, extrasRequest);
|
||||
WxPayUnifiedOrderV3Request request = buildPayRequestV3(reqDTO);
|
||||
|
||||
WxPayUnifiedOrderV3Request.Payer payer = new WxPayUnifiedOrderV3Request.Payer();
|
||||
payer.setOpenid(reqDTO.getChannelExtras().get(CommonConstant.WX_OPEN_ID));
|
||||
WxPayUnifiedOrderV3Result.JsapiResult jsapiResult = client.createOrderV3(TradeTypeEnum.JSAPI, request);
|
||||
|
||||
log.info("微信预支付接口调用完成");
|
||||
// 检查微信预支付响应是否为空
|
||||
if (wechatResp == null) {
|
||||
log.error("微信预支付接口返回空响应,订单号:{}", payOrder.getPayOrderSn());
|
||||
if (Objects.isNull(jsapiResult)) {
|
||||
log.error("微信预支付接口返回空响应,订单号:{}", reqDTO.getPayOrderSn());
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "微信预支付接口返回空响应");
|
||||
}
|
||||
|
||||
log.info("微信预支付接口调用成功,响应数据:{}", wechatResp);
|
||||
|
||||
// 检查响应中的关键字段
|
||||
if (wechatResp.getPackageVal() == null || wechatResp.getPackageVal().isEmpty()) {
|
||||
log.error("微信预支付响应中packageVal为空,订单号:{}", payOrder.getPayOrderSn());
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "微信预支付响应中packageVal为空");
|
||||
}
|
||||
log.info("微信预支付接口调用成功,响应数据:{}", jsapiResult);
|
||||
|
||||
// 构造返回结果
|
||||
PayOrderRespDTO response = new PayOrderRespDTO();
|
||||
response.setChannelCode(payOrder.getChannelCode());
|
||||
response.setChannelCode(reqDTO.getChannelCode());
|
||||
response.setStatus(PayStatusEnum.PENDING.getCode());
|
||||
|
||||
// 构造微信支付参数
|
||||
Map<String, Object> payParams = new HashMap<>();
|
||||
payParams.put("appId", wechatResp.getAppId());
|
||||
payParams.put("timeStamp", wechatResp.getTimeStamp());
|
||||
payParams.put("nonceStr", wechatResp.getNonceStr());
|
||||
payParams.put("package", wechatResp.getPackageVal());
|
||||
payParams.put("signType", wechatResp.getSignType());
|
||||
payParams.put("paySign", wechatResp.getPaySign());
|
||||
response.setPayParams(payParams);
|
||||
response.setPayParams(jsapiResult);
|
||||
|
||||
// 从 packageVal 中提取 prepayId
|
||||
String prepayId = extractPrepayIdFromPackage(wechatResp.getPackageVal());
|
||||
String prepayId = extractPrepayIdFromPackage(jsapiResult.getPackageValue());
|
||||
response.setPrepayId(prepayId);
|
||||
|
||||
log.info("微信预支付订单创建成功,订单号:{},支付金额:{}元,prepayId:{}",
|
||||
payOrder.getPayOrderSn(), MoneyUtil.centToYuan(payOrder.getTotalAmount()), prepayId);
|
||||
reqDTO.getPayOrderSn(), MoneyUtil.centToYuan(reqDTO.getTotalAmount()), prepayId);
|
||||
|
||||
// 验证关键字段是否成功设置
|
||||
if (response.getPrepayId() == null || response.getPrepayId().isEmpty()) {
|
||||
log.error("prepayId为空,订单号:{}", payOrder.getPayOrderSn());
|
||||
log.error("prepayId为空,订单号:{}", reqDTO.getPayOrderSn());
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "prepayId为空");
|
||||
}
|
||||
|
||||
if (response.getPayParams() == null) {
|
||||
log.error("payParams为空,订单号:{}", payOrder.getPayOrderSn());
|
||||
log.error("payParams为空,订单号:{}", reqDTO.getPayOrderSn());
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "payParams为空");
|
||||
}
|
||||
|
||||
@ -136,96 +103,11 @@ public class WechatJsApiPayClient extends AbstractPayClient {
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("创建微信预支付订单失败,订单号:{},支付金额:{}元",
|
||||
payOrder.getPayOrderSn(), MoneyUtil.centToYuan(payOrder.getTotalAmount()), e);
|
||||
reqDTO.getPayOrderSn(), MoneyUtil.centToYuan(reqDTO.getTotalAmount()), e);
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "微信预支付失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付状态
|
||||
*
|
||||
* @param orderSn 订单号
|
||||
* @return 支付订单
|
||||
*/
|
||||
@Override
|
||||
protected PayOrderEntity doQueryPayStatus(String orderSn) {
|
||||
log.info("查询微信支付状态,订单号:{}", orderSn);
|
||||
PayOrderEntity payOrder = payOrderService.getOne(
|
||||
new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn)
|
||||
.eq(PayOrderEntity::getChannelCode, getChannel())
|
||||
);
|
||||
if (payOrder == null) {
|
||||
log.warn("支付订单不存在,订单号:{}", orderSn);
|
||||
return null;
|
||||
}
|
||||
if (Integer.valueOf(PayStatusEnum.SUCCESS.getCode()).equals(payOrder.getStatus()) ||
|
||||
Integer.valueOf(PayStatusEnum.PAY_FAILED.getCode()).equals(payOrder.getStatus())) {
|
||||
return payOrder;
|
||||
}
|
||||
return payOrder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public PayOrderCallBackRespDTO parseOrderNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
Transaction transaction = wechatPlayClient.parseOrderNotifyV3(headers, body).orElseThrow(() -> new CommonException(ResultCodeEnum.WX_PREPAY_ERROR));
|
||||
PayOrderCallBackRespDTO payOrderCallBackRespDTO = new PayOrderCallBackRespDTO();
|
||||
payOrderCallBackRespDTO.setPayOrderSn(transaction.getOutTradeNo());
|
||||
payOrderCallBackRespDTO.setPayStatus(PayStatusEnum.fromWxOrderCode(transaction.getTradeState().name()));
|
||||
payOrderCallBackRespDTO.setPaySuccessTime(DateUtils.parseStringToLocalDateTime(transaction.getSuccessTime()));
|
||||
payOrderCallBackRespDTO.setTransactionId(transaction.getTransactionId());
|
||||
payOrderCallBackRespDTO.setResponse(JSONUtil.toJsonStr(transaction));
|
||||
return payOrderCallBackRespDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭支付订单
|
||||
*
|
||||
* @param orderSn 订单号
|
||||
* @return 处理结果
|
||||
*/
|
||||
@Override
|
||||
protected boolean doClosePayOrder(String orderSn) {
|
||||
try {
|
||||
log.info("关闭微信支付订单,订单号:{}", orderSn);
|
||||
// 查询订单
|
||||
PayOrderEntity payOrder = payOrderService.getOne(
|
||||
new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn)
|
||||
.eq(PayOrderEntity::getChannelCode, getChannel()));
|
||||
AssertUtils.notNull(payOrder, ResultCodeEnum.ORDER_NOT_FOUND);
|
||||
// 只有待支付状态的订单才能关闭
|
||||
if (!Integer.valueOf(PayStatusEnum.PENDING.getCode()).equals(payOrder.getStatus())) {
|
||||
log.warn("订单状态不允许关闭,订单号:{},当前状态:{}", orderSn, payOrder.getStatus());
|
||||
return false;
|
||||
}
|
||||
// 更新订单状态为已取消
|
||||
boolean updateResult = payOrderService.update(
|
||||
new LambdaUpdateWrapper<PayOrderEntity>()
|
||||
.set(PayOrderEntity::getStatus, PayStatusEnum.CANCEL.getCode())
|
||||
.eq(PayOrderEntity::getId, payOrder.getId()));
|
||||
// 更新扩展表
|
||||
if (updateResult) {
|
||||
payOrderExtensionService.update(
|
||||
new LambdaUpdateWrapper<PayOrderExtensionEntity>()
|
||||
.eq(PayOrderExtensionEntity::getOrderId, payOrder.getId()));
|
||||
}
|
||||
// 调用微信关闭订单接口
|
||||
try {
|
||||
// 获取支付渠道配置
|
||||
boolean closeResult = wechatPlayClient.closeOrderWithConfig(orderSn);
|
||||
log.info("调用微信关闭订单接口结果:{},订单号:{}", closeResult, orderSn);
|
||||
} catch (Exception e) {
|
||||
log.error("调用微信关闭订单接口失败,订单号:{},错误信息:{}", orderSn, e.getMessage(), e);
|
||||
}
|
||||
log.info("关闭微信支付订单结果:{},订单号:{}", updateResult, orderSn);
|
||||
return updateResult;
|
||||
} catch (Exception e) {
|
||||
log.error("关闭微信支付订单失败,订单号:{}", orderSn, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 packageVal 中提取 prepayId
|
||||
@ -242,62 +124,4 @@ public class WechatJsApiPayClient extends AbstractPayClient {
|
||||
log.warn("无法从 packageVal 中提取 prepayId:{}", packageVal);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected RefundSubmitRespDTO doRefundOrder(PayRefundReqDTO payRefundReqDTO) {
|
||||
String outTradeNo = payRefundReqDTO.getOutTradeNo();
|
||||
String refundReason = payRefundReqDTO.getReason();
|
||||
Long refundAmount = payRefundReqDTO.getRefundPrice();
|
||||
Long totalAmount = payRefundReqDTO.getPayPrice();
|
||||
String refundSn = payRefundReqDTO.getOutRefundNo();
|
||||
try {
|
||||
log.info("开始处理微信退款订单:{}", payRefundReqDTO);
|
||||
//调用微信客户端提交退款
|
||||
RefundSubmitRespDTO wechatResp = wechatPlayClient.submitRefund(outTradeNo, refundReason, refundAmount, totalAmount, refundSn);
|
||||
log.info("微信退款接口调用完成,订单号:{},退款单号:{}", outTradeNo, refundSn);
|
||||
return wechatResp;
|
||||
} catch (Exception e) {
|
||||
log.error("处理微信退款订单失败,订单号:{},退款单号:{}", outTradeNo, refundSn, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款状态
|
||||
*
|
||||
* @param refundSn 退款单号
|
||||
* @return 退款订单信息
|
||||
*/
|
||||
public RefundSubmitRespDTO doQueryRefundStatus(String refundSn) {
|
||||
try {
|
||||
log.info("查询微信退款状态,退款单号:{}", refundSn);
|
||||
|
||||
// 调用微信查询退款接口
|
||||
RefundSubmitRespDTO wechatResp = wechatPlayClient.queryRefund(refundSn);
|
||||
|
||||
log.info("微信查询退款接口调用完成,退款单号:{}", refundSn);
|
||||
|
||||
return wechatResp;
|
||||
} catch (Exception e) {
|
||||
log.error("查询微信退款状态失败,退款单号:{}", refundSn, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退款回调
|
||||
*
|
||||
* @param headers 回调请求头
|
||||
* @param params 回调请求参数
|
||||
* @param body 回调请求体
|
||||
* @return RefundSubmitRespDTO
|
||||
*/
|
||||
public Optional<RefundNotificationRespDTO> parseRefundNotify(Map<String, String> headers, Map<String, String> params, String body) {
|
||||
try {
|
||||
log.info("开始处理微信退款回调");
|
||||
return wechatPlayClient.parseRefundNotifyV3(headers, body);
|
||||
} catch (Exception e) {
|
||||
log.error("处理微信退款回调失败", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.seer.teach.pay.app.client.wechat;
|
||||
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.exception.CommonException;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@Slf4j
|
||||
public class WechatNativePayClient extends AbstractWxPayClient {
|
||||
|
||||
public WechatNativePayClient(String configJson) {
|
||||
super(PayChannelEnum.WECHAT_NATIVE.getCode(), configJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
super.doInit(WxPayConstants.TradeType.NATIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderRespDTO doPayOrder(PayOrderReqDTO payOrderReqDTO) {
|
||||
WxPayUnifiedOrderV3Request request = buildPayRequestV3(payOrderReqDTO);
|
||||
|
||||
try {
|
||||
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
||||
|
||||
if(StringUtils.isNotEmpty(response)){
|
||||
PayOrderRespDTO payOrderRespDTO = new PayOrderRespDTO();
|
||||
payOrderRespDTO.setChannelCode(payOrderReqDTO.getChannelCode());
|
||||
payOrderRespDTO.setPayOrderSn(payOrderReqDTO.getPayOrderSn());
|
||||
payOrderRespDTO.setStatus(PayStatusEnum.PENDING.getCode());
|
||||
payOrderRespDTO.setQrCodeUrl(response);
|
||||
return payOrderRespDTO;
|
||||
}
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "微信二维码预支付失败:");
|
||||
} catch (WxPayException e) {
|
||||
throw new CommonException(ResultCodeEnum.WX_PREPAY_ERROR, "微信二维码预支付失败:");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayChannelEnum getChannel() {
|
||||
return PayChannelEnum.WECHAT_NATIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncCallback() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,44 +1,69 @@
|
||||
package com.seer.teach.pay.app.client.wechat.core;
|
||||
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.seer.teach.common.config.WxConfig;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.RefundStatusEnum;
|
||||
import com.seer.teach.common.exception.CommonException;
|
||||
import com.seer.teach.common.config.WxConfig;
|
||||
import com.seer.teach.common.utils.DateUtils;
|
||||
import com.seer.teach.pay.app.client.convert.PayClientConvert;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.app.client.convert.PayClientConvert;
|
||||
import com.seer.teach.pay.app.client.wechat.core.resp.WechatPayOrderResp;
|
||||
import com.seer.teach.pay.app.client.wechat.core.resp.WechatPrepayTransactionResp;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.app.client.wechat.core.resp.WechatTransferDetailResp;
|
||||
import com.seer.teach.pay.app.client.wechat.core.resp.WechatTransferQueryResp;
|
||||
import com.seer.teach.pay.app.client.wechat.core.resp.WechatTransferResp;
|
||||
import com.seer.teach.pay.app.util.ConfigDecryptUtil;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.wechat.pay.java.core.Config;
|
||||
import com.wechat.pay.java.core.RSAPublicKeyConfig;
|
||||
import com.wechat.pay.java.core.notification.NotificationConfig;
|
||||
import com.wechat.pay.java.core.notification.NotificationParser;
|
||||
import com.wechat.pay.java.core.notification.RequestParam;
|
||||
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.*;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.Payer;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
|
||||
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
|
||||
import com.wechat.pay.java.service.payments.model.Transaction;
|
||||
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
|
||||
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
|
||||
import com.wechat.pay.java.service.refund.RefundService;
|
||||
import com.wechat.pay.java.service.refund.model.*;
|
||||
import com.wechat.pay.java.service.refund.model.AmountReq;
|
||||
import com.wechat.pay.java.service.refund.model.CreateRequest;
|
||||
import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
|
||||
import com.wechat.pay.java.service.refund.model.Refund;
|
||||
import com.wechat.pay.java.service.refund.model.RefundNotification;
|
||||
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
|
||||
import com.wechat.pay.java.service.transferbatch.model.*;
|
||||
import com.wechat.pay.java.service.transferbatch.model.GetTransferBatchByOutNoRequest;
|
||||
import com.wechat.pay.java.service.transferbatch.model.GetTransferDetailByOutNoRequest;
|
||||
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferRequest;
|
||||
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferResponse;
|
||||
import com.wechat.pay.java.service.transferbatch.model.TransferBatchEntity;
|
||||
import com.wechat.pay.java.service.transferbatch.model.TransferBatchGet;
|
||||
import com.wechat.pay.java.service.transferbatch.model.TransferDetailCompact;
|
||||
import com.wechat.pay.java.service.transferbatch.model.TransferDetailEntity;
|
||||
import com.wechat.pay.java.service.transferbatch.model.TransferDetailInput;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.util.*;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
|
||||
|
||||
/**
|
||||
* 微信支付客户端
|
||||
@ -64,6 +89,8 @@ public class WechatPlayClient {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private NativePayService nativePayService;
|
||||
|
||||
private String configJson;
|
||||
|
||||
private String appId;
|
||||
@ -107,8 +134,12 @@ public class WechatPlayClient {
|
||||
.merchantSerialNumber(merchantSerialNumber)
|
||||
.apiV3Key(apiV3Key)
|
||||
.build();
|
||||
// 初始化证书服务
|
||||
|
||||
jsapiServiceExtension = new JsapiServiceExtension.Builder().config(config).build();
|
||||
|
||||
|
||||
nativePayService = new NativePayService.Builder().config(config).build();
|
||||
|
||||
NotificationConfig notificationConfig = new RSAPublicKeyConfig.Builder()
|
||||
.merchantId(mchId)
|
||||
.privateKey(privateKeyPem)
|
||||
@ -147,7 +178,7 @@ public class WechatPlayClient {
|
||||
public WechatPrepayTransactionResp prepayPay(PayOrderReqDTO payOrder, Object extrasRequest) {
|
||||
String sn = payOrder.getPayOrderSn();
|
||||
Integer userId = payOrder.getUserId();
|
||||
Long totalAmount = payOrder.getTotalAmount();
|
||||
Integer totalAmount = payOrder.getTotalAmount();
|
||||
log.info("开始处理微信预支付请求,订单号: {}, 用户ID: {}, 支付金额: {}分", sn, userId, totalAmount);
|
||||
|
||||
// 构建支付请求
|
||||
@ -201,6 +232,35 @@ public class WechatPlayClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预支付,获取二维码地址(使用默认配置)
|
||||
* <p>
|
||||
* NATIVE支付场景,商户调用该接口在微信支付下单,生成用于调起支付的二维码,
|
||||
* 获取二维码地址。
|
||||
* <see><a href='https://pay.weixin.qq.com/doc/v3/merchant/4012791877'>微信NATIVE下单</a></see>
|
||||
*
|
||||
* @param payOrder 支付订单信息
|
||||
* @param extrasRequest 额外参数
|
||||
* @return 二维码地址
|
||||
*
|
||||
*/
|
||||
public Optional<String> nativePrepayPay(PayOrderReqDTO payOrder, Object extrasRequest){
|
||||
com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest request = new com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest();
|
||||
request.setOutTradeNo(payOrder.getPayOrderSn());
|
||||
request.setDescription(payOrder.getDescription());
|
||||
request.setNotifyUrl(jsPayNotifyUrl);
|
||||
com.wechat.pay.java.service.payments.nativepay.model.Amount amount = new com.wechat.pay.java.service.payments.nativepay.model.Amount();
|
||||
amount.setTotal(payOrder.getTotalAmount().intValue());
|
||||
amount.setCurrency("CNY");
|
||||
request.setAmount(amount);
|
||||
request.setTimeExpire(TemporalAccessorUtil.format(payOrder.getExpireTime().atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN));
|
||||
|
||||
PrepayResponse prepay = nativePayService.prepay(request);
|
||||
if(Objects.nonNull(prepay)){
|
||||
return Optional.of(prepay.getCodeUrl());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理PEM内容,确保符合微信SDK要求
|
||||
|
||||
@ -53,18 +53,16 @@ public class AppPayOrderController {
|
||||
@SaCheckPermission("app:order:status")
|
||||
public ResultBean<AppPayOrderStatusResp> queryPayStatus(
|
||||
@Parameter(description = "订单号") @PathVariable("orderSn") String orderSn) {
|
||||
PayOrderEntity payOrder = appPayOrderService.queryPayStatus(orderSn);
|
||||
AppPayOrderStatusResp result = AppPayOrderConvert.INSTANCE.convertOne(payOrder);
|
||||
return ResultBean.success(result);
|
||||
AppPayOrderStatusResp payOrder = appPayOrderService.queryPayStatus(orderSn);
|
||||
return ResultBean.success(payOrder);
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "关闭支付订单")
|
||||
@PostMapping("/close/{orderSn}")
|
||||
@SaCheckPermission("app:order:close")
|
||||
public ResultBean<Boolean> closePayOrder(
|
||||
public ResultBean<String> closePayOrder(
|
||||
@Parameter(description = "订单号", required = true) @PathVariable("orderSn") String orderSn) {
|
||||
Boolean result = appPayOrderService.closePayOrder(orderSn);
|
||||
return ResultBean.success(result);
|
||||
return ResultBean.success(appPayOrderService.closePayOrder(orderSn));
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ -22,4 +23,8 @@ public class OrderPayReq {
|
||||
|
||||
@Schema(description = "商户订单编号")
|
||||
private String merchantOrderSn;
|
||||
|
||||
@Schema(description = "回跳地址")
|
||||
@URL(message = "回跳地址的格式必须是 URL")
|
||||
private String returnUrl;
|
||||
}
|
||||
@ -17,4 +17,7 @@ public class AppPayOrderResp {
|
||||
|
||||
@Schema(description = "支付订单号")
|
||||
private String payOrderSn;
|
||||
|
||||
@Schema(description = "二维码地址")
|
||||
private String qrCodeUrl;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.seer.teach.pay.app.pay.service;
|
||||
|
||||
import com.seer.teach.pay.app.pay.controller.req.OrderPayReq;
|
||||
import com.seer.teach.pay.app.pay.controller.resp.AppPayOrderResp;
|
||||
import com.seer.teach.pay.app.pay.controller.resp.AppPayOrderStatusResp;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
|
||||
import java.util.Map;
|
||||
@ -30,12 +31,12 @@ public interface IAppPayOrderService {
|
||||
* @param orderSn 订单号
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
PayOrderEntity queryPayStatus(String orderSn);
|
||||
AppPayOrderStatusResp queryPayStatus(String orderSn);
|
||||
|
||||
/**
|
||||
* 关闭支付订单
|
||||
* @param orderSn
|
||||
* @return
|
||||
*/
|
||||
Boolean closePayOrder(String orderSn);
|
||||
String closePayOrder(String orderSn);
|
||||
}
|
||||
@ -1,28 +1,34 @@
|
||||
package com.seer.teach.pay.app.pay.service.impl;
|
||||
|
||||
import com.alibaba.nacos.shaded.com.google.common.collect.Maps;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.constants.CommonConstant;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.pay.PayOrderSourceEnum;
|
||||
import com.seer.teach.common.enums.pay.PayStatusEnum;
|
||||
import com.seer.teach.common.utils.AssertUtils;
|
||||
import com.seer.teach.pay.app.client.PayClient;
|
||||
import com.seer.teach.pay.app.client.PayClientFactory;
|
||||
import com.seer.teach.pay.app.client.convert.PayClientConvert;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderReqDTO;
|
||||
import com.seer.teach.pay.app.pay.controller.req.OrderPayReq;
|
||||
import com.seer.teach.pay.app.client.dto.PayOrderRespDTO;
|
||||
import com.seer.teach.pay.app.pay.controller.resp.AppPayOrderResp;
|
||||
import com.seer.teach.pay.app.pay.convert.AppPayOrderConvert;
|
||||
import com.seer.teach.pay.app.pay.service.IAppPayOrderService;
|
||||
import com.seer.teach.pay.app.notify.service.OrderPayNotifyHandler;
|
||||
import com.seer.teach.pay.app.notify.service.OrderProcessStrategyFactory;
|
||||
import com.seer.teach.pay.app.client.PayClient;
|
||||
import com.seer.teach.pay.app.client.factory.PayClientFactory;
|
||||
import com.seer.teach.pay.app.pay.controller.req.OrderPayReq;
|
||||
import com.seer.teach.pay.app.pay.controller.resp.AppPayOrderResp;
|
||||
import com.seer.teach.pay.app.pay.controller.resp.AppPayOrderStatusResp;
|
||||
import com.seer.teach.pay.app.pay.convert.AppPayOrderConvert;
|
||||
import com.seer.teach.pay.app.pay.service.IAppPayOrderService;
|
||||
import com.seer.teach.pay.entity.PayChannelEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.service.IPayChannelService;
|
||||
import com.seer.teach.pay.service.IPayOrderService;
|
||||
import com.seer.teach.user.api.UserInfoServiceApi;
|
||||
import com.seer.teach.user.api.dto.UserAuthDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
@ -41,6 +47,12 @@ public class AppPayOrderServiceImpl implements IAppPayOrderService {
|
||||
|
||||
private final IPayChannelService payChannelService;
|
||||
|
||||
private final UserInfoServiceApi userInfoServiceApi;
|
||||
|
||||
@Value("${server.servlet.context-path:}")
|
||||
private String contextPath;
|
||||
|
||||
|
||||
@Override
|
||||
public AppPayOrderResp payOrder(OrderPayReq orderPayReq) {
|
||||
log.info("开始处理支付订单,渠道:{},商户订单号:{}", orderPayReq.getChannelCode(), orderPayReq.getMerchantOrderSn());
|
||||
@ -58,30 +70,44 @@ public class AppPayOrderServiceImpl implements IAppPayOrderService {
|
||||
AssertUtils.isTrue(payOrderEntity.getStatus() == PayStatusEnum.PENDING.getCode() || payOrderEntity.getStatus() == PayStatusEnum.PAY_FAILED.getCode(), ResultCodeEnum.PAY_ORDER_IS_PROCESSED);
|
||||
|
||||
// 4.获取对应的支付客户端
|
||||
PayClient payCLient = payClientFactory.getPayClient(orderPayReq.getChannelCode());
|
||||
PayClient payClient = payClientFactory.getPayClient(PayChannelEnum.fromCode(channelCode), payChannel.getConfigJson());
|
||||
|
||||
// 5,执行支付订单
|
||||
PayOrderReqDTO payOrderReqDTO = PayClientConvert.INSTANCE.convert2PayOrderReqDTO(payOrderEntity,orderPayReq);
|
||||
PayOrderRespDTO response = payCLient.payOrder(payOrderReqDTO);
|
||||
// 5,设置支付客户端的参数
|
||||
PayOrderReqDTO payOrderReqDTO = PayClientConvert.INSTANCE.convert2PayOrderReqDTO(payOrderEntity, orderPayReq);
|
||||
if( PayChannelEnum.WECHAT_JSAPI.getCode().equalsIgnoreCase(channelCode)){
|
||||
UserAuthDTO userAuthDTO = userInfoServiceApi.getUserAuthByUserIdAndAppId(payOrderEntity.getUserId(), payChannel.getAppId());
|
||||
if(Objects.nonNull(userAuthDTO)){
|
||||
Map<String, String> channelExtras = payOrderReqDTO.getChannelExtras() == null ? Maps.newHashMap() : payOrderReqDTO.getChannelExtras();
|
||||
channelExtras.put(CommonConstant.WX_OPEN_ID, userAuthDTO.getOpenId());
|
||||
}
|
||||
}
|
||||
payOrderReqDTO.setNotifyUrl(getNotifyUrl(payChannel));
|
||||
payOrderReqDTO.setReturnUrl(orderPayReq.getReturnUrl());
|
||||
// 6,执行支付
|
||||
PayOrderRespDTO response = payClient.payOrder(payOrderReqDTO);
|
||||
response.setChannelId(payChannel.getId());
|
||||
|
||||
if (!payCLient.isAsyncCallback()) {
|
||||
if (!payClient.isAsyncCallback()) {
|
||||
handlePayResult(response);
|
||||
}
|
||||
log.info("支付订单处理完成,订单号:{}", orderPayReq.getMerchantOrderSn());
|
||||
return AppPayOrderConvert.INSTANCE.convertOne(response);
|
||||
}
|
||||
|
||||
private String getNotifyUrl(PayChannelEntity payChannel) {
|
||||
return payChannel.getNotifyDomain() + contextPath + "/app/callback/pay/" + payChannel.getChannelCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean payCallback(String channelCode, Map<String, String> headers, Map<String, String> params, String body) {
|
||||
log.info("开始处理统一支付回调,渠道编码:{}", channelCode);
|
||||
try {
|
||||
// 根据渠道编码获取对应的支付策略
|
||||
PayChannelEnum channel = PayChannelEnum.fromCode(channelCode);
|
||||
PayChannelEntity payChannel = payChannelService.checkChannelByChannelCode(channelCode);
|
||||
|
||||
AssertUtils.notNull(channel, ResultCodeEnum.PAY_CHANNEL_IS_NOT_EXIST);
|
||||
AssertUtils.notNull(payChannel, ResultCodeEnum.PAY_CHANNEL_IS_NOT_EXIST);
|
||||
|
||||
PayClient payClient = payClientFactory.getPayClient(channel);
|
||||
PayClient payClient = payClientFactory.getPayClient(PayChannelEnum.fromCode(channelCode), payChannel.getConfigJson());
|
||||
// 处理支付回调
|
||||
PayOrderRespDTO response = payClient.handlePayCallback(headers, params, body);
|
||||
|
||||
@ -128,11 +154,20 @@ public class AppPayOrderServiceImpl implements IAppPayOrderService {
|
||||
* @return 支付订单
|
||||
*/
|
||||
@Override
|
||||
public PayOrderEntity queryPayStatus(String orderSn) {
|
||||
public AppPayOrderStatusResp queryPayStatus(String orderSn) {
|
||||
log.info("查询支付状态,订单号:{}", orderSn);
|
||||
// 根据订单号查询支付订单,获取支付渠道编码
|
||||
PayClient payCLient = getPayStrategy(orderSn);
|
||||
return payCLient.queryPayStatus(orderSn);
|
||||
PayOrderEntity payOrder = payOrderService.getOrderByOrderSn(orderSn);
|
||||
if (Objects.isNull(payOrder)) {
|
||||
return null;
|
||||
}
|
||||
AppPayOrderStatusResp appPayOrderStatusResp = AppPayOrderConvert.INSTANCE.convertOne(payOrder);
|
||||
|
||||
PayClient payCLient = getPayClient(orderSn);
|
||||
PayOrderRespDTO payOrderRespDTO = payCLient.queryPayStatus(orderSn);
|
||||
if (Objects.nonNull(payOrderRespDTO)) {
|
||||
appPayOrderStatusResp.setStatus(payOrderRespDTO.getStatus());
|
||||
}
|
||||
return appPayOrderStatusResp;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,25 +177,27 @@ public class AppPayOrderServiceImpl implements IAppPayOrderService {
|
||||
* @return 关闭结果
|
||||
*/
|
||||
@Override
|
||||
public Boolean closePayOrder(String orderSn) {
|
||||
public String closePayOrder(String orderSn) {
|
||||
log.info("关闭支付状态,订单号:{}", orderSn);
|
||||
PayClient payCLient = getPayStrategy(orderSn);
|
||||
PayClient payCLient = getPayClient(orderSn);
|
||||
return payCLient.closePayOrder(orderSn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单号获取支付策略
|
||||
* 根据订单号获取支付客户端
|
||||
* 提取公共方法
|
||||
*
|
||||
* @param orderSn 订单号
|
||||
* @return 支付策略
|
||||
* @return 支付客户端
|
||||
*/
|
||||
private PayClient getPayStrategy(String orderSn) {
|
||||
private PayClient getPayClient(String orderSn) {
|
||||
PayOrderEntity payOrder = payOrderService.getOne(new LambdaQueryWrapper<PayOrderEntity>()
|
||||
.eq(PayOrderEntity::getOrderSn, orderSn));
|
||||
AssertUtils.notNull(payOrder, ResultCodeEnum.ORDER_NOT_FOUND);
|
||||
String channelCode = payOrder.getChannelCode();
|
||||
|
||||
PayChannelEntity payChannel = payChannelService.checkChannelByChannelCode(channelCode);
|
||||
log.info("订单号:{},支付渠道:{}", orderSn, channelCode);
|
||||
return payClientFactory.getPayClient(channelCode);
|
||||
return payClientFactory.getPayClient(PayChannelEnum.fromCode(channelCode), payChannel.getConfigJson());
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,6 @@ import com.seer.teach.common.enums.pay.PayChannelEnum;
|
||||
import com.seer.teach.common.enums.ResultCodeEnum;
|
||||
import com.seer.teach.common.enums.pay.PayOrderSourceEnum;
|
||||
import com.seer.teach.common.enums.pay.RefundStatusEnum;
|
||||
import com.seer.teach.common.enums.pay.RefundTypeEnum;
|
||||
import com.seer.teach.common.exception.CommonException;
|
||||
import com.seer.teach.common.utils.AssertUtils;
|
||||
import com.seer.teach.common.utils.OrderIdGenerator;
|
||||
@ -12,15 +11,17 @@ import com.seer.teach.pay.api.refund.dto.RefundOrderRequestDTO;
|
||||
import com.seer.teach.pay.app.client.PayClient;
|
||||
import com.seer.teach.pay.app.client.convert.PayClientConvert;
|
||||
import com.seer.teach.pay.app.client.dto.RefundNotificationRespDTO;
|
||||
import com.seer.teach.pay.app.client.factory.PayClientFactory;
|
||||
import com.seer.teach.pay.app.client.PayClientFactory;
|
||||
import com.seer.teach.pay.app.notify.service.OrderPayNotifyHandler;
|
||||
import com.seer.teach.pay.app.notify.service.OrderProcessStrategyFactory;
|
||||
import com.seer.teach.pay.app.client.dto.PayRefundReqDTO;
|
||||
import com.seer.teach.pay.app.refund.service.IAppRefundOrderService;
|
||||
import com.seer.teach.pay.app.refund.service.dto.RefundResultDTO;
|
||||
import com.seer.teach.pay.dto.RefundSubmitRespDTO;
|
||||
import com.seer.teach.pay.entity.PayChannelEntity;
|
||||
import com.seer.teach.pay.entity.PayOrderEntity;
|
||||
import com.seer.teach.pay.entity.RefundOrderEntity;
|
||||
import com.seer.teach.pay.service.IPayChannelService;
|
||||
import com.seer.teach.pay.service.IPayOrderService;
|
||||
import com.seer.teach.pay.service.IRefundOrderService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -43,6 +44,8 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
|
||||
private final IPayOrderService payOrderService;
|
||||
|
||||
private final IPayChannelService payChannelService;
|
||||
|
||||
private final IRefundOrderService refundOrderService;
|
||||
|
||||
private final OrderProcessStrategyFactory orderProcessStrategyFactory;
|
||||
@ -94,7 +97,10 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
PayChannelEnum channel = PayChannelEnum.fromCode(channelCode);
|
||||
log.info("获取支付渠道结果:{}", channel);
|
||||
AssertUtils.notNull(channel, ResultCodeEnum.PAY_CHANNEL_NOT_SUPPORTED);
|
||||
PayClient payClient = payClientFactory.getPayClient(channel);
|
||||
|
||||
PayChannelEntity payChannel = payChannelService.checkChannelByChannelCode(channelCode);
|
||||
|
||||
PayClient payClient = payClientFactory.getPayClient(channel,payChannel.getConfigJson());
|
||||
log.info("获取支付客户端完成:{}", payClient != null);
|
||||
|
||||
// 执行退款订单
|
||||
@ -103,8 +109,8 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
.outTradeNo(payOrder.getOrderSn())
|
||||
.outRefundNo(refundSn)
|
||||
.reason(refundRequest.getRefundReason())
|
||||
.payPrice(refundRequest.getTotalAmount())
|
||||
.refundPrice(refundRequest.getRefundAmount())
|
||||
.payPrice(refundRequest.getTotalAmount().intValue())
|
||||
.refundPrice(refundRequest.getRefundAmount().intValue())
|
||||
.userId(refundRequest.getUserId())
|
||||
.build();
|
||||
log.info("构建退款请求参数完成,请求参数:{}", payRefundReqDTO);
|
||||
@ -140,7 +146,7 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
log.info("查询退款状态,退款单号:{}", refundSn);
|
||||
try {
|
||||
// 根据退款单号查询支付订单,获取支付渠道编码
|
||||
PayClient payClient = getPayStrategy(refundSn);
|
||||
PayClient payClient = getPayClient(refundSn);
|
||||
log.info("获取支付客户端完成,退款单号:{}", refundSn);
|
||||
|
||||
RefundSubmitRespDTO result = payClient.doQueryRefundStatus(refundSn);
|
||||
@ -157,12 +163,14 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
public boolean refundCallback(String channelCode, Map<String, String> headers, Map<String, String> params, String body) {
|
||||
log.info("开始处理统一退款回调,渠道编码:{}", channelCode);
|
||||
try {
|
||||
// 根据渠道编码获取对应的支付策略
|
||||
// 根据渠道编码获取对应的支付客户端
|
||||
PayChannelEnum channel = PayChannelEnum.fromCode(channelCode);
|
||||
log.info("获取的支付策略:{}", channel);
|
||||
log.info("获取的支付客户端:{}", channel);
|
||||
AssertUtils.notNull(channel, ResultCodeEnum.PAY_CHANNEL_NOT_SUPPORTED);
|
||||
|
||||
PayClient payClient = payClientFactory.getPayClient(channel);
|
||||
PayChannelEntity payChannel = payChannelService.checkChannelByChannelCode(channelCode);
|
||||
|
||||
PayClient payClient = payClientFactory.getPayClient(channel,payChannel.getConfigJson());
|
||||
log.info("获取支付客户端完成,渠道编码:{}", channelCode);
|
||||
|
||||
// 处理退款回调
|
||||
@ -249,13 +257,13 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据退款单号获取支付策略
|
||||
* 根据退款单号获取支付客户端
|
||||
*
|
||||
* @param refundSn 退款单号
|
||||
* @return 支付策略
|
||||
* @return 支付客户端
|
||||
*/
|
||||
private PayClient getPayStrategy(String refundSn) {
|
||||
log.info("开始获取支付策略,退款单号:{}", refundSn);
|
||||
private PayClient getPayClient(String refundSn) {
|
||||
log.info("开始获取支付客户端,退款单号:{}", refundSn);
|
||||
try {
|
||||
// 根据退款单号查询退款订单
|
||||
RefundOrderEntity refundOrder = refundOrderService.getRefundOrderByRefundSn(refundSn);
|
||||
@ -268,11 +276,12 @@ public class AppRefundOrderServiceImpl implements IAppRefundOrderService {
|
||||
|
||||
PayChannelEnum channel = PayChannelEnum.fromCode(channelCode);
|
||||
log.info("支付渠道枚举转换结果:{}", channel);
|
||||
PayClient result = payClientFactory.getPayClient(channel);
|
||||
log.info("获取支付策略完成,退款单号:{},支付策略:{}", refundSn, result != null);
|
||||
return result;
|
||||
PayChannelEntity payChannel = payChannelService.checkChannelByChannelCode(channelCode);
|
||||
|
||||
PayClient payClient = payClientFactory.getPayClient(channel,payChannel.getConfigJson());
|
||||
return payClient;
|
||||
} catch (Exception e) {
|
||||
log.error("获取支付策略发生异常,退款单号:{}", refundSn, e);
|
||||
log.error("获取支付客户端发生异常,退款单号:{}", refundSn, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,16 @@
|
||||
<artifactId>wechatpay-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user