Explorar o código

fix API额度

zoujiajian hai 1 mes
pai
achega
d65749bb09

+ 91 - 19
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -82,7 +82,10 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import javax.servlet.http.HttpServletRequest;
 import java.math.BigDecimal;
@@ -246,6 +249,8 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private final TicketRedemptionCodeRecordMapper ticketRedemptionCodeRecordMapper;
 
+    private final PlatformTransactionManager transactionManager;
+
 	private static final Date GPT_ACTIVITY_PAY_TIME_BEGIN = DateUtil.parse("2026-05-01 00:00:00");
 
 	private static final String GPT_ACTIVITY_REDEEM_NAME = "GPT代充/独立赠送";
@@ -599,6 +604,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
         // PLUS 仅 skuNum == 1 的独立规格可领;与实际领取接口共用资格校验。
         ticket.setCanReceiveGptApiQuota(isGptApiQuotaSourceTicket(sourceOrder, ticket.getGoodsId(), ticket.getSkuNum())
                 && !hasReceivedGptApiQuota(userIds, ticket.getGoodsId(), yearQuota)
+                && !hasPendingGptApiQuota(userIds)
                 && (!yearQuota || hasNewGptApiQuotaPurchase(userIds, ticket.getGoodsId(), sourceOrder)));
 	}
 
@@ -607,7 +613,9 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
                 .in(TicketRedemptionCodeRecord::getUserId, userIds)
                 .eq(TicketRedemptionCodeRecord::getSkuId, Constant.GPT_ACTIVITY_GIFT_SKU_ID)
                 .eq(TicketRedemptionCodeRecord::getSourceType, RedisKey.GPT_API_QUOTA_KEY)
-                .eq(TicketRedemptionCodeRecord::getStatus, "success");
+                .and(wrapper -> wrapper.eq(TicketRedemptionCodeRecord::getStatus, "success")
+                        .or(issued -> issued.isNotNull(TicketRedemptionCodeRecord::getRedeemCode)
+                                .ne(TicketRedemptionCodeRecord::getRedeemCode, "")));
         if (yearQuota) {
             // 两种年付共享一次50元资格;原5元不占用,历史已发50元仍计入。
             query.in(TicketRedemptionCodeRecord::getSourceGoodsId, Constant.GPT_RECHARGE_GOODS_ID, Constant.GPT_PLUS_GID)
@@ -619,6 +627,15 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
         return ticketRedemptionCodeRecordMapper.selectCount(query) > 0;
     }
 
