实现登录联通

lzt
哆哆咯哆哆咯 4 weeks ago
parent 26c19221d8
commit 27f48ff43a

@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -1,7 +1,21 @@
package com.luojia_channel.common.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
// 设置单页最大限制(如不限制可设为 -1
paginationInterceptor.setMaxLimit(1000L);
// 设置是否溢出总页数true自动调整页码false抛出异常
paginationInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}

@ -12,7 +12,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
// 拦截器
registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login",
"/user/register"

@ -0,0 +1,7 @@
package com.luojia_channel.common.exception;
public class FileException extends BaseException{
public FileException(String msg){
super(500, msg);
}
}

@ -34,7 +34,6 @@ public class AuthInterceptor implements HandlerInterceptor {
response.setHeader("New-Access-Token", JWTUtil.TOKEN_PREFIX + user.getAccessToken());
response.setHeader("New-Refresh-Token", user.getRefreshToken());
}
// 将用户信息存入请求上下文
UserContext.setUser(user);
return true;
} catch (UserException ex) {

@ -27,7 +27,7 @@ public final class JWTUtil {
private static final long ACCESS_EXPIRATION = 60 * 60 * 1000; //一小时
private static final long REFRESH_EXPIRATION = 60 * 60 * 24 * 15 * 1000; //15天
private static final Long NEED_REFRESH_TTL = 7L;
private static final long NEED_REFRESH_TTL = 60 * 60 * 24 * 7 * 1000; //7天
private static final String USER_ID_KEY = "userId";
private static final String USER_NAME_KEY = "username";
public static final String TOKEN_PREFIX = "Bearer ";
@ -144,7 +144,7 @@ public final class JWTUtil {
String newAccessToken = generateAccessToken(user);
String newRefreshToken = generateRefreshToken(user);
// 惰性刷新refreshToken
Long ttl = redisUtil.getExpire(redisKey, TimeUnit.DAYS);
Long ttl = redisUtil.getExpire(redisKey, TimeUnit.MILLISECONDS);
if(ttl < NEED_REFRESH_TTL)
redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS);
user.setAccessToken(newAccessToken);

@ -48,7 +48,7 @@ public class RedisUtil {
public boolean hasKey(String key) {
Boolean result = redisTemplate.hasKey(key);
return result != null;
return result != null && result;
}
public Long getExpire(String key, TimeUnit timeUnit){

@ -0,0 +1,6 @@
{
"name": "luojia-island",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

@ -59,6 +59,7 @@
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@ -81,6 +82,12 @@
<version>1.2.83</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
@ -88,6 +95,12 @@
<version>5.8.24</version>
</dependency>
<!-- minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.12</version>
</dependency>
</dependencies>
<build>

@ -20,12 +20,6 @@
<version>1.0.0</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -49,6 +43,7 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
@ -60,6 +55,7 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

@ -0,0 +1,31 @@
package com.luojia_channel.modules.file.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
try {
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
return minioClient;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}

@ -0,0 +1,7 @@
package com.luojia_channel.modules.file.constants;
public class FileConstant {
public static final String CHUNK_BUCKET = "chunks";
public static final String CHUNK_PREFIX = "file:chunks:";
public static final long MAX_UPLOAD_SIZE = 10*1024*1024;
}

@ -0,0 +1,17 @@
package com.luojia_channel.modules.file.dto;
import lombok.Builder;
import lombok.Data;
// 合并分片DTO
@Data
@Builder
public class CompleteUploadDTO {
private String fileMd5;
private Integer totalChunks;
private String fileType;
private String fileName;
}

@ -0,0 +1,22 @@
package com.luojia_channel.modules.file.dto;
import lombok.Builder;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Builder
public class UploadChunkDTO {
// 分片文件
private MultipartFile file;
// 整个文件的md5
private String fileMd5;
// 分片序号从0开始
private Integer chunkNumber;
// 分片总数
private Integer totalChunks;
private String fileType;
private String fileName;
}

@ -0,0 +1,15 @@
package com.luojia_channel.modules.file.dto;
import lombok.Builder;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Builder
public class UploadFileDTO {
private MultipartFile file;
// 文件类型image or video
private String fileType;
// 文件md5
private String fileMd5;
}

@ -0,0 +1,32 @@
package com.luojia_channel.modules.file.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("lj_file")
@Builder
public class LjFile {
private Long id;
private String fileName;
private String fileUrl;
private Long fileSize;
private String fileMd5;
private String fileType;
private Integer fileStatus; // 0正在上传1上传成功2失败3待审核
private Long userId; // 上传用户id
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

@ -0,0 +1,9 @@
package com.luojia_channel.modules.file.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.file.entity.LjFile;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LjFileMapper extends BaseMapper<LjFile> {
}

@ -0,0 +1,15 @@
package com.luojia_channel.modules.file.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.modules.file.dto.CompleteUploadDTO;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.entity.LjFile;
public interface FileService extends IService<LjFile> {
Boolean createBucket(String name);
Boolean deleteBucket(String name);
Long uploadFile(UploadFileDTO uploadFileDTO);
Boolean uploadChunk(UploadChunkDTO chunkDTO);
Long completeUpload(CompleteUploadDTO completeDTO);
}

@ -0,0 +1,267 @@
package com.luojia_channel.modules.file.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.exception.FileException;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.dto.CompleteUploadDTO;
import com.luojia_channel.modules.file.entity.LjFile;
import com.luojia_channel.modules.file.mapper.LjFileMapper;
import com.luojia_channel.modules.file.service.FileService;
import com.luojia_channel.modules.file.utils.GeneratePathUtil;
import com.luojia_channel.modules.file.utils.ValidateFileUtil;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_BUCKET;
import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_PREFIX;
@Service
@Slf4j
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<LjFileMapper, LjFile> implements FileService {
private final MinioClient minioClient;
private final RedisUtil redisUtil;
private final ValidateFileUtil validateFileUtil;
private final GeneratePathUtil generatePathUtil;
private void init(){
createBucket("videos");
createBucket("images");
createBucket("chunks");
}
@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());
}
} catch (Exception e) {
throw new FileException("创建桶失败");
}
return true;
}
@Override
public Boolean deleteBucket(String name){
try {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(name).build());
} catch (Exception e) {
throw new FileException("删除桶失败");
}
return true;
}
// 普通上传
@Override
@Transactional(rollbackFor = Exception.class)
public Long uploadFile(UploadFileDTO fileDTO) {
validateFileUtil.validateFile(fileDTO);
Long fileId = validateFileUtil.getExistedFileId(fileDTO.getFileMd5());
if(fileId != null){
return fileId;
}
// 普通上传
try {
String fileName = fileDTO.getFile().getOriginalFilename();
String bucket = generatePathUtil.getBucketName(fileDTO.getFileType());
String objectName = generatePathUtil.getObjectName(fileName,
fileDTO.getFileMd5());
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(fileDTO.getFile().getInputStream(),
fileDTO.getFile().getSize(), -1)
.build()
);
return saveFileToDB(fileName, objectName, fileDTO.getFile().getSize(),
fileDTO.getFileMd5(), fileDTO.getFileType(), 1);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage());
throw new FileException("文件上传失败");
}
}
private Long saveFileToDB(String fileName, String fileUrl, Long fileSize,
String fileMd5, String fileType, Integer fileStatus){
Long userId = UserContext.getUserId();
if(userId == null){
throw new FileException("不存在的用户试图上传文件");
}
LjFile file= LjFile.builder()
.fileName(fileName)
.fileUrl(fileUrl)
.fileSize(fileSize)
.fileMd5(fileMd5)
.fileType(fileType)
.fileStatus(fileStatus)
.userId(userId)
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.build();
if (!save(file)) {
throw new FileException("文件记录保存失败");
}
return file.getId();
}
// 分片上传
@Override
public Boolean uploadChunk(UploadChunkDTO chunkDTO) {
// 验证分片信息
validateFileUtil.validateChunk(chunkDTO);
// 判断整个文件是否存在
if (validateFileUtil.getExistedFileId(chunkDTO.getFileMd5()) != null) {
return true;
}
// 检查分片是否已上传
if (validateFileUtil.isChunkUploaded(chunkDTO.getFileMd5(), chunkDTO.getChunkNumber())) {
return true;
}
// 保存分片到MinIO临时Bucket
String objectName = generatePathUtil
.getChunkObjectName(chunkDTO.getFileMd5(), chunkDTO.getChunkNumber());
// 一小时内保存进度
String key = CHUNK_PREFIX + chunkDTO.getFileMd5();
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(CHUNK_BUCKET)
.object(objectName)
.stream(chunkDTO.getFile().getInputStream(),
chunkDTO.getFile().getSize(), -1)
.build()
);
redisUtil.sAdd(key, chunkDTO.getChunkNumber());
redisUtil.expire(key, 1, TimeUnit.HOURS);
return true;
} catch (Exception e) {
// 遇到异常删除已上传的分片
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(CHUNK_BUCKET)
.object(objectName)
.build());
redisUtil.sRemove(key, chunkDTO.getChunkNumber());
} catch (Exception ex) {
log.error("删除分片失败: {}", ex.getMessage(), ex);
}
log.error("分片上传失败: {}", e.getMessage());
throw new FileException("分片上传失败");
}
}
// 合并分片
@Override
@Transactional(rollbackFor = Exception.class)
public Long completeUpload(CompleteUploadDTO completeDTO) {
// 验证分片完整性
if (!validateFileUtil.
areAllChunksUploaded(completeDTO.getFileMd5(), completeDTO.getTotalChunks())) {
throw new FileException("存在未上传的分片");
}
// 合并分片到目标Bucket
String bucket = generatePathUtil.getBucketName(completeDTO.getFileType());
String objectName = generatePathUtil
.getObjectName(completeDTO.getFileName(), completeDTO.getFileMd5());
try {
List<ComposeSource> sources = new ArrayList<>();
for (int i = 0; i < completeDTO.getTotalChunks(); i++) {
String chunkObjectName = generatePathUtil
.getChunkObjectName(completeDTO.getFileMd5(), i);
sources.add(
ComposeSource.builder()
.bucket(CHUNK_BUCKET)
.object(chunkObjectName)
.build());
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.sources(sources)
.bucket(bucket)
.object(objectName)
.build());
// 获得文件大小
long fileSize = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build()
).size();
// 保存文件到数据库
Long fileId = saveFileToDB(completeDTO.getFileName(), objectName, fileSize,
completeDTO.getFileMd5(), completeDTO.getFileType(), 3);
// 删除临时分片
deleteChunks(completeDTO.getFileMd5());
return fileId;
} catch (Exception e) {
log.error("合并分片失败: {}", e.getMessage());
throw new FileException("合并分片失败");
}
}
// 删除临时分片,与遍历删除相比该操作只需要进行一次io
public void deleteChunks(String fileMd5) {
try {
// 获取需要删除的对象列表
Iterable<Result<Item>> items = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(CHUNK_BUCKET)
.prefix(fileMd5 + "/")
.build()
);
// 转换为 DeleteObject 列表
List<DeleteObject> deleteObjects = new ArrayList<>();
for (Result<Item> result : items) {
Item item = result.get();
log.info("要删除的分片文件:{}", item.objectName());
deleteObjects.add(new DeleteObject(item.objectName()));
}
// 执行删除操作
if (!deleteObjects.isEmpty()) {
log.info("正在删除 {} 个分片文件", deleteObjects.size());
Iterable<Result<DeleteError>> results = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(CHUNK_BUCKET)
.objects(deleteObjects)
.build()
);
// 遍历结果以触发删除
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
if (error != null) {
log.error("删除文件 {} 失败: {}", error.objectName(), error.message());
}
}
redisUtil.delete(CHUNK_PREFIX + fileMd5);
log.info("分片删除完成");
} else {
log.warn("未找到需要删除的分片文件fileMd5={}", fileMd5);
}
} catch (Exception e) {
log.error("删除分片失败: {}", e.getMessage(), e);
throw new FileException("删除分片失败");
}
}
}

