diff --git a/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/pay/PayChannelEnum.java b/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/pay/PayChannelEnum.java index 2dcb949..ef1818d 100644 --- a/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/pay/PayChannelEnum.java +++ b/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/pay/PayChannelEnum.java @@ -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; } /** diff --git a/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java b/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java index 951a7a1..2e2d00c 100644 --- a/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java +++ b/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java @@ -75,4 +75,9 @@ public interface CommonConstant { * 启用 */ Integer ENABLE = 1; + + /** + * 微信openId + */ + String WX_OPEN_ID = "openId"; } \ No newline at end of file diff --git a/seer-dependencies/pom.xml b/seer-dependencies/pom.xml index 95b9bf4..7d8e00a 100644 --- a/seer-dependencies/pom.xml +++ b/seer-dependencies/pom.xml @@ -57,7 +57,9 @@ 0.2.17 - 4.7.5.B + 4.8.1.B + 4.35.79.ALL + 4.8.1.B 4.30.2 @@ -84,7 +86,7 @@ 2.17.2 3.3.1 - 4.7.5.B + 2.7.7 4.0.3 @@ -298,6 +300,18 @@ ${weixin-java.version} + + com.alipay.sdk + alipay-sdk-java + ${alipay-sdk-java.version} + + + org.bouncycastle + bcprov-jdk15on + + + + net.dongliu apk-parser diff --git a/seer-mall/seer-mall-service-app/src/main/java/com/seer/teach/mall/app/order/service/impl/AppOrderServiceImpl.java b/seer-mall/seer-mall-service-app/src/main/java/com/seer/teach/mall/app/order/service/impl/AppOrderServiceImpl.java index babbfb4..1287eaa 100644 --- a/seer-mall/seer-mall-service-app/src/main/java/com/seer/teach/mall/app/order/service/impl/AppOrderServiceImpl.java +++ b/seer-mall/seer-mall-service-app/src/main/java/com/seer/teach/mall/app/order/service/impl/AppOrderServiceImpl.java @@ -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; diff --git a/seer-pay/seer-pay-data-model/src/main/java/com/seer/teach/pay/entity/PayChannelEntity.java b/seer-pay/seer-pay-data-model/src/main/java/com/seer/teach/pay/entity/PayChannelEntity.java index 05ba01a..271c7d9 100644 --- a/seer-pay/seer-pay-data-model/src/main/java/com/seer/teach/pay/entity/PayChannelEntity.java +++ b/seer-pay/seer-pay-data-model/src/main/java/com/seer/teach/pay/entity/PayChannelEntity.java @@ -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格式) */ diff --git a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/AdminPayChannelController.java b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/AdminPayChannelController.java index 6b05509..68f1a30 100644 --- a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/AdminPayChannelController.java +++ b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/AdminPayChannelController.java @@ -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> getPayChannelCodeList(){ + return ResultBean.success(payChannelService.getPayChannelCodeList()); + } @GetMapping("/list") @Operation(summary = "获取支付渠道列表") diff --git a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelSaveReq.java b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelSaveReq.java index 8f28405..5be9c49 100644 --- a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelSaveReq.java +++ b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelSaveReq.java @@ -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格式) */ diff --git a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelUpdateReq.java b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelUpdateReq.java index a702775..8be6b3f 100644 --- a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelUpdateReq.java +++ b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/req/AdminPayChannelUpdateReq.java @@ -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 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; + /** * 排序 */ diff --git a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelCodeResp.java b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelCodeResp.java new file mode 100644 index 0000000..95e4163 --- /dev/null +++ b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelCodeResp.java @@ -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; +} diff --git a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelResp.java b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelResp.java index 74b0c6f..8f18cf2 100644 --- a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelResp.java +++ b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/controller/resp/PayChannelResp.java @@ -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 payInfo; diff --git a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/service/AdminPayChannelService.java b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/service/AdminPayChannelService.java index 444dae9..a58cf85 100644 --- a/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/service/AdminPayChannelService.java +++ b/seer-pay/seer-pay-service-admin/src/main/java/com/seer/teach/pay/admin/pay/service/AdminPayChannelService.java @@ -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() - .eq(PayChannelEntity::getChannelCode, params.getChannelCode()) - ); + PayChannelEntity one = payChannelService.getOne(new LambdaQueryWrapper().eq(PayChannelEntity::getChannelCode, params.getChannelCode())); AssertUtils.isNull(one, ResultCodeEnum.PAY_CHANNEL_ALREADY_EXISTS); // 获取配置信息并加密 Map encryptedConfig = PayConfigEncryptUtils.encryptConfig(params.getConfig()); @@ -105,4 +104,12 @@ public class AdminPayChannelService { resp.setPayInfo(decrypted); return resp; } + + /** + * 获取支付渠道编码列表 + * @return + */ + public List getPayChannelCodeList() { + return Arrays.stream(PayChannelEnum.values()).map( payChannelEnum -> new PayChannelCodeResp(payChannelEnum.getCode(), payChannelEnum.getDescription(), payChannelEnum.getType()) ).toList(); + } } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/application.yml b/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/application.yml index 1ca1604..566f23e 100644 --- a/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/application.yml +++ b/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/application.yml @@ -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: diff --git a/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/db/mysql/V1.0.0__init.sql b/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/db/mysql/V1.0.0__init.sql index d59af7c..eed4859 100644 --- a/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/db/mysql/V1.0.0__init.sql +++ b/seer-pay/seer-pay-service-app-bootstrap/src/main/resources/db/mysql/V1.0.0__init.sql @@ -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 '逻辑删除', diff --git a/seer-pay/seer-pay-service-app/pom.xml b/seer-pay/seer-pay-service-app/pom.xml index 5cf6d68..9cd6f97 100644 --- a/seer-pay/seer-pay-service-app/pom.xml +++ b/seer-pay/seer-pay-service-app/pom.xml @@ -56,6 +56,24 @@ springdoc-openapi-starter-webmvc-ui + + com.github.binarywang + weixin-java-pay + + + + + com.alipay.sdk + alipay-sdk-java + 4.35.79.ALL + + + org.bouncycastle + bcprov-jdk15on + + + + cn.hutool hutool-all diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/AbstractPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/AbstractPayClient.java index db317de..de900a4 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/AbstractPayClient.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/AbstractPayClient.java @@ -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 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 headers, Map 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 headers, Map 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); } diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClient.java index 3d970d2..da9907d 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClient.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClient.java @@ -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); /** * 判断是否是异步回调 diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClientFactory.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClientFactory.java new file mode 100644 index 0000000..5259a5c --- /dev/null +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/PayClientFactory.java @@ -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 CLIENT_MAP = new ConcurrentHashMap<>(); + + private final Map> 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 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); + } +} \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AbstractAlipayPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AbstractAlipayPayClient.java new file mode 100644 index 0000000..fb2f58b --- /dev/null +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AbstractAlipayPayClient.java @@ -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 parseRefundNotify(Map headers, Map params, String body) { + return Optional.empty(); + } + + @Override + public RefundSubmitRespDTO doQueryRefundStatus(String refundSn) { + return null; + } + + @Override + protected PayOrderCallBackRespDTO parseOrderNotify(Map headers, Map params, String body) { + // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 + Map 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); + } + +} \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AlipayPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AlipayPayClient.java deleted file mode 100644 index 057fcb1..0000000 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AlipayPayClient.java +++ /dev/null @@ -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 parseRefundNotify(Map headers, Map 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 headers, Map 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 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() - .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() - .eq(PayOrderEntity::getOrderSn, orderSn) - .eq(PayOrderEntity::getChannelCode, getChannel()) - ); - - if (payOrder == null) { - log.warn("支付订单不存在,订单号:{}", orderSn); - return false; - } - - // 更新订单状态为已取消 - boolean updateResult = payOrderService.update( - new LambdaUpdateWrapper() - .set(PayOrderEntity::getStatus, PayStatusEnum.CANCEL.getCode()) - .eq(PayOrderEntity::getId, payOrder.getId()) - ); - - // 更新扩展表状态 - if (updateResult) { - payOrderExtensionService.update( - new LambdaUpdateWrapper() - .eq(PayOrderExtensionEntity::getOrderId, payOrder.getId()) - ); - } - - // TODO: 调用支付宝关闭订单接口 - - log.info("关闭支付宝支付订单结果:{},订单号:{}", updateResult, orderSn); - return updateResult; - - } catch (Exception e) { - log.error("关闭支付宝支付订单失败,订单号:{}", orderSn, e); - return false; - } - } -} \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AlipayQrPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AlipayQrPayClient.java new file mode 100644 index 0000000..d0537c8 --- /dev/null +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/alipay/AlipayQrPayClient.java @@ -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; + } +} diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/coin/CoinPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/coin/CoinPayClient.java index 798e125..80990eb 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/coin/CoinPayClient.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/coin/CoinPayClient.java @@ -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 headers, Map 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() - .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() - .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() - .set(PayOrderEntity::getStatus, PayStatusEnum.CANCEL.getCode()) - .eq(PayOrderEntity::getId, payOrder.getId())); - // 更新扩展表 - if (updateResult) { - payOrderExtensionService.update( - new LambdaUpdateWrapper() - .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 ""; } } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/convert/PayClientConvert.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/convert/PayClientConvert.java index 4685e6b..a93447d 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/convert/PayClientConvert.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/convert/PayClientConvert.java @@ -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); } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderCallBackRespDTO.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderCallBackRespDTO.java index 67e5f33..bc0252d 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderCallBackRespDTO.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderCallBackRespDTO.java @@ -42,4 +42,13 @@ public class PayOrderCallBackRespDTO { * 订单来源 */ private Integer orderSource; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; } diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderReqDTO.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderReqDTO.java index d5c3d26..882b370 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderReqDTO.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderReqDTO.java @@ -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 channelExtras = new HashMap<>(); } diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderRespDTO.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderRespDTO.java index e45fd23..f52c074 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderRespDTO.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayOrderRespDTO.java @@ -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; } diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayRefundReqDTO.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayRefundReqDTO.java index fb9f989..bede303 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayRefundReqDTO.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/dto/PayRefundReqDTO.java @@ -33,12 +33,12 @@ public class PayRefundReqDTO { * * 目前微信支付在退款的时候,必须传递该字段 */ - private Long payPrice; + private Integer payPrice; /** * 退款金额,单位:分 */ - private Long refundPrice; + private Integer refundPrice; /** * 用户ID diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/factory/PayClientFactory.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/factory/PayClientFactory.java deleted file mode 100644 index e3c3b00..0000000 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/factory/PayClientFactory.java +++ /dev/null @@ -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 payStrategies; - - private final IPayChannelService payChannelService; - - private static final Map 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 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; - } -} \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/AbstractWxPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/AbstractWxPayClient.java new file mode 100644 index 0000000..247e8af --- /dev/null +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/AbstractWxPayClient.java @@ -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 headers, Map 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 parseRefundNotify(Map headers, Map 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 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 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; + } + } +} diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatJsApiPayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatJsApiPayClient.java index d8e2081..d8cdd97 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatJsApiPayClient.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatJsApiPayClient.java @@ -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 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() - .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 headers, Map 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() - .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() - .set(PayOrderEntity::getStatus, PayStatusEnum.CANCEL.getCode()) - .eq(PayOrderEntity::getId, payOrder.getId())); - // 更新扩展表 - if (updateResult) { - payOrderExtensionService.update( - new LambdaUpdateWrapper() - .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 parseRefundNotify(Map headers, Map params, String body) { - try { - log.info("开始处理微信退款回调"); - return wechatPlayClient.parseRefundNotifyV3(headers, body); - } catch (Exception e) { - log.error("处理微信退款回调失败", e); - throw e; - } - } } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatNativePayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatNativePayClient.java new file mode 100644 index 0000000..f39dc0f --- /dev/null +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/WechatNativePayClient.java @@ -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; + } +} \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/core/WechatPlayClient.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/core/WechatPlayClient.java index a28846c..231f2de 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/core/WechatPlayClient.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/client/wechat/core/WechatPlayClient.java @@ -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 { } } + /** + * 预支付,获取二维码地址(使用默认配置) + *

