ClaudeServiceImpl.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. package com.cyksj.service.claude.impl;
  2. import cn.hutool.core.date.DateTime;
  3. import cn.hutool.core.lang.UUID;
  4. import cn.hutool.core.util.RandomUtil;
  5. import cn.hutool.core.util.StrUtil;
  6. import cn.hutool.http.HttpRequest;
  7. import cn.hutool.http.HttpResponse;
  8. import cn.hutool.http.HttpUtil;
  9. import cn.hutool.http.Method;
  10. import cn.hutool.json.JSONObject;
  11. import cn.hutool.json.JSONUtil;
  12. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  13. import com.cyksj.common.exception.BusinessRuntimeException;
  14. import com.cyksj.common.util.Jsons;
  15. import com.cyksj.common.util.StringUtil;
  16. import com.cyksj.mapper.*;
  17. import com.cyksj.model.entity.*;
  18. import com.cyksj.model.request.gpt.ConversationRequest;
  19. import com.cyksj.model.response.ConversationLimitResponse;
  20. import com.cyksj.model.views.ClaudeUserView;
  21. import com.cyksj.redis.RedisService;
  22. import com.cyksj.service.claude.ClaudeService;
  23. import com.cyksj.service.sys.SysConfigService;
  24. import com.cyksj.service.user.UserBindRelationService;
  25. import lombok.RequiredArgsConstructor;
  26. import lombok.extern.slf4j.Slf4j;
  27. import org.apache.commons.lang3.StringUtils;
  28. import org.springframework.dao.DuplicateKeyException;
  29. import org.springframework.stereotype.Service;
  30. import java.math.BigDecimal;
  31. import java.math.RoundingMode;
  32. import java.time.LocalDateTime;
  33. import java.util.List;
  34. import java.util.Optional;
  35. import java.util.Set;
  36. /**
  37. * @author chan
  38. * @date 2024/8/8 18:34
  39. */
  40. @Service
  41. @RequiredArgsConstructor
  42. @Slf4j
  43. public class ClaudeServiceImpl implements ClaudeService {
  44. private final ClaudeUserMapper claudeUserMapper;
  45. private final GroupsRelationMapper groupsRelationMapper;
  46. private final GroupsMapper groupsMapper;
  47. private final GoodsDonSkuMapper skuMapper;
  48. private final UserBindRelationService userBindRelationService;
  49. private final UserMapper userMapper;
  50. private final SysConfigService sysConfigService;
  51. private final RedisService redisService;
  52. private final ClaudeUserCarUsedRecordMapper claudeUserCarUsedRecordMapper;
  53. private final ClaudeUserConversationRecordMapper claudeUserConversationRecordMapper;
  54. @Override
  55. public ClaudeUserView getUserInfo(long userId, Long relationId) {
  56. ClaudeUser claudeUser = checkCarAccount(userId, relationId);
  57. if (claudeUser != null) {
  58. GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
  59. GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
  60. GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
  61. ClaudeUserView view = ClaudeUserView.builder()
  62. .skuName(goodsDonSku.getSubTitle())
  63. .expireTime(claudeUser.getExpireTime())
  64. .limitNum(claudeUser.getLimitNum())
  65. .limitTime(claudeUser.getLimitTime())
  66. .isRenewPackage(claudeUser.getRenewOrderId() != null ? true : false)
  67. .renewSkuId(claudeUser.getRenewSkuId())
  68. .renewOrderId(claudeUser.getRenewOrderId())
  69. // .use(Math.min(claudeUser.getLimitNum(), getConversationCount(claudeUser.getUserToken(), claudeUser.getLimitTime())))
  70. .build();
  71. if (view.getRenewSkuId() != null) {
  72. Optional.ofNullable(skuMapper.selectById(view.getRenewSkuId())).ifPresent(renewSku -> view.setRenewSpecVal(renewSku.getSpecVal()));
  73. }
  74. return view;
  75. } else {
  76. throw BusinessRuntimeException.getInstance("您还未购买该车票");
  77. }
  78. }
  79. @Override
  80. public ClaudeUser findByUserTokenAndExpireTimeAfter(String userToken, LocalDateTime now) {
  81. return claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getUserToken, userToken).gt(ClaudeUser::getExpireTime, now));
  82. }
  83. @Override
  84. public ConversationLimitResponse conversationLimit(String userToken, ClaudeUser user) {
  85. return isConversationAllowed(userToken, user.getLimitNum(), user.getLimitTime());
  86. }
  87. @Override
  88. public void carLimited(String carId, String userToken, Long expTime) {
  89. redisService.set("claude:clears_in:" + carId, expTime, expTime);
  90. try {
  91. //记录用户触发限制
  92. ClaudeUserCarUsedRecord claudeUserCarUsedRecord = new ClaudeUserCarUsedRecord();
  93. claudeUserCarUsedRecord.setUserToken(userToken);
  94. claudeUserCarUsedRecord.setCarId(carId);
  95. String carChatKey = RedisService.key.CLAUDE_CAR_CHAT.getName() + carId;
  96. Long carChatNum = redisService.zCard(carChatKey);
  97. if (carChatNum != null) {
  98. claudeUserCarUsedRecord.setNum(Integer.parseInt(carChatNum.toString()));
  99. }
  100. claudeUserCarUsedRecordMapper.insert(claudeUserCarUsedRecord);
  101. log.info("记录userToken:{} 触发限制 carId:{}", userToken, carId);
  102. } catch (Exception e) {
  103. log.info("记录userToken:{} 触发限制 carId:{} 操作失败e:{}", userToken, carId, StringUtil.getErrorText(e));
  104. }
  105. }
  106. @Override
  107. public ClaudeUser getClaudeUser(long userId, Long relationId) {
  108. GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
  109. GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
  110. GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
  111. if (goodsDonSku != null && goodsDonSku.getGoodsId() == 75 && goodsDonSku.getIsMirror()) {
  112. return getClaudeUser(userId, relationId, groupsRelation, goodsDonSku);
  113. } else {
  114. throw BusinessRuntimeException.getInstance("服务器出了点问题");
  115. }
  116. }
  117. @Override
  118. public void saveConversationRecord(String userToken, ConversationRequest conversationRequest) {
  119. ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getUserToken, userToken));
  120. ClaudeUserConversationRecord claudeUserConversationRecord = new ClaudeUserConversationRecord();
  121. claudeUserConversationRecord.setUserToken(userToken);
  122. claudeUserConversationRecord.setCarId(conversationRequest.getCarId());
  123. if (StringUtils.isNotBlank(conversationRequest.getConversation_id())) {
  124. claudeUserConversationRecord.setConversationId(conversationRequest.getConversation_id());
  125. }
  126. claudeUserConversationRecord.setModel(conversationRequest.getModel());
  127. claudeUserConversationRecordMapper.insert(claudeUserConversationRecord);
  128. String key = RedisService.key.CLAUDE_CONVERSATION_LIMIT.getName() + ":" + userToken;
  129. long currentTimeMillis = System.currentTimeMillis();
  130. if (redisService.hasKey("claude:clears_in:" + conversationRequest.getCarId())) {
  131. redisService.del("claude:clears_in:" + conversationRequest.getCarId());
  132. }
  133. // 如果未超过限制,记录当前请求的时间戳
  134. redisService.zAdd(key, currentTimeMillis, currentTimeMillis);
  135. // 设置ZSet的过期时间,窗口大小加上一段冗余时间
  136. redisService.expire(key, (claudeUser.getLimitTime() * 60 * 60) + 20);
  137. if (StringUtils.isNotBlank(conversationRequest.getCarId())) {
  138. updateExperienceAndScore(conversationRequest.getCarId(), System.currentTimeMillis());
  139. }
  140. }
  141. @Override
  142. public ClaudeUser checkCarAccount(Long userId, Long relationId) {
  143. GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
  144. GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
  145. GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
  146. if (goodsDonSku != null && goodsDonSku.getGoodsId() == 75 && goodsDonSku.getIsMirror() && goodsDonSku.getIsCar()) {
  147. return getClaudeUser(userId, relationId, groupsRelation, goodsDonSku);
  148. } else {
  149. return null;
  150. }
  151. }
  152. @Override
  153. public String getCarLoginUrl(String domain, Long userId, Long relationId, String carId) {
  154. GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
  155. GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
  156. GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
  157. if (goodsDonSku != null && goodsDonSku.getIsMirror() && goodsDonSku.getIsCar()) {
  158. ClaudeUser claudeUser = getClaudeUser(userId, relationId, groupsRelation, goodsDonSku);
  159. String DOMAIN = domain;
  160. if (StrUtil.isEmpty(DOMAIN)) {
  161. DOMAIN = "https://claudeplus.com.cn";
  162. try {
  163. SysConfig sysConfig = sysConfigService.getOne(Wrappers.lambdaQuery(SysConfig.class).eq(SysConfig::getSysKey, "mirror_claude_domain"));
  164. if (sysConfig != null) {
  165. String sysValue = sysConfig.getSysValue();
  166. if (JSONUtil.isJsonArray(sysValue)) {
  167. List<String> domainList = JSONUtil.parseArray(sysValue).toList(String.class);
  168. DOMAIN = domainList.get(RandomUtil.randomInt(domainList.size()));
  169. }
  170. }
  171. } catch (Exception e) {
  172. log.error("随机抽取域名错误. msg:{}", StringUtil.getErrorText(e));
  173. }
  174. }
  175. log.info("domain:{},用户:{},所在车次:{},座位id:{},在{}获取跳转Claude镜像的登录url", DOMAIN, userId, groupsTrips.getId(), relationId, DateTime.now());
  176. return DOMAIN + "/auth/logintoken?carid=" + carId + "&usertoken=" + claudeUser.getUserToken();
  177. } else {
  178. throw BusinessRuntimeException.getInstance("服务器出了点问题");
  179. }
  180. }
  181. /**
  182. * 获取指定用户ID在滑动窗口内的提问次数
  183. *
  184. * @param userToken 用户token
  185. * @return 滑动窗口内的请求次数
  186. */
  187. @Override
  188. public Long getConversationCount(String userToken, Long limitTime) {
  189. JSONObject conversationCount = getConversationCount(userToken);
  190. Long count = Optional.ofNullable(conversationCount.getLong("count")).orElse(0l);
  191. return new BigDecimal(count).divide(BigDecimal.valueOf(4000),0, RoundingMode.HALF_UP).longValue();
  192. }
  193. @Override
  194. public Long carLimit(String carId) {
  195. if(redisService.hasKey("claude:clears_in:" + carId)){
  196. return redisService.getExpire("claude:clears_in:" + carId);
  197. }
  198. return 0L;
  199. }
  200. @Override
  201. public void clearNum(String userToken) throws Exception {
  202. HttpRequest request = new HttpRequest("http://147.79.20.183:9611/claude/cleanUserTokenLimit?userToken=" + userToken);
  203. HttpResponse execute = request.method(Method.GET).setConnectionTimeout(3000).timeout(3000).execute();
  204. String body = execute.body();
  205. JSONObject re = Jsons.parseObject(body, JSONObject.class);
  206. if (!body.contains("清除成功")) {
  207. throw BusinessRuntimeException.getInstance(re.getStr("message"));
  208. }
  209. }
  210. @Override
  211. public void clearNumByRelationId(Long relationId) throws Exception {
  212. ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getRelationId, relationId));
  213. HttpRequest request = new HttpRequest("http://147.79.20.183:9611/claude/cleanUserTokenLimit?userToken=" + claudeUser.getUserToken());
  214. HttpResponse execute = request.method(Method.GET).setConnectionTimeout(3000).timeout(3000).execute();
  215. String body = execute.body();
  216. JSONObject re = Jsons.parseObject(body, JSONObject.class);
  217. if (!body.contains("清除成功")) {
  218. throw BusinessRuntimeException.getInstance(re.getStr("message"));
  219. }
  220. }
  221. /**
  222. * 获取车位信息
  223. *
  224. * @param userId
  225. * @param relationId
  226. * @return
  227. */
  228. private GroupsRelation getGroupsRelation(Long userId, Long relationId) {
  229. List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
  230. GroupsRelation groupsRelation = groupsRelationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).in(GroupsRelation::getUserId, userIdList).eq(GroupsRelation::getId, relationId));
  231. if (groupsRelation == null) {
  232. throw BusinessRuntimeException.getInstance("车票不存在");
  233. }
  234. return groupsRelation;
  235. }
  236. /**
  237. * 获取车队信息
  238. *
  239. * @param groupsRelation
  240. * @return
  241. */
  242. private GroupsTrips getGroupsTrips(GroupsRelation groupsRelation) {
  243. GroupsTrips groupsTrips = groupsMapper.selectById(groupsRelation.getGroupsId());
  244. if (groupsTrips == null) {
  245. throw BusinessRuntimeException.getInstance("车队异常");
  246. }
  247. return groupsTrips;
  248. }
  249. private ClaudeUser getClaudeUser(Long userId, Long relationId, GroupsRelation groupsRelation, GoodsDonSku goodsDonSku) {
  250. ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getRelationId, relationId));
  251. User user = userMapper.selectById(userId);
  252. if (claudeUser == null) {
  253. claudeUser = createClaudeUser(groupsRelation, user, goodsDonSku);
  254. } else {
  255. updateClaudeUser(groupsRelation, user, claudeUser);
  256. }
  257. return claudeUser;
  258. }
  259. /**
  260. * 创建chatgptUser
  261. */
  262. private synchronized ClaudeUser createClaudeUser(GroupsRelation groupsRelation, User user, GoodsDonSku goodsDonSku) {
  263. ClaudeUser claudeUser = new ClaudeUser();
  264. if (goodsDonSku.getIsCar()) {
  265. claudeUser.setLimitNum(goodsDonSku.getGptLimitNum());
  266. claudeUser.setLimitTime(goodsDonSku.getGptLimitTime());
  267. }
  268. try {
  269. claudeUser.setExpireTime(groupsRelation.getExpiryTime());
  270. claudeUser.setIsPlus(true);
  271. claudeUser.setName(user.getNickname());
  272. claudeUser.setRelationId(groupsRelation.getId());
  273. claudeUser.setUserToken(UUID.randomUUID().toString());
  274. claudeUserMapper.insert(claudeUser);
  275. } catch (DuplicateKeyException e) {
  276. log.error("重复插入Claude镜像用户:{}token,errmsg:{}", user.getNickname(), StringUtil.getErrorText(e));
  277. }
  278. return claudeUser;
  279. }
  280. private void updateClaudeUser(GroupsRelation groupsRelation, User user, ClaudeUser claudeUser) {
  281. claudeUser.setName(user.getNickname());
  282. claudeUser.setExpireTime(groupsRelation.getExpiryTime());
  283. claudeUserMapper.updateById(claudeUser);
  284. }
  285. /**
  286. * 检查是否允许进行进行提问
  287. *
  288. * @param userToken 用户token
  289. * @return true 如果允许请求,false 如果请求被限制
  290. */
  291. public ConversationLimitResponse isConversationAllowed(String userToken, int limit, Long limitTime) {
  292. String key = RedisService.key.CLAUDE_CONVERSATION_LIMIT.getName() + ":" + userToken;
  293. long currentTimeMillis = System.currentTimeMillis();
  294. long windowStartMillis = currentTimeMillis - (limitTime * 60 * 60) * 1000;
  295. ConversationLimitResponse conversationLimitResponse = new ConversationLimitResponse();
  296. // 清除时间窗口之前的请求记录
  297. redisService.zRemoveRangeByScore(key, 0, windowStartMillis);
  298. Long currentSize = redisService.zCard(key);
  299. if (currentSize != null && currentSize >= limit) {
  300. // 如果当前请求次数超过限制,则拒绝请求
  301. Set<Object> times = redisService.zRangeByScore(key, 0, currentTimeMillis, 0, 1);
  302. Long oldestTime = (Long) times.stream().findFirst().orElse(null);
  303. if (oldestTime != null) {
  304. // 下一次可用时间是最早请求时间之后的3小时
  305. long nextAvailableTime = oldestTime + (limitTime * 60 * 60 * 1000);
  306. conversationLimitResponse.setNextAvailableTime(nextAvailableTime);
  307. }
  308. conversationLimitResponse.setLimited(true);
  309. //用户次数使用限制
  310. ClaudeUserCarUsedRecord claudeUserCarUsedRecord = new ClaudeUserCarUsedRecord();
  311. claudeUserCarUsedRecord.setUserToken(userToken);
  312. claudeUserCarUsedRecord.setIsUserLimit(true);
  313. claudeUserCarUsedRecordMapper.insert(claudeUserCarUsedRecord);
  314. return conversationLimitResponse;
  315. } else {
  316. conversationLimitResponse.setLimited(false);
  317. return conversationLimitResponse;
  318. }
  319. }
  320. // 更新体验并计算评分
  321. public void updateExperienceAndScore(String carId, long timestamp) {
  322. // 定义键名
  323. String key = RedisService.key.CLAUDE_CAR_CHAT.getName() + carId;
  324. // 更新体验数据
  325. redisService.zAdd(key, timestamp, String.valueOf(timestamp));
  326. // 清理旧数据(可选)和重新计算评分(根据需要实现)
  327. cleanupOldExperiencesAndRecalculateScore(carId, timestamp);
  328. }
  329. // 清理旧数据和重新计算评分
  330. public void cleanupOldExperiencesAndRecalculateScore(String carId, long currentTimestamp) {
  331. long threeHoursAgo = currentTimestamp - (6 * 60 * 60 * 1000); // 3小时前的时间戳
  332. // 清理高级体验旧数据
  333. redisService.zRemoveRangeByScore(RedisService.key.CLAUDE_CAR_CHAT.getName() + carId, 0, threeHoursAgo);
  334. // 重新计算评分
  335. recalculateScore(carId, currentTimestamp);
  336. }
  337. // 重新计算指定车队的评分
  338. private void recalculateScore(String carId, long currentTimestamp) {
  339. // 实际应用中,你需要根据高级体验和低级体验的数量重新计算得分
  340. double highExperienceScore = (double) redisService.zCount(RedisService.key.CLAUDE_CAR_CHAT.getName() + carId, currentTimestamp - (6 * 60 * 60 * 1000), currentTimestamp) * 2;
  341. redisService.zAdd(RedisService.key.CLAUDE_CAR_SCORES.getName(), highExperienceScore, carId);
  342. }
  343. public JSONObject getConversationCount(String userToken) {
  344. try {
  345. String res = HttpUtil.get("http://147.79.20.183:9611/claude/getConversationCount?userToken=" + userToken);
  346. return new JSONObject(res);
  347. }catch (Exception e){
  348. return new JSONObject("{\"count\": 0}");
  349. }
  350. }
  351. }