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,零蓝新依赖)。
*
*
本期实现:
*
* - {@code POST /pay/omg/create} — 发起 AIO 幕前支付(US1,@Auth)。返回含 gatewayUrl 的 form 字段,
* 前端构建隐藏 form submit 到 OMG 收银台(ChoosePayment=ALL)。
* - {@code POST /pay/omg/notify} — OMG 服务端回调(US2,@Anonymous)。验签 CheckMacValue + 幂等 + 金额 +
* RtnCode/SimulatePaid 校验 → markSuccess → 更新订单 payStatus=1 + 推送用户/商家 → 回纯串 {@code 1|OK}。
* - {@code GET|POST /pay/omg/return} — 完成页引导(US2,@Anonymous)。仅 302 回前端结果页,不改订单状态。
*
*
* @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,避免重复活跃行。
*
* 流程:校验登录/订单归属/未支付/金额/门店 → 查门店 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().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 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 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 防堆积核心)。
*
* 优先复用新鲜期内活跃未付且未取号(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 解密)。
*
*
记 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 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().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),不改订单状态(以 notify 为准)。
*/
@Anonymous
@RequestMapping(value = "/return", method = {RequestMethod.GET, RequestMethod.POST})
public void returnCallback(@ModelAttribute OmgCallbackRequest callback,
HttpServletResponse response) throws IOException {
String ddId = "";
try {
Map 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 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().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 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().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 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 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 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().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 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)共用。
*
* 幂等与自愈:流水已 {@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().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 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 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 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()
.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 parseJsonObject(String json) {
Map 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 parsePaymentInfo(String json) {
Set allowed = Set.of("BankCode", "vAccount", "PaymentNo", "ExpireDate",
"PaymentType", "TradeNo", "MerchantTradeNo", "RtnCode", "RtnMsg",
"CVSStoreID", "CVSStoreName", "PayFrom", "Barcode1", "Barcode2", "Barcode3");
Map raw = parseJsonObject(json);
raw.keySet().removeIf(key -> !allowed.contains(key));
return raw;
}
private String auditJson(Map form) {
Map 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;
}
}