+    private boolean hasPendingGptApiQuota(List<Long> userIds) {
+        // 发放中或远端结果不确定时不允许自动重发,避免 Redis 锁过期后重复领取。
+        return ticketRedemptionCodeRecordMapper.selectCount(Wrappers.lambdaQuery(TicketRedemptionCodeRecord.class)
+                .in(TicketRedemptionCodeRecord::getUserId, userIds)
+                .eq(TicketRedemptionCodeRecord::getSkuId, Constant.GPT_ACTIVITY_GIFT_SKU_ID)
+                .eq(TicketRedemptionCodeRecord::getSourceType, RedisKey.GPT_API_QUOTA_KEY)
+                .eq(TicketRedemptionCodeRecord::getStatus, "processing")) > 0;
+    }
+
     private boolean hasNewGptApiQuotaPurchase(List<Long> userIds, Long sourceGoodsId, OrderDon sourceOrder) {
         Date purchaseTime = sourceOrder == null ? null
                 : Optional.ofNullable(sourceOrder.getPayTime()).orElse(sourceOrder.getCreatedTime());
@@ -631,7 +648,9 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
                 .eq(TicketRedemptionCodeRecord::getSkuId, Constant.GPT_ACTIVITY_GIFT_SKU_ID)
                 .eq(TicketRedemptionCodeRecord::getSourceType, RedisKey.GPT_API_QUOTA_KEY)
                 .eq(TicketRedemptionCodeRecord::getSourceGoodsId, sourceGoodsId)
-                .eq(TicketRedemptionCodeRecord::getStatus, "success")
+                .and(wrapper -> wrapper.eq(TicketRedemptionCodeRecord::getStatus, "success")
+                        .or(issued -> issued.isNotNull(TicketRedemptionCodeRecord::getRedeemCode)
+                                .ne(TicketRedemptionCodeRecord::getRedeemCode, "")))
                 .and(wrapper -> wrapper.lt(TicketRedemptionCodeRecord::getQuota, YEAR_RECHARGE_API_QUOTA)
                         .or().isNull(TicketRedemptionCodeRecord::getQuota))
                 .and(wrapper -> wrapper.ge(TicketRedemptionCodeRecord::getCreatedTime, purchaseTime)
@@ -639,6 +658,35 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
         return previousClaims == 0;
     }
 
+    private void reserveGptApiQuota(List<Long> userIds, OrderDon sourceOrder,
+                                   TicketRedemptionCodeRecord record, boolean yearQuota) {
+        TransactionTemplate transaction = new TransactionTemplate(transactionManager);
+        transaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+        transaction.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
+        transaction.executeWithoutResult(status -> {
+            // 先按固定顺序锁关联用户,再检查并占位;即使 Redis 锁超时也不能并发发放。
+            for (Long relatedUserId : new TreeSet<>(userIds)) {
+                User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class)
+                        .select(User::getId).eq(User::getId, relatedUserId).last("for update"));
+                if (user == null) {
+                    throw BusinessRuntimeException.getInstance("用户不存在,无法领取");
+                }
+            }
+            if (hasReceivedGptApiQuota(userIds, record.getSourceGoodsId(), yearQuota)) {
+                throw BusinessRuntimeException.getInstance(yearQuota ? "您已领取50元API额度" : "您已领取");
+            }
+            if (hasPendingGptApiQuota(userIds)) {
+                throw BusinessRuntimeException.getInstance("额度正在发放或结果待确认,请勿重复领取;长时间未到账请联系客服");
+            }
+            if (yearQuota && !hasNewGptApiQuotaPurchase(userIds, record.getSourceGoodsId(), sourceOrder)) {
+                throw BusinessRuntimeException.getInstance("请再次购买年付后领取50元API额度");
+            }
+            if (ticketRedemptionCodeRecordMapper.insert(record) != 1) {
+                throw BusinessRuntimeException.getInstance("领取记录保存失败,请稍后再试");
+            }
+        });
+    }
+
 	@Override
 	public GroupsRelation getSimilarExpiredGroupRelation(GoodsDonSku sku, List<Long> errorsRelationIds, String ip) {
 		Long skuId = sku.getId();
@@ -2867,7 +2915,8 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 			userIds = List.of(userId);
 		}
 		String lockKey = String.format("%s:%s", RedisKey.GPT_API_QUOTA_KEY, Collections.min(userIds));
