perf: 优化系统性能和资源管理
- 优化JWT解析器性能,复用JwtParser减少重复创建 - 使用Slf4j日志框架替代System.out.println - 添加图片压缩功能,减少存储和带宽消耗 - 预编译正则表达式提升Markdown图片提取性能 - 重构限流拦截器,使用ConcurrentHashMap提高并发性能 - 添加数据库索引优化查询性能 - 为MarkdownFile实体添加autoResultMap配置
This commit is contained in:
@@ -14,7 +14,7 @@ import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(name = "文本实体")
|
||||
@TableName("`markdown_file`")
|
||||
@TableName(value = "`markdown_file`", autoResultMap = true)
|
||||
public class MarkdownFile implements Serializable {
|
||||
@Schema(description = "文本id",implementation = Long.class)
|
||||
@TableId(type = IdType.AUTO)
|
||||
@@ -62,4 +62,4 @@ public class MarkdownFile implements Serializable {
|
||||
@Schema(description = "是否私密 0-公开 1-私密", implementation = Integer.class)
|
||||
@TableField("is_private")
|
||||
private Integer isPrivate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.test.bijihoudaun.common.response.R;
|
||||
import com.test.bijihoudaun.common.response.ResultCode;
|
||||
import com.test.bijihoudaun.util.MemoryProtector;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -11,12 +13,16 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 限流拦截器 - 支持按 IP 和按用户双重限流,带容量限制
|
||||
* 限流拦截器 - 支持按 IP 和按用户双重限流,使用 ConcurrentHashMap 提高并发性能
|
||||
* 带定期清理机制防止内存泄漏
|
||||
*/
|
||||
@Slf4j
|
||||
public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
@@ -29,59 +35,116 @@ public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
private static final int MAX_LOGIN_REQUESTS_PER_MINUTE_USER = 10;
|
||||
// 时间窗口(毫秒)
|
||||
private static final long WINDOW_SIZE_MS = 60_000;
|
||||
// 最大存储记录数(防止内存溢出)
|
||||
private static final int MAX_RECORDS = 50000;
|
||||
// 清理间隔(毫秒):每5分钟清理一次
|
||||
private static final long CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
// IP 级别限流
|
||||
private static final LRUCache<String, RequestCounter> ipCounters = new LRUCache<>(MAX_RECORDS / 2);
|
||||
private static final LRUCache<String, RequestCounter> ipLoginCounters = new LRUCache<>(MAX_RECORDS / 4);
|
||||
// 用户级别限流
|
||||
private static final LRUCache<String, RequestCounter> userCounters = new LRUCache<>(MAX_RECORDS / 4);
|
||||
private static final LRUCache<String, RequestCounter> userLoginCounters = new LRUCache<>(MAX_RECORDS / 4);
|
||||
// 使用 ConcurrentHashMap + 原子操作,避免锁竞争
|
||||
private static final ConcurrentHashMap<String, RequestCounter> ipCounters = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, RequestCounter> ipLoginCounters = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, RequestCounter> userCounters = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, RequestCounter> userLoginCounters = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
// 定期清理线程池
|
||||
private ScheduledExecutorService cleanupScheduler;
|
||||
|
||||
private static class RequestCounter {
|
||||
int count;
|
||||
long windowStart;
|
||||
private final AtomicInteger count = new AtomicInteger(0);
|
||||
private volatile long windowStart;
|
||||
// 记录最后访问时间,用于清理
|
||||
private volatile long lastAccessTime;
|
||||
|
||||
RequestCounter() {
|
||||
this.count = 1;
|
||||
this.windowStart = System.currentTimeMillis();
|
||||
this.lastAccessTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean incrementAndCheck(int maxRequests) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - windowStart > WINDOW_SIZE_MS) {
|
||||
// 新窗口
|
||||
count = 1;
|
||||
windowStart = now;
|
||||
return true;
|
||||
lastAccessTime = now;
|
||||
long currentWindow = windowStart;
|
||||
|
||||
if (now - currentWindow > WINDOW_SIZE_MS) {
|
||||
// 尝试进入新窗口
|
||||
synchronized (this) {
|
||||
if (windowStart == currentWindow) {
|
||||
// 确实需要新窗口
|
||||
windowStart = now;
|
||||
count.set(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 其他线程已经更新了窗口,继续检查
|
||||
}
|
||||
count++;
|
||||
return count <= maxRequests;
|
||||
|
||||
int currentCount = count.incrementAndGet();
|
||||
return currentCount <= maxRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期(超过2个时间窗口没有访问)
|
||||
*/
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() - lastAccessTime > WINDOW_SIZE_MS * 2;
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 启动定期清理任务
|
||||
cleanupScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "rate-limit-cleanup");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
cleanupScheduler.scheduleAtFixedRate(this::cleanupExpiredCounters,
|
||||
CLEANUP_INTERVAL_MS, CLEANUP_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
log.info("限流拦截器初始化完成,清理间隔:{}分钟", CLEANUP_INTERVAL_MS / 60000);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
// 关闭清理线程池
|
||||
if (cleanupScheduler != null && !cleanupScheduler.isShutdown()) {
|
||||
cleanupScheduler.shutdown();
|
||||
try {
|
||||
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
cleanupScheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
cleanupScheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
log.info("限流拦截器已销毁");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的计数器
|
||||
*/
|
||||
private void cleanupExpiredCounters() {
|
||||
try {
|
||||
int removed = 0;
|
||||
removed += cleanupMap(ipCounters);
|
||||
removed += cleanupMap(ipLoginCounters);
|
||||
removed += cleanupMap(userCounters);
|
||||
removed += cleanupMap(userLoginCounters);
|
||||
if (removed > 0) {
|
||||
log.debug("限流计数器清理完成,移除 {} 个过期条目", removed);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("限流计数器清理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的 LRU 缓存实现
|
||||
* 清理单个 Map 中的过期条目
|
||||
* 使用 removeIf 方法避免并发修改异常
|
||||
*/
|
||||
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||||
private final int maxSize;
|
||||
|
||||
LRUCache(int maxSize) {
|
||||
super(maxSize, 0.75f, true);
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||
boolean shouldRemove = size() > maxSize;
|
||||
if (shouldRemove) {
|
||||
log.debug("限流记录达到上限,移除最旧的记录");
|
||||
}
|
||||
return shouldRemove;
|
||||
}
|
||||
private int cleanupMap(ConcurrentHashMap<String, RequestCounter> map) {
|
||||
int sizeBefore = map.size();
|
||||
// 使用 ConcurrentHashMap 的 removeIf 方法,线程安全
|
||||
map.entrySet().removeIf(entry -> entry.getValue().isExpired());
|
||||
return sizeBefore - map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,19 +180,14 @@ public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
*/
|
||||
private boolean checkIpLimit(String clientIp, boolean isLoginRequest,
|
||||
HttpServletResponse response) throws Exception {
|
||||
LRUCache<String, RequestCounter> counters = isLoginRequest ? ipLoginCounters : ipCounters;
|
||||
ConcurrentHashMap<String, RequestCounter> counters = isLoginRequest ? ipLoginCounters : ipCounters;
|
||||
int maxRequests = isLoginRequest ? MAX_LOGIN_REQUESTS_PER_MINUTE : MAX_REQUESTS_PER_MINUTE;
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
RequestCounter counter = counters.computeIfAbsent(clientIp, k -> new RequestCounter());
|
||||
RequestCounter counter = counters.computeIfAbsent(clientIp, k -> new RequestCounter());
|
||||
|
||||
if (!counter.incrementAndCheck(maxRequests)) {
|
||||
writeRateLimitResponse(response, "请求过于频繁,请稍后再试");
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
if (!counter.incrementAndCheck(maxRequests)) {
|
||||
writeRateLimitResponse(response, "请求过于频繁,请稍后再试");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -139,19 +197,14 @@ public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
*/
|
||||
private boolean checkUserLimit(String username, boolean isLoginRequest,
|
||||
HttpServletResponse response) throws Exception {
|
||||
LRUCache<String, RequestCounter> counters = isLoginRequest ? userLoginCounters : userCounters;
|
||||
ConcurrentHashMap<String, RequestCounter> counters = isLoginRequest ? userLoginCounters : userCounters;
|
||||
int maxRequests = isLoginRequest ? MAX_LOGIN_REQUESTS_PER_MINUTE_USER : MAX_REQUESTS_PER_MINUTE_USER;
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
RequestCounter counter = counters.computeIfAbsent(username, k -> new RequestCounter());
|
||||
RequestCounter counter = counters.computeIfAbsent(username, k -> new RequestCounter());
|
||||
|
||||
if (!counter.incrementAndCheck(maxRequests)) {
|
||||
writeRateLimitResponse(response, "您的操作过于频繁,请稍后再试");
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
if (!counter.incrementAndCheck(maxRequests)) {
|
||||
writeRateLimitResponse(response, "您的操作过于频繁,请稍后再试");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.test.bijihoudaun.scheduler;
|
||||
|
||||
import com.test.bijihoudaun.service.ImageCleanupService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -10,6 +11,7 @@ import org.springframework.stereotype.Component;
|
||||
* 定期自动清理冗余图片
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ImageCleanupScheduler {
|
||||
|
||||
@Autowired
|
||||
@@ -22,6 +24,7 @@ public class ImageCleanupScheduler {
|
||||
@Scheduled(cron = "0 0 3 * * ?")
|
||||
public void scheduledCleanup() {
|
||||
int deletedCount = imageCleanupService.cleanupRedundantImages();
|
||||
System.out.println("定时清理任务完成,清理了 " + deletedCount + " 个冗余图片");
|
||||
// 优化:使用日志框架代替 System.out.println
|
||||
log.info("定时清理任务完成,清理了 {} 个冗余图片", deletedCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
import com.test.bijihoudaun.util.ImageCompressor;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -16,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -79,7 +81,18 @@ public class ImageServiceImpl
|
||||
String storedName = UUID.randomUUID() + extension;
|
||||
|
||||
Path filePath = uploadPath.resolve(storedName);
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
|
||||
// 优化:压缩图片后再保存
|
||||
InputStream compressedStream = ImageCompressor.compressIfNeeded(
|
||||
file.getInputStream(), contentType, file.getSize());
|
||||
|
||||
if (compressedStream != null) {
|
||||
// 使用压缩后的图片
|
||||
Files.copy(compressedStream, filePath);
|
||||
} else {
|
||||
// 使用原图
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
}
|
||||
|
||||
Image image = new Image();
|
||||
image.setOriginalName(originalFilename);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.test.bijihoudaun.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 图片压缩工具类
|
||||
* 用于压缩上传的图片,减少存储和带宽
|
||||
*/
|
||||
@Slf4j
|
||||
public class ImageCompressor {
|
||||
|
||||
// 最大宽度(像素)
|
||||
private static final int MAX_WIDTH = 1920;
|
||||
// 最大高度(像素)
|
||||
private static final int MAX_HEIGHT = 1080;
|
||||
// 压缩质量(0.0 - 1.0)
|
||||
private static final float COMPRESS_QUALITY = 0.85f;
|
||||
// 需要压缩的最小文件大小(字节)
|
||||
private static final long COMPRESS_THRESHOLD = 500 * 1024; // 500KB
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
* @param inputStream 原始图片输入流
|
||||
* @param contentType 图片类型
|
||||
* @param originalSize 原始文件大小
|
||||
* @return 压缩后的输入流,如果不需要压缩则返回null
|
||||
*/
|
||||
public static InputStream compressIfNeeded(InputStream inputStream, String contentType, long originalSize) {
|
||||
// 小于阈值的图片不压缩
|
||||
if (originalSize < COMPRESS_THRESHOLD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 只压缩 JPG 和 PNG
|
||||
if (!contentType.equals("image/jpeg") && !contentType.equals("image/png")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedImage originalImage = ImageIO.read(inputStream);
|
||||
if (originalImage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算新的尺寸
|
||||
int originalWidth = originalImage.getWidth();
|
||||
int originalHeight = originalImage.getHeight();
|
||||
|
||||
// 如果图片尺寸小于限制,只进行质量压缩
|
||||
int newWidth = originalWidth;
|
||||
int newHeight = originalHeight;
|
||||
|
||||
// 如果图片尺寸超过限制,等比例缩放
|
||||
if (originalWidth > MAX_WIDTH || originalHeight > MAX_HEIGHT) {
|
||||
double scale = Math.min(
|
||||
(double) MAX_WIDTH / originalWidth,
|
||||
(double) MAX_HEIGHT / originalHeight
|
||||
);
|
||||
newWidth = (int) (originalWidth * scale);
|
||||
newHeight = (int) (originalHeight * scale);
|
||||
}
|
||||
|
||||
// 创建压缩后的图片
|
||||
BufferedImage compressedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = compressedImage.createGraphics();
|
||||
|
||||
// 设置高质量渲染
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制图片
|
||||
g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
|
||||
g2d.dispose();
|
||||
|
||||
// 输出为字节流
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
String formatName = contentType.equals("image/png") ? "png" : "jpg";
|
||||
ImageIO.write(compressedImage, formatName, baos);
|
||||
|
||||
byte[] compressedBytes = baos.toByteArray();
|
||||
|
||||
// 如果压缩后更大,返回null使用原图
|
||||
if (compressedBytes.length >= originalSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("图片压缩成功:{} -> {} ({}%)",
|
||||
formatSize(originalSize),
|
||||
formatSize(compressedBytes.length),
|
||||
(compressedBytes.length * 100 / originalSize));
|
||||
|
||||
return new ByteArrayInputStream(compressedBytes);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("图片压缩失败,使用原图", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
private static String formatSize(long size) {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return String.format("%.2fKB", size / 1024.0);
|
||||
} else {
|
||||
return String.format("%.2fMB", size / (1024.0 * 1024));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,14 @@ public class JwtTokenUtil {
|
||||
private Long expiration;
|
||||
|
||||
private Key key;
|
||||
// 优化:复用 JwtParser,避免每次请求都创建
|
||||
private io.jsonwebtoken.JwtParser jwtParser;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
// 优化:只创建一次 parser
|
||||
this.jwtParser = Jwts.parserBuilder().setSigningKey(key).build();
|
||||
}
|
||||
|
||||
// 从token中获取用户名
|
||||
@@ -47,9 +51,9 @@ public class JwtTokenUtil {
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
// 为了从token中获取任何信息,我们都需要密钥
|
||||
// 优化:使用复用的 parser
|
||||
private Claims getAllClaimsFromToken(String token) {
|
||||
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
|
||||
return jwtParser.parseClaimsJws(token).getBody();
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
@@ -76,4 +80,4 @@ public class JwtTokenUtil {
|
||||
final String username = getUsernameFromToken(token);
|
||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@ import java.util.ArrayList;
|
||||
*/
|
||||
public class MarkdownImageExtractor {
|
||||
|
||||
// 优化:预编译正则表达式,避免每次调用都编译
|
||||
private static final Pattern IMAGE_FILENAME_PATTERN = Pattern.compile(
|
||||
"!\\[.*?\\]\\([^)]*?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\\.[a-zA-Z0-9]+)\\)"
|
||||
);
|
||||
|
||||
// 优化:预编译URL匹配正则
|
||||
private static final Pattern IMAGE_URL_PATTERN = Pattern.compile(
|
||||
"!\\[.*?\\]\\(([^)]+)\\)"
|
||||
);
|
||||
|
||||
/**
|
||||
* 从Markdown内容中提取图片文件名
|
||||
* 支持各种URL格式:
|
||||
@@ -26,10 +36,8 @@ public class MarkdownImageExtractor {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 使用正则表达式匹配Markdown图片语法中的文件名
|
||||
// 模式:  其中url以UUID格式的文件名结尾
|
||||
Pattern pattern = Pattern.compile("!\\[.*?\\]\\([^)]*?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\\.[a-zA-Z0-9]+)\\)");
|
||||
Matcher matcher = pattern.matcher(markdownContent);
|
||||
// 使用预编译的正则表达式
|
||||
Matcher matcher = IMAGE_FILENAME_PATTERN.matcher(markdownContent);
|
||||
|
||||
List<String> filenames = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
@@ -65,9 +73,8 @@ public class MarkdownImageExtractor {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 匹配Markdown图片语法中的完整URL
|
||||
Pattern pattern = Pattern.compile("!\\[.*?\\]\\(([^)]+)\\)");
|
||||
Matcher matcher = pattern.matcher(markdownContent);
|
||||
// 使用预编译的正则表达式
|
||||
Matcher matcher = IMAGE_URL_PATTERN.matcher(markdownContent);
|
||||
|
||||
List<String> urls = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
-- 数据库性能优化索引
|
||||
-- 创建时间: 2024-03-03
|
||||
|
||||
-- markdown_file 表索引
|
||||
-- 按分组查询
|
||||
CREATE INDEX IF NOT EXISTS idx_markdown_grouping_id ON markdown_file(grouping_id);
|
||||
-- 按删除状态查询(软删除)
|
||||
CREATE INDEX IF NOT EXISTS idx_markdown_is_deleted ON markdown_file(is_deleted);
|
||||
-- 按创建时间排序
|
||||
CREATE INDEX IF NOT EXISTS idx_markdown_created_at ON markdown_file(created_at);
|
||||
-- 复合索引:查询未删除的分组笔记
|
||||
CREATE INDEX IF NOT EXISTS idx_markdown_grouping_deleted ON markdown_file(grouping_id, is_deleted);
|
||||
|
||||
-- image 表索引
|
||||
-- 按 markdown_id 查询(关联查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_image_markdown_id ON image(markdown_id);
|
||||
-- 按存储文件名查询
|
||||
CREATE INDEX IF NOT EXISTS idx_image_stored_name ON image(stored_name);
|
||||
-- 按创建时间查询(清理旧图片)
|
||||
CREATE INDEX IF NOT EXISTS idx_image_created_at ON image(created_at);
|
||||
|
||||
-- grouping 表索引
|
||||
-- 按父分组查询
|
||||
CREATE INDEX IF NOT EXISTS idx_grouping_parent_id ON grouping(parent_id);
|
||||
-- 按删除状态查询
|
||||
CREATE INDEX IF NOT EXISTS idx_grouping_is_deleted ON grouping(is_deleted);
|
||||
|
||||
-- trash 表索引
|
||||
-- 按用户查询
|
||||
CREATE INDEX IF NOT EXISTS idx_trash_user_id ON trash(user_id);
|
||||
-- 按删除时间排序(清理过期数据)
|
||||
CREATE INDEX IF NOT EXISTS idx_trash_deleted_at ON trash(deleted_at);
|
||||
-- 按类型查询
|
||||
CREATE INDEX IF NOT EXISTS idx_trash_item_type ON trash(item_type);
|
||||
|
||||
-- user 表索引
|
||||
-- 按用户名查询(登录)
|
||||
CREATE INDEX IF NOT EXISTS idx_user_username ON user(username);
|
||||
Reference in New Issue
Block a user