package com.talentcard.server.business.payment; import java.util.Map; import com.alibaba.fastjson.JSON; import com.talentcard.server.business.merchant.AutoWithdrawalResultService; import com.talentcard.server.business.merchant.MerchantSettlementService; import com.talentcard.server.business.payment.huifu.HuifuSignatureService; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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; @RestController @RequestMapping("/api/payments/huifu") public class PaymentCallbackController { private final HuifuPaymentCallbackService callbackService; private final AutoWithdrawalResultService withdrawalService; private final HuifuSignatureService signatureService; private final MerchantSettlementService settlementService; public PaymentCallbackController(HuifuPaymentCallbackService callbackService, AutoWithdrawalResultService withdrawalService, HuifuSignatureService signatureService, MerchantSettlementService settlementService) { this.callbackService = callbackService; this.withdrawalService = withdrawalService; this.signatureService = signatureService; this.settlementService = settlementService; } @PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity notify(@RequestParam Map form) { return ResponseEntity.ok(callbackService.handle(form.get("resp_data"), form.get("sign"))); } /** * 主动取现的异步通知。验签只接受斗拱平台公钥的签名;请求号必须对得上一条本地提现记录, * 否则视为未知通知。真正的状态收敛由 {@link AutoWithdrawalResultService} 幂等地完成, * 因此重复通知、通道的两次通知(msg_type 01 通道 / 02 账务)都会得到同一个最终状态。 */ @PostMapping(value = "/withdrawal-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity withdrawalNotify(@RequestParam Map form) { String payload = form.get("resp_data"), signature = form.get("sign"); if (!signatureService.verifyCallbackPayload(payload, signature)) throw new BusinessException(ErrorCode.FORBIDDEN, "Invalid Huifu withdrawal callback signature"); @SuppressWarnings("unchecked") Map data = JSON.parseObject(payload, Map.class); String no = data.get("req_seq_id") == null ? "" : String.valueOf(data.get("req_seq_id")).trim(); if (no.isBlank() || !withdrawalService.exists(no)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu withdrawal callback"); withdrawalService.handleNotification(no, data); return ResponseEntity.ok("RECV_ORD_ID_" + no); } @PostMapping(value = "/merchant-business-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity merchantBusinessNotify(@RequestParam Map form) { String payload = form.get("resp_data"), signature = form.get("sign"); if (!signatureService.verifyCallbackPayload(payload, signature)) throw new BusinessException(ErrorCode.FORBIDDEN, "Invalid Huifu merchant business callback signature"); @SuppressWarnings("unchecked") Map data = JSON.parseObject(payload, Map.class); String no = data.get("req_seq_id") == null ? "" : String.valueOf(data.get("req_seq_id")).trim(); if (!settlementService.handleBankCardCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu merchant business callback"); return ResponseEntity.ok("RECV_ORD_ID_" + no); } @PostMapping(value = "/merchant-onboarding-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity merchantOnboardingNotify(@RequestParam Map form) { String payload = form.get("resp_data"), signature = form.get("sign"); if (!signatureService.verifyCallbackPayload(payload, signature)) throw new BusinessException(ErrorCode.FORBIDDEN, "Invalid Huifu merchant onboarding callback signature"); @SuppressWarnings("unchecked") Map data = JSON.parseObject(payload, Map.class); String no = data.get("req_seq_id") == null ? "" : String.valueOf(data.get("req_seq_id")).trim(); if (!settlementService.handleOnboardingCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu merchant onboarding callback"); return ResponseEntity.ok("RECV_ORD_ID_" + no); } @PostMapping(value = "/merchant-wechat-config-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity merchantWechatConfigNotify(@RequestParam Map form) { Map data = verifiedData(form, "WeChat merchant configuration"); if (!settlementService.handleWechatConfigCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu WeChat merchant configuration callback"); return ResponseEntity.ok("RECV_ORD_ID_" + acknowledgementId(data)); } @PostMapping(value = "/merchant-business-open-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity merchantBusinessOpenNotify(@RequestParam Map form) { Map data = verifiedData(form, "merchant business opening"); if (!settlementService.handleBusinessOpenCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu merchant business opening callback"); return ResponseEntity.ok("RECV_ORD_ID_" + acknowledgementId(data)); } @SuppressWarnings("unchecked") private Map verifiedData(Map form, String operation) { String payload = form.get("resp_data"), signature = form.get("sign"); if (!signatureService.verifyCallbackPayload(payload, signature)) throw new BusinessException(ErrorCode.FORBIDDEN, "Invalid Huifu " + operation + " callback signature"); Map callback = JSON.parseObject(payload, Map.class); Object nested = callback.get("data"); if (nested instanceof Map value) return (Map) value; if (nested instanceof String value && !value.isBlank()) return JSON.parseObject(value, Map.class); return callback; } private String acknowledgementId(Map data) { Object value = data.get("ord_id"); if (value == null || String.valueOf(value).isBlank()) value = data.get("req_seq_id"); return value == null ? "" : String.valueOf(value).trim(); } }