Эх сурвалжийг харах

Merge branch 'claude_code_func'

# Conflicts:
#	netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java
zoujiajian 1 жил өмнө
parent
commit
f92cde148a
54 өөрчлөгдсөн 2422 нэмэгдсэн , 12 устгасан
  1. 7 0
      netflix-common/src/main/java/com/cyksj/common/constant/Constant.java
  2. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/ClaudeCodeUserMapper.java
  3. 17 0
      netflix-dao/src/main/java/com/cyksj/mapper/ClaudeCodeUserRenewExpiryRecordMapper.java
  4. 1 0
      netflix-dao/src/main/java/com/cyksj/mapper/GroupsRelationMapper.java
  5. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/AnnouncementMapper.java
  6. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthClientMapper.java
  7. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthTokenMapper.java
  8. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/RelayServerMapper.java
  9. 33 0
      netflix-dao/src/main/java/com/cyksj/model/entity/Announcement.java
  10. 72 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeCodeUser.java
  11. 34 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeCodeUserRenewExpiryRecord.java
  12. 12 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java
  13. 7 1
      netflix-dao/src/main/java/com/cyksj/model/entity/GroupsRelation.java
  14. 29 0
      netflix-dao/src/main/java/com/cyksj/model/entity/OAuthClient.java
  15. 27 0
      netflix-dao/src/main/java/com/cyksj/model/entity/OAuthToken.java
  16. 24 0
      netflix-dao/src/main/java/com/cyksj/model/entity/RelayServer.java
  17. 10 0
      netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java
  18. 3 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/GroupsRelationView.java
  19. 32 0
      netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeApiKeysReq.java
  20. 37 0
      netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeApiKeysStatusReq.java
  21. 24 0
      netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeDelReq.java
  22. 68 0
      netflix-dao/src/main/java/com/cyksj/model/response/ClaudeCodePointsHistoryResp.java
  23. 20 0
      netflix-dao/src/main/java/com/cyksj/model/response/claudecode/ClaudeCodeResp.java
  24. 27 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodePackageView.java
  25. 74 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserApiKeysView.java
  26. 52 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserInfoView.java
  27. 75 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserPackageView.java
  28. 70 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserPointsDetailView.java
  29. 10 0
      netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuFrontView.java
  30. 12 0
      netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuRenewView.java
  31. 12 0
      netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuView.java
  32. 12 0
      netflix-dao/src/main/java/com/cyksj/model/views/RenewUpgradePackageFrontView.java
  33. 10 0
      netflix-dao/src/main/java/com/cyksj/model/views/TicketRenewUpgradeView.java
  34. 18 0
      netflix-dao/src/main/resources/mapper/ClaudeCodeUserRenewExpiryRecordMapper.xml
  35. 11 0
      netflix-dao/src/main/resources/mapper/GroupRelationMapper.xml
  36. 35 0
      netflix-service/src/main/java/com/cyksj/service/claude/AnnouncementService.java
  37. 69 0
      netflix-service/src/main/java/com/cyksj/service/claude/ClaudeCodeService.java
  38. 14 0
      netflix-service/src/main/java/com/cyksj/service/claude/OAuthClaudeCodeService.java
  39. 28 0
      netflix-service/src/main/java/com/cyksj/service/claude/RelayService.java
  40. 73 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/AnnouncementServiceImpl.java
  41. 548 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java
  42. 117 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/OAuthClaudeCodeServiceImpl.java
  43. 57 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/RelayServiceImpl.java
  44. 35 0
      netflix-service/src/main/java/com/cyksj/service/exchange/impl/ExchangeCodeServiceImpl.java
  45. 5 1
      netflix-service/src/main/java/com/cyksj/service/groups/impl/GroupFunServiceImpl.java
  46. 1 1
      netflix-service/src/main/java/com/cyksj/service/mange/impl/CmsOrderDonServiceImpl.java
  47. 61 1
      netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java
  48. 4 0
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderCommonServiceImpl.java
  49. 33 0
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java
  50. 19 4
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java
  51. 33 3
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java
  52. 377 0
      netflix-web/src/main/java/com/cyksj/web/controller/claude/ClaudeCodeController.java
  53. 0 0
      netflix-web/src/main/resources/application-prd.yml
  54. 12 1
      netflix-web/src/main/resources/application-pre.yml

+ 7 - 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";
+
 	/**
 	 * 免登录渠道
 	 */
@@ -631,4 +633,9 @@ public interface Constant {
 	 * GPT代充平台id
 	 */
 	Long GPT_RECHARGE_GOODS_ID = 40L;
+
+	/**
+	 * claude code平台id
+	 */
+	Long CLAUDE_CODE_GOODS_ID = 102L;
 }

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/ClaudeCodeUserMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeCodeUser;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/8/11 14:00
+ */
+public interface ClaudeCodeUserMapper extends BaseMapper<ClaudeCodeUser> {
+}

+ 17 - 0
netflix-dao/src/main/java/com/cyksj/mapper/ClaudeCodeUserRenewExpiryRecordMapper.java

@@ -0,0 +1,17 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeCodeUserRenewExpiryRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserRenewExpiryRecordMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/8/11 13:39
+ */
+public interface ClaudeCodeUserRenewExpiryRecordMapper extends BaseMapper<ClaudeCodeUserRenewExpiryRecord> {
+	ClaudeCodeUserRenewExpiryRecord getOtherRenewSku(@Param("orderId") Long orderId, @Param("relationId") Long relationId, @Param("userIds") List<Long> userIds);;
+}

+ 1 - 0
netflix-dao/src/main/java/com/cyksj/mapper/GroupsRelationMapper.java

