From 392cc52fd22190b8918de4fcdaf332463b904686 Mon Sep 17 00:00:00 2001 From: ikmkj Date: Mon, 2 Mar 2026 02:01:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=AC=94=E8=AE=B0?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E7=9A=84=E8=87=AA=E5=8A=A8=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=8A=9F=E8=83=BD=E4=B8=8EUI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 重构用户登录注册逻辑与数据验证 fix: 修复图片上传安全漏洞与路径处理问题 perf: 优化笔记列表分页加载与滚动性能 style: 改进侧边栏菜单的视觉设计与交互体验 chore: 更新环境变量与数据库连接配置 docs: 添加用户信息视图对象的Swagger文档 test: 增加用户注册登录的输入验证测试 ci: 配置JWT密钥环境变量与安全设置 build: 调整前端构建配置与模块加载方式 --- .../common/advice/GlobalExceptionHandler.java | 56 +-- .../bijihoudaun/config/SecurityConfig.java | 6 +- .../test/bijihoudaun/config/WebConfig.java | 29 +- .../controller/ImageController.java | 59 ++- .../controller/MarkdownController.java | 4 +- .../controller/UserController.java | 27 +- .../com/test/bijihoudaun/entity/UserVO.java | 29 ++ .../interceptor/RateLimitInterceptor.java | 84 ++++ .../mapper/MarkdownFileMapper.java | 8 +- .../service/impl/ImageServiceImpl.java | 50 ++- .../service/impl/MarkdownFileServiceImpl.java | 11 +- .../service/impl/UserServiceImpl.java | 48 +- .../src/main/resources/application-dev.yml | 2 +- .../src/main/resources/application.yml | 8 +- biji-qianduan/.env.development | 2 +- biji-qianduan/index.html | 2 +- biji-qianduan/src/api/CommonApi.js | 2 +- biji-qianduan/src/components/HomePage.vue | 110 ++++- biji-qianduan/src/components/RegisterPage.vue | 10 +- .../src/components/home/NoteEditor.vue | 126 ++++-- .../src/components/home/SidebarMenu.vue | 409 ++++++++++++++---- biji-qianduan/src/stores/user.js | 8 +- docker/.env | 3 +- 23 files changed, 811 insertions(+), 282 deletions(-) create mode 100644 biji-houdaun/src/main/java/com/test/bijihoudaun/entity/UserVO.java create mode 100644 biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/RateLimitInterceptor.java diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java index dbe315e..f8d48fc 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java @@ -17,81 +17,63 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException; import java.util.List; import java.util.stream.Collectors; -/** - * 全局异常处理器 - */ @RestControllerAdvice public class GlobalExceptionHandler { - // 打印日志 private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - /** - * 处理业务异常 - */ @ExceptionHandler(BaseException.class) public R handleBaseException(BaseException e, HttpServletRequest request) { + log.warn("Business exception at {}: {}", request.getRequestURI(), e.getMessage()); return R.fail(e.getResultCode().getCode(), e.getMessage()); } - /** - * 处理注册异常 - */ @ExceptionHandler(RegistrationException.class) - public R handleRegistrationException(RegistrationException e) { - log.warn("Registration attempt failed: {}", e.getMessage()); + public R handleRegistrationException(RegistrationException e, HttpServletRequest request) { + log.warn("Registration failed at {}: {}", request.getRequestURI(), e.getMessage()); return R.fail(e.getMessage()); } - /** - * 处理文件大小超出限制异常 - */ @ExceptionHandler(MaxUploadSizeExceededException.class) - public R handleFileSizeLimitExceeded() { - log.error("文件大小超出限制"); + public R handleFileSizeLimitExceeded(HttpServletRequest request) { + log.warn("File size exceeded at {}", request.getRequestURI()); return R.fail("文件大小超过限制"); } - /** - * 处理参数校验异常 - */ @ExceptionHandler(MethodArgumentNotValidException.class) - public R> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + public R> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { List errors = e.getBindingResult().getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.toList()); - log.error("参数校验异常:{}", errors.get(0)); + log.warn("Validation failed at {}: {}", request.getRequestURI(), errors.get(0)); return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0)); } - /** - * 处理绑定异常 - */ @ExceptionHandler(BindException.class) - public R> handleBindException(BindException e) { + public R> handleBindException(BindException e, HttpServletRequest request) { List errors = e.getBindingResult().getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.toList()); - log.error("参数校验异常:{}", errors.get(0)); + log.warn("Bind exception at {}: {}", request.getRequestURI(), errors.get(0)); return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0)); } - /** - * 处理请求体格式错误异常 (例如JSON格式错误) - */ @ExceptionHandler(org.springframework.http.converter.HttpMessageNotReadableException.class) - public R handleHttpMessageNotReadableException(org.springframework.http.converter.HttpMessageNotReadableException e) { - log.error("请求参数格式不正确: {}", e.getMessage()); + public R handleHttpMessageNotReadableException(HttpServletRequest request) { + log.warn("Invalid request body at {}", request.getRequestURI()); return R.fail(ResultCode.VALIDATE_FAILED.getCode(), "请求参数格式不正确"); } - /** - * 处理其他所有未捕获的异常 - */ + @ExceptionHandler(IllegalArgumentException.class) + public R handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) { + log.warn("Illegal argument at {}: {}", request.getRequestURI(), e.getMessage()); + return R.fail(ResultCode.VALIDATE_FAILED.getCode(), "参数错误"); + } + @ExceptionHandler(Exception.class) - public R handleException(Exception e) { - log.error("系统异常:", e); // 打印完整的堆栈信息 + public R handleException(Exception e, HttpServletRequest request) { + log.error("Unexpected error at {} - Error type: {}", request.getRequestURI(), e.getClass().getSimpleName()); return R.fail(ResultCode.FAILED.getCode(), "系统繁忙,请稍后再试"); } } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java index 7e2b66c..6bb5d59 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java @@ -90,11 +90,7 @@ public class SecurityConfig { .requestMatchers(org.springframework.http.HttpMethod.GET, "/api/groupings/**", // 获取分组 "/api/images/preview/**", // 预览图片 - "/api/markdown/files", // 获取所有文件 - "/api/markdown/search", // 搜索文件 - "/api/markdown/grouping/**", // 按分组获取文件 - "/api/markdown/recent", // 获取最近文件 - "/api/markdown/{id}", // 获取单个文件内容 + "/api/markdown/**", // 所有Markdown相关的GET请求 "/api/system/registration/status" // 检查注册是否开启 ).permitAll() diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/WebConfig.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/WebConfig.java index 9625772..993d8d7 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/WebConfig.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/WebConfig.java @@ -1,7 +1,10 @@ package com.test.bijihoudaun.config; +import com.test.bijihoudaun.interceptor.RateLimitInterceptor; +import com.test.bijihoudaun.interceptor.XSSInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -11,24 +14,28 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns("*") // 允许所有源 + .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") - .allowCredentials(false) // 不允许凭证,否则与通配符冲突 - .maxAge(3600); // 预检请求缓存时间 + .allowCredentials(false) + .maxAge(3600); } -/** - * 重写父类方法,配置静态资源处理器 - * 该方法用于配置静态资源的访问路径和实际存储位置 - * - * @param registry 资源处理器注册对象,用于注册静态资源处理器 - */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - // 添加资源处理器,配置"/uploads/**"路径下的请求 - // 将这些请求映射到服务器的"uploads/"目录 registry.addResourceHandler("/uploads/**") .addResourceLocations("file:uploads/"); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new RateLimitInterceptor()) + .addPathPatterns("/**") + .order(1); + + registry.addInterceptor(new XSSInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns("/api/markdown/**", "/api/images/**", "/doc.html", "/webjars/**", "/v3/api-docs/**") + .order(2); + } } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java index eb75b91..9c716c9 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; import com.test.bijihoudaun.common.response.R; import com.test.bijihoudaun.entity.Image; import com.test.bijihoudaun.service.ImageService; +import com.test.bijihoudaun.util.SecurityUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; @@ -17,9 +18,11 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; -@Tag(name = "markdown接口") +@Tag(name = "图片接口") @RestController @RequestMapping("/api/images") public class ImageController { @@ -48,6 +51,9 @@ public class ImageController { @Operation(summary = "根据id删除图片") @PostMapping("/{id}") public R deleteImage(@PathVariable Long id) { + if (!SecurityUtil.isUserAuthenticated()) { + return R.fail("请先登录"); + } boolean result = imageService.deleteImage(id); if (result) { return R.success(); @@ -56,38 +62,63 @@ public class ImageController { } } - /** - * 在线预览(图片、视频、音频、pdf 等浏览器可直接识别的类型) - */ - @GetMapping("/preview/{url}") @Operation(summary = "在线预览", description = "浏览器直接打开文件流") + @GetMapping("/preview/{url}") public void preview(@PathVariable String url, HttpServletResponse resp) throws IOException { if (StrUtil.isBlank(url)) { resp.setStatus(404); resp.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\",\"data\":null}"); return; } - File file = new File(rootPath + File.separator + url); - if (!file.exists()) { + + String sanitizedUrl = sanitizeFileName(url); + if (sanitizedUrl == null) { + resp.setStatus(403); + resp.getWriter().write("{\"code\":403,\"msg\":\"非法文件路径\",\"data\":null}"); + return; + } + + Path basePath = Paths.get(rootPath).normalize().toAbsolutePath(); + Path filePath = basePath.resolve(sanitizedUrl).normalize(); + + if (!filePath.startsWith(basePath)) { + resp.setStatus(403); + resp.getWriter().write("{\"code\":403,\"msg\":\"非法文件路径\",\"data\":null}"); + return; + } + + File file = filePath.toFile(); + if (!file.exists() || !file.isFile()) { resp.setStatus(404); resp.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\",\"data\":null}"); return; } + String contentTypeFromFileExtension = getContentTypeFromFileExtension(url); - // 设置正确的 MIME resp.setContentType(contentTypeFromFileExtension); - // 设置文件长度,支持断点续传 resp.setContentLengthLong(file.length()); - // 写出文件流 try (FileInputStream in = new FileInputStream(file)) { StreamUtils.copy(in, resp.getOutputStream()); } } + + private String sanitizeFileName(String fileName) { + if (StrUtil.isBlank(fileName)) { + return null; + } + if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\") || fileName.contains(":")) { + return null; + } + return fileName; + } @Operation(summary = "根据url删除图片") @PostMapping("/deleteByUrl") public R deleteImageByUrl(@RequestParam String url) { + if (!SecurityUtil.isUserAuthenticated()) { + return R.fail("请先登录"); + } boolean result = imageService.deleteImageByUrl(url); if (result) { return R.success(); @@ -99,6 +130,9 @@ public class ImageController { @Operation(summary = "根据url批量删除图片") @PostMapping("/batch") public R deleteImageByUrls(@RequestBody List urls) { + if (!SecurityUtil.isUserAuthenticated()) { + return R.fail("请先登录"); + } boolean result = imageService.deleteImageByUrls(urls); if (result) { return R.success(); @@ -108,11 +142,6 @@ public class ImageController { } - /** - * 根据文件扩展名获取内容类型 - * @param fileName 文件名 - * @return 对应的MIME类型 - */ private String getContentTypeFromFileExtension(String fileName) { if (StrUtil.isBlank(fileName) || !StrUtil.contains(fileName, '.')) { return "application/octet-stream"; diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java index 7b88680..79acd9d 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/MarkdownController.java @@ -108,8 +108,8 @@ public class MarkdownController { @Operation(summary = "获取最近更新的笔记") @GetMapping("/recent") - public R> getRecentFiles() { - List files = markdownFileService.getRecentFiles(12); + public R> getRecentFiles(@RequestParam(defaultValue = "16") int limit) { + List files = markdownFileService.getRecentFiles(limit); return R.success(files); } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java index 2e5ec70..6da2b02 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/UserController.java @@ -4,6 +4,7 @@ import com.test.bijihoudaun.bo.UpdatePasswordBo; import cn.hutool.core.util.ObjectUtil; import com.test.bijihoudaun.common.response.R; import com.test.bijihoudaun.entity.User; +import com.test.bijihoudaun.entity.UserVO; import com.test.bijihoudaun.service.RegistrationCodeService; import com.test.bijihoudaun.service.SystemSettingService; import com.test.bijihoudaun.service.UserService; @@ -11,6 +12,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.authentication.BadCredentialsException; @@ -41,14 +43,18 @@ public class UserController { @Parameter(name = "registrationCode", description = "注册码", required = true) }) @PostMapping("/register") - public R register(String username, String password, String email, String registrationCode){ + public R register(String username, String password, String email, String registrationCode){ if (!systemSettingService.isRegistrationEnabled()) { return R.fail("注册功能已关闭"); } if (!registrationCodeService.validateCode(registrationCode)) { return R.fail("无效或已过期的注册码"); } - return R.success(userService.register(username,password,email)); + User user = userService.register(username, password, email); + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + userVO.setId(String.valueOf(user.getId())); + return R.success(userVO); } @Operation(summary = "用户登录") @@ -57,12 +63,21 @@ public class UserController { @Parameter(name = "password", description = "密码",required = true) }) @PostMapping("/login") - public R> login(String username, String password){ + public R> login(String username, String password){ try { String token = userService.login(username, password); - Map tokenMap = new HashMap<>(); - tokenMap.put("token", token); - return R.success(tokenMap); + User user = userService.getOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper().eq("username", username)); + + Map result = new HashMap<>(); + result.put("token", token); + + Map userInfo = new HashMap<>(); + userInfo.put("id", String.valueOf(user.getId())); + userInfo.put("username", user.getUsername()); + userInfo.put("email", user.getEmail()); + result.put("userInfo", userInfo); + + return R.success(result); } catch (BadCredentialsException e) { return R.fail("用户名或密码错误"); } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/entity/UserVO.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/entity/UserVO.java new file mode 100644 index 0000000..c188d3b --- /dev/null +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/entity/UserVO.java @@ -0,0 +1,29 @@ +package com.test.bijihoudaun.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@Schema(description = "用户信息视图对象") +public class UserVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "用户id") + private String id; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "用户创建时间") + private Date createdAt; + + @Schema(description = "用户更新时间") + private Date updatedAt; +} diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/RateLimitInterceptor.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/RateLimitInterceptor.java new file mode 100644 index 0000000..77c32a1 --- /dev/null +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/RateLimitInterceptor.java @@ -0,0 +1,84 @@ +package com.test.bijihoudaun.interceptor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.test.bijihoudaun.common.response.R; +import com.test.bijihoudaun.common.response.ResultCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class RateLimitInterceptor implements HandlerInterceptor { + + private static final int MAX_REQUESTS_PER_MINUTE = 60; + private static final int MAX_LOGIN_REQUESTS_PER_MINUTE = 5; + private static final long WINDOW_SIZE_MS = 60_000; + + private final Map requestCounters = new ConcurrentHashMap<>(); + private final Map loginCounters = new ConcurrentHashMap<>(); + + private static class RequestCounter { + AtomicInteger count; + long windowStart; + + RequestCounter() { + this.count = new AtomicInteger(1); + this.windowStart = System.currentTimeMillis(); + } + + boolean incrementAndCheck(int maxRequests) { + long now = System.currentTimeMillis(); + if (now - windowStart > WINDOW_SIZE_MS) { + synchronized (this) { + if (now - windowStart > WINDOW_SIZE_MS) { + count.set(1); + windowStart = now; + return true; + } + } + } + return count.incrementAndGet() <= maxRequests; + } + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String clientIp = getClientIp(request); + String requestUri = request.getRequestURI(); + + boolean isLoginRequest = "/api/user/login".equals(requestUri); + Map counters = isLoginRequest ? loginCounters : requestCounters; + int maxRequests = isLoginRequest ? MAX_LOGIN_REQUESTS_PER_MINUTE : MAX_REQUESTS_PER_MINUTE; + + RequestCounter counter = counters.computeIfAbsent(clientIp, k -> new RequestCounter()); + + if (!counter.incrementAndCheck(maxRequests)) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(429); + ObjectMapper mapper = new ObjectMapper(); + response.getWriter().write(mapper.writeValueAsString( + R.fail(ResultCode.FAILED.getCode(), isLoginRequest ? "登录请求过于频繁,请稍后再试" : "请求过于频繁,请稍后再试") + )); + return false; + } + + return true; + } + + private String getClientIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + return ip; + } +} diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/MarkdownFileMapper.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/MarkdownFileMapper.java index f3c8f37..df5319a 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/MarkdownFileMapper.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/MarkdownFileMapper.java @@ -48,6 +48,12 @@ public interface MarkdownFileMapper extends BaseMapper { * 获取所有笔记ID * @return 所有笔记ID列表 */ - @Select("SELECT id FROM `markdown_file` WHERE is_deleted = 0") + @Select("SELECT id, grouping_id, `title`, file_name, `content`, created_at, updated_at, is_deleted, deleted_at, deleted_by, is_private FROM `markdown_file` WHERE is_deleted = 0") List findAllIds(); + + @Select("SELECT mf.id, mf.grouping_id, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " + + "FROM `markdown_file` mf " + + "LEFT JOIN `grouping` g ON mf.grouping_id = g.id " + + "WHERE mf.id = #{id} AND mf.is_deleted = 0") + MarkdownFileVO selectByIdWithGrouping(@Param("id") Long id); } \ No newline at end of file diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/ImageServiceImpl.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/ImageServiceImpl.java index 45eaa85..f564611 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/ImageServiceImpl.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/ImageServiceImpl.java @@ -1,11 +1,11 @@ package com.test.bijihoudaun.service.impl; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.stream.CollectorUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.test.bijihoudaun.common.exception.BusinessException; import com.test.bijihoudaun.entity.Image; import com.test.bijihoudaun.mapper.ImageMapper; import com.test.bijihoudaun.service.ImageService; @@ -19,9 +19,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.LocalDateTime; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.UUID; @Service @@ -30,6 +30,12 @@ public class ImageServiceImpl extends ServiceImpl implements ImageService { + private static final Set ALLOWED_EXTENSIONS = Set.of( + ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg" + ); + + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; + @Value("${file.upload-dir}") private String uploadDir; @Resource @@ -37,28 +43,50 @@ public class ImageServiceImpl @Override public Image uploadImage( MultipartFile file, Long userId, Long markdownId) throws IOException { - // 创建上传目录 + if (file == null || file.isEmpty()) { + throw new BusinessException("上传文件不能为空"); + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || originalFilename.isEmpty()) { + throw new BusinessException("文件名无效"); + } + + if (file.getSize() > MAX_FILE_SIZE) { + throw new BusinessException("文件大小超过限制(最大10MB)"); + } + + int lastDotIndex = originalFilename.lastIndexOf("."); + if (lastDotIndex == -1) { + throw new BusinessException("文件缺少扩展名"); + } + + String extension = originalFilename.substring(lastDotIndex).toLowerCase(); + if (!ALLOWED_EXTENSIONS.contains(extension)) { + throw new BusinessException("不支持的文件类型,仅支持图片文件"); + } + + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new BusinessException("文件内容类型无效"); + } + Path uploadPath = Paths.get(uploadDir); if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } - // 生成唯一文件名 - String originalFilename = file.getOriginalFilename(); - String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String storedName = UUID.randomUUID() + extension; - // 保存文件 Path filePath = uploadPath.resolve(storedName); Files.copy(file.getInputStream(), filePath); - // 创建图片实体 Image image = new Image(); image.setOriginalName(originalFilename); image.setStoredName(storedName); image.setUrl("/api/images/preview/" + storedName); image.setSize(file.getSize()); - image.setContentType(file.getContentType()); + image.setContentType(contentType); image.setCreatedAt(new Date()); image.setMarkdownId(markdownId); @@ -73,11 +101,9 @@ public class ImageServiceImpl return false; } try { - // 删除文件系统中的图片 Path filePath = Paths.get(uploadDir, image.getStoredName()); Files.deleteIfExists(filePath); - // 删除数据库记录 this.removeById(id); return true; } catch (IOException e) { @@ -92,11 +118,9 @@ public class ImageServiceImpl return false; } try { - // 删除文件系统中的图片 Path filePath = Paths.get(uploadDir, image.getStoredName()); Files.deleteIfExists(filePath); - // 删除数据库记录 this.removeById(image.getId()); return true; } catch (IOException e) { diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/MarkdownFileServiceImpl.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/MarkdownFileServiceImpl.java index bce54cf..9bf2c11 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/MarkdownFileServiceImpl.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/MarkdownFileServiceImpl.java @@ -40,21 +40,24 @@ public class MarkdownFileServiceImpl public MarkdownFile updateMarkdownContent(MarkdownFile markdownFile) { long id; markdownFile.setUpdatedAt(new Date()); - // 如果ID为空或0,则视为新文件 if (ObjectUtil.isNull(markdownFile.getId()) || markdownFile.getId() == 0L) { long l = snowflakeIdGenerator.nextId(); markdownFile.setId(l); markdownFile.setCreatedAt(new Date()); - this.save(markdownFile); // 使用MyBatis-Plus的save方法 + this.save(markdownFile); id=l; } else { - this.updateById(markdownFile); // 使用MyBatis-Plus的updateById方法 + this.updateById(markdownFile); id=markdownFile.getId(); } List strings = MarkdownImageExtractor.extractImageFilenames(markdownFile.getContent()); - // 异步处理图片文件名同步 syncImageNames(id, strings); + + MarkdownFileVO result = markdownFileMapper.selectByIdWithGrouping(id); + if (result != null) { + return result; + } return markdownFile; } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java index de1123d..6b884ed 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/UserServiceImpl.java @@ -22,38 +22,46 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; -import java.util.Calendar; import java.util.Date; @Service @Transactional public class UserServiceImpl extends ServiceImpl implements UserService, UserDetailsService { + private static final int MIN_USERNAME_LENGTH = 2; + private static final int MAX_USERNAME_LENGTH = 8; + private static final int MIN_PASSWORD_LENGTH = 6; + private static final int MAX_PASSWORD_LENGTH = 12; + @Autowired private UserMapper userMapper; -/** - * 重写Spring Security的loadUserByUsername方法,用于用户认证 - * @param username 用户名 - * @return UserDetails 用户详细信息 - * @throws UsernameNotFoundException 当用户未找到时抛出此异常 - */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - // 根据用户名从数据库查询用户信息 User user = userMapper.findByUsername(username); - // 判断用户是否存在,如果不存在则抛出异常 if (ObjectUtil.isNull(user)) { throw new UsernameNotFoundException("User not found with username: " + username); } - // 返回UserDetails对象,包含用户名、密码和权限列表 - // 这里使用Spring Security提供的User类实现UserDetails接口 - // 参数分别为:用户名,密码,权限集合(这里使用空集合表示无额外权限) - return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); // 账号,密码,权限 + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); } @Override public User register(String username, String password, String email) { + if (username == null || username.trim().isEmpty()) { + throw new RegistrationException("用户名不能为空"); + } + username = username.trim(); + if (username.length() < MIN_USERNAME_LENGTH || username.length() > MAX_USERNAME_LENGTH) { + throw new RegistrationException("用户名长度必须在" + MIN_USERNAME_LENGTH + "-" + MAX_USERNAME_LENGTH + "位之间"); + } + + if (password == null || password.isEmpty()) { + throw new RegistrationException("密码不能为空"); + } + if (password.length() < MIN_PASSWORD_LENGTH || password.length() > MAX_PASSWORD_LENGTH) { + throw new RegistrationException("密码长度必须在" + MIN_PASSWORD_LENGTH + "-" + MAX_PASSWORD_LENGTH + "位之间"); + } + String encrypt = PasswordUtils.encrypt(password); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, username); @@ -69,7 +77,6 @@ public class UserServiceImpl extends ServiceImpl implements Us user.setUsername(username); user.setPassword(encrypt); user.setEmail(email); - // user.setCreatedAt(new Date()); // Let the database handle the default value userMapper.insert(user); return user; } @@ -97,7 +104,6 @@ public class UserServiceImpl extends ServiceImpl implements Us queryWrapper.eq(User::getId, id) .eq(User::getToken, token); User user = getOne(queryWrapper); - // 修改过期检查逻辑 return ObjectUtil.isNotNull(user) && new Date().before(user.getTokenEnddata()); } @@ -107,11 +113,19 @@ public class UserServiceImpl extends ServiceImpl implements Us if (ObjectUtil.isNull(user)) { throw new BusinessException("用户不存在"); } + + String newPassword = updatePasswordBo.getNewPassword(); + if (newPassword == null || newPassword.isEmpty()) { + throw new BusinessException("新密码不能为空"); + } + if (newPassword.length() < MIN_PASSWORD_LENGTH || newPassword.length() > MAX_PASSWORD_LENGTH) { + throw new BusinessException("密码长度必须在" + MIN_PASSWORD_LENGTH + "-" + MAX_PASSWORD_LENGTH + "位之间"); + } + if (!PasswordUtils.verify(updatePasswordBo.getOldPassword(), user.getPassword())) { throw new BusinessException("旧密码不正确"); } - String newPassword = PasswordUtils.encrypt(updatePasswordBo.getNewPassword()); - user.setPassword(newPassword); + user.setPassword(PasswordUtils.encrypt(newPassword)); updateById(user); } } \ No newline at end of file diff --git a/biji-houdaun/src/main/resources/application-dev.yml b/biji-houdaun/src/main/resources/application-dev.yml index a7806cc..66efb6f 100644 --- a/biji-houdaun/src/main/resources/application-dev.yml +++ b/biji-houdaun/src/main/resources/application-dev.yml @@ -13,7 +13,7 @@ spring: # driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://hdy16-16.311169.xyz:20001/biji_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8 + url: jdbc:mysql://hdy-hk-8-8.311169.xyz:3306/biji_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8 username: biji_user password: Ll12331100 jpa: diff --git a/biji-houdaun/src/main/resources/application.yml b/biji-houdaun/src/main/resources/application.yml index b520e8e..a81600d 100644 --- a/biji-houdaun/src/main/resources/application.yml +++ b/biji-houdaun/src/main/resources/application.yml @@ -33,7 +33,7 @@ datacenter: # JWT 配置 jwt: - secret: V2VsbCwgSSBzdXBwb3NlIHRoYXQgaWYgeW91J3JlIHJlYWRpbmcgdGhpcywgeW91J3JlIHByZXR0eSBjdXJpb3VzLg== # 这是一个足够长的Base64编码密钥,满足HS512的要求 - expiration: 86400 # token有效期,单位秒,这里是24小时 - header: Authorization # JWT存储的请求头 - tokenHead: "Bearer " # JWT负载中拿到开头 + secret: ${JWT_SECRET:V2VsbCwgSSBzdXBwb3NlIHRoYXQgaWYgeW91J3JlIHJlYWRpbmcgdGhpcywgeW91J3JlIHByZXR0eSBjdXJpb3VzLg==} + expiration: 86400 + header: Authorization + tokenHead: "Bearer " diff --git a/biji-qianduan/.env.development b/biji-qianduan/.env.development index 9b9b1ea..2084bf3 100644 --- a/biji-qianduan/.env.development +++ b/biji-qianduan/.env.development @@ -1 +1 @@ -VITE_API_BASE_URL=http://localhost:80 +VITE_API_BASE_URL=http://localhost:8084 diff --git a/biji-qianduan/index.html b/biji-qianduan/index.html index 37b0b63..5adda7b 100644 --- a/biji-qianduan/index.html +++ b/biji-qianduan/index.html @@ -28,6 +28,6 @@
- + diff --git a/biji-qianduan/src/api/CommonApi.js b/biji-qianduan/src/api/CommonApi.js index 7b9c13b..d8dad63 100644 --- a/biji-qianduan/src/api/CommonApi.js +++ b/biji-qianduan/src/api/CommonApi.js @@ -95,7 +95,7 @@ export const updateMarkdownTitle = (id, newName) => { } // 获取最近更新的笔记 -export const getRecentFiles = () => axiosApi.get('/api/markdown/recent'); +export const getRecentFiles = (limit = 16) => axiosApi.get(`/api/markdown/recent?limit=${limit}`); diff --git a/biji-qianduan/src/components/HomePage.vue b/biji-qianduan/src/components/HomePage.vue index 1f24e34..a5b184e 100644 --- a/biji-qianduan/src/components/HomePage.vue +++ b/biji-qianduan/src/components/HomePage.vue @@ -56,12 +56,19 @@ @upload-markdown="handleMarkdownUpload" @toggle-collapse="isCollapsed = !isCollapsed" /> -
+
+
+ + 加载中... +
+
+ 加载更多 +
@@ -157,7 +164,7 @@ import MoveNoteDialog from './home/dialogs/MoveNoteDialog.vue'; import SystemSettingsDialog from './home/dialogs/SystemSettingsDialog.vue'; import UpdatePasswordDialog from './home/dialogs/UpdatePasswordDialog.vue'; import PrivacyDialog from './home/dialogs/PrivacyDialog.vue'; -import { Plus } from "@element-plus/icons-vue"; +import { Plus, Loading } from "@element-plus/icons-vue"; // Basic Setup const userStore = useUserStore(); @@ -167,6 +174,11 @@ const router = useRouter(); const searchKeyword = ref(''); const categoryTree = ref([]); const groupMarkdownFiles = ref([]); +const displayedFiles = ref([]); +const currentPage = ref(0); +const pageSize = ref(16); +const isLoadingMore = ref(false); +const noteListWrapper = ref(null); const showEditor = ref(false); const selectedFile = ref(null); const editData = ref(null); @@ -188,6 +200,10 @@ const showPrivacyDialog = ref(false); const itemToRename = ref(null); const fileToImport = ref(null); +const hasMoreFiles = computed(() => { + return displayedFiles.value.length < groupMarkdownFiles.value.length; +}); + const resetEdit = () => { editData.value = null; }; @@ -234,11 +250,38 @@ const resetToHomeView = async () => { showEditor.value = false; searchKeyword.value = ''; activeMenu.value = 'all'; + currentPage.value = 0; try { - groupMarkdownFiles.value = await getRecentFiles() || []; + groupMarkdownFiles.value = await getRecentFiles(100) || []; + updateDisplayedFiles(); } catch (error) { ElMessage.error('获取最近文件失败: ' + error.message); groupMarkdownFiles.value = []; + displayedFiles.value = []; + } +}; + +const updateDisplayedFiles = () => { + const start = 0; + const end = (currentPage.value + 1) * pageSize.value; + displayedFiles.value = groupMarkdownFiles.value.slice(start, end); +}; + +const loadMoreFiles = () => { + if (isLoadingMore.value || !hasMoreFiles.value) return; + isLoadingMore.value = true; + + setTimeout(() => { + currentPage.value++; + updateDisplayedFiles(); + isLoadingMore.value = false; + }, 300); +}; + +const handleScroll = (e) => { + const { scrollTop, scrollHeight, clientHeight } = e.target; + if (scrollHeight - scrollTop - clientHeight < 100 && hasMoreFiles.value && !isLoadingMore.value) { + loadMoreFiles(); } }; @@ -248,12 +291,15 @@ const handleSelectFile = async (data) => { try { const files = await markdownList(data.id); groupMarkdownFiles.value = files || []; + currentPage.value = 0; + updateDisplayedFiles(); selectedFile.value = null; showEditor.value = false; activeMenu.value = `group-${data.id}`; } catch (error) { ElMessage.error('获取笔记列表失败: ' + error.message); groupMarkdownFiles.value = []; + displayedFiles.value = []; } }; @@ -272,30 +318,45 @@ const handleCreateNote = (payload) => { const handleEditorBack = (data) => { showEditor.value = false; - previewFile(data); + if (data && data.id) { + const fileWithGrouping = { + ...data, + groupingName: data.groupingName || getCategoryName(data.groupingId) + }; + selectedFile.value = fileWithGrouping; + } else { + selectedFile.value = null; + resetToHomeView(); + } +}; + +const getCategoryName = (groupId) => { + if (!groupId) return ''; + const findName = (items) => { + for (const item of items) { + if (item.id === groupId) return item.grouping; + if (item.children) { + const name = findName(item.children); + if (name) return name; + } + } + return null; + }; + return findName(categoryTree.value) || ''; }; const handleSaveSuccess = (updatedFile) => { selectedFile.value = updatedFile; - // 修复:保存成功后不退出编辑页面 - // showEditor.value = false; const index = groupMarkdownFiles.value.findIndex(f => f.id === updatedFile.id); if (index !== -1) { groupMarkdownFiles.value[index] = updatedFile; } else { - // 如果是新创建的笔记(之前ID为null),添加到列表开头 groupMarkdownFiles.value.unshift(updatedFile); } - + + updateDisplayedFiles(); fetchGroupings(); - - // 延迟清空 editData,确保所有响应式更新完成后再清理状态 - // Delay clearing editData to ensure all reactive updates are complete before cleaning up the state. - setTimeout(() => { - // 修复:保存成功后不清空editData,保持编辑状态 - // resetEdit(); - }, 100); // A short delay is usually sufficient }; const previewFile = async (file) => { @@ -390,6 +451,8 @@ const handleSearch = async () => { } try { groupMarkdownFiles.value = await searchMarkdown(searchKeyword.value) || []; + currentPage.value = 0; + updateDisplayedFiles(); selectedFile.value = null; showEditor.value = false; activeMenu.value = 'search'; @@ -569,4 +632,19 @@ watch([selectedFile, showEditor], ([newFile, newShowEditor]) => { height: 56px; box-shadow: 0 4px 12px rgba(0,0,0,.15); } + +.loading-more { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + gap: 8px; + color: var(--text-color-secondary); +} + +.load-more-trigger { + display: flex; + justify-content: center; + padding: 20px; +} \ No newline at end of file diff --git a/biji-qianduan/src/components/RegisterPage.vue b/biji-qianduan/src/components/RegisterPage.vue index 00426a3..8ee1310 100644 --- a/biji-qianduan/src/components/RegisterPage.vue +++ b/biji-qianduan/src/components/RegisterPage.vue @@ -76,8 +76,14 @@ const validatePass = (rule, value, callback) => { }; const registerRules = { - username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], - password: [{ required: true, message: '请输入密码', trigger: 'blur' }], + username: [ + { required: true, message: '请输入用户名', trigger: 'blur' }, + { min: 2, max: 8, message: '用户名长度必须在2-8位之间', trigger: 'blur' } + ], + password: [ + { required: true, message: '请输入密码', trigger: 'blur' }, + { min: 6, max: 12, message: '密码长度必须在6-12位之间', trigger: 'blur' } + ], confirmPassword: [{ validator: validatePass, trigger: 'blur' }], registrationCode: [{ required: true, message: '请输入注册码', trigger: 'blur' }], }; diff --git a/biji-qianduan/src/components/home/NoteEditor.vue b/biji-qianduan/src/components/home/NoteEditor.vue index 4ebc5b1..7a4fefe 100644 --- a/biji-qianduan/src/components/home/NoteEditor.vue +++ b/biji-qianduan/src/components/home/NoteEditor.vue @@ -3,7 +3,7 @@

{{ editData.title }}

- 返回 + 返回 保存 {{ saveStatus }}
@@ -29,13 +29,18 @@ const props = defineProps({ const emit = defineEmits(['back', 'update:editData']); const vditor = ref(null); -const bijiId=ref(null); - +const currentId = ref(null); +const isInitialized = ref(false); const saveStatus = ref(''); let saveTimeout = null; -const isProgrammaticChange = ref(false); +let lastSavedContent = ref(''); +let isSaving = ref(false); const initVditor = () => { + if (vditor.value) { + vditor.value.destroy(); + } + vditor.value = new Vditor('vditor-editor', { height: 'calc(100vh - 120px)', mode: 'ir', @@ -43,23 +48,26 @@ const initVditor = () => { enable: false, }, after: () => { - if (props.editData && props.editData.content) { - isProgrammaticChange.value = true; - vditor.value.setValue(props.editData.content); - isProgrammaticChange.value = false; - } - vditor.value.focus(); + isInitialized.value = true; + if (props.editData && props.editData.content) { + vditor.value.setValue(props.editData.content); + lastSavedContent.value = props.editData.content; + } + if (props.editData && props.editData.id) { + currentId.value = props.editData.id; + } + vditor.value.focus(); }, input: (value) => { - if (isProgrammaticChange.value) { - return; - } - // 启动定时器,延迟5秒后执行保存 + if (!isInitialized.value) return; + clearTimeout(saveTimeout); saveStatus.value = '正在输入...'; saveTimeout = setTimeout(() => { - save(value); - }, 5000); + if (!isSaving.value && value !== lastSavedContent.value) { + save(value); + } + }, 3000); }, upload: { accept: 'image/*', @@ -69,7 +77,6 @@ const initVditor = () => { uploadImage(file).then(res => { const url = res.url; - // 使用 file.name 替代 files.name 保证一致性 const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; vditor.value.insertValue(`![${file.name}](${baseUrl}${url})`); }).catch(() => { @@ -81,33 +88,72 @@ const initVditor = () => { }; const save = async (value) => { + if (isSaving.value) return; + clearTimeout(saveTimeout); const content = typeof value === 'string' ? value : vditor.value.getValue(); + + if (content === lastSavedContent.value && currentId.value) { + return; + } + + isSaving.value = true; try { saveStatus.value = '正在保存...'; - // 发送完整的笔记对象,确保包含所有必要字段 - const response = await updateMarkdown({ - id: props.editData.id? props.editData.id : bijiId.value, + + const payload = { + id: currentId.value || props.editData.id || null, content: content, title: props.editData.title, groupingId: props.editData.groupingId, fileName: props.editData.fileName, isPrivate: props.editData.isPrivate - }); - // 确保获取到后端返回的数据,包括可能的新ID - - bijiId.value = response.id; - // 保存成功,更新状态 - saveStatus.value = '已保存'; - // 发送更新后的笔记数据(包含可能的新ID) - emit('update:editData', { ...props.editData, content: content }); + }; + + const response = await updateMarkdown(payload); + + if (response && response.id) { + currentId.value = response.id; + lastSavedContent.value = content; + + const updatedFile = { + ...props.editData, + id: response.id, + content: content, + groupingId: response.groupingId, + groupingName: response.groupingName, + title: response.title, + isPrivate: response.isPrivate + }; + + emit('update:editData', updatedFile); + saveStatus.value = '已保存'; + } } catch (error) { - // 保存失败,更新状态并显示错误消息 saveStatus.value = '保存失败'; ElMessage.error('保存失败: ' + (error.message || '未知错误')); + } finally { + isSaving.value = false; } }; +const handleBack = async () => { + const content = vditor.value ? vditor.value.getValue() : ''; + if (content !== lastSavedContent.value && !isSaving.value) { + await save(content); + } + + const returnData = { + ...props.editData, + id: currentId.value || props.editData.id, + content: content, + groupingId: props.editData.groupingId, + groupingName: props.editData.groupingName + }; + + emit('back', returnData); +}; + onMounted(() => { initVditor(); }); @@ -116,27 +162,17 @@ onBeforeUnmount(() => { clearTimeout(saveTimeout); if (vditor.value) { vditor.value.destroy(); + vditor.value = null; } - if (bijiId.value){ - // 发送完整的笔记对象,确保包含所有必要字段 - updateMarkdown({ - id: bijiId.value, - content: vditor.value.getValue(), - title: props.editData.title, - groupingId: props.editData.groupingId, - fileName: props.editData.fileName, - isPrivate: props.editData.isPrivate - }); - } - // 离开页面后清空 bijiId.value 变量 - bijiId.value = null; + currentId.value = null; + isInitialized.value = false; }); watch(() => props.editData, (newVal, oldVal) => { - if (vditor.value && newVal && newVal.id !== oldVal?.id) { - isProgrammaticChange.value = true; + if (vditor.value && isInitialized.value && newVal && newVal.id !== oldVal?.id) { vditor.value.setValue(newVal.content || ''); - isProgrammaticChange.value = false; + lastSavedContent.value = newVal.content || ''; + currentId.value = newVal.id; saveStatus.value = ''; } }, { deep: true }); diff --git a/biji-qianduan/src/components/home/SidebarMenu.vue b/biji-qianduan/src/components/home/SidebarMenu.vue index 94d4f92..848d9d7 100644 --- a/biji-qianduan/src/components/home/SidebarMenu.vue +++ b/biji-qianduan/src/components/home/SidebarMenu.vue @@ -1,74 +1,75 @@ @@ -161,17 +162,24 @@ const renderMenu = (item) => { diff --git a/biji-qianduan/src/stores/user.js b/biji-qianduan/src/stores/user.js index 1b4b605..c80ccc8 100644 --- a/biji-qianduan/src/stores/user.js +++ b/biji-qianduan/src/stores/user.js @@ -1,6 +1,5 @@ import { defineStore } from 'pinia'; -import { ref } from 'vue'; -import { login as loginApi } from '../api/CommonApi'; // 假设你的API调用函数是这样组织的 +import { login as loginApi } from '../api/CommonApi'; export const useUserStore = defineStore('user', { state: () => ({ @@ -13,8 +12,9 @@ export const useUserStore = defineStore('user', { const response = await loginApi({ username, password }); if (response && response.token) { this.token = response.token; - // 你可能还需要一个接口来获取用户信息 - // this.userInfo = await getUserInfo(); + if (response.userInfo) { + this.userInfo = response.userInfo; + } return true; } return false; diff --git a/docker/.env b/docker/.env index 88eed80..ad7665a 100644 --- a/docker/.env +++ b/docker/.env @@ -1,3 +1,4 @@ DB_URL=jdbc:mysql://panel-jp.998521.xyz:37857/biji_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8 DB_USERNAME=biji_user -DB_PASSWORD=Ll12331100 \ No newline at end of file +DB_PASSWORD=Ll12331100 +JWT_SECRET=eW91ci1zdXBlci1zZWN1cmUtand0LXNlY3JldC1rZXktZm9yLXByb2R1Y3Rpb24tdXNlLW9ubHk= \ No newline at end of file