Parcourir la source

Merge branch 'gemini_mirror'

chenbiao il y a 7 mois
Parent
commit
751b65aca2

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

@@ -428,6 +428,8 @@ public interface Constant {
 
 	Long GROK_GOODS_ID = 88l;
 
+	Long GEMINI_GOODS_ID = 101l;
+
 	Long HBO_GO_GOODS_ID = 8L;
 
 	/**

+ 15 - 0
netflix-dao/src/main/java/com/cyksj/mapper/GeminiUserMapper.java

@@ -0,0 +1,15 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.GeminiUser;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * Gemini用户Mapper
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Mapper
+public interface GeminiUserMapper extends BaseMapper<GeminiUser> {
+}

+ 59 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GeminiUser.java

@@ -0,0 +1,59 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ejlchina.searcher.bean.DbField;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * Gemini镜像用户表
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+@TableName("gemini_user")
+public class GeminiUser 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;
+
+    /**
+     * 是否是VIP
+     */
+    private Boolean isVip;
+}

+ 33 - 0
netflix-dao/src/main/java/com/cyksj/model/request/GeminiOauthReq.java

@@ -0,0 +1,33 @@
+package com.cyksj.model.request;
+
+import lombok.Data;
+
+/**
+ * Gemini OAuth请求
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiOauthReq {
+
+    /**
+     * 用户Token
+     */
+    private String usertoken;
+
+    /**
+     * 车队ID
+     */
+    private String carid;
+
+    /**
+     * 节点类型
+     */
+    private String nodeType;
+
+    /**
+     * 套餐类型
+     */
+    private String planType;
+}

+ 38 - 0
netflix-dao/src/main/java/com/cyksj/model/response/GeminiCarAccount.java

@@ -0,0 +1,38 @@
+package com.cyksj.model.response;
+
+import lombok.Data;
+
+/**
+ * Gemini车队账号信息
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiCarAccount {
+
+    /**
+     * 账号ID
+     */
+    private Integer id;
+
+    /**
+     * 账号名称
+     */
+    private String name;
+
+    /**
+     * 显示名称
+     */
+    private String nickname;
+
+    /**
+     * 账号类型: pro/free/ultra
+     */
+    private String type;
+
+    /**
+     * 是否禁用
+     */
+    private Boolean disabled;
+}

+ 35 - 0
netflix-dao/src/main/java/com/cyksj/model/response/GeminiCarPageData.java

@@ -0,0 +1,35 @@
+package com.cyksj.model.response;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * Gemini车队分页数据
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiCarPageData {
+
+    /**
+     * 账号列表
+     */
+    private List<GeminiCarAccount> accounts;
+
+    /**
+     * 总数
+     */
+    private Integer total;
+
+    /**
+     * 页码
+     */
+    private Integer page;
+
+    /**
+     * 每页数量
+     */
+    private Integer size;
+}

+ 33 - 0
netflix-dao/src/main/java/com/cyksj/model/response/GeminiCarPageResponse.java

@@ -0,0 +1,33 @@
+package com.cyksj.model.response;
+
+import lombok.Data;
+
+/**
+ * Gemini车队分页响应
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiCarPageResponse {
+
+    /**
+     * 响应码
+     */
+    private Integer code;
+
+    /**
+     * 响应消息
+     */
+    private String messages;
+
+    /**
+     * 数据
+     */
+    private GeminiCarPageData data;
+
+    /**
+     * 公告内容
+     */
+    private String notice;
+}

+ 41 - 0
netflix-dao/src/main/java/com/cyksj/model/response/GeminiCarStatusResponse.java

@@ -0,0 +1,41 @@
+package com.cyksj.model.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * Gemini车队状态响应
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiCarStatusResponse {
+
+    /**
+     * 是否为付费账号
+     */
+    private Boolean isPlus;
+
+    /**
+     * 账号是否可用
+     */
+    private Boolean accountReady;
+
+    /**
+     * 当前使用次数
+     */
+    private Integer count;
+
+    /**
+     * 个人限额重置剩余秒数
+     */
+    @JsonProperty("clears_in")
+    private Integer clearsIn;
+
+    /**
+     * 团队限额重置剩余秒数
+     */
+    @JsonProperty("team_clears_in")
+    private Integer teamClearsIn;
+}

