zoujiajian преди 1 година
родител
ревизия
6347a2ce0e

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

@@ -324,6 +324,8 @@ public interface Constant {
 	 */
 	String GROK_MIRROR_ONLY_ACCOUNT = "grok123";
 
+	String CLAUDE_CODE_ONLY_ACCOUNT = "claudecode123";
+
 	/**
 	 * 免登录渠道
 	 */

+ 13 - 1
netflix-dao/src/main/java/com/cyksj/model/entity/GroupsRelation.java

@@ -1,5 +1,6 @@
 package com.cyksj.model.entity;
 
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.ejlchina.searcher.bean.DbIgnore;
 import lombok.AllArgsConstructor;
@@ -159,8 +160,19 @@ public class GroupsRelation extends BaseEntity {
      */
     private Boolean isVip;
 
+    /**
+     * claude code积分上限
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer codePoints;
 
-    @AllArgsConstructor
+    /**
+     * claude code每小时恢复积分
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer codeCreditRecovery;
+
+	@AllArgsConstructor
     @Getter
     public enum Status{
         locking("锁定中"),

+ 4 - 3
netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeApiKeysStatusReq.java

@@ -4,7 +4,6 @@ import lombok.Getter;
 import lombok.Setter;
 
 import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 
 /**
  * 项目名: yhlxj11111111
@@ -22,6 +21,8 @@ public class ClaudeCodeApiKeysStatusReq {
 	@NotEmpty
 	private Integer id;
 
+	private String keyValue;
+
 	/**
 	 * 用户id 不传入
 	 */
@@ -31,6 +32,6 @@ public class ClaudeCodeApiKeysStatusReq {
 	 * api key状态
 	 * active(活跃), inactive(禁用), revoked(已撤销)
 	 */
-	@NotNull
-	private Boolean status;
+	@NotEmpty
+	private String status;
 }

+ 28 - 0
netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeDelReq.java

@@ -0,0 +1,28 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeDelReq
+ * 创建者: JavaZou
+ * 创建时间:2025/8/11 10:40
+ */
+@Getter
+@Setter
+public class ClaudeCodeDelReq {
+	@NotNull
+	private Long keyId;
+
+	/**
+	 * 无需传入
+	 */
+	private Long userId;
+
+	@NotEmpty
+	private String keyValue;
+}

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

@@ -234,6 +234,18 @@ public class RenewalView {
     @DbField("gr.is_vip")
     private Boolean isVip;
 
+    /**
+     * claude code积分上限
+     */
+    @DbField("gr.code_points")
+    private Integer codePoints;
+
+    /**
+     * claude code每小时恢复积分
+     */
+    @DbField("gr.code_credit_recovery")
+    private Integer codeCreditRecovery;
+
     @DbIgnore
     private Date payTime;
 

+ 12 - 5
netflix-service/src/main/java/com/cyksj/service/claude/ClaudeCodeService.java

@@ -3,6 +3,7 @@ package com.cyksj.service.claude;
 import com.cyksj.model.entity.GoodsDonSku;
 import com.cyksj.model.request.ClaudeCodeApiKeysReq;
 import com.cyksj.model.request.ClaudeCodeApiKeysStatusReq;
+import com.cyksj.model.request.ClaudeCodeDelReq;
 import com.cyksj.model.response.ClaudeCodePointsHistoryResp;
 import com.cyksj.model.response.claudecode.ClaudeCodeResp;
 import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
@@ -21,22 +22,22 @@ public interface ClaudeCodeService {
 	/**
 	 * 生成claude code用户套餐信息
 	 */
-	void generateClaudeCodeUserInfo(Long relationId, Long userId, GoodsDonSku sku);
+	void generateOrUpdateClaudeCodeUserInfo(Long relationId, Long userId, GoodsDonSku sku, Integer orderType);
 
 	/**
 	 * 用户创建api keys
 	 */
-	void createApiKeys(ClaudeCodeApiKeysReq req);
+	void createApiKeys(ClaudeCodeApiKeysReq req) throws Exception;
 
 	/**
 	 * 用户更新api key状态
 	 */
-	void updateApiKeysStatus(ClaudeCodeApiKeysStatusReq req);
+	void updateApiKeysStatus(ClaudeCodeApiKeysStatusReq req) throws Exception;
 
 	/**
 	 * 删除api key
 	 */
-	void deleteApiKeysById(Long userId, Long keyId);
+	void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception;
 
 	/**
 	 * 用户api keys列表
@@ -50,7 +51,13 @@ public interface ClaudeCodeService {
 
 	ClaudeCodePointsHistoryResp getUserPointsDetail(Long userId, String type, Integer page, Integer limit) throws Exception;
 
-	void getUserPointsStatisticsData(Long userId) throws Exception;
+	ClaudeCodeResp getUserPointsStatisticsData(Long userId) throws Exception;
 
 	ClaudeCodeResp getUserDashboard(Long userId) throws Exception;
+
+	ClaudeCodeResp getUserBalance(Long userId) throws Exception;
+
+	ClaudeCodeResp getCreditsAnalytics(Long userId, String start, String end, String tz, String page, String limit, String order, String type) throws Exception;
+
+	void invalidateUserCache(Long relationUserId);
 }

+ 172 - 29
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java

@@ -1,22 +1,30 @@
 package com.cyksj.service.claude.impl;
 
 import cn.hutool.core.date.DateTime;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.http.HttpResponse;
 import cn.hutool.http.HttpUtil;
+import cn.hutool.http.Method;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.constant.Constant;
+import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.util.Jsons;
 import com.cyksj.mapper.GroupsRelationMapper;
 import com.cyksj.model.entity.GoodsDonSku;
 import com.cyksj.model.entity.GroupsRelation;
+import com.cyksj.model.manage.views.GroupsRelationView;
 import com.cyksj.model.request.ClaudeCodeApiKeysReq;
 import com.cyksj.model.request.ClaudeCodeApiKeysStatusReq;
+import com.cyksj.model.request.ClaudeCodeDelReq;
 import com.cyksj.model.response.ClaudeCodePointsHistoryResp;
 import com.cyksj.model.response.claudecode.ClaudeCodeResp;
 import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
 import com.cyksj.model.views.ClaudeCodeUserPackageView;
 import com.cyksj.service.claude.ClaudeCodeService;
 import com.cyksj.service.user.UserBindRelationService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -40,12 +48,14 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 
 	private final GroupsRelationMapper relationMapper;
 
+	private final BeanSearcher beanSearcher;
+
 	private final static String CLAUDE_CODE_API_PREFIX = "https://relay01.yhlxj.com/";
 
 	public static final String CLAUDE_CODE_API_ADMIN_KEY = "claudecodeyhlxjclaude";
 
 	@Override
-	public void generateClaudeCodeUserInfo(Long relationId, Long userId, GoodsDonSku sku) {
+	public void generateOrUpdateClaudeCodeUserInfo(Long relationId, Long userId, GoodsDonSku sku, Integer orderType) {
 		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
 		GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).eq(GroupsRelation::getId, relationId).in(GroupsRelation::getUserId, userIdList));
 		Date expiryTime = relation.getExpiryTime();
@@ -53,17 +63,26 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 		Integer codeCreditRecovery = sku.getCodeCreditRecovery();
 		//生成claude code用户套餐
 		try {
-			createOrUpdateClaudeCodeUserPackage(userId, sku.getSpecVal(), codePoints, codeCreditRecovery, expiryTime);
+			createOrUpdateClaudeCodeUserPackage(relation.getUserId(), sku.getSpecVal(), codePoints, codeCreditRecovery, expiryTime);
+			//设置车位的套餐
+			relation.setCodePoints(codePoints);
+			relation.setCodeCreditRecovery(codeCreditRecovery);
+			relationMapper.updateById(relation);
+			if (orderType == 2) {
+				invalidateUserCache(userId);
+			}
 		} catch (Exception e) {
 
 		}
 	}
 
 	@Override
-	public void createApiKeys(ClaudeCodeApiKeysReq req) {
+	public void createApiKeys(ClaudeCodeApiKeysReq req) throws Exception {
 		Long userId = req.getUserId();
 		if (checkClaudeCodeUser(userId)) {
-			String apiKeyUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId);
+			Long claudeCodeUserId = getClaudeCodeUserId(userId);
+			Assert.notNull(claudeCodeUserId, "您的claude code账号已过期");
+			String apiKeyUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", claudeCodeUserId);
 			Map<String, Object> parmas = new HashMap<>();
 			parmas.put("name", req.getName());
 			Integer expiresDays = req.getExpiresDays();
@@ -71,62 +90,159 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 				parmas.put("expires_days", expiresDays);
 			}
 			String responseBody = executeClaudeCodePostApi(apiKeyUrl, parmas.toString());
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			if (!claudeCodeResp.getSuccess()) {
+				throw BusinessRuntimeException.getInstance("创建失败");
+			}
 		}
 	}
 
 	@Override
-	public void updateApiKeysStatus(ClaudeCodeApiKeysStatusReq req) {
-
+	public void updateApiKeysStatus(ClaudeCodeApiKeysStatusReq req) throws Exception {
+		Long userId = req.getUserId();
+		Integer keyId = req.getId();
+		String status = req.getStatus();
+		String keyValue = req.getKeyValue();
+		if (checkClaudeCodeUser(userId)) {
+			Long claudeCodeUserId = getClaudeCodeUserId(userId);
+			Assert.notNull(claudeCodeUserId, "您的claude code账号已过期");
+			String apiKeyUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/keys/{userId}/{keyId}", claudeCodeUserId, keyId);
+			Map<String, Object> parmas = new HashMap<>();
+			parmas.put("status", status);
+			String responseBody = executeClaudeCodePostApi(apiKeyUrl, parmas.toString());
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			if (claudeCodeResp.getSuccess()) {
+				//更新用户apikey缓存
+				updateUserApiKeysCache(keyValue);
+			}
+		}
 	}
 
 	@Override
-	public void deleteApiKeysById(Long userId, Long keyId) {
+	public void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception {
+		Long userId = delReq.getUserId();
+		Long keyId = delReq.getKeyId();
+		String keyValue = delReq.getKeyValue();
+		if (checkClaudeCodeUser(userId)) {
+			Long claudeCodeUserId = getClaudeCodeUserId(userId);
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/keys/{userId}/{keyId}", claudeCodeUserId, keyId);
+			HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).execute();
+			String responseBody = execute.body();
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			if (claudeCodeResp.getSuccess()) {
+				//更新用户apikey缓存
+				updateUserApiKeysCache(keyValue);
+			}
+		}
+	}
 
+	/**
+	 * 更新api key缓存
+	 * @param keyValue
+	 */
+	private void updateUserApiKeysCache(String keyValue) {
+		String url = CLAUDE_CODE_API_PREFIX + "api/cache/invalidate/apikey";
+		Map<String, Object> parmas = new HashMap<>();
+		parmas.put("api_key", keyValue);
+		executeClaudeCodePostApi(url,parmas.toString());
 	}
 
 	@Override
 	public List<ClaudeCodeUserApiKeysView> getUserApiKeys(long userId) throws Exception {
-		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId);
-		String responseBody = executeClaudeCodeGetApi(url);
-		ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
-		if (claudeCodeResp.getSuccess()) {
-			return Jsons.parseList(claudeCodeResp.getData(), ClaudeCodeUserApiKeysView.class);
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", userId);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			if (claudeCodeResp.getSuccess()) {
+				return Jsons.parseList(claudeCodeResp.getData(), ClaudeCodeUserApiKeysView.class);
+			}
 		}
 		return null;
 	}
 
 	@Override
 	public ClaudeCodeUserPackageView getUserPackage(long userId) throws Exception {
-		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
-		String responseBody = executeClaudeCodeGetApi(url);
-		ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
-		if (claudeCodeResp.getSuccess()) {
-			return Jsons.parseObject(responseBody, ClaudeCodeUserPackageView.class);
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			if (claudeCodeResp.getSuccess()) {
+				return Jsons.parseObject(responseBody, ClaudeCodeUserPackageView.class);
+			}
 		}
 		return null;
 	}
 
 	@Override
 	public ClaudeCodePointsHistoryResp getUserPointsDetail(Long userId, String type, Integer page, Integer limit) throws Exception {
-		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/{userId}/credits/history?page=%s&limit=%s&type=%s", userId, page, limit, type);
-		String responseBody = executeClaudeCodeGetApi(url);
-		ClaudeCodePointsHistoryResp historyResp = Jsons.parseObject(responseBody, ClaudeCodePointsHistoryResp.class);
-		return historyResp;
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/{userId}/credits/history?page=%s&limit=%s&type=%s", claudeCodeUserId, page, limit, type);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodePointsHistoryResp historyResp = Jsons.parseObject(responseBody, ClaudeCodePointsHistoryResp.class);
+			return historyResp;
+		}
+		return null;
 	}
 
 	@Override
-	public void getUserPointsStatisticsData(Long userId) throws Exception {
-		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/usage/stats", userId);
-		String responseBody = executeClaudeCodeGetApi(url);
-		ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+	public ClaudeCodeResp getUserPointsStatisticsData(Long userId) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/usage/stats", userId);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			return claudeCodeResp;
+		}
+		return null;
 	}
 
 	@Override
 	public ClaudeCodeResp getUserDashboard(Long userId) throws Exception {
-		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/dashboard", userId);
-		String responseBody = executeClaudeCodeGetApi(url);
-		ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
-		return claudeCodeResp;
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/dashboard", userId);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			return claudeCodeResp;
+		}
+		return null;
+	}
+
+	@Override
+	public ClaudeCodeResp getUserBalance(Long userId) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/balance", userId);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			return claudeCodeResp;
+		}
+		return null;
+	}
+
+	@Override
+	public ClaudeCodeResp getCreditsAnalytics(Long userId, String start, String end, String tz, String page, String limit, String order, String type) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/credits/analytics", userId);
+			String responseBody = executeClaudeCodeGetApi(url);
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			return claudeCodeResp;
+		}
+		return null;
+	}
+
+
+	/**
+	 * 用户缓存失效
+	 */
+	@Override
+	public void invalidateUserCache(Long userId) {
+		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/cache/invalidate/user/%s", userId);
+		executeClaudeCodeGetApi(url);
 	}
 
 	public void createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer codePoints, Integer codeCreditRecovery, Date expiryTime) throws Exception {
@@ -155,6 +271,21 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 		return count > 0;
 	}
 
