|
|
@@ -20,6 +20,8 @@ import com.ruoyi.system.service.IPosOrderLinePaymentService;
|
|
|
import com.ruoyi.system.service.IPosOrderService;
|
|
|
import com.ruoyi.system.service.IPosStoreService;
|
|
|
import com.ruoyi.system.service.IPosStoreLinePayService;
|
|
|
+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;
|
|
|
@@ -35,6 +37,7 @@ import java.util.UUID;
|
|
|
/** Merchant-presented LINE Pay Offline My Code orchestration. */
|
|
|
@Service
|
|
|
public class LinePayOfflineService {
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(LinePayOfflineService.class);
|
|
|
private static final String MODE_OFFLINE = "OFFLINE";
|
|
|
private static final Set<String> AUTH_REQUIRED_CODES = Set.of("1145", "1169");
|
|
|
private static final Set<String> UNCERTAIN_CODES = Set.of(
|
|
|
@@ -109,6 +112,9 @@ public class LinePayOfflineService {
|
|
|
if (!MODE_OFFLINE.equals(active.getPaymentMode())) {
|
|
|
throw new ServiceException(MessageUtils.message("line.pay.offline.attempt.blocked"));
|
|
|
}
|
|
|
+ // 已有活跃流水:本次扫码不再发给 LINE,前端凭 reusedAttempt 提示不可重复提交
|
|
|
+ log.info("LINE Pay offline pay reuses active attempt, ddId={}, paymentId={}, status={}",
|
|
|
+ ddId, active.getId(), active.getStatus());
|
|
|
return result(active, true);
|
|
|
}
|
|
|
PosStoreLinePay credential = credentialService.getEnabledCurrent(order.getMdId());
|
|
|
@@ -118,9 +124,15 @@ public class LinePayOfflineService {
|
|
|
IntentSelection selection = createIntent(order, credential);
|
|
|
PosOrderLinePayment intent = selection.payment();
|
|
|
if (!selection.created() || !"REQUESTING".equals(intent.getStatus())) {
|
|
|
+ log.info("LINE Pay offline pay returns existing attempt, ddId={}, paymentId={}, status={}",
|
|
|
+ ddId, intent.getId(), intent.getStatus());
|
|
|
return result(intent, true);
|
|
|
}
|
|
|
LinePayCredential gatewayCredential = toCredential(credential);
|
|
|
+ // 发往 LINE 前留下完整上下文(金额/流水/付款码掩码),网关超时排查依赖这一行
|
|
|
+ log.info("LINE Pay offline gateway pay start, ddId={}, paymentId={}, lineOrderId={}, storeId={}, amount={}, currency={}, myCode={}",
|
|
|
+ ddId, intent.getId(), intent.getLineOrderId(), intent.getStoreId(), intent.getAmount(),
|
|
|
+ intent.getCurrency(), maskMyCode(oneTimeKey));
|
|
|
try {
|
|
|
LinePayOfflineRequest request = new LinePayOfflineRequest(intent.getLineOrderId(),
|
|
|
ddId, "Order " + ddId, intent.getAmount(), intent.getCurrency(), oneTimeKey);
|
|
|
@@ -131,6 +143,9 @@ public class LinePayOfflineService {
|
|
|
() -> linePayClient.payOffline(gatewayCredential, request));
|
|
|
return handlePayResponse(intent, response);
|
|
|
} catch (Exception unknown) {
|
|
|
+ // 网关调用结果未知(超时/断网):异常在此被吞并转 REQUEST_UNKNOWN,无日志则完全无法事后排查
|
|
|
+ log.error("LINE Pay offline gateway pay ended unknown, ddId={}, paymentId={}, lineOrderId={}",
|
|
|
+ ddId, intent.getId(), intent.getLineOrderId(), unknown);
|
|
|
int changed = paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
|
|
|
shortly(), unknownDeadline());
|
|
|
if (changed != 1) {
|
|
|
@@ -162,12 +177,17 @@ public class LinePayOfflineService {
|
|
|
}
|
|
|
PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
|
|
|
if (credential == null) {
|
|
|
+ // 凭证被删后查单无从发起,静默转人工会让问题不可见
|
|
|
+ log.warn("LINE Pay offline credential gone to manual review, ddId={}, paymentId={}, credentialId={}",
|
|
|
+ payment.getDdId(), payment.getId(), payment.getCredentialId());
|
|
|
int changed = paymentService.markManualReview(
|
|
|
payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
}
|
|
|
boolean expired = payment.getReconcileDeadline() != null
|
|
|
&& !new Date().before(payment.getReconcileDeadline());
|
|
|
+ log.info("LINE Pay offline reconcile start, source={}, paymentId={}, ddId={}, status={}, expired={}",
|
|
|
+ source, paymentId, payment.getDdId(), payment.getStatus(), expired);
|
|
|
try {
|
|
|
LinePayCredential gatewayCredential = toCredential(credential);
|
|
|
LinePayResponse response = auditService == null
|
|
|
@@ -178,6 +198,9 @@ public class LinePayOfflineService {
|
|
|
() -> linePayClient.checkOffline(gatewayCredential, payment.getLineOrderId()));
|
|
|
return handleCheckResponse(payment, response, expired);
|
|
|
} catch (Exception unknown) {
|
|
|
+ // 查单异常被吞并改期/转人工,不记录则对账停摆无从发现
|
|
|
+ log.error("LINE Pay offline reconcile query failed, source={}, paymentId={}, ddId={}, expired={}",
|
|
|
+ source, paymentId, payment.getDdId(), expired, unknown);
|
|
|
if (expired) {
|
|
|
int changed = paymentService.markManualReview(
|
|
|
payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
@@ -202,6 +225,8 @@ public class LinePayOfflineService {
|
|
|
payment.setTransactionId(response.transactionId());
|
|
|
payment.setStatus("WAITING_AUTH");
|
|
|
payment.setUpdateTime(new Date());
|
|
|
+ log.info("LINE Pay offline pay waiting auth, ddId={}, paymentId={}, returnCode={}, transactionId={}",
|
|
|
+ payment.getDdId(), payment.getId(), response.returnCode(), response.transactionId());
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
if (response != null && response.isSuccess()) {
|
|
|
@@ -216,6 +241,9 @@ public class LinePayOfflineService {
|
|
|
payment.setStatus("FAILED");
|
|
|
payment.setActiveDdId(null);
|
|
|
payment.setUpdateTime(new Date());
|
|
|
+ log.warn("LINE Pay offline pay definite failure, ddId={}, paymentId={}, httpStatus={}, returnCode={}, returnMessage={}",
|
|
|
+ payment.getDdId(), payment.getId(), response.httpStatus(), response.returnCode(),
|
|
|
+ response.returnMessage());
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
int changed = paymentService.markRequestUnknown(payment.getId(), payment.getVersion(),
|
|
|
@@ -225,6 +253,9 @@ public class LinePayOfflineService {
|
|
|
}
|
|
|
payment.setStatus("REQUEST_UNKNOWN");
|
|
|
payment.setUpdateTime(new Date());
|
|
|
+ log.warn("LINE Pay offline pay result uncertain, ddId={}, paymentId={}, httpStatus={}, returnCode={}, returnMessage={}",
|
|
|
+ payment.getDdId(), payment.getId(), response.httpStatus(), response.returnCode(),
|
|
|
+ response.returnMessage());
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
|
|
|
@@ -242,10 +273,19 @@ public class LinePayOfflineService {
|
|
|
if (expired) {
|
|
|
int changed = paymentService.markManualReview(
|
|
|
payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
+ if (changed == 1) {
|
|
|
+ log.warn("LINE Pay offline auth expired to manual review, ddId={}, paymentId={}",
|
|
|
+ payment.getDdId(), payment.getId());
|
|
|
+ }
|
|
|
return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
}
|
|
|
int changed = paymentService.markOfflineWaitingAuth(payment.getId(), payment.getVersion(),
|
|
|
response.transactionId(), shortly(), waitingAuthDeadline(payment));
|
|
|
+ if (changed == 1) {
|
|
|
+ // 与 rescheduled 同属对账常态路径(60 秒一轮),debug 避免刷屏
|
|
|
+ log.debug("LINE Pay offline reconcile still waiting auth, ddId={}, paymentId={}, gatewayStatus={}",
|
|
|
+ payment.getDdId(), payment.getId(), gatewayStatus);
|
|
|
+ }
|
|
|
return changed == 1 ? "WAITING_AUTH" : reload(payment).getStatus();
|
|
|
}
|
|
|
if ("COMPLETE".equals(gatewayStatus)) {
|
|
|
@@ -254,11 +294,19 @@ public class LinePayOfflineService {
|
|
|
if ("CANCEL".equals(gatewayStatus)) {
|
|
|
int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
|
|
|
payment.getStatus(), "CANCELLED_OR_EXPIRED");
|
|
|
+ if (changed == 1) {
|
|
|
+ log.info("LINE Pay offline payment cancelled at gateway, ddId={}, paymentId={}, transactionId={}",
|
|
|
+ payment.getDdId(), payment.getId(), response.transactionId());
|
|
|
+ }
|
|
|
return changed == 1 ? "CANCELLED_OR_EXPIRED" : reload(payment).getStatus();
|
|
|
}
|
|
|
if ("FAIL".equals(gatewayStatus)) {
|
|
|
int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
|
|
|
payment.getStatus(), "FAILED");
|
|
|
+ if (changed == 1) {
|
|
|
+ log.warn("LINE Pay offline payment failed at gateway, ddId={}, paymentId={}, transactionId={}",
|
|
|
+ payment.getDdId(), payment.getId(), response.transactionId());
|
|
|
+ }
|
|
|
return changed == 1 ? "FAILED" : reload(payment).getStatus();
|
|
|
}
|
|
|
return rescheduleOrReview(payment, expired);
|
|
|
@@ -278,6 +326,8 @@ public class LinePayOfflineService {
|
|
|
return result(reload(payment), false);
|
|
|
}
|
|
|
payment.setStatus("MANUAL_REVIEW");
|
|
|
+ log.warn("LINE Pay offline complete without transactionId, ddId={}, paymentId={}, lineOrderId={}",
|
|
|
+ payment.getDdId(), payment.getId(), payment.getLineOrderId());
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
if (!payment.getLineOrderId().equals(orderId) || actualAmount == null
|
|
|
@@ -301,6 +351,9 @@ public class LinePayOfflineService {
|
|
|
payment.setPaymentProvider(provider);
|
|
|
payment.setCapturedAmount(actualAmount);
|
|
|
payment.setStatus("MANUAL_REVIEW");
|
|
|
+ log.warn("LINE Pay offline evidence mismatch to manual review, ddId={}, paymentId={}, lineOrderId={}, gatewayOrderId={}, captured={}, transactionId={}",
|
|
|
+ payment.getDdId(), payment.getId(), payment.getLineOrderId(), orderId,
|
|
|
+ actualAmount, transactionId);
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
if (!payment.getAmount().equals(actualAmount)) {
|
|
|
@@ -314,6 +367,8 @@ public class LinePayOfflineService {
|
|
|
payment.setPaymentProvider(provider);
|
|
|
payment.setCapturedAmount(actualAmount);
|
|
|
payment.setStatus("AMOUNT_MISMATCH");
|
|
|
+ log.warn("LINE Pay offline amount mismatch, ddId={}, paymentId={}, expected={}, captured={}, transactionId={}",
|
|
|
+ payment.getDdId(), payment.getId(), payment.getAmount(), actualAmount, transactionId);
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
factService.applyPaidFact(payment.getId(), transactionId, provider, new Date());
|
|
|
@@ -322,6 +377,8 @@ public class LinePayOfflineService {
|
|
|
payment.setCapturedAmount(payment.getAmount());
|
|
|
payment.setStatus("PAID");
|
|
|
payment.setUpdateTime(new Date());
|
|
|
+ log.info("LINE Pay offline payment PAID, ddId={}, paymentId={}, transactionId={}, provider={}, amount={}",
|
|
|
+ payment.getDdId(), payment.getId(), transactionId, provider, payment.getAmount());
|
|
|
return result(payment, false);
|
|
|
}
|
|
|
|
|
|
@@ -329,12 +386,21 @@ public class LinePayOfflineService {
|
|
|
if (expired) {
|
|
|
int changed = paymentService.markManualReview(
|
|
|
payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
+ if (changed == 1) {
|
|
|
+ log.warn("LINE Pay offline reconcile expired to manual review, ddId={}, paymentId={}, status={}",
|
|
|
+ payment.getDdId(), payment.getId(), payment.getStatus());
|
|
|
+ }
|
|
|
return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
}
|
|
|
int changed = paymentService.rescheduleOffline(
|
|
|
payment.getId(), payment.getVersion(), payment.getStatus(),
|
|
|
shortly(), payment.getReconcileDeadline() == null
|
|
|
? unknownDeadline() : payment.getReconcileDeadline());
|
|
|
+ if (changed == 1) {
|
|
|
+ // 改期是常态路径(每分钟对账一轮都会触发),放 debug 避免刷屏
|
|
|
+ log.debug("LINE Pay offline reconcile rescheduled, ddId={}, paymentId={}, status={}",
|
|
|
+ payment.getDdId(), payment.getId(), payment.getStatus());
|
|
|
+ }
|
|
|
return changed == 1 ? payment.getStatus() : reload(payment).getStatus();
|
|
|
}
|
|
|
|
|
|
@@ -345,6 +411,8 @@ public class LinePayOfflineService {
|
|
|
newLineOrderId(), credential.getId(), order.getMdId(), order.getAmount(),
|
|
|
"TWD", MODE_OFFLINE, unknownDeadline()), true);
|
|
|
} catch (DuplicateKeyException duplicate) {
|
|
|
+ // 活跃流水唯一键冲突 = 并发扫码,后到者复用先到者流水
|
|
|
+ log.info("LINE Pay offline intent duplicate key, ddId={}, attempt={}", order.getDdId(), attempt);
|
|
|
PosOrderLinePayment active = paymentService.getActiveByDdId(
|
|
|
String.valueOf(order.getDdId()));
|
|
|
if (active != null) {
|
|
|
@@ -463,6 +531,14 @@ public class LinePayOfflineService {
|
|
|
credential.getChannelSecret());
|
|
|
}
|
|
|
|
|
|
+ /** 付款码掩码:仅保留后 4 位;完整 oneTimeKey 属一次性凭证,禁止写入日志。 */
|
|
|
+ private static String maskMyCode(String oneTimeKey) {
|
|
|
+ if (oneTimeKey == null || oneTimeKey.length() < 4) {
|
|
|
+ return "****";
|
|
|
+ }
|
|
|
+ return "****" + oneTimeKey.substring(oneTimeKey.length() - 4);
|
|
|
+ }
|
|
|
+
|
|
|
private static String normalizeDdId(String ddId) {
|
|
|
if (ddId == null || ddId.trim().isEmpty()) {
|
|
|
throw new ServiceException(MessageUtils.message("line.pay.order.required"));
|