@@ -84,4 +84,5 @@ public interface GroupsRelationMapper extends BaseMapper<GroupsRelation> {
 
 	List<UserTicketView> selectUserTickets(@Param("userIds") List<Long> userIds, @Param("goodsId") Long goodsId, @Param("skuId") Long skuId);
 
+	Integer selectCountByGoodsId(@Param("userIds") List<Long> userIdList, @Param("goodsId") Long goodsId, @Param("now") DateTime now);
 }

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/claudecode/AnnouncementMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper.claudecode;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.Announcement;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 公告Mapper
+ */
+@Mapper
+public interface AnnouncementMapper extends BaseMapper<Announcement> {
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthClientMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper.claudecode;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.OAuthClient;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * OAuth客户端Mapper
+ */
+@Mapper
+public interface OAuthClientMapper extends BaseMapper<OAuthClient> {
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthTokenMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper.claudecode;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.OAuthToken;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * OAuth令牌Mapper
+ */
+@Mapper
+public interface OAuthTokenMapper extends BaseMapper<OAuthToken> {
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/claudecode/RelayServerMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper.claudecode;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.RelayServer;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 中继服务器Mapper
+ */
+@Mapper
+public interface RelayServerMapper extends BaseMapper<RelayServer> {
+}

+ 33 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/Announcement.java

@@ -0,0 +1,33 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 公告实体
+ */
+@Data
+@TableName("announcements")
+public class Announcement extends BaseEntity {
+
+    
+    private String title;
+    
+    private String description;
+    
+    private String type;
+    
+    private String language;
+    
+    private LocalDateTime startDate;
+    
+    private LocalDateTime endDate;
+    
+    private Integer status;
+    
+    private Integer priority;
+}

+ 72 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeCodeUser.java

@@ -0,0 +1,72 @@
+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.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUser
+ * 创建者: JavaZou
+ * 创建时间:2025/8/11 13:59
+ */
+@Getter
+@Setter
+public class ClaudeCodeUser extends BaseEntity{
+	private Long userId;
+
+	/**
+	 * 套餐
+	 */
+	private String planName;
+
+	private Long relationId;
+
+	@DbIgnore
+	@TableField(exist = false)
+	private Integer oriCodePoints;
+
+	/**
+	 * claude code积分上限
+	 */
+	@TableField(updateStrategy = FieldStrategy.IGNORED)
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	@TableField(updateStrategy = FieldStrategy.IGNORED)
+	private Integer codeCreditRecovery;
+
+	/**
+	 * code 有效期
+	 */
+	private Date expiryTime;
+
+	/**
+	 * 续费升级订单id
+	 */
+	@TableField(updateStrategy = FieldStrategy.IGNORED)
+	private Long renewOrderId;
+
+	/**
+	 * 续费升级skuid
+	 */
+	@TableField(updateStrategy = FieldStrategy.IGNORED)
+	private Long renewSkuId;
+
+	/**
+	 * 续费升级过期时间
+	 */
+	@TableField(updateStrategy = FieldStrategy.IGNORED)
+	private Date renewExpiryTime;
+
+	/**
+	 * 需要重试 默认false
+	 */
+	private Boolean isRetry;
+}

+ 34 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeCodeUserRenewExpiryRecord.java

@@ -0,0 +1,34 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserRenewExpiryRecord
+ * 创建者: JavaZou
+ * 创建时间:2025/8/11 13:37
+ */
+@Getter
+@Setter
+public class ClaudeCodeUserRenewExpiryRecord extends BaseEntity{
+	private Long userId;
+
+	private Long relationId;
+
+	private Long renewSkuId;
+
+	private Long orderId;
+
+	/**
+	 * 过期时间
+	 */
+	private Date expiryTime;
+
+	/**
+	 * 是否失效 默认false
+	 */
+	private Boolean isValid;
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java

@@ -267,4 +267,16 @@ public class GoodsDonSku extends BaseEntity {
      */
     @TableField(updateStrategy = FieldStrategy.IGNORED)
     private String remark;
+
+    /**
+     * claude code积分上限
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer codePoints;
+
+    /**
+     * claude code每小时恢复积分
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer codeCreditRecovery;
 }

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

@@ -169,8 +169,14 @@ public class GroupsRelation extends BaseEntity {
      */
     private Boolean isVip;
 
+    /**
+     * 是否是claude code 续费升级
+     */
+    @DbIgnore
+    @TableField(exist = false)
+    private Boolean isClaudeCodeUpgrade;
 
-    @AllArgsConstructor
+	@AllArgsConstructor
     @Getter
     public enum Status{
         locking("锁定中"),

+ 29 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/OAuthClient.java

@@ -0,0 +1,29 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * OAuth客户端实体
+ */
+@Data
+@TableName("oauth_clients")
+public class OAuthClient extends BaseEntity {
+
+    private String clientSecret;
+
+    private String clientId;
+    
+    private String clientName;
+    
+    private String redirectUris;
+    
+    private Integer tokenLifetime;
+    
+    private Integer status;
+
+}

+ 27 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/OAuthToken.java

@@ -0,0 +1,27 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * OAuth令牌实体
+ */
+@Data
+@TableName("oauth_tokens")
+public class OAuthToken extends BaseEntity {
+    
+    private String tokenValue;
+    
+    private Long userId;
+
+    private String keyValue;
+    
+    private String clientId;
+    
+    private Integer status;
+}

+ 24 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/RelayServer.java

@@ -0,0 +1,24 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+/**
+ * 中继服务器实体
+ */
+@Data
+@TableName("relay_servers")
+public class RelayServer extends BaseEntity {
+    
+    private String host;
+    
+    private String name;
+    
+    private String region;
+    
+    private Integer status;
+    
+    private Integer priority;
+    
+    private Integer latencyMs;
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java

@@ -188,6 +188,16 @@ public class ReqGoodsSkuInsert {
          * 备注
          */
         private String remark;
+
+        /**
+         * claude code积分上限
+         */
+        private Integer codePoints;
+
+        /**
+         * claude code每小时恢复积分
+         */
+        private Integer codeCreditRecovery;
     }
 
 }

+ 3 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/GroupsRelationView.java

@@ -35,6 +35,9 @@ public class GroupsRelationView {
     @DbField("sku.is_mirror")
     private Boolean isMirror;
 
+    @DbField("sku.spec_val")
+    private String specVal;
+
     @DbField("a.account")
     @JsonIgnore
     private String tripsAccount;

+ 32 - 0
netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeApiKeysReq.java

@@ -0,0 +1,32 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeApiKeysReq
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 11:39
+ */
+@Getter
+@Setter
+public class ClaudeCodeApiKeysReq {
+	/**
+	 * key name
+	 */
+	@NotEmpty
+	private String name;
+
+	/**
+	 * 用户id 不传入
+	 */
+	private Long userId;
+
+	/**
+	 * 过期时长day 不提供永不过期
+	 */
+	private Integer expiresDays;
+}

+ 37 - 0
netflix-dao/src/main/java/com/cyksj/model/request/ClaudeCodeApiKeysStatusReq.java

@@ -0,0 +1,37 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeApiKeysStatusReq
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 11:46
+ */
+@Getter
+@Setter
+public class ClaudeCodeApiKeysStatusReq {
+
+	/**
+	 * key id
+	 */
+	@NotEmpty
+	private Integer id;
+
+	private String keyValue;
+
+	/**
+	 * 用户id 不传入
+	 */
+	private Long userId;
+
+	/**
+	 * api key状态
+	 * active(活跃), inactive(禁用), revoked(已撤销)
+	 */
+	@NotEmpty
+	private String status;
+}

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

@@ -0,0 +1,24 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+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;
+}

+ 68 - 0
netflix-dao/src/main/java/com/cyksj/model/response/ClaudeCodePointsHistoryResp.java

@@ -0,0 +1,68 @@
+package com.cyksj.model.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+
+@Getter
+@Setter
+public class ClaudeCodePointsHistoryResp {
+
+	private List<Record> items;
+
+	/**
+	 * 当前页
+	 */
+	private Integer page;
+
+	@JsonProperty("page_size")
+	private Integer pageSize;
+
+	/**
+	 * 总记录数
+	 */
+	private Integer total;
+
+	@JsonProperty("total_pages")
+	private Integer totalPages;
+
+	@Getter
+	@Setter
+	public static class Record {
+		/**
+		 * 变动金额
+		 */
+		private Long amount;
+		/**
+		 * 变动后余额
+		 */
+		private Long balanceAfter;
+		/**
+		 * 创建时间
+		 */
+		private OffsetDateTime createdAt;
+		/**
+		 * 描述
+		 */
+		private String description;
+		/**
+		 * 记录ID
+		 */
+		private Long id;
+		/**
+		 * 关联ID
+		 */
+		private String referenceId;
+		/**
+		 * 关联类型
+		 */
+		private String referenceType;
+		/**
+		 * 类型
+		 */
+		private String type;
+	}
+}

+ 20 - 0
netflix-dao/src/main/java/com/cyksj/model/response/claudecode/ClaudeCodeResp.java

@@ -0,0 +1,20 @@
+package com.cyksj.model.response.claudecode;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeResp
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 11:28
+ */
+@Getter
+@Setter
+public class ClaudeCodeResp {
+	private Object data;
+
+	private Integer code;
+
+	private String message;
+}

+ 27 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodePackageView.java

@@ -0,0 +1,27 @@
+package com.cyksj.model.views;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodePackageView
+ * 创建者: JavaZou
+ * 创建时间:2025/8/12 10:44
+ */
+@Getter
+@Setter
+public class ClaudeCodePackageView {
+
+	/**
+	 * claude code规格
+	 */
+	private List<GoodsDonSkuView> skus;
+
+	/**
+	 * 升级套餐
+	 */
+	private List<RenewUpgradePackageFrontView> upgrades;
+}

+ 74 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserApiKeysView.java

@@ -0,0 +1,74 @@
+package com.cyksj.model.views;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserApiKeysView
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 12:13
+ */
+@Getter
+@Setter
+public class ClaudeCodeUserApiKeysView {
+
+	/**
+	 * API Key ID
+	 */
+	private Long id;
+
+	/**
+	 * 用户ID
+	 */
+	private Long userId;
+
+	/**
+	 * API Key值(仅创建时返回)
+	 */
+	@JsonProperty("key_value")
+	private String keyValue;
+
+	/**
+	 * 最后使用时间
+	 */
+	@JsonProperty("last_used_at")
+	private Date lastUsedAt;
+
+	/**
+	 * 最后使用IP
+	 */
+	@JsonProperty("last_used_ip")
+	private String lastUsedIp;
+
+	/**
+	 * API Key名称
+	 */
+	private String name;
+
+	/**
+	 * 前缀
+	 */
+	private String prefix;
+
+	/**
+	 * 状态
+	 * active(活跃), inactive(禁用), revoked(已撤销)
+	 */
+	private String status;
+
+	/**
+	 * 创建时间
+	 */
+	@JsonProperty("created_at")
+	private Date createdAt;
+
+	/**
+	 * 过期时间
+	 */
+	@JsonProperty("expires_at")
+	private Date expiresAt;
+}

+ 52 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserInfoView.java

@@ -0,0 +1,52 @@
+package com.cyksj.model.views;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserInfoView
+ * 创建者: JavaZou
+ * 创建时间:2025/8/11 14:37
+ */
+@Getter
+@Setter
+@Builder
+public class ClaudeCodeUserInfoView {
+	/**
+	 * 套餐名
+	 */
+	private String planName;
+
+	private Long relationId;
+
+	/**
+	 * claude code积分上限
+	 */
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	private Integer codeCreditRecovery;
+
+	/**
+	 * code 有效期
+	 */
+	private Date expiryTime;
+
+	private Long renewOrderId;
+
+	/**
+	 * 续费升级skuId
+	 */
+	private Long renewSkuId;
+
+	/**
+	 * 续费升级过期时间
+	 */
+	private Date renewExpiryTime;
+}

+ 75 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserPackageView.java

@@ -0,0 +1,75 @@
+package com.cyksj.model.views;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserPackageView
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 15:19
+ */
+@Getter
+@Setter
+public class ClaudeCodeUserPackageView {
+	/**
+	 * 用户ID
+	 */
+	private Long userId;
+
+	/**
+	 * 可用积分
+	 */
+	private Integer availableCredits;
+
+	/**
+	 * 积分上限
+	 */
+	private Integer creditLimit;
+
+	/**
+	 * 每日恢复积分
+	 */
+	private Integer creditRecovery;
+
+	/**
+	 * 结束日期
+	 */
+	private Date endDate;
+	/**
+	 * 最后恢复时间
+	 */
+	private Date lastRecoveryAt;
+
+	/**
+	 * 下次恢复时间
+	 */
+	private Date nextRecoveryAt;
+
+	/**
+	 * 套餐名称
+	 */
+	private String planName;
+
+	/**
+	 * 是否启用恢复
+	 */
+	private Boolean recoveryEnabled;
+
+	/**
+	 * 开始日期
+	 */
+	private Date start_date;
+
+	/**
+	 * 状态
+	 */
+	private String status;
+
+	/**
+	 * 已使用积分
+	 */
+	private Integer usedCredits;
+}

+ 70 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserPointsDetailView.java

@@ -0,0 +1,70 @@
+package com.cyksj.model.views;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeUserPointsDetailView
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 15:25
+ */
+@Getter
+@Setter
+public class ClaudeCodeUserPointsDetailView {
+	private Integer total;
+
+	private Integer page;
+
+	private Integer limit;
+
+	private List<Record> records;
+
+	@Getter
+	@Setter
+	public static class Record {
+
+		/**
+		 * 记录ID
+		 */
+		private Long id;
+
+		/**
+		 * 变动金额
+		 */
+		private Integer amount;
+
+		/**
+		 * 变动后余额
+		 */
+		private Integer balanceAfter;
+
+		/**
+		 * 创建时间
+		 */
+		private Date created_at;
+
+		/**
+		 * 描述
+		 */
+		private String description;
+
+		/**
+		 * 关联ID
+		 */
+		private String referenceId;
+
+		/**
+		 * 关联类型
+		 */
+		private String referenceType;
+
+		/**
+		 * 类型
+		 */
+		private String type;
+	}
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuFrontView.java

@@ -226,4 +226,14 @@ public class GoodsDonSkuFrontView {
 	 * 其他模型限制时间
 	 */
 	private Long otherLimitTime;
+
+	/**
+	 * claude code积分上限
+	 */
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	private Integer codeCreditRecovery;
 }

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

@@ -87,4 +87,16 @@ public class GoodsDonSkuRenewView {
 	 */
 	@DbIgnore
 	private Long originalOrderId;
+
+	/**
+	 * claude code积分上限
+	 */
+	@DbField("sku.code_points")
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	@DbField("sku.code_credit_recovery")
+	private Integer codeCreditRecovery;
 }

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

@@ -61,4 +61,16 @@ public class GoodsDonSkuView {
 
 	@DbField("gdk.is_mirror")
 	private Boolean isMirror;
+
+	/**
+	 * claude code积分上限
+	 */
+	@DbField("gdk.code_points")
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	@DbField("gdk.code_credit_recovery")
+	private Integer codeCreditRecovery;
 }

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

@@ -90,5 +90,17 @@ public class RenewUpgradePackageFrontView {
 
 		@DbField("sku.price")
 		private BigDecimal price;
+
+		/**
+		 * claude code积分上限
+		 */
+		@DbField("sku.code_points")
+		private Integer codePoints;
+
+		/**
+		 * claude code每小时恢复积分
+		 */
+		@DbField("sku.code_credit_recovery")
+		private Integer codeCreditRecovery;
 	}
 }

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/model/views/TicketRenewUpgradeView.java

@@ -80,4 +80,14 @@ public class TicketRenewUpgradeView {
 	private BigDecimal packagePrice;
 
 	private GoodsDonSkuRenewView renewSku;
+
+	/**
+	 * claude code积分上限
+	 */
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	private Integer codeCreditRecovery;
 }

+ 18 - 0
netflix-dao/src/main/resources/mapper/ClaudeCodeUserRenewExpiryRecordMapper.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.cyksj.mapper.ClaudeCodeUserRenewExpiryRecordMapper">
+
+    <select id="getOtherRenewSku" resultType="com.cyksj.model.entity.ClaudeCodeUserRenewExpiryRecord">
+        select orr.* from (select * from claude_code_user_renew_expiry_record where order_id != #{orderId}
+        and is_valid = 0
+        and user_id in
+        <foreach collection="userIds" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>) orr inner join order_don o on o.id = orr.order_id
+        and o.status not in ('close','noPayment','refund')
+        and o.order_type = 2 and o.relation_id = #{relationId}
+        inner join goods_don_sku sku on sku.id = o.sku_id
+        order by sku.months desc,sku.code_points desc
+        limit 1
+    </select>
+</mapper>

+ 11 - 0
netflix-dao/src/main/resources/mapper/GroupRelationMapper.xml

@@ -424,4 +424,15 @@
             and gt.sku_id = #{skuId}
         </if>
     </select>
+
+    <select id="selectCountByGoodsId" resultType="java.lang.Integer">
+        select count(1) from groups_relation gr inner join groups_trips gt on gt.id = gr.groups_id inner join
+        goods_don_sku sku on sku.id = gt.sku_id
+        where sku.goods_id = #{goodsId}
+          and gr.user_id in
+        <foreach collection="userIds" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>
+        and gr.expiry_time > #{now}
+    </select>
 </mapper>

+ 35 - 0
netflix-service/src/main/java/com/cyksj/service/claude/AnnouncementService.java

@@ -0,0 +1,35 @@
+package com.cyksj.service.claude;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.Announcement;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author chan
+ * @date 2025/8/11 18:03
+ */
+public interface AnnouncementService extends IService<Announcement> {
+    /**
+     * 获取有效公告列表
+     */
+    List<Announcement> getActiveAnnouncements(String language);
+
+    /**
+     * 创建公告
+     */
+    void createAnnouncement(Announcement announcement);
+
+    /**
+     * 更新公告
+     */
+    void updateAnnouncement(Announcement announcement);
+
+    /**
+     * 删除公告
+     */
+    void deleteAnnouncement(Long id);
+
+}

+ 69 - 0
netflix-service/src/main/java/com/cyksj/service/claude/ClaudeCodeService.java

@@ -0,0 +1,69 @@
+package com.cyksj.service.claude;
+
+import com.cyksj.model.entity.ClaudeCodeUser;
+import com.cyksj.model.entity.GoodsDonSku;
+import com.cyksj.model.entity.GroupsRelation;
+import com.cyksj.model.request.ClaudeCodeApiKeysReq;
+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.ClaudeCodeUserInfoView;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeService
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 10:55
+ */
+public interface ClaudeCodeService {
+
+	/**
+	 * 生成claude code用户套餐信息
+	 */
+	void generateOrUpdateClaudeCodeUserInfo(Long orderId, Long relationId, Long userId, GoodsDonSku sku, Integer orderType);
+
+	/**
+	 * 用户创建api keys
+	 */
+	ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception;
+
+	/**
+	 * 删除api key
+	 */
+	void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception;
+
+	/**
+	 * 用户api keys列表
+	 */
+	List<ClaudeCodeUserApiKeysView> getUserApiKeys(long userId) throws Exception;
+
+	ClaudeCodePointsHistoryResp getUserPointsDetail(Long userId, String type, Integer page, Integer limit) throws Exception;
+
+	ClaudeCodeResp getUserDashboard(Long userId) throws Exception;
+
+	ClaudeCodeResp getUserBalance(Long userId) throws Exception;
+
+	ClaudeCodeResp getCreditsAnalytics(Long userId, String start, String end, String tz, Integer page, Integer limit, String order, String type) throws Exception;
+
+	void invalidateUserCache(Long relationUserId);
+
+	ClaudeCodeUserInfoView getClaudeCodeUserSubsInfo(Long userId);
+
+	Boolean checkClaudeCodeUser(Long userId);
+
+	void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime);
+
+	/**
+	 * 删除用户claude code 套餐
+	 */
+	Boolean delUserPackages(Long userId);
+
+	/**
+	 * 处理续费升级 TODO
+	 */
+	void handleClaudeCodeUserPackageSku(GroupsRelation relation, GoodsDonSku sku, Long orderId);
+}

+ 14 - 0
netflix-service/src/main/java/com/cyksj/service/claude/OAuthClaudeCodeService.java

@@ -0,0 +1,14 @@
+package com.cyksj.service.claude;
+
+
+import com.cyksj.model.entity.OAuthClient;
+import com.cyksj.model.entity.OAuthToken;
+
+public interface OAuthClaudeCodeService {
+    OAuthClient validateClient(String clientId, String clientSecret);
+
+    String generateAccessToken(Long userId, String clientId) throws Exception;
+
+    OAuthToken validateToken(String code, String tokenValue);
+
+}

+ 28 - 0
netflix-service/src/main/java/com/cyksj/service/claude/RelayService.java

@@ -0,0 +1,28 @@
+package com.cyksj.service.claude;
+
+import com.cyksj.model.entity.RelayServer;
+
+import java.util.List;
+
+/**
+ * 中继服务器服务
+ */
+public interface RelayService {
+
+    List<RelayServer> getAvailableRelays(String domain);
+    
+    /**
+     * 创建中继服务器
+     */
+    void createRelayServer(RelayServer relayServer);
+    
+    /**
+     * 更新中继服务器
+     */
+    void updateRelayServer(RelayServer relayServer);
+    
+    /**
+     * 删除中继服务器
+     */
+    void deleteRelayServer(Long id);
+}

+ 73 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/AnnouncementServiceImpl.java

@@ -0,0 +1,73 @@
+package com.cyksj.service.claude.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.mapper.claudecode.AnnouncementMapper;
+import com.cyksj.model.entity.Announcement;
+import com.cyksj.service.claude.AnnouncementService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author chan
+ * @date 2025/8/11 18:03
+ */
+@Service
+@RequiredArgsConstructor
+
+public class AnnouncementServiceImpl extends ServiceImpl<AnnouncementMapper, Announcement> implements AnnouncementService {
+
+    /**
+     * 公告服务
+     */
+
+    private final AnnouncementMapper announcementMapper;
+
+    /**
+     * 获取有效公告列表
+     */
+    @Override
+    public List<Announcement> getActiveAnnouncements(String language) {
+        QueryWrapper<Announcement> wrapper = new QueryWrapper<>();
+        wrapper.eq("status", 1)
+                .eq("language", language)
+                .and(w -> w.isNull("start_date").or().le("start_date", new Date()))
+                .and(w -> w.isNull("end_date").or().ge("end_date", new Date()))
+                .orderByDesc("priority")
+                .orderByDesc("created_at");
+
+        return announcementMapper.selectList(wrapper);
+    }
+
+    /**
+     * 创建公告
+     */
+    @Override
+    public void createAnnouncement(Announcement announcement) {
+        announcement.setCreatedTime(new Date());
+        announcement.setUpdateTime(new Date());
+        announcementMapper.insert(announcement);
+    }
+
+    /**
+     * 更新公告
+     */
+    @Override
+    public void updateAnnouncement(Announcement announcement) {
+        announcement.setUpdateTime(new Date());
+        announcementMapper.updateById(announcement);
+    }
+
+    /**
+     * 删除公告
+     */
+    @Override
+    public void deleteAnnouncement(Long id) {
+        announcementMapper.deleteById(id);
+    }
+
+}

+ 548 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java

@@ -0,0 +1,548 @@
+package com.cyksj.service.claude.impl;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpException;
+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.common.util.StringUtil;
+import com.cyksj.mapper.*;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.manage.views.GroupsRelationView;
+import com.cyksj.model.request.ClaudeCodeApiKeysReq;
+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.ClaudeCodeUserInfoView;
+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 com.github.rholder.retry.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeServiceImpl
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 10:55
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class ClaudeCodeServiceImpl implements ClaudeCodeService {
+	private final UserBindRelationService userBindRelationService;
+
+	private final GroupsRelationMapper relationMapper;
+
+	private final BeanSearcher beanSearcher;
+
+	private final ClaudeCodeUserRenewExpiryRecordMapper claudeCodeUserRenewExpiryRecordMapper;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
+	private final GoodsDonSkuMapper skuMapper;
+
+	private final GroupsMapper groupsMapper;
+
+	private final OrderDonMapper orderDonMapper;
+
+	private final static String CLAUDE_CODE_API_PREFIX = "https://relay01.yhlxj.com/";
+
+	public static final String CLAUDE_CODE_API_ADMIN_KEY = "claudecodeyhlxjclaude";
+
+	//连接超时时间 毫秒
+	public static final int CONNECT_MILLISECONDS = 10000;
+
+	@Override
+	public void generateOrUpdateClaudeCodeUserInfo(Long orderId, 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));
+		GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+		GoodsDonSku relationSku = skuMapper.selectById(groupsTrips.getSkuId());
+		Long thisCodeOriSkuId = relationSku.getId();
+		Long relationUserId = relation.getUserId();
+		//此时车票的套餐规格积分
+		Integer thisRelationCodePoints = null;
+		ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
+		if (claudeCodeUser != null) {
+			thisRelationCodePoints = claudeCodeUser.getCodePoints();
+		}
+		Date expiryTime = relation.getExpiryTime();
+		Integer codePoints = sku.getCodePoints();
+		Integer codeCreditRecovery = sku.getCodeCreditRecovery();
+		Long skuId = sku.getId();
+		Boolean isUpgrade = false;
+		Date renewExpiryTime = null;
+		//若是续费升级 记录此时续费升级的过期时间
+		if (thisRelationCodePoints != null && thisRelationCodePoints <= codePoints && !ObjectUtil.equal(thisCodeOriSkuId, skuId)) {
+			if (sku.getDays() != null) {
+				renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays());
+			} else {
+				renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths());
+			}
+			isUpgrade = true;
+			saveClaudeCodeRenewExpiryRecord(orderId, relationUserId, relationId, renewExpiryTime, skuId);
+		}
+		generateOrUpdateClaudeCodeUser(relationUserId, relationId, codePoints, codeCreditRecovery, expiryTime, isUpgrade, orderId, skuId, renewExpiryTime, sku.getSpecVal());
+		try {
+			//生成claude code或更新用户套餐
+			ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(relation.getUserId(), sku.getSpecVal(), codePoints, codeCreditRecovery, expiryTime);
+			if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
+				//重试更新code userInfo数据
+				setClaudeCodeUserRetryUpdateInfo(relationId);
+				return;
+			}
+			//清缓存
+			if (orderType == 2) {
+				invalidateUserCache(relationUserId);
+			}
+		} catch (Exception e) {
+			log.error("生成或更新claude code用户:{}套餐skuId:{} 失败,error_info:{}", userId, skuId, StringUtil.getErrorText(e));
+			//重试更新code userInfo数据
+			setClaudeCodeUserRetryUpdateInfo(relationId);
+		}
+	}
+
+	private void generateOrUpdateClaudeCodeUser(Long relationUserId, Long relationId, Integer codePoints, Integer codeCreditRecovery, Date expiryTime, Boolean isUpgrade, Long orderId, Long renewSkuId, Date renewExpiryTime, String planName) {
+		ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
+		if (claudeCodeUser == null) {
+			claudeCodeUser = new ClaudeCodeUser();
+		}
+		if (isUpgrade) {
+			claudeCodeUser.setRenewOrderId(orderId);
+			claudeCodeUser.setRenewSkuId(renewSkuId);
+			claudeCodeUser.setRenewExpiryTime(renewExpiryTime);
+		}
+		claudeCodeUser.setUserId(relationUserId);
+		claudeCodeUser.setPlanName(planName);
+		claudeCodeUser.setRelationId(relationId);
+		claudeCodeUser.setExpiryTime(expiryTime);
+		claudeCodeUser.setCodePoints(codePoints);
+		claudeCodeUser.setCodeCreditRecovery(codeCreditRecovery);
+		if (claudeCodeUser.getId() == null) {
+			try {
+				claudeCodeUserMapper.insert(claudeCodeUser);
+			} catch (DuplicateKeyException e) {
+				log.info("插入claude code user 重复");
+			}
+		} else claudeCodeUserMapper.updateById(claudeCodeUser);
+	}
+
+	@Override
+	public ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception {
+		Long userId = req.getUserId();
+		if (checkClaudeCodeUser(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();
+			if (expiresDays != null) {
+				parmas.put("expires_days", expiresDays);
+			}
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(apiKeyUrl, Jsons.toJson(parmas));
+			if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
+				throw BusinessRuntimeException.getInstance("创建失败");
+			}
+			return claudeCodeResp;
+		}
+		throw new BusinessRuntimeException("您还没有claudecode账号");
+	}
+
+	@Override
+	public void deleteApiKeysById(ClaudeCodeDelReq delReq) throws Exception {
+		Long userId = delReq.getUserId();
+		Long keyId = delReq.getKeyId();
+		if (checkClaudeCodeUser(userId)) {
+			Long claudeCodeUserId = getClaudeCodeUserId(userId);
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/keys/%s/%s", claudeCodeUserId, keyId);
+			HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(CONNECT_MILLISECONDS).execute();
+			String responseBody = execute.body();
+			ClaudeCodeResp claudeCodeResp = Jsons.parseObject(responseBody, ClaudeCodeResp.class);
+			if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
+				log.error("删除key失败:{}", claudeCodeResp.getMessage());
+				throw BusinessRuntimeException.getInstance("删除失败");
+			}
+		}
+	}
+
+	@Override
+	public List<ClaudeCodeUserApiKeysView> getUserApiKeys(long userId) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/keys", claudeCodeUserId);
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
+			return Jsons.parseList(claudeCodeResp.getData(), ClaudeCodeUserApiKeysView.class);
+		}
+		return null;
+	}
+
+	@Override
+	public ClaudeCodePointsHistoryResp getUserPointsDetail(Long userId, String type, Integer page, Integer limit) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/credits/history?page=%s&limit=%s&type=%s", claudeCodeUserId, page, limit, type);
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
+			ClaudeCodePointsHistoryResp historyResp = Jsons.parseObject(claudeCodeResp.getData(), ClaudeCodePointsHistoryResp.class);
+			return historyResp;
+		}
+		return null;
+	}
+
+	@Override
+	public ClaudeCodeResp getUserDashboard(Long userId) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/dashboard", claudeCodeUserId);
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
+			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", claudeCodeUserId);
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
+			return claudeCodeResp;
+		}
+		return null;
+	}
+
+	@Override
+	public ClaudeCodeResp getCreditsAnalytics(Long userId, String start, String end, String tz, Integer page, Integer limit, String order, String type) throws Exception {
+		Long claudeCodeUserId = getClaudeCodeUserId(userId);
+		if (start == null) {
+			DateTime now = DateTime.now();
+			start = DateUtil.offsetDay(now, -1).toString();
+			end = now.toString();
+		}
+		if (claudeCodeUserId != null) {
+			String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/credits/analytics?start=%s&end=%s&tz=%s&page=%s&limit=%s&order=%s&type=%s", claudeCodeUserId, start, end, tz, page, limit, order, type);
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodeGetApi(url);
+			return claudeCodeResp;
+		}
+		return null;
+	}
+
+
+	/**
+	 * 用户缓存失效
+	 */
+	@Override
+	public void invalidateUserCache(Long userId) {
+		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/cache/invalidate/user/%s", userId);
+		try {
+			executeClaudeCodePostApi(url, null);
+		} catch (Exception e) {
+
+		}
+	}
+
+	@Override
+	public ClaudeCodeUserInfoView getClaudeCodeUserSubsInfo(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()).op(Operator.GreaterThan)
+				.build());
+		ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
+		//车票不存在
+		if (groupsRelationView == null) {
+			//套餐是否存在
+			if (claudeCodeUser != null) {
+				//清除用户套餐
+				if (delUserPackages(claudeCodeUser.getUserId())) {
+					claudeCodeUserMapper.deleteById(claudeCodeUser.getId());
+				}
+			}
+			return null;
+		}
+		if (claudeCodeUser == null) {
+			OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
+					.eq(OrderDon::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
+					.eq(OrderDon::getRelationId, groupsRelationView.getId())
+					.in(OrderDon::getUserId, userIdList)
+					.notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
+					.orderByDesc(OrderDon::getId)
+					.last("limit 1"));
+			if (orderDon != null) {
+				GoodsDonSku sku = skuMapper.selectById(orderDon.getSkuId());
+				generateOrUpdateClaudeCodeUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
+				claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
+			}
+		} else {
+			claudeCodeUser.setExpiryTime(groupsRelationView.getExpiryTime());
+			claudeCodeUserMapper.updateById(claudeCodeUser);
+		}
+		//是否需要重试更新 cladueCode userInfo信息
+		if (claudeCodeUser.getIsRetry()) {
+			try {
+				ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), claudeCodeUser.getExpiryTime());
+				if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
+					claudeCodeUser.setIsRetry(Boolean.TRUE);
+					claudeCodeUserMapper.updateById(claudeCodeUser);
+				} else {
+					claudeCodeUser.setIsRetry(Boolean.FALSE);
+					claudeCodeUserMapper.updateById(claudeCodeUser);
+				}
+			} catch (Exception e) {
+
+			}
+		}
+		return ClaudeCodeUserInfoView.builder()
+				.planName(claudeCodeUser.getPlanName())
+				.relationId(claudeCodeUser.getRelationId())
+				.codePoints(claudeCodeUser.getCodePoints())
+				.codeCreditRecovery(claudeCodeUser.getCodeCreditRecovery())
+				.expiryTime(claudeCodeUser.getExpiryTime())
+				.renewSkuId(claudeCodeUser.getRenewSkuId())
+				.renewOrderId(claudeCodeUser.getRenewOrderId())
+				.renewExpiryTime(claudeCodeUser.getRenewExpiryTime())
+				.build();
+	}
+
+	public ClaudeCodeResp createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer codePoints, Integer codeCreditRecovery, Date expiryTime) throws Exception {
+		Map<String, Object> parmas = new HashMap<>();
+		parmas.put("plan_name", planName.replaceAll(";", ""));
+		parmas.put("credit_limit", codePoints);
+		parmas.put("credit_recovery", codeCreditRecovery);
+		parmas.put("expire_time", DateUtil.format(expiryTime,"yyyy-MM-dd HH:mm:ss"));
+		String planUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
+		ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(planUrl, Jsons.toJson(parmas));
+		if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
+			log.info("用户claude code用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, claudeCodeResp.getMessage());
+		}
+		return claudeCodeResp;
+	}
+
+
+	/**
+	 * 校验是否是claude code用户
+	 * @param userId
+	 * @return
+	 */
+	@Override
+	public Boolean checkClaudeCodeUser(Long userId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		Integer count = relationMapper.selectCountByGoodsId(userIdList, Constant.CLAUDE_CODE_GOODS_ID, DateTime.now());
+		return count > 0;
+	}
+
+	@Override
+	public void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime) {
+		try {
+			ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), expiryTime);
+			if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
+				//重试标记
+				claudeCodeUser.setIsRetry(Boolean.TRUE);
+				claudeCodeUserMapper.updateById(claudeCodeUser);
+				return;
+			}
+			//刷新缓存
+			invalidateUserCache(claudeCodeUser.getUserId());
+		} catch (Exception e) {
+			claudeCodeUser.setIsRetry(Boolean.TRUE);
+			claudeCodeUserMapper.updateById(claudeCodeUser);
+		}
+	}
+
+	@Override
+	public Boolean delUserPackages(Long userId) {
+		String url = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
+		try {
+			ClaudeCodeResp claudeCodeResp = executeClaudeCodeDeletedApi(url);
+			if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
+				log.error("删除用户:{} claude code套餐失败:{}", userId, claudeCodeResp.getMessage());
+				return Boolean.FALSE;
+			}
+			return Boolean.TRUE;
+		} catch (Exception e) {
+			log.error("删除用户:{} claude code套餐报错:{}", userId, StringUtil.getErrorText(e));
+		}
+		return Boolean.FALSE;
+	}
+
+	@Override
+	public void handleClaudeCodeUserPackageSku(GroupsRelation relation, GoodsDonSku sku, Long orderId) {
+		Long groupsId = relation.getGroupsId();
+		GroupsTrips groupsTrips = groupsMapper.selectById(groupsId);
+		GoodsDonSku oriRelationSku = skuMapper.selectById(groupsTrips.getSkuId());
+		Integer oriRelationSkuCodePoints = oriRelationSku.getCodePoints();
+		Integer presentCodePoints = sku.getCodePoints();
+		Long oriRelationSkuId = oriRelationSku.getId();
+		Long toUseSkuId = sku.getId();
+		//若是续费升级 记录此时续费升级的过期时间
+		Boolean isUpgrade = false;
+		Date renewExpiryTime = null;
+		if (oriRelationSkuCodePoints <= presentCodePoints && !ObjectUtil.equal(oriRelationSkuId, toUseSkuId)) {
+			if (sku.getDays() != null) {
+				renewExpiryTime = DateUtil.offsetDay(DateTime.now(), sku.getDays());
+			} else {
+				renewExpiryTime = DateUtil.offsetMonth(DateTime.now(), sku.getMonths());
+			}
+			isUpgrade = true;
+			saveClaudeCodeRenewExpiryRecord(orderId, relation.getUserId(), relation.getId(), renewExpiryTime, sku.getId());
+		}
+	}
+
+	/**
+	 * 获取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()).op(Operator.GreaterThan)
+				.build());
+		if (groupsRelationView != null) {
+			return groupsRelationView.getUserId();
+		}
+		return null;
+	}
+
+	/**
+	 * claude code GET请求
+	 */
+	public ClaudeCodeResp executeClaudeCodeGetApi(String url) {
+		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
+		try {
+			return build.call(() -> {
+				try {
+					HttpResponse execute = HttpUtil.createGet(url)
+							.header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY)
+							.setConnectionTimeout(CONNECT_MILLISECONDS)
+							.execute();
+					ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
+					if (!Constant.SUCCESS.equals(resp.getMessage())) {
+						log.info("claude code GET URL:{}接口返回msg:{}", url, resp.getMessage());
+					}
+					return resp;
+				} catch (HttpException e) {
+					return null;
+				}
+			});
+		} catch (ExecutionException | RetryException e) {
+			log.info("重试调用claude code GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
+			throw BusinessRuntimeException.getInstance("接口请求失败");
+		}
+	}
+
+
+	/**
+	 * claude code Post请求
+	 */
+	public ClaudeCodeResp executeClaudeCodePostApi(String url, String body) {
+		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
+		try {
+			return build.call(() -> {
+				try {
+					HttpResponse execute = HttpUtil.createPost(url)
+							.header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY)
+							.setConnectionTimeout(CONNECT_MILLISECONDS)
+							.body(body)
+							.execute();
+					ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
+					if (!Constant.SUCCESS.equals(resp.getMessage())) {
+						log.info("claude code POST URL:{}接口返回msg:{}", url, resp.getMessage());
+					}
+					return resp;
+				} catch (Exception e) {
+					return null;
+				}
+			});
+		} catch (ExecutionException | RetryException e) {
+			log.info("重试调用claude code POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
+			throw BusinessRuntimeException.getInstance("接口请求失败");
+		}
+	}
+
+	/**
+	 * claude code DELETE请求
+	 */
+	public ClaudeCodeResp executeClaudeCodeDeletedApi(String url) {
+		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
+		try {
+			return build.call(() -> {
+				try {
+					HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url).header("X-Admin-Key", CLAUDE_CODE_API_ADMIN_KEY).setConnectionTimeout(CONNECT_MILLISECONDS).execute();
+					ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
+					if (!Constant.SUCCESS.equals(resp.getMessage())) {
+						log.info("claude code DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
+					}
+					return resp;
+				} catch (HttpException e) {
+					return null;
+				}
+			});
+		} catch (ExecutionException | RetryException e) {
+			log.info("重试调用claude code DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
+			throw BusinessRuntimeException.getInstance("接口请求失败");
+		}
+	}
+
+	/**
+	 * 记录claude code 续费升级的过期时间
+	 */
+	private void saveClaudeCodeRenewExpiryRecord(Long orderId, Long relationUserId, Long relationId, Date expiryTime, Long skuId) {
+		ClaudeCodeUserRenewExpiryRecord renewExpiryRecord = new ClaudeCodeUserRenewExpiryRecord();
+		renewExpiryRecord.setOrderId(orderId);
+		renewExpiryRecord.setUserId(relationUserId);
+		renewExpiryRecord.setRelationId(relationId);
+		renewExpiryRecord.setRenewSkuId(skuId);
+		renewExpiryRecord.setExpiryTime(expiryTime);
+		claudeCodeUserRenewExpiryRecordMapper.insert(renewExpiryRecord);
+	}
+
+	/**
+	 * claude code用户信息重试更新信息
+	 */
+	public void setClaudeCodeUserRetryUpdateInfo(Long relationId) {
+		ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
+		if (claudeCodeUser != null) {
+			claudeCodeUser.setIsRetry(Boolean.TRUE);
+			claudeCodeUserMapper.updateById(claudeCodeUser);
+		}
+	}
+
+	public Retryer getApiRetryer(Integer sleepSecond, Integer attemptNum) {
+		Retryer build = RetryerBuilder.newBuilder()
+				.retryIfException()
+				//运行时异常重试
+				.retryIfRuntimeException()
+				//false重试
+				.retryIfResult(res -> res == null)
+				//1s 间隔
+				.withWaitStrategy(WaitStrategies.fixedWait(sleepSecond, TimeUnit.SECONDS))
+				//停止策略 : 尝试请求3次
+				.withStopStrategy(StopStrategies.stopAfterAttempt(attemptNum)).build();
+		return build;
+	}
+
+}

