refactor: 优化图片处理与数据库连接配置

This commit is contained in:
ikmkj
2026-03-03 17:16:44 +08:00
parent a805ff905e
commit 6d5233cb4b
16 changed files with 176 additions and 75 deletions

View File

@@ -6,6 +6,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
@@ -14,10 +15,20 @@ public class AsyncConfig {
@Bean("imageNameSyncExecutor")
public Executor imageNameSyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
// 核心线程数
executor.setCorePoolSize(2);
// 最大线程数
executor.setMaxPoolSize(5);
// 队列容量
executor.setQueueCapacity(100);
// 线程名前缀
executor.setThreadNamePrefix("imageNameSync-");
// 拒绝策略:由调用线程处理
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务完成后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间(秒)
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}

View File

@@ -18,8 +18,7 @@ import java.util.Date;
public class Grouping implements Serializable {
@Schema(description = "分组id",implementation = Long.class)
@TableId(type = IdType.ASSIGN_ID)
@JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段
@TableField("id")
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
@Schema(description ="上级id",implementation = Long.class)

View File

@@ -16,8 +16,7 @@ import java.util.Date;
public class Image {
@Schema(description = "图片id",implementation = Long.class)
@TableId(type = IdType.AUTO)
@JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段
@TableField("id")
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
@Schema(description = " 外键关联Markdown文件ID标识图片所属文档",implementation = Long.class )

View File

@@ -12,7 +12,6 @@ public class ImageName {
@Schema(description = "图片名称id", implementation = Long.class)
@TableId(type = IdType.AUTO)
@JsonFormat(shape = JsonFormat.Shape.STRING)
@TableField("id")
private Long id;
@Schema(description = "关联的Markdown文件ID", implementation = Long.class)

View File

@@ -18,8 +18,7 @@ import java.util.Date;
public class MarkdownFile implements Serializable {
@Schema(description = "文本id",implementation = Long.class)
@TableId(type = IdType.AUTO)
@JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段
@TableField("id")
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
@Schema(description = "分组表id",implementation = Long.class)

View File

@@ -1,6 +1,7 @@
package com.test.bijihoudaun.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -11,5 +12,6 @@ public class MarkdownFileVO extends MarkdownFile {
private String groupingName;
@TableField(exist = false)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long groupingId;
}

View File

@@ -18,7 +18,6 @@ public class RegistrationCode implements Serializable {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID", name = "id")
@TableField("id")
private Long id;
@Schema(description = "注册码", name = "code")

View File

@@ -17,7 +17,6 @@ public class SystemSetting implements Serializable {
@TableId
@Schema(description = "设置键", name = "settingKey")
@TableField("`setting_key`")
private String settingKey;
@Schema(description = "设置值", name = "settingValue")

View File

@@ -16,8 +16,7 @@ import java.util.Date;
public class User {
@Schema(description = "用户id",implementation = Long.class)
@TableId(type = IdType.AUTO)
@JsonFormat(shape = JsonFormat.Shape.STRING) // 仅作用于此字段
@TableField("id")
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
@Schema(description = "用户名",implementation = String.class)

View File

@@ -15,7 +15,10 @@ import org.apache.ibatis.annotations.Update;
@Mapper
public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
/**
* 查询最近更新的笔记列表不包含content大字段提高性能
*/
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, null as `content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
"FROM `markdown_file` mf " +
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
"WHERE mf.is_deleted = 0 " +
@@ -23,14 +26,20 @@ public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
"LIMIT #{limit}")
List<MarkdownFileVO> selectRecentWithGrouping(@Param("limit") int limit);
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
/**
* 根据分类ID查询笔记列表不包含content大字段提高性能
*/
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, null as `content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
"FROM `markdown_file` mf " +
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
"WHERE mf.grouping_id = #{groupingId} AND mf.is_deleted = 0 " +
"ORDER BY mf.updated_at DESC")
List<MarkdownFileVO> selectByGroupingIdWithGrouping(@Param("groupingId") String groupingId);
@Select("SELECT id, grouping_id, `title`, file_name, `content`, created_at, updated_at, is_deleted, deleted_at, deleted_by, is_private FROM `markdown_file` WHERE is_deleted = 1")
/**
* 查询已删除的笔记不包含content大字段
*/
@Select("SELECT id, grouping_id, `title`, file_name, null as `content`, created_at, updated_at, is_deleted, deleted_at, deleted_by, is_private FROM `markdown_file` WHERE is_deleted = 1")
List<MarkdownFile> selectDeleted();
@Delete("DELETE FROM `markdown_file` WHERE id = #{id}")
@@ -48,8 +57,15 @@ public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
* 获取所有笔记ID
* @return 所有笔记ID列表
*/
@Select("SELECT id, grouping_id, `title`, file_name, `content`, created_at, updated_at, is_deleted, deleted_at, deleted_by, is_private FROM `markdown_file` WHERE is_deleted = 0")
List<Integer> findAllIds();
@Select("SELECT id FROM `markdown_file` WHERE is_deleted = 0")
List<Long> findAllIds();
/**
* 获取所有笔记的ID和content用于图片清理任务
* @return 所有笔记的ID和content
*/
@Select("SELECT id, `content` FROM `markdown_file` WHERE is_deleted = 0")
List<MarkdownFile> selectAllIdsAndContent();
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
"FROM `markdown_file` mf " +

View File

@@ -39,17 +39,15 @@ public class ImageCleanupService {
*/
@Transactional
public int cleanupRedundantImages() {
// 获取所有笔记ID
List<Integer> allMarkdownIds = markdownFileMapper.findAllIds();
// 批量获取所有笔记ID和content避免N+1查询
List<MarkdownFile> allMarkdownFiles = markdownFileMapper.selectAllIdsAndContent();
// 用于存储所有被引用的图片文件名
Set<String> referencedImageNames = new HashSet<>();
// 遍历所有笔记,收集被引用的图片
for (Integer markdownId : allMarkdownIds) {
MarkdownFile markdownFile = markdownFileMapper.selectById(markdownId);
for (MarkdownFile markdownFile : allMarkdownFiles) {
String content = (markdownFile != null) ? markdownFile.getContent() : "";
List<String> imageNames = MarkdownImageExtractor.extractImageFilenames(content);
referencedImageNames.addAll(imageNames);
}

View File

@@ -132,12 +132,25 @@ public class ImageServiceImpl
if (CollUtil.isEmpty(urls)) {
return false;
}
for (String url : urls) {
Image image = imageMapper.selectOne(new QueryWrapper<Image>().eq("url", url));
if (ObjectUtil.isNotNull(image)) {
this.deleteImageByUrl(url);
// 批量查询避免N+1问题
List<Image> images = imageMapper.selectList(new QueryWrapper<Image>().in("url", urls));
if (CollUtil.isEmpty(images)) {
return false;
}
// 批量删除文件和数据库记录
for (Image image : images) {
try {
Path filePath = Paths.get(uploadDir, image.getStoredName());
Files.deleteIfExists(filePath);
} catch (IOException e) {
throw new RuntimeException("删除图片文件失败: " + image.getStoredName(), e);
}
}
// 批量删除数据库记录
List<Long> ids = images.stream().map(Image::getId).toList();
this.removeByIds(ids);
return true;
}
}

View File

@@ -165,8 +165,8 @@ public class MarkdownFileServiceImpl
imageName.setMarkdownId(markdownId);
return imageName;
}).toList();
// 批量插入新的文件名
list.forEach(imageName -> imageNameMapper.insert(imageName));
// 批量插入新的文件名使用MyBatis Plus批量插入
imageNameMapper.insert(list);
}
} else {
// 数据库中已有记录,需要对比处理

View File

@@ -1,21 +1,27 @@
spring:
datasource:
# driver-class-name: org.sqlite.JDBC
# url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db
# jpa:
# hibernate:
# ddl-auto: none
# show-sql: true
# properties:
# hibernate:
# format_sql: true
# dialect: org.hibernate.dialect.SQLiteDialect
#
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://hdy-hk-8-8.311169.xyz:3306/biji_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username: biji_user
password: Ll12331100
# HikariCP连接池优化配置
hikari:
# 连接池名称
pool-name: BijiHikariPool
# 最小空闲连接数
minimum-idle: 5
# 最大连接数
maximum-pool-size: 20
# 连接空闲超时时间(毫秒)
idle-timeout: 300000
# 连接最大存活时间(毫秒)
max-lifetime: 1200000
# 连接超时时间(毫秒)
connection-timeout: 20000
# 测试连接是否可用的SQL
connection-test-query: SELECT 1
# 自动提交
auto-commit: true
jpa:
hibernate:
ddl-auto: update
@@ -32,8 +38,12 @@ mybatis-plus:
map-underscore-to-camel-case: true
# 启用安全模式防止SQL注入
safe-mode: true
# 配置日志输出
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 全局表前缀(如果有的话)
# table-prefix: t_

View File

@@ -1,38 +1,43 @@
spring:
datasource:
# driver-class-name: org.sqlite.JDBC
# url: jdbc:sqlite:/data/mydatabase.db
# jpa:
# hibernate:
# ddl-auto: none
# show-sql: false
# properties:
# hibernate:
# format_sql: false
# dialect: org.hibernate.dialect.SQLiteDialect
# 上面是 默认配置数据库为sqlite下面是 配置mysql。从环境 变量中获取
driver-class-name: ${DB_DRIVER:com.mysql.cj.jdbc.Driver}
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
# HikariCP连接池优化配置生产环境
hikari:
# 连接池名称
pool-name: BijiHikariPool
# 最小空闲连接数
minimum-idle: 10
# 最大连接数
maximum-pool-size: 50
# 连接空闲超时时间(毫秒)
idle-timeout: 600000
# 连接最大存活时间(毫秒)
max-lifetime: 1800000
# 连接超时时间(毫秒)
connection-timeout: 30000
# 测试连接是否可用的SQL
connection-test-query: SELECT 1
# 自动提交
auto-commit: true
jpa:
hibernate:
ddl-auto: update
show-sql: true
show-sql: false
properties:
hibernate:
format_sql: true
format_sql: false
dialect: org.hibernate.dialect.MySQLDialect
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
# 生产环境关闭SQL日志
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名

View File

@@ -4,19 +4,21 @@
<el-header class="header" v-if="!isMobile">
<h1 @click="$emit('reset-view')" style="cursor: pointer; flex-grow: 1;">我的笔记</h1>
<div class="actions">
<el-input
:model-value="searchKeyword"
@update:model-value="$emit('update:searchKeyword', $event)"
placeholder="搜索笔记标题"
class="search-input"
@keyup.enter="$emit('search')"
>
<template #append>
<el-button @click="$emit('search')">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<div class="search-box">
<el-input
:model-value="searchKeyword"
@update:model-value="handleSearchInput"
placeholder="搜索笔记标题"
class="search-input"
@keyup.enter="$emit('search')"
clearable
@clear="handleClear"
>
<template #suffix>
<el-icon class="search-icon" @click="$emit('search')"><Search /></el-icon>
</template>
</el-input>
</div>
<div v-if="userStore.isLoggedIn" class="user-actions">
<span class="welcome-text">欢迎, {{ userStore.userInfo?.username }}</span>
<el-button type="danger" @click="$emit('logout')">退出</el-button>
@@ -58,14 +60,16 @@
<div v-if="isMobile" class="mobile-search-container">
<el-input
:model-value="searchKeyword"
@update:model-value="$emit('update:searchKeyword', $event)"
@update:model-value="handleSearchInput"
placeholder="搜索笔记标题"
class="mobile-search-input"
@keyup.enter="$emit('search')"
@clear="handleClear"
clearable
size="large"
>
<template #prefix>
<el-icon><Search /></el-icon>
<el-icon class="mobile-search-icon"><Search /></el-icon>
</template>
</el-input>
</div>
@@ -104,6 +108,21 @@ const handleUpload = (file) => {
emit('upload-markdown', file);
return false; // Prevent el-upload's default behavior
};
// 处理搜索输入 - 当输入为空时自动回到首页
const handleSearchInput = (value) => {
emit('update:searchKeyword', value);
// 当清空输入时,自动触发回到首页
if (!value || value.trim() === '') {
emit('reset-view');
}
};
// 处理清空按钮点击
const handleClear = () => {
emit('update:searchKeyword', '');
emit('reset-view');
};
</script>
<style scoped>
@@ -130,8 +149,42 @@ const handleUpload = (file) => {
align-items: center;
}
.search-box {
display: flex;
align-items: center;
}
:deep(.search-input .el-input__wrapper) {
border-radius: var(--border-radius) !important;
border-radius: 20px !important;
padding-right: 8px;
transition: all 0.3s ease;
}
:deep(.search-input .el-input__wrapper:hover) {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
:deep(.search-input .el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
.search-icon {
cursor: pointer;
color: var(--el-text-color-placeholder);
font-size: 16px;
padding: 4px;
border-radius: 50%;
transition: all 0.3s ease;
}
.search-icon:hover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.mobile-search-icon {
color: var(--el-text-color-placeholder);
font-size: 18px;
}
.user-actions, .guest-actions {