| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109 |
- 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;
- }
- }
|