Procházet zdrojové kódy

新增 claude 镜像

chenbiao před 2 roky
rodič
revize
2c2908ccbe
27 změnil soubory, kde provedl 1204 přidání a 28 odebrání
  1. 3 0
      netflix-common/src/main/java/com/cyksj/common/constant/Constant.java
  2. 11 0
      netflix-dao/src/main/java/com/cyksj/mapper/ClaudeSessionMapper.java
  3. 11 0
      netflix-dao/src/main/java/com/cyksj/mapper/ClaudeUserCarUsedRecordMapper.java
  4. 11 0
      netflix-dao/src/main/java/com/cyksj/mapper/ClaudeUserConversationRecordMapper.java
  5. 11 0
      netflix-dao/src/main/java/com/cyksj/mapper/ClaudeUserMapper.java
  6. 43 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeSession.java
  7. 52 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeUser.java
  8. 32 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeUserCarUsedRecord.java
  9. 42 0
      netflix-dao/src/main/java/com/cyksj/model/entity/ClaudeUserConversationRecord.java
  10. 6 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java
  11. 5 2
      netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java
  12. 75 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCarInfoView.java
  13. 20 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeConversionLimitView.java
  14. 41 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeUserView.java
  15. 2 0
      netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuFrontView.java
  16. 6 0
      netflix-dao/src/main/java/com/cyksj/model/views/RenewalView.java
  17. 3 0
      netflix-dao/src/main/java/com/cyksj/model/views/YhShopGoodsSkuFrontView.java
  18. 6 0
      netflix-service/src/main/java/com/cyksj/redis/RedisService.java
  19. 34 0
      netflix-service/src/main/java/com/cyksj/service/claude/ClaudeService.java
  20. 12 0
      netflix-service/src/main/java/com/cyksj/service/claude/ClaudeSessionService.java
  21. 370 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeServiceImpl.java
  22. 67 0
      netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeSessionServiceImpl.java
  23. 4 1
      netflix-service/src/main/java/com/cyksj/service/groups/impl/GroupFunServiceImpl.java
  24. 11 0
      netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java
  25. 35 18
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java
  26. 105 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/mirror/CmsClaudeController.java
  27. 186 7
      netflix-web/src/main/java/com/cyksj/web/controller/mirror/MirrorController.java

+ 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/ClaudeSessionMapper.java

@@ -0,0 +1,11 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.ClaudeSession;
+
+/**
+ * @author chan
+ * @date 2024/8/9 10:47
+ */
+public interface ClaudeSessionMapper extends BaseMapper<ClaudeSession> {
+}

+ 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> {
+}

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

@@ -0,0 +1,43 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:46
+ */
+@Data
+@SearchBean(tables = "claude_session cs")
+public class ClaudeSession extends BaseEntity {
+
+    @TableField("deleted_at")
+    @DbField("deleted_at")
+    private Date deletedAt;
+
+    private String email;
+
+    private String password;
+
+    private Boolean status;
+
+    private Boolean isPlus;
+
+    @DbField("carID")
+    @TableField("carID")
+    private String carID;
+
+    @DbField("officialSession")
+    @TableField("officialSession")
+    private String officialSession;
+
+    private String remark;
+
+    private Long sort;
+
+    private double score;
+}

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

@@ -0,0 +1,52 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbField;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:36
+ */
+@Data
+public class ClaudeUser extends BaseEntity {
+
+
+    private String name;
+
+    @DbField("userToken")
+    @TableField("userToken")
+    private String userToken;
+
+    @TableField("expireTime")
+    private Date expireTime;
+
+    @TableField("isPlus")
+    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;
+
+}

+ 6 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java

@@ -231,4 +231,10 @@ public class GoodsDonSku extends BaseEntity {
      */
     @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Integer sorted;
+
+    /**
+     * 备注
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private String remark;
 }

+ 5 - 2
netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java

@@ -1,7 +1,5 @@
 package com.cyksj.model.manage.request;
 
-import com.baomidou.mybatisplus.annotation.FieldStrategy;
-import com.baomidou.mybatisplus.annotation.TableField;
 import com.cyksj.model.entity.GoodsDiscountPlusPurchaseRelation;
 import lombok.Getter;
 import lombok.Setter;
@@ -163,6 +161,11 @@ public class ReqGoodsSkuInsert {
          * mj relax次数
          */
         private Integer mjRelaxNum;
+
+        /**
+         * 备注
+         */
+        private String remark;
     }
 
 }

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

+ 41 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeUserView.java