+ 117 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/OAuthClaudeCodeServiceImpl.java

@@ -0,0 +1,117 @@
+package com.cyksj.service.claude.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.json.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.mapper.claudecode.OAuthClientMapper;
+import com.cyksj.mapper.claudecode.OAuthTokenMapper;
+import com.cyksj.model.entity.OAuthClient;
+import com.cyksj.model.entity.OAuthToken;
+import com.cyksj.model.request.ClaudeCodeApiKeysReq;
+import com.cyksj.model.response.claudecode.ClaudeCodeResp;
+import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
+import com.cyksj.redis.RedisService;
+import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.claude.OAuthClaudeCodeService;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * OAuth服务
+ */
+@Service
+@RequiredArgsConstructor
+public class OAuthClaudeCodeServiceImpl implements OAuthClaudeCodeService {
+
+    private final OAuthClientMapper oauthClientMapper;
+
+    private final OAuthTokenMapper oauthTokenMapper;
+
+    private final RedisService redisService;
+
+    private final ClaudeCodeService claudeCodeService;
+
+    /**
+     * 验证客户端凭据
+     */
+    @Override
+    public OAuthClient validateClient(String clientId, String clientSecret) {
+        QueryWrapper<OAuthClient> wrapper = new QueryWrapper<>();
+        wrapper.eq("client_id", clientId)
+                .eq("client_secret", clientSecret)
+                .eq("status", 1);
+        return oauthClientMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 生成访问令牌
+     */
+    @Override
+    public String generateAccessToken(Long userId, String clientId) throws Exception {
+        if (claudeCodeService.checkClaudeCodeUser(userId)) {
+            List<ClaudeCodeUserApiKeysView> userApiKeys = claudeCodeService.getUserApiKeys(userId);
+            if (CollectionUtil.isEmpty(userApiKeys)) {
+                ClaudeCodeApiKeysReq claudeCodeApiKeysReq = new ClaudeCodeApiKeysReq();
+                claudeCodeApiKeysReq.setName("系统创建");
+                claudeCodeApiKeysReq.setUserId(userId);
+                claudeCodeApiKeysReq.setExpiresDays(30);
+                ClaudeCodeResp apiKeys = claudeCodeService.createApiKeys(claudeCodeApiKeysReq);
+                JSONObject jsonObject = new JSONObject(apiKeys.getData());
+                String token = jsonObject.getStr("key_value");
+                // 保存token
+                OAuthToken oAuthToken = new OAuthToken();
+                oAuthToken.setTokenValue(token);
+                oAuthToken.setKeyValue(token);
+                oAuthToken.setUserId(userId);
+                oAuthToken.setClientId(clientId);
+                oAuthToken.setStatus(1);
+                oAuthToken.setCreatedTime(new Date());
+
+                oauthTokenMapper.insert(oAuthToken);
+                return token;
+
+            }else {
+                String keyValue = userApiKeys.get(0).getKeyValue();
+                OAuthToken oAuthToken = oauthTokenMapper.selectOne(Wrappers.lambdaQuery(OAuthToken.class).eq(OAuthToken::getClientId, clientId).eq(OAuthToken::getUserId, userId));
+                if (oAuthToken == null) {
+                    oAuthToken = new OAuthToken();
+                    oAuthToken.setKeyValue(keyValue);
+                    oAuthToken.setStatus(1);
+                    oAuthToken.setUserId(userId);
+                    oauthTokenMapper.insert(oAuthToken);
+                }else {
+                    oAuthToken = new OAuthToken();
+                    oAuthToken.setKeyValue(keyValue);
+                    oAuthToken.setStatus(1);
+                    oAuthToken.setUserId(userId);
+                    oauthTokenMapper.updateById(oAuthToken);
+                }
+                return userApiKeys.get(0).getKeyValue();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 验证访问令牌
+     */
+    @Override
+    public OAuthToken validateToken(String code, String key) {
+        LambdaQueryWrapper<OAuthToken> wrapper = Wrappers.lambdaQuery(OAuthToken.class);
+        if (StringUtils.isNotBlank(key)) {
+            wrapper.eq(OAuthToken::getKeyValue, key).eq(OAuthToken::getStatus, 1);
+        } else {
+            wrapper.eq(OAuthToken::getTokenValue, key).eq(OAuthToken::getStatus, 1);
+
+        }
+        return oauthTokenMapper.selectOne(wrapper);
+    }
+
+}

+ 57 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/RelayServiceImpl.java

@@ -0,0 +1,57 @@
+package com.cyksj.service.claude.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.mapper.claudecode.RelayServerMapper;
+import com.cyksj.model.entity.RelayServer;
+import com.cyksj.service.claude.RelayService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 中继服务器服务
+ */
+@Service
+@RequiredArgsConstructor
+public class RelayServiceImpl implements RelayService {
+    
+    private final RelayServerMapper relayServerMapper;
+    
+    /**
+     * 获取可用中继服务器列表
+     */
+    public List<RelayServer> getAvailableRelays(String domain) {
+        QueryWrapper<RelayServer> wrapper = new QueryWrapper<>();
+        wrapper.eq("status", 1)
+                .orderByDesc("priority")
+                .orderByAsc("latency_ms");
+        
+        if (domain != null && !domain.isEmpty()) {
+            wrapper.like("host", domain);
+        }
+        
+        return relayServerMapper.selectList(wrapper);
+    }
+    
+    /**
+     * 创建中继服务器
+     */
+    public void createRelayServer(RelayServer relayServer) {
+        relayServerMapper.insert(relayServer);
+    }
+    
+    /**
+     * 更新中继服务器
+     */
+    public void updateRelayServer(RelayServer relayServer) {
+        relayServerMapper.updateById(relayServer);
+    }
+    
+    /**
+     * 删除中继服务器
+     */
+    public void deleteRelayServer(Long id) {
+        relayServerMapper.deleteById(id);
+    }
+}

+ 35 - 0
netflix-service/src/main/java/com/cyksj/service/exchange/impl/ExchangeCodeServiceImpl.java

@@ -23,14 +23,19 @@ import com.cyksj.mapper.manage.exchange.ExchangeCodeMapper;
 import com.cyksj.mapper.market.draw.LuckyDrawRecordMapper;
 import com.cyksj.model.entity.*;
 import com.cyksj.model.excel.ExcelExchangeCodeData;
+import com.cyksj.model.manage.views.GroupsRelationView;
 import com.cyksj.model.request.ExchangeCodeReq;
 import com.cyksj.model.request.ExchangeCodeTicket;
 import com.cyksj.redis.RedisService;
+import com.cyksj.service.claude.ClaudeCodeService;
 import com.cyksj.service.exchange.ExchangeCodeService;
 import com.cyksj.service.relation.GroupRelationFrontService;
 import com.cyksj.service.relation.GroupsRelationExpiryRecordService;
 import com.cyksj.service.shop.YhShopFrontService;
+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.beans.BeanUtils;
@@ -92,6 +97,12 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 
 	private final UserDistributeExchangeCodeUseRecordMapper userDistributeExchangeCodeUseRecordMapper;
 
+	private final UserBindRelationService userBindRelationService;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
+	private final ClaudeCodeService claudeCodeService;
+
 	@Override
 	@Transactional(rollbackFor = Throwable.class)
 	public List<ExcelExchangeCodeData> createExchangeCode(ExchangeCodeReq exchangeCodeReq) throws Exception {
@@ -379,6 +390,11 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 			orderDon.setPayTitle(payTitle.toString());
 			orderDonMapper.insert(orderDon);
 			log.info("用户{}兑换规格:{}车位成功", user.getId(), orderDon.getSkuId());
+
+			if (relation.getIsClaudeCodeUpgrade() != null && relation.getIsClaudeCodeUpgrade()) {
+				//claude code 是否升级
+				claudeCodeService.handleClaudeCodeUserPackageSku(relation, sku, orderDon.getId());
+			}
 		}
 		if (orderDonTye == OrderDon.Type.EX_CODE) {
 			//同步车票是否重购 记录
@@ -406,6 +422,25 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 
 	@Override
 	public GroupsRelation getRelationNoOrder(GoodsDonSku sku, Long userId, Long yhsId) {
+		//claude code商品兑换 只能兑换一次 后续兑换增加车票时长
+		if (sku.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID) {
+			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()).op(Operator.GreaterThan).build());
+			if (groupsRelationView != null) {
+//				Date expiryTime = groupsRelationView.getExpiryTime();
+//				if (sku.getDays() != null) {
+//					expiryTime = DateUtil.offsetDay(expiryTime, sku.getDays());
+//				} else {
+//					expiryTime = DateUtil.offsetMonth(expiryTime, sku.getMonths());
+//				}
+//				GroupsRelation relation = groupsRelationMapper.selectById(groupsRelationView.getId());
+//				relation.setExpiryTime(expiryTime);
+//				groupsRelationMapper.updateById(relation);
+//				relation.setIsClaudeCodeUpgrade(true);
+//				return relation;
+				throw BusinessRuntimeException.getInstance("您当前已有车票,暂时无法兑换");
+			}
+		}
 		Integer retryNum = 1;
 		List<Long> errorsRelationIds = new ArrayList<>();
 		GroupsRelation relation = getFinalRelation(sku, userId, retryNum, yhsId, errorsRelationIds);

+ 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).

+ 1 - 1
netflix-service/src/main/java/com/cyksj/service/mange/impl/CmsOrderDonServiceImpl.java

@@ -408,7 +408,7 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
         orderDon.setStatus(OrderDon.Status.refund);
         orderDonMapper.updateById(orderDon);
         //续费升级订单
-        if (orderDon.getOrderType() == 2 && sku.getIsMirror()) {
+        if (orderDon.getOrderType() == 2 && (orderDon.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID || sku.getIsMirror())) {
             mirrorUserCleatService.handleRenewUpgradeOrders(orderDon, sku);
         }
         //提交过工单的订单

+ 61 - 1
netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java

@@ -8,6 +8,7 @@ import com.cyksj.common.constant.Constant;
 import com.cyksj.mapper.*;
 import com.cyksj.model.entity.*;
 import com.cyksj.redis.RedisService;
+import com.cyksj.service.claude.ClaudeCodeService;
 import com.cyksj.service.mange.MirrorUserCleatService;
 import com.cyksj.service.user.UserBindRelationService;
 import lombok.RequiredArgsConstructor;
@@ -56,6 +57,12 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 
 	private final UserTicketClearedRecordMapper userTicketClearedRecordMapper;
 
+	private final ClaudeCodeUserRenewExpiryRecordMapper claudeCodeUserRenewExpiryRecordMapper;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
+	private final ClaudeCodeService claudeCodeService;
+
 	/**
 	 * 删除镜像User
 	 */
@@ -124,14 +131,67 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 
 	@Override
 	public void handleRenewUpgradeOrders(OrderDon orderDon, GoodsDonSku sku) {
+		Long goodsId = orderDon.getGoodsId();
 		Long orderId = orderDon.getId();
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(orderDon.getUserId(), null);
+		//claude code user
+		if (goodsId == Constant.CLAUDE_CODE_GOODS_ID) {
+			ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class)
+					.eq(ClaudeCodeUser::getRelationId, orderDon.getRelationId())
+					.last("limit 1"));
+			if (claudeCodeUser == null) {
+				return;
+			}
+			Long presentRenewSkuId = claudeCodeUser.getRenewSkuId();
+			if (presentRenewSkuId == null) {
+				return;
+			}
+			GroupsRelation relation = groupsRelationMapper.selectById(orderDon.getRelationId());
+			GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+			GoodsDonSku finalSku = skuMapper.selectById(groupsTrips.getSkuId());
+			ClaudeCodeUserRenewExpiryRecord renewExpiryRecord = claudeCodeUserRenewExpiryRecordMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUserRenewExpiryRecord.class).eq(ClaudeCodeUserRenewExpiryRecord::getOrderId, orderId).eq(ClaudeCodeUserRenewExpiryRecord::getIsValid, Boolean.FALSE).last("limit 1"));
+			Date renewExpiryTime = null;
+			Integer oriCodePoints = claudeCodeUser.getCodePoints();
+			claudeCodeUser.setOriCodePoints(oriCodePoints);
+			claudeCodeUser.setRenewSkuId(null);
+			claudeCodeUser.setRenewOrderId(null);
+			if (renewExpiryRecord != null) {
+				//是否存在其他续费升级
+				ClaudeCodeUserRenewExpiryRecord otherRenewRecord = claudeCodeUserRenewExpiryRecordMapper.getOtherRenewSku(orderDon.getId(), relation.getId(), userIdList);
+				GoodsDonSku otherRenewSku = null;
+				if (otherRenewRecord != null) {
+					otherRenewSku = skuMapper.selectById(otherRenewRecord.getRenewSkuId());
+					renewExpiryTime = otherRenewRecord.getExpiryTime();
+				}
+				//续费升级
+				if (otherRenewSku != null) {
+					finalSku = otherRenewSku;
+					claudeCodeUser.setRenewOrderId(otherRenewRecord.getOrderId());
+					claudeCodeUser.setRenewSkuId(otherRenewRecord.getRenewSkuId());
+				}
+				//将续费有效置为过期
+				renewExpiryRecord.setIsValid(true);
+				claudeCodeUserRenewExpiryRecordMapper.updateById(renewExpiryRecord);
+			}
+			//还原镜像user
+			claudeCodeUser.setCodePoints(finalSku.getCodePoints());
+			claudeCodeUser.setCodeCreditRecovery(finalSku.getCodeCreditRecovery());
+			claudeCodeUser.setRenewExpiryTime(renewExpiryTime);
+			claudeCodeUser.setPlanName(finalSku.getSpecVal());
+			claudeCodeUserMapper.updateById(claudeCodeUser);
+			log.info("还原镜像claude code user套餐成功");
+			//调用api更新claude code用户信息
+			Date expiryTime = relation.getExpiryTime();
+			claudeCodeService.updateClaudeCodeUserPackageInfo(claudeCodeUser, expiryTime);
+			return;
+		}
 		OrderDonRenewRecord orderDonRenewRecord = orderDonRenewRecordMapper.selectOne(Wrappers.lambdaQuery(OrderDonRenewRecord.class)
 				.eq(OrderDonRenewRecord::getOrderId, orderId)
+				.eq(OrderDonRenewRecord::getIsValid, Boolean.FALSE)
 				.last("limit 1"));
 		if (orderDonRenewRecord == null) {
 			return;
 		}
-		List<Long> userIdList = userBindRelationService.getRelationUserIdList(orderDon.getUserId(), null);
 		//Claude pro
 		if (orderDon.getGoodsId() == 75) {
 			ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class)

+ 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
+							}
 						}
 					}
 				}

