ClaudeCodeController.java 12 KB

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