|
@@ -0,0 +1,586 @@
|
|
|
|
|
+package com.haha.service.payment.payscore.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
|
|
|
+import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
|
|
|
+import com.haha.common.enums.PaymentChannel;
|
|
|
|
|
+import com.haha.service.payment.config.WxPayConfig;
|
|
|
|
|
+import com.haha.service.payment.payscore.*;
|
|
|
|
|
+import com.wechat.pay.java.core.exception.HttpException;
|
|
|
|
|
+import com.wechat.pay.java.core.exception.MalformedMessageException;
|
|
|
|
|
+import com.wechat.pay.java.core.exception.ServiceException;
|
|
|
|
|
+import com.wechat.pay.java.core.exception.ValidationException;
|
|
|
|
|
+import com.wechat.pay.java.core.http.HttpClient;
|
|
|
|
|
+import com.wechat.pay.java.core.http.HttpMethod;
|
|
|
|
|
+import com.wechat.pay.java.core.http.HttpRequest;
|
|
|
|
|
+import com.wechat.pay.java.core.http.HttpResponse;
|
|
|
|
|
+import com.wechat.pay.java.core.http.JsonRequestBody;
|
|
|
|
|
+import com.wechat.pay.java.core.http.MediaType;
|
|
|
|
|
+import com.wechat.pay.java.core.notification.NotificationParser;
|
|
|
|
|
+import com.wechat.pay.java.core.notification.RequestParam;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.OffsetDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 微信支付分策略实现
|
|
|
|
|
+ * 使用微信支付 V3 SDK core 模块的 HttpClient 发送自定义请求
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+public class WxPayScoreStrategy implements PayScoreStrategy {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String PAY_SCORE_BASE_URL = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder";
|
|
|
|
|
+
|
|
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired(required = false)
|
|
|
|
|
+ private HttpClient wxPayHttpClient;
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired(required = false)
|
|
|
|
|
+ private NotificationParser wxNotificationParser;
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired(required = false)
|
|
|
|
|
+ private WxPayConfig wxPayConfig;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PaymentChannel getChannel() {
|
|
|
|
|
+ return PaymentChannel.WECHAT;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayScoreResult createServiceOrder(PayScoreCreateRequest request) {
|
|
|
|
|
+ log.info("[微信支付分] 创建服务订单 - 订单号: {}, 设备: {}", request.getOutOrderNo(), request.getStartDeviceId());
|
|
|
|
|
+
|
|
|
|
|
+ // 空检查
|
|
|
|
|
+ PayScoreResult checkResult = checkServiceAvailable();
|
|
|
|
|
+ if (checkResult != null) {
|
|
|
|
|
+ return checkResult;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 构建请求体 JSON
|
|
|
|
|
+ ObjectNode body = objectMapper.createObjectNode();
|
|
|
|
|
+ body.put("out_order_no", request.getOutOrderNo());
|
|
|
|
|
+ body.put("appid", wxPayConfig.getAppId());
|
|
|
|
|
+ body.put("service_id", wxPayConfig.getServiceId());
|
|
|
|
|
+ body.put("service_introduction", request.getServiceIntroduction());
|
|
|
|
|
+ body.put("notify_url", wxPayConfig.getPayScoreNotifyUrl());
|
|
|
|
|
+ body.put("need_user_confirm", true);
|
|
|
|
|
+
|
|
|
|
|
+ // 后付费项目
|
|
|
|
|
+ if (request.getPostPayments() != null && !request.getPostPayments().isEmpty()) {
|
|
|
|
|
+ ArrayNode postPayments = objectMapper.createArrayNode();
|
|
|
|
|
+ for (PayScoreCreateRequest.PostPayment payment : request.getPostPayments()) {
|
|
|
|
|
+ ObjectNode paymentObj = objectMapper.createObjectNode();
|
|
|
|
|
+ paymentObj.put("name", payment.getName());
|
|
|
|
|
+ paymentObj.put("amount", yuanToFen(payment.getAmount()));
|
|
|
|
|
+ if (payment.getDescription() != null) {
|
|
|
|
|
+ paymentObj.put("description", payment.getDescription());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (payment.getCount() != null) {
|
|
|
|
|
+ paymentObj.put("count", payment.getCount());
|
|
|
|
|
+ }
|
|
|
|
|
+ postPayments.add(paymentObj);
|
|
|
|
|
+ }
|
|
|
|
|
+ body.set("post_payments", postPayments);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 风险金
|
|
|
|
|
+ ObjectNode riskFund = objectMapper.createObjectNode();
|
|
|
|
|
+ riskFund.put("name", request.getRiskFundName() != null ? request.getRiskFundName() : "DEPOSIT");
|
|
|
|
|
+ riskFund.put("amount", yuanToFen(request.getRiskFundAmount()));
|
|
|
|
|
+ body.set("risk_fund", riskFund);
|
|
|
|
|
+
|
|
|
|
|
+ // 服务时间段
|
|
|
|
|
+ ObjectNode timeRange = objectMapper.createObjectNode();
|
|
|
|
|
+ timeRange.put("start_time", request.getStartTime() != null ? request.getStartTime() : "OnAccept");
|
|
|
|
|
+ if (request.getEndTime() != null) {
|
|
|
|
|
+ timeRange.put("end_time", request.getEndTime());
|
|
|
|
|
+ }
|
|
|
|
|
+ body.set("time_range", timeRange);
|
|
|
|
|
+
|
|
|
|
|
+ // 服务位置(设备)
|
|
|
|
|
+ if (request.getStartDeviceId() != null) {
|
|
|
|
|
+ ObjectNode location = objectMapper.createObjectNode();
|
|
|
|
|
+ location.put("start_location", request.getStartDeviceId());
|
|
|
|
|
+ if (request.getEndDeviceId() != null) {
|
|
|
|
|
+ location.put("end_location", request.getEndDeviceId());
|
|
|
|
|
+ }
|
|
|
|
|
+ body.set("location", location);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 附加数据
|
|
|
|
|
+ if (request.getAttach() != null) {
|
|
|
|
|
+ body.put("attach", request.getAttach());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 用户标识(如果需要免确认,可传入 openid)
|
|
|
|
|
+ if (request.getOpenId() != null) {
|
|
|
|
|
+ body.put("openid", request.getOpenId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String requestBody = objectMapper.writeValueAsString(body);
|
|
|
|
|
+ log.info("[微信支付分] 创建订单请求体: {}", requestBody);
|
|
|
|
|
+
|
|
|
|
|
+ // 发送 POST 请求
|
|
|
|
|
+ HttpRequest httpRequest = new HttpRequest.Builder()
|
|
|
|
|
+ .httpMethod(HttpMethod.POST)
|
|
|
|
|
+ .url(PAY_SCORE_BASE_URL)
|
|
|
|
|
+ .body(new JsonRequestBody.Builder().body(requestBody).build())
|
|
|
|
|
+ .addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析响应
|
|
|
|
|
+ String responseBody = response.getServiceResponse();
|
|
|
|
|
+ log.info("[微信支付分] 创建订单响应: body={}", responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ JsonNode respJson = objectMapper.readTree(responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ String outOrderNo = getJsonString(respJson, "out_order_no");
|
|
|
|
|
+ String state = getJsonString(respJson, "state");
|
|
|
|
|
+ String packageStr = getJsonString(respJson, "package");
|
|
|
|
|
+
|
|
|
|
|
+ PayScoreResult result = PayScoreResult.success(outOrderNo, state);
|
|
|
|
|
+ result.setPackageStr(packageStr);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> rawData = new HashMap<>();
|
|
|
|
|
+ rawData.put("response", responseBody);
|
|
|
|
|
+ result.setRawData(rawData);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[微信支付分] 创建订单成功 - 订单号: {}, 状态: {}, package: {}", outOrderNo, state, packageStr);
|
|
|
|
|
+ return result;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (HttpException e) {
|
|
|
|
|
+ log.error("[微信支付分] 创建订单HTTP异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
|
|
|
|
|
+ } catch (ServiceException e) {
|
|
|
|
|
+ log.error("[微信支付分] 创建订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
|
|
|
|
|
+ } catch (MalformedMessageException e) {
|
|
|
|
|
+ log.error("[微信支付分] 创建订单消息解析异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[微信支付分] 创建订单未知异常", e);
|
|
|
|
|
+ return PayScoreResult.fail("UNKNOWN_ERROR", "创建订单失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayScoreResult completeServiceOrder(PayScoreCompleteRequest request) {
|
|
|
|
|
+ log.info("[微信支付分] 完结服务订单 - 订单号: {}, 总金额: {}元", request.getOutOrderNo(), request.getTotalAmount());
|
|
|
|
|
+
|
|
|
|
|
+ // 空检查
|
|
|
|
|
+ PayScoreResult checkResult = checkServiceAvailable();
|
|
|
|
|
+ if (checkResult != null) {
|
|
|
|
|
+ return checkResult;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 构建请求体 JSON
|
|
|
|
|
+ ObjectNode body = objectMapper.createObjectNode();
|
|
|
|
|
+ body.put("appid", wxPayConfig.getAppId());
|
|
|
|
|
+ body.put("service_id", wxPayConfig.getServiceId());
|
|
|
|
|
+
|
|
|
|
|
+ // 后付费项目(实际收费)
|
|
|
|
|
+ if (request.getPostPayments() != null && !request.getPostPayments().isEmpty()) {
|
|
|
|
|
+ ArrayNode postPayments = objectMapper.createArrayNode();
|
|
|
|
|
+ for (PayScoreCreateRequest.PostPayment payment : request.getPostPayments()) {
|
|
|
|
|
+ ObjectNode paymentObj = objectMapper.createObjectNode();
|
|
|
|
|
+ paymentObj.put("name", payment.getName());
|
|
|
|
|
+ paymentObj.put("amount", yuanToFen(payment.getAmount()));
|
|
|
|
|
+ if (payment.getDescription() != null) {
|
|
|
|
|
+ paymentObj.put("description", payment.getDescription());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (payment.getCount() != null) {
|
|
|
|
|
+ paymentObj.put("count", payment.getCount());
|
|
|
|
|
+ }
|
|
|
|
|
+ postPayments.add(paymentObj);
|
|
|
|
|
+ }
|
|
|
|
|
+ body.set("post_payments", postPayments);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 商户优惠
|
|
|
|
|
+ if (request.getPostDiscounts() != null && !request.getPostDiscounts().isEmpty()) {
|
|
|
|
|
+ ArrayNode postDiscounts = objectMapper.createArrayNode();
|
|
|
|
|
+ for (PayScoreCompleteRequest.PostDiscount discount : request.getPostDiscounts()) {
|
|
|
|
|
+ ObjectNode discountObj = objectMapper.createObjectNode();
|
|
|
|
|
+ discountObj.put("name", discount.getName());
|
|
|
|
|
+ if (discount.getDescription() != null) {
|
|
|
|
|
+ discountObj.put("description", discount.getDescription());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (discount.getAmount() != null) {
|
|
|
|
|
+ discountObj.put("amount", yuanToFen(discount.getAmount()));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (discount.getCount() != null) {
|
|
|
|
|
+ discountObj.put("count", discount.getCount());
|
|
|
|
|
+ }
|
|
|
|
|
+ postDiscounts.add(discountObj);
|
|
|
|
|
+ }
|
|
|
|
|
+ body.set("post_discounts", postDiscounts);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 总金额
|
|
|
|
|
+ body.put("total_amount", yuanToFen(request.getTotalAmount()));
|
|
|
|
|
+
|
|
|
|
|
+ // 服务时间段
|
|
|
|
|
+ if (request.getEndTime() != null) {
|
|
|
|
|
+ ObjectNode timeRange = objectMapper.createObjectNode();
|
|
|
|
|
+ timeRange.put("end_time", request.getEndTime());
|
|
|
|
|
+ body.set("time_range", timeRange);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 服务位置(设备)
|
|
|
|
|
+ if (request.getStartDeviceId() != null || request.getEndDeviceId() != null) {
|
|
|
|
|
+ ObjectNode location = objectMapper.createObjectNode();
|
|
|
|
|
+ if (request.getStartDeviceId() != null) {
|
|
|
|
|
+ location.put("start_location", request.getStartDeviceId());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getEndDeviceId() != null) {
|
|
|
|
|
+ location.put("end_location", request.getEndDeviceId());
|
|
|
|
|
+ }
|
|
|
|
|
+ body.set("location", location);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String requestBody = objectMapper.writeValueAsString(body);
|
|
|
|
|
+ String url = PAY_SCORE_BASE_URL + "/" + request.getOutOrderNo() + "/complete";
|
|
|
|
|
+ log.info("[微信支付分] 完结订单请求: url={}, body={}", url, requestBody);
|
|
|
|
|
+
|
|
|
|
|
+ // 发送 POST 请求
|
|
|
|
|
+ HttpRequest httpRequest = new HttpRequest.Builder()
|
|
|
|
|
+ .httpMethod(HttpMethod.POST)
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .body(new JsonRequestBody.Builder().body(requestBody).build())
|
|
|
|
|
+ .addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析响应体
|
|
|
|
|
+ String responseBody = response.getServiceResponse();
|
|
|
|
|
+ log.info("[微信支付分] 完结订单响应: body={}", responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ if (responseBody != null && !responseBody.isEmpty()) {
|
|
|
|
|
+ JsonNode respJson = objectMapper.readTree(responseBody);
|
|
|
|
|
+ String outOrderNo = getJsonString(respJson, "out_order_no");
|
|
|
|
|
+ String state = getJsonString(respJson, "state");
|
|
|
|
|
+ log.info("[微信支付分] 完结订单成功 - 订单号: {}, 状态: {}", outOrderNo, state);
|
|
|
|
|
+ return PayScoreResult.success(outOrderNo != null ? outOrderNo : request.getOutOrderNo(),
|
|
|
|
|
+ state != null ? state : "USER_PAYING");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[微信支付分] 完结订单成功 - 订单号: {}", request.getOutOrderNo());
|
|
|
|
|
+ return PayScoreResult.success(request.getOutOrderNo(), "USER_PAYING");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (HttpException e) {
|
|
|
|
|
+ log.error("[微信支付分] 完结订单HTTP异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
|
|
|
|
|
+ } catch (ServiceException e) {
|
|
|
|
|
+ log.error("[微信支付分] 完结订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
|
|
|
|
|
+ } catch (MalformedMessageException e) {
|
|
|
|
|
+ log.error("[微信支付分] 完结订单消息解析异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[微信支付分] 完结订单未知异常", e);
|
|
|
|
|
+ return PayScoreResult.fail("UNKNOWN_ERROR", "完结订单失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayScoreResult cancelServiceOrder(PayScoreCancelRequest request) {
|
|
|
|
|
+ log.info("[微信支付分] 取消服务订单 - 订单号: {}, 原因: {}", request.getOutOrderNo(), request.getReason());
|
|
|
|
|
+
|
|
|
|
|
+ // 空检查
|
|
|
|
|
+ PayScoreResult checkResult = checkServiceAvailable();
|
|
|
|
|
+ if (checkResult != null) {
|
|
|
|
|
+ return checkResult;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 构建请求体 JSON
|
|
|
|
|
+ ObjectNode body = objectMapper.createObjectNode();
|
|
|
|
|
+ body.put("appid", wxPayConfig.getAppId());
|
|
|
|
|
+ body.put("service_id", wxPayConfig.getServiceId());
|
|
|
|
|
+ body.put("reason", request.getReason() != null ? request.getReason() : "用户取消");
|
|
|
|
|
+
|
|
|
|
|
+ String requestBody = objectMapper.writeValueAsString(body);
|
|
|
|
|
+ String url = PAY_SCORE_BASE_URL + "/" + request.getOutOrderNo() + "/cancel";
|
|
|
|
|
+ log.info("[微信支付分] 取消订单请求: url={}, body={}", url, requestBody);
|
|
|
|
|
+
|
|
|
|
|
+ // 发送 POST 请求
|
|
|
|
|
+ HttpRequest httpRequest = new HttpRequest.Builder()
|
|
|
|
|
+ .httpMethod(HttpMethod.POST)
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .body(new JsonRequestBody.Builder().body(requestBody).build())
|
|
|
|
|
+ .addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析响应体
|
|
|
|
|
+ String responseBody = response.getServiceResponse();
|
|
|
|
|
+ log.info("[微信支付分] 取消订单响应: body={}", responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ if (responseBody != null && !responseBody.isEmpty()) {
|
|
|
|
|
+ JsonNode respJson = objectMapper.readTree(responseBody);
|
|
|
|
|
+ String outOrderNo = getJsonString(respJson, "out_order_no");
|
|
|
|
|
+ String state = getJsonString(respJson, "state");
|
|
|
|
|
+ log.info("[微信支付分] 取消订单成功 - 订单号: {}, 状态: {}", outOrderNo, state);
|
|
|
|
|
+ return PayScoreResult.success(outOrderNo != null ? outOrderNo : request.getOutOrderNo(),
|
|
|
|
|
+ state != null ? state : "REVOKED");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[微信支付分] 取消订单成功 - 订单号: {}", request.getOutOrderNo());
|
|
|
|
|
+ return PayScoreResult.success(request.getOutOrderNo(), "REVOKED");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (HttpException e) {
|
|
|
|
|
+ log.error("[微信支付分] 取消订单HTTP异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
|
|
|
|
|
+ } catch (ServiceException e) {
|
|
|
|
|
+ log.error("[微信支付分] 取消订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
|
|
|
|
|
+ } catch (MalformedMessageException e) {
|
|
|
|
|
+ log.error("[微信支付分] 取消订单消息解析异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[微信支付分] 取消订单未知异常", e);
|
|
|
|
|
+ return PayScoreResult.fail("UNKNOWN_ERROR", "取消订单失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayScoreResult queryServiceOrder(String outOrderNo) {
|
|
|
|
|
+ log.info("[微信支付分] 查询服务订单 - 订单号: {}", outOrderNo);
|
|
|
|
|
+
|
|
|
|
|
+ // 空检查
|
|
|
|
|
+ PayScoreResult checkResult = checkServiceAvailable();
|
|
|
|
|
+ if (checkResult != null) {
|
|
|
|
|
+ return checkResult;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 构建查询 URL
|
|
|
|
|
+ String url = PAY_SCORE_BASE_URL
|
|
|
|
|
+ + "?out_order_no=" + outOrderNo
|
|
|
|
|
+ + "&service_id=" + wxPayConfig.getServiceId()
|
|
|
|
|
+ + "&appid=" + wxPayConfig.getAppId();
|
|
|
|
|
+ log.info("[微信支付分] 查询订单请求: url={}", url);
|
|
|
|
|
+
|
|
|
|
|
+ // 发送 GET 请求
|
|
|
|
|
+ HttpRequest httpRequest = new HttpRequest.Builder()
|
|
|
|
|
+ .httpMethod(HttpMethod.GET)
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析响应
|
|
|
|
|
+ String responseBody = response.getServiceResponse();
|
|
|
|
|
+ log.info("[微信支付分] 查询订单响应: body={}", responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ JsonNode respJson = objectMapper.readTree(responseBody);
|
|
|
|
|
+
|
|
|
|
|
+ String respOutOrderNo = getJsonString(respJson, "out_order_no");
|
|
|
|
|
+ String state = getJsonString(respJson, "state");
|
|
|
|
|
+
|
|
|
|
|
+ PayScoreResult result = PayScoreResult.success(respOutOrderNo, state);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析总金额
|
|
|
|
|
+ if (respJson.has("total_amount") && !respJson.get("total_amount").isNull()) {
|
|
|
|
|
+ int totalAmountFen = respJson.get("total_amount").asInt();
|
|
|
|
|
+ result.setTotalAmount(fenToYuan(totalAmountFen));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> rawData = new HashMap<>();
|
|
|
|
|
+ rawData.put("response", responseBody);
|
|
|
|
|
+ result.setRawData(rawData);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[微信支付分] 查询订单成功 - 订单号: {}, 状态: {}, 金额: {}元",
|
|
|
|
|
+ respOutOrderNo, state, result.getTotalAmount());
|
|
|
|
|
+ return result;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (HttpException e) {
|
|
|
|
|
+ log.error("[微信支付分] 查询订单HTTP异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
|
|
|
|
|
+ } catch (ServiceException e) {
|
|
|
|
|
+ log.error("[微信支付分] 查询订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
|
|
|
|
|
+ } catch (MalformedMessageException e) {
|
|
|
|
|
+ log.error("[微信支付分] 查询订单消息解析异常: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[微信支付分] 查询订单未知异常", e);
|
|
|
|
|
+ return PayScoreResult.fail("UNKNOWN_ERROR", "查询订单失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayScoreCallbackResult parseCallback(Map<String, Object> params) {
|
|
|
|
|
+ log.info("[微信支付分] 解析回调通知 - 参数Keys: {}", params.keySet());
|
|
|
|
|
+
|
|
|
|
|
+ // 检查服务可用性
|
|
|
|
|
+ if (wxNotificationParser == null) {
|
|
|
|
|
+ log.error("[微信支付分] 回调解析服务未初始化,请检查微信支付配置");
|
|
|
|
|
+ return PayScoreCallbackResult.fail("回调解析服务未初始化,请检查配置");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 从 params 中提取微信 V3 回调所需的信息
|
|
|
|
|
+ String timestamp = (String) params.get("Wechatpay-Timestamp");
|
|
|
|
|
+ String nonce = (String) params.get("Wechatpay-Nonce");
|
|
|
|
|
+ String signature = (String) params.get("Wechatpay-Signature");
|
|
|
|
|
+ String serial = (String) params.get("Wechatpay-Serial");
|
|
|
|
|
+ String signType = (String) params.getOrDefault("Wechatpay-Signature-Type", "WECHATPAY2-SHA256-RSA2048");
|
|
|
|
|
+ String body = (String) params.get("body");
|
|
|
|
|
+
|
|
|
|
|
+ log.debug("[微信支付分] 回调请求头: timestamp={}, nonce={}, serial={}, signType={}",
|
|
|
|
|
+ timestamp, nonce, serial, signType);
|
|
|
|
|
+
|
|
|
|
|
+ // 构建 RequestParam
|
|
|
|
|
+ RequestParam requestParam = new RequestParam.Builder()
|
|
|
|
|
+ .serialNumber(serial)
|
|
|
|
|
+ .nonce(nonce)
|
|
|
|
|
+ .timestamp(timestamp)
|
|
|
|
|
+ .signature(signature)
|
|
|
|
|
+ .signType(signType)
|
|
|
|
|
+ .body(body)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ // SDK 自动完成签名验证 + AES-256-GCM 解密
|
|
|
|
|
+ // 支付分回调数据结构与普通支付不同,使用 String.class 获取解密后的 JSON
|
|
|
|
|
+ String decryptedJson = wxNotificationParser.parse(requestParam, String.class);
|
|
|
|
|
+ log.info("[微信支付分] 回调解密数据: {}", decryptedJson);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 JSON
|
|
|
|
|
+ JsonNode dataJson = objectMapper.readTree(decryptedJson);
|
|
|
|
|
+
|
|
|
|
|
+ // 从外层 body 获取 event_type(回调通知外层结构)
|
|
|
|
|
+ String eventType = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ JsonNode outerBody = objectMapper.readTree(body);
|
|
|
|
|
+ eventType = getJsonString(outerBody, "event_type");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[微信支付分] 解析外层body获取event_type失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果外层没有,尝试从解密数据中获取
|
|
|
|
|
+ if (eventType == null && dataJson.has("event_type")) {
|
|
|
|
|
+ eventType = getJsonString(dataJson, "event_type");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String outOrderNo = getJsonString(dataJson, "out_order_no");
|
|
|
|
|
+ String state = getJsonString(dataJson, "state");
|
|
|
|
|
+ String openId = getJsonString(dataJson, "openid");
|
|
|
|
|
+
|
|
|
|
|
+ PayScoreCallbackResult result = PayScoreCallbackResult.success(eventType, outOrderNo, state);
|
|
|
|
|
+ result.setOpenId(openId);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析总金额
|
|
|
|
|
+ if (dataJson.has("total_amount") && !dataJson.get("total_amount").isNull()) {
|
|
|
|
|
+ int totalAmountFen = dataJson.get("total_amount").asInt();
|
|
|
|
|
+ result.setTotalAmount(fenToYuan(totalAmountFen));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析支付时间
|
|
|
|
|
+ if (dataJson.has("pay_succ_time") && !dataJson.get("pay_succ_time").isNull()) {
|
|
|
|
|
+ String paySuccTime = dataJson.get("pay_succ_time").asText();
|
|
|
|
|
+ result.setPayTime(parsePayTime(paySuccTime));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[微信支付分] 回调解析成功 - eventType: {}, outOrderNo: {}, state: {}, openId: {}, amount: {}元",
|
|
|
|
|
+ eventType, outOrderNo, state, openId, result.getTotalAmount());
|
|
|
|
|
+ return result;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (ValidationException e) {
|
|
|
|
|
+ log.error("[微信支付分] 回调签名验证失败: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreCallbackResult.fail("签名验证失败: " + e.getMessage());
|
|
|
|
|
+ } catch (MalformedMessageException e) {
|
|
|
|
|
+ log.error("[微信支付分] 回调消息解析失败: {}", e.getMessage(), e);
|
|
|
|
|
+ return PayScoreCallbackResult.fail("消息解析失败: " + e.getMessage());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[微信支付分] 回调处理未知异常", e);
|
|
|
|
|
+ return PayScoreCallbackResult.fail("回调处理失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 私有辅助方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查服务可用性
|
|
|
|
|
+ */
|
|
|
|
|
+ private PayScoreResult checkServiceAvailable() {
|
|
|
|
|
+ if (wxPayHttpClient == null) {
|
|
|
|
|
+ log.error("[微信支付分] HTTP客户端未初始化,请检查微信支付配置");
|
|
|
|
|
+ return PayScoreResult.fail("SERVICE_UNAVAILABLE", "支付分服务未配置,HTTP客户端未初始化");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (wxPayConfig == null) {
|
|
|
|
|
+ log.error("[微信支付分] 支付配置未初始化");
|
|
|
|
|
+ return PayScoreResult.fail("CONFIG_UNAVAILABLE", "支付分服务未配置");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (wxPayConfig.getServiceId() == null || wxPayConfig.getServiceId().isEmpty()) {
|
|
|
|
|
+ log.error("[微信支付分] 支付分服务ID未配置");
|
|
|
|
|
+ return PayScoreResult.fail("CONFIG_ERROR", "支付分服务ID未配置");
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 元转分
|
|
|
|
|
+ */
|
|
|
|
|
+ private int yuanToFen(BigDecimal yuan) {
|
|
|
|
|
+ if (yuan == null) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return yuan.multiply(BigDecimal.valueOf(100)).intValue();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 分转元
|
|
|
|
|
+ */
|
|
|
|
|
+ private BigDecimal fenToYuan(int fen) {
|
|
|
|
|
+ return BigDecimal.valueOf(fen).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全获取 JSON 字符串值
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getJsonString(JsonNode json, String key) {
|
|
|
|
|
+ if (json.has(key) && !json.get(key).isNull()) {
|
|
|
|
|
+ return json.get(key).asText();
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析支付时间
|
|
|
|
|
+ */
|
|
|
|
|
+ private LocalDateTime parsePayTime(String timeStr) {
|
|
|
|
|
+ if (timeStr == null || timeStr.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 微信返回的时间格式: 2018-06-08T10:34:56+08:00 (ISO 8601)
|
|
|
|
|
+ OffsetDateTime offsetDateTime = OffsetDateTime.parse(timeStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
|
|
|
+ return offsetDateTime.toLocalDateTime();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[微信支付分] 支付时间解析失败: {}, 使用当前时间", timeStr);
|
|
|
|
|
+ return LocalDateTime.now();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|