微信支付、退款

localizer
ljw 2025-10-17 16:19:54 +08:00
parent 0fce5abc9f
commit 246c6baf52
16 changed files with 295 additions and 507 deletions

View File

@ -1,12 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
@Data
public class BindParam {
private Long userId;
private String wxCode;
}

View File

@ -1,55 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
/**
* 退DTO
*/
@Data
public class RefundRequestDto {
/**
* ID
*/
@NotNull(message = "订单ID不能为空")
private Long orderId;
/**
* 退退使退
*/
private BigDecimal refundAmount;
/**
* 退
*/
@Size(max = 200, message = "退款原因不能超过200个字符")
private String reason;
/**
* 退退使
*/
private WechatRefundParams wechatRefundParams;
@Data
public static class WechatRefundParams {
/**
* 退
*/
@Size(max = 64, message = "商户退款单号不能超过64个字符")
private String outRefundNo;
/**
* 退
*/
private String fundsAccount;
/**
* 退
*/
private String notifyUrl;
}
}

View File

@ -5,11 +5,13 @@ import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StreamUtils;
@Configuration
@ConditionalOnClass(WxPayService.class)
@ -26,7 +28,22 @@ public class WxPayConfiguration {
config.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
config.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
config.setMchKey(StringUtils.trimToNull(this.properties.getApiKey()));
config.setKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
String keyPath = StringUtils.trimToNull(this.properties.getPrivateKeyPath());
config.setKeyPath(keyPath);
// 优先支持以 classpath: 开头的 p12 证书路径,直接加载为内存字节,避免路径解析兼容性问题
if (StringUtils.isNotBlank(keyPath) && keyPath.startsWith("classpath:")) {
String classpathLocation = keyPath.substring("classpath:".length());
try {
ClassPathResource resource = new ClassPathResource(classpathLocation.startsWith("/") ? classpathLocation.substring(1) : classpathLocation);
if (resource.exists()) {
byte[] keyBytes = StreamUtils.copyToByteArray(resource.getInputStream());
config.setKeyContent(keyBytes);
}
} catch (Exception ignored) {
// 忽略此处异常,保持 SDK 以文件路径方式继续尝试读取
}
}
// 可以指定是否使用沙箱环境
// config.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl();

View File

@ -35,10 +35,6 @@ public class WxPayProperties {
*/
private String privateKeyPath;
private String privateCertPath;
private String privateCertP12Path;
/**
*
*/

View File

@ -1,10 +1,21 @@
package com.njzscloud.supervisory.wxPay.controller;
import com.google.common.base.Strings;
import cn.hutool.core.util.IdUtil;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.google.common.base.Strings;
import com.njzscloud.common.core.ex.ExceptionMsg;
import com.njzscloud.common.core.ex.Exceptions;
import com.njzscloud.common.core.utils.R;
import com.njzscloud.common.security.ex.UserLoginException;
import com.njzscloud.common.wechat.WechatUtil;
import com.njzscloud.common.wechat.param.Code2SessionParam;
import com.njzscloud.common.wechat.result.Code2SessionResult;
import com.njzscloud.supervisory.money.contant.MoneyChangeCategory;
import com.njzscloud.supervisory.money.pojo.entity.MoneyAccountEntity;
import com.njzscloud.supervisory.money.pojo.entity.MoneyChangeDetailEntity;
@ -13,15 +24,17 @@ import com.njzscloud.supervisory.money.service.MoneyChangeDetailService;
import com.njzscloud.supervisory.order.contant.PaymentStatus;
import com.njzscloud.supervisory.order.contant.PaymentWay;
import com.njzscloud.supervisory.order.contant.SettlementWay;
import com.njzscloud.supervisory.wxPay.dto.WechatPayJsapiOrderResponseDto;
import com.njzscloud.supervisory.wxPay.dto.RefundRequestDto;
import com.njzscloud.supervisory.order.pojo.entity.OrderExpenseItemsEntity;
import com.njzscloud.supervisory.order.pojo.entity.OrderInfoEntity;
import com.njzscloud.supervisory.order.pojo.param.PaymentItemParam;
import com.njzscloud.supervisory.order.pojo.param.PaymentParam;
import com.njzscloud.supervisory.wxPay.param.PaymentParam;
import com.njzscloud.supervisory.order.pojo.result.PaymentContextResult;
import com.njzscloud.supervisory.order.service.OrderExpenseItemsService;
import com.njzscloud.supervisory.order.service.OrderInfoService;
import com.njzscloud.supervisory.wxPay.config.WxPayProperties;
import com.njzscloud.supervisory.wxPay.service.WeChatPayService;
import com.njzscloud.supervisory.wxPay.utils.RequestHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
@ -29,7 +42,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@ -49,6 +61,8 @@ public class PaymentController {
private final MoneyAccountService moneyAccountService;
private final MoneyChangeDetailService moneyChangeDetailService;
private final WeChatPayService wechatPayService;
private final WxPayService wxPayService;
private final WxPayProperties properties;
/**
* 使SDK
@ -146,12 +160,13 @@ public class PaymentController {
wxRequest.setOutTradeNo(outTradeNo);
wxRequest.setBody("订单支付-" + ctx.getSn());
wxRequest.setTotalFee(ctx.getSettleMoney().multiply(new BigDecimal("100")).intValue()); // 转换为分
wxRequest.setOpenid(getCurrentUserOpenid());
wxRequest.setOpenid(getCurrentUserOpenid(paymentParam.getWxCode()));
wxRequest.setTradeType("JSAPI");
wxRequest.setNotifyUrl("https://your-domain.com/payment/wechat/notify"); // 需要配置实际的回调地址
wxRequest.setSpbillCreateIp(RequestHolder.getClientIP());
wxRequest.setNotifyUrl(properties.getNotifyUrl()); // 需要配置实际的回调地址
// 调用微信支付服务
WechatPayJsapiOrderResponseDto response = wechatPayService.createJsapiOrder(wxRequest);
WxPayMpOrderResult result = (WxPayMpOrderResult) wechatPayService.createJsapiOrder(wxRequest);
// 更新订单状态为待支付
orderInfoService.lambdaUpdate()
@ -163,10 +178,10 @@ public class PaymentController {
log.info("微信支付订单创建成功生产级SDK订单ID{},微信订单号:{}", paymentParam.getOrderId(), outTradeNo);
return R.success(response);
} catch (Exception e) {
return R.success(result);
} catch (WxPayException e) {
log.error("微信支付订单创建失败订单ID{},错误信息:{}", paymentParam.getOrderId(), e.getMessage(), e);
throw Exceptions.clierr("微信支付订单创建失败:" + e.getMessage());
WxPayNotifyResponse.fail(e.getMessage());
}
}
@ -213,10 +228,16 @@ public class PaymentController {
/**
* openid
*/
public String getCurrentUserOpenid() {
// 这里需要根据实际业务逻辑获取当前用户的openid
// 可能通过JWT token、session等方式获取
return "test_openid"; // 临时返回测试值
public String getCurrentUserOpenid(String wxCode) {
// 调用微信API获取openId和unionId
Code2SessionResult code2SessionResult = WechatUtil.code2Session(new Code2SessionParam().setJs_code(wxCode));
Integer errcode = code2SessionResult.getErrcode();
if (errcode != null && errcode != 0) {
log.error("微信登录失败, errcode: {}, errmsg: {}", errcode, code2SessionResult.getErrmsg());
throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "微信登录失败");
}
return code2SessionResult.getOpenid();
}
/**
@ -234,14 +255,14 @@ public class PaymentController {
}
/**
* ID
* sn
*/
private Long extractOrderIdFromOutTradeNo(String outTradeNo) {
private String extractOrderIdFromOutTradeNo(String outTradeNo) {
try {
if (outTradeNo != null && outTradeNo.startsWith("ORDER_")) {
String[] parts = outTradeNo.split("_");
if (parts.length >= 2) {
return Long.parseLong(parts[1]);
return parts[1];
}
}
} catch (Exception e) {
@ -254,59 +275,159 @@ public class PaymentController {
*
*/
@PostMapping("/wechat/notify")
public String wechatPayNotify(@RequestParam String xmlData) {
public String wechatPayNotify(@RequestBody String xmlData) {
try {
log.info("收到微信支付回调:{}", xmlData);
return wechatPayService.handleNotify(xmlData);
} catch (Exception e) {
log.error("处理微信支付回调异常:{}", e.getMessage(), e);
return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理异常]]></return_msg></xml>";
WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(xmlData);
String orderSn = extractOrderIdFromOutTradeNo(notifyResult.getOutTradeNo());
if (orderSn != null) {
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getSn, orderSn)
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiZhiFu)
.set(OrderInfoEntity::getPayTime, LocalDateTime.now())
.update();
log.info("微信支付成功,商户订单号:{},微信交易号:{}",
orderSn, notifyResult.getTransactionId());
} else {
log.warn("无法从商户订单号中提取订单");
}
return WxPayNotifyResponse.success("处理成功!");
} catch (WxPayException e) {
log.error(e.getMessage());
return WxPayNotifyResponse.fail(e.getMessage());
}
}
/**
*
* 退 - 退退
*/
@PostMapping("/wechat/query")
public R<?> queryWechatOrder(@RequestParam String outTradeNo) {
@PostMapping("/refund")
public R<?> refund(@RequestBody RefundRequestDto refundRequest) {
try {
WxPayOrderQueryResult result = wechatPayService.queryOrder(outTradeNo);
return R.success(result);
// 验证订单存在性
OrderInfoEntity orderInfo = orderInfoService.getById(refundRequest.getOrderId());
if (orderInfo == null) {
throw Exceptions.clierr("订单不存在");
}
// 验证订单状态
if (!PaymentStatus.YiZhiFu.getVal().equals(orderInfo.getPaymentStatus().getVal())) {
throw Exceptions.clierr("订单未支付,无法退款");
}
// 获取支付上下文
PaymentContextResult ctx = orderInfoService.paymentContext(refundRequest.getOrderId());
if (ctx == null) {
throw Exceptions.clierr("获取支付上下文失败");
}
// 根据支付方式处理退款
if (PaymentWay.WX.getVal().equals(ctx.getPaymentCategory())) {
// 微信退款
//生成退款单号
String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr();
int money = ctx.getSettleMoney().multiply(new BigDecimal("100")).intValue();
// 微信退全款
String notifyUrl = properties.getRefundNotifyUrl();
wechatPayService.refund(orderInfo.getOutTradeNo(), orderSn, money, money, notifyUrl);
return R.success("退款成功");
} else if (PaymentWay.COMPANY.getVal().equals(ctx.getPaymentCategory())) {
// 公司退款
return processCompanyRefund(refundRequest, ctx);
} else {
throw Exceptions.clierr("不支持的支付方式");
}
} catch (Exception e) {
log.error("查询微信支付订单失败,订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
return R.failed();
log.error("退款处理异常订单ID{}", refundRequest.getOrderId(), e);
return R.failed(ExceptionMsg.CLI_ERR_MSG, "退款处理失败:" + e.getMessage());
}
}
/**
*
* 退
*/
@PostMapping("/wechat/close")
public R<?> closeWechatOrder(@RequestParam String outTradeNo) {
try {
wechatPayService.closeOrder(outTradeNo);
return R.success("订单关闭成功");
} catch (Exception e) {
log.error("关闭微信支付订单失败,订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
return R.failed();
private R<?> processCompanyRefund(RefundRequestDto refundRequest, PaymentContextResult ctx) {
if (refundRequest.getRefundAmount() == null) {
throw Exceptions.clierr("公司退款金额不能为空");
}
if (refundRequest.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw Exceptions.clierr("退款金额必须大于0");
}
if (refundRequest.getRefundAmount().compareTo(ctx.getSettleMoney()) > 0) {
throw Exceptions.clierr("退款金额不能超过订单已结算金额");
}
// 如果订单再次退款,本次退款金额+已退款金额不能大于结算金额
if (refundRequest.getRefundAmount().add(ctx.getRefundMoney()).compareTo(ctx.getSettleMoney()) > 0) {
throw Exceptions.clierr("退款金额总和不能超过订单已结算金额");
}
// 恢复公司账户余额
restoreCompanyBalance(ctx, refundRequest.getRefundAmount(), refundRequest.getOrderId(), refundRequest.getReason());
// 更新订单状态为已退款
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, refundRequest.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiTuiKuan)
.set(OrderInfoEntity::getRefundTime, LocalDateTime.now())
.update();
log.info("公司退款成功订单ID{},退款金额:{}", refundRequest.getOrderId(), refundRequest.getRefundAmount());
return R.success("退款成功");
}
/**
* 退
*
*/
@PostMapping("/wechat/refund")
public R<?> refundWechatOrder(@RequestParam String outTradeNo,
@RequestParam String outRefundNo,
@RequestParam int totalFee,
@RequestParam int refundFee,
@RequestParam(required = false) String refundDesc) {
private void restoreCompanyBalance(PaymentContextResult ctx, BigDecimal refundAmount, Long orderId, String reason) {
if (ctx.getCompanyUserId() == null || ctx.getCompanyAccountId() == null) {
throw Exceptions.clierr("公司账户信息不完整,无法退款");
}
// 恢复公司账户余额
BigDecimal oldBalance = ctx.getCompanyBalance();
BigDecimal newBalance = oldBalance.add(refundAmount);
MoneyAccountEntity companyAccount = new MoneyAccountEntity()
.setId(ctx.getCompanyAccountId())
.setMoney(newBalance);
moneyAccountService.updateById(companyAccount);
// 记录资金明细(这里需要实现资金明细记录功能)
MoneyChangeDetailEntity changeDetail = new MoneyChangeDetailEntity()
.setCompanyId(ctx.getTransCompanyId())
.setOrderId(orderId)
.setMoneyAccountId(companyAccount.getId())
.setOldMoney(oldBalance)
.setDelta(refundAmount) // 扣减为负数
.setNewMoney(newBalance)
.setMoneyChangeCategory(MoneyChangeCategory.DingDanTuiKuan)
.setMemo(reason);
moneyChangeDetailService.save(changeDetail);
log.info("公司账户余额恢复成功账户ID{},恢复金额:{}", ctx.getCompanyAccountId(), refundAmount);
}
/**
* 退
*/
@PostMapping("/wechat/refundNotify")
public String parseRefundNotifyResult(@RequestBody String xmlData) {
try {
String result = wechatPayService.refund(outTradeNo, outRefundNo, totalFee, refundFee, refundDesc);
return R.success(result);
} catch (Exception e) {
log.error("申请微信支付退款失败,订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
return R.failed();
WxPayRefundNotifyResult result = wxPayService.parseRefundNotifyResult(xmlData);
String orderSn = extractOrderIdFromOutTradeNo(result.getReqInfo().getOutTradeNo());
// 更新订单状态为已退款
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getSn, orderSn)
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiTuiKuan)
.set(OrderInfoEntity::getRefundTime, LocalDateTime.now())
.update();
return WxPayNotifyResponse.success("退款成功!");
} catch (WxPayException e) {
log.error(e.getMessage());
return WxPayNotifyResponse.fail(e.getMessage());
}
}
}

View File

@ -0,0 +1,32 @@
package com.njzscloud.supervisory.wxPay.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
/**
* 退DTO
*/
@Data
public class RefundRequestDto {
/**
* ID
*/
@NotNull(message = "订单ID不能为空")
private Long orderId;
/**
* 退退使退
*/
private BigDecimal refundAmount;
/**
* 退
*/
@Size(max = 200, message = "退款原因不能超过200个字符")
private String reason;
}

View File

@ -1,45 +0,0 @@
package com.njzscloud.supervisory.wxPay.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* JSAPI/DTO
*/
@Data
public class WechatPayJsapiOrderDto {
/**
*
*/
@NotBlank(message = "商品描述不能为空")
@Size(max = 127, message = "商品描述不能超过127个字符")
private String description;
/**
*
*/
@NotBlank(message = "商户订单号不能为空")
@Size(min = 6, max = 32, message = "商户订单号长度必须在6-32个字符之间")
private String outTradeNo;
/**
* openid
*/
@NotBlank(message = "用户openid不能为空")
private String openid;
/**
*
*/
@NotNull(message = "订单金额不能为空")
private Long total;
/**
*
*/
private String currency = "CNY";
}

View File

@ -1,46 +0,0 @@
package com.njzscloud.supervisory.wxPay.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* JSAPI/DTO
*/
@Data
public class WechatPayJsapiOrderResponseDto {
private String appId;
/**
*
*/
@JsonProperty("prepay_id")
private String prepayId;
/**
*
*/
private String timeStamp;
/**
*
*/
private String nonceStr;
/**
*
*/
@JsonProperty("package")
private String packageValue;
/**
*
*/
private String signType;
/**
*
*/
private String paySign;
}

View File

@ -1,5 +1,6 @@
package com.njzscloud.supervisory.order.pojo.param;
package com.njzscloud.supervisory.wxPay.param;
import com.njzscloud.supervisory.order.pojo.param.PaymentItemParam;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ -30,6 +31,9 @@ public class PaymentParam {
*/
private BigDecimal settleTotalMoney;
/**
* code
*/
private String wxCode;
/**

View File

@ -3,7 +3,6 @@ package com.njzscloud.supervisory.wxPay.service;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.njzscloud.supervisory.wxPay.dto.WechatPayJsapiOrderResponseDto;
/**
*
@ -16,7 +15,7 @@ public interface WeChatPayService {
* @param request
* @return
*/
WechatPayJsapiOrderResponseDto createJsapiOrder(WxPayUnifiedOrderRequest request) throws WxPayException;
Object createJsapiOrder(WxPayUnifiedOrderRequest request) throws WxPayException;
/**
*
@ -38,30 +37,14 @@ public interface WeChatPayService {
/**
* 退
*
* @param outTradeNo
* @param outTradeNo
* @param outRefundNo 退
* @param totalFee
* @param refundFee 退
* @param refundDesc 退
* @param totalFee
* @param refundFee 退
* @param notifyUrl 退
* @return 退
* @throws WxPayException
*/
String refund(String outTradeNo, String outRefundNo, int totalFee, int refundFee, String refundDesc) throws WxPayException;
/**
*
*
* @param xmlData XML
* @return
*/
boolean verifyNotify(String xmlData);
/**
*
*
* @param xmlData XML
* @return
*/
String handleNotify(String xmlData);
String refund(String outTradeNo, String outRefundNo, int totalFee, int refundFee, String notifyUrl) throws WxPayException;
}

View File

@ -1,23 +1,17 @@
package com.njzscloud.supervisory.wxPay.service.impl;
import com.alibaba.fastjson2.JSON;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.njzscloud.supervisory.wxPay.dto.WechatPayJsapiOrderResponseDto;
import com.njzscloud.supervisory.wxPay.service.WeChatPayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
/**
*
*/
@ -28,49 +22,17 @@ public class WeChatPayServiceImpl implements WeChatPayService {
private final WxPayService wxPayService;
private static final String WECHAT_PAY_API_BASE = "https://api.mch.weixin.qq.com";
private static final String JSAPI_ORDER_PATH = "/v3/pay/transactions/jsapi";
private static final String ORDER_QUERY_PATH_PREFIX = "/v3/pay/transactions/out-trade-no/";
private static final String ORDER_CLOSE_PATH_SUFFIX = "/close";
private static final String REFUND_PATH = "/v3/refund/domestic/refunds";
private static final String REFUND_QUERY_PATH_PREFIX = "/v3/refund/domestic/refunds/";
@Override
public WechatPayJsapiOrderResponseDto createJsapiOrder(WxPayUnifiedOrderRequest request) throws WxPayException {
try {
log.info("开始创建微信支付JSAPI订单商户订单号{}", request.getOutTradeNo());
// 调用微信支付SDK创建订单
WxPayUnifiedOrderResult result = wxPayService.createOrder(request);
if (result == null) {
throw new WxPayException("微信支付订单创建失败,返回结果为空");
}
// 构建响应DTO
WechatPayJsapiOrderResponseDto responseDto = new WechatPayJsapiOrderResponseDto();
responseDto.setAppId(result.getAppid());
responseDto.setPrepayId(result.getPrepayId());
responseDto.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000));
responseDto.setNonceStr(result.getNonceStr());
responseDto.setPackageValue("prepay_id=" + result.getPrepayId());
responseDto.setSignType("RSA");
// 生成小程序调起支付的签名
String paySign = generatePaySign(responseDto);
responseDto.setPaySign(paySign);
log.info("微信支付JSAPI订单创建成功预支付ID{}", result.getPrepayId());
return responseDto;
} catch (WxPayException e) {
log.error("微信支付订单创建失败,商户订单号:{},错误信息:{}", request.getOutTradeNo(), e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("微信支付订单创建异常,商户订单号:{},错误信息:{}", request.getOutTradeNo(), e.getMessage(), e);
throw new WxPayException("微信支付订单创建异常:" + e.getMessage(), e);
public Object createJsapiOrder(WxPayUnifiedOrderRequest request) throws WxPayException {
log.info("开始创建微信支付JSAPI订单入参为{}", JSON.toJSONString(request));
// 调用微信支付SDK创建订单
Object result = wxPayService.createOrder(request);
if (result == null) {
throw new WxPayException("微信支付订单创建失败,返回结果为空");
}
return result;
}
/**
@ -110,26 +72,26 @@ public class WeChatPayServiceImpl implements WeChatPayService {
/**
* 退
*
* @param outTradeNo
* @param outTradeNo
* @param outRefundNo 退
* @param totalFee
* @param refundFee 退
* @param refundDesc 退
* @param totalFee
* @param refundFee 退
* @param notifyUrl 退
* @return 退
* @throws WxPayException
*/
public String refund(String outTradeNo, String outRefundNo, int totalFee, int refundFee, String refundDesc) throws WxPayException {
public String refund(String outTradeNo, String outRefundNo, int totalFee, int refundFee, String notifyUrl) throws WxPayException {
try {
log.info("开始申请微信支付退款,商户订单号:{},退款单号:{},退款金额:{}", outTradeNo, outRefundNo, refundFee);
WxPayRefundRequest refundRequest = WxPayRefundRequest.newBuilder()
.outTradeNo(outTradeNo)
.outRefundNo(outRefundNo)
.totalFee(totalFee)
.refundFee(refundFee)
.refundDesc(refundDesc)
.build();
.outTradeNo(outTradeNo)
.outRefundNo(outRefundNo)
.totalFee(totalFee)
.refundFee(refundFee)
.notifyUrl(notifyUrl)
.build();
WxPayRefundResult refundResult = wxPayService.refund(refundRequest);
return refundResult.getRefundId();
} catch (WxPayException e) {
@ -138,153 +100,4 @@ public class WeChatPayServiceImpl implements WeChatPayService {
}
}
/**
*
*
*
* 1. key=valueASCII
* 2.
* 3.
* 4. sign
* 5.
*
* @param responseDto DTO
* @return
*/
private String generatePaySign(WechatPayJsapiOrderResponseDto responseDto) {
try {
// 构建签名字符串
StringBuilder signStr = new StringBuilder();
signStr.append("appId=").append(responseDto.getAppId())
.append("&nonceStr=").append(responseDto.getNonceStr())
.append("&package=").append(responseDto.getPackageValue())
.append("&signType=").append(responseDto.getSignType())
.append("&timeStamp=").append(responseDto.getTimeStamp());
// 获取商户密钥(需要从配置中获取)
String apiKey = wxPayService.getConfig().getMchKey();
// 添加商户密钥
signStr.append("&key=").append(apiKey);
// 使用MD5签名
String signString = signStr.toString();
log.debug("待签名字符串:{}", signString);
// 使用MD5签名这里需要使用MD5工具类
String paySign = md5(signString).toUpperCase();
log.debug("生成的支付签名:{}", paySign);
return paySign;
} catch (Exception e) {
log.error("生成支付签名失败:{}", e.getMessage(), e);
throw new RuntimeException("生成支付签名失败", e);
}
}
/**
* MD5
*
* @param data
* @return MD5
*/
private String md5(String data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
log.error("MD5签名失败{}", e.getMessage(), e);
throw new RuntimeException("MD5签名失败", e);
}
}
/**
*
*
* @param xmlData XML
* @return
*/
public boolean verifyNotify(String xmlData) {
try {
// 验证签名暂时返回true实际项目中需要实现正确的签名验证逻辑
return true;
} catch (Exception e) {
log.error("验证支付回调签名失败:{}", e.getMessage(), e);
return false;
}
}
/**
*
*
* @param xmlData XML
* @return
*/
public String handleNotify(String xmlData) {
try {
log.info("开始处理微信支付回调:{}", xmlData);
// 验证签名
if (!verifyNotify(xmlData)) {
log.error("支付回调签名验证失败");
return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>";
}
// 解析回调数据
Map<String, String> notifyData = parseXmlToMap(xmlData);
String outTradeNo = notifyData.get("out_trade_no");
String transactionId = notifyData.get("transaction_id");
String resultCode = notifyData.get("result_code");
log.info("支付回调处理成功,商户订单号:{},微信交易号:{},结果:{}", outTradeNo, transactionId, resultCode);
// 这里可以添加业务逻辑,比如更新订单状态等
// TODO: 根据实际业务需求处理支付成功后的逻辑
return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
} catch (Exception e) {
log.error("处理支付回调异常:{}", e.getMessage(), e);
return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理异常]]></return_msg></xml>";
}
}
/**
* XML
*
* @param xmlData XML
* @return Map
*/
private Map<String, String> parseXmlToMap(String xmlData) {
Map<String, String> result = new HashMap<>();
try {
// 简单的XML解析提取标签和值
String[] lines = xmlData.split("\n");
for (String line : lines) {
line = line.trim();
if (line.startsWith("<") && line.endsWith(">") && !line.startsWith("</")) {
int start = line.indexOf(">") + 1;
int end = line.lastIndexOf("<");
if (start > 0 && end > start) {
String key = line.substring(1, line.indexOf(">"));
String value = line.substring(start, end);
// 移除CDATA包装
if (value.startsWith("<![CDATA[") && value.endsWith("]]>")) {
value = value.substring(9, value.length() - 3);
}
result.put(key, value);
}
}
}
} catch (Exception e) {
log.error("解析XML数据失败{}", e.getMessage(), e);
}
return result;
}
}

View File

@ -0,0 +1,23 @@
package com.njzscloud.supervisory.wxPay.utils;
import cn.hutool.extra.servlet.ServletUtil;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
public class RequestHolder {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
public static String getClientIP() {
HttpServletRequest request = getHttpServletRequest();
if (request == null) {
return null;
}
return ServletUtil.getClientIP(request);
}
}

View File

@ -23,7 +23,8 @@ spring:
- /bulletin/paging
- /truck_location_track/**
- /fdx
- /payment/wechat/callback
- /payment/wechat/notify
- /payment/wechat/refundNotify
app:
default-place:
province: 320000
@ -71,13 +72,12 @@ wechat:
# 证书序列号
cert-serial-no: 1BCB1533688F349541C7B636EF67C666828BADBA
# 文件路径
private-key-path: classpath:cert/apiclient_key.pem
private-cert-path: classpath:cert/apiclient_cert.pem
private-cert-p12-path: classpath:cert/apiclient_key.p12
private-key-path: classpath:cert/apiclient_cert.p12
# private-key-path: D:/project/再昇云/代码/njzscloud/njzscloud-svr/src/main/resources/cert/apiclient_cert.p12
# 支付回调地址
notify-url: https://your-domain.com/api/payment/wechat/callback
notify-url: http://115.29.236.92:8082/payment/wechat/notify
# 退款回调地址
refund-notify-url: https://your-domain.com/api/payment/wechat/refundCallback
refund-notify-url: http://115.29.236.92:8082/payment/wechat/refundNotify
mqtt:
enabled: true
broker: tcp://139.224.54.144:1883

View File

@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEJDCCAwygAwIBAgIUG8sVM2iPNJVBx7Y272fGZoKLrbowDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUxMDEzMDI1OTIxWhcNMzAxMDEyMDI1OTIxWjB+MRMwEQYDVQQDDAox
NzI5NzAzMTEwMRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xKjAoBgNVBAsM
Iea7geW3nuW4guWFtOa7geWunuS4muaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04x
ETAPBgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAveXhbJ0COwMCbfVwyjhiMRRvLwmQlOvxnjwVncGZK8RbNiel0z1xtlJs8+nv
s2T/j2ln5ceRjpnzAErSqmq+Y16l859kTOMkly3B1u3CPAf415tZRrHG3KEM19Fi
z+lcxyo/VHgGvJuVVPqV0/n3OY8nz4VTvFL+jDGtyTGJlTmX8EekJ3e7tzyQrGS9
+twC7NxQC0Ob11/rGksT/dnVlSSZu4VlYwDFfUeTsGXIV1NbRmnvsjy30AHQW4dI
DLWYyT30xloKL02pN7g3NJB1qm3g5L1wby97zOhJBtdZPINq2ubahN2pPNF3Jf/2
pqoZ9W70AO0kJ+6eOir7t02n2QIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0P
BAQDAgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1
cy5jb20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFE
Mzk3NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMz
QTg3QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBABwjPgD83RY2
NJZswumIh70IeEsthu7H/uHQJhkHxqetMRcbarr6uSAzdaXrk4aq/h/UQ+kzVjd4
VCcx3YRX2tkxhAnfVJEnQTFYtQ5kTWa83P/W2mrQm/XjnMWa2RD44lhld14kx/zO
gBhQHZcG8jQiNoJspIXIaTxvzU6XsDh2muPA0Ris6l4/83z7dNguWPzAxAD+B1Nn
60KP1eOj+Jjw9wLOqWmtI48zFU8MHDzjYmeFtu4QSJy1eMn7gNKQ91pSjR0r2fzS
ujLMBgtY657UEXPlwtVSzohnWCr0hAnMrFsxMuNPkeOOg+QuXOZoOa1J84aBu1oU
SFzt6sqGLgM=
-----END CERTIFICATE-----

View File

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC95eFsnQI7AwJt
9XDKOGIxFG8vCZCU6/GePBWdwZkrxFs2J6XTPXG2Umzz6e+zZP+PaWflx5GOmfMA
StKqar5jXqXzn2RM4ySXLcHW7cI8B/jXm1lGscbcoQzX0WLP6VzHKj9UeAa8m5VU
+pXT+fc5jyfPhVO8Uv6MMa3JMYmVOZfwR6Qnd7u3PJCsZL363ALs3FALQ5vXX+sa
SxP92dWVJJm7hWVjAMV9R5OwZchXU1tGae+yPLfQAdBbh0gMtZjJPfTGWgovTak3
uDc0kHWqbeDkvXBvL3vM6EkG11k8g2ra5tqE3ak80Xcl//amqhn1bvQA7SQn7p46
Kvu3TafZAgMBAAECggEAFu2vtzaJ82gcnfw5nyEJshVei1ZjT6fhZ5KnpYGfpeoy
fSpyWjwcA8CWCSlg2M9EECPQ+KyNBaPO9GUUZKE41bSy4uSG7sWqQFq1Aed5QVhR
Em9eayvB8Be9/jz46lOp8R5HwUIwxgsfD8Cl4I9Y62k+jE7z2lRHCdRhwF4r14mE
cKZCi8JclOQkEkWN0R3gEZuwOV2LUYwHT1TPGMsSIoTKz66K1C45wN1HViL+XI1A
VgwFnzFKbixB4tmU8QfcB/BiR5dLaurN2oCLcIh9/goNAq3W9t8R0c4vhpRtVthg
SeYB4MonUsWUOmA46O+aQsnVshIFmHQUDp3571o6UQKBgQDe4GZQWlQryUDAKGco
4B0PBoJia460Q+ll+0uFcDHsV5abmpJ4qwQ04OTRNwNRyh9Ni0uqH400zABpoKC8
W+wfZdHfJeQHOtKc+hTh8hMOKHENvFpq79qY0iFpTPXoCP5xRdZ2y7460Lx2Sz8f
oABfLD1zZkFCtg4KlEGATcD6HQKBgQDaHsWQK1FtFBpjYSjmDzTN73t7hGjCNOtx
sscOhIb6sP0Dc5BlmBX+3O04D0KMToSdAezAeHvC1K9SotkYE+MYRlMRxxpTgRMT
kyGKFnYTaH4yu9ntDHtEEkvk2GPSmpG3m6d61DNCZM5Y0hGEIrIJpaK31PI5bSoY
jyrpdmuX7QKBgGJ9ZZlCoYmL8Po5oIS2rQCzzrBxeWl9zUlMyG3aS6V2e06d+vYq
FLzNWTB/EA93s2eQE8rFEKBxOglzezRhprGlTB5BX0g2nVXTSaEjWXpbMsJwJsC0
57O6wBsJcG2uc8pYo1Jx7MmO7omT7Pwrt55XLXyuhk5Vp1TTeTKaqZmdAoGAcFjB
MczeF+zu2plE05qpfBkJ5UkCHGXdj6ah2NxyGdXnvEtd6LY5PhjVJrbVdCdNLwG6
zDhCpsudslamszwrEGnIh7Oh37jv9nkmIVYIY17DAaa4TNzM8PbrmGfMosFypLvL
u/xomp4Jmr44v3KPeWCTO1S3ntSXhibX14BjUtECgYAp9K5TOTD8bPX1IVz3nZvB
ADA1U87AQJ0zrvGjHCOCY//hngQw5Q+oKb24Uo3TkEfkhuXr58AGRPqnRoOYeCUm
cS3bjLkqOQN6fukRsT1Lb+FG0UZh7DdakUo9UtSUanWl3AdVdLWHWJmV4hZ2E5Fi
GM/N+FRqmebPyJRFNhQY0Q==
-----END PRIVATE KEY-----

10
pom.xml
View File

@ -181,6 +181,16 @@
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>cert/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>cert/**</include>
</includes>
</resource>
</resources>
<plugins>