|
|
@@ -1,6 +1,8 @@
|
|
|
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.utils.PayPush;
|
|
|
import com.ruoyi.app.utils.event.PushEventService;
|
|
|
@@ -32,6 +34,10 @@ 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.GetMapping;
|
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
@@ -47,7 +53,11 @@ import java.text.SimpleDateFormat;
|
|
|
import java.util.Date;
|
|
|
import java.util.Enumeration;
|
|
|
import java.util.LinkedHashMap;
|
|
|
+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,零蓝新依赖)。
|
|
|
@@ -98,6 +108,8 @@ public class OmgPayController extends BaseController {
|
|
|
private String returnUrl;
|
|
|
@Value("${omg.order-result-url}")
|
|
|
private String orderResultUrl;
|
|
|
+ @Value("${omg.payment-info-url}")
|
|
|
+ private String paymentInfoUrl;
|
|
|
|
|
|
// ============================ US1:发起 AIO 幕前支付 ============================
|
|
|
|
|
|
@@ -109,6 +121,7 @@ public class OmgPayController extends BaseController {
|
|
|
@Auth
|
|
|
@RepeatSubmit(interval = 1000, message = "请求过于频繁")
|
|
|
@PostMapping("/create")
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public AjaxResult create(@RequestParam String orderid, HttpServletRequest request) {
|
|
|
String token = request.getHeader("token");
|
|
|
String userId;
|
|
|
@@ -128,6 +141,9 @@ public class OmgPayController extends BaseController {
|
|
|
if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
|
|
|
return error("无权操作该订单");
|
|
|
}
|
|
|
+ if (order.getState() != null && order.getState() == 4L) {
|
|
|
+ return error("订单已取消,不可重新支付");
|
|
|
+ }
|
|
|
if (order.getPayStatus() != null && order.getPayStatus() == 1L) {
|
|
|
return error("订单已支付");
|
|
|
}
|
|
|
@@ -145,20 +161,27 @@ public class OmgPayController extends BaseController {
|
|
|
}
|
|
|
OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
|
|
|
- String merchantTradeNo = genMerchantTradeNo(orderid);
|
|
|
+ String merchantTradeNo = createPaymentAttempt(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", new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
|
|
|
+ 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("ItemName", "order " + orderid);
|
|
|
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);
|
|
|
}
|
|
|
@@ -166,9 +189,6 @@ public class OmgPayController extends BaseController {
|
|
|
// 组参 + CheckMacValue,返回含 gatewayUrl 的 form 字段
|
|
|
Map<String, String> form = omgPay.createAioForm(baseUrl, cfg, params);
|
|
|
|
|
|
- // 落 OMG 支付流水(pay_status=0)
|
|
|
- paymentService.createPayment(orderid, merchantTradeNo, order.getMdId(), cred.getMerchantId(), order.getAmount(), "ALL");
|
|
|
-
|
|
|
// 更新订单 payType=7 / payUrl=gatewayUrl(仅更这两个字段)
|
|
|
PosOrder upd = new PosOrder();
|
|
|
upd.setId(order.getId());
|
|
|
@@ -181,14 +201,29 @@ public class OmgPayController extends BaseController {
|
|
|
return success(form);
|
|
|
}
|
|
|
|
|
|
- /** MerchantTradeNo = "OMG" + ddId(清洗为英数,≤20 字元)。重新发起的幂等/后缀策略见 D6,MVP 先用 ddId。 */
|
|
|
- private String genMerchantTradeNo(String ddId) {
|
|
|
- String cleaned = ddId == null ? "" : ddId.replaceAll("[^0-9A-Za-z]", "");
|
|
|
- String no = "OMG" + cleaned;
|
|
|
- if (no.length() > 20) {
|
|
|
- no = no.substring(0, 20);
|
|
|
+ /**
|
|
|
+ * 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- return no;
|
|
|
+ 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 支付结果回调 ============================
|
|
|
@@ -202,21 +237,32 @@ public class OmgPayController extends BaseController {
|
|
|
*/
|
|
|
@Anonymous
|
|
|
@PostMapping(value = "/notify", produces = "text/plain;charset=UTF-8")
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public String notify(HttpServletRequest request) {
|
|
|
- Map<String, String> form = collectForm(request);
|
|
|
+ Map<String, String> form;
|
|
|
+ try {
|
|
|
+ form = collectForm(request);
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ log.warn("OMG callback rejected: {}", e.getMessage());
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
|
|
|
// 记录 IPN 日志
|
|
|
try {
|
|
|
IpnLog ipnLog = new IpnLog();
|
|
|
ipnLog.setIp(new com.ruoyi.app.utils.IpUtils().getIpAddr(request));
|
|
|
- ipnLog.setIpnLog(form.toString());
|
|
|
+ ipnLog.setIpnLog(auditJson(form));
|
|
|
ipnLogService.insertIpnLog(ipnLog);
|
|
|
} catch (Exception e) {
|
|
|
log.warn("记 OMG IPN 日志失败", e);
|
|
|
}
|
|
|
|
|
|
String merchantId = form.get("MerchantID");
|
|
|
- PosStoreOmg cred = storeOmgService.getEnabledCredentialByMerchantId(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";
|
|
|
@@ -230,47 +276,89 @@ public class OmgPayController extends BaseController {
|
|
|
|
|
|
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");
|
|
|
|
|
|
- // 幂等:同 tradeNo 已成功则直接返回
|
|
|
- PosOrderOmgPayment exist = paymentService.getByTradeNo(tradeNo);
|
|
|
- if (exist != null && Integer.valueOf(1).equals(exist.getPayStatus())) {
|
|
|
- return "1|OK";
|
|
|
- }
|
|
|
-
|
|
|
// 订单关联:由 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 (order.getAmount() == null || tradeAmt != order.getAmount()) {
|
|
|
- log.error("OMG 回调金额不符: ddId={}, orderAmt={}, callbackAmt={}", ddId, order.getAmount(), tradeAmt);
|
|
|
+ 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)) {
|
|
|
- // 核销成功
|
|
|
- int n = paymentService.markSuccess(payment.getId(), tradeNo, paymentType, rtnCode,
|
|
|
- form.get("RtnMsg"), form.get("AuthCode"), parsePayTime(paymentDate), form.toString());
|
|
|
- if (n > 0) {
|
|
|
- // 首次成功:更新订单 state=0(待接单)/payStatus=1 + 推送用户/商家
|
|
|
- handlePaymentSuccess(order);
|
|
|
+ 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"), form.toString());
|
|
|
+ paymentService.markFail(payment.getId(), rtnCode, form.get("RtnMsg"), auditJson(form));
|
|
|
log.warn("OMG 回调交易失败: ddId={}, rtnCode={}, rtnMsg={}", ddId, rtnCode, form.get("RtnMsg"));
|
|
|
} else {
|
|
|
// SimulatePaid=1 模拟支付:不发货,仅记录
|
|
|
@@ -289,15 +377,23 @@ public class OmgPayController extends BaseController {
|
|
|
try {
|
|
|
Map<String, String> form = collectForm(request);
|
|
|
String mtn = form.get("MerchantTradeNo");
|
|
|
- // MerchantTradeNo = "OMG" + ddId,去前缀还原 ddId
|
|
|
- if (mtn != null && mtn.startsWith("OMG")) {
|
|
|
- ddId = mtn.substring(3);
|
|
|
+ // 由 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, StandardCharsets.UTF_8));
|
|
|
+ response.sendRedirect(orderResultUrl + sep + "ddId=" + URLEncoder.encode(ddId == null ? "" : ddId, StandardCharsets.UTF_8));
|
|
|
}
|
|
|
|
|
|
// ============================ US3:ATM/超商 取号回调 + 取号查询 ============================
|
|
|
@@ -310,18 +406,28 @@ public class OmgPayController extends BaseController {
|
|
|
@Anonymous
|
|
|
@PostMapping(value = "/paymentInfo", produces = "text/plain;charset=UTF-8")
|
|
|
public String paymentInfoCallback(HttpServletRequest request) {
|
|
|
- Map<String, String> form = collectForm(request);
|
|
|
+ Map<String, String> form;
|
|
|
+ try {
|
|
|
+ form = collectForm(request);
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ log.warn("OMG paymentInfo rejected: {}", e.getMessage());
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
try {
|
|
|
IpnLog ipnLog = new IpnLog();
|
|
|
ipnLog.setIp(new com.ruoyi.app.utils.IpUtils().getIpAddr(request));
|
|
|
- ipnLog.setIpnLog(form.toString());
|
|
|
+ ipnLog.setIpnLog(auditJson(form));
|
|
|
ipnLogService.insertIpnLog(ipnLog);
|
|
|
} catch (Exception e) {
|
|
|
log.warn("记 OMG paymentInfo IPN 日志失败", e);
|
|
|
}
|
|
|
|
|
|
String merchantId = form.get("MerchantID");
|
|
|
- PosStoreOmg cred = storeOmgService.getEnabledCredentialByMerchantId(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";
|
|
|
@@ -337,8 +443,19 @@ public class OmgPayController extends BaseController {
|
|
|
log.warn("OMG paymentInfo 无对应发起记录: merchantTradeNo={}", merchantTradeNo);
|
|
|
return "1|OK";
|
|
|
}
|
|
|
- // 落取号信息(BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate),不改 pay_status
|
|
|
- paymentService.markPaymentInfo(payment.getId(), form.get("TradeNo"), form.toString());
|
|
|
+ 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";
|
|
|
}
|
|
|
@@ -370,7 +487,7 @@ public class OmgPayController extends BaseController {
|
|
|
result.put("payType", p.getPayType());
|
|
|
result.put("amount", p.getAmount());
|
|
|
result.put("payStatus", p.getPayStatus());
|
|
|
- result.put("info", parseKv(p.getCallbackRaw()));
|
|
|
+ result.put("info", parsePaymentInfo(p.getCallbackRaw()));
|
|
|
return success(result);
|
|
|
}
|
|
|
|
|
|
@@ -385,8 +502,9 @@ public class OmgPayController extends BaseController {
|
|
|
@PostMapping("/refund")
|
|
|
public AjaxResult refund(@RequestParam String orderid, HttpServletRequest request) {
|
|
|
String token = request.getHeader("token");
|
|
|
+ String userId;
|
|
|
try {
|
|
|
- new JwtUtil().getusid(token);
|
|
|
+ userId = new JwtUtil().getusid(token);
|
|
|
} catch (Exception e) {
|
|
|
return error(MessageUtils.message("no.order.id.error"));
|
|
|
}
|
|
|
@@ -394,6 +512,9 @@ public class OmgPayController extends BaseController {
|
|
|
if (order == null) {
|
|
|
return error("订单不存在");
|
|
|
}
|
|
|
+ if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
|
|
|
+ return error("无权操作该订单");
|
|
|
+ }
|
|
|
return refundOrder(order);
|
|
|
}
|
|
|
|
|
|
@@ -413,42 +534,58 @@ public class OmgPayController extends BaseController {
|
|
|
return error("订单未支付,无需退款");
|
|
|
}
|
|
|
String ddId = String.valueOf(order.getDdId());
|
|
|
- PosOrderOmgPayment payment = paymentService.getLatestByDdId(ddId);
|
|
|
+ PosOrderOmgPayment payment = paymentService.getLatestPaidByDdId(ddId);
|
|
|
if (payment == null) {
|
|
|
return error("无 OMG 支付流水");
|
|
|
}
|
|
|
- int amount = order.getAmount() == null ? 0 : order.getAmount();
|
|
|
+ int amount = payment.getAmount() == null ? 0 : payment.getAmount();
|
|
|
String payType = payment.getPayType();
|
|
|
+ if (amount <= 0 || payment.getTradeNo() == null || payment.getTradeNo().isEmpty()) {
|
|
|
+ return error("OMG 支付流水不完整");
|
|
|
+ }
|
|
|
+ if (order.getAmount() == null || order.getAmount().intValue() != amount) {
|
|
|
+ return error("OMG 支付流水金额与订单不一致");
|
|
|
+ }
|
|
|
|
|
|
// ATM/超商/BarcodeATM 无退款 API → 记录待人工
|
|
|
- if (payType == null || payType.startsWith("ATM_") || payType.startsWith("CVS_") || payType.startsWith("BarcodeATM_")) {
|
|
|
+ if (!"Credit_CreditCard".equals(payType)) {
|
|
|
refundService.record(payment.getId(), ddId, payment.getTradeNo(), null, amount, null,
|
|
|
"延期支付方式无退款API,待人工在 OMG 后台处理", "");
|
|
|
return error("该支付方式需人工在 OMG 后台退款");
|
|
|
}
|
|
|
|
|
|
- PosStoreOmg cred = storeOmgService.getEnabledCredential(order.getMdId());
|
|
|
- if (cred == null) {
|
|
|
+ if (paymentService.markRefunding(payment.getId()) == 0) {
|
|
|
+ return error("退款已处理或正在处理中");
|
|
|
+ }
|
|
|
+
|
|
|
+ PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(payment.getMerchantId());
|
|
|
+ if (cred == null || cred.getStoreId() == null || !cred.getStoreId().equals(payment.getStoreId())) {
|
|
|
+ paymentService.restorePaidFromRefunding(payment.getId());
|
|
|
return error("门店 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,
|
|
|
- "ERROR: " + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()), "");
|
|
|
- return error("OMG 退款服务暂不可用");
|
|
|
+ "退款结果未知,需对账: " + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()), "");
|
|
|
+ return error("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 ? "" : resp.toString());
|
|
|
+ resp == null ? "" : JSON.toJSONString(resp));
|
|
|
|
|
|
if (rtnCode == 1) {
|
|
|
- paymentService.markRefunded(payment.getId());
|
|
|
+ if (paymentService.markRefunded(payment.getId()) == 0) {
|
|
|
+ log.error("OMG refund succeeded but ledger transition failed: paymentId={}", payment.getId());
|
|
|
+ return error("OMG 退款成功,账务状态待核对");
|
|
|
+ }
|
|
|
PosOrder upd = new PosOrder();
|
|
|
upd.setId(order.getId());
|
|
|
upd.setPayStatus(2L);
|
|
|
@@ -456,55 +593,261 @@ public class OmgPayController extends BaseController {
|
|
|
orderLogHelper.logSync(ddId, 0, null, "系统", "OMG 信用卡退款成功");
|
|
|
return success("退款成功");
|
|
|
}
|
|
|
+ paymentService.restorePaidFromRefunding(payment.getId());
|
|
|
return error("OMG 退款失败:" + rtnMsg);
|
|
|
}
|
|
|
|
|
|
+ // ============================ 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(@RequestParam String orderid, HttpServletRequest request) {
|
|
|
+ String token = request.getHeader("token");
|
|
|
+ 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};
|
|
|
+ }
|
|
|
+ // 订单已取消:不查 OMG、不补单
|
|
|
+ if (order.getState() != null && order.getState() == 4L) {
|
|
|
+ return new int[]{0, 0};
|
|
|
+ }
|
|
|
+ PosOrderOmgPayment payment = paymentService.getLatestByDdId(ddId);
|
|
|
+ if (payment == null) {
|
|
|
+ return new int[]{0, 0};
|
|
|
+ }
|
|
|
+ Integer ps = payment.getPayStatus();
|
|
|
+ if (Integer.valueOf(1).equals(ps)) {
|
|
|
+ // 流水已付:若订单未核销(跨事务中断残留)则自愈补推订单状态;否则幂等返回。
|
|
|
+ // (state==4 已在方法入口拦截,此处订单必非取消态。)
|
|
|
+ if (order.getPayStatus() == null || order.getPayStatus() == 0L) {
|
|
|
+ handlePaymentSuccess(order);
|
|
|
+ }
|
|
|
+ return new int[]{1, 0};
|
|
|
+ }
|
|
|
+ if (ps != null && ps != 0) {
|
|
|
+ // 2失败/3已退/4退款中:终态,不再查询
|
|
|
+ return new int[]{ps == 2 ? 2 : 0, 0};
|
|
|
+ }
|
|
|
+
|
|
|
+ PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(payment.getMerchantId());
|
|
|
+ if (cred == null || cred.getStoreId() == null || !cred.getStoreId().equals(payment.getStoreId())) {
|
|
|
+ log.warn("[OMG-{}] 门店凭证不可用: ddId={}", source, ddId);
|
|
|
+ return new int[]{0, 0};
|
|
|
+ }
|
|
|
+ OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
+
|
|
|
+ Map<String, String> resp;
|
|
|
+ try {
|
|
|
+ resp = omgPay.queryTrade(baseUrl, cfg, payment.getMerchantTradeNo());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("[OMG-{}] queryTrade 失败(下轮重试): ddId={}, err={}", source, ddId, e.getMessage());
|
|
|
+ return new int[]{0, 0};
|
|
|
+ }
|
|
|
+ String tradeStatus = resp.get("TradeStatus");
|
|
|
+ String tradeNo = resp.get("TradeNo");
|
|
|
+ String paymentType = resp.get("PaymentType");
|
|
|
+ int respAmt = toInt(resp.get("TradeAmt"), -1);
|
|
|
+ if ("1".equals(tradeStatus)) {
|
|
|
+ if (tradeNo == null || tradeNo.isEmpty()
|
|
|
+ || payment.getAmount() == null || respAmt != payment.getAmount()
|
|
|
+ || order.getAmount() == null || order.getAmount().intValue() != payment.getAmount()) {
|
|
|
+ log.error("[OMG-{}] 补单金额/字段校验失败: ddId={}, paymentAmt={}, orderAmt={}, queryAmt={}, tradeNo={}",
|
|
|
+ source, ddId, payment.getAmount(), order.getAmount(), respAmt, tradeNo);
|
|
|
+ orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单金额/字段不符,需人工核对");
|
|
|
+ return new int[]{1, 0};
|
|
|
+ }
|
|
|
+ Date payTime = parsePayTime(resp.get("PaymentDate"));
|
|
|
+ Date tradeDate = parsePayTime(resp.get("TradeDate"));
|
|
|
+ if (payTime == null || tradeDate == null) {
|
|
|
+ log.warn("[OMG-{}] 补单响应日期非法: ddId={}", source, ddId);
|
|
|
+ return new int[]{1, 0};
|
|
|
+ }
|
|
|
+ applyPaidResult(payment, order, tradeNo, paymentType, 1, resp.get("RtnMsg"),
|
|
|
+ null, payTime, tradeDate, auditJson(resp));
|
|
|
+ orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单成功");
|
|
|
+ log.info("[OMG-{}] 补单成功: ddId={}, tradeNo={}", source, ddId, tradeNo);
|
|
|
+ return new int[]{1, 1};
|
|
|
+ } else if ("10200095".equals(tradeStatus)) {
|
|
|
+ paymentService.markFail(payment.getId(), null, "OMG查询返回失败(10200095)", auditJson(resp));
|
|
|
+ orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单:交易失败(10200095)");
|
|
|
+ return new int[]{2, 0};
|
|
|
+ }
|
|
|
+ // TradeStatus=0 未付(延期支付付款前常态)或其他未知值:保持未支付,定时任务下轮再查
|
|
|
+ return new int[]{0, 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 货到付款同款) ============================
|
|
|
|
|
|
- /** 支付成功:更新订单 state=0/payStatus=1 + 订单日志 + 推送用户(支付成功)/商家(新订单)。骑手推送平台当前为空实现,此处不补。 */
|
|
|
+ /** 支付成功:原子更新订单支付状态并写日志;事务提交后再推送,避免外部调用干扰账务事务。 */
|
|
|
private void handlePaymentSuccess(PosOrder order) {
|
|
|
- try {
|
|
|
- PosOrder upd = new PosOrder();
|
|
|
- upd.setId(order.getId());
|
|
|
- upd.setState(0L);
|
|
|
- upd.setPayStatus(1L);
|
|
|
- posOrderService.saveOrUpdate(upd);
|
|
|
+ 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支付成功回调");
|
|
|
+ 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 state = String.valueOf(order.getState());
|
|
|
String title = MessageUtils.message("no.message.push.message");
|
|
|
- String body = OrderPushBodyDto.getJson(ddId, state, 0);
|
|
|
+ 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);
|
|
|
+ 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);
|
|
|
+ pushEventService.PublisherEvent(sh.getUserId(), title,
|
|
|
+ MessageUtils.message("no.message.push.new.order"), body);
|
|
|
}
|
|
|
- // 可接单骑手推送:平台 PosOrderController.sendQsPush 当前为空实现(与货到付款/蓝新一致),此处不补
|
|
|
} catch (Exception e) {
|
|
|
- log.error("OMG 支付成功业务处理异常: ddId={}", order.getDdId(), e);
|
|
|
+ log.error("OMG 支付成功推送异常: ddId={}", order.getDdId(), e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ============================ 辅助 ============================
|
|
|
|
|
|
/** 收集 form-urlencoded 回调参数为 Map(OMG 明文参数,含 CheckMacValue)。 */
|
|
|
- private Map<String, String> collectForm(HttpServletRequest req) {
|
|
|
+ Map<String, String> collectForm(HttpServletRequest req) {
|
|
|
Map<String, String> map = new LinkedHashMap<>();
|
|
|
Enumeration<String> names = req.getParameterNames();
|
|
|
while (names.hasMoreElements()) {
|
|
|
String n = names.nextElement();
|
|
|
- map.put(n, req.getParameter(n));
|
|
|
+ String[] values = req.getParameterValues(n);
|
|
|
+ if (n == null || n.isEmpty() || n.length() > 100 || values == null || values.length != 1
|
|
|
+ || values[0] == null || values[0].length() > 4096) {
|
|
|
+ throw new IllegalArgumentException("invalid or duplicate callback parameter");
|
|
|
+ }
|
|
|
+ map.put(n, values[0]);
|
|
|
+ if (map.size() > 100) {
|
|
|
+ throw new IllegalArgumentException("too many callback parameters");
|
|
|
+ }
|
|
|
}
|
|
|
return map;
|
|
|
}
|
|
|
@@ -520,37 +863,59 @@ public class OmgPayController extends BaseController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 解析 callbackRaw(LinkedHashMap.toString 形如 {A=1, B=2})为 Map,供前端读 ATM/超商取号信息。 */
|
|
|
- private Map<String, String> parseKv(String kv) {
|
|
|
+ /** 解析 callbackRaw(JSON)为 Map,供前端读 ATM/超商取号信息。 */
|
|
|
+ private Map<String, String> parseJsonObject(String json) {
|
|
|
Map<String, String> map = new LinkedHashMap<>();
|
|
|
- if (kv == null || kv.isEmpty()) {
|
|
|
+ if (json == null || json.isEmpty()) {
|
|
|
return map;
|
|
|
}
|
|
|
- String s = kv;
|
|
|
- if (s.startsWith("{") && s.endsWith("}")) {
|
|
|
- s = s.substring(1, s.length() - 1);
|
|
|
- }
|
|
|
- for (String pair : s.split(",")) {
|
|
|
- int idx = pair.indexOf('=');
|
|
|
- if (idx > 0) {
|
|
|
- map.put(pair.substring(0, idx).trim(), pair.substring(idx + 1).trim());
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
- /** 解析回调 PaymentDate(兼容 yyyy/MM/dd 与 yyyy-MM-dd),失败取当前时间。 */
|
|
|
+ /** 取号页只返回支付凭证字段,避免把信用卡额外回传参数暴露给客户端。 */
|
|
|
+ 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 new Date();
|
|
|
+ return null;
|
|
|
}
|
|
|
String[] fmts = {"yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss"};
|
|
|
for (String f : fmts) {
|
|
|
try {
|
|
|
- return new SimpleDateFormat(f).parse(s);
|
|
|
+ SimpleDateFormat format = new SimpleDateFormat(f);
|
|
|
+ format.setLenient(false);
|
|
|
+ format.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
|
|
|
+ return format.parse(s);
|
|
|
} catch (Exception ignore) {
|
|
|
}
|
|
|
}
|
|
|
- return new Date();
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|