|
@@ -1,22 +1,50 @@
|
|
|
package com.etotem.cfc.service;
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
|
|
-import com.etotem.cfc.service.api.PaymentServiceInterface;
|
|
|
|
|
-import javax.annotation.Resource;
|
|
|
|
|
-
|
|
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.etotem.cfc.entity.PackageOrder;
|
|
import com.etotem.cfc.entity.PackageOrder;
|
|
|
import com.etotem.cfc.mapper.PackageOrderMapper;
|
|
import com.etotem.cfc.mapper.PackageOrderMapper;
|
|
|
-import com.etotem.cfc.service.CommissionService;
|
|
|
|
|
|
|
+import com.etotem.cfc.service.api.PaymentServiceInterface;
|
|
|
|
|
+import okhttp3.MediaType;
|
|
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
|
|
+import okhttp3.Request;
|
|
|
|
|
+import okhttp3.RequestBody;
|
|
|
|
|
+import okhttp3.Response;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import javax.crypto.Cipher;
|
|
|
|
|
+import javax.crypto.spec.GCMParameterSpec;
|
|
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
|
+import java.io.FileInputStream;
|
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.KeyFactory;
|
|
|
|
|
+import java.security.MessageDigest;
|
|
|
|
|
+import java.security.PrivateKey;
|
|
|
|
|
+import java.security.Signature;
|
|
|
|
|
+import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
|
|
+import java.util.Base64;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 支付服务
|
|
|
|
|
- */
|
|
|
|
|
@Service
|
|
@Service
|
|
|
-public class PaymentService {
|
|
|
|
|
|
|
+public class PaymentService implements PaymentServiceInterface {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
|
|
|
|
|
+ private static final String WECHAT_API_BASE = "https://api.mch.weixin.qq.com";
|
|
|
|
|
+ private static final MediaType JSON_MEDIA = MediaType.parse("application/json; charset=utf-8");
|
|
|
|
|
|
|
|
@Resource
|
|
@Resource
|
|
|
private PackageOrderMapper packageOrderMapper;
|
|
private PackageOrderMapper packageOrderMapper;
|
|
@@ -24,58 +52,80 @@ public class PaymentService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private CommissionService commissionService;
|
|
private CommissionService commissionService;
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 创建微信支付订单
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ @Value("${wechat.appid}")
|
|
|
|
|
+ private String appid;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.mch-id}")
|
|
|
|
|
+ private String mchId;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.api-v3-key:}")
|
|
|
|
|
+ private String apiV3Key;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.mch-serial-no:}")
|
|
|
|
|
+ private String mchSerialNo;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.private-key-path:}")
|
|
|
|
|
+ private String privateKeyPath;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${wechat.notify-url}")
|
|
|
|
|
+ private String notifyUrl;
|
|
|
|
|
+
|
|
|
|
|
+ private PrivateKey privateKey;
|
|
|
|
|
+ private final OkHttpClient httpClient;
|
|
|
|
|
+
|
|
|
|
|
+ public PaymentService() {
|
|
|
|
|
+ httpClient = new OkHttpClient.Builder()
|
|
|
|
|
+ .connectTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .writeTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @PostConstruct
|
|
|
|
|
+ public void init() {
|
|
|
|
|
+ if (privateKeyPath != null && !privateKeyPath.isEmpty()) {
|
|
|
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
|
|
|
|
|
+ new FileInputStream(privateKeyPath), StandardCharsets.UTF_8))) {
|
|
|
|
|
+ String keyContent = reader.lines()
|
|
|
|
|
+ .filter(line -> !line.startsWith("-----"))
|
|
|
|
|
+ .collect(Collectors.joining());
|
|
|
|
|
+ byte[] keyBytes = Base64.getDecoder().decode(keyContent);
|
|
|
|
|
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
|
|
|
|
+ KeyFactory kf = KeyFactory.getInstance("RSA");
|
|
|
|
|
+ privateKey = kf.generatePrivate(spec);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("加载微信支付私钥失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
public Map<String, Object> createWechatPayOrder(String orderNo, String description, Integer amount) {
|
|
public Map<String, Object> createWechatPayOrder(String orderNo, String description, Integer amount) {
|
|
|
- // TODO: 调用微信支付API创建订单
|
|
|
|
|
- // 这里简化处理,实际应该调用微信支付SDK
|
|
|
|
|
-
|
|
|
|
|
- Map<String, Object> result = new java.util.HashMap<>();
|
|
|
|
|
- result.put("orderNo", orderNo);
|
|
|
|
|
- result.put("prepayId", "wx_prepay_" + orderNo);
|
|
|
|
|
- result.put("appId", "wx5ba8038ef16fb245");
|
|
|
|
|
- result.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
|
|
|
|
|
- result.put("nonceStr", generateNonceStr());
|
|
|
|
|
- result.put("package", "prepay_id=wx_prepay_" + orderNo);
|
|
|
|
|
- result.put("signType", "MD5");
|
|
|
|
|
- result.put("paySign", generateSign());
|
|
|
|
|
-
|
|
|
|
|
- return result;
|
|
|
|
|
|
|
+ return createWechatPrepay(orderNo, description, amount);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 创建支付宝订单
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ @Override
|
|
|
public Map<String, Object> createAlipayOrder(String orderNo, String description, Integer amount) {
|
|
public Map<String, Object> createAlipayOrder(String orderNo, String description, Integer amount) {
|
|
|
- // TODO: 调用支付宝支付API创建订单
|
|
|
|
|
- // 这里简化处理,实际应该调用支付宝SDK
|
|
|
|
|
-
|
|
|
|
|
- Map<String, Object> result = new java.util.HashMap<>();
|
|
|
|
|
- result.put("orderNo", orderNo);
|
|
|
|
|
- result.put("orderString", generateOrderString(orderNo, description, amount));
|
|
|
|
|
-
|
|
|
|
|
- return result;
|
|
|
|
|
|
|
+ throw new UnsupportedOperationException("支付宝支付暂未集成");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 处理支付回调
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ @Override
|
|
|
public boolean handlePaymentCallback(String orderNo, String transactionId, String payMethod) {
|
|
public boolean handlePaymentCallback(String orderNo, String transactionId, String payMethod) {
|
|
|
LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
|
|
LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
|
|
|
wrapper.eq(PackageOrder::getOrderNo, orderNo);
|
|
wrapper.eq(PackageOrder::getOrderNo, orderNo);
|
|
|
-
|
|
|
|
|
PackageOrder order = packageOrderMapper.selectOne(wrapper);
|
|
PackageOrder order = packageOrderMapper.selectOne(wrapper);
|
|
|
if (order == null) {
|
|
if (order == null) {
|
|
|
|
|
+ log.warn("订单不存在: {}", orderNo);
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 更新订单状态
|
|
|
|
|
|
|
+ if ("paid".equals(order.getStatus())) {
|
|
|
|
|
+ log.info("订单已支付,跳过: {}", orderNo);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
order.setStatus("paid");
|
|
order.setStatus("paid");
|
|
|
order.setTransactionId(transactionId);
|
|
order.setTransactionId(transactionId);
|
|
|
order.setPaidAt(new Date());
|
|
order.setPaidAt(new Date());
|
|
|
order.setUpdatedAt(new Date());
|
|
order.setUpdatedAt(new Date());
|
|
|
-
|
|
|
|
|
boolean updated = packageOrderMapper.updateById(order) > 0;
|
|
boolean updated = packageOrderMapper.updateById(order) > 0;
|
|
|
if (updated) {
|
|
if (updated) {
|
|
|
commissionService.settle(order.getId(), "package", order.getUserId(),
|
|
commissionService.settle(order.getId(), "package", order.getUserId(),
|
|
@@ -84,36 +134,264 @@ public class PaymentService {
|
|
|
return updated;
|
|
return updated;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 查询订单状态
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ @Override
|
|
|
public PackageOrder getOrderStatus(String orderNo) {
|
|
public PackageOrder getOrderStatus(String orderNo) {
|
|
|
LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
|
|
LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
|
|
|
wrapper.eq(PackageOrder::getOrderNo, orderNo);
|
|
wrapper.eq(PackageOrder::getOrderNo, orderNo);
|
|
|
-
|
|
|
|
|
return packageOrderMapper.selectOne(wrapper);
|
|
return packageOrderMapper.selectOne(wrapper);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 生成随机字符串
|
|
|
|
|
- */
|
|
|
|
|
- private String generateNonceStr() {
|
|
|
|
|
- return java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 32);
|
|
|
|
|
|
|
+ public Map<String, Object> createWechatPrepay(String orderNo, String description, Integer totalFee) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("appid", appid);
|
|
|
|
|
+ body.put("mchid", mchId);
|
|
|
|
|
+ body.put("description", description);
|
|
|
|
|
+ body.put("out_trade_no", orderNo);
|
|
|
|
|
+ body.put("notify_url", notifyUrl);
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject amount = new JSONObject();
|
|
|
|
|
+ amount.put("total", totalFee);
|
|
|
|
|
+ amount.put("currency", "CNY");
|
|
|
|
|
+ body.put("amount", amount);
|
|
|
|
|
+
|
|
|
|
|
+ String respBody = doPost("/v3/pay/transactions/jsapi", body.toJSONString());
|
|
|
|
|
+ JSONObject resp = JSON.parseObject(respBody);
|
|
|
|
|
+ String prepayId = resp.getString("prepay_id");
|
|
|
|
|
+
|
|
|
|
|
+ if (prepayId == null) {
|
|
|
|
|
+ log.error("微信统一下单失败: {}", respBody);
|
|
|
|
|
+ throw new RuntimeException("微信统一下单失败: "
|
|
|
|
|
+ + resp.getString("code") + " " + resp.getString("message"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
|
|
|
|
|
+ String nonceStr = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ String packageStr = "prepay_id=" + prepayId;
|
|
|
|
|
+ String paySign = generatePaySign(appid, timeStamp, nonceStr, packageStr);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("appId", appid);
|
|
|
|
|
+ result.put("timeStamp", timeStamp);
|
|
|
|
|
+ result.put("nonceStr", nonceStr);
|
|
|
|
|
+ result.put("package", packageStr);
|
|
|
|
|
+ result.put("signType", "RSA");
|
|
|
|
|
+ result.put("paySign", paySign);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("微信预支付订单创建成功: orderNo={}, prepayId={}", orderNo, prepayId);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("创建微信预支付订单异常: orderNo={}", orderNo, e);
|
|
|
|
|
+ throw new RuntimeException("创建微信预支付订单失败: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 生成签名
|
|
|
|
|
- */
|
|
|
|
|
- private String generateSign() {
|
|
|
|
|
- // TODO: 实际应该使用微信支付SDK生成签名
|
|
|
|
|
- return "mock_sign_" + System.currentTimeMillis();
|
|
|
|
|
|
|
+ public Map<String, Object> handleWechatNotify(String requestBody, String signatureHeader) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject body = JSON.parseObject(requestBody);
|
|
|
|
|
+ JSONObject resource = body.getJSONObject("resource");
|
|
|
|
|
+ if (resource == null) {
|
|
|
|
|
+ log.error("微信通知数据缺少resource字段");
|
|
|
|
|
+ Map<String, Object> err = new HashMap<>();
|
|
|
|
|
+ err.put("code", "FAIL");
|
|
|
|
|
+ err.put("message", "参数错误");
|
|
|
|
|
+ return err;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String ciphertext = resource.getString("ciphertext");
|
|
|
|
|
+ String associatedData = resource.getString("associated_data");
|
|
|
|
|
+ String nonce = resource.getString("nonce");
|
|
|
|
|
+
|
|
|
|
|
+ String plaintext = decryptAes256Gcm(ciphertext, associatedData, nonce);
|
|
|
|
|
+ JSONObject payResult = JSON.parseObject(plaintext);
|
|
|
|
|
+
|
|
|
|
|
+ String orderNo = payResult.getString("out_trade_no");
|
|
|
|
|
+ String transactionId = payResult.getString("transaction_id");
|
|
|
|
|
+
|
|
|
|
|
+ log.info("微信支付通知: orderNo={}, transactionId={}", orderNo, transactionId);
|
|
|
|
|
+
|
|
|
|
|
+ handlePaymentCallback(orderNo, transactionId, "wechat");
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("code", "SUCCESS");
|
|
|
|
|
+ result.put("message", "OK");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("处理微信支付通知异常", e);
|
|
|
|
|
+ Map<String, Object> err = new HashMap<>();
|
|
|
|
|
+ err.put("code", "FAIL");
|
|
|
|
|
+ err.put("message", "处理异常");
|
|
|
|
|
+ return err;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 生成支付宝订单字符串
|
|
|
|
|
- */
|
|
|
|
|
- private String generateOrderString(String orderNo, String description, Integer amount) {
|
|
|
|
|
- // TODO: 实际应该使用支付宝SDK生成订单字符串
|
|
|
|
|
- return "alipay_sdk_order_" + orderNo + "_" + amount;
|
|
|
|
|
|
|
+ public PackageOrder queryPaymentStatus(String orderNo) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String path = "/v3/pay/transactions/out-trade-no/" + orderNo + "?mchid=" + mchId;
|
|
|
|
|
+ String respBody = doGet(path);
|
|
|
|
|
+
|
|
|
|
|
+ if (respBody.isEmpty()) {
|
|
|
|
|
+ log.warn("查询微信支付状态返回空: orderNo={}", orderNo);
|
|
|
|
|
+ return getOrderStatus(orderNo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject resp = JSON.parseObject(respBody);
|
|
|
|
|
+ String tradeState = resp.getString("trade_state");
|
|
|
|
|
+
|
|
|
|
|
+ if ("SUCCESS".equals(tradeState)) {
|
|
|
|
|
+ String transactionId = resp.getString("transaction_id");
|
|
|
|
|
+ PackageOrder order = getOrderStatus(orderNo);
|
|
|
|
|
+ if (order != null && !"paid".equals(order.getStatus())) {
|
|
|
|
|
+ handlePaymentCallback(orderNo, transactionId, "wechat");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return getOrderStatus(orderNo);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("查询微信支付状态异常: orderNo={}", orderNo, e);
|
|
|
|
|
+ return getOrderStatus(orderNo);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void refund(String orderNo, Integer amount, String reason) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ PackageOrder order = getOrderStatus(orderNo);
|
|
|
|
|
+ Integer totalFee = order != null ? order.getPrice() : amount;
|
|
|
|
|
+
|
|
|
|
|
+ String outRefundNo = "RF" + orderNo + UUID.randomUUID().toString().replace("-", "").substring(0, 4).toUpperCase();
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
|
|
+ body.put("out_trade_no", orderNo);
|
|
|
|
|
+ body.put("out_refund_no", outRefundNo);
|
|
|
|
|
+
|
|
|
|
|
+ if (reason != null && !reason.isEmpty()) {
|
|
|
|
|
+ body.put("reason", reason);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSONObject amountObj = new JSONObject();
|
|
|
|
|
+ amountObj.put("refund", amount);
|
|
|
|
|
+ amountObj.put("total", totalFee);
|
|
|
|
|
+ amountObj.put("currency", "CNY");
|
|
|
|
|
+ body.put("amount", amountObj);
|
|
|
|
|
+
|
|
|
|
|
+ String respBody = doPost("/v3/refund/domestic/refunds", body.toJSONString());
|
|
|
|
|
+ JSONObject resp = JSON.parseObject(respBody);
|
|
|
|
|
+ String status = resp.getString("status");
|
|
|
|
|
+
|
|
|
|
|
+ log.info("微信退款结果: orderNo={}, outRefundNo={}, status={}", orderNo, outRefundNo, status);
|
|
|
|
|
+
|
|
|
|
|
+ if ("SUCCESS".equals(status) || "PROCESSING".equals(status)) {
|
|
|
|
|
+ log.info("退款请求成功: orderNo={}, outRefundNo={}", orderNo, outRefundNo);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.error("微信退款失败: orderNo={}, response={}", orderNo, respBody);
|
|
|
|
|
+ throw new RuntimeException("退款失败: " + resp.getString("code") + " " + resp.getString("message"));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("微信退款异常: orderNo={}", orderNo, e);
|
|
|
|
|
+ throw new RuntimeException("退款失败: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String doGet(String path) throws Exception {
|
|
|
|
|
+ String url = WECHAT_API_BASE + path;
|
|
|
|
|
+ String auth = buildAuthHeader("GET", path, "");
|
|
|
|
|
+
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .header("Authorization", auth)
|
|
|
|
|
+ .header("Accept", "application/json")
|
|
|
|
|
+ .header("User-Agent", "cfc-backend")
|
|
|
|
|
+ .get()
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ String respBody = response.body() != null ? response.body().string() : "";
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ log.error("微信API GET请求失败: status={}, path={}, body={}", response.code(), path, respBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ return respBody;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String doPost(String path, String body) throws Exception {
|
|
|
|
|
+ String url = WECHAT_API_BASE + path;
|
|
|
|
|
+ String auth = buildAuthHeader("POST", path, body);
|
|
|
|
|
+
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .header("Authorization", auth)
|
|
|
|
|
+ .header("Accept", "application/json")
|
|
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
|
|
+ .header("User-Agent", "cfc-backend")
|
|
|
|
|
+ .post(RequestBody.create(JSON_MEDIA, body))
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ String respBody = response.body() != null ? response.body().string() : "";
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ log.error("微信API POST请求失败: status={}, path={}, body={}", response.code(), path, respBody);
|
|
|
|
|
+ }
|
|
|
|
|
+ return respBody;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildAuthHeader(String method, String path, String body) throws Exception {
|
|
|
|
|
+ if (privateKey == null) {
|
|
|
|
|
+ log.warn("微信支付私钥未配置,使用模拟认证头");
|
|
|
|
|
+ return "WECHATPAY2-SHA256-RSA2048 mchid=\"" + mchId
|
|
|
|
|
+ + "\",nonce_str=\"mock\",timestamp=\"0\",serial_no=\"mock\",signature=\"mock\"";
|
|
|
|
|
+ }
|
|
|
|
|
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
|
|
|
|
+ String nonce = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ String message = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n";
|
|
|
|
|
+
|
|
|
|
|
+ Signature sign = Signature.getInstance("SHA256withRSA");
|
|
|
|
|
+ sign.initSign(privateKey);
|
|
|
|
|
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ String signature = Base64.getEncoder().encodeToString(sign.sign());
|
|
|
|
|
+
|
|
|
|
|
+ return "WECHATPAY2-SHA256-RSA2048 "
|
|
|
|
|
+ + "mchid=\"" + mchId + "\","
|
|
|
|
|
+ + "nonce_str=\"" + nonce + "\","
|
|
|
|
|
+ + "timestamp=\"" + timestamp + "\","
|
|
|
|
|
+ + "serial_no=\"" + mchSerialNo + "\","
|
|
|
|
|
+ + "signature=\"" + signature + "\"";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String generatePaySign(String appId, String timeStamp, String nonceStr, String packageStr) {
|
|
|
|
|
+ if (privateKey == null) {
|
|
|
|
|
+ return "mock_pay_sign_" + System.currentTimeMillis();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String message = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n";
|
|
|
|
|
+ Signature sign = Signature.getInstance("SHA256withRSA");
|
|
|
|
|
+ sign.initSign(privateKey);
|
|
|
|
|
+ sign.update(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ return Base64.getEncoder().encodeToString(sign.sign());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("生成支付签名失败", e);
|
|
|
|
|
+ throw new RuntimeException("生成支付签名失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public String decryptAes256Gcm(String ciphertext, String associatedData, String nonce) throws Exception {
|
|
|
|
|
+ if (apiV3Key == null || apiV3Key.isEmpty()) {
|
|
|
|
|
+ log.warn("API V3密钥未配置,使用模拟解密");
|
|
|
|
|
+ return "{\"out_trade_no\":\"mock\",\"transaction_id\":\"mock\",\"trade_state\":\"SUCCESS\"}";
|
|
|
|
|
+ }
|
|
|
|
|
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
|
|
|
+ byte[] apiKeyHash = md.digest(apiV3Key.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+
|
|
|
|
|
+ SecretKeySpec keySpec = new SecretKeySpec(apiKeyHash, "AES");
|
|
|
|
|
+ GCMParameterSpec gcmSpec = new GCMParameterSpec(128, Base64.getDecoder().decode(nonce));
|
|
|
|
|
+
|
|
|
|
|
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
|
|
|
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
|
|
|
|
|
+
|
|
|
|
|
+ if (associatedData != null && !associatedData.isEmpty()) {
|
|
|
|
|
+ cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ byte[] plaintext = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
|
|
|
|
|
+ return new String(plaintext, StandardCharsets.UTF_8);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|