feat(grouping): 新增分组功能并优化 Markdown 文件操作

- 新增分组实体、控制器、服务和映射器
- 实现分组创建、获取、更新和删除接口
- 优化 Markdown 文件创建、获取和删除接口- 新增全局异常处理和日志记录
- 更新数据库表结构和字段类型
- 重构前端页面,支持分组和 Markdown 文件展示
This commit is contained in:
ikmkj
2025-06-17 20:46:10 +08:00
parent 8b43b68e62
commit 4557bd49f9
29 changed files with 4286 additions and 97 deletions

View File

@@ -5,6 +5,8 @@ import com.test.bijihoudaun.common.exception.BaseException;
import com.test.bijihoudaun.common.response.R; import com.test.bijihoudaun.common.response.R;
import com.test.bijihoudaun.common.response.ResultCode; import com.test.bijihoudaun.common.response.ResultCode;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -20,6 +22,8 @@ import java.util.stream.Collectors;
*/ */
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
// 打印日志
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/** /**
* 处理业务异常 * 处理业务异常
@@ -34,6 +38,7 @@ public class GlobalExceptionHandler {
*/ */
@ExceptionHandler(MaxUploadSizeExceededException.class) @ExceptionHandler(MaxUploadSizeExceededException.class)
public R<String> handleFileSizeLimitExceeded() { public R<String> handleFileSizeLimitExceeded() {
log.error("文件大小超出限制");
return R.fail("文件大小超过限制"); return R.fail("文件大小超过限制");
} }
@@ -46,6 +51,7 @@ public class GlobalExceptionHandler {
.stream() .stream()
.map(FieldError::getDefaultMessage) .map(FieldError::getDefaultMessage)
.collect(Collectors.toList()); .collect(Collectors.toList());
log.error("参数校验异常:{}", errors.get(0));
return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0)); return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0));
} }
@@ -58,6 +64,7 @@ public class GlobalExceptionHandler {
.stream() .stream()
.map(FieldError::getDefaultMessage) .map(FieldError::getDefaultMessage)
.collect(Collectors.toList()); .collect(Collectors.toList());
log.error("参数校验异常:{}", errors.get(0));
return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0)); return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0));
} }
@@ -66,6 +73,7 @@ public class GlobalExceptionHandler {
*/ */
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) { public R<Void> handleException(Exception e) {
return R.fail(ResultCode.FAILED.getCode(), "系统繁忙,请稍后再试"); log.error("系统异常:{}", e.getMessage());
return R.fail(ResultCode.FAILED.getCode(), "系统繁忙,请稍后再试:" + e.getMessage());
} }
} }

View File

@@ -42,6 +42,13 @@ public class Knife4jConfig {
.pathsToMatch("/api/user/**") .pathsToMatch("/api/user/**")
.build(); .build();
} }
@Bean
public GroupedOpenApi GroupingApi() {
return GroupedOpenApi.builder()
.group("分组接口")
.pathsToMatch("/api/groupings/**")
.build();
}
} }

View File

@@ -11,11 +11,11 @@ public class WebConfig implements WebMvcConfigurer {
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOrigins("*") .allowedOrigins("http://localhost:5173") // 明确指定前端来源
.allowedMethods("*") .allowedMethods("*")
.allowedHeaders("*") .allowedHeaders("*")
.allowCredentials(true) // 允许凭证
.maxAge(3600); // 预检请求缓存时间 .maxAge(3600); // 预检请求缓存时间
;
} }
@Override @Override

View File

