Parcourir la source

claudecode test

chan il y a 1 an
Parent
commit
eac255f5dc
51 fichiers modifiés avec 2911 ajouts et 1 suppressions
  1. 1 1
      netflix-common/pom.xml
  2. 14 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/AnnouncementMapper.java
  3. 7 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/ClaudeCodeUserMapper.java
  4. 14 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/DeviceCodeMapper.java
  5. 14 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthClientMapper.java
  6. 14 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthTokenMapper.java
  7. 14 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/RelayServerMapper.java
  8. 74 0
      netflix-dao/src/main/java/com/cyksj/model/dto/ClaudeCodeWhoami.java
  9. 66 0
      netflix-dao/src/main/java/com/cyksj/model/entity/Announcement.java
  10. 73 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeCodeUser.java
  11. 53 0
      netflix-dao/src/main/java/com/cyksj/model/entity/DeviceCode.java
  12. 57 0
      netflix-dao/src/main/java/com/cyksj/model/entity/OAuthClient.java
  13. 52 0
      netflix-dao/src/main/java/com/cyksj/model/entity/OAuthToken.java
  14. 73 0
      netflix-dao/src/main/java/com/cyksj/model/entity/RelayServer.java
  15. 317 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/AdminApiService.java
  16. 125 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/AdminApiServiceDemo.java
  17. 58 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/AnnouncementService.java
  18. 177 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/OAuthService.java
  19. 60 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/RelayService.java
  20. 249 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/client/AdminApiClient.java
  21. 56 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/config/AdminApiConfig.java
  22. 13 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/BatchUserStatusRequest.java
  23. 13 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/CreateApiKeyRequest.java
  24. 26 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/CreateUserPlanRequest.java
  25. 11 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/CreditAdjustRequest.java
  26. 11 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/InvalidateApiKeyCacheRequest.java
  27. 13 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/UpdateApiKeyStatusRequest.java
  28. 16 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/AdminApiResponse.java
  29. 13 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyCacheResponse.java
  30. 16 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyDeleteResponse.java
  31. 25 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyInfo.java
  32. 27 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyResponse.java
  33. 17 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyStatusResponse.java
  34. 31 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/BatchUserStatusItem.java
  35. 17 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CacheInvalidateResponse.java
  36. 21 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CacheStatsResponse.java
  37. 20 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CreditAdjustResponse.java
  38. 26 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CreditHistoryItem.java
  39. 65 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CreditsAnalyticsResponse.java
  40. 66 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/DashboardResponse.java
  41. 20 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/PageResponse.java
  42. 20 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/RecoveryStatusResponse.java
  43. 16 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/RecoveryTriggerResponse.java
  44. 52 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/UsageStatsResponse.java
  45. 37 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/UserBalanceResponse.java
  46. 45 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/UserPlanResponse.java
  47. 48 0
      netflix-service/src/main/java/com/cyksj/service/claudecode/exception/AdminApiException.java
  48. 30 0
      netflix-web/src/main/java/com/cyksj/web/config/OAuthFilterConfig.java
  49. 525 0
      netflix-web/src/main/java/com/cyksj/web/controller/claudecode/ClaudeCodeController.java
  50. 103 0
      netflix-web/src/main/java/com/cyksj/web/filter/OAuthAuthFilter.java
  51. 0 0
      netflix-web/src/main/resources/application-dev.yml

+ 1 - 1
netflix-common/pom.xml

@@ -108,7 +108,7 @@
         <dependency>
             <groupId>com.tencentcloudapi</groupId>
             <artifactId>tencentcloud-sdk-java</artifactId>
-            <version>4.0.11</version>
+            <version>3.1.1319</version>
         </dependency>
     </dependencies>
 

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

@@ -0,0 +1,14 @@
+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
+ * @author chan
+ * @date 2025/8/10
+ */
+@Mapper
+public interface AnnouncementMapper extends BaseMapper<Announcement> {
+}

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

@@ -0,0 +1,7 @@
+package com.cyksj.mapper.claudecode;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeCodeUser;
+
+public interface ClaudeCodeUserMapper extends BaseMapper<ClaudeCodeUser> {
+}

+ 14 - 0
netflix-dao/src/main/java/com/cyksj/mapper/claudecode/DeviceCodeMapper.java

@@ -0,0 +1,14 @@
+package com.cyksj.mapper.claudecode;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.DeviceCode;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 设备验证码Mapper
+ * @author chan
+ * @date 2025/8/10
+ */
+@Mapper
+public interface DeviceCodeMapper extends BaseMapper<DeviceCode> {
+}

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

@@ -0,0 +1,14 @@
+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
+ * @author chan
+ * @date 2025/8/10
+ */
+@Mapper
+public interface OAuthClientMapper extends BaseMapper<OAuthClient> {
+}

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

@@ -0,0 +1,14 @@
+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
+ * @author chan
+ * @date 2025/8/10
+ */
+@Mapper
+public interface OAuthTokenMapper extends BaseMapper<OAuthToken> {
+}

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

@@ -0,0 +1,14 @@
+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
+ * @author chan
+ * @date 2025/8/10
+ */
+@Mapper
+public interface RelayServerMapper extends BaseMapper<RelayServer> {
+}

+ 74 - 0
netflix-dao/src/main/java/com/cyksj/model/dto/ClaudeCodeWhoami.java

@@ -0,0 +1,74 @@
+package com.cyksj.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * Claude Code whoami响应
+ * @author chan
+ * @date 2025/8/10
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+public class ClaudeCodeWhoami implements Serializable {
+    
+    /**
+     * 是否已登录
+     */
+    private Boolean isLogin;
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    
+    /**
+     * Claude Code用户ID
+     */
+    private Long claudeCodeUserId;
+    
+    /**
+     * 名称
+     */
+    private String name;
+    
+    /**
+     * 头像
+     */
+    private String img;
+    
+    /**
+     * 套餐
+     */
+    private String plan;
+    
+    /**
+     * 积分上限
+     */
+    private Long creditLimit;
+    
+    /**
+     * 每小时恢复
+     */
+    private Long creditRecovery;
+    
+    /**
+     * 过期时间
+     */
+    private Date expireTime;
+    
+    /**
+     * 备注
+     */
+    private String remark;
+    
+    /**
+     * 消息
+     */
+    private String message;
+}

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

@@ -0,0 +1,66 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.cyksj.model.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 公告实体类
+ * @author chan
+ * @date 2025/8/10
+ */
+@Data
+@TableName("announcements")
+public class Announcement extends BaseEntity {
+
+    /**
+     * 公告标题
+     */
+    private String title;
+
+    /**
+     * 公告内容
+     */
+    private String description;
+
+    /**
+     * 公告类型:info/warning/error/success
+     */
+    private String type;
+
+    /**
+     * 目标受众:all/new_users/existing_users
+     */
+    @TableField("target_audience")
+    private String targetAudience;
+
+    /**
+     * 语言
+     */
+    private String language;
+
+    /**
+     * 开始时间
+     */
+    @TableField("start_date")
+    private Date startDate;
+
+    /**
+     * 结束时间
+     */
+    @TableField("end_date")
+    private Date endDate;
+
+    /**
+     * 状态:active/inactive
+     */
+    private String status;
+
+    /**
+     * 优先级
+     */
+    private Integer priority;
+}

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

@@ -0,0 +1,73 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.cyksj.model.BaseEntity;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @date 2025/8/10 17:10
+ */
+@Data
+@TableName("claude_code_user")
+@SearchBean(tables = "claude_code_user")
+public class ClaudeCodeUser extends BaseEntity {
+
+    /**
+     * 名称
+     */
+    private String name;
+
+    /**
+     * 头像
+     */
+    private String img;
+
+    /**
+     * 用户凭证
+     */
+    @TableField("userToken")
+    private String userToken;
+
+    /**
+     * 过期时间
+     */
+    @TableField("expireTime")
+    private Date expireTime;
+
+    /**
+     * 车票id
+     */
+    @TableField("relationId")
+    private Long relationId;
+
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 套餐
+     */
+    private String plan;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 积分上限
+     */
+    private Long creditLimit;
+
+    /**
+     * 每小时恢复
+     */
+    private Long creditRecovery;
+}

+ 53 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/DeviceCode.java

@@ -0,0 +1,53 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.cyksj.model.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 设备验证码实体类
+ * @author chan
+ * @date 2025/8/10
+ */
+@Data
+@TableName("device_codes")
+public class DeviceCode extends BaseEntity {
+
+    /**
+     * 设备代码
+     */
+    @TableField("device_code")
+    private String deviceCode;
+
+    /**
+     * 用户代码
+     */
+    @TableField("user_code")
+    private String userCode;
+
+    /**
+     * 客户端ID
+     */
+    @TableField("client_id")
+    private String clientId;
+
+    /**
+     * 用户ID(确认后填充)
+     */
+    @TableField("user_id")
+    private Long userId;
+
+    /**
+     * 过期时间
+     */
+    @TableField("expires_at")
+    private Date expiresAt;
+
+    /**
+     * 状态:pending/confirmed/expired/used
+     */
+    private String status;
+}

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

@@ -0,0 +1,57 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.cyksj.model.BaseEntity;
+import lombok.Data;
+
+/**
+ * OAuth客户端实体类
+ * @author chan
+ * @date 2025/8/10
+ */
+@Data
+@TableName("oauth_clients")
+public class OAuthClient extends BaseEntity {
+
+    /**
+     * 客户端ID
+     */
+    @TableField("client_id")
+    private String clientId;
+
+    /**
+     * 客户端密钥
+     */
+    @TableField("client_secret")
+    private String clientSecret;
+
+    /**
+     * 客户端名称
+     */
+    @TableField("client_name")
+    private String clientName;
+
+    /**
+     * 重定向URI列表(JSON格式)
+     */
+    @TableField("redirect_uris")
+    private String redirectUris;
+
+    /**
+     * 允许的权限范围(JSON格式)
+     */
+    @TableField("allowed_scopes")
+    private String allowedScopes;
+
+    /**
+     * Token有效期(秒)
+     */
+    @TableField("token_lifetime")
+    private Integer tokenLifetime;
+
+    /**
+     * 状态:active/inactive
+     */
+    private String status;
+}

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

