diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/file/service/impl/FileServiceImpl.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/file/service/impl/FileServiceImpl.java index 0b8a1ae..f09acd0 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/file/service/impl/FileServiceImpl.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/file/service/impl/FileServiceImpl.java @@ -17,6 +17,7 @@ import io.minio.http.Method; import io.minio.messages.DeleteError; import io.minio.messages.DeleteObject; import io.minio.messages.Item; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -51,21 +52,58 @@ public class FileServiceImpl extends ServiceImpl implement //线程池 private static final ExecutorService SAVE_TO_DB_EXECUTOR = Executors.newFixedThreadPool(10); + @PostConstruct private void init(){ - createBucket("videos"); - createBucket("images"); - createBucket("chunks"); + try { + createBucketWithPolicy("videos"); + createBucketWithPolicy("images"); + createBucketWithPolicy("chunks"); + log.info("MinIO桶初始化完成"); + } catch (Exception e) { + log.error("MinIO桶初始化失败: {}", e.getMessage(), e); + } + } + + /** + * 创建桶并设置公共读取策略 + */ + private void createBucketWithPolicy(String name) throws Exception { + boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build()); + if (!isExist) { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build()); + log.info("创建桶: {}", name); + + // 设置桶策略为公共读取 + String readOnlyPolicy = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": ["*"]}, + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::%s/*"] + } + ] + } + """.formatted(name); + + minioClient.setBucketPolicy( + SetBucketPolicyArgs.builder() + .bucket(name) + .config(readOnlyPolicy) + .build() + ); + log.info("设置桶{}的公共读取策略成功", name); + } } @Override public Boolean createBucket(String name){ try { - boolean isExist = minioClient. - bucketExists(BucketExistsArgs.builder().bucket(name).build()); - if (!isExist) { - minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build()); - } + createBucketWithPolicy(name); } catch (Exception e) { + log.error("创建桶失败: {}", e.getMessage(), e); throw new FileException("创建桶失败"); } return true; diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserInfoController.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserInfoController.java index 486e7c7..d51e745 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserInfoController.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserInfoController.java @@ -1,9 +1,8 @@ package com.luojia_channel.modules.user.controller; import com.luojia_channel.common.domain.Result; -import com.luojia_channel.modules.file.dto.UploadFileDTO; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; -import com.luojia_channel.modules.user.dto.UserInfoDTO; +import com.luojia_channel.modules.user.vo.UserInfoVO; import com.luojia_channel.modules.user.service.UserInfoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -30,7 +29,7 @@ public class UserInfoController { @ApiResponse(responseCode = "200", description = "获取成功"), @ApiResponse(responseCode = "500", description = "获取失败,用户不存在或未登录") }) - public Result getUserInfo(@RequestParam(required = false) Long userId) { + public Result getUserInfo(@RequestParam(required = false) Long userId) { return Result.success(userInfoService.getUserInfo(userId)); } diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserLoginController.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserLoginController.java index 471041a..f43fdfb 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserLoginController.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/controller/UserLoginController.java @@ -7,6 +7,7 @@ import com.luojia_channel.modules.user.utils.CaptchaUtils; import com.luojia_channel.modules.user.dto.UserLoginDTO; import com.luojia_channel.modules.user.dto.UserRegisterDTO; import com.luojia_channel.modules.user.service.UserLoginService; +import com.luojia_channel.modules.user.vo.UserInfoVO; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -34,6 +35,7 @@ import java.util.concurrent.TimeUnit; public class UserLoginController { private final UserLoginService userLoginService; private final RedisUtil redisUtil; + @PostMapping("/login") @Operation( summary = "用户登录", @@ -41,13 +43,31 @@ public class UserLoginController { tags = {"用户管理"} ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "登录成功",content = @Content(schema = @Schema(implementation = UserDTO.class))), + @ApiResponse(responseCode = "200", description = "登录成功",content = @Content(schema = @Schema(implementation = UserInfoVO.class))), @ApiResponse(responseCode = "500", description = "登录失败,用户名或密码错误") }) - public Result login(@RequestBody UserLoginDTO userLoginDTO){ + public Result login(@RequestBody UserLoginDTO userLoginDTO){ return Result.success(userLoginService.login(userLoginDTO)); } + @PostMapping("/check-login") + @Operation( + summary = "检查用户登录状态", + description = "通过accessToken和refreshToken验证用户登录状态", + tags = {"用户管理"} + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "验证成功"), + @ApiResponse(responseCode = "500", description = "验证失败,token无效或已过期") + }) + public Result checkLogin(@RequestHeader(value = "Authorization", required = false) String accessToken, + @RequestHeader(value = "X-Refresh-Token", required = false) String refreshToken) { + if (accessToken != null && accessToken.startsWith("Bearer ")) { + accessToken = accessToken.substring(7); + } + return Result.success(userLoginService.checkLogin(accessToken, refreshToken)); + } + @PostMapping("/register") @Operation( summary = "用户注册", diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserInfoService.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserInfoService.java index 735936d..2cbecdf 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserInfoService.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserInfoService.java @@ -1,9 +1,8 @@ package com.luojia_channel.modules.user.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.luojia_channel.modules.file.dto.UploadFileDTO; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; -import com.luojia_channel.modules.user.dto.UserInfoDTO; +import com.luojia_channel.modules.user.vo.UserInfoVO; import com.luojia_channel.modules.user.entity.User; import org.springframework.web.multipart.MultipartFile; @@ -20,5 +19,5 @@ public interface UserInfoService extends IService { * @param userId 用户ID,如果为null则获取当前登录用户信息 * @return 用户信息DTO */ - UserInfoDTO getUserInfo(Long userId); + UserInfoVO getUserInfo(Long userId); } diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserLoginService.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserLoginService.java index 420dcdd..2c142f0 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserLoginService.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/UserLoginService.java @@ -5,11 +5,12 @@ import com.luojia_channel.common.domain.UserDTO; import com.luojia_channel.modules.user.dto.UserLoginDTO; import com.luojia_channel.modules.user.dto.UserRegisterDTO; import com.luojia_channel.modules.user.entity.User; +import com.luojia_channel.modules.user.vo.UserInfoVO; import jakarta.servlet.http.HttpServletRequest; public interface UserLoginService extends IService { - UserDTO login(UserLoginDTO userLoginDTO); + UserInfoVO login(UserLoginDTO userLoginDTO); UserDTO checkLogin(String accessToken, String refreshToken); diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserInfoServiceImpl.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserInfoServiceImpl.java index 2f3a2cc..bcfe6c0 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserInfoServiceImpl.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserInfoServiceImpl.java @@ -5,12 +5,9 @@ import cn.hutool.crypto.digest.BCrypt; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.luojia_channel.common.exception.UserException; import com.luojia_channel.common.utils.UserContext; -import com.luojia_channel.modules.file.dto.UploadFileDTO; import com.luojia_channel.modules.file.service.impl.FileServiceImpl; -import com.luojia_channel.modules.file.utils.GeneratePathUtil; -import com.luojia_channel.modules.file.utils.ValidateFileUtil; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; -import com.luojia_channel.modules.user.dto.UserInfoDTO; +import com.luojia_channel.modules.user.vo.UserInfoVO; import com.luojia_channel.modules.user.entity.User; import com.luojia_channel.modules.user.mapper.UserMapper; import com.luojia_channel.modules.user.service.UserInfoService; @@ -68,7 +65,7 @@ public class UserInfoServiceImpl extends ServiceImpl implement } @Override - public UserInfoDTO getUserInfo(Long userId) { + public UserInfoVO getUserInfo(Long userId) { // 如果userId为null,则获取当前登录用户的ID if (userId == null) { userId = UserContext.getUserId(); @@ -84,7 +81,7 @@ public class UserInfoServiceImpl extends ServiceImpl implement } // 转换为DTO对象 - UserInfoDTO userInfoDTO = UserInfoDTO.builder() + UserInfoVO userInfoVO = UserInfoVO.builder() .id(user.getId()) .username(user.getUsername()) .phone(user.getPhone()) @@ -96,6 +93,6 @@ public class UserInfoServiceImpl extends ServiceImpl implement .status(user.getStatus()) .build(); - return userInfoDTO; + return userInfoVO; } } diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserLoginServiceImpl.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserLoginServiceImpl.java index f978234..39307a4 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserLoginServiceImpl.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/service/impl/UserLoginServiceImpl.java @@ -17,6 +17,7 @@ import com.luojia_channel.modules.user.service.UserLoginService; import com.luojia_channel.common.utils.JWTUtil; import com.luojia_channel.common.utils.RedisUtil; import com.luojia_channel.modules.user.utils.ValidateUserUtil; +import com.luojia_channel.modules.user.vo.UserInfoVO; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -69,16 +70,24 @@ public class UserLoginServiceImpl extends ServiceImpl implemen /** * 生成并存储双token - * @param userDTO + * @param userId 用户ID + * @param username 用户名 + * @return 包含access和refresh token的数组 */ - private void generateTokens(UserDTO userDTO){ + private String[] generateTokens(Long userId, String username){ + UserDTO userDTO = UserDTO.builder() + .userId(userId) + .username(username) + .build(); + String accessToken = jwtUtil.generateAccessToken(userDTO); String refreshToken = jwtUtil.generateRefreshToken(userDTO); - userDTO.setAccessToken(accessToken); - userDTO.setRefreshToken(refreshToken); + //存储refreshToken到redis - String key = "refresh_token:" + userDTO.getUserId(); + String key = REFRESH_TOKEN_PREFIX + userId; redisUtil.set(key, refreshToken, EXPIRE_TIME, TimeUnit.DAYS); + + return new String[]{accessToken, refreshToken}; } /** @@ -88,19 +97,33 @@ public class UserLoginServiceImpl extends ServiceImpl implemen * @return */ @Override - public UserDTO login(UserLoginDTO userLoginDTO) { + public UserInfoVO login(UserLoginDTO userLoginDTO) { String userFlag = userLoginDTO.getUserFlag(); String password = userLoginDTO.getPassword(); User user = getUserByFlag(userFlag); if (!BCrypt.checkpw(password, user.getPassword())) { throw new UserException("密码错误"); } - UserDTO userDTO = UserDTO.builder() - .userId(user.getId()) + + // 生成token + String[] tokens = generateTokens(user.getId(), user.getUsername()); + + // 构建返回的用户信息 + UserInfoVO userInfoVO = UserInfoVO.builder() + .id(user.getId()) .username(user.getUsername()) + .phone(user.getPhone()) + .email(user.getEmail()) + .avatar(user.getAvatar()) + .gender(user.getGender()) + .college(user.getCollege()) + .role(user.getRole()) + .status(user.getStatus()) + .accessToken(tokens[0]) + .refreshToken(tokens[1]) .build(); - generateTokens(userDTO); - return userDTO; + + return userInfoVO; } /** @@ -157,7 +180,13 @@ public class UserLoginServiceImpl extends ServiceImpl implemen .userId(user.getId()) .username(user.getUsername()) .build(); - generateTokens(userDTO); + String accessToken = jwtUtil.generateAccessToken(userDTO); + String refreshToken = jwtUtil.generateRefreshToken(userDTO); + userDTO.setAccessToken(accessToken); + userDTO.setRefreshToken(refreshToken); + //存储refreshToken到redis + String key = "refresh_token:" + userDTO.getUserId(); + redisUtil.set(key, refreshToken, EXPIRE_TIME, TimeUnit.DAYS); return userDTO; } } diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/dto/UserInfoDTO.java b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/vo/UserInfoVO.java similarity index 77% rename from 珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/dto/UserInfoDTO.java rename to 珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/vo/UserInfoVO.java index 3ea217c..f23205b 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/dto/UserInfoDTO.java +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/java/com/luojia_channel/modules/user/vo/UserInfoVO.java @@ -1,4 +1,4 @@ -package com.luojia_channel.modules.user.dto; +package com.luojia_channel.modules.user.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; @@ -10,8 +10,8 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor -@Schema(description = "用户信息DTO") -public class UserInfoDTO { +@Schema(description = "用户信息VO") +public class UserInfoVO { @Schema( description = "用户ID" ) @@ -56,4 +56,14 @@ public class UserInfoDTO { description = "状态(1正常,2冻结)" ) private Integer status; + + @Schema( + description = "访问令牌" + ) + private String accessToken; + + @Schema( + description = "刷新令牌" + ) + private String refreshToken; } \ No newline at end of file diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application-local.yaml b/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application-local.yaml index 70a6636..3f3466e 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application-local.yaml +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application-local.yaml @@ -14,8 +14,8 @@ password: 123456 minio: endpoint: http://localhost:9000 - accessKey: minioadmin - secretKey: minioadmin + accessKey: root + secretKey: 12345678 #lj: # db: diff --git a/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application.yaml b/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application.yaml index 7cbd4aa..4ac786d 100644 --- a/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application.yaml +++ b/珞珈岛-项目相关文件/luojia-island/service/src/main/resources/application.yaml @@ -65,5 +65,8 @@ mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.luojia.luojia_channel.modules.*.entity - +-management: + - health: + - elasticsearch: + - enabled: false diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Header.vue b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Header.vue index 2dfd8d3..033d092 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Header.vue +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Header.vue @@ -43,7 +43,6 @@ 用户头像
-

