Jelajahi Sumber

feat: VirtualPayController 接收虚拟支付消息推送
/api/internal/virtual-pay/notify(公开) + ios-refund-query,返回微信ErrCode协议

asus 2 bulan lalu
induk
melakukan
19184d6fc2

+ 118 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/VirtualPayController.java

@@ -0,0 +1,118 @@
+package com.etotem.cfc.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.etotem.cfc.service.VirtualPayService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 小程序虚拟支付消息推送接收端点。
+ * 微信服务器推送 URL 配置指向本控制器;/api/internal/ 已在 JwtInterceptor.PUBLIC_PATHS 前缀公开。
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/internal/virtual-pay")
+public class VirtualPayController {
+
+    @Resource
+    private VirtualPayService virtualPayService;
+
+    /** 微信虚拟支付发货/退款等消息推送(xpay_goods_deliver_notify / xpay_refund_notify / 其他) */
+    @PostMapping("/notify")
+    public Map<String, Object> notify(HttpServletRequest request) {
+        try {
+            // 读原始 body(getReader(),参照 ProductOrderController.notify() L120-155 范式)
+            BufferedReader reader = request.getReader();
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            String requestBody = sb.toString();
+            log.info("虚拟支付消息推送: body={}", requestBody);
+
+            JSONObject body = JSON.parseObject(requestBody);
+            if (body == null) {
+                log.error("虚拟支付消息推送body解析失败: {}", requestBody);
+                return successResult();
+            }
+
+            // 按 Event 类型分发(设计文档 §6.2)
+            // 微信虚拟支付推送字段: Event(如 xpay_goods_deliver_notify / xpay_refund_notify)、OutTradeNo、TransactionId、其他业务字段
+            // 若官方文档使用其他字段名,做多 key 兜底
+            String event = body.getString("Event");
+            if (event == null || event.isEmpty()) {
+                event = body.getString("EventType");
+            }
+            Map<String, Object> payload = new HashMap<>(body.getInnerMap());
+
+            if ("xpay_goods_deliver_notify".equals(event)) {
+                return virtualPayService.handleDeliverNotify(payload);
+            } else if ("xpay_refund_notify".equals(event)) {
+                return virtualPayService.handleRefundNotify(payload);
+            } else {
+                // 未知事件(含 xpay_coin_pay_notify):记日志忽略,道具直购不涉及
+                log.info("虚拟支付推送未知事件,忽略: event={}", event);
+                return successResult();
+            }
+        } catch (Exception e) {
+            // 解析失败返回成功 ack,避免微信重试死循环(最多15次)
+            log.error("处理虚拟支付推送异常,返回成功ack", e);
+            return successResult();
+        }
+    }
+
+    /** iOS App Store 退款问询应答(3秒内应答;3次未应答默认"不确定"交Apple裁决) */
+    @PostMapping("/ios-refund-query")
+    public Map<String, Object> iosRefundQuery(HttpServletRequest request) {
+        try {
+            BufferedReader reader = request.getReader();
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            String requestBody = sb.toString();
+            log.info("iOS退款问询: body={}", requestBody);
+
+            JSONObject body = JSON.parseObject(requestBody);
+            // 校验 payload 基本格式(如失败记日志返回最低响应)
+            if (body == null) {
+                log.error("iOS退款问询body解析失败: {}", requestBody);
+                return iosRefundBaseResponse();
+            }
+
+            // TODO Task 8/9: 按 OutTradeNo 查订阅状态后返回 {"IsAgree": true/false, "Reason": "..."}
+            // 未接入 Apple 服务端通知前返回协议规定的最低响应(默认不同意退款,交 Apple 裁决)
+            return iosRefundBaseResponse();
+        } catch (Exception e) {
+            log.error("处理iOS退款问询异常", e);
+            return iosRefundBaseResponse();
+        }
+    }
+
+    /** 微信协议成功应答 */
+    private Map<String, Object> successResult() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("ErrCode", 0);
+        result.put("ErrMsg", "success");
+        return result;
+    }
+
+    /** iOS 退款问询最低响应(协议规定应答字段,未接入时默认不同意由 Apple 裁决) */
+    private Map<String, Object> iosRefundBaseResponse() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("IsAgree", false);
+        result.put("Reason", "SERVER_NOT_IMPLEMENTED");
+        return result;
+    }
+}