+ 33 - 0
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -79,6 +79,7 @@ import com.cyksj.model.request.*;
 import com.cyksj.model.views.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
+import com.cyksj.service.claude.ClaudeCodeService;
 import com.cyksj.service.coin.UserGalaxyCoinService;
 import com.cyksj.service.corp.WxCorpOps;
 import com.cyksj.service.coupon.CouponFontService;
@@ -385,6 +386,10 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 	private final UserBalanceSourceRecordMapper userBalanceSourceRecordMapper;
 
+	private final ClaudeCodeService claudeCodeService;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
 	private final List<String> noChekGoodsTemp = List.of("Apple One", "Youtube", "Spotify");
 
 	@Transactional(rollbackFor = Throwable.class)
@@ -458,6 +463,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;
@@ -1232,6 +1246,19 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 			}
 		}
 
+		//claude code续费 校验续费套餐等级
+		if (goodsDon.getId() == Constant.CLAUDE_CODE_GOODS_ID) {
+			ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
+			Long thisCodeRenewSkuId = claudeCodeUser.getRenewSkuId();
+			if (thisCodeRenewSkuId != null) {
+				//claude code积分上限
+				GoodsDonSku renewSku = goodsDonSkuMapper.selectById(renewSkuId);
+				if (claudeCodeUser.getCodePoints() < renewSku.getCodePoints()) {
+					throw BusinessRuntimeException.getInstance("选择的续费套餐错误");
+				}
+			}
+		}
+
 		String payOpenId = user.getOpenId();
 
 		/* 生成订单 */
