微信支付

localizer
ljw 2025-09-27 16:26:14 +08:00
parent 8c34ebb528
commit 99c86f5d2e
20 changed files with 2032 additions and 14 deletions

View File

@ -0,0 +1,295 @@
# PaymentController 接口文档
## 概述
PaymentController 是支付相关的控制器,提供订单支付、微信支付、退款等功能。
## 基础信息
- **基础路径**: `/payment`
- **控制器**: `PaymentController`
- **包路径**: `com.njzscloud.supervisory.order.controller`
---
## 接口列表
### 1. 发起支付
**接口路径**: `POST /payment/pay`
**功能描述**: 发起订单支付,支持公司支付和微信支付两种方式
**请求参数**:
```json
{
"orderId": 123456,
"paymentCategory": "company|wx",
"settleTotalMoney": 100.00,
"items": [
{
"id": 1,
"expenseItemName": "运费",
"settleMoney": 50.00
}
]
}
```
**参数说明**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| orderId | Long | 是 | 订单ID |
| paymentCategory | String | 是 | 支付方式company(公司支付)、wx(微信支付) |
| settleTotalMoney | BigDecimal | 是 | 结算总金额 |
| items | Array | 是 | 支付清单项 |
| items[].id | Long | 是 | 费用项ID |
| items[].expenseItemName | String | 是 | 费用项名称 |
| items[].settleMoney | BigDecimal | 是 | 结算金额 |
**响应示例**:
```json
{
"code": 0,
"success": true,
"msg": "成功",
"data": {
"prepayId": "wx123456789",
"timeStamp": "1234567890",
"nonceStr": "abc123",
"package": "prepay_id=wx123456789",
"signType": "RSA",
"paySign": "signature"
}
}
```
**错误码**:
- 参数不完整或支付总金额小于0
- 支付清单项不合法
- 支付方式不合法
- 订单不存在
- 公司账户余额不足
---
### 2. 微信支付回调
**接口路径**: `POST /payment/wechat/callback`
**功能描述**: 微信支付结果回调接口
**请求参数**:
```json
{
"id": "event_id",
"createTime": "2023-01-01T00:00:00+08:00",
"eventType": "TRANSACTION.SUCCESS",
"resource": {
"originalType": "transaction",
"ciphertext": "encrypted_data"
}
}
```
**响应示例**:
```
SUCCESS
```
**说明**: 此接口由微信支付平台调用,用于通知支付结果
---
### 3. 查询微信支付订单状态
**接口路径**: `GET /payment/wechat/query/{outTradeNo}`
**功能描述**: 查询微信支付订单的支付状态
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| outTradeNo | String | 是 | 商户订单号 |
**响应示例**:
```json
{
"code": 0,
"success": true,
"msg": "成功",
"data": {
"tradeState": "SUCCESS",
"tradeStateDesc": "支付成功",
"transactionId": "wx123456789",
"outTradeNo": "ORDER_123_1234567890"
}
}
```
---
### 4. 申请微信支付退款
**接口路径**: `POST /payment/wechat/refund`
**功能描述**: 申请微信支付退款
**请求参数**:
```json
{
"outTradeNo": "ORDER_123_1234567890",
"outRefundNo": "REFUND_123_1234567890",
"total": 10000,
"refund": 5000,
"reason": "用户申请退款"
}
```
**参数说明**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| outTradeNo | String | 是 | 原商户订单号 |
| outRefundNo | String | 是 | 商户退款单号 |
| total | Integer | 是 | 原订单金额(分) |
| refund | Integer | 是 | 退款金额(分) |
| reason | String | 否 | 退款原因 |
**响应示例**:
```json
{
"code": 0,
"success": true,
"msg": "成功",
"data": {
"refundId": "wx_refund_123456789",
"outRefundNo": "REFUND_123_1234567890",
"status": "SUCCESS"
}
}
```
---
### 5. 绑定微信账号信息
**接口路径**: `POST /payment/wechat/bind`
**功能描述**: 绑定用户微信账号信息获取openId和unionId
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| userId | Long | 是 | 用户ID |
| wxCode | String | 是 | 微信授权码 |
**响应示例**:
```json
{
"code": 0,
"success": true,
"msg": "微信账号绑定成功",
"data": null
}
```
**错误处理**:
- 用户不存在
- 微信登录失败
---
## 数据模型
### PaymentParam (支付参数)
```java
public class PaymentParam {
private Long orderId; // 订单ID
private String paymentCategory; // 支付方式
private BigDecimal settleTotalMoney; // 结算总金额
private List<PaymentItemParam> items; // 支付清单
}
```
### PaymentItemParam (支付清单项)
```java
public class PaymentItemParam {
private Long id; // 费用项ID
private String expenseItemName; // 费用项名称
private BigDecimal settleMoney; // 结算金额
}
```
### WechatPayCallbackDto (微信支付回调)
```java
public class WechatPayCallbackDto {
private String id; // 事件ID
private String createTime; // 创建时间
private String eventType; // 事件类型
private Resource resource; // 资源信息
}
```
### WechatPayRefundDto (微信退款参数)
```java
public class WechatPayRefundDto {
private String outTradeNo; // 商户订单号
private String outRefundNo; // 商户退款单号
private Integer total; // 原订单金额(分)
private Integer refund; // 退款金额(分)
private String reason; // 退款原因
}
```
---
## 业务规则
### 支付流程
1. **参数校验**: 验证支付参数完整性
2. **金额校验**: 验证支付金额与数据库一致
3. **支付处理**:
- 公司支付:直接扣减公司账户余额
- 微信支付:创建微信支付订单
4. **状态更新**: 更新订单支付状态
### 公司支付规则
- 需要验证公司账户余额是否充足
- 直接扣减公司账户余额
- 记录资金变动明细
- 支付成功后订单状态变更为已完成
### 微信支付规则
- 创建微信支付订单
- 订单状态变更为待支付
- 等待微信支付回调确认支付结果
- 支付成功后订单状态变更为已完成
### 退款规则
- 只能对已支付的订单申请退款
- 退款成功后订单状态变更为已退款
- 支持部分退款
---
## 错误码说明
| 错误码 | 说明 |
|--------|------|
| 0 | 成功 |
| 11111 | 系统异常 |
| 40001 | 参数错误 |
| 40002 | 业务异常 |
---
## 注意事项
1. **事务处理**: 支付接口使用`@Transactional`注解确保数据一致性
2. **金额精度**: 所有金额计算使用`BigDecimal`类型
3. **微信支付**: 金额需要转换为分乘以100
4. **回调验证**: 微信支付回调需要验证签名
5. **幂等性**: 支付接口需要保证幂等性,避免重复支付
6. **日志记录**: 关键操作都有详细的日志记录
---
## 更新日志
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| 1.0.0 | 2024-01-01 | 初始版本,包含基础支付功能 |
| 1.1.0 | 2024-01-15 | 新增微信账号绑定功能 |

