package com.cyksj.service.claude.impl; import cn.hutool.core.date.DateTime; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.http.Method; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.cyksj.common.exception.BusinessRuntimeException; import com.cyksj.common.util.Jsons; import com.cyksj.common.util.StringUtil; import com.cyksj.mapper.*; import com.cyksj.model.entity.*; import com.cyksj.model.request.gpt.ConversationRequest; import com.cyksj.model.response.ConversationLimitResponse; import com.cyksj.model.views.ClaudeUserView; import com.cyksj.redis.RedisService; import com.cyksj.service.claude.ClaudeService; import com.cyksj.service.sys.SysConfigService; import com.cyksj.service.user.UserBindRelationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.Set; /** * @author chan * @date 2024/8/8 18:34 */ @Service @RequiredArgsConstructor @Slf4j public class ClaudeServiceImpl implements ClaudeService { private final ClaudeUserMapper claudeUserMapper; private final GroupsRelationMapper groupsRelationMapper; private final GroupsMapper groupsMapper; private final GoodsDonSkuMapper skuMapper; private final UserBindRelationService userBindRelationService; private final UserMapper userMapper; private final SysConfigService sysConfigService; private final RedisService redisService; private final ClaudeUserCarUsedRecordMapper claudeUserCarUsedRecordMapper; private final ClaudeUserConversationRecordMapper claudeUserConversationRecordMapper; @Override public ClaudeUserView getUserInfo(long userId, Long relationId) { ClaudeUser claudeUser = checkCarAccount(userId, relationId); if (claudeUser != null) { GroupsRelation groupsRelation = getGroupsRelation(userId, relationId); GroupsTrips groupsTrips = getGroupsTrips(groupsRelation); GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId()); ClaudeUserView view = ClaudeUserView.builder() .skuName(goodsDonSku.getSubTitle()) .expireTime(claudeUser.getExpireTime()) .limitNum(claudeUser.getLimitNum()) .limitTime(claudeUser.getLimitTime()) .isRenewPackage(claudeUser.getRenewOrderId() != null ? true : false) .renewSkuId(claudeUser.getRenewSkuId()) .renewOrderId(claudeUser.getRenewOrderId()) // .use(Math.min(claudeUser.getLimitNum(), getConversationCount(claudeUser.getUserToken(), claudeUser.getLimitTime()))) .build(); if (view.getRenewSkuId() != null) { Optional.ofNullable(skuMapper.selectById(view.getRenewSkuId())).ifPresent(renewSku -> view.setRenewSpecVal(renewSku.getSpecVal())); } return view; } else { throw BusinessRuntimeException.getInstance("您还未购买该车票"); } } @Override public ClaudeUser findByUserTokenAndExpireTimeAfter(String userToken, LocalDateTime now) { return claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getUserToken, userToken).gt(ClaudeUser::getExpireTime, now)); } @Override public ConversationLimitResponse conversationLimit(String userToken, ClaudeUser user) { return isConversationAllowed(userToken, user.getLimitNum(), user.getLimitTime()); } @Override public void carLimited(String carId, String userToken, Long expTime) { redisService.set("claude:clears_in:" + carId, expTime, expTime); try { //记录用户触发限制 ClaudeUserCarUsedRecord claudeUserCarUsedRecord = new ClaudeUserCarUsedRecord(); claudeUserCarUsedRecord.setUserToken(userToken); claudeUserCarUsedRecord.setCarId(carId); String carChatKey = RedisService.key.CLAUDE_CAR_CHAT.getName() + carId; Long carChatNum = redisService.zCard(carChatKey); if (carChatNum != null) { claudeUserCarUsedRecord.setNum(Integer.parseInt(carChatNum.toString())); } claudeUserCarUsedRecordMapper.insert(claudeUserCarUsedRecord); log.info("记录userToken:{} 触发限制 carId:{}", userToken, carId); } catch (Exception e) { log.info("记录userToken:{} 触发限制 carId:{} 操作失败e:{}", userToken, carId, StringUtil.getErrorText(e)); } } @Override public ClaudeUser getClaudeUser(long userId, Long relationId) { GroupsRelation groupsRelation = getGroupsRelation(userId, relationId); GroupsTrips groupsTrips = getGroupsTrips(groupsRelation); GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId()); if (goodsDonSku != null && goodsDonSku.getGoodsId() == 75 && goodsDonSku.getIsMirror()) { return getClaudeUser(userId, relationId, groupsRelation, goodsDonSku); } else { throw BusinessRuntimeException.getInstance("服务器出了点问题"); } } @Override public void saveConversationRecord(String userToken, ConversationRequest conversationRequest) { ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getUserToken, userToken)); ClaudeUserConversationRecord claudeUserConversationRecord = new ClaudeUserConversationRecord(); claudeUserConversationRecord.setUserToken(userToken); claudeUserConversationRecord.setCarId(conversationRequest.getCarId()); if (StringUtils.isNotBlank(conversationRequest.getConversation_id())) { claudeUserConversationRecord.setConversationId(conversationRequest.getConversation_id()); } claudeUserConversationRecord.setModel(conversationRequest.getModel()); claudeUserConversationRecordMapper.insert(claudeUserConversationRecord); String key = RedisService.key.CLAUDE_CONVERSATION_LIMIT.getName() + ":" + userToken; long currentTimeMillis = System.currentTimeMillis(); if (redisService.hasKey("claude:clears_in:" + conversationRequest.getCarId())) { redisService.del("claude:clears_in:" + conversationRequest.getCarId()); } // 如果未超过限制,记录当前请求的时间戳 redisService.zAdd(key, currentTimeMillis, currentTimeMillis); // 设置ZSet的过期时间,窗口大小加上一段冗余时间 redisService.expire(key, (claudeUser.getLimitTime() * 60 * 60) + 20); if (StringUtils.isNotBlank(conversationRequest.getCarId())) { updateExperienceAndScore(conversationRequest.getCarId(), System.currentTimeMillis()); } } @Override public ClaudeUser checkCarAccount(Long userId, Long relationId) { GroupsRelation groupsRelation = getGroupsRelation(userId, relationId); GroupsTrips groupsTrips = getGroupsTrips(groupsRelation); GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId()); if (goodsDonSku != null && goodsDonSku.getGoodsId() == 75 && goodsDonSku.getIsMirror() && goodsDonSku.getIsCar()) { return getClaudeUser(userId, relationId, groupsRelation, goodsDonSku); } else { return null; } } @Override public String getCarLoginUrl(String domain, Long userId, Long relationId, String carId) { GroupsRelation groupsRelation = getGroupsRelation(userId, relationId); GroupsTrips groupsTrips = getGroupsTrips(groupsRelation); GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId()); if (goodsDonSku != null && goodsDonSku.getIsMirror() && goodsDonSku.getIsCar()) { ClaudeUser claudeUser = getClaudeUser(userId, relationId, groupsRelation, goodsDonSku); String DOMAIN = domain; if (StrUtil.isEmpty(DOMAIN)) { DOMAIN = "https://claudeplus.com.cn"; try { SysConfig sysConfig = sysConfigService.getOne(Wrappers.lambdaQuery(SysConfig.class).eq(SysConfig::getSysKey, "mirror_claude_domain")); if (sysConfig != null) { String sysValue = sysConfig.getSysValue(); if (JSONUtil.isJsonArray(sysValue)) { List domainList = JSONUtil.parseArray(sysValue).toList(String.class); DOMAIN = domainList.get(RandomUtil.randomInt(domainList.size())); } } } catch (Exception e) { log.error("随机抽取域名错误. msg:{}", StringUtil.getErrorText(e)); } } log.info("domain:{},用户:{},所在车次:{},座位id:{},在{}获取跳转Claude镜像的登录url", DOMAIN, userId, groupsTrips.getId(), relationId, DateTime.now()); return DOMAIN + "/auth/logintoken?carid=" + carId + "&usertoken=" + claudeUser.getUserToken(); } else { throw BusinessRuntimeException.getInstance("服务器出了点问题"); } } /** * 获取指定用户ID在滑动窗口内的提问次数 * * @param userToken 用户token * @return 滑动窗口内的请求次数 */ @Override public Long getConversationCount(String userToken, Long limitTime) { JSONObject conversationCount = getConversationCount(userToken); Long count = Optional.ofNullable(conversationCount.getLong("count")).orElse(0l); return new BigDecimal(count).divide(BigDecimal.valueOf(4000),0, RoundingMode.HALF_UP).longValue(); } @Override public Long carLimit(String carId) { if(redisService.hasKey("claude:clears_in:" + carId)){ return redisService.getExpire("claude:clears_in:" + carId); } return 0L; } @Override public void clearNum(String userToken) throws Exception { HttpRequest request = new HttpRequest("http://147.79.20.183:9611/claude/cleanUserTokenLimit?userToken=" + userToken); HttpResponse execute = request.method(Method.GET).setConnectionTimeout(3000).timeout(3000).execute(); String body = execute.body(); JSONObject re = Jsons.parseObject(body, JSONObject.class); if (!body.contains("清除成功")) { throw BusinessRuntimeException.getInstance(re.getStr("message")); } } @Override public void clearNumByRelationId(Long relationId) throws Exception { ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getRelationId, relationId)); HttpRequest request = new HttpRequest("http://147.79.20.183:9611/claude/cleanUserTokenLimit?userToken=" + claudeUser.getUserToken()); HttpResponse execute = request.method(Method.GET).setConnectionTimeout(3000).timeout(3000).execute(); String body = execute.body(); JSONObject re = Jsons.parseObject(body, JSONObject.class); if (!body.contains("清除成功")) { throw BusinessRuntimeException.getInstance(re.getStr("message")); } } /** * 获取车位信息 * * @param userId * @param relationId * @return */ private GroupsRelation getGroupsRelation(Long userId, Long relationId) { List userIdList = userBindRelationService.getRelationUserIdList(userId, null); GroupsRelation groupsRelation = groupsRelationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).in(GroupsRelation::getUserId, userIdList).eq(GroupsRelation::getId, relationId)); if (groupsRelation == null) { throw BusinessRuntimeException.getInstance("车票不存在"); } return groupsRelation; } /** * 获取车队信息 * * @param groupsRelation * @return */ private GroupsTrips getGroupsTrips(GroupsRelation groupsRelation) { GroupsTrips groupsTrips = groupsMapper.selectById(groupsRelation.getGroupsId()); if (groupsTrips == null) { throw BusinessRuntimeException.getInstance("车队异常"); } return groupsTrips; } private ClaudeUser getClaudeUser(Long userId, Long relationId, GroupsRelation groupsRelation, GoodsDonSku goodsDonSku) { ClaudeUser claudeUser = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class).eq(ClaudeUser::getRelationId, relationId)); User user = userMapper.selectById(userId); if (claudeUser == null) { claudeUser = createClaudeUser(groupsRelation, user, goodsDonSku); } else { updateClaudeUser(groupsRelation, user, claudeUser); } return claudeUser; } /** * 创建chatgptUser */ private synchronized ClaudeUser createClaudeUser(GroupsRelation groupsRelation, User user, GoodsDonSku goodsDonSku) { ClaudeUser claudeUser = new ClaudeUser(); if (goodsDonSku.getIsCar()) { claudeUser.setLimitNum(goodsDonSku.getGptLimitNum()); claudeUser.setLimitTime(goodsDonSku.getGptLimitTime()); } try { claudeUser.setExpireTime(groupsRelation.getExpiryTime()); claudeUser.setIsPlus(true); claudeUser.setName(user.getNickname()); claudeUser.setRelationId(groupsRelation.getId()); claudeUser.setUserToken(UUID.randomUUID().toString()); claudeUserMapper.insert(claudeUser); } catch (DuplicateKeyException e) { log.error("重复插入Claude镜像用户:{}token,errmsg:{}", user.getNickname(), StringUtil.getErrorText(e)); } return claudeUser; } private void updateClaudeUser(GroupsRelation groupsRelation, User user, ClaudeUser claudeUser) { claudeUser.setName(user.getNickname()); claudeUser.setExpireTime(groupsRelation.getExpiryTime()); claudeUserMapper.updateById(claudeUser); } /** * 检查是否允许进行进行提问 * * @param userToken 用户token * @return true 如果允许请求,false 如果请求被限制 */ public ConversationLimitResponse isConversationAllowed(String userToken, int limit, Long limitTime) { String key = RedisService.key.CLAUDE_CONVERSATION_LIMIT.getName() + ":" + userToken; long currentTimeMillis = System.currentTimeMillis(); long windowStartMillis = currentTimeMillis - (limitTime * 60 * 60) * 1000; ConversationLimitResponse conversationLimitResponse = new ConversationLimitResponse(); // 清除时间窗口之前的请求记录 redisService.zRemoveRangeByScore(key, 0, windowStartMillis); Long currentSize = redisService.zCard(key); if (currentSize != null && currentSize >= limit) { // 如果当前请求次数超过限制,则拒绝请求 Set times = redisService.zRangeByScore(key, 0, currentTimeMillis, 0, 1); Long oldestTime = (Long) times.stream().findFirst().orElse(null); if (oldestTime != null) { // 下一次可用时间是最早请求时间之后的3小时 long nextAvailableTime = oldestTime + (limitTime * 60 * 60 * 1000); conversationLimitResponse.setNextAvailableTime(nextAvailableTime); } conversationLimitResponse.setLimited(true); //用户次数使用限制 ClaudeUserCarUsedRecord claudeUserCarUsedRecord = new ClaudeUserCarUsedRecord(); claudeUserCarUsedRecord.setUserToken(userToken); claudeUserCarUsedRecord.setIsUserLimit(true); claudeUserCarUsedRecordMapper.insert(claudeUserCarUsedRecord); return conversationLimitResponse; } else { conversationLimitResponse.setLimited(false); return conversationLimitResponse; } } // 更新体验并计算评分 public void updateExperienceAndScore(String carId, long timestamp) { // 定义键名 String key = RedisService.key.CLAUDE_CAR_CHAT.getName() + carId; // 更新体验数据 redisService.zAdd(key, timestamp, String.valueOf(timestamp)); // 清理旧数据(可选)和重新计算评分(根据需要实现) cleanupOldExperiencesAndRecalculateScore(carId, timestamp); } // 清理旧数据和重新计算评分 public void cleanupOldExperiencesAndRecalculateScore(String carId, long currentTimestamp) { long threeHoursAgo = currentTimestamp - (6 * 60 * 60 * 1000); // 3小时前的时间戳 // 清理高级体验旧数据 redisService.zRemoveRangeByScore(RedisService.key.CLAUDE_CAR_CHAT.getName() + carId, 0, threeHoursAgo); // 重新计算评分 recalculateScore(carId, currentTimestamp); } // 重新计算指定车队的评分 private void recalculateScore(String carId, long currentTimestamp) { // 实际应用中,你需要根据高级体验和低级体验的数量重新计算得分 double highExperienceScore = (double) redisService.zCount(RedisService.key.CLAUDE_CAR_CHAT.getName() + carId, currentTimestamp - (6 * 60 * 60 * 1000), currentTimestamp) * 2; redisService.zAdd(RedisService.key.CLAUDE_CAR_SCORES.getName(), highExperienceScore, carId); } public JSONObject getConversationCount(String userToken) { try { String res = HttpUtil.get("http://147.79.20.183:9611/claude/getConversationCount?userToken=" + userToken); return new JSONObject(res); }catch (Exception e){ return new JSONObject("{\"count\": 0}"); } } }