@@ -0,0 +1,41 @@
+package com.cyksj.model.views;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author chan
+ * @date 2024/8/9 12:01
+ */
+@Data
+@Builder
+public class ClaudeUserView {
+
+    /**
+     * 规格名称
+     */
+    private String skuName;
+
+    /**
+     * 过期时间
+     */
+    private Date expireTime;
+
+    /**
+     * 限制时间
+     */
+    private Long limitTime;
+
+    /**
+     * 限制次数
+     */
+    private Integer limitNum;
+
+    /**
+     * 使用次数
+     */
+    private Long use;
+}

+ 2 - 0
netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuFrontView.java

@@ -154,4 +154,6 @@ public class GoodsDonSkuFrontView {
 	private Boolean isCar;
 
 	private Boolean isMirror;
+
+	private String remark;
 }

+ 6 - 0
netflix-dao/src/main/java/com/cyksj/model/views/RenewalView.java

@@ -204,6 +204,12 @@ public class RenewalView {
     @DbField("sku.mj_relax_num")
     private Integer mjRelaxNum;
 
+    /**
+     * 镜像限制次数
+     */
+    @DbField("sku.gpt_limit_num")
+    private Integer gptLimitNum;
+
     /**
      * mj 套餐包最低单价
      */

+ 3 - 0
netflix-dao/src/main/java/com/cyksj/model/views/YhShopGoodsSkuFrontView.java

@@ -142,4 +142,7 @@ public class YhShopGoodsSkuFrontView {
 
 	@DbField("sku.is_mirror")
 	private Boolean isMirror;
+
+	@DbField("sku.remark")
+	private Boolean remark;
 }

+ 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),

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

@@ -0,0 +1,34 @@
+package com.cyksj.service.claude;
+
+import com.cyksj.model.entity.ClaudeUser;
+import com.cyksj.model.request.gpt.ConversationRequest;
+import com.cyksj.model.response.ConversationLimitResponse;
+import com.cyksj.model.views.ClaudeUserView;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author chan
+ * @date 2024/8/8 18:34
+ */
+public interface ClaudeService {
+    ClaudeUserView getUserInfo(long userId, Long relationId);
+
+    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);
+
+    ClaudeUser checkCarAccount(Long userId, Long relationId);
+
+    String getCarLoginUrl(Long userId, Long relationId, String carId);
+
+    Long getConversationCount(String userToken, Long limitTime);
+
+    Long carLimit(String carId);
+}

+ 12 - 0
netflix-service/src/main/java/com/cyksj/service/claude/ClaudeSessionService.java

@@ -0,0 +1,12 @@
+package com.cyksj.service.claude;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.ClaudeSession;
+
+/**
+ * @author chan
+ * @date 2024/8/9 10:46
+ */
+public interface ClaudeSessionService extends IService<ClaudeSession>  {
+    void getSession(String email, String password, String officialSession, Boolean isPlus);
+}

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

@@ -0,0 +1,370 @@
+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.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.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 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());
+
+            return ClaudeUserView.builder()
+                    .skuName(goodsDonSku.getSubTitle())
+                    .expireTime(claudeUser.getExpireTime())
+                    .limitNum(claudeUser.getLimitNum())
+                    .limitTime(claudeUser.getLimitTime())
+                    .use(Math.min(claudeUser.getLimitNum(), getConversationCount(claudeUser.getUserToken(), claudeUser.getLimitTime())))
+                    .build();
+        } 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(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;
+    }
+
+    @Override
+    public Long carLimit(String carId) {
+        if(redisService.hasKey("claude:clears_in:" + carId)){
+            return redisService.getExpire("claude:clears_in:" + carId);
+        }
+        return 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.CLAUDE_CAR_SCORES.getName(), highExperienceScore, carId);
+    }
+
+
+}

+ 67 - 0
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeSessionServiceImpl.java