@@ -3143,6 +3170,12 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 		//查看该订单是否有关闭的余额
 		deductIfCloseOrderReturnBalance(orderDon.getId(), UserBalanceSourceRecord.Source.order_close);
+
+		//购买claude code
+		if (orderDon.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID) {
+			//生成所需的claude code用户信息
+			claudeCodeService.generateOrUpdateClaudeCodeUserInfo(orderDon.getId(), orderDon.getRelationId(), orderDon.getUserId(), sku, orderDon.getOrderType());
+		}
 	}
 
 	private void handleUserMirrorSHopOrderDon(OrderDon orderDon) throws Exception {

+ 19 - 4
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java

@@ -1,13 +1,14 @@
 package com.cyksj.service.relation.impl;
 
-import com.cyksj.mapper.GoodsDonSkuMapper;
-import com.cyksj.mapper.GroupsMapper;
-import com.cyksj.mapper.GroupsRelationMapper;
-import com.cyksj.mapper.UserTicketClearedRecordMapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.constant.Constant;
+import com.cyksj.mapper.*;
+import com.cyksj.model.entity.ClaudeCodeUser;
 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 +41,10 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 
 	private final MirrorUserCleatService mirrorUserCleatService;
 
+	private final ClaudeCodeService claudeCodeService;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
 	@Override
 	public void clearTicket(GroupsRelation relation, UserTicketClearedRecord.Source source) {
 		Long relationId = relation.getId();
@@ -129,6 +134,16 @@ 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());
+					//删除用户套餐
+					if (claudeCodeService.delUserPackages(groupsRelation.getUserId())) {
+						//清除claude code user
+						claudeCodeUserMapper.delete(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId));
+					}
+				}
 			}
 		}
 	}

