新增支付接口:支持支付宝pc沙箱支付流程、新增微信native预留接口

This commit is contained in:
gaoqr
2025-05-21 16:40:54 +08:00
parent 3fcd0abbf7
commit f94bf9f38e
28 changed files with 1198 additions and 113 deletions
@@ -0,0 +1,63 @@
package com.cf.imes.framework.common.exception;
import com.cf.imes.framework.common.exception.enums.ServiceErrorCodeRange;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付系统异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayException extends RuntimeException {
/**
* 业务错误码
*
* @see ServiceErrorCodeRange
*/
private Integer code;
/**
* 错误提示
*/
private String message;
public PayException(Throwable cause) {
super(cause);
}
/**
* 空构造方法,避免反序列化问题
*/
public PayException() {
}
public PayException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMsg();
}
public PayException(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public PayException setCode(Integer code) {
this.code = code;
return this;
}
@Override
public String getMessage() {
return message;
}
public PayException setMessage(String message) {
this.message = message;
return this;
}
}
@@ -0,0 +1,70 @@
package com.cf.imes.framework.pay.config;
import com.cf.imes.framework.common.util.encrypt.AesUtils;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Gqr
* @since 2025/4/15 16:24
*/
@ConfigurationProperties(prefix = "chenfeng.pay.alipay")
@Data
public class ChenfengAlipayConfig extends com.alipay.api.AlipayConfig {
@Value("${chenfeng.encrypt.publicKey:}")
private String publicKey;
/**
* 销售产品码
*/
private String productCode = "FAST_INSTANT_TRADE_PAY";
/**
* 二维码模式:效果参见:https://help.pingxx.com/article/1137360/
*/
private String qrPayMode = "1";
/**
* 给支付宝的回调地址
*/
private String notifyUrl;
// 加密数据
@Override
public void setAppId(String appId) {
if (StringUtils.isNotEmpty(publicKey)) {
super.setAppId(AesUtils.decrypt(appId, publicKey));
} else {
super.setAppId(appId);
}
}
@Override
public void setPrivateKey(String privateKey) {
if (StringUtils.isNotEmpty(publicKey)) {
super.setPrivateKey(AesUtils.decrypt(privateKey, publicKey));
} else {
super.setPrivateKey(privateKey);
}
}
@Override
public void setAlipayPublicKey(String alipayPublicKey) {
if (StringUtils.isNotEmpty(publicKey)) {
super.setAlipayPublicKey(AesUtils.decrypt(alipayPublicKey, publicKey));
} else {
super.setAlipayPublicKey(alipayPublicKey);
}
}
public void setNotifyUrl(String notifyUrl) {
if (StringUtils.isNotEmpty(publicKey)) {
this.notifyUrl = AesUtils.decrypt(notifyUrl, publicKey);
} else {
this.notifyUrl = notifyUrl;
}
}
}
@@ -3,6 +3,7 @@ package com.cf.imes.framework.pay.config;
import com.cf.imes.framework.pay.core.client.PayClientFactory;
import com.cf.imes.framework.pay.core.client.impl.PayClientFactoryImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
@@ -11,6 +12,7 @@ import org.springframework.context.annotation.Bean;
* @author 晨丰科技
*/
@AutoConfiguration
@EnableConfigurationProperties({ChenfengAlipayConfig.class, ChenfengWechatPayConfig.class})
public class ChenfengPayAutoConfiguration {
@Bean
@@ -0,0 +1,14 @@
package com.cf.imes.framework.pay.config;
import com.github.binarywang.wxpay.config.WxPayConfig;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Gqr
* @since 2025/4/15 16:52
*/
@ConfigurationProperties(prefix = "chenfeng.pay.wechat")
@Data
public class ChenfengWechatPayConfig extends WxPayConfig {
}
@@ -1,6 +1,6 @@
package com.cf.imes.framework.pay.core.client.dto.order;
import com.cf.imes.framework.pay.core.client.exception.PayException;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.cf.imes.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import lombok.Data;
@@ -1,6 +1,7 @@
package com.cf.imes.framework.pay.core.client.dto.order;
import com.cf.imes.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
@@ -11,6 +12,9 @@ import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Map;
import static com.cf.imes.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static com.cf.imes.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 统一下单 Request DTO
*
@@ -70,6 +74,7 @@ public class PayOrderUnifiedReqDTO {
* 支付过期时间
*/
@NotNull(message = "支付过期时间不能为空")
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
private LocalDateTime expireTime;
// ========== 拓展参数 ==========
@@ -1,6 +1,6 @@
package com.cf.imes.framework.pay.core.client.dto.refund;
import com.cf.imes.framework.pay.core.client.exception.PayException;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import lombok.Data;
@@ -1,17 +0,0 @@
package com.cf.imes.framework.pay.core.client.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付系统异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayException extends RuntimeException {
public PayException(Throwable cause) {
super(cause);
}
}
@@ -10,7 +10,7 @@ import com.cf.imes.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import com.cf.imes.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import com.cf.imes.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import com.cf.imes.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import com.cf.imes.framework.pay.core.client.exception.PayException;
import com.cf.imes.framework.common.exception.PayException;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@@ -8,7 +8,7 @@ import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import com.cf.imes.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import com.cf.imes.framework.pay.core.client.exception.PayException;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import com.cf.imes.framework.test.core.ut.BaseMockitoUnitTest;
import com.alipay.api.AlipayApiException;
@@ -5,7 +5,7 @@ import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.framework.pay.core.client.exception.PayException;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.request.AlipayTradePrecreateRequest;
@@ -3,6 +3,7 @@ package com.cf.imes.framework.web.core.handler;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.util.monitor.TracerUtils;
@@ -98,6 +99,9 @@ public class GlobalExceptionHandler {
if (ex instanceof HttpMessageNotReadableException) {
return httpMessageNotReadableExceptionHandler(request, (HttpMessageNotReadableException) ex);
}
if (ex instanceof PayException) {
return payExceptionHandler((PayException) ex);
}
return defaultExceptionHandler(request, ex);
}
@@ -260,6 +264,18 @@ public class GlobalExceptionHandler {
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 支付异常 PayException
*
* @param ex
* @return
*/
@ExceptionHandler(value = PayException.class)
public CommonResult payExceptionHandler(PayException ex) {
log.info(String.format("==========[payExceptionHandler]==========%s", ex.getMessage()));
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 处理数据库DuplicateKeyException,违反唯一约束异常
*
@@ -421,24 +421,33 @@ public class ErrorCodeConstants {
public static final ErrorCode INVOICE_FILE_IS_NULL_NOT_DOWNLOAD = new ErrorCode(1_002_043_006, "发票附件不存在,无法下载");
public static final ErrorCode INVOICE_FILE_DATA_ERROR = new ErrorCode(1_002_043_007, "发票附件下载失败,请重新下载或稍等重试");
//=========== 支付相关 1-002-044-000 ============
public static final ErrorCode PAY_CHANNEL_NOT_SUPPORT = new ErrorCode(1_002_044_000, "暂不支持的支付渠道,请使用其他支付渠道");
public static final ErrorCode PAY_ORDER_NOT_EXIST = new ErrorCode(1_002_044_001, "支付订单不存在,支付编号:{},请重新发起支付");
public static final ErrorCode PAY_ORDER_CHANNEL_QUERY_NOT_EXIST = new ErrorCode(1_002_044_002, "如果您已确认扫码支付,请稍后刷新页面,支付平台可能存在延迟到账");
public static final ErrorCode PAY_ORDER_NOTIFY_DATA_PARSE_ERROR = new ErrorCode(1_002_044_003, "解析支付回调数据异常");
public static final ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1_002_044_004, "支付订单不处于待支付");
public static final ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_002_044_005, "发起渠道支付异常,请联系客服");
public static final ErrorCode PRODUCTS_NO_EXIST = new ErrorCode(1_002_044_001, "当前产品不存在");
public static final ErrorCode PRODUCTS_DETAIL_NO_EXIST = new ErrorCode(1_002_044_002, "当前产品的规则不存在");
public static final ErrorCode PRODUCTS_DURATION_NOT_NULL = new ErrorCode(1_002_044_003, "购买方式为时长时,需确定产品时长");
public static final ErrorCode PRODUCTS_OUTPUT_NOT_NULL = new ErrorCode(1_002_044_004, "购买方式为产量时,需确定产量数量");
public static final ErrorCode PRODUCTS_DETAIL_DURATION_EXIST = new ErrorCode(1_002_044_005, "存在当前时长的产品定价规则");
public static final ErrorCode PRODUCTS_DETAIL_OUTPUT_EXIST = new ErrorCode(1_002_044_006, "存在当前产量的产品定价规则");
public static final ErrorCode PRODUCTS_DURATION_NUM_ERR = new ErrorCode(1_002_044_007, "产品定价规则,时长数据设置有误");
public static final ErrorCode PRODUCTS_DURATION_INT_ERR = new ErrorCode(1_002_044_008, "产品定价规则,时长数据为正整数");
public static final ErrorCode PRODUCTS_DURATION_UNIT_ERR = new ErrorCode(1_002_044_009, "产品定价规则,时长单位设置有误");
//=========== 产品相关 1-002-045-000 ============
public static final ErrorCode PRODUCTS_NO_EXIST = new ErrorCode(1_002_045_001, "当前产品不存在");
public static final ErrorCode PRODUCTS_DETAIL_NO_EXIST = new ErrorCode(1_002_045_002, "当前产品的规则不存在");
public static final ErrorCode PRODUCTS_DURATION_NOT_NULL = new ErrorCode(1_002_045_003, "购买方式为时长时,需确定产品时长");
public static final ErrorCode PRODUCTS_OUTPUT_NOT_NULL = new ErrorCode(1_002_045_004, "购买方式为产量时,需确定产量数量");
public static final ErrorCode PRODUCTS_DETAIL_DURATION_EXIST = new ErrorCode(1_002_045_005, "存在当前时长的产品定价规则");
public static final ErrorCode PRODUCTS_DETAIL_OUTPUT_EXIST = new ErrorCode(1_002_045_006, "存在当前产量的产品定价规则");
public static final ErrorCode PRODUCTS_DURATION_NUM_ERR = new ErrorCode(1_002_045_007, "产品定价规则,时长数据设置有误");
public static final ErrorCode PRODUCTS_DURATION_INT_ERR = new ErrorCode(1_002_045_008, "产品定价规则,时长数据为正整数");
public static final ErrorCode PRODUCTS_DURATION_UNIT_ERR = new ErrorCode(1_002_045_009, "产品定价规则,时长单位设置有误");
public static final ErrorCode ADVERTISEMENT_POSITION_NO_EXIST = new ErrorCode(1_003_001_001, "广告所选放置位置不存在");
public static final ErrorCode ADVERTISEMENT_NO_EXIST = new ErrorCode(1_003_001_002, "广告不存在");
public static final ErrorCode ADVERTISEMENT_STATUS_ERROR = new ErrorCode(1_003_001_003, "当前广告已经发布,无需立即发布");
public static final ErrorCode ADVERTISEMENT_STATUS_ERROR2 = new ErrorCode(1_003_001_004, "当前广告已过结束时间,请点击重新发布");
public static final ErrorCode ADVERTISEMENT_END_ERROR = new ErrorCode(1_003_001_005, "当前广告未发布,无需结束发布");
public static final ErrorCode ADVERTISEMENT_END_ERROR2 = new ErrorCode(1_003_001_006, "当前广告已结束,无需立即结束");
public static final ErrorCode ADVERTISEMENT_FAIL = new ErrorCode(1_003_001_007, "自动发布失败");
//=========== 广告相关 1-002-046-000 ============
public static final ErrorCode ADVERTISEMENT_POSITION_NO_EXIST = new ErrorCode(1_002_046_001, "广告所选放置位置不存在");
public static final ErrorCode ADVERTISEMENT_NO_EXIST = new ErrorCode(1_002_046_002, "广告不存在");
public static final ErrorCode ADVERTISEMENT_STATUS_ERROR = new ErrorCode(1_002_046_003, "当前广告已经发布,无需立即发布");
public static final ErrorCode ADVERTISEMENT_STATUS_ERROR2 = new ErrorCode(1_002_046_004, "当前广告已过结束时间,请点击重新发布");
public static final ErrorCode ADVERTISEMENT_END_ERROR = new ErrorCode(1_002_046_005, "当前广告未发布,无需结束发布");
public static final ErrorCode ADVERTISEMENT_END_ERROR2 = new ErrorCode(1_002_046_006, "当前广告已结束,无需立即结束");
public static final ErrorCode ADVERTISEMENT_FAIL = new ErrorCode(1_002_046_007, "自动发布失败");
public static final ErrorCode RECHARGE_ACTIVE_NO_EXIST = new ErrorCode(1_004_001_001, "当前赠送金活动不存在");
@@ -1,16 +1,11 @@
package com.cf.imes.module.system.controller.admin.pay;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.generator.SnowflakeGenerator;
import cn.hutool.http.Method;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.module.system.controller.admin.pay.vo.PayOrderUnifiedReqVO;
import com.cf.imes.module.system.service.pay.PayOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -21,94 +16,50 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
import static com.cf.imes.framework.common.pojo.CommonResult.success;
/**
* @author Gqr
* @since 2025/4/8 16:09
*/
//@Tag(name = "管理后台 - 应用注册信息")
@Tag(name = "管理后台 - 统一支付")
@RestController
@RequestMapping("/system/pay")
@Validated
@Slf4j
public class PayController {
// @Resource
// private SnowflakeIdWorker3rd snowflakeIdWorker3rd;
@Resource
private PayOrderService payOrderService;
@GetMapping("")
// @Operation(summary = "获得应用注册")
// @Parameter(name = "id", description = "编号", required = true, example = "1024")
// @PreAuthorize("@ss.hasPermission('system:application:query')")
@PermitAll
public CommonResult<String> getApplication() throws AlipayApiException {
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// ① 通用的参数
SnowflakeGenerator snowflakeGenerator = new SnowflakeGenerator();
String outTradeNo = snowflakeGenerator.next().toString();
model.setOutTradeNo(outTradeNo);
model.setSubject("产品名称");
model.setBody("描述");
model.setTotalAmount("0.11");
model.setTimeExpire(LocalDateTimeUtil.format(LocalDateTime.now().plus(100, ChronoUnit.SECONDS), NORM_DATETIME_FORMATTER));
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// ② 个性化的参数
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
model.setQrPayMode("1"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/
// ③ 支付宝 PC 支付有两种展示模式:FORM、URL
String displayMode = PayOrderDisplayModeEnum.URL.getMode();
@PostMapping()
@Operation(summary = "创建支付订单")
public CommonResult<PayOrderRespDTO> initiatePay(@RequestBody PayOrderUnifiedReqVO payOrderUnifiedReqVO) throws Throwable {
PayOrderRespDTO payOrderRespDTO = payOrderService.initiatePay(payOrderUnifiedReqVO);
return success(payOrderRespDTO);
}
// 1.2 构建 AlipayTradePagePayRequest 请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setBizModel(model);
request.setNotifyUrl("https://chenfeng.tech:3345/admin-api/system/pay/order/" + outTradeNo);
request.setReturnUrl("");
// 2.1 执行请求
AlipayTradePagePayResponse response;
AlipayConfig alipayConfig = new AlipayConfig();
alipayConfig.setServerUrl("https://openapi-sandbox.dl.alipaydev.com/gateway.do");
alipayConfig.setFormat("json");
alipayConfig.setCharset("utf-8");
alipayConfig.setSignType("RSA2");
alipayConfig.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzOXNjRYBHpsJ2WGm43QB4ktKY6m24xf0Mepn5iHPyHtvbBPQXa3lyG7Nztxwk+oL5jbxEj+QB6nNVL2anOJT9fzr96qEwNc9a+7RnOeGDVfWN/FRTEYw0oExzqLoiO1rZlEWUpFpFnhQkEcBco/SJEJAE9WOe9N8y2O5B4EU1ik5667u8fzyC+XukTj/NhIhqdQdN1Vr953rFI8eh3ToDYcnvLjq2yt6zLLbhSxhg6mvw7dapHTyPitqkm/OhgfcNjlfSuTAIj7KtgluFA2eZzWqwbJmeKCqv///6YQl8/sFpHDmVY39WN0hQwoEFTo/dcWChsrN1cH1cbooIWdQ7AgMBAAECggEAO1cjSFQjtBWXv11oq8eot0NNrUm5SHttXGtffcpbMWhcPPk35GU2UbywPvj6vZegNAZktoK56JR62VKXd3h5hDFTE+1Sr0axwBsk0STN8RFN+1P3i4V82ZPVnFnaPylN3EqGez+rj+9WFdc2zLWQaq1sMegZsvC60fgXcwJRh/cT7FKLJe3qxb2nJJB+d+6yEHKMgaHSICv3b7SpGuhJaq8O62iY1oDj/u9EKgLtU28kiAunvXHnby1F6T+Fq26wCn3Q0GnRKWAhGXElXGjAY8hzcR0RBGLuROJbW3oa1wXX1DOMnRiAaHaFXsj7TTkGc3sCyGOLb9RgVaR9jQ9fwQKBgQDmQLQRACspFJeL640mO7C42P2zeu9hcHPypOudkMZIXobvvwZkP7w6rIv9XbG8xah7P5kNbaM1KP3ke5pNHnrCiphEv8rbmHlfoSaNKSS/omLAAa/Xm0OqMUOgpHe3PFITsiE2SARIqZaQoaR8+UUkJsRnIyLuf43SoOG1FC9F2wKBgQDHQ/4He/V/IxuTac7O+qMM9lFu+uyhsc+bMecyylh0AoADmscg4l98Kcs+IS5QB1HYEYo1fNElJ1Rs8iREHm+iK3h4xKIrG6KhObTVnqfYlCz2MYiHDW252KOqPdyxpeEuMSpoIVdcsvDBb+yYCTOT/DfX16eRSyl2pSv7SkFpIQKBgQDih1PgyfaskvrKCPO0YtNp+OoZE2DBz53zYjgAq/e4KW++HYUPSflJDIspuW1tGFW69AYDRRNl8K9hhA15w/vou+w07zCIPYlkASrphEoIU52t47r5d2Lp5U/wkv7JAoqJoB22xsGYL2jXYIBxB9jzSUS/Z70tQcbpK6kDuxkLMwKBgGgEyz/W6qFyjzuVmTqcxa7Hilz3qb0CmBA4rarqy9/LSk4XZul4okWrf4gwQ//MwmGbTl17iaXucPjqMhpJFRc/wASzCMCeDXhCKm71AkNhnlLqKo3taaEBQPZmkqU9pV26lpuDBBrE4j7oEUaQTxdqKzCGR9flt4BS2rGz4o9hAoGAYPufk/GigoOzUhv8KS2Vm7syIEr0jLOM87AmoO5DQrKGLoy80Nvjr5XuAhTwB0UnFUuchyFimmew3Bbi3iTCjAUmC06w4KWcTbY2Lu4YQpZU7gkFViuAWLIuJgN2xmWaCoaiphbVqB/+Wew4gFVgGXF39AUJS4fiXAIj4QiWO/I=");
alipayConfig.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtNsXwyaUwihNYoDLXZbET+jroEDUk1DfarrbcMPZy/uIW423KSDwTQi9SPuR4EgwB4sHy/UFhLe95gk0LnhZeZ0ddTSxsZ/Ps5eHlwxV0CR1Xw5K+yMNKyXkSuKDx5pbIv+xUveWRlOqte9fZDOKfXMplEjKJS/CeosXyKyzKiO8bXuYSBv4+lt6RkgFdKjEPGE5AqtvqyGDN86CHe2zf0lJUWLUwb4nlWpoW/6SihhG9+MKUrnBGPI132lZWkmZz3MriKb6yGo4/7Ospm+oWZYGj7vzI+WX25mFONFzmLx8q8h3PnZfODr7uXenRbaVraj7LRAneVSKKi3sAzdonwIDAQAB");
alipayConfig.setAppId("9021000142647599");
DefaultAlipayClient client = new DefaultAlipayClient(alipayConfig);
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
} else {
response = client.pageExecute(request, Method.GET.name());
}
System.out.println(1);
// 2.2 处理结果
if (!response.isSuccess()) {
// return buildClosedPayOrderRespDTO(reqDTO, response);
}
return success(response.getBody());
// PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
// reqDTO.getOutTradeNo(), response);
// return success(true);
@GetMapping("/order/{channelCode}/{outTradeNo}")
@Operation(summary = "查询支付订单")
public CommonResult<Boolean> queryPayOrder(@PathVariable("outTradeNo") String outTradeNo, @PathVariable("channelCode") int channelCode) {
payOrderService.queryPayOrderStatus(outTradeNo, channelCode);
return success(true);
}
@PostMapping(value = "/order/{channelId}")
// @Operation(summary = "支付渠道的统一【支付】回调")
@PostMapping(value = "/notify/order/{channelCode}")
@Operation(summary = "支付渠道回调")
@PermitAll
public String notifyOrder(@PathVariable("channelId") Long channelId,
public String notifyOrder(@PathVariable("channelCode") Integer channelCode,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
log.debug("[notifyOrder][channelCode({}) 回调数据({}/{})]", channelCode, params, body);
payOrderService.doOrderNotify(channelCode, params, body);
// 支付接口回调相应必须是"success"
return "success";
}
}
@@ -0,0 +1,45 @@
package com.cf.imes.module.system.controller.admin.pay.vo;
import com.cf.imes.module.system.validation.pay.PayChannelCodeValid;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 统一支付vo
*
* @author Gqr
* @since 2025/4/15 14:30
*/
@Data
@Valid
public class PayOrderUnifiedReqVO {
/**
* 商品标题
*/
@NotEmpty(message = "商品标题不能为空")
@Length(max = 32, message = "商品标题不能超过 32")
private String subject;
/**
* 支付金额,单位:元
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private BigDecimal price;
@NotNull(message = "订单失效时间不能为空")
private LocalDateTime expireTime;
@Schema(description = "支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@PayChannelCodeValid
@NotNull(message = "支付渠道编码不能为空")
private Integer channelCode;
}
@@ -0,0 +1,95 @@
package com.cf.imes.module.system.dal.dataobject.pay;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cf.imes.framework.mybatis.core.dataobject.BaseDO;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import com.cf.imes.module.system.enums.pay.PayOrderStatusEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 支付订单
*
* @author Gqr
* @since 2025/4/15 11:34
*/
@TableName("pay_order")
@KeySequence("pay_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class PayOrderDO extends BaseDO {
/**
* 支付订单编号(主键)
*/
@TableId
private Long id;
/**
* 组织id
*/
@TableField(fill = FieldFill.INSERT)
private Long organId;
/**
* 渠道编号
*/
private PayChannelCodeEnum channelCode;
/**
* 商品标题
*/
private String subject;
/**
* 支付金额
*/
private BigDecimal price;
/**
* 支付订单号:P + 当前id + 时间戳(yyyyMMddHHmmss
*/
private String payNo;
/**
* 支付状态
*/
private PayOrderStatusEnum status;
/**
* 订单失效时间
*/
private LocalDateTime expireTime;
/**
* 渠道方回调时间
*/
private LocalDateTime notifyTime;
/**
* 渠道方支付成功时间
*/
private LocalDateTime successTime;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
/**
* 支付渠道的同步/异步通知的内容
*
*/
private String channelNotifyData;
}
@@ -0,0 +1,15 @@
package com.cf.imes.module.system.dal.mysql.pay;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cf.imes.module.system.dal.dataobject.pay.PayOrderDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 支付订单 Mapper
*
* @author Gqr
* @since 2025/4/15 11:41
*/
@Mapper
public interface PayOrderMapper extends BaseMapper<PayOrderDO> {
}
@@ -0,0 +1,38 @@
package com.cf.imes.module.system.enums.pay;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付渠道编码
*
* @author Gqr
* @since 2024/7/5 9:25
*/
@Getter
@AllArgsConstructor
public enum PayChannelCodeEnum {
/**
* 支付宝
*/
ALIPAY_PC(0),
/**
* 微信
*/
WECHAT_NATIVE(1);
@EnumValue
private final Integer code;
@JsonCreator
public static PayChannelCodeEnum fromType(Integer type) {
for (PayChannelCodeEnum value : values()) {
if (value.getCode().equals(type)) {
return value;
}
}
return null;
}
}
@@ -0,0 +1,66 @@
package com.cf.imes.module.system.enums.pay;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 支付订单状态
*
* @author Gqr
* @since 2025/4/15 11:48
*/
@Getter
@AllArgsConstructor
public enum PayOrderStatusEnum {
/**
* 等待支付
*/
WAITING(0),
/**
* 支付成功
*/
SUCCESS(10),
/**
* 支付失败
*/
FAIL(20);
@EnumValue
private final Integer status;
@JsonCreator
public static PayOrderStatusEnum fromType(Integer type) {
for (PayOrderStatusEnum value : values()) {
if (value.getStatus().equals(type)) {
return value;
}
}
return null;
}
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
/**
* 判断是否支付关闭
*
* @param status 状态
* @return 是否支付关闭
*/
public static boolean isClosed(Integer status) {
return Objects.equals(status, SUCCESS.getStatus()) || Objects.equals(status, FAIL.getStatus());
}
}
@@ -0,0 +1,42 @@
package com.cf.imes.module.system.service.pay;
import com.alipay.api.AlipayApiException;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.module.system.controller.admin.pay.vo.PayOrderUnifiedReqVO;
import java.util.Map;
/**
* 支付订单接口
*
* @author Gqr
* @since 2025/4/17 17:29
*/
public interface PayOrderService {
/**
* 发起支付
*
* @param payOrderUnifiedReqVO
* @return
* @throws AlipayApiException
*/
PayOrderRespDTO initiatePay(PayOrderUnifiedReqVO payOrderUnifiedReqVO);
/**
* 查询支付订单支付状态
* 未支付、超时等都抛出支付异常
*
* @param outTradeNo
* @return
*/
void queryPayOrderStatus(String outTradeNo, int channelCode);
/**
* 解析和处理支付回调数据
*
* @param params
* @param body
* @return
*/
void doOrderNotify(Integer channelCode, Map<String, String> params, String body);
}
@@ -0,0 +1,328 @@
package com.cf.imes.module.system.service.pay;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.common.util.json.JsonUtils;
import com.cf.imes.framework.common.util.validation.ValidationUtils;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.pay.config.ChenfengAlipayConfig;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import com.cf.imes.module.system.service.pay.factory.impl.AbstractPayHandler;
import com.cf.imes.module.system.service.pay.factory.PayHandlerFactory;
import com.cf.imes.module.system.controller.admin.pay.vo.PayOrderUnifiedReqVO;
import com.cf.imes.module.system.dal.dataobject.pay.PayOrderDO;
import com.cf.imes.module.system.dal.mysql.pay.PayOrderMapper;
import com.cf.imes.module.system.enums.pay.PayOrderStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_CHANNEL_QUERY_NOT_EXIST;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_NOTIFY_DATA_PARSE_ERROR;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_NOT_EXIST;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_ORDER_SUBMIT_CHANNEL_ERROR;
/**
* 支付订单接口 实现类
*
* @author Gqr
* @since 2025/4/17 17:29
*/
@Service
@Validated
@Slf4j
public class PayOrderServiceImpl implements PayOrderService {
@Resource
private PayHandlerFactory payHandlerFactory;
@Resource
private PayOrderMapper payOrderMapper;
@Resource
private Validator validator;
private static final String PAY_ORDER_PREFIX_FORMAT = "P%dO%s%s";
@Resource
private ChenfengAlipayConfig alipayConfig;
@Override
@Transactional(rollbackFor = Exception.class)
public PayOrderRespDTO initiatePay(PayOrderUnifiedReqVO payOrderUnifiedReqVO) {
// 主动校验一次入参
ValidationUtils.validate(validator, payOrderUnifiedReqVO);
Integer channelCode = payOrderUnifiedReqVO.getChannelCode();
// 获取渠道方处理器
AbstractPayHandler payHandler = payHandlerFactory.getPayHandler(channelCode);
// 创建支付订单
PayOrderDO payOrder = createPayOrder(payOrderUnifiedReqVO);
payOrderMapper.insert(payOrder);
// 发起渠道方支付
PayOrderUnifiedReqDTO payOrderUnifiedReqDTO = null;
try {
payOrderUnifiedReqDTO = createPayOrderUnifiedReqDTO(payOrder);
return payHandler.unifiedOrder(payOrderUnifiedReqDTO);
} catch (Throwable e) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
channelCode, JSON.toJSONString(payOrderUnifiedReqDTO), e);
throw new PayException(PAY_ORDER_SUBMIT_CHANNEL_ERROR);
}
}
/**
* 创建支付订单
*
* @param payOrderUnifiedReqVO
* @return
*/
private PayOrderDO createPayOrder(PayOrderUnifiedReqVO payOrderUnifiedReqVO) {
PayOrderDO payOrderDO = new PayOrderDO();
// 校验获取组织id
Long organId = validOrganId();
Long payOrderId = IdUtil.getSnowflake(0, 0).nextId();
payOrderDO.setId(payOrderId);
payOrderDO.setPayNo(String.format(PAY_ORDER_PREFIX_FORMAT, organId, payOrderId, DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN)));
payOrderDO.setStatus(PayOrderStatusEnum.WAITING);
payOrderDO.setExpireTime(payOrderUnifiedReqVO.getExpireTime());
payOrderDO.setPrice(payOrderUnifiedReqVO.getPrice());
payOrderDO.setSubject(payOrderUnifiedReqVO.getSubject());
payOrderDO.setChannelCode(PayChannelCodeEnum.fromType(payOrderUnifiedReqVO.getChannelCode()));
payOrderDO.setOrganId(organId);
return payOrderDO;
}
/**
* 创建支付渠道方请求对象
*
* @param payOrderDO
* @return
*/
private PayOrderUnifiedReqDTO createPayOrderUnifiedReqDTO(PayOrderDO payOrderDO) {
PayOrderUnifiedReqDTO payOrderUnifiedReqDTO = new PayOrderUnifiedReqDTO();
String subject = payOrderDO.getSubject();
payOrderUnifiedReqDTO.setSubject(subject);
payOrderUnifiedReqDTO.setBody(subject);
payOrderUnifiedReqDTO.setOutTradeNo(payOrderDO.getPayNo());
payOrderUnifiedReqDTO.setPrice(payOrderDO.getPrice().intValue());
payOrderUnifiedReqDTO.setExpireTime(payOrderDO.getExpireTime());
payOrderUnifiedReqDTO.setNotifyUrl(String.format(alipayConfig.getNotifyUrl(), payOrderDO.getChannelCode().getCode()));
return payOrderUnifiedReqDTO;
}
@Override
public void queryPayOrderStatus(String outTradeNo, int channelCode) {
// 校验支付订单是否存在
PayOrderDO payOrderDO = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getPayNo, outTradeNo)
.eq(PayOrderDO::getOrganId, validOrganId())
.eq(PayOrderDO::getDeleted, false)
);
if (ObjectUtils.isNull(payOrderDO)) {
throw exception(PAY_ORDER_NOT_EXIST, outTradeNo);
}
// 订单已被更新成功,直接支付成功了
if (PayOrderStatusEnum.SUCCESS.getStatus().equals(payOrderDO.getStatus())) {
return;
}
// 支付订单未成功,查询渠道方
// 获取渠道方处理器
AbstractPayHandler payHandler = payHandlerFactory.getPayHandler(channelCode);
try {
PayOrderRespDTO payOrderRespDTO = payHandler.queryPayOrder(outTradeNo);
boolean paySuccess = PayOrderStatusRespEnum.SUCCESS.getStatus().equals(payOrderRespDTO.getStatus());
if (!paySuccess) {
throw new PayException(PAY_ORDER_CHANNEL_QUERY_NOT_EXIST);
}
} catch (Throwable e) {
log.error("渠道方查询支付订单异常", e);
throw new PayException(PAY_ORDER_CHANNEL_QUERY_NOT_EXIST);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void doOrderNotify(Integer channelCode, Map<String, String> params, String body) {
// 获取渠道方处理器
AbstractPayHandler payHandler = payHandlerFactory.getPayHandler(channelCode);
try {
// 解析回调数据
PayOrderRespDTO payOrderRespDTO = payHandler.doParseOrderNotify(params, body);
// 处理支付回调数据
this.notifyOrder(payOrderRespDTO);
} catch (Throwable e) {
log.error("[parseOrderNotify][客户端({}) params({}) body({}) headers({}) 解析失败]", channelCode, params, body, e);
throw new PayException(PAY_ORDER_NOTIFY_DATA_PARSE_ERROR);
}
}
/**
* 处理回调数据成功/失败
*
* @param notify
*/
public void notifyOrder(PayOrderRespDTO notify) {
// 从payNo中解析组织id
Long organId = getOrganIdFromPayNo(notify.getOutTradeNo());
// 情况一:支付成功的回调
if (PayOrderStatusEnum.isSuccess(notify.getStatus())) {
notifyOrderSuccess(notify, organId);
return;
}
// 情况二:支付失败的回调
if (PayOrderStatusEnum.isClosed(notify.getStatus())) {
notifyOrderClosed(notify, organId);
}
}
/**
* 从payNo中解析组织id
*
* @param outTradeNo
* @return
*/
private Long getOrganIdFromPayNo(String outTradeNo) {
Pattern pattern = Pattern.compile("P(\\d+)O");
Matcher matcher = pattern.matcher(outTradeNo);
if (matcher.find()) {
return Long.valueOf(matcher.group(1)); // 提取 P 和 O 中间的数字
} else {
throw new ServiceException(GlobalErrorCodeConstants.UNAUTHORIZED);
}
}
/**
* 支付成功回调处理
*
* @param notify
*/
private void notifyOrderSuccess(PayOrderRespDTO notify, Long organId) {
// 1. 判断 PayOrderDO 是否处于待支付
String outTradeNo = notify.getOutTradeNo();
PayOrderDO order = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getPayNo, outTradeNo)
.eq(PayOrderDO::getOrganId, organId));
if (ObjectUtil.isNull(order)) {
throw exception(PAY_ORDER_NOT_EXIST, outTradeNo);
}
// 如果已经是成功,直接返回,不用重复更新
if (PayOrderStatusEnum.isSuccess(order.getStatus().getStatus())) {
log.debug("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId());
return;
}
if (!PayOrderStatusEnum.WAITING.equals(order.getStatus())) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrderDO
int updateCounts = payOrderMapper.update(new LambdaUpdateWrapper<PayOrderDO>()
.eq(PayOrderDO::getId, order.getId())
.eq(PayOrderDO::getStatus, PayOrderStatusEnum.WAITING)
.eq(PayOrderDO::getOrganId, organId)
.set(PayOrderDO::getStatus, PayOrderStatusEnum.SUCCESS.getStatus())
.set(PayOrderDO::getSuccessTime, notify.getSuccessTime())
.set(PayOrderDO::getNotifyTime, LocalDateTime.now())
.set(PayOrderDO::getChannelNotifyData, JsonUtils.zipString(JSON.toJSONString(notify))));
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.debug("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId());
}
/**
* 支付失败回调处理
*
* @param notify
*/
private void notifyOrderClosed(PayOrderRespDTO notify, Long organId) {
// 1. 查询 PayOrderDO
String outTradeNo = notify.getOutTradeNo();
PayOrderDO order = payOrderMapper.selectOne(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getPayNo, outTradeNo)
.eq(PayOrderDO::getOrganId, organId));
if (ObjectUtil.isNull(order)) {
throw exception(PAY_ORDER_NOT_EXIST, outTradeNo);
}
Integer orderStatus = order.getStatus().getStatus();
if (PayOrderStatusEnum.isClosed(orderStatus)) { // 如果已经是关闭,直接返回,不用重复更新
log.debug("[notifyOrderClosed][order({}) 已经是支付关闭,无需更新]", order.getId());
return;
}
// 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单
if (PayOrderStatusEnum.isSuccess(orderStatus)) {
log.debug("[notifyOrderClosed][order({}) 是已支付,无需更新为支付关闭]", order.getId());
return;
}
if (ObjectUtil.notEqual(orderStatus, PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrderDO
int updateCounts = payOrderMapper.update(new LambdaUpdateWrapper<PayOrderDO>()
.eq(PayOrderDO::getId, order.getId())
.eq(PayOrderDO::getStatus, PayOrderStatusEnum.WAITING)
.eq(PayOrderDO::getOrganId, organId)
.set(PayOrderDO::getStatus, PayOrderStatusEnum.FAIL.getStatus())
.set(PayOrderDO::getNotifyTime, LocalDateTime.now())
.set(PayOrderDO::getChannelErrorCode, notify.getChannelErrorCode())
.set(PayOrderDO::getChannelErrorMsg, notify.getChannelErrorMsg())
.set(PayOrderDO::getChannelNotifyData, JsonUtils.zipString(JSON.toJSONString(notify))));
if (updateCounts == 0) { // 校验状态,必须是待支付
throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
}
log.debug("[notifyOrderClosed][order({}) 更新为支付关闭]", order.getId());
}
/**
* 校验获取当前token请求下的组织id
*
* @return
*/
private Long validOrganId() {
// 校验token
Long organId = OrganContextHolder.getOrganId();
if (ObjectUtil.isNull(organId)) {
throw new ServiceException(GlobalErrorCodeConstants.UNAUTHORIZED);
}
return organId;
}
}
@@ -0,0 +1,52 @@
package com.cf.imes.module.system.service.pay.factory;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.exception.PayException;
import com.cf.imes.module.system.service.pay.factory.impl.alipay.AlipayPcPayHandler;
import com.cf.imes.module.system.service.pay.factory.impl.AbstractPayHandler;
import com.cf.imes.module.system.service.pay.factory.impl.wechat.WechatNativePayHandler;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.PAY_CHANNEL_NOT_SUPPORT;
/**
* 支付处理器工厂
*
* @author Gqr
* @since 2025/4/15 15:43
*/
@Component
public class PayHandlerFactory {
private final ApplicationContext applicationContext;
@Autowired
public PayHandlerFactory(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 根据渠道编码获取对应的支付处理器
*
* @param channelCode
* @return
*/
public AbstractPayHandler getPayHandler(int channelCode) {
PayChannelCodeEnum payChannelCodeEnum = PayChannelCodeEnum.fromType(channelCode);
if (ObjectUtil.isNull(payChannelCodeEnum)) {
throw new PayException(PAY_CHANNEL_NOT_SUPPORT);
}
switch (payChannelCodeEnum) {
// 支付宝电脑支付
case ALIPAY_PC:
return applicationContext.getBean(AlipayPcPayHandler.class);
// 微信native支付
case WECHAT_NATIVE:
return applicationContext.getBean(WechatNativePayHandler.class);
default:
throw new PayException(PAY_CHANNEL_NOT_SUPPORT);
}
}
}
@@ -0,0 +1,40 @@
package com.cf.imes.module.system.service.pay.factory.impl;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import java.util.Map;
/**
* 支付处理器抽象类
*
* @author Gqr
* @since 2025/4/15 15:47
*/
public abstract class AbstractPayHandler {
/**
* 统一支付
*
* @param reqDTO
* @return
*/
public abstract PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable;
/**
* 查询支付订单
*
* @param outTradeNo
*/
public abstract PayOrderRespDTO queryPayOrder(String outTradeNo) throws Throwable;
/**
* 解析支付回调数据
*
* @param params
* @param body
* @return
*/
public abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable;
}
@@ -0,0 +1,151 @@
package com.cf.imes.module.system.service.pay.factory.impl.alipay;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import com.alipay.api.AlipayApiException;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.domain.AlipayTradeQueryModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.cf.imes.framework.common.util.object.ObjectUtils;
import com.cf.imes.framework.pay.config.ChenfengAlipayConfig;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import com.cf.imes.module.system.service.pay.factory.impl.AbstractPayHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
/**
* 支付宝电脑支付处理器
*
* @author Gqr
* @since 2025/4/15 15:46
*/
@Slf4j
@Service("alipayPcPayHandler")
public class AlipayPcPayHandler extends AbstractPayHandler {
@Resource
private ChenfengAlipayConfig alipayConfig;
@Override
public PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// ① 通用的参数
String outTradeNo = reqDTO.getOutTradeNo();
model.setOutTradeNo(outTradeNo);
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getSubject());
model.setTotalAmount(String.valueOf(reqDTO.getPrice()));
model.setTimeExpire(LocalDateTimeUtil.format(reqDTO.getExpireTime(), NORM_DATETIME_FORMATTER));
model.setProductCode(alipayConfig.getProductCode());
model.setQrPayMode(alipayConfig.getQrPayMode());
// 1.2 构建 AlipayTradePagePayRequest 请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
DefaultAlipayClient client = new DefaultAlipayClient(alipayConfig);
AlipayTradePagePayResponse response = client.pageExecute(request, Method.GET.name());
// 2.2 处理结果
if (!response.isSuccess()) {
Assert.isFalse(response.isSuccess());
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
return PayOrderRespDTO.waitingOf(null, response.getBody(),
outTradeNo, response);
}
@Override
public PayOrderRespDTO queryPayOrder(String outTradeNo) throws Throwable {
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(outTradeNo);
// 1.2 构建 AlipayTradeQueryRequest 请求
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizModel(model);
DefaultAlipayClient client = new DefaultAlipayClient(alipayConfig);
AlipayTradeQueryResponse response = client.execute(request);
if (!response.isSuccess()) { // 不成功,例如说订单不存在
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
// 2.2 解析订单的状态
Integer status = parseStatus(response.getTradeStatus());
Assert.notNull(status, () -> {
throw new IllegalArgumentException(CharSequenceUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
});
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
outTradeNo, response);
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
// 1. 校验回调数据
verifyNotifyData(params);
// 2. 解析订单的状态
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
Integer status = parseStatus(bodyObj.get("trade_status"));
Assert.notNull(status, (Supplier<Throwable>) () -> {
throw new IllegalArgumentException(CharSequenceUtil.format("body({}) 的 trade_status 不正确", body));
});
return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
bodyObj.get("out_trade_no"), body);
}
/**
* 校验回调数据
*
* @param params 回调参数
* @throws Throwable 验签失败时抛出异常
*/
protected void verifyNotifyData(Map<String, String> params) throws AlipayApiException {
boolean verify = AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipayPublicKey(),
StandardCharsets.UTF_8.name(), alipayConfig.getSignType());
Assert.isTrue(verify, "验签结果不通过");
}
/**
* 解析查询状态
*
* @param tradeStatus
* @return
*/
private static Integer parseStatus(String tradeStatus) {
return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
: ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus()
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
}
protected LocalDateTime parseTime(String str) {
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
}
}
@@ -0,0 +1,35 @@
package com.cf.imes.module.system.service.pay.factory.impl.wechat;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.cf.imes.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import com.cf.imes.module.system.service.pay.factory.impl.AbstractPayHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 微信native支付处理器
*
* @author Gqr
* @since 2025/4/15 15:46
*/
@Service("wechatNativePayHandler")
@Slf4j
public class WechatNativePayHandler extends AbstractPayHandler {
@Override
public PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
return null;
}
@Override
public PayOrderRespDTO queryPayOrder(String outTradeNo) {
return null;
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
return null;
}
}
@@ -0,0 +1,30 @@
package com.cf.imes.module.system.validation.pay;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 支付渠道编码校验注解
* @author Gqr
* @since 2025/4/18 10:21
*/
@Target({
ElementType.FIELD,
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {PayChannelCodeValidator.class}
)
public @interface PayChannelCodeValid {
String message() default "不支持的支付渠道,0:支付宝、1:微信";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@@ -0,0 +1,28 @@
package com.cf.imes.module.system.validation.pay;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.module.system.enums.pay.PayChannelCodeEnum;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 支付渠道编码校验器
*
* @author Gqr
* @since 2025/4/18 10:22
*/
public class PayChannelCodeValidator implements ConstraintValidator<PayChannelCodeValid, Integer> {
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (ObjectUtil.isNull(value)) {
return true;
}
for (PayChannelCodeEnum codeEnum : PayChannelCodeEnum.values()) {
if (ObjectUtil.equal(codeEnum.getCode(), value)) {
return true;
}
}
return false;
}
}
@@ -237,4 +237,11 @@ chenfeng:
gray:
default-version: 1.0.0
version: 1.0.0
pay:
alipay:
serverUrl: https://openapi-sandbox.dl.alipaydev.com/gateway.do
appId: 8wb0qYthTUqf9290UJfLejTcOA5CU1wiAhmpLbXmwDCCt+ZakcBPBK1a88E=
privateKey: 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
alipayPublicKey: KbUGE7zsPBKWPx0/zMcAdLRldKrcgy2LRiOrqOBm7y4iPJLXTYtXFJT15p2JMQiyOM8mqiYQR0OEDwfrKxDgHXLtIut/e0EAk9bZwZoLrqVe0pN1huseRC38zEglbWil3yz+tZfHZuAaEq2EzvWJltX/tRKOzNBqAT9l+UvnqNedgCFB9MY2EErqJjbn/at3gYCkV2idt31ghF0z8SEQ5h9ztNYtmTvk8c8ipi0/hIjX8XryN6mlOvGQfyZ4rWrBWBuZ6iD/SyuThwxKmFYU4dhHbHVpBrva5nJWU43CP7RVaJm649nNJnce+Ppj2Tv8+ugSLe5poZWo2zmGe3Xn5WmN6OTZyRI1pnmn2/hGxtOMmTZJX7Gh0pI4v3wmU0gOd/gbRlqlvN+u12T92nyH8MQrHdA7UXuttAtolYUa/Rli/20qHX69QtOtvfZk8mNvDSj9dDvODseGcrFHm07jyfxpaIkQDeN75cQ3NwNgDQEW8urQszaU1IJ7hc+20tt0Z3td4+7MfiVPpGfVxsGE66gwLgpB8wsxEhyBTaBr7osMLqOv
notifyUrl: VvJ5DOVuGOBw39j7tyrvsEEtLSseVM8T3u9kqsWKago+iI5ao8JzoggmeSe+N9LrsYxutK4fkGpmr4jOGJ3JTQFZ7YBXLxf2oLIXOZH9O27EnDtm
debug: false