refactor: 优化图片处理与数据库连接配置
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -17,7 +17,6 @@ public class SystemSetting implements Serializable {
|
||||
|
||||
@TableId
|
||||
@Schema(description = "设置键", name = "settingKey")
|
||||
@TableField("`setting_key`")
|
||||
private String settingKey;
|
||||
|
||||
@Schema(description = "设置值", name = "settingValue")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 " +
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
// 数据库中已有记录,需要对比处理
|
||||
|
||||
@@ -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_
|
||||
@@ -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 # 全局逻辑删除的实体字段名
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user