瀏覽代碼

新增 claude 镜像

chenbiao 2 年之前
父節點
當前提交
e340474acc

+ 3 - 0
netflix-common/src/main/java/com/cyksj/common/constant/Constant.java

@@ -308,6 +308,9 @@ public interface Constant {
 	 */
 	String MJ_MIRROR_ONLY_ACCOUNT = "mjmirror1213";
 
+
+	String CLAUDE_MIRROR_ONLY_ACCOUNT = "claudecar123";
+
 	/**
 	 * 免登录渠道
 	 */

+ 11 - 0
netflix-dao/src/main/java/com/cyksj/mapper/ClaudeUserCarUsedRecordMapper.java

@@ -0,0 +1,11 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeUserCarUsedRecord;
+
+/**
+ * @author chan
+ * @date 2024/8/8 19:30
+ */
+public interface ClaudeUserCarUsedRecordMapper extends BaseMapper<ClaudeUserCarUsedRecord> {
+}

+ 11 - 0
netflix-dao/src/main/java/com/cyksj/mapper/ClaudeUserConversationRecordMapper.java

@@ -0,0 +1,11 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeUserConversationRecord;
+
+/**
+ * @author chan
+ * @date 2024/8/8 19:46
+ */
+public interface ClaudeUserConversationRecordMapper extends BaseMapper<ClaudeUserConversationRecord> {
+}

+ 11 - 0
netflix-dao/src/main/java/com/cyksj/mapper/ClaudeUserMapper.java

@@ -0,0 +1,11 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeUser;
+
+/**
+ * @author chan
+ * @date 2024/8/8 19:00
+ */
+public interface ClaudeUserMapper extends BaseMapper<ClaudeUser> {
+}

+ 8 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeSession.java

@@ -0,0 +1,8 @@
+package com.cyksj.model.entity;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:46
+ */
+public class ClaudeSession {
+}

+ 49 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeUser.java

@@ -0,0 +1,49 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:36
+ */
+@Data
+public class ClaudeUser extends BaseEntity {
+
+    private Date deletedAt;
+
+    private String name;
+
+    private String userToken;
+
+    private Date expireTime;
+
+    private Boolean isPlus;
+
+    private String remark;
+
+    /**
+     * 车票id
+     */
+    @TableField("relationId")
+    private Long relationId;
+
+    /**
+     * 限制次数
+     */
+    private Integer limitNum;
+
+    /**
+     * 限制时间
+     */
+    private Long limitTime;
+
+
+    /**
+     * 是否封禁
+     */
+    private Boolean isBlock;
+}

+ 32 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeUserCarUsedRecord.java

@@ -0,0 +1,32 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author chan
+ * @date 2024/8/8 19:29
+ */
+@TableName("chatgpt_user_car_used_record")
+@Getter
+@Setter
+public class ClaudeUserCarUsedRecord extends BaseEntity{
+
+
+    private String carId;
+
+    @TableField("userToken")
+    private String userToken;
+
+    /**
+     * 使用次数
+     */
+    private Integer num;
+
+    /**
+     * true用户次数限制
+     */
+    private Boolean isUserLimit;
+}

+ 42 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeUserConversationRecord.java

@@ -0,0 +1,42 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+/**
+ * @author chan
+ * @date 2024/3/18 17:10
+ */
+@Data
+@TableName("claude_user_conversation_record")
+@SearchBean(tables = "claude_user_conversation_record")
+public class ClaudeUserConversationRecord extends BaseEntity {
+
+
+    /**
+     * 凭证
+     */
+    private String userToken;
+
+    /**
+     * 会话id
+     */
+    private String conversationId;
+
+    /**
+     * 消息id
+     */
+    private String messageId;
+
+    /**
+     * 模型
+     */
+    private String model;
+
+    /**
+     * 车次id
+     */
+    private String carId;
+
+}

+ 75 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCarInfoView.java

@@ -0,0 +1,75 @@
+package com.cyksj.model.views;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.DbIgnore;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024/4/8 17:02
+ */
+@Data
+@SearchBean(
+        tables = "claude_session cs"
+)
+public class ClaudeCarInfoView {
+
+    /**
+     * 车次id
+     */
+    @DbField("cs.carId")
+    private String carId;
+
+    /**
+     * 车次名称
+     */
+    @DbField("cs.car_name")
+    private String carName;
+
+    /**
+     * 车次状态 繁忙 空闲 停运
+     */
+    @DbIgnore
+    private String status;
+
+
+    /**
+     * 得分
+     */
+    @DbField("cs.score")
+    private Double score;
+
+    /**
+     * 可用时间
+     */
+    @DbIgnore
+    private Date expTime;
+
+    /**
+     * 使用次数
+     */
+    @DbIgnore
+    private Integer use;
+
+
+    @DbField("cs.isPlus")
+    private Integer isPlus;
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ClaudeCarInfoView that = (ClaudeCarInfoView) o;
+        return Objects.equals(carId, that.carId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(carId);
+    }
+}

+ 20 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeConversionLimitView.java

@@ -0,0 +1,20 @@
+package com.cyksj.model.views;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:29
+ */
+@Data
+public class ClaudeConversionLimitView {
+
+    private Error error;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @Data
+    public static class Error{
+        private String message;
+    }
+}

+ 6 - 0
netflix-service/src/main/java/com/cyksj/redis/RedisService.java

@@ -833,6 +833,12 @@ public class RedisService {
         CHATGPT_CAR_SCORES("chatgpt:car:scores","chatGpt 车队分数", 48 * 60 * 60L),
         CHATGPT_CAR_HIGH_CHAT("chatgpt:car:high:chat:","chatGpt gpt4 对话次数", 48 * 60 * 60L),
         CHATGPT_CAR_LOW_CHAT("chatgpt:car:low:chat:","chatGpt 3.5 对话次数", 48 * 60 * 60L),
+
+        //claude
+        CLAUDE_CONVERSATION_LIMIT("claude:conversation:limit:", "claude 对话限制", 3 * 60 * 60L),
+        CLAUDE_CAR_CHAT("claude:car:chat:","claude 对话次数", 48 * 60 * 60L),
+        CLAUDE_CAR_SCORES("claude:car:scores","claude 车队分数", 48 * 60 * 60L),
+
         //车票到期前每日通知key
         TICKET_EXPIRY_NOTIFY_DAY("ticket_expiry_notify_day:%s:%s", "车票到期前每日通知key", 60 * 60 * 24L),
         MIDJOURNEY_FAST_LIMIT("midjourney:fast:limit:", "midjourney fast次数", 60 * 60 * 48L),

+ 30 - 0
netflix-service/src/main/java/com/cyksj/service/claude/ClaudeService.java

@@ -0,0 +1,30 @@
+package com.cyksj.service.claude;
+
+import com.cyksj.model.entity.ChatgptUser;
+import com.cyksj.model.entity.ClaudeUser;
+import com.cyksj.model.request.gpt.ConversationRequest;
+import com.cyksj.model.response.ConversationLimitResponse;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:34
+ */
+public interface ClaudeService {
+    ClaudeUser findByUserTokenAndExpireTimeAfter(String userToken, LocalDateTime now);
+
+    ConversationLimitResponse conversationLimit(String userToken, ClaudeUser user);
+
+    void carLimited(String carId, String finalUserToken, Long expTime);
+
+    ClaudeUser getClaudeUser(long userId, Long relationId);
+
+    void saveConversationRecord(String userToken, ConversationRequest conversationRequest);
+
+    Boolean checkCarAccount(Long userId, Long relationId);
+
+    String getCarLoginUrl(Long userId, Long relationId, String carId);
+
+    Long getConversationCount(String userToken, Long limitTime);
+}

+ 341 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeServiceImpl.java

@@ -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);
+    }
+
+
+}

