commit 9d744548d1b89463911faba68f9507d16cf96721
Author: eric <465889110@qq.com>
Date: Thu Aug 21 17:33:28 2025 +0800
no message
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..fd863fa
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+# 使用 OpenJDK 8 镜像构建
+FROM openjdk:8-jdk
+# 添加作者信息
+LABEL maintainer="123879394@qq.com"
+
+WORKDIR /app
+# 复制 jar 包
+COPY target/*.jar app.jar
+# 设置 JVM 参数和启动命令
+ENTRYPOINT ["java", "-jar", "-Xms64m", "-Xmx128m", "app.jar"]
\ No newline at end of file
diff --git a/Dockerfile.buildagent b/Dockerfile.buildagent
new file mode 100644
index 0000000..f893f6c
--- /dev/null
+++ b/Dockerfile.buildagent
@@ -0,0 +1,15 @@
+# 使用 JDK 8 作为构建环境
+FROM openjdk:8-jdk
+
+# 使用阿里云的 apt 镜像源(Debian 11 bullseye)
+RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
+ sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
+
+# 更新包列表并安装 Maven 和 Git
+RUN apt update && \
+ apt install -y maven git && \
+ mvn --version && \
+ git --version
+
+# 设置工作目录
+WORKDIR /home/jenkins/workspace
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..143d9da
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,85 @@
+pipeline {
+ agent any
+// tools {
+// maven 'M3' // 必须在 Jenkins → Manage Jenkins → Global Tool Configuration 中配置过
+// }
+ environment {
+ APP_NAME = "sczx_singlepay"
+ DOCKER_IMAGE = "${APP_NAME}:latest"
+ CONTAINER_NAME = "${APP_NAME}-container"
+ }
+
+ stages {
+// stage('Checkout') {
+// steps {
+// echo "📦 正在拉取代码..."
+// git branch: 'main', url: 'http://115.190.8.52:3000/sczx_group/sczx_order.git'
+// }
+// }
+
+ stage('Build with Maven in JDK 8') {
+ agent {
+ dockerfile {
+ filename "Dockerfile.buildagent"
+ }
+ }
+ steps {
+ echo "🛠️ 正在使用 Maven 构建..."
+ sh 'mvn clean package -s settings.xml'
+ }
+ }
+
+ stage('Check Jar File') {
+ agent any
+ steps {
+ sh 'ls -la target/' // 确保 jar 文件存在
+ }
+ }
+
+ stage('Build Docker Image') {
+ agent any
+ steps {
+ echo "🐋 正在构建 Docker 镜像..."
+ sh 'docker build -t "$DOCKER_IMAGE" .'
+ }
+ }
+
+ stage('Stop Old Container') {
+ agent any
+ steps {
+ echo "🛑 正在停止旧的容器(如果存在)..."
+ sh '''
+ if [ "$(docker ps -f 'name=sczx_singlepay-container' --format '{{.Status}}')" ]; then
+ docker stop sczx_singlepay-container
+ docker rm sczx_singlepay-container
+ fi
+ '''
+ }
+ }
+
+ stage('Run New Container') {
+ agent any
+ steps {
+ echo "🟢 正在运行新的容器..."
+ sh """
+ docker run -d \
+ --name \${CONTAINER_NAME} \
+ --network sczx-net \
+ -p 8016:8016 \
+ -e JAVA_OPTS="-Xms256m -Xmx512m -Duser.timezone=Asia/Shanghai" \
+ --restart always \
+ \${DOCKER_IMAGE}
+ """
+ }
+ }
+ }
+
+ post {
+ success {
+ echo "🎉 构建成功!"
+ }
+ failure {
+ echo "❌ 构建失败,请检查日志!"
+ }
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..cdbb910
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,255 @@
+
+
+ 4.0.0
+
+ com.sczx
+ sczx_singlepay
+ 1.0.0
+ jar
+
+ sczx_singlepay
+ sczx_singlepay service
+
+
+ 1.8
+ 2.3.12.RELEASE
+ Hoxton.SR12
+ 2.2.9.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.12.RELEASE
+
+
+
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-alibaba-dependencies
+ ${spring-cloud-alibaba.version}
+ pom
+ import
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+ org.springframework.cloud
+ spring-cloud-commons
+
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+
+
+
+ org.springframework.retry
+ spring-retry
+ 1.3.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 2.3.1
+
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ 3.5.3.1
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ mysql
+ mysql-connector-java
+ 8.0.33
+
+
+
+
+ io.springfox
+ springfox-swagger2
+ 2.9.2
+
+
+
+ io.springfox
+ springfox-swagger-ui
+ 2.9.2
+
+
+
+
+ javax.xml.bind
+ jaxb-api
+ 2.3.1
+
+
+
+ io.projectreactor
+ reactor-core
+ 3.2.2.RELEASE
+
+
+
+
+ org.mapstruct
+ mapstruct
+ 1.5.5.Final
+
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+ provided
+
+
+
+
+ com.alibaba
+ fastjson
+ 1.2.83
+
+
+
+
+ com.github.wechatpay-apiv3
+ wechatpay-java
+ 0.2.14
+
+
+
+
+ commons-codec
+ commons-codec
+ 1.15
+
+
+
+
+
+
+ sczx_singlepay
+
+
+ src/main/resources
+
+ *.yml
+
+ true
+
+
+ src/main/resources
+
+ *.yml
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 2.3.12.RELEASE
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sczx_singlepay.iml b/sczx_singlepay.iml
new file mode 100644
index 0000000..988da39
--- /dev/null
+++ b/sczx_singlepay.iml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.xml b/settings.xml
new file mode 100644
index 0000000..06004f8
--- /dev/null
+++ b/settings.xml
@@ -0,0 +1,12 @@
+
+
+
+ aliyun-maven
+ *
+ Aliyun Maven
+ https://maven.aliyun.com/repository/public
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/sczx/pay/Application.java b/src/main/java/com/sczx/pay/Application.java
new file mode 100644
index 0000000..b122ee2
--- /dev/null
+++ b/src/main/java/com/sczx/pay/Application.java
@@ -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"));
+ }
+}
diff --git a/src/main/java/com/sczx/pay/config/DynamicWXPayConfig.java b/src/main/java/com/sczx/pay/config/DynamicWXPayConfig.java
new file mode 100644
index 0000000..4ac799f
--- /dev/null
+++ b/src/main/java/com/sczx/pay/config/DynamicWXPayConfig.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/config/SwaggerConfig.java b/src/main/java/com/sczx/pay/config/SwaggerConfig.java
new file mode 100644
index 0000000..21050f4
--- /dev/null
+++ b/src/main/java/com/sczx/pay/config/SwaggerConfig.java
@@ -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();
+ }
+}
diff --git a/src/main/java/com/sczx/pay/config/WXPayConfigImpl.java b/src/main/java/com/sczx/pay/config/WXPayConfigImpl.java
new file mode 100644
index 0000000..6818094
--- /dev/null
+++ b/src/main/java/com/sczx/pay/config/WXPayConfigImpl.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/controller/PaymentController.java b/src/main/java/com/sczx/pay/controller/PaymentController.java
new file mode 100644
index 0000000..f30e0a5
--- /dev/null
+++ b/src/main/java/com/sczx/pay/controller/PaymentController.java
@@ -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 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 errorResult = new HashMap<>();
+ errorResult.put("return_code", "FAIL");
+ errorResult.put("return_msg", "查询异常: " + e.getMessage());
+ return errorResult;
+ }
+ }
+
+ /**
+ * 关闭订单接口
+ */
+ @PostMapping("/close/{companyId}/{outTradeNo}")
+ public Map 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 errorResult = new HashMap<>();
+ errorResult.put("return_code", "FAIL");
+ errorResult.put("return_msg", "关闭异常: " + e.getMessage());
+ return errorResult;
+ }
+ }
+
+ /**
+ * 申请退款接口
+ */
+ @PostMapping("/refund")
+ public Map refund(@RequestBody RefundRequest request) {
+ logger.info("收到退款请求: {}", request);
+ try {
+ return wechatPayService.refund(request);
+ } catch (Exception e) {
+ logger.error("退款异常,公司ID: {}, 订单号: {}", request.getCompanyId(), request.getOutTradeNo(), e);
+ Map errorResult = new HashMap<>();
+ errorResult.put("return_code", "FAIL");
+ errorResult.put("return_msg", "退款异常: " + e.getMessage());
+ return errorResult;
+ }
+ }
+
+ /**
+ * 查询退款接口
+ */
+ @GetMapping("/refundQuery/{companyId}")
+ public Map 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 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 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 response = new HashMap<>();
+ response.put("return_code", returnCode);
+ response.put("return_msg", returnMsg);
+ try {
+ return WXPayUtil.mapToXml(response);
+ } catch (Exception e) {
+ return "";
+ }
+ }
+}
diff --git a/src/main/java/com/sczx/pay/dto/PaymentRequest.java b/src/main/java/com/sczx/pay/dto/PaymentRequest.java
new file mode 100644
index 0000000..b841cd1
--- /dev/null
+++ b/src/main/java/com/sczx/pay/dto/PaymentRequest.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/dto/PaymentResponse.java b/src/main/java/com/sczx/pay/dto/PaymentResponse.java
new file mode 100644
index 0000000..8187cbd
--- /dev/null
+++ b/src/main/java/com/sczx/pay/dto/PaymentResponse.java
@@ -0,0 +1,38 @@
+package com.sczx.pay.dto;
+
+import java.util.Map;
+
+/**
+ * 支付响应数据传输对象
+ */
+public class PaymentResponse {
+ private String code;
+ private String message;
+ private Map 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 getPayData() {
+ return payData;
+ }
+
+ public void setPayData(Map payData) {
+ this.payData = payData;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/dto/RefundRequest.java b/src/main/java/com/sczx/pay/dto/RefundRequest.java
new file mode 100644
index 0000000..0232228
--- /dev/null
+++ b/src/main/java/com/sczx/pay/dto/RefundRequest.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/entity/CompanyWechatConfig.java b/src/main/java/com/sczx/pay/entity/CompanyWechatConfig.java
new file mode 100644
index 0000000..390bf33
--- /dev/null
+++ b/src/main/java/com/sczx/pay/entity/CompanyWechatConfig.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/entity/PaymentRecord.java b/src/main/java/com/sczx/pay/entity/PaymentRecord.java
new file mode 100644
index 0000000..549bc2e
--- /dev/null
+++ b/src/main/java/com/sczx/pay/entity/PaymentRecord.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/entity/RefundRecord.java b/src/main/java/com/sczx/pay/entity/RefundRecord.java
new file mode 100644
index 0000000..b8f820b
--- /dev/null
+++ b/src/main/java/com/sczx/pay/entity/RefundRecord.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java b/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java
new file mode 100644
index 0000000..9dcb125
--- /dev/null
+++ b/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java
@@ -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);
+}
diff --git a/src/main/java/com/sczx/pay/mapper/PaymentRecordMapper.java b/src/main/java/com/sczx/pay/mapper/PaymentRecordMapper.java
new file mode 100644
index 0000000..4918a0b
--- /dev/null
+++ b/src/main/java/com/sczx/pay/mapper/PaymentRecordMapper.java
@@ -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);
+}
diff --git a/src/main/java/com/sczx/pay/mapper/RefundRecordMapper.java b/src/main/java/com/sczx/pay/mapper/RefundRecordMapper.java
new file mode 100644
index 0000000..cf4c4f6
--- /dev/null
+++ b/src/main/java/com/sczx/pay/mapper/RefundRecordMapper.java
@@ -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);
+}
diff --git a/src/main/java/com/sczx/pay/sdk/WXPay.java b/src/main/java/com/sczx/pay/sdk/WXPay.java
new file mode 100644
index 0000000..6579de4
--- /dev/null
+++ b/src/main/java/com/sczx/pay/sdk/WXPay.java
@@ -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 unifiedOrder(Map reqData) throws Exception {
+ return this.unifiedOrder(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+ public Map unifiedOrder(Map 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 orderQuery(Map reqData) throws Exception {
+ return this.orderQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+ public Map orderQuery(Map 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 closeOrder(Map reqData) throws Exception {
+ return this.closeOrder(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+ public Map closeOrder(Map 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 refund(Map reqData) throws Exception {
+ return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+ public Map refund(Map 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 refundQuery(Map reqData) throws Exception {
+ return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+ public Map refundQuery(Map 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 reqData) throws Exception {
+ return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+ public String downloadBill(Map 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 fillRequestData(Map 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 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 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 respData) {
+ try {
+ return WXPayUtil.isSignatureValid(respData, this.config.getKey());
+ } catch (Exception e) {
+ logger.error("验证响应签名异常", e);
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/sczx/pay/sdk/WXPayConfig.java b/src/main/java/com/sczx/pay/sdk/WXPayConfig.java
new file mode 100644
index 0000000..8c446f9
--- /dev/null
+++ b/src/main/java/com/sczx/pay/sdk/WXPayConfig.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/service/WechatPayService.java b/src/main/java/com/sczx/pay/service/WechatPayService.java
new file mode 100644
index 0000000..5e8a3ef
--- /dev/null
+++ b/src/main/java/com/sczx/pay/service/WechatPayService.java
@@ -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 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 result = wxPay.unifiedOrder(reqData);
+
+ logger.info("微信统一下单结果: {}", result);
+
+ // 处理返回结果
+ if ("SUCCESS".equals(result.get("return_code"))) {
+ if ("SUCCESS".equals(result.get("result_code"))) {
+ // 构造小程序支付参数
+ Map 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 orderQuery(Long companyId, String outTradeNo) throws Exception {
+ // 根据companyId获取微信支付配置
+ CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
+ if (companyConfig == null) {
+ Map 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 reqData = new HashMap<>();
+ reqData.put("out_trade_no", outTradeNo);
+
+ return wxPay.orderQuery(reqData);
+ }
+
+ /**
+ * 关闭订单
+ */
+ public Map closeOrder(Long companyId, String outTradeNo) throws Exception {
+ // 根据companyId获取微信支付配置
+ CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
+ if (companyConfig == null) {
+ Map 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 reqData = new HashMap<>();
+ reqData.put("out_trade_no", outTradeNo);
+
+ return wxPay.closeOrder(reqData);
+ }
+
+ /**
+ * 申请退款
+ */
+ @Transactional
+ public Map refund(RefundRequest request) throws Exception {
+ // 根据companyId获取微信支付配置
+ CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(request.getCompanyId());
+ if (companyConfig == null) {
+ Map 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 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 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 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 refundQuery(Long companyId, String outTradeNo) throws Exception {
+ // 根据companyId获取微信支付配置
+ CompanyWechatConfig companyConfig = companyWechatConfigMapper.getWechatConfigByCompanyId(companyId);
+ if (companyConfig == null) {
+ Map 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 reqData = new HashMap<>();
+ reqData.put("out_trade_no", outTradeNo);
+
+ return wxPay.refundQuery(reqData);
+ }
+
+ /**
+ * 构造小程序支付参数
+ */
+ private Map buildPayData(String prepayId, DynamicWXPayConfig wxPayConfig) throws Exception {
+ Map 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 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 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 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;
+ }
+ }
+}
diff --git a/src/main/java/com/sczx/pay/utils/ComputerInfo.java b/src/main/java/com/sczx/pay/utils/ComputerInfo.java
new file mode 100644
index 0000000..e8afc29
--- /dev/null
+++ b/src/main/java/com/sczx/pay/utils/ComputerInfo.java
@@ -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 getMacAddressList() throws IOException {
+ final ArrayList macAddressList = new ArrayList();
+ 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 macList = getMacAddressList();
+ for (Iterator 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());
+ }
+}
diff --git a/src/main/java/com/sczx/pay/utils/IPUtils.java b/src/main/java/com/sczx/pay/utils/IPUtils.java
new file mode 100644
index 0000000..441b901
--- /dev/null
+++ b/src/main/java/com/sczx/pay/utils/IPUtils.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/sczx/pay/utils/WXPayUtil.java b/src/main/java/com/sczx/pay/utils/WXPayUtil.java
new file mode 100644
index 0000000..0ca32d0
--- /dev/null
+++ b/src/main/java/com/sczx/pay/utils/WXPayUtil.java
@@ -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 data, String key) throws Exception {
+ return generateSignature(data, key, "MD5");
+ }
+
+ /**
+ * 生成签名
+ */
+ public static String generateSignature(Map data, String key, String signType) throws Exception {
+ Set 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 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 xmlToMap(String strXML) throws Exception {
+ Map 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 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 data = xmlToMap(xmlString);
+ return isSignatureValid(data, key);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..8fda62d
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -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