|
@@ -5,6 +5,7 @@ import com.cyksj.common.exception.BusinessRuntimeException;
|
|
|
import com.cyksj.common.util.StringUtil;
|
|
import com.cyksj.common.util.StringUtil;
|
|
|
import com.cyksj.mapper.GoodsDonMapper;
|
|
import com.cyksj.mapper.GoodsDonMapper;
|
|
|
import com.cyksj.mapper.GoodsDonSkuMapper;
|
|
import com.cyksj.mapper.GoodsDonSkuMapper;
|
|
|
|
|
+import com.cyksj.mapper.GroupsRelationMapper;
|
|
|
import com.cyksj.mapper.OrderDonMapper;
|
|
import com.cyksj.mapper.OrderDonMapper;
|
|
|
import com.cyksj.model.entity.GoodsDon;
|
|
import com.cyksj.model.entity.GoodsDon;
|
|
|
import com.cyksj.model.entity.GoodsDonSku;
|
|
import com.cyksj.model.entity.GoodsDonSku;
|
|
@@ -46,7 +47,9 @@ import org.springframework.transaction.support.TransactionTemplate;
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
import java.math.RoundingMode;
|
|
|
|
|
+import java.util.Date;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
+import java.util.Objects;
|
|
|
import java.util.UUID;
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
@Slf4j
|
|
@Slf4j
|
|
@@ -60,6 +63,7 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
private final OrderDonMapper orderDonMapper;
|
|
private final OrderDonMapper orderDonMapper;
|
|
|
private final GoodsDonMapper goodsDonMapper;
|
|
private final GoodsDonMapper goodsDonMapper;
|
|
|
private final GoodsDonSkuMapper goodsDonSkuMapper;
|
|
private final GoodsDonSkuMapper goodsDonSkuMapper;
|
|
|
|
|
+ private final GroupsRelationMapper groupsRelationMapper;
|
|
|
private final OrderDonTicketRecordService ticketRecordService;
|
|
private final OrderDonTicketRecordService ticketRecordService;
|
|
|
private final OrderDonBusinessEventService businessEventService;
|
|
private final OrderDonBusinessEventService businessEventService;
|
|
|
private final ExchangeCodeService exchangeCodeService;
|
|
private final ExchangeCodeService exchangeCodeService;
|
|
@@ -239,7 +243,8 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
- requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(), token));
|
|
|
|
|
|
|
+ requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(),
|
|
|
|
|
+ record.getRelationId(), token));
|
|
|
delivered++;
|
|
delivered++;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
requiresNew(() -> ticketRecordService.markFailed(
|
|
requiresNew(() -> ticketRecordService.markFailed(
|
|
@@ -299,7 +304,8 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
- requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(), token));
|
|
|
|
|
|
|
+ requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(),
|
|
|
|
|
+ record.getRelationId(), token));
|
|
|
deliveredNow++;
|
|
deliveredNow++;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
requiresNew(() -> ticketRecordService.markFailed(
|
|
requiresNew(() -> ticketRecordService.markFailed(
|
|
@@ -411,10 +417,32 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void deliverClaimedSlot(OrderDon order, GoodsDon goods, GoodsDonSku sku,
|
|
private void deliverClaimedSlot(OrderDon order, GoodsDon goods, GoodsDonSku sku,
|
|
|
- Long recordId, String workerToken) {
|
|
|
|
|
|
|
+ Long recordId, Long previousRelationId, String workerToken) {
|
|
|
if (!ticketRecordService.lockClaim(recordId, workerToken)) {
|
|
if (!ticketRecordService.lockClaim(recordId, workerToken)) {
|
|
|
throw new IllegalStateException("ticket slot claim expired");
|
|
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.
|
|
// One slot attempt runs in one transaction; any allocation retry must start from a clean state.
|
|
|
Long yhsId = order.getYhsId() == null ? 0L : order.getYhsId();
|
|
Long yhsId = order.getYhsId() == null ? 0L : order.getYhsId();
|
|
|
GroupsRelation relation = exchangeCodeService.getRelationNoOrderOnce(sku, order.getUserId(), yhsId);
|
|
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) {
|
|
private void registerRelationLockCleanup(Long relationId, Long userId) {
|
|
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
|
|
@Override
|
|
@Override
|