|
|
@@ -1,544 +0,0 @@
|
|
|
-package com.etotem.cfc.controller;
|
|
|
-
|
|
|
-import com.alibaba.fastjson.JSON;
|
|
|
-import com.alibaba.fastjson.JSONObject;
|
|
|
-import com.etotem.cfc.common.Result;
|
|
|
-import com.etotem.cfc.entity.DanshopPaymentRecord;
|
|
|
-import com.etotem.cfc.mapper.DanshopPaymentRecordMapper;
|
|
|
-import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.springframework.beans.factory.annotation.Value;
|
|
|
-import org.springframework.http.ResponseEntity;
|
|
|
-import org.springframework.web.bind.annotation.PostMapping;
|
|
|
-import org.springframework.web.bind.annotation.RequestBody;
|
|
|
-import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
-import org.springframework.web.bind.annotation.RestController;
|
|
|
-import org.springframework.web.client.RestTemplate;
|
|
|
-
|
|
|
-import javax.annotation.Resource;
|
|
|
-import java.util.Date;
|
|
|
-import java.util.HashMap;
|
|
|
-import java.util.Map;
|
|
|
-import java.util.UUID;
|
|
|
-
|
|
|
-/**
|
|
|
- * Danshop 支付代理控制器
|
|
|
- * <p>
|
|
|
- * 供 danshop 的 CfcPaymentPlugin 调用,代理微信支付流程。
|
|
|
- * CFC 负责与微信支付直接交互,danshop 无需配置微信支付。
|
|
|
- */
|
|
|
-@Slf4j
|
|
|
-@RestController
|
|
|
-@RequestMapping("/api/internal/payment")
|
|
|
-public class DanshopPaymentController {
|
|
|
-
|
|
|
- @Resource
|
|
|
- private DanshopPaymentRecordMapper paymentRecordMapper;
|
|
|
-
|
|
|
- @Value("${wechat.appid}")
|
|
|
- private String appid;
|
|
|
-
|
|
|
- @Value("${wechat.mch-id}")
|
|
|
- private String mchId;
|
|
|
-
|
|
|
- @Value("${wechat.mch-key}")
|
|
|
- private String mchKey;
|
|
|
-
|
|
|
- @Value("${wechat.notify-url}")
|
|
|
- private String wechatNotifyUrl;
|
|
|
-
|
|
|
- @Value("${wechat.danshop-notify-url:${wechat.notify-url}}")
|
|
|
- private String danshopWechatNotifyUrl;
|
|
|
-
|
|
|
- @Value("${danshop.base-url:http://localhost:8888}")
|
|
|
- private String danshopBaseUrl;
|
|
|
-
|
|
|
- private final RestTemplate restTemplate = new RestTemplate();
|
|
|
-
|
|
|
- /**
|
|
|
- * 小程序支付(JSAPI)
|
|
|
- * 由 danshop CfcPaymentPlugin.mpPay() 调用
|
|
|
- */
|
|
|
- @PostMapping("/mp-pay")
|
|
|
- public Result<Map<String, Object>> mpPay(@RequestBody Map<String, Object> params) {
|
|
|
- try {
|
|
|
- String orderSn = (String) params.get("orderSn");
|
|
|
- String orderType = (String) params.getOrDefault("orderType", "ORDER");
|
|
|
- Integer amount = params.get("amount") instanceof Integer
|
|
|
- ? (Integer) params.get("amount")
|
|
|
- : Integer.valueOf(params.get("amount").toString());
|
|
|
- String description = (String) params.getOrDefault("description", "");
|
|
|
- String openid = (String) params.get("openid");
|
|
|
-
|
|
|
- if (orderSn == null || amount == null || openid == null) {
|
|
|
- return Result.error(400, "缺少必要参数: orderSn, amount, openid");
|
|
|
- }
|
|
|
-
|
|
|
- // 1. 创建支付记录
|
|
|
- String outTradeNo = "CFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
|
|
|
-
|
|
|
- DanshopPaymentRecord record = new DanshopPaymentRecord();
|
|
|
- record.setOutTradeNo(outTradeNo);
|
|
|
- record.setDanshopOrderSn(orderSn);
|
|
|
- record.setDanshopOrderType(orderType);
|
|
|
- record.setAmount(amount);
|
|
|
- record.setOpenid(openid);
|
|
|
- record.setStatus("pending");
|
|
|
- record.setCreatedAt(new Date());
|
|
|
- record.setUpdatedAt(new Date());
|
|
|
- paymentRecordMapper.insert(record);
|
|
|
-
|
|
|
- // 记录 danshop 回调 URL
|
|
|
- record.setNotifyUrl(danshopBaseUrl + "/buyer/payment/cashier/notify/CFC");
|
|
|
- paymentRecordMapper.updateById(record);
|
|
|
-
|
|
|
- // 2. 调用微信统一下单接口(使用独立的 danshop notify URL)
|
|
|
- String wxUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
|
|
|
-
|
|
|
- JSONObject wxParams = new JSONObject();
|
|
|
- wxParams.put("appid", appid);
|
|
|
- wxParams.put("mch_id", mchId);
|
|
|
- wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
|
|
|
- wxParams.put("body", description.length() > 32 ? description.substring(0, 32) : description);
|
|
|
- wxParams.put("out_trade_no", outTradeNo);
|
|
|
- wxParams.put("total_fee", amount);
|
|
|
- wxParams.put("spbill_create_ip", "127.0.0.1");
|
|
|
- wxParams.put("notify_url", danshopWechatNotifyUrl);
|
|
|
- wxParams.put("trade_type", "JSAPI");
|
|
|
- wxParams.put("openid", openid);
|
|
|
-
|
|
|
- // 生成签名
|
|
|
- String sign = generateSign(wxParams, mchKey);
|
|
|
- wxParams.put("sign", sign);
|
|
|
-
|
|
|
- log.info("微信统一下单: outTradeNo={}, amount={}, openid={}", outTradeNo, amount, openid);
|
|
|
-
|
|
|
- ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
|
|
|
- JSONObject wxResult = JSON.parseObject(response.getBody());
|
|
|
-
|
|
|
- if ("SUCCESS".equals(wxResult.getString("return_code"))
|
|
|
- && "SUCCESS".equals(wxResult.getString("result_code"))) {
|
|
|
-
|
|
|
- String prepayId = wxResult.getString("prepay_id");
|
|
|
-
|
|
|
- // 3. 生成前端支付参数
|
|
|
- Map<String, Object> payParams = new HashMap<>();
|
|
|
- payParams.put("appId", appid);
|
|
|
- payParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
|
|
|
- payParams.put("nonceStr", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
|
|
|
- payParams.put("package", "prepay_id=" + prepayId);
|
|
|
- payParams.put("signType", "MD5");
|
|
|
-
|
|
|
- // 生成 paySign
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
- sb.append("appId=").append(appid).append("&");
|
|
|
- sb.append("nonceStr=").append(payParams.get("nonceStr")).append("&");
|
|
|
- sb.append("package=").append(payParams.get("package")).append("&");
|
|
|
- sb.append("signType=MD5&");
|
|
|
- sb.append("timeStamp=").append(payParams.get("timeStamp")).append("&");
|
|
|
- sb.append("key=").append(mchKey);
|
|
|
- String paySign = md5Upper(sb.toString());
|
|
|
- payParams.put("paySign", paySign);
|
|
|
-
|
|
|
- // 4. 附加信息
|
|
|
- payParams.put("outTradeNo", outTradeNo);
|
|
|
- payParams.put("orderSn", orderSn);
|
|
|
- payParams.put("recordId", record.getId());
|
|
|
-
|
|
|
- log.info("微信下单成功: prepayId={}", prepayId);
|
|
|
- return Result.success(payParams);
|
|
|
- } else {
|
|
|
- String errMsg = wxResult.getString("return_msg");
|
|
|
- log.error("微信下单失败: errCode={}, errMsg={}",
|
|
|
- wxResult.getString("err_code"), errMsg);
|
|
|
- return Result.error(500, "微信下单失败: " + errMsg);
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("CFC支付代理异常", e);
|
|
|
- return Result.error(500, "支付异常: " + e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * H5 支付
|
|
|
- */
|
|
|
- @PostMapping("/h5-pay")
|
|
|
- public Result<Map<String, Object>> h5Pay(@RequestBody Map<String, Object> params) {
|
|
|
- try {
|
|
|
- String orderSn = (String) params.get("orderSn");
|
|
|
- Integer amount = params.get("amount") instanceof Integer
|
|
|
- ? (Integer) params.get("amount")
|
|
|
- : Integer.valueOf(params.get("amount").toString());
|
|
|
- String description = (String) params.getOrDefault("description", "");
|
|
|
-
|
|
|
- String outTradeNo = "CFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
|
|
|
-
|
|
|
- String wxUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
|
|
|
- JSONObject wxParams = new JSONObject();
|
|
|
- wxParams.put("appid", appid);
|
|
|
- wxParams.put("mch_id", mchId);
|
|
|
- wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
|
|
|
- wxParams.put("body", description.length() > 32 ? description.substring(0, 32) : description);
|
|
|
- wxParams.put("out_trade_no", outTradeNo);
|
|
|
- wxParams.put("total_fee", amount);
|
|
|
- wxParams.put("spbill_create_ip", "127.0.0.1");
|
|
|
- wxParams.put("notify_url", wechatNotifyUrl);
|
|
|
- wxParams.put("trade_type", "MWEB");
|
|
|
-
|
|
|
- String sign = generateSign(wxParams, mchKey);
|
|
|
- wxParams.put("sign", sign);
|
|
|
-
|
|
|
- ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
|
|
|
- JSONObject wxResult = JSON.parseObject(response.getBody());
|
|
|
-
|
|
|
- if ("SUCCESS".equals(wxResult.getString("return_code"))
|
|
|
- && "SUCCESS".equals(wxResult.getString("result_code"))) {
|
|
|
-
|
|
|
- Map<String, Object> result = new HashMap<>();
|
|
|
- result.put("mwebUrl", wxResult.getString("mweb_url"));
|
|
|
- result.put("outTradeNo", outTradeNo);
|
|
|
- return Result.success(result);
|
|
|
- }
|
|
|
- return Result.error(500, "微信下单失败");
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("CFC H5支付异常", e);
|
|
|
- return Result.error(500, "支付异常: " + e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Native 支付(扫码)
|
|
|
- */
|
|
|
- @PostMapping("/native-pay")
|
|
|
- public Result<Map<String, Object>> nativePay(@RequestBody Map<String, Object> params) {
|
|
|
- try {
|
|
|
- String orderSn = (String) params.get("orderSn");
|
|
|
- Integer amount = params.get("amount") instanceof Integer
|
|
|
- ? (Integer) params.get("amount")
|
|
|
- : Integer.valueOf(params.get("amount").toString());
|
|
|
- String description = (String) params.getOrDefault("description", "");
|
|
|
-
|
|
|
- String outTradeNo = "CFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
|
|
|
-
|
|
|
- String wxUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
|
|
|
- JSONObject wxParams = new JSONObject();
|
|
|
- wxParams.put("appid", appid);
|
|
|
- wxParams.put("mch_id", mchId);
|
|
|
- wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
|
|
|
- wxParams.put("body", description.length() > 32 ? description.substring(0, 32) : description);
|
|
|
- wxParams.put("out_trade_no", outTradeNo);
|
|
|
- wxParams.put("total_fee", amount);
|
|
|
- wxParams.put("spbill_create_ip", "127.0.0.1");
|
|
|
- wxParams.put("notify_url", wechatNotifyUrl);
|
|
|
- wxParams.put("trade_type", "NATIVE");
|
|
|
-
|
|
|
- String sign = generateSign(wxParams, mchKey);
|
|
|
- wxParams.put("sign", sign);
|
|
|
-
|
|
|
- ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
|
|
|
- JSONObject wxResult = JSON.parseObject(response.getBody());
|
|
|
-
|
|
|
- if ("SUCCESS".equals(wxResult.getString("return_code"))
|
|
|
- && "SUCCESS".equals(wxResult.getString("result_code"))) {
|
|
|
-
|
|
|
- Map<String, Object> result = new HashMap<>();
|
|
|
- result.put("codeUrl", wxResult.getString("code_url"));
|
|
|
- result.put("outTradeNo", outTradeNo);
|
|
|
- return Result.success(result);
|
|
|
- }
|
|
|
- return Result.error(500, "微信下单失败");
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("CFC Native支付异常", e);
|
|
|
- return Result.error(500, "支付异常: " + e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 支付成功回调通知(由 cfc 内部 WeChat Pay notify 处理完后调用此方法通知 danshop)
|
|
|
- * 此接口由 PackagePaymentController.handlePayNotify() 在收到微信通知后调用
|
|
|
- */
|
|
|
- @PostMapping("/notify-callback")
|
|
|
- public Result<Void> handleDanshopNotifyCallback(@RequestBody Map<String, Object> params) {
|
|
|
- try {
|
|
|
- String outTradeNo = (String) params.get("outTradeNo");
|
|
|
- String transactionId = (String) params.get("transactionId");
|
|
|
- String status = (String) params.getOrDefault("status", "paid");
|
|
|
-
|
|
|
- if (outTradeNo == null) {
|
|
|
- return Result.error(400, "缺少 outTradeNo");
|
|
|
- }
|
|
|
-
|
|
|
- // 查找支付记录
|
|
|
- DanshopPaymentRecord record = paymentRecordMapper.selectOne(
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DanshopPaymentRecord>()
|
|
|
- .eq(DanshopPaymentRecord::getOutTradeNo, outTradeNo)
|
|
|
- );
|
|
|
-
|
|
|
- if (record == null) {
|
|
|
- log.warn("未找到支付记录: outTradeNo={}", outTradeNo);
|
|
|
- return Result.error(404, "支付记录不存在");
|
|
|
- }
|
|
|
-
|
|
|
- // 更新记录状态
|
|
|
- record.setStatus(status);
|
|
|
- record.setTransactionId(transactionId);
|
|
|
- record.setUpdatedAt(new Date());
|
|
|
- paymentRecordMapper.updateById(record);
|
|
|
-
|
|
|
- // 通知 danshop
|
|
|
- if (record.getNotifyUrl() != null) {
|
|
|
- Map<String, Object> notifyBody = new HashMap<>();
|
|
|
- notifyBody.put("orderSn", record.getDanshopOrderSn());
|
|
|
- notifyBody.put("orderType", record.getDanshopOrderType());
|
|
|
- notifyBody.put("transactionId", transactionId);
|
|
|
- notifyBody.put("amount", record.getAmount() / 100.0);
|
|
|
- notifyBody.put("status", status);
|
|
|
-
|
|
|
- try {
|
|
|
- log.info("通知 danshop 支付结果: url={}, body={}", record.getNotifyUrl(), notifyBody);
|
|
|
- restTemplate.postForEntity(record.getNotifyUrl(), notifyBody, String.class);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("通知 danshop 失败: {}", e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return Result.success("ok", null);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("处理 danshop 支付回调异常", e);
|
|
|
- return Result.error(500, "处理异常: " + e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 微信支付异步通知(WeChat Pay → cfc)
|
|
|
- * 当用户完成微信支付后,微信服务器回调此接口
|
|
|
- */
|
|
|
- @PostMapping("/wechat-notify")
|
|
|
- public String handleWechatNotify(@RequestBody String xmlData) {
|
|
|
- log.info("Danshop支付-微信异步通知: xmlData={}", xmlData);
|
|
|
- try {
|
|
|
- // 解析 XML
|
|
|
- Map<String, String> wxParams = parseXml(xmlData);
|
|
|
-
|
|
|
- String returnCode = wxParams.get("return_code");
|
|
|
- if (!"SUCCESS".equals(returnCode)) {
|
|
|
- log.warn("微信通知失败: return_code={}", returnCode);
|
|
|
- return xmlResponse("FAIL", "支付失败");
|
|
|
- }
|
|
|
-
|
|
|
- // 验签
|
|
|
- String sign = wxParams.get("sign");
|
|
|
- wxParams.remove("sign");
|
|
|
- String calculatedSign = generateSignFromMap(wxParams, mchKey);
|
|
|
- if (!sign.equals(calculatedSign)) {
|
|
|
- log.error("微信通知签名验证失败");
|
|
|
- return xmlResponse("FAIL", "签名验证失败");
|
|
|
- }
|
|
|
-
|
|
|
- String outTradeNo = wxParams.get("out_trade_no");
|
|
|
- String transactionId = wxParams.get("transaction_id");
|
|
|
-
|
|
|
- if (outTradeNo == null) {
|
|
|
- log.error("微信通知缺少 out_trade_no");
|
|
|
- return xmlResponse("FAIL", "参数缺失");
|
|
|
- }
|
|
|
-
|
|
|
- // 查找支付记录
|
|
|
- DanshopPaymentRecord record = paymentRecordMapper.selectOne(
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DanshopPaymentRecord>()
|
|
|
- .eq(DanshopPaymentRecord::getOutTradeNo, outTradeNo)
|
|
|
- );
|
|
|
-
|
|
|
- if (record == null) {
|
|
|
- log.warn("微信通知: 未找到支付记录 outTradeNo={}", outTradeNo);
|
|
|
- return xmlResponse("FAIL", "记录不存在");
|
|
|
- }
|
|
|
-
|
|
|
- // 更新记录
|
|
|
- record.setStatus("paid");
|
|
|
- record.setTransactionId(transactionId);
|
|
|
- record.setUpdatedAt(new Date());
|
|
|
- paymentRecordMapper.updateById(record);
|
|
|
-
|
|
|
- log.info("Danshop支付-微信支付成功: outTradeNo={}, transactionId={}, danshopOrderSn={}",
|
|
|
- outTradeNo, transactionId, record.getDanshopOrderSn());
|
|
|
-
|
|
|
- // 通知 danshop
|
|
|
- if (record.getNotifyUrl() != null) {
|
|
|
- Map<String, Object> notifyBody = new HashMap<>();
|
|
|
- notifyBody.put("orderSn", record.getDanshopOrderSn());
|
|
|
- notifyBody.put("orderType", record.getDanshopOrderType());
|
|
|
- notifyBody.put("transactionId", transactionId);
|
|
|
- notifyBody.put("amount", record.getAmount() / 100.0);
|
|
|
- notifyBody.put("status", "paid");
|
|
|
-
|
|
|
- try {
|
|
|
- log.info("通知 danshop 支付结果: url={}", record.getNotifyUrl());
|
|
|
- restTemplate.postForEntity(record.getNotifyUrl(), notifyBody, String.class);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("通知 danshop 支付结果失败: {}", e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return xmlResponse("SUCCESS", "OK");
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("处理 Danshop 支付微信通知异常", e);
|
|
|
- return xmlResponse("FAIL", "处理异常");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 退款处理
|
|
|
- * 由 danshop CfcPaymentPlugin.refund() 调用
|
|
|
- */
|
|
|
- @PostMapping("/refund")
|
|
|
- public Result<Void> refund(@RequestBody Map<String, Object> params) {
|
|
|
- try {
|
|
|
- String orderSn = (String) params.get("orderSn");
|
|
|
- Double amount = params.get("amount") instanceof Double
|
|
|
- ? (Double) params.get("amount")
|
|
|
- : Double.valueOf(params.get("amount").toString());
|
|
|
- String reason = (String) params.getOrDefault("reason", "");
|
|
|
- String refundNo = (String) params.getOrDefault("refundNo", "");
|
|
|
-
|
|
|
- if (orderSn == null || amount == null) {
|
|
|
- return Result.error(400, "缺少必要参数: orderSn, amount");
|
|
|
- }
|
|
|
-
|
|
|
- // 查找支付记录
|
|
|
- DanshopPaymentRecord record = paymentRecordMapper.selectOne(
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DanshopPaymentRecord>()
|
|
|
- .eq(DanshopPaymentRecord::getDanshopOrderSn, orderSn)
|
|
|
- .orderByDesc(DanshopPaymentRecord::getId)
|
|
|
- .last("LIMIT 1")
|
|
|
- );
|
|
|
-
|
|
|
- if (record == null) {
|
|
|
- log.warn("退款时未找到支付记录: orderSn={}", orderSn);
|
|
|
- return Result.error(404, "未找到支付记录");
|
|
|
- }
|
|
|
-
|
|
|
- String outRefundNo = "RFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 4).toUpperCase();
|
|
|
-
|
|
|
- // 调用微信退款 API
|
|
|
- String wxUrl = "https://api.mch.weixin.qq.com/secapi/pay/refund";
|
|
|
-
|
|
|
- JSONObject wxParams = new JSONObject();
|
|
|
- wxParams.put("appid", appid);
|
|
|
- wxParams.put("mch_id", mchId);
|
|
|
- wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
|
|
|
- wxParams.put("out_trade_no", record.getOutTradeNo());
|
|
|
- wxParams.put("out_refund_no", outRefundNo);
|
|
|
- wxParams.put("total_fee", record.getAmount());
|
|
|
- wxParams.put("refund_fee", (int) (amount * 100));
|
|
|
- wxParams.put("refund_desc", reason);
|
|
|
-
|
|
|
- String sign = generateSign(wxParams, mchKey);
|
|
|
- wxParams.put("sign", sign);
|
|
|
-
|
|
|
- log.info("微信退款请求: outTradeNo={}, refundFee={}", record.getOutTradeNo(), amount * 100);
|
|
|
-
|
|
|
- // 退款需要双向证书,这里使用 RestTemplate(生产环境需配置证书)
|
|
|
- // 注意:微信退款 API 需要双向证书,目前简化处理
|
|
|
- // TODO: 生产环境配置微信支付证书
|
|
|
- try {
|
|
|
- ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
|
|
|
- JSONObject wxResult = JSON.parseObject(response.getBody());
|
|
|
-
|
|
|
- if ("SUCCESS".equals(wxResult.getString("return_code"))
|
|
|
- && "SUCCESS".equals(wxResult.getString("result_code"))) {
|
|
|
- record.setStatus("refunded");
|
|
|
- record.setRefundNo(outRefundNo);
|
|
|
- record.setUpdatedAt(new Date());
|
|
|
- paymentRecordMapper.updateById(record);
|
|
|
-
|
|
|
- log.info("微信退款成功: outRefundNo={}", outRefundNo);
|
|
|
- return Result.success("ok", null);
|
|
|
- } else {
|
|
|
- log.error("微信退款失败: {}", wxResult.getString("return_msg"));
|
|
|
- return Result.error(500, "退款失败: " + wxResult.getString("return_msg"));
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("微信退款 API 调用失败(可能未配置证书): {}", e.getMessage());
|
|
|
- // 即使微信退款失败,也记录退款状态
|
|
|
- record.setStatus("refunded");
|
|
|
- record.setRefundNo(outRefundNo);
|
|
|
- record.setUpdatedAt(new Date());
|
|
|
- paymentRecordMapper.updateById(record);
|
|
|
- return Result.success("ok, but wechat refund may need certificate config", null);
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("CFC退款处理异常", e);
|
|
|
- return Result.error(500, "退款异常: " + e.getMessage());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ==================== 工具方法 ====================
|
|
|
-
|
|
|
- private String generateSign(JSONObject params, String key) {
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
- params.keySet().stream().sorted().forEach(k -> {
|
|
|
- if (params.getString(k) != null && !params.getString(k).isEmpty()) {
|
|
|
- sb.append(k).append("=").append(params.getString(k)).append("&");
|
|
|
- }
|
|
|
- });
|
|
|
- sb.append("key=").append(key);
|
|
|
- return md5Upper(sb.toString());
|
|
|
- }
|
|
|
-
|
|
|
- private String generateSignFromMap(Map<String, String> params, String key) {
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
- params.keySet().stream().sorted().forEach(k -> {
|
|
|
- String v = params.get(k);
|
|
|
- if (v != null && !v.isEmpty()) {
|
|
|
- sb.append(k).append("=").append(v).append("&");
|
|
|
- }
|
|
|
- });
|
|
|
- sb.append("key=").append(key);
|
|
|
- return md5Upper(sb.toString());
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 简单 XML 解析为 Map
|
|
|
- */
|
|
|
- private Map<String, String> parseXml(String xml) {
|
|
|
- Map<String, String> map = new HashMap<>();
|
|
|
- try {
|
|
|
- javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
|
|
|
- javax.xml.parsers.DocumentBuilder builder = factory.newDocumentBuilder();
|
|
|
- org.w3c.dom.Document doc = builder.parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
|
|
|
- org.w3c.dom.Element root = doc.getDocumentElement();
|
|
|
- org.w3c.dom.NodeList nodeList = root.getChildNodes();
|
|
|
- for (int i = 0; i < nodeList.getLength(); i++) {
|
|
|
- org.w3c.dom.Node node = nodeList.item(i);
|
|
|
- if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
|
|
|
- map.put(node.getNodeName(), node.getTextContent());
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("XML解析失败", e);
|
|
|
- }
|
|
|
- return map;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 构建微信 XML 响应
|
|
|
- */
|
|
|
- private String xmlResponse(String returnCode, String returnMsg) {
|
|
|
- return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code>"
|
|
|
- + "<return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
|
|
|
- }
|
|
|
-
|
|
|
- private String md5Upper(String str) {
|
|
|
- try {
|
|
|
- java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
|
|
|
- byte[] digest = md.digest(str.getBytes("UTF-8"));
|
|
|
- StringBuilder hex = new StringBuilder();
|
|
|
- for (byte b : digest) {
|
|
|
- hex.append(String.format("%02x", b));
|
|
|
- }
|
|
|
- return hex.toString().toUpperCase();
|
|
|
- } catch (Exception e) {
|
|
|
- throw new RuntimeException("MD5失败", e);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|