@@ -0,0 +1,52 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.cyksj.model.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * OAuth令牌实体类
+ * @author chan
+ * @date 2025/8/10
+ */
+@Data
+@TableName("oauth_tokens")
+public class OAuthToken extends BaseEntity {
+
+    /**
+     * Token值
+     */
+    @TableField("token_value")
+    private String tokenValue;
+
+    /**
+     * 用户ID
+     */
+    @TableField("user_id")
+    private Long userId;
+
+    /**
+     * 客户端ID
+     */
+    @TableField("client_id")
+    private String clientId;
+
+    /**
+     * 过期时间
+     */
+    @TableField("expires_at")
+    private Date expiresAt;
+
+    /**
+     * 权限范围
+     */
+    private String scope;
+
+    /**
+     * 状态:active/revoked/expired
+     */
+    private String status;
+}

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

@@ -0,0 +1,73 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.cyksj.model.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 中继服务器实体类
+ * @author chan
+ * @date 2025/8/10
+ */
+@Data
+@TableName("relay_servers")
+public class RelayServer extends BaseEntity {
+
+    /**
+     * 主机地址
+     */
+    private String host;
+
+    /**
+     * 服务器名称
+     */
+    private String name;
+
+    /**
+     * 地区
+     */
+    private String region;
+
+    /**
+     * 状态:active/inactive/maintenance
+     */
+    private String status;
+
+    /**
+     * 优先级
+     */
+    private Integer priority;
+
+    /**
+     * 延迟毫秒数
+     */
+    @TableField("latency_ms")
+    private Integer latencyMs;
+
+    /**
+     * 最大并发请求数
+     */
+    @TableField("max_concurrent_requests")
+    private Integer maxConcurrentRequests;
+
+    /**
+     * 当前负载 0.0-1.0
+     */
+    @TableField("current_load")
+    private Double currentLoad;
+
+    /**
+     * 健康检查URL
+     */
+    @TableField("health_check_url")
+    private String healthCheckUrl;
+
+    /**
+     * 最后健康检查时间
+     */
+    @TableField("last_health_check")
+    private Date lastHealthCheck;
+}

+ 317 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/AdminApiService.java

