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; import com.cyksj.model.response.ClaudeCodePointsHistoryResp; import com.cyksj.model.response.claudecode.ClaudeCodeResp; import com.cyksj.model.views.ClaudeCodeUserApiKeysView; import com.cyksj.model.views.ClaudeCodeUserInfoView; import com.cyksj.service.claude.ClaudeCodeService; import com.cyksj.service.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 * 项目名: yhlxj11111111 * 文件名: ClaudeCodeController * 创建者: JavaZou * 创建时间:2025/8/8 11:37 */ @Slf4j @RestController @RequestMapping("/applet/claude/code") @RequiredArgsConstructor public class ClaudeCodeController { private final OAuthClaudeCodeService oAuthClaudeCodeService; private final ClaudeCodeService claudeCodeService; private final BeanSearcher beanSearcher; private final HttpServletRequest request; private final UserBindRelationService userBindRelationService; private final AnnouncementService announcementService; private final RelayService relayService; /** * 用户创建API KEY */ @PostMapping("/create/apiKeys") public Result createApiKeys(@RequestBody @Validated ClaudeCodeApiKeysReq req) throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); req.setUserId(userId); claudeCodeService.createApiKeys(req); return GatewayResponse.SUCCESS.newBuilder().toResult(); } /** * 用户api keys列表 */ @GetMapping("/get/apiKeys") public Result> getUserApiKeys() throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); List userApiKeys = claudeCodeService.getUserApiKeys(userId); return GatewayResponse.SUCCESS.newBuilder().toResult(userApiKeys); } /** * 删除api key */ @DeleteMapping("/delete/apiKeys") public Result deleteApiKeysById(@RequestBody ClaudeCodeDelReq delReq) throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); delReq.setUserId(userId); claudeCodeService.deleteApiKeysById(delReq); return GatewayResponse.SUCCESS.newBuilder().toResult(); } //================================================用户============================================================ /** * 用户订阅车票信息 */ @GetMapping("/get/user/subs/info") public Result getUserSubsInfo() { long userId = StpUserUtil.getLoginIdAsLong(); ClaudeCodeUserInfoView claudeCodeUserInfoView = claudeCodeService.getClaudeCodeUserSubsInfo(userId); return GatewayResponse.SUCCESS.newBuilder().toResult(claudeCodeUserInfoView); } /** * 用户订阅历史 */ @GetMapping("/get/user/orders") public Result> getUserOrders() { long userId = StpUserUtil.getLoginIdAsLong(); List userIdList = userBindRelationService.getRelationUserIdList(userId, null); SearchResult search = beanSearcher.search(OrderDonView.class, MapUtils.flatBuilder(request.getParameterMap()) .field(OrderDonView::getUserId, userIdList).op(Operator.InList) .field(OrderDonView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID) .field(OrderDonView::getStatus, Constant.noOrderAllStatus).op(Operator.NotIn) .build()); return GatewayResponse.SUCCESS.newBuilder().toResult(search); } /** * 用户积分明细 * - `page` (int, optional): 页码,默认1 * - `limit` (int, optional): 每页数量,默认20,最大100 * - `type` (string, optional): 积分类型过滤 */ @GetMapping("/get/user/points/detail") public Result getUserPointsDetail(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String type) throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); ClaudeCodePointsHistoryResp resp = claudeCodeService.getUserPointsDetail(userId, type, page, limit); return GatewayResponse.SUCCESS.newBuilder().toResult(resp); } /** * 仪表盘聚合 * 返回 当前订阅/余额卡片 + 今日汇总 + 今日请求列表 */ @GetMapping("/get/dashboard") public Result getUserDashboard() throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); ClaudeCodeResp resp = claudeCodeService.getUserDashboard(userId); return GatewayResponse.SUCCESS.newBuilder().toResult(resp); } /** * 余额卡片 * 单独返回余额卡片数据 */ @GetMapping("/get/balance") public Result getUserBalance() throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); ClaudeCodeResp resp = claudeCodeService.getUserBalance(userId); return GatewayResponse.SUCCESS.newBuilder().toResult(resp); } /** * 图表 + 明细联动(每小时聚合) * - `start` (string, required): `YYYY-MM-DD HH:MM:SS`,例如 `2025-08-08 12:00:00` * - `end` (string, required): `YYYY-MM-DD HH:MM:SS`,需大于 `start` * - `tz` (string, optional): 时区,默认 `Asia/Shanghai` * - `page` (int, optional): 明细页码,默认 1 * - `limit` (int, optional): 明细每页数量,默认 20,最大 100 * - `order` (string, optional): `asc|desc`,默认 `desc` * - `type` (string, optional): 明细类型过滤,`consume|purchase|recovery|refund|reward|adjustment|all`,默认 `all` */ @GetMapping("/get/credits/analytics") public Result getCreditsAnalytics(String start, String end, @RequestParam(defaultValue = "Asia/Shanghai") String tz, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String order, String type) throws Exception { long userId = StpUserUtil.getLoginIdAsLong(); 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> verifyToken(@RequestBody Map 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 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> 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 announcements = announcementService.getActiveAnnouncements(language); return GatewayResponse.SUCCESS.newBuilder().toResult(announcements); } catch (Exception e) { throw BusinessRuntimeException.getInstance("Failed to fetch announcements"); } } /** * 获取中继服务器列表 */ @GetMapping("/relays") public Result> getRelays(@RequestParam(required = false) String domain) { try { // OAuth认证 authenticateOAuth(request); List relays = relayService.getAvailableRelays(domain); return GatewayResponse.SUCCESS.newBuilder().toResult(relays); } catch (Exception e) { throw BusinessRuntimeException.getInstance("Failed to fetch relay servers"); } } }