+ 38 - 0
netflix-dao/src/main/java/com/cyksj/model/response/GeminiConversationCountResponse.java

@@ -0,0 +1,38 @@
+package com.cyksj.model.response;
+
+import lombok.Data;
+
+/**
+ * Gemini 对话次数响应
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiConversationCountResponse {
+
+    /**
+     * 已使用次数
+     */
+    private Long count;
+
+    /**
+     * 总限制次数
+     */
+    private Integer limit;
+
+    /**
+     * 剩余可用次数
+     */
+    private Long remaining;
+
+    /**
+     * 时间窗口(小时)
+     */
+    private Long time;
+
+    /**
+     * 是否VIP用户
+     */
+    private Boolean isVip;
+}

+ 65 - 0
netflix-dao/src/main/java/com/cyksj/model/views/GeminiCarInfoView.java

@@ -0,0 +1,65 @@
+package com.cyksj.model.views;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * Gemini车队信息视图
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+public class GeminiCarInfoView {
+
+    /**
+     * 车次id
+     */
+    private String carId;
+
+    /**
+     * 车次名称
+     */
+    private String carName;
+
+    /**
+     * 车次状态 繁忙 空闲 停运
+     */
+    private String status;
+
+    /**
+     * 得分
+     */
+    private Double score;
+
+    /**
+     * 可用时间
+     */
+    private Date expTime;
+
+    /**
+     * 使用次数
+     */
+    private Integer use;
+
+    /**
+     * 账号类型: 3=ultra, 2=pro, 1=free
+     */
+    private Integer isPlus;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 限额重置剩余秒数
+     */
+    private Long clearsIn;
+
+    /**
+     * 账号是否可用
+     */
+    private Boolean accountReady;
+}

+ 43 - 0
netflix-dao/src/main/java/com/cyksj/model/views/GeminiUserView.java

@@ -0,0 +1,43 @@
+package com.cyksj.model.views;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * Gemini用户视图
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Data
+@Builder
+public class GeminiUserView {
+
+    /**
+     * 规格名称
+     */
+    private String skuName;
+
+    /**
+     *  用户token
+     */
+    private String userToken;
+
+    /**
+     * 过期时间
+     */
+    private Date expireTime;
+
+    /**
+     * 限制时间
+     */
+    private Long limitTime;
+
+    /**
+     * 限制次数
+     */
+    private Integer limitNum;
+
+}

+ 81 - 0
netflix-service/src/main/java/com/cyksj/service/gemini/GeminiService.java

@@ -0,0 +1,81 @@
+package com.cyksj.service.gemini;
+
+import com.cyksj.model.entity.GeminiUser;
+import com.cyksj.model.response.ConversationLimitResponse;
+import com.cyksj.model.response.GeminiCarPageResponse;
+import com.cyksj.model.response.GeminiCarStatusResponse;
+import com.cyksj.model.response.GeminiConversationCountResponse;
+import com.cyksj.model.views.GeminiCarInfoView;
+import com.cyksj.model.views.GeminiUserView;
+import com.ejlchina.searcher.SearchResult;
+
+import java.time.LocalDateTime;
+
+/**
+ * Gemini镜像服务
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+public interface GeminiService {
+
+    /**
+     * 获取用户信息
+     */
+    GeminiUserView getUserInfo(long userId, Long relationId);
+
+    /**
+     * 根据token查找用户
+     */
+    GeminiUser findByUserTokenAndExpireTimeAfter(String userToken, LocalDateTime now);
+
+    /**
+     * 检查车票账户
+     */
+    GeminiUser checkCarAccount(Long userId, Long relationId);
+
+    /**
+     * 获取GeminiUser
+     */
+    GeminiUser getGeminiUser(long userId, Long relationId);
+
+    /**
+     * 获取登录URL
+     */
+    String getCarLoginUrl(String domain, Long userId, Long relationId, String carId);
+
+    /**
+     * 清除次数
+     */
+    void clearNum(String userToken) throws Exception;
+
+    /**
+     * 根据relationId清除次数
+     */
+    void clearNumByRelationId(Long relationId) throws Exception;
+
+    /**
+     * 调用镜像端 /carpage 获取车队列表
+     */
+    GeminiCarPageResponse getCarPage(String domain, int page, int size);
+
+    /**
+     * 调用镜像端 /status 获取账号状态
+     */
+    GeminiCarStatusResponse getCarStatus(String domain, String carId);
+
+    /**
+     * 获取车队列表
+     */
+    SearchResult<GeminiCarInfoView> getCarInfoList(String domain, int page, int size);
+
+    /**
+     * 获取单个车队状态
+     */
+    GeminiCarInfoView getCarInfo(String domain, String carId);
+
+    /**
+     * 获取用户对话次数详情
+     */
+    GeminiConversationCountResponse getConversationCountDetail(String userToken);
+}

