package com.ruoyi.app.pay; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ruoyi.app.order.dto.OrderPushBodyDto; import com.ruoyi.app.utils.PayPush; import com.ruoyi.app.utils.event.PushEventService; import com.ruoyi.app.utils.newebpay.NewebPay; import com.ruoyi.app.utils.newebpay.NewebPayConfig; import com.ruoyi.app.utils.newebpay.NewebPayEncryptUtil; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.RepeatSubmit; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.system.domain.InfoUser; import com.ruoyi.system.domain.IpnLog; import com.ruoyi.system.domain.PosOrder; import com.ruoyi.system.domain.PosOrderPayment; import com.ruoyi.system.domain.PosStoreNewebpay; import com.ruoyi.system.mapper.RiderPositionMapper; import com.ruoyi.system.service.IInfoUserService; import com.ruoyi.system.service.IIpnLogService; import com.ruoyi.system.service.IPosOrderPaymentService; import com.ruoyi.system.service.IPosOrderService; import com.ruoyi.system.service.IPosStoreNewebpayService; import com.ruoyi.system.utils.Auth; import com.ruoyi.system.utils.JwtUtil; import com.ruoyi.system.utils.OrderLogHelper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; 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.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.Map; /** * 蓝新金流(NewebPay) 线上支付 Controller。 * *

本期实现: *

* 单笔查询 /pay/newebpay/query(US5)后续补充。 * *

回调成功后的业务链路(更新 payStatus + 订单日志 + 推送用户/商家/骑手)参照 * * @author ruoyi * @date 2026-06-22 */ @RestController @RequestMapping("/pay/newebpay") public class NewebpayPayController extends BaseController { private static final Logger log = LoggerFactory.getLogger(NewebpayPayController.class); /** payType 取值:蓝新在线支付(发起时写入 pos_order.pay_type;具体方式 CREDIT/LINEPAY/APPLEPAY 由回调写入 pos_order_payment)。 */ public static final String PAY_TYPE_NEWEBPAY = "6"; @Autowired private IPosOrderService posOrderService; @Autowired private IPosStoreNewebpayService posStoreNewebpayService; @Autowired private IPosOrderPaymentService posOrderPaymentService; @Autowired private NewebPay newebPay; @Autowired private IInfoUserService infoUserService; @Autowired private RiderPositionMapper riderPositionMapper; @Autowired private PushEventService pushEventService; @Autowired private OrderLogHelper orderLogHelper; @Autowired private IIpnLogService ipnLogService; /** 复用 PayController 的可接单骑手异步推送(避免重写复杂调度)。 */ @Value("${newebpay.base-url}") private String baseUrl; @Value("${newebpay.notify-url}") private String notifyUrl; @Value("${newebpay.return-url}") private String returnUrl; @Value("${newebpay.front-result-url}") private String frontResultUrl; @Value("${newebpay.mpg-version}") private String mpgVersion; // ============================ US1:发起 MPG 幕前支付 ============================ /** * 发起蓝新 MPG 幕前支付。校验订单 → 查门店启用凭证 → 生成商店订单号 → * 组装并加密 TradeInfo → 落流水 + 更新订单 payType → 返回 form 字段供前端 Form Post 跳转蓝新付款页。 */ @Anonymous @Auth @RepeatSubmit(interval = 1000, message = "请求过于频繁") @PostMapping("/create") public AjaxResult create(@RequestParam String orderid, HttpServletRequest request) { String token = request.getHeader("token"); JwtUtil jwtUtil = new JwtUtil(); String userId; try { userId = jwtUtil.getusid(token); } catch (Exception e) { return error(MessageUtils.message("no.order.id.error")); } if (userId == null || userId.isEmpty()) { return error("请先登录"); } PosOrder order = posOrderService.getOne(new QueryWrapper().eq("dd_id", orderid)); if (order == null) { return error(MessageUtils.message("no.order.id.error")); } if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) { return error("无权操作该订单"); } if (order.getPayStatus() != null && order.getPayStatus() == 1L) { return error("订单已支付"); } if (order.getAmount() == null || order.getAmount() <= 0) { return error("订单金额异常"); } Long storeId = order.getMdId(); PosStoreNewebpay cfg = posStoreNewebpayService.getEnabledConfig(storeId); if (cfg == null) { return error("该门店暂不支持线上支付"); } String merchantOrderNo = genMerchantOrderNo(orderid); // TradeInfo 明文(NDNF §4.2.1) Map tradeParams = new LinkedHashMap<>(); tradeParams.put("MerchantID", cfg.getMerchantId()); tradeParams.put("RespondType", "JSON"); tradeParams.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L)); tradeParams.put("Version", mpgVersion); tradeParams.put("MerchantOrderNo", merchantOrderNo); tradeParams.put("Amt", order.getAmount()); tradeParams.put("ItemDesc", "order " + orderid); tradeParams.put("NotifyURL", notifyUrl); tradeParams.put("ReturnURL", returnUrl); applyPaymentSwitch(tradeParams, cfg.getEnabledPayments()); NewebPayConfig npc = new NewebPayConfig(cfg.getMerchantId(), cfg.getHashKey(), cfg.getHashIv()); Map form = newebPay.createMpgForm(baseUrl, npc, tradeParams, mpgVersion); posOrderPaymentService.createPayment(orderid, merchantOrderNo, storeId, cfg.getMerchantId(), order.getAmount()); PosOrder upd = new PosOrder(); upd.setId(order.getId()); upd.setPayType(PAY_TYPE_NEWEBPAY); upd.setPayUrl(form.get("gatewayUrl")); posOrderService.saveOrUpdate(upd); return success(MessageUtils.message("no.order.create.success"), form); } // ============================ US2:NotifyURL 支付结果回调 ============================ /** * 蓝新支付结果背景通知(@Anonymous,蓝新服务器 Form Post)。 * *

记 IPN 日志 → 按 MerchantID 查凭证 → 验签 TradeSha → 解密 TradeInfo → * 幂等(trade_no) + 金额校验 + 订单关联 → 成功则更新 payStatus=1 并推送用户/商家/骑手; * 失败则记录。任何异常都不抛错响应(蓝新收到非成功会重试),但绝不错误更新订单。 */ @Anonymous @PostMapping("/notify") public JSONObject notify(HttpServletRequest request) { JSONObject resp = new JSONObject(); resp.put("Status", "SUCCESS"); resp.put("Message", "OK"); Map form = collectForm(request); String ip = new com.ruoyi.app.utils.IpUtils().getIpAddr(request); String merchantId = (String) form.get("MerchantID"); String tradeInfo = (String) form.get("TradeInfo"); String tradeSha = (String) form.get("TradeSha"); String status = (String) form.get("Status"); // 记录 IPN 日志 try { IpnLog ipnLog = new IpnLog(); ipnLog.setIp(ip); ipnLog.setIpnLog(form.toString()); ipnLogService.insertIpnLog(ipnLog); } catch (Exception e) { log.warn("记蓝新 IPN 日志失败", e); } PosStoreNewebpay cfg = posStoreNewebpayService.getByMerchantId(merchantId); if (cfg == null || cfg.getHashKey() == null || tradeInfo == null) { log.warn("蓝新回调无匹配凭证或缺少 TradeInfo: merchantId={}", merchantId); return resp; } // 验签 String expectedSha = NewebPayEncryptUtil.genTradeSha(tradeInfo, cfg.getHashKey(), cfg.getHashIv()); if (tradeSha == null || !tradeSha.equals(expectedSha)) { log.warn("蓝新回调验签失败: merchantId={}, expected={}, got={}", merchantId, expectedSha, tradeSha); return resp; } // 解密 Map d; try { d = parseQuery(NewebPayEncryptUtil.decrypt(tradeInfo, cfg.getHashKey(), cfg.getHashIv())); } catch (Exception e) { log.warn("蓝新回调解密失败: merchantId={}", merchantId, e); return resp; } String tradeNo = getStr(d, "TradeNo"); String merchantOrderNo = getStr(d, "MerchantOrderNo"); // 幂等:同 tradeNo 已成功则直接返回 PosOrderPayment exist = posOrderPaymentService.getByTradeNo(tradeNo); if (exist != null && exist.getPayStatus() != null && exist.getPayStatus() == 1) { return resp; } // 订单关联 PosOrderPayment payment = posOrderPaymentService.getByMerchantOrderNo(merchantOrderNo); if (payment == null) { log.warn("蓝新回调无对应发起记录: merchantOrderNo={}", merchantOrderNo); return resp; } String ddId = payment.getDdId(); PosOrder order = posOrderService.getOne(new QueryWrapper().eq("dd_id", ddId)); if (order == null) { log.warn("蓝新回调订单不存在: ddId={}", ddId); return resp; } // 金额校验 int amt = parseInt(getStr(d, "Amt"), -1); if (amt < 0 || order.getAmount() == null || order.getAmount() != amt) { log.error("蓝新回调金额不符: ddId={}, orderAmt={}, callbackAmt={}", ddId, order.getAmount(), amt); return resp; } String payType = mapPaymentType(d); String auth = getStr(d, "Auth"); Date payTime = parsePayTime(getStr(d, "PayTime")); if ("SUCCESS".equals(status)) { int n = posOrderPaymentService.markSuccess(ddId, merchantOrderNo, payment.getStoreId(), merchantId, amt, tradeNo, payType, auth, payTime, d.toString()); if (n > 0) { // 首次成功:更新订单支付状态并推送(state 保持 0 待商家接单) handlePaymentSuccess(order, d); } } else { posOrderPaymentService.markFail(merchantOrderNo, tradeNo, payType, d.toString()); log.warn("蓝新回调交易失败: ddId={}, status={}, message={}", ddId, status, getStr(d, "Message")); } return resp; } /** * 支付完成返回页(@Anonymous)。仅引导回前端结果页,修改订单状态(状态以 notify 为准)。 */ @Anonymous @RequestMapping(value = "/return", method = {org.springframework.web.bind.annotation.RequestMethod.GET, org.springframework.web.bind.annotation.RequestMethod.POST}) public void returnCallback(HttpServletRequest request, HttpServletResponse response) throws IOException { String ddId = ""; try { Map form = collectForm(request); String merchantId = (String) form.get("MerchantID"); String tradeInfo = (String) form.get("TradeInfo"); PosStoreNewebpay cfg = posStoreNewebpayService.getByMerchantId(merchantId); if (cfg != null && cfg.getHashKey() != null && tradeInfo != null) { Map d = parseQuery(NewebPayEncryptUtil.decrypt(tradeInfo, cfg.getHashKey(), cfg.getHashIv())); PosOrderPayment p = posOrderPaymentService.getByMerchantOrderNo(getStr(d, "MerchantOrderNo")); if (p != null) { ddId = p.getDdId(); } } } catch (Exception e) { log.warn("蓝新 ReturnURL 解析失败", e); } String sep = frontResultUrl.contains("?") ? "&" : "?"; response.sendRedirect(frontResultUrl + sep + "ddId=" + URLEncoder.encode(ddId, StandardCharsets.UTF_8)); } // ============================ 支付成功业务链路(参照 PayController.payipn) ============================ /** 支付成功:更新订单 payStatus=1(state 不变)+ 订单日志 + 推送用户/商家/骑手。 */ private void handlePaymentSuccess(PosOrder order, Map decryptedMap) { try { PosOrder upd = new PosOrder(); upd.setId(order.getId()); upd.setState(0L); upd.setPayStatus(1L); posOrderService.saveOrUpdate(upd); orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", "系统收到蓝新支付成功回调"); InfoUser user = order.getUserId() == null ? null : infoUserService.getById(order.getUserId()); InfoUser shanghu = order.getShId() == null ? null : infoUserService.getById(order.getShId()); if (user == null) { return; } PayPush push = new PayPush(); Map orderMap = OrderPushBodyDto.getMap(String.valueOf(order.getDdId()), "1", 0, 1); String json = OrderPushBodyDto.margeMapGetJsonString(orderMap, decryptedMap); String title = MessageUtils.message("no.message.push.message"); push.apppush(user.getCid(), title, MessageUtils.message("no.message.push.payment.success"), json); pushEventService.PublisherEvent(user.getUserId(), title, MessageUtils.message("no.message.push.payment.success"), json); if (shanghu != null) { push.shpush(shanghu.getCid(), title, MessageUtils.message("no.message.push.new.order"), json); pushEventService.PublisherEvent(shanghu.getUserId(), title, MessageUtils.message("no.message.push.new.order"), json); } // 可接单骑手推送(外卖),复用 PayController 的异步实现 try { String body = OrderPushBodyDto.getJson(String.valueOf(order.getDdId()), String.valueOf(order.getState()), 1); //推送可接单骑手 } catch (Exception e) { log.warn("蓝新支付成功骑手推送异常: ddId={}", order.getDdId(), e); } } catch (Exception e) { log.error("蓝新支付成功业务处理异常: ddId={}", order.getDdId(), e); } } // ============================ 辅助 ============================ private static String genMerchantOrderNo(String orderid) { String safe = orderid == null ? "" : orderid.replaceAll("[^A-Za-z0-9_]", ""); return "NB" + safe; } private static void applyPaymentSwitch(Map params, String enabledPayments) { if (enabledPayments == null || enabledPayments.isEmpty()) { params.put("CREDIT", 1); return; } String eps = enabledPayments.toUpperCase(); params.put("CREDIT", eps.contains("CREDIT") ? 1 : 0); params.put("LINEPAY", eps.contains("LINEPAY") ? 1 : 0); params.put("APPLEPAY", eps.contains("APPLEPAY") ? 1 : 0); } /** 收集 form 参数到 Map(值非空的单值参数)。 */ private static Map collectForm(HttpServletRequest request) { Map m = new LinkedHashMap<>(); Enumeration names = request.getParameterNames(); while (names.hasMoreElements()) { String n = names.nextElement(); String[] vals = request.getParameterValues(n); if (vals != null && vals.length == 1 && !vals[0].isEmpty()) { m.put(n, vals[0]); } } return m; } /** 解析 a=1&b=2(URL decode)为 Map。 */ private static Map parseQuery(String query) { Map m = new LinkedHashMap<>(); if (query == null || query.isEmpty()) { return m; } for (String pair : query.split("&")) { int idx = pair.indexOf('='); if (idx <= 0) { continue; } String k = urlDecode(pair.substring(0, idx)); String v = idx + 1 <= pair.length() ? urlDecode(pair.substring(idx + 1)) : ""; m.put(k, v); } return m; } private static String urlDecode(String s) { try { return URLDecoder.decode(s, StandardCharsets.UTF_8); } catch (Exception e) { return s; } } private static String getStr(Map m, String k) { Object v = m.get(k); return v == null ? "" : v.toString(); } private static int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } } private static Date parsePayTime(String s) { if (s == null || s.isEmpty()) { return null; } try { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(s); } catch (Exception e) { return null; } } /** 蓝新回调支付方式映射:优先 PaymentMethod(APPLEPAY),否则 PaymentType(CREDIT/LINEPAY...)。 */ private static String mapPaymentType(Map d) { String pm = getStr(d, "PaymentMethod"); if (!pm.isEmpty()) { return pm.toUpperCase(); } return getStr(d, "PaymentType").toUpperCase(); } // TODO US5: POST /pay/newebpay/query(单笔查询 + 补单) }