+ * NATIVE支付场景,商户调用该接口在微信支付下单,生成用于调起支付的二维码, + * 获取二维码地址。 + * 微信NATIVE下单 + * + * @param payOrder 支付订单信息 + * @param extrasRequest 额外参数 + * @return 二维码地址 + * + */ + public Optional 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要求 diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/AppPayOrderController.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/AppPayOrderController.java index 1999719..577aaca 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/AppPayOrderController.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/AppPayOrderController.java @@ -53,18 +53,16 @@ public class AppPayOrderController { @SaCheckPermission("app:order:status") public ResultBean 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 closePayOrder( + public ResultBean closePayOrder( @Parameter(description = "订单号", required = true) @PathVariable("orderSn") String orderSn) { - Boolean result = appPayOrderService.closePayOrder(orderSn); - return ResultBean.success(result); + return ResultBean.success(appPayOrderService.closePayOrder(orderSn)); } } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/req/OrderPayReq.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/req/OrderPayReq.java index fc83ee4..9281326 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/req/OrderPayReq.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/req/OrderPayReq.java @@ -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; } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/resp/AppPayOrderResp.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/resp/AppPayOrderResp.java index df0736e..9fc958e 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/resp/AppPayOrderResp.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/controller/resp/AppPayOrderResp.java @@ -17,4 +17,7 @@ public class AppPayOrderResp { @Schema(description = "支付订单号") private String payOrderSn; + + @Schema(description = "二维码地址") + private String qrCodeUrl; } diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/IAppPayOrderService.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/IAppPayOrderService.java index 12a91a3..f2d4987 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/IAppPayOrderService.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/IAppPayOrderService.java @@ -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); } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/impl/AppPayOrderServiceImpl.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/impl/AppPayOrderServiceImpl.java index 49adc16..14772c0 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/impl/AppPayOrderServiceImpl.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/pay/service/impl/AppPayOrderServiceImpl.java @@ -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 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 headers, Map 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() .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()); } } \ No newline at end of file diff --git a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/refund/service/impl/AppRefundOrderServiceImpl.java b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/refund/service/impl/AppRefundOrderServiceImpl.java index a01489e..be76527 100644 --- a/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/refund/service/impl/AppRefundOrderServiceImpl.java +++ b/seer-pay/seer-pay-service-app/src/main/java/com/seer/teach/pay/app/refund/service/impl/AppRefundOrderServiceImpl.java @@ -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 headers, Map 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; } } diff --git a/seer-pay/seer-pay-service/pom.xml b/seer-pay/seer-pay-service/pom.xml index ac8ebb9..ebf923a 100644 --- a/seer-pay/seer-pay-service/pom.xml +++ b/seer-pay/seer-pay-service/pom.xml @@ -43,6 +43,16 @@ wechatpay-java + + com.github.binarywang + weixin-java-pay + + + + com.alipay.sdk + alipay-sdk-java + + org.springframework.retry spring-retry