支付相关

localizer
ljw 2025-10-17 09:37:20 +08:00
parent 8ae65e6d55
commit c8ab79494a
19 changed files with 763 additions and 1732 deletions

View File

@ -97,7 +97,11 @@
<artifactId>hutool-all</artifactId>
<version>5.8.35</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.7.0</version>
</dependency>
</dependencies>

View File

@ -1,471 +0,0 @@
package com.njzscloud.supervisory.order.controller;
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;
import com.njzscloud.supervisory.money.service.MoneyAccountService;
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.order.pojo.dto.*;
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.order.pojo.result.PaymentContextResult;
import com.njzscloud.supervisory.order.service.OrderExpenseItemsService;
import com.njzscloud.supervisory.order.service.OrderInfoService;
import com.njzscloud.supervisory.order.service.WechatPayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
*
*/
@Slf4j
@RestController
@RequestMapping("/payment")
@RequiredArgsConstructor
public class PaymentController {
private final OrderExpenseItemsService orderExpenseItemsService;
private final OrderInfoService orderInfoService;
private final MoneyAccountService moneyAccountService;
private final MoneyChangeDetailService moneyChangeDetailService;
private final WechatPayService wechatPayService;
/**
*
*/
@PostMapping("/pay")
@Transactional(rollbackFor = Exception.class)
public R<?> pay(@RequestBody PaymentParam paymentParam) {
// 入参校验占位支付方式、订单ID、支付清单不能为空
if (paymentParam == null
|| paymentParam.getOrderId() == null
|| Strings.isNullOrEmpty(paymentParam.getPaymentCategory())
|| paymentParam.getSettleTotalMoney() == null
|| paymentParam.getSettleTotalMoney().signum() < 0
|| paymentParam.getItems() == null) {
throw Exceptions.clierr("参数不完整或支付总金额小于0");
}
// 校验支付清单id、名称、金额
boolean invalidItem = paymentParam.getItems().stream().anyMatch(it ->
it == null
|| it.getId() == null
|| it.getExpenseItemName() == null || it.getExpenseItemName().length() == 0
|| it.getSettleMoney() == null || it.getSettleMoney().signum() < 0
);
if (invalidItem) {
throw Exceptions.clierr("支付清单项不合法id/名称/金额)");
}
// 校验支付方式
if (paymentParam.getPaymentCategory() == null ||
!(PaymentWay.WX.getVal().equals(paymentParam.getPaymentCategory()) ||
PaymentWay.COMPANY.getVal().equals(paymentParam.getPaymentCategory()))) {
throw Exceptions.clierr("支付方式不合法");
}
// 校验数据库中结算金额是否与请求一致
for (PaymentItemParam it : paymentParam.getItems()) {
OrderExpenseItemsEntity entity = orderExpenseItemsService.getById(it.getId());
if (entity == null) {
throw Exceptions.clierr("支付清单项不存在id=" + it.getId());
}
BigDecimal dbSettle = entity.getSettleMoney();
BigDecimal reqSettle = it.getSettleMoney();
if (dbSettle == null) {
throw Exceptions.clierr("系统结算金额缺失id=" + it.getId());
}
if (dbSettle.compareTo(reqSettle) != 0) {
throw Exceptions.clierr("结算金额不一致【" + it.getExpenseItemName() + "】(id=" + it.getId()
+ "),系统:" + dbSettle + ",请求:" + reqSettle);
}
}
// 查询订单及相关支付上下文(公司、司机账户)
PaymentContextResult ctx = orderInfoService.paymentContext(paymentParam.getOrderId());
if (ctx == null || ctx.getOrderId() == null) {
throw Exceptions.clierr("订单不存在");
}
// 验证总金额是否与参数中的结算总金额一致
if (paymentParam.getSettleTotalMoney() == null ||
paymentParam.getSettleTotalMoney().compareTo(ctx.getSettleMoney()) != 0) {
throw Exceptions.clierr("结算总金额与实际总金额不一致");
}
// 验证总金额是否与参数中的结算总金额一致(这里已经是第二层验证了)
// 第一层验证在循环中已经完成:每个清单项的金额与数据库一致
// 第二层验证:参数中的结算总金额与数据库统计总金额一致
// 根据支付方式处理支付
if (PaymentWay.COMPANY.getVal().equals(paymentParam.getPaymentCategory())) {
// 公司支付:根据订单 trans_company_id -> biz_company.user_id -> money_account
if (ctx.getTransCompanyId() == null || ctx.getCompanyUserId() == null) {
throw Exceptions.clierr("订单未关联清运公司,无法公司支付");
}
// 根据结算方式判断是否需要检查余额
if (SettlementWay.BALANCE.getVal().equals(ctx.getSettlementWay())) {
// 余额结算:允许本次支付后余额为负数,但仅限于余额首次变为负数
if (ctx.getCompanyBalance() == null) {
ctx.setCompanyBalance(BigDecimal.ZERO);
}
if (ctx.getCompanyBalance().compareTo(BigDecimal.ZERO) < 0) {
// 已经是负数,禁止再次支付
throw Exceptions.clierr("公司账户余额不足");
}
}
// 直接扣减公司账户余额
deductCompanyBalance(ctx, ctx.getSettleMoney(), paymentParam.getOrderId());
} else if (PaymentWay.WX.getVal().equals(paymentParam.getPaymentCategory())) {
// 微信支付:创建微信支付订单
WechatPayJsapiOrderDto orderDto = new WechatPayJsapiOrderDto();
orderDto.setOutTradeNo("ORDER_" + ctx.getSn() + "_" + System.currentTimeMillis());
orderDto.setDescription("订单支付-" + ctx.getSn());
orderDto.setTotal(ctx.getSettleMoney().multiply(new BigDecimal("100")).longValue()); // 转换为分
orderDto.setOpenid(getOpenId(paymentParam.getWxCode()));
WechatPayJsapiOrderResponseDto response = wechatPayService.createMiniProgramOrder(orderDto);
// 更新订单状态为待支付
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, paymentParam.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.WeiZhiFu)
.set(OrderInfoEntity::getPaymentCategory, paymentParam.getPaymentCategory())
.set(OrderInfoEntity::getOutTradeNo, orderDto.getOutTradeNo())
.update();
log.info("微信支付订单创建成功订单ID{},微信订单号:{}", paymentParam.getOrderId(), orderDto.getOutTradeNo());
return R.success(response);
} else {
throw Exceptions.clierr("不支持的支付方式");
}
// 3. 更新订单支付状态为已支付(仅公司支付需要)
if (PaymentWay.COMPANY.getVal().equals(paymentParam.getPaymentCategory())) {
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, paymentParam.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiZhiFu)
// .set(OrderInfoEntity::getOrderStatus, OrderStatus.YiWanCheng.getVal())
.set(OrderInfoEntity::getPaymentCategory, paymentParam.getPaymentCategory())
.set(OrderInfoEntity::getPayTime, LocalDateTime.now())
.update();
log.info("订单支付成功订单ID{},支付方式:{},支付金额:{}",
paymentParam.getOrderId(), paymentParam.getPaymentCategory(), ctx.getSettleMoney());
}
return R.success();
}
/**
*
*/
private void deductCompanyBalance(PaymentContextResult ctx, BigDecimal amount, Long orderId) {
// 验证资金账户信息
if (ctx.getCompanyAccountId() == null) {
throw Exceptions.clierr("公司资金账户不存在");
}
BigDecimal oldBalance = ctx.getCompanyBalance();
BigDecimal newBalance = oldBalance.subtract(amount);
// 更新账户余额(支持负数余额)
MoneyAccountEntity companyAccount = new MoneyAccountEntity()
.setId(ctx.getCompanyAccountId())
.setMoney(newBalance);
moneyAccountService.updateById(companyAccount);
// 记录资金变动明细
MoneyChangeDetailEntity changeDetail = new MoneyChangeDetailEntity()
// .setUserId(ctx.getCompanyUserId())
.setCompanyId(ctx.getTransCompanyId())
.setOrderId(orderId)
.setMoneyAccountId(companyAccount.getId())
.setOldMoney(oldBalance)
.setDelta(amount.negate()) // 扣减为负数
.setNewMoney(newBalance)
.setMoneyChangeCategory(MoneyChangeCategory.DingDanKouKuan)
.setMemo("订单支付扣款订单ID" + orderId + ",结算方式:" + ctx.getSettlementWay());
moneyChangeDetailService.save(changeDetail);
log.info("公司账户扣减成功用户ID{},扣减金额:{},余额:{} -> {},结算方式:{}",
ctx.getCompanyUserId(), amount, oldBalance, newBalance, ctx.getSettlementWay());
}
/**
*
*/
@PostMapping("/wechat/callback")
public ResponseEntity<String> wechatPayCallback(@RequestBody WechatPayCallbackDto callbackDto) {
try {
// 验证签名
boolean isValid = wechatPayService.verifyCallback(
callbackDto.getId(),
callbackDto.getCreateTime(),
callbackDto.getEventType(),
callbackDto.getResource().getCiphertext()
);
if (!isValid) {
log.error("微信支付回调签名验证失败");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("FAIL");
}
// 处理支付结果
if ("SUCCESS".equals(callbackDto.getResource().getOriginalType())) {
// 支付成功,更新订单状态
String outTradeNo = callbackDto.getResource().getCiphertext(); // 需要解密获取
Long orderId = extractOrderIdFromOutTradeNo(outTradeNo);
if (orderId != null) {
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, orderId)
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiZhiFu)
// .set(OrderInfoEntity::getOrderStatus, OrderStatus.YiWanCheng.getVal())
.set(OrderInfoEntity::getSn, callbackDto.getResource().getCiphertext()) // 需要解密获取
.set(OrderInfoEntity::getPayTime, LocalDateTime.now())
.update();
log.info("微信支付成功订单ID{},微信交易号:{}", orderId, callbackDto.getResource().getCiphertext());
}
}
return ResponseEntity.ok("SUCCESS");
} catch (Exception e) {
log.error("处理微信支付回调异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("FAIL");
}
}
/**
*
*/
@GetMapping("/wechat/query/{outTradeNo}")
public R<?> queryWechatPayOrder(@PathVariable String outTradeNo) {
try {
WechatPayOrderQueryResponseDto result = wechatPayService.queryOrder(outTradeNo);
return R.success(result);
} catch (Exception e) {
log.error("查询微信支付订单异常,订单号:{}", outTradeNo, e);
return R.failed();
}
}
/**
* 退
*/
@PostMapping("/wechat/refund")
public R<?> wechatPayRefund(@RequestBody WechatPayRefundDto refundDto) {
try {
WechatPayRefundResponseDto result = wechatPayService.refund(refundDto);
// 更新订单退款状态
if ("SUCCESS".equals(result.getStatus())) {
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getSn, refundDto.getOutTradeNo())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiTuiKuan)
.set(OrderInfoEntity::getRefundTime, LocalDateTime.now())
.update();
log.info("微信支付退款成功,订单号:{},退款单号:{}", refundDto.getOutTradeNo(), result.getRefundId());
}
return R.success(result);
} catch (Exception e) {
log.error("微信支付退款异常,订单号:{}", refundDto.getOutTradeNo(), e);
return R.failed();
}
}
/**
* 退 - 退退
*/
@PostMapping("/refund")
public R<?> refund(@RequestBody RefundRequestDto refundRequest) {
try {
// 验证订单存在性
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())) {
// 微信退款
// return processWechatRefund(refundRequest, orderInfo, ctx);
return R.success("退款成功");
} else if (PaymentWay.COMPANY.getVal().equals(ctx.getPaymentCategory())) {
// 公司退款
return processCompanyRefund(refundRequest, ctx);
} else {
throw Exceptions.clierr("不支持的支付方式");
}
} catch (Exception e) {
log.error("退款处理异常订单ID{}", refundRequest.getOrderId(), e);
return R.failed(ExceptionMsg.CLI_ERR_MSG, "退款处理失败:" + e.getMessage());
}
}
/**
* 退
*/
private R<?> processWechatRefund(RefundRequestDto refundRequest, OrderInfoEntity orderInfo, PaymentContextResult ctx) {
if (refundRequest.getWechatRefundParams() == null) {
throw Exceptions.clierr("微信退款参数不能为空");
}
// 构建微信退款请求
WechatPayRefundDto refundDto = new WechatPayRefundDto();
// refundDto.setOutTradeNo(orderInfo.getOutTradeNo());
// refundDto.setOutRefundNo(refundRequest.getWechatRefundParams().getOutRefundNo());
// refundDto.setReason(refundRequest.getReason());
// refundDto.setRefund(orderInfo.getSettleMoney().multiply(new BigDecimal("100")).longValue()); // 转换为分
// refundDto.setTotal(orderInfo.getSettleMoney().multiply(new BigDecimal("100")).longValue()); // 转换为分
// refundDto.setFundsAccount(refundRequest.getWechatRefundParams().getFundsAccount());
// refundDto.setNotifyUrl(refundRequest.getWechatRefundParams().getNotifyUrl());
// 调用微信退款接口
WechatPayRefundResponseDto response = wechatPayService.refund(refundDto);
// 更新订单状态为已退款
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, refundRequest.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiTuiKuan)
.set(OrderInfoEntity::getRefundTime, LocalDateTime.now())
.update();
log.info("微信退款成功订单ID{},退款单号:{}", refundRequest.getOrderId(), refundDto.getOutRefundNo());
return R.success(response);
}
/**
* 退
*/
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("退款成功");
}
/**
*
*/
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);
}
/**
* ID
*/
private Long extractOrderIdFromOutTradeNo(String outTradeNo) {
try {
if (outTradeNo != null && outTradeNo.startsWith("ORDER_")) {
String[] parts = outTradeNo.split("_");
if (parts.length >= 2) {
return Long.parseLong(parts[1]);
}
}
} catch (Exception e) {
log.error("解析订单号失败:{}", outTradeNo, e);
}
return null;
}
public String getOpenId(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();
}
}

