| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- 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。
- *
- * <p>本期实现:
- * <ul>
- * <li>{@code POST /pay/newebpay/create} — 发起 MPG 幕前支付(US1)。</li>
- * <li>{@code POST /pay/newebpay/notify} — NotifyURL 支付结果回调(US2,@Anonymous)。</li>
- * <li>{@code GET|POST /pay/newebpay/return} — ReturnURL 完成页引导(US2,@Anonymous)。</li>
- * </ul>
- * 单笔查询 /pay/newebpay/query(US5)后续补充。
- *
- * <p>回调成功后的业务链路(更新 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<PosOrder>().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<String, Object> 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<String, String> 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)。
- *
- * <p>记 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<String, Object> 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<String, Object> 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<PosOrder>().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)。仅引导回前端结果页,<b>不</b>修改订单状态(状态以 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> collectForm(HttpServletRequest request) {
- Map<String, Object> m = new LinkedHashMap<>();
- Enumeration<String> 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<String, Object> parseQuery(String query) {
- Map<String, Object> 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<String, Object> 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<String, Object> d) {
- String pm = getStr(d, "PaymentMethod");
- if (!pm.isEmpty()) {
- return pm.toUpperCase();
- }
- return getStr(d, "PaymentType").toUpperCase();
- }
- // TODO US5: POST /pay/newebpay/query(单笔查询 + 补单)
- }
|