@@ -0,0 +1,51 @@
package com.test.bijihoudaun.controller;
import com.test.bijihoudaun.entity.Grouping;
import com.test.bijihoudaun.service.GroupingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "分组接口")
@RestController
@RequestMapping("/api/groupings")
public class GroupingController {
@Autowired
private GroupingService groupingService;
@Operation(summary = "创建分组")
@PostMapping
public ResponseEntity<Grouping> createGrouping(@RequestBody Grouping grouping) {
Grouping created = groupingService.createGrouping(grouping);
return ResponseEntity.ok(created);
}
@Operation(summary = "获取全部分组")
@GetMapping
public ResponseEntity<List<Grouping>> getAllGroupings() {
List<Grouping> groupings = groupingService.getAllGroupings();
return ResponseEntity.ok(groupings);
}
@Operation(summary = "更新分组名称")
@PutMapping("/{id}")
public ResponseEntity<Grouping> updateGrouping(
@PathVariable Long id,
@RequestBody Grouping grouping) {
grouping.setId(id);
Grouping updated = groupingService.updateGrouping(grouping);
return ResponseEntity.ok(updated);
}
@Operation(summary = "删除分组")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteGrouping(@PathVariable Long id) {
groupingService.deleteGrouping(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -25,18 +25,16 @@ public class ImageController {
@Operation(summary = "上传图片") @Operation(summary = "上传图片")
@Parameters({ @Parameters({
@Parameter(name = "userId", description = "用户id", required = true),
@Parameter(name = "markdownId", description = "markdownid", required = true), @Parameter(name = "markdownId", description = "markdownid", required = true),
@Parameter(name = "file", description = "图片文件", required = true) @Parameter(name = "file", description = "图片文件", required = true)
}) })
@PostMapping @PostMapping
public ResponseEntity<Image> uploadImage( public ResponseEntity<Image> uploadImage(
@RequestParam Long userId,
@RequestParam(required = false) Long markdownId, @RequestParam(required = false) Long markdownId,
@RequestParam("file") MultipartFile file) { @RequestParam("file") MultipartFile file) {
try { try {
Image image = imageService.uploadImage(userId, markdownId, file); Image image = imageService.uploadImage(markdownId, file);
return ResponseEntity.ok(image); return ResponseEntity.ok(image);
} catch (IOException e) { } catch (IOException e) {
return ResponseEntity.status(500).build(); return ResponseEntity.status(500).build();

View File

@@ -1,5 +1,6 @@
package com.test.bijihoudaun.controller; package com.test.bijihoudaun.controller;
import com.test.bijihoudaun.common.response.R;
import com.test.bijihoudaun.entity.MarkdownFile; import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.service.MarkdownFileService; import com.test.bijihoudaun.service.MarkdownFileService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -9,6 +10,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "markdown接口") @Tag(name = "markdown接口")
@RestController @RestController
@RequestMapping("/api/markdown") @RequestMapping("/api/markdown")
@@ -17,6 +21,13 @@ public class MarkdownController {
@Autowired @Autowired
private MarkdownFileService markdownFileService; private MarkdownFileService markdownFileService;
@Operation(summary = "测试")
@GetMapping("/test")
public R<List<MarkdownFile>> test() {
List<MarkdownFile> test = markdownFileService.test();
return R.success(test);
}
@Operation(summary = "预览markdown文件") @Operation(summary = "预览markdown文件")
@Parameters({ @Parameters({
@Parameter(name = "id", description = "文件ID", required = true) @Parameter(name = "id", description = "文件ID", required = true)
@@ -32,20 +43,20 @@ public class MarkdownController {
@Operation(summary = "创建markdown文件") @Operation(summary = "创建markdown文件")
@Parameters({ @Parameters({
@Parameter(name = "userId", description = "用户id",required = true), @Parameter(name = "groupingId", description = "分组id",required = true),
@Parameter(name = "title", description = "标题",required = true), @Parameter(name = "title", description = "标题",required = true),
@Parameter(name = "fileName", description = "文件名",required = true), @Parameter(name = "fileName", description = "文件名",required = true),
@Parameter(name = "content", description = "内容",required = true) @Parameter(name = "content", description = "内容",required = true)
}) })
@PostMapping @PostMapping
public ResponseEntity<MarkdownFile> createMarkdown( public ResponseEntity<MarkdownFile> createMarkdown(
@RequestParam Long userId, @RequestParam Long groupingId,
@RequestParam String title, @RequestParam String title,
@RequestParam String fileName, @RequestParam String fileName,
@RequestBody String content) { @RequestBody String content) {
MarkdownFile file = markdownFileService.createMarkdownFile( MarkdownFile file = markdownFileService.createMarkdownFile(
userId, title, fileName, content); groupingId, title, fileName, content);
return ResponseEntity.ok(file); return ResponseEntity.ok(file);
} }
@@ -67,4 +78,28 @@ public class MarkdownController {
} }
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
@Operation(summary = "获取所有Markdown文件")
@GetMapping
public ResponseEntity<List<MarkdownFile>> getAllMarkdownFiles() {
// 固定用户ID=1因为是个人笔记
List<MarkdownFile> files = markdownFileService.getAllMarkdownFiles();
return ResponseEntity.ok(files);
}
@Operation(summary = "删除Markdown文件")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMarkdown(@PathVariable Long id) {
if (markdownFileService.deleteMarkdownFile(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
@Operation(summary = "根据分组ID获取Markdown文件")
@GetMapping("/grouping/{groupingId}")
public ResponseEntity<List<MarkdownFile>> getFilesByGroupingId(@PathVariable String groupingId) {
List<MarkdownFile> files = markdownFileService.getFilesByGroupingId(groupingId);
return ResponseEntity.ok(files);
}
} }

View File

@@ -0,0 +1,18 @@
package com.test.bijihoudaun.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "分组实体")
@TableName("grouping")
public class Grouping {
@Schema(description = "分组id",implementation = Long.class)
@TableId(type = IdType.AUTO)
private Long id;
@Schema(description = "分组名称",implementation = String.class)
private String grouping;
}

View File

@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.util.Date;
@Data @Data
@Schema(name = "图片实体") @Schema(name = "图片实体")
@@ -16,8 +16,6 @@ public class Image {
@Schema(description = "图片id",implementation = Long.class) @Schema(description = "图片id",implementation = Long.class)
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
@Schema(description = "外键关联用户ID标识图片所有者\n",implementation = Long.class )
private Long userId;
@Schema(description = " 外键关联Markdown文件ID标识图片所属文档",implementation = Long.class ) @Schema(description = " 外键关联Markdown文件ID标识图片所属文档",implementation = Long.class )
private Long markdownId; private Long markdownId;
@@ -38,6 +36,6 @@ public class Image {
@TableField("content_type") @TableField("content_type")
private String contentType; private String contentType;
@Schema(description = "图片上传时间",implementation = LocalDateTime.class ) @Schema(description = "图片上传时间",implementation = Date.class )
private LocalDateTime createdAt; private Date createdAt;
} }

View File

@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.util.Date;
@Data @Data
@Schema(name = "文本实体") @Schema(name = "文本实体")
@@ -16,7 +16,9 @@ public class MarkdownFile {
@Schema(description = "文本id",implementation = Long.class) @Schema(description = "文本id",implementation = Long.class)
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
@Schema(description = "用户id",implementation = Long.class) @Schema(description = "分组表id",implementation = Long.class)
private Long groupingId;
@Schema(description = "用户ID",implementation = Long.class)
private Long userId; private Long userId;
@Schema(description = "文本标题",implementation = String.class) @Schema(description = "文本标题",implementation = String.class)
private String title; private String title;
@@ -27,8 +29,8 @@ public class MarkdownFile {
@Schema(description = "Markdown内容存储实际文档内容",implementation = String.class) @Schema(description = "Markdown内容存储实际文档内容",implementation = String.class)
private String content; private String content;
@Schema(description = "创建时间",implementation = LocalDateTime.class) @Schema(description = "创建时间",implementation = Date.class)
private LocalDateTime createdAt; private Date createdAt;
@Schema(description = "更新时间",implementation = LocalDateTime.class) @Schema(description = "更新时间",implementation = Date.class)
private LocalDateTime updatedAt; private Date updatedAt;
} }

View File

@@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.util.Date;
@Data @Data
@Schema(name = "用户实体") @Schema(name = "用户实体")
@@ -21,12 +21,12 @@ public class User {
private String password; private String password;
@Schema(description = "邮箱",implementation = String.class) @Schema(description = "邮箱",implementation = String.class)
private String email; private String email;
@Schema(description = "用户创建时间",implementation = String.class) @Schema(description = "用户创建时间",implementation = Date.class)
private LocalDateTime createdAt; private Date createdAt;
@Schema(description = "用户更新时间",implementation = String.class) @Schema(description = "用户更新时间",implementation = Date.class)
private LocalDateTime updatedAt; private Date updatedAt;
@Schema(description = "用户token",implementation = String.class) @Schema(description = "用户token",implementation = String.class)
private String token; private String token;
@Schema(description = "用户token过期时间",implementation = String.class) @Schema(description = "用户token过期时间",implementation = Date.class)
private LocalDateTime tokenEnddata; private Date tokenEnddata;
} }

View File

@@ -0,0 +1,9 @@
package com.test.bijihoudaun.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.test.bijihoudaun.entity.Grouping;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface GroupingMapper extends BaseMapper<Grouping> {
}

View File

@@ -0,0 +1,12 @@
package com.test.bijihoudaun.service;
import com.test.bijihoudaun.entity.Grouping;
import java.util.List;
public interface GroupingService {
Grouping createGrouping(Grouping grouping);
List<Grouping> getAllGroupings();
Grouping updateGrouping(Grouping grouping);
void deleteGrouping(Long id);
}

View File

@@ -10,28 +10,20 @@ import java.util.List;
public interface ImageService extends IService<Image> { public interface ImageService extends IService<Image> {
/** /**
* 上传图片 * 上传图片
* @param userId 用户ID
* @param markdownId Markdown文件ID可选 * @param markdownId Markdown文件ID可选
* @param file 图片文件 * @param file 图片文件
* @return 上传的图片对象 * @return 上传的图片对象
* @throws IOException 文件操作异常 * @throws IOException 文件操作异常
*/ */
Image uploadImage(Long userId, Long markdownId, MultipartFile file) throws IOException; Image uploadImage(Long markdownId, MultipartFile file) throws IOException;
/** /**
* 删除图片 * 删除图片
* @param id 图片ID * @param id 图片ID
* @param userId 用户ID用于权限验证
* @return 是否删除成功 * @return 是否删除成功
*/ */
boolean deleteImage(Long id, Long userId); boolean deleteImage(Long id);
/**
* 获取用户的图片列表
* @param userId 用户ID
* @return 图片列表
*/
List<Image> getUserImages(Long userId);
/** /**
* 获取Markdown文件关联的图片 * 获取Markdown文件关联的图片

View File

@@ -8,13 +8,13 @@ import java.util.List;
public interface MarkdownFileService extends IService<MarkdownFile> { public interface MarkdownFileService extends IService<MarkdownFile> {
/** /**
* 创建Markdown文件 * 创建Markdown文件
* @param userId 用户ID * @param groupingId 分组ID
* @param title 文件标题 * @param title 文件标题
* @param fileName 文件名 * @param fileName 文件名
* @param content 文件内容 * @param content 文件内容
* @return 创建的文件对象 * @return 创建的文件对象
*/ */
MarkdownFile createMarkdownFile(Long userId, String title, String fileName, String content); MarkdownFile createMarkdownFile(Long groupingId, String title, String fileName, String content);
/** /**
* 更新Markdown内容 * 更新Markdown内容
@@ -31,10 +31,26 @@ public interface MarkdownFileService extends IService<MarkdownFile> {
*/ */
MarkdownFile getMarkdownById(Long id); MarkdownFile getMarkdownById(Long id);
/** /**
* 获取用户的所有Markdown文件 * 根据分组ID获取Markdown文件
* @param userId 用户ID * @param groupingId 分组ID
* @return 文件列表 * @return 文件列表
*/ */
List<MarkdownFile> getUserFiles(Long userId); List<MarkdownFile> getFilesByGroupingId(String groupingId);
/**
* 删除Markdown文件
* @param id 文件ID
* @return 是否删除成功
*/
boolean deleteMarkdownFile(Long id);
List<MarkdownFile> test();
/**
* 获取所有Markdown文件
* @return 文件列表
*/
List<MarkdownFile> getAllMarkdownFiles();
} }

View File

@@ -0,0 +1,37 @@
package com.test.bijihoudaun.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.test.bijihoudaun.entity.Grouping;
import com.test.bijihoudaun.mapper.GroupingMapper;
import com.test.bijihoudaun.service.GroupingService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class GroupingServiceImpl
extends ServiceImpl<GroupingMapper, Grouping>
implements GroupingService {
@Override
public Grouping createGrouping(Grouping grouping) {
this.save(grouping);
return grouping;
}
@Override
public List<Grouping> getAllGroupings() {
return this.list();
}
@Override
public Grouping updateGrouping(Grouping grouping) {
this.updateById(grouping);
return grouping;
}
@Override
public void deleteGrouping(Long id) {
this.removeById(id);
}
}

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.test.bijihoudaun.entity.Image; import com.test.bijihoudaun.entity.Image;
import com.test.bijihoudaun.mapper.ImageMapper; import com.test.bijihoudaun.mapper.ImageMapper;
import com.test.bijihoudaun.service.ImageService; import com.test.bijihoudaun.service.ImageService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -15,6 +16,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -25,9 +27,11 @@ public class ImageServiceImpl
@Value("${file.upload-dir}") @Value("${file.upload-dir}")
private String uploadDir; private String uploadDir;
@Resource
private ImageMapper imageMapper;
@Override @Override
public Image uploadImage(Long userId, Long markdownId, MultipartFile file) throws IOException { public Image uploadImage(Long markdownId, MultipartFile file) throws IOException {
// 创建上传目录 // 创建上传目录
Path uploadPath = Paths.get(uploadDir); Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) { if (!Files.exists(uploadPath)) {
@@ -45,30 +49,24 @@ public class ImageServiceImpl
// 创建图片实体 // 创建图片实体
Image image = new Image(); Image image = new Image();
image.setUserId(userId);
image.setMarkdownId(markdownId); image.setMarkdownId(markdownId);
image.setOriginalName(originalFilename); image.setOriginalName(originalFilename);
image.setStoredName(storedName); image.setStoredName(storedName);
image.setUrl("/uploads/" + storedName); image.setUrl("/uploads/" + storedName);
image.setSize(file.getSize()); image.setSize(file.getSize());
image.setContentType(file.getContentType()); image.setContentType(file.getContentType());
image.setCreatedAt(LocalDateTime.now()); image.setCreatedAt(new Date());
this.save(image); this.save(image);
return image; return image;
} }
@Override @Override
public boolean deleteImage(Long id, Long userId) { public boolean deleteImage(Long id) {
QueryWrapper<Image> queryWrapper = new QueryWrapper<>(); Image image = imageMapper.selectById(id);
queryWrapper.eq("id", id)
.eq("user_id", userId);
Image image = this.getOne(queryWrapper);
if (image == null) { if (image == null) {
return false; return false;
} }
try { try {
// 删除文件系统中的图片 // 删除文件系统中的图片
Path filePath = Paths.get(uploadDir, image.getStoredName()); Path filePath = Paths.get(uploadDir, image.getStoredName());
@@ -82,14 +80,6 @@ public class ImageServiceImpl
} }
} }
@Override
public List<Image> getUserImages(Long userId) {
QueryWrapper<Image> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId)
.orderByDesc("created_at");
return this.list(queryWrapper);
}
@Override @Override
public List<Image> getMarkdownImages(Long markdownId) { public List<Image> getMarkdownImages(Long markdownId) {
QueryWrapper<Image> queryWrapper = new QueryWrapper<>(); QueryWrapper<Image> queryWrapper = new QueryWrapper<>();

View File

@@ -6,10 +6,13 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.test.bijihoudaun.entity.MarkdownFile; import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.mapper.MarkdownFileMapper; import com.test.bijihoudaun.mapper.MarkdownFileMapper;
import com.test.bijihoudaun.service.MarkdownFileService; import com.test.bijihoudaun.service.MarkdownFileService;
import com.test.bijihoudaun.util.SnowflakeIdGenerator;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
import java.util.List; import java.util.List;
@Service @Service
@@ -17,17 +20,23 @@ public class MarkdownFileServiceImpl
extends ServiceImpl<MarkdownFileMapper, MarkdownFile> extends ServiceImpl<MarkdownFileMapper, MarkdownFile>
implements MarkdownFileService { implements MarkdownFileService {
@Autowired
MarkdownFileMapper markdownFileMapper;
@Resource
SnowflakeIdGenerator snowflakeIdGenerator;
@Override @Override
public MarkdownFile createMarkdownFile(Long userId, String title, String fileName, String content) { public MarkdownFile createMarkdownFile(Long groupingId, String title, String fileName, String content) {
MarkdownFile file = new MarkdownFile(); MarkdownFile file = new MarkdownFile();
file.setUserId(userId); file.setId(snowflakeIdGenerator.nextId());
file.setGroupingId(groupingId);
file.setTitle(title); file.setTitle(title);
file.setFileName(fileName); file.setFileName(fileName);
file.setContent(content); file.setContent(content);
file.setCreatedAt(LocalDateTime.now()); file.setCreatedAt(new Date());
file.setUpdatedAt(LocalDateTime.now()); file.setUpdatedAt(new Date());
this.save(file); markdownFileMapper.insert(file);
return file; return file;
} }
@@ -36,7 +45,7 @@ public class MarkdownFileServiceImpl
MarkdownFile file = this.getById(id); MarkdownFile file = this.getById(id);
if (file != null) { if (file != null) {
file.setContent(content); file.setContent(content);
file.setUpdatedAt(LocalDateTime.now()); file.setUpdatedAt(new Date());
this.updateById(file); this.updateById(file);
} }
return file; return file;
@@ -47,11 +56,28 @@ public class MarkdownFileServiceImpl
return this.getById(id); return this.getById(id);
} }
@Override @Override
public List<MarkdownFile> getUserFiles(Long userId) { public List<MarkdownFile> getFilesByGroupingId(String groupingId) {
QueryWrapper<MarkdownFile> queryWrapper = new QueryWrapper<>(); QueryWrapper<MarkdownFile> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId) queryWrapper.eq("grouping_id", groupingId)
.orderByDesc("updated_at"); .orderByDesc("updated_at");
return this.list(queryWrapper); return this.list(queryWrapper);
} }
@Override
public boolean deleteMarkdownFile(Long id) {
return this.removeById(id);
}
@Override
public List<MarkdownFile> test() {
List<MarkdownFile> markdownFiles = markdownFileMapper.selectList(null);
return markdownFiles;
}
@Override
public List<MarkdownFile> getAllMarkdownFiles() {
return markdownFileMapper.selectList(null);
}
} }

View File

@@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Calendar;
import java.util.Date;
@Service @Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@@ -36,7 +38,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
user.setUsername(username); user.setUsername(username);
user.setPassword(encrypt); user.setPassword(encrypt);
user.setEmail(email); user.setEmail(email);
user.setCreatedAt(LocalDateTime.now()); user.setCreatedAt(new Date());
userMapper.insert(user); userMapper.insert(user);
return user; return user;
} }
@@ -52,7 +54,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
} }
user.setToken(UuidV7.uuidNoHyphen()); user.setToken(UuidV7.uuidNoHyphen());
// 过期时间:当前时间+3天的时间 // 过期时间:当前时间+3天的时间
user.setTokenEnddata(LocalDateTime.now().plusDays(3)); // 修改时间计算方式
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 3); // 增加3天
user.setTokenEnddata(calendar.getTime());
userMapper.updateById(user); userMapper.updateById(user);
return user; return user;
} }
@@ -68,8 +73,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
queryWrapper.eq(User::getId, id) queryWrapper.eq(User::getId, id)
.eq(User::getToken, token); .eq(User::getToken, token);
User user = getOne(queryWrapper); User user = getOne(queryWrapper);
// 新增过期检查 // 修改过期检查逻辑
return user != null && LocalDateTime.now().isBefore(user.getTokenEnddata()); return user != null && new Date().before(user.getTokenEnddata());
} }
} }

