|
|
@@ -1,1109 +0,0 @@
|
|
|
-package com.ruoyi.app.pay;
|
|
|
-
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
-import com.alibaba.fastjson2.JSON;
|
|
|
-import com.ruoyi.app.order.dto.OrderPushBodyDto;
|
|
|
-import com.ruoyi.app.order.OrderLifecycleService;
|
|
|
-import com.ruoyi.app.pay.dto.OmgCallbackRequest;
|
|
|
-import com.ruoyi.app.pay.dto.OmgOrderRequest;
|
|
|
-import com.ruoyi.app.pay.dto.OmgRefundOutcome;
|
|
|
-import com.ruoyi.app.utils.PayPush;
|
|
|
-import com.ruoyi.app.utils.event.PushEventService;
|
|
|
-import com.ruoyi.app.utils.omg.OmgCheckMacValue;
|
|
|
-import com.ruoyi.app.utils.omg.OmgPay;
|
|
|
-import com.ruoyi.app.utils.omg.OmgPayConfig;
|
|
|
-import com.ruoyi.app.utils.omg.OmgQueryThrottle;
|
|
|
-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.PosOrderOmgPayment;
|
|
|
-import com.ruoyi.system.domain.PosStoreOmg;
|
|
|
-import com.ruoyi.system.service.IInfoUserService;
|
|
|
-import com.ruoyi.system.service.IIpnLogService;
|
|
|
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
|
|
|
-import com.ruoyi.system.service.IPosOrderOmgRefundService;
|
|
|
-import com.ruoyi.system.service.IPosOrderService;
|
|
|
-import com.ruoyi.system.service.IPosStoreOmgService;
|
|
|
-import com.ruoyi.system.utils.Auth;
|
|
|
-import com.ruoyi.system.utils.JwtUtil;
|
|
|
-import com.ruoyi.system.utils.OrderLogHelper;
|
|
|
-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.dao.DuplicateKeyException;
|
|
|
-import org.springframework.transaction.annotation.Transactional;
|
|
|
-import org.springframework.transaction.support.TransactionSynchronization;
|
|
|
-import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
|
-import org.springframework.web.bind.annotation.*;
|
|
|
-
|
|
|
-import java.io.IOException;
|
|
|
-import java.net.URLEncoder;
|
|
|
-import java.nio.charset.StandardCharsets;
|
|
|
-import java.text.SimpleDateFormat;
|
|
|
-import java.util.Date;
|
|
|
-import java.util.LinkedHashMap;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Locale;
|
|
|
-import java.util.Map;
|
|
|
-import java.util.Set;
|
|
|
-import java.util.TimeZone;
|
|
|
-import java.util.UUID;
|
|
|
-
|
|
|
-/**
|
|
|
- * OMG(歐買尬/FunPoint) AIO 线上支付 Controller(独立于 newebpay,零蓝新依赖)。
|
|
|
- *
|
|
|
- * <p>本期实现:
|
|
|
- * <ul>
|
|
|
- * <li>{@code POST /pay/omg/create} — 发起 AIO 幕前支付(US1,@Auth)。返回含 gatewayUrl 的 form 字段,
|
|
|
- * 前端构建隐藏 form submit 到 OMG 收银台(ChoosePayment=ALL)。</li>
|
|
|
- * <li>{@code POST /pay/omg/notify} — OMG 服务端回调(US2,@Anonymous)。验签 CheckMacValue + 幂等 + 金额 +
|
|
|
- * RtnCode/SimulatePaid 校验 → markSuccess → 更新订单 payStatus=1 + 推送用户/商家 → 回纯串 {@code 1|OK}。</li>
|
|
|
- * <li>{@code GET|POST /pay/omg/return} — 完成页引导(US2,@Anonymous)。仅 302 回前端结果页,不改订单状态。</li>
|
|
|
- * </ul>
|
|
|
- *
|
|
|
- * @author ruoyi
|
|
|
- * @date 2026-07-29
|
|
|
- */
|
|
|
-@RestController
|
|
|
-@RequestMapping("/pay/omg")
|
|
|
-public class OmgPayController extends BaseController {
|
|
|
-
|
|
|
- private static final Logger log = LoggerFactory.getLogger(OmgPayController.class);
|
|
|
-
|
|
|
- /** payType 取值:OMG 在线支付(发起时写入 pos_order.pay_type;具体方式如 Credit_CreditCard、ATM 系列、CVS 系列等由回调写入 pos_order_omg_payment.pay_type)。 */
|
|
|
- public static final String PAY_TYPE_OMG = "2";
|
|
|
-
|
|
|
- @Autowired
|
|
|
- private IPosOrderService posOrderService;
|
|
|
- @Autowired
|
|
|
- private IPosStoreOmgService storeOmgService;
|
|
|
- @Autowired
|
|
|
- private IPosOrderOmgPaymentService paymentService;
|
|
|
- @Autowired
|
|
|
- private OmgPay omgPay;
|
|
|
- @Autowired
|
|
|
- private IIpnLogService ipnLogService;
|
|
|
- @Autowired
|
|
|
- private IInfoUserService infoUserService;
|
|
|
- @Autowired
|
|
|
- private PushEventService pushEventService;
|
|
|
- @Autowired
|
|
|
- private OrderLogHelper orderLogHelper;
|
|
|
- @Autowired
|
|
|
- private IPosOrderOmgRefundService refundService;
|
|
|
- @Autowired
|
|
|
- private OrderLifecycleService orderLifecycleService;
|
|
|
- @Autowired
|
|
|
- private PaymentCreateGuardService paymentCreateGuardService;
|
|
|
- @Autowired
|
|
|
- private OmgQueryThrottle omgQueryThrottle;
|
|
|
-
|
|
|
- @Value("${omg.base-url}")
|
|
|
- private String baseUrl;
|
|
|
- @Value("${omg.return-url}")
|
|
|
- private String returnUrl;
|
|
|
- @Value("${omg.order-result-url}")
|
|
|
- private String orderResultUrl;
|
|
|
- @Value("${omg.payment-info-url}")
|
|
|
- private String paymentInfoUrl;
|
|
|
-
|
|
|
- /** create 复用新鲜期(分钟):窗口内且 trade_no 为空的活跃行复用 MTN,超期则轮换新建(T058 防堆积,stage 实测调整)。 */
|
|
|
- @Value("${omg.create.reuse-fresh-minutes:3}")
|
|
|
- private int reuseFreshMinutes;
|
|
|
-
|
|
|
- // ============================ US1:发起 AIO 幕前支付 ============================
|
|
|
-
|
|
|
- /**
|
|
|
- * 发起 OMG AIO 幕前支付。校验订单归属/未支付/金额 → 查门店启用凭证 → 生成 MerchantTradeNo →
|
|
|
- * 组参 + CheckMacValue → 落流水(pay_status=0) + 更新订单 payType=PAY_TYPE_OMG/payUrl → 返回 form 字段供前端 Form Post。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @Auth
|
|
|
- @RepeatSubmit(interval = 1000, message = "请求过于频繁")
|
|
|
- @PostMapping("/create")
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
- public AjaxResult create(@RequestHeader String token,
|
|
|
- @RequestBody(required = false) OmgOrderRequest request) {
|
|
|
- if (invalidOrderRequest(request)) {
|
|
|
- return error(MessageUtils.message("no.order.id.error"));
|
|
|
- }
|
|
|
- // 按 ddId 串行化发起(T058 防堆积):同一订单并发 create 在分布式锁内排队,锁释放在事务提交后,
|
|
|
- // 保证后一个请求能看到前一个已提交的活跃流水 → 命中复用而非新建(详见 PaymentCreateGuardService)。
|
|
|
- return paymentCreateGuardService == null ? createUnderLock(token, request)
|
|
|
- : paymentCreateGuardService.withLock(request.getOrderid(),
|
|
|
- () -> createUnderLock(token, request));
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 发起支付的实际业务逻辑,在 {@link #create} 的 {@code @Transactional} 与 {@link PaymentCreateGuardService}
|
|
|
- * 分布式锁内执行。锁释放在事务 afterCommit,确保并发请求看不到未提交的活跃流水 INSERT,避免重复活跃行。
|
|
|
- *
|
|
|
- * <p>流程:校验登录/订单归属/未支付/金额/门店 → 查门店 OMG 凭证 → 复用或新建 MerchantTradeNo →
|
|
|
- * 组 AIO 参 + CheckMacValue → 落流水(pay_status=0) + 更新订单 payType/payUrl → 返回 form 字段。
|
|
|
- */
|
|
|
- private AjaxResult createUnderLock(String token, OmgOrderRequest request) {
|
|
|
- if (invalidOrderRequest(request)) {
|
|
|
- return error(MessageUtils.message("no.order.id.error"));
|
|
|
- }
|
|
|
- String orderid = request.getOrderid();
|
|
|
- String userId;
|
|
|
- try {
|
|
|
- userId = new 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 (!PAY_TYPE_OMG.equals(order.getPayType())) {
|
|
|
- return error("订单支付方式不是 OMG");
|
|
|
- }
|
|
|
- if (order.getState() != null && order.getState() == 4L) {
|
|
|
- return error("订单已取消,不可重新支付");
|
|
|
- }
|
|
|
- if (order.getPayStatus() != null && order.getPayStatus() == 1L) {
|
|
|
- return error("订单已支付");
|
|
|
- }
|
|
|
- if (order.getAmount() == null || order.getAmount() <= 0) {
|
|
|
- return error("订单金额异常");
|
|
|
- }
|
|
|
- if (order.getMdId() == null) {
|
|
|
- return error("订单门店缺失");
|
|
|
- }
|
|
|
-
|
|
|
- // 门店 OMG 凭证(已开通且启用)→ 转 OmgPayConfig(Controller 层完成,避免 system→admin 反向依赖)
|
|
|
- PosStoreOmg cred = storeOmgService.getEnabledCredential(order.getMdId());
|
|
|
- if (cred == null) {
|
|
|
- return error("该门店暂不支持线上支付");
|
|
|
- }
|
|
|
- OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
-
|
|
|
- String merchantTradeNo = resolveMerchantTradeNo(order, cred);
|
|
|
-
|
|
|
- // OMG AIO 参数(contracts/api.md §A1/B1);InvoiceMark=N 固定(发票走 ezPay);EncryptType=1 固定(SHA256)
|
|
|
- SimpleDateFormat fmt = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
|
|
|
- fmt.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
|
|
|
- Map<String, String> params = new LinkedHashMap<>();
|
|
|
- params.put("MerchantID", cred.getMerchantId());
|
|
|
- params.put("MerchantTradeNo", merchantTradeNo);
|
|
|
- params.put("MerchantTradeDate", fmt.format(new Date()));
|
|
|
- params.put("PaymentType", "aio");
|
|
|
- params.put("TotalAmount", String.valueOf(order.getAmount()));
|
|
|
- params.put("TradeDesc", "food order " + orderid);
|
|
|
- params.put("ItemName", "order " + orderid);
|
|
|
- params.put("ReturnURL", returnUrl);
|
|
|
- params.put("ChoosePayment", "ALL");
|
|
|
- params.put("EncryptType", "1");
|
|
|
- params.put("InvoiceMark", "N");
|
|
|
- params.put("NeedExtraPaidInfo", "Y");
|
|
|
- if (paymentInfoUrl != null && !paymentInfoUrl.isEmpty()) {
|
|
|
- params.put("PaymentInfoURL", paymentInfoUrl);
|
|
|
- }
|
|
|
- if (orderResultUrl != null && !orderResultUrl.isEmpty()) {
|
|
|
- params.put("OrderResultURL", orderResultUrl);
|
|
|
- }
|
|
|
-
|
|
|
- // 组参 + CheckMacValue,返回含 gatewayUrl 的 form 字段
|
|
|
- Map<String, String> form = omgPay.createAioForm(baseUrl, cfg, params);
|
|
|
-
|
|
|
- // 更新订单 payType=OMG(PAY_TYPE_OMG) / payUrl=gatewayUrl(仅更这两个字段)
|
|
|
- PosOrder upd = new PosOrder();
|
|
|
- upd.setId(order.getId());
|
|
|
- upd.setPayType(PAY_TYPE_OMG);
|
|
|
- upd.setPayUrl(form.get("gatewayUrl"));
|
|
|
- posOrderService.saveOrUpdate(upd);
|
|
|
-
|
|
|
- log.info("[OMG] create orderid={}, merchantTradeNo={}, amount={}, gatewayUrl={}",
|
|
|
- orderid, merchantTradeNo, order.getAmount(), form.get("gatewayUrl"));
|
|
|
- return success(form);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 解析本次发起用的 MerchantTradeNo(T058 防堆积核心)。
|
|
|
- *
|
|
|
- * <p>优先复用新鲜期内活跃未付且未取号(trade_no IS NULL)的旧 MTN(连点/重发起命中 → 不新建流水);
|
|
|
- * 未命中则先把旧行轮换为历史(is_active 1→0,pay_status 不动,保留接迟到 notify 与审计),再新建活跃行。
|
|
|
- * 必须在 create 的 @Transactional 与 PaymentCreateGuardService 分布式锁内调用。
|
|
|
- */
|
|
|
- private String resolveMerchantTradeNo(PosOrder order, PosStoreOmg cred) {
|
|
|
- String ddId = String.valueOf(order.getDdId());
|
|
|
- PosOrderOmgPayment reusable = paymentService.getActiveForReuse(ddId, reuseFreshMinutes);
|
|
|
- if (reusable != null) {
|
|
|
- log.info("[OMG] create reuse orderid={}, merchantTradeNo={} (fresh within {}min, trade_no null)",
|
|
|
- order.getDdId(), reusable.getMerchantTradeNo(), reuseFreshMinutes);
|
|
|
- return reusable.getMerchantTradeNo();
|
|
|
- }
|
|
|
- paymentService.markActiveHistorical(ddId);
|
|
|
- return createPaymentAttempt(order, cred);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * MerchantTradeNo 生成:OMG 要求全平台永久唯一、≤20 字元、英数大小写混合,且不可复用。
|
|
|
- * 使用 "OMG" + UUID 随机片段,并依靠数据库唯一索引与冲突重试兜底;ddId 由支付流水反查。
|
|
|
- */
|
|
|
- private String createPaymentAttempt(PosOrder order, PosStoreOmg cred) {
|
|
|
- for (int attempt = 0; attempt < 3; attempt++) {
|
|
|
- String merchantTradeNo = genMerchantTradeNo();
|
|
|
- try {
|
|
|
- paymentService.createPayment(String.valueOf(order.getDdId()), merchantTradeNo, order.getMdId(),
|
|
|
- cred.getMerchantId(), order.getAmount(), "ALL");
|
|
|
- return merchantTradeNo;
|
|
|
- } catch (DuplicateKeyException e) {
|
|
|
- if (attempt == 2) {
|
|
|
- throw e;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- throw new IllegalStateException("Unable to allocate OMG MerchantTradeNo");
|
|
|
- }
|
|
|
-
|
|
|
- private String genMerchantTradeNo() {
|
|
|
- String random = UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
|
|
|
- return "OMG" + random.substring(0, 17);
|
|
|
- }
|
|
|
-
|
|
|
- // ============================ US2:ReturnURL 支付结果回调 ============================
|
|
|
-
|
|
|
- /**
|
|
|
- * OMG 支付结果回调(@Anonymous,OMG 服务端 Form Post)。OMG 明文参数 + 单 CheckMacValue(无 AES 解密)。
|
|
|
- *
|
|
|
- * <p>记 IPN → 按 MerchantID 查凭证 → 验签 → trade_no 幂等 → 金额校验 → RtnCode==1 且 SimulatePaid!=1
|
|
|
- * → markSuccess + 推送 → 回纯串 {@code 1|OK}(注意:非 JSON,与蓝新不同)。任何异常/校验失败都仍回 1|OK
|
|
|
- * (OMG 收到非成功会重试),但绝不错误更新订单。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @PostMapping(value = "/notify", produces = "text/plain;charset=UTF-8")
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
- public String notify(@ModelAttribute OmgCallbackRequest callback,
|
|
|
- @RequestHeader(value = "X-Forwarded-For", required = false) String forwardedFor) {
|
|
|
- Map<String, String> form;
|
|
|
- try {
|
|
|
- form = callback.toParameterMap();
|
|
|
- } catch (IllegalArgumentException e) {
|
|
|
- log.warn("OMG callback rejected: {}", e.getMessage());
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- // 记录 IPN 日志
|
|
|
- try {
|
|
|
- IpnLog ipnLog = new IpnLog();
|
|
|
- ipnLog.setIp(callbackIp(forwardedFor));
|
|
|
- ipnLog.setIpnLog(auditJson(form));
|
|
|
- ipnLog.setType("omg");
|
|
|
- ipnLogService.insertIpnLog(ipnLog);
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("记 OMG IPN 日志失败", e);
|
|
|
- }
|
|
|
-
|
|
|
- String merchantId = form.get("MerchantID");
|
|
|
- if (merchantId == null || merchantId.isEmpty()) {
|
|
|
- log.warn("OMG callback missing MerchantID");
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(merchantId);
|
|
|
- if (cred == null) {
|
|
|
- log.warn("OMG 回调无匹配凭证: merchantId={}", merchantId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- // 验签(OMG 无解密,直接对全部回调参 + CheckMacValue 重算比对)
|
|
|
- if (!OmgCheckMacValue.verify(form, cred.getHashKey(), cred.getHashIv())) {
|
|
|
- log.warn("OMG 回调验签失败: merchantId={}", merchantId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- String tradeNo = form.get("TradeNo");
|
|
|
- String merchantTradeNo = form.get("MerchantTradeNo");
|
|
|
- if (tradeNo == null || tradeNo.isEmpty() || merchantTradeNo == null || merchantTradeNo.isEmpty()) {
|
|
|
- log.warn("OMG callback missing trade identifiers: merchantId={}", merchantId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- int rtnCode = toInt(form.get("RtnCode"), -1);
|
|
|
- int tradeAmt = toInt(form.get("TradeAmt"), -1);
|
|
|
- String simulatePaid = form.get("SimulatePaid");
|
|
|
- String paymentType = form.get("PaymentType");
|
|
|
- String paymentDate = form.get("PaymentDate");
|
|
|
-
|
|
|
- // 订单关联:由 MerchantTradeNo 反查流水与订单
|
|
|
- PosOrderOmgPayment payment = paymentService.getByMerchantTradeNo(merchantTradeNo);
|
|
|
- if (payment == null) {
|
|
|
- log.warn("OMG 回调无对应发起记录: merchantTradeNo={}", merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- if (!merchantId.equals(payment.getMerchantId()) || payment.getStoreId() == null
|
|
|
- || cred.getStoreId() == null || !payment.getStoreId().equals(cred.getStoreId())) {
|
|
|
- log.warn("OMG callback credential/ledger mismatch: merchantId={}, merchantTradeNo={}", merchantId, merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- PosOrderOmgPayment exist = paymentService.getByTradeNo(tradeNo);
|
|
|
- if (exist != null && !payment.getId().equals(exist.getId())) {
|
|
|
- log.warn("OMG TradeNo already belongs to another payment: tradeNo={}", tradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- String ddId = payment.getDdId();
|
|
|
- PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
|
|
|
- if (order == null) {
|
|
|
- log.warn("OMG 回调订单不存在: ddId={}", ddId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- if (order.getMdId() == null || !order.getMdId().equals(payment.getStoreId())
|
|
|
- || !order.getMdId().equals(cred.getStoreId())) {
|
|
|
- log.warn("OMG callback order/store mismatch: ddId={}", ddId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- // 金额校验(以平台实际应收金额为准)
|
|
|
- if (payment.getAmount() == null || order.getAmount() == null
|
|
|
- || tradeAmt != payment.getAmount() || order.getAmount().intValue() != payment.getAmount()) {
|
|
|
- log.error("OMG callback amount mismatch: ddId={}, paymentAmt={}, orderAmt={}, callbackAmt={}",
|
|
|
- ddId, payment.getAmount(), order.getAmount(), tradeAmt);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- boolean terminalOrder = (order.getState() != null && order.getState() == 4L)
|
|
|
- || (order.getPayStatus() != null && order.getPayStatus() == 2L);
|
|
|
-
|
|
|
- // 流水已成功但订单未核销时,重复回调用于补偿上一次订单更新失败。
|
|
|
- if (Integer.valueOf(1).equals(payment.getPayStatus())) {
|
|
|
- if (!tradeNo.equals(payment.getTradeNo())) {
|
|
|
- log.warn("OMG paid callback TradeNo mismatch: merchantTradeNo={}", merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- if (payment.getPayType() != null && paymentType != null && !paymentType.equals(payment.getPayType())) {
|
|
|
- log.warn("OMG paid callback PaymentType mismatch: merchantTradeNo={}", merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- if (rtnCode == 1 && !"1".equals(simulatePaid)
|
|
|
- && !Long.valueOf(1L).equals(order.getPayStatus())) {
|
|
|
- recordPaidOrderWithoutFulfillment(order, terminalOrder
|
|
|
- ? "OMG支付成功回调晚于订单取消/退款,需立即退款或人工核对"
|
|
|
- : "系统补偿OMG已支付流水与订单支付状态不一致");
|
|
|
- }
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- if (rtnCode == 1 && !"1".equals(simulatePaid)) {
|
|
|
- Date payTime = parsePayTime(paymentDate);
|
|
|
- Date tradeDate = parsePayTime(form.get("TradeDate"));
|
|
|
- if (payTime == null || tradeDate == null) {
|
|
|
- log.warn("OMG callback contains invalid date: merchantTradeNo={}", merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- String authCode = form.get("auth_code");
|
|
|
- if (authCode == null) {
|
|
|
- authCode = form.get("AuthCode");
|
|
|
- }
|
|
|
- // 核销成功(notify/补单共用:幂等 markSuccess + 订单状态流转 + 推送)
|
|
|
- applyPaidResult(payment, order, tradeNo, paymentType, rtnCode, form.get("RtnMsg"),
|
|
|
- authCode, payTime, tradeDate, auditJson(form));
|
|
|
- } else if (rtnCode != 1) {
|
|
|
- paymentService.markFail(payment.getId(), rtnCode, form.get("RtnMsg"), auditJson(form));
|
|
|
- log.warn("OMG 回调交易失败: ddId={}, rtnCode={}, rtnMsg={}", ddId, rtnCode, form.get("RtnMsg"));
|
|
|
- } else {
|
|
|
- // SimulatePaid=1 模拟支付:不发货,仅记录
|
|
|
- log.warn("OMG 回调为模拟支付(SimulatePaid=1),不发货: ddId={}", ddId);
|
|
|
- }
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 支付完成返回页(@Anonymous)。仅 302 引导回前端结果页(带 ddId),<b>不</b>改订单状态(以 notify 为准)。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @RequestMapping(value = "/return", method = {RequestMethod.GET, RequestMethod.POST})
|
|
|
- public void returnCallback(@ModelAttribute OmgCallbackRequest callback,
|
|
|
- HttpServletResponse response) throws IOException {
|
|
|
- String ddId = "";
|
|
|
- try {
|
|
|
- Map<String, String> form = callback.toParameterMap();
|
|
|
- String mtn = form.get("MerchantTradeNo");
|
|
|
- // 由 MerchantTradeNo 反查流水拿 ddId(MTN 不再编码 ddId)
|
|
|
- if (mtn != null && !mtn.isEmpty()) {
|
|
|
- PosOrderOmgPayment p = paymentService.getByMerchantTradeNo(mtn);
|
|
|
- if (p != null && p.getDdId() != null) {
|
|
|
- ddId = p.getDdId();
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("OMG ReturnURL 解析失败", e);
|
|
|
- }
|
|
|
- if (orderResultUrl == null || orderResultUrl.isEmpty()) {
|
|
|
- log.warn("OMG ReturnURL 未配置 omg.order-result-url,无法重定向");
|
|
|
- response.setStatus(204);
|
|
|
- return;
|
|
|
- }
|
|
|
- String sep = orderResultUrl.contains("?") ? "&" : "?";
|
|
|
- response.sendRedirect(orderResultUrl + sep + "ddId=" + URLEncoder.encode(ddId == null ? "" : ddId, StandardCharsets.UTF_8));
|
|
|
- }
|
|
|
-
|
|
|
- // ============================ US3:ATM/超商 取号回调 + 取号查询 ============================
|
|
|
-
|
|
|
- /**
|
|
|
- * OMG ATM/超商取号回调(@Anonymous,PaymentInfoURL)。验签后把虚帐/缴费码原始报文
|
|
|
- * (BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate 等)落到流水的 callbackRaw,不改 pay_status;
|
|
|
- * 实际付款后 OMG 再回调 /pay/omg/notify(RtnCode=1)走 US2 核销。回纯串 {@code 1|OK}。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @PostMapping(value = "/paymentInfo", produces = "text/plain;charset=UTF-8")
|
|
|
- public String paymentInfoCallback(@ModelAttribute OmgCallbackRequest callback,
|
|
|
- @RequestHeader(value = "X-Forwarded-For", required = false) String forwardedFor) {
|
|
|
- Map<String, String> form;
|
|
|
- try {
|
|
|
- form = callback.toParameterMap();
|
|
|
- } catch (IllegalArgumentException e) {
|
|
|
- log.warn("OMG paymentInfo rejected: {}", e.getMessage());
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- try {
|
|
|
- IpnLog ipnLog = new IpnLog();
|
|
|
- ipnLog.setIp(callbackIp(forwardedFor));
|
|
|
- ipnLog.setIpnLog(auditJson(form));
|
|
|
- ipnLog.setType("omg");
|
|
|
- ipnLogService.insertIpnLog(ipnLog);
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("记 OMG paymentInfo IPN 日志失败", e);
|
|
|
- }
|
|
|
-
|
|
|
- String merchantId = form.get("MerchantID");
|
|
|
- if (merchantId == null || merchantId.isEmpty()) {
|
|
|
- log.warn("OMG paymentInfo missing MerchantID");
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(merchantId);
|
|
|
- if (cred == null) {
|
|
|
- log.warn("OMG paymentInfo 无匹配凭证: merchantId={}", merchantId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- if (!OmgCheckMacValue.verify(form, cred.getHashKey(), cred.getHashIv())) {
|
|
|
- log.warn("OMG paymentInfo 验签失败: merchantId={}", merchantId);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- String merchantTradeNo = form.get("MerchantTradeNo");
|
|
|
- PosOrderOmgPayment payment = paymentService.getByMerchantTradeNo(merchantTradeNo);
|
|
|
- if (payment == null) {
|
|
|
- log.warn("OMG paymentInfo 无对应发起记录: merchantTradeNo={}", merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- if (!merchantId.equals(payment.getMerchantId()) || payment.getStoreId() == null
|
|
|
- || cred.getStoreId() == null || !payment.getStoreId().equals(cred.getStoreId())) {
|
|
|
- log.warn("OMG paymentInfo credential/ledger mismatch: merchantTradeNo={}", merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- // 取号成功 RtnCode:ATM=2,CVS/BarcodeATM=10100073(取号本身非付款;付款成功另走 /notify 的 RtnCode=1)
|
|
|
- int pickupRtnCode = toInt(form.get("RtnCode"), -1);
|
|
|
- if (pickupRtnCode != 2 && pickupRtnCode != 10100073) {
|
|
|
- log.warn("OMG paymentInfo 取号非成功 rtnCode={}, merchantTradeNo={}", pickupRtnCode, merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
- // 落取号信息(BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate)为 JSON,不改 pay_status
|
|
|
- paymentService.markPaymentInfo(payment.getId(), form.get("TradeNo"), auditJson(form));
|
|
|
- log.info("[OMG] paymentInfo 已记录取号信息: ddId={}, merchantTradeNo={}", payment.getDdId(), merchantTradeNo);
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 取号信息查询(前端 ATM/超商结果页展示虚帐/缴费码 + 期限用,@Auth)。
|
|
|
- * 返回 payType/amount/payStatus + 解析后的取号字段 info。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @Auth
|
|
|
- @GetMapping("/paymentInfo/{orderid}")
|
|
|
- public AjaxResult getPaymentInfo(@RequestHeader String token, @PathVariable String orderid) {
|
|
|
- String userId;
|
|
|
- try {
|
|
|
- userId = new 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 || order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
|
|
|
- return error("无权操作该订单");
|
|
|
- }
|
|
|
- // 优先取已付/退款行(展示回执);无则取最新行(未付款场景展示 ATM/超商虚帐)
|
|
|
- PosOrderOmgPayment p = paymentService.getLatestRefundableByDdId(orderid);
|
|
|
- if (p == null) {
|
|
|
- p = paymentService.getLatestByDdId(orderid);
|
|
|
- }
|
|
|
- if (p == null) {
|
|
|
- return error("无支付记录");
|
|
|
- }
|
|
|
- Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- result.put("payType", p.getPayType());
|
|
|
- result.put("amount", p.getAmount());
|
|
|
- result.put("payStatus", p.getPayStatus());
|
|
|
- result.put("info", parsePaymentInfo(p.getCallbackRaw()));
|
|
|
- return success(result);
|
|
|
- }
|
|
|
-
|
|
|
- // ============================ US4:订单取消与退款 ============================
|
|
|
-
|
|
|
- /**
|
|
|
- * OMG 退款(@Auth,供取消链路/管理员调用)。信用卡(含 Apple Pay) 调 DoAction(Action=R 退刷);
|
|
|
- * ATM/超商/BarcodeATM 无退款 API → 记录待人工在 OMG 后台处理。仅正式端点可用(stage DoAction 不可用)。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @Auth
|
|
|
- @PostMapping("/refund")
|
|
|
- public AjaxResult refund(@RequestHeader String token,
|
|
|
- @RequestBody(required = false) OmgOrderRequest request) {
|
|
|
- if (invalidOrderRequest(request)) {
|
|
|
- return error(MessageUtils.message("no.order.id.error"));
|
|
|
- }
|
|
|
- String orderid = request.getOrderid();
|
|
|
- String userId;
|
|
|
- try {
|
|
|
- userId = new 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("订单不存在");
|
|
|
- }
|
|
|
- if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
|
|
|
- return error("无权操作该订单");
|
|
|
- }
|
|
|
- return refundOrder(order);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * OMG 退款核心(public,供取消链路 {@code cancelOrder} 直接调用,免重复实现)。
|
|
|
- * 信用卡(含 Apple Pay) 调 DoAction(Action=R 退刷);ATM/超商/BarcodeATM 记人工。仅正式端点可用。
|
|
|
- * 返回 success=已退款;error=需人工或失败(refund 已落记录可重试)。
|
|
|
- */
|
|
|
- public AjaxResult refundOrder(PosOrder order) {
|
|
|
- OmgRefundOutcome outcome = refundOrderOutcome(order);
|
|
|
- if (outcome.getStatus() == OmgRefundOutcome.Status.REFUNDED
|
|
|
- || outcome.getStatus() == OmgRefundOutcome.Status.IDEMPOTENT) {
|
|
|
- try {
|
|
|
- orderLifecycleService.finalizeSystemOmgRefund(order.getId());
|
|
|
- return success(outcome.getMessage());
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("OMG refund succeeded but order state synchronization failed: orderId={}, errorType={}",
|
|
|
- order == null ? null : order.getId(), e.getClass().getSimpleName());
|
|
|
- return error("OMG 退款成功,本地订单状态待同步");
|
|
|
- }
|
|
|
- }
|
|
|
- return error(outcome.getMessage());
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 返回明确退款结果,供管理端根据资金事实同步订单状态。
|
|
|
- */
|
|
|
- public OmgRefundOutcome refundOrderOutcome(PosOrder order) {
|
|
|
- if (order == null) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "订单不存在");
|
|
|
- }
|
|
|
- if (!PAY_TYPE_OMG.equals(order.getPayType())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "该订单非 OMG 支付");
|
|
|
- }
|
|
|
- if (order.getPayStatus() == null || order.getPayStatus() != 1L) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "订单未支付,无需退款");
|
|
|
- }
|
|
|
- String ddId = String.valueOf(order.getDdId());
|
|
|
- PosOrderOmgPayment payment = paymentService.getLatestRefundableByDdId(ddId);
|
|
|
- if (payment == null) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "无 OMG 支付流水");
|
|
|
- }
|
|
|
- if (Integer.valueOf(3).equals(payment.getPayStatus())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.IDEMPOTENT, "OMG 已完成退款");
|
|
|
- }
|
|
|
- if (Integer.valueOf(4).equals(payment.getPayStatus())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN,
|
|
|
- "OMG 退款结果待确认,请勿重复发起");
|
|
|
- }
|
|
|
- if (!Integer.valueOf(1).equals(payment.getPayStatus())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 支付流水不是已支付状态");
|
|
|
- }
|
|
|
- int amount = payment.getAmount() == null ? 0 : payment.getAmount();
|
|
|
- String payType = payment.getPayType();
|
|
|
- if (amount <= 0 || payment.getTradeNo() == null || payment.getTradeNo().isEmpty()) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 支付流水不完整");
|
|
|
- }
|
|
|
- if (order.getAmount() == null || order.getAmount().intValue() != amount) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 支付流水金额与订单不一致");
|
|
|
- }
|
|
|
-
|
|
|
- // ATM/超商/BarcodeATM 无退款 API → 记录待人工
|
|
|
- if (!"Credit_CreditCard".equals(payType)) {
|
|
|
- List<com.ruoyi.system.domain.PosOrderOmgRefund> records = refundService.listByPayment(payment.getId());
|
|
|
- boolean pending = records != null && records.stream()
|
|
|
- .anyMatch(row -> row.getAction() == null && row.getRtnCode() == null);
|
|
|
- boolean completed = records != null && records.stream()
|
|
|
- .anyMatch(row -> row.getAction() == null && Integer.valueOf(1).equals(row.getRtnCode()));
|
|
|
- if (completed) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.IDEMPOTENT, "OMG 人工退款已确认完成");
|
|
|
- }
|
|
|
- if (!pending) {
|
|
|
- refundService.record(payment.getId(), ddId, payment.getTradeNo(), null, amount, null,
|
|
|
- "延期支付方式无退款API,待人工在 OMG 后台处理", "");
|
|
|
- }
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.MANUAL_PENDING,
|
|
|
- "该支付方式需在 OMG 后台人工退款,订单暂保持已支付");
|
|
|
- }
|
|
|
-
|
|
|
- if (paymentService.markRefunding(payment.getId()) == 0) {
|
|
|
- PosOrderOmgPayment latest = paymentService.getLatestRefundableByDdId(ddId);
|
|
|
- if (latest != null && Integer.valueOf(3).equals(latest.getPayStatus())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.IDEMPOTENT, "OMG 已完成退款");
|
|
|
- }
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "退款已处理或正在处理中");
|
|
|
- }
|
|
|
-
|
|
|
- PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(payment.getMerchantId());
|
|
|
- if (cred == null || cred.getStoreId() == null || !cred.getStoreId().equals(payment.getStoreId())) {
|
|
|
- paymentService.restorePaidFromRefunding(payment.getId());
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "门店 OMG 凭证不可用");
|
|
|
- }
|
|
|
- OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
-
|
|
|
- // 信用卡(含 Apple Pay)→ DoAction(Action=R 退刷);MVP 统一 R,失败再按状态分支(D7)
|
|
|
- refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, null,
|
|
|
- "退款请求处理中", "");
|
|
|
- Map<String, String> resp;
|
|
|
- try {
|
|
|
- resp = omgPay.doAction(baseUrl, cfg, payment.getMerchantTradeNo(), payment.getTradeNo(), "R", amount);
|
|
|
- } catch (Exception e) {
|
|
|
- refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, null,
|
|
|
- "退款结果未知,需对账", "");
|
|
|
- log.error("OMG refund outcome unknown: ddId={}, paymentId={}, errorType={}",
|
|
|
- ddId, payment.getId(), e.getClass().getSimpleName());
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN,
|
|
|
- "OMG 退款结果待确认,请勿重复发起");
|
|
|
- }
|
|
|
- int rtnCode = toInt(resp == null ? null : resp.get("RtnCode"), -1);
|
|
|
- String rtnMsg = resp == null ? "" : resp.get("RtnMsg");
|
|
|
- refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, rtnCode, rtnMsg,
|
|
|
- resp == null ? "" : JSON.toJSONString(resp));
|
|
|
-
|
|
|
- if (rtnCode == 1) {
|
|
|
- if (paymentService.markRefunded(payment.getId()) == 0) {
|
|
|
- log.error("OMG refund succeeded but ledger transition failed: paymentId={}", payment.getId());
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "OMG 退款成功,账务状态待核对");
|
|
|
- }
|
|
|
- orderLogHelper.logSync(ddId, 0, null, "系统", "OMG 信用卡退款成功");
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.REFUNDED, "退款成功");
|
|
|
- }
|
|
|
- paymentService.restorePaidFromRefunding(payment.getId());
|
|
|
- log.warn("OMG refund failed: ddId={}, paymentId={}, rtnCode={}", ddId, payment.getId(), rtnCode);
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 退款失败,请核对后重试");
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 管理员已在 OMG 后台核实完成延期支付退款后,收口支付流水。
|
|
|
- */
|
|
|
- public OmgRefundOutcome confirmManualRefundOutcome(PosOrder order) {
|
|
|
- if (order == null || !PAY_TYPE_OMG.equals(order.getPayType())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "该订单非 OMG 支付");
|
|
|
- }
|
|
|
- String ddId = String.valueOf(order.getDdId());
|
|
|
- PosOrderOmgPayment payment = paymentService.getLatestRefundableByDdId(ddId);
|
|
|
- if (payment == null || "Credit_CreditCard".equals(payment.getPayType())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "无可确认的 OMG 人工退款待办");
|
|
|
- }
|
|
|
- List<com.ruoyi.system.domain.PosOrderOmgRefund> records = refundService.listByPayment(payment.getId());
|
|
|
- boolean pending = records != null && records.stream()
|
|
|
- .anyMatch(row -> row.getAction() == null && row.getRtnCode() == null);
|
|
|
- boolean completed = records != null && records.stream()
|
|
|
- .anyMatch(row -> row.getAction() == null && Integer.valueOf(1).equals(row.getRtnCode()));
|
|
|
- if (!pending && !completed) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "无可确认的 OMG 人工退款待办");
|
|
|
- }
|
|
|
- if (Integer.valueOf(4).equals(payment.getPayStatus())) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "OMG 退款状态正在处理中");
|
|
|
- }
|
|
|
- if (!Integer.valueOf(3).equals(payment.getPayStatus())) {
|
|
|
- if (!Integer.valueOf(1).equals(payment.getPayStatus())
|
|
|
- || paymentService.markRefunding(payment.getId()) == 0
|
|
|
- || paymentService.markRefunded(payment.getId()) == 0) {
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "OMG 人工退款状态发生变化,请刷新");
|
|
|
- }
|
|
|
- }
|
|
|
- if (!completed) {
|
|
|
- refundService.record(payment.getId(), ddId, payment.getTradeNo(), null, payment.getAmount(), 1,
|
|
|
- "管理员确认已在 OMG 后台完成人工退款", "");
|
|
|
- }
|
|
|
- return OmgRefundOutcome.of(OmgRefundOutcome.Status.REFUNDED, "已确认 OMG 人工退款完成");
|
|
|
- }
|
|
|
-
|
|
|
- // ============================ US6:漏单补单(回调可靠性 / callback-reconcile.md) ============================
|
|
|
-
|
|
|
- /**
|
|
|
- * 方案A 被动补单(@Auth)。前端结果页轮询仍 {@code payStatus=0} 时调用:后端查 OMG 真实状态并补单。
|
|
|
- * 返回 {@code {payStatus:0未付/1已付/2失败, reconciled:true=本次触发补单}}。严格幂等(见 reconcileByQuery)。
|
|
|
- */
|
|
|
- @Anonymous
|
|
|
- @Auth
|
|
|
- @RepeatSubmit(interval = 2000, message = "查询过于频繁")
|
|
|
- @PostMapping("/query")
|
|
|
- public AjaxResult query(@RequestHeader String token,
|
|
|
- @RequestBody(required = false) OmgOrderRequest request) {
|
|
|
- if (invalidOrderRequest(request)) {
|
|
|
- return error(MessageUtils.message("no.order.id.error"));
|
|
|
- }
|
|
|
- String orderid = request.getOrderid();
|
|
|
- String userId;
|
|
|
- try {
|
|
|
- userId = new 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 (!PAY_TYPE_OMG.equals(order.getPayType())) {
|
|
|
- return error("该订单非 OMG 支付");
|
|
|
- }
|
|
|
- if (order.getState() != null && order.getState() == 4L) {
|
|
|
- return error("订单已取消");
|
|
|
- }
|
|
|
- Map<String, Object> result = new LinkedHashMap<>();
|
|
|
- Long payStatus = order.getPayStatus();
|
|
|
- // 订单已核销/已退款 → 直接返回,不重复查 OMG
|
|
|
- if (payStatus != null && (payStatus == 1L || payStatus == 2L)) {
|
|
|
- result.put("payStatus", payStatus == 1L ? 1 : 2);
|
|
|
- result.put("reconciled", false);
|
|
|
- return success(result);
|
|
|
- }
|
|
|
- int[] res = reconcileByQuery(orderid, "query");
|
|
|
- result.put("payStatus", res[0]);
|
|
|
- result.put("reconciled", res[1] == 1);
|
|
|
- return success(result);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 查询 OMG 真实交易状态并按结果补单/标失败。{@code /query}(被动补单/方案A) 与定时任务(方案B)共用。
|
|
|
- *
|
|
|
- * <p>幂等与自愈:流水已 {@code pay_status=1} 时,若订单未核销(跨事务中断残留)则补推订单状态,
|
|
|
- * 否则直接返回不重复处理;流水处于失败/退款终态不再查询。
|
|
|
- *
|
|
|
- * @param ddId 订单号
|
|
|
- * @param source 调用来源标记("query" 被动补单 / "scheduled" 定时补单),仅用于日志
|
|
|
- * @return {@code int[2]} = {payStatus, reconciled}:payStatus 0未付/1已付/2失败;reconciled 1=本次触发补单核销
|
|
|
- */
|
|
|
- public int[] reconcileByQuery(String ddId, String source) {
|
|
|
- PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
|
|
|
- if (order == null) {
|
|
|
- return new int[]{0, 0};
|
|
|
- }
|
|
|
- if (order.getState() != null && order.getState() == 4L) {
|
|
|
- return new int[]{0, 0};
|
|
|
- }
|
|
|
-
|
|
|
- // ① 自愈:流水已付(pay_status IN(1,3,4))但订单未核销(跨事务中断残留)→ 补推订单状态
|
|
|
- PosOrderOmgPayment paid = paymentService.getLatestPaidByDdId(ddId);
|
|
|
- if (paid != null) {
|
|
|
- boolean isPaid = Integer.valueOf(1).equals(paid.getPayStatus());
|
|
|
- if (isPaid && (order.getPayStatus() == null || order.getPayStatus() == 0L)) {
|
|
|
- handlePaymentSuccess(order);
|
|
|
- }
|
|
|
- return new int[]{isPaid ? 1 : 0, 0};
|
|
|
- }
|
|
|
-
|
|
|
- // 闸3:per-ddId 扫描间隔节流(/query 60s / 定时 180s),挡前端轮询放大、防 OMG 403 自残(T055)
|
|
|
- if (!omgQueryThrottle.acquireDdIdSlot(ddId, source)) {
|
|
|
- log.debug("[OMG-{}] per-ddId 节流,跳过本轮 OMG 查询: ddId={}", source, ddId);
|
|
|
- return new int[]{order.getPayStatus() == null ? 0 : order.getPayStatus().intValue(), 0};
|
|
|
- }
|
|
|
-
|
|
|
- // ② 遍历全量未付(pay_status=0,含历史行)queryTrade,任一已付即补单核销(补较早 MTN 如 id=26)
|
|
|
- List<PosOrderOmgPayment> unpaid = paymentService.listUnpaidByDdId(ddId);
|
|
|
- if (unpaid == null || unpaid.isEmpty()) {
|
|
|
- return new int[]{0, 0};
|
|
|
- }
|
|
|
- int lastStatus = 0;
|
|
|
- for (PosOrderOmgPayment p : unpaid) {
|
|
|
- PosStoreOmg cred = storeOmgService.getCredentialByMerchantIdAndStoreId(p.getMerchantId(), p.getStoreId());
|
|
|
- if (cred == null) {
|
|
|
- log.error("[OMG-{}] 门店凭证不可用: ddId={}, mtcn={}", source, ddId, p.getMerchantTradeNo());
|
|
|
- continue;
|
|
|
- }
|
|
|
- // per-MerchantID 令牌桶节流(1token/3s,burst1):拿不到令牌跳过本行本轮,不阻塞(防 OMG 403)
|
|
|
- if (!omgQueryThrottle.tryAcquireMerchantToken(p.getMerchantId())) {
|
|
|
- log.debug("[OMG-{}] MerchantID 令牌桶限流,跳过本行本轮: ddId={}, mtcn={}", source, ddId, p.getMerchantTradeNo());
|
|
|
- continue;
|
|
|
- }
|
|
|
- OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
- Map<String, String> resp;
|
|
|
- try {
|
|
|
- resp = omgPay.queryTrade(baseUrl, cfg, p.getMerchantTradeNo());
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("[OMG-{}] queryTrade 失败(下轮重试): ddId={}, mtcn={}, err={}: {}",
|
|
|
- source, ddId, p.getMerchantTradeNo(), e.getClass().getName(), e.getMessage());
|
|
|
- Map<String, String> errLog = new LinkedHashMap<>();
|
|
|
- errLog.put("ddId", ddId);
|
|
|
- errLog.put("mtcn", p.getMerchantTradeNo());
|
|
|
- errLog.put("source", source);
|
|
|
- errLog.put("error", e.getClass().getName() + ": " + e.getMessage());
|
|
|
- IpnLog ipnLog = new IpnLog();
|
|
|
- ipnLog.setType("omg_query_error");
|
|
|
- ipnLog.setIpnLog(JSON.toJSONString(errLog));
|
|
|
- ipnLogService.insertIpnLog(ipnLog);
|
|
|
- // OMG 按 MerchantID 限流(HTTP 403)时停止遍历,避免连发罚(T055 令牌桶节流完整前的过渡)
|
|
|
- if (e.getMessage() != null && e.getMessage().contains("HTTP 403")) {
|
|
|
- log.warn("[OMG-{}] queryTrade 触发 OMG 403 限流,停止本单遍历(下轮重试): ddId={}", source, ddId);
|
|
|
- return new int[]{lastStatus, 0};
|
|
|
- }
|
|
|
- continue;
|
|
|
- }
|
|
|
- String tradeStatus = resp.get("TradeStatus");
|
|
|
- if ("1".equals(tradeStatus)) {
|
|
|
- String tradeNo = resp.get("TradeNo");
|
|
|
- String paymentType = resp.get("PaymentType");
|
|
|
- int respAmt = toInt(resp.get("TradeAmt"), -1);
|
|
|
- if (tradeNo == null || tradeNo.isEmpty()
|
|
|
- || p.getAmount() == null || respAmt != p.getAmount()
|
|
|
- || order.getAmount() == null || order.getAmount().intValue() != p.getAmount()) {
|
|
|
- log.error("[OMG-{}] 补单金额/字段校验失败: ddId={}, mtcn={}, paymentAmt={}, orderAmt={}, queryAmt={}, tradeNo={}",
|
|
|
- source, ddId, p.getMerchantTradeNo(), p.getAmount(), order.getAmount(), respAmt, tradeNo);
|
|
|
- orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单金额/字段不符,需人工核对");
|
|
|
- continue;
|
|
|
- }
|
|
|
- Date payTime = parsePayTime(resp.get("PaymentDate"));
|
|
|
- Date tradeDate = parsePayTime(resp.get("TradeDate"));
|
|
|
- if (payTime == null || tradeDate == null) {
|
|
|
- log.warn("[OMG-{}] 补单响应日期非法: ddId={}, mtcn={}", source, ddId, p.getMerchantTradeNo());
|
|
|
- continue;
|
|
|
- }
|
|
|
- applyPaidResult(p, order, tradeNo, paymentType, 1, resp.get("RtnMsg"),
|
|
|
- null, payTime, tradeDate, auditJson(resp));
|
|
|
- orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单成功");
|
|
|
- log.info("[OMG-{}] 补单成功: ddId={}, mtcn={}, tradeNo={}", source, ddId, p.getMerchantTradeNo(), tradeNo);
|
|
|
- return new int[]{1, 1};
|
|
|
- } else if ("10200095".equals(tradeStatus)) {
|
|
|
- String tradeNo = resp.get("TradeNo");
|
|
|
- if (tradeNo == null || tradeNo.isEmpty()) {
|
|
|
- // 从未与 OMG 建立交易(未取号)→ markFail(0→2,CAS 允许 2→1 复活)
|
|
|
- paymentService.markFail(p.getId(), null, "OMG查询返回失败(10200095)", auditJson(resp));
|
|
|
- orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单:交易失败(10200095),mtcn=" + p.getMerchantTradeNo());
|
|
|
- lastStatus = 2;
|
|
|
- } else {
|
|
|
- // 已取号(可能 OMG 延迟建案/结算滞后)→ 不标失败,下轮再查
|
|
|
- log.warn("[OMG-{}] queryTrade 返回 10200095 但已取号(tradeNo={}),不标失败: ddId={}, mtcn={}",
|
|
|
- source, tradeNo, ddId, p.getMerchantTradeNo());
|
|
|
- }
|
|
|
- } else {
|
|
|
- // TradeStatus=0 未付(延期支付付款前常态)或未知 → no-op,定时任务下轮再查
|
|
|
- log.warn("[OMG-{}] queryTrade 返回未付/未知: ddId={}, mtcn={}, TradeStatus={}",
|
|
|
- source, ddId, p.getMerchantTradeNo(), tradeStatus);
|
|
|
- }
|
|
|
- }
|
|
|
- return new int[]{lastStatus, 0};
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 应用「已支付」结果(notify 回调成功分支、/query 被动补单、定时补单 三处共用)。
|
|
|
- * 幂等:markSuccess 按 trade_no CAS,已 pay_status=1 返回 0 → 不重复改单/推送。
|
|
|
- * 若订单已进终态(取消/退款),只记录资金事实不触发发货推送(需人工退款/核对)。
|
|
|
- */
|
|
|
- private void applyPaidResult(PosOrderOmgPayment payment, PosOrder order, String tradeNo,
|
|
|
- String payType, int rtnCode, String rtnMsg, String authCode,
|
|
|
- Date payTime, Date tradeDate, String callbackRaw) {
|
|
|
- boolean terminalOrder = (order.getState() != null && order.getState() == 4L)
|
|
|
- || (order.getPayStatus() != null && order.getPayStatus() == 2L);
|
|
|
- int n = paymentService.markSuccess(payment.getId(), tradeNo, payType, rtnCode, rtnMsg,
|
|
|
- authCode, payTime, tradeDate, callbackRaw);
|
|
|
- if (n <= 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
- if (terminalOrder) {
|
|
|
- recordPaidOrderWithoutFulfillment(order,
|
|
|
- "OMG支付成功晚于订单取消/退款,需立即退款或人工核对");
|
|
|
- log.error("OMG late successful payment requires refund/reconciliation: ddId={}, tradeNo={}",
|
|
|
- order.getDdId(), tradeNo);
|
|
|
- } else {
|
|
|
- // 首次成功:原子核销订单后,在事务提交后推送用户/商家
|
|
|
- handlePaymentSuccess(order);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ============================ 支付成功业务链路(参照 PosOrderController.sendHdfkMessage 货到付款同款) ============================
|
|
|
-
|
|
|
- /** 支付成功:原子更新订单支付状态并写日志;事务提交后再推送,避免外部调用干扰账务事务。 */
|
|
|
- private void handlePaymentSuccess(PosOrder order) {
|
|
|
- PosOrder upd = new PosOrder();
|
|
|
- upd.setPayStatus(1L);
|
|
|
- boolean updated = posOrderService.update(upd, new UpdateWrapper<PosOrder>()
|
|
|
- .eq("id", order.getId())
|
|
|
- .eq("state", 0)
|
|
|
- .eq("pay_status", 0));
|
|
|
- if (!updated) {
|
|
|
- PosOrder latest = posOrderService.getById(order.getId());
|
|
|
- if (latest != null && (Long.valueOf(1L).equals(latest.getPayStatus())
|
|
|
- || Long.valueOf(2L).equals(latest.getPayStatus())
|
|
|
- || Long.valueOf(4L).equals(latest.getState()))) {
|
|
|
- recordPaidOrderWithoutFulfillment(latest,
|
|
|
- "OMG支付核销遇到订单状态并发变化,需退款或人工核对");
|
|
|
- return;
|
|
|
- }
|
|
|
- throw new IllegalStateException("原子更新OMG订单支付状态失败");
|
|
|
- }
|
|
|
-
|
|
|
- orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", "系统收到OMG支付成功回调");
|
|
|
- runAfterCommit(() -> pushPaymentSuccess(order));
|
|
|
- }
|
|
|
-
|
|
|
- /** 只记录资金事实,不改变订单业务状态、不触发发货推送。 */
|
|
|
- private void recordPaidOrderWithoutFulfillment(PosOrder order, String logContent) {
|
|
|
- PosOrder upd = new PosOrder();
|
|
|
- upd.setId(order.getId());
|
|
|
- upd.setPayStatus(1L);
|
|
|
- if (!posOrderService.saveOrUpdate(upd)) {
|
|
|
- throw new IllegalStateException("补偿OMG订单支付状态失败");
|
|
|
- }
|
|
|
- orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", logContent);
|
|
|
- }
|
|
|
-
|
|
|
- private void runAfterCommit(Runnable action) {
|
|
|
- if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
|
|
- action.run();
|
|
|
- return;
|
|
|
- }
|
|
|
- TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
|
|
- @Override
|
|
|
- public void afterCommit() {
|
|
|
- action.run();
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- private void pushPaymentSuccess(PosOrder order) {
|
|
|
- try {
|
|
|
- String ddId = String.valueOf(order.getDdId());
|
|
|
- String title = MessageUtils.message("no.message.push.message");
|
|
|
- String body = OrderPushBodyDto.getJson(ddId, "0", 0);
|
|
|
-
|
|
|
- InfoUser user = order.getUserId() == null ? null : infoUserService.getById(order.getUserId());
|
|
|
- if (user != null) {
|
|
|
- PayPush push = new PayPush();
|
|
|
- push.apppush(user.getCid(), title, MessageUtils.message("no.message.push.payment.success"), body);
|
|
|
- pushEventService.PublisherEvent(user.getUserId(), title,
|
|
|
- MessageUtils.message("no.message.push.payment.success"), body);
|
|
|
- }
|
|
|
-
|
|
|
- InfoUser sh = order.getShId() == null ? null : infoUserService.getById(order.getShId());
|
|
|
- if (sh != null) {
|
|
|
- PayPush push = new PayPush();
|
|
|
- push.shpush(sh.getCid(), title, MessageUtils.message("no.message.push.new.order"), body);
|
|
|
- pushEventService.PublisherEvent(sh.getUserId(), title,
|
|
|
- MessageUtils.message("no.message.push.new.order"), body);
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("OMG 支付成功推送异常: ddId={}", order.getDdId(), e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // ============================ 辅助 ============================
|
|
|
-
|
|
|
- private String callbackIp(String forwardedFor) {
|
|
|
- if (forwardedFor == null || forwardedFor.isBlank()) {
|
|
|
- return "unknown";
|
|
|
- }
|
|
|
- String firstIp = forwardedFor.split(",", 2)[0].trim();
|
|
|
- return firstIp.isEmpty() || firstIp.length() > 64 ? "unknown" : firstIp;
|
|
|
- }
|
|
|
-
|
|
|
- private boolean invalidOrderRequest(OmgOrderRequest request) {
|
|
|
- if (request == null || request.getOrderid() == null) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- String orderid = request.getOrderid();
|
|
|
- return orderid.trim().isEmpty();
|
|
|
- }
|
|
|
-
|
|
|
- private int toInt(String s, int def) {
|
|
|
- if (s == null || s.isEmpty()) {
|
|
|
- return def;
|
|
|
- }
|
|
|
- try {
|
|
|
- return Integer.parseInt(s);
|
|
|
- } catch (Exception e) {
|
|
|
- return def;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /** 解析 callbackRaw(JSON)为 Map,供前端读 ATM/超商取号信息。 */
|
|
|
- private Map<String, String> parseJsonObject(String json) {
|
|
|
- Map<String, String> map = new LinkedHashMap<>();
|
|
|
- if (json == null || json.isEmpty()) {
|
|
|
- return map;
|
|
|
- }
|
|
|
- try {
|
|
|
- com.alibaba.fastjson2.JSONObject obj = JSON.parseObject(json);
|
|
|
- for (String k : obj.keySet()) {
|
|
|
- Object v = obj.get(k);
|
|
|
- map.put(k, v == null ? "" : v.toString());
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("解析 callbackRaw JSON 失败", e);
|
|
|
- }
|
|
|
- return map;
|
|
|
- }
|
|
|
-
|
|
|
- /** 取号页只返回支付凭证字段,避免把信用卡额外回传参数暴露给客户端。 */
|
|
|
- private Map<String, String> parsePaymentInfo(String json) {
|
|
|
- Set<String> allowed = Set.of("BankCode", "vAccount", "PaymentNo", "ExpireDate",
|
|
|
- "PaymentType", "TradeNo", "MerchantTradeNo", "RtnCode", "RtnMsg",
|
|
|
- "CVSStoreID", "CVSStoreName", "PayFrom", "Barcode1", "Barcode2", "Barcode3");
|
|
|
- Map<String, String> raw = parseJsonObject(json);
|
|
|
- raw.keySet().removeIf(key -> !allowed.contains(key));
|
|
|
- return raw;
|
|
|
- }
|
|
|
-
|
|
|
- private String auditJson(Map<String, String> form) {
|
|
|
- Map<String, String> audit = new LinkedHashMap<>(form);
|
|
|
-// audit.remove("CheckMacValue");
|
|
|
-// audit.remove("AuthCode");
|
|
|
-// audit.remove("auth_code");
|
|
|
-// audit.remove("card4no");
|
|
|
-// audit.remove("card6no");
|
|
|
- return JSON.toJSONString(audit);
|
|
|
- }
|
|
|
-
|
|
|
- /** 严格解析 OMG 回调日期;非法日期返回 null,避免把坏数据伪装成当前时间。 */
|
|
|
- private Date parsePayTime(String s) {
|
|
|
- if (s == null || s.isEmpty()) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- String[] fmts = {"yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss"};
|
|
|
- for (String f : fmts) {
|
|
|
- try {
|
|
|
- SimpleDateFormat format = new SimpleDateFormat(f);
|
|
|
- format.setLenient(false);
|
|
|
- format.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
|
|
|
- return format.parse(s);
|
|
|
- } catch (Exception ignore) {
|
|
|
- }
|
|
|
- }
|
|
|
- return null;
|
|
|
- }
|
|
|
-}
|