+ 33 - 3
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -45,6 +45,7 @@ import com.cyksj.model.views.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.ChatGptAccountService;
 import com.cyksj.service.chatgpt.ChatGptAuthService;
+import com.cyksj.service.claude.ClaudeCodeService;
 import com.cyksj.service.claude.ClaudeService;
 import com.cyksj.service.groups.GroupsFuncService;
 import com.cyksj.service.luma.LumaService;
@@ -204,6 +205,10 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private final ChatgptUserMapper chatgptUserMapper;
 
+	private final ClaudeCodeUserRenewExpiryRecordMapper claudeCodeUserRenewExpiryRecordMapper;
+
+	private final ClaudeCodeService claudeCodeService;
+
 	@Override
 	public SearchResult<RenewalView> getMyTicket(long userId, Boolean isLoginPopularize, String customId, String account, String orderNo, Integer cmt) {
 		User u = userMapper.selectById(userId);
@@ -1855,7 +1860,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 		if (groupsRelationView != null) {
 			String lang = request.getHeader("lang");
 			//clause pro
-			if (groupsRelationView.getGoodsId() == 75l) {
+			if (groupsRelationView.getGoodsId() == Constant.CLAUDE_PRO_GID) {
 				try {
 					ClaudeUserView userInfo = claudeService.getUserInfo(groupsRelationView.getUserId(), groupsRelationView.getId());
 					Long renewSkuId = userInfo.getRenewSkuId();
@@ -1869,7 +1874,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 				}
 			}
 
-			if (groupsRelationView.getGoodsId() == 18) {
+			if (groupsRelationView.getGoodsId() == Constant.GPT_PLUS_GID) {
 				try {
 					//gpt用户镜像
 					ChatGptUserView userInfo = chatGptAccountService.getUserInfo(groupsRelationView.getUserId(), groupsRelationView.getId());
@@ -1916,6 +1921,31 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 				} catch (Exception e) {
 				}
 			}
+			//claude code
+			if (groupsRelationView.getGoodsId() == Constant.CLAUDE_CODE_GOODS_ID) {
+				try {
+					//gpt用户镜像
+					ClaudeCodeUserInfoView userInfo = claudeCodeService.getClaudeCodeUserSubsInfo(userId);
+					ticketRenewUpgradeView.setCodePoints(userInfo.getCodePoints());
+					ticketRenewUpgradeView.setCodeCreditRecovery(userInfo.getCodeCreditRecovery());
+					ticketRenewUpgradeView.setIsRenewPackage(userInfo.getRenewSkuId() != null ? true : false);
+					Long renewSkuId = userInfo.getRenewSkuId();
+					if (renewSkuId != null) {
+						GoodsDonSkuRenewView renewSku = beanSearcher.searchFirst(GoodsDonSkuRenewView.class, MapUtils.builder().field(GoodsDonSkuRenewView::getSkuId, renewSkuId).build());
+						Optional.ofNullable(claudeCodeUserRenewExpiryRecordMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUserRenewExpiryRecord.class)
+								.eq(ClaudeCodeUserRenewExpiryRecord::getOrderId, userInfo.getRenewOrderId())
+								.last("limit 1"))).ifPresent(e -> renewSku.setOriginalOrderId(e.getOrderId()));
+						if (lang != null && !StrUtil.equals(UserPageLanguage.Language.zh_CN.name(), lang)) {
+							String languageSpecVal = backInfoTranslationsFrontService.getLanguageSpecVal(renewSku.getSkuId(), lang);
+							if (StrUtil.isNotBlank(languageSpecVal)) {
+								renewSku.setSpecVal(languageSpecVal);
+							}
+						}
+						ticketRenewUpgradeView.setRenewSku(renewSku);
+					}
+				} catch (Exception e) {
+				}
+			}
 		}
 		return ticketRenewUpgradeView;
 	}