+	/**
+	 * 获取claude code用户id
+	 */
+	public Long getClaudeCodeUserId(Long userId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder()
+				.field(GroupsRelationView::getUserId, userIdList).op(Operator.InList)
+				.field(GroupsRelationView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
+				.field(GroupsRelationView::getExpiryTime, DateTime.now())
+				.build());
+		if (groupsRelationView != null) {
+			return groupsRelationView.getUserId();
+		}
+		return null;
+	}
 
 	/**
 	 * claude code GET请求
@@ -168,6 +299,18 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 		return execute.body();
 	}
 
+	/**
+	 * claude code GET请求
+	 */
+	public String executeClaudeCodeDeleteApi(String url) {
+		HttpResponse execute = HttpUtil.createPost(url)
+				.header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY)
+				.setConnectionTimeout(3000)
+				.setReadTimeout(3000)
+				.execute();
+		return execute.body();
+	}
+
 
 	/**
 	 * claude code Post请求

+ 5 - 1
netflix-service/src/main/java/com/cyksj/service/groups/impl/GroupFunServiceImpl.java

@@ -100,7 +100,7 @@ public class GroupFunServiceImpl implements GroupsFuncService {
 		Boolean isMirror = sku.getIsMirror();
 		Boolean isCar = sku.getIsCar();
 		String account = null;
-		if ((goodsId == 88 && isMirror) || (goodsId == 87 && isMirror) || (goodsId == 18 && isCar) || (goodsId == 26 && isMirror) || (goodsId == 75 && isCar)) {
+		if ((goodsId == Constant.CLAUDE_CODE_GOODS_ID) || (goodsId == 88 && isMirror) || (goodsId == 87 && isMirror) || (goodsId == 18 && isCar) || (goodsId == 26 && isMirror) || (goodsId == 75 && isCar)) {
 			if (goodsId == 18) {
 				account = Constant.GPT_CAR_ONLY_ACCOUNT;
 			}
@@ -116,6 +116,10 @@ public class GroupFunServiceImpl implements GroupsFuncService {
 			if (goodsId == 88) {
 				account = Constant.GROK_MIRROR_ONLY_ACCOUNT;
 			}
+			//claude code
+			if (goodsId == Constant.CLAUDE_CODE_GOODS_ID) {
+				account = Constant.CLAUDE_CODE_ONLY_ACCOUNT;
+			}
 			AccountView accountView = beanSearcher.searchFirst(AccountView.class, MapUtils.builder()
 					.field(AccountView::getGoodsId, goodsId)
 					.field(AccountView::getAccount, account).

+ 4 - 0
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderCommonServiceImpl.java

@@ -139,6 +139,10 @@ public class OrderCommonServiceImpl implements OrderCommonService {
 						groupsRelationMapper.updateById(relation);
 						if (expiryTime.compareTo(now) <= 0) {
 							clearService.clearTicket(relation, UserTicketClearedRecord.Source.refund);
+						} else {
+							if (sku.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID) {
+								//是否还存在其他 TODO
+							}
 						}
 					}
 				}

+ 21 - 5
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -456,6 +456,15 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 				throw new BusinessRuntimeException("您有未支付订单.");
 			}
 		}
+		//claude code限购
+		if (goodsId == Constant.CLAUDE_CODE_GOODS_ID) {
+			//是否拥有这张车票
+			List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+			Integer selectCount = groupsRelationMapper.selectCountByGoodsId(userIdList, Constant.CLAUDE_CODE_GOODS_ID, DateTime.now());
+			if (selectCount > 0) {
+				throw BusinessRuntimeException.getInstance("claude code车票限购一次");
+			}
+		}
 		//校验奈飞限购信息
 		checkNfPayLimit(goodsId, userId);
 		GroupsRelation relation = null;
@@ -1230,6 +1239,16 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 			}
 		}
 
+		//claude code续费 校验续费套餐等级
+		if (goodsDon.getId() == Constant.CLAUDE_CODE_GOODS_ID) {
+			Integer codePoints = relation.getCodePoints();
+			GoodsDonSku renewSku = goodsDonSkuMapper.selectById(renewSkuId);
+			//claude code积分上限
+			if (renewSku.getCodePoints() < codePoints) {
+				throw BusinessRuntimeException.getInstance("选择的续费套餐错误");
+			}
+		}
+
 		String payOpenId = user.getOpenId();
 
 		/* 生成订单 */