View File

@ -1,239 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
/**
* DTO
*/
@Data
public class WechatPayCallbackDto {
/**
* ID
*/
private String id;
/**
*
*/
private String createTime;
/**
*
*/
private String eventType;
/**
*
*/
private String resourceType;
/**
*
*/
private ResourceData resource;
/**
*
*/
@Data
public static class ResourceData {
/**
*
*/
private String algorithm;
/**
*
*/
private String ciphertext;
/**
*
*/
private String associatedData;
/**
*
*/
private String originalType;
/**
*
*/
private String nonce;
}
/**
*
*/
@Data
public static class PaymentInfo {
/**
* APPID
*/
private String spAppid;
/**
*
*/
private String spMchid;
/**
* APPID
*/
private String subAppid;
/**
*
*/
private String subMchid;
/**
*
*/
private String outTradeNo;
/**
*
*/
private String transactionId;
/**
*
*/
private String tradeType;
/**
*
*/
private String tradeState;
/**
*
*/
private String tradeStateDesc;
/**
*
*/
private String bankType;
/**
*
*/
private String successTime;
/**
*
*/
private PayerInfo payer;
/**
*
*/
private AmountInfo amount;
/**
*
*/
private SceneInfo sceneInfo;
/**
*
*/
private PromotionDetail promotionDetail;
}
@Data
public static class PayerInfo {
/**
* openid
*/
private String openid;
}
@Data
public static class AmountInfo {
/**
*
*/
private Long total;
/**
*
*/
private Long payerTotal;
/**
*
*/
private String currency;
/**
*
*/
private String payerCurrency;
}
@Data
public static class SceneInfo {
/**
*
*/
private String deviceId;
}
@Data
public static class PromotionDetail {
/**
* ID
*/
private String couponId;
/**
*
*/
private String name;
/**
*
*/
private String scope;
/**
*
*/
private String type;
/**
*
*/
private Long amount;
/**
* ID
*/
private String stockId;
/**
*
*/
private Long wechatpayContribute;
/**
*
*/
private Long merchantContribute;
/**
*
*/
private Long otherContribute;
/**
*
*/
private String currency;
}
}

