|
@@ -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";
|
|
|
|
|
+ }
|
|
|
|
|
+}
|