{{ userInfo.username }}

修改个人信息 @@ -379,45 +378,38 @@ watch(() => userStore.userInfo, (newInfo) => { z-index: 1000; display: flex; flex-direction: column; - align-items: center; /* 水平居中用户名 */ + align-items: center; /* 水平居中 */ text-align: center; /* 确保文字居中 */ - gap: 10px; /* 增加子元素之间的间距 */ - width: 200px; /* 固定宽度,确保布局一致 */ -} - -/* 用户名样式 */ -.user-name { - font-size: 20px; - font-weight: bold; - margin-bottom: 10px; - color: #54ac52; + width: 140px; /* 减小宽度 */ } +/* 按钮容器样式 */ .button-container { display: flex; flex-direction: column; /* 改为垂直排列 */ gap: 8px; /* 按钮间间距 */ - width: 100%; /* 按钮容器占满父容器宽度 */ + width: 100%; /* 确保按钮宽度一致 */ } -/* 悬浮板块按钮样式 */ +/* 下拉菜单按钮样式 */ .dropdown-btn { - width: 100%; /* 按钮占据父容器的100%宽度 */ - padding: 8px 10px; - background: none; - border: 0; + padding: 8px 0; + background-color: transparent; + border: none; border-radius: 4px; - text-align: center; - font-size: 14px; color: #333; + font-size: 14px; cursor: pointer; - transition: background-color 0.3s, color 0.3s; - text-decoration: none; /* 去除router-link的下划线 */ + transition: background-color 0.2s, color 0.2s; + text-decoration: none; + display: block; + width: 100%; + text-align: center; } .dropdown-btn:hover { - background-color: #f0f0f0; - color: #6fbd87; + background-color: #f0f5ff; + color: #54ac52; } .admin-btn { diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Login.vue b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Login.vue index 04a03ff..19e715e 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Login.vue +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/components/Login.vue @@ -209,16 +209,14 @@ async function login() { const response = await request.post('/user/login', loginData); if (response.code === 200) { - // 登录成功,保存token到本地存储 - const { accessToken, refreshToken } = response.data; - localStorage.setItem('accessToken', accessToken); - localStorage.setItem('refreshToken', refreshToken); - - userStore.login({ - avatar:require ('@/assets/default-avatar/boy_1.png'), - userName: '珈人一号', - userid:1 - }); + // 登录成功,将用户信息传递给用户存储 + const userData = response.data; + + // 确保用户名和ID字段名称一致 + userData.userName = userData.username; + userData.userid = userData.id; + + userStore.login(userData); ElMessage({ message: '登录成功', diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/main.js b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/main.js index 010716c..efbb69b 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/main.js +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/main.js @@ -4,10 +4,48 @@ import router from './router'; // 确保引入了 router import { createPinia } from 'pinia'; import ELementPlus from 'element-plus'; import 'element-plus/dist/index.css'; +import { checkLoginStatus } from './utils/auth'; +import { useUserStore } from './stores/user'; const app = createApp(App); const pinia = createPinia(); // 创建 Pinia 实例 app.use(pinia); // 注册 Pinia + +// 尝试从localStorage恢复用户状态(立即执行,不等待API) +const userStore = useUserStore(); +const userId = localStorage.getItem('userId'); +const username = localStorage.getItem('username'); +const avatar = localStorage.getItem('avatar'); +const role = localStorage.getItem('role'); +const accessToken = localStorage.getItem('accessToken'); +const refreshToken = localStorage.getItem('refreshToken'); + +// 如果本地存储中有用户信息,立即恢复登录状态 +if (userId && username && accessToken && refreshToken) { + userStore.login({ + userid: userId, + userName: username, + avatar: avatar || '', + role: role || 1, + accessToken, + refreshToken + }); + console.log('从本地存储恢复用户登录状态:', username); +} + app.use(router); // 注册 vue-router app.use(ELementPlus); // 注册 element-plus -app.mount('#app'); \ No newline at end of file + +// 挂载应用 +app.mount('#app'); + +// 在应用启动后异步验证登录状态,不阻塞应用启动 +setTimeout(() => { + checkLoginStatus() + .then(isLoggedIn => { + console.log('登录状态验证完成,用户登录状态:', isLoggedIn ? '已登录' : '未登录'); + }) + .catch(error => { + console.error('登录状态验证失败,但不影响应用使用:', error); + }); +}, 500); \ No newline at end of file diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/router/index.js b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/router/index.js index f20a21a..b036fd9 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/router/index.js +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/router/index.js @@ -8,6 +8,9 @@ import NotificationList from '@/views/NotificationList.vue'; import ChangeInformation from '@/views/ChangeInformation.vue'; import FeedBack from '@/views/FeedBack.vue'; import PostPublish from '@/views/PostPublish.vue'; +import { useUserStore } from '@/stores/user.js'; +import { checkLoginStatus } from '@/utils/auth'; +import { ElMessage } from 'element-plus'; const routes = [ { @@ -33,18 +36,21 @@ const routes = [ { path: '/user', name: 'UserPage', - component: UserPage + component: UserPage, + meta: { requiresAuth: true } }, {//通知页面 path: '/notificationlist', name: 'NotificationList', - component: NotificationList + component: NotificationList, + meta: { requiresAuth: true } }, {//详细通知页面 path: '/notification/:id', name: 'NotificationDetail', component: () => import('@/views/NotificationDetail.vue'), - props: true + props: true, + meta: { requiresAuth: true } }, {//反馈页面 path: '/feedback', @@ -55,13 +61,15 @@ const routes = [ //修改个人信息界面 path:'/changeinformation', name:'ChangeInformation', - component:ChangeInformation + component:ChangeInformation, + meta: { requiresAuth: true } }, { // 发布帖子页面 path: '/postpublish', name: 'PostPublish', - component: PostPublish + component: PostPublish, + meta: { requiresAuth: true } }, { // 管理员页面 @@ -95,7 +103,7 @@ const routes = [ component: () => import('@/views/admin/AdminCategories.vue') } ], - meta: { requiresAdmin: true } + meta: { requiresAdmin: true, requiresAuth: true } } ]; @@ -104,4 +112,86 @@ const router = createRouter({ routes }); +// 全局前置守卫 +router.beforeEach(async (to, from, next) => { + const userStore = useUserStore(); + + // 检查路由是否需要登录 + if (to.matched.some(record => record.meta.requiresAuth)) { + // 如果用户未登录,先尝试从localStorage恢复登录状态 + if (!userStore.isLoggedIn) { + console.log('访问需要登录的页面,但用户未登录,尝试恢复登录状态...'); + + // 尝试从localStorage恢复用户状态 + const userId = localStorage.getItem('userId'); + const username = localStorage.getItem('username'); + const accessToken = localStorage.getItem('accessToken'); + const refreshToken = localStorage.getItem('refreshToken'); + const avatar = localStorage.getItem('avatar'); + const role = localStorage.getItem('role'); + + // 如果localStorage中有用户信息,先恢复登录状态 + if (userId && username && accessToken && refreshToken) { + userStore.login({ + userid: userId, + userName: username, + avatar: avatar || '', + role: role || 1, + accessToken, + refreshToken + }); + console.log('从localStorage恢复用户登录状态:', username); + + // 异步验证token有效性,但不阻塞导航 + setTimeout(() => { + checkLoginStatus().catch(err => { + console.error('Token验证失败,但不影响当前导航:', err); + }); + }, 100); + + // 继续导航 + next(); + return; + } + + // 如果localStorage中没有用户信息,尝试通过token验证登录状态 + try { + console.log('尝试通过API验证登录状态...'); + const isLoggedIn = await checkLoginStatus(); + if (!isLoggedIn) { + ElMessage.warning('请先登录'); + next({ path: '/' }); + return; + } + } catch (error) { + console.error('登录状态验证失败:', error); + ElMessage.warning('登录状态验证失败,请重新登录'); + next({ path: '/' }); + return; + } + } + } + + // 检查路由是否需要管理员权限 + if (to.matched.some(record => record.meta.requiresAdmin)) { + // 检查用户是否登录且是否有管理员权限 + if (!userStore.isLoggedIn) { + ElMessage.error('请先登录'); + next({ path: '/' }); + return; + } + + // 检查用户是否有管理员权限 + const role = parseInt(userStore.userInfo.role || localStorage.getItem('role') || 1); + if (!(role === 2 || role === 3)) { + ElMessage.error('您没有管理员权限'); + next({ path: '/' }); + return; + } + } + + // 继续导航 + next(); +}); + export default router; \ No newline at end of file diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/stores/user.js b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/stores/user.js index 7f7b642..c504cec 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/stores/user.js +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/stores/user.js @@ -9,18 +9,65 @@ export const useUserStore = defineStore('user', { avatar: '', // 用户头像 email: '', // 用户邮箱 phone: '', // 用户手机号 - studentId: '', // 学号 college: '', // 学院 gender: 0, // 性别,0-未知,1-男,2-女 + role: 1, // 角色,1-普通用户,2-管理员,3-超级管理员 + status: 1, // 状态,1-正常,2-冻结 }, }), actions: { login(userData) { this.isLoggedIn = true; this.userInfo = { - ...userData, + userid: userData.userid || 0, + username: userData.userName || '', + avatar: userData.avatar || '', + email: userData.email || '', + phone: userData.phone || '', + college: userData.college || '', + gender: userData.gender || 0, + role: userData.role || 1, + status: userData.status || 1 }; + + // 保存令牌到本地存储(如果有的话) + if (userData.accessToken) { + localStorage.setItem('accessToken', userData.accessToken); + } + if (userData.refreshToken) { + localStorage.setItem('refreshToken', userData.refreshToken); + } + + // 保存基本用户信息到localStorage,用于页面刷新时恢复 + localStorage.setItem('userId', this.userInfo.userid); + localStorage.setItem('username', this.userInfo.username); + localStorage.setItem('avatar', this.userInfo.avatar || ''); + localStorage.setItem('role', this.userInfo.role); + + console.log('用户登录成功,状态已更新:', this.userInfo); }, + + // 更新用户信息 + updateUserInfo(userData) { + this.userInfo = { + ...this.userInfo, + username: userData.username || this.userInfo.username, + avatar: userData.avatar || this.userInfo.avatar, + email: userData.email || this.userInfo.email, + phone: userData.phone || this.userInfo.phone, + college: userData.college || this.userInfo.college, + gender: userData.gender !== undefined ? userData.gender : this.userInfo.gender, + role: userData.role || this.userInfo.role, + status: userData.status || this.userInfo.status + }; + + // 更新localStorage中的用户信息 + localStorage.setItem('username', this.userInfo.username); + localStorage.setItem('avatar', this.userInfo.avatar || ''); + + console.log('用户信息已更新:', this.userInfo); + }, + logout() { this.isLoggedIn = false; this.userInfo = { @@ -29,11 +76,21 @@ export const useUserStore = defineStore('user', { avatar: '', email: '', phone: '', - studentId: '', college: '', gender: 0, - isPublic: true + role: 1, + status: 1 }; + + // 清除本地存储的令牌和用户信息 + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('userId'); + localStorage.removeItem('username'); + localStorage.removeItem('avatar'); + localStorage.removeItem('role'); + + console.log('用户已登出'); }, }, }); \ No newline at end of file diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/auth.js b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/auth.js new file mode 100644 index 0000000..8c66371 --- /dev/null +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/auth.js @@ -0,0 +1,132 @@ +import axios from 'axios'; +import { useUserStore } from '@/stores/user'; + + +/** + * 检查用户登录状态 + * 如果本地存储中有token,则尝试验证token有效性 + * @returns {Promise} 是否登录成功 + */ +export async function checkLoginStatus() { + const accessToken = localStorage.getItem('accessToken'); + const refreshToken = localStorage.getItem('refreshToken'); + + // 如果没有token,直接返回未登录 + if (!accessToken || !refreshToken) { + console.log('本地没有找到token,无需验证登录状态'); + return false; + } + + // 检查token格式是否正确 + if (!isValidToken(accessToken) || !isValidToken(refreshToken)) { + console.log('Token格式不正确,清除无效token'); + clearAuthTokens(); + return false; + } + + try { + console.log('正在验证登录状态...'); + // 使用axios直接发送请求,避免request拦截器中可能的循环依赖 + const response = await axios.post('/user/check-login', null, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Refresh-Token': refreshToken + }, + // 设置超时时间,避免长时间等待 + timeout: 5000 + }); + + if (response.data && response.data.code === 200 && response.data.data) { + console.log('登录状态有效,用户信息:', response.data.data); + + // 更新用户状态 + const userStore = useUserStore(); + userStore.login({ + userid: response.data.data.userId, + userName: response.data.data.username, + avatar: response.data.data.avatar, + role: response.data.data.role, + accessToken: response.data.data.accessToken || accessToken, + refreshToken: response.data.data.refreshToken || refreshToken + }); + + // 如果返回了新的token,更新本地存储 + if (response.data.data.accessToken) { + localStorage.setItem('accessToken', response.data.data.accessToken); + } + if (response.data.data.refreshToken) { + localStorage.setItem('refreshToken', response.data.data.refreshToken); + } + + return true; + } else { + console.log('登录状态已过期或无效'); + // 如果后端明确表示token无效,则清除token + if (response.data && response.data.code === 401) { + clearAuthTokens(); + } + return false; + } + } catch (error) { + console.error('验证登录状态失败:', error); + + // 如果是网络错误、超时或服务器未响应,保留token并尝试从localStorage恢复 + if (!error.response || error.code === 'ECONNABORTED') { + console.log('网络错误或服务器未响应,尝试从localStorage恢复登录状态'); + + // 如果localStorage中有用户信息,则维持登录状态 + const userId = localStorage.getItem('userId'); + const username = localStorage.getItem('username'); + const avatar = localStorage.getItem('avatar'); + const role = localStorage.getItem('role'); + + if (userId && username) { + const userStore = useUserStore(); + userStore.login({ + userid: userId, + userName: username, + avatar: avatar || '', + role: role || 1, + accessToken: accessToken, + refreshToken: refreshToken + }); + console.log('从localStorage成功恢复用户登录状态:', username); + return true; + } + + return false; + } + + // 只有在确认token格式错误时才清除 + if (error.response && error.response.status === 500 && + error.response.data?.message?.includes('token')) { + console.log('Token格式错误,清除token'); + clearAuthTokens(); + } + return false; + } +} + +/** + * 检查token格式是否有效 + * @param {string} token 要检查的token + * @returns {boolean} 是否有效 + */ +function isValidToken(token) { + // 简单检查token是否为字符串且长度合理 + return typeof token === 'string' && token.length > 10; +} + +/** + * 清除认证令牌 + */ +export function clearAuthTokens() { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('userId'); + localStorage.removeItem('username'); + localStorage.removeItem('avatar'); + localStorage.removeItem('role'); + const userStore = useUserStore(); + userStore.logout(); +} \ No newline at end of file diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/request.js b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/request.js index beb9a31..bd69ba8 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/request.js +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/utils/request.js @@ -1,9 +1,43 @@ import axios from 'axios'; +import { clearAuthTokens } from './auth'; +import { ElMessage } from 'element-plus'; const request = axios.create({ timeout: 5000 }); +// 是否正在刷新token +let isRefreshing = false; +// 等待刷新token的请求队列 +let requestsQueue = []; + +// 执行队列中的请求 +const processQueue = (error, token = null) => { + requestsQueue.forEach(prom => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + requestsQueue = []; +}; + +// 判断是否是需要登录才能访问的API +const isAuthRequiredApi = (url) => { + // 这些API需要登录才能访问,如果未登录会返回401 + const authRequiredApis = [ + '/user/info', + '/post/publish', + '/notification', + '/user/info/avatar', + '/user/info/update', + '/user/info/password' + ]; + + return authRequiredApis.some(api => url.includes(api)); +}; + //响应拦截器 request.interceptors.response.use( response => { @@ -14,6 +48,106 @@ request.interceptors.response.use( }, error => { console.error('请求错误:', error); + + // 如果响应状态码是401或500,可能是token过期或格式错误 + if (error.response && (error.response.status === 401 || + (error.response.status === 500 && error.response.data?.message?.includes('token')))) { + const originalRequest = error.config; + + // 判断是否是需要登录的API + const needAuth = isAuthRequiredApi(originalRequest.url); + + // 如果是500错误且与token相关,直接清除token并拒绝请求 + if (error.response.status === 500 && error.response.data?.message?.includes('token')) { + console.error('Token格式错误:', error.response.data.message); + clearAuthTokens(); + if (needAuth) { + ElMessage.warning('登录已失效,请重新登录'); + } + return Promise.reject(error); + } + + // 防止重复刷新token + if (!isRefreshing) { + isRefreshing = true; + + // 尝试使用refreshToken获取新的accessToken + const refreshToken = localStorage.getItem('refreshToken'); + const accessToken = localStorage.getItem('accessToken'); + + if (!refreshToken || !accessToken || typeof refreshToken !== 'string' || typeof accessToken !== 'string') { + if (needAuth) { + ElMessage.warning('请先登录'); + } + // 只有当token格式明显不正确时才清除 + if ((refreshToken && typeof refreshToken !== 'string') || + (accessToken && typeof accessToken !== 'string')) { + clearAuthTokens(); + } + isRefreshing = false; + return Promise.reject(error); + } + + // 调用check-login接口刷新token + return axios.post('/user/check-login', null, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Refresh-Token': refreshToken + } + }).then(res => { + if (res.data && res.data.code === 200 && res.data.data) { + // 更新token + const { accessToken, refreshToken } = res.data.data; + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('refreshToken', refreshToken); + + // 更新原始请求的Authorization头 + originalRequest.headers['Authorization'] = `Bearer ${accessToken}`; + originalRequest.headers['X-Refresh-Token'] = refreshToken; + + // 处理队列中的请求 + processQueue(null, accessToken); + + // 重新发送原始请求 + return axios(originalRequest); + } else { + processQueue(new Error('刷新Token失败'), null); + if (needAuth) { + // 不清除token,只提示用户 + ElMessage.warning('请先登录'); + } + return Promise.reject(error); + } + }).catch(err => { + processQueue(err, null); + if (needAuth) { + // 只有在确认token格式错误时才清除 + if (err.response && err.response.status === 500 && + err.response.data?.message?.includes('token')) { + clearAuthTokens(); + } + ElMessage.warning('请先登录'); + } + return Promise.reject(err); + }).finally(() => { + isRefreshing = false; + }); + } else { + // 将请求加入队列 + return new Promise((resolve, reject) => { + requestsQueue.push({ + resolve: token => { + originalRequest.headers['Authorization'] = `Bearer ${token}`; + resolve(axios(originalRequest)); + }, + reject: err => { + reject(err); + } + }); + }); + } + } + return Promise.reject(error); } ); @@ -21,13 +155,31 @@ request.interceptors.response.use( //请求拦截器 request.interceptors.request.use( config => { - console.log('Request:',config); - if (!config.url.includes('/captcha') && !config.url.includes('/verify-captcha')) { + console.log('Request:', config.url); + // 只对非认证相关的API添加token + if (!config.url.includes('/captcha') && !config.url.includes('/verify-captcha') && + !config.url.includes('/user/login') && !config.url.includes('/user/register')) { const token = localStorage.getItem('accessToken'); const refreshToken = localStorage.getItem('refreshToken'); - if (token) { + + // 检查token格式是否正确 + if (token && refreshToken && typeof token === 'string' && typeof refreshToken === 'string') { config.headers['Authorization'] = `Bearer ${token}`; - config.headers['X-Refresh-Token'] = refreshToken; + config.headers['X-Refresh-Token'] = refreshToken; + console.log('添加token到请求头:', config.url); + } else if ((token && typeof token !== 'string') || + (refreshToken && typeof refreshToken !== 'string')) { + // 只有当token格式明显错误时才清除 + console.warn('Token格式不正确,清除token'); + clearAuthTokens(); + } else if (!token || !refreshToken) { + // 如果没有token但访问需要认证的API,尝试从localStorage恢复 + const userId = localStorage.getItem('userId'); + const username = localStorage.getItem('username'); + + if (userId && username) { + console.warn('访问需要认证的API但没有token,可能需要重新登录'); + } } } return config; diff --git a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/views/ChangeInformation.vue b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/views/ChangeInformation.vue index 1be8a30..1777486 100644 --- a/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/views/ChangeInformation.vue +++ b/珞珈岛-项目相关文件/luojia-island/vue-frontend/src/views/ChangeInformation.vue @@ -6,7 +6,12 @@

头像

- 当前头像 + 当前头像 新头像预览