View File

@ -1,177 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
/**
* DTO
*/
@Data
public class WechatPayOrderQueryResponseDto {
/**
* APPID
*/
private String spAppid;
/**
*
*/
private String spMchid;
/**
* APPID
*/
private String subAppid;
/**
*
*/
private String subMchid;
/**
*
*/
private String outTradeNo;
/**
*
*/
private String transactionId;
/**
*
*/
private String tradeType;
/**
*
*/
private String tradeState;
/**
*
*/
private String tradeStateDesc;
/**
*
*/
private String bankType;
/**
*
*/
private String successTime;
/**
*
*/
private PayerInfo payer;
/**
*
*/
private AmountInfo amount;
/**
*
*/
private SceneInfo sceneInfo;
/**
*
*/
private PromotionDetail promotionDetail;
@Data
public static class PayerInfo {
/**
* openid
*/
private String openid;
}
@Data
public static class AmountInfo {
/**
*
*/
private Long total;
/**
*
*/
private Long payerTotal;
/**
*
*/
private String currency;
/**
*
*/
private String payerCurrency;
}
@Data
public static class SceneInfo {
/**
*
*/
private String deviceId;
}
@Data
public static class PromotionDetail {
/**
* ID
*/
private String couponId;
/**
*
*/
private String name;
/**
*
*/
private String scope;
/**
*
*/
private String type;
/**
*
*/
private Long amount;
/**
* ID
*/
private String stockId;
/**
*
*/
private Long wechatpayContribute;
/**
*
*/
private Long merchantContribute;
/**
*
*/
private Long otherContribute;
/**
*
*/
private String currency;
}
}

View File