+ 417 - 0
netflix-service/src/main/java/com/cyksj/service/gemini/impl/GeminiServiceImpl.java

@@ -0,0 +1,417 @@
+package com.cyksj.service.gemini.impl;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.http.Method;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.util.StringUtil;
+import com.cyksj.mapper.*;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.response.ConversationLimitResponse;
+import com.cyksj.model.response.GeminiCarAccount;
+import com.cyksj.model.response.GeminiCarPageResponse;
+import com.cyksj.model.response.GeminiCarStatusResponse;
+import com.cyksj.model.response.GeminiConversationCountResponse;
+import com.cyksj.model.views.GeminiCarInfoView;
+import com.cyksj.model.views.GeminiUserView;
+import com.cyksj.redis.RedisService;
+import com.cyksj.service.gemini.GeminiService;
+import com.cyksj.service.sys.SysConfigService;
+import com.cyksj.service.user.UserBindRelationService;
+import com.ejlchina.searcher.SearchResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Gemini镜像服务实现
+ *
+ * @author chan
+ * @date 2025/1/4
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class GeminiServiceImpl implements GeminiService {
+
+    private final GeminiUserMapper geminiUserMapper;
+    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 String getDefaultDomain(String domain) {
+        if (StrUtil.isNotEmpty(domain)) {
+            return domain;
+        }
+        String defaultDomain = "https://gemini.bestaistore.com";
+        try {
+            SysConfig sysConfig = sysConfigService.getOne(
+                    Wrappers.lambdaQuery(SysConfig.class).eq(SysConfig::getSysKey, "mirror_gemini_domain")
+            );
+            if (sysConfig != null) {
+                String sysValue = sysConfig.getSysValue();
+                if (JSONUtil.isJsonArray(sysValue)) {
+                    List<String> domainList = JSONUtil.parseArray(sysValue).toList(String.class);
+                    defaultDomain = domainList.get(RandomUtil.randomInt(domainList.size()));
+                }
+            }
+        } catch (Exception e) {
+            log.error("获取Gemini域名错误. msg:{}", StringUtil.getErrorText(e));
+        }
+        return defaultDomain;
+    }
+
+    @Override
+    public GeminiUserView getUserInfo(long userId, Long relationId) {
+        GeminiUser geminiUser = checkCarAccount(userId, relationId);
+        if (geminiUser != null) {
+            GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
+            GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
+            GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
+
+            GeminiUserView view = GeminiUserView.builder()
+                    .skuName(goodsDonSku.getSubTitle())
+                    .expireTime(geminiUser.getExpireTime())
+                    .userToken(geminiUser.getUserToken())
+                    .limitNum(geminiUser.getLimitNum())
+                    .limitTime(geminiUser.getLimitTime())
+                    .build();
+            return view;
+        } else {
+            throw BusinessRuntimeException.getInstance("您还未购买该车票");
+        }
+    }
+
+    @Override
+    public GeminiUser findByUserTokenAndExpireTimeAfter(String userToken, LocalDateTime now) {
+        return geminiUserMapper.selectOne(
+                Wrappers.lambdaQuery(GeminiUser.class)
+                        .eq(GeminiUser::getUserToken, userToken)
+                        .gt(GeminiUser::getExpireTime, now)
+        );
+    }
+
+    @Override
+    public GeminiUser checkCarAccount(Long userId, Long relationId) {
+        GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
+        GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
+        GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
+
+        if (goodsDonSku != null && goodsDonSku.getIsMirror() && goodsDonSku.getIsCar()) {
+            return getGeminiUser(userId, relationId, groupsRelation, goodsDonSku);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public GeminiUser getGeminiUser(long userId, Long relationId) {
+        GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
+        GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
+        GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
+
+        if (goodsDonSku != null && goodsDonSku.getIsMirror()) {
+            return getGeminiUser(userId, relationId, groupsRelation, goodsDonSku);
+        } else {
+            throw BusinessRuntimeException.getInstance("服务器出了点问题");
+        }
+    }
+
+    @Override
+    public String getCarLoginUrl(String domain, Long userId, Long relationId, String carId) {
+        GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
+        GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
+        GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
+
+        if (goodsDonSku != null && goodsDonSku.getIsMirror() && goodsDonSku.getIsCar()) {
+            GeminiUser geminiUser = getGeminiUser(userId, relationId, groupsRelation, goodsDonSku);
+            String DOMAIN = getDefaultDomain(domain);
+
+            log.info("domain:{},用户:{},所在车次:{},座位id:{},在{}获取跳转Gemini镜像的登录url",
+                    DOMAIN, userId, groupsTrips.getId(), relationId, DateTime.now());
+
+            // /auth/logintoken?access_token={token}&account={carId}
+            return DOMAIN + "/auth/logintoken?access_token=" + geminiUser.getUserToken() + "&account=" + carId;
+        } else {
+            throw BusinessRuntimeException.getInstance("服务器出了点问题");
+        }
+    }
+
+    /**
+     * Gemini镜像端API域名
+     */
+    private static final String GEMINI_MIRROR_API = "https://saas.galaxyva.com";
+
+    @Override
+    public void clearNum(String userToken) throws Exception {
+        String url = GEMINI_MIRROR_API + "/gemini/cleanUserTokenLimit?userToken=" + userToken;
+        HttpRequest request = new HttpRequest(url);
+        HttpResponse execute = request.method(Method.GET).setConnectionTimeout(3000).timeout(3000).execute();
+        String body = execute.body();
+        JSONObject re = JSONUtil.parseObj(body);
+        if (!body.contains("清除成功")) {
+            throw BusinessRuntimeException.getInstance(re.getStr("message"));
+        }
+    }
+
+    @Override
+    public void clearNumByRelationId(Long relationId) throws Exception {
+        String url = GEMINI_MIRROR_API + "/gemini/cleanUserTokenLimit?relationId=" + relationId;
+        HttpRequest request = new HttpRequest(url);
+        HttpResponse execute = request.method(Method.GET).setConnectionTimeout(3000).timeout(3000).execute();
+        String body = execute.body();
+        JSONObject re = JSONUtil.parseObj(body);
+        if (!body.contains("清除成功")) {
+            throw BusinessRuntimeException.getInstance(re.getStr("message"));
+        }
+    }
+
+    @Override
+    public GeminiCarPageResponse getCarPage(String domain, int page, int size) {
+        String DOMAIN = getDefaultDomain(domain);
+        try {
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("page", page);
+            requestBody.put("size", size);
+
+            String res = HttpUtil.post(DOMAIN + "/carpage", requestBody.toString());
+            return JSONUtil.toBean(res, GeminiCarPageResponse.class);
+        } catch (Exception e) {
+            log.error("调用Gemini /carpage 接口失败: {}", StringUtil.getErrorText(e));
+            GeminiCarPageResponse response = new GeminiCarPageResponse();
+            response.setCode(-1);
+            response.setMessages("获取车队列表失败");
+            return response;
+        }
+    }
+
+    @Override
+    public GeminiCarStatusResponse getCarStatus(String domain, String carId) {
+        String DOMAIN = getDefaultDomain(domain);
+        try {
+            String res = HttpUtil.get(DOMAIN + "/status?carid=" + carId);
+            return JSONUtil.toBean(res, GeminiCarStatusResponse.class);
+        } catch (Exception e) {
+            log.error("调用Gemini /status 接口失败: {}", StringUtil.getErrorText(e));
+            GeminiCarStatusResponse response = new GeminiCarStatusResponse();
+            response.setAccountReady(false);
+            return response;
+        }
+    }
+
+    /**
+     * 获取车位信息
+     */
+    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;
+    }
+
+    /**
+     * 获取车队信息
+     */
+    private GroupsTrips getGroupsTrips(GroupsRelation groupsRelation) {
+        GroupsTrips groupsTrips = groupsMapper.selectById(groupsRelation.getGroupsId());
+        if (groupsTrips == null) {
+            throw BusinessRuntimeException.getInstance("车队异常");
+        }
+        return groupsTrips;
+    }
+
+    private GeminiUser getGeminiUser(Long userId, Long relationId, GroupsRelation groupsRelation, GoodsDonSku goodsDonSku) {
+        GeminiUser geminiUser = geminiUserMapper.selectOne(
+                Wrappers.lambdaQuery(GeminiUser.class).eq(GeminiUser::getRelationId, relationId)
+        );
+        User user = userMapper.selectById(userId);
+        if (geminiUser == null) {
+            geminiUser = createGeminiUser(groupsRelation, user, goodsDonSku);
+        } else {
+            updateGeminiUser(groupsRelation, user, geminiUser);
+        }
+        return geminiUser;
+    }
+
+    /**
+     * 创建GeminiUser
+     */
+    private synchronized GeminiUser createGeminiUser(GroupsRelation groupsRelation, User user, GoodsDonSku goodsDonSku) {
+        GeminiUser geminiUser = new GeminiUser();
+        if (goodsDonSku.getIsCar()) {
+            geminiUser.setLimitNum(goodsDonSku.getGptLimitNum());
+            geminiUser.setLimitTime(goodsDonSku.getGptLimitTime());
+        }
+        try {
+            geminiUser.setExpireTime(groupsRelation.getExpiryTime());
+            geminiUser.setIsPlus(true);
+            geminiUser.setName(user.getNickname());
+            geminiUser.setRelationId(groupsRelation.getId());
+            geminiUser.setUserToken(UUID.randomUUID().toString());
+            geminiUserMapper.insert(geminiUser);
+        } catch (DuplicateKeyException e) {
+            log.error("重复插入Gemini镜像用户:{}token,errmsg:{}", user.getNickname(), StringUtil.getErrorText(e));
+        }
+        return geminiUser;
+    }
+
+    private void updateGeminiUser(GroupsRelation groupsRelation, User user, GeminiUser geminiUser) {
+        geminiUser.setName(user.getNickname());
+        geminiUser.setExpireTime(groupsRelation.getExpiryTime());
+        geminiUserMapper.updateById(geminiUser);
+    }
+
+    @Override
+    public SearchResult<GeminiCarInfoView> getCarInfoList(String domain, int page, int size) {
+        GeminiCarPageResponse carPageResponse = getCarPage(domain, page, size);
+
+        List<GeminiCarInfoView> carInfoList = new ArrayList<>();
+
+        if (carPageResponse.getCode() != null && carPageResponse.getCode() == 0
+                && carPageResponse.getData() != null
+                && carPageResponse.getData().getAccounts() != null) {
+
+            for (GeminiCarAccount account : carPageResponse.getData().getAccounts()) {
+                GeminiCarInfoView carInfo = new GeminiCarInfoView();
+                carInfo.setCarId(account.getName());
+                carInfo.setCarName(account.getNickname());
+
+                // 根据type设置isPlus: ultra=3, pro=2, free=1
+                if ("ultra".equalsIgnoreCase(account.getType())) {
+                    carInfo.setIsPlus(3);
+                } else if ("pro".equalsIgnoreCase(account.getType())) {
+                    carInfo.setIsPlus(2);
+                } else {
+                    carInfo.setIsPlus(1);
+                }
+
+                // 获取状态信息
+                if (account.getDisabled() != null && account.getDisabled()) {
+                    carInfo.setStatus("停运");
+                    carInfo.setAccountReady(false);
+                } else {
+                    // 调用 /status 接口获取详细状态
+                    GeminiCarStatusResponse statusResponse = getCarStatus(domain, account.getName());
+                    if (statusResponse.getAccountReady() != null && statusResponse.getAccountReady()) {
+                        carInfo.setAccountReady(true);
+                        carInfo.setClearsIn(statusResponse.getClearsIn() != null ? statusResponse.getClearsIn().longValue() : 0L);
+                        carInfo.setUse(statusResponse.getCount());
+
+                        if (carInfo.getClearsIn() > 0) {
+                            carInfo.setStatus("停运");
+                            carInfo.setExpTime(new DateTime(System.currentTimeMillis() + carInfo.getClearsIn() * 1000));
+                        } else {
+                            // 根据使用次数判断繁忙程度
+                            carInfo.setScore(statusResponse.getCount() != null ? statusResponse.getCount().doubleValue() : 0.0);
+                            carInfo.setStatus(carInfo.getScore() > 50 ? "繁忙" : "空闲");
+                        }
+                    } else {
+                        carInfo.setAccountReady(false);
+                        carInfo.setStatus("停运");
+                    }
+                }
+
+                carInfoList.add(carInfo);
+            }
+        }
+
+        int total = 0;
+        if (carPageResponse.getData() != null && carPageResponse.getData().getTotal() != null) {
+            total = carPageResponse.getData().getTotal();
+        }
+
+        SearchResult<GeminiCarInfoView> result = new SearchResult<>(carInfoList);
+        result.setTotalCount(total);
+        return result;
+    }
+
+    @Override
+    public GeminiCarInfoView getCarInfo(String domain, String carId) {
+        GeminiCarStatusResponse statusResponse = getCarStatus(domain, carId);
+
+        GeminiCarInfoView carInfo = new GeminiCarInfoView();
+        carInfo.setCarId(carId);
+        carInfo.setCarName(carId);
+
+        if (statusResponse.getAccountReady() != null && statusResponse.getAccountReady()) {
+            carInfo.setAccountReady(true);
+            carInfo.setClearsIn(statusResponse.getClearsIn() != null ? statusResponse.getClearsIn().longValue() : 0L);
+            carInfo.setUse(statusResponse.getCount());
+
+            // 根据isPlus设置账号类型
+            if (statusResponse.getIsPlus() != null && statusResponse.getIsPlus()) {
+                carInfo.setIsPlus(2); // pro
+            } else {
+                carInfo.setIsPlus(1); // free
+            }
+
+            if (carInfo.getClearsIn() > 0) {
+                carInfo.setStatus("停运");
+                carInfo.setExpTime(new DateTime(System.currentTimeMillis() + carInfo.getClearsIn() * 1000));
+            } else {
+                carInfo.setScore(statusResponse.getCount() != null ? statusResponse.getCount().doubleValue() : 0.0);
+                carInfo.setStatus(carInfo.getScore() > 50 ? "繁忙" : "空闲");
+            }
+        } else {
+            carInfo.setAccountReady(false);
+            carInfo.setStatus("停运");
+        }
+
+        return carInfo;
+    }
+
+    @Override
+    public GeminiConversationCountResponse getConversationCountDetail(String userToken) {
+        try {
+            String url = GEMINI_MIRROR_API + "/gemini/getConversationCount?userToken=" + userToken;
+            String res = HttpUtil.get(url, 5000);
+            JSONObject json = JSONUtil.parseObj(res);
+
+            // 检查是否有错误
+            if (json.containsKey("message")) {
+                throw BusinessRuntimeException.getInstance(json.getStr("message"));
+            }
+
+            GeminiConversationCountResponse response = new GeminiConversationCountResponse();
+            response.setCount(json.getLong("count"));
+            response.setLimit(json.getInt("limit"));
+            response.setRemaining(json.getLong("remaining"));
+            response.setTime(json.getLong("time"));
+            response.setIsVip(json.getBool("isVip"));
+
+            return response;
+        } catch (BusinessRuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("调用Gemini getConversationCount接口失败: {}", StringUtil.getErrorText(e));
+            throw BusinessRuntimeException.getInstance("获取对话次数失败");
+        }
+    }
+}

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

@@ -56,6 +56,8 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 
 	private final GrokUserMapper grokUserMapper;
 
+	private final GeminiUserMapper geminiUserMapper;
+
 	private final OrderDonUpgradeRenewRecordMapper orderDonUpgradeRenewRecordMapper;
 
 	private final UserTicketClearedRecordMapper userTicketClearedRecordMapper;
@@ -133,6 +135,18 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 						.eq(GrokUser::getRelationId, relationId));
 			}
 		}
