feat(recycle-bin): 实现回收站功能

- 在数据库中添加逻辑删除字段和相关索引- 新增回收站相关实体类和接口
- 实现回收站列表查询、项目恢复、永久删除和清空回收站等功能
- 前端集成回收站接口,支持回收站页面操作
This commit is contained in:
ikmkj
2025-07-31 23:09:58 +08:00
parent 56633dfd3b
commit 1491cfc330
13 changed files with 254 additions and 10 deletions

View File

@@ -3,6 +3,7 @@ package com.test.bijihoudaun.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -10,11 +11,12 @@ import org.springframework.context.annotation.Configuration;
public class MybatisPlusConfig { public class MybatisPlusConfig {
/** /**
* 添加分页插件 * 添加分页插件和逻辑删除插件
*/ */
@Bean @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQLITE)); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQLITE));
return interceptor; return interceptor;
} }

View File

@@ -0,0 +1,47 @@
package com.test.bijihoudaun.controller;
import com.test.bijihoudaun.common.response.R;
import com.test.bijihoudaun.entity.TrashItemVo;
import com.test.bijihoudaun.service.TrashService;
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.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/trash")
@Tag(name = "回收站管理")
public class TrashController {
@Autowired
private TrashService trashService;
@GetMapping
@Operation(summary = "获取回收站列表")
public R<List<TrashItemVo>> getTrashItems() {
return R.success(trashService.getTrashItems());
}
@PostMapping("/restore/{type}/{id}")
@Operation(summary = "恢复项目")
public R<Void> restoreItem(@PathVariable String type, @PathVariable String id) {
trashService.restoreItem(id, type);
return R.success();
}
@DeleteMapping("/permanently/{type}/{id}")
@Operation(summary = "永久删除项目")
public R<Void> permanentlyDeleteItem(@PathVariable String type, @PathVariable String id) {
trashService.permanentlyDeleteItem(id, type);
return R.success();
}
@DeleteMapping("/clean")
@Operation(summary = "清空回收站")
public R<Void> cleanTrash() {
trashService.cleanTrash();
return R.success();
}
}

View File

