|
|
@@ -0,0 +1,175 @@
|
|
|
+package com.ruoyi.app.flashdelivery.payment;
|
|
|
+import com.ruoyi.system.domain.flash.*;
|
|
|
+import com.ruoyi.system.mapper.flash.*;
|
|
|
+import com.ruoyi.system.service.IInfoRiderLinePayService;
|
|
|
+import com.ruoyi.system.domain.InfoRiderLinePay;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
+import com.ruoyi.common.exception.ServiceException;
|
|
|
+import com.ruoyi.common.utils.MessageUtils;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import java.util.*;
|
|
|
+/** 短事务资金事实;锁序固定为订单→支付→退款,任何 HTTP 都在调用方事务外。 */
|
|
|
+@Service
|
|
|
+public class FlashLinePayFactService {
|
|
|
+ private final FlashDeliveryOrderMapper orders;
|
|
|
+ private final FlashDeliveryLinePaymentMapper payments;
|
|
|
+ private final FlashDeliveryLineRefundMapper refunds;
|
|
|
+ private final IInfoRiderLinePayService credentials;
|
|
|
+ @org.springframework.beans.factory.annotation.Value("${line-pay.reconcile.row-lease-seconds:120}") private long rowLeaseSeconds=120;
|
|
|
+ @org.springframework.beans.factory.annotation.Value("${line-pay.reconcile.auth-deadline-minutes:30}") private long authDeadlineMinutes=30;
|
|
|
+ @org.springframework.beans.factory.annotation.Value("${line-pay.reconcile.unknown-deadline-hours:24}") private long unknownDeadlineHours=24;
|
|
|
+ public FlashLinePayFactService(FlashDeliveryOrderMapper o,FlashDeliveryLinePaymentMapper p,FlashDeliveryLineRefundMapper r,IInfoRiderLinePayService c){orders=o;payments=p;refunds=r;credentials=c;}
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public FlashDeliveryLinePayment createIntent(Long userId,Long orderId){
|
|
|
+ FlashDeliveryOrder order=orders.selectByIdForUpdate(orderId);
|
|
|
+ if(order==null || !Objects.equals(userId,order.getUserId())) throw fail("not.found");
|
|
|
+ if(!"ACCEPTED".equals(order.getStatus()) || order.getRiderId()==null || !"3".equals(order.getPayType())
|
|
|
+ || order.getPickedUpAt()!=null || order.getPickedUpStopCount()!=null && order.getPickedUpStopCount()>0) throw fail("accept.required");
|
|
|
+ if(order.getAmount()==null || order.getAmount()<=0 || order.getAmount()>Integer.MAX_VALUE || !"TWD".equals(order.getCurrency())) throw fail("amount.invalid");
|
|
|
+ FlashDeliveryLinePayment previous=payments.selectLatestForUpdate(orderId);
|
|
|
+ // 未知资金结果不释放活跃键;禁止利用网络超时发起第二次扣款。
|
|
|
+ if(previous!=null && (previous.getActiveOrderId()!=null || "PAID".equals(previous.getStatus()))) return previous;
|
|
|
+ InfoRiderLinePay credential=credentials.getEnabledCurrent(order.getRiderId());
|
|
|
+ if(credential==null) throw fail("config.invalid");
|
|
|
+ Date now=new Date(); FlashDeliveryLinePayment row=new FlashDeliveryLinePayment();
|
|
|
+ row.setOrderId(orderId);row.setRiderId(order.getRiderId());row.setCredentialId(credential.getId());
|
|
|
+ row.setLineOrderId("FLP"+UUID.randomUUID().toString().replace("-",""));row.setAmount(Math.toIntExact(order.getAmount()));row.setCurrency("TWD");
|
|
|
+ row.setStatus("CREATED");row.setActiveOrderId(orderId);row.setVersion(0L);row.setCreateTime(now);row.setUpdateTime(now);
|
|
|
+ row.setNextReconcileAt(now);row.setReconcileDeadline(new Date(now.getTime()+Math.max(1,unknownDeadlineHours)*60*60*1000L));
|
|
|
+ if(payments.insert(row)!=1) throw fail("unknown");return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public FlashDeliveryLinePayment claim(Long id,String expected,String next){
|
|
|
+ FlashDeliveryLinePayment preliminary=payments.selectById(id);if(preliminary==null)return null;
|
|
|
+ FlashDeliveryOrder order=orders.selectByIdForUpdate(preliminary.getOrderId());
|
|
|
+ FlashDeliveryLinePayment row=payments.selectForUpdate(id);
|
|
|
+ if(row==null || !expected.equals(row.getStatus()) || order==null || !"ACCEPTED".equals(order.getStatus())
|
|
|
+ || !Objects.equals(row.getRiderId(),order.getRiderId()) || !Objects.equals(row.getAmount().longValue(),order.getAmount()) || !"3".equals(order.getPayType()))return null;
|
|
|
+ if("CREATED".equals(expected)){
|
|
|
+ // 领取首次请求时再次检查凭证,意图创建后的禁用/轮换不能继续新收款。
|
|
|
+ InfoRiderLinePay current=credentials.getEnabledCurrent(order.getRiderId());
|
|
|
+ if(current==null || !Objects.equals(current.getId(),row.getCredentialId()))return null;
|
|
|
+ }
|
|
|
+ row.setStatus(next);row.setLeaseUntil(new Date(System.currentTimeMillis()+Math.max(120,rowLeaseSeconds)*1000L));save(row);return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void requestResult(Long id,Long version,String transactionId,String url){
|
|
|
+ FlashDeliveryLinePayment row=lockAttempt(id);
|
|
|
+ if(row==null || !Objects.equals(row.getVersion(),version) || !"REQUESTING".equals(row.getStatus()))return;
|
|
|
+ row.setTransactionId(transactionId);row.setPaymentUrl(url);row.setStatus("WAITING_AUTH");row.setLeaseUntil(null);
|
|
|
+ row.setReconcileDeadline(new Date(System.currentTimeMillis()+Math.max(1,authDeadlineMinutes)*60*1000L));save(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void setStatus(Long id,Long expectedVersion,String expectedStatus,String nextStatus,boolean terminal){
|
|
|
+ FlashDeliveryLinePayment row=lockAttempt(id);
|
|
|
+ if(row==null || !Objects.equals(row.getVersion(),expectedVersion) || !Objects.equals(row.getStatus(),expectedStatus))return;
|
|
|
+ row.setStatus(nextStatus);row.setLeaseUntil(null);if(terminal)row.setActiveOrderId(null);save(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public boolean applyPaidFact(Long id,String tx,long amount,Date at){
|
|
|
+ return applyGatewayFact(id,tx,amount,at,null,false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 已退款的 Retrieve 与到账一起落库,避免提交 PAID 后再拦截取件的竞态窗口。 */
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public boolean applyGatewayFact(Long id,String tx,long amount,Date at,String refundTx,boolean hasRefund){
|
|
|
+ FlashDeliveryLinePayment preliminary=payments.selectById(id);if(preliminary==null)return false;
|
|
|
+ FlashDeliveryOrder order=orders.selectByIdForUpdate(preliminary.getOrderId());
|
|
|
+ FlashDeliveryLinePayment row=payments.selectForUpdate(id);if(row==null || order==null)return false;
|
|
|
+ if(tx==null || amount<=0 || amount>Integer.MAX_VALUE || row.getTransactionId()!=null && !row.getTransactionId().equals(tx))throw fail("unknown");
|
|
|
+ boolean valid=amount==row.getAmount() && Objects.equals(row.getRiderId(),order.getRiderId()) && Objects.equals(amount,order.getAmount()) && "3".equals(order.getPayType());
|
|
|
+ row.setTransactionId(tx);row.setCapturedAmount(Math.toIntExact(amount));if(row.getPaidAt()==null)row.setPaidAt(at);
|
|
|
+ row.setStatus(valid && (!hasRefund || refundTx!=null)?"PAID":"MANUAL_REVIEW");row.setLeaseUntil(null);save(row);
|
|
|
+ FlashDeliveryLineRefund existing=refunds.selectByPaymentIdForUpdate(id);
|
|
|
+ if(hasRefund)existing=refundIntent(row,at,refundTx==null?"MANUAL_REVIEW":"REFUNDED",refundTx);
|
|
|
+ int paymentStatus=existing!=null && "REFUNDED".equals(existing.getStatus())?2:valid && !hasRefund?1:0;
|
|
|
+ if(orders.update(null,new UpdateWrapper<FlashDeliveryOrder>().eq("id",order.getId()).eq("pay_type","3").set("payment_status",paymentStatus).set("update_time",at))!=1)throw fail("unknown");
|
|
|
+ // 迟到付款只补偿退款,不恢复取消订单;意图和资金事实同事务落库。
|
|
|
+ if("CANCELLED".equals(order.getStatus()) && existing==null)refundIntent(row,at,"CREATED",null);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void ensureCancellationRefundIntent(Long orderId){
|
|
|
+ FlashDeliveryOrder order=orders.selectByIdForUpdate(orderId);if(order==null || !"3".equals(order.getPayType()))return;
|
|
|
+ FlashDeliveryLinePayment row=payments.selectLatestForUpdate(orderId);
|
|
|
+ if(row==null || row.getCapturedAmount()==null || row.getCapturedAmount()<=0)return;
|
|
|
+ if(refunds.selectByPaymentIdForUpdate(row.getId())!=null)return;
|
|
|
+ refundIntent(row,new Date(),"CREATED",null);
|
|
|
+ }
|
|
|
+ private FlashDeliveryLineRefund refundIntent(FlashDeliveryLinePayment payment,Date now,String status,String refundTx){
|
|
|
+ FlashDeliveryLineRefund existing=refunds.selectByPaymentIdForUpdate(payment.getId());
|
|
|
+ if(existing!=null){
|
|
|
+ if(refundTx!=null && !"REFUNDED".equals(existing.getStatus())){
|
|
|
+ existing=refunds.selectForUpdate(existing.getId());existing.setStatus("REFUNDED");existing.setRefundTransactionId(refundTx);existing.setRefundedAt(now);existing.setLeaseUntil(null);saveRefund(existing);
|
|
|
+ }return existing;
|
|
|
+ }
|
|
|
+ FlashDeliveryLineRefund refund=new FlashDeliveryLineRefund();refund.setPaymentId(payment.getId());refund.setAmount(payment.getCapturedAmount());
|
|
|
+ refund.setStatus(status);refund.setRefundTransactionId(refundTx);if(refundTx!=null)refund.setRefundedAt(now);
|
|
|
+ refund.setVersion(0L);refund.setCreateTime(now);refund.setUpdateTime(now);refund.setNextReconcileAt(now);
|
|
|
+ refund.setReconcileDeadline(new Date(now.getTime()+Math.max(1,unknownDeadlineHours)*60*60*1000L));if(refunds.insert(refund)!=1)throw fail("unknown");return refund;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 在取件已有订单行锁内调用;骑手手工收款标志不是 LINE Pay 到账证据。 */
|
|
|
+ public void assertPickupPaid(FlashDeliveryOrder order){
|
|
|
+ if(!"3".equals(order.getPayType()))return;
|
|
|
+ // requireRider 可能先建立 RR 快照;等订单锁后用当前读,不能放行退款前的旧 PAID。
|
|
|
+ FlashDeliveryLinePayment row=payments.selectLatestForUpdate(order.getId());
|
|
|
+ if(row==null || !"PAID".equals(row.getStatus()) || row.getPaidAt()==null || row.getTransactionId()==null
|
|
|
+ || row.getCapturedAmount()==null || !Objects.equals(row.getCapturedAmount().longValue(),order.getAmount())
|
|
|
+ || !Objects.equals(row.getRiderId(),order.getRiderId()) || refunds.selectByPaymentIdForUpdate(row.getId())!=null)throw fail("pickup.unpaid");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public FlashDeliveryLineRefund claimRefund(Long id){
|
|
|
+ FlashDeliveryLineRefund initial=refunds.selectById(id);if(initial==null)return null;
|
|
|
+ FlashDeliveryLinePayment payment=payments.selectById(initial.getPaymentId());if(payment==null)return null;
|
|
|
+ FlashDeliveryOrder order=orders.selectByIdForUpdate(payment.getOrderId());payments.selectForUpdate(payment.getId());
|
|
|
+ FlashDeliveryLineRefund row=refunds.selectForUpdate(id);
|
|
|
+ if(row==null || order==null || !"CANCELLED".equals(order.getStatus()) || !"CREATED".equals(row.getStatus()))return null;
|
|
|
+ row.setStatus("PROCESSING");row.setLeaseUntil(new Date(System.currentTimeMillis()+Math.max(120,rowLeaseSeconds)*1000L));saveRefund(row);return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void refundStatus(Long id,Long version,String expected,String next){
|
|
|
+ FlashDeliveryLineRefund row=lockRefund(id);
|
|
|
+ // 查询途中可能已有其他事务写入全额退款,截止时间不得降级可信终态。
|
|
|
+ if(row==null || Set.of("REFUNDED","FAILED","MANUAL_REVIEW").contains(row.getStatus()) || !Objects.equals(version,row.getVersion()) || !expected.equals(row.getStatus()))return;
|
|
|
+ row.setStatus(next);row.setLeaseUntil(null);saveRefund(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void applyRefundedFact(Long refundId,String refundTx,Date at){
|
|
|
+ FlashDeliveryLineRefund row=lockRefund(refundId);if(row==null || "REFUNDED".equals(row.getStatus()))return;
|
|
|
+ if(refundTx==null || refundTx.isBlank())throw fail("unknown");
|
|
|
+ FlashDeliveryLinePayment payment=payments.selectById(row.getPaymentId());
|
|
|
+ row.setStatus("REFUNDED");row.setRefundTransactionId(refundTx);row.setRefundedAt(at);row.setLeaseUntil(null);saveRefund(row);
|
|
|
+ if(orders.update(null,new UpdateWrapper<FlashDeliveryOrder>().eq("id",payment.getOrderId()).eq("pay_type","3").set("payment_status",2).set("update_time",at))!=1)throw fail("unknown");
|
|
|
+ }
|
|
|
+ private FlashDeliveryLinePayment lockAttempt(Long id){FlashDeliveryLinePayment row=payments.selectById(id);if(row==null)return null;orders.selectByIdForUpdate(row.getOrderId());return payments.selectForUpdate(id);}
|
|
|
+ /** 每次探测都推进下次时间,配合最早到期排序避免前一批未决记录长期占住队列。 */
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void deferPaymentProbe(Long id){
|
|
|
+ FlashDeliveryLinePayment row=lockAttempt(id);
|
|
|
+ if(row==null || !Set.of("CREATED","REQUESTING","WAITING_AUTH","CONFIRMING","REQUEST_UNKNOWN","CONFIRM_UNKNOWN").contains(row.getStatus())
|
|
|
+ || row.getLeaseUntil()!=null && row.getLeaseUntil().after(new Date()))return;
|
|
|
+ save(row);
|
|
|
+ }
|
|
|
+ @Transactional(rollbackFor=Exception.class)
|
|
|
+ public void deferRefundProbe(Long id){
|
|
|
+ FlashDeliveryLineRefund row=lockRefund(id);
|
|
|
+ if(row==null || !Set.of("CREATED","PROCESSING","UNKNOWN").contains(row.getStatus())
|
|
|
+ || row.getLeaseUntil()!=null && row.getLeaseUntil().after(new Date()))return;
|
|
|
+ saveRefund(row);
|
|
|
+ }
|
|
|
+ private FlashDeliveryLineRefund lockRefund(Long id){FlashDeliveryLineRefund row=refunds.selectById(id);if(row==null)return null;FlashDeliveryLinePayment p=payments.selectById(row.getPaymentId());if(p==null)return null;orders.selectByIdForUpdate(p.getOrderId());payments.selectForUpdate(p.getId());return refunds.selectForUpdate(id);}
|
|
|
+ private void save(FlashDeliveryLinePayment row){long expected=row.getVersion();row.setUpdateTime(new Date());row.setNextReconcileAt(new Date(System.currentTimeMillis()+30_000L));if(payments.updateVersioned(row,expected)!=1)throw fail("unknown");row.setVersion(expected+1);}
|
|
|
+ private void saveRefund(FlashDeliveryLineRefund row){long expected=row.getVersion();row.setUpdateTime(new Date());row.setNextReconcileAt(new Date(System.currentTimeMillis()+30_000L));if(refunds.updateVersioned(row,expected)!=1)throw fail("unknown");row.setVersion(expected+1);}
|
|
|
+ private static ServiceException fail(String key){return new ServiceException(MessageUtils.message("flash.line."+key));}
|
|
|
+}
|