@ -0,0 +1,35 @@
package com.luojia_channel.modules.file.utils;
import com.luojia_channel.common.exception.FileException;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component
public class GeneratePathUtil {
public String getBucketName(String fileType) {
return switch (fileType) {
case "image" -> "images";
case "video" -> "videos";
default -> throw new FileException("无效的文件类型");
};
}
// 分片对象路径格式fileMd5/chunk_{number}
public String getChunkObjectName(String fileMd5, int chunkNumber) {
return String.format("%s/chunk_%d", fileMd5, chunkNumber);
}
// year/month/day/fileMd5/suffix
public String getObjectName(String fileName, String fileMd5) {
String suffix = fileName.contains(".") ?
fileName.substring(fileName.lastIndexOf(".")) : "";
LocalDate now = LocalDate.now();
return String.format("%d/%02d/%02d/%s%s",
now.getYear(),
now.getMonthValue(),
now.getDayOfMonth(),
fileMd5,
suffix);
}
}

@ -0,0 +1,102 @@
package com.luojia_channel.modules.file.utils;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.luojia_channel.common.exception.FileException;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.entity.LjFile;
import com.luojia_channel.modules.file.mapper.LjFileMapper;
import io.minio.MinioClient;
import io.minio.StatObjectArgs;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import static com.luojia_channel.modules.file.constants.FileConstant.*;
@Component
@RequiredArgsConstructor
public class ValidateFileUtil {
private final LjFileMapper ljFileMapper;
private final RedisUtil redisUtil;
private final GeneratePathUtil generatePathUtil;
private final MinioClient minioClient;
// 验证分片参数
public void validateChunk(UploadChunkDTO chunkDTO) {
if (chunkDTO.getFile().isEmpty()) {
throw new FileException("分片数据不能为空");
}
if (StrUtil.isBlank(chunkDTO.getFileMd5())) {
throw new FileException("文件MD5缺失");
}
if (chunkDTO.getChunkNumber() < 0) {
throw new FileException("分片编号不能为负数");
}
if (chunkDTO.getTotalChunks() <= 0) {
throw new FileException("总分片数必须大于0");
}
if(chunkDTO.getChunkNumber() >= chunkDTO.getTotalChunks()){
throw new FileException("分片序号不能大于分片总数");
}
}
// 校验普通文件上传
public void validateFile(UploadFileDTO dto) {
if (dto.getFile().isEmpty()) {
throw new FileException("文件不能为空");
}
// MD5校验
if (StrUtil.isBlank(dto.getFileMd5())) {
throw new FileException("文件MD5缺失");
}
// 类型校验
if (!Arrays.asList("image", "video").contains(dto.getFileType())) {
throw new FileException("不支持的文件类型");
}
// 大小校验
if(dto.getFile().getSize() > MAX_UPLOAD_SIZE){
throw new FileException("上传文件大小过大");
}
}
// 秒传检查
public Long getExistedFileId(String fileMd5) {
LjFile file = ljFileMapper.selectOne(Wrappers.<LjFile>lambdaQuery()
.eq(LjFile::getFileMd5, fileMd5));
if(file == null){
return null;
}
return file.getId();
}
// 检查分片是否已上传
public boolean isChunkUploaded(String fileMd5, int chunkNumber) {
String objectName = generatePathUtil.getChunkObjectName(fileMd5, chunkNumber);
String redisKey = CHUNK_PREFIX + fileMd5;
// 检查redis中是否已记录该分片
if (redisUtil.sIsMember(redisKey, chunkNumber)) {
return true;
}
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(CHUNK_BUCKET)
.object(objectName)
.build()
);
return true;
} catch (Exception e) {
return false;
}
}
// 检查所有分片是否已上传
public boolean areAllChunksUploaded(String fileMd5, int totalChunks) {
String key = CHUNK_PREFIX + fileMd5;
List<Integer> uploadedChunks = redisUtil.sGet(key);
return uploadedChunks.size() == totalChunks;
}
}

