|
|
@@ -0,0 +1,341 @@
|
|
|
+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.json.JSONUtil;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
+import com.cyksj.common.exception.BusinessRuntimeException;
|
|
|
+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.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.time.LocalDateTime;
|
|
|
+import java.util.List;
|
|
|
+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 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.CHATGPT_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 Boolean 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()) {
|
|
|
+ ClaudeUser claudeUser = getClaudeUser(userId, relationId, groupsRelation, goodsDonSku);
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getCarLoginUrl(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 = "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<String> 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) {
|
|
|
+ String key = RedisService.key.CLAUDE_CONVERSATION_LIMIT.getName() + ":" + userToken;
|
|
|
+ long currentTimeMillis = System.currentTimeMillis();
|
|
|
+ long windowStartMillis = currentTimeMillis - (limitTime * 60 * 60) * 1000;
|
|
|
+
|
|
|
+ // 清除时间窗口之前的请求记录(可选,根据需要决定是否在此处清理过期记录)
|
|
|
+ redisService.zRemoveRangeByScore(key, 0, windowStartMillis);
|
|
|
+
|
|
|
+ // 获取当前窗口内的请求次数
|
|
|
+ Long currentSize = redisService.zCard(key);
|
|
|
+ return currentSize != null ? currentSize : 0L;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取车位信息
|
|
|
+ *
|
|
|
+ * @param userId
|
|
|
+ * @param relationId
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private GroupsRelation getGroupsRelation(Long userId, Long relationId) {
|
|
|
+ List<Long> 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<Object> 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 - (3 * 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 - (3 * 60 * 60 * 1000), currentTimestamp) * 2;
|
|
|
+ redisService.zAdd(RedisService.key.CHATGPT_CAR_SCORES.getName(), highExperienceScore, carId);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|