View File

@@ -1,7 +1,7 @@
spring: spring:
datasource: datasource:
driver-class-name: org.sqlite.JDBC driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:C:\\it\\houtaigunli\\liu\\mydatabase.db url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db
jpa: jpa:
hibernate: hibernate:
ddl-auto: none ddl-auto: none

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@kangc/v-md-editor": "^2.3.18",
"vue": "^3.5.13" "vue": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,26 +1,23 @@
<template> <template>
<div id="app"></div> <div id="app">
<router-view></router-view>
</div>
</template> </template>
<script setup> <script setup>
// 这里可以添加全局逻辑
</script> </script>
<style scoped> <style>
.logo { /* 全局样式 */
height: 6em; #app {
padding: 1.5em; font-family: Avenir, Helvetica, Arial, sans-serif;
will-change: filter; -webkit-font-smoothing: antialiased;
transition: filter 300ms; -moz-osx-font-smoothing: grayscale;
} color: #2c3e50;
.logo:hover { margin: 0;
filter: drop-shadow(0 0 2em #646cffaa); padding: 0;
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
} }
</style> </style>

View File

@@ -0,0 +1,266 @@
<template>
<div class="home-page">
<el-container style="height: 100vh;">
<!-- 左侧菜单栏 -->
<el-aside width="300px" style="background-color: #f5f7fa; border-right: 1px solid #ebeef5;">
<div class="sidebar-header">
<h3>笔记分类</h3>
<el-button @click="isCollapsed = !isCollapsed" size="small" type="text">
{{ isCollapsed ? '展开' : '收起' }}
</el-button>
</div>
<el-menu
v-if="!isCollapsed"
default-active="all"
class="el-menu-vertical-demo"
@select="handleMenuSelect"
>
<el-menu-item index="all">
<span>全部笔记</span>
</el-menu-item>
<el-submenu v-for="group in groupings" :key="group.id" :index="'group-'+group.id">
<template #title>
<span>{{ group.grouping }}</span>
</template>
<el-menu-item
v-for="file in groupFiles[group.id] || []"
:key="file.id"
:index="'file-'+file.id"
>
{{ file.title }}
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!-- 右侧内容区 -->
<el-main style="padding: 20px;">
<div class="header" v-if="!selectedFile">
<h1>我的笔记</h1>
<div class="actions">
<el-button type="primary" @click="showEditor = true">新建笔记</el-button>
<el-upload
action=""
:show-file-list="false"
:before-upload="handleMarkdownUpload"
accept=".md"
class="upload-btn"
>
<el-button type="success">上传Markdown</el-button>
</el-upload>
</div>
</div>
<div v-if="selectedFile" class="file-preview">
<div class="preview-header">
<h2>{{ selectedFile.title }}</h2>
<div>
<el-button type="primary" @click="editNote(selectedFile)">编辑</el-button>
<el-button @click="selectedFile = null">返回列表</el-button>
</div>
</div>
<div class="markdown-preview">
<v-md-preview :text="selectedFile.content"></v-md-preview>
</div>
</div>
</el-main>
</el-container>
<el-dialog v-model="showEditor" title="Markdown编辑器" width="80%">
<MarkdownEditor v-if="showEditor" :fileId="currentFileId" @close="showEditor = false" />
</el-dialog>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import MarkdownEditor from './MarkdownEditor.vue';
import VMdPreview from '@kangc/v-md-editor/lib/preview';
import '@kangc/v-md-editor/lib/style/preview.css';
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js';
import '@kangc/v-md-editor/lib/theme/style/github.css';
VMdPreview.use(githubTheme);
export default {
components: {
MarkdownEditor,
[VMdPreview.name]: VMdPreview
},
setup() {
const API_BASE_URL = 'http://localhost:8083';
const markdownFiles = ref([]);
const groupings = ref([]);
const groupFiles = ref({});
const showEditor = ref(false);
const currentFileId = ref(null);
const selectedFile = ref(null);
const isCollapsed = ref(false);
// 获取所有分组
const fetchGroupings = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/api/groupings`);
groupings.value = response.data;
// 为每个分组获取文件
for (const group of groupings.value) {
const filesRes = await axios.get(`${API_BASE_URL}/api/markdown/grouping/${group.id}`);
groupFiles.value[group.id] = filesRes.data;
}
} catch (error) {
ElMessage.error('获取分组失败: ' + error.message);
}
};
// 获取所有Markdown文件
const fetchMarkdownFiles = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/api/markdown`);
markdownFiles.value = response.data;
} catch (error) {
ElMessage.error('获取笔记列表失败: ' + error.message);
}
};
// 编辑笔记
const editNote = (file) => {
currentFileId.value = file.id;
showEditor.value = true;
};
// 删除笔记
const deleteNote = async (file) => {
try {
await axios.delete(`${API_BASE_URL}/api/markdown/${file.id}`);
ElMessage.success('删除成功');
fetchMarkdownFiles();
fetchGroupings(); // 重新加载分组和文件
} catch (error) {
ElMessage.error('删除失败: ' + error.message);
}
};
// 菜单选择处理
const handleMenuSelect = (index) => {
if (index === 'all') {
selectedFile.value = null;
} else if (index.startsWith('file-')) {
const fileId = index.split('-')[1];
const file = markdownFiles.value.find(f => f.id == fileId);
if (file) {
selectedFile.value = file;
}
}
};
// 上传Markdown文件处理
const handleMarkdownUpload = (file) => {
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target.result;
const fileName = file.name.replace('.md', '');
try {
const params = new URLSearchParams();
params.append('userId', '1');
params.append('title', fileName);
params.append('fileName', fileName);
await axios.post(`${API_BASE_URL}/api/markdown`, content, {
params: params,
headers: {
'Content-Type': 'text/plain'
}
});
ElMessage.success('上传成功');
fetchMarkdownFiles();
fetchGroupings(); // 重新加载分组和文件
} catch (error) {
ElMessage.error('上传失败: ' + error.message);
}
};
reader.readAsText(file);
return false; // 阻止默认上传行为
};
onMounted(() => {
fetchMarkdownFiles();
fetchGroupings();
});
return {
markdownFiles,
groupings,
groupFiles,
showEditor,
currentFileId,
selectedFile,
isCollapsed,
editNote,
deleteNote,
handleMarkdownUpload,
handleMenuSelect
};
}
};
</script>
<style scoped>
.home-page {
height: 100vh;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #ebeef5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.actions {
display: flex;
gap: 10px;
}
.upload-btn {
display: inline-block;
margin-left: 10px;
}
.file-preview {
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fff;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.markdown-preview {
min-height: 500px;
padding: 20px;
border: 1px solid #f0f0f0;
border-radius: 4px;
background: #fff;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div class="markdown-editor">
<div class="editor-header">
<el-input v-model="title" placeholder="标题" class="title-input" />
<el-input v-model="fileName" placeholder="文件名" class="file-input" />
</div>
<v-md-editor
v-model="content"
height="500px"
:disabled-menus="[]"
@upload-image="handleImageUpload"
></v-md-editor>
<div class="editor-footer">
<el-button type="primary" @click="saveMarkdown">保存</el-button>
<el-button @click="previewMarkdown">预览</el-button>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
export default {
name: 'MarkdownEditor',
setup() {
const title = ref('');
const fileName = ref('');
const content = ref('');
const userId = ref(1); // 示例用户ID实际应用中从登录信息获取
const currentFileId = ref(null);
const saveMarkdown = async () => {
try {
const API_BASE_URL = 'http://localhost:8083';
let response;
if (currentFileId.value) {
// 更新已有文件content作为请求体
response = await axios.post(`${API_BASE_URL}/api/markdown/${currentFileId.value}`,
content.value, {
withCredentials: true,
headers: {
'Content-Type': 'text/plain'
}
}
);
} else {
// 创建新文件userId, title, fileName作为查询参数content作为请求体
const params = new URLSearchParams();
params.append('userId', userId.value);
params.append('title', title.value);
params.append('fileName', fileName.value);
response = await axios.post(`${API_BASE_URL}/api/markdown`,
content.value, {
params: params,
withCredentials: true,
headers: {
'Content-Type': 'text/plain'
}
}
);
currentFileId.value = response.data.id;
}
ElMessage.success('保存成功');
} catch (error) {
ElMessage.error('保存失败: ' + error.message);
}
};
const previewMarkdown = () => {
if (!currentFileId.value) {
ElMessage.warning('请先保存文档');
return;
}
window.open(`/api/markdown/${currentFileId.value}`, '_blank');
};
const handleImageUpload = async (event, insertImage, files) => {
const file = files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('userId', userId.value);
formData.append('markdownId', currentFileId.value);
try {
const API_BASE_URL = 'http://localhost:8083';
const response = await axios.post(`${API_BASE_URL}/api/images`, formData, {
withCredentials: true,
headers: {
'Content-Type': 'multipart/form-data'
}
});
// 插入图片到编辑器
insertImage({
url: response.data.url,
desc: response.data.originalName
});
} catch (error) {
ElMessage.error('图片上传失败: ' + error.message);
}
};
return {
title,
fileName,
content,
saveMarkdown,
previewMarkdown,
handleImageUpload
};
}
};
</script>
<style scoped>
.markdown-editor {
padding: 20px;
}
.editor-header {
display: flex;
margin-bottom: 15px;
}
.title-input {
flex: 2;
margin-right: 10px;
}
.file-input {
flex: 1;
}
.editor-footer {
margin-top: 15px;
text-align: right;
}
</style>

View File

@@ -1,4 +1,21 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import VMdEditor from '@kangc/v-md-editor';
import '@kangc/v-md-editor/lib/style/base-editor.css';
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
createApp(App).mount('#app') const app = createApp(App)
// 配置Markdown编辑器
VMdEditor.use(vuepressTheme);
app.use(VMdEditor);
// 使用Element Plus和路由
app.use(ElementPlus)
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,33 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../components/HomePage.vue';
import MarkdownEditor from '../components/MarkdownEditor.vue';
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: HomePage
},
{
path: '/editor',
name: 'Editor',
component: MarkdownEditor
},
{
path: '/editor/:id',
name: 'EditMarkdown',
component: MarkdownEditor,
props: true
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;

View File

@@ -7,3 +7,7 @@ vite+vue3.js怎么支持markdown文件的上传预览、编辑、创建呢
2、分析好项目我要实现其中的markdown文件的上传预览、编辑、创建并且是带有图片的能在前端实现上传预览、编辑、创建等功能要符合项目用到的技术、版本和业务不要修改无关的文件。 要求使用MCP服务器中的context7工具来查询最新的文档 2、分析好项目我要实现其中的markdown文件的上传预览、编辑、创建并且是带有图片的能在前端实现上传预览、编辑、创建等功能要符合项目用到的技术、版本和业务不要修改无关的文件。 要求使用MCP服务器中的context7工具来查询最新的文档
3、现在开始实现上述的功能不要去分析前端现在先弄好后端先实现上传、预览、编辑、创建等功能要符合项目用到的技术、版本和业务不要修改无关的文件。 要求使用MCP服务器中的context7工具用来查询最新的文档确保我想要的功能是符合项目的要求并且是符合项目的业务逻辑。 3、现在开始实现上述的功能不要去分析前端现在先弄好后端先实现上传、预览、编辑、创建等功能要符合项目用到的技术、版本和业务不要修改无关的文件。 要求使用MCP服务器中的context7工具用来查询最新的文档确保我想要的功能是符合项目的要求并且是符合项目的业务逻辑。
MarkdownFile(id=325279554144964608, userId=1, title=测试, fileName=测试, content=userId=1&title=%E6%B5%8B%E8%AF%95&fileName=%E6%B5%8B%E8%AF%95&content=%E6%B5%8B%E8%AF%95, createdAt=2025-06-16T22:24:49.110387, updatedAt=2025-06-16T22:24:49.110387)

Binary file not shown.

View File

@@ -20,8 +20,7 @@ CREATE TABLE IF NOT EXISTS markdown_file (
file_name TEXT NOT NULL, file_name TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
); );
-- 图片表 -- 图片表
@@ -34,7 +33,5 @@ CREATE TABLE IF NOT EXISTS image (
url TEXT NOT NULL, url TEXT NOT NULL,
size INTEGER NOT NULL, size INTEGER NOT NULL,
content_type TEXT NOT NULL, content_type TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
FOREIGN KEY (markdown_id) REFERENCES markdown_file(id) ON DELETE SET NULL
); );