|
|
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.security.MessageDigest;
|
|
|
import java.time.LocalDateTime;
|
|
|
@@ -74,35 +75,67 @@ public class OmgPaymentNotifyService {
|
|
|
log.warn("OMG notify rejected merchantTradeNo={} reason=invalid_gateway_facts", merchantTradeNo);
|
|
|
return false;
|
|
|
}
|
|
|
- if (facts.rtnCode != 1) {
|
|
|
+ OmgPaymentGatewayFacts gatewayFacts = new OmgPaymentGatewayFacts(
|
|
|
+ OmgPaymentFactSource.NOTIFY, attempt.getMerchantId(), merchantTradeNo,
|
|
|
+ attempt.getAmount(), facts.rtnCode, facts.rtnMsg, facts.tradeNo,
|
|
|
+ facts.paymentType, facts.paymentDate, facts.tradeDate,
|
|
|
+ facts.paymentTypeChargeFee, facts.simulatePaid);
|
|
|
+ return applyVerifiedFacts(order, attempt, gatewayFacts) != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Applies a signed QueryTradeInfo result so a lost callback cannot leave a paid order unpaid.
|
|
|
+ * The row locks and irreversible state rules are identical to the callback path.
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public OmgPaymentSettlementResult synchronizeVerifiedQuery(OmgPaymentGatewayFacts facts) {
|
|
|
+ if (facts == null || facts.source() != OmgPaymentFactSource.QUERY) {
|
|
|
+ throw new IllegalArgumentException("verified OMG query facts are required");
|
|
|
+ }
|
|
|
+ OmgPaymentAttempt discovered = attempts.selectByMerchantTradeNo(facts.merchantTradeNo());
|
|
|
+ if (discovered == null || !sameIdentity(facts, discovered)) {
|
|
|
+ throw new IllegalArgumentException("OMG query facts do not identify an attempt");
|
|
|
+ }
|
|
|
+ OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(discovered.getDdId());
|
|
|
+ OmgPaymentAttempt attempt = attempts.selectByMerchantTradeNoForUpdate(facts.merchantTradeNo());
|
|
|
+ if (attempt == null || !sameIdentity(facts, attempt)) {
|
|
|
+ throw new IllegalArgumentException("OMG query attempt changed while locking");
|
|
|
+ }
|
|
|
+ return applyVerifiedFacts(order, attempt, facts);
|
|
|
+ }
|
|
|
+
|
|
|
+ private OmgPaymentSettlementResult applyVerifiedFacts(OmgPaymentOrderSnapshot order,
|
|
|
+ OmgPaymentAttempt attempt,
|
|
|
+ OmgPaymentGatewayFacts facts) {
|
|
|
+ boolean paid = facts.resultCode() == 1;
|
|
|
+ if (!paid) {
|
|
|
if (attempt.getAttemptStatus() != null && attempt.getAttemptStatus() == STATUS_PAID) {
|
|
|
- log.warn("OMG notify ignored late failure merchantTradeNo={}, rtnCode={}, rtnMsg={}",
|
|
|
- merchantTradeNo, facts.rtnCode, facts.rtnMsg);
|
|
|
- return true;
|
|
|
+ log.warn("OMG {} ignored late failure merchantTradeNo={}, code={}, message={}",
|
|
|
+ facts.source(), attempt.getMerchantTradeNo(), facts.resultCode(), facts.resultMessage());
|
|
|
+ return OmgPaymentSettlementResult.PAID;
|
|
|
}
|
|
|
if (attempt.getAttemptStatus() != null && attempt.getAttemptStatus() == STATUS_SUPERSEDED) {
|
|
|
- log.warn("OMG notify accepted failure for superseded attempt merchantTradeNo={}, "
|
|
|
- + "rtnCode={}, rtnMsg={}", merchantTradeNo, facts.rtnCode, facts.rtnMsg);
|
|
|
- return true;
|
|
|
+ log.warn("OMG {} accepted failure for superseded attempt merchantTradeNo={}, code={}",
|
|
|
+ facts.source(), attempt.getMerchantTradeNo(), facts.resultCode());
|
|
|
+ return OmgPaymentSettlementResult.FAILED;
|
|
|
}
|
|
|
requireSingleUpdate(attempts.markFailed(toUpdate(attempt, facts)), "mark failed");
|
|
|
- log.warn("OMG payment failed merchantTradeNo={}, tradeNo={}, rtnCode={}, rtnMsg={}",
|
|
|
- merchantTradeNo, facts.tradeNo, facts.rtnCode, facts.rtnMsg);
|
|
|
- return true;
|
|
|
+ log.warn("OMG payment failed source={}, merchantTradeNo={}, tradeNo={}, code={}, message={}",
|
|
|
+ facts.source(), attempt.getMerchantTradeNo(), facts.tradeNo(),
|
|
|
+ facts.resultCode(), facts.resultMessage());
|
|
|
+ return OmgPaymentSettlementResult.FAILED;
|
|
|
}
|
|
|
|
|
|
if (order == null) {
|
|
|
- log.error("OMG paid notify rejected merchantTradeNo={} reason=order_not_found", merchantTradeNo);
|
|
|
- return false;
|
|
|
+ throw new IllegalStateException("OMG paid fact has no order");
|
|
|
}
|
|
|
if (attempt.getAttemptStatus() != null && attempt.getAttemptStatus() == STATUS_PAID) {
|
|
|
- if (attempt.getTradeNo() != null && !attempt.getTradeNo().equals(facts.tradeNo)) {
|
|
|
- log.error("OMG paid duplicate conflict merchantTradeNo={}, storedTradeNo={}, callbackTradeNo={}",
|
|
|
- merchantTradeNo, attempt.getTradeNo(), facts.tradeNo);
|
|
|
- return false;
|
|
|
+ if (attempt.getTradeNo() != null && !attempt.getTradeNo().equals(facts.tradeNo())) {
|
|
|
+ throw new IllegalStateException("OMG paid fact conflicts with stored TradeNo");
|
|
|
}
|
|
|
- log.info("OMG paid duplicate accepted merchantTradeNo={}, tradeNo={}", merchantTradeNo, facts.tradeNo);
|
|
|
- return true;
|
|
|
+ log.info("OMG paid duplicate accepted source={}, merchantTradeNo={}, tradeNo={}",
|
|
|
+ facts.source(), attempt.getMerchantTradeNo(), facts.tradeNo());
|
|
|
+ return OmgPaymentSettlementResult.PAID;
|
|
|
}
|
|
|
|
|
|
requireSingleUpdate(attempts.markPaid(toUpdate(attempt, facts)), "mark paid");
|
|
|
@@ -112,16 +145,19 @@ public class OmgPaymentNotifyService {
|
|
|
}
|
|
|
int otherPaid = attempts.countOtherPaidAttempts(attempt.getDdId(), attempt.getId());
|
|
|
if (order.getState() != null && order.getState() == 4L) {
|
|
|
- log.error("OMG paid after order cancellation orderId={}, merchantTradeNo={}, tradeNo={}, simulatePaid={}",
|
|
|
- attempt.getDdId(), merchantTradeNo, facts.tradeNo, facts.simulatePaid);
|
|
|
+ log.error("OMG paid after order cancellation source={}, orderId={}, merchantTradeNo={}, tradeNo={}",
|
|
|
+ facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(), facts.tradeNo());
|
|
|
}
|
|
|
if (otherPaid > 0) {
|
|
|
- log.error("OMG multiple paid attempts orderId={}, merchantTradeNo={}, tradeNo={}, otherPaidCount={}",
|
|
|
- attempt.getDdId(), merchantTradeNo, facts.tradeNo, otherPaid);
|
|
|
+ log.error("OMG multiple paid attempts source={}, orderId={}, merchantTradeNo={}, "
|
|
|
+ + "tradeNo={}, otherPaidCount={}",
|
|
|
+ facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(),
|
|
|
+ facts.tradeNo(), otherPaid);
|
|
|
}
|
|
|
- log.info("OMG payment marked paid orderId={}, merchantTradeNo={}, tradeNo={}, amount={}, simulatePaid={}",
|
|
|
- attempt.getDdId(), merchantTradeNo, facts.tradeNo, attempt.getAmount(), facts.simulatePaid);
|
|
|
- return true;
|
|
|
+ log.info("OMG payment marked paid source={}, orderId={}, merchantTradeNo={}, tradeNo={}, amount={}",
|
|
|
+ facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(),
|
|
|
+ facts.tradeNo(), attempt.getAmount());
|
|
|
+ return OmgPaymentSettlementResult.PAID;
|
|
|
}
|
|
|
|
|
|
private boolean verifyTrust(OmgNotifyRequest request, OmgPaymentAttempt attempt) {
|
|
|
@@ -138,8 +174,7 @@ public class OmgPaymentNotifyService {
|
|
|
}
|
|
|
String expected = signer.sign(request.signingFields(),
|
|
|
attempt.getHashKeySnapshot(), attempt.getHashIvSnapshot());
|
|
|
- return MessageDigest.isEqual(expected.getBytes(StandardCharsets.US_ASCII),
|
|
|
- actual.toUpperCase().getBytes(StandardCharsets.US_ASCII));
|
|
|
+ return secureEquals(expected, actual);
|
|
|
}
|
|
|
|
|
|
private static boolean factsDoNotIdentifySameAttempt(OmgNotifyRequest request, OmgPaymentAttempt attempt) {
|
|
|
@@ -154,8 +189,8 @@ public class OmgPaymentNotifyService {
|
|
|
|
|
|
private static ParsedFacts parseFacts(OmgNotifyRequest request) {
|
|
|
Integer rtnCode = requiredInteger(request.value("RtnCode"));
|
|
|
- Integer fee = requiredInteger(request.value("PaymentTypeChargeFee"));
|
|
|
- if (fee < 0) {
|
|
|
+ BigDecimal fee = requiredDecimal(request.value("PaymentTypeChargeFee"));
|
|
|
+ if (fee.signum() < 0) {
|
|
|
throw new IllegalArgumentException("invalid PaymentTypeChargeFee");
|
|
|
}
|
|
|
Integer simulatePaid = requiredInteger(request.value("SimulatePaid"));
|
|
|
@@ -183,22 +218,34 @@ public class OmgPaymentNotifyService {
|
|
|
paymentDate, tradeDate, fee, simulatePaid);
|
|
|
}
|
|
|
|
|
|
- private static OmgPaymentAttempt toUpdate(OmgPaymentAttempt attempt, ParsedFacts facts) {
|
|
|
+ private static OmgPaymentAttempt toUpdate(OmgPaymentAttempt attempt, OmgPaymentGatewayFacts facts) {
|
|
|
OmgPaymentAttempt update = new OmgPaymentAttempt();
|
|
|
update.setId(attempt.getId());
|
|
|
- update.setTradeNo(facts.tradeNo);
|
|
|
- update.setRtnCode(facts.rtnCode);
|
|
|
- update.setRtnMsg(limit(facts.rtnMsg, 200));
|
|
|
- update.setPaymentType(limit(facts.paymentType, 20));
|
|
|
- update.setPaymentDate(facts.paymentDate);
|
|
|
- update.setTradeDate(facts.tradeDate);
|
|
|
- update.setPaymentTypeChargeFee(facts.paymentTypeChargeFee);
|
|
|
- update.setSimulatePaid(facts.simulatePaid);
|
|
|
+ update.setTradeNo(facts.tradeNo());
|
|
|
+ update.setRtnCode(facts.resultCode());
|
|
|
+ update.setRtnMsg(limit(facts.resultMessage(), 200));
|
|
|
+ update.setPaymentType(limit(facts.paymentType(), 20));
|
|
|
+ update.setPaymentDate(facts.paymentDate());
|
|
|
+ update.setTradeDate(facts.tradeDate());
|
|
|
+ update.setPaymentTypeChargeFee(facts.paymentTypeChargeFee());
|
|
|
+ update.setSimulatePaid(facts.simulatePaid());
|
|
|
update.setLastNotifyTime(new Date());
|
|
|
update.setUpdateTime(new Date());
|
|
|
return update;
|
|
|
}
|
|
|
|
|
|
+ private static boolean sameIdentity(OmgPaymentGatewayFacts facts, OmgPaymentAttempt attempt) {
|
|
|
+ return facts.merchantTradeNo().equals(attempt.getMerchantTradeNo())
|
|
|
+ && facts.merchantId().equals(attempt.getMerchantId())
|
|
|
+ && facts.amount() == attempt.getAmount();
|
|
|
+ }
|
|
|
+
|
|
|
+ static boolean secureEquals(String expected, String actual) {
|
|
|
+ return expected != null && actual != null
|
|
|
+ && MessageDigest.isEqual(expected.toUpperCase().getBytes(StandardCharsets.US_ASCII),
|
|
|
+ actual.toUpperCase().getBytes(StandardCharsets.US_ASCII));
|
|
|
+ }
|
|
|
+
|
|
|
private static Date parseDate(String value, boolean required) {
|
|
|
if (isBlank(value)) {
|
|
|
if (required) {
|
|
|
@@ -221,6 +268,17 @@ public class OmgPaymentNotifyService {
|
|
|
return parsed;
|
|
|
}
|
|
|
|
|
|
+ private static BigDecimal requiredDecimal(String value) {
|
|
|
+ if (isBlank(value)) {
|
|
|
+ throw new IllegalArgumentException("invalid decimal");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return new BigDecimal(value);
|
|
|
+ } catch (NumberFormatException exception) {
|
|
|
+ throw new IllegalArgumentException("invalid decimal", exception);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private static Integer parseInteger(String value) {
|
|
|
try {
|
|
|
return isBlank(value) ? null : Integer.valueOf(value);
|
|
|
@@ -244,6 +302,7 @@ public class OmgPaymentNotifyService {
|
|
|
}
|
|
|
|
|
|
private record ParsedFacts(int rtnCode, String rtnMsg, String tradeNo, String paymentType,
|
|
|
- Date paymentDate, Date tradeDate, int paymentTypeChargeFee, int simulatePaid) {
|
|
|
+ Date paymentDate, Date tradeDate,
|
|
|
+ BigDecimal paymentTypeChargeFee, int simulatePaid) {
|
|
|
}
|
|
|
}
|