|
@@ -1,8 +1,12 @@
|
|
|
package com.cyksj.web.controller.claude;
|
|
package com.cyksj.web.controller.claude;
|
|
|
|
|
|
|
|
import com.cyksj.common.constant.Constant;
|
|
import com.cyksj.common.constant.Constant;
|
|
|
|
|
+import com.cyksj.common.exception.BusinessRuntimeException;
|
|
|
import com.cyksj.dto.Result;
|
|
import com.cyksj.dto.Result;
|
|
|
import com.cyksj.enums.GatewayResponse;
|
|
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.manage.views.OrderDonView;
|
|
|
import com.cyksj.model.request.ClaudeCodeApiKeysReq;
|
|
import com.cyksj.model.request.ClaudeCodeApiKeysReq;
|
|
|
import com.cyksj.model.request.ClaudeCodeDelReq;
|
|
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.ClaudeCodeUserApiKeysView;
|
|
|
import com.cyksj.model.views.ClaudeCodeUserInfoView;
|
|
import com.cyksj.model.views.ClaudeCodeUserInfoView;
|
|
|
import com.cyksj.service.claude.ClaudeCodeService;
|
|
import com.cyksj.service.claude.ClaudeCodeService;
|
|
|
|
|
+import com.cyksj.service.claude.OAuthClaudeCodeService;
|
|
|
|
|
+import com.cyksj.service.claude.RelayService;
|
|
|
import com.cyksj.service.user.UserBindRelationService;
|
|
import com.cyksj.service.user.UserBindRelationService;
|
|
|
|
|
+import com.cyksj.service.claude.AnnouncementService;
|
|
|
|
|
+import com.cyksj.model.entity.OAuthToken;
|
|
|
import com.cyksj.web.util.StpUserUtil;
|
|
import com.cyksj.web.util.StpUserUtil;
|
|
|
import com.ejlchina.searcher.BeanSearcher;
|
|
import com.ejlchina.searcher.BeanSearcher;
|
|
|
import com.ejlchina.searcher.SearchResult;
|
|
import com.ejlchina.searcher.SearchResult;
|
|
|
import com.ejlchina.searcher.param.Operator;
|
|
import com.ejlchina.searcher.param.Operator;
|
|
|
import com.ejlchina.searcher.util.MapUtils;
|
|
import com.ejlchina.searcher.util.MapUtils;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
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.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* server/claude code
|
|
* server/claude code
|
|
@@ -31,10 +47,14 @@ import java.util.List;
|
|
|
* 创建者: JavaZou
|
|
* 创建者: JavaZou
|
|
|
* 创建时间:2025/8/8 11:37
|
|
* 创建时间:2025/8/8 11:37
|
|
|
*/
|
|
*/
|
|
|
|
|
+@Slf4j
|
|
|
@RestController
|
|
@RestController
|
|
|
@RequestMapping("/applet/claude/code")
|
|
@RequestMapping("/applet/claude/code")
|
|
|
@RequiredArgsConstructor
|
|
@RequiredArgsConstructor
|
|
|
public class ClaudeCodeController {
|
|
public class ClaudeCodeController {
|
|
|
|
|
+
|
|
|
|
|
+ private final OAuthClaudeCodeService oAuthClaudeCodeService;
|
|
|
|
|
+
|
|
|
private final ClaudeCodeService claudeCodeService;
|
|
private final ClaudeCodeService claudeCodeService;
|
|
|
|
|
|
|
|
private final BeanSearcher beanSearcher;
|
|
private final BeanSearcher beanSearcher;
|
|
@@ -43,6 +63,10 @@ public class ClaudeCodeController {
|
|
|
|
|
|
|
|
private final UserBindRelationService userBindRelationService;
|
|
private final UserBindRelationService userBindRelationService;
|
|
|
|
|
|
|
|
|
|
+ private final AnnouncementService announcementService;
|
|
|
|
|
+
|
|
|
|
|
+ private final RelayService relayService;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 用户创建API KEY
|
|
* 用户创建API KEY
|
|
|
*/
|
|
*/
|
|
@@ -154,4 +178,174 @@ public class ClaudeCodeController {
|
|
|
ClaudeCodeResp resp = claudeCodeService.getCreditsAnalytics(userId, start, end, tz, page, limit, order, type);
|
|
ClaudeCodeResp resp = claudeCodeService.getCreditsAnalytics(userId, start, end, tz, page, limit, order, type);
|
|
|
return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
|
|
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");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|