no message

This commit is contained in:
2025-08-21 17:33:28 +08:00
commit 9d744548d1
27 changed files with 2880 additions and 0 deletions

View 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"));
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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>";
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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);
}

View 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);
}

View 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);
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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());
}
}

View 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;
}
}

View 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);
}
}