@@ -3136,11 +3155,8 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 		//购买claude code
 		if (orderDon.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID) {
-			//首次购买
-			if (orderDon.getOrderType() == 1) {
-				//生成所需的claude code用户信息
-				claudeCodeService.generateClaudeCodeUserInfo(orderDon.getRelationId(), orderDon.getUserId(), sku);
-			}
+			//生成所需的claude code用户信息
+			claudeCodeService.generateOrUpdateClaudeCodeUserInfo(orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
 		}
 	}
 

+ 9 - 0
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java

@@ -1,5 +1,6 @@
 package com.cyksj.service.relation.impl;
 
+import com.cyksj.common.constant.Constant;
 import com.cyksj.mapper.GoodsDonSkuMapper;
 import com.cyksj.mapper.GroupsMapper;
 import com.cyksj.mapper.GroupsRelationMapper;
@@ -8,6 +9,7 @@ import com.cyksj.model.entity.GoodsDonSku;
 import com.cyksj.model.entity.GroupsRelation;
 import com.cyksj.model.entity.UserTicketClearedRecord;
 import com.cyksj.model.manage.views.GroupsRelationView;
+import com.cyksj.service.claude.ClaudeCodeService;
 import com.cyksj.service.mange.MirrorUserCleatService;
 import com.cyksj.service.relation.GroupRelationClearService;
 import com.ejlchina.searcher.BeanSearcher;