-		if (!redisService.setNx(lockKey, userId, 60L)) {
+        String lockToken = UUID.randomUUID().toString();
+		if (!redisService.setNx(lockKey, lockToken, 60L)) {
 			throw BusinessRuntimeException.getInstance("领取中,请稍后再试");
 		}
 		try {
@@ -2902,12 +2951,6 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
             }
 			boolean yearQuota = isYearRechargeSku(sourceGoodsId, sourceSku.getMonths(), sourceSku.getNum());
 			Integer quota = yearQuota ? YEAR_RECHARGE_API_QUOTA : DEFAULT_API_QUOTA;
-			if (hasReceivedGptApiQuota(userIds, sourceGoodsId, yearQuota)) {
-				throw BusinessRuntimeException.getInstance(yearQuota ? "您已领取50元API额度" : "您已领取");
-			}
-			if (yearQuota && !hasNewGptApiQuotaPurchase(userIds, sourceGoodsId, sourceOrder)) {
-				throw BusinessRuntimeException.getInstance("请再次购买年付后领取50元API额度");
-			}
 
 			TicketRedemptionCodeRecord record = new TicketRedemptionCodeRecord();
 			record.setUserId(userId);
@@ -2917,13 +2960,19 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 			record.setSkuId(Constant.GPT_ACTIVITY_GIFT_SKU_ID);
 			record.setSourceType(RedisKey.GPT_API_QUOTA_KEY);
 			record.setRedeemName(GPT_ACTIVITY_REDEEM_NAME);
+			record.setQuota(quota);
 			record.setStatus("processing");
 			record.setRelationExpiryTime(sourceRelation.getExpiryTime());
-			ticketRedemptionCodeRecordMapper.insert(record);
+            reserveGptApiQuota(userIds, sourceOrder, record, yearQuota);
 
-			GoodsDonSku giftSku = goodsDonSkuMapper.selectById(Constant.GPT_ACTIVITY_GIFT_SKU_ID);
+			GoodsDonSku giftSku = null;
 			GroupsRelation giftRelation = null;
+            boolean redeemRequested = false;
 			try {
+                giftSku = goodsDonSkuMapper.selectById(Constant.GPT_ACTIVITY_GIFT_SKU_ID);
+                if (giftSku == null) {
+                    throw BusinessRuntimeException.getInstance("赠品规格不存在,请联系客服");
+                }
 				giftRelation = getReusableGptActivityGiftRelation(userIds, giftSku.getId(), sourceGoodsId);
 				if (giftRelation == null) {
 					Long giftRelationId = getTicket(userId, giftSku.getId(), Boolean.FALSE);
@@ -2935,6 +2984,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 						throw BusinessRuntimeException.getInstance("系统繁忙,请重新领取");
 					}
 				}
+                redeemRequested = true;
 				GptActivityRedeemResult redeemResult = requestGptActivityRedeemCode(Constant.AI_API_SVC_DOMAIN, GPT_ACTIVITY_REDEEM_NAME, quota);
 				record.setRelationId(giftRelation.getId());
 				record.setGoodsId(giftSku.getGoodsId());
@@ -2946,26 +2996,41 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 				record.setStatus("success");
 				record.setRelationExpiryTime(giftRelation.getExpiryTime());
 				record.setApiResponse(redeemResult.getApiResponse());
-				ticketRedemptionCodeRecordMapper.updateById(record);
+                if (ticketRedemptionCodeRecordMapper.updateById(record) != 1) {
+                    throw BusinessRuntimeException.getInstance("额度发放结果待确认,请联系客服,勿重复领取");
+                }
 
 				ReceiveGptActivityTicketResp resp = new ReceiveGptActivityTicketResp();
 				resp.setRelationId(giftRelation.getId());
 				resp.setRedeemCode(redeemResult.getRedeemCode());
 				return resp;
 			} catch (Exception e) {
-				record.setGoodsId(giftSku.getGoodsId());
+                if (giftSku != null && giftRelation != null) {
+                    record.setGoodsId(giftSku.getGoodsId());
+                }
                 record.setSourceGoodsId(sourceGoodsId);
-                record.setStatus("error");
+                // 已调用远端后,即使超时/落库失败也可能已发码;保留占位,禁止自动重发。
+                record.setStatus(redeemRequested ? "processing" : "error");
 				record.setErrorMsg(StringUtil.getErrorText(e));
 				if (giftRelation != null) {
 					record.setRelationId(giftRelation.getId());
 					record.setRelationExpiryTime(giftRelation.getExpiryTime());
 				}
-				ticketRedemptionCodeRecordMapper.updateById(record);
+                try {
+                    if (ticketRedemptionCodeRecordMapper.updateById(record) != 1) {
+                        log.error("GPT API quota claim record update failed, recordId={}", record.getId());
+                    }
+                } catch (Exception persistenceError) {
+                    // 初始 processing 已独立提交;保存失败时仍保留防重占位。
+                    if (persistenceError != e) {
+                        e.addSuppressed(persistenceError);
+                    }
+                    log.error("GPT API quota claim result persistence failed, recordId={}", record.getId(), persistenceError);
+                }
 				throw e;
 			}
 		} finally {
-			redisService.del(lockKey);
+			redisService.deleteIfValueEquals(lockKey, lockToken);
 		}
 	}
 
@@ -3077,7 +3142,14 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
                 .orderByDesc(TicketRedemptionCodeRecord::getId)
                 .last("limit 1"));
         if (retryRecord != null) {
-            return groupsRelationMapper.selectById(retryRecord.getRelationId());
+            GroupsRelation giftRelation = groupsRelationMapper.selectById(retryRecord.getRelationId());
+            // 发券前失败的旧记录可能仍指向付费源票,只能复用当前用户的赠品票。
+            if (giftRelation != null && userIds.contains(giftRelation.getUserId()) && giftRelation.getGroupsId() != null) {
+                GroupsTrips giftGroup = groupsMapper.selectById(giftRelation.getGroupsId());
+                if (giftGroup != null && Objects.equals(giftGroup.getSkuId(), giftSkuId)) {
+                    return giftRelation;
+                }
+            }
         }
         return null;
     }
@@ -3085,7 +3157,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 		return quota == null || quota <= 0 ? DEFAULT_API_QUOTA : quota;
 	}
 
-	private GptActivityRedeemResult requestGptActivityRedeemCode(String apiDomain, String redeemName, Integer quota) throws Exception {
+	GptActivityRedeemResult requestGptActivityRedeemCode(String apiDomain, String redeemName, Integer quota) throws Exception {
 		Map<String, Object> params = new HashMap<>();
 		params.put("name", redeemName);
 		params.put("count", 1);
@@ -3120,7 +3192,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	@lombok.Getter
 	@lombok.Setter
-	private static class GptActivityRedeemResult {
+	static class GptActivityRedeemResult {
 		private String redeemCode;
 		private String apiResponse;
 		private Integer quota;