@ -1,213 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
/**
* 退DTO
*/
@Data
public class WechatPayRefundCallbackDto {
/**
* ID
*/
private String id;
/**
*
*/
private String createTime;
/**
*
*/
private String eventType;
/**
*
*/
private String resourceType;
/**
*
*/
private ResourceData resource;
/**
*
*/
@Data
public static class ResourceData {
/**
*
*/
private String algorithm;
/**
*
*/
private String ciphertext;
/**
*
*/
private String associatedData;
/**
*
*/
private String originalType;
/**
*
*/
private String nonce;
}
/**
* 退
*/
@Data
public static class RefundInfo {
/**
* APPID
*/
private String spAppid;
/**
*
*/
private String spMchid;
/**
* APPID
*/
private String subAppid;
/**
*
*/
private String subMchid;
/**
*
*/
private String outTradeNo;
/**
*
*/
private String transactionId;
/**
* 退
*/
private String outRefundNo;
/**
* 退
*/
private String refundId;
/**
* 退
*/
private String refundStatus;
/**
* 退
*/
private String successTime;
/**
* 退
*/
private String userReceivedAccount;
/**
* 退
*/
private RefundAmount amount;
/**
* 退
*/
private PromotionDetail promotionDetail;
}
@Data
public static class RefundAmount {
/**
*
*/
private Long total;
/**
* 退
*/
private Long refund;
/**
*
*/
private Long payerTotal;
/**
* 退
*/
private Long payerRefund;
}
@Data
public static class PromotionDetail {
/**
* ID
*/
private String couponId;
/**
*
*/
private String name;
/**
*
*/
private String scope;
/**
*
*/
private String type;
/**
*
*/
private Long amount;
/**
* ID
*/
private String stockId;
/**
*
*/
private Long wechatpayContribute;
/**
*
*/
private Long merchantContribute;
/**
*
*/
private Long otherContribute;
/**
*
*/
private String currency;
}
}

View File

@ -1,62 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 退DTO
*/
@Data
public class WechatPayRefundDto {
/**
*
*/
@NotBlank(message = "商户订单号不能为空")
@Size(max = 32, message = "商户订单号不能超过32个字符")
private String outTradeNo;
/**
*
*/
@Size(max = 32, message = "微信支付订单号不能超过32个字符")
private String transactionId;
/**
* 退
*/
@NotBlank(message = "商户退款单号不能为空")
@Size(max = 64, message = "商户退款单号不能超过64个字符")
private String outRefundNo;
/**
* 退
*/
@Size(max = 80, message = "退款原因不能超过80个字符")
private String reason;
/**
* 退
*/
@NotNull(message = "退款金额不能为空")
private Long refund;
/**
*
*/
@NotNull(message = "原订单金额不能为空")
private Long total;
/**
* 退
*/
private String fundsAccount;
/**
* 退
*/
private String notifyUrl;
}

View File

@ -1,126 +0,0 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
/**
* 退DTO
*/
@Data
public class WechatPayRefundResponseDto {
/**
* 退
*/
private String refundId;
/**
* 退
*/
private String outRefundNo;
/**
* 退
*/
private String status;
/**
* 退
*/
private String userReceivedAccount;
/**
* 退
*/
private String successTime;
/**
* 退
*/
private String createTime;
/**
* 退
*/
private RefundAmount amount;
/**
* 退
*/
private PromotionDetail promotionDetail;
@Data
public static class RefundAmount {
/**
*
*/
private Long total;
/**
* 退
*/
private Long refund;
/**
*
*/
private Long payerTotal;
/**
* 退
*/
private Long payerRefund;
}
@Data
public static class PromotionDetail {
/**
* ID
*/
private String couponId;
/**
*
*/
private String name;
/**
*
*/
private String scope;
/**
*
*/
private String type;
/**
*
*/
private Long amount;
/**
* ID
*/
private String stockId;
/**
*
*/
private Long wechatpayContribute;
/**
*
*/
private Long merchantContribute;
/**
*
*/
private Long otherContribute;
/**
*
*/
private String currency;
}
}

View File

@ -1,75 +0,0 @@
package com.njzscloud.supervisory.order.service;
import com.njzscloud.supervisory.order.pojo.dto.*;
/**
*
*/
public interface WechatPayService {
/**
*
* @param orderDto
* @return
*/
WechatPayJsapiOrderResponseDto createMiniProgramOrder(WechatPayJsapiOrderDto orderDto);
/**
* JSAPI
* @param orderDto
* @return
*/
WechatPayJsapiOrderResponseDto createJsapiOrder(WechatPayJsapiOrderDto orderDto);
/**
*
* @param outTradeNo
* @return
*/
WechatPayOrderQueryResponseDto queryOrder(String outTradeNo);
/**
*
* @param outTradeNo
* @return
*/
boolean closeOrder(String outTradeNo);
/**
* 退
* @param refundDto 退
* @return 退
*/
WechatPayRefundResponseDto refund(WechatPayRefundDto refundDto);
/**
* 退
* @param outRefundNo 退
* @return 退
*/
WechatPayRefundResponseDto queryRefund(String outRefundNo);
/**
*
* @param callbackDto
* @return
*/
String handlePaymentCallback(WechatPayCallbackDto callbackDto);
/**
* 退
* @param callbackDto
* @return
*/
String handleRefundCallback(WechatPayRefundCallbackDto callbackDto);
/**
*
* @param timestamp
* @param nonce
* @param body
* @param signature
* @return
*/
boolean verifyCallback(String timestamp, String nonce, String body, String signature);
}

View File

