Ver Fonte

优化代码,添加接口

liuchengsen há 1 mês atrás
pai
commit
74bf5c3ab1

+ 3 - 0
zhijiayun-gateway/src/main/resources/db/schema.sql

@@ -38,6 +38,9 @@ CREATE TABLE IF NOT EXISTS `t_user` (
     `last_login_ip`         VARCHAR(45)  DEFAULT NULL COMMENT '最后登录IP',
     `last_login_device`     VARCHAR(100) DEFAULT NULL COMMENT '最后登录设备信息',
     `login_source`          VARCHAR(20)  DEFAULT NULL COMMENT '登录来源:WINDOWS/MINIAPP/ANDROID/IOS/WECHAT',
+    `token_version`         BIGINT       DEFAULT 0 COMMENT 'Token版本号(单设备登录控制)',
+    `refresh_token_hash`    VARCHAR(64)  DEFAULT NULL COMMENT 'Refresh Token SHA-256哈希',
+    `refresh_token_status`  VARCHAR(10)  DEFAULT NULL COMMENT 'Refresh Token状态:ACTIVE/USED',
     `create_time`           DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time`           DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     PRIMARY KEY (`id`),

+ 3 - 0
zhijiayun-gateway/src/test/resources/test-schema.sql

@@ -44,6 +44,9 @@ CREATE TABLE IF NOT EXISTS t_user (
     last_login_ip   VARCHAR(45)   DEFAULT NULL,
     last_login_device VARCHAR(100) DEFAULT NULL,
     login_source    VARCHAR(20)   DEFAULT NULL,
+    token_version   BIGINT        DEFAULT 0,
+    refresh_token_hash VARCHAR(64)  DEFAULT NULL,
+    refresh_token_status VARCHAR(10) DEFAULT NULL,
     create_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
     update_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
     CONSTRAINT uk_phone UNIQUE (phone)

+ 8 - 3
zhijiayun-test/src/main/resources/static/invite-test.html

@@ -703,7 +703,9 @@ const FIELD_DESC = {
         ['userInfo.version', 'String', '等级版本(冗余字段,可能为null)'],
         ['needBindPhone', 'Boolean', '是否需要绑定手机号'],
         ['tempToken', 'String', '临时令牌(用于绑定手机号,通常为null)'],
-        ['newUser', 'Boolean', '是否为新注册用户']
+        ['newUser', 'Boolean', '是否为新注册用户'],
+        ['kickedPreviousSession', 'Boolean', '是否踢出了旧设备(true=旧设备已被强制下线)'],
+        ['kickedDeviceInfo', 'String', '被踢出设备信息(如 "Windows / 192.168.1.1")']
     ],
     'userInfo': [
         ['id', 'Long', '用户ID(主键)'],
@@ -1249,7 +1251,8 @@ function renderAuthModule() {
          {label:'邀请码(inviteCode,可选)', name:'inviteCode', placeholder:'ABC123', default:''}], true, 'loginResponse', '已有微信登录的用户绑定手机号')}
     ${testSection('auth-wechat-callback', '微信回调', 'GET', 'get', '/api/auth/wechat/callback?code=test_code', '公开', 'auth-public', null, false, null, '微信扫码登录前端跳转回调')}
     ${testSection('auth-refresh', '刷新Token', 'POST', 'post', '/api/auth/refresh', '公开', 'auth-public',
-        [{label:'refreshToken(refreshToken)', name:'refreshToken', placeholder:'输入refreshToken', default:''}], false, 'loginResponse', '使用refreshToken获取新的accessToken')}
+        [{label:'refreshToken(refreshToken)', name:'refreshToken', placeholder:'输入refreshToken', default:''}], false, 'loginResponse', '使用refreshToken获取新的accessToken(Refresh Token一次性旋转,重用即被踢)')}
+    ${testSection('auth-heartbeat', '心跳保活', 'POST', 'post', '/api/auth/heartbeat', '需认证', 'auth-required', null, true, 'loginResponse', '客户端定时调用保持登录态(建议4分钟一次),返回新Token对。旧Token自然过期前仍可用')}
     ${testSection('auth-userinfo', '获取用户信息', 'GET', 'get', '/api/auth/user/info', '需认证', 'auth-required', null, true, 'userInfo', '获取当前登录用户的基本信息')}
     ${testSection('auth-setpwd', '设置密码', 'POST', 'post', '/api/auth/password/set', '需认证', 'auth-required',
         [{label:'旧密码(oldPassword,可选)', name:'oldPassword', placeholder:'首次设置可不填', default:''},
@@ -1663,7 +1666,9 @@ function renderAdminUserModule() {
          {label:'角色(role)', name:'role', placeholder:'USER/ADMIN/SUPER_ADMIN', default:''}], true, null, '超级管理员查询用户列表')}
     ${testSection('admin-user-role', '修改用户角色', 'PUT', 'put', '/api/admin/users/{userId}/role', '需认证', 'auth-required',
         [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'},
-         {label:'目标角色(role)', name:'role', placeholder:'USER/ADMIN/SUPER_ADMIN', default:'ADMIN'}], false, null, '超级管理员修改用户角色')}`;
+         {label:'目标角色(role)', name:'role', placeholder:'USER/ADMIN/SUPER_ADMIN', default:'ADMIN'}], false, null, '超级管理员修改用户角色')}
+    ${testSection('admin-user-kick', '强制踢出用户', 'POST', 'post', '/api/admin/users/{userId}/kick', '需认证', 'auth-required',
+        [{label:'用户ID(userId)', name:'userId', placeholder:'1', default:'1'}], false, null, '强制踢出用户:递增tokenVersion+清空RefreshToken,用户下次请求返回1304')}`;
 }
 
 function renderAdminAuditModule() {

+ 7 - 7
zhijiayun-user/src/main/java/com/xuekairui/user/controller/SearchRecordController.java

@@ -1,5 +1,6 @@
 package com.xuekairui.user.controller;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.xuekairui.common.Result;
 import com.xuekairui.user.dto.SearchRecordResponse;
 import com.xuekairui.user.dto.SearchRecordSaveRequest;
@@ -10,8 +11,6 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
-
 /**
  * 搜索记录控制器
  * 主搜索页"历史检索"接口,存储完整的比价搜索快照
@@ -39,15 +38,16 @@ public class SearchRecordController {
     }
 
     /**
-     * 读取当前用户最近的主搜索历史
-     * GET /api/search-record/history?limit=20
+     * 分页读取当前用户最近的主搜索历史
+     * GET /api/search-record/history?page=1&size=20
      */
     @GetMapping("/history")
-    public Result<List<SearchRecordResponse>> listRecent(
+    public Result<Page<SearchRecordResponse>> listRecent(
             HttpServletRequest request,
-            @RequestParam(defaultValue = "20") Integer limit) {
+            @RequestParam(defaultValue = "1") Integer pageNo,
+            @RequestParam(defaultValue = "20") Integer pageSize) {
         Long userId = (Long) request.getAttribute("userId");
-        return Result.success(searchRecordService.listRecent(userId, limit));
+        return Result.success(searchRecordService.listRecent(userId, pageNo, pageSize));
     }
 
     /**

+ 0 - 9
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/SearchRecordMapper.java

@@ -3,19 +3,10 @@ package com.xuekairui.user.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.xuekairui.user.entity.SearchRecord;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
-import org.apache.ibatis.annotations.Select;
-
-import java.util.List;
 
 /**
  * 搜索记录 Mapper
  */
 @Mapper
 public interface SearchRecordMapper extends BaseMapper<SearchRecord> {
-
-    /** 查询用户最近的搜索记录 */
-    @Select("SELECT * FROM t_search_record WHERE user_id = #{userId} " +
-            "ORDER BY searched_at DESC LIMIT #{limit}")
-    List<SearchRecord> findRecentByUserId(@Param("userId") Long userId, @Param("limit") int limit);
 }

+ 1 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -393,6 +393,7 @@ public class AuthService {
             }
         }
         // 邀请人信息
+        assert user != null;
         Long inviterId = user.getInviterId();
         String inviterNickname = null;
         if (inviterId != null) {

+ 11 - 7
zhijiayun-user/src/main/java/com/xuekairui/user/service/SearchRecordService.java

@@ -1,6 +1,7 @@
 package com.xuekairui.user.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.xuekairui.common.BusinessException;
@@ -15,8 +16,6 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
-import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * 搜索记录服务
@@ -49,12 +48,17 @@ public class SearchRecordService {
     }
 
     /**
-     * 读取当前用户最近的主搜索历史
+     * 分页读取当前用户最近的主搜索历史
      */
-    public List<SearchRecordResponse> listRecent(Long userId, Integer limit) {
-        int actualLimit = Math.min(limit != null && limit > 0 ? limit : 20, 50);
-        List<SearchRecord> records = searchRecordMapper.findRecentByUserId(userId, actualLimit);
-        return records.stream().map(this::toResponse).collect(Collectors.toList());
+    public Page<SearchRecordResponse> listRecent(Long userId, Integer page, Integer size) {
+        int pageNo = page != null && page > 0 ? page : 1;
+        int pageSize = Math.min(size != null && size > 0 ? size : 20, 50);
+        Page<SearchRecord> pageParam = new Page<>(pageNo, pageSize);
+        LambdaQueryWrapper<SearchRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SearchRecord::getUserId, userId)
+                .orderByDesc(SearchRecord::getSearchedAt);
+        Page<SearchRecord> resultPage = searchRecordMapper.selectPage(pageParam, wrapper);
+        return (Page<SearchRecordResponse>) resultPage.convert(this::toResponse);
     }
 
     /**