View File

@ -0,0 +1,29 @@
package com.njzscloud.supervisory.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory());
return restTemplate;
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(10000); // 连接超时10秒
factory.setReadTimeout(30000); // 读取超时30秒
return factory;
}
}

View File

@ -0,0 +1,49 @@
package com.njzscloud.supervisory.order.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 {
/**
* APPID
*/
private String subAppId;
/**
*
*/
private String subMchId;
/**
* API
*/
private String apiKey;
/**
*
*/
private String certSerialNo;
/**
*
*/
private String privateKeyPath;
/**
*
*/
private String notifyUrl;
/**
* 退
*/
private String refundNotifyUrl;
}

View File

@ -1,28 +1,39 @@
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.OrderStatus;
import com.njzscloud.supervisory.order.contant.PaymentStatus;
import com.njzscloud.supervisory.order.contant.PaymentWay;
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 com.njzscloud.supervisory.sys.user.pojo.entity.UserAccountEntity;
import com.njzscloud.supervisory.sys.user.service.UserAccountService;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.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.*;
import java.math.BigDecimal;
@ -39,6 +50,8 @@ public class PaymentController {
private final OrderInfoService orderInfoService;
private final MoneyAccountService moneyAccountService;
private final MoneyChangeDetailService moneyChangeDetailService;
private final WechatPayService wechatPayService;
private final UserAccountService userAccountService;
/**
@ -123,20 +136,43 @@ public class PaymentController {
// 直接扣减公司账户余额
deductCompanyBalance(ctx, dbTotalAmount, paymentParam.getOrderId());
} else if (PaymentWay.WX.getVal().equals(paymentParam.getPaymentCategory())) {
// 微信支付:当前不接入,模拟失败以触发回滚
throw Exceptions.clierr("微信支付失败(占位):已回滚之前扣款");
// 微信支付:创建微信支付订单
WechatPayJsapiOrderDto orderDto = new WechatPayJsapiOrderDto();
orderDto.setOutTradeNo("ORDER_" + paymentParam.getOrderId() + "_" + System.currentTimeMillis());
orderDto.setDescription("订单支付-" + paymentParam.getOrderId());
orderDto.setTotal(dbTotalAmount.multiply(new BigDecimal("100")).longValue()); // 转换为分
// TODO: 需要从上下文中获取用户openid暂时使用固定值
orderDto.setOpenid("test_openid");
WechatPayJsapiOrderResponseDto response = wechatPayService.createMiniProgramOrder(orderDto);
// 更新订单状态为待支付
orderInfoService.lambdaUpdate()
.eq(OrderInfoEntity::getId, paymentParam.getOrderId())
.set(OrderInfoEntity::getPaymentStatus, PaymentStatus.WeiZhiFu)
.set(OrderInfoEntity::getPaymentCategory, paymentParam.getPaymentCategory())
.set(OrderInfoEntity::getSn, orderDto.getOutTradeNo())
.update();
log.info("微信支付订单创建成功订单ID{},微信订单号:{}", paymentParam.getOrderId(), orderDto.getOutTradeNo());
return R.success(response);
} else {
throw Exceptions.clierr("不支持的支付方式");
}
// 3. 更新订单支付状态为已支付
orderInfoService.lambdaUpdate()
.eq(com.njzscloud.supervisory.order.pojo.entity.OrderInfoEntity::getId, paymentParam.getOrderId())
.set(com.njzscloud.supervisory.order.pojo.entity.OrderInfoEntity::getPaymentStatus, PaymentStatus.YiZhiFu)
.update();
// 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())
.update();
log.info("订单支付成功订单ID{},支付方式:{},支付金额:{}",
paymentParam.getOrderId(), paymentParam.getPaymentCategory(), dbTotalAmount);
log.info("订单支付成功订单ID{},支付方式:{},支付金额:{}",
paymentParam.getOrderId(), paymentParam.getPaymentCategory(), dbTotalAmount);
}
return R.success();
}
@ -176,6 +212,139 @@ public class PaymentController {
ctx.getCompanyUserId(), amount, oldBalance, newBalance);
}
/**
*
*/
@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()) // 需要解密获取
.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)
.update();
log.info("微信支付退款成功,订单号:{},退款单号:{}", refundDto.getOutTradeNo(), result.getRefundId());
}
return R.success(result);
} catch (Exception e) {
log.error("微信支付退款异常,订单号:{}", refundDto.getOutTradeNo(), e);
return R.failed();
}
}
/**
* 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/bind")
public R<?> bindWechatAccount(@RequestParam Long userId, @RequestParam String wxCode) {
// 通过userId查询用户账号信息
UserAccountEntity userAccount = userAccountService.getOne(
Wrappers.<UserAccountEntity>lambdaQuery()
.eq(UserAccountEntity::getUserId, userId)
);
if (userAccount == null) {
throw new UserLoginException(ExceptionMsg.CLI_ERR_MSG, "用户不存在");
}
// 调用微信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, "微信登录失败");
}
String openid = code2SessionResult.getOpenid();
String unionid = code2SessionResult.getUnionid();
// 更新用户账号的微信信息
userAccount.setWechatOpenid(openid);
userAccount.setWechatUnionid(unionid);
userAccountService.updateById(userAccount);
return R.success("微信账号绑定成功");
}
}

View File

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

@ -0,0 +1,45 @@
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;
/**
* JSAPI/DTO
*/
@Data
public class WechatPayJsapiOrderDto {
/**
*
*/
@NotBlank(message = "商品描述不能为空")
@Size(max = 127, message = "商品描述不能超过127个字符")
private String description;
/**
*
*/
@NotBlank(message = "商户订单号不能为空")
@Size(min = 6, max = 32, message = "商户订单号长度必须在6-32个字符之间")
private String outTradeNo;
/**
* openid
*/
@NotBlank(message = "用户openid不能为空")
private String openid;
/**
*
*/
@NotNull(message = "订单金额不能为空")
private Long total;
/**
*
*/
private String currency = "CNY";
}

