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