@@ -3,15 +3,19 @@ package com.test.bijihoudaun.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data @Data
@Schema(name = "分组实体") @Schema(name = "分组实体")
@TableName("grouping") @TableName("grouping")
public class Grouping { public class Grouping implements Serializable {
@Schema(description = "分组id",implementation = Long.class) @Schema(description = "分组id",implementation = Long.class)
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段 @JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段
@@ -24,4 +28,14 @@ public class Grouping {
@Schema(description = "分组名称",implementation = String.class) @Schema(description = "分组名称",implementation = String.class)
private String grouping; private String grouping;
@Schema(description = "是否删除 0-未删除 1-已删除", implementation = Integer.class)
@TableLogic
private Integer isDeleted;
@Schema(description = "删除时间", implementation = Date.class)
private Date deletedAt;
@Schema(description = "删除人ID", implementation = Long.class)
private Long deletedBy;
} }

View File

@@ -3,17 +3,19 @@ package com.test.bijihoudaun.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
import java.util.Date; import java.util.Date;
@Data @Data
@Schema(name = "文本实体") @Schema(name = "文本实体")
@TableName("markdown_file") @TableName("markdown_file")
public class MarkdownFile { public class MarkdownFile implements Serializable {
@Schema(description = "文本id",implementation = Long.class) @Schema(description = "文本id",implementation = Long.class)
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
@JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段 @JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段
@@ -33,4 +35,14 @@ public class MarkdownFile {
private Date createdAt; private Date createdAt;
@Schema(description = "更新时间",implementation = Date.class) @Schema(description = "更新时间",implementation = Date.class)
private Date updatedAt; private Date updatedAt;
@Schema(description = "是否删除 0-未删除 1-已删除", implementation = Integer.class)
@TableLogic
private Integer isDeleted;
@Schema(description = "删除时间", implementation = Date.class)
private Date deletedAt;
@Schema(description = "删除人ID", implementation = Long.class)
private Long deletedBy;
} }

View File

@@ -0,0 +1,26 @@
package com.test.bijihoudaun.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@Schema(name = "回收站项目视图对象")
public class TrashItemVo {
@Schema(description = "项目ID")
private String id;
@Schema(description = "项目名称(笔记标题或分组名称)")
private String name;
@Schema(description = "项目类型note 或 group")
private String type;
@Schema(description = "删除时间")
private Date deletedAt;
@Schema(description = "删除者ID")
private String deletedBy;
}

View File

@@ -0,0 +1,33 @@
package com.test.bijihoudaun.service;
import com.test.bijihoudaun.entity.TrashItemVo;
import java.util.List;
public interface TrashService {
/**
* 获取回收站中的所有项目
* @return 回收站项目列表
*/
List<TrashItemVo> getTrashItems();
/**
* 恢复指定的回收站项目
* @param id 项目ID
* @param type 项目类型 ("note" 或 "group")
*/
void restoreItem(String id, String type);
/**
* 永久删除指定的回收站项目
* @param id 项目ID
* @param type 项目类型 ("note" 或 "group")
*/
void permanentlyDeleteItem(String id, String type);
/**
* 清空回收站
*/
void cleanTrash();
}

View File

@@ -0,0 +1,99 @@
package com.test.bijihoudaun.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.test.bijihoudaun.entity.Grouping;
import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.entity.TrashItemVo;
import com.test.bijihoudaun.mapper.GroupingMapper;
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
import com.test.bijihoudaun.service.TrashService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class TrashServiceImpl implements TrashService {
@Autowired
private MarkdownFileMapper markdownFileMapper;
@Autowired
private GroupingMapper groupingMapper;
@Override
public List<TrashItemVo> getTrashItems() {
// 查询已删除的笔记
List<TrashItemVo> deletedNotes = markdownFileMapper.selectList(new QueryWrapper<MarkdownFile>().eq("is_deleted", 1))
.stream()
.map(file -> {
TrashItemVo vo = new TrashItemVo();
vo.setId(String.valueOf(file.getId()));
vo.setName(file.getTitle());
vo.setType("note");
vo.setDeletedAt(file.getDeletedAt());
vo.setDeletedBy(String.valueOf(file.getDeletedBy()));
return vo;
})
.collect(Collectors.toList());
// 查询已删除的分组
List<TrashItemVo> deletedGroups = groupingMapper.selectList(new QueryWrapper<Grouping>().eq("is_deleted", 1))
.stream()
.map(group -> {
TrashItemVo vo = new TrashItemVo();
vo.setId(String.valueOf(group.getId()));
vo.setName(group.getGrouping());
vo.setType("group");
vo.setDeletedAt(group.getDeletedAt());
vo.setDeletedBy(String.valueOf(group.getDeletedBy()));
return vo;
})
.collect(Collectors.toList());
// 合并并返回
return Stream.concat(deletedNotes.stream(), deletedGroups.stream()).collect(Collectors.toList());
}
@Override
@Transactional
public void restoreItem(String id, String type) {
if ("note".equals(type)) {
MarkdownFile file = new MarkdownFile();
file.setId(Long.parseLong(id));
file.setIsDeleted(0);
file.setDeletedAt(null);
file.setDeletedBy(null);
markdownFileMapper.updateById(file);
} else if ("group".equals(type)) {
Grouping group = new Grouping();
group.setId(Long.parseLong(id));
group.setIsDeleted(0);
group.setDeletedAt(null);
group.setDeletedBy(null);
groupingMapper.updateById(group);
}
}
@Override
@Transactional
public void permanentlyDeleteItem(String id, String type) {
if ("note".equals(type)) {
markdownFileMapper.deleteById(Long.parseLong(id));
} else if ("group".equals(type)) {
// 删除分组时,也删除其下的所有笔记
groupingMapper.deleteById(Long.parseLong(id));
markdownFileMapper.delete(new QueryWrapper<MarkdownFile>().eq("grouping_id", id));
}
}
@Override
@Transactional
public void cleanTrash() {
markdownFileMapper.delete(new QueryWrapper<MarkdownFile>().eq("is_deleted", 1));
groupingMapper.delete(new QueryWrapper<Grouping>().eq("is_deleted", 1));
}
}

View File

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

View File

@@ -29,6 +29,11 @@ mybatis-plus:
mapper-locations: classpath:mapper/*.xml mapper-locations: classpath:mapper/*.xml
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# JWT 配置 # JWT 配置
jwt: jwt:

View File

@@ -120,10 +120,10 @@ export const MD5 = (data, file) => {
export const getTrash = () => axiosApi.get('/api/trash'); export const getTrash = () => axiosApi.get('/api/trash');
// 恢复项目 // 恢复项目
export const restoreTrashItem = (id) => axiosApi.post(`/api/trash/restore/${id}`); export const restoreTrashItem = (id, type) => axiosApi.post(`/api/trash/restore/${type}/${id}`);
// 彻底删除 // 彻底删除
export const permanentlyDeleteItem = (id) => axiosApi.delete(`/api/trash/permanently/${id}`); export const permanentlyDeleteItem = (id, type) => axiosApi.delete(`/api/trash/permanently/${type}/${id}`);
// 清空回收站 // 清空回收站
export const cleanTrash = () => axiosApi.delete('/api/trash/clean'); export const cleanTrash = () => axiosApi.delete('/api/trash/clean');

View File

@@ -48,7 +48,7 @@ const fetchTrashItems = async () => {
const handleRestore = async (item) => { const handleRestore = async (item) => {
try { try {
await restoreTrashItem(item.id); await restoreTrashItem(item.id, item.type);
ElMessage.success('恢复成功'); ElMessage.success('恢复成功');
fetchTrashItems(); fetchTrashItems();
} catch (error) { } catch (error) {
@@ -63,7 +63,7 @@ const handleDeletePermanently = async (item) => {
type: 'warning', type: 'warning',
}); });
try { try {
await permanentlyDeleteItem(item.id); await permanentlyDeleteItem(item.id, item.type);
ElMessage.success('已永久删除'); ElMessage.success('已永久删除');
fetchTrashItems(); fetchTrashItems();
} catch (error) { } catch (error) {

Binary file not shown.

View File

@@ -21,7 +21,10 @@ 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,
is_deleted INTEGER DEFAULT 0,
deleted_at DATETIME,
deleted_by INTEGER
); );
-- 图片表 -- 图片表
@@ -42,5 +45,8 @@ CREATE TABLE "grouping" (
"id" INTEGER NOT NULL DEFAULT 0, "id" INTEGER NOT NULL DEFAULT 0,
"grouping" TEXT NOT NULL, "grouping" TEXT NOT NULL,
"parentId" INTEGER, "parentId" INTEGER,
"is_deleted" INTEGER DEFAULT 0,
"deleted_at" DATETIME,
"deleted_by" INTEGER,
PRIMARY KEY ("id") PRIMARY KEY ("id")
); );