View File

@ -0,0 +1,40 @@
package com.njzscloud.supervisory.order.pojo.dto;
import lombok.Data;
/**
* JSAPI/DTO
*/
@Data
public class WechatPayJsapiOrderResponseDto {
/**
*
*/
private String prepayId;
/**
*
*/
private String timeStamp;
/**
*
*/
private String nonceStr;
/**
*
*/
private String packageValue;
/**
*
*/
private String signType;
/**
*
*/
private String paySign;
}

View File

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

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

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

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

@ -202,6 +202,11 @@ public class OrderInfoEntity {
*/
private String checkerMemo;
/**
*
*/
private String paymentCategory;
/**
*
*/

View File

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

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

@ -22,4 +22,6 @@ public interface AuthMapper {
MyResult selectUser(@Param("ew") QueryWrapper<Object> ew);
SearchCompanyResult selectCompanyInfo(@Param("userId") Long userId);
String selectWechatOpenidByUserId(@Param("userId") Long userId);
}

View File

@ -38,4 +38,10 @@ public class MyResult extends UserDetail {
*
*/
private String phone;
/**
* openId
*/
private String wechatOpenid;
}

View File

@ -83,11 +83,13 @@ public class AuthService implements IUserService, IRoleService {
List<EndpointResource> endpointResources = authMapper.selectUserEndpoint(userId);
SearchCompanyResult company = authMapper.selectCompanyInfo(userId);
Set<String> strings = authMapper.selectRoleByUserId(userId);
String wechatOpenid = authMapper.selectWechatOpenidByUserId(userId);
return BeanUtil.copyProperties(userEntity, MyResult.class)
.setRoles(strings)
.setMenus(menuResources)
.setEndpoints(endpointResources)
.setCompany(company);
.setCompany(company)
.setWechatOpenid(wechatOpenid != null ? wechatOpenid : "");
}
}