@@ -0,0 +1,317 @@
+package com.cyksj.service.claudecode;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.log.Log;
+import cn.hutool.log.LogFactory;
+import com.cyksj.service.claudecode.client.AdminApiClient;
+import com.cyksj.service.claudecode.dto.request.*;
+import com.cyksj.service.claudecode.dto.response.*;
+import com.cyksj.service.claudecode.exception.AdminApiException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class AdminApiService {
+    
+    private static final Log log = LogFactory.get();
+    
+    @Autowired
+    private AdminApiClient adminApiClient;
+    
+    // ========== 用户套餐管理 ==========
+    
+    /**
+     * 创建或更新用户套餐
+     */
+    public UserPlanResponse createOrUpdateUserPlan(Long userId, CreateUserPlanRequest request) {
+        try {
+            String url = "/api/users/" + userId + "/plan";
+            AdminApiResponse<UserPlanResponse> response = adminApiClient.put(url, request, UserPlanResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("创建或更新用户套餐失败, userId: {}", userId, e);
+            throw new AdminApiException("创建或更新用户套餐失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 查询用户套餐
+     */
+    public UserPlanResponse getUserPlan(Long userId) {
+        try {
+            String url = "/api/users/" + userId + "/plan";
+            AdminApiResponse<UserPlanResponse> response = adminApiClient.get(url, UserPlanResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("查询用户套餐失败, userId: {}", userId, e);
+            throw new AdminApiException("查询用户套餐失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 调整用户积分
+     */
+    public CreditAdjustResponse adjustUserCredits(Long userId, CreditAdjustRequest request) {
+        try {
+            String url = "/api/users/" + userId + "/credits/adjust";
+            AdminApiResponse<CreditAdjustResponse> response = adminApiClient.post(url, request, CreditAdjustResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("调整用户积分失败, userId: {}", userId, e);
+            throw new AdminApiException("调整用户积分失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 查询用户余额
+     */
+    public UserBalanceResponse getUserBalance(Long userId) {
+        try {
+            String url = "/api/users/" + userId + "/balance";
+            AdminApiResponse<UserBalanceResponse> response = adminApiClient.get(url, UserBalanceResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("查询用户余额失败, userId: {}", userId, e);
+            throw new AdminApiException("查询用户余额失败: " + e.getMessage());
+        }
+    }
+    
+    // ========== API Key管理 ==========
+    
+    /**
+     * 创建API Key
+     */
+    public ApiKeyResponse createApiKey(Long userId, CreateApiKeyRequest request) {
+        try {
+            String url = "/api/users/" + userId + "/keys";
+            AdminApiResponse<ApiKeyResponse> response = adminApiClient.post(url, request, ApiKeyResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("创建API Key失败, userId: {}", userId, e);
+            throw new AdminApiException("创建API Key失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 查询用户API Keys
+     */
+    public List<ApiKeyInfo> getUserApiKeys(Long userId) {
+        try {
+            String url = "/api/users/" + userId + "/keys";
+            AdminApiResponse<List<ApiKeyInfo>> response = adminApiClient.getList(url, ApiKeyInfo.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("查询用户API Keys失败, userId: {}", userId, e);
+            throw new AdminApiException("查询用户API Keys失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新API Key状态
+     */
+    public ApiKeyStatusResponse updateApiKeyStatus(Long userId, Long keyId, UpdateApiKeyStatusRequest request) {
+        try {
+            String url = "/api/keys/" + userId + "/status?keyId=" + keyId;
+            AdminApiResponse<ApiKeyStatusResponse> response = adminApiClient.put(url, request, ApiKeyStatusResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("更新API Key状态失败, userId: {}, keyId: {}", userId, keyId, e);
+            throw new AdminApiException("更新API Key状态失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除API Key
+     */
+    public ApiKeyDeleteResponse deleteApiKey(Long userId, Long keyId) {
+        try {
+            String url = "/api/keys/" + userId + "?keyId=" + keyId;
+            AdminApiResponse<ApiKeyDeleteResponse> response = adminApiClient.delete(url, ApiKeyDeleteResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("删除API Key失败, userId: {}, keyId: {}", userId, keyId, e);
+            throw new AdminApiException("删除API Key失败: " + e.getMessage());
+        }
+    }
+    
+    // ========== 缓存管理 ==========
+    
+    /**
+     * 清理用户缓存
+     */
+    public CacheInvalidateResponse invalidateUserCache(Long userId) {
+        try {
+            String url = "/api/cache/invalidate/user/" + userId;
+            AdminApiResponse<CacheInvalidateResponse> response = adminApiClient.post(url, null, CacheInvalidateResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("清理用户缓存失败, userId: {}", userId, e);
+            throw new AdminApiException("清理用户缓存失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 清理API Key缓存
+     */
+    public ApiKeyCacheResponse invalidateApiKeyCache(String apiKey) {
+        try {
+            String url = "/api/cache/invalidate/apikey";
+            InvalidateApiKeyCacheRequest request = new InvalidateApiKeyCacheRequest();
+            request.setApiKey(apiKey);
+            AdminApiResponse<ApiKeyCacheResponse> response = adminApiClient.post(url, request, ApiKeyCacheResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("清理API Key缓存失败, apiKey: {}", apiKey, e);
+            throw new AdminApiException("清理API Key缓存失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取缓存统计信息
+     */
+    public CacheStatsResponse getCacheStats() {
+        try {
+            String url = "/api/cache/stats";
+            AdminApiResponse<CacheStatsResponse> response = adminApiClient.get(url, CacheStatsResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("获取缓存统计信息失败", e);
+            throw new AdminApiException("获取缓存统计信息失败: " + e.getMessage());
+        }
+    }
+    
+    // ========== 统计和历史 ==========
+    
+    /**
+     * 查询积分变化明细
+     */
+    public PageResponse<CreditHistoryItem> getCreditHistory(Long userId, Integer page, Integer limit, String type) {
+        try {
+            String url = "/api/users/" + userId + "/credits/history";
+            if (page != null || limit != null || StrUtil.isNotBlank(type)) {
+                url += "?";
+                if (page != null) url += "page=" + page + "&";
+                if (limit != null) url += "limit=" + limit + "&";
+                if (StrUtil.isNotBlank(type)) url += "type=" + type + "&";
+                url = url.substring(0, url.length() - 1);
+            }
+            AdminApiResponse<PageResponse> response = adminApiClient.get(url, PageResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("查询积分变化明细失败, userId: {}", userId, e);
+            throw new AdminApiException("查询积分变化明细失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户使用统计
+     */
+    public UsageStatsResponse getUserUsageStats(Long userId, Integer days) {
+        try {
+            String url = "/api/users/" + userId + "/usage/stats";
+            if (days != null) {
+                url += "?days=" + days;
+            }
+            AdminApiResponse<UsageStatsResponse> response = adminApiClient.get(url, UsageStatsResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("获取用户使用统计失败, userId: {}", userId, e);
+            throw new AdminApiException("获取用户使用统计失败: " + e.getMessage());
+        }
+    }
+    
+    // ========== 批量操作 ==========
+    
+    /**
+     * 批量查询用户状态
+     */
+    public List<BatchUserStatusItem> getBatchUserStatus(List<Long> userIds) {
+        try {
+            if (CollUtil.isEmpty(userIds) || userIds.size() > 100) {
+                throw new AdminApiException("用户ID列表为空或超过100个");
+            }
+            
+            String url = "/api/users/batch-status";
+            BatchUserStatusRequest request = new BatchUserStatusRequest();
+            request.setUserIds(userIds);
+            AdminApiResponse<List<BatchUserStatusItem>> response = adminApiClient.postList(url, request, BatchUserStatusItem.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("批量查询用户状态失败, userIds: {}", userIds, e);
+            throw new AdminApiException("批量查询用户状态失败: " + e.getMessage());
+        }
+    }
+    
+    // ========== 系统管理 ==========
+    
+    /**
+     * 查询积分恢复状态
+     */
+    public RecoveryStatusResponse getRecoveryStatus() {
+        try {
+            String url = "/api/system/recovery/status";
+            AdminApiResponse<RecoveryStatusResponse> response = adminApiClient.get(url, RecoveryStatusResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("查询积分恢复状态失败", e);
+            throw new AdminApiException("查询积分恢复状态失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 手动触发积分恢复
+     */
+    public RecoveryTriggerResponse triggerRecovery() {
+        try {
+            String url = "/api/system/recovery/trigger";
+            AdminApiResponse<RecoveryTriggerResponse> response = adminApiClient.post(url, null, RecoveryTriggerResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("手动触发积分恢复失败", e);
+            throw new AdminApiException("手动触发积分恢复失败: " + e.getMessage());
+        }
+    }
+    
+    // ========== 高级统计接口 ==========
+    
+    /**
+     * 获取用户仪表盘数据
+     */
+    public DashboardResponse getUserDashboard(Long userId) {
+        try {
+            String url = "/api/users/" + userId + "/dashboard";
+            AdminApiResponse<DashboardResponse> response = adminApiClient.get(url, DashboardResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("获取用户仪表盘数据失败, userId: {}", userId, e);
+            throw new AdminApiException("获取用户仪表盘数据失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分图表分析
+     */
+    public CreditsAnalyticsResponse getCreditsAnalytics(Long userId, String start, String end, 
+                                                        String tz, Integer page, Integer limit, 
+                                                        String order, String type) {
+        try {
+            String url = "/api/users/" + userId + "/credits/analytics";
+            url += "?start=" + start + "&end=" + end;
+            
+            if (StrUtil.isNotBlank(tz)) url += "&tz=" + tz;
+            if (page != null) url += "&page=" + page;
+            if (limit != null) url += "&limit=" + limit;
+            if (StrUtil.isNotBlank(order)) url += "&order=" + order;
+            if (StrUtil.isNotBlank(type)) url += "&type=" + type;
+            
+            AdminApiResponse<CreditsAnalyticsResponse> response = adminApiClient.get(url, CreditsAnalyticsResponse.class);
+            return response.getData();
+        } catch (Exception e) {
+            log.error("获取积分图表分析失败, userId: {}", userId, e);
+            throw new AdminApiException("获取积分图表分析失败: " + e.getMessage());
+        }
+    }
+}

+ 125 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/AdminApiServiceDemo.java

@@ -0,0 +1,125 @@
+package com.cyksj.service.claudecode;
+
+import cn.hutool.core.date.DateUtil;
+import com.cyksj.service.claudecode.dto.request.*;
+import com.cyksj.service.claudecode.dto.response.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * AdminApiService使用示例
+ */
+@Component
+public class AdminApiServiceDemo {
+    
+    @Autowired
+    private AdminApiService adminApiService;
+    
+    /**
+     * 创建用户套餐示例
+     */
+    public void createUserPlanDemo() {
+        Long userId = 1001L;
+        
+        CreateUserPlanRequest request = new CreateUserPlanRequest();
+        request.setPlanName("基础套餐");
+        request.setCreditLimit(10000L);
+        request.setCreditRecovery(100L);
+        request.setDurationDays(30);
+        // request.setExpireTime("2025-09-01 15:30:20"); // 可选择明确的过期时间
+        
+        UserPlanResponse response = adminApiService.createOrUpdateUserPlan(userId, request);
+        System.out.println("用户套餐创建成功: " + response.getPlanName());
+    }
+    
+    /**
+     * 调整用户积分示例
+     */
+    public void adjustUserCreditsDemo() {
+        Long userId = 1001L;
+        
+        CreditAdjustRequest request = new CreditAdjustRequest();
+        request.setAmount(-500L); // 扣除500积分
+        request.setDescription("扣除违规使用积分");
+        request.setType("adjustment");
+        
+        CreditAdjustResponse response = adminApiService.adjustUserCredits(userId, request);
+        System.out.println("积分调整成功,新余额: " + response.getNewBalance());
+    }
+    
+    /**
+     * 创建API Key示例
+     */
+    public void createApiKeyDemo() {
+        Long userId = 1001L;
+        
+        CreateApiKeyRequest request = new CreateApiKeyRequest();
+        request.setName("我的API Key");
+        request.setExpiresDays(30);
+        
+        ApiKeyResponse response = adminApiService.createApiKey(userId, request);
+        System.out.println("API Key创建成功: " + response.getKeyValue());
+    }
+    
+    /**
+     * 批量查询用户状态示例
+     */
+    public void batchQueryUserStatusDemo() {
+        List<Long> userIds = Arrays.asList(1001L, 1002L, 1003L);
+        
+        List<BatchUserStatusItem> responses = adminApiService.getBatchUserStatus(userIds);
+        responses.forEach(item -> {
+            System.out.println("用户ID: " + item.getUserId() + 
+                             ", 状态: " + item.getStatus() + 
+                             ", 可用积分: " + item.getAvailableCredits());
+        });
+    }
+    
+    /**
+     * 获取用户仪表盘数据示例
+     */
+    public void getUserDashboardDemo() {
+        Long userId = 1001L;
+        
+        DashboardResponse response = adminApiService.getUserDashboard(userId);
+        System.out.println("用户套餐: " + response.getSubscription().getPlanName());
+        System.out.println("今日请求数: " + response.getTodaySummary().getRequests());
+        System.out.println("今日消费积分: " + response.getTodaySummary().getCreditsConsumed());
+    }
+    
+    /**
+     * 获取积分分析报告示例
+     */
+    public void getCreditsAnalyticsDemo() {
+        Long userId = 1001L;
+        String start = "2024-08-08 00:00:00";
+        String end = "2024-08-10 23:59:59";
+        
+        CreditsAnalyticsResponse response = adminApiService.getCreditsAnalytics(
+            userId, start, end, "Asia/Shanghai", 1, 20, "desc", "all"
+        );
+        
+        System.out.println("分析时间段: " + response.getMeta().getStart() + " ~ " + response.getMeta().getEnd());
+        System.out.println("总消费: " + response.getSummary().getConsumed());
+        System.out.println("总补充: " + response.getSummary().getRecharged());
+        System.out.println("净变化: " + response.getSummary().getNetChange());
+    }
+    
+    /**
+     * 系统管理示例
+     */
+    public void systemManagementDemo() {
+        // 查询积分恢复状态
+        RecoveryStatusResponse status = adminApiService.getRecoveryStatus();
+        System.out.println("待恢复用户数: " + status.getPendingRecoveryUsers());
+        System.out.println("系统状态: " + status.getSystemStatus());
+        
+        // 手动触发积分恢复
+        RecoveryTriggerResponse trigger = adminApiService.triggerRecovery();
+        System.out.println("恢复结果: " + trigger.getMessage());
+        System.out.println("恢复用户数: " + trigger.getRecoveredUsers());
+    }
+}

+ 58 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/AnnouncementService.java

@@ -0,0 +1,58 @@
+package com.cyksj.service.claudecode;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.mapper.claudecode.AnnouncementMapper;
+import com.cyksj.model.entity.Announcement;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 公告服务
+ * @author chan
+ * @date 2025/8/10
+ */
+@Service
+@RequiredArgsConstructor
+public class AnnouncementService {
+
+    private final AnnouncementMapper announcementMapper;
+
+    /**
+     * 获取活跃公告列表
+     */
+    public List<Announcement> getActiveAnnouncements(String language) {
+        QueryWrapper<Announcement> wrapper = new QueryWrapper<>();
+        wrapper.eq("status", "active")
+                .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("create_time");
+
+        return announcementMapper.selectList(wrapper);
+    }
+
+    /**
+     * 创建公告
+     */
+    public boolean createAnnouncement(Announcement announcement) {
+        return announcementMapper.insert(announcement) > 0;
+    }
+
+    /**
+     * 更新公告
+     */
+    public boolean updateAnnouncement(Announcement announcement) {
+        return announcementMapper.updateById(announcement) > 0;
+    }
+
+    /**
+     * 删除公告
+     */
+    public boolean deleteAnnouncement(Long id) {
+        return announcementMapper.deleteById(id) > 0;
+    }
+}

+ 177 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/OAuthService.java

@@ -0,0 +1,177 @@
+package com.cyksj.service.claudecode;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.mapper.claudecode.DeviceCodeMapper;
+import com.cyksj.mapper.claudecode.OAuthClientMapper;
+import com.cyksj.mapper.claudecode.OAuthTokenMapper;
+import com.cyksj.model.entity.DeviceCode;
+import com.cyksj.model.entity.OAuthClient;
+import com.cyksj.model.entity.OAuthToken;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.security.SecureRandom;
+import java.util.Date;
+
+/**
+ * OAuth认证服务
+ * @author chan
+ * @date 2025/8/10
+ */
+@Service
+@RequiredArgsConstructor
+public class OAuthService {
+
+    private final OAuthTokenMapper oauthTokenMapper;
+    private final OAuthClientMapper oauthClientMapper;
+    private final DeviceCodeMapper deviceCodeMapper;
+    private final SecureRandom secureRandom = new SecureRandom();
+
+    /**
+     * 生成访问令牌
+     */
+    public String generateAccessToken(Long userId, String clientId) {
+        // 生成随机token
+        byte[] tokenBytes = new byte[32];
+        secureRandom.nextBytes(tokenBytes);
+        StringBuilder tokenValue = new StringBuilder();
+        for (byte b : tokenBytes) {
+            tokenValue.append(String.format("%02x", b));
+        }
+
+        // 保存到数据库
+        OAuthToken token = new OAuthToken();
+        token.setTokenValue(tokenValue.toString());
+        token.setUserId(userId);
+        token.setClientId(clientId);
+        token.setExpiresAt(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000L)); // 24小时有效期
+        token.setStatus("active");
+
+        int result = oauthTokenMapper.insert(token);
+        if (result <= 0) {
+            throw BusinessRuntimeException.getInstance("生成访问令牌失败");
+        }
+
+        return tokenValue.toString();
+    }
+
+    /**
+     * 验证访问令牌
+     */
+    public OAuthToken validateToken(String tokenValue) {
+        QueryWrapper<OAuthToken> wrapper = new QueryWrapper<>();
+        wrapper.eq("token_value", tokenValue)
+                .eq("status", "active")
+                .gt("expires_at", new Date());
+
+        OAuthToken token = oauthTokenMapper.selectOne(wrapper);
+        if (token == null) {
+            throw BusinessRuntimeException.getInstance("无效或已过期的令牌");
+        }
+
+        return token;
+    }
+
+    /**
+     * 生成设备验证码
+     */
+    public DeviceCode generateDeviceCode(String clientId) {
+        String deviceCode = generateRandomString(32);
+        String userCode = generateUserCode();
+
+        DeviceCode device = new DeviceCode();
+        device.setDeviceCode(deviceCode);
+        device.setUserCode(userCode);
+        device.setClientId(clientId);
+        device.setExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000L)); // 10分钟有效期
+        device.setStatus("pending");
+
+        int result = deviceCodeMapper.insert(device);
+        if (result <= 0) {
+            throw BusinessRuntimeException.getInstance("生成设备验证码失败");
+        }
+
+        return device;
+    }
+
+    /**
+     * 验证设备验证码
+     */
+    public DeviceCode validateDeviceCode(String userCode) {
+        QueryWrapper<DeviceCode> wrapper = new QueryWrapper<>();
+        wrapper.eq("user_code", userCode)
+                .eq("status", "confirmed")
+                .gt("expires_at", new Date());
+
+        DeviceCode device = deviceCodeMapper.selectOne(wrapper);
+        if (device == null) {
+            throw BusinessRuntimeException.getInstance("无效或已过期的验证码");
+        }
+
+        // 标记为已使用
+        device.setStatus("used");
+        deviceCodeMapper.updateById(device);
+
+        return device;
+    }
+
+    /**
+     * 确认设备验证码
+     */
+    public void confirmDeviceCode(String userCode, Long userId) {
+        QueryWrapper<DeviceCode> wrapper = new QueryWrapper<>();
+        wrapper.eq("user_code", userCode)
+                .eq("status", "pending")
+                .gt("expires_at", new Date());
+
+        DeviceCode device = deviceCodeMapper.selectOne(wrapper);
+        if (device == null) {
+            throw BusinessRuntimeException.getInstance("无效或已过期的验证码");
+        }
+
+        device.setUserId(userId);
+        device.setStatus("confirmed");
+        
+        int result = deviceCodeMapper.updateById(device);
+        if (result <= 0) {
+            throw BusinessRuntimeException.getInstance("确认设备验证码失败");
+        }
+    }
+
+    /**
+     * 验证客户端凭据
+     */
+    public OAuthClient validateClient(String clientId, String clientSecret) {
+        QueryWrapper<OAuthClient> wrapper = new QueryWrapper<>();
+        wrapper.eq("client_id", clientId)
+                .eq("client_secret", clientSecret)
+                .eq("status", "active");
+
+        OAuthClient client = oauthClientMapper.selectOne(wrapper);
+        if (client == null) {
+            throw BusinessRuntimeException.getInstance("无效的客户端凭据");
+        }
+
+        return client;
+    }
+
+    private String generateRandomString(int length) {
+        byte[] bytes = new byte[length];
+        secureRandom.nextBytes(bytes);
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+
+    private String generateUserCode() {
+        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+        StringBuilder code = new StringBuilder();
+        for (int i = 0; i < 8; i++) {
+            code.append(chars.charAt(secureRandom.nextInt(chars.length())));
+        }
+        return code.toString();
+    }
+}

+ 60 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/RelayService.java

@@ -0,0 +1,60 @@
+package com.cyksj.service.claudecode;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.mapper.claudecode.RelayServerMapper;
+import com.cyksj.model.entity.RelayServer;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 中继服务器服务
+ * @author chan
+ * @date 2025/8/10
+ */
+@Service
+@RequiredArgsConstructor
+public class RelayService {
+
+    private final RelayServerMapper relayServerMapper;
+
+    /**
+     * 获取可用中继服务器列表
+     */
+    public List<RelayServer> getActiveRelays(String domain) {
+        QueryWrapper<RelayServer> wrapper = new QueryWrapper<>();
+        wrapper.eq("status", "active");
+        
+        if (StringUtils.hasText(domain)) {
+            wrapper.like("host", domain);
+        }
+        
+        wrapper.orderByDesc("priority")
+                .orderByAsc("latency_ms");
+
+        return relayServerMapper.selectList(wrapper);
+    }
+
+    /**
+     * 创建中继服务器
+     */
+    public boolean createRelay(RelayServer relay) {
+        return relayServerMapper.insert(relay) > 0;
+    }
+
+    /**
+     * 更新中继服务器
+     */
+    public boolean updateRelay(RelayServer relay) {
+        return relayServerMapper.updateById(relay) > 0;
+    }
+
+    /**
+     * 删除中继服务器
+     */
+    public boolean deleteRelay(Long id) {
+        return relayServerMapper.deleteById(id) > 0;
+    }
+}

+ 249 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/client/AdminApiClient.java

@@ -0,0 +1,249 @@
+package com.cyksj.service.claudecode.client;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.http.Method;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONUtil;
+import cn.hutool.log.Log;
+import cn.hutool.log.LogFactory;
+import com.cyksj.service.claudecode.config.AdminApiConfig;
+import com.cyksj.service.claudecode.dto.response.AdminApiResponse;
+import com.cyksj.service.claudecode.exception.AdminApiException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+
+@Component
+public class AdminApiClient {
+    
+    private static final Log log = LogFactory.get();
+    
+    @Autowired
+    private AdminApiConfig config;
+    
+    private static final String ADMIN_KEY_HEADER = "X-Admin-Key";
+    private static final String CONTENT_TYPE = "application/json";
+    
+    /**
+     * 发送GET请求
+     */
+    public <T> AdminApiResponse<T> get(String path, Class<T> responseClass) {
+        return execute(Method.GET, path, null, responseClass);
+    }
+    
+    /**
+     * 发送GET请求并返回列表
+     */
+    public <T> AdminApiResponse<List<T>> getList(String path, Class<T> itemClass) {
+        return executeForList(Method.GET, path, null, itemClass);
+    }
+    
+    /**
+     * 发送POST请求
+     */
+    public <T> AdminApiResponse<T> post(String path, Object requestBody, Class<T> responseClass) {
+        return execute(Method.POST, path, requestBody, responseClass);
+    }
+    
+    /**
+     * 发送POST请求并返回列表
+     */
+    public <T> AdminApiResponse<List<T>> postList(String path, Object requestBody, Class<T> itemClass) {
+        return executeForList(Method.POST, path, requestBody, itemClass);
+    }
+    
+    /**
+     * 发送PUT请求
+     */
+    public <T> AdminApiResponse<T> put(String path, Object requestBody, Class<T> responseClass) {
+        return execute(Method.PUT, path, requestBody, responseClass);
+    }
+    
+    /**
+     * 发送DELETE请求
+     */
+    public <T> AdminApiResponse<T> delete(String path, Class<T> responseClass) {
+        return execute(Method.DELETE, path, null, responseClass);
+    }
+    
+    /**
+     * 带分页参数的GET请求
+     */
+    public <T> AdminApiResponse<T> get(String path, Class<T> responseClass, Class<?> itemClass) {
+        return execute(Method.GET, path, null, responseClass);
+    }
+    
+    /**
+     * 执行HTTP请求
+     */
+    @SuppressWarnings("unchecked")
+    private <T> AdminApiResponse<T> execute(Method method, String path, Object requestBody, Class<T> responseClass) {
+        String fullUrl = config.getBaseUrl() + path;
+        
+        try {
+            HttpRequest request = HttpUtil.createRequest(method, fullUrl);
+            
+            // 设置请求头
+            request.header(ADMIN_KEY_HEADER, config.getAdminApiKey())
+                   .header("Content-Type", CONTENT_TYPE)
+                   .timeout(config.getTimeout());
+            
+            // 设置请求体
+            if (requestBody != null) {
+                String jsonBody = JSONUtil.toJsonStr(requestBody);
+                request.body(jsonBody);
+                log.debug("Admin API Request: {} {} - Body: {}", method, fullUrl, jsonBody);
+            } else {
+                log.debug("Admin API Request: {} {}", method, fullUrl);
+            }
+            
+            // 发送请求
+            HttpResponse response = request.execute();
+            String responseBody = response.body();
+            
+            log.debug("Admin API Response: {} - Status: {} - Body: {}", 
+                     fullUrl, response.getStatus(), responseBody);
+            
+            // 检查HTTP状态码
+            if (!response.isOk()) {
+                log.error("Admin API请求失败: {} - Status: {} - Response: {}", 
+                         fullUrl, response.getStatus(), responseBody);
+                throw new AdminApiException("HTTP请求失败: " + response.getStatus() + " - " + responseBody);
+            }
+            
+            // 解析响应
+            if (StrUtil.isBlank(responseBody)) {
+                throw new AdminApiException("响应体为空");
+            }
+            
+            // 先解析为泛型对象
+            AdminApiResponse<?> tempResponse = JSONUtil.toBean(responseBody, AdminApiResponse.class);
+            
+            // 检查业务状态码
+            if (tempResponse.getCode() != 0) {
+                String errorMsg = "API业务错误: code=" + tempResponse.getCode() + 
+                                 ", message=" + tempResponse.getMessage();
+                if (StrUtil.isNotBlank(tempResponse.getDetail())) {
+                    errorMsg += ", detail=" + tempResponse.getDetail();
+                }
+                log.error(errorMsg);
+                throw new AdminApiException(errorMsg);
+            }
+            
+            // 创建最终结果对象
+            AdminApiResponse<T> apiResponse = new AdminApiResponse<>();
+            apiResponse.setCode(tempResponse.getCode());
+            apiResponse.setMessage(tempResponse.getMessage());
+            apiResponse.setDetail(tempResponse.getDetail());
+            
+            // 解析data字段
+            if (tempResponse.getData() != null && responseClass != Object.class) {
+                T dataObj = JSONUtil.toBean(JSONUtil.toJsonStr(tempResponse.getData()), responseClass);
+                apiResponse.setData(dataObj);
+            } else {
+                apiResponse.setData((T) tempResponse.getData());
+            }
+            
+            return apiResponse;
+            
+        } catch (AdminApiException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("Admin API请求异常: {} {}", method, fullUrl, e);
+            throw new AdminApiException("请求执行异常: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 执行返回列表的HTTP请求
+     */
+    @SuppressWarnings("unchecked")
+    private <T> AdminApiResponse<List<T>> executeForList(Method method, String path, Object requestBody, Class<T> itemClass) {
+        String fullUrl = config.getBaseUrl() + path;
+        
+        try {
+            HttpRequest request = HttpUtil.createRequest(method, fullUrl);
+            
+            // 设置请求头
+            request.header(ADMIN_KEY_HEADER, config.getAdminApiKey())
+                   .header("Content-Type", CONTENT_TYPE)
+                   .timeout(config.getTimeout());
+            
+            // 设置请求体
+            if (requestBody != null) {
+                String jsonBody = JSONUtil.toJsonStr(requestBody);
+                request.body(jsonBody);
+                log.debug("Admin API Request: {} {} - Body: {}", method, fullUrl, jsonBody);
+            } else {
+                log.debug("Admin API Request: {} {}", method, fullUrl);
+            }
+            
+            // 发送请求
+            HttpResponse response = request.execute();
+            String responseBody = response.body();
+            
+            log.debug("Admin API Response: {} - Status: {} - Body: {}", 
+                     fullUrl, response.getStatus(), responseBody);
+            
+            // 检查HTTP状态码
+            if (!response.isOk()) {
+                log.error("Admin API请求失败: {} - Status: {} - Response: {}", 
+                         fullUrl, response.getStatus(), responseBody);
+                throw new AdminApiException("HTTP请求失败: " + response.getStatus() + " - " + responseBody);
+            }
+            
+            // 解析响应
+            if (StrUtil.isBlank(responseBody)) {
+                throw new AdminApiException("响应体为空");
+            }
+            
+            // 先解析为泛型对象
+            AdminApiResponse<?> tempResponse = JSONUtil.toBean(responseBody, AdminApiResponse.class);
+            
+            // 检查业务状态码
+            if (tempResponse.getCode() != 0) {
+                String errorMsg = "API业务错误: code=" + tempResponse.getCode() + 
+                                 ", message=" + tempResponse.getMessage();
+                if (StrUtil.isNotBlank(tempResponse.getDetail())) {
+                    errorMsg += ", detail=" + tempResponse.getDetail();
+                }
+                log.error(errorMsg);
+                throw new AdminApiException(errorMsg);
+            }
+            
+            // 创建最终结果对象
+            AdminApiResponse<List<T>> apiResponse = new AdminApiResponse<>();
+            apiResponse.setCode(tempResponse.getCode());
+            apiResponse.setMessage(tempResponse.getMessage());
+            apiResponse.setDetail(tempResponse.getDetail());
+            
+            // 解析data字段为List
+            if (tempResponse.getData() != null) {
+                try {
+                    String dataJson = JSONUtil.toJsonStr(tempResponse.getData());
+                    JSONArray jsonArray = JSONUtil.parseArray(dataJson);
+                    List<T> dataList = jsonArray.toList(itemClass);
+                    apiResponse.setData(dataList);
+                } catch (Exception e) {
+                    log.warn("解析List数据失败,使用直接转换: {}", e.getMessage());
+                    // 如果解析失败,尝试直接转换(可能会有类型转换异常,但会被外层捕获)
+                    apiResponse.setData((List<T>) tempResponse.getData());
+                }
+            }
+            
+            return apiResponse;
+            
+        } catch (AdminApiException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("Admin API请求异常: {} {}", method, fullUrl, e);
+            throw new AdminApiException("请求执行异常: " + e.getMessage(), e);
+        }
+    }
+}

+ 56 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/config/AdminApiConfig.java

@@ -0,0 +1,56 @@
+package com.cyksj.service.claudecode.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "claudecode")
+public class AdminApiConfig {
+    
+    /**
+     * 管理API的基础URL
+     */
+    private String baseUrl = "https://relay01.yhlxj.com";
+    
+    /**
+     * 管理API密钥
+     */
+    private String adminApiKey;
+    
+    /**
+     * 请求超时时间(毫秒)
+     */
+    private int timeout = 30000;
+    
+    /**
+     * 连接超时时间(毫秒)
+     */
+    private int connectTimeout = 10000;
+    
+    /**
+     * 读取超时时间(毫秒)
+     */
+    private int readTimeout = 30000;
+    
+    /**
+     * 是否启用请求日志
+     */
+    private boolean logEnabled = true;
+    
+    /**
+     * 是否启用响应日志
+     */
+    private boolean responseLogEnabled = false;
+    
+    /**
+     * 重试次数
+     */
+    private int retryCount = 3;
+    
+    /**
+     * 重试间隔(毫秒)
+     */
+    private long retryInterval = 1000;
+}

+ 13 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/BatchUserStatusRequest.java

@@ -0,0 +1,13 @@
+package com.cyksj.service.claudecode.dto.request;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class BatchUserStatusRequest {
+    
+    @JsonProperty("user_ids")
+    private List<Long> userIds;
+}

+ 13 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/CreateApiKeyRequest.java

@@ -0,0 +1,13 @@
+package com.cyksj.service.claudecode.dto.request;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class CreateApiKeyRequest {
+    
+    private String name;
+    
+    @JsonProperty("expires_days")
+    private Integer expiresDays;
+}

+ 26 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/CreateUserPlanRequest.java

@@ -0,0 +1,26 @@
+package com.cyksj.service.claudecode.dto.request;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class CreateUserPlanRequest {
+    
+    @JsonProperty("plan_name")
+    private String planName;
+    
+    @JsonProperty("credit_limit")
+    private Long creditLimit;
+    
+    @JsonProperty("credit_recovery")
+    private Long creditRecovery;
+    
+    @JsonProperty("duration_days")
+    private Integer durationDays;
+    
+    @JsonProperty("expire_time")
+    private String expireTime;
+
+    @JsonProperty("nick_name")
+    private String nickName;
+}

+ 11 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/CreditAdjustRequest.java

@@ -0,0 +1,11 @@
+package com.cyksj.service.claudecode.dto.request;
+
+import lombok.Data;
+
+@Data
+public class CreditAdjustRequest {
+    
+    private Long amount;
+    private String description;
+    private String type;
+}

+ 11 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/InvalidateApiKeyCacheRequest.java

@@ -0,0 +1,11 @@
+package com.cyksj.service.claudecode.dto.request;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class InvalidateApiKeyCacheRequest {
+    
+    @JsonProperty("api_key")
+    private String apiKey;
+}

+ 13 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/request/UpdateApiKeyStatusRequest.java

@@ -0,0 +1,13 @@
+package com.cyksj.service.claudecode.dto.request;
+
+import lombok.Data;
+
+@Data
+public class UpdateApiKeyStatusRequest {
+    
+    private String status;
+    
+    private Boolean enabled;
+    
+    private String remark;
+}

+ 16 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/AdminApiResponse.java

@@ -0,0 +1,16 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import lombok.Data;
+
+@Data
+public class AdminApiResponse<T> {
+    
+    private Integer code;
+    private String message;
+    private T data;
+    private String detail;
+    
+    public boolean isSuccess() {
+        return code != null && code == 0;
+    }
+}

+ 13 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyCacheResponse.java

@@ -0,0 +1,13 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class ApiKeyCacheResponse {
+    
+    @JsonProperty("api_key_prefix")
+    private String apiKeyPrefix;
+    
+    private String message;
+}

+ 16 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyDeleteResponse.java

@@ -0,0 +1,16 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class ApiKeyDeleteResponse {
+    
+    @JsonProperty("key_id")
+    private Long keyId;
+    
+    private Boolean deleted;
+    
+    @JsonProperty("deleted_at")
+    private String deletedAt;
+}

+ 25 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyInfo.java

@@ -0,0 +1,25 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class ApiKeyInfo {
+    
+    private Long id;
+    private String name;
+    private String prefix;
+    private String status;
+    
+    @JsonProperty("last_used_at")
+    private String lastUsedAt;
+    
+    @JsonProperty("last_used_ip")
+    private String lastUsedIp;
+    
+    @JsonProperty("expires_at")
+    private String expiresAt;
+    
+    @JsonProperty("created_at")
+    private String createdAt;
+}

+ 27 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyResponse.java

@@ -0,0 +1,27 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class ApiKeyResponse {
+    
+    private Long id;
+    
+    @JsonProperty("user_id")
+    private Long userId;
+    
+    private String name;
+    
+    @JsonProperty("key_value")
+    private String keyValue;
+    
+    private String prefix;
+    private String status;
+    
+    @JsonProperty("expires_at")
+    private String expiresAt;
+    
+    @JsonProperty("created_at")
+    private String createdAt;
+}

+ 17 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/ApiKeyStatusResponse.java

@@ -0,0 +1,17 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class ApiKeyStatusResponse {
+    
+    @JsonProperty("key_id")
+    private Long keyId;
+    
+    @JsonProperty("new_status")
+    private String newStatus;
+    
+    @JsonProperty("updated_at")
+    private String updatedAt;
+}

+ 31 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/BatchUserStatusItem.java

@@ -0,0 +1,31 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class BatchUserStatusItem {
+    
+    @JsonProperty("user_id")
+    private Long userId;
+    
+    @JsonProperty("plan_name")
+    private String planName;
+    
+    private String status;
+    
+    @JsonProperty("credit_limit")
+    private Long creditLimit;
+    
+    @JsonProperty("used_credits")
+    private Long usedCredits;
+    
+    @JsonProperty("available_credits")
+    private Long availableCredits;
+    
+    @JsonProperty("expires_at")
+    private String expiresAt;
+    
+    @JsonProperty("is_expired")
+    private Boolean isExpired;
+}

+ 17 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CacheInvalidateResponse.java

@@ -0,0 +1,17 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CacheInvalidateResponse {
+    
+    @JsonProperty("user_id")
+    private Long userId;
+    
+    private List<String> cleared;
+    
+    private String message;
+}

+ 21 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CacheStatsResponse.java

@@ -0,0 +1,21 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CacheStatsResponse {
+    
+    @JsonProperty("cache_type")
+    private String cacheType;
+    
+    @JsonProperty("l1_cache")
+    private String l1Cache;
+    
+    @JsonProperty("l2_cache")
+    private String l2Cache;
+    
+    private List<String> features;
+}

+ 20 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CreditAdjustResponse.java

@@ -0,0 +1,20 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class CreditAdjustResponse {
+    
+    @JsonProperty("user_id")
+    private Long userId;
+    
+    @JsonProperty("adjustment_amount")
+    private Long adjustmentAmount;
+    
+    @JsonProperty("new_balance")
+    private Long newBalance;
+    
+    private String type;
+    private String description;
+}

+ 26 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CreditHistoryItem.java

@@ -0,0 +1,26 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class CreditHistoryItem {
+    
+    private Long id;
+    private String type;
+    private Long amount;
+    
+    @JsonProperty("balance_after")
+    private Long balanceAfter;
+    
+    private String description;
+    
+    @JsonProperty("reference_type")
+    private String referenceType;
+    
+    @JsonProperty("reference_id")
+    private Long referenceId;
+    
+    @JsonProperty("created_at")
+    private String createdAt;
+}

+ 65 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/CreditsAnalyticsResponse.java

@@ -0,0 +1,65 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CreditsAnalyticsResponse {
+    
+    private AnalyticsMeta meta;
+    private AnalyticsSummary summary;
+    private List<AnalyticsSeriesItem> series;
+    private AnalyticsHistory history;
+    
+    @Data
+    public static class AnalyticsMeta {
+        private String start;
+        private String end;
+        private String interval;
+        private String timezone;
+        @JsonProperty("bucket_count")
+        private Integer bucketCount;
+    }
+    
+    @Data
+    public static class AnalyticsSummary {
+        private Long consumed;
+        private Long recharged;
+        @JsonProperty("net_change")
+        private Long netChange;
+    }
+    
+    @Data
+    public static class AnalyticsSeriesItem {
+        private String ts;
+        private Long consumed;
+        private Long recharged;
+        @JsonProperty("net_change")
+        private Long netChange;
+    }
+    
+    @Data
+    public static class AnalyticsHistory {
+        private Integer page;
+        private Integer limit;
+        private Long total;
+        private List<AnalyticsHistoryRecord> records;
+    }
+    
+    @Data
+    public static class AnalyticsHistoryRecord {
+        private Long id;
+        private String time;
+        private String type;
+        private Long amount;
+        @JsonProperty("balance_after")
+        private Long balanceAfter;
+        private String description;
+        @JsonProperty("reference_type")
+        private String referenceType;
+        @JsonProperty("reference_id")
+        private Long referenceId;
+    }
+}

+ 66 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/DashboardResponse.java

@@ -0,0 +1,66 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DashboardResponse {
+    
+    private UserSubscription subscription;
+    
+    @JsonProperty("today_summary")
+    private TodaySummary todaySummary;
+    
+    @JsonProperty("today_requests")
+    private List<TodayRequest> todayRequests;
+    
+    @Data
+    public static class UserSubscription {
+        @JsonProperty("user_id")
+        private Long userId;
+        @JsonProperty("plan_name")
+        private String planName;
+        @JsonProperty("credit_limit")
+        private Long creditLimit;
+        @JsonProperty("used_credits")
+        private Long usedCredits;
+        @JsonProperty("available_credits")
+        private Long availableCredits;
+        @JsonProperty("credit_recovery_per_hour")
+        private Long creditRecoveryPerHour;
+        private String status;
+        @JsonProperty("start_date")
+        private String startDate;
+        @JsonProperty("end_date")
+        private String endDate;
+        @JsonProperty("recovery_enabled")
+        private Boolean recoveryEnabled;
+        @JsonProperty("last_recharge_at")
+        private String lastRechargeAt;
+    }
+    
+    @Data
+    public static class TodaySummary {
+        private Long requests;
+        @JsonProperty("credits_consumed")
+        private Long creditsConsumed;
+        @JsonProperty("credits_recharged")
+        private Long creditsRecharged;
+        private String since;
+        private String until;
+    }
+    
+    @Data
+    public static class TodayRequest {
+        private Long id;
+        private String time;
+        private String model;
+        @JsonProperty("credits_cost")
+        private Long creditsCost;
+        private String status;
+        @JsonProperty("api_key_id")
+        private Long apiKeyId;
+    }
+}

+ 20 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/PageResponse.java

@@ -0,0 +1,20 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class PageResponse<T> {
+    
+    private List<T> items;
+    private Integer page;
+    
+    @JsonProperty("page_size")
+    private Integer pageSize;
+    private Long total;
+    
+    @JsonProperty("total_pages") 
+    private Integer totalPages;
+}

+ 20 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/RecoveryStatusResponse.java

@@ -0,0 +1,20 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RecoveryStatusResponse {
+    
+    @JsonProperty("pending_recovery_users")
+    private Integer pendingRecoveryUsers;
+    
+    @JsonProperty("last_recovery_at")
+    private String lastRecoveryAt;
+    
+    @JsonProperty("next_recovery_at")
+    private String nextRecoveryAt;
+    
+    @JsonProperty("system_status")
+    private String systemStatus;
+}

+ 16 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/RecoveryTriggerResponse.java

@@ -0,0 +1,16 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RecoveryTriggerResponse {
+    
+    private String message;
+    
+    @JsonProperty("recovered_users")
+    private Integer recoveredUsers;
+    
+    @JsonProperty("trigger_by")
+    private String triggerBy;
+}

+ 52 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/UsageStatsResponse.java

@@ -0,0 +1,52 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class UsageStatsResponse {
+    
+    @JsonProperty("period_days")
+    private Integer periodDays;
+    
+    @JsonProperty("total_requests")
+    private Long totalRequests;
+    
+    @JsonProperty("successful_requests")
+    private Long successfulRequests;
+    
+    @JsonProperty("failed_requests")
+    private Long failedRequests;
+    
+    @JsonProperty("total_input_tokens")
+    private Long totalInputTokens;
+    
+    @JsonProperty("total_output_tokens")
+    private Long totalOutputTokens;
+    
+    @JsonProperty("total_credits_consumed")
+    private Long totalCreditsConsumed;
+    
+    @JsonProperty("average_request_time")
+    private Long averageRequestTime;
+    
+    @JsonProperty("most_used_model")
+    private String mostUsedModel;
+    
+    @JsonProperty("daily_breakdown")
+    private List<DailyUsageBreakdown> dailyBreakdown;
+    
+    @Data
+    public static class DailyUsageBreakdown {
+        private String date;
+        private Long requests;
+        @JsonProperty("credits_consumed")
+        private Long creditsConsumed;
+        @JsonProperty("input_tokens")
+        private Long inputTokens;
+        @JsonProperty("output_tokens")
+        private Long outputTokens;
+    }
+}

+ 37 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/UserBalanceResponse.java

@@ -0,0 +1,37 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class UserBalanceResponse {
+    
+    @JsonProperty("user_id")
+    private Long userId;
+    
+    @JsonProperty("plan_name")
+    private String planName;
+    
+    @JsonProperty("credit_limit")
+    private Long creditLimit;
+    
+    @JsonProperty("used_credits")
+    private Long usedCredits;
+    
+    @JsonProperty("available_credits")
+    private Long availableCredits;
+    
+    @JsonProperty("credit_recovery_per_hour")
+    private Long creditRecoveryPerHour;
+    
+    private String status;
+    
+    @JsonProperty("start_date")
+    private String startDate;
+    
+    @JsonProperty("end_date")
+    private String endDate;
+    
+    @JsonProperty("last_recharge_at")
+    private String lastRechargeAt;
+}

+ 45 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/dto/response/UserPlanResponse.java

@@ -0,0 +1,45 @@
+package com.cyksj.service.claudecode.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class UserPlanResponse {
+    
+    @JsonProperty("user_id")
+    private Long userId;
+    
+    @JsonProperty("plan_name")
+    private String planName;
+    
+    @JsonProperty("credit_limit")
+    private Long creditLimit;
+    
+    @JsonProperty("used_credits")
+    private Long usedCredits;
+    
+    @JsonProperty("available_credits")
+    private Long availableCredits;
+    
+    @JsonProperty("credit_recovery")
+    private Long creditRecovery;
+    
+    @JsonProperty("start_date")
+    private String startDate;
+    
+    @JsonProperty("end_date")
+    private String endDate;
+    
+    private String status;
+    
+    private String operation;
+    
+    @JsonProperty("recovery_enabled")
+    private Boolean recoveryEnabled;
+    
+    @JsonProperty("last_recharge_at")
+    private String lastRechargeAt;
+}

+ 48 - 0
netflix-service/src/main/java/com/cyksj/service/claudecode/exception/AdminApiException.java

@@ -0,0 +1,48 @@
+package com.cyksj.service.claudecode.exception;
+
+public class AdminApiException extends RuntimeException {
+    
+    private Integer code;
+    private String detail;
+    
+    public AdminApiException(String message) {
+        super(message);
+    }
+    
+    public AdminApiException(String message, Throwable cause) {
+        super(message, cause);
+    }
+    
+    public AdminApiException(Integer code, String message) {
+        super(message);
+        this.code = code;
+    }
+    
+    public AdminApiException(Integer code, String message, String detail) {
+        super(message);
+        this.code = code;
+        this.detail = detail;
+    }
+    
+    public AdminApiException(Integer code, String message, String detail, Throwable cause) {
+        super(message, cause);
+        this.code = code;
+        this.detail = detail;
+    }
+    
+    public Integer getCode() {
+        return code;
+    }
+    
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+    
+    public String getDetail() {
+        return detail;
+    }
+    
+    public void setDetail(String detail) {
+        this.detail = detail;
+    }
+}

+ 30 - 0
netflix-web/src/main/java/com/cyksj/web/config/OAuthFilterConfig.java

@@ -0,0 +1,30 @@
+package com.cyksj.web.config;
+
+import com.cyksj.web.filter.OAuthAuthFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+
+/**
+ * OAuth过滤器配置
+ * @author chan
+ * @date 2025/8/10
+ */
+@Configuration
+@RequiredArgsConstructor
+public class OAuthFilterConfig {
+
+    private final OAuthAuthFilter oauthAuthFilter;
+
+    @Bean
+    public FilterRegistrationBean<OAuthAuthFilter> oauthAuthFilterRegistration() {
+        FilterRegistrationBean<OAuthAuthFilter> registration = new FilterRegistrationBean<>();
+        registration.setFilter(oauthAuthFilter);
+        registration.addUrlPatterns("/api/*");
+        registration.setName("oauthAuthFilter");
+        registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
+        return registration;
+    }
+}

+ 525 - 0
netflix-web/src/main/java/com/cyksj/web/controller/claudecode/ClaudeCodeController.java

@@ -0,0 +1,525 @@
+package com.cyksj.web.controller.claudecode;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.claudecode.ClaudeCodeUserMapper;
+import com.cyksj.model.dto.ClaudeCodeWhoami;
+import com.cyksj.model.entity.ClaudeCodeUser;
+import com.cyksj.model.entity.DeviceCode;
+import com.cyksj.model.entity.OAuthToken;
+import com.cyksj.model.entity.Announcement;
+import com.cyksj.model.entity.RelayServer;
+import com.cyksj.service.claudecode.AdminApiService;
+import com.cyksj.service.claudecode.OAuthService;
+import com.cyksj.service.claudecode.AnnouncementService;
+import com.cyksj.service.claudecode.RelayService;
+import com.cyksj.service.claudecode.dto.request.UpdateApiKeyStatusRequest;
+import com.cyksj.service.claudecode.dto.response.*;
+import com.cyksj.web.util.StpUserUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.view.RedirectView;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * claudecode相关服务
+ * @author chan
+ * @date 2025/8/10 17:20
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/applets/claudecode")
+public class ClaudeCodeController {
+
+    private final ClaudeCodeUserMapper claudeCodeUserMapper;
+    private final AdminApiService adminApiService;
+    private final OAuthService oauthService;
+    private final AnnouncementService announcementService;
+    private final RelayService relayService;
+
+    /**
+     * whoami接口 - 获取当前登录用户信息和套餐信息
+     */
+    @GetMapping("/whoami")
+    public Result<ClaudeCodeWhoami> whoami() {
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询claudecodeuser表
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        ClaudeCodeWhoami result = new ClaudeCodeWhoami()
+                .setIsLogin(true)
+                .setUserId(userId);
+        
+        if (claudeCodeUser != null) {
+            result.setClaudeCodeUserId(claudeCodeUser.getId())
+                    .setName(claudeCodeUser.getName())
+                    .setImg(claudeCodeUser.getImg())
+                    .setPlan(claudeCodeUser.getPlan())
+                    .setCreditLimit(claudeCodeUser.getCreditLimit())
+                    .setCreditRecovery(claudeCodeUser.getCreditRecovery())
+                    .setExpireTime(claudeCodeUser.getExpireTime())
+                    .setRemark(claudeCodeUser.getRemark());
+        } else {
+            result.setClaudeCodeUserId(null)
+                    .setPlan(null)
+                    .setMessage("用户暂未开通Claude Code服务");
+        }
+        
+        return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+    }
+
+    /**
+     * 获取控制台数据
+     */
+    @GetMapping("/dashboard")
+    public Result<DashboardResponse> getDashboard() {
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询Claude Code用户ID
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        if (claudeCodeUser == null) {
+            throw BusinessRuntimeException.getInstance("用户暂未开通Claude Code服务");
+        }
+        
+        try {
+            DashboardResponse dashboard = adminApiService.getUserDashboard(claudeCodeUser.getId());
+            return GatewayResponse.SUCCESS.newBuilder().toResult(dashboard);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("获取控制台数据失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取积分历史和分析数据
+     */
+    @GetMapping("/credits/analytics")
+    public Result<CreditsAnalyticsResponse> getCreditsAnalytics(
+            @RequestParam String start,
+            @RequestParam String end,
+            @RequestParam(required = false) String tz,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "20") Integer limit,
+            @RequestParam(required = false) String order,
+            @RequestParam(required = false) String type) {
+        
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询Claude Code用户ID
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        if (claudeCodeUser == null) {
+            throw BusinessRuntimeException.getInstance("用户暂未开通Claude Code服务");
+        }
+        
+        try {
+            CreditsAnalyticsResponse analytics = adminApiService.getCreditsAnalytics(
+                    claudeCodeUser.getId(), start, end, tz, page, limit, order, type);
+            return GatewayResponse.SUCCESS.newBuilder().toResult(analytics);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("获取积分历史失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 查询用户余额
+     */
+    @GetMapping("/balance")
+    public Result<UserBalanceResponse> getUserBalance() {
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询Claude Code用户ID
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        if (claudeCodeUser == null) {
+            throw BusinessRuntimeException.getInstance("用户暂未开通Claude Code服务");
+        }
+        
+        try {
+            UserBalanceResponse balance = adminApiService.getUserBalance(claudeCodeUser.getId());
+            return GatewayResponse.SUCCESS.newBuilder().toResult(balance);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("查询用户余额失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 查询用户API Keys
+     */
+    @GetMapping("/apikeys")
+    public Result<List<ApiKeyInfo>> getUserApiKeys() {
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询Claude Code用户ID
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        if (claudeCodeUser == null) {
+            throw BusinessRuntimeException.getInstance("用户暂未开通Claude Code服务");
+        }
+        
+        try {
+            List<ApiKeyInfo> apiKeys = adminApiService.getUserApiKeys(claudeCodeUser.getId());
+            return GatewayResponse.SUCCESS.newBuilder().toResult(apiKeys);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("查询API Keys失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 更新API Key状态
+     */
+    @PutMapping("/apikeys/{keyId}/status")
+    public Result<ApiKeyStatusResponse> updateApiKeyStatus(
+            @PathVariable Long keyId,
+            @RequestBody Map<String, Object> request) {
+        
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询Claude Code用户ID
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        if (claudeCodeUser == null) {
+            throw BusinessRuntimeException.getInstance("用户暂未开通Claude Code服务");
+        }
+        
+        try {
+            // 构建请求对象
+            UpdateApiKeyStatusRequest updateRequest = new UpdateApiKeyStatusRequest();
+            if (request.containsKey("enabled")) {
+                updateRequest.setEnabled((Boolean) request.get("enabled"));
+            }
+            if (request.containsKey("remark")) {
+                updateRequest.setRemark((String) request.get("remark"));
+            }
+            
+            ApiKeyStatusResponse response = adminApiService.updateApiKeyStatus(
+                    claudeCodeUser.getId(), keyId, updateRequest);
+            return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("更新API Key状态失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除API Key
+     */
+    @DeleteMapping("/apikeys/{keyId}")
+    public Result<ApiKeyDeleteResponse> deleteApiKey(@PathVariable Long keyId) {
+        if (!StpUserUtil.isLogin()) {
+            throw BusinessRuntimeException.getInstance("用户未登录");
+        }
+        
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        
+        // 查询Claude Code用户ID
+        QueryWrapper<ClaudeCodeUser> wrapper = new QueryWrapper<>();
+        wrapper.eq("userId", userId);
+        ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(wrapper);
+        
+        if (claudeCodeUser == null) {
+            throw BusinessRuntimeException.getInstance("用户暂未开通Claude Code服务");
+        }
+        
+        try {
+            ApiKeyDeleteResponse response = adminApiService.deleteApiKey(claudeCodeUser.getId(), keyId);
+            return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("删除API Key失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== OAuth 认证接口组 (/api/sso) ====================
+
+    /**
+     * OAuth授权端点
+     * GET /api/sso/authorize
+     */
+    @GetMapping("/api/sso/authorize")
+    public void authorize(
+            @RequestParam("client_id") String clientId,
+            @RequestParam("redirect_uri") String redirectUri,
+            @RequestParam("state") String state,
+            @RequestParam(value = "device_flow", defaultValue = "false") boolean deviceFlow,
+            HttpServletRequest request,
+            HttpServletResponse response) throws IOException {
+        
+        // 验证必填参数
+        if (!StringUtils.hasText(clientId) || !StringUtils.hasText(redirectUri) || !StringUtils.hasText(state)) {
+            response.setStatus(400);
+            response.getWriter().write("{\"error\": \"missing required parameters\"}");
+            return;
+        }
+        
+        if (deviceFlow) {
+            // 设备流模式:显示验证码页面
+            showDeviceCodePage(clientId, redirectUri, state, response);
+        } else {
+            // 自动流模式:重定向到登录页面或直接处理
+            handleAutoFlow(clientId, redirectUri, state, request, response);
+        }
+    }
+
+    /**
+     * Token验证端点
+     * POST /api/sso/verify-token
+     */
+    @PostMapping("/api/sso/verify-token")
+    public Result<Map<String, Object>> verifyToken(@RequestBody Map<String, String> request) {
+        String token = request.get("token");
+        String clientId = request.get("client_id");
+        String clientSecret = request.get("client_secret");
+        
+        if (!StringUtils.hasText(token) || !StringUtils.hasText(clientId) || !StringUtils.hasText(clientSecret)) {
+            throw BusinessRuntimeException.getInstance("缺少必要参数");
+        }
+        
+        try {
+            // 验证客户端凭据
+            oauthService.validateClient(clientId, clientSecret);
+            
+            // 验证token
+            OAuthToken oauthToken = oauthService.validateToken(token);
+            
+            // 获取用户信息
+            ClaudeCodeUser user = claudeCodeUserMapper.selectById(oauthToken.getUserId());
+            if (user == null) {
+                throw BusinessRuntimeException.getInstance("用户不存在");
+            }
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("authenticated", true);
+            result.put("userId", user.getUserId().toString());
+            result.put("email", user.getName()); // 使用name作为email
+            
+            return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("Token验证失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 设备验证码验证端点
+     * POST /api/sso/verify-code
+     */
+    @PostMapping("/api/sso/verify-code")
+    public Result<Map<String, Object>> verifyCode(@RequestBody Map<String, String> request) {
+        String code = request.get("code");
+        String clientId = request.get("client_id");
+        String clientSecret = request.get("client_secret");
+        
+        if (!StringUtils.hasText(code) || !StringUtils.hasText(clientId) || !StringUtils.hasText(clientSecret)) {
+            throw BusinessRuntimeException.getInstance("缺少必要参数");
+        }
+        
+        try {
+            // 验证客户端凭据
+            oauthService.validateClient(clientId, clientSecret);
+            
+            // 验证设备验证码
+            DeviceCode device = oauthService.validateDeviceCode(code);
+            
+            // 生成访问令牌
+            String token = oauthService.generateAccessToken(device.getUserId(), device.getClientId());
+            
+            // 获取用户信息
+            ClaudeCodeUser user = claudeCodeUserMapper.selectById(device.getUserId());
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("token", token);
+            result.put("userId", user.getUserId().toString());
+            result.put("email", user.getName());
+            
+            return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("验证码验证失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 公告系统接口组 (/api) ====================
+
+    /**
+     * 获取公告
+     * GET /api/announcements
+     */
+    @GetMapping("/api/announcements")
+    public Result<Map<String, Object>> getAnnouncements(
+            @RequestHeader(value = "Accept-Language", defaultValue = "en") String acceptLanguage,
+            HttpServletRequest request) {
+        
+        // 从Accept-Language获取语言偏好,默认英语
+        String language = parseLanguage(acceptLanguage);
+        
+        try {
+            List<Announcement> announcements = announcementService.getActiveAnnouncements(language);
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("announcements", announcements);
+            
+            return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("获取公告失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 中继选择接口组 (/api) ====================
+
+    /**
+     * 中继列表端点
+     * GET /api/relays
+     */
+    @GetMapping("/api/relays")
+    public Result<Map<String, Object>> getRelays(@RequestParam(value = "domain", required = false) String domain) {
+        try {
+            List<RelayServer> relays = relayService.getActiveRelays(domain);
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("relays", relays);
+            
+            return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("获取中继列表失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 私有辅助方法 ====================
+
+    private void showDeviceCodePage(String clientId, String redirectUri, String state, HttpServletResponse response) 
+            throws IOException {
+        try {
+            // 生成设备验证码
+            DeviceCode device = oauthService.generateDeviceCode(clientId);
+            
+            // 渲染设备验证码页面
+            String html = renderDeviceCodeHTML(device.getUserCode(), redirectUri, state);
+            response.setContentType("text/html;charset=UTF-8");
+            response.getWriter().write(html);
+        } catch (Exception e) {
+            response.setStatus(500);
+            response.getWriter().write("{\"error\": \"failed to generate device code\"}");
+        }
+    }
+
+    private void handleAutoFlow(String clientId, String redirectUri, String state,
+                               HttpServletRequest request, HttpServletResponse response) throws IOException {
+        try {
+            // 检查用户是否已登录
+            if (!StpUserUtil.isLogin()) {
+                // 重定向到登录页面
+                String loginUrl = "/login?client_id=" + clientId + 
+                    "&redirect_uri=" + java.net.URLEncoder.encode(redirectUri, "UTF-8") + 
+                    "&state=" + state;
+                response.sendRedirect(loginUrl);
+                return;
+            }
+            
+            // 用户已登录,生成token并重定向
+            Long userId = StpUserUtil.getLoginIdAsLong();
+            String token = oauthService.generateAccessToken(userId, clientId);
+            
+            // 重定向回客户端
+            String callbackUrl = redirectUri + "?token=" + token + "&state=" + state;
+            response.sendRedirect(callbackUrl);
+        } catch (Exception e) {
+            response.setStatus(500);
+            response.getWriter().write("{\"error\": \"failed to handle authorization\"}");
+        }
+    }
+
+    private String renderDeviceCodeHTML(String userCode, String redirectUri, String state) {
+        return "<!DOCTYPE html>\n" +
+                "<html>\n" +
+                "<head>\n" +
+                "    <title>Device Authorization</title>\n" +
+                "    <style>\n" +
+                "        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }\n" +
+                "        .code { font-size: 24px; font-weight: bold; color: #007cba; text-align: center; margin: 20px 0; }\n" +
+                "        .instructions { background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }\n" +
+                "        button { background: #007cba; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; }\n" +
+                "        button:hover { background: #005a8b; }\n" +
+                "    </style>\n" +
+                "</head>\n" +
+                "<body>\n" +
+                "    <h1>Device Authorization Required</h1>\n" +
+                "    <div class=\"instructions\">\n" +
+                "        <p>To complete the authorization process, please:</p>\n" +
+                "        <ol>\n" +
+                "            <li>Log in to your account if you haven't already</li>\n" +
+                "            <li>Enter the following verification code:</li>\n" +
+                "        </ol>\n" +
+                "    </div>\n" +
+                "    <div class=\"code\">" + userCode + "</div>\n" +
+                "    <div style=\"text-align: center; margin: 30px 0;\">\n" +
+                "        <button onclick=\"confirmCode()\">I have entered the code</button>\n" +
+                "    </div>\n" +
+                "    <script>\n" +
+                "        function confirmCode() {\n" +
+                "            alert('Please paste this code in your terminal and press Enter: " + userCode + "');\n" +
+                "        }\n" +
+                "    </script>\n" +
+                "</body>\n" +
+                "</html>";
+    }
+
+    private String parseLanguage(String acceptLanguage) {
+        if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
+            return "en";
+        }
+        
+        // 简单解析Accept-Language头
+        String[] languages = acceptLanguage.split(",");
+        for (String lang : languages) {
+            String langCode = lang.trim().split(";")[0].toLowerCase();
+            if (langCode.startsWith("zh")) {
+                return "zh-CN";
+            }
+        }
+        return "en";
+    }
+}

+ 103 - 0
netflix-web/src/main/java/com/cyksj/web/filter/OAuthAuthFilter.java

@@ -0,0 +1,103 @@
+package com.cyksj.web.filter;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.mapper.claudecode.OAuthTokenMapper;
+import com.cyksj.model.entity.OAuthToken;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * OAuth认证过滤器
+ * 支持OAuth Token认证
+ * @author chan
+ * @date 2025/8/10
+ */
+@Component
+@RequiredArgsConstructor
+public class OAuthAuthFilter implements Filter {
+
+    private final OAuthTokenMapper oauthTokenMapper;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        
+        String requestURI = httpRequest.getRequestURI();
+        
+        // 只对 /api/ 开头的路径进行OAuth认证(排除/api/sso)
+        if (requestURI.startsWith("/api/") && !requestURI.startsWith("/api/sso/")) {
+            if (!authenticateOAuth(httpRequest, httpResponse)) {
+                return; // 认证失败,直接返回
+            }
+        }
+        
+        chain.doFilter(request, response);
+    }
+    
+    private boolean authenticateOAuth(HttpServletRequest request, HttpServletResponse response) 
+            throws IOException {
+        
+        // 从Authorization header获取token
+        String authHeader = request.getHeader("Authorization");
+        if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
+            sendUnauthorizedResponse(response, "缺少Authorization头或格式错误");
+            return false;
+        }
+        
+        String tokenValue = authHeader.substring(7); // 移除"Bearer "前缀
+        if (!StringUtils.hasText(tokenValue)) {
+            sendUnauthorizedResponse(response, "Token为空");
+            return false;
+        }
+        
+        try {
+            // 验证token
+            QueryWrapper<OAuthToken> wrapper = new QueryWrapper<>();
+            wrapper.eq("token_value", tokenValue)
+                    .eq("status", "active")
+                    .gt("expires_at", new Date());
+            
+            OAuthToken token = oauthTokenMapper.selectOne(wrapper);
+            if (token == null) {
+                sendUnauthorizedResponse(response, "无效或已过期的令牌");
+                return false;
+            }
+            
+            // 将用户信息添加到request属性中
+            request.setAttribute("oauth_user_id", token.getUserId());
+            request.setAttribute("oauth_client_id", token.getClientId());
+            request.setAttribute("auth_type", "oauth");
+            
+            return true;
+            
+        } catch (Exception e) {
+            sendUnauthorizedResponse(response, "Token验证失败: " + e.getMessage());
+            return false;
+        }
+    }
+    
+    private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType("application/json;charset=UTF-8");
+        
+        Map<String, Object> errorResponse = new HashMap<>();
+        errorResponse.put("error", "Unauthorized");
+        errorResponse.put("message", message);
+        
+        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
+    }
+}

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
netflix-web/src/main/resources/application-dev.yml


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff