zoujiajian 2 tygodni temu
rodzic
commit
1551cd6344

+ 2 - 2
netflix-dao/src/main/resources/mapper/GroupRelationMapper.xml

@@ -399,7 +399,7 @@
         select g.id as goods_id,g.title,g.logo from groups_relation gr inner join groups_trips gt on gt.id = gr.groups_id
         inner join goods_don_sku sku on sku.id = gt.sku_id inner join goods_don g on g.id = sku.goods_id
         where gr.status in ('validity', 'outside')
-        and gr.cmt != 2
+        and (gr.cmt is null or gr.cmt != 2)
         and (gr.expiry_time is null or gr.expiry_time > #{now} or (sku.is_mirror and gr.expiry_time > DATE_ADD(#{now},interval -7 day)))
         and gr.user_id != 0
         and gr.user_id in
@@ -470,7 +470,7 @@
             #{item}
         </foreach>
         and gr.status in ('validity','outside')
-        and gr.cmt != 2
+        and (gr.cmt is null or gr.cmt != 2)
         and gr.is_vip = 0
         and (gr.expiry_time is null or gr.expiry_time > now() or (gdk.is_mirror and gr.expiry_time >
         DATE_ADD(now(),interval -7 day)))

+ 8 - 3
netflix-service/src/main/java/com/cyksj/service/exchange/impl/ExchangeCodeServiceImpl.java

@@ -558,9 +558,14 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 				relation.setStatus(GroupsRelation.Status.validity);
 			}
 			relation.setYhsId(yhsId);