@ -1,350 +0,0 @@
package com.njzscloud.supervisory.order.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njzscloud.supervisory.order.config.WechatPayConfig;
import com.njzscloud.supervisory.order.pojo.dto.*;
import com.njzscloud.supervisory.order.service.WechatPayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.*;
/**
*
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatPayServiceImpl implements WechatPayService {
private final WechatPayConfig wechatPayConfig;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private static final String WECHAT_PAY_API_BASE = "https://api.mch.weixin.qq.com";
private static final String JSAPI_ORDER_URL = WECHAT_PAY_API_BASE + "/v3/pay/transactions/jsapi";
private static final String ORDER_QUERY_URL = WECHAT_PAY_API_BASE + "/v3/pay/transactions/out-trade-no/";
private static final String ORDER_CLOSE_URL = WECHAT_PAY_API_BASE + "/v3/pay/transactions/out-trade-no/";
private static final String REFUND_URL = WECHAT_PAY_API_BASE + "/v3/refund/domestic/refunds";
private static final String REFUND_QUERY_URL = WECHAT_PAY_API_BASE + "/v3/refund/domestic/refunds/";
@Override
public WechatPayJsapiOrderResponseDto createMiniProgramOrder(WechatPayJsapiOrderDto orderDto) {
return createJsapiOrder(orderDto);
}
@Override
public WechatPayJsapiOrderResponseDto createJsapiOrder(WechatPayJsapiOrderDto orderDto) {
try {
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("appid", wechatPayConfig.getSubAppId());
requestBody.put("mchid", wechatPayConfig.getSubMchId());
requestBody.put("description", orderDto.getDescription());
requestBody.put("out_trade_no", orderDto.getOutTradeNo());
requestBody.put("notify_url", wechatPayConfig.getNotifyUrl());
// 金额信息
Map<String, Object> amount = new HashMap<>();
amount.put("total", orderDto.getTotal());
amount.put("currency", orderDto.getCurrency());
requestBody.put("amount", amount);
// 支付者信息
Map<String, Object> payer = new HashMap<>();
payer.put("openid", orderDto.getOpenid());
requestBody.put("payer", payer);
// 发送请求
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(requestBody), headers);
ResponseEntity<WechatPayJsapiOrderResponseDto> response = restTemplate.exchange(
JSAPI_ORDER_URL,
HttpMethod.POST,
entity,
WechatPayJsapiOrderResponseDto.class
);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
WechatPayJsapiOrderResponseDto responseDto = response.getBody();
// 生成小程序支付参数
responseDto.setPaySign(generateMiniProgramPaySign(responseDto));
return responseDto;
}
throw new RuntimeException("创建订单失败");
} catch (Exception e) {
log.error("创建JSAPI订单失败", e);
throw new RuntimeException("创建订单失败: " + e.getMessage());
}
}
@Override
public WechatPayOrderQueryResponseDto queryOrder(String outTradeNo) {
try {
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<WechatPayOrderQueryResponseDto> response = restTemplate.exchange(
ORDER_QUERY_URL + outTradeNo + "?mchid=" + wechatPayConfig.getSubMchId(),
HttpMethod.GET,
entity,
WechatPayOrderQueryResponseDto.class
);
return response.getBody();
} catch (Exception e) {
log.error("查询订单失败", e);
throw new RuntimeException("查询订单失败: " + e.getMessage());
}
}
@Override
public boolean closeOrder(String outTradeNo) {
try {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("mchid", wechatPayConfig.getSubMchId());
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(requestBody), headers);
ResponseEntity<Void> response = restTemplate.exchange(
ORDER_CLOSE_URL + outTradeNo + "/close",
HttpMethod.POST,
entity,
Void.class
);
return response.getStatusCode() == HttpStatus.NO_CONTENT;
} catch (Exception e) {
log.error("关闭订单失败", e);
return false;
}
}
@Override
public WechatPayRefundResponseDto refund(WechatPayRefundDto refundDto) {
try {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("out_trade_no", refundDto.getOutTradeNo());
requestBody.put("out_refund_no", refundDto.getOutRefundNo());
requestBody.put("reason", refundDto.getReason());
requestBody.put("notify_url", wechatPayConfig.getRefundNotifyUrl());
// 金额信息
Map<String, Object> amount = new HashMap<>();
amount.put("refund", refundDto.getRefund());
amount.put("total", refundDto.getTotal());
amount.put("currency", "CNY");
requestBody.put("amount", amount);
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(requestBody), headers);
ResponseEntity<WechatPayRefundResponseDto> response = restTemplate.exchange(
REFUND_URL,
HttpMethod.POST,
entity,
WechatPayRefundResponseDto.class
);
return response.getBody();
} catch (Exception e) {
log.error("申请退款失败", e);
throw new RuntimeException("申请退款失败: " + e.getMessage());
}
}
@Override
public WechatPayRefundResponseDto queryRefund(String outRefundNo) {
try {
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<WechatPayRefundResponseDto> response = restTemplate.exchange(
REFUND_QUERY_URL + outRefundNo,
HttpMethod.GET,
entity,
WechatPayRefundResponseDto.class
);
return response.getBody();
} catch (Exception e) {
log.error("查询退款失败", e);
throw new RuntimeException("查询退款失败: " + e.getMessage());
}
}
@Override
public String handlePaymentCallback(WechatPayCallbackDto callbackDto) {
try {
// 解密回调数据
WechatPayCallbackDto.PaymentInfo paymentInfo = decryptCallbackData(
callbackDto.getResource().getCiphertext(),
callbackDto.getResource().getAssociatedData(),
callbackDto.getResource().getNonce()
);
// 处理支付结果
if ("SUCCESS".equals(paymentInfo.getTradeState())) {
// 支付成功,更新订单状态
log.info("支付成功,订单号: {}, 微信支付单号: {}",
paymentInfo.getOutTradeNo(), paymentInfo.getTransactionId());
// TODO: 更新订单状态到数据库
}
return "SUCCESS";
} catch (Exception e) {
log.error("处理支付回调失败", e);
return "FAIL";
}
}
@Override
public String handleRefundCallback(WechatPayRefundCallbackDto callbackDto) {
try {
// 解密回调数据
WechatPayRefundCallbackDto.RefundInfo refundInfo = decryptRefundCallbackData(
callbackDto.getResource().getCiphertext(),
callbackDto.getResource().getAssociatedData(),
callbackDto.getResource().getNonce()
);
// 处理退款结果
if ("SUCCESS".equals(refundInfo.getRefundStatus())) {
// 退款成功,更新订单状态
log.info("退款成功,退款单号: {}, 微信退款单号: {}",
refundInfo.getOutRefundNo(), refundInfo.getRefundId());
// TODO: 更新订单状态到数据库
}
return "SUCCESS";
} catch (Exception e) {
log.error("处理退款回调失败", e);
return "FAIL";
}
}
@Override
public boolean verifyCallback(String timestamp, String nonce, String body, String signature) {
try {
String message = timestamp + "\n" + nonce + "\n" + body + "\n";
String expectedSignature = generateSignature(message);
return expectedSignature.equals(signature);
} catch (Exception e) {
log.error("验证回调签名失败", e);
return false;
}
}
/**
*
*/
private HttpHeaders createHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "WECHATPAY2-SHA256-RSA2048 " + generateAuthorization());
headers.set("Accept", "application/json");
return headers;
}
/**
*
*/
private String generateAuthorization() {
// TODO: 实现微信支付签名生成逻辑
return "mock_authorization";
}
/**
*
*/
private String generateMiniProgramPaySign(WechatPayJsapiOrderResponseDto responseDto) {
try {
Map<String, String> params = new HashMap<>();
params.put("appId", wechatPayConfig.getSubAppId());
params.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("nonceStr", generateNonceStr());
params.put("package", "prepay_id=" + responseDto.getPrepayId());
params.put("signType", "RSA");
// 生成签名
String sign = generateSignature(buildSignString(params));
params.put("paySign", sign);
return objectMapper.writeValueAsString(params);
} catch (Exception e) {
log.error("生成小程序支付签名失败", e);
throw new RuntimeException("生成支付签名失败");
}
}
/**
*
*/
private String generateSignature(String message) {
try {
// TODO: 使用商户私钥生成RSA签名
return "mock_signature";
} catch (Exception e) {
log.error("生成签名失败", e);
throw new RuntimeException("生成签名失败");
}
}
/**
*
*/
private String buildSignString(Map<String, String> params) {
return params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.reduce((a, b) -> a + "&" + b)
.orElse("");
}
/**
*
*/
private String generateNonceStr() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
*
*/
private WechatPayCallbackDto.PaymentInfo decryptCallbackData(String ciphertext, String associatedData, String nonce) {
try {
// TODO: 实现AES-256-GCM解密
return new WechatPayCallbackDto.PaymentInfo();
} catch (Exception e) {
log.error("解密回调数据失败", e);
throw new RuntimeException("解密回调数据失败");
}
}
/**
* 退
*/
private WechatPayRefundCallbackDto.RefundInfo decryptRefundCallbackData(String ciphertext, String associatedData, String nonce) {
try {
// TODO: 实现AES-256-GCM解密
return new WechatPayRefundCallbackDto.RefundInfo();
} catch (Exception e) {
log.error("解密退款回调数据失败", e);
throw new RuntimeException("解密退款回调数据失败");
}
}
}

