no message
This commit is contained in:
29
src/main/java/com/sczx/pay/Application.java
Normal file
29
src/main/java/com/sczx/pay/Application.java
Normal file
@ -0,0 +1,29 @@
|
||||
package com.sczx.pay;
|
||||
|
||||
import com.sczx.pay.utils.ComputerInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient // 启用服务注册与发现
|
||||
@EnableRetry
|
||||
@EnableTransactionManagement
|
||||
@MapperScan("com.sczx.pay.mapper") // 扫描 Mapper 接口
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
|
||||
Environment environment = context.getBean(Environment.class);
|
||||
log.info("启动成功,后端服务API地址:http://{}:{}/swagger-ui.html", ComputerInfo.getIpAddr(), environment.getProperty("server.port"));
|
||||
}
|
||||
}
|
||||
83
src/main/java/com/sczx/pay/config/DynamicWXPayConfig.java
Normal file
83
src/main/java/com/sczx/pay/config/DynamicWXPayConfig.java
Normal file
@ -0,0 +1,83 @@
|
||||
package com.sczx.pay.config;
|
||||
|
||||
import com.sczx.pay.sdk.WXPayConfig;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 动态微信支付配置类
|
||||
*/
|
||||
@Component
|
||||
public class DynamicWXPayConfig extends WXPayConfig {
|
||||
|
||||
private String appId;
|
||||
private String mchId;
|
||||
private String key;
|
||||
private String notifyUrl;
|
||||
|
||||
// 构造函数
|
||||
public DynamicWXPayConfig() {}
|
||||
|
||||
public DynamicWXPayConfig(String appId, String mchId, String key, String notifyUrl) {
|
||||
this.appId = appId;
|
||||
this.mchId = mchId;
|
||||
this.key = key;
|
||||
this.notifyUrl = notifyUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAppID() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMchID() {
|
||||
return mchId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getCertStream() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpConnectTimeoutMs() {
|
||||
return 8000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpReadTimeoutMs() {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public void setMchId(String mchId) {
|
||||
this.mchId = mchId;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getNotifyUrl() {
|
||||
return notifyUrl;
|
||||
}
|
||||
|
||||
public void setNotifyUrl(String notifyUrl) {
|
||||
this.notifyUrl = notifyUrl;
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/sczx/pay/config/SwaggerConfig.java
Normal file
47
src/main/java/com/sczx/pay/config/SwaggerConfig.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.sczx.pay.config;
|
||||
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.ParameterBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.schema.ModelRef;
|
||||
import springfox.documentation.service.ApiInfo;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
@EnableSwagger2
|
||||
public class SwaggerConfig {
|
||||
@Bean
|
||||
public Docket createRestApi() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
.apiInfo(apiInfo())
|
||||
.select()
|
||||
.apis(RequestHandlerSelectors.basePackage("com.sczx.pay.controller")) // 修改为你的 controller 包路径
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
.globalOperationParameters(Arrays.asList(
|
||||
new ParameterBuilder()
|
||||
.name("Authorization")
|
||||
.description("Bearer Token")
|
||||
.modelRef(new ModelRef("string"))
|
||||
.parameterType("header")
|
||||
.required(true)
|
||||
.build()
|
||||
));
|
||||
}
|
||||
|
||||
private ApiInfo apiInfo() {
|
||||
return new ApiInfoBuilder()
|
||||
.title("支付服务接口文档")
|
||||
.description("sczx_singlepay服务接口文档")
|
||||
.version("1.0")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
85
src/main/java/com/sczx/pay/config/WXPayConfigImpl.java
Normal file
85
src/main/java/com/sczx/pay/config/WXPayConfigImpl.java
Normal file
@ -0,0 +1,85 @@
|
||||
package com.sczx.pay.config;
|
||||
|
||||
import com.sczx.pay.sdk.WXPayConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 微信支付配置实现类
|
||||
*/
|
||||
@Configuration
|
||||
public class WXPayConfigImpl extends WXPayConfig {
|
||||
|
||||
@Value("${wechat.pay.app-id}")
|
||||
private String appId;
|
||||
|
||||
@Value("${wechat.pay.mch-id}")
|
||||
private String mchId;
|
||||
|
||||
@Value("${wechat.pay.key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${wechat.pay.notify-url}")
|
||||
private String notifyUrl;
|
||||
|
||||
@Override
|
||||
public String getAppID() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMchID() {
|
||||
return mchId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getCertStream() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpConnectTimeoutMs() {
|
||||
return 8000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpReadTimeoutMs() {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
// getter和setter方法
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public String getMchId() {
|
||||
return mchId;
|
||||
}
|
||||
|
||||
public void setMchId(String mchId) {
|
||||
this.mchId = mchId;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.apiKey = key;
|
||||
}
|
||||
|
||||
public String getNotifyUrl() {
|
||||
return notifyUrl;
|
||||
}
|
||||
|
||||
public void setNotifyUrl(String notifyUrl) {
|
||||
this.notifyUrl = notifyUrl;
|
||||
}
|
||||
}
|
||||
177
src/main/java/com/sczx/pay/controller/PaymentController.java
Normal file
177
src/main/java/com/sczx/pay/controller/PaymentController.java
Normal file
@ -0,0 +1,177 @@
|
||||
package com.sczx.pay.controller;
|
||||
|
||||
import com.sczx.pay.dto.PaymentRequest;
|
||||
import com.sczx.pay.dto.PaymentResponse;
|
||||
import com.sczx.pay.dto.RefundRequest;
|
||||
import com.sczx.pay.service.WechatPayService;
|
||||
import com.sczx.pay.utils.WXPayUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/payment")
|
||||
public class PaymentController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PaymentController.class);
|
||||
|
||||
@Autowired
|
||||
private WechatPayService wechatPayService;
|
||||
|
||||
/**
|
||||
* 小程序统一下单接口
|
||||
*/
|
||||
@PostMapping("/unifiedOrder")
|
||||
public PaymentResponse unifiedOrder(@RequestBody PaymentRequest request) {
|
||||
logger.info("收到支付请求: {}", request);
|
||||
return wechatPayService.unifiedOrder(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单接口
|
||||
*/
|
||||
@GetMapping("/query/{companyId}/{outTradeNo}")
|
||||
public Map<String, String> orderQuery(@PathVariable Long companyId, @PathVariable String outTradeNo) {
|
||||
logger.info("收到订单查询请求,公司ID: {}, 订单号: {}", companyId, outTradeNo);
|
||||
try {
|
||||
return wechatPayService.orderQuery(companyId, outTradeNo);
|
||||
} catch (Exception e) {
|
||||
logger.error("订单查询异常,公司ID: {}, 订单号: {}", companyId, outTradeNo, e);
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "查询异常: " + e.getMessage());
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭订单接口
|
||||
*/
|
||||
@PostMapping("/close/{companyId}/{outTradeNo}")
|
||||
public Map<String, String> closeOrder(@PathVariable Long companyId, @PathVariable String outTradeNo) {
|
||||
logger.info("收到关闭订单请求,公司ID: {}, 订单号: {}", companyId, outTradeNo);
|
||||
try {
|
||||
return wechatPayService.closeOrder(companyId, outTradeNo);
|
||||
} catch (Exception e) {
|
||||
logger.error("关闭订单异常,公司ID: {}, 订单号: {}", companyId, outTradeNo, e);
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "关闭异常: " + e.getMessage());
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款接口
|
||||
*/
|
||||
@PostMapping("/refund")
|
||||
public Map<String, String> refund(@RequestBody RefundRequest request) {
|
||||
logger.info("收到退款请求: {}", request);
|
||||
try {
|
||||
return wechatPayService.refund(request);
|
||||
} catch (Exception e) {
|
||||
logger.error("退款异常,公司ID: {}, 订单号: {}", request.getCompanyId(), request.getOutTradeNo(), e);
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "退款异常: " + e.getMessage());
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款接口
|
||||
*/
|
||||
@GetMapping("/refundQuery/{companyId}")
|
||||
public Map<String, String> refundQuery(@PathVariable Long companyId, @RequestParam String outTradeNo) {
|
||||
logger.info("收到退款查询请求,公司ID: {}, 订单号: {}", companyId, outTradeNo);
|
||||
try {
|
||||
return wechatPayService.refundQuery(companyId, outTradeNo);
|
||||
} catch (Exception e) {
|
||||
logger.error("退款查询异常,公司ID: {}, 订单号: {}", companyId, outTradeNo, e);
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "退款查询异常: " + e.getMessage());
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付结果通知
|
||||
*/
|
||||
@PostMapping("/notify/{companyId}")
|
||||
public String notify(@PathVariable Long companyId, HttpServletRequest request) {
|
||||
try {
|
||||
// 读取微信回调数据
|
||||
StringBuilder sb = new StringBuilder();
|
||||
BufferedReader reader = request.getReader();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
|
||||
String xmlData = sb.toString();
|
||||
logger.info("收到微信支付通知,公司ID: {}, 数据: {}", companyId, xmlData);
|
||||
|
||||
// 解析XML数据
|
||||
Map<String, String> notifyMap = WXPayUtil.xmlToMap(xmlData);
|
||||
|
||||
// 验证签名
|
||||
if (!wechatPayService.verifyNotifySign(companyId, notifyMap)) {
|
||||
logger.warn("微信支付通知签名验证失败,公司ID: {}", companyId);
|
||||
return buildResponse("FAIL", "签名失败");
|
||||
}
|
||||
|
||||
String returnCode = notifyMap.get("return_code");
|
||||
if (!"SUCCESS".equals(returnCode)) {
|
||||
logger.warn("微信支付通知返回失败,公司ID: {}: {}", companyId, notifyMap.get("return_msg"));
|
||||
return buildResponse("FAIL", "返回失败");
|
||||
}
|
||||
|
||||
String resultCode = notifyMap.get("result_code");
|
||||
if (!"SUCCESS".equals(resultCode)) {
|
||||
logger.warn("微信支付业务失败,公司ID: {}: {}", companyId, notifyMap.get("err_code_des"));
|
||||
return buildResponse("FAIL", "业务失败");
|
||||
}
|
||||
|
||||
// 处理支付成功的业务逻辑
|
||||
String outTradeNo = notifyMap.get("out_trade_no");
|
||||
String transactionId = notifyMap.get("transaction_id");
|
||||
String totalFee = notifyMap.get("total_fee");
|
||||
|
||||
// 更新数据库中的订单状态
|
||||
boolean success = wechatPayService.processPaySuccessNotify(companyId, notifyMap);
|
||||
if (success) {
|
||||
logger.info("支付成功,公司ID: {}, 订单号: {}, 微信交易号: {}, 金额: {}",
|
||||
companyId, outTradeNo, transactionId, totalFee);
|
||||
return buildResponse("SUCCESS", "OK");
|
||||
} else {
|
||||
logger.error("更新支付状态失败,公司ID: {}, 订单号: {}", companyId, outTradeNo);
|
||||
return buildResponse("FAIL", "更新支付状态失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理微信支付通知异常,公司ID: {}", companyId, e);
|
||||
return buildResponse("FAIL", "处理异常");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildResponse(String returnCode, String returnMsg) {
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("return_code", returnCode);
|
||||
response.put("return_msg", returnMsg);
|
||||
try {
|
||||
return WXPayUtil.mapToXml(response);
|
||||
} catch (Exception e) {
|
||||
return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[构建响应异常]]></return_msg></xml>";
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/main/java/com/sczx/pay/dto/PaymentRequest.java
Normal file
74
src/main/java/com/sczx/pay/dto/PaymentRequest.java
Normal file
@ -0,0 +1,74 @@
|
||||
package com.sczx.pay.dto;
|
||||
|
||||
/**
|
||||
* 支付请求数据传输对象
|
||||
*/
|
||||
public class PaymentRequest {
|
||||
private Long companyId; // 公司ID
|
||||
private String body; // 商品描述
|
||||
private String outTradeNo; // 商户订单号
|
||||
private Integer totalFee; // 总金额,单位为分
|
||||
private String spbillCreateIp; // 终端IP
|
||||
private String openId; // 用户标识
|
||||
private String attach; // 附加数据
|
||||
|
||||
// 构造函数
|
||||
public PaymentRequest() {}
|
||||
|
||||
// getter和setter方法
|
||||
public Long getCompanyId() {
|
||||
return companyId;
|
||||
}
|
||||
|
||||
public void setCompanyId(Long companyId) {
|
||||
this.companyId = companyId;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setBody(String body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public String getOutTradeNo() {
|
||||
return outTradeNo;
|
||||
}
|
||||
|
||||
public void setOutTradeNo(String outTradeNo) {
|
||||
this.outTradeNo = outTradeNo;
|
||||
}
|
||||
|
||||
public Integer getTotalFee() {
|
||||
return totalFee;
|
||||
}
|
||||
|
||||
public void setTotalFee(Integer totalFee) {
|
||||
this.totalFee = totalFee;
|
||||
}
|
||||
|
||||
public String getSpbillCreateIp() {
|
||||
return spbillCreateIp;
|
||||
}
|
||||
|
||||
public void setSpbillCreateIp(String spbillCreateIp) {
|
||||
this.spbillCreateIp = spbillCreateIp;
|
||||
}
|
||||
|
||||
public String getOpenId() {
|
||||
return openId;
|
||||
}
|
||||
|
||||
public void setOpenId(String openId) {
|
||||
this.openId = openId;
|
||||
}
|
||||
|
||||
public String getAttach() {
|
||||
return attach;
|
||||
}
|
||||
|
||||
public void setAttach(String attach) {
|
||||
this.attach = attach;
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/sczx/pay/dto/PaymentResponse.java
Normal file
38
src/main/java/com/sczx/pay/dto/PaymentResponse.java
Normal file
@ -0,0 +1,38 @@
|
||||
package com.sczx.pay.dto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付响应数据传输对象
|
||||
*/
|
||||
public class PaymentResponse {
|
||||
private String code;
|
||||
private String message;
|
||||
private Map<String, String> payData;
|
||||
|
||||
public PaymentResponse() {}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Map<String, String> getPayData() {
|
||||
return payData;
|
||||
}
|
||||
|
||||
public void setPayData(Map<String, String> payData) {
|
||||
this.payData = payData;
|
||||
}
|
||||
}
|
||||
65
src/main/java/com/sczx/pay/dto/RefundRequest.java
Normal file
65
src/main/java/com/sczx/pay/dto/RefundRequest.java
Normal file
@ -0,0 +1,65 @@
|
||||
package com.sczx.pay.dto;
|
||||
|
||||
/**
|
||||
* 退款请求数据传输对象
|
||||
*/
|
||||
public class RefundRequest {
|
||||
private Long companyId; // 公司ID
|
||||
private String outTradeNo; // 商户订单号
|
||||
private String outRefundNo; // 商户退款单号
|
||||
private Integer totalFee; // 订单金额(分)
|
||||
private Integer refundFee; // 退款金额(分)
|
||||
private String refundDesc; // 退款原因
|
||||
|
||||
// 构造函数
|
||||
public RefundRequest() {}
|
||||
|
||||
// getter和setter方法
|
||||
public Long getCompanyId() {
|
||||
return companyId;
|
||||
}
|
||||
|
||||
public void setCompanyId(Long companyId) {
|
||||
this.companyId = companyId;
|
||||
}
|
||||
|
||||
public String getOutTradeNo() {
|
||||
return outTradeNo;
|
||||
}
|
||||
|
||||
public void setOutTradeNo(String outTradeNo) {
|
||||
this.outTradeNo = outTradeNo;
|
||||
}
|
||||
|
||||
public String getOutRefundNo() {
|
||||
return outRefundNo;
|
||||
}
|
||||
|
||||
public void setOutRefundNo(String outRefundNo) {
|
||||
this.outRefundNo = outRefundNo;
|
||||
}
|
||||
|
||||
public Integer getTotalFee() {
|
||||
return totalFee;
|
||||
}
|
||||
|
||||
public void setTotalFee(Integer totalFee) {
|
||||
this.totalFee = totalFee;
|
||||
}
|
||||
|
||||
public Integer getRefundFee() {
|
||||
return refundFee;
|
||||
}
|
||||
|
||||
public void setRefundFee(Integer refundFee) {
|
||||
this.refundFee = refundFee;
|
||||
}
|
||||
|
||||
public String getRefundDesc() {
|
||||
return refundDesc;
|
||||
}
|
||||
|
||||
public void setRefundDesc(String refundDesc) {
|
||||
this.refundDesc = refundDesc;
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/sczx/pay/entity/CompanyWechatConfig.java
Normal file
44
src/main/java/com/sczx/pay/entity/CompanyWechatConfig.java
Normal file
@ -0,0 +1,44 @@
|
||||
package com.sczx.pay.entity;
|
||||
|
||||
/**
|
||||
* 公司微信支付配置实体类
|
||||
*/
|
||||
public class CompanyWechatConfig {
|
||||
private Long id;
|
||||
private String mchId;
|
||||
private String apikey;
|
||||
|
||||
// 构造函数
|
||||
public CompanyWechatConfig() {}
|
||||
|
||||
public CompanyWechatConfig(Long id, String mchId, String apikey) {
|
||||
this.id = id;
|
||||
this.mchId = mchId;
|
||||
this.apikey = apikey;
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMchId() {
|
||||
return mchId;
|
||||
}
|
||||
|
||||
public void setMchId(String mchId) {
|
||||
this.mchId = mchId;
|
||||
}
|
||||
|
||||
public String getApikey() {
|
||||
return apikey;
|
||||
}
|
||||
|
||||
public void setApikey(String apikey) {
|
||||
this.apikey = apikey;
|
||||
}
|
||||
}
|
||||
167
src/main/java/com/sczx/pay/entity/PaymentRecord.java
Normal file
167
src/main/java/com/sczx/pay/entity/PaymentRecord.java
Normal file
@ -0,0 +1,167 @@
|
||||
package com.sczx.pay.entity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 支付记录实体类
|
||||
*/
|
||||
public class PaymentRecord {
|
||||
private Long id;
|
||||
private Long companyId;
|
||||
private String outTradeNo; // 商户订单号
|
||||
private String transactionId; // 支付平台交易号
|
||||
private BigDecimal totalFee; // 订单金额
|
||||
private String body; // 商品描述
|
||||
private String openid; // 用户标识(微信支付专用)
|
||||
private String tradeState; // 交易状态
|
||||
private String tradeStateDesc; // 交易状态描述
|
||||
private Date createTime; // 创建时间
|
||||
private Date updateTime; // 更新时间
|
||||
private Date payTime; // 支付时间
|
||||
private String attach; // 附加数据
|
||||
private String payChannel; // 支付渠道:WECHAT-微信支付,ALIPAY-支付宝
|
||||
private String buyerId; // 买家用户ID(支付宝专用)
|
||||
|
||||
// 支付渠道枚举
|
||||
public enum PayChannel {
|
||||
WECHAT("微信支付"),
|
||||
ALIPAY("支付宝");
|
||||
|
||||
private final String description;
|
||||
|
||||
PayChannel(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
public PaymentRecord() {
|
||||
this.payChannel = PayChannel.WECHAT.name(); // 默认为微信支付
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getCompanyId() {
|
||||
return companyId;
|
||||
}
|
||||
|
||||
public void setCompanyId(Long companyId) {
|
||||
this.companyId = companyId;
|
||||
}
|
||||
|
||||
public String getOutTradeNo() {
|
||||
return outTradeNo;
|
||||
}
|
||||
|
||||
public void setOutTradeNo(String outTradeNo) {
|
||||
this.outTradeNo = outTradeNo;
|
||||
}
|
||||
|
||||
public String getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public void setTransactionId(String transactionId) {
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalFee() {
|
||||
return totalFee;
|
||||
}
|
||||
|
||||
public void setTotalFee(BigDecimal totalFee) {
|
||||
this.totalFee = totalFee;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setBody(String body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public String getOpenid() {
|
||||
return openid;
|
||||
}
|
||||
|
||||
public void setOpenid(String openid) {
|
||||
this.openid = openid;
|
||||
}
|
||||
|
||||
public String getTradeState() {
|
||||
return tradeState;
|
||||
}
|
||||
|
||||
public void setTradeState(String tradeState) {
|
||||
this.tradeState = tradeState;
|
||||
}
|
||||
|
||||
public String getTradeStateDesc() {
|
||||
return tradeStateDesc;
|
||||
}
|
||||
|
||||
public void setTradeStateDesc(String tradeStateDesc) {
|
||||
this.tradeStateDesc = tradeStateDesc;
|
||||
}
|
||||
|
||||
public Date getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(Date createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public Date getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(Date updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public Date getPayTime() {
|
||||
return payTime;
|
||||
}
|
||||
|
||||
public void setPayTime(Date payTime) {
|
||||
this.payTime = payTime;
|
||||
}
|
||||
|
||||
public String getAttach() {
|
||||
return attach;
|
||||
}
|
||||
|
||||
public void setAttach(String attach) {
|
||||
this.attach = attach;
|
||||
}
|
||||
|
||||
public String getPayChannel() {
|
||||
return payChannel;
|
||||
}
|
||||
|
||||
public void setPayChannel(String payChannel) {
|
||||
this.payChannel = payChannel;
|
||||
}
|
||||
|
||||
public String getBuyerId() {
|
||||
return buyerId;
|
||||
}
|
||||
|
||||
public void setBuyerId(String buyerId) {
|
||||
this.buyerId = buyerId;
|
||||
}
|
||||
}
|
||||
158
src/main/java/com/sczx/pay/entity/RefundRecord.java
Normal file
158
src/main/java/com/sczx/pay/entity/RefundRecord.java
Normal file
@ -0,0 +1,158 @@
|
||||
package com.sczx.pay.entity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 退款记录实体类
|
||||
*/
|
||||
public class RefundRecord {
|
||||
private Long id;
|
||||
private Long companyId;
|
||||
private String outTradeNo; // 商户订单号
|
||||
private String outRefundNo; // 商户退款单号
|
||||
private String refundId; // 支付平台退款单号
|
||||
private BigDecimal totalFee; // 订单金额
|
||||
private BigDecimal refundFee; // 退款金额
|
||||
private String refundStatus; // 退款状态
|
||||
private String refundStatusDesc; // 退款状态描述
|
||||
private String refundDesc; // 退款原因
|
||||
private Date createTime; // 创建时间
|
||||
private Date updateTime; // 更新时间
|
||||
private Date refundTime; // 退款成功时间
|
||||
private String payChannel; // 支付渠道:WECHAT-微信支付,ALIPAY-支付宝
|
||||
|
||||
// 支付渠道枚举
|
||||
public enum PayChannel {
|
||||
WECHAT("微信支付"),
|
||||
ALIPAY("支付宝");
|
||||
|
||||
private final String description;
|
||||
|
||||
PayChannel(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
public RefundRecord() {
|
||||
this.payChannel = PayChannel.WECHAT.name(); // 默认为微信支付
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getCompanyId() {
|
||||
return companyId;
|
||||
}
|
||||
|
||||
public void setCompanyId(Long companyId) {
|
||||
this.companyId = companyId;
|
||||
}
|
||||
|
||||
public String getOutTradeNo() {
|
||||
return outTradeNo;
|
||||
}
|
||||
|
||||
public void setOutTradeNo(String outTradeNo) {
|
||||
this.outTradeNo = outTradeNo;
|
||||
}
|
||||
|
||||
public String getOutRefundNo() {
|
||||
return outRefundNo;
|
||||
}
|
||||
|
||||
public void setOutRefundNo(String outRefundNo) {
|
||||
this.outRefundNo = outRefundNo;
|
||||
}
|
||||
|
||||
public String getRefundId() {
|
||||
return refundId;
|
||||
}
|
||||
|
||||
public void setRefundId(String refundId) {
|
||||
this.refundId = refundId;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalFee() {
|
||||
return totalFee;
|
||||
}
|
||||
|
||||
public void setTotalFee(BigDecimal totalFee) {
|
||||
this.totalFee = totalFee;
|
||||
}
|
||||
|
||||
public BigDecimal getRefundFee() {
|
||||
return refundFee;
|
||||
}
|
||||
|
||||
public void setRefundFee(BigDecimal refundFee) {
|
||||
this.refundFee = refundFee;
|
||||
}
|
||||
|
||||
public String getRefundStatus() {
|
||||
return refundStatus;
|
||||
}
|
||||
|
||||
public void setRefundStatus(String refundStatus) {
|
||||
this.refundStatus = refundStatus;
|
||||
}
|
||||
|
||||
public String getRefundStatusDesc() {
|
||||
return refundStatusDesc;
|
||||
}
|
||||
|
||||
public void setRefundStatusDesc(String refundStatusDesc) {
|
||||
this.refundStatusDesc = refundStatusDesc;
|
||||
}
|
||||
|
||||
public String getRefundDesc() {
|
||||
return refundDesc;
|
||||
}
|
||||
|
||||
public void setRefundDesc(String refundDesc) {
|
||||
this.refundDesc = refundDesc;
|
||||
}
|
||||
|
||||
public Date getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(Date createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public Date getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(Date updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public Date getRefundTime() {
|
||||
return refundTime;
|
||||
}
|
||||
|
||||
public void setRefundTime(Date refundTime) {
|
||||
this.refundTime = refundTime;
|
||||
}
|
||||
|
||||
public String getPayChannel() {
|
||||
return payChannel;
|
||||
}
|
||||
|
||||
public void setPayChannel(String payChannel) {
|
||||
this.payChannel = payChannel;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.sczx.pay.mapper;
|
||||
|
||||
import com.sczx.pay.entity.CompanyWechatConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface CompanyWechatConfigMapper {
|
||||
|
||||
/**
|
||||
* 根据公司ID获取微信支付配置
|
||||
* @param companyId 公司ID
|
||||
* @return 微信支付配置信息
|
||||
*/
|
||||
@Select("SELECT id, wechat_receiving_account AS mchId, wechat_key AS apikey FROM zc_company WHERE id = #{companyId}")
|
||||
CompanyWechatConfig getWechatConfigByCompanyId(@Param("companyId") Long companyId);
|
||||
}
|
||||
62
src/main/java/com/sczx/pay/mapper/PaymentRecordMapper.java
Normal file
62
src/main/java/com/sczx/pay/mapper/PaymentRecordMapper.java
Normal file
@ -0,0 +1,62 @@
|
||||
package com.sczx.pay.mapper;
|
||||
|
||||
import com.sczx.pay.entity.PaymentRecord;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Mapper
|
||||
public interface PaymentRecordMapper {
|
||||
|
||||
/**
|
||||
* 插入支付记录
|
||||
*/
|
||||
@Insert("INSERT INTO zc_payment_record (company_id, out_trade_no, transaction_id, total_fee, body, openid, " +
|
||||
"trade_state, trade_state_desc, create_time, update_time, pay_time, attach, pay_channel, buyer_id) " +
|
||||
"VALUES (#{companyId}, #{outTradeNo}, #{transactionId}, #{totalFee}, #{body}, #{openid}, " +
|
||||
"#{tradeState}, #{tradeStateDesc}, #{createTime}, #{updateTime}, #{payTime}, #{attach}, #{payChannel}, #{buyerId})")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id")
|
||||
int insertPaymentRecord(PaymentRecord paymentRecord);
|
||||
|
||||
/**
|
||||
* 根据商户订单号查询支付记录
|
||||
*/
|
||||
@Select("SELECT * FROM zc_payment_record WHERE out_trade_no = #{outTradeNo} LIMIT 1")
|
||||
PaymentRecord getPaymentRecordByOutTradeNo(@Param("outTradeNo") String outTradeNo);
|
||||
|
||||
/**
|
||||
* 根据商户订单号更新支付状态
|
||||
*/
|
||||
@Update("UPDATE zc_payment_record SET trade_state = #{tradeState}, trade_state_desc = #{tradeStateDesc}, " +
|
||||
"transaction_id = #{transactionId}, pay_time = #{payTime}, update_time = #{updateTime} " +
|
||||
"WHERE out_trade_no = #{outTradeNo}")
|
||||
int updatePaymentStatus(@Param("outTradeNo") String outTradeNo,
|
||||
@Param("tradeState") String tradeState,
|
||||
@Param("tradeStateDesc") String tradeStateDesc,
|
||||
@Param("transactionId") String transactionId,
|
||||
@Param("payTime") Date payTime,
|
||||
@Param("updateTime") Date updateTime);
|
||||
|
||||
/**
|
||||
* 根据商户订单号更新为支付成功状态(微信支付)
|
||||
*/
|
||||
@Update("UPDATE zc_payment_record SET trade_state = 'SUCCESS', trade_state_desc = '支付成功', " +
|
||||
"transaction_id = #{transactionId}, pay_time = #{payTime}, update_time = #{updateTime} " +
|
||||
"WHERE out_trade_no = #{outTradeNo}")
|
||||
int updateToSuccess(@Param("outTradeNo") String outTradeNo,
|
||||
@Param("transactionId") String transactionId,
|
||||
@Param("payTime") Date payTime,
|
||||
@Param("updateTime") Date updateTime);
|
||||
|
||||
/**
|
||||
* 根据商户订单号更新为支付成功状态(支付宝)
|
||||
*/
|
||||
@Update("UPDATE zc_payment_record SET trade_state = 'SUCCESS', trade_state_desc = '支付成功', " +
|
||||
"transaction_id = #{transactionId}, buyer_id = #{buyerId}, pay_time = #{payTime}, update_time = #{updateTime} " +
|
||||
"WHERE out_trade_no = #{outTradeNo}")
|
||||
int updateToSuccessForAlipay(@Param("outTradeNo") String outTradeNo,
|
||||
@Param("transactionId") String transactionId,
|
||||
@Param("buyerId") String buyerId,
|
||||
@Param("payTime") Date payTime,
|
||||
@Param("updateTime") Date updateTime);
|
||||
}
|
||||
39
src/main/java/com/sczx/pay/mapper/RefundRecordMapper.java
Normal file
39
src/main/java/com/sczx/pay/mapper/RefundRecordMapper.java
Normal file
@ -0,0 +1,39 @@
|
||||
package com.sczx.pay.mapper;
|
||||
|
||||
import com.sczx.pay.entity.RefundRecord;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Mapper
|
||||
public interface RefundRecordMapper {
|
||||
|
||||
/**
|
||||
* 插入退款记录
|
||||
*/
|
||||
@Insert("INSERT INTO zc_refund_record (company_id, out_trade_no, out_refund_no, refund_id, total_fee, refund_fee, " +
|
||||
"refund_status, refund_status_desc, refund_desc, create_time, update_time, refund_time, pay_channel) " +
|
||||
"VALUES (#{companyId}, #{outTradeNo}, #{outRefundNo}, #{refundId}, #{totalFee}, #{refundFee}, " +
|
||||
"#{refundStatus}, #{refundStatusDesc}, #{refundDesc}, #{createTime}, #{updateTime}, #{refundTime}, #{payChannel})")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id")
|
||||
int insertRefundRecord(RefundRecord refundRecord);
|
||||
|
||||
/**
|
||||
* 根据商户退款单号查询退款记录
|
||||
*/
|
||||
@Select("SELECT * FROM zc_refund_record WHERE out_refund_no = #{outRefundNo} LIMIT 1")
|
||||
RefundRecord getRefundRecordByOutRefundNo(@Param("outRefundNo") String outRefundNo);
|
||||
|
||||
/**
|
||||
* 根据商户退款单号更新退款状态
|
||||
*/
|
||||
@Update("UPDATE zc_refund_record SET refund_status = #{refundStatus}, refund_status_desc = #{refundStatusDesc}, " +
|
||||
"refund_id = #{refundId}, refund_time = #{refundTime}, update_time = #{updateTime} " +
|
||||
"WHERE out_refund_no = #{outRefundNo}")
|
||||
int updateRefundStatus(@Param("outRefundNo") String outRefundNo,
|
||||
@Param("refundStatus") String refundStatus,
|
||||
@Param("refundStatusDesc") String refundStatusDesc,
|
||||
@Param("refundId") String refundId,
|
||||
@Param("refundTime") Date refundTime,
|
||||
@Param("updateTime") Date updateTime);
|
||||
}
|
||||
244
src/main/java/com/sczx/pay/sdk/WXPay.java
Normal file
244
src/main/java/com/sczx/pay/sdk/WXPay.java
Normal file
@ -0,0 +1,244 @@
|
||||
package com.sczx.pay.sdk;
|
||||
|
||||
import com.sczx.pay.utils.WXPayUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付接口
|
||||
*/
|
||||
public class WXPay {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WXPay.class);
|
||||
|
||||
private WXPayConfig config;
|
||||
private boolean autoReport;
|
||||
private boolean useSandbox;
|
||||
private String baseUrl = "https://api.mch.weixin.qq.com";
|
||||
|
||||
public WXPay(WXPayConfig config) {
|
||||
this(config, true, false);
|
||||
}
|
||||
|
||||
public WXPay(WXPayConfig config, boolean autoReport) {
|
||||
this(config, autoReport, false);
|
||||
}
|
||||
|
||||
public WXPay(WXPayConfig config, boolean autoReport, boolean useSandbox) {
|
||||
this.config = config;
|
||||
this.autoReport = autoReport;
|
||||
this.useSandbox = useSandbox;
|
||||
if (useSandbox) {
|
||||
this.baseUrl = "https://api.mch.weixin.qq.com/sandboxnew";
|
||||
} else {
|
||||
this.baseUrl = "https://api.mch.weixin.qq.com";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一下单
|
||||
*/
|
||||
public Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
|
||||
return this.unifiedOrder(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
|
||||
}
|
||||
|
||||
public Map<String, String> unifiedOrder(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String url;
|
||||
if (this.useSandbox) {
|
||||
url = this.baseUrl + "/pay/unifiedorder";
|
||||
} else {
|
||||
url = this.baseUrl + "/pay/unifiedorder";
|
||||
}
|
||||
if (this.autoReport) {
|
||||
// TODO: 添加自动上报逻辑
|
||||
}
|
||||
String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
|
||||
return WXPayUtil.xmlToMap(respXml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单查询
|
||||
*/
|
||||
public Map<String, String> orderQuery(Map<String, String> reqData) throws Exception {
|
||||
return this.orderQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
|
||||
}
|
||||
|
||||
public Map<String, String> orderQuery(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String url;
|
||||
if (this.useSandbox) {
|
||||
url = this.baseUrl + "/pay/orderquery";
|
||||
} else {
|
||||
url = this.baseUrl + "/pay/orderquery";
|
||||
}
|
||||
String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
|
||||
return WXPayUtil.xmlToMap(respXml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
*/
|
||||
public Map<String, String> closeOrder(Map<String, String> reqData) throws Exception {
|
||||
return this.closeOrder(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
|
||||
}
|
||||
|
||||
public Map<String, String> closeOrder(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String url;
|
||||
if (this.useSandbox) {
|
||||
url = this.baseUrl + "/pay/closeorder";
|
||||
} else {
|
||||
url = this.baseUrl + "/pay/closeorder";
|
||||
}
|
||||
String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
|
||||
return WXPayUtil.xmlToMap(respXml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
*/
|
||||
public Map<String, String> refund(Map<String, String> reqData) throws Exception {
|
||||
return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
|
||||
}
|
||||
|
||||
public Map<String, String> refund(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String url;
|
||||
if (this.useSandbox) {
|
||||
url = this.baseUrl + "/secapi/pay/refund";
|
||||
} else {
|
||||
url = this.baseUrl + "/secapi/pay/refund";
|
||||
}
|
||||
String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
|
||||
return WXPayUtil.xmlToMap(respXml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款查询
|
||||
*/
|
||||
public Map<String, String> refundQuery(Map<String, String> reqData) throws Exception {
|
||||
return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
|
||||
}
|
||||
|
||||
public Map<String, String> refundQuery(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String url;
|
||||
if (this.useSandbox) {
|
||||
url = this.baseUrl + "/pay/refundquery";
|
||||
} else {
|
||||
url = this.baseUrl + "/pay/refundquery";
|
||||
}
|
||||
String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
|
||||
return WXPayUtil.xmlToMap(respXml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载对账单
|
||||
*/
|
||||
public String downloadBill(Map<String, String> reqData) throws Exception {
|
||||
return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
|
||||
}
|
||||
|
||||
public String downloadBill(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String url;
|
||||
if (this.useSandbox) {
|
||||
url = this.baseUrl + "/pay/downloadbill";
|
||||
} else {
|
||||
url = this.baseUrl + "/pay/downloadbill";
|
||||
}
|
||||
return this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充请求数据(生成签名)
|
||||
*/
|
||||
private Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
|
||||
reqData.put("appid", config.getAppID());
|
||||
reqData.put("mch_id", config.getMchID());
|
||||
if (!reqData.containsKey("nonce_str")) {
|
||||
reqData.put("nonce_str", WXPayUtil.generateNonceStr());
|
||||
}
|
||||
reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey()));
|
||||
return reqData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 无证书请求
|
||||
*/
|
||||
private String requestWithoutCert(String url, Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String xml = WXPayUtil.mapToXml(reqData);
|
||||
logger.info("微信支付请求URL: {}, 请求数据: {}", url, xml);
|
||||
|
||||
String response = httpRequest(url, xml, connectTimeoutMs, readTimeoutMs);
|
||||
logger.info("微信支付响应数据: {}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有证书请求
|
||||
*/
|
||||
private String requestWithCert(String url, Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
String xml = WXPayUtil.mapToXml(reqData);
|
||||
logger.info("微信支付请求URL: {}, 请求数据: {}", url, xml);
|
||||
|
||||
String response = httpRequest(url, xml, connectTimeoutMs, readTimeoutMs);
|
||||
logger.info("微信支付响应数据: {}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP请求
|
||||
*/
|
||||
private String httpRequest(String url, String requestData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
|
||||
java.net.HttpURLConnection connection = null;
|
||||
try {
|
||||
java.net.URL reqUrl = new java.net.URL(url);
|
||||
connection = (java.net.HttpURLConnection) reqUrl.openConnection();
|
||||
|
||||
// 设置请求参数
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(true);
|
||||
connection.setDoInput(true);
|
||||
connection.setUseCaches(false);
|
||||
connection.setRequestProperty("Content-Type", "application/xml;charset=UTF-8");
|
||||
connection.setConnectTimeout(connectTimeoutMs);
|
||||
connection.setReadTimeout(readTimeoutMs);
|
||||
|
||||
// 发送请求数据
|
||||
if (requestData != null) {
|
||||
java.io.OutputStream outputStream = connection.getOutputStream();
|
||||
outputStream.write(requestData.getBytes("UTF-8"));
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
// 读取响应数据
|
||||
java.io.InputStream inputStream = connection.getInputStream();
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream, "UTF-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
reader.close();
|
||||
inputStream.close();
|
||||
|
||||
return response.toString();
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断支付结果
|
||||
*/
|
||||
public boolean isResponseSignatureValid(Map<String, String> respData) {
|
||||
try {
|
||||
return WXPayUtil.isSignatureValid(respData, this.config.getKey());
|
||||
} catch (Exception e) {
|
||||
logger.error("验证响应签名异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/sczx/pay/sdk/WXPayConfig.java
Normal file
43
src/main/java/com/sczx/pay/sdk/WXPayConfig.java
Normal file
@ -0,0 +1,43 @@
|
||||
package com.sczx.pay.sdk;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 微信支付配置抽象类
|
||||
*/
|
||||
public abstract class WXPayConfig {
|
||||
|
||||
/**
|
||||
* 获取 App ID
|
||||
*/
|
||||
public abstract String getAppID();
|
||||
|
||||
/**
|
||||
* 获取商户号
|
||||
*/
|
||||
public abstract String getMchID();
|
||||
|
||||
/**
|
||||
* 获取 API 密钥
|
||||
*/
|
||||
public abstract String getKey();
|
||||
|
||||
/**
|
||||
* 获取证书内容
|
||||
*/
|
||||
public abstract InputStream getCertStream();
|
||||
|
||||
/**
|
||||
* HTTP(S) 连接超时时间,单位毫秒
|
||||
*/
|
||||
public int getHttpConnectTimeoutMs() {
|
||||
return 6*1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP(S) 读数据超时时间,单位毫秒
|
||||
*/
|
||||
public int getHttpReadTimeoutMs() {
|
||||
return 8*1000;
|
||||
}
|
||||
}
|
||||
433
src/main/java/com/sczx/pay/service/WechatPayService.java
Normal file
433
src/main/java/com/sczx/pay/service/WechatPayService.java
Normal file
@ -0,0 +1,433 @@
|
||||
package com.sczx.pay.service;
|
||||
|
||||
import com.sczx.pay.config.DynamicWXPayConfig;
|
||||
import com.sczx.pay.dto.PaymentRequest;
|
||||
import com.sczx.pay.dto.PaymentResponse;
|
||||
import com.sczx.pay.dto.RefundRequest;
|
||||
import com.sczx.pay.entity.CompanyWechatConfig;
|
||||
import com.sczx.pay.entity.PaymentRecord;
|
||||
import com.sczx.pay.entity.RefundRecord;
|
||||
import com.sczx.pay.mapper.CompanyWechatConfigMapper;
|
||||
import com.sczx.pay.mapper.PaymentRecordMapper;
|
||||
import com.sczx.pay.mapper.RefundRecordMapper;
|
||||
import com.sczx.pay.sdk.WXPay;
|
||||
import com.sczx.pay.utils.WXPayUtil;
|
||||
import com.sczx.pay.utils.IPUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付服务类
|
||||
*/
|
||||
@Service
|
||||
public class WechatPayService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WechatPayService.class);
|
||||
|
||||
@Autowired
|
||||
private CompanyWechatConfigMapper companyWechatConfigMapper;
|
||||
|
||||
@Autowired
|
||||
private PaymentRecordMapper paymentRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private RefundRecordMapper refundRecordMapper;
|
||||
|
||||
@Value("${wechat.pay.app-id}")
|
||||
private String appId;
|
||||
|
||||
@Value("${wechat.pay.notify-url}")
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 小程序统一下单
|
||||
*/
|
||||
@Transactional
|
||||
public PaymentResponse unifiedOrder(PaymentRequest request) {
|
||||
PaymentResponse response = new PaymentResponse();
|
||||
|
||||
try {
|
||||
// 根据companyId获取微信支付配置
|
||||
CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(request.getCompanyId());
|
||||
if (companyConfig == null) {
|
||||
response.setCode("FAIL");
|
||||
response.setMessage("未找到公司对应的微信支付配置");
|
||||
return response;
|
||||
}
|
||||
|
||||
// 创建动态配置
|
||||
DynamicWXPayConfig wxPayConfig = new DynamicWXPayConfig(
|
||||
appId,
|
||||
companyConfig.getMchId(),
|
||||
companyConfig.getApikey(),
|
||||
notifyUrl
|
||||
);
|
||||
|
||||
WXPay wxPay = new WXPay(wxPayConfig);
|
||||
|
||||
// 构造请求参数
|
||||
Map<String, String> reqData = new HashMap<>();
|
||||
reqData.put("body", request.getBody());
|
||||
reqData.put("out_trade_no", request.getOutTradeNo());
|
||||
reqData.put("total_fee", String.valueOf(request.getTotalFee()));
|
||||
// 自动获取服务器公网IP
|
||||
reqData.put("spbill_create_ip", IPUtils.getServerPublicIP());
|
||||
reqData.put("notify_url", notifyUrl);
|
||||
reqData.put("trade_type", "JSAPI");
|
||||
reqData.put("openid", request.getOpenId());
|
||||
|
||||
if (request.getAttach() != null) {
|
||||
reqData.put("attach", request.getAttach());
|
||||
}
|
||||
|
||||
// 调用微信统一下单接口
|
||||
Map<String, String> result = wxPay.unifiedOrder(reqData);
|
||||
|
||||
logger.info("微信统一下单结果: {}", result);
|
||||
|
||||
// 处理返回结果
|
||||
if ("SUCCESS".equals(result.get("return_code"))) {
|
||||
if ("SUCCESS".equals(result.get("result_code"))) {
|
||||
// 构造小程序支付参数
|
||||
Map<String, String> payData = buildPayData(result.get("prepay_id"), wxPayConfig);
|
||||
response.setCode("SUCCESS");
|
||||
response.setMessage("下单成功");
|
||||
response.setPayData(payData);
|
||||
|
||||
// 记录支付信息到数据库
|
||||
recordPaymentInfo(request, companyConfig);
|
||||
} else {
|
||||
response.setCode("FAIL");
|
||||
response.setMessage(result.get("err_code_des"));
|
||||
}
|
||||
} else {
|
||||
response.setCode("FAIL");
|
||||
response.setMessage(result.get("return_msg"));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("微信统一下单异常", e);
|
||||
response.setCode("ERROR");
|
||||
response.setMessage("系统异常: " + e.getMessage());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录支付信息到数据库
|
||||
*/
|
||||
private void recordPaymentInfo(PaymentRequest request, CompanyWechatConfig companyConfig) {
|
||||
try {
|
||||
PaymentRecord paymentRecord = new PaymentRecord();
|
||||
paymentRecord.setCompanyId(request.getCompanyId());
|
||||
paymentRecord.setOutTradeNo(request.getOutTradeNo());
|
||||
paymentRecord.setTotalFee(new BigDecimal(request.getTotalFee()).divide(new BigDecimal(100))); // 转换为元
|
||||
paymentRecord.setBody(request.getBody());
|
||||
paymentRecord.setOpenid(request.getOpenId());
|
||||
paymentRecord.setTradeState("NOTPAY"); // 未支付
|
||||
paymentRecord.setTradeStateDesc("未支付");
|
||||
paymentRecord.setCreateTime(new Date());
|
||||
paymentRecord.setUpdateTime(new Date());
|
||||
paymentRecord.setAttach(request.getAttach());
|
||||
paymentRecord.setPayChannel(PaymentRecord.PayChannel.WECHAT.name()); // 设置支付渠道为微信支付
|
||||
|
||||
paymentRecordMapper.insertPaymentRecord(paymentRecord);
|
||||
logger.info("支付记录已保存,订单号: {}", request.getOutTradeNo());
|
||||
} catch (Exception e) {
|
||||
logger.error("保存支付记录异常,订单号: {}", request.getOutTradeNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单
|
||||
*/
|
||||
public Map<String, String> orderQuery(Long companyId, String outTradeNo) throws Exception {
|
||||
// 根据companyId获取微信支付配置
|
||||
CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
|
||||
if (companyConfig == null) {
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "未找到公司对应的微信支付配置");
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
// 创建动态配置
|
||||
DynamicWXPayConfig wxPayConfig = new DynamicWXPayConfig(
|
||||
appId,
|
||||
companyConfig.getMchId(),
|
||||
companyConfig.getApikey(),
|
||||
notifyUrl
|
||||
);
|
||||
|
||||
WXPay wxPay = new WXPay(wxPayConfig);
|
||||
|
||||
Map<String, String> reqData = new HashMap<>();
|
||||
reqData.put("out_trade_no", outTradeNo);
|
||||
|
||||
return wxPay.orderQuery(reqData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
*/
|
||||
public Map<String, String> closeOrder(Long companyId, String outTradeNo) throws Exception {
|
||||
// 根据companyId获取微信支付配置
|
||||
CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
|
||||
if (companyConfig == null) {
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "未找到公司对应的微信支付配置");
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
// 创建动态配置
|
||||
DynamicWXPayConfig wxPayConfig = new DynamicWXPayConfig(
|
||||
appId,
|
||||
companyConfig.getMchId(),
|
||||
companyConfig.getApikey(),
|
||||
notifyUrl
|
||||
);
|
||||
|
||||
WXPay wxPay = new WXPay(wxPayConfig);
|
||||
|
||||
Map<String, String> reqData = new HashMap<>();
|
||||
reqData.put("out_trade_no", outTradeNo);
|
||||
|
||||
return wxPay.closeOrder(reqData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, String> refund(RefundRequest request) throws Exception {
|
||||
// 根据companyId获取微信支付配置
|
||||
CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(request.getCompanyId());
|
||||
if (companyConfig == null) {
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "未找到公司对应的微信支付配置");
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
// 创建动态配置
|
||||
DynamicWXPayConfig wxPayConfig = new DynamicWXPayConfig(
|
||||
appId,
|
||||
companyConfig.getMchId(),
|
||||
companyConfig.getApikey(),
|
||||
notifyUrl
|
||||
);
|
||||
|
||||
WXPay wxPay = new WXPay(wxPayConfig);
|
||||
|
||||
Map<String, String> reqData = new HashMap<>();
|
||||
reqData.put("out_trade_no", request.getOutTradeNo());
|
||||
reqData.put("out_refund_no", request.getOutRefundNo());
|
||||
reqData.put("total_fee", String.valueOf(request.getTotalFee()));
|
||||
reqData.put("refund_fee", String.valueOf(request.getRefundFee()));
|
||||
|
||||
if (request.getRefundDesc() != null) {
|
||||
reqData.put("refund_desc", request.getRefundDesc());
|
||||
}
|
||||
|
||||
// 退款需要证书,这里调用带证书的接口
|
||||
Map<String, String> result = wxPay.refund(reqData);
|
||||
|
||||
// 记录退款信息到数据库
|
||||
if ("SUCCESS".equals(result.get("return_code"))) {
|
||||
recordRefundInfo(request, companyConfig, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录退款信息到数据库
|
||||
*/
|
||||
private void recordRefundInfo(RefundRequest request, CompanyWechatConfig companyConfig, Map<String, String> result) {
|
||||
try {
|
||||
RefundRecord refundRecord = new RefundRecord();
|
||||
refundRecord.setCompanyId(request.getCompanyId());
|
||||
refundRecord.setOutTradeNo(request.getOutTradeNo());
|
||||
refundRecord.setOutRefundNo(request.getOutRefundNo());
|
||||
refundRecord.setTotalFee(new BigDecimal(request.getTotalFee()).divide(new BigDecimal(100))); // 转换为元
|
||||
refundRecord.setRefundFee(new BigDecimal(request.getRefundFee()).divide(new BigDecimal(100))); // 转换为元
|
||||
refundRecord.setRefundDesc(request.getRefundDesc());
|
||||
refundRecord.setCreateTime(new Date());
|
||||
refundRecord.setUpdateTime(new Date());
|
||||
|
||||
if ("SUCCESS".equals(result.get("result_code"))) {
|
||||
refundRecord.setRefundStatus("PROCESSING"); // 退款处理中
|
||||
refundRecord.setRefundStatusDesc("退款处理中");
|
||||
refundRecord.setRefundId(result.get("refund_id"));
|
||||
} else {
|
||||
refundRecord.setRefundStatus("FAIL"); // 退款失败
|
||||
refundRecord.setRefundStatusDesc(result.get("err_code_des"));
|
||||
}
|
||||
|
||||
refundRecordMapper.insertRefundRecord(refundRecord);
|
||||
logger.info("退款记录已保存,退款单号: {}", request.getOutRefundNo());
|
||||
} catch (Exception e) {
|
||||
logger.error("保存退款记录异常,退款单号: {}", request.getOutRefundNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款
|
||||
*/
|
||||
public Map<String, String> refundQuery(Long companyId, String outTradeNo) throws Exception {
|
||||
// 根据companyId获取微信支付配置
|
||||
CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
|
||||
if (companyConfig == null) {
|
||||
Map<String, String> errorResult = new HashMap<>();
|
||||
errorResult.put("return_code", "FAIL");
|
||||
errorResult.put("return_msg", "未找到公司对应的微信支付配置");
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
// 创建动态配置
|
||||
DynamicWXPayConfig wxPayConfig = new DynamicWXPayConfig(
|
||||
appId,
|
||||
companyConfig.getMchId(),
|
||||
companyConfig.getApikey(),
|
||||
notifyUrl
|
||||
);
|
||||
|
||||
WXPay wxPay = new WXPay(wxPayConfig);
|
||||
|
||||
Map<String, String> reqData = new HashMap<>();
|
||||
reqData.put("out_trade_no", outTradeNo);
|
||||
|
||||
return wxPay.refundQuery(reqData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造小程序支付参数
|
||||
*/
|
||||
private Map<String, String> buildPayData(String prepayId, DynamicWXPayConfig wxPayConfig) throws Exception {
|
||||
Map<String, String> payData = new HashMap<>();
|
||||
payData.put("appId", wxPayConfig.getAppID());
|
||||
payData.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
|
||||
payData.put("nonceStr", WXPayUtil.generateNonceStr());
|
||||
payData.put("package", "prepay_id=" + prepayId);
|
||||
payData.put("signType", "MD5");
|
||||
|
||||
// 生成签名
|
||||
String paySign = WXPayUtil.generateSignature(payData, wxPayConfig.getKey());
|
||||
payData.put("paySign", paySign);
|
||||
|
||||
return payData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证微信支付通知签名
|
||||
*/
|
||||
public boolean verifyNotifySign(Long companyId, Map<String, String> notifyData) {
|
||||
try {
|
||||
// 根据companyId获取微信支付配置
|
||||
CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
|
||||
if (companyConfig == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return WXPayUtil.isSignatureValid(notifyData, companyConfig.getApikey());
|
||||
} catch (Exception e) {
|
||||
logger.error("验证微信支付通知签名异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付成功通知并更新订单状态
|
||||
*/
|
||||
@Transactional
|
||||
public boolean processPaySuccessNotify(Long companyId, Map<String, String> notifyData) {
|
||||
try {
|
||||
String outTradeNo = notifyData.get("out_trade_no");
|
||||
String transactionId = notifyData.get("transaction_id");
|
||||
String totalFee = notifyData.get("total_fee");
|
||||
|
||||
// 更新支付记录状态
|
||||
int updated = paymentRecordMapper.updateToSuccess(
|
||||
outTradeNo,
|
||||
transactionId,
|
||||
new Date(), // 支付时间
|
||||
new Date() // 更新时间
|
||||
);
|
||||
|
||||
if (updated > 0) {
|
||||
logger.info("微信支付记录状态已更新,订单号: {}, 微信交易号: {}", outTradeNo, transactionId);
|
||||
// TODO: 在这里调用其他业务服务更新实际订单状态
|
||||
return true;
|
||||
} else {
|
||||
logger.warn("未找到对应的微信支付记录,订单号: {}", outTradeNo);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("处理微信支付成功通知异常,订单号: {}", notifyData.get("out_trade_no"), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退款成功通知并更新退款状态
|
||||
*/
|
||||
@Transactional
|
||||
public boolean processRefundSuccessNotify(Long companyId, Map<String, String> notifyData) {
|
||||
try {
|
||||
String outRefundNo = notifyData.get("out_refund_no");
|
||||
String refundId = notifyData.get("refund_id");
|
||||
String refundStatus = notifyData.get("refund_status");
|
||||
|
||||
// 根据退款状态更新退款记录
|
||||
String statusDesc = "";
|
||||
switch (refundStatus) {
|
||||
case "SUCCESS":
|
||||
statusDesc = "退款成功";
|
||||
break;
|
||||
case "REFUNDCLOSE":
|
||||
statusDesc = "退款关闭";
|
||||
break;
|
||||
case "PROCESSING":
|
||||
statusDesc = "退款处理中";
|
||||
break;
|
||||
case "CHANGE":
|
||||
statusDesc = "退款异常";
|
||||
break;
|
||||
default:
|
||||
statusDesc = "未知状态";
|
||||
}
|
||||
|
||||
int updated = refundRecordMapper.updateRefundStatus(
|
||||
outRefundNo,
|
||||
refundStatus,
|
||||
statusDesc,
|
||||
refundId,
|
||||
"SUCCESS".equals(refundStatus) ? new Date() : null, // 退款成功时间
|
||||
new Date() // 更新时间
|
||||
);
|
||||
|
||||
if (updated > 0) {
|
||||
logger.info("退款记录状态已更新,退款单号: {}, 微信退款单号: {}, 状态: {}", outRefundNo, refundId, refundStatus);
|
||||
// TODO: 在这里调用其他业务服务更新实际订单退款状态
|
||||
return true;
|
||||
} else {
|
||||
logger.warn("未找到对应的退款记录,退款单号: {}", outRefundNo);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("处理退款成功通知异常,退款单号: {}", notifyData.get("out_refund_no"), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/main/java/com/sczx/pay/utils/ComputerInfo.java
Normal file
159
src/main/java/com/sczx/pay/utils/ComputerInfo.java
Normal file
@ -0,0 +1,159 @@
|
||||
package com.sczx.pay.utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/*
|
||||
* <取网卡物理地址--
|
||||
* 1.在Windows,Linux系统下均可用;
|
||||
* 2.通过ipconifg,ifconfig获得计算机信息;
|
||||
* 3.再用模式匹配方式查找MAC地址,与操作系统的语言无关>
|
||||
*
|
||||
* //* Description: <取计算机名--从环境变量中取>
|
||||
* abstract 限制继承/创建实例
|
||||
*/
|
||||
public abstract class ComputerInfo {
|
||||
private static String macAddressStr = null;
|
||||
private static String computerName = System.getenv().get("COMPUTERNAME");
|
||||
|
||||
private static final String[] windowsCommand = { "ipconfig", "/all" };
|
||||
private static final String[] linuxCommand = { "/sbin/ifconfig", "-a" };
|
||||
private static final String[] macCommand = { "ifconfig", "-a" };
|
||||
private static final Pattern macPattern = Pattern.compile(".*((:?[0-9a-f]{2}[-:]){5}[0-9a-f]{2}).*",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 获取多个网卡地址
|
||||
*
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private final static List<String> getMacAddressList() throws IOException {
|
||||
final ArrayList<String> macAddressList = new ArrayList<String>();
|
||||
final String os = System.getProperty("os.name");
|
||||
final String command[];
|
||||
|
||||
if (os.startsWith("Windows")) {
|
||||
command = windowsCommand;
|
||||
} else if (os.startsWith("Linux")) {
|
||||
command = linuxCommand;
|
||||
} else if (os.startsWith("Mac")){
|
||||
command = macCommand;
|
||||
}
|
||||
else {
|
||||
throw new IOException("Unknow operating system:" + os);
|
||||
}
|
||||
// 执行命令
|
||||
final Process process = Runtime.getRuntime().exec(command);
|
||||
|
||||
BufferedReader bufReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
for (String line = null; (line = bufReader.readLine()) != null;) {
|
||||
Matcher matcher = macPattern.matcher(line);
|
||||
if (matcher.matches()) {
|
||||
macAddressList.add(matcher.group(1));
|
||||
// macAddressList.add(matcher.group(1).replaceAll("[-:]",
|
||||
// ""));//去掉MAC中的“-”
|
||||
}
|
||||
}
|
||||
|
||||
process.destroy();
|
||||
bufReader.close();
|
||||
return macAddressList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个网卡地址(多个网卡时从中获取一个)
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getMacAddress() {
|
||||
if (macAddressStr == null || macAddressStr.equals("")) {
|
||||
StringBuffer sb = new StringBuffer(); // 存放多个网卡地址用,目前只取一个非0000000000E0隧道的值
|
||||
try {
|
||||
List<String> macList = getMacAddressList();
|
||||
for (Iterator<String> iter = macList.iterator(); iter.hasNext();) {
|
||||
String amac = iter.next();
|
||||
if (!"0000000000E0".equals(amac)) {
|
||||
sb.append(amac);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
macAddressStr = sb.toString();
|
||||
|
||||
}
|
||||
|
||||
return macAddressStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电脑名
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getComputerName() {
|
||||
if (computerName == null || computerName.equals("")) {
|
||||
computerName = System.getenv().get("COMPUTERNAME");
|
||||
}
|
||||
return computerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getIpAddrAndName() throws IOException {
|
||||
return InetAddress.getLocalHost().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getIpAddr() throws IOException {
|
||||
return InetAddress.getLocalHost().getHostAddress().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电脑唯一标识
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getComputerID() {
|
||||
String id = getMacAddress();
|
||||
if (id == null || id.equals("")) {
|
||||
try {
|
||||
id = getIpAddrAndName();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return computerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制创建实例
|
||||
*/
|
||||
private ComputerInfo() {
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
System.out.println(ComputerInfo.getMacAddress());
|
||||
System.out.println(ComputerInfo.getComputerName());
|
||||
System.out.println(ComputerInfo.getIpAddr());
|
||||
System.out.println(ComputerInfo.getIpAddrAndName());
|
||||
}
|
||||
}
|
||||
135
src/main/java/com/sczx/pay/utils/IPUtils.java
Normal file
135
src/main/java/com/sczx/pay/utils/IPUtils.java
Normal file
@ -0,0 +1,135 @@
|
||||
package com.sczx.pay.utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* IP地址工具类
|
||||
*/
|
||||
public class IPUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(IPUtils.class);
|
||||
|
||||
// 多个IP查询服务,以防某个服务不可用
|
||||
private static final String[] IP_SERVICES = {
|
||||
"https://api.ipify.org",
|
||||
"https://icanhazip.com",
|
||||
"https://ident.me"
|
||||
};
|
||||
|
||||
private static String cachedPublicIP = null;
|
||||
private static long lastFetchTime = 0;
|
||||
private static final long CACHE_DURATION = 30 * 60 * 1000; // 30分钟缓存
|
||||
|
||||
/**
|
||||
* 获取服务器公网IP地址
|
||||
* @return 公网IP地址
|
||||
*/
|
||||
public static String getServerPublicIP() {
|
||||
// 检查缓存
|
||||
if (cachedPublicIP != null && (System.currentTimeMillis() - lastFetchTime) < CACHE_DURATION) {
|
||||
return cachedPublicIP;
|
||||
}
|
||||
|
||||
// 首先尝试从外部服务获取公网IP
|
||||
String publicIP = fetchPublicIPFromExternalService();
|
||||
if (publicIP != null && !publicIP.isEmpty()) {
|
||||
cachedPublicIP = publicIP;
|
||||
lastFetchTime = System.currentTimeMillis();
|
||||
return publicIP;
|
||||
}
|
||||
|
||||
// 如果外部服务不可用,则获取本地IP
|
||||
try {
|
||||
String localIP = InetAddress.getLocalHost().getHostAddress();
|
||||
logger.warn("无法获取公网IP,使用本地IP: {}", localIP);
|
||||
return localIP;
|
||||
} catch (UnknownHostException e) {
|
||||
logger.error("获取本地IP失败", e);
|
||||
return "127.0.0.1";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从外部服务获取公网IP
|
||||
* @return 公网IP地址
|
||||
*/
|
||||
private static String fetchPublicIPFromExternalService() {
|
||||
for (String serviceUrl : IP_SERVICES) {
|
||||
try {
|
||||
String ip = fetchIPFromService(serviceUrl);
|
||||
if (isValidIP(ip)) {
|
||||
logger.info("成功从 {} 获取公网IP: {}", serviceUrl, ip);
|
||||
return ip;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("从服务 {} 获取IP失败: {}", serviceUrl, e.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定服务获取IP
|
||||
* @param serviceUrl 服务URL
|
||||
* @return IP地址
|
||||
* @throws Exception 网络异常
|
||||
*/
|
||||
private static String fetchIPFromService(String serviceUrl) throws Exception {
|
||||
URL url = new URL(serviceUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream()))) {
|
||||
return reader.readLine().trim();
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证IP地址是否有效
|
||||
* @param ip IP地址
|
||||
* @return 是否有效
|
||||
*/
|
||||
private static boolean isValidIP(String ip) {
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 简单的IP格式验证
|
||||
String[] parts = ip.split("\\.");
|
||||
if (parts.length != 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String part : parts) {
|
||||
try {
|
||||
int num = Integer.parseInt(part);
|
||||
if (num < 0 || num > 255) {
|
||||
return false;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 排除私有IP和回环地址
|
||||
if (ip.startsWith("127.") || ip.startsWith("10.") ||
|
||||
ip.startsWith("192.168.") || ip.startsWith("172.")) {
|
||||
// 私有地址也可以接受,因为可能在内网环境中使用
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
167
src/main/java/com/sczx/pay/utils/WXPayUtil.java
Normal file
167
src/main/java/com/sczx/pay/utils/WXPayUtil.java
Normal file
@ -0,0 +1,167 @@
|
||||
package com.sczx.pay.utils;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
public class WXPayUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WXPayUtil.class);
|
||||
private static final String CHARSET = "UTF-8";
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
public static String generateNonceStr() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
public static String generateSignature(Map<String, String> data, String key) throws Exception {
|
||||
return generateSignature(data, key, "MD5");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
public static String generateSignature(Map<String, String> data, String key, String signType) throws Exception {
|
||||
Set<String> keySet = data.keySet();
|
||||
String[] keyArray = keySet.toArray(new String[0]);
|
||||
Arrays.sort(keyArray);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String k : keyArray) {
|
||||
if (k.equals("sign")) {
|
||||
continue;
|
||||
}
|
||||
String value = data.get(k);
|
||||
if (value != null && value.trim().length() > 0) {
|
||||
sb.append(k).append("=").append(value.trim()).append("&");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("key=").append(key);
|
||||
|
||||
if ("MD5".equals(signType)) {
|
||||
return DigestUtils.md5Hex(sb.toString()).toUpperCase();
|
||||
} else if ("HMAC-SHA256".equals(signType)) {
|
||||
return HMACSHA256(sb.toString(), key);
|
||||
} else {
|
||||
throw new Exception("Invalid sign_type: " + signType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC-SHA256签名
|
||||
*/
|
||||
private static String HMACSHA256(String data, String key) throws Exception {
|
||||
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(CHARSET), "HmacSHA256");
|
||||
sha256_HMAC.init(secret_key);
|
||||
byte[] array = sha256_HMAC.doFinal(data.getBytes(CHARSET));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte item : array) {
|
||||
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
|
||||
}
|
||||
return sb.toString().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证签名
|
||||
*/
|
||||
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
|
||||
if (!data.containsKey("sign")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String sign = data.get("sign");
|
||||
String calculatedSign = generateSignature(data, key);
|
||||
return sign.equals(calculatedSign);
|
||||
}
|
||||
|
||||
/**
|
||||
* XML字符串转换为Map
|
||||
*/
|
||||
public static Map<String, String> xmlToMap(String strXML) throws Exception {
|
||||
Map<String, String> data = new HashMap<>();
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
Document doc = documentBuilder.parse(new ByteArrayInputStream(strXML.getBytes(CHARSET)));
|
||||
|
||||
doc.getDocumentElement().normalize();
|
||||
NodeList nodeList = doc.getDocumentElement().getChildNodes();
|
||||
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
Node node = nodeList.item(i);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
Element element = (Element) node;
|
||||
data.put(element.getNodeName(), element.getTextContent());
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map转换为XML字符串
|
||||
*/
|
||||
public static String mapToXml(Map<String, String> data) throws Exception {
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
Document document = documentBuilder.newDocument();
|
||||
|
||||
Element root = document.createElement("xml");
|
||||
document.appendChild(root);
|
||||
|
||||
for (String key : data.keySet()) {
|
||||
String value = data.get(key);
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
|
||||
Element filed = document.createElement(key);
|
||||
filed.appendChild(document.createTextNode(value));
|
||||
root.appendChild(filed);
|
||||
}
|
||||
|
||||
TransformerFactory tf = TransformerFactory.newInstance();
|
||||
Transformer transformer = tf.newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
DOMSource source = new DOMSource(document);
|
||||
StringWriter writer = new StringWriter();
|
||||
StreamResult result = new StreamResult(writer);
|
||||
transformer.transform(source, result);
|
||||
|
||||
return writer.getBuffer().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证微信支付通知签名
|
||||
*/
|
||||
public static boolean isSignatureValid(String xmlString, String key) throws Exception {
|
||||
Map<String, String> data = xmlToMap(xmlString);
|
||||
return isSignatureValid(data, key);
|
||||
}
|
||||
}
|
||||
63
src/main/resources/application.yml
Normal file
63
src/main/resources/application.yml
Normal file
@ -0,0 +1,63 @@
|
||||
|
||||
server:
|
||||
port: 8019
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: sczx-pay # 微服务名称
|
||||
http:
|
||||
encoding:
|
||||
charset: UTF-8
|
||||
enabled: true
|
||||
force: true
|
||||
mvc:
|
||||
async:
|
||||
request-timeout: -1
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 127.0.0.1:8848 # Nacos 地址
|
||||
group: DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
env: dev
|
||||
lifecycle:
|
||||
timeout-per-shutdown-phase: 30s # 设置优雅停机时间
|
||||
datasource:
|
||||
url: jdbc:mysql://115.190.8.52:3306/sczx?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
|
||||
username: sczx_user
|
||||
password: Sczx123@
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
auto-commit: true
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*" # 暴露所有监控端点
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
|
||||
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*.xml
|
||||
type-aliases-package: com.sczx.pay.po # 实体类包路径
|
||||
configuration:
|
||||
mapUnderscoreToCamelCase: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL(调试用)
|
||||
|
||||
|
||||
wechat:
|
||||
pay:
|
||||
app-id: your_app_id
|
||||
mch-id: your_mch_id
|
||||
key: your_api_key
|
||||
notify-url: https://yourdomain.com/api/payment/notify
|
||||
Reference in New Issue
Block a user