|
|
@@ -16,6 +16,7 @@ import com.cyksj.model.entity.TaskType;
|
|
|
import com.cyksj.model.entity.UserBalanceSourceRecord;
|
|
|
import com.cyksj.model.entity.SysConfig;
|
|
|
import com.cyksj.model.dto.GalaxyCoinConsumeRateDto;
|
|
|
+import com.cyksj.model.response.MultiQuantityRedeliveryResult;
|
|
|
import com.cyksj.mapper.UserBalanceSourceRecordMapper;
|
|
|
import com.cyksj.mapper.market.task.TaskTypeMapper;
|
|
|
import com.cyksj.common.constant.TaskTypeEnum;
|
|
|
@@ -54,6 +55,7 @@ import java.util.UUID;
|
|
|
public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService {
|
|
|
|
|
|
private static final int DELIVERY_MAX_RETRIES = 5;
|
|
|
+ private static final int MANUAL_DELIVERY_LEASE_MINUTES = 5;
|
|
|
|
|
|
private final OrderDonMapper orderDonMapper;
|
|
|
private final GoodsDonMapper goodsDonMapper;
|
|
|
@@ -187,7 +189,7 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
try {
|
|
|
deliver(order.getId());
|
|
|
} catch (Exception e) {
|
|
|
- // Payment is already committed. Durable ticket slots let the recovery job retry safely.
|
|
|
+ // Payment is already committed. Durable ticket slots allow safe manual redelivery.
|
|
|
log.error("Immediate multi-quantity ticket delivery failed, orderId:{}", order.getId(), e);
|
|
|
}
|
|
|
try {
|
|
|
@@ -250,6 +252,125 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
return delivered;
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public MultiQuantityRedeliveryResult redeliverManually(Long orderId) {
|
|
|
+ if (orderId == null || orderId <= 0) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单ID不能为空");
|
|
|
+ }
|
|
|
+ OrderDon order = requiresNewResult(() -> lockOrderForManualRedelivery(orderId));
|
|
|
+
|
|
|
+ List<OrderDonTicketRecord> beforeRecords = ticketRecordService.listByOrderId(orderId);
|
|
|
+ validateManualRedeliveryRecords(order, beforeRecords);
|
|
|
+ int deliveredBefore = countSuccessfulTickets(beforeRecords);
|
|
|
+ if (deliveredBefore >= order.getNum()) {
|
|
|
+ return new MultiQuantityRedeliveryResult(order.getNum(), deliveredBefore,
|
|
|
+ 0, deliveredBefore, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // A restarted service may leave an owned slot behind. Only an expired lease is reclaimed.
|
|
|
+ ticketRecordService.recoverExpiredProcessingForOrder(
|
|
|
+ orderId, MANUAL_DELIVERY_LEASE_MINUTES);
|
|
|
+
|
|
|
+ GoodsDon goods = goodsDonMapper.selectById(order.getGoodsId());
|
|
|
+ GoodsDonSku sku = goodsDonSkuMapper.selectById(order.getSkuId());
|
|
|
+ if (goods == null || sku == null) {
|
|
|
+ throw BusinessRuntimeException.getInstance("商品或规格不存在,无法补发车票");
|
|
|
+ }
|
|
|
+
|
|
|
+ int deliveredNow = 0;
|
|
|
+ // This is a snapshot: one customer-service click attempts every eligible slot at most once.
|
|
|
+ List<OrderDonTicketRecord> candidates = ticketRecordService.listByOrderId(orderId);
|
|
|
+ validateManualRedeliveryRecords(order, candidates);
|
|
|
+ boolean hasProcessingSlot = candidates.stream().anyMatch(record ->
|
|
|
+ record.getStatus() == OrderDonTicketRecord.Status.processing);
|
|
|
+ boolean hasEligibleSlot = candidates.stream().anyMatch(record ->
|
|
|
+ record.getStatus() == OrderDonTicketRecord.Status.pending
|
|
|
+ || record.getStatus() == OrderDonTicketRecord.Status.failed);
|
|
|
+ if (!hasEligibleSlot && hasProcessingSlot) {
|
|
|
+ throw BusinessRuntimeException.getInstance("车票正在发放中,请五分钟后再补发");
|
|
|
+ }
|
|
|
+ for (OrderDonTicketRecord record : candidates) {
|
|
|
+ if (record.getStatus() != OrderDonTicketRecord.Status.pending
|
|
|
+ && record.getStatus() != OrderDonTicketRecord.Status.failed) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String token = UUID.randomUUID().toString();
|
|
|
+ if (!requiresNewResult(() -> claimTicketManually(orderId, record.getId(), token))) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ requiresNew(() -> deliverClaimedSlot(order, goods, sku, record.getId(), token));
|
|
|
+ deliveredNow++;
|
|
|
+ } catch (Exception e) {
|
|
|
+ requiresNew(() -> ticketRecordService.markFailed(
|
|
|
+ record.getId(), token, StringUtil.getErrorText(e)));
|
|
|
+ log.error("Manual multi-quantity ticket redelivery failed, orderId:{}, slotId:{}",
|
|
|
+ orderId, record.getId(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ requiresNewResult(() -> completeIfAllTicketsAvailable(orderId));
|
|
|
+ int deliveredTotal = ticketRecordService.countSuccess(orderId);
|
|
|
+ return new MultiQuantityRedeliveryResult(order.getNum(), deliveredBefore,
|
|
|
+ deliveredNow, deliveredTotal, Math.max(0, order.getNum() - deliveredTotal));
|
|
|
+ }
|
|
|
+
|
|
|
+ private OrderDon lockOrderForManualRedelivery(Long orderId) {
|
|
|
+ OrderDon order = orderDonMapper.selectByIdForUpdate(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单不存在");
|
|
|
+ }
|
|
|
+ if (order.getNum() == null || order.getNum() <= 1
|
|
|
+ || ticketRecordService.listByOrderId(orderId).size() != order.getNum()) {
|
|
|
+ throw BusinessRuntimeException.getInstance("仅支持多件商品订单补发车票");
|
|
|
+ }
|
|
|
+ if (order.getStatus() != OrderDon.Status.hasPayment
|
|
|
+ && order.getStatus() != OrderDon.Status.complete) {
|
|
|
+ throw BusinessRuntimeException.getInstance("当前订单状态不允许补发车票");
|
|
|
+ }
|
|
|
+ OrderDonBusinessEvent refundEvent = businessEventService.get(
|
|
|
+ orderId, OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (refundEvent != null && (refundEvent.getStatus() == OrderDonBusinessEvent.Status.processing
|
|
|
+ || refundEvent.getStatus() == OrderDonBusinessEvent.Status.success)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单正在退款或已退款,禁止补发车票");
|
|
|
+ }
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateManualRedeliveryRecords(OrderDon order,
|
|
|
+ List<OrderDonTicketRecord> records) {
|
|
|
+ if (records.size() != order.getNum()) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单车票槽数量异常,禁止补发");
|
|
|
+ }
|
|
|
+ boolean refundOrCleared = records.stream().anyMatch(record ->
|
|
|
+ record.getStatus() == OrderDonTicketRecord.Status.refunding
|
|
|
+ || record.getStatus() == OrderDonTicketRecord.Status.refunded
|
|
|
+ || record.getStatus() == OrderDonTicketRecord.Status.cleared);
|
|
|
+ if (refundOrCleared) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单正在退款、已退款或车票已清除,禁止补发");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int countSuccessfulTickets(List<OrderDonTicketRecord> records) {
|
|
|
+ return (int) records.stream()
|
|
|
+ .filter(record -> record.getStatus() == OrderDonTicketRecord.Status.success)
|
|
|
+ .count();
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean claimTicketManually(Long orderId, Long recordId, String workerToken) {
|
|
|
+ OrderDon latest = orderDonMapper.selectByIdForUpdate(orderId);
|
|
|
+ if (latest == null || (latest.getStatus() != OrderDon.Status.hasPayment
|
|
|
+ && latest.getStatus() != OrderDon.Status.complete)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ OrderDonBusinessEvent refundEvent = businessEventService.get(
|
|
|
+ orderId, OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (refundEvent != null && (refundEvent.getStatus() == OrderDonBusinessEvent.Status.processing
|
|
|
+ || refundEvent.getStatus() == OrderDonBusinessEvent.Status.success)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单正在退款或已退款,补发已停止");
|
|
|
+ }
|
|
|
+ return ticketRecordService.claimManually(orderId, recordId, workerToken);
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Throwable.class)
|
|
|
public boolean completeIfAllTicketsAvailable(Long orderId) {
|
|
|
@@ -421,8 +542,13 @@ public class MultiQuantityOrderServiceImpl implements MultiQuantityOrderService
|
|
|
if (config == null || config.getSysValue() == null || config.getSysValue().isBlank()) {
|
|
|
throw new IllegalStateException("galaxy coin cashback configuration is missing");
|
|
|
}
|
|
|
- GalaxyCoinConsumeRateDto rate = com.cyksj.common.util.Jsons.parseObject(
|
|
|
- (Object) config.getSysValue(), GalaxyCoinConsumeRateDto.class);
|
|
|
+ GalaxyCoinConsumeRateDto rate;
|
|
|
+ try {
|
|
|
+ rate = com.cyksj.common.util.Jsons.parseObject(
|
|
|
+ config.getSysValue(), GalaxyCoinConsumeRateDto.class);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new IllegalStateException("invalid galaxy coin cashback configuration", e);
|
|
|
+ }
|
|
|
if (rate == null || rate.getMoney() == null || rate.getGalaxyCoin() == null
|
|
|
|| rate.getMoney().compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
throw new IllegalStateException("invalid galaxy coin cashback configuration");
|