Ver Fonte

Merge branch 'gpt_pay_zs_api'

zoujiajian há 3 meses atrás
pai
commit
34178b7551

+ 113 - 0
docs/manager-statistics-ai-usage-data.md

@@ -0,0 +1,113 @@
+# 用户 AI 用量每日统计接口
+
+## 接口信息
+
+- 接口地址:`GET /manager/statistics/get/aiUsageData`
+- 用途:查询用户 AI 用量的每日统计列表
+- 数据来源:`daily_usage_statistics_record`
+- 默认排序:按 `usageDate` 倒序
+- 返回类型:分页列表
+
+## 请求参数
+
+该接口使用 Bean Searcher 查询参数,支持分页和按返回字段筛选。
+
+### 分页参数
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+| --- | --- | --- | --- | --- |
+| `start` | number | 否 | `1` | 页码,从 1 开始 |
+| `limit` | number | 否 | `10` | 每页条数 |
+
+### 常用筛选参数
+
+| 参数名 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `type` | string | 否 | AI 类型 |
+| `usageDate` | string | 否 | 使用日期,精确匹配 |
+| `usageDate-op` | string | 否 | `usageDate` 的查询操作符,例如 `bt` 表示区间查询 |
+| `usageDate-0` | string | 否 | 区间开始日期,配合 `usageDate-op=bt` 使用 |
+| `usageDate-1` | string | 否 | 区间结束日期,配合 `usageDate-op=bt` 使用 |
+
+可筛选字段与返回记录字段一致:`id`、`createdTime`、`updateTime`、`type`、`usageDate`、`userCount`、`dailyLimitTotal`、`usageUserCount`、`usedUsd`、`requestCount`。
+
+## 请求示例
+
+```http
+GET /manager/statistics/get/aiUsageData?start=1&limit=10&type=codex
+```
+
+按日期区间查询:
+
+```http
+GET /manager/statistics/get/aiUsageData?start=1&limit=10&usageDate-op=bt&usageDate-0=2026-05-01&usageDate-1=2026-05-13
+```
+
+## 响应字段
+
+### 外层结构
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `code` | number | 状态码 |
+| `msg` | string | 状态描述 |
+| `flag` | boolean | 是否成功 |
+| `data` | object | 查询结果 |
+
+### `data`
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `totalCount` | number | 总记录数 |
+| `dataList` | array | 当前页数据 |
+| `summaries` | array | 汇总数据;当前接口未配置汇总时通常为空数组 |
+
+### `data.dataList[]`
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `id` | number | 记录 ID |
+| `createdTime` | string | 创建时间 |
+| `updateTime` | string | 更新时间 |
+| `type` | string | AI 类型 |
+| `usageDate` | string | 统计日期 |
+| `userCount` | number | 用户总数 |
+| `dailyLimitTotal` | number | 当日可用额度合计 |
+| `usageUserCount` | number | 当日使用用户数 |
+| `usedUsd` | number | 当日用量,单位 USD |
+| `requestCount` | number | 请求次数 |
+
+## 响应示例
+
+```json
+{
+  "code": 200,
+  "msg": "success",
+  "flag": true,
+  "data": {
+    "totalCount": 1,
+    "dataList": [
+      {
+        "id": 1,
+        "createdTime": "2026-05-13 00:00:00",
+        "updateTime": "2026-05-13 00:00:00",
+        "type": "codex",
+        "usageDate": "2026-05-13 00:00:00",
+        "userCount": 100,
+        "dailyLimitTotal": 500.00,
+        "usageUserCount": 80,
+        "usedUsd": 123.45,
+        "requestCount": 300
+      }
+    ],
+    "summaries": []
+  }
+}
+```
+
+## 前端对接备注
+
+- 列表默认已按 `usageDate` 倒序,前端不传排序参数即可。
+- `start` 是页码,不是偏移量。
+- 金额/用量字段建议按数字处理,展示时再格式化小数位。
+- 日期字段由后端 `Date` 序列化输出,实际格式以当前项目 Jackson 配置为准。

