NewebpayPayController.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. package com.ruoyi.app.pay;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  4. import com.ruoyi.app.order.dto.OrderPushBodyDto;
  5. import com.ruoyi.app.utils.PayPush;
  6. import com.ruoyi.app.utils.event.PushEventService;
  7. import com.ruoyi.app.utils.newebpay.NewebPay;
  8. import com.ruoyi.app.utils.newebpay.NewebPayConfig;
  9. import com.ruoyi.app.utils.newebpay.NewebPayEncryptUtil;
  10. import com.ruoyi.common.annotation.Anonymous;
  11. import com.ruoyi.common.annotation.RepeatSubmit;
  12. import com.ruoyi.common.core.controller.BaseController;
  13. import com.ruoyi.common.core.domain.AjaxResult;
  14. import com.ruoyi.common.utils.MessageUtils;
  15. import com.ruoyi.system.domain.InfoUser;
  16. import com.ruoyi.system.domain.IpnLog;
  17. import com.ruoyi.system.domain.PosOrder;
  18. import com.ruoyi.system.domain.PosOrderPayment;
  19. import com.ruoyi.system.domain.PosStoreNewebpay;
  20. import com.ruoyi.system.mapper.RiderPositionMapper;
  21. import com.ruoyi.system.service.IInfoUserService;
  22. import com.ruoyi.system.service.IIpnLogService;
  23. import com.ruoyi.system.service.IPosOrderPaymentService;
  24. import com.ruoyi.system.service.IPosOrderService;
  25. import com.ruoyi.system.service.IPosStoreNewebpayService;
  26. import com.ruoyi.system.utils.Auth;
  27. import com.ruoyi.system.utils.JwtUtil;
  28. import com.ruoyi.system.utils.OrderLogHelper;
  29. import jakarta.servlet.http.HttpServletRequest;
  30. import jakarta.servlet.http.HttpServletResponse;
  31. import org.slf4j.Logger;
  32. import org.slf4j.LoggerFactory;
  33. import org.springframework.beans.factory.annotation.Autowired;
  34. import org.springframework.beans.factory.annotation.Value;
  35. import org.springframework.web.bind.annotation.PostMapping;
  36. import org.springframework.web.bind.annotation.RequestMapping;
  37. import org.springframework.web.bind.annotation.RequestParam;
  38. import org.springframework.web.bind.annotation.RestController;
  39. import java.io.IOException;
  40. import java.net.URLDecoder;
  41. import java.net.URLEncoder;
  42. import java.nio.charset.StandardCharsets;
  43. import java.text.SimpleDateFormat;
  44. import java.util.Date;
  45. import java.util.Enumeration;
  46. import java.util.LinkedHashMap;
  47. import java.util.Map;
  48. /**
  49. * 蓝新金流(NewebPay) 线上支付 Controller。
  50. *
  51. * <p>本期实现:
  52. * <ul>
  53. * <li>{@code POST /pay/newebpay/create} — 发起 MPG 幕前支付(US1)。</li>
  54. * <li>{@code POST /pay/newebpay/notify} — NotifyURL 支付结果回调(US2,@Anonymous)。</li>
  55. * <li>{@code GET|POST /pay/newebpay/return} — ReturnURL 完成页引导(US2,@Anonymous)。</li>
  56. * </ul>
  57. * 单笔查询 /pay/newebpay/query(US5)后续补充。
  58. *
  59. * <p>回调成功后的业务链路(更新 payStatus + 订单日志 + 推送用户/商家/骑手)参照
  60. *
  61. * @author ruoyi
  62. * @date 2026-06-22
  63. */
  64. @RestController
  65. @RequestMapping("/pay/newebpay")
  66. public class NewebpayPayController extends BaseController {
  67. private static final Logger log = LoggerFactory.getLogger(NewebpayPayController.class);
  68. /** payType 取值:蓝新在线支付(发起时写入 pos_order.pay_type;具体方式 CREDIT/LINEPAY/APPLEPAY 由回调写入 pos_order_payment)。 */
  69. public static final String PAY_TYPE_NEWEBPAY = "6";
  70. @Autowired
  71. private IPosOrderService posOrderService;
  72. @Autowired
  73. private IPosStoreNewebpayService posStoreNewebpayService;
  74. @Autowired
  75. private IPosOrderPaymentService posOrderPaymentService;
  76. @Autowired
  77. private NewebPay newebPay;
  78. @Autowired
  79. private IInfoUserService infoUserService;
  80. @Autowired
  81. private RiderPositionMapper riderPositionMapper;
  82. @Autowired
  83. private PushEventService pushEventService;
  84. @Autowired
  85. private OrderLogHelper orderLogHelper;
  86. @Autowired
  87. private IIpnLogService ipnLogService;
  88. /** 复用 PayController 的可接单骑手异步推送(避免重写复杂调度)。 */
  89. @Value("${newebpay.base-url}")
  90. private String baseUrl;
  91. @Value("${newebpay.notify-url}")
  92. private String notifyUrl;
  93. @Value("${newebpay.return-url}")
  94. private String returnUrl;
  95. @Value("${newebpay.front-result-url}")
  96. private String frontResultUrl;
  97. @Value("${newebpay.mpg-version}")
  98. private String mpgVersion;
  99. // ============================ US1:发起 MPG 幕前支付 ============================
  100. /**
  101. * 发起蓝新 MPG 幕前支付。校验订单 → 查门店启用凭证 → 生成商店订单号 →
  102. * 组装并加密 TradeInfo → 落流水 + 更新订单 payType → 返回 form 字段供前端 Form Post 跳转蓝新付款页。
  103. */
  104. @Anonymous
  105. @Auth
  106. @RepeatSubmit(interval = 1000, message = "请求过于频繁")
  107. @PostMapping("/create")
  108. public AjaxResult create(@RequestParam String orderid, HttpServletRequest request) {
  109. String token = request.getHeader("token");
  110. JwtUtil jwtUtil = new JwtUtil();
  111. String userId;
  112. try {
  113. userId = jwtUtil.getusid(token);
  114. } catch (Exception e) {
  115. return error(MessageUtils.message("no.order.id.error"));
  116. }
  117. if (userId == null || userId.isEmpty()) {
  118. return error("请先登录");
  119. }
  120. PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
  121. if (order == null) {
  122. return error(MessageUtils.message("no.order.id.error"));
  123. }
  124. if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
  125. return error("无权操作该订单");
  126. }
  127. if (order.getPayStatus() != null && order.getPayStatus() == 1L) {
  128. return error("订单已支付");
  129. }
  130. if (order.getAmount() == null || order.getAmount() <= 0) {
  131. return error("订单金额异常");
  132. }
  133. Long storeId = order.getMdId();
  134. PosStoreNewebpay cfg = posStoreNewebpayService.getEnabledConfig(storeId);
  135. if (cfg == null) {
  136. return error("该门店暂不支持线上支付");
  137. }
  138. String merchantOrderNo = genMerchantOrderNo(orderid);
  139. // TradeInfo 明文(NDNF §4.2.1)
  140. Map<String, Object> tradeParams = new LinkedHashMap<>();
  141. tradeParams.put("MerchantID", cfg.getMerchantId());
  142. tradeParams.put("RespondType", "JSON");
  143. tradeParams.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
  144. tradeParams.put("Version", mpgVersion);
  145. tradeParams.put("MerchantOrderNo", merchantOrderNo);
  146. tradeParams.put("Amt", order.getAmount());
  147. tradeParams.put("ItemDesc", "order " + orderid);
  148. tradeParams.put("NotifyURL", notifyUrl);
  149. tradeParams.put("ReturnURL", returnUrl);
  150. applyPaymentSwitch(tradeParams, cfg.getEnabledPayments());
  151. NewebPayConfig npc = new NewebPayConfig(cfg.getMerchantId(), cfg.getHashKey(), cfg.getHashIv());
  152. Map<String, String> form = newebPay.createMpgForm(baseUrl, npc, tradeParams, mpgVersion);
  153. posOrderPaymentService.createPayment(orderid, merchantOrderNo, storeId, cfg.getMerchantId(), order.getAmount());
  154. PosOrder upd = new PosOrder();
  155. upd.setId(order.getId());
  156. upd.setPayType(PAY_TYPE_NEWEBPAY);
  157. upd.setPayUrl(form.get("gatewayUrl"));
  158. posOrderService.saveOrUpdate(upd);
  159. return success(MessageUtils.message("no.order.create.success"), form);
  160. }
  161. // ============================ US2:NotifyURL 支付结果回调 ============================
  162. /**
  163. * 蓝新支付结果背景通知(@Anonymous,蓝新服务器 Form Post)。
  164. *
  165. * <p>记 IPN 日志 → 按 MerchantID 查凭证 → 验签 TradeSha → 解密 TradeInfo →
  166. * 幂等(trade_no) + 金额校验 + 订单关联 → 成功则更新 payStatus=1 并推送用户/商家/骑手;
  167. * 失败则记录。任何异常都不抛错响应(蓝新收到非成功会重试),但绝不错误更新订单。
  168. */
  169. @Anonymous
  170. @PostMapping("/notify")
  171. public JSONObject notify(HttpServletRequest request) {
  172. JSONObject resp = new JSONObject();
  173. resp.put("Status", "SUCCESS");
  174. resp.put("Message", "OK");
  175. Map<String, Object> form = collectForm(request);
  176. String ip = new com.ruoyi.app.utils.IpUtils().getIpAddr(request);
  177. String merchantId = (String) form.get("MerchantID");
  178. String tradeInfo = (String) form.get("TradeInfo");
  179. String tradeSha = (String) form.get("TradeSha");
  180. String status = (String) form.get("Status");
  181. // 记录 IPN 日志
  182. try {
  183. IpnLog ipnLog = new IpnLog();
  184. ipnLog.setIp(ip);
  185. ipnLog.setIpnLog(form.toString());
  186. ipnLogService.insertIpnLog(ipnLog);
  187. } catch (Exception e) {
  188. log.warn("记蓝新 IPN 日志失败", e);
  189. }
  190. PosStoreNewebpay cfg = posStoreNewebpayService.getByMerchantId(merchantId);
  191. if (cfg == null || cfg.getHashKey() == null || tradeInfo == null) {
  192. log.warn("蓝新回调无匹配凭证或缺少 TradeInfo: merchantId={}", merchantId);
  193. return resp;
  194. }
  195. // 验签
  196. String expectedSha = NewebPayEncryptUtil.genTradeSha(tradeInfo, cfg.getHashKey(), cfg.getHashIv());
  197. if (tradeSha == null || !tradeSha.equals(expectedSha)) {
  198. log.warn("蓝新回调验签失败: merchantId={}, expected={}, got={}", merchantId, expectedSha, tradeSha);
  199. return resp;
  200. }
  201. // 解密
  202. Map<String, Object> d;
  203. try {
  204. d = parseQuery(NewebPayEncryptUtil.decrypt(tradeInfo, cfg.getHashKey(), cfg.getHashIv()));
  205. } catch (Exception e) {
  206. log.warn("蓝新回调解密失败: merchantId={}", merchantId, e);
  207. return resp;
  208. }
  209. String tradeNo = getStr(d, "TradeNo");
  210. String merchantOrderNo = getStr(d, "MerchantOrderNo");
  211. // 幂等:同 tradeNo 已成功则直接返回
  212. PosOrderPayment exist = posOrderPaymentService.getByTradeNo(tradeNo);
  213. if (exist != null && exist.getPayStatus() != null && exist.getPayStatus() == 1) {
  214. return resp;
  215. }
  216. // 订单关联
  217. PosOrderPayment payment = posOrderPaymentService.getByMerchantOrderNo(merchantOrderNo);
  218. if (payment == null) {
  219. log.warn("蓝新回调无对应发起记录: merchantOrderNo={}", merchantOrderNo);
  220. return resp;
  221. }
  222. String ddId = payment.getDdId();
  223. PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
  224. if (order == null) {
  225. log.warn("蓝新回调订单不存在: ddId={}", ddId);
  226. return resp;
  227. }
  228. // 金额校验
  229. int amt = parseInt(getStr(d, "Amt"), -1);
  230. if (amt < 0 || order.getAmount() == null || order.getAmount() != amt) {
  231. log.error("蓝新回调金额不符: ddId={}, orderAmt={}, callbackAmt={}", ddId, order.getAmount(), amt);
  232. return resp;
  233. }
  234. String payType = mapPaymentType(d);
  235. String auth = getStr(d, "Auth");
  236. Date payTime = parsePayTime(getStr(d, "PayTime"));
  237. if ("SUCCESS".equals(status)) {
  238. int n = posOrderPaymentService.markSuccess(ddId, merchantOrderNo, payment.getStoreId(),
  239. merchantId, amt, tradeNo, payType, auth, payTime, d.toString());
  240. if (n > 0) {
  241. // 首次成功:更新订单支付状态并推送(state 保持 0 待商家接单)
  242. handlePaymentSuccess(order, d);
  243. }
  244. } else {
  245. posOrderPaymentService.markFail(merchantOrderNo, tradeNo, payType, d.toString());
  246. log.warn("蓝新回调交易失败: ddId={}, status={}, message={}", ddId, status, getStr(d, "Message"));
  247. }
  248. return resp;
  249. }
  250. /**
  251. * 支付完成返回页(@Anonymous)。仅引导回前端结果页,<b>不</b>修改订单状态(状态以 notify 为准)。
  252. */
  253. @Anonymous
  254. @RequestMapping(value = "/return", method = {org.springframework.web.bind.annotation.RequestMethod.GET,
  255. org.springframework.web.bind.annotation.RequestMethod.POST})
  256. public void returnCallback(HttpServletRequest request, HttpServletResponse response) throws IOException {
  257. String ddId = "";
  258. try {
  259. Map<String, Object> form = collectForm(request);
  260. String merchantId = (String) form.get("MerchantID");
  261. String tradeInfo = (String) form.get("TradeInfo");
  262. PosStoreNewebpay cfg = posStoreNewebpayService.getByMerchantId(merchantId);
  263. if (cfg != null && cfg.getHashKey() != null && tradeInfo != null) {
  264. Map<String, Object> d = parseQuery(NewebPayEncryptUtil.decrypt(tradeInfo, cfg.getHashKey(), cfg.getHashIv()));
  265. PosOrderPayment p = posOrderPaymentService.getByMerchantOrderNo(getStr(d, "MerchantOrderNo"));
  266. if (p != null) {
  267. ddId = p.getDdId();
  268. }
  269. }
  270. } catch (Exception e) {
  271. log.warn("蓝新 ReturnURL 解析失败", e);
  272. }
  273. String sep = frontResultUrl.contains("?") ? "&" : "?";
  274. response.sendRedirect(frontResultUrl + sep + "ddId=" + URLEncoder.encode(ddId, StandardCharsets.UTF_8));
  275. }
  276. // ============================ 支付成功业务链路(参照 PayController.payipn) ============================
  277. /** 支付成功:更新订单 payStatus=1(state 不变)+ 订单日志 + 推送用户/商家/骑手。 */
  278. private void handlePaymentSuccess(PosOrder order, Map<String, Object> decryptedMap) {
  279. try {
  280. PosOrder upd = new PosOrder();
  281. upd.setId(order.getId());
  282. upd.setState(0L);
  283. upd.setPayStatus(1L);
  284. posOrderService.saveOrUpdate(upd);
  285. orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", "系统收到蓝新支付成功回调");
  286. InfoUser user = order.getUserId() == null ? null : infoUserService.getById(order.getUserId());
  287. InfoUser shanghu = order.getShId() == null ? null : infoUserService.getById(order.getShId());
  288. if (user == null) {
  289. return;
  290. }
  291. PayPush push = new PayPush();
  292. Map<String, Object> orderMap = OrderPushBodyDto.getMap(String.valueOf(order.getDdId()), "1", 0, 1);
  293. String json = OrderPushBodyDto.margeMapGetJsonString(orderMap, decryptedMap);
  294. String title = MessageUtils.message("no.message.push.message");
  295. push.apppush(user.getCid(), title, MessageUtils.message("no.message.push.payment.success"), json);
  296. pushEventService.PublisherEvent(user.getUserId(), title, MessageUtils.message("no.message.push.payment.success"), json);
  297. if (shanghu != null) {
  298. push.shpush(shanghu.getCid(), title, MessageUtils.message("no.message.push.new.order"), json);
  299. pushEventService.PublisherEvent(shanghu.getUserId(), title, MessageUtils.message("no.message.push.new.order"), json);
  300. }
  301. // 可接单骑手推送(外卖),复用 PayController 的异步实现
  302. try {
  303. String body = OrderPushBodyDto.getJson(String.valueOf(order.getDdId()), String.valueOf(order.getState()), 1);
  304. //推送可接单骑手
  305. } catch (Exception e) {
  306. log.warn("蓝新支付成功骑手推送异常: ddId={}", order.getDdId(), e);
  307. }
  308. } catch (Exception e) {
  309. log.error("蓝新支付成功业务处理异常: ddId={}", order.getDdId(), e);
  310. }
  311. }
  312. // ============================ 辅助 ============================
  313. private static String genMerchantOrderNo(String orderid) {
  314. String safe = orderid == null ? "" : orderid.replaceAll("[^A-Za-z0-9_]", "");
  315. return "NB" + safe;
  316. }
  317. private static void applyPaymentSwitch(Map<String, Object> params, String enabledPayments) {
  318. if (enabledPayments == null || enabledPayments.isEmpty()) {
  319. params.put("CREDIT", 1);
  320. return;
  321. }
  322. String eps = enabledPayments.toUpperCase();
  323. params.put("CREDIT", eps.contains("CREDIT") ? 1 : 0);
  324. params.put("LINEPAY", eps.contains("LINEPAY") ? 1 : 0);
  325. params.put("APPLEPAY", eps.contains("APPLEPAY") ? 1 : 0);
  326. }
  327. /** 收集 form 参数到 Map(值非空的单值参数)。 */
  328. private static Map<String, Object> collectForm(HttpServletRequest request) {
  329. Map<String, Object> m = new LinkedHashMap<>();
  330. Enumeration<String> names = request.getParameterNames();
  331. while (names.hasMoreElements()) {
  332. String n = names.nextElement();
  333. String[] vals = request.getParameterValues(n);
  334. if (vals != null && vals.length == 1 && !vals[0].isEmpty()) {
  335. m.put(n, vals[0]);
  336. }
  337. }
  338. return m;
  339. }
  340. /** 解析 a=1&b=2(URL decode)为 Map。 */
  341. private static Map<String, Object> parseQuery(String query) {
  342. Map<String, Object> m = new LinkedHashMap<>();
  343. if (query == null || query.isEmpty()) {
  344. return m;
  345. }
  346. for (String pair : query.split("&")) {
  347. int idx = pair.indexOf('=');
  348. if (idx <= 0) {
  349. continue;
  350. }
  351. String k = urlDecode(pair.substring(0, idx));
  352. String v = idx + 1 <= pair.length() ? urlDecode(pair.substring(idx + 1)) : "";
  353. m.put(k, v);
  354. }
  355. return m;
  356. }
  357. private static String urlDecode(String s) {
  358. try {
  359. return URLDecoder.decode(s, StandardCharsets.UTF_8);
  360. } catch (Exception e) {
  361. return s;
  362. }
  363. }
  364. private static String getStr(Map<String, Object> m, String k) {
  365. Object v = m.get(k);
  366. return v == null ? "" : v.toString();
  367. }
  368. private static int parseInt(String s, int def) {
  369. try {
  370. return Integer.parseInt(s);
  371. } catch (Exception e) {
  372. return def;
  373. }
  374. }
  375. private static Date parsePayTime(String s) {
  376. if (s == null || s.isEmpty()) {
  377. return null;
  378. }
  379. try {
  380. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(s);
  381. } catch (Exception e) {
  382. return null;
  383. }
  384. }
  385. /** 蓝新回调支付方式映射:优先 PaymentMethod(APPLEPAY),否则 PaymentType(CREDIT/LINEPAY...)。 */
  386. private static String mapPaymentType(Map<String, Object> d) {
  387. String pm = getStr(d, "PaymentMethod");
  388. if (!pm.isEmpty()) {
  389. return pm.toUpperCase();
  390. }
  391. return getStr(d, "PaymentType").toUpperCase();
  392. }
  393. // TODO US5: POST /pay/newebpay/query(单笔查询 + 补单)
  394. }