View File

@ -0,0 +1,36 @@
package com.njzscloud.supervisory.wxPay.config;
import com.github.binarywang.wxpay.config.WxPayConfig;
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.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;
@Configuration
@ConditionalOnClass(WxPayService.class)
@EnableConfigurationProperties(WxPayProperties.class)
@AllArgsConstructor
public class WxPayConfiguration {
private final WxPayProperties properties;
@Bean
@ConditionalOnMissingBean
public WxPayService wxService() {
WxPayConfig config = new WxPayConfig();
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()));
// 可以指定是否使用沙箱环境
// config.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(config);
return wxPayService;
}
}

View File

@ -1,26 +1,24 @@
package com.njzscloud.supervisory.order.config;
package com.njzscloud.supervisory.wxPay.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
*
*/
@Data
@Component
@ConfigurationProperties(prefix = "wechat.pay")
public class WechatPayConfig {
public class WxPayProperties {
/**
* APPID
* APPID
*/
private String subAppId;
private String appId;
/**
*
*
*/
private String subMchId;
private String mchId;
/**
* API
@ -37,6 +35,10 @@ public class WechatPayConfig {
*/
private String privateKeyPath;
private String privateCertPath;
private String privateCertP12Path;
/**
*
*/
@ -46,4 +48,5 @@ public class WechatPayConfig {
* 退
*/
private String refundNotifyUrl;
}

View File

@ -0,0 +1,312 @@
package com.njzscloud.supervisory.wxPay.controller;
import com.google.common.base.Strings;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.njzscloud.common.core.ex.Exceptions;
import com.njzscloud.common.core.utils.R;
import com.njzscloud.supervisory.money.contant.MoneyChangeCategory;
import com.njzscloud.supervisory.money.pojo.entity.MoneyAccountEntity;
import com.njzscloud.supervisory.money.pojo.entity.MoneyChangeDetailEntity;
import com.njzscloud.supervisory.money.service.MoneyAccountService;
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.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.order.pojo.result.PaymentContextResult;
import com.njzscloud.supervisory.order.service.OrderExpenseItemsService;
import com.njzscloud.supervisory.order.service.OrderInfoService;
import com.njzscloud.supervisory.wxPay.service.WeChatPayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
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;
/**
*
* 使SDK
*/
@Slf4j
@RestController
@RequestMapping("/payment")
@RequiredArgsConstructor
public class PaymentController {
private final OrderExpenseItemsService orderExpenseItemsService;
private final OrderInfoService orderInfoService;
private final MoneyAccountService moneyAccountService;
private final MoneyChangeDetailService moneyChangeDetailService;
private final WeChatPayService wechatPayService;
/**
* 使SDK
*/
@PostMapping("/pay")
@Transactional(rollbackFor = Exception.class)
public R<?> pay(@RequestBody PaymentParam paymentParam) {
// 入参校验支付方式、订单ID、支付清单不能为空
if (paymentParam == null
|| paymentParam.getOrderId() == null
|| Strings.isNullOrEmpty(paymentParam.getPaymentCategory())
|| paymentParam.getSettleTotalMoney() == null
|| paymentParam.getSettleTotalMoney().signum() < 0
|| paymentParam.getItems() == null) {
throw Exceptions.clierr("参数不完整或支付总金额小于0");
}
// 校验支付清单id、名称、金额
boolean invalidItem = paymentParam.getItems().stream().anyMatch(it ->
it == null
|| it.getId() == null
|| it.getExpenseItemName() == null || it.getExpenseItemName().length() == 0
|| it.getSettleMoney() == null || it.getSettleMoney().signum() < 0
);
if (invalidItem) {
throw Exceptions.clierr("支付清单项不合法id/名称/金额)");
}
// 校验支付方式
if (paymentParam.getPaymentCategory() == null ||
!(PaymentWay.WX.getVal().equals(paymentParam.getPaymentCategory()) ||
PaymentWay.COMPANY.getVal().equals(paymentParam.getPaymentCategory()))) {
throw Exceptions.clierr("支付方式不合法");
}
// 校验数据库中结算金额是否与请求一致
for (PaymentItemParam it : paymentParam.getItems()) {
OrderExpenseItemsEntity entity = orderExpenseItemsService.getById(it.getId());
if (null == entity) {
throw Exceptions.clierr("支付清单项不存在id=" + it.getId());
}
BigDecimal dbSettle = entity.getSettleMoney();
if (dbSettle == null || dbSettle.compareTo(it.getSettleMoney()) != 0) {
throw Exceptions.clierr("支付清单项结算金额不匹配【" + it.getExpenseItemName() + "】(id=" + it.getId()
+ "),系统:" + dbSettle + ",请求:" + it.getSettleMoney());
}
}
// 查询订单及相关支付上下文
PaymentContextResult ctx = orderInfoService.paymentContext(paymentParam.getOrderId());
if (ctx == null || ctx.getOrderId() == null) {
throw Exceptions.clierr("订单不存在");
}
// 验证总金额是否与参数中的结算总金额一致
if (paymentParam.getSettleTotalMoney() == null ||
paymentParam.getSettleTotalMoney().compareTo(ctx.getSettleMoney()) != 0) {
throw Exceptions.clierr("结算总金额与实际总金额不一致");
}
// 根据支付方式处理
if (PaymentWay.COMPANY.getVal().equals(paymentParam.getPaymentCategory())) {
// 公司支付:根据订单 trans_company_id -> biz_company.user_id -> money_account
if (null == ctx.getTransCompanyId()) {
throw Exceptions.clierr("订单未关联清运公司,无法公司支付");
}
// 根据结算方式判断是否需要检查余额
if (SettlementWay.BALANCE.getVal().equals(ctx.getSettlementWay())) {
// 余额结算:允许本次支付后余额为负数,但仅限于余额首次变为负数
if (ctx.getCompanyBalance() == null) {
ctx.setCompanyBalance(BigDecimal.ZERO);
}
if (ctx.getCompanyBalance().compareTo(BigDecimal.ZERO) < 0) {
// 已经是负数,禁止再次支付
throw Exceptions.clierr("公司账户余额不足");
}
}
handleCompanyPay(ctx, ctx.getSettleMoney(), paymentParam.getOrderId());
// 3. 更新订单支付状态为已支付
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, paymentParam.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.YiZhiFu)
.set(OrderInfoEntity::getPaymentCategory, paymentParam.getPaymentCategory())
.set(OrderInfoEntity::getPayTime, LocalDateTime.now())
.update();
log.info("订单支付成功订单ID{},支付方式:{},支付金额:{}",
paymentParam.getOrderId(), paymentParam.getPaymentCategory(), ctx.getSettleMoney());
} else if (PaymentWay.WX.getVal().equals(paymentParam.getPaymentCategory())) {
try {
// 构建微信支付请求
String outTradeNo = generateOutTradeNo(ctx.getSn());
WxPayUnifiedOrderRequest wxRequest = new WxPayUnifiedOrderRequest();
wxRequest.setOutTradeNo(outTradeNo);
wxRequest.setBody("订单支付-" + ctx.getSn());
wxRequest.setTotalFee(ctx.getSettleMoney().multiply(new BigDecimal("100")).intValue()); // 转换为分
wxRequest.setOpenid(getCurrentUserOpenid());
wxRequest.setTradeType("JSAPI");
wxRequest.setNotifyUrl("https://your-domain.com/payment/wechat/notify"); // 需要配置实际的回调地址
// 调用微信支付服务
WechatPayJsapiOrderResponseDto response = wechatPayService.createJsapiOrder(wxRequest);
// 更新订单状态为待支付
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, paymentParam.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.WeiZhiFu)
.set(OrderInfoEntity::getPaymentCategory, paymentParam.getPaymentCategory())
.set(OrderInfoEntity::getOutTradeNo, outTradeNo)
.update();
log.info("微信支付订单创建成功生产级SDK订单ID{},微信订单号:{}", paymentParam.getOrderId(), outTradeNo);
return R.success(response);
} catch (Exception e) {
log.error("微信支付订单创建失败订单ID{},错误信息:{}", paymentParam.getOrderId(), e.getMessage(), e);
throw Exceptions.clierr("微信支付订单创建失败:" + e.getMessage());
}
}
return R.success();
}
/**
*
*/
public void handleCompanyPay(PaymentContextResult ctx, BigDecimal amount, Long orderId) {
// 验证资金账户信息
if (ctx.getCompanyAccountId() == null) {
throw Exceptions.clierr("公司资金账户不存在");
}
BigDecimal oldBalance = ctx.getCompanyBalance();
BigDecimal newBalance = oldBalance.subtract(amount);
// 更新账户余额(支持负数余额)
MoneyAccountEntity companyAccount = new MoneyAccountEntity()
.setId(ctx.getCompanyAccountId())
.setMoney(newBalance);
moneyAccountService.updateById(companyAccount);
// 记录资金变动明细
MoneyChangeDetailEntity changeDetail = new MoneyChangeDetailEntity()
// .setUserId(ctx.getCompanyUserId())
.setCompanyId(ctx.getTransCompanyId())
.setOrderId(orderId)
.setMoneyAccountId(companyAccount.getId())
.setOldMoney(oldBalance)
.setDelta(amount.negate()) // 扣减为负数
.setNewMoney(newBalance)
.setMoneyChangeCategory(MoneyChangeCategory.DingDanKouKuan)
.setMemo("订单支付扣款订单ID" + orderId + ",结算方式:" + ctx.getSettlementWay());
moneyChangeDetailService.save(changeDetail);
log.info("公司账户扣减成功用户ID{},扣减金额:{},余额:{} -> {},结算方式:{}",
ctx.getCompanyUserId(), amount, oldBalance, newBalance, ctx.getSettlementWay());
}
/**
* openid
*/
public String getCurrentUserOpenid() {
// 这里需要根据实际业务逻辑获取当前用户的openid
// 可能通过JWT token、session等方式获取
return "test_openid"; // 临时返回测试值
}
/**
* <=32-_-
*/
private String generateOutTradeNo(String sn) {
String safeSn = sn == null ? "" : sn.replaceAll("[^0-9A-Za-z_-]", "");
String suffix = String.valueOf(System.currentTimeMillis() % 100000000L); // 8位以内
String base = "ORDER_" + safeSn + "_" + suffix;
if (base.length() <= 32) {
return base;
}
// 超长则从左侧截断仅保留最后32位仍保持后缀时间以提高唯一性
return base.substring(base.length() - 32);
}
/**
* ID
*/
private Long extractOrderIdFromOutTradeNo(String outTradeNo) {
try {
if (outTradeNo != null && outTradeNo.startsWith("ORDER_")) {
String[] parts = outTradeNo.split("_");
if (parts.length >= 2) {
return Long.parseLong(parts[1]);
}
}
} catch (Exception e) {
log.error("解析订单号失败:{}", outTradeNo, e);
}
return null;
}
/**
*
*/
@PostMapping("/wechat/notify")
public String wechatPayNotify(@RequestParam 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>";
}
}
/**
*
*/
@PostMapping("/wechat/query")
public R<?> queryWechatOrder(@RequestParam String outTradeNo) {
try {
WxPayOrderQueryResult result = wechatPayService.queryOrder(outTradeNo);
return R.success(result);
} catch (Exception e) {
log.error("查询微信支付订单失败,订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
return R.failed();
}
}
/**
*
*/
@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();
}
}
/**
* 退
*/
@PostMapping("/wechat/refund")
public R<?> refundWechatOrder(@RequestParam String outTradeNo,
@RequestParam String outRefundNo,
@RequestParam int totalFee,
@RequestParam int refundFee,
@RequestParam(required = false) String refundDesc) {
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();
}
}
}

