| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- package com.ruoyi.app.omgpay;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.ruoyi.app.order.DeliveryOrderNotificationService;
- import com.ruoyi.app.order.MerchantNotificationRouter;
- import com.ruoyi.app.order.dto.OrderPushBodyDto;
- import com.ruoyi.app.omgpay.dto.OmgNotifyRequest;
- import com.ruoyi.system.domain.PosOrder;
- import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
- import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
- import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
- import com.ruoyi.system.service.IPosOrderService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.slf4j.Logger;
- 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;
- import java.time.ZoneId;
- import java.time.format.DateTimeFormatter;
- import java.time.format.DateTimeParseException;
- import java.util.Date;
- import java.util.List;
- /** Verifies and applies one final OMG payment notification transactionally. */
- @Service
- public class OmgPaymentNotifyService {
- private static final Logger log = LoggerFactory.getLogger(OmgPaymentNotifyService.class);
- private static final int STATUS_PAID = 1;
- private static final int STATUS_SUPERSEDED = 3;
- private static final DateTimeFormatter OMG_DATE = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
- private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
- private static final List<String> REQUIRED_FIELDS = List.of(
- "MerchantID", "MerchantTradeNo", "StoreID", "RtnCode", "RtnMsg", "TradeNo", "TradeAmt",
- "PaymentDate", "PaymentType", "PaymentTypeChargeFee", "TradeDate", "SimulatePaid",
- "CustomField1", "CustomField2", "CustomField3", "CustomField4", "CheckMacValue");
- private final IOmgPaymentAttemptService attempts;
- private final OmgCheckMacSigner signer;
- @Autowired(required = false)
- private IPosOrderService posOrderService;
- @Autowired(required = false)
- private DeliveryOrderNotificationService deliveryOrderNotificationService;
- @Autowired(required = false)
- private MerchantNotificationRouter merchantNotificationRouter;
- public OmgPaymentNotifyService(IOmgPaymentAttemptService attempts, OmgCheckMacSigner signer) {
- this.attempts = attempts;
- this.signer = signer;
- }
- @Transactional(rollbackFor = Exception.class)
- public boolean process(OmgNotifyRequest request) {
- if (request == null || !request.isValid() || !hasRequiredFields(request)) {
- log.warn("OMG notify rejected reason=malformed_or_missing_fields");
- return false;
- }
- String merchantTradeNo = request.value("MerchantTradeNo");
- if (isBlank(merchantTradeNo) || merchantTradeNo.length() > 20) {
- log.warn("OMG notify rejected reason=invalid_merchant_trade_no");
- return false;
- }
- OmgPaymentAttempt discovered = attempts.selectByMerchantTradeNo(merchantTradeNo);
- if (discovered == null) {
- log.warn("OMG notify rejected merchantTradeNo={} reason=attempt_not_found", merchantTradeNo);
- return false;
- }
- if (factsDoNotIdentifySameAttempt(request, discovered)) {
- log.warn("OMG notify rejected merchantTradeNo={} reason=prelock_fact_mismatch", merchantTradeNo);
- return false;
- }
- OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(discovered.getDdId());
- OmgPaymentAttempt attempt = attempts.selectByMerchantTradeNoForUpdate(merchantTradeNo);
- if (attempt == null || !verifyTrust(request, attempt)) {
- log.warn("OMG notify rejected merchantTradeNo={} reason=trust_validation_failed", merchantTradeNo);
- return false;
- }
- ParsedFacts facts;
- try {
- facts = parseFacts(request);
- } catch (IllegalArgumentException exception) {
- log.warn("OMG notify rejected merchantTradeNo={} reason=invalid_gateway_facts", merchantTradeNo);
- return false;
- }
- 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 {} 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 {} 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 source={}, merchantTradeNo={}, tradeNo={}, code={}, message={}",
- facts.source(), attempt.getMerchantTradeNo(), facts.tradeNo(),
- facts.resultCode(), facts.resultMessage());
- return OmgPaymentSettlementResult.FAILED;
- }
- if (order == null) {
- 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())) {
- throw new IllegalStateException("OMG paid fact conflicts with stored TradeNo");
- }
- 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");
- attempts.supersedeOtherCreated(attempt.getDdId(), attempt.getId());
- if (order.getPayStatus() == null || order.getPayStatus() != 1L) {
- requireSingleUpdate(attempts.markOrderPaid(attempt.getDdId()), "mark order paid");
- openDeliveryOrderToRiders(attempt.getDdId(), order.getState());
- }
- int otherPaid = attempts.countOtherPaidAttempts(attempt.getDdId(), attempt.getId());
- if (order.getState() != null && order.getState() == 4L) {
- 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 source={}, orderId={}, merchantTradeNo={}, "
- + "tradeNo={}, otherPaidCount={}",
- facts.source(), attempt.getDdId(), attempt.getMerchantTradeNo(),
- facts.tradeNo(), otherPaid);
- }
- 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 void openDeliveryOrderToRiders(String ddId, Long state) {
- if (Long.valueOf(4L).equals(state) || posOrderService == null
- || (deliveryOrderNotificationService == null && merchantNotificationRouter == null)) {
- return;
- }
- PosOrder paidOrder = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>()
- .eq(PosOrder::getDdId, ddId));
- if (paidOrder == null) {
- return;
- }
- paidOrder.setPayStatus(1L);
- if (Long.valueOf(0L).equals(paidOrder.getType()) && deliveryOrderNotificationService != null) {
- deliveryOrderNotificationService.notifyOrderAvailable(paidOrder);
- } else if (merchantNotificationRouter != null) {
- String orderNo = String.valueOf(paidOrder.getDdId());
- String body = OrderPushBodyDto.getJson(orderNo, String.valueOf(paidOrder.getState()), 0);
- merchantNotificationRouter.sendStoreNotification(paidOrder.getMdId(), paidOrder.getShId(),
- "no.message.push.message", "no.message.push.new.order", body, orderNo);
- }
- }
- private boolean verifyTrust(OmgNotifyRequest request, OmgPaymentAttempt attempt) {
- if (!request.value("MerchantID").equals(attempt.getMerchantId())) {
- return false;
- }
- Integer amount = parseInteger(request.value("TradeAmt"));
- if (amount == null || !amount.equals(attempt.getAmount())) {
- return false;
- }
- String actual = request.value("CheckMacValue");
- if (actual == null || !actual.matches("(?i)[0-9a-f]{64}")) {
- return false;
- }
- String expected = signer.sign(request.signingFields(),
- attempt.getHashKeySnapshot(), attempt.getHashIvSnapshot());
- return secureEquals(expected, actual);
- }
- private static boolean factsDoNotIdentifySameAttempt(OmgNotifyRequest request, OmgPaymentAttempt attempt) {
- Integer amount = parseInteger(request.value("TradeAmt"));
- return !request.value("MerchantID").equals(attempt.getMerchantId())
- || amount == null || !amount.equals(attempt.getAmount());
- }
- private static boolean hasRequiredFields(OmgNotifyRequest request) {
- return REQUIRED_FIELDS.stream().allMatch(request::contains);
- }
- private static ParsedFacts parseFacts(OmgNotifyRequest request) {
- Integer rtnCode = requiredInteger(request.value("RtnCode"));
- BigDecimal fee = requiredDecimal(request.value("PaymentTypeChargeFee"));
- if (fee.signum() < 0) {
- throw new IllegalArgumentException("invalid PaymentTypeChargeFee");
- }
- Integer simulatePaid = requiredInteger(request.value("SimulatePaid"));
- if (simulatePaid != 0 && simulatePaid != 1) {
- throw new IllegalArgumentException("invalid SimulatePaid");
- }
- String tradeNo = request.value("TradeNo");
- if ((rtnCode == 1 && isBlank(tradeNo)) || (tradeNo != null && tradeNo.length() > 20)) {
- throw new IllegalArgumentException("invalid TradeNo");
- }
- if (isBlank(tradeNo)) {
- tradeNo = null;
- }
- String paymentType = request.value("PaymentType");
- if (isBlank(paymentType) || paymentType.length() > 20) {
- throw new IllegalArgumentException("invalid PaymentType");
- }
- Date paymentDate = parseDate(request.value("PaymentDate"), rtnCode == 1);
- Date tradeDate = parseDate(request.value("TradeDate"), true);
- String rtnMsg = request.value("RtnMsg");
- if (rtnMsg == null || rtnMsg.length() > 200) {
- throw new IllegalArgumentException("invalid RtnMsg");
- }
- return new ParsedFacts(rtnCode, rtnMsg, tradeNo, paymentType,
- paymentDate, tradeDate, fee, simulatePaid);
- }
- private static OmgPaymentAttempt toUpdate(OmgPaymentAttempt attempt, OmgPaymentGatewayFacts facts) {
- OmgPaymentAttempt update = new OmgPaymentAttempt();
- update.setId(attempt.getId());
- 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) {
- throw new IllegalArgumentException("required date missing");
- }
- return null;
- }
- try {
- return Date.from(LocalDateTime.parse(value, OMG_DATE).atZone(TAIPEI).toInstant());
- } catch (DateTimeParseException exception) {
- throw new IllegalArgumentException("invalid date", exception);
- }
- }
- private static Integer requiredInteger(String value) {
- Integer parsed = parseInteger(value);
- if (parsed == null) {
- throw new IllegalArgumentException("invalid integer");
- }
- 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);
- } catch (NumberFormatException exception) {
- return null;
- }
- }
- private static void requireSingleUpdate(int count, String action) {
- if (count != 1) {
- throw new IllegalStateException("OMG notify failed to " + action);
- }
- }
- private static boolean isBlank(String value) {
- return value == null || value.isBlank();
- }
- private static String limit(String value, int length) {
- return value == null || value.length() <= length ? value : value.substring(0, length);
- }
- private record ParsedFacts(int rtnCode, String rtnMsg, String tradeNo, String paymentType,
- Date paymentDate, Date tradeDate,
- BigDecimal paymentTypeChargeFee, int simulatePaid) {
- }
- }
|