支付相关
parent
8ae65e6d55
commit
c8ab79494a
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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("解密退款回调数据失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.njzscloud.supervisory.order.pojo.dto;
|
||||
package com.njzscloud.supervisory.wxPay.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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=value的格式,并按照参数名ASCII字典序排序
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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-----
|
||||
Loading…
Reference in New Issue