package com.ruoyi.payment.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ruoyi.common.core.domain.R; import com.ruoyi.order.api.feignClient.ChargingOrderClient; import com.ruoyi.order.api.feignClient.OrderClient; import com.ruoyi.payment.api.vo.IcbcPaymentReq; import com.ruoyi.payment.api.vo.IcbcPaymentResp; import com.ruoyi.payment.api.vo.IcbcRefundReq; import com.ruoyi.payment.api.vo.IcbcRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp; import com.ruoyi.payment.icbc.config.IcbcPayProperties; import com.ruoyi.payment.icbc.service.IcbcPayService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.math.BigDecimal; import java.io.PrintWriter; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * 工行统一支付入口。 */ @Slf4j @RestController @RequestMapping("/icbc") public class IcbcPayController { private static final String TEST_ORDER_PREFIX = "ICBCTEST"; private static final String TEST_SUB_MER_ID = "231011990055"; private static final String TEST_SUB_MER_PRTCL_NO = "2310119900550201"; @Resource private IcbcPayService icbcPayService; @Resource private IcbcPayProperties icbcPayProperties; @Resource private ChargingOrderClient chargingOrderClient; @Resource private OrderClient orderClient; @PostMapping("/payment") public R payment(@RequestBody IcbcPaymentReq req) { log.info("工行支付请求入参:{}", JSON.toJSONString(req)); IcbcPaymentResp resp = icbcPayService.payment(req); log.info("工行支付返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/payment") public R testPayment(@RequestBody(required = false) IcbcPaymentReq req) { log.info("工行测试支付请求入参:{}", JSON.toJSONString(req)); IcbcPaymentReq paymentReq = req == null ? new IcbcPaymentReq() : req; paymentReq.setOutTradeNo(TEST_ORDER_PREFIX + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); if (paymentReq.getAmount() == null || paymentReq.getAmount().compareTo(BigDecimal.ZERO) <= 0) { paymentReq.setAmount(new BigDecimal("0.01")); } if (paymentReq.getSubject() == null || paymentReq.getSubject().trim().isEmpty()) { paymentReq.setSubject("工行测试支付"); } if (paymentReq.getBody() == null || paymentReq.getBody().trim().isEmpty()) { paymentReq.setBody("工行测试支付"); } if (paymentReq.getPaymentType() == null) { paymentReq.setPaymentType(1); } log.info("工行测试支付实际下单参数:{}", JSON.toJSONString(paymentReq)); IcbcPaymentResp resp = icbcPayService.payment(paymentReq); log.info("工行测试支付返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/query") public R testQuery(@RequestBody Map req) { String outTradeNo = getString(req, "outTradeNo"); log.info("工行测试查询请求入参:{}", JSON.toJSONString(req)); IcbcPaymentResp resp = icbcPayService.query(outTradeNo); log.info("工行测试查询返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/refund") public R testRefund(@RequestBody(required = false) IcbcRefundReq req) { req = req == null ? new IcbcRefundReq() : req; log.info("工行测试退款请求入参:{}", JSON.toJSONString(req)); if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) { req.setRefundAmount(new BigDecimal("0.01")); } if (req.getTotalAmount() == null || req.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) { req.setTotalAmount(req.getRefundAmount()); } if (req.getOutRefundNo() == null || req.getOutRefundNo().trim().isEmpty()) { req.setOutRefundNo("ICBCREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getReason() == null || req.getReason().trim().isEmpty()) { req.setReason("工行测试退款"); } log.info("工行测试退款实际参数:{}", JSON.toJSONString(req)); IcbcRefundResp resp = icbcPayService.refund(req); log.info("工行测试退款返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/sub-order/split") public R testSplitSubOrder(@RequestBody(required = false) IcbcSubOrderSplitReq req) { req = req == null ? new IcbcSubOrderSplitReq() : req; log.info("工行测试分账请求入参:{}", JSON.toJSONString(req)); if (req.getSubOrderId() == null || req.getSubOrderId().trim().isEmpty()) { req.setSubOrderId("ICBCSUB" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) { req.setSeqNo(req.getChargingOrderCode()); } if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) { req.setBusiType("1"); } if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) { req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); } if (req.getClassifyAmt() == null || req.getClassifyAmt().compareTo(BigDecimal.ZERO) <= 0) { req.setClassifyAmt(new BigDecimal("0.01")); } if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) { req.setSubMerId(TEST_SUB_MER_ID); } if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) { req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO); } if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) { req.setOriMerId(icbcPayProperties.getMerId()); } log.info("工行测试分账实际参数:{}", JSON.toJSONString(req)); IcbcSubOrderSplitResp resp = icbcPayService.splitSubOrder(req); log.info("工行测试分账返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/sub-order/refund") public R testRefundSubOrder(@RequestBody(required = false) IcbcSubOrderRefundReq req) { req = req == null ? new IcbcSubOrderRefundReq() : req; log.info("工行测试分账退款请求入参:{}", JSON.toJSONString(req)); if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) { req.setSeqNo(req.getChargingOrderCode()); } if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) { req.setBusiType("1"); } if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) { req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); } if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) { req.setRefundAmount(new BigDecimal("0.01")); } if (req.getRetSubOrderId() == null || req.getRetSubOrderId().trim().isEmpty()) { req.setRetSubOrderId("ICBCSUBREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getOuttrxSerialNo() == null || req.getOuttrxSerialNo().trim().isEmpty()) { req.setOuttrxSerialNo("ICBCSUBREFUNDNO" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) { req.setSubMerId(TEST_SUB_MER_ID); } if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) { req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO); } if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) { req.setOriMerId(icbcPayProperties.getMerId()); } log.info("工行测试分账退款实际参数:{}", JSON.toJSONString(req)); IcbcSubOrderRefundResp resp = icbcPayService.refundSubOrder(req); log.info("工行测试分账退款返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/query") public R query(@RequestParam("outTradeNo") String outTradeNo) { return R.ok(icbcPayService.query(outTradeNo)); } @PostMapping("/close") public R close(@RequestParam("outTradeNo") String outTradeNo) { return R.ok(icbcPayService.close(outTradeNo)); } @PostMapping("/refund") public R refund(@RequestBody IcbcRefundReq req) { return R.ok(icbcPayService.refund(req)); } @PostMapping("/sub-order/split") public R splitSubOrder(@RequestBody IcbcSubOrderSplitReq req) { return R.ok(icbcPayService.splitSubOrder(req)); } @PostMapping("/sub-order/refund") public R refundSubOrder(@RequestBody IcbcSubOrderRefundReq req) { return R.ok(icbcPayService.refundSubOrder(req)); } @PostMapping("/sub-order/query") public R querySubOrder(@RequestBody IcbcSubOrderQueryReq req) { return R.ok(icbcPayService.querySubOrder(req)); } @RequestMapping(value = "/callBack", method = {RequestMethod.GET, RequestMethod.POST}) public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception { Map requestParams = new LinkedHashMap<>(request.getParameterMap()); String rawBody = requestParams.isEmpty() ? readRequestBody(request) : ""; mergeJsonBody(requestParams, rawBody); boolean success = false; log.info("工行支付回调原始请求,method:{},uri:{},query:{},contentType:{},params:{},body:{},headers:{}", request.getMethod(), request.getRequestURI(), request.getQueryString(), request.getContentType(), JSON.toJSONString(requestParams), rawBody, summarizeHeaders(request)); try { Map callback; try { callback = icbcPayService.parseNotify(requestParams); } catch (Exception verifyException) { if (!isSignatureVerificationFailure(verifyException)) { throw verifyException; } callback = recoverCallbackByPaymentQuery(requestParams); log.warn("工行支付回调验签失败,已通过主动查询确认支付成功并继续处理,订单号:{},工行订单号:{}", callback.get("out_trade_no"), callback.get("transaction_id")); } log.info("工行支付回调解析结果:{}", JSON.toJSONString(callback)); dispatchPaymentCallback(callback); success = true; } catch (Exception e) { log.error("工行支付回调处理失败,已记录原始请求并返回fail等待重试", e); } finally { writeCallbackResponse(response, success); } } Map recoverCallbackByPaymentQuery(Map requestParams) { String appId = firstRequestParam(requestParams, "app_id"); if (appId == null || !appId.equals(icbcPayProperties.getAppId())) { throw new IllegalArgumentException("工行回调APPID与当前配置不一致,禁止查询补偿"); } String bizContent = firstRequestParam(requestParams, "biz_content"); JSONObject callbackBiz = JSON.parseObject(bizContent); String outTradeNo = callbackBiz == null ? null : callbackBiz.getString("out_trade_no"); if (outTradeNo == null || outTradeNo.trim().isEmpty() || outTradeNo.length() > 64) { throw new IllegalArgumentException("工行回调缺少有效订单号,无法查询补偿"); } IcbcPaymentResp payment = icbcPayService.query(outTradeNo); JSONObject query = payment == null || payment.getRawResponse() == null ? null : JSON.parseObject(payment.getRawResponse()); if (query == null || query.getIntValue("returnCode") != 0 || !"0".equals(query.getString("pay_status"))) { throw new IllegalArgumentException("工行主动查询未确认支付成功"); } String queriedOutTradeNo = query.getString("out_trade_no"); if (!outTradeNo.equals(queriedOutTradeNo)) { throw new IllegalArgumentException("工行主动查询返回订单号不一致"); } String orderId = query.getString("order_id"); String attach = query.getString("attach"); if (orderId == null || orderId.trim().isEmpty() || attach == null || attach.trim().isEmpty()) { throw new IllegalArgumentException("工行主动查询缺少订单流水或支付类型"); } Map callback = new HashMap<>(); callback.put("out_trade_no", queriedOutTradeNo); callback.put("transaction_id", orderId); callback.put("attach", attach); callback.put("return_code", "0"); callback.put("return_msg", "主动查询确认支付成功"); return callback; } private boolean isSignatureVerificationFailure(Exception exception) { Throwable current = exception; while (current != null) { if (current.getMessage() != null && current.getMessage().contains("工行回调验签失败")) { return true; } current = current.getCause(); } return false; } private String firstRequestParam(Map requestParams, String name) { String[] values = requestParams == null ? null : requestParams.get(name); return values == null || values.length == 0 ? null : values[0]; } void dispatchPaymentCallback(Map callback) { String outTradeNo = callback.get("out_trade_no"); String transactionId = callback.get("transaction_id"); String attach = callback.get("attach"); if (outTradeNo != null && outTradeNo.startsWith(TEST_ORDER_PREFIX)) { log.info("工行测试支付回调成功,订单号:{},流水号:{},支付类型:{}", outTradeNo, transactionId, attach); return; } if (outTradeNo != null && outTradeNo.startsWith("HY")) { orderClient.vipCallBack(outTradeNo, transactionId); return; } if (outTradeNo != null && outTradeNo.startsWith("GW")) { orderClient.callBack(outTradeNo, transactionId); return; } if (outTradeNo == null || !outTradeNo.startsWith("CD")) { throw new IllegalArgumentException("工行回调订单类型不支持:" + outTradeNo); } Integer paymentType = Integer.valueOf(attach); if (Integer.valueOf(1).equals(paymentType)) { chargingOrderClient.chargingOrderWXCallback(outTradeNo, transactionId, attach); return; } if (Integer.valueOf(2).equals(paymentType)) { chargingOrderClient.chargingOrderALICallback(outTradeNo, transactionId, attach); return; } if (Integer.valueOf(3).equals(paymentType)) { chargingOrderClient.chargingOrderALIH5Callback(outTradeNo, transactionId, attach); return; } throw new IllegalArgumentException("工行回调支付类型不支持:" + attach); } private String getString(Map req, String key) { if (req == null || req.get(key) == null) { throw new IllegalArgumentException("缺少参数:" + key); } return String.valueOf(req.get(key)); } private String readRequestBody(HttpServletRequest request) { StringBuilder body = new StringBuilder(); try (BufferedReader reader = request.getReader()) { String line; while ((line = reader.readLine()) != null) { body.append(line); } } catch (Exception e) { log.warn("读取工行支付回调body失败", e); } return body.toString(); } private void mergeJsonBody(Map requestParams, String rawBody) { if (rawBody == null || rawBody.trim().isEmpty()) { return; } try { JSONObject jsonObject = JSON.parseObject(rawBody); if (jsonObject == null) { return; } for (Map.Entry entry : jsonObject.entrySet()) { if (entry.getValue() != null && !requestParams.containsKey(entry.getKey())) { requestParams.put(entry.getKey(), new String[]{String.valueOf(entry.getValue())}); } } } catch (Exception e) { log.warn("工行支付回调body不是JSON格式,body:{}", rawBody); } } private String summarizeHeaders(HttpServletRequest request) { Map headers = new LinkedHashMap<>(); Enumeration names = request.getHeaderNames(); while (names != null && names.hasMoreElements()) { String name = names.nextElement(); if ("user-agent".equalsIgnoreCase(name) || "host".equalsIgnoreCase(name) || "x-forwarded-for".equalsIgnoreCase(name) || "x-real-ip".equalsIgnoreCase(name)) { headers.put(name, request.getHeader(name)); } } return JSON.toJSONString(headers); } private void writeCallbackResponse(HttpServletResponse response, boolean success) throws Exception { response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.println(success ? "success" : "fail"); writer.flush(); writer.close(); } }