@@ -0,0 +1,67 @@
+package com.cyksj.service.claude.impl;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.Method;
+import cn.hutool.json.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.mapper.ClaudeSessionMapper;
+import com.cyksj.model.entity.ClaudeSession;
+import com.cyksj.service.claude.ClaudeSessionService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chan
+ * @date 2024/8/9 10:46
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ClaudeSessionServiceImpl extends ServiceImpl<ClaudeSessionMapper, ClaudeSession> implements ClaudeSessionService {
+
+
+    private static final String MIRROR_CLAUDE_DOMAIN = "https://claudeplus.com.cn";
+
+    @Override
+    public void getSession(String email, String password, String officialSession, Boolean isPlus) {
+        ClaudeSession one = this.getOne(Wrappers.lambdaQuery(ClaudeSession.class).orderByDesc(ClaudeSession::getId).last(" limit 1"));
+        if (isPlus == null) {
+            isPlus = true;
+        }
+        try {
+            JSONObject loginResult = getLoginResult(email, password, officialSession, isPlus ? "Claude-PRO-" + one.getId() + 1 : "Claude-" + one.getId() + 1);
+
+            //{
+            //    "code": 1000,
+            //        "message": "BaseResMessage",
+            //        "data": {
+            //    "id": 1
+            //}
+            //}
+
+            if (!loginResult.getInt("code").equals(1000)) {
+                log.error("新增claude 镜像账号异常,res:{}", loginResult);
+            }
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("登录获取token错误 error:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取登录结果
+     */
+    private JSONObject getLoginResult(String email, String password, String officialSession, String carId) throws Exception {
+        HttpRequest request = new HttpRequest(MIRROR_CLAUDE_DOMAIN + "/adminapi/chatgpt/session/add");
+        request.form("email", email);
+        request.form("password", password);
+        request.form("officialSession", officialSession);
+        request.form("carID", carId);
+        HttpResponse execute = request.method(Method.POST).execute();
+        return new JSONObject(execute.body());
+    }
+
+}

+ 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).

+ 11 - 0
netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java

@@ -43,6 +43,8 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 
 	private final GroupsMapper groupsMapper;
 
+	private final ClaudeUserMapper claudeUserMapper;
+
 	@Override
 	public void delMirrorUser(Long goodsId, Long relationId) {
 		//gpt镜像
@@ -64,6 +66,15 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 				redisService.del(RedisService.key.MIDJOURNEY_USER.getName() + user.getUserToken());
 			}
 		}
+		//clause pro
+		if (goodsId != null && goodsId == 75) {
+			ClaudeUser user = claudeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeUser.class)
+					.eq(ClaudeUser::getRelationId, relationId).last("limit 1"));
+			if (user != null) {
+				claudeUserMapper.delete(Wrappers.lambdaQuery(ClaudeUser.class)
+						.eq(ClaudeUser::getRelationId, relationId));
+			}
+		}
 	}
 
 	@Override

+ 35 - 18
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -34,6 +34,7 @@ import com.cyksj.model.views.*;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.ChatGptAccountService;
 import com.cyksj.service.chatgpt.ChatGptAuthService;
+import com.cyksj.service.claude.ClaudeService;
 import com.cyksj.service.groups.GroupsFuncService;
 import com.cyksj.service.mail.CuiQiuMailService;
 import com.cyksj.service.mail.MailService;
@@ -142,7 +143,10 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private final RenewUpgradePackageMapper renewUpgradePackageMapper;
 
+	private final ClaudeService claudeService;
+
 	@Override
+
 	public SearchResult<RenewalView> getMyTicket(long userId, Boolean isLoginPopularize, String customId, String account) {
 		User u = userMapper.selectById(userId);
 		if (u == null) throw BusinessRuntimeException.getGatewayApiCode(GatewayApiCode.CMS_TOKEN_VALID);
@@ -259,9 +263,9 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 				}
 				ticket.setPayTime(payTime);
 			}
