|
|
@@ -0,0 +1,113 @@
|
|
|
+package com.xuekairui.payment.controller;
|
|
|
+
|
|
|
+import com.xuekairui.payment.service.AlipayCallbackRecordService;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestParam;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 支付宝开放能力回调接口
|
|
|
+ *
|
|
|
+ * <p>与 {@link PaymentCallbackController} 不同,本控制器处理的是非支付类回调:
|
|
|
+ * <ol>
|
|
|
+ * <li>应用网关:接收支付宝主动推送的消息(如退款、投诉、账单等)</li>
|
|
|
+ * <li>授权回调:接收支付宝 OAuth 授权码回调</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * <p>这两个地址需在支付宝开放平台后台的“应用网关地址”和“授权回调地址”中配置,
|
|
|
+ * 不应与支付异步通知地址混用。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/payment/alipay")
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class AlipayOpenController {
|
|
|
+
|
|
|
+ private final AlipayCallbackRecordService callbackRecordService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 支付宝应用网关入口
|
|
|
+ *
|
|
|
+ * <p>支付宝会把各类异步消息推送到此地址,例如:
|
|
|
+ * <ul>
|
|
|
+ * <li>alipay.trade.refund.depositback.completed(退款的保证金解冻完成)</li>
|
|
|
+ * <li>alipay.open.auth.userauth.cancelled(用户取消授权)</li>
|
|
|
+ * <li>各种账单/发票/对账通知</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p>当前会持久化到 {@code t_alipay_callback_record} 表并返回 success,避免支付宝重试。
|
|
|
+ * 后续若需处理具体业务,可根据 {@code msg_method} 字段分发。
|
|
|
+ *
|
|
|
+ * @see <a href="https://opendocs.alipay.com/open/194/106292">支付宝异步通知说明</a>
|
|
|
+ */
|
|
|
+ @PostMapping("/gateway")
|
|
|
+ public String gateway(HttpServletRequest request) {
|
|
|
+ Map<String, String> params = extractAllParams(request);
|
|
|
+ String msgMethod = params.get("msg_method");
|
|
|
+ String appId = params.get("app_id");
|
|
|
+ log.info("收到支付宝应用网关推送: msgMethod={}, appId={}, params={}", msgMethod, appId, maskSensitiveParams(params));
|
|
|
+
|
|
|
+ callbackRecordService.saveGatewayRecord(appId, msgMethod, params);
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 支付宝授权回调入口
|
|
|
+ *
|
|
|
+ * <p>用户在支付宝完成 OAuth 授权后,支付宝会重定向到此地址,携带 {@code auth_code}、
|
|
|
+ * {@code app_id}、{@code source} 等参数。</p>
|
|
|
+ *
|
|
|
+ * <p>当前会持久化到 {@code t_alipay_callback_record} 表并返回简单成功页面。</p>
|
|
|
+ *
|
|
|
+ * @see <a href="https://opendocs.alipay.com/open/284/106035">支付宝用户授权文档</a>
|
|
|
+ */
|
|
|
+ @GetMapping("/auth-callback")
|
|
|
+ public String authCallback(
|
|
|
+ @RequestParam(value = "auth_code", required = false) String authCode,
|
|
|
+ @RequestParam(value = "app_id", required = false) String appId,
|
|
|
+ @RequestParam(value = "source", required = false) String source,
|
|
|
+ @RequestParam(value = "state", required = false) String state) {
|
|
|
+
|
|
|
+ log.info("收到支付宝授权回调: appId={}, source={}, state={}", appId, source, state);
|
|
|
+ callbackRecordService.saveAuthCallbackRecord(appId, source, state, authCode);
|
|
|
+
|
|
|
+ // 如需真正换取 access_token,可在此调用 alipay.system.oauth.token
|
|
|
+ return "<html><body><h1>授权回调已收到</h1><p>授权码已记录,可关闭此页面。</p></body></html>";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取所有请求参数
|
|
|
+ */
|
|
|
+ private Map<String, String> extractAllParams(HttpServletRequest request) {
|
|
|
+ Map<String, String> params = new HashMap<>();
|
|
|
+ request.getParameterMap().forEach((key, values) -> {
|
|
|
+ if (values != null && values.length > 0) {
|
|
|
+ params.put(key, values[0]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return params;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对网关参数做脱敏,避免日志泄露敏感内容
|
|
|
+ */
|
|
|
+ private Map<String, String> maskSensitiveParams(Map<String, String> params) {
|
|
|
+ Map<String, String> masked = new HashMap<>(params);
|
|
|
+ if (masked.containsKey("sign")) {
|
|
|
+ masked.put("sign", "***");
|
|
|
+ }
|
|
|
+ if (masked.containsKey("biz_content")) {
|
|
|
+ String biz = masked.get("biz_content");
|
|
|
+ masked.put("biz_content", biz.length() > 100 ? biz.substring(0, 100) + "..." : biz);
|
|
|
+ }
|
|
|
+ return masked;
|
|
|
+ }
|
|
|
+}
|