View File

@ -57,3 +57,15 @@ wechat:
app-id: wx3c06d9dd4e56c58d
app-secret: ff280a71a4c06fc2956178f8c472ef96
base-url: https://api.weixin.qq.com
pay:
# 子商户配置
sub-app-id: wx3c06d9dd4e56c58d
sub-mch-id: 1900000100
# API密钥32位字符串
api-key: your-32-character-api-key-here
# 证书序列号
cert-serial-no: your-cert-serial-number
# 私钥文件路径
private-key-path: classpath:cert/apiclient_key.pem
# 支付回调地址
notify-url: https://your-domain.com/api/payment/wechat/callback

View File

@ -106,4 +106,10 @@
FROM biz_company
WHERE user_id = #{userId}
</select>
<select id="selectWechatOpenidByUserId" resultType="java.lang.String">
SELECT wechat_openid
FROM sys_user_account
WHERE user_id = #{userId} AND deleted = 0
</select>
</mapper>

View File

@ -0,0 +1,116 @@
# 微信支付配置说明
## 配置项说明
根据[微信支付官方文档](https://pay.weixin.qq.com/doc/v3/merchant/4012791897),以下是简化的微信支付配置说明(仅包含必填参数):
### 1. 基础配置
```yaml
wechat:
pay:
# 子商户配置
sub-app-id: wx3c06d9dd4e56c58d # 子商户APPID
sub-mch-id: 1900000100 # 子商户号
# API密钥32位字符串
api-key: your-32-character-api-key-here
# 证书序列号
cert-serial-no: your-cert-serial-number
# 私钥文件路径
private-key-path: classpath:cert/apiclient_key.pem
# 支付回调地址
notify-url: https://your-domain.com/api/payment/wechat/callback
```
### 2. 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| sub-app-id | String | 是 | 子商户APPID |
| sub-mch-id | String | 是 | 子商户号 |
| api-key | String | 是 | API密钥32位字符串 |
| cert-serial-no | String | 是 | 证书序列号 |
| private-key-path | String | 是 | 私钥文件路径 |
| notify-url | String | 是 | 支付成功回调地址 |
### 3. 证书配置
1. 将微信支付商户私钥文件 `apiclient_key.pem` 放置在 `src/main/resources/cert/` 目录下
2. 确保证书序列号与私钥文件匹配
3. 私钥文件格式应为 PEM 格式
### 4. 回调地址配置
- 支付回调地址:用于接收支付成功通知
- 退款回调地址:用于接收退款成功通知
- 回调地址必须是 HTTPS 协议
- 回调地址需要能够接收 POST 请求
### 5. 安全注意事项
1. **API密钥安全**
- API密钥是32位字符串请妥善保管
- 不要将API密钥提交到代码仓库
- 建议使用环境变量或配置中心管理
2. **证书安全**
- 私钥文件不要提交到代码仓库
- 确保证书文件权限设置正确
- 定期更新证书
3. **回调地址安全**
- 使用HTTPS协议
- 验证回调签名
- 处理重复通知
### 6. 环境配置
#### 开发环境 (application-dev.yml)
```yaml
wechat:
pay:
# 使用测试商户号和测试APPID
sub-app-id: wx3c06d9dd4e56c58d
sub-mch-id: 1900000100
api-key: your-test-api-key
cert-serial-no: your-test-cert-serial
private-key-path: classpath:cert/test_apiclient_key.pem
notify-url: https://test-domain.com/api/payment/wechat/callback
```
#### 生产环境 (application-prod.yml)
```yaml
wechat:
pay:
# 使用正式商户号和正式APPID
sub-app-id: wx3c06d9dd4e56c58d
sub-mch-id: 1900000100
api-key: ${WECHAT_PAY_API_KEY}
cert-serial-no: ${WECHAT_PAY_CERT_SERIAL}
private-key-path: classpath:cert/prod_apiclient_key.pem
notify-url: https://your-domain.com/api/payment/wechat/callback
```
### 7. 常见问题
1. **证书序列号获取**
- 登录微信支付商户平台
- 在"账户中心" -> "API安全" -> "API证书"中查看
2. **API密钥设置**
- 在微信支付商户平台设置32位API密钥
- 确保与代码中配置的密钥一致
3. **回调地址配置**
- 确保回调地址可以正常访问
- 使用HTTPS协议
- 处理跨域问题
4. **测试环境**
- 使用微信支付沙箱环境进行测试
- 测试商户号和正式商户号不同
- 测试环境需要特殊配置