+ 4 - 1
netflix-service/src/main/java/com/cyksj/service/groups/impl/GroupFunServiceImpl.java

@@ -100,13 +100,16 @@ public class GroupFunServiceImpl implements GroupsFuncService {
 		Boolean isMirror = sku.getIsMirror();
 		Boolean isCar = sku.getIsCar();
 		String account = null;
-		if ((goodsId == 18 && isCar) || (goodsId == 26 && isMirror)) {
+		if ((goodsId == 18 && isCar) || (goodsId == 26 && isMirror) || goodsId == 75 && isCar) {
 			if (goodsId == 18) {
 				account = Constant.GPT_CAR_ONLY_ACCOUNT;
 			}
 			if (goodsId == 26) {
 				account = Constant.MJ_MIRROR_ONLY_ACCOUNT;
 			}
+			if (goodsId == 75) {
+				account = Constant.CLAUDE_MIRROR_ONLY_ACCOUNT;
+			}
 			AccountView accountView = beanSearcher.searchFirst(AccountView.class, MapUtils.builder()
 					.field(AccountView::getGoodsId, goodsId)
 					.field(AccountView::getAccount, account).

+ 164 - 4
netflix-web/src/main/java/com/cyksj/web/controller/mirror/MirrorController.java

@@ -14,11 +14,9 @@ import com.cyksj.mapper.renew.RenewUpgradePackageMapper;
 import com.cyksj.model.entity.*;
 import com.cyksj.model.request.gpt.ConversationRequest;
 import com.cyksj.model.response.ConversationLimitResponse;
-import com.cyksj.model.views.ChatGptUserConversationRecordHistoryView;
-import com.cyksj.model.views.ChatGptUserView;
-import com.cyksj.model.views.ChatgptCarInfoView;
-import com.cyksj.model.views.ChatgptConversionLimitView;
+import com.cyksj.model.views.*;
 import com.cyksj.service.chatgpt.ChatGptAccountService;
+import com.cyksj.service.claude.ClaudeService;
 import com.cyksj.service.midjourney.MidjourneyAccountService;
 import com.cyksj.web.util.StpUserUtil;
 import com.ejlchina.searcher.BeanSearcher;
@@ -59,6 +57,8 @@ public class MirrorController {
 
     private final ChatGptAccountService chatGptAccountService;
 
+    private final ClaudeService claudeService;
+
     private final HttpServletResponse response;
 
     private final HttpServletRequest request;
@@ -405,4 +405,164 @@ public class MirrorController {
         }
         return GatewayResponse.SUCCESS.newBuilder().toResult();
     }
+
+
+    // ---------------------------------- claude ----------------------------------
+
+    /**
+     * Claude车队跳转登录
+     *
+     * @param carId 车队id
+     */
+    @GetMapping("/claudeMirror/{relationId}/{carId}")
+    public Result<String> claudeCarLogin(@PathVariable Long relationId, @PathVariable String carId) throws IOException {
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        claudeService.checkCarAccount(userId, relationId);
+        String url = claudeService.getCarLoginUrl(userId, relationId, carId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(url);
+    }
+
+    /**
+     * Claude车票跳转登录
+     *
+     * @param userToken 用户token
+     * @param carId     车票id
+     * @return 登陆结果
+     */
+    @RequestMapping("/claude/oauth")
+    public Result<String> claudeOauth(@RequestParam("usertoken") String userToken, @RequestParam("carid") String carId) {
+        try {
+            log.info("用户通过userToken:{}访问claude镜像车队carId:{}", userToken, carId);
+            ClaudeUser user = claudeService.findByUserTokenAndExpireTimeAfter(userToken, LocalDateTime.now());
+            if (user == null) {
+                return GatewayResponse.SUCCESS.newBuilder().setMsg("用户不存在或已过期").setCode(0).toResult();
+            }
+
+            return GatewayResponse.SUCCESS.newBuilder().setMsg("登陆成功").setCode(1).toResult(StringUtil.getString(user.getRelationId()));
+        } catch (Exception e) {
+            log.error("服务器错误", e);
+            return GatewayResponse.FAIL.newBuilder().setMsg("服务器错误").setCode(0).toResult();
+        }
+    }
+
+    /**
+     *  Claude会话限制
+     */
+    @RequestMapping("/claude/conversation/limit")
+    public ClaudeConversionLimitView claudeConversationLimit() {
+        //1.从request获取请求头Authorization 并取 值内 'Bearer ' 后的值为usertoken
+        String authorization = request.getHeader("Authorization");
+        String carid = request.getHeader("Carid");
+        String userToken = authorization.substring(7);
+        //2.根据usertoken 查询 ClaudeUser  表中的记录
+        ClaudeUser user = claudeService.findByUserTokenAndExpireTimeAfter(userToken, LocalDateTime.now());
+        //3.如果记录不存在,返回状态码 401
+        if (user == null) {
+            response.setStatus(400);
+            ClaudeConversionLimitView claudeConversionLimitView = new ClaudeConversionLimitView();
+            ClaudeConversionLimitView.Error error = new ClaudeConversionLimitView.Error();
+            error.setMessage("看来您还没车票呢,是不是还没登录呀,赶紧去车票内重新选择上车 \uD83D\uDE97 [点击直达](https://nf.video/ticket)");
+            claudeConversionLimitView.setError(error);
+            return claudeConversionLimitView;
+        }else {
+            if (user.getIsBlock() != null && user.getIsBlock()) {
+                response.setStatus(400);
+                ClaudeConversionLimitView chatgptConversionLimitView = new ClaudeConversionLimitView();
+                ClaudeConversionLimitView.Error error = new ClaudeConversionLimitView.Error();
+                error.setMessage("抱歉,您因违反银河录像局使用条款已被封禁.");
+                chatgptConversionLimitView.setError(error);
+                return chatgptConversionLimitView;
+            }
+
+                ConversationLimitResponse conversationLimitResponse = claudeService.conversationLimit(userToken, user);
+
+                if (conversationLimitResponse.isLimited()) {
+                    response.setStatus(429);
+                    ClaudeConversionLimitView claudeConversionLimitView = new ClaudeConversionLimitView();
+                    ClaudeConversionLimitView.Error error = new ClaudeConversionLimitView.Error();
+                    //String url = "[点击此处升级](https://nf.video/ticket?relationId=%d&pType=pay)";
+                    //
+                    //String format = String.format(url, user.getRelationId());
+                    error.setMessage("您目前的套餐已达到使用上限,预计 " + DateUtil.format(new Date(conversationLimitResponse.getNextAvailableTime()), "yyyy-MM-dd HH:mm:ss") + " 恢复");
+                    claudeConversionLimitView.setError(error);
+                    return claudeConversionLimitView;
+                } else {
+                    response.setStatus(200);
+                }
+        }
+
+        return new ClaudeConversionLimitView();
+    }
+
+
+    /**
+     *  Claude触发车队限制
+     */
+    @GetMapping("/claude/car/limited")
+    public Result<String> claudeCarLimited(String carId, Long expTime) {
+        String authorization = request.getHeader("Authorization");
+        String userToken = "";
+        if(StringUtils.isNotBlank(authorization)){
+            try {
+                userToken = authorization.substring(7);
+            }catch (Exception e){
+                log.error("获取用户token失败",e);
+                return GatewayResponse.SUCCESS.newBuilder().toResult();
+            }
+        }
+
+        String finalUserToken = userToken;
+        TASK_EXECUTOR.execute(()->{
+            claudeService.carLimited(carId, finalUserToken, expTime);
+        });
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+
+    /**
+     *  Claude获取车队列表
+     *
+     * @param relationId
+     * @return
+     */
+    @GetMapping("/claude/cars")
+    public Result<SearchResult<ClaudeCarInfoView>> claudeCarInfoList(Long relationId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        //查看用户是否存在车票
+        ClaudeUser claudeUser = claudeService.getClaudeUser(userId, relationId);
+        if (claudeUser == null) {
+            throw BusinessRuntimeException.getInstance("您还未购买车票");
+        }
+        SearchResult<ClaudeCarInfoView> carInfoList = beanSearcher.search(ClaudeCarInfoView.class, MapUtils.flatBuilder(request.getParameterMap()).field(ClaudeCarInfoView::getStatus, true).build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(carInfoList);
+    }
+
+
+    /**
+     *  Claude对话成功后记录次数
+     */
+    @RequestMapping("/claude/conversation/notifyUrl")
+    public void claudeConversationNotifyUrl() {
+        //1.从request获取请求头Authorization 并取 值内 'Bearer ' 后的值为usertoken
+        String authorization = request.getHeader("Authorization");
+        String carid = request.getHeader("Carid");
+        String model = request.getHeader("Model");
+        String conversation_id = request.getHeader("Convid");
+        ConversationRequest conversationRequest = new ConversationRequest();
+        conversationRequest.setCarId(carid);
+        conversationRequest.setModel(model);
+        conversationRequest.setConversation_id(conversation_id);
+        String userToken = authorization.substring(7);
+        //2.根据usertoken 查询 chatgpt_user 表中的记录
+        ClaudeUser user = claudeService.findByUserTokenAndExpireTimeAfter(userToken, LocalDateTime.now());
+        //3.如果记录不存在,返回状态码 401
+        if (user == null) {
+            return;
+        }
+        if(StringUtils.isNotBlank(conversationRequest.getCarId())){
+            TASK_EXECUTOR.execute(() -> {
+                claudeService.saveConversationRecord(userToken, conversationRequest);
+            });
+        }
+    }
 }