优化支付模块的代码,增加微信扫码支付的功能

This commit is contained in:
Wang 2026-01-30 14:24:14 +08:00
parent 623cdd23b0
commit cac17af0fe
38 changed files with 1132 additions and 689 deletions

View File

@ -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;
}
/**

View File

@ -75,4 +75,9 @@ public interface CommonConstant {
* 启用
*/
Integer ENABLE = 1;
/**
* 微信openId
*/
String WX_OPEN_ID = "openId";
}

View File

@ -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>

View File

@ -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;

View File

@ -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格式
*/

View File

@ -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 = "获取支付渠道列表")

View File

@ -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格式
*/

View File

@ -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;
/**
* 排序
*/

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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:

View File

@ -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 '逻辑删除',

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
/**
* 判断是否是异步回调

View File

@ -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 支付渠道编码WECHATALIPAY
* @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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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 "";
}
}

View File

@ -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);
}

View File

@ -42,4 +42,13 @@ public class PayOrderCallBackRespDTO {
* 订单来源
*/
private Integer orderSource;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时错误信息
*/
private String channelErrorMsg;
}

View File

@ -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<>();
}

View File

@ -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;
}

View File

@ -33,12 +33,12 @@ public class PayRefundReqDTO {
*
* 目前微信支付在退款的时候必须传递该字段
*/
private Long payPrice;
private Integer payPrice;
/**
* 退款金额单位
*/
private Long refundPrice;
private Integer refundPrice;
/**
* 用户ID

View File

@ -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 支付渠道编码WECHATALIPAY
* @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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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要求

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -17,4 +17,7 @@ public class AppPayOrderResp {
@Schema(description = "支付订单号")
private String payOrderSn;
@Schema(description = "二维码地址")
private String qrCodeUrl;
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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>