ClaudeCodeController.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. package com.cyksj.web.controller.claude;
  2. import com.cyksj.common.constant.Constant;
  3. import com.cyksj.common.exception.BusinessRuntimeException;
  4. import com.cyksj.dto.Result;
  5. import com.cyksj.enums.GatewayResponse;
  6. import com.cyksj.model.entity.Announcement;
  7. import com.cyksj.model.entity.OAuthClient;
  8. import com.cyksj.model.entity.OAuthToken;
  9. import com.cyksj.model.entity.RelayServer;
  10. import com.cyksj.model.manage.views.OrderDonView;
  11. import com.cyksj.model.request.ClaudeCodeApiKeysReq;
  12. import com.cyksj.model.request.ClaudeCodeDelReq;
  13. import com.cyksj.model.response.ClaudeCodePointsHistoryResp;
  14. import com.cyksj.model.response.claudecode.ClaudeCodeResp;
  15. import com.cyksj.model.views.ClaudeCodeUserApiKeysView;
  16. import com.cyksj.model.views.ClaudeCodeUserInfoView;
  17. import com.cyksj.service.claude.AnnouncementService;
  18. import com.cyksj.service.claude.ClaudeCodeService;
  19. import com.cyksj.service.claude.OAuthClaudeCodeService;
  20. import com.cyksj.service.claude.RelayService;
  21. import com.cyksj.service.user.UserBindRelationService;
  22. import com.cyksj.web.util.StpUserUtil;
  23. import com.ejlchina.searcher.BeanSearcher;
  24. import com.ejlchina.searcher.SearchResult;
  25. import com.ejlchina.searcher.param.Operator;
  26. import com.ejlchina.searcher.util.MapUtils;
  27. import lombok.RequiredArgsConstructor;
  28. import lombok.extern.slf4j.Slf4j;
  29. import org.springframework.validation.annotation.Validated;
  30. import org.springframework.web.bind.annotation.*;
  31. import javax.servlet.http.HttpServletRequest;
  32. import javax.servlet.http.HttpServletResponse;
  33. import java.io.IOException;
  34. import java.net.URLEncoder;
  35. import java.nio.charset.StandardCharsets;
  36. import java.util.HashMap;
  37. import java.util.List;
  38. import java.util.Map;
  39. /**
  40. * server/claude code
  41. * 项目名: yhlxj11111111
  42. * 文件名: ClaudeCodeController
  43. * 创建者: JavaZou
  44. * 创建时间:2025/8/8 11:37
  45. */
  46. @Slf4j
  47. @RestController
  48. @RequestMapping("/applet/claude/code")
  49. @RequiredArgsConstructor
  50. public class ClaudeCodeController {
  51. private final OAuthClaudeCodeService oAuthClaudeCodeService;
  52. private final ClaudeCodeService claudeCodeService;
  53. private final BeanSearcher beanSearcher;
  54. private final HttpServletRequest request;
  55. private final UserBindRelationService userBindRelationService;
  56. private final AnnouncementService announcementService;
  57. private final RelayService relayService;
  58. /**
  59. * 用户创建API KEY
  60. */
  61. @PostMapping("/create/apiKeys")
  62. public Result<String> createApiKeys(@RequestBody @Validated ClaudeCodeApiKeysReq req) throws Exception {
  63. long userId = StpUserUtil.getLoginIdAsLong();
  64. req.setUserId(userId);
  65. claudeCodeService.createApiKeys(req);
  66. return GatewayResponse.SUCCESS.newBuilder().toResult();
  67. }
  68. /**
  69. * 用户api keys列表
  70. */
  71. @GetMapping("/get/apiKeys")
  72. public Result<List<ClaudeCodeUserApiKeysView>> getUserApiKeys() throws Exception {
  73. long userId = StpUserUtil.getLoginIdAsLong();
  74. List<ClaudeCodeUserApiKeysView> userApiKeys = claudeCodeService.getUserApiKeys(userId);
  75. return GatewayResponse.SUCCESS.newBuilder().toResult(userApiKeys);
  76. }
  77. /**
  78. * 删除api key
  79. */
  80. @DeleteMapping("/delete/apiKeys")
  81. public Result<String> deleteApiKeysById(@RequestBody ClaudeCodeDelReq delReq) throws Exception {
  82. long userId = StpUserUtil.getLoginIdAsLong();
  83. delReq.setUserId(userId);
  84. claudeCodeService.deleteApiKeysById(delReq);
  85. return GatewayResponse.SUCCESS.newBuilder().toResult();
  86. }
  87. //================================================用户============================================================
  88. /**
  89. * 用户订阅车票信息
  90. */
  91. @GetMapping("/get/user/subs/info")
  92. public Result<ClaudeCodeUserInfoView> getUserSubsInfo() {
  93. long userId = StpUserUtil.getLoginIdAsLong();
  94. ClaudeCodeUserInfoView claudeCodeUserInfoView = claudeCodeService.getClaudeCodeUserSubsInfo(userId);
  95. return GatewayResponse.SUCCESS.newBuilder().toResult(claudeCodeUserInfoView);
  96. }
  97. /**
  98. * 用户订阅历史
  99. */
  100. @GetMapping("/get/user/orders")
  101. public Result<SearchResult<OrderDonView>> getUserOrders() {
  102. long userId = StpUserUtil.getLoginIdAsLong();
  103. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  104. SearchResult<OrderDonView> search = beanSearcher.search(OrderDonView.class, MapUtils.flatBuilder(request.getParameterMap())
  105. .field(OrderDonView::getUserId, userIdList).op(Operator.InList)
  106. .field(OrderDonView::getGoodsId, Constant.CLAUDE_CODE_GOODS_ID)
  107. .field(OrderDonView::getStatus, Constant.noOrderAllStatus).op(Operator.NotIn)
  108. .build());
  109. return GatewayResponse.SUCCESS.newBuilder().toResult(search);
  110. }
  111. /**
  112. * 用户积分明细
  113. * - `page` (int, optional): 页码,默认1
  114. * - `limit` (int, optional): 每页数量,默认20,最大100
  115. * - `type` (string, optional): 积分类型过滤
  116. */
  117. @GetMapping("/get/user/points/detail")
  118. public Result<ClaudeCodePointsHistoryResp> getUserPointsDetail(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String type) throws Exception {
  119. long userId = StpUserUtil.getLoginIdAsLong();
  120. ClaudeCodePointsHistoryResp resp = claudeCodeService.getUserPointsDetail(userId, type, page, limit);
  121. return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
  122. }
  123. /**
  124. * 仪表盘聚合
  125. * 返回 当前订阅/余额卡片 + 今日汇总 + 今日请求列表
  126. */
  127. @GetMapping("/get/dashboard")
  128. public Result<ClaudeCodeResp> getUserDashboard() throws Exception {
  129. long userId = StpUserUtil.getLoginIdAsLong();
  130. ClaudeCodeResp resp = claudeCodeService.getUserDashboard(userId);
  131. return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
  132. }
  133. /**
  134. * 余额卡片
  135. * 单独返回余额卡片数据
  136. */
  137. @GetMapping("/get/balance")
  138. public Result<ClaudeCodeResp> getUserBalance() throws Exception {
  139. long userId = StpUserUtil.getLoginIdAsLong();
  140. ClaudeCodeResp resp = claudeCodeService.getUserBalance(userId);
  141. return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
  142. }
  143. /**
  144. * 图表 + 明细联动(每小时聚合)
  145. * - `start` (string, required): `YYYY-MM-DD HH:MM:SS`,例如 `2025-08-08 12:00:00`
  146. * - `end` (string, required): `YYYY-MM-DD HH:MM:SS`,需大于 `start`
  147. * - `tz` (string, optional): 时区,默认 `Asia/Shanghai`
  148. * - `page` (int, optional): 明细页码,默认 1
  149. * - `limit` (int, optional): 明细每页数量,默认 20,最大 100
  150. * - `order` (string, optional): `asc|desc`,默认 `desc`
  151. * - `type` (string, optional): 明细类型过滤,`consume|purchase|recovery|refund|reward|adjustment|all`,默认 `all`
  152. */
  153. @GetMapping("/get/credits/analytics")
  154. public Result<ClaudeCodeResp> 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 {
  155. long userId = StpUserUtil.getLoginIdAsLong();
  156. ClaudeCodeResp resp = claudeCodeService.getCreditsAnalytics(userId, start, end, tz, page, limit, order, type);
  157. return GatewayResponse.SUCCESS.newBuilder().toResult(resp);
  158. }
  159. //================================================Claude Code OAuth接口============================================================
  160. /**
  161. * OAuth认证验证方法
  162. */
  163. private Long authenticateOAuth(HttpServletRequest request) {
  164. String authHeader = request.getHeader("Authorization");
  165. if (authHeader == null || !authHeader.startsWith("Bearer ")) {
  166. throw BusinessRuntimeException.getInstance("OAuth token required");
  167. }
  168. String tokenValue = authHeader.substring(7); // 移除"Bearer "前缀
  169. // 验证token
  170. OAuthToken token = oAuthClaudeCodeService.validateToken(null, tokenValue);
  171. if (token == null) {
  172. throw BusinessRuntimeException.getInstance("Invalid or expired token");
  173. }
  174. return token.getUserId();
  175. }
  176. /**
  177. * OAuth授权端点
  178. */
  179. @GetMapping("/authorize")
  180. public void authorize(@RequestParam(name = "client_id") String clientId,
  181. @RequestParam(name = "redirect_uri") String redirectUri,
  182. @RequestParam String state,
  183. HttpServletResponse response) throws IOException {
  184. try {
  185. // 验证参数
  186. if (clientId == null || clientId.isEmpty() ||
  187. redirectUri == null || redirectUri.isEmpty() ||
  188. state == null || state.isEmpty()) {
  189. response.sendError(400, "Missing required parameters");
  190. return;
  191. }
  192. // 验证客户端ID
  193. if (!isValidClientId(clientId)) {
  194. response.sendError(400, "Invalid client_id");
  195. return;
  196. }
  197. // 检查用户登录状态
  198. if (!StpUserUtil.isLogin()) {
  199. // 重定向到登录页面
  200. String loginUrl = "/login?client_id=" + clientId +
  201. "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) +
  202. "&state=" + state;
  203. response.sendRedirect(loginUrl);
  204. return;
  205. }
  206. // 用户已登录,生成token
  207. Long userId = StpUserUtil.getLoginIdAsLong();
  208. String token = oAuthClaudeCodeService.generateAccessToken(userId, clientId);
  209. if (token == null) {
  210. throw BusinessRuntimeException.getInstance("您还没有claudecode账号");
  211. }
  212. // 重定向回客户端
  213. String callbackUrl = redirectUri + "?token=" + token + "&state=" + state;
  214. response.sendRedirect(callbackUrl);
  215. log.info("OAuth授权成功: userId={}, clientId={}, token={}", userId, clientId, token.substring(0, 8) + "...");
  216. } catch (Exception e) {
  217. log.error("OAuth授权失败", e);
  218. response.sendError(500, "Authorization failed");
  219. }
  220. }
  221. /**
  222. * Token验证端点
  223. */
  224. @PostMapping("/verify-token")
  225. public Result<Map<String, Object>> verifyToken(@RequestBody Map<String, String> request) {
  226. try {
  227. String token = request.get("token");
  228. String code = request.get("code");
  229. String clientId = request.get("client_id");
  230. String clientSecret = request.get("client_secret");
  231. // 验证客户端凭据
  232. OAuthClient client = oAuthClaudeCodeService.validateClient(clientId, clientSecret);
  233. if (client == null) {
  234. throw BusinessRuntimeException.getInstance("Invalid client credentials");
  235. }
  236. // 验证token
  237. OAuthToken oauthToken = oAuthClaudeCodeService.validateToken(code,token);
  238. if (oauthToken == null) {
  239. throw BusinessRuntimeException.getInstance("Invalid or expired token");
  240. }
  241. // 返回用户信息
  242. Map<String, Object> result = new HashMap<>();
  243. result.put("authenticated", true);
  244. result.put("userId", oauthToken.getUserId());
  245. result.put("token", oauthToken.getKeyValue());
  246. result.put("email", getUserEmail(oauthToken.getUserId()));
  247. log.info("Token验证成功: userId={}, clientId={}", oauthToken.getUserId(), clientId);
  248. return GatewayResponse.SUCCESS.newBuilder().toResult(result);
  249. } catch (Exception e) {
  250. log.error("Token验证失败", e);
  251. throw BusinessRuntimeException.getInstance("Token verification failed");
  252. }
  253. }
  254. /**
  255. * 验证客户端ID是否有效
  256. */
  257. private boolean isValidClientId(String clientId) {
  258. // 这里可以添加更严格的客户端ID验证逻辑
  259. return "vvxked850tk1ikvhpk07s47a2vnppa7t".equals(clientId);
  260. }
  261. /**
  262. * 获取用户邮箱
  263. */
  264. private String getUserEmail(Long userId) {
  265. // 这里需要根据实际的用户表结构来获取用户邮箱
  266. return "user" + userId + "@example.com";
  267. }
  268. /**
  269. * 获取公告列表
  270. */
  271. @GetMapping("/announcements")
  272. public Result<List<Announcement>> getAnnouncements(@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN") String language) {
  273. try {
  274. // OAuth认证
  275. authenticateOAuth(request);
  276. // 处理语言参数
  277. if (language.startsWith("en")) {
  278. language = "en";
  279. } else {
  280. language = "zh-CN";
  281. }
  282. List<Announcement> announcements = announcementService.getActiveAnnouncements(language);
  283. return GatewayResponse.SUCCESS.newBuilder().toResult(announcements);
  284. } catch (Exception e) {
  285. throw BusinessRuntimeException.getInstance("Failed to fetch announcements");
  286. }
  287. }
  288. /**
  289. * 获取中继服务器列表
  290. */
  291. @GetMapping("/relays")
  292. public Result<List<RelayServer>> getRelays(@RequestParam(required = false) String domain) {
  293. try {
  294. // OAuth认证
  295. authenticateOAuth(request);
  296. List<RelayServer> relays = relayService.getAvailableRelays(domain);
  297. return GatewayResponse.SUCCESS.newBuilder().toResult(relays);
  298. } catch (Exception e) {
  299. throw BusinessRuntimeException.getInstance("Failed to fetch relay servers");
  300. }
  301. }
  302. }