Explorar el Código

新增claudecode 授权登录

chenbiao hace 1 año
padre
commit
fa7421c514
Se han modificado 18 ficheros con 710 adiciones y 3 borrados
  1. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/AnnouncementMapper.java
  2. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthClientMapper.java
  3. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/OAuthTokenMapper.java
  4. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/claudecode/RelayServerMapper.java
  5. 33 0
      netflix-dao/src/main/java/com/cyksj/model/entity/Announcement.java
  6. 27 0
      netflix-dao/src/main/java/com/cyksj/model/entity/OAuthClient.java
  7. 27 0
      netflix-dao/src/main/java/com/cyksj/model/entity/OAuthToken.java
  8. 24 0
      netflix-dao/src/main/java/com/cyksj/model/entity/RelayServer.java
  9. 35 0
      netflix-service/src/main/java/com/cyksj/service/claude/AnnouncementService.java
  10. 1 1
      netflix-service/src/main/java/com/cyksj/service/claude/ClaudeCodeService.java
  11. 17 0
      netflix-service/src/main/java/com/cyksj/service/claude/OAuthClaudeCodeService.java
  12. 28 0
      netflix-service/src/main/java/com/cyksj/service/claude/RelayService.java
  13. 73 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/AnnouncementServiceImpl.java
  14. 3 1
      netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java
  15. 131 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/OAuthClaudeCodeServiceImpl.java
  16. 57 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/RelayServiceImpl.java
  17. 194 0
      netflix-web/src/main/java/com/cyksj/web/controller/claude/ClaudeCodeController.java
  18. 12 1
      netflix-web/src/main/resources/application-pre.yml

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -28,7 +28,7 @@ public interface ClaudeCodeService {
 	/**
 	 * 用户创建api keys
 	 */
-	void createApiKeys(ClaudeCodeApiKeysReq req) throws Exception;
+	ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception;
 
 	/**
 	 * 用户更新api key状态

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

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

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

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

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

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

+ 3 - 1
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java

@@ -141,7 +141,7 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 	}
 
 	@Override
-	public void createApiKeys(ClaudeCodeApiKeysReq req) throws Exception {
+	public ClaudeCodeResp createApiKeys(ClaudeCodeApiKeysReq req) throws Exception {
 		Long userId = req.getUserId();
 		if (checkClaudeCodeUser(userId)) {
 			Long claudeCodeUserId = getClaudeCodeUserId(userId);
@@ -158,7 +158,9 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 			if (!claudeCodeResp.getSuccess()) {
 				throw BusinessRuntimeException.getInstance("创建失败");
 			}
+			return claudeCodeResp;
 		}
+		throw new BusinessRuntimeException("您还没有claudecode账号");
 	}
 
 	@Override

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

@@ -0,0 +1,131 @@
+package com.cyksj.service.claude.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.date.DateField;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.json.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.cyksj.mapper.claudecode.OAuthClientMapper;
+import com.cyksj.mapper.claudecode.OAuthTokenMapper;
+import com.cyksj.model.entity.OAuthClient;
+import com.cyksj.model.entity.OAuthToken;
+import com.cyksj.model.request.ClaudeCodeApiKeysReq;
+import com.cyksj.model.response.claudecode.ClaudeCodeResp;
+import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
+import com.cyksj.redis.RedisService;
+import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.claude.OAuthClaudeCodeService;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * OAuth服务
+ */
+@Service
+@RequiredArgsConstructor
+public class OAuthClaudeCodeServiceImpl implements OAuthClaudeCodeService {
+
+    private final OAuthClientMapper oauthClientMapper;
+    private final OAuthTokenMapper oauthTokenMapper;
+
+    private final RedisService redisService;
+
+    private final ClaudeCodeService claudeCodeService;
+
+    /**
+     * 验证客户端凭据
+     */
+    @Override
+    public OAuthClient validateClient(String clientId, String clientSecret) {
+        QueryWrapper<OAuthClient> wrapper = new QueryWrapper<>();
+        wrapper.eq("id", clientId)
+                .eq("client_secret", clientSecret)
+                .eq("status", 1);
+        return oauthClientMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 生成访问令牌
+     */
+    @Override
+    public String generateAccessToken(Long userId, String clientId) throws Exception {
+
+        List<ClaudeCodeUserApiKeysView> userApiKeys = claudeCodeService.getUserApiKeys(userId);
+        if (CollectionUtil.isEmpty(userApiKeys)) {
+            ClaudeCodeApiKeysReq claudeCodeApiKeysReq = new ClaudeCodeApiKeysReq();
+            claudeCodeApiKeysReq.setName("系统创建");
+            claudeCodeApiKeysReq.setUserId(userId);
+            claudeCodeApiKeysReq.setExpiresDays(30);
+            ClaudeCodeResp apiKeys = claudeCodeService.createApiKeys(claudeCodeApiKeysReq);
+            JSONObject jsonObject = new JSONObject(apiKeys.getData());
+            String token = jsonObject.getStr("key_value");
+            // 保存token
+            OAuthToken oAuthToken = new OAuthToken();
+            oAuthToken.setTokenValue(token);
+            oAuthToken.setUserId(userId);
+            oAuthToken.setClientId(clientId);
+            oAuthToken.setStatus(1);
+            oAuthToken.setCreatedTime(new Date());
+
+            oauthTokenMapper.insert(oAuthToken);
+            return token;
+
+        }
+
+        return null;
+    }
+
+    /**
+     * 验证访问令牌
+     */
+    @Override
+    public OAuthToken validateToken(String code, String key) {
+        if (StringUtils.isNotBlank(key)) {
+
+        } else {
+            QueryWrapper<OAuthToken> wrapper = new QueryWrapper<>();
+            wrapper.eq("token", code)
+                    .eq("status", 1)
+                    .gt("expires_at", LocalDateTime.now());
+            return oauthTokenMapper.selectOne(wrapper);
+        }
+        QueryWrapper<OAuthToken> wrapper = new QueryWrapper<>();
+        wrapper.eq("keyValue", key)
+                .eq("status", 1)
+                .gt("expires_at", LocalDateTime.now());
+        return oauthTokenMapper.selectOne(wrapper);
+
+    }
+
+    /**
+     * 撤销令牌
+     */
+    @Override
+    public void revokeToken(String tokenValue) {
+        QueryWrapper<OAuthToken> wrapper = new QueryWrapper<>();
+        wrapper.eq("token_value", tokenValue);
+
+        OAuthToken token = new OAuthToken();
+        token.setStatus(0);
+
+        oauthTokenMapper.update(token, wrapper);
+    }
+
+    /**
+     * 清理过期令牌
+     */
+    @Override
+    public void cleanExpiredTokens() {
+        QueryWrapper<OAuthToken> wrapper = new QueryWrapper<>();
+        wrapper.lt("expires_at", LocalDateTime.now());
+        oauthTokenMapper.delete(wrapper);
+    }
+}

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

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

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

@@ -1,8 +1,12 @@
 package com.cyksj.web.controller.claude;
 
 import com.cyksj.common.constant.Constant;
+import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.Announcement;
+import com.cyksj.model.entity.OAuthClient;
+import com.cyksj.model.entity.RelayServer;
 import com.cyksj.model.manage.views.OrderDonView;
 import com.cyksj.model.request.ClaudeCodeApiKeysReq;
 import com.cyksj.model.request.ClaudeCodeDelReq;
@@ -11,18 +15,30 @@ import com.cyksj.model.response.claudecode.ClaudeCodeResp;
 import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
 import com.cyksj.model.views.ClaudeCodeUserInfoView;
 import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.claude.OAuthClaudeCodeService;
+import com.cyksj.service.claude.RelayService;
 import com.cyksj.service.user.UserBindRelationService;
+import com.cyksj.service.claude.AnnouncementService;
+import com.cyksj.model.entity.OAuthToken;
 import com.cyksj.web.util.StpUserUtil;
 import com.ejlchina.searcher.BeanSearcher;
 import com.ejlchina.searcher.SearchResult;
 import com.ejlchina.searcher.param.Operator;
 import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.ArrayList;
 
 /**
  * server/claude code
@@ -31,10 +47,14 @@ import java.util.List;
  * 创建者: JavaZou
  * 创建时间:2025/8/8 11:37
  */
+@Slf4j
 @RestController
 @RequestMapping("/applet/claude/code")
 @RequiredArgsConstructor
 public class ClaudeCodeController {
+
+	private final OAuthClaudeCodeService oAuthClaudeCodeService;
+
 	private final ClaudeCodeService claudeCodeService;
 
 	private final BeanSearcher beanSearcher;
@@ -43,6 +63,10 @@ public class ClaudeCodeController {
 
 	private final UserBindRelationService userBindRelationService;
 
+	private final AnnouncementService announcementService;
+
+	private final RelayService relayService;
+
 	/**
 	 * 用户创建API KEY
 	 */
@@ -154,4 +178,174 @@ public class ClaudeCodeController {
 		ClaudeCodeResp resp = claudeCodeService.getCreditsAnalytics(userId, start, end, tz, page, limit, order, type);
 		return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
 	}
+
+	//================================================Claude Code OAuth接口============================================================
+
+
+	/**
+	 * OAuth认证验证方法
+	 */
+	private Long authenticateOAuth(HttpServletRequest request) {
+		String authHeader = request.getHeader("Authorization");
+		if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+			throw BusinessRuntimeException.getInstance("OAuth token required");
+		}
+
+		String tokenValue = authHeader.substring(7); // 移除"Bearer "前缀
+
+		// 验证token
+		OAuthToken token = oAuthClaudeCodeService.validateToken(null, tokenValue);
+		if (token == null) {
+			throw BusinessRuntimeException.getInstance("Invalid or expired token");
+		}
+
+		return token.getUserId();
+	}
+
+	/**
+	 * OAuth授权端点
+	 */
+	@GetMapping("/authorize")
+	public void authorize(@RequestParam String clientId,
+						  @RequestParam String redirectUri,
+						  @RequestParam String state,
+						  HttpServletResponse response) throws IOException {
+
+		try {
+			// 验证参数
+			if (clientId == null || clientId.isEmpty() ||
+					redirectUri == null || redirectUri.isEmpty() ||
+					state == null || state.isEmpty()) {
+				response.sendError(400, "Missing required parameters");
+				return;
+			}
+
+			// 验证客户端ID
+			if (!isValidClientId(clientId)) {
+				response.sendError(400, "Invalid client_id");
+				return;
+			}
+
+			// 检查用户登录状态
+			if (!StpUserUtil.isLogin()) {
+				// 重定向到登录页面
+				String loginUrl = "/login?client_id=" + clientId +
+						"&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) +
+						"&state=" + state;
+				response.sendRedirect(loginUrl);
+				return;
+			}
+
+			// 用户已登录,生成token
+			Long userId = StpUserUtil.getLoginIdAsLong();
+			String token = oAuthClaudeCodeService.generateAccessToken(userId, clientId);
+
+			// 重定向回客户端
+			String callbackUrl = redirectUri + "?token=" + token + "&state=" + state;
+			response.sendRedirect(callbackUrl);
+
+			log.info("OAuth授权成功: userId={}, clientId={}, token={}", userId, clientId, token.substring(0, 8) + "...");
+
+		} catch (Exception e) {
+			log.error("OAuth授权失败", e);
+			response.sendError(500, "Authorization failed");
+		}
+	}
+
+	/**
+	 * Token验证端点
+	 */
+	@PostMapping("/verify-token")
+	public Result<Map<String, Object>> verifyToken(@RequestBody Map<String, String> request) {
+
+		try {
+			String token = request.get("token");
+			String code = request.get("code");
+			String clientId = request.get("client_id");
+			String clientSecret = request.get("client_secret");
+
+			// 验证客户端凭据
+			OAuthClient client = oAuthClaudeCodeService.validateClient(clientId, clientSecret);
+			if (client == null) {
+				throw BusinessRuntimeException.getInstance("Invalid client credentials");
+			}
+
+			// 验证token
+			OAuthToken oauthToken = oAuthClaudeCodeService.validateToken(code,token);
+			if (oauthToken == null) {
+				throw BusinessRuntimeException.getInstance("Invalid or expired token");
+			}
+
+			// 返回用户信息
+			Map<String, Object> result = new HashMap<>();
+			result.put("authenticated", true);
+			result.put("userId", oauthToken.getUserId());
+			result.put("token", oauthToken.getKeyValue());
+			result.put("email", getUserEmail(oauthToken.getUserId()));
+
+			log.info("Token验证成功: userId={}, clientId={}", oauthToken.getUserId(), clientId);
+
+			return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+
+		} catch (Exception e) {
+			log.error("Token验证失败", e);
+			throw BusinessRuntimeException.getInstance("Token verification failed");
+		}
+	}
+
+	/**
+	 * 验证客户端ID是否有效
+	 */
+	private boolean isValidClientId(String clientId) {
+		// 这里可以添加更严格的客户端ID验证逻辑
+		return "vvxked850tk1ikvhpk07s47a2vnppa7t".equals(clientId);
+	}
+
+	/**
+	 * 获取用户邮箱
+	 */
+	private String getUserEmail(Long userId) {
+		// 这里需要根据实际的用户表结构来获取用户邮箱
+		return "user" + userId + "@example.com";
+	}
+
+	/**
+	 * 获取公告列表
+	 */
+	@GetMapping("/announcements")
+	public Result<List<Announcement>> getAnnouncements(@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN") String language) {
+		try {
+			// OAuth认证
+			authenticateOAuth(request);
+
+			// 处理语言参数
+			if (language.startsWith("en")) {
+				language = "en";
+			} else {
+				language = "zh-CN";
+			}
+
+			List<Announcement> announcements = announcementService.getActiveAnnouncements(language);
+			return GatewayResponse.SUCCESS.newBuilder().toResult(announcements);
+		} catch (Exception e) {
+			throw BusinessRuntimeException.getInstance("Failed to fetch announcements");
+		}
+	}
+
+	/**
+	 * 获取中继服务器列表
+	 */
+	@GetMapping("/relays")
+	public Result<List<RelayServer>> getRelays(@RequestParam(required = false) String domain) {
+		try {
+			// OAuth认证
+			authenticateOAuth(request);
+
+			List<RelayServer> relays = relayService.getAvailableRelays(domain);
+			return GatewayResponse.SUCCESS.newBuilder().toResult(relays);
+		} catch (Exception e) {
+			throw BusinessRuntimeException.getInstance("Failed to fetch relay servers");
+		}
+	}
+
 }

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

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