@@ -40,6 +42,8 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 
 	private final MirrorUserCleatService mirrorUserCleatService;
 
+	private final ClaudeCodeService claudeCodeService;
+
 	@Override
 	public void clearTicket(GroupsRelation relation, UserTicketClearedRecord.Source source) {
 		Long relationId = relation.getId();
@@ -129,6 +133,11 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 				if (source != UserTicketClearedRecord.Source.vip_deduct) {
 					mirrorUserCleatService.delMirrorUser(groupsRelation.getGoodsId(), groupsRelation.getId(), userTicketClearedRecord.getId());
 				}
+				//claude code
+				if (groupsRelation.getGoodsId() != null && groupsRelation.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID) {
+					//用户失效 车票用户id
+					claudeCodeService.invalidateUserCache(groupsRelation.getUserId());
+				}
 			}
 		}
 	}

+ 86 - 10
netflix-web/src/main/java/com/cyksj/web/controller/claude/ClaudeCodeController.java

@@ -1,19 +1,30 @@
 package com.cyksj.web.controller.claude;
 
+import cn.hutool.core.date.DateTime;
+import com.cyksj.common.constant.Constant;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.request.ClaudeCodeApiKeysReq;
 import com.cyksj.model.request.ClaudeCodeApiKeysStatusReq;