@ -1,30 +0,0 @@
package com.luojia_channel.modules.user.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) //禁用CSRF保护
.cors(cors -> cors.disable()) //禁用CORS测试似乎需要没有详细测试过
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login",
"/user/register",
"/user/hello").permitAll() //允许所有用户访问登录和注册接口
.anyRequest().authenticated() //其他请求需要认证
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

@ -1,11 +1,38 @@
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.service.UserInfoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/user/info")
@RequiredArgsConstructor
public class UserInfoController {
private final UserInfoService userInfoService;
@PostMapping("/update")
public Result<Void> updateInfo(@RequestBody UserChangeInfoDTO userChangeInfoDTO){
userInfoService.updateInfo(userChangeInfoDTO);
return Result.success();
}
@PostMapping("/password")
public Result<Void> updatePassword(@RequestParam String password){
userInfoService.updatePassword(password);
return Result.success();
}
@PostMapping("/avatar")
public Result<Void> updateAvatar(@RequestParam("file") MultipartFile file,
@RequestParam("fileType") String fileType,
@RequestParam("fileMd5") String fileMd5) {
UploadFileDTO fileDTO = UploadFileDTO.builder()
.file(file).fileType(fileType).fileMd5(fileMd5)
.build();
userInfoService.updateAvatar(fileDTO);
return Result.success();
}
}