@@ -1969,7 +1999,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private BigDecimal getRenewDiscount(Long goodsId, Long skuId, Boolean isMirror, Integer skuNum) {
 		//镜像或者独立规格
-		if (isMirror || (skuNum != null && skuNum == 1)) {
+		if (goodsId == Constant.CLAUDE_CODE_GOODS_ID || isMirror || (skuNum != null && skuNum == 1)) {
 			RenewUpgradePackage renewUpgradePackage = renewUpgradePackageMapper.selectOne(Wrappers.lambdaQuery(RenewUpgradePackage.class)
 					.eq(RenewUpgradePackage::getGoodsId, goodsId)
 					.eq(RenewUpgradePackage::getStatus, 1)

+ 377 - 0
netflix-web/src/main/java/com/cyksj/web/controller/claude/ClaudeCodeController.java

@@ -0,0 +1,377 @@
+package com.cyksj.web.controller.claude;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+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.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.ClaudeCodeUserMapper;
+import com.cyksj.mapper.GroupsMapper;
+import com.cyksj.mapper.GroupsRelationMapper;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.manage.views.OrderDonView;
+import com.cyksj.model.request.ClaudeCodeApiKeysReq;
+import com.cyksj.model.request.ClaudeCodeDelReq;
+import com.cyksj.model.response.ClaudeCodePointsHistoryResp;
+import com.cyksj.model.response.claudecode.ClaudeCodeResp;
+import com.cyksj.model.views.*;
+import com.cyksj.service.claude.AnnouncementService;
+import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.claude.OAuthClaudeCodeService;
+import com.cyksj.service.claude.RelayService;
+import com.cyksj.service.user.UserBindRelationService;
+import com.cyksj.service.user.UserService;
+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 lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * server/claude code
+ * 项目名: yhlxj11111111
+ * 文件名: ClaudeCodeController
+ * 创建者: JavaZou
+ * 创建时间:2025/8/8 11:37
+ */
+@Slf4j
+@RestController
+@RequestMapping("/applet/claude/code")
+@RequiredArgsConstructor
+public class ClaudeCodeController {
+
+	private final OAuthClaudeCodeService oAuthClaudeCodeService;
+
+	private final ClaudeCodeService claudeCodeService;
+
+	private final BeanSearcher beanSearcher;
+
+	private final HttpServletRequest request;
+
+	private final UserBindRelationService userBindRelationService;
+
+	private final AnnouncementService announcementService;
+
+	private final RelayService relayService;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
+	private final GroupsRelationMapper relationMapper;
+
+	private final GroupsMapper groupsMapper;
+
+	private final UserService userService;
+	/**
+	 * 用户创建API KEY
+	 */
+	@PostMapping("/create/apiKeys")
+	public ClaudeCodeResp createApiKeys(@RequestBody @Validated ClaudeCodeApiKeysReq req) throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		req.setUserId(userId);
+		ClaudeCodeResp resp = claudeCodeService.createApiKeys(req);
+		return resp;
+	}
+
+	/**
+	 * 用户api keys列表
+	 */
+	@GetMapping("/get/apiKeys")
+	public Result<List<ClaudeCodeUserApiKeysView>> getUserApiKeys() throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		List<ClaudeCodeUserApiKeysView> userApiKeys = claudeCodeService.getUserApiKeys(userId);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(userApiKeys);
+	}
+
+	/**
+	 * 删除api key
+	 */
+	@DeleteMapping("/delete/apiKeys")
+	public Result<String> deleteApiKeysById(@RequestBody ClaudeCodeDelReq delReq) throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		delReq.setUserId(userId);
+		claudeCodeService.deleteApiKeysById(delReq);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	//================================================用户============================================================
+
+	/**
+	 * 用户订阅车票信息
+	 */
+	@GetMapping("/get/user/subs/info")
+	public Result<ClaudeCodeUserInfoView> getUserSubsInfo() {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodeUserInfoView claudeCodeUserInfoView = claudeCodeService.getClaudeCodeUserSubsInfo(userId);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(claudeCodeUserInfoView);
+	}
+
+	/**
+	 * 用户订阅历史
+	 */
+	@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);
+	}
+
+	/**
+	 * 用户积分明细
+	 * - `page` (int, optional): 页码,默认1
+	 * - `limit` (int, optional): 每页数量,默认20,最大100
+	 * - `type` (string, optional): 积分类型过滤
+	 */
+	@GetMapping("/get/user/points/detail")
+	public ClaudeCodePointsHistoryResp getUserPointsDetail(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String type) throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodePointsHistoryResp resp = claudeCodeService.getUserPointsDetail(userId, type, page, limit);
+		return resp;
+	}
+
+	/**
+	 * 仪表盘聚合
+	 * 返回 当前订阅/余额卡片 + 今日汇总 + 今日请求列表
+	 */
+	@GetMapping("/get/dashboard")
+	public ClaudeCodeResp getUserDashboard() throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodeResp resp = claudeCodeService.getUserDashboard(userId);
+		return resp;
+	}
+
+
+	/**
+	 * 余额卡片
+	 * 单独返回余额卡片数据
+	 */
+	@GetMapping("/get/balance")
+	public ClaudeCodeResp getUserBalance() throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodeResp resp = claudeCodeService.getUserBalance(userId);
+		return 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 ClaudeCodeResp getCreditsAnalytics(String start, String end, @RequestParam(defaultValue = "Asia/Shanghai") String tz, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, @RequestParam(defaultValue = "desc") String order, @RequestParam(defaultValue = "all") String type) throws Exception {
+		long userId = StpUserUtil.getLoginIdAsLong();
+		ClaudeCodeResp resp = claudeCodeService.getCreditsAnalytics(userId, start, end, tz, page, limit, order, type);
+		return resp;
+	}
+
+	/**
+	 * claude code可选套餐
+	 */
+	@GetMapping("/get/packages")
+	public Result<ClaudeCodePackageView> getClaudeCodePackages() {
+		Long userId = StpUserUtil.getUserIdAfterLogin();
+		ClaudeCodePackageView claudeCodePackageView = new ClaudeCodePackageView();
+		if (userId != null) {
+			List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+			ClaudeCodeUser user = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class)
+					.in(ClaudeCodeUser::getUserId, userIdList)
+					.last("limit 1"));
+			if (user != null) {
+				Integer userCodePoints = user.getCodePoints();
+				GroupsRelation relation = relationMapper.selectById(user.getRelationId());
+				GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+				Long skuId = groupsTrips.getSkuId();
+				List<RenewUpgradePackageFrontView> renewUpgradePackageFrontViews = beanSearcher.searchAll(RenewUpgradePackageFrontView.class, MapUtils.builder().field(RenewUpgradePackageFrontView::getSkuId, skuId).build());
+				renewUpgradePackageFrontViews.forEach(renew -> {
+					try {
+						List<Long> upgradeSkuIds = Jsons.parseList(renew.getUpgradeSkuIds(), Long.class);
+						if (user.getRenewSkuId() != null) {
+							upgradeSkuIds.remove(user.getRenewSkuId());
+						}
+						List<RenewUpgradePackageFrontView.RenewSkuView> renewSkuViews = new ArrayList<>();
+						if (CollUtil.isNotEmpty(upgradeSkuIds)) {
+							renewSkuViews = beanSearcher.searchAll(RenewUpgradePackageFrontView.RenewSkuView.class, MapUtils.builder()
+									//高套餐
+									.field(RenewUpgradePackageFrontView.RenewSkuView::getCodePoints, userCodePoints).op(Operator.GreaterThan)
+									.field(RenewUpgradePackageFrontView.RenewSkuView::getSkuId, upgradeSkuIds).op(Operator.InList)
+									.build());
+						}
+						renew.setRenewSkus(renewSkuViews);
+					} catch (Exception e) {
+					}
+				});
+				claudeCodePackageView.setUpgrades(renewUpgradePackageFrontViews);
+			}
+		}
+		List<GoodsDonSkuView> goodsDonSkuViews = beanSearcher.searchAll(GoodsDonSkuView.class, MapUtils.builder().field(GoodsDonSkuView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID).field(GoodsDonSkuView::getGoodsDonSkuStatus, Boolean.TRUE).build());
+		claudeCodePackageView.setSkus(goodsDonSkuViews);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(claudeCodePackageView);
+	}
+
+	//================================================Claude Code OAuth接口============================================================
+
+
+	/**
+	 * OAuth认证验证方法
+	 */
+	private Long authenticateOAuth(HttpServletRequest request) {
+		String authHeader = request.getHeader("Authorization");
+		if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+			throw BusinessRuntimeException.getInstance("OAuth token required");
+		}
+
+		String tokenValue = authHeader.substring(7); // 移除"Bearer "前缀
+
+		// 验证token
+		OAuthToken token = oAuthClaudeCodeService.validateToken(null, tokenValue);
+		if (token == null) {
+			throw BusinessRuntimeException.getInstance("Invalid or expired token");
+		}
+
+		return token.getUserId();
+	}
+
+	/**
+	 * OAuth授权端点
+	 */
+	@GetMapping("/authorize")
+	public Result<String> authorize(@RequestParam(name = "client_id") String clientId,
+									@RequestParam String state) throws Exception {
+		// 验证参数
+		if (clientId == null || clientId.isEmpty() ||
+				state == null || state.isEmpty()) {
+			throw BusinessRuntimeException.getInstance("缺少必填参数");
+		}
+		if (!StpUserUtil.isLogin()) {
+			throw BusinessRuntimeException.getInstance("请先登录");
+		} else {
+			Long userId = StpUserUtil.getLoginIdAsLong();
+			String token = oAuthClaudeCodeService.generateAccessToken(userId, clientId);
+			if (StrUtil.isNotBlank(token)) {
+				log.info("OAuth授权成功: userId={}, clientId={}, token={}", userId, clientId, token.substring(0, 8) + "...");
+			}
+			return GatewayResponse.SUCCESS.newBuilder().toResult(token);
+		}
+	}
+
+	/**
+	 * Token验证端点
+	 */
+	@PostMapping("/verify-token")
+	public Map<String, Object> verifyToken(@RequestBody Map<String, String> request) {
+
+		try {
+			String token = request.get("token");
+			String code = request.get("code");
+			String clientId = request.get("client_id");
+			String clientSecret = request.get("client_secret");
+
+			// 验证客户端凭据
+			OAuthClient client = oAuthClaudeCodeService.validateClient(clientId, clientSecret);
+			if (client == null) {
+				throw BusinessRuntimeException.getInstance("Invalid client credentials");
+			}
+
+			// 验证token
+			OAuthToken oauthToken = oAuthClaudeCodeService.validateToken(code,token);
+			if (oauthToken == null) {
+				throw BusinessRuntimeException.getInstance("Invalid or expired token");
+			}
+			User user = userService.getById(oauthToken.getUserId());
+			// 返回用户信息
+			Map<String, Object> result = new HashMap<>();
+			result.put("authenticated", true);
+			result.put("userId", oauthToken.getUserId());
+			result.put("nickName", user.getNickname());
+			result.put("headImg", user.getHeadimgurl());
+			result.put("token", oauthToken.getKeyValue());
+			result.put("email", getUserEmail(oauthToken.getUserId()));
+
+			log.info("Token验证成功: userId={}, clientId={}", oauthToken.getUserId(), clientId);
+
+			return result;
+
+		} catch (Exception e) {
+			log.error("Token验证失败", e);
+			throw BusinessRuntimeException.getInstance("Token verification failed");
+		}
+	}
+
+	/**
+	 * 验证客户端ID是否有效
+	 */
+	private boolean isValidClientId(String clientId) {
+		// 这里可以添加更严格的客户端ID验证逻辑
+		return "vvxked850tk1ikvhpk07s47a2vnppa7t".equals(clientId);
+	}
+
+	/**
+	 * 获取用户邮箱
+	 */
+	private String getUserEmail(Long userId) {
+		// 这里需要根据实际的用户表结构来获取用户邮箱
+		return "user" + userId + "@example.com";
+	}
+
+	/**
+	 * 获取公告列表
+	 */
+	@GetMapping("/announcements")
+	public Result<List<Announcement>> getAnnouncements(@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN") String language) {
+		try {
+			// OAuth认证
+			authenticateOAuth(request);
+
+			// 处理语言参数
+			if (language.startsWith("en")) {
+				language = "en";
+			} else {
+				language = "zh-CN";
+			}
+
+			List<Announcement> announcements = announcementService.getActiveAnnouncements(language);
+			return GatewayResponse.SUCCESS.newBuilder().toResult(announcements);
+		} catch (Exception e) {
+			throw BusinessRuntimeException.getInstance("Failed to fetch announcements");
+		}
+	}
+
+	/**
+	 * 获取中继服务器列表
+	 */
+	@GetMapping("/relays")
+	public Result<List<RelayServer>> getRelays(@RequestParam(required = false) String domain) {
+		try {
+			// OAuth认证
+			authenticateOAuth(request);
+
+			List<RelayServer> relays = relayService.getAvailableRelays(domain);
+			return GatewayResponse.SUCCESS.newBuilder().toResult(relays);
+		} catch (Exception e) {
+			throw BusinessRuntimeException.getInstance("Failed to fetch relay servers");
+		}
+	}
+
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
netflix-web/src/main/resources/application-prd.yml


+ 12 - 1
netflix-web/src/main/resources/application-pre.yml

@@ -209,4 +209,15 @@ midjourney:
 
 luma:
   url: https://luma.galaxydvd.com
-  videoUrl: https://luma.galaxydvd.com/videoTest/web
+  videoUrl: https://luma.galaxydvd.com/videoTest/web
+
+oauth:
+  default_client:
+    id: "vvxked850tk1ikvhpk07s47a2vnppa7t"
+    secret: "famdkhfn7sle9ranli50uh8vkhrou95mu3j6tp173jkv7piv52mtinsdbv68knsz"
+    name: "Claude Code Official Client"
+
+announcements:
+  enabled: true
+  default_language: "en"
+  cache_ttl: 300 # 5分钟缓存

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно