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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,10 @@ import org.apache.ibatis.annotations.Update;
@Mapper @Mapper
public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> { 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 " + "FROM `markdown_file` mf " +
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " + "LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
"WHERE mf.is_deleted = 0 " + "WHERE mf.is_deleted = 0 " +
@@ -23,14 +26,20 @@ public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
"LIMIT #{limit}") "LIMIT #{limit}")
List<MarkdownFileVO> selectRecentWithGrouping(@Param("limit") int 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 " + "FROM `markdown_file` mf " +
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " + "LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
"WHERE mf.grouping_id = #{groupingId} AND mf.is_deleted = 0 " + "WHERE mf.grouping_id = #{groupingId} AND mf.is_deleted = 0 " +
"ORDER BY mf.updated_at DESC") "ORDER BY mf.updated_at DESC")
List<MarkdownFileVO> selectByGroupingIdWithGrouping(@Param("groupingId") String groupingId); 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(); List<MarkdownFile> selectDeleted();
@Delete("DELETE FROM `markdown_file` WHERE id = #{id}") @Delete("DELETE FROM `markdown_file` WHERE id = #{id}")
@@ -48,8 +57,15 @@ public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
* 获取所有笔记ID * 获取所有笔记ID
* @return 所有笔记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") @Select("SELECT id FROM `markdown_file` WHERE is_deleted = 0")
List<Integer> findAllIds(); 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 " + @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 " + "FROM `markdown_file` mf " +

View File

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

View File

@@ -132,12 +132,25 @@ public class ImageServiceImpl
if (CollUtil.isEmpty(urls)) { if (CollUtil.isEmpty(urls)) {
return false; return false;
} }
for (String url : urls) { // 批量查询避免N+1问题
Image image = imageMapper.selectOne(new QueryWrapper<Image>().eq("url", url)); List<Image> images = imageMapper.selectList(new QueryWrapper<Image>().in("url", urls));
if (ObjectUtil.isNotNull(image)) { if (CollUtil.isEmpty(images)) {
this.deleteImageByUrl(url); 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; return true;
} }
} }

View File

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

View File

@@ -1,21 +1,27 @@
spring: spring:
datasource: 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 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 url: jdbc:mysql://hdy-hk-8-8.311169.xyz:3306/biji_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username: biji_user username: biji_user
password: Ll12331100 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: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: update
@@ -32,8 +38,12 @@ mybatis-plus:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
# 启用安全模式防止SQL注入 # 启用安全模式防止SQL注入
safe-mode: true safe-mode: true
# 配置日志输出
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config: global-config:
db-config: db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名 logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1) 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: spring:
datasource: 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} driver-class-name: ${DB_DRIVER:com.mysql.cj.jdbc.Driver}
url: ${DB_URL} url: ${DB_URL}
username: ${DB_USERNAME} username: ${DB_USERNAME}
password: ${DB_PASSWORD} 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: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: update
show-sql: true show-sql: false
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: false
dialect: org.hibernate.dialect.MySQLDialect dialect: org.hibernate.dialect.MySQLDialect
# MyBatis-Plus配置 # MyBatis-Plus配置
mybatis-plus: mybatis-plus:
mapper-locations: classpath:mapper/*.xml mapper-locations: classpath:mapper/*.xml
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
# 生产环境关闭SQL日志
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config: global-config:
db-config: db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名 logic-delete-field: isDeleted # 全局逻辑删除的实体字段名

View File

@@ -4,19 +4,21 @@
<el-header class="header" v-if="!isMobile"> <el-header class="header" v-if="!isMobile">
<h1 @click="$emit('reset-view')" style="cursor: pointer; flex-grow: 1;">我的笔记</h1> <h1 @click="$emit('reset-view')" style="cursor: pointer; flex-grow: 1;">我的笔记</h1>
<div class="actions"> <div class="actions">
<el-input <div class="search-box">
:model-value="searchKeyword" <el-input
@update:model-value="$emit('update:searchKeyword', $event)" :model-value="searchKeyword"
placeholder="搜索笔记标题" @update:model-value="handleSearchInput"
class="search-input" placeholder="搜索笔记标题"
@keyup.enter="$emit('search')" class="search-input"
> @keyup.enter="$emit('search')"
<template #append> clearable
<el-button @click="$emit('search')"> @clear="handleClear"
<el-icon><Search /></el-icon> >
</el-button> <template #suffix>
</template> <el-icon class="search-icon" @click="$emit('search')"><Search /></el-icon>
</el-input> </template>
</el-input>
</div>
<div v-if="userStore.isLoggedIn" class="user-actions"> <div v-if="userStore.isLoggedIn" class="user-actions">
<span class="welcome-text">欢迎, {{ userStore.userInfo?.username }}</span> <span class="welcome-text">欢迎, {{ userStore.userInfo?.username }}</span>
<el-button type="danger" @click="$emit('logout')">退出</el-button> <el-button type="danger" @click="$emit('logout')">退出</el-button>
@@ -58,14 +60,16 @@
<div v-if="isMobile" class="mobile-search-container"> <div v-if="isMobile" class="mobile-search-container">
<el-input <el-input
:model-value="searchKeyword" :model-value="searchKeyword"
@update:model-value="$emit('update:searchKeyword', $event)" @update:model-value="handleSearchInput"
placeholder="搜索笔记标题" placeholder="搜索笔记标题"
class="mobile-search-input" class="mobile-search-input"
@keyup.enter="$emit('search')" @keyup.enter="$emit('search')"
@clear="handleClear"
clearable
size="large" size="large"
> >
<template #prefix> <template #prefix>
<el-icon><Search /></el-icon> <el-icon class="mobile-search-icon"><Search /></el-icon>
</template> </template>
</el-input> </el-input>
</div> </div>
@@ -104,6 +108,21 @@ const handleUpload = (file) => {
emit('upload-markdown', file); emit('upload-markdown', file);
return false; // Prevent el-upload's default behavior 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> </script>
<style scoped> <style scoped>
@@ -130,8 +149,42 @@ const handleUpload = (file) => {
align-items: center; align-items: center;
} }
.search-box {
display: flex;
align-items: center;
}
:deep(.search-input .el-input__wrapper) { :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 { .user-actions, .guest-actions {