+
+		//gemini
+		if (goodsId != null && goodsId == Constant.GEMINI_GOODS_ID) {
+			GeminiUser user = geminiUserMapper.selectOne(Wrappers.lambdaQuery(GeminiUser.class)
+					.eq(GeminiUser::getRelationId, relationId).last("limit 1"));
+			if (user != null) {
+				userToken = user.getUserToken();
+				geminiUserMapper.delete(Wrappers.lambdaQuery(GeminiUser.class)
+						.eq(GeminiUser::getRelationId, relationId));
+			}
+		}
+
 		if (clearId != null && StrUtil.isNotBlank(userToken)) {
 			userTicketClearedRecordMapper.update(null, Wrappers.lambdaUpdate(UserTicketClearedRecord.class)
 					.set(UserTicketClearedRecord::getUserToken, userToken)

+ 138 - 0
netflix-web/src/main/java/com/cyksj/web/controller/mirror/MirrorController.java

@@ -19,9 +19,13 @@ import com.cyksj.mapper.WebInfoMapper;
 import com.cyksj.mapper.renew.RenewUpgradePackageMapper;
 import com.cyksj.model.dto.ChatgptUserVerifyDto;
 import com.cyksj.model.entity.*;
+import com.cyksj.model.request.GeminiOauthReq;
+import com.cyksj.model.response.ConversationLimitResponse;
+import com.cyksj.model.response.GeminiConversationCountResponse;
 import com.cyksj.model.views.*;
 import com.cyksj.service.chatgpt.ChatGptAccountService;
 import com.cyksj.service.claude.ClaudeService;
+import com.cyksj.service.gemini.GeminiService;
 import com.cyksj.service.grok.GrokService;
 import com.cyksj.service.luma.LumaService;
 import com.cyksj.service.midjourney.MidjourneyAccountService;
@@ -100,6 +104,8 @@ public class MirrorController {
 
     private final GrokService grokService;
 
+    private final GeminiService geminiService;
+
     private final NanoUserQuotaService nanoUserQuotaService;
 
     private final WebInfoMapper webInfoMapper;
@@ -617,6 +623,138 @@ public class MirrorController {
     }
 
 
+    //------------------------ Gemini -------------------------------
+
+    /**
+     * 获取 Gemini 车票信息
+     */
+    @GetMapping("/gemini/userInfo")
+    public Result<GeminiUserView> getGeminiUserInfo(Long relationId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        GeminiUserView user = geminiService.getUserInfo(userId, relationId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(user);
+    }
+
+    /**
+     * Gemini 车队跳转登录
+     */
+    @GetMapping("/geminiMirror/{relationId}/{carId}")
+    public Result<String> geminiCarLogin(String domain, @PathVariable Long relationId, @PathVariable String carId) throws IOException {
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        geminiService.checkCarAccount(userId, relationId);
+        String url = geminiService.getCarLoginUrl(domain, userId, relationId, carId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(url);
+    }
+
+    /**
+     * Gemini OAuth 鉴权
+     */
+    @PostMapping("/gemini/oauth")
+    public String geminiOauth(@RequestBody GeminiOauthReq req) {
+        try {
+            log.info("Gemini OAuth验证 userToken:{} carId:{}", req.getUsertoken(), req.getCarid());
+            GeminiUser user = geminiService.findByUserTokenAndExpireTimeAfter(req.getUsertoken(), LocalDateTime.now());
+
+            JSONObject result = new JSONObject();
+            if (user == null) {
+                result.put("code", 0);
+                result.put("msg", "用户不存在或已过期");
+                return result.toString();
+            }
+
+            result.put("code", 1);
+            result.put("msg", "success");
+            result.put("grant", user.getIsPlus() ? "pro" : "free");
+
+            return result.toString();
+        } catch (Exception e) {
+            log.error("Gemini OAuth验证错误", e);
+            JSONObject result = new JSONObject();
+            result.put("code", 0);
+            result.put("msg", "服务器错误");
+            return result.toString();
+        }
+    }
+
+    /**
+     * Gemini 获取车队列表
+     */
+    @GetMapping("/gemini/cars")
+    public Result<SearchResult<GeminiCarInfoView>> geminiCarInfoList(
+            Long relationId,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int size,
+            String domain) {
+        Long userId = StpUserUtil.getUserIdAfterLogin();
+        if (relationId != null) {
+            GeminiUser geminiUser = geminiService.getGeminiUser(userId, relationId);
+            if (geminiUser == null) {
+                throw BusinessRuntimeException.getInstance("您还未购买车票");
+            }
+        }
+
+        SearchResult<GeminiCarInfoView> carInfoList = geminiService.getCarInfoList(domain, page, size);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(carInfoList);
+    }
+
+    /**
+     * Gemini 获取账号状态
+     */
+    @GetMapping("/gemini/status")
+    public Result<GeminiCarInfoView> geminiCarStatus(String domain, String carId) {
+        GeminiCarInfoView carInfo = geminiService.getCarInfo(domain, carId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(carInfo);
+    }
+
+    /**
+     * Gemini 查询用户对话次数
+     */
+    @GetMapping("/gemini/getConversationCount")
+    public String geminiGetConversationCount(String userToken) {
+        try {
+            GeminiConversationCountResponse response = geminiService.getConversationCountDetail(userToken);
+            JSONObject result = new JSONObject();
+            result.put("count", response.getCount());
+            result.put("limit", response.getLimit());
+            result.put("remaining", response.getRemaining());
+            result.put("time", response.getTime());
+            result.put("isVip", response.getIsVip());
+            return result.toString();
+        } catch (Exception e) {
+            log.error("Gemini getConversationCount error", e);
+            JSONObject result = new JSONObject();
+            result.put("message", e.getMessage());
+            return result.toString();
+        }
+    }
+
+    /**
+     * Gemini 清除用户限制
+     */
+    @GetMapping("/gemini/cleanUserTokenLimit")
+    public String geminiCleanUserTokenLimit(String userToken, Long relationId) {
+        try {
+            if (userToken != null) {
+                geminiService.clearNum(userToken);
+            } else if (relationId != null) {
+                geminiService.clearNumByRelationId(relationId);
+            } else {
+                JSONObject result = new JSONObject();
+                result.put("message", "userToken 或 relationId 至少传递一个");
+                return result.toString();
+            }
+            JSONObject result = new JSONObject();
+            result.put("msg", "清除成功");
+            return result.toString();
+        } catch (Exception e) {
+            log.error("Gemini cleanUserTokenLimit error", e);
+            JSONObject result = new JSONObject();
+            result.put("message", e.getMessage());
+            return result.toString();
+        }
+    }
+
+
     /**
      * 根据showId 查询批发网站
      */