+import com.cyksj.model.request.ClaudeCodeDelReq;
 import com.cyksj.model.response.ClaudeCodePointsHistoryResp;
 import com.cyksj.model.response.claudecode.ClaudeCodeResp;
 import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
 import com.cyksj.model.views.ClaudeCodeUserPackageView;
+import com.cyksj.model.views.RenewalView;
 import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.user.UserBindRelationService;
 import com.cyksj.web.util.StpUserUtil;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 
 /**
@@ -28,11 +39,17 @@ import java.util.List;
 public class ClaudeCodeController {
 	private final ClaudeCodeService claudeCodeService;
 
+	private final BeanSearcher beanSearcher;
+
+	private final HttpServletRequest request;
+
+	private final UserBindRelationService userBindRelationService;
+
 	/**
 	 * 用户创建API KEY
 	 */
 	@PostMapping("/create/apiKeys")
-	public Result<String> createApiKeys(@RequestBody @Validated ClaudeCodeApiKeysReq req) {
+	public Result<String> createApiKeys(@RequestBody @Validated ClaudeCodeApiKeysReq req) throws Exception {
 		long userId = StpUserUtil.getLoginIdAsLong();
 		req.setUserId(userId);
 		claudeCodeService.createApiKeys(req);
@@ -43,7 +60,7 @@ public class ClaudeCodeController {
 	 * 更新api key状态
 	 */
 	@PutMapping("/update/apiKeys/status")
-	public Result<String> updateApiKeysStatus(@RequestBody @Validated ClaudeCodeApiKeysStatusReq req) {
+	public Result<String> updateApiKeysStatus(@RequestBody @Validated ClaudeCodeApiKeysStatusReq req) throws Exception {
 		long userId = StpUserUtil.getLoginIdAsLong();
 		req.setUserId(userId);
 		claudeCodeService.updateApiKeysStatus(req);
@@ -63,14 +80,15 @@ public class ClaudeCodeController {
 	/**
 	 * 删除api key
 	 */
-	@DeleteMapping("/delete/apiKeys/{id}")
-	public Result<String> deleteApiKeysById(@PathVariable Long id) {
+	@DeleteMapping("/delete/apiKeys")
+	public Result<String> deleteApiKeysById(@RequestBody ClaudeCodeDelReq delReq) throws Exception {
 		long userId = StpUserUtil.getLoginIdAsLong();
-		claudeCodeService.deleteApiKeysById(userId, id);
+		delReq.setUserId(userId);
+		claudeCodeService.deleteApiKeysById(delReq);
 		return GatewayResponse.SUCCESS.newBuilder().toResult();
 	}
 
-	//================================================用户积分============================================================
+	//================================================用户============================================================
 
 
 	/**
@@ -83,6 +101,36 @@ public class ClaudeCodeController {
 		return GatewayResponse.SUCCESS.newBuilder().toResult(claudeCodeUserPackageView);
 	}
 
+	/**
+	 * 用户订阅车票信息
+	 */
+	@GetMapping("/get/user/subs/info")
+	public Result<RenewalView> getUserSubsInfo() {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		RenewalView renewalView = beanSearcher.searchFirst(RenewalView.class, MapUtils.flatBuilder(request.getParameterMap())
+				.field(RenewalView::getUserId, userIdList).op(Operator.InList)
+				.field(RenewalView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
+				.field(RenewalView::getExpiryTime, DateTime.now()).op(Operator.GreaterThan)
+				.build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(renewalView);
+	}
+
+	/**
+	 * 用户订阅历史
+	 */
+	@GetMapping("/get/user/orders")
+	public Result<SearchResult<OrderDonView>> getUserOrders() {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		SearchResult<OrderDonView> search = beanSearcher.search(OrderDonView.class, MapUtils.flatBuilder(request.getParameterMap())
+				.field(OrderDonView::getUserId, userIdList).op(Operator.InList)
+				.field(OrderDonView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
+				.field(OrderDonView::getStatus, Constant.noOrderAllStatus).op(Operator.NotIn)
+				.build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
+
 	/**
 	 * 用户积分明细
 	 */
@@ -97,10 +145,10 @@ public class ClaudeCodeController {
 	 * 用户使用数据统计
 	 */
 	@GetMapping("/get/user/points/statistics/data")
-	public Result<String> getUserPointsStatisticsData() throws Exception {
+	public Result<ClaudeCodeResp> getUserPointsStatisticsData() throws Exception {
 		long userId = StpUserUtil.getLoginIdAsLong();
-		claudeCodeService.getUserPointsStatisticsData(userId);
-		return GatewayResponse.SUCCESS.newBuilder().toResult();
+		ClaudeCodeResp resp = claudeCodeService.getUserPointsStatisticsData(userId);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
 	}
 
 	/**
@@ -108,10 +156,38 @@ public class ClaudeCodeController {
 	 * 返回 当前订阅/余额卡片 + 今日汇总 + 今日请求列表
 	 */
 	@GetMapping("/get/dashboard")
-	public Result<ClaudeCodeResp> getUserDashboard() {
+	public Result<ClaudeCodeResp> getUserDashboard() throws Exception {
 		long userId = StpUserUtil.getLoginIdAsLong();
 		ClaudeCodeResp resp = claudeCodeService.getUserDashboard(userId);
 		return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
 	}
 
+
+	/**
+	 * 余额卡片
+	 * 单独返回余额卡片数据
+	 */
+	@GetMapping("/get/balance")
+	public Result<ClaudeCodeResp> getUserBalance() throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodeResp resp = claudeCodeService.getUserBalance(userId);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
+	}
+
+	/**
+	 * 图表 + 明细联动(每小时聚合)
+	 * - `start` (string, required): `YYYY-MM-DD HH:MM:SS`,例如 `2025-08-08 12:00:00`
+	 * - `end` (string, required): `YYYY-MM-DD HH:MM:SS`,需大于 `start`
+	 * - `tz` (string, optional): 时区,默认 `Asia/Shanghai`
+	 * - `page` (int, optional): 明细页码,默认 1
+	 * - `limit` (int, optional): 明细每页数量,默认 20,最大 100
+	 * - `order` (string, optional): `asc|desc`,默认 `desc`
+	 * - `type` (string, optional): 明细类型过滤,`consume|purchase|recovery|refund|reward|adjustment|all`,默认 `all`
+	 */
+	@GetMapping("/get/credits/analytics")
+	public Result<ClaudeCodeResp> getCreditsAnalytics(String start, String end, String tz, String page, String limit, String order, String type) throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodeResp resp = claudeCodeService.getCreditsAnalytics(userId, start, end, tz, page, limit, order, type);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
+	}
 }