-			if (yhsId != null && yhsId != 0) {
-				relation.setCmt(1);
-			}
+			// Persist the shop type for every newly claimed relation.  New seats are
+			// created with a nullable cmt, while the user-ticket query intentionally
+			// excludes mirror-shop seats (cmt = 2).  Leaving a platform seat as NULL
+			// therefore makes it disappear from the user's ticket list because SQL
+			// `cmt <> 2` does not match NULL.  A normal platform seat is explicitly 0;
+			// a regular shop seat remains 1 and mirror-shop orders can still promote it
+			// to 2 in OrderDonServiceImpl after the order is created.
+			relation.setCmt(yhsId != null && yhsId > 0 ? 1 : 0);
 			int claimed;
 			if (relation.getStatus() == GroupsRelation.Status.outside) {
 				claimed = groupsRelationMapper.claimOutsideRelation(relation.getId(), userId,

+ 72 - 3
netflix-service/src/main/java/com/cyksj/service/order/impl/MultiQuantityOrderServiceImpl.java

@@ -5,6 +5,7 @@ import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.util.StringUtil;
 import com.cyksj.mapper.GoodsDonMapper;
 import com.cyksj.mapper.GoodsDonSkuMapper;
+import com.cyksj.mapper.GroupsRelationMapper;
 import com.cyksj.mapper.OrderDonMapper;
 import com.cyksj.model.entity.GoodsDon;
 import com.cyksj.model.entity.GoodsDonSku;
@@ -46,7 +47,9 @@ import org.springframework.transaction.support.TransactionTemplate;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.util.Date;
 import java.util.List;
+import java.util.Objects;
 import java.util.UUID;
 
 @Slf4j
@@ -60,6 +63,7 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
     private final OrderDonMapper orderDonMapper;
     private final GoodsDonMapper goodsDonMapper;
     private final GoodsDonSkuMapper goodsDonSkuMapper;
+    private final GroupsRelationMapper groupsRelationMapper;
     private final OrderDonTicketRecordService ticketRecordService;
     private final OrderDonBusinessEventService businessEventService;
     private final ExchangeCodeService exchangeCodeService;
@@ -239,7 +243,8 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
                 continue;
             }
             try {
-                requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(), token));
+                requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(),
+                        record.getRelationId(), token));
                 delivered++;
             } catch (Exception e) {
                 requiresNew(() -> ticketRecordService.markFailed(
@@ -299,7 +304,8 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
                 continue;
             }
             try {
-                requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(), token));
+                requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(),
+                        record.getRelationId(), token));
                 deliveredNow++;
             } catch (Exception e) {
                 requiresNew(() -> ticketRecordService.markFailed(
@@ -411,10 +417,32 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
     }
 
     private void deliverClaimedSlot(OrderDon order, GoodsDon goods, GoodsDonSku sku,
-                                    Long recordId, String workerToken) {
+                                    Long recordId, Long previousRelationId, String workerToken) {
         if (!ticketRecordService.lockClaim(recordId, workerToken)) {
             throw new IllegalStateException("ticket slot claim expired");
         }
+
+        /*
+         * A failed slot may still retain the relation_id of its previous ticket.
+         * Reallocating in that case creates an untracked/orphan seat. Reuse the
+         * old relation while it is still owned by this user and has the same SKU.
+         * A relation already reused by somebody else is never touched; allocation
+         * then follows the normal path below.
+         */
+        GroupsRelation previous = findReusablePreviousRelation(order, sku, recordId,
+                previousRelationId);
+        if (previous != null) {
+            if (!ticketRecordService.markSuccess(recordId, previous.getId(), workerToken)) {
+                throw new IllegalStateException("ticket slot claim expired");
+            }
+            try {
+                expiryRecordService.syncGroupsRelationExpiryRecord(order, previous.getId(), goods, sku);
+            } catch (DuplicateKeyException ignored) {
+                // Ancillary record; durable slot ownership is already committed.
+            }
+            return;
+        }
+
         // One slot attempt runs in one transaction; any allocation retry must start from a clean state.
 		Long yhsId = order.getYhsId() == null ? 0L : order.getYhsId();
 		GroupsRelation relation = exchangeCodeService.getRelationNoOrderOnce(sku, order.getUserId(), yhsId);
@@ -432,6 +460,47 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
 		}
 	}
 
+    /**
+     * Return a previous relation only if it is still a usable ticket for this
+     * order user and the same SKU. A same-user stale relation is rejected rather
+     * than silently allocating a second seat; a relation owned by another user
+     * is treated as already reused and is left untouched.
+     */
+    private GroupsRelation findReusablePreviousRelation(OrderDon order, GoodsDonSku sku,
+                                                        Long recordId, Long previousRelationId) {
+        if (previousRelationId == null || previousRelationId <= 0) {
+            return null;
+        }
+        // Lock the relation while deciding whether it can be reused. Clear/change
+        // operations acquire the ticket-slot lock first, so this preserves the
+        // same lock order and prevents a stale ownership decision.
+        GroupsRelation previous = groupsRelationMapper.selectByIdForUpdate(previousRelationId);
+        if (previous == null || !Objects.equals(previous.getUserId(), order.getUserId())) {
+            return null;
+        }
+        Long previousSkuId = groupsRelationMapper.getGroupTripsSkuIdByRelationId(previousRelationId);
+        if (!Objects.equals(previousSkuId, sku.getId())) {
+            throw new IllegalStateException("previous ticket SKU does not match; clear it before redelivery");
+        }
+        OrderDonTicketRecord active = ticketRecordService.getActiveByRelationId(previousRelationId);
+        if (active != null && !Objects.equals(active.getId(), recordId)) {
+            throw new IllegalStateException("previous ticket is already bound to another order slot; manual reconciliation required");
+        }
+        if (!isUsablePreviousRelation(previous)) {
+            throw new IllegalStateException("previous ticket is still bound but no longer usable; clear it before redelivery");
+        }
+        return previous;
+    }
+
+    private boolean isUsablePreviousRelation(GroupsRelation relation) {
+        GroupsRelation.Status status = relation.getStatus();
+        if (status != GroupsRelation.Status.validity && status != GroupsRelation.Status.outside) {
+            return false;
+        }
+        Date expiryTime = relation.getExpiryTime();
+        return expiryTime == null || expiryTime.after(new Date());
+    }
+
 	private void registerRelationLockCleanup(Long relationId, Long userId) {
 		TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
 			@Override

+ 8 - 1
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -3180,7 +3180,14 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 		relation.setUserId(userId);
 		//重新置为锁定
 		relation.setStatus(GroupsRelation.Status.locking);
-		relation.setYhsId(yhsId);
+		Long relationYhsId = yhsId == null ? 0L : yhsId;
+		relation.setYhsId(relationYhsId);
+		// Persist a type only for legacy seats whose cmt is NULL. Preserve cmt = 2
+		// on an existing mirror relation until the mirror-shop validation below
+		// applies its explicit shop ownership.
+		if (relation.getCmt() == null) {
+			relation.setCmt(relationYhsId > 0 ? 1 : 0);
+		}
 		groupsRelationMapper.updateById(relation);
 
 		Long skuId = sku.getId();

+ 3 - 1
netflix-service/src/main/java/com/cyksj/service/order/impl/UserRealOrderBenefitsImpl.java

@@ -219,7 +219,9 @@ public class UserRealOrderBenefitsImpl implements UserRealOrderBenefitsService {
 			}
 		}
 		if (cmt == null || cmt != 2) {
-			builder.field(RenewalView::getCmt, 2).op(Operator.NotEqual);
+			// SQL NULL does not satisfy `<> 2`; keep legacy platform tickets visible
+			// while still excluding mirror-shop tickets (cmt = 2).
+			builder.field(RenewalView::getCmt).sql("$1 is null or $1 <> ?", 2);
 		}
 		ticketViews.forEach(ticket -> {
 			List<GroupsRelationView> relationViewList = beanSearcher.searchAll(GroupsRelationView.class, builder

+ 6 - 3
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -270,9 +270,6 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 				builder.field(RenewalView::getCmt, cmt).field(RenewalView::getYhsId, userMirrorShop.getId());
 			}
 		}
-		if (cmt == null || cmt != 2) {
-			builder.field(RenewalView::getCmt, 2).op(Operator.NotEqual);
-		}
 //		Long yhsId = yhShopFrontService.getYhsIdByCustomId(customId);
 		DateTime now = DateTime.now();
 		if (StrUtil.isNotBlank(account)) {
@@ -286,6 +283,12 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 			conditionSql += String.format(" or (sku.is_mirror and gr.expiry_time > '%s')", DateUtil.offsetDay(now, -7));
 		}
 		conditionSql += ")";
+		// Keep legacy platform tickets whose cmt is NULL visible. SQL's
+		// `NULL <> 2` evaluates to UNKNOWN, so the old builder predicate
+		// silently hid these otherwise valid tickets.
+		if (cmt == null || cmt != 2) {
+			conditionSql += " and (gr.cmt is null or gr.cmt <> 2)";
+		}
 		if (StrUtil.isNotEmpty(orderNo)) {
 			OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
 					.eq(OrderDon::getOrderNo, orderNo)

+ 3 - 1
netflix-web/src/main/java/com/cyksj/web/controller/user/UserController.java

@@ -156,7 +156,9 @@ public class UserController {
             }
         }
         if (cmt == null || cmt != 2) {
-            builder.field(RenewalView::getCmt, 2).op(Operator.NotEqual);
+            // SQL NULL does not satisfy `<> 2`; keep legacy platform tickets visible
+            // while still excluding mirror-shop tickets (cmt = 2).
+            builder.field(RenewalView::getCmt).sql("$1 is null or $1 <> ?", 2);
         }
         List<RenewalView> renewalViews = beanSearcher.searchAll(RenewalView.class, builder
                 .field(RenewalView::getUserId, 0).op(Operator.GreaterThan)