feat(image): 实现 Markdown 图片文件名同步

- 新增 ImageName 实体类和对应的 Mapper- 在 MarkdownFileService 中添加图片文件名同步方法
- 优化 HomePage 组件,支持实时预览 Markdown 内容
- 新增 MarkdownImageExtractor 工具类,用于提取 Markdown 中的图片文件名
This commit is contained in:
ikmkj
2025-08-01 22:25:36 +08:00
parent 165bd5ea92
commit 15091c315e
10 changed files with 271 additions and 27 deletions

View File

@@ -2,10 +2,12 @@ package com.test.bijihoudaun;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class BijiHoudaunApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,24 @@
package com.test.bijihoudaun.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("imageNameSyncExecutor")
public Executor imageNameSyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("imageNameSync-");
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,24 @@
package com.test.bijihoudaun.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "图片名称实体")
@TableName("image_name")
public class ImageName {
@Schema(description = "图片名称id", implementation = Long.class)
@TableId(type = IdType.AUTO)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
@Schema(description = "关联的Markdown文件ID", implementation = Long.class)
@TableField("markdown_id")
private Long markdownId;
@Schema(description = "文件名", implementation = String.class)
@TableField("file_name")
private String fileName;
}

View File

@@ -0,0 +1,10 @@
package com.test.bijihoudaun.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.test.bijihoudaun.entity.ImageName;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ImageNameMapper extends BaseMapper<ImageName> {
// 可以在这里添加自定义方法
}

View File

@@ -1,15 +1,20 @@
package com.test.bijihoudaun.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.test.bijihoudaun.entity.ImageName;
import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.entity.MarkdownFileVO;
import com.test.bijihoudaun.mapper.ImageNameMapper;
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
import com.test.bijihoudaun.service.MarkdownFileService;
import com.test.bijihoudaun.util.MarkdownImageExtractor;
import com.test.bijihoudaun.util.SnowflakeIdGenerator;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
@@ -23,20 +28,30 @@ public class MarkdownFileServiceImpl
@Autowired
MarkdownFileMapper markdownFileMapper;
@Resource
ImageNameMapper imageNameMapper;
@Resource
SnowflakeIdGenerator snowflakeIdGenerator;
@Override
public MarkdownFile updateMarkdownContent(MarkdownFile markdownFile) {
long id;
markdownFile.setUpdatedAt(new Date());
// 如果ID为空或0则视为新文件
if (markdownFile.getId() == null || markdownFile.getId() == 0L) {
markdownFile.setId(snowflakeIdGenerator.nextId());
long l = snowflakeIdGenerator.nextId();
markdownFile.setId(l);
markdownFile.setCreatedAt(new Date());
this.save(markdownFile); // 使用MyBatis-Plus的save方法
id=l;
} else {
this.updateById(markdownFile); // 使用MyBatis-Plus的updateById方法
id=markdownFile.getId();
}
List<String> strings = MarkdownImageExtractor.extractImageFilenames(markdownFile.getContent());
// 异步处理图片文件名同步
syncImageNames(id, strings);
return markdownFile;
}
@@ -62,8 +77,7 @@ public class MarkdownFileServiceImpl
@Override
public List<MarkdownFile> test() {
List<MarkdownFile> markdownFiles = markdownFileMapper.selectList(null);
return markdownFiles;
return markdownFileMapper.selectList(null);
}
@Override
@@ -93,4 +107,63 @@ public class MarkdownFileServiceImpl
public List<MarkdownFileVO> getRecentFiles(int limit) {
return markdownFileMapper.selectRecentWithGrouping(limit);
}
@Async("imageNameSyncExecutor")
public void syncImageNames(Long markdownId, List<String> strings) {
// 查询数据库中已存在的文件名
List<ImageName> imageNames = imageNameMapper.selectList(new LambdaUpdateWrapper<ImageName>()
.eq(ImageName::getMarkdownId, markdownId));
// 若是数据库中的数据为null则插入
if (CollUtil.isEmpty(imageNames)) {
if (CollUtil.isNotEmpty(strings)) {
List<ImageName> list = strings.stream().map(fileName -> {
ImageName imageName = new ImageName();
imageName.setFileName(fileName);
imageName.setMarkdownId(markdownId);
return imageName;
}).toList();
// 批量插入新的文件名
list.forEach(imageName -> imageNameMapper.insert(imageName));
}
} else {
// 数据库中已有记录,需要对比处理
// 获取数据库中的文件名列表
List<String> dbFileNames = imageNames.stream()
.map(ImageName::getFileName)
.toList();
// 找出需要新增的文件名在strings中但不在数据库中
List<String> toInsert = strings.stream()
.filter(fileName -> !dbFileNames.contains(fileName))
.toList();
// 找出需要删除的记录在数据库中但不在strings中
List<ImageName> toDelete = imageNames.stream()
.filter(imageName -> !strings.contains(imageName.getFileName()))
.toList();
// 插入新增的文件名
if (CollUtil.isNotEmpty(toInsert)) {
List<ImageName> insertList = toInsert.stream().map(fileName -> {
ImageName imageName = new ImageName();
imageName.setFileName(fileName);
imageName.setMarkdownId(markdownId);
return imageName;
}).toList();
imageNameMapper.insert(insertList);
}
// 删除不再需要的记录
if (CollUtil.isNotEmpty(toDelete)) {
List<Long> deleteIds = toDelete.stream()
.map(ImageName::getId)
.toList();
imageNameMapper.deleteByIds(deleteIds);
}
}
}
}

View File

@@ -0,0 +1,79 @@
package com.test.bijihoudaun.util;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.List;
import java.util.ArrayList;
/**
* Markdown图片提取工具类
* 用于从Markdown文本中提取图片文件名
*/
public class MarkdownImageExtractor {
/**
* 从Markdown内容中提取图片文件名
* 支持各种URL格式:
* - 绝对URL: http://example.com/path/uuid.png
* - 相对URL: /path/uuid.png
* - 协议相对URL: //example.com/path/uuid.png
*
* @param markdownContent 包含图片的Markdown文本
* @return 图片文件名列表
*/
public static List<String> extractImageFilenames(String markdownContent) {
if (markdownContent == null || markdownContent.isEmpty()) {
return new ArrayList<>();
}
// 使用正则表达式匹配Markdown图片语法中的文件名
// 模式: ![alt](url) 其中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);
List<String> filenames = new ArrayList<>();
while (matcher.find()) {
filenames.add(matcher.group(1));
}
return filenames;
}
/**
* 检查Markdown内容中是否包含指定的图片文件名
*
* @param markdownContent Markdown文本
* @param filename 要查找的文件名
* @return 是否包含该文件名
*/
public static boolean containsImage(String markdownContent, String filename) {
if (markdownContent == null || filename == null) {
return false;
}
return extractImageFilenames(markdownContent).contains(filename);
}
/**
* 从Markdown内容中提取图片URL
*
* @param markdownContent 包含图片的Markdown文本
* @return 图片URL列表
*/
public static List<String> extractImageUrls(String markdownContent) {
if (markdownContent == null || markdownContent.isEmpty()) {
return new ArrayList<>();
}
// 匹配Markdown图片语法中的完整URL
Pattern pattern = Pattern.compile("!\\[.*?\\]\\(([^)]+)\\)");
Matcher matcher = pattern.matcher(markdownContent);
List<String> urls = new ArrayList<>();
while (matcher.find()) {
urls.add(matcher.group(1));
}
return urls;
}
}

View File

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