View File

@ -1,4 +1,4 @@
package com.njzscloud.supervisory.order.pojo.dto;
package com.njzscloud.supervisory.wxPay.dto;
import lombok.Data;

View File

@ -1,5 +1,6 @@
package com.njzscloud.supervisory.order.pojo.dto;
package com.njzscloud.supervisory.wxPay.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
@ -7,10 +8,13 @@ import lombok.Data;
*/
@Data
public class WechatPayJsapiOrderResponseDto {
private String appId;
/**
*
*/
@JsonProperty("prepay_id")
private String prepayId;
/**
@ -26,6 +30,7 @@ public class WechatPayJsapiOrderResponseDto {
/**
*
*/
@JsonProperty("package")
private String packageValue;
/**
@ -37,4 +42,5 @@ public class WechatPayJsapiOrderResponseDto {
*
*/
private String paySign;
}

View File

@ -0,0 +1,67 @@
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;
/**
*
*/
public interface WeChatPayService {
/**
* JSAPI
*
* @param request
* @return
*/
WechatPayJsapiOrderResponseDto createJsapiOrder(WxPayUnifiedOrderRequest request) throws WxPayException;
/**
*
*
* @param outTradeNo
* @return
* @throws WxPayException
*/
WxPayOrderQueryResult queryOrder(String outTradeNo) throws WxPayException;
/**
*
*
* @param outTradeNo
* @throws WxPayException
*/
void closeOrder(String outTradeNo) throws WxPayException;
/**
* 退
*
* @param outTradeNo
* @param outRefundNo 退
* @param totalFee
* @param refundFee 退
* @param refundDesc 退
* @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);
}

View File

@ -0,0 +1,290 @@
package com.njzscloud.supervisory.wxPay.service.impl;
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;
/**
*
*/
@Slf4j
@Service
@RequiredArgsConstructor
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);
}
}
/**
*
*
* @param outTradeNo
* @return
* @throws WxPayException
*/
public WxPayOrderQueryResult queryOrder(String outTradeNo) throws WxPayException {
try {
log.info("开始查询微信支付订单,商户订单号:{}", outTradeNo);
return wxPayService.queryOrder(null, outTradeNo);
} catch (WxPayException e) {
log.error("查询微信支付订单失败,商户订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
throw e;
}
}
/**
*
*
* @param outTradeNo
* @throws WxPayException
*/
public void closeOrder(String outTradeNo) throws WxPayException {
try {
log.info("开始关闭微信支付订单,商户订单号:{}", outTradeNo);
wxPayService.closeOrder(outTradeNo);
log.info("微信支付订单关闭成功,商户订单号:{}", outTradeNo);
} catch (WxPayException e) {
log.error("关闭微信支付订单失败,商户订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
throw e;
}
}
/**
* 退
*
* @param outTradeNo
* @param outRefundNo 退
* @param totalFee
* @param refundFee 退
* @param refundDesc 退
* @return 退
* @throws WxPayException
*/
public String refund(String outTradeNo, String outRefundNo, int totalFee, int refundFee, String refundDesc) throws WxPayException {
try {
log.info("开始申请微信支付退款,商户订单号:{},退款单号:{},退款金额:{}", outTradeNo, outRefundNo, refundFee);
WxPayRefundRequest refundRequest = WxPayRefundRequest.newBuilder()
.outTradeNo(outTradeNo)
.outRefundNo(outRefundNo)
.totalFee(totalFee)
.refundFee(refundFee)
.refundDesc(refundDesc)
.build();
WxPayRefundResult refundResult = wxPayService.refund(refundRequest);
return refundResult.getRefundId();
} catch (WxPayException e) {
log.error("申请微信支付退款失败,商户订单号:{},错误信息:{}", outTradeNo, e.getMessage(), e);
throw e;
}
}
/**
*
*
*
* 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

@ -23,6 +23,7 @@ spring:
- /bulletin/paging
- /truck_location_track/**
- /fdx
- /payment/wechat/callback
app:
default-place:
province: 320000
@ -63,20 +64,20 @@ wechat:
app-secret: 66c98dc487a372acb4f1931b38fee8ff
base-url: https://api.weixin.qq.com
pay:
# 子商户配置
# sub-app-id: wx3c06d9dd4e56c58d
sub-app-id: wx989ea47a5ddf9bfb
sub-mch-id: 1729703110
app-id: wx989ea47a5ddf9bfb
mch-id: 1729703110
# API密钥32位字符串
api-key: KXM36nZCXji1sQt75tGk77k7b2K5RBpf
# api-key: 66c98dc487a372acb4f1931b38fee8ff
# 证书序列号
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
# 支付回调地址
notify-url: https://your-domain.com/api/payment/wechat/callback
# 退款回调地址
refund-notify-url: https://your-domain.com/api/payment/wechat/refundCallback
mqtt:
enabled: true
broker: tcp://139.224.54.144:1883

View File

@ -0,0 +1,25 @@
-----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-----