MD编辑器更新

UI更新
This commit is contained in:
2026-09-23 14:07:19 +08:00
parent 429e8a7bb7
commit ec21ba0876
379 changed files with 15504 additions and 606 deletions
@@ -1,6 +1,7 @@
package com.accounting.controller;
import com.accounting.dto.NoteAttachmentResponse;
import com.accounting.dto.NoteFolderAssignRequest;
import com.accounting.dto.NoteImportRequest;
import com.accounting.dto.NotePageResponse;
import com.accounting.dto.NoteRequest;
@@ -45,10 +46,14 @@ public class NoteController {
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String tag,
@RequestParam(defaultValue = "all") String scope,
@RequestParam(required = false) Long folderId,
@RequestParam(defaultValue = "updated") String sort,
Authentication authentication) {
Long userId = getUserId(authentication);
return ResponseEntity.ok(noteService.list(userId, keyword, tag, page, size));
return ResponseEntity.ok(
noteService.list(userId, scope, folderId, keyword, tag, sort, page, size));
}
@Operation(summary = "查询笔记详情(含正文)")
@@ -94,7 +99,7 @@ public class NoteController {
Authentication authentication) {
Long userId = getUserId(authentication);
int imported = noteService.importBatch(userId, request.getNotes());
int imported = noteService.importBatch(userId, request.getNotes(), request.getFolderId());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
@@ -102,6 +107,21 @@ public class NoteController {
return ResponseEntity.ok(result);
}
@Operation(summary = "把笔记移动到某个目录(folderId 为 null 表示移到未分类)")
@PutMapping("/{id}/folder")
public ResponseEntity<Map<String, Object>> assignFolder(
@PathVariable Long id,
@RequestBody NoteFolderAssignRequest request,
Authentication authentication) {
Long userId = getUserId(authentication);
noteService.assignFolder(userId, id, request.getFolderId());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return ResponseEntity.ok(result);
}
@Operation(summary = "上传图片附件,返回可直接在 Markdown 中引用的地址")
@PostMapping("/attachments")
public ResponseEntity<NoteAttachmentResponse> uploadAttachment(
@@ -0,0 +1,106 @@
package com.accounting.controller;
import com.accounting.dto.FolderMoveRequest;
import com.accounting.dto.FolderNode;
import com.accounting.dto.FolderRequest;
import com.accounting.dto.FolderTreeResponse;
import com.accounting.entity.User;
import com.accounting.mapper.UserMapper;
import com.accounting.service.NoteFolderService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 笔记目录
*
* <p>路径挂在 {@code /api/notes/folders} 下,和 {@code /api/notes/{id}} 并存 ——
* Spring 的路径匹配对字面量段的优先级高于变量段,所以 {@code folders} 不会被
* 当成笔记 id,这一点和已有的 {@code /api/notes/tags} 是同一个机制。</p>
*/
@Tag(name = "笔记目录", description = "笔记文件夹的增删改查与移动")
@RestController
@RequestMapping("/api/notes/folders")
public class NoteFolderController {
@Autowired
private NoteFolderService folderService;
@Autowired
private UserMapper userMapper;
@Operation(summary = "获取目录树(含各目录的笔记数,数量包含子目录)")
@GetMapping
public ResponseEntity<FolderTreeResponse> tree(Authentication authentication) {
return ResponseEntity.ok(folderService.tree(getUserId(authentication)));
}
@Operation(summary = "新建目录")
@PostMapping
public ResponseEntity<FolderNode> create(
@RequestBody FolderRequest request,
Authentication authentication) {
Long userId = getUserId(authentication);
return ResponseEntity.ok(
folderService.create(userId, request.getName(), request.getParentId()));
}
@Operation(summary = "重命名目录")
@PutMapping("/{id}")
public ResponseEntity<Map<String, Object>> rename(
@PathVariable Long id,
@RequestBody FolderRequest request,
Authentication authentication) {
folderService.rename(getUserId(authentication), id, request.getName());
return ResponseEntity.ok(ok());
}
@Operation(summary = "移动目录到新的父目录(parentId 为 null 表示移到根目录)")
@PutMapping("/{id}/move")
public ResponseEntity<Map<String, Object>> move(
@PathVariable Long id,
@RequestBody FolderMoveRequest request,
Authentication authentication) {
folderService.move(getUserId(authentication), id, request.getParentId());
return ResponseEntity.ok(ok());
}
@Operation(summary = "删除目录(子目录与笔记上移到父目录,不删除笔记)")
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> delete(
@PathVariable Long id,
Authentication authentication) {
folderService.delete(getUserId(authentication), id);
return ResponseEntity.ok(ok());
}
private Map<String, Object> ok() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return result;
}
private Long getUserId(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getUsername, username)
);
if (user == null) {
throw new IllegalArgumentException("用户不存在");
}
return user.getId();
}
}
@@ -0,0 +1,14 @@
package com.accounting.dto;
import lombok.Data;
/**
* 移动目录(改变它的父目录)
*
* <p>{@code parentId} 为 null 表示移动到根目录下。</p>
*/
@Data
public class FolderMoveRequest {
private Long parentId;
}
@@ -0,0 +1,30 @@
package com.accounting.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 目录树节点
*/
@Data
public class FolderNode {
private Long id;
private String name;
/** 父目录 ID,null = 根目录下的一级目录 */
private Long parentId;
/**
* 该目录下的笔记数(**含所有子目录**)
*
* <p>这样父目录的数量不会和"点进去看到的条数"对不上:
* 列表默认就是按"当前目录含子目录"来查的,两边口径一致。</p>
*/
private int noteCount;
private List<FolderNode> children = new ArrayList<>();
}
@@ -0,0 +1,15 @@
package com.accounting.dto;
import lombok.Data;
/**
* 新建 / 重命名目录
*/
@Data
public class FolderRequest {
private String name;
/** 新建时指定父目录;null = 建在根目录下 */
private Long parentId;
}
@@ -0,0 +1,22 @@
package com.accounting.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 目录树接口的返回体
*/
@Data
public class FolderTreeResponse {
/** 目录树(只含真实目录,根层级) */
private List<FolderNode> folders = new ArrayList<>();
/** 未分类的笔记数 */
private int uncategorizedCount;
/** 全部笔记数,含未分类 */
private int totalCount;
}
@@ -0,0 +1,17 @@
package com.accounting.dto;
import lombok.Data;
/**
* 把笔记移动到某个目录
*
* <p>单独开一个接口而不是复用 {@code PUT /api/notes/{id}},是因为后者是
* 部分更新语义(字段为 null 表示"不改动"),没法表达"把笔记移回未分类"。
* 这里 folderId 为 null 是明确的"未分类"。</p>
*/
@Data
public class NoteFolderAssignRequest {
/** 目标目录 ID,null = 移到未分类 */
private Long folderId;
}
@@ -20,6 +20,9 @@ public class NoteImportRequest {
@Valid
private List<Item> notes;
/** 导入到哪个目录;null = 未分类。由前端按当前所在目录传入 */
private Long folderId;
@Data
public static class Item {
@@ -16,6 +16,9 @@ public class NoteListItem {
private Long id;
/** 所属目录 ID,null = 未分类 */
private Long folderId;
private String title;
/** 摘要,由正文自动提取 */
@@ -13,6 +13,9 @@ public class NoteResponse {
private Long id;
/** 所属目录 ID,null = 未分类 */
private Long folderId;
private String title;
private String content;
@@ -24,6 +24,9 @@ public class Note {
private Long userId;
/** 所属目录 ID,null = 未分类 */
private Long folderId;
private String title;
/** Markdown 正文 */
@@ -0,0 +1,38 @@
package com.accounting.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 笔记目录(文件夹)
*
* <p>树形结构,parentId 为 null 表示根目录下的目录。</p>
*/
@Data
@TableName("note_folder")
public class NoteFolder {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
/** 父目录 ID,null = 位于根目录 */
private Long parentId;
private String name;
private Integer sortOrder;
@TableLogic
private Integer deleted;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@@ -0,0 +1,9 @@
package com.accounting.mapper;
import com.accounting.entity.NoteFolder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NoteFolderMapper extends BaseMapper<NoteFolder> {
}
@@ -3,7 +3,26 @@ package com.accounting.mapper;
import com.accounting.entity.Note;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface NoteMapper extends BaseMapper<Note> {
/**
* 按目录统计笔记数(只统计当前目录直接挂的,不含子目录)
*
* <p>用注解写原生 SQL 是为了能 group by 聚合 —— BaseMapper 不带这个能力。
* 这里显式带上 {@code deleted = 0},因为自定义 SQL 不会走 MyBatis-Plus
* 的逻辑删除自动拼接。</p>
*
* <p>folder_id 为 NULL 的那一行表示未分类笔记。</p>
*/
@Select("SELECT folder_id, COUNT(*) AS cnt FROM note "
+ "WHERE user_id = #{userId} AND deleted = 0 "
+ "GROUP BY folder_id")
List<Map<String, Object>> countGroupByFolder(@Param("userId") Long userId);
}
@@ -0,0 +1,295 @@
package com.accounting.service;
import com.accounting.dto.FolderNode;
import com.accounting.dto.FolderTreeResponse;
import com.accounting.entity.Note;
import com.accounting.entity.NoteFolder;
import com.accounting.mapper.NoteFolderMapper;
import com.accounting.mapper.NoteMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 笔记目录(文件夹)
*
* <p>树的深度和节点数在个人使用场景下都很小(几百个顶天),
* 所以不做递归 SQL,一次性把该用户的目录全查出来在内存里组树。</p>
*/
@Service
public class NoteFolderService {
@Autowired
private NoteFolderMapper folderMapper;
@Autowired
private NoteMapper noteMapper;
private static final int NAME_MAX_LENGTH = 100;
// ==================================================================
// 查询
// ==================================================================
/**
* 目录树 + 各目录的笔记数
*
* <p>目录上的笔记数是**含子目录**的累计值,和列表接口
* 「按当前目录含子目录查询」的口径保持一致,
* 不会出现"父目录显示 5、点进去只有 2"的困惑。</p>
*/
public FolderTreeResponse tree(Long userId) {
List<NoteFolder> folders = folderMapper.selectList(
new LambdaQueryWrapper<NoteFolder>()
.eq(NoteFolder::getUserId, userId)
.orderByAsc(NoteFolder::getSortOrder)
.orderByAsc(NoteFolder::getId));
Map<Long, Integer> directCounts = new HashMap<>();
int uncategorized = 0;
int total = 0;
for (Map<String, Object> row : noteMapper.countGroupByFolder(userId)) {
int count = toInt(row.get("cnt"));
total += count;
Object folderId = row.get("folder_id");
if (folderId == null) {
uncategorized = count;
} else {
directCounts.put(((Number) folderId).longValue(), count);
}
}
Map<Long, FolderNode> nodeById = new LinkedHashMap<>();
for (NoteFolder folder : folders) {
FolderNode node = new FolderNode();
node.setId(folder.getId());
node.setName(folder.getName());
node.setParentId(folder.getParentId());
node.setNoteCount(directCounts.getOrDefault(folder.getId(), 0));
nodeById.put(folder.getId(), node);
}
List<FolderNode> roots = new ArrayList<>();
for (FolderNode node : nodeById.values()) {
FolderNode parent = node.getParentId() == null ? null : nodeById.get(node.getParentId());
// 父目录查不到(数据异常)时当根节点处理,避免整棵子树丢失
if (parent == null) {
roots.add(node);
} else {
parent.getChildren().add(node);
}
}
// 自底向上把子目录的数量累加到父目录
for (FolderNode root : roots) {
accumulateCount(root);
}
FolderTreeResponse response = new FolderTreeResponse();
response.setFolders(roots);
response.setUncategorizedCount(uncategorized);
response.setTotalCount(total);
return response;
}
private int accumulateCount(FolderNode node) {
int sum = node.getNoteCount();
for (FolderNode child : node.getChildren()) {
sum += accumulateCount(child);
}
node.setNoteCount(sum);
return sum;
}
/**
* 某个目录及其所有子孙目录的 id 集合(含自身)。
* NoteService 按目录查询笔记时用它拼 IN 条件。
*/
public Set<Long> subtreeIds(Long userId, Long rootId) {
if (rootId == null) {
return new LinkedHashSet<>();
}
List<NoteFolder> folders = folderMapper.selectList(
new LambdaQueryWrapper<NoteFolder>()
.select(NoteFolder::getId, NoteFolder::getParentId)
.eq(NoteFolder::getUserId, userId));
Map<Long, List<Long>> childrenOf = new HashMap<>();
for (NoteFolder folder : folders) {
childrenOf.computeIfAbsent(folder.getParentId(), k -> new ArrayList<>()).add(folder.getId());
}
Set<Long> result = new LinkedHashSet<>();
collect(rootId, childrenOf, result);
return result;
}
private void collect(Long id, Map<Long, List<Long>> childrenOf, Set<Long> result) {
// 正常情况下不会有环(move 里做了校验),这里再兜一层防止脏数据把请求拖死
if (!result.add(id)) {
return;
}
for (Long child : childrenOf.getOrDefault(id, List.of())) {
collect(child, childrenOf, result);
}
}
// ==================================================================
// 写入
// ==================================================================
@Transactional
public FolderNode create(Long userId, String name, Long parentId) {
String cleanName = requireName(name);
if (parentId != null) {
requireOwned(userId, parentId);
}
ensureNameNotTaken(userId, parentId, cleanName, null);
NoteFolder folder = new NoteFolder();
folder.setUserId(userId);
folder.setName(cleanName);
folder.setParentId(parentId);
folder.setSortOrder(0);
folderMapper.insert(folder);
FolderNode node = new FolderNode();
node.setId(folder.getId());
node.setName(folder.getName());
node.setParentId(folder.getParentId());
node.setNoteCount(0);
return node;
}
@Transactional
public void rename(Long userId, Long id, String name) {
NoteFolder folder = requireOwned(userId, id);
String cleanName = requireName(name);
ensureNameNotTaken(userId, folder.getParentId(), cleanName, id);
folder.setName(cleanName);
folderMapper.updateById(folder);
}
/**
* 移动目录到新的父目录
*/
@Transactional
public void move(Long userId, Long id, Long newParentId) {
NoteFolder folder = requireOwned(userId, id);
if (newParentId != null) {
requireOwned(userId, newParentId);
// 不能把目录挂到自己的子目录下,否则会形成一个断开的环
if (subtreeIds(userId, id).contains(newParentId)) {
throw new IllegalArgumentException("不能把目录移动到它自己的子目录里");
}
}
// 用 UpdateWrapper 而不是 updateById:后者会忽略 null 字段,
// 导致"移动到根目录"(parentId = null)静默失败
folderMapper.update(null, new LambdaUpdateWrapper<NoteFolder>()
.eq(NoteFolder::getId, id)
.eq(NoteFolder::getUserId, userId)
.set(NoteFolder::getParentId, newParentId));
}
/**
* 删除目录
*
* <p>子目录和笔记**上移到被删目录的父级**,不跟着删除 —— 先保数据,
* 用户真要把笔记一起清了应该去删笔记。</p>
*/
@Transactional
public void delete(Long userId, Long id) {
NoteFolder folder = requireOwned(userId, id);
Long newParentId = folder.getParentId();
// 1. 子目录上移
folderMapper.update(null, new LambdaUpdateWrapper<NoteFolder>()
.eq(NoteFolder::getUserId, userId)
.eq(NoteFolder::getParentId, id)
.set(NoteFolder::getParentId, newParentId));
// 2. 笔记上移(目录本身只是归类,笔记内容不受影响)
noteMapper.update(null, new LambdaUpdateWrapper<Note>()
.eq(Note::getUserId, userId)
.eq(Note::getFolderId, id)
.set(Note::getFolderId, newParentId));
// 3. 目录本身走逻辑删除
folderMapper.deleteById(id);
}
// ==================================================================
// 内部工具
// ==================================================================
public NoteFolder requireOwned(Long userId, Long id) {
NoteFolder folder = folderMapper.selectById(id);
if (folder == null || !folder.getUserId().equals(userId)) {
throw new IllegalArgumentException("目录不存在");
}
return folder;
}
/** 目标父目录在移动前也需要校验,供别的服务复用 */
public void requireOwnedIfPresent(Long userId, Long id) {
if (id != null) {
requireOwned(userId, id);
}
}
private String requireName(String name) {
if (!StringUtils.hasText(name)) {
throw new IllegalArgumentException("目录名不能为空");
}
String clean = name.trim();
if (clean.length() > NAME_MAX_LENGTH) {
clean = clean.substring(0, NAME_MAX_LENGTH);
}
return clean;
}
/**
* 同一层级下不允许重名,否则用户分不清点哪个。
* [excludeId] 用于重命名时排除自己。
*/
private void ensureNameNotTaken(Long userId, Long parentId, String name, Long excludeId) {
LambdaQueryWrapper<NoteFolder> wrapper = new LambdaQueryWrapper<NoteFolder>()
.eq(NoteFolder::getUserId, userId)
.eq(NoteFolder::getName, name);
if (parentId == null) {
wrapper.isNull(NoteFolder::getParentId);
} else {
wrapper.eq(NoteFolder::getParentId, parentId);
}
if (excludeId != null) {
wrapper.ne(NoteFolder::getId, excludeId);
}
if (folderMapper.selectCount(wrapper) > 0) {
throw new IllegalArgumentException("同一层级下已经有叫「" + name + "」的目录了");
}
}
private static int toInt(Object value) {
return value instanceof Number ? ((Number) value).intValue() : 0;
}
}
@@ -8,6 +8,7 @@ import com.accounting.dto.NoteResponse;
import com.accounting.entity.Note;
import com.accounting.mapper.NoteMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -19,6 +20,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -28,6 +30,16 @@ public class NoteService {
@Autowired
private NoteMapper noteMapper;
@Autowired
private NoteFolderService folderService;
/** 查询范围:全部笔记 */
private static final String SCOPE_ALL = "all";
/** 查询范围:未分类(folder_id 为空) */
private static final String SCOPE_UNCATEGORIZED = "uncategorized";
/** 查询范围:指定目录(含子目录) */
private static final String SCOPE_FOLDER = "folder";
/** 摘要最大长度 */
private static final int SUMMARY_MAX_LENGTH = 150;
@@ -55,14 +67,18 @@ public class NoteService {
/**
* 分页查询笔记列表(不含正文)
*
* @param scope all(默认,全部笔记)/ uncategorized(未分类)/ folder(指定目录)
* @param folderId 仅 scope=folder 时有意义,查询含该目录及其所有子目录
* @param keyword 关键词,同时匹配标题和正文,可为空
* @param tag 标签精确匹配,可为空
* @param sort updated(默认)/ created / title
*/
public NotePageResponse list(Long userId, String keyword, String tag, int page, int size) {
public NotePageResponse list(Long userId, String scope, Long folderId, String keyword,
String tag, String sort, int page, int size) {
int safePage = Math.max(page, 1);
int safeSize = Math.min(Math.max(size, 1), MAX_PAGE_SIZE);
long total = noteMapper.selectCount(buildListWrapper(userId, keyword, tag));
long total = noteMapper.selectCount(buildListWrapper(userId, scope, folderId, keyword, tag));
List<Note> rows;
if (total == 0) {
@@ -70,9 +86,7 @@ public class NoteService {
} else {
int offset = (safePage - 1) * safeSize;
rows = noteMapper.selectList(
buildListWrapper(userId, keyword, tag)
.orderByDesc(Note::getIsPinned)
.orderByDesc(Note::getUpdateTime)
applySort(buildListWrapper(userId, scope, folderId, keyword, tag), sort)
// 项目里没有配置 MyBatis-Plus 的 PaginationInnerInterceptor,
// selectPage 不会真正生成 LIMIT,所以这里直接拼。
// offset/size 都是上面校验过的 int,不存在注入问题。
@@ -180,16 +194,39 @@ public class NoteService {
noteMapper.deleteById(note.getId());
}
/**
* 把笔记移动到某个目录
*
* <p>单独一个方法而不是走 {@link #update},是因为后者是部分更新语义
* (字段为 null 表示不改动),没法表达"移回未分类"。
* 这里的 folderId 为 null 是明确的「未分类」。</p>
*/
@Transactional
public void assignFolder(Long userId, Long noteId, Long folderId) {
requireOwnedNote(userId, noteId);
// 目标目录必须存在且属于当前用户;null 表示未分类,允许
folderService.requireOwnedIfPresent(userId, folderId);
// 用 UpdateWrapper:updateById 会忽略 null 字段,
// 导致"移回未分类"静默失败
noteMapper.update(null, new LambdaUpdateWrapper<Note>()
.eq(Note::getId, noteId)
.eq(Note::getUserId, userId)
.set(Note::getFolderId, folderId));
}
/**
* 批量导入
*
* @param folderId 导入到哪个目录,null = 未分类
* @return 实际写入的条数
*/
@Transactional
public int importBatch(Long userId, List<NoteImportRequest.Item> items) {
public int importBatch(Long userId, List<NoteImportRequest.Item> items, Long folderId) {
if (items == null || items.isEmpty()) {
return 0;
}
folderService.requireOwnedIfPresent(userId, folderId);
int count = 0;
for (NoteImportRequest.Item item : items) {
@@ -197,6 +234,7 @@ public class NoteService {
Note note = new Note();
note.setUserId(userId);
note.setFolderId(folderId);
note.setTitle(resolveTitle(item.getTitle(), content, item.getSourcePath()));
note.setContent(content);
note.setSummary(buildSummary(content));
@@ -213,10 +251,26 @@ public class NoteService {
// 内部工具
// ==================================================================
private LambdaQueryWrapper<Note> buildListWrapper(Long userId, String keyword, String tag) {
private LambdaQueryWrapper<Note> buildListWrapper(Long userId, String scope, Long folderId,
String keyword, String tag) {
LambdaQueryWrapper<Note> wrapper = new LambdaQueryWrapper<Note>()
.eq(Note::getUserId, userId);
if (SCOPE_UNCATEGORIZED.equals(scope)) {
wrapper.isNull(Note::getFolderId);
} else if (SCOPE_FOLDER.equals(scope) && folderId != null) {
// 目录查询默认含子目录:点开「工作」应该看到「工作/服务器」里的东西,
// 而不是一片空白。目录上显示的笔记数也是这个口径。
Set<Long> ids = folderService.subtreeIds(userId, folderId);
if (ids.isEmpty()) {
// in() 传空集合会生成非法的 `IN ()`,这里直接让它查不到数据
wrapper.apply("1 = 0");
} else {
wrapper.in(Note::getFolderId, ids);
}
}
// scope=all(默认)或没传 folderId 时不做目录过滤
if (StringUtils.hasText(keyword)) {
String kw = keyword.trim();
wrapper.and(q -> q.like(Note::getTitle, kw).or().like(Note::getContent, kw));
@@ -230,6 +284,25 @@ public class NoteService {
return wrapper;
}
/**
* 排序
*
* <p>置顶永远是第一排序键 —— 那是用户显式做的选择,
* 不该因为切了排序方式就被冲掉。</p>
*/
private LambdaQueryWrapper<Note> applySort(LambdaQueryWrapper<Note> wrapper, String sort) {
wrapper.orderByDesc(Note::getIsPinned);
if ("created".equals(sort)) {
wrapper.orderByDesc(Note::getCreateTime);
} else if ("title".equals(sort)) {
wrapper.orderByAsc(Note::getTitle);
} else {
wrapper.orderByDesc(Note::getUpdateTime);
}
return wrapper;
}
/**
* 取出属于该用户的笔记,找不到或不属于该用户都抛异常
*/
@@ -330,6 +403,7 @@ public class NoteService {
private NoteListItem toListItem(Note note) {
NoteListItem item = new NoteListItem();
item.setId(note.getId());
item.setFolderId(note.getFolderId());
item.setTitle(note.getTitle());
item.setSummary(note.getSummary());
item.setTagList(parseTags(note.getTags()));
@@ -341,6 +415,7 @@ public class NoteService {
private NoteResponse toResponse(Note note) {
NoteResponse response = new NoteResponse();
response.setId(note.getId());
response.setFolderId(note.getFolderId());
response.setTitle(note.getTitle());
response.setContent(note.getContent());
response.setTagList(parseTags(note.getTags()));