+ 10 - 0
netflix-common/src/main/java/com/cyksj/common/constant/Constant.java

@@ -858,4 +858,14 @@ public interface Constant {
 	 * Adobe 全家桶
 	 */
 	Long ADOBE_GOODS_ID = 45L;
+
+    /**
+     * API额度兑换码车票ID
+     */
+    Long GPT_ACTIVITY_GIFT_SKU_ID = 817L;
+
+    /**
+     * AI API调度域名
+     */
+    String AI_API_SVC_DOMAIN = "https://svc.apinebula.com";
 }

+ 2 - 0
netflix-common/src/main/java/com/cyksj/dto/RedisKey.java

@@ -74,4 +74,6 @@ public class RedisKey {
     public static String CHATGPT_USER_RESTRICTION ="chatgpt:user:restriction:";
 
     public static String GOODS_SUBSCRIBE_ALERT_KEY = "goods_subscribe_alert_key:";
+
+    public static String GPT_API_QUOTA_KEY = "gpt_pay";
 }

+ 1 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/TicketRedemptionCodeRecord.java

@@ -13,6 +13,7 @@ public class TicketRedemptionCodeRecord extends BaseEntity {
     private Long relationId;
     private Long goodsId;
     private Long skuId;
+    private Long sourceGoodsId;
     private String sourceType;
     private String redeemName;
     private Integer quota;

+ 17 - 0
netflix-dao/src/main/java/com/cyksj/model/request/ReceiveGptActivityTicketReq.java

@@ -0,0 +1,17 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+
+@Getter
+@Setter
+public class ReceiveGptActivityTicketReq {
+
+    /**
+     * 用于校验领取资格的车票 ID
+     */
+    @NotNull
+    private Long relationId;
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/model/response/GptActivityRedeemResult.java

@@ -0,0 +1,13 @@
+package com.cyksj.model.response;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class GptActivityRedeemResult {
+    private String redeemCode;
+    private String apiResponse;
+    private Integer quota;
+    private Long expiredTime;
+}

+ 19 - 0
netflix-dao/src/main/java/com/cyksj/model/response/ReceiveGptActivityTicketResp.java

@@ -0,0 +1,19 @@
+package com.cyksj.model.response;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ReceiveGptActivityTicketResp {
+
+    /**
+     * 领取后发放的 817 规格车票 ID
+     */
+    private Long relationId;
+
+    /**
+     * 第三方兑换码
+     */
+    private String redeemCode;
+}

+ 6 - 0
netflix-dao/src/main/java/com/cyksj/model/views/RenewalView.java

@@ -611,6 +611,12 @@ public class RenewalView {
     @DbIgnore
     private Boolean receivedGptGift;
 
+    /**
+     * 是否可领取GPT API额度
+     */
+    @DbIgnore
+    private Boolean canReceiveGptApiQuota;
+
     /**
      * 是否是会员抵扣车票
      */

+ 3 - 0
netflix-service/src/main/java/com/cyksj/service/relation/GroupRelationFrontService.java

@@ -13,6 +13,7 @@ import com.cyksj.model.request.TicketLoginReq;
 import com.cyksj.model.response.NetflixErrorAccountStatus;
 import com.cyksj.model.response.NetflixRecoverResp;
 import com.cyksj.model.response.NetflixRefundInfoResp;
+import com.cyksj.model.response.ReceiveGptActivityTicketResp;
 import com.cyksj.model.views.*;
 import com.ejlchina.searcher.SearchResult;
 
@@ -137,4 +138,6 @@ public interface GroupRelationFrontService {
 	OrderRefundDto getYoutubeRefundInfo(long userId, Long relationId);
 
     String getEmailCode(long userId, Long relationId) throws Exception;
+
+	ReceiveGptActivityTicketResp receiveGptActivityTicket(long userId, Long relationId) throws Exception;
 }

+ 199 - 0
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -9,6 +9,7 @@ import cn.hutool.core.lang.Assert;
 import cn.hutool.core.lang.Validator;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpResponse;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONObject;
@@ -20,6 +21,7 @@ import com.cyksj.common.snowflake.Sequence;
 import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
 import com.cyksj.common.util.Jsons;
 import com.cyksj.common.util.StringUtil;
+import com.cyksj.dto.RedisKey;
 import com.cyksj.enums.GatewayApiCode;
 import com.cyksj.mapper.*;
 import com.cyksj.mapper.codex.CodexUserRenewExpiryRecordMapper;
@@ -44,6 +46,7 @@ import com.cyksj.model.request.TicketLoginReq;
 import com.cyksj.model.response.NetflixErrorAccountStatus;
 import com.cyksj.model.response.NetflixRecoverResp;
 import com.cyksj.model.response.NetflixRefundInfoResp;
+import com.cyksj.model.response.ReceiveGptActivityTicketResp;
 import com.cyksj.model.views.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.ChatGptAccountService;
@@ -71,6 +74,7 @@ import com.ejlchina.searcher.SearchResult;
 import com.ejlchina.searcher.param.Operator;
 import com.ejlchina.searcher.util.MapBuilder;
 import com.ejlchina.searcher.util.MapUtils;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.github.rholder.retry.*;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -236,6 +240,10 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private final NetflixCompensateReceivedRecordMapper netflixCompensateReceivedRecordMapper;
 
+	private final TicketRedemptionCodeRecordMapper ticketRedemptionCodeRecordMapper;
+
+	private static final Date GPT_ACTIVITY_PAY_TIME_BEGIN = DateUtil.parse("2026-05-01 00:00:00");
+
 	@Override
 	public SearchResult<RenewalView> getMyTicket(long userId, Boolean isLoginPopularize, String customId, String account, String orderNo, Integer cmt) {
 		User u = userMapper.selectById(userId);
@@ -420,6 +428,9 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 						ticket.setIsComplete(orderDon.getStatus() == OrderDon.Status.complete ? true : false);
 					}
 				}
+                if (isGptApiQuotaSourceTicket(orderDon, ticket.getGoodsId(), ticket.getSkuNum())) {
+                    markGptApiQuotaReceiveStatus(userIds, ticket);
+                }
 				if (lang != null && !StrUtil.equals(UserPageLanguage.Language.zh_CN.name(), lang)) {
 					String languageSpecVal = backInfoTranslationsFrontService.getLanguageSpecVal(zhSkuId, lang);
 					if (StrUtil.isNotBlank(languageSpecVal)) {
@@ -572,6 +583,16 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 		return true;
 	}
 
+	private void markGptApiQuotaReceiveStatus(List<Long> userIds, RenewalView ticket) {
+        Integer successCount = ticketRedemptionCodeRecordMapper.selectCount(Wrappers.lambdaQuery(TicketRedemptionCodeRecord.class)
+                .in(TicketRedemptionCodeRecord::getUserId, userIds)
+                .eq(TicketRedemptionCodeRecord::getSourceGoodsId, ticket.getGoodsId())
+                .eq(TicketRedemptionCodeRecord::getSkuId, Constant.GPT_ACTIVITY_GIFT_SKU_ID)
+                .eq(TicketRedemptionCodeRecord::getSourceType, RedisKey.GPT_API_QUOTA_KEY)
+                .eq(TicketRedemptionCodeRecord::getStatus, "success"));
+        ticket.setCanReceiveGptApiQuota(successCount == 0);
+	}
+
 	@Override
 	public GroupsRelation getSimilarExpiredGroupRelation(GoodsDonSku sku, List<Long> errorsRelationIds, String ip) {
 		Long skuId = sku.getId();
@@ -2730,6 +2751,184 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
     }
 
+	@Override
+	public ReceiveGptActivityTicketResp receiveGptActivityTicket(long userId, Long sourceRelationId) throws Exception {
+		List<Long> userIds = userBindRelationService.getRelationUserIdList(userId, null);
+		if (CollUtil.isEmpty(userIds)) {
+			userIds = List.of(userId);
+		}
+		String lockKey = String.format("%s:%s", RedisKey.GPT_API_QUOTA_KEY, Collections.min(userIds));
+		if (!redisService.setNx(lockKey, userId, 60L)) {
+			throw BusinessRuntimeException.getInstance("领取中,请稍后再试");
+		}
+		try {
+			GroupsRelationView sourceRelation = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder()
+					.field(GroupsRelationView::getId, sourceRelationId)
+					.field(GroupsRelationView::getUserId, userIds).op(Operator.InList)
+					.field(GroupsRelationView::getStatus, List.of(GroupsRelation.Status.validity.name(), GroupsRelation.Status.outside.name())).op(Operator.InList)
+					.build());
+			if (sourceRelation == null || (sourceRelation.getExpiryTime() != null && sourceRelation.getExpiryTime().before(DateTime.now()))) {
+				throw BusinessRuntimeException.getInstance("车票不存在或已过期");
+			}
+
+			GoodsDonSku sourceSku = goodsDonSkuMapper.selectById(sourceRelation.getSkuId());
+			if (sourceSku == null) {
+				throw BusinessRuntimeException.getInstance("车票规格不存在");
+			}
+			OrderDon sourceOrder = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
+					.eq(OrderDon::getRelationId, sourceRelationId)
+					.in(OrderDon::getUserId, userIds)
+                    .eq(OrderDon::getOrderType, 1)
+                    .notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
+					.and(wrapper -> wrapper.ge(OrderDon::getPayTime, GPT_ACTIVITY_PAY_TIME_BEGIN)
+							.or(sub -> sub.isNull(OrderDon::getPayTime).ge(OrderDon::getCreatedTime, GPT_ACTIVITY_PAY_TIME_BEGIN)))
+					.orderByDesc(OrderDon::getId)
+					.last("limit 1"));
+			if (sourceOrder == null) {
+				throw BusinessRuntimeException.getInstance("不符合领取要求");
+			}
+            Long sourceGoodsId = sourceRelation.getGoodsId();
+            if (!isGptApiQuotaSourceTicket(sourceOrder, sourceGoodsId, sourceSku.getNum())) {
+                throw BusinessRuntimeException.getInstance("当前车票不符合领取条件");
+            }
+
+            Integer receivedCount = 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::getSourceGoodsId, sourceGoodsId)
+                    .eq(TicketRedemptionCodeRecord::getStatus, "success"));
+			if (receivedCount > 0) {
+				throw BusinessRuntimeException.getInstance("您已领取");
+			}
+
+			TicketRedemptionCodeRecord record = new TicketRedemptionCodeRecord();
+			record.setUserId(userId);
+			record.setRelationId(sourceRelationId);
+			record.setGoodsId(sourceRelation.getGoodsId());
+            record.setSourceGoodsId(sourceGoodsId);
+			record.setSkuId(Constant.GPT_ACTIVITY_GIFT_SKU_ID);
+			record.setSourceType(RedisKey.GPT_API_QUOTA_KEY);
+			record.setRedeemName("GPT代充/独立赠送");
+			record.setStatus("processing");
+			record.setRelationExpiryTime(sourceRelation.getExpiryTime());
+			ticketRedemptionCodeRecordMapper.insert(record);
+
+			GoodsDonSku giftSku = goodsDonSkuMapper.selectById(Constant.GPT_ACTIVITY_GIFT_SKU_ID);
+			GroupsRelation giftRelation = null;
+			try {
+				giftRelation = getReusableGptActivityGiftRelation(userIds, giftSku.getId(), sourceGoodsId);
+				if (giftRelation == null) {
+					Long giftRelationId = getTicket(userId, giftSku.getId(), Boolean.FALSE);
+					if (giftRelationId == null) {
+						throw BusinessRuntimeException.getInstance("系统繁忙,请重新领取");
+					}
+					giftRelation = groupsRelationMapper.selectById(giftRelationId);
+					if (giftRelation == null) {
+						throw BusinessRuntimeException.getInstance("系统繁忙,请重新领取");
+					}
+				}
+				GptActivityRedeemResult redeemResult = requestGptActivityRedeemCode(Constant.AI_API_SVC_DOMAIN, giftRelation, giftSku);
+				record.setRelationId(giftRelation.getId());
+				record.setGoodsId(giftSku.getGoodsId());
+                record.setSourceGoodsId(sourceGoodsId);
+				record.setRedeemCode(redeemResult.getRedeemCode());
+				record.setQuota(redeemResult.getQuota());
+				record.setRedeemCount(0);
+				record.setExpiredTime(redeemResult.getExpiredTime());
+				record.setStatus("success");
+				record.setRelationExpiryTime(giftRelation.getExpiryTime());
+				record.setApiResponse(redeemResult.getApiResponse());
+				ticketRedemptionCodeRecordMapper.updateById(record);
+
+				ReceiveGptActivityTicketResp resp = new ReceiveGptActivityTicketResp();
+				resp.setRelationId(giftRelation.getId());
+				resp.setRedeemCode(redeemResult.getRedeemCode());
+				return resp;
+			} catch (Exception e) {
+				record.setGoodsId(giftSku.getGoodsId());
+                record.setSourceGoodsId(sourceGoodsId);
+                record.setStatus("error");
+				record.setErrorMsg(StringUtil.getErrorText(e));
+				if (giftRelation != null) {
+					record.setRelationId(giftRelation.getId());
+					record.setRelationExpiryTime(giftRelation.getExpiryTime());
+				}
+				ticketRedemptionCodeRecordMapper.updateById(record);
+				throw e;
+			}
+		} finally {
+			redisService.del(lockKey);
+		}
+	}
+
+    private boolean isGptApiQuotaSourceTicket(OrderDon orderDon, Long goodsId, Integer skuNum) {
+        if (orderDon == null) {
+            return false;
+        }
+        return (Constant.GPT_RECHARGE_GOODS_ID.equals(goodsId)
+                || (Constant.GPT_PLUS_GID.equals(goodsId) && Objects.equals(skuNum, 1))) && orderDon.getCreatedTime().after(DateUtil.parse("2026-05-01"));
+    }
+
+    private GroupsRelation getReusableGptActivityGiftRelation(List<Long> userIds, Long giftSkuId, Long sourceGoodsId) {
+        TicketRedemptionCodeRecord retryRecord = ticketRedemptionCodeRecordMapper.selectOne(Wrappers.lambdaQuery(TicketRedemptionCodeRecord.class)
+                .in(TicketRedemptionCodeRecord::getUserId, userIds)
+                .eq(TicketRedemptionCodeRecord::getSkuId, giftSkuId)
+                .eq(TicketRedemptionCodeRecord::getSourceGoodsId, sourceGoodsId)
+                .eq(TicketRedemptionCodeRecord::getSourceType, RedisKey.GPT_API_QUOTA_KEY)
+                .eq(TicketRedemptionCodeRecord::getStatus, "error")
+                .isNotNull(TicketRedemptionCodeRecord::getRelationId)
+                .orderByDesc(TicketRedemptionCodeRecord::getId)
+                .last("limit 1"));
+        if (retryRecord != null) {
+            return groupsRelationMapper.selectById(retryRecord.getRelationId());
+        }
+        return null;
+    }
+	private GptActivityRedeemResult requestGptActivityRedeemCode(String apiDomain, GroupsRelation giftRelation, GoodsDonSku giftSku) throws Exception {
+        //默认5刀额度
+        Integer quota = 2500000;
+		Map<String, Object> params = new HashMap<>();
+		params.put("name", "GPT代充/独立赠送");
+		params.put("count", 1);
+		params.put("quota", quota);
+		String apiUrl = StrUtil.removeSuffix(apiDomain, "/") + "/api/redemption/";
+        String response = HttpRequest.post(apiUrl)
+                .header("Content-Type", "application/json")
+                .header("Authorization", "Bearer " + Constant.NEBULA_API_AUTH_KEY)
+                .header("New-Api-User", Constant.NEBULA_NEW_API_USER)
+                .body(Jsons.toJson(params))
+                .timeout(15000)
+                .execute()
+                .body();
+		if (StrUtil.isBlank(response)) {
+			throw BusinessRuntimeException.getInstance("系统异常,请重新兑换");
+		}
+		JsonNode responseNode = Jsons.parser().readTree(response);
+		if (!responseNode.path("success").asBoolean(false)) {
+			String message = responseNode.path("message").asText("系统异常,请重新兑换");
+			throw BusinessRuntimeException.getInstance(message);
+		}
+		JsonNode dataNode = responseNode.path("data");
+		if (!dataNode.isArray() || dataNode.size() == 0 || StrUtil.isBlank(dataNode.get(0).asText())) {
+			throw BusinessRuntimeException.getInstance("系统异常,请重新兑换");
+		}
+		GptActivityRedeemResult result = new GptActivityRedeemResult();
+		result.setRedeemCode(dataNode.get(0).asText());
+		result.setApiResponse(response);
+		result.setQuota(quota);
+		return result;
+	}
+
+	@lombok.Getter
+	@lombok.Setter
+	private static class GptActivityRedeemResult {
+		private String redeemCode;
+		private String apiResponse;
+		private Integer quota;
+		private Long expiredTime;
+	}
+
     private GroupsRelationView checkYoutubeTime(Long relationId, Long userId) {
 		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
 		GroupsRelationView relationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder()

+ 13 - 5
netflix-web/src/main/java/com/cyksj/web/controller/group/GroupRelationController.java

@@ -33,10 +33,7 @@ import com.cyksj.model.entity.*;
 import com.cyksj.model.manage.views.AccountView;
 import com.cyksj.model.manage.views.GroupsRelationView;
 import com.cyksj.model.request.*;
-import com.cyksj.model.response.GptStandbyResp;
-import com.cyksj.model.response.NetflixErrorAccountStatus;
-import com.cyksj.model.response.NetflixRecoverResp;
-import com.cyksj.model.response.NetflixRefundInfoResp;
+import com.cyksj.model.response.*;
 import com.cyksj.model.views.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.ChatGptAuthService;
@@ -83,7 +80,6 @@ import java.util.stream.Collectors;
 @RestController
 @RequestMapping("/applets/group/relation")
 public class GroupRelationController {
-
     private final BeanSearcher beanSearcher;
 
     private final OrderDonService orderDonService;
@@ -265,6 +261,17 @@ public class GroupRelationController {
         return GatewayResponse.SUCCESS.newBuilder().toResult(record.getRedeemCode());
     }
 
+    /**
+     * 购买GPT活动API兑换码额度
+     */
+    @PostMapping("/receive/code/quota")
+    @NoSubmit
+    public Result<ReceiveGptActivityTicketResp> receiveGptActivityTicket(@RequestBody @Validated ReceiveGptActivityTicketReq req) throws Exception {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ReceiveGptActivityTicketResp resp = groupRelationFrontService.receiveGptActivityTicket(userId, req.getRelationId());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
+    }
+
     /**
      * 记录查看ChatGPT车票密码
      */
@@ -1655,4 +1662,5 @@ public class GroupRelationController {
         OrderRefundDto orderRefundDto = groupRelationFrontService.getYoutubeRefundInfo(userId, relationId);
         return GatewayResponse.SUCCESS.newBuilder().toResult(orderRefundDto);
     }
+
 }