-			if (ticket.getGoodsId() == 26 && ticket.getIsMirror()){
+			if (ticket.getGoodsId() == 26 && ticket.getIsMirror()) {
 				List<UpgradePackage> upgradePackages = upgradePackageMapper.selectList(Wrappers.lambdaQuery(UpgradePackage.class).apply("JSON_CONTAINS(sku_ids,'\"" + ticket.getSkuId() + "\"')").eq(UpgradePackage::getStatus, Boolean.TRUE).eq(UpgradePackage::getDeleted, Boolean.TRUE));
-				if (CollectionUtil.isNotEmpty(upgradePackages)){
+				if (CollectionUtil.isNotEmpty(upgradePackages)) {
 					upgradePackages.stream().map(e -> e.getPrice().divide(BigDecimal.valueOf(e.getNum()), 2, RoundingMode.HALF_UP)).min(BigDecimal::compareTo).ifPresent(ticket::setMjPackagePrice);
 				}
 				try {
@@ -281,23 +285,36 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 				}
 			}
 			if (ticket.getGoodsId() == 18 && ticket.getIsCar() && ticket.getExpiryTime() != null) {
-				//gpt用户镜像
-				ChatGptUserView userInfo = chatGptAccountService.getUserInfo(userId, ticket.getRelationId());
-				ticket.setGptUserLimitNum(userInfo.getLimitNum());
-				ticket.setGptUserLimitTime(userInfo.getLimitTime());
-				ticket.setUse(userInfo.getUse());
-				Integer count = upgradePackageMapper.selectCount(Wrappers.lambdaQuery(UpgradePackage.class).apply("JSON_CONTAINS(sku_ids,'\"" + ticket.getSkuId() + "\"')")
-						.gt(UpgradePackage::getNum, userInfo.getLimitNum())
-						.eq(UpgradePackage::getStatus, Boolean.TRUE)
-						.eq(UpgradePackage::getGptType, 0)
-						.eq(UpgradePackage::getDeleted, Boolean.TRUE));
-				ticket.setUpgrade(count > 0 ? true : false);
-				ticket.setPackageId(userInfo.getPackageId());
-				if (userInfo.getPackageId() != 0) {
-					UpgradePackage upgradePackage = upgradePackageMapper.selectById(userInfo.getPackageId());
-					ticket.setPackagePrice(upgradePackage.getPrice());
+				try {
+					//gpt用户镜像
+					ChatGptUserView userInfo = chatGptAccountService.getUserInfo(userId, ticket.getRelationId());
+					ticket.setGptUserLimitNum(userInfo.getLimitNum());
+					ticket.setGptUserLimitTime(userInfo.getLimitTime());
+					ticket.setUse(userInfo.getUse());
+					Integer count = upgradePackageMapper.selectCount(Wrappers.lambdaQuery(UpgradePackage.class).apply("JSON_CONTAINS(sku_ids,'\"" + ticket.getSkuId() + "\"')")
+							.gt(UpgradePackage::getNum, userInfo.getLimitNum())
+							.eq(UpgradePackage::getStatus, Boolean.TRUE)
+							.eq(UpgradePackage::getGptType, 0)
+							.eq(UpgradePackage::getDeleted, Boolean.TRUE));
+					ticket.setUpgrade(count > 0 ? true : false);
+					ticket.setPackageId(userInfo.getPackageId());
+					if (userInfo.getPackageId() != 0) {
+						UpgradePackage upgradePackage = upgradePackageMapper.selectById(userInfo.getPackageId());
+						ticket.setPackagePrice(upgradePackage.getPrice());
+					}
+					ticket.setIsRenewPackage(userInfo.getIsRenewPackage());
+				} catch (Exception e) {
+				}
+			}
+
+			if (ticket.getGoodsId() == 75l && ticket.getExpiryTime() != null) {
+				try {
+					ClaudeUserView userInfo = claudeService.getUserInfo(userId, ticket.getRelationId());
+					ticket.setGptUserLimitNum(userInfo.getLimitNum());
+					ticket.setGptUserLimitTime(userInfo.getLimitTime());
+					ticket.setUse(userInfo.getUse());
+				} catch (Exception e) {
 				}
-				ticket.setIsRenewPackage(userInfo.getIsRenewPackage());
 			}
 			List<GroupsRelationUserView> relationUserViews = groupsRelationMapper.getUserRelationUserViews(ticket.getGroupId(), ticket.getRelationId(), ticket.getUserId(), List.of(GroupsRelation.Status.validity, GroupsRelation.Status.outside));
 			ticket.setRelationViews(relationUserViews);

+ 105 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/mirror/CmsClaudeController.java

@@ -0,0 +1,105 @@
+package com.cyksj.web.controller.manage.mirror;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.ClaudeSession;
+import com.cyksj.model.views.ChatgptCarSessionView;
+import com.cyksj.redis.RedisService;
+import com.cyksj.service.claude.ClaudeSessionService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Optional;
+
+/**
+ * @author chan
+ * @date 2024/8/9 10:02
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/manage/mirror/claude")
+public class CmsClaudeController {
+
+    private final BeanSearcher beanSearcher;
+
+    private final ClaudeSessionService claudeSessionService;
+
+    private final RedisService redisService;
+
+    private final HttpServletRequest request;
+
+    /**
+     * 获取镜像车队账号列表
+     */
+    @GetMapping("/get")
+    public Result<SearchResult<ChatgptCarSessionView>> getMirrorCarSession() {
+        SearchResult<ChatgptCarSessionView> search = beanSearcher.search(ChatgptCarSessionView.class, MapUtils.flatBuilder(request.getParameterMap())
+                .orderBy(ChatgptCarSessionView::getId).desc()
+                .build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+    }
+
+    /**
+     * 编辑镜像车队账号列表
+     */
+    @PutMapping("/update")
+    public Result<String> updateMirrorCarSession(@RequestBody ClaudeSession claudeSession) {
+        Long id = claudeSession.getId();
+        ClaudeSession dbChatSession = claudeSessionService.getById(id);
+        if (!ObjectUtil.equal(dbChatSession.getEmail(), claudeSession.getEmail()) || !ObjectUtil.equal(dbChatSession.getPassword(), claudeSession.getPassword())) {
+            claudeSessionService.getSession(dbChatSession.getEmail(), dbChatSession.getPassword(), claudeSession.getOfficialSession(), claudeSession.getIsPlus());
+        }
+        Integer selectCount = claudeSessionService.getBaseMapper().selectCount(Wrappers.lambdaQuery(ClaudeSession.class)
+                .and(e -> e.eq(ClaudeSession::getEmail, claudeSession.getEmail())
+                        .or().eq(ClaudeSession::getCarID, claudeSession.getCarID()))
+                .ne(ClaudeSession::getId, claudeSession.getId()));
+        if (selectCount > 0) {
+            throw BusinessRuntimeException.getInstance("请检验车队ID或账号是否已存在");
+        }
+        claudeSessionService.updateById(claudeSession);
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+    /**
+     * 删除镜像车队账号
+     */
+    @DeleteMapping("/delete/{id}")
+    public Result<String> deleteMirrorCarSession(@PathVariable Long id) {
+        Optional.ofNullable(claudeSessionService.getById(id))
+                .ifPresent(e -> {
+                    String carId = e.getCarID();
+                    if (StrUtil.isNotBlank(carId)) {
+                        claudeSessionService.removeById(e.getId());
+                        String highCarChatKey = RedisService.key.CLAUDE_CAR_CHAT.getName() + carId;
+                        redisService.del(highCarChatKey);
+                    }
+                });
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+    /**
+     * 新增镜像车队账号
+     */
+    @PostMapping("/post/mirror/car/session")
+    public Result<String> postMirrorCarSession(@RequestBody ClaudeSession claudeSession) {
+        Integer selectCount = claudeSessionService.getBaseMapper().selectCount(Wrappers.lambdaQuery(ClaudeSession.class)
+                .eq(ClaudeSession::getCarID, claudeSession.getCarID()).or().eq(ClaudeSession::getEmail, claudeSession.getEmail()));
+        if (selectCount > 0) {
+            throw BusinessRuntimeException.getInstance("请检验车队ID或账号是否已存在");
+        }
+       claudeSessionService.getSession(claudeSession.getEmail(), claudeSession.getPassword(), claudeSession.getOfficialSession(),claudeSession.getIsPlus());
+
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+}

+ 186 - 7
netflix-web/src/main/java/com/cyksj/web/controller/mirror/MirrorController.java

@@ -1,5 +1,6 @@
 package com.cyksj.web.controller.mirror;
 
+import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.EnvCommonService;
@@ -14,11 +15,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;
@@ -34,17 +33,15 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.swing.text.html.Option;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
-import java.util.Optional;
 
 /**
- * /server/镜像服务
+ * /server/镜像服务/claude
  *
  * @author chan
  * @date 2024/3/19 14:41
@@ -59,6 +56,8 @@ public class MirrorController {
 
     private final ChatGptAccountService chatGptAccountService;
 
+    private final ClaudeService claudeService;
+
     private final HttpServletResponse response;
 
     private final HttpServletRequest request;
@@ -405,4 +404,184 @@ public class MirrorController {
         }
         return GatewayResponse.SUCCESS.newBuilder().toResult();
     }
+
+
+    // ---------------------------------- claude ----------------------------------
+
+
+    /**
+     * 获取claude车票信息
+     */
+    @GetMapping("/claude/userInfo")
+    public Result<ClaudeUserView> getClaudeUserInfo(Long relationId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ClaudeUserView user = claudeService.getUserInfo(userId, relationId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(user);
+    }
+
+    /**
+     * 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());
+        carInfoList.getDataList().forEach((claudeCarInfoView)->{
+            Long aLong = claudeService.carLimit(claudeCarInfoView.getCarId());
+            if (aLong != 0L) {
+                claudeCarInfoView.setStatus("停运");
+                claudeCarInfoView.setExpTime(new DateTime(System.currentTimeMillis() + aLong * 1000));
+            }else {
+                claudeCarInfoView.setStatus(claudeCarInfoView.getScore() > 50 ? "繁忙":"空闲");
+            }
+        });
+        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);
+            });
+        }
+    }
 }