@ -5,6 +5,7 @@ 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.service.UserLoginService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -24,8 +25,8 @@ public class UserLoginController {
}
@PostMapping("/logout")
public Result logout(@RequestParam String accessToken){
userLoginService.logout(accessToken);
public Result<Void> logout(HttpServletRequest request){
userLoginService.logout(request);
return Result.success();
}

@ -0,0 +1,20 @@
package com.luojia_channel.modules.user.dto;
import lombok.Data;
@Data
public class UserChangeInfoDTO {
private String username;
private String phone;
private String email;
private String studentId;
private String avatar;
private Integer gender;
private String college;
}

@ -6,5 +6,6 @@ import lombok.Data;
public class UserLoginDTO {
// 用户标志,支持学号,手机号,邮箱
private String userFlag;
private String password;
}

@ -6,8 +6,6 @@ import lombok.Data;
public class UserRegisterDTO {
private String username;
private String realName;
private String password;
private String phone;
@ -15,6 +13,4 @@ public class UserRegisterDTO {
private String email;
private String studentId;
private String captcha;
}

@ -14,44 +14,29 @@ public class User implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
*
*/
private String username;
/**
*
*/
private String realName;
/**
*
*/
private String password;
/**
*
*/
private String phone;
/**
*
*/
private String email;
private String studentId;
/**
*
* url
*/
private String studentId;
private String avatar;
/**
* (012)
*/
private Integer gender;
/**
*
*/
private LocalDateTime createTime;
private LocalDateTime updateTime;

@ -1,7 +1,16 @@
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.entity.User;
import org.springframework.web.multipart.MultipartFile;
public interface UserInfoService extends IService<User> {
void updateInfo(UserChangeInfoDTO userChangeInfoDTO);
void updatePassword(String password);
void updateAvatar(UploadFileDTO uploadFileDTO);
}

@ -5,6 +5,7 @@ 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 jakarta.servlet.http.HttpServletRequest;
public interface UserLoginService extends IService<User> {
@ -12,7 +13,7 @@ public interface UserLoginService extends IService<User> {
UserDTO checkLogin(String accessToken, String refreshToken);
void logout(String accessToken);
void logout(HttpServletRequest request);
UserDTO register(UserRegisterDTO userRegisterDTO);
}

@ -1,13 +1,99 @@
package com.luojia_channel.modules.user.service.impl;
import cn.hutool.core.bean.BeanUtil;
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.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import com.luojia_channel.modules.user.service.UserInfoService;
import com.luojia_channel.modules.user.utils.ValidateUserUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.io.InputStream;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class UserInfoServiceImpl extends ServiceImpl<UserMapper, User> implements UserInfoService {
private final UserMapper userMapper;
private final ValidateUserUtil validateUserUtil;
private final FileServiceImpl minioService;
private final GeneratePathUtil generatePathUtil;
private final ValidateFileUtil validateFileUtil;
@Override
public void updateInfo(UserChangeInfoDTO userChangeInfoDTO) {
Long userId = UserContext.getUserId();
User currentUser = userMapper.selectById(userId);
if(currentUser == null){
throw new UserException("用户不存在");
}
validateUserUtil.validateFormats(userChangeInfoDTO, userId);
User user = BeanUtil.copyProperties(userChangeInfoDTO, User.class);
user.setId(userId);
user.setUpdateTime(LocalDateTime.now());
updateById(user);
}
@Override
public void updatePassword(String password) {
Long userId = UserContext.getUserId();
User user = userMapper.selectById(userId);
if(user == null){
throw new UserException("用户不存在");
}
if (!password.matches(ValidateUserUtil.PASSWORD_REGEX)) {
throw new UserException("密码格式不符合要求");
}
if (BCrypt.checkpw(password, user.getPassword())) {
throw new UserException("修改密码不能与原密码相同");
}
String encodedPassword = BCrypt.hashpw(password, BCrypt.gensalt());
user.setPassword(encodedPassword);
user.setUpdateTime(LocalDateTime.now());
updateById(user);
}
@Override
public void updateAvatar(UploadFileDTO uploadFileDTO) {
Long userId = UserContext.getUserId();
User user = userMapper.selectById(userId);
if(user == null){
throw new UserException("用户不存在");
}
validateFileUtil.validateFile(uploadFileDTO);
try {
// TODO 下述工作应该是前端干的----------
/*
InputStream inputStream = uploadFileDTO.getFile().getInputStream();
String fileMd5 = DigestUtils.md5DigestAsHex(inputStream);
String fileType = "image";
uploadFileDTO.setFileType(fileType);
uploadFileDTO.setFileMd5(fileMd5);
// TODO ending---------
*/
minioService.uploadFile(uploadFileDTO);
String filePath = generatePathUtil
.getObjectName(uploadFileDTO.getFile().getOriginalFilename(),
uploadFileDTO.getFileMd5());
// 直接设置成url省去一次读数据库操作
user.setAvatar(filePath);
updateById(user);
} catch (Exception e) {
throw new UserException("上传头像失败");
}
}
}

@ -2,6 +2,7 @@ package com.luojia_channel.modules.user.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -15,7 +16,8 @@ import com.luojia_channel.modules.user.mapper.UserMapper;
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.ValidateParameterUtil;
import com.luojia_channel.modules.user.utils.ValidateUserUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -35,7 +37,7 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
private final UserMapper userMapper;
private final RedisUtil redisUtil;
private final JWTUtil jwtUtil;
private final ValidateParameterUtil validateParameterUtil;
private final ValidateUserUtil validateUserUtil;
/**
*
@ -46,17 +48,17 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
if (StrUtil.isBlank(userFlag)) {
throw new UserException("用户标识不能为空");
}
// 使用正则表达式判断类型,之前直接判断长度,虽然不合法的数据在数据库中仍然查不到
//使用正则表达式判断类型,之前直接判断长度,虽然不合法的数据在数据库中仍然查不到
boolean isEmail = userFlag.contains("@");
boolean isPhone = !isEmail && userFlag.matches(ValidateParameterUtil.PHONE_REGEX);
boolean isPhone = !isEmail && userFlag.matches(ValidateUserUtil.PHONE_REGEX);
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
if (isEmail) {
wrapper.eq(User::getEmail, userFlag);
} else if (isPhone) {
wrapper.eq(User::getPhone, userFlag);
} else {
// 默认学号登录
wrapper.eq(User::getStudentId, userFlag);
//默认用户名登录
wrapper.eq(User::getUsername, userFlag);
}
User user = userMapper.selectOne(wrapper);
if (user == null) {
@ -89,9 +91,8 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
public UserDTO login(UserLoginDTO userLoginDTO) {
String userFlag = userLoginDTO.getUserFlag();
String password = userLoginDTO.getPassword();
// TODO 选择密码加密格式
User user = getUserByFlag(userFlag);
if(!user.getPassword().equals(password)) {
if (!BCrypt.checkpw(password, user.getPassword())) {
throw new UserException("密码错误");
}
UserDTO userDTO = UserDTO.builder()
@ -115,11 +116,14 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
/**
*
* @param accessToken
*/
@Override
public void logout(String accessToken) {
public void logout(HttpServletRequest request) {
String accessToken = request.getHeader("Authorization");
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户不存在");
}
// 删除refreshToken
String refreshKey = REFRESH_TOKEN_PREFIX + userId;
redisUtil.delete(refreshKey);
@ -141,8 +145,11 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
@Transactional(rollbackFor = Exception.class)
public UserDTO register(UserRegisterDTO userRegisterDTO) {
// 校验注册参数
validateParameterUtil.validateUser(userRegisterDTO);
validateUserUtil.validateRegisterUser(userRegisterDTO);
User user = BeanUtil.copyProperties(userRegisterDTO, User.class);
// 加密
String encodedPassword = BCrypt.hashpw(userRegisterDTO.getPassword(), BCrypt.gensalt());
user.setPassword(encodedPassword);
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
save(user);

@ -3,6 +3,7 @@ package com.luojia_channel.modules.user.utils;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
@ -11,12 +12,12 @@ import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ValidateParameterUtil {
public class ValidateUserUtil {
// 参数校验正则表达式,学号校验不一定正确
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
public static final String STUDENTID_REGEX = "^(?:\\d{12,13}|[A-Z]{2}\\d{8,10})$";
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
public static final String PASSWORD_REGEX = "^\\w{6,32}$";
private final UserMapper userMapper;
/**
@ -24,7 +25,7 @@ public class ValidateParameterUtil {
*
* @param userRegisterDTO
*/
public void validateUser(UserRegisterDTO userRegisterDTO) {
public void validateRegisterUser(UserRegisterDTO userRegisterDTO) {
String username = userRegisterDTO.getUsername();
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
@ -54,15 +55,12 @@ public class ValidateParameterUtil {
// 格式校验,未来更改用户信息时可能使用
// TODO 实际上,用户更改信息校验时数据库查询的不是是否存在,而是是否等于要修改的用户
public void validateFormats(UserRegisterDTO userRegisterDTO) {
String username = userRegisterDTO.getUsername();
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
String captcha = userRegisterDTO.getCaptcha();
// 仅对非空字段做格式校验
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username))) {
@ -71,11 +69,31 @@ public class ValidateParameterUtil {
if(!password.matches(PASSWORD_REGEX)){
throw new UserException("密码格式错误");
}
validateUserFlag(phone, email, studentId, null);
}
public void validateFormats(UserChangeInfoDTO userChangeInfoDTO, Long currentUserId){
String username = userChangeInfoDTO.getUsername();
String phone = userChangeInfoDTO.getPhone();
String email = userChangeInfoDTO.getEmail();
String studentId = userChangeInfoDTO.getStudentId();
// String college = userChangeInfoDTO.getCollege();
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, userChangeInfoDTO.getUsername())
.ne(currentUserId != null, User::getId, currentUserId))) {
throw new UserException("用户名已被使用");
}
validateUserFlag(phone, email, studentId, currentUserId);
}
private void validateUserFlag(String phone, String email, String studentId, Long currentUserId) {
if(StrUtil.isNotBlank(phone)){
if(!phone.matches(PHONE_REGEX))
throw new UserException("手机号格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getPhone, phone))) {
.eq(User::getPhone, phone)
.ne(currentUserId != null, User::getId, currentUserId))) {
throw new UserException("手机已存在");
}
}
@ -83,7 +101,8 @@ public class ValidateParameterUtil {
if(!email.matches(EMAIL_REGEX))
throw new UserException("邮箱格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getEmail, email))) {
.eq(User::getEmail, email)
.ne(currentUserId != null, User::getId, currentUserId))) {
throw new UserException("邮箱已存在");
}
}
@ -91,7 +110,8 @@ public class ValidateParameterUtil {
if(!studentId.matches(STUDENTID_REGEX))
throw new UserException("学号格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getStudentId, studentId))) {
.eq(User::getStudentId, studentId)
.ne(currentUserId != null, User::getId, currentUserId))) {
throw new UserException("学号已存在");
}
}

@ -1,22 +1,8 @@
# 本地开发环境
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
lj:
db:
host: localhost
passwprd: 123456
password: 123456
redis:
host: localhost
port: 6379
@ -26,3 +12,8 @@ lj:
port: 15672
username: root
password: 123456
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin

@ -36,6 +36,11 @@ spring:
concurrency: 5
max-concurrency: 10
prefetch: 1
# minio配置
minio:
endpoint: ${lj.minio.endpoint}
accessKey: ${lj.minio.accessKey}
secretKey: ${lj.minio.secretKey}
mybatis-plus:
configuration:

@ -1,4 +1,6 @@
## 用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
@ -7,6 +9,7 @@ CREATE TABLE `user` (
`phone` VARCHAR(20) UNIQUE COMMENT '注册手机号',
`email` VARCHAR(100) UNIQUE COMMENT '邮箱',
`student_id` VARCHAR(20) UNIQUE COMMENT '学号',
`avatar` VARCHAR(255) COMMENT '头像URL',
`gender` TINYINT DEFAULT 0 COMMENT '性别0未知1男2女',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@ -19,3 +22,84 @@ CREATE TABLE `user` (
UNIQUE INDEX `uk_email` (`email`),
UNIQUE INDEX `uk_student_id` (`student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
## 文件表
DROP TABLE IF EXISTS `lj_file`;
CREATE TABLE `lj_file` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名',
`file_url` VARCHAR(512) NOT NULL COMMENT 'MinIO存储路径',
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
`file_md5` VARCHAR(32) NOT NULL COMMENT '文件MD5值',
`file_type` VARCHAR(50) NOT NULL COMMENT '文件类型image/video',
`file_status` INT NOT NULL DEFAULT 0 COMMENT '文件状态0:正在上传, 1:上传成功, 2:失败或删除, 3:审核中)',
`user_id` BIGINT NOT NULL COMMENT '上传用户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件存储表';
## 图文帖子表
DROP TABLE IF EXISTS `post`;
CREATE TABLE `post` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`title` VARCHAR(255) NOT NULL COMMENT '标题',
`cover_id` BIGINT NOT NULL COMMENT '封面图片ID',
`content` TEXT NOT NULL COMMENT '文字内容',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
`comment_count` INT DEFAULT 0 COMMENT '评论数',
`favorite_count` INT DEFAULT 0 COMMENT '收藏数',
`user_id` BIGINT NOT NULL COMMENT '发布用户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`cover_id`) REFERENCES `lj_file`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图文帖子表';
## 帖子图片关联表
DROP TABLE IF EXISTS `post_image`;
CREATE TABLE `post_image` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`post_id` BIGINT NOT NULL COMMENT '图文帖子ID',
`file_id` BIGINT NOT NULL COMMENT '图片文件ID',
FOREIGN KEY (`post_id`) REFERENCES `post`(`id`),
FOREIGN KEY (`file_id`) REFERENCES `lj_file`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图文帖子与图片关联表';
## 视频表
DROP TABLE IF EXISTS `video`;
CREATE TABLE `video` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`title` VARCHAR(255) NOT NULL COMMENT '标题',
`cover_id` BIGINT NOT NULL COMMENT '封面图片ID',
`video_file_id` BIGINT NOT NULL COMMENT '视频文件ID',
`play_count` BIGINT DEFAULT 0 COMMENT '播放次数',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
`comment_count` INT DEFAULT 0 COMMENT '评论数',
`favorite_count` INT DEFAULT 0 COMMENT '收藏数',
`user_id` BIGINT NOT NULL COMMENT '发布用户ID',
`duration` INT NOT NULL COMMENT '视频时长(秒)',
`category` VARCHAR(50) NOT NULL COMMENT '分类(如“音乐”、“游戏”)',
`tags` VARCHAR(255) COMMENT '标签(逗号分隔)',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`cover_id`) REFERENCES `lj_file`(`id`),
FOREIGN KEY (`video_file_id`) REFERENCES `lj_file`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频表';
## 评论表
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`content` TEXT NOT NULL COMMENT '评论内容',
`user_id` BIGINT NOT NULL COMMENT '评论用户ID',
`post_type` VARCHAR(20) NOT NULL COMMENT '帖子类型post/video',
`post_id` BIGINT NOT NULL COMMENT '关联的帖子ID',
`parent_comment_id` BIGINT COMMENT '父评论ID',
`top_id` BIGINT COMMENT '顶层评论ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

@ -0,0 +1,96 @@
package com.luojia_channel;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.file.dto.CompleteUploadDTO;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.service.FileService;
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 io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import java.util.concurrent.TimeUnit;
import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_PREFIX;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringBootTest
public class FileServiceTests {
@Autowired
private FileServiceImpl fileService;
@Autowired
private MinioClient minioClient; // 模拟 MinIO 客户端
@Autowired
private RedisUtil redisUtil; // 模拟 Redis
@Autowired
private ValidateFileUtil validateFileUtil; // 模拟验证工具
@Autowired
private GeneratePathUtil generatePathUtil; // 模拟路径生成工具
// 其他辅助变量如测试文件内容、MD5 等)
private static final String TEST_FILE_NAME = "test.jpg";
private static final String TEST_FILE_MD5 = "test_md5";
private static final String TEST_FILE_CONTENT = "test_content";
private static final int TOTAL_CHUNKS = 2;
@Test
public void testUploadChunk_NormalCase() throws Exception {
// 准备测试数据
MockMultipartFile file = new MockMultipartFile(
"file",
TEST_FILE_NAME,
"image",
TEST_FILE_CONTENT.getBytes()
);
UploadChunkDTO chunkDTO = UploadChunkDTO.builder().build();
chunkDTO.setFile(file);
chunkDTO.setFileMd5(TEST_FILE_MD5);
chunkDTO.setChunkNumber(0);
chunkDTO.setTotalChunks(TOTAL_CHUNKS);
chunkDTO.setFileType("image");
chunkDTO.setFileName(TEST_FILE_NAME);
fileService.uploadChunk(chunkDTO);
}
@Test
public void testCompleteUpload_Success() throws Exception {
// 准备合并前的环境
CompleteUploadDTO completeDTO = CompleteUploadDTO.builder().build();
completeDTO.setFileMd5(TEST_FILE_MD5);
completeDTO.setTotalChunks(TOTAL_CHUNKS);
completeDTO.setFileType("image");
completeDTO.setFileName(TEST_FILE_NAME);
//fileService.completeUpload(completeDTO);
fileService.deleteChunks(TEST_FILE_MD5);
//fileService.deleteBucket("chunks");
/*
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket("chunks")
.object("test_md5/chunk_0")
.build()
);
System.out.println("删除成功");
} catch (Exception e) {
System.err.println("删除失败: " + e.getMessage());
}
*/
}
}

@ -0,0 +1,9 @@
package com.luojia_channel;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class LuojiaChannelApplicationTests {
}

@ -1,22 +1,8 @@
# 本地开发环境
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
lj:
db:
host: localhost
passwprd: 123456
password: 123456
redis:
host: localhost
port: 6379
@ -26,3 +12,8 @@ lj:
port: 15672
username: root
password: 123456
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin

@ -36,6 +36,11 @@ spring:
concurrency: 5
max-concurrency: 10
prefetch: 1
# minio配置
minio:
endpoint: ${lj.minio.endpoint}
accessKey: ${lj.minio.accessKey}
secretKey: ${lj.minio.secretKey}
mybatis-plus:
configuration:

@ -1,4 +1,6 @@
## 用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
@ -7,6 +9,7 @@ CREATE TABLE `user` (
`phone` VARCHAR(20) UNIQUE COMMENT '注册手机号',
`email` VARCHAR(100) UNIQUE COMMENT '邮箱',
`student_id` VARCHAR(20) UNIQUE COMMENT '学号',
`avatar` VARCHAR(255) COMMENT '头像URL',
`gender` TINYINT DEFAULT 0 COMMENT '性别0未知1男2女',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@ -19,3 +22,84 @@ CREATE TABLE `user` (
UNIQUE INDEX `uk_email` (`email`),
UNIQUE INDEX `uk_student_id` (`student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
## 文件表
DROP TABLE IF EXISTS `lj_file`;
CREATE TABLE `lj_file` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名',
`file_url` VARCHAR(512) NOT NULL COMMENT 'MinIO存储路径',
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
`file_md5` VARCHAR(32) NOT NULL COMMENT '文件MD5值',
`file_type` VARCHAR(50) NOT NULL COMMENT '文件类型image/video',
`file_status` INT NOT NULL DEFAULT 0 COMMENT '文件状态0:正在上传, 1:上传成功, 2:失败或删除, 3:审核中)',
`user_id` BIGINT NOT NULL COMMENT '上传用户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件存储表';
## 图文帖子表
DROP TABLE IF EXISTS `post`;
CREATE TABLE `post` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`title` VARCHAR(255) NOT NULL COMMENT '标题',
`cover_id` BIGINT NOT NULL COMMENT '封面图片ID',
`content` TEXT NOT NULL COMMENT '文字内容',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
`comment_count` INT DEFAULT 0 COMMENT '评论数',
`favorite_count` INT DEFAULT 0 COMMENT '收藏数',
`user_id` BIGINT NOT NULL COMMENT '发布用户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`cover_id`) REFERENCES `lj_file`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图文帖子表';
## 帖子图片关联表
DROP TABLE IF EXISTS `post_image`;
CREATE TABLE `post_image` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`post_id` BIGINT NOT NULL COMMENT '图文帖子ID',
`file_id` BIGINT NOT NULL COMMENT '图片文件ID',
FOREIGN KEY (`post_id`) REFERENCES `post`(`id`),
FOREIGN KEY (`file_id`) REFERENCES `lj_file`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图文帖子与图片关联表';
## 视频表
DROP TABLE IF EXISTS `video`;
CREATE TABLE `video` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`title` VARCHAR(255) NOT NULL COMMENT '标题',
`cover_id` BIGINT NOT NULL COMMENT '封面图片ID',
`video_file_id` BIGINT NOT NULL COMMENT '视频文件ID',
`play_count` BIGINT DEFAULT 0 COMMENT '播放次数',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
`comment_count` INT DEFAULT 0 COMMENT '评论数',
`favorite_count` INT DEFAULT 0 COMMENT '收藏数',
`user_id` BIGINT NOT NULL COMMENT '发布用户ID',
`duration` INT NOT NULL COMMENT '视频时长(秒)',
`category` VARCHAR(50) NOT NULL COMMENT '分类(如“音乐”、“游戏”)',
`tags` VARCHAR(255) COMMENT '标签(逗号分隔)',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`cover_id`) REFERENCES `lj_file`(`id`),
FOREIGN KEY (`video_file_id`) REFERENCES `lj_file`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频表';
## 评论表
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`content` TEXT NOT NULL COMMENT '评论内容',
`user_id` BIGINT NOT NULL COMMENT '评论用户ID',
`post_type` VARCHAR(20) NOT NULL COMMENT '帖子类型post/video',
`post_id` BIGINT NOT NULL COMMENT '关联的帖子ID',
`parent_comment_id` BIGINT COMMENT '父评论ID',
`top_id` BIGINT COMMENT '顶层评论ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

@ -10,7 +10,9 @@
"dependencies": {
"axios": "^1.8.4",
"core-js": "^3.8.3",
"vue": "^3.2.13"
"element-plus": "^2.9.8",
"vue": "^3.2.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
@ -1759,6 +1761,15 @@
"node": ">=6.9.0"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@ -1769,6 +1780,15 @@
"node": ">=10.0.0"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
@ -1829,6 +1849,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -2008,6 +2053,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.5.tgz",
@ -2209,6 +2265,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",
@ -2321,6 +2392,12 @@
"@types/node": "*"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.0.tgz",
@ -3052,6 +3129,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.13",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz",
@ -3146,6 +3229,94 @@
"dev": true,
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"license": "MIT",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@ -3605,6 +3776,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
@ -4927,6 +5104,12 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/debounce/-/debounce-1.2.1.tgz",
@ -5332,6 +5515,32 @@
"dev": true,
"license": "ISC"
},
"node_modules/element-plus": {
"version": "2.9.8",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.8.tgz",
"integrity": "sha512-srViUaUdfblBKGMeuEPiXxxKlH5aUmKqEwmhb/At9Sj91DbU6od/jYN1955cTnzt3wTSA7GfnZF7UiRX9sdRHg==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.13",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -5495,7 +5704,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
@ -7573,9 +7781,25 @@
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -7876,6 +8100,12 @@
"node": ">= 4.0.0"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@ -8313,6 +8543,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-2.0.2.tgz",
@ -11546,6 +11782,21 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/vue-router": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",
"integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",

@ -10,7 +10,9 @@
"dependencies": {
"axios": "^1.8.4",
"core-js": "^3.8.3",
"vue": "^3.2.13"
"element-plus": "^2.9.8",
"vue": "^3.2.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",

@ -3,13 +3,13 @@
<div class="form-container">
<h2>登录</h2>
<!-- 登录方式选择 -->
<!-- 登录方式选择 -->
<div class="login-type">
<span
v-for="type in loginTypes"
:key="type.value"
:class="{ active: currentType === type.value }"
@click="currentType = type.value">
@click="switchLoginType(type.value)">
{{ type.label }}
</span>
</div>
@ -17,7 +17,7 @@
<!-- 登录表单 -->
<form @submit.prevent="login" class="login-form">
<div class="input-group">
<!-- 用户标识输入框用户名/学号/邮箱/手机 -->
<!-- 用户标识输入框用户名/邮箱/手机 -->
<input
:type="inputType"
v-model="loginForm.userFlag"
@ -33,13 +33,13 @@
placeholder="验证码"
required />
<!-- 获取验证码按钮 -->
<button type="button" @click="sendCode" :disabled="cooldown > 0">
<button type="button" @click="sendCode" :disabled="cooldown > 0 || !isValidInput">
{{ cooldown > 0 ? `${cooldown}s` : '获取验证码' }}
</button>
</div>
</template>
<!-- 密码输入框非验证码登录时显示 -->
<!-- 密码输入框仅用户名登陆时显示 -->
<template v-else>
<input
type="password"
@ -53,15 +53,15 @@
<button type="submit">登录</button>
</form>
<!-- 自动登录选项 -->
<!-- 记住登录选项 -->
<div class="remember-login">
<input
type="checkbox"
id="remember"
v-model="loginForm.remember" />
<label for="remember">自动登录</label>
<label for="remember">记住登录</label>
<!-- 切换到注册表单的链接 -->
<!-- 切换到注册的链接 -->
<p class="switch-form">
没有账号
<button @click="$emit('toggleForm')" class="link-button">注册</button>
@ -73,31 +73,32 @@
<script>
import { ref, computed } from 'vue';
import axios from 'axios';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
export default {
name: 'UserLogin',
//TODO: props, { emit }
setup() {
//
const currentType = ref('username'); //
const cooldown = ref(0); //
const currentType = ref('username'); //
const cooldown = ref(0); //
//
//
const loginTypes = [
{ label: '用户名登录', value: 'username' }, //
{ label: '学号登录', value: 'student' }, //
{ label: '邮箱登录', value: 'email' }, //
{ label: '手机登录', value: 'phone' } //
{ label: '用户名登录', value: 'username' },
{ label: '邮箱登录', value: 'email' },
{ label: '手机登录', value: 'phone' }
];
//
//
const loginForm = ref({
userFlag: '', // ///
userFlag: '', //
password: '', //
verifyCode: '', // /使
remember: false //
});
//
//
const inputType = computed(() => {
switch(currentType.value) {
case 'email': return 'email'; //
@ -106,35 +107,60 @@ export default {
}
});
//
//
const placeholder = computed(() => {
switch(currentType.value) {
case 'username': return '用户名';
case 'student': return '学号';
case 'email': return '邮箱';
case 'phone': return '手机号';
case 'username': return '请输入用户名';
case 'email': return '请输入邮箱';
case 'phone': return '请输入手机号';
default: return '';
}
});
//
//
const showVerifyCode = computed(() => {
return ['email', 'phone'].includes(currentType.value);
});
//
//
const isValidInput = computed(() => {
const value = loginForm.value.userFlag
if(!value) return false; //
switch(currentType.value) {
case 'email':
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/.test(value);
case 'phone':
return /^1[3-9]\d{9}$/.test(value);
default:
return value.length > 0; //
}
});
//
const isvalidForm = computed(() => {
if(!isValidInput.value) return false;
if(showVerifyCode.value){
return loginForm.value.verifyCode.length === 6; //
} else {
return loginForm.value.password.length >= 6; //
}
});
//
async function sendCode() {
if (cooldown.value > 0) return; //
if (cooldown.value > 0 || !isValidInput.value) return; //
try {
//
//TODO:
const response = await axios.post('/user/sendCode', {
type: currentType.value,
target: loginForm.value.userFlag
});
if (response.data.success) {
// 60
//60
cooldown.value = 60;
const timer = setInterval(() => {
cooldown.value--;
@ -148,34 +174,71 @@ export default {
}
}
//
//
async function login() {
if (!isvalidForm.value) return;
try {
//
const response = await axios.post('/user/login', {
...loginForm.value,
loginType: currentType.value
});
const loginData = {
userFlag: loginForm.value.userFlag,
password: loginForm.value.password
}
if (response.data.success) {
// TODO:
if (showVerifyCode.value) {
loginData.verifyCode = loginForm.value.verifyCode; //
}
//post
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);
ElMessage({
message: '登录成功',
type: 'success',
duration: 500
});
//TODO:
}else{
//
ElMessage({
message: '登陆失败,请检查用户名或密码',
type: 'error',
duration: 500
});
}
} catch (error) {
console.error('登录失败:', error);
alert(error.response?.message ||'登录失败,请稍后重试');
}
}
//
const switchLoginType = (type) => {
currentType.value = type;
loginForm.value.userFlag = '';
loginForm.value.password = '';
loginForm.value.verifyCode = '';
}
// 使
return {
currentType, //
loginTypes, //
loginForm, //
inputType, //
placeholder, //
showVerifyCode, //
cooldown, //
sendCode, //
login //
currentType,
loginTypes,
loginForm,
inputType,
placeholder,
showVerifyCode,
cooldown,
sendCode,
login,
switchLoginType,
isValidInput
};
}
}
@ -193,7 +256,7 @@ export default {
margin: auto;
}
/* 登录方式切换栏样式 - 包含用户名/学号/邮箱/手机登录选项 */
/* 登录方式切换栏样式 - 包含用户名/邮箱/手机登录选项 */
.login-type {
display: flex;
justify-content: space-around;
@ -232,7 +295,7 @@ export default {
margin-bottom: 20px;
}
/* 通用输入框样式 - 适用于用户名/学号/邮箱/手机号/密码输入框 */
/* 通用输入框样式 - 适用于用户名/邮箱/手机号/密码输入框 */
input {
width: 93%;
padding: 11px;

@ -39,7 +39,7 @@
<button
type="button"
@click="sendCode"
:disabled="cooldown > 0">
:disabled="cooldown > 0 || !isValidInput">
{{ cooldown > 0 ? `${cooldown}s` : '获取验证码' }}
</button>
</div>
@ -93,13 +93,14 @@
<script>
import { ref, computed } from 'vue';
import axios from 'axios';
import request from '@/utils/request';
import {ElMessage} from 'element-plus'
export default {
name: 'UserRegister',
setup() {
//
const currentType = ref('username'); //
const cooldown = ref(0); //
setup(props, { emit }) {
const currentType = ref('username');
const cooldown = ref(0); //
//
const registerTypes = [
@ -110,19 +111,34 @@ export default {
//
const registerForm = ref({
username: '', //
email: '', //
phone: '', //
password: '', //
confirmPassword: '', //
verifyCode: '', // (/)
captcha: '' //
username: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
verifyCode: '', //(/)
captcha: '' //
});
// URL
//
const isValidInput = computed(() => {
const value = registerForm.value[currentType.value]
if (!value) return false
switch(currentType.value) {
case 'email':
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(value)
case 'phone':
return /^1[3-9]\d{9}$/.test(value)
default:
return false
}
})
//URL
const captchaUrl = ref('/user/captcha');
//
//
const inputType = computed(() => {
switch(currentType.value) {
case 'email': return 'email';
@ -131,7 +147,7 @@ export default {
}
});
//
//
const placeholder = computed(() => {
switch(currentType.value) {
case 'email': return '请输入邮箱';
@ -140,23 +156,28 @@ export default {
}
});
//
///
const showVerifyCode = computed(() => {
return ['email', 'phone'].includes(currentType.value);
});
// /使
async function sendCode() {
if (cooldown.value > 0) return;
if (cooldown.value > 0 || !isValidInput.value) return;
try {
//
const response = await axios.post(`/user/send-code`, {
//TODO
const response = await request.post(`/user/send-code`, {
type: currentType.value,
target: registerForm.value[currentType.value]
});
if (response.data.success) {
if (response.success) {
ElMessage({
message: '验证码已发送',
type: 'info'
});
// 60
cooldown.value = 60;
const timer = setInterval(() => {
@ -168,6 +189,7 @@ export default {
}
} catch (error) {
console.error('发送验证码失败:', error);
alert('发送验证码失败,请稍后重试');
}
}
@ -177,36 +199,75 @@ export default {
captchaUrl.value = `/user/captcha?t=${new Date().getTime()}`;
}
//
//TODO:
//
async function register() {
try {
const response = await axios.post('/user/register', {
...registerForm.value,
registerType: currentType.value
//
if (registerForm.value.password !== registerForm.value.confirmPassword) {
ElMessage({
message: '两次输入的密码不一致',
type: 'error',
duration: 500
});
return;
}
if (response.data.success) {
//
this.$emit('toggleForm');
}
} catch (error) {
//
const registerData = {
username: registerForm.value.username,
password: registerForm.value.password
};
//
switch (currentType.value) {
case 'email':
registerData.email = registerForm.value.email;
break;
case 'phone':
registerData.phone = registerForm.value.phone;
break;
}
const response = await axios.post('/user/register', registerData);
if (response.code === 200) {
ElMessage({
message: '注册成功',
type: 'success',
duration: 500
});
//
emit('toggleForm');
} else {
ElMessage({
message: '注册失败',
type: 'error',
duration: 500
});
}
}
catch (error) {
console.error('注册失败:', error);
alert(error.response?.message || '注册失败,请稍后重试');
}
}
// 使
return {
currentType, //
registerTypes, //
registerForm, //
captchaUrl, // URL
inputType, //
placeholder, //
showVerifyCode, //
cooldown, //
sendCode, //
refreshCaptcha, //
register //
currentType,
registerTypes,
registerForm,
captchaUrl,
inputType,
placeholder,
showVerifyCode,
cooldown,
sendCode,
refreshCaptcha,
register,
isValidInput
};
}
}

@ -1,8 +1,11 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router'; // 确保引入了 router
import ELementPlus from 'element-plus';
import 'element-plus/dist/index.css';
const app = createApp(App);
app.use(router); // 注册 vue-router
app.use(ELementPlus); // 注册 element-plus
app.mount('#app');

@ -30,7 +30,7 @@ const routes = [
];
const router = createRouter({
history: createWebHistory(/*process.env.BASE_URL*/),
history: createWebHistory(process.env.BASE_URL),
routes
});

@ -0,0 +1,36 @@
import axios from 'axios';
const request = axios.create({
timeout: 5000
});
//响应拦截器
request.interceptors.response.use(
response => {
if (response.data.code === 200) {
return response.data;
}
return Promise.reject(response.data);
},
error => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
//请求拦截器
request.interceptors.request.use(
config => {
console.log('Request:',config);
const token = localStorage.getItem('accessToken');
if (token) {
config.headers['Authorization'] = token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
export default request;

@ -1,4 +1,16 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
transpileDependencies: true,
devServer: {
port: 8080,
proxy: {
'/user': {
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: {
'^/user': '/user'
}
},
}
}
})

Loading…
Cancel
Save