diff --git a/src/main/java/com/accounting/config/SecurityConfig.java b/src/main/java/com/accounting/config/SecurityConfig.java
index 9c4610f..fdf9935 100644
--- a/src/main/java/com/accounting/config/SecurityConfig.java
+++ b/src/main/java/com/accounting/config/SecurityConfig.java
@@ -37,6 +37,11 @@ public class SecurityConfig {
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/gold-price/**").permitAll()
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**").permitAll()
+ // Web 后台管理页本身是静态页面,加载时不可能带 JWT;
+ // 页面里的数据请求仍然走 /api/notes/** 走正常鉴权
+ .requestMatchers("/admin/**").permitAll()
+ // 笔记附件(图片)。文件名是 UUID,不可枚举
+ .requestMatchers("/uploads/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
diff --git a/src/main/java/com/accounting/config/WebMvcConfig.java b/src/main/java/com/accounting/config/WebMvcConfig.java
new file mode 100644
index 0000000..0577210
--- /dev/null
+++ b/src/main/java/com/accounting/config/WebMvcConfig.java
@@ -0,0 +1,35 @@
+package com.accounting.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.nio.file.Paths;
+
+/**
+ * 把上传目录暴露成静态资源
+ *
+ *
原有的 FileUtil 只负责把文件写进 {@code ${file.upload.path}},
+ * 但项目里一直没有任何地方把它映射出去,所以历史上传的图片
+ * 没有任何 URL 能访问到(OCR 场景只把图片喂给阿里云,
+ * 不需要回显,所以一直没暴露这个问题)。
+ *
+ * 笔记要在 Markdown 里引用图片,必须补上这层映射。
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ @Value("${file.upload.path}")
+ private String uploadPath;
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ // 用 toUri() 而不是直接拼 "file:" ,否则在 Windows 开发环境下
+ // 路径分隔符会出问题
+ String location = Paths.get(uploadPath).toUri().toString();
+
+ registry.addResourceHandler("/uploads/**")
+ .addResourceLocations(location);
+ }
+}
diff --git a/src/main/java/com/accounting/controller/NoteController.java b/src/main/java/com/accounting/controller/NoteController.java
new file mode 100644
index 0000000..2d3a84c
--- /dev/null
+++ b/src/main/java/com/accounting/controller/NoteController.java
@@ -0,0 +1,132 @@
+package com.accounting.controller;
+
+import com.accounting.dto.NoteAttachmentResponse;
+import com.accounting.dto.NoteImportRequest;
+import com.accounting.dto.NotePageResponse;
+import com.accounting.dto.NoteRequest;
+import com.accounting.dto.NoteResponse;
+import com.accounting.entity.User;
+import com.accounting.mapper.UserMapper;
+import com.accounting.service.NoteService;
+import com.accounting.util.FileUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+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 org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "笔记管理", description = "笔记的增删改查、批量导入、附件上传")
+@RestController
+@RequestMapping("/api/notes")
+public class NoteController {
+
+ @Autowired
+ private NoteService noteService;
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Autowired
+ private FileUtil fileUtil;
+
+ @Operation(summary = "分页查询笔记列表(不含正文)")
+ @GetMapping
+ public ResponseEntity list(
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "20") int size,
+ @RequestParam(required = false) String keyword,
+ @RequestParam(required = false) String tag,
+ Authentication authentication) {
+
+ Long userId = getUserId(authentication);
+ return ResponseEntity.ok(noteService.list(userId, keyword, tag, page, size));
+ }
+
+ @Operation(summary = "查询笔记详情(含正文)")
+ @GetMapping("/{id}")
+ public ResponseEntity detail(@PathVariable Long id, Authentication authentication) {
+ Long userId = getUserId(authentication);
+ return ResponseEntity.ok(noteService.detail(userId, id));
+ }
+
+ @Operation(summary = "新建笔记")
+ @PostMapping
+ public ResponseEntity create(@RequestBody NoteRequest request, Authentication authentication) {
+ Long userId = getUserId(authentication);
+ return ResponseEntity.ok(noteService.create(userId, request));
+ }
+
+ @Operation(summary = "更新笔记")
+ @PutMapping("/{id}")
+ public ResponseEntity update(
+ @PathVariable Long id,
+ @RequestBody NoteRequest request,
+ Authentication authentication) {
+
+ Long userId = getUserId(authentication);
+ return ResponseEntity.ok(noteService.update(userId, id, request));
+ }
+
+ @Operation(summary = "删除笔记(逻辑删除)")
+ @DeleteMapping("/{id}")
+ public ResponseEntity