优化多线程转换方法 添加异步转换提示

This commit is contained in:
高雄
2026-01-15 15:43:13 +08:00
parent b5579ae890
commit 2425bea9b6
20 changed files with 3431 additions and 1376 deletions

View File

@@ -28,7 +28,6 @@
<!-- ========== PDF 处理 ========== --> <!-- ========== PDF 处理 ========== -->
<pdfbox.version>3.0.6</pdfbox.version> <pdfbox.version>3.0.6</pdfbox.version>
<itextpdf.version>5.5.13.4</itextpdf.version>
<!-- ========== 图像处理 ========== --> <!-- ========== 图像处理 ========== -->
<jai-imageio.version>1.4.0</jai-imageio.version> <jai-imageio.version>1.4.0</jai-imageio.version>
@@ -51,7 +50,6 @@
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version> <concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
<!-- ========== 网络通信 ========== --> <!-- ========== 网络通信 ========== -->
<httpclient.version>5.6</httpclient.version>
<httpcomponents.version>4.5.16</httpcomponents.version> <httpcomponents.version>4.5.16</httpcomponents.version>
<commons-net.version>3.12.0</commons-net.version> <commons-net.version>3.12.0</commons-net.version>

View File

@@ -23,15 +23,20 @@
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
<repositories> <repositories>
<repository>
<id>aspose-maven-repository</id> <!-- Aspose 仓库且只启用 releases -->
<url>https://repository.aspose.com/repo</url> <repository>
<snapshots> <id>aspose-maven-repository</id>
<enabled>false</enabled> <url>https://repository.aspose.com/repo</url>
</snapshots> <releases>
</repository> <enabled>true</enabled>
</repositories> </releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies> <dependencies>
<!-- ========== Spring Boot 框架依赖 ========== --> <!-- ========== Spring Boot 框架依赖 ========== -->
@@ -115,11 +120,6 @@
<artifactId>pdfbox-tools</artifactId> <artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version> <version>${pdfbox.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>${itextpdf.version}</version>
</dependency>
<!-- ========== 压缩文件处理 ========== --> <!-- ========== 压缩文件处理 ========== -->
<dependency> <dependency>
@@ -303,18 +303,6 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>${httpclient.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,18 +1,12 @@
#######################################不可动态配置重启生效####################################### ####################################### 服务器基础配置不可动态配置需重启生效#######################################
server.port = ${KK_SERVER_PORT:8012} server.port = ${KK_SERVER_PORT:8012}
server.servlet.context-path= ${KK_CONTEXT_PATH:/} server.servlet.context-path = ${KK_CONTEXT_PATH:/}
server.servlet.encoding.charset = utf-8 server.servlet.encoding.charset = utf-8
#启用GZIP压缩功能
server.compression.enabled = true server.compression.enabled = true
#允许压缩的响应缓冲区最小字节数默认2048
server.compression.min-response-size = 2048 server.compression.min-response-size = 2048
#压缩格式 server.compression.mime-types = application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain,font/woff,application/font-woff,font/eot,image/svg+xml,image/x-icon
server.compression.mime-types=application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain,font/woff,application/font-woff,font/eot,image/svg+xml,image/x-icon spring.servlet.multipart.max-file-size = 500MB
# 文件上传限制前端 spring.servlet.multipart.max-request-size = 500MB
spring.servlet.multipart.max-file-size=500MB
#文件上传限制
spring.servlet.multipart.max-request-size=500MB
## Freemarker 配置
spring.freemarker.template-loader-path = classpath:/web/ spring.freemarker.template-loader-path = classpath:/web/
spring.freemarker.cache = false spring.freemarker.cache = false
spring.freemarker.charset = UTF-8 spring.freemarker.charset = UTF-8
@@ -22,226 +16,119 @@ spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true spring.freemarker.expose-session-attributes = true
spring.freemarker.request-context-attribute = request spring.freemarker.request-context-attribute = request
spring.freemarker.suffix = .ftl spring.freemarker.suffix = .ftl
# Spring Boot Actuator 健康检查配置 management.endpoints.web.exposure.include = health,info,metrics
# 开启健康检查端点 management.endpoint.health.show-details = always
management.endpoints.web.exposure.include=health,info,metrics management.health.defaults.enabled = true
# 显示详细的健康检查信息生产环境建议设置为when-authorized
management.endpoint.health.show-details=always
# 启用健康检查组件
management.health.defaults.enabled=true
####################################### Office文档处理配置部分支持动态配置#######################################
# office设置
#openoffice或LibreOffice home路径
#office.home = C:\\Program Files (x86)\\OpenOffice 4
office.home = ${KK_OFFICE_HOME:default} office.home = ${KK_OFFICE_HOME:default}
## office转换服务的端口默认开启两个进程
office.plugin.server.ports = 2001,2002 office.plugin.server.ports = 2001,2002
## office 转换服务 task 超时时间默认五分钟
office.plugin.task.timeout = 5m office.plugin.task.timeout = 5m
#此属性设置office进程在重新启动之前可以执行的最大任务数0表示无限数量的任务永远不会重新启动
office.plugin.task.maxtasksperprocess = 200 office.plugin.task.maxtasksperprocess = 200
#此属性设置处理任务所允许的最长时间如果任务的处理时间长于此超时则此任务将中止并处理下一个任务
office.plugin.task.taskexecutiontimeout = 5m office.plugin.task.taskexecutiontimeout = 5m
#生成限制 默认不限制 使用方法 (1-5)
office.pagerange = ${KK_OFFICE_PAGERANGE:false} office.pagerange = ${KK_OFFICE_PAGERANGE:false}
#生成水印 默认不启用 使用方法 (kkFileView) office.watermark = ${KK_OFFICE_WATERMARK:false}
office.watermark = ${KK_OFFICE_WATERMARK:false}
#OFFICE JPEG图片压缩
office.quality = ${KK_OFFICE_QUALITY:80} office.quality = ${KK_OFFICE_QUALITY:80}
#图像分辨率限制
office.maximageresolution = ${KK_OFFICE_MAXIMAGERESOLUTION:150} office.maximageresolution = ${KK_OFFICE_MAXIMAGERESOLUTION:150}
#导出书签
office.exportbookmarks = ${KK_OFFICE_EXPORTBOOKMARKS:true} office.exportbookmarks = ${KK_OFFICE_EXPORTBOOKMARKS:true}
#批注作为PDF的注释
office.exportnotes = ${KK_OFFICE_EXPORTNOTES:true} office.exportnotes = ${KK_OFFICE_EXPORTNOTES:true}
#加密文档 生成的PDF文档 添加密码(密码为加密文档的密码)
office.documentopenpasswords = ${KK_OFFICE_DOCUMENTOPENPASSWORD:true} office.documentopenpasswords = ${KK_OFFICE_DOCUMENTOPENPASSWORD:true}
#xlsx格式前端解析
office.type.web = ${KK_OFFICE_TYPE_WEB:web} office.type.web = ${KK_OFFICE_TYPE_WEB:web}
####################################### 文件存储与缓存配置 #######################################
# 其他核心设置
#预览生成资源路径默认为打包根路径下的file目录下
#file.dir = D:\\kkFileview\\
file.dir = ${KK_FILE_DIR:default} file.dir = ${KK_FILE_DIR:default}
#允许预览的本地文件夹 默认不允许任何本地文件被预览
#WINDOWS参考 local.preview.dir = \D:\\kkFileview\\1\\1.txt (注意前面必须添加反斜杠)
#LINUX参考 local.preview.dir = /opt/1.txt (注意前面必须是正斜杠)
#使用方法 windows file://d:/1/1.txt linux file:/opt/1/1.txt
#file 协议参考https://datatracker.ietf.org/doc/html/rfc8089
local.preview.dir = ${KK_LOCAL_PREVIEW_DIR:default} local.preview.dir = ${KK_LOCAL_PREVIEW_DIR:default}
#是否启用缓存
cache.enabled = ${KK_CACHE_ENABLED:true} cache.enabled = ${KK_CACHE_ENABLED:true}
#缓存实现类型不配默认为内嵌RocksDB(type = default)实现可配置为redis(type = redis)实现需要配置spring.redisson.address等参数 JDK 内置对象实现type = jdk, cache.type = ${KK_CACHE_TYPE:jdk}
cache.type = ${KK_CACHE_TYPE:jdk}
#redis 连接只有当cache.type = redis时才有用
# single: 单机模式
# address: redis://localhost:6379
# cluster: 集群模式
# 每个节点逗号分隔同时每个节点前必须以redis://开头。
# address: redis://localhost:6379,redis://localhost:6378,...
# sentinel: 哨兵模式
# 每个节点逗号分隔同时每个节点前必须以redis://开头。
# address: redis://localhost:6379,redis://localhost:6378,...
# master-slave: 主从模式
# 每个节点逗号分隔第一个为主节点其余为从节点同时每个节点前必须以redis://开头。
# address: redis://localhost:6379,redis://localhost:6378,...
spring.redisson.mode = single spring.redisson.mode = single
spring.redisson.address = ${KK_SPRING_REDISSON_ADDRESS:redis://127.0.0.1:6379} spring.redisson.address = ${KK_SPRING_REDISSON_ADDRESS:redis://127.0.0.1:6379}
spring.redisson.password = ${KK_SPRING_REDISSON_PASSWORD:} spring.redisson.password = ${KK_SPRING_REDISSON_PASSWORD:}
#redis 设置库
spring.redisson.database = ${KK_SPRING_REDISSON_DATABASE:0} spring.redisson.database = ${KK_SPRING_REDISSON_DATABASE:0}
#哨兵模式 打开 masterName设置
#spring.redisson.masterName = kkfile
#缓存是否自动清理 true 为开启注释掉或其他值都为关闭
cache.clean.enabled = ${KK_CACHE_CLEAN_ENABLED:true} cache.clean.enabled = ${KK_CACHE_CLEAN_ENABLED:true}
#缓存自动清理时间cache.clean.enabled = true时才有用cron表达式基于Quartz cron
cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?} cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
#######################################可在运行时动态配置#######################################
#提供预览服务的地址默认从请求url读如果使用nginx等反向代理需要手动设置 ####################################### 安全与访问控制配置支持动态配置#######################################
#base.url = https://file.keking.cn
base.url = ${KK_BASE_URL:default} base.url = ${KK_BASE_URL:default}
# ========== 安全配置重要==========
# 信任站点白名单配置多个用','隔开
# 安全提示为防止SSRF攻击强烈建议配置信任主机白名单
# 如果不配置系统将默认拒绝所有外部文件预览请求
#
# 配置示例
# trust.host = kkview.cn,yourdomain.com,cdn.example.com
#
# 如果需要允许所有域名不推荐仅用于测试环境请设置为
# trust.host = *
#
# 当前配置
trust.host = * trust.host = *
not.trust.host = ${KK_NOT_TRUST_HOST:default}
# 不信任站点黑名单配置多个用','隔开
# 黑名单优先级高于白名单设置后将禁止预览来自这些站点的文件
# 建议配置禁止访问内网地址和本地地址
# not.trust.host = localhost,127.0.0.1,0.0.0.0,192.168.*,10.*,172.16.*
not.trust.host= ${KK_NOT_TRUST_HOST:default}
#文本类型默认如下可自定义添加
simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
#FTP模块设置
#预览源为FTP时 FTP用户名可在ftp url后面加参数ftp.username=ftpuser指定不指定默认用配置的
ftp.username = ${KK_FTP_USERNAME:ftpuser}
#预览源为FTP时 FTP密码可在ftp url后面加参数ftp.password=123456指定不指定默认用配置的
ftp.password = ${KK_FTP_PASSWORD:123456}
#预览源为FTP时, FTP连接默认ControlEncoding(根据FTP服务器操作系统选择Linux一般为UTF-8Windows一般为GBK)可在ftp url后面加参数ftp.control.encoding=UTF-8指定不指定默认用配置的
ftp.control.encoding = ${KK_FTP_CONTROL_ENCODING:UTF-8}
#视频设置
#多媒体类型默认如下可自定义添加
media = ${KK_MEDIA:mp3,wav,mp4,flv,mpd,m3u8,ts,mpeg,m4a}
#是否开启多媒体类型转视频格式转换,目前可转换视频格式有avi,mov,wmv,3gp,rm
#请谨慎开启此功能建议异步调用添加到处理队列并且增加任务队列处理线程防止视频转换占用完线程资源转换比较耗费时间,并且控制了只能串行处理转换任务
media.convert.disable = ${KK_MEDIA_CONVERT_DISABLE:true}
#支持转换的视频类型
convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm,mpeg}
#PDF预览模块设置
#配置PDF文件生成图片的像素大小dpi 越高图片质量越清晰同时也会消耗更多的计算资源
pdf2jpg.dpi = ${KK_PDF2JPG_DPI:144}
#PDF转换超时设置 (低于50页) 温馨提示这里数字仅供参考
pdf.timeout =${KK_pdf_TIMEOUT:90}
#PDF转换超时设置 (高于50小于200页)
pdf.timeout80 =${KK_PDF_TIMEOUT80:180}
#PDF转换超时设置 (大于200页)
pdf.timeout200 =${KK_PDF_TIMEOUT200:300}
#PDF转换线程设置
pdf.thread =${KK_PDF_THREAD:5}
#是否禁止演示模式
pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
#是否禁止打开文件
pdf.openFile.disable = ${KK_PDF_OPEN_FILE_DISABLE:true}
#是否禁止打印转换生成的pdf文件
pdf.print.disable = ${KK_PDF_PRINT_DISABLE:true}
#是否禁止下载转换生成的pdf文件
pdf.download.disable = ${KK_PDF_DOWNLOAD_DISABLE:true}
#是否禁止bookmark
pdf.bookmark.disable = ${KK_PDF_BOOKMARK_DISABLE:true}
#是否禁止签名
pdf.disable.editing = ${KK_PDF_DISABLE_EDITING:false}
#office类型文档(word ppt)样式默认为图片(image)可配置为pdf预览时也有按钮切换
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
#是否关闭office预览切换开关默认为false可配置为true关闭
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
#水印内容
#watermark.txt = ${WATERMARK_TXT:凯京科技内部文件严禁外泄}
#如需取消水印内容设置为空即可watermark.txt = ${WATERMARK_TXT:}
watermark.txt = ${WATERMARK_TXT:}
#水印x轴间隔
watermark.x.space = ${WATERMARK_X_SPACE:10}
#水印y轴间隔
watermark.y.space = ${WATERMARK_Y_SPACE:10}
#水印字体
watermark.font = ${WATERMARK_FONT:微软雅黑}
#水印字体大小
watermark.fontsize = ${WATERMARK_FONTSIZE:18px}
#水印字体颜色
watermark.color = ${WATERMARK_COLOR:black}
#水印透明度要求设置在大于等于0.005小于1
watermark.alpha = ${WATERMARK_ALPHA:0.2}
#水印宽度
watermark.width = ${WATERMARK_WIDTH:180}
#水印高度
watermark.height = ${WATERMARK_HEIGHT:80}
#水印倾斜度数要求设置在大于等于0小于90
watermark.angle = ${WATERMARK_ANGLE:10}
#首页功能设置
#是否禁用首页文件上传
file.upload.disable = false
# 备案信息默认为空
beian = ${KK_BEIAN:default}
#禁止上传类型
prohibit = ${KK_PROHIBIT:exe,dll,dat} prohibit = ${KK_PROHIBIT:exe,dll,dat}
#启用验证码删除文件 默认关闭
delete.captcha= ${KK_DELETE_CAPTCHA:false}
#删除密码
delete.password = ${KK_DELETE_PASSWORD:123456}
#删除 转换后OFFICECADTIFF压缩包源文件 默认开启 节约磁盘空间
delete.source.file = ${KK_DELETE_SOURCE_FILE:true}
#首页初始化加载第一页
home.pagenumber = ${DEFAULT_HOME_PAGENUMBER:1}
#首页是否分页
home.pagination = ${DEFAULT_HOME_PAGINATION:true}
#首页初始化单页记录数
home.pagesize = ${DEFAULT_HOME_PAGSIZE:15}
#首页显示查询框
home.search = ${DEFAULT_HOME_SEARCH:true}
#Tif类型设置 ####################################### 文件格式与预览配置支持动态配置#######################################
#Tif类型图片浏览模式tif利用前端js插件浏览jpg转换为jpg后前端显示pdf转换为pdf后显示便于打印 simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
media = ${KK_MEDIA:mp3,wav,mp4,flv,mpd,m3u8,ts,mpeg,m4a}
convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm,mpeg}
tif.preview.type = ${KK_TIF_PREVIEW_TYPE:tif} tif.preview.type = ${KK_TIF_PREVIEW_TYPE:tif}
#Cad类型设置
#Cad类型图片浏览模式tif利用前端js插件浏览svg转换为svg显示pdf转换为pdf后显示便于打印
cad.preview.type = ${KK_CAD_PREVIEW_TYPE:svg} cad.preview.type = ${KK_CAD_PREVIEW_TYPE:svg}
#Cad转换超时设置 office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
cad.timeout =${KK_CAD_TIMEOUT:90} office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
#Cad转换线程设置 pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
cad.thread =${KK_CAD_THREAD:5} pdf.openFile.disable = ${KK_PDF_OPEN_FILE_DISABLE:true}
pdf.print.disable = ${KK_PDF_PRINT_DISABLE:true}
pdf.download.disable = ${KK_PDF_DOWNLOAD_DISABLE:true}
pdf.bookmark.disable = ${KK_PDF_BOOKMARK_DISABLE:true}
pdf.disable.editing = ${KK_PDF_DISABLE_EDITING:false}
watermark.txt = ${WATERMARK_TXT:}
watermark.x.space = ${WATERMARK_X_SPACE:10}
watermark.y.space = ${WATERMARK_Y_SPACE:10}
watermark.font = ${WATERMARK_FONT:微软雅黑}
watermark.fontsize = ${WATERMARK_FONTSIZE:18px}
watermark.color = ${WATERMARK_COLOR:black}
watermark.alpha = ${WATERMARK_ALPHA:0.2}
watermark.width = ${WATERMARK_WIDTH:180}
watermark.height = ${WATERMARK_HEIGHT:80}
watermark.angle = ${WATERMARK_ANGLE:10}
cad.watermark = true
####################################### 权限配置 ####################################### ####################################### 性能与资源管理配置支持动态配置#######################################
kk.Picturespreview=true pdf.max.threads = 10
kk.Getcorsfile=true cad.thread = ${KK_CAD_THREAD:5}
kk.addTask=true tif.thread = 5
kk.Key=false media.timeout.enabled = true
# 启用AES 接入方法 ase秘钥 必须16位 接入秘钥必须相同 false (为不启用) media.small.file.timeout = 30
ase.key= 1234567890123456 media.medium.file.timeout = 60
media.large.file.timeout = 180
media.xl.file.timeout = 300
media.xxl.file.timeout = 600
media.xxxl.file.timeout = 1200
pdf.timeout.small = 90
pdf.timeout.medium = 180
pdf.timeout.large = 300
pdf.timeout.xlarge = 600
cad.timeout = ${KK_CAD_TIMEOUT:90}
tif.timeout = 90
media.convert.max.size = 300
media.convert.disable = ${KK_MEDIA_CONVERT_DISABLE:true}
pdf.dpi.enabled = true
pdf2jpg.dpi = ${KK_PDF2JPG_DPI:144}
pdf.dpi.small = 150
pdf.dpi.medium = 120
pdf.dpi.large = 96
pdf.dpi.xlarge = 72
pdf.dpi.xxlarge = 72
#basic.name=192.168.0.1:name:123456,192.168.0.2:name:123456,www.xxx.com:admin:pass 域名+用户名+密码 多个接入用,分割 ####################################### FTP文件访问配置支持动态配置#######################################
basic.name=10.99.1.2:aaa:bbb ftp.username = false
useragent=99999 ####################################### 首页与文件管理配置支持动态配置#######################################
file.upload.disable = false
beian = ${KK_BEIAN:default}
home.pagenumber = ${DEFAULT_HOME_PAGENUMBER:1}
home.pagination = ${DEFAULT_HOME_PAGINATION:true}
home.pagesize = ${DEFAULT_HOME_PAGSIZE:15}
home.search = ${DEFAULT_HOME_SEARCH:true}
delete.captcha = ${KK_DELETE_CAPTCHA:false}
delete.password = ${KK_DELETE_PASSWORD:123456}
delete.source.file = ${KK_DELETE_SOURCE_FILE:true}
####################################### 权限与认证配置支持动态配置#######################################
kk.Picturespreview = true
kk.Getcorsfile = true
kk.addTask = true
kk.Key = false
ase.key = 1234567890123456
basic.name = 10.99.1.2:aaa:bbb
useragent = 99999
####################################### 高级功能与兼容性配置支持动态配置#######################################
kk.ignore.ssl = true
kk.enable.redirect = true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -15,35 +15,38 @@ import java.util.Properties;
import java.util.concurrent.*; import java.util.concurrent.*;
/** /**
* @auther: chenjh * 配置刷新组件 - 动态配置管理
* @time: 2019/4/10 16:16 * 功能:监听配置文件变化,实现热更新配置
* @description 使用 WatchService 监听配置文件变化,实现事件驱动的配置更新
*/ */
@Component @Component
public class ConfigRefreshComponent { public class ConfigRefreshComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class); private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class);
// 线程池和任务调度器
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService watchServiceExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService watchServiceExecutor = Executors.newSingleThreadExecutor();
private final Object lock = new Object(); private final Object lock = new Object();
// 防抖延迟时间(单位:秒) // 防抖延迟时间(秒)
private static final long DEBOUNCE_DELAY_SECONDS = 5; private static final long DEBOUNCE_DELAY_SECONDS = 5;
// 任务和状态管理
private ScheduledFuture<?> scheduledReloadTask; private ScheduledFuture<?> scheduledReloadTask;
private WatchService watchService; private WatchService watchService;
private volatile boolean running = true; private volatile boolean running = true;
/**
* 初始化方法 - 启动配置监听
*/
@PostConstruct @PostConstruct
void init() { void init() {
// 初始化时立即加载一次配置
loadConfig(); loadConfig();
// 启动监听线程
watchServiceExecutor.submit(this::watchConfigFile); watchServiceExecutor.submit(this::watchConfigFile);
} }
/**
* 销毁方法 - 清理资源
*/
@PreDestroy @PreDestroy
void destroy() { void destroy() {
running = false; running = false;
@@ -74,8 +77,6 @@ public class ConfigRefreshComponent {
} }
watchService = FileSystems.getDefault().newWatchService(); watchService = FileSystems.getDefault().newWatchService();
// 注册监听目录的修改事件
configDir.register(watchService, configDir.register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_CREATE,
@@ -91,7 +92,6 @@ public class ConfigRefreshComponent {
WatchEvent.Kind<?> kind = event.kind(); WatchEvent.Kind<?> kind = event.kind();
Path changedPath = (Path) event.context(); Path changedPath = (Path) event.context();
// 检查是否是目标配置文件的变化
if (changedPath.equals(configPath.getFileName())) { if (changedPath.equals(configPath.getFileName())) {
handleConfigChange(kind); handleConfigChange(kind);
} }
@@ -131,7 +131,6 @@ public class ConfigRefreshComponent {
if (kind == StandardWatchEventKinds.ENTRY_MODIFY || if (kind == StandardWatchEventKinds.ENTRY_MODIFY ||
kind == StandardWatchEventKinds.ENTRY_CREATE) { kind == StandardWatchEventKinds.ENTRY_CREATE) {
// 使用防抖机制:取消之前的任务,重新调度新任务
synchronized (lock) { synchronized (lock) {
if (scheduledReloadTask != null && !scheduledReloadTask.isDone()) { if (scheduledReloadTask != null && !scheduledReloadTask.isDone()) {
scheduledReloadTask.cancel(false); scheduledReloadTask.cancel(false);
@@ -158,7 +157,6 @@ public class ConfigRefreshComponent {
Properties properties = new Properties(); Properties properties = new Properties();
String configFilePath = ConfigUtils.getCustomizedConfigPath(); String configFilePath = ConfigUtils.getCustomizedConfigPath();
// 检查文件是否存在
Path configPath = Paths.get(configFilePath); Path configPath = Paths.get(configFilePath);
if (!Files.exists(configPath)) { if (!Files.exists(configPath)) {
LOGGER.warn("配置文件不存在: {}", configFilePath); LOGGER.warn("配置文件不存在: {}", configFilePath);
@@ -169,7 +167,6 @@ public class ConfigRefreshComponent {
properties.load(bufferedReader); properties.load(bufferedReader);
ConfigUtils.restorePropertiesFromEnvFormat(properties); ConfigUtils.restorePropertiesFromEnvFormat(properties);
// 解析并设置配置项
updateConfigConstants(properties); updateConfigConstants(properties);
setWatermarkConfig(properties); setWatermarkConfig(properties);
@@ -181,6 +178,7 @@ public class ConfigRefreshComponent {
} }
} }
} }
/** /**
* 更新配置常量 * 更新配置常量
*/ */
@@ -213,8 +211,6 @@ public class ConfigRefreshComponent {
// 4. FTP配置 // 4. FTP配置
String ftpUsername = properties.getProperty("ftp.username", ConfigConstants.DEFAULT_FTP_USERNAME); String ftpUsername = properties.getProperty("ftp.username", ConfigConstants.DEFAULT_FTP_USERNAME);
String ftpPassword = properties.getProperty("ftp.password", ConfigConstants.DEFAULT_FTP_PASSWORD);
String ftpControlEncoding = properties.getProperty("ftp.control.encoding", ConfigConstants.DEFAULT_FTP_CONTROL_ENCODING);
// 5. 路径配置 // 5. 路径配置
String baseUrl = properties.getProperty("base.url", ConfigConstants.DEFAULT_VALUE); String baseUrl = properties.getProperty("base.url", ConfigConstants.DEFAULT_VALUE);
@@ -231,10 +227,6 @@ public class ConfigRefreshComponent {
String pdfBookmarkDisable = properties.getProperty("pdf.bookmark.disable", ConfigConstants.DEFAULT_PDF_BOOKMARK_DISABLE); String pdfBookmarkDisable = properties.getProperty("pdf.bookmark.disable", ConfigConstants.DEFAULT_PDF_BOOKMARK_DISABLE);
String pdfDisableEditing = properties.getProperty("pdf.disable.editing", ConfigConstants.DEFAULT_PDF_DISABLE_EDITING); String pdfDisableEditing = properties.getProperty("pdf.disable.editing", ConfigConstants.DEFAULT_PDF_DISABLE_EDITING);
int pdf2JpgDpi = Integer.parseInt(properties.getProperty("pdf2jpg.dpi", ConfigConstants.DEFAULT_PDF2_JPG_DPI)); int pdf2JpgDpi = Integer.parseInt(properties.getProperty("pdf2jpg.dpi", ConfigConstants.DEFAULT_PDF2_JPG_DPI));
int pdfTimeout = Integer.parseInt(properties.getProperty("pdf.timeout", ConfigConstants.DEFAULT_PDF_TIMEOUT));
int pdfTimeout80 = Integer.parseInt(properties.getProperty("pdf.timeout80", ConfigConstants.DEFAULT_PDF_TIMEOUT80));
int pdfTimeout200 = Integer.parseInt(properties.getProperty("pdf.timeout200", ConfigConstants.DEFAULT_PDF_TIMEOUT200));
int pdfThread = Integer.parseInt(properties.getProperty("pdf.thread", ConfigConstants.DEFAULT_PDF_THREAD));
// 8. CAD配置 // 8. CAD配置
String cadTimeout = properties.getProperty("cad.timeout", ConfigConstants.DEFAULT_CAD_TIMEOUT); String cadTimeout = properties.getProperty("cad.timeout", ConfigConstants.DEFAULT_CAD_TIMEOUT);
@@ -247,25 +239,66 @@ public class ConfigRefreshComponent {
boolean deleteSourceFile = Boolean.parseBoolean(properties.getProperty("delete.source.file", ConfigConstants.DEFAULT_DELETE_SOURCE_FILE)); boolean deleteSourceFile = Boolean.parseBoolean(properties.getProperty("delete.source.file", ConfigConstants.DEFAULT_DELETE_SOURCE_FILE));
boolean deleteCaptcha = Boolean.parseBoolean(properties.getProperty("delete.captcha", ConfigConstants.DEFAULT_DELETE_CAPTCHA)); boolean deleteCaptcha = Boolean.parseBoolean(properties.getProperty("delete.captcha", ConfigConstants.DEFAULT_DELETE_CAPTCHA));
// 10. 首页配置 // 10. TIF配置
String tifTimeout = properties.getProperty("tif.timeout", ConfigConstants.DEFAULT_TIF_TIMEOUT);
int tifThread = Integer.parseInt(properties.getProperty("tif.thread", ConfigConstants.DEFAULT_TIF_THREAD));
// 11. 首页配置
String beian = properties.getProperty("beian", ConfigConstants.DEFAULT_BEIAN); String beian = properties.getProperty("beian", ConfigConstants.DEFAULT_BEIAN);
String homePageNumber = properties.getProperty("home.pagenumber", ConfigConstants.DEFAULT_HOME_PAGENUMBER); String homePageNumber = properties.getProperty("home.pagenumber", ConfigConstants.DEFAULT_HOME_PAGENUMBER);
String homePagination = properties.getProperty("home.pagination", ConfigConstants.DEFAULT_HOME_PAGINATION); String homePagination = properties.getProperty("home.pagination", ConfigConstants.DEFAULT_HOME_PAGINATION);
String homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE); String homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
String homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH); String homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
// 11. 权限配置 // 12. 权限配置
String key = properties.getProperty("kk.Key", ConfigConstants.DEFAULT_KEY); String key = properties.getProperty("kk.Key", ConfigConstants.DEFAULT_KEY);
boolean picturesPreview = Boolean.parseBoolean(properties.getProperty("kk.Picturespreview", ConfigConstants.DEFAULT_PICTURES_PREVIEW)); boolean picturesPreview = Boolean.parseBoolean(properties.getProperty("kk.Picturespreview", ConfigConstants.DEFAULT_PICTURES_PREVIEW));
boolean getCorsFile = Boolean.parseBoolean(properties.getProperty("kk.Getcorsfile", ConfigConstants.DEFAULT_GET_CORS_FILE)); boolean getCorsFile = Boolean.parseBoolean(properties.getProperty("kk.Getcorsfile", ConfigConstants.DEFAULT_GET_CORS_FILE));
boolean addTask = Boolean.parseBoolean(properties.getProperty("kk.addTask", ConfigConstants.DEFAULT_ADD_TASK)); boolean addTask = Boolean.parseBoolean(properties.getProperty("kk.addTask", ConfigConstants.DEFAULT_ADD_TASK));
String aesKey = properties.getProperty("ase.key", ConfigConstants.DEFAULT_AES_KEY); String aesKey = properties.getProperty("ase.key", ConfigConstants.DEFAULT_AES_KEY);
// 12. UserAgent配置
// 13. UserAgent配置
String userAgent = properties.getProperty("useragent", ConfigConstants.DEFAULT_USER_AGENT); String userAgent = properties.getProperty("useragent", ConfigConstants.DEFAULT_USER_AGENT);
// 13. Basic认证配置 // 14. Basic认证配置
String basicName = properties.getProperty("basic.name", ConfigConstants.DEFAULT_BASIC_NAME); String basicName = properties.getProperty("basic.name", ConfigConstants.DEFAULT_BASIC_NAME);
// 15. 视频转换配置
int mediaConvertMaxSize = Integer.parseInt(properties.getProperty("media.convert.max.size", ConfigConstants.DEFAULT_MEDIA_CONVERT_MAX_SIZE));
boolean mediaTimeoutEnabled = Boolean.parseBoolean(properties.getProperty("media.timeout.enabled", ConfigConstants.DEFAULT_MEDIA_TIMEOUT_ENABLED));
int mediaSmallFileTimeout = Integer.parseInt(properties.getProperty("media.small.file.timeout", ConfigConstants.DEFAULT_MEDIA_SMALL_FILE_TIMEOUT));
int mediaMediumFileTimeout = Integer.parseInt(properties.getProperty("media.medium.file.timeout", ConfigConstants.DEFAULT_MEDIA_MEDIUM_FILE_TIMEOUT));
int mediaLargeFileTimeout = Integer.parseInt(properties.getProperty("media.large.file.timeout", ConfigConstants.DEFAULT_MEDIA_LARGE_FILE_TIMEOUT));
int mediaXLFileTimeout = Integer.parseInt(properties.getProperty("media.xl.file.timeout", ConfigConstants.DEFAULT_MEDIA_XL_FILE_TIMEOUT));
int mediaXXLFileTimeout = Integer.parseInt(properties.getProperty("media.xxl.file.timeout", ConfigConstants.DEFAULT_MEDIA_XXL_FILE_TIMEOUT));
int mediaXXXLFileTimeout = Integer.parseInt(properties.getProperty("media.xxxl.file.timeout", ConfigConstants.DEFAULT_MEDIA_XXXL_FILE_TIMEOUT));
// 16. PDF DPI配置
boolean pdfDpiEnabled = Boolean.parseBoolean(properties.getProperty("pdf.dpi.enabled", ConfigConstants.DEFAULT_PDF_DPI_ENABLED).trim());
int pdfSmallDpi = Integer.parseInt(properties.getProperty("pdf.dpi.small", ConfigConstants.DEFAULT_PDF_SMALL_DTI).trim());
int pdfMediumDpi = Integer.parseInt(properties.getProperty("pdf.dpi.medium", ConfigConstants.DEFAULT_PDF_MEDIUM_DPI).trim());
int pdfLargeDpi = Integer.parseInt(properties.getProperty("pdf.dpi.large", ConfigConstants.DEFAULT_PDF_LARGE_DPI).trim());
int pdfXLargeDpi = Integer.parseInt(properties.getProperty("pdf.dpi.xlarge", ConfigConstants.DEFAULT_PDF_XLARGE_DPI).trim());
int pdfXXLargeDpi = Integer.parseInt(properties.getProperty("pdf.dpi.xxlarge", ConfigConstants.DEFAULT_PDF_XXLARGE_DPI).trim());
// 17. PDF超时配置
int pdfTimeoutSmall = Integer.parseInt(properties.getProperty("pdf.timeout.small", ConfigConstants.DEFAULT_PDF_TIMEOUT_SMALL).trim());
int pdfTimeoutMedium = Integer.parseInt(properties.getProperty("pdf.timeout.medium", ConfigConstants.DEFAULT_PDF_TIMEOUT_MEDIUM).trim());
int pdfTimeoutLarge = Integer.parseInt(properties.getProperty("pdf.timeout.large", ConfigConstants.DEFAULT_PDF_TIMEOUT_LARGE).trim());
int pdfTimeoutXLarge = Integer.parseInt(properties.getProperty("pdf.timeout.xlarge", ConfigConstants.DEFAULT_PDF_TIMEOUT_XLARGE).trim());
// 18. PDF线程配置
int pdfMaxThreads = Integer.parseInt(properties.getProperty("pdf.max.threads", ConfigConstants.DEFAULT_PDF_MAX_THREADS).trim());
// 19. CAD水印配置
boolean cadwatermark = Boolean.parseBoolean(properties.getProperty("cad.watermark", ConfigConstants.DEFAULT_CAD_WATERMARK));
// 20. SSL忽略配置
boolean ignoreSSL = Boolean.parseBoolean(properties.getProperty("kk.ignore.ssl", ConfigConstants.DEFAULT_IGNORE_SSL));
// 21. 重定向启用配置
boolean enableRedirect = Boolean.parseBoolean(properties.getProperty("kk.enable.redirect", ConfigConstants.DEFAULT_ENABLE_REDIRECT));
// 设置配置值 // 设置配置值
// 1. 缓存配置 // 1. 缓存配置
ConfigConstants.setCacheEnabledValueValue(cacheEnabled); ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
@@ -291,8 +324,6 @@ public class ConfigRefreshComponent {
// 4. FTP配置 // 4. FTP配置
ConfigConstants.setFtpUsernameValue(ftpUsername); ConfigConstants.setFtpUsernameValue(ftpUsername);
ConfigConstants.setFtpPasswordValue(ftpPassword);
ConfigConstants.setFtpControlEncodingValue(ftpControlEncoding);
// 5. 路径配置 // 5. 路径配置
ConfigConstants.setBaseUrlValue(baseUrl); ConfigConstants.setBaseUrlValue(baseUrl);
@@ -309,10 +340,6 @@ public class ConfigRefreshComponent {
ConfigConstants.setPdfBookmarkDisableValue(pdfBookmarkDisable); ConfigConstants.setPdfBookmarkDisableValue(pdfBookmarkDisable);
ConfigConstants.setPdfDisableEditingValue(pdfDisableEditing); ConfigConstants.setPdfDisableEditingValue(pdfDisableEditing);
ConfigConstants.setPdf2JpgDpiValue(pdf2JpgDpi); ConfigConstants.setPdf2JpgDpiValue(pdf2JpgDpi);
ConfigConstants.setPdfTimeoutValue(pdfTimeout);
ConfigConstants.setPdfTimeout80Value(pdfTimeout80);
ConfigConstants.setPdfTimeout200Value(pdfTimeout200);
ConfigConstants.setPdfThreadValue(pdfThread);
// 8. CAD配置 // 8. CAD配置
ConfigConstants.setCadTimeoutValue(cadTimeout); ConfigConstants.setCadTimeoutValue(cadTimeout);
@@ -325,25 +352,65 @@ public class ConfigRefreshComponent {
ConfigConstants.setDeleteSourceFileValue(deleteSourceFile); ConfigConstants.setDeleteSourceFileValue(deleteSourceFile);
ConfigConstants.setDeleteCaptchaValue(deleteCaptcha); ConfigConstants.setDeleteCaptchaValue(deleteCaptcha);
// 10. 首页配置 // 10. TIF配置
ConfigConstants.setTifTimeoutValue(tifTimeout);
ConfigConstants.setTifThreadValue(tifThread);
// 11. 首页配置
ConfigConstants.setBeianValue(beian); ConfigConstants.setBeianValue(beian);
ConfigConstants.setHomePageNumberValue(homePageNumber); ConfigConstants.setHomePageNumberValue(homePageNumber);
ConfigConstants.setHomePaginationValue(homePagination); ConfigConstants.setHomePaginationValue(homePagination);
ConfigConstants.setHomePageSizeValue(homePageSize); ConfigConstants.setHomePageSizeValue(homePageSize);
ConfigConstants.setHomeSearchValue(homeSearch); ConfigConstants.setHomeSearchValue(homeSearch);
// 11. 权限配置 // 12. 权限配置
ConfigConstants.setKeyValue(key); ConfigConstants.setKeyValue(key);
ConfigConstants.setPicturesPreviewValue(picturesPreview); ConfigConstants.setPicturesPreviewValue(picturesPreview);
ConfigConstants.setGetCorsFileValue(getCorsFile); ConfigConstants.setGetCorsFileValue(getCorsFile);
ConfigConstants.setAddTaskValue(addTask); ConfigConstants.setAddTaskValue(addTask);
ConfigConstants.setaesKeyValue(aesKey); ConfigConstants.setaesKeyValue(aesKey);
// 12. UserAgent配置 // 13. UserAgent配置
ConfigConstants.setUserAgentValue(userAgent); ConfigConstants.setUserAgentValue(userAgent);
// 13. Basic认证配置 // 14. Basic认证配置
ConfigConstants.setBasicNameValue(basicName); ConfigConstants.setBasicNameValue(basicName);
// 15. 视频转换配置
ConfigConstants.setMediaConvertMaxSizeValue(mediaConvertMaxSize);
ConfigConstants.setMediaTimeoutEnabledValue(mediaTimeoutEnabled);
ConfigConstants.setMediaSmallFileTimeoutValue(mediaSmallFileTimeout);
ConfigConstants.setMediaMediumFileTimeoutValue(mediaMediumFileTimeout);
ConfigConstants.setMediaLargeFileTimeoutValue(mediaLargeFileTimeout);
ConfigConstants.setMediaXLFileTimeoutValue(mediaXLFileTimeout);
ConfigConstants.setMediaXXLFileTimeoutValue(mediaXXLFileTimeout);
ConfigConstants.setMediaXXXLFileTimeoutValue(mediaXXXLFileTimeout);
// 19. CAD水印配置
ConfigConstants.setCadwatermarkValue(cadwatermark);
// 20. SSL忽略配置
ConfigConstants.setIgnoreSSLValue(ignoreSSL);
// 21. 重定向启用配置
ConfigConstants.setEnableRedirectValue(enableRedirect);
// 16. PDF DPI配置
ConfigConstants.setPdfDpiEnabledValue(pdfDpiEnabled);
ConfigConstants.setPdfSmallDpiValue(pdfSmallDpi);
ConfigConstants.setPdfMediumDpiValue(pdfMediumDpi);
ConfigConstants.setPdfLargeDpiValue(pdfLargeDpi);
ConfigConstants.setPdfXLargeDpiValue(pdfXLargeDpi);
ConfigConstants.setPdfXXLargeDpiValue(pdfXXLargeDpi);
// 17. PDF超时配置
ConfigConstants.setPdfTimeoutSmallValue(pdfTimeoutSmall);
ConfigConstants.setPdfTimeoutMediumValue(pdfTimeoutMedium);
ConfigConstants.setPdfTimeoutLargeValue(pdfTimeoutLarge);
ConfigConstants.setPdfTimeoutXLargeValue(pdfTimeoutXLarge);
// 18. PDF线程配置
ConfigConstants.setPdfMaxThreadsValue(pdfMaxThreads);
} }
/** /**

View File

@@ -34,6 +34,7 @@ public interface FilePreview {
String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported"; String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
String XLSX_FILE_PREVIEW_PAGE = "officeweb"; String XLSX_FILE_PREVIEW_PAGE = "officeweb";
String CSV_FILE_PREVIEW_PAGE = "csv"; String CSV_FILE_PREVIEW_PAGE = "csv";
String WAITING_FILE_PREVIEW_PAGE = "waiting";
String filePreviewHandle(String url, Model model, FileAttribute fileAttribute); String filePreviewHandle(String url, Model model, FileAttribute fileAttribute);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.File; import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -28,6 +30,7 @@ public class OfficeToPdfService {
public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException { public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException {
Instant startTime = Instant.now();
File outputFile = new File(outputFilePath_end); File outputFile = new File(outputFilePath_end);
// 假如目标路径不存在,则新建该路径 // 假如目标路径不存在,则新建该路径
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
@@ -65,7 +68,42 @@ public class OfficeToPdfService {
} else { } else {
builder = LocalConverter.builder().storeProperties(customProperties); builder = LocalConverter.builder().storeProperties(customProperties);
} }
builder.build().convert(inputFile).to(outputFile).execute();
try {
builder.build().convert(inputFile).to(outputFile).execute();
// 计算转换耗时
Instant endTime = Instant.now();
Duration duration = Duration.between(startTime, endTime);
// 格式化显示耗时(支持不同时间单位)
String durationFormatted;
if (duration.toMinutes() > 0) {
durationFormatted = String.format("%d分%d秒",
duration.toMinutes(),
duration.toSecondsPart());
} else if (duration.toSeconds() > 0) {
durationFormatted = String.format("%d.%03d秒",
duration.toSeconds(),
duration.toMillisPart());
} else {
durationFormatted = String.format("%d毫秒", duration.toMillis());
}
logger.info("文件转换成功:{} -> {},耗时:{}",
inputFile.getName(),
outputFile.getName(),
durationFormatted);
} catch (OfficeException e) {
Instant endTime = Instant.now();
Duration duration = Duration.between(startTime, endTime);
logger.error("文件转换失败:{},已耗时:{}毫秒,错误信息:{}",
inputFile.getName(),
duration.toMillis(),
e.getMessage());
throw e;
}
} }
@@ -97,4 +135,4 @@ public class OfficeToPdfService {
return inputFilePath.substring(inputFilePath.lastIndexOf(".") + 1); return inputFilePath.substring(inputFilePath.lastIndexOf(".") + 1);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,472 @@
package cn.keking.service;
import cn.keking.config.ConfigConstants;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.apache.commons.imaging.Imaging;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* TIF文件转换服务 - 虚拟线程版本 (JDK 21+)
*/
@Component
public class TifToPdfService {
private static final Logger logger = LoggerFactory.getLogger(TifToPdfService.class);
private static final String FILE_DIR = ConfigConstants.getFileDir();
// 虚拟线程执行器
private ExecutorService virtualThreadExecutor;
@PostConstruct
public void init() {
try {
// 创建虚拟线程执行器
this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
logger.info("TIF转换虚拟线程执行器初始化完成");
} catch (Exception e) {
logger.error("虚拟线程执行器初始化失败", e);
// 降级为固定线程池
this.virtualThreadExecutor = Executors.newFixedThreadPool(
Math.min(getMaxConcurrentConversions(), Runtime.getRuntime().availableProcessors() * 2)
);
logger.warn("使用固定线程池作为降级方案");
}
}
/**
* TIF转JPG - 虚拟线程版本
*/
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,
boolean forceUpdatedCache) throws Exception {
String fileName = new File(strInputFile).getName();
Instant startTime = Instant.now();
try {
List<String> result = performTifToJpgConversionVirtual(
strInputFile, strOutputFile, forceUpdatedCache
);
Duration elapsedTime = Duration.between(startTime, Instant.now());
boolean success = result != null && !result.isEmpty();
logger.info("TIF转换{} - 文件: {}, 耗时: {}ms, 页数: {}",
success ? "成功" : "失败", fileName, elapsedTime.toMillis(),
result != null ? result.size() : 0);
return result != null ? result : Collections.emptyList();
} catch (Exception e) {
logger.error("TIF转JPG失败: {}, 耗时: {}ms", fileName,
Duration.between(startTime, Instant.now()).toMillis(), e);
throw e;
}
}
/**
* 虚拟线程执行TIF转JPG转换
*/
private List<String> performTifToJpgConversionVirtual(String strInputFile, String strOutputFile,
boolean forceUpdatedCache) throws Exception {
Instant totalStart = Instant.now();
String baseUrl = BaseUrlFilter.getBaseUrl();
String outputDirPath = strOutputFile.substring(0, strOutputFile.lastIndexOf('.'));
File tiffFile = new File(strInputFile);
if (!tiffFile.exists()) {
throw new FileNotFoundException("文件不存在: " + strInputFile);
}
File outputDir = new File(outputDirPath);
if (!outputDir.exists() && !outputDir.mkdirs()) {
throw new IOException("创建目录失败: " + outputDirPath);
}
// 加载所有图片
List<BufferedImage> images;
try {
images = Imaging.getAllBufferedImages(tiffFile);
// logger.info("TIF文件加载完成共{}页,文件: {}",images.size(), strInputFile);
} catch (IOException e) {
handleImagingException(e, strInputFile);
throw e;
}
int pageCount = images.size();
if (pageCount == 0) {
logger.warn("TIF文件没有可转换的页面: {}", strInputFile);
return Collections.emptyList();
}
List<String> result = convertPagesVirtualThreads(images, outputDirPath, baseUrl, forceUpdatedCache);
Duration totalTime = Duration.between(totalStart, Instant.now());
logger.info("TIF转换PNG完成总页数: {}, 总耗时: {}ms", pageCount, totalTime.toMillis());
return result;
}
/**
* 使用虚拟线程并行转换页面
*/
private List<String> convertPagesVirtualThreads(List<BufferedImage> images, String outputDirPath,
String baseUrl, boolean forceUpdatedCache) {
int pageCount = images.size();
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger skipCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
// 用于收集结果的并发列表
List<String> imageUrls = Collections.synchronizedList(new ArrayList<>(pageCount));
Instant startTime = Instant.now();
try {
// 使用虚拟线程并行处理所有页面
List<CompletableFuture<Void>> futures = IntStream.range(0, pageCount)
.mapToObj(pageIndex -> CompletableFuture.runAsync(() -> {
try {
BufferedImage image = images.get(pageIndex);
// 使用PNG格式质量更好
String fileName = outputDirPath + File.separator + pageIndex + ".png";
File outputFile = new File(fileName);
if (forceUpdatedCache || !outputFile.exists()) {
// 创建目录
File parentDir = outputFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
boolean writeSuccess = ImageIO.write(image, "png", outputFile);
if (!writeSuccess) {
throw new IOException("无法写入PNG格式");
}
successCount.incrementAndGet();
} else {
skipCount.incrementAndGet();
}
// 构建URL
String relativePath = fileName.replace(FILE_DIR, "");
String url = baseUrl + WebUtils.encodeFileName(relativePath);
imageUrls.add(url);
} catch (Exception e) {
logger.error("转换页 {} 失败: {}", pageIndex, e.getMessage());
errorCount.incrementAndGet();
}
}, virtualThreadExecutor))
.toList();
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.completeOnTimeout(null, getConversionTimeout(), TimeUnit.SECONDS)
.join();
} catch (Exception e) {
logger.error("虚拟线程并行转换异常", e);
}
Duration elapsedTime = Duration.between(startTime, Instant.now());
logger.info("TIF虚拟线程转换统计: 成功={}, 跳过={}, 失败={}, 总页数={}, 耗时={}ms",successCount.get(), skipCount.get(), errorCount.get(), pageCount,
elapsedTime.toMillis());
// 按页码排序
return imageUrls.stream()
.sorted(Comparator.comparing(url -> {
// 从URL中提取页码进行排序
String fileName = url.substring(url.lastIndexOf('/') + 1);
return Integer.parseInt(fileName.substring(0, fileName.lastIndexOf('.')));
}))
.collect(Collectors.toList());
}
/**
* TIF转PDF - 虚拟线程版本
*/
public void convertTif2Pdf(String strJpgFile, String strPdfFile,
boolean forceUpdatedCache) throws Exception {
String fileName = new File(strJpgFile).getName();
Instant startTime = Instant.now();
try {
File pdfFile = new File(strPdfFile);
// 检查缓存
if (!forceUpdatedCache && pdfFile.exists()) {
logger.info("PDF文件已存在跳过转换: {}", strPdfFile);
return;
}
boolean result = performTifToPdfConversionVirtual(strJpgFile, strPdfFile);
Duration elapsedTime = Duration.between(startTime, Instant.now());
logger.info("TIF转PDF{} - 文件: {}, 耗时: {}ms",
result ? "成功" : "失败", fileName, elapsedTime.toMillis());
if (!result) {
throw new Exception("TIF转PDF失败");
}
} catch (Exception e) {
logger.error("TIF转PDF失败: {}, 耗时: {}ms", fileName,
Duration.between(startTime, Instant.now()).toMillis(), e);
throw e;
}
}
/**
* 虚拟线程执行TIF转PDF转换保持顺序
*/
private boolean performTifToPdfConversionVirtual(String strJpgFile, String strPdfFile) throws Exception {
Instant totalStart = Instant.now();
File tiffFile = new File(strJpgFile);
try (PDDocument document = new PDDocument()) {
// 直接使用Imaging获取所有图像
List<BufferedImage> images = Imaging.getAllBufferedImages(tiffFile);
if (images.isEmpty()) {
logger.warn("TIFF文件没有可转换的页面: {}", strJpgFile);
return false;
}
int pageCount = images.size();
AtomicInteger processedCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
// 创建页面处理结果的列表
List<CompletableFuture<ProcessedPageResult>> futures = new ArrayList<>(pageCount);
// 为每个页面创建处理任务
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
final int currentPageIndex = pageIndex;
BufferedImage originalImage = images.get(pageIndex);
CompletableFuture<ProcessedPageResult> future = CompletableFuture.supplyAsync(() -> {
try {
// 处理图像(耗时的操作)
BufferedImage processedImage = processImageVirtualOptimized(originalImage);
// 返回处理结果,包含页码和处理后的图像
return new ProcessedPageResult(currentPageIndex, processedImage);
} catch (Exception e) {
logger.error("异步处理页 {} 失败", currentPageIndex + 1, e);
errorCount.incrementAndGet();
return null;
}
}, virtualThreadExecutor);
futures.add(future);
}
// 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 设置超时
allFutures.completeOnTimeout(null, getConversionTimeout(), TimeUnit.SECONDS).join();
// 按顺序收集处理结果
List<ProcessedPageResult> results = new ArrayList<>(pageCount);
for (CompletableFuture<ProcessedPageResult> future : futures) {
ProcessedPageResult result = future.get();
if (result != null) {
results.add(result);
}
}
// 按页码排序(确保顺序)
results.sort(Comparator.comparingInt(ProcessedPageResult::pageIndex));
// 按顺序添加到PDF文档
for (ProcessedPageResult result : results) {
try {
// 创建页面
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
// 转换为PDImageXObject
PDImageXObject pdImage = LosslessFactory.createFromImage(document, result.processedImage());
// 计算位置并绘制图像
float[] position = calculateImagePositionOptimized(page, pdImage);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(pdImage, position[0], position[1], position[2], position[3]);
}
// 释放资源
result.processedImage().flush();
processedCount.incrementAndGet();
} catch (Exception e) {
logger.error("添加页 {} 到PDF失败", result.pageIndex() + 1, e);
errorCount.incrementAndGet();
}
}
// 保存PDF
document.save(strPdfFile);
// Duration totalTime = Duration.between(totalStart, Instant.now());
//logger.info("PDF异步转换完成: {}, 总页数: {}, 成功: {}, 失败: {}, 总耗时: {}ms", strPdfFile, pageCount, processedCount.get(), errorCount.get(), totalTime.toMillis());
return processedCount.get() > 0;
}
}
/**
* 页面处理结果类
*/
private record ProcessedPageResult(int pageIndex, BufferedImage processedImage) {
}
/**
* 优化的图像处理方法
*/
private BufferedImage processImageVirtualOptimized(BufferedImage original) {
int targetDPI = 150;
float a4WidthInch = 8.27f;
float a4HeightInch = 11.69f;
int maxWidth = (int) (a4WidthInch * targetDPI);
int maxHeight = (int) (a4HeightInch * targetDPI);
if (original.getWidth() <= maxWidth && original.getHeight() <= maxHeight) {
return original;
}
double scaleX = (double) maxWidth / original.getWidth();
double scaleY = (double) maxHeight / original.getHeight();
double scale = Math.min(scaleX, scaleY);
int newWidth = (int) (original.getWidth() * scale);
int newHeight = (int) (original.getHeight() * scale);
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g.drawImage(original, 0, 0, newWidth, newHeight, null);
g.dispose();
return resized;
}
/**
* 优化的位置计算方法
*/
private float[] calculateImagePositionOptimized(PDPage page, PDImageXObject pdImage) {
float margin = 5;
float pageWidth = page.getMediaBox().getWidth() - 2 * margin;
float pageHeight = page.getMediaBox().getHeight() - 2 * margin;
float imageWidth = pdImage.getWidth();
float imageHeight = pdImage.getHeight();
float scale = Math.min(pageWidth / imageWidth, pageHeight / imageHeight);
float scaledWidth = imageWidth * scale;
float scaledHeight = imageHeight * scale;
float x = (pageWidth - scaledWidth) / 2 + margin;
float y = (pageHeight - scaledHeight) / 2 + margin;
return new float[]{x, y, scaledWidth, scaledHeight};
}
/**
* 异常处理
*/
private void handleImagingException(IOException e, String filePath) {
String message = e.getMessage();
if (message != null && message.contains("Only sequential, baseline JPEGs are supported at the moment")) {
logger.warn("不支持的非基线JPEG格式文件{}", filePath, e);
} else {
logger.error("TIF转JPG异常文件路径{}", filePath, e);
}
}
/**
* 获取转换超时时间
*/
private long getConversionTimeout() {
try {
String timeoutStr = ConfigConstants.getTifTimeout();
if (timeoutStr != null && !timeoutStr.trim().isEmpty()) {
long timeout = Long.parseLong(timeoutStr);
if (timeout > 0) {
return Math.min(timeout, 600L);
}
}
} catch (NumberFormatException e) {
logger.warn("解析TIF转换超时时间失败使用默认值300秒", e);
}
return 300L;
}
/**
* 获取最大并发数
*/
private int getMaxConcurrentConversions() {
try {
int maxConcurrent = ConfigConstants.getTifThread();
if (maxConcurrent > 0) {
return Math.min(maxConcurrent, 50);
}
} catch (Exception e) {
logger.error("获取并发数配置失败,使用默认值", e);
}
return 4;
}
@PreDestroy
public void shutdown() {
logger.info("开始关闭TIF转换服务...");
if (virtualThreadExecutor != null && !virtualThreadExecutor.isShutdown()) {
try {
virtualThreadExecutor.shutdown();
if (!virtualThreadExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
virtualThreadExecutor.shutdownNow();
}
logger.info("虚拟线程执行器已关闭");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
virtualThreadExecutor.shutdownNow();
}
}
logger.info("TIF转换服务已关闭");
}
}

View File

@@ -1,426 +0,0 @@
package cn.keking.service;
import cn.keking.config.ConfigConstants;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.io.FileChannelRandomAccessSource;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.RandomAccessFileOrArray;
import com.itextpdf.text.pdf.codec.TiffImage;
import org.apache.commons.imaging.Imaging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Component
public class TifToService {
private static final int FIT_WIDTH = 500;
private static final int FIT_HEIGHT = 900;
private static final Logger logger = LoggerFactory.getLogger(TifToService.class);
private static final String FILE_DIR = ConfigConstants.getFileDir();
// 用于文档同步的锁对象
private final Object documentLock = new Object();
// 专用线程池用于TIF转换
private static ExecutorService tifConversionPool;
static {
initThreadPool();
}
private static void initThreadPool() {
int corePoolSize = getOptimalThreadCount();
int maxPoolSize = corePoolSize * 2;
long keepAliveTime = 60L;
tifConversionPool = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("tif-convert-thread-" + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
logger.info("TIF转换线程池初始化完成核心线程数: {}, 最大线程数: {}", corePoolSize, maxPoolSize);
}
private static int getOptimalThreadCount() {
int cpuCores = Runtime.getRuntime().availableProcessors();
// 对于I/O密集型任务可以设置更多的线程
return Math.min(cpuCores * 2, 16);
}
/**
* 创建 RandomAccessFileOrArray 实例(修复已弃用的构造函数)
*/
private RandomAccessFileOrArray createRandomAccessFileOrArray(File file) throws IOException {
RandomAccessFile aFile = new RandomAccessFile(file, "r");
FileChannel inChannel = aFile.getChannel();
FileChannelRandomAccessSource fcra = new FileChannelRandomAccessSource(inChannel);
return new RandomAccessFileOrArray(fcra);
}
/**
* TIF转JPG - 支持多线程并行处理
*/
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,
boolean forceUpdatedCache) throws Exception {
return convertTif2Jpg(strInputFile, strOutputFile, forceUpdatedCache, true);
}
/**
* TIF转JPG - 可选择是否启用并行处理
* @param parallelProcessing 是否启用并行处理
*/
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,
boolean forceUpdatedCache, boolean parallelProcessing) throws Exception {
String baseUrl = BaseUrlFilter.getBaseUrl();
String outputDirPath = strOutputFile.substring(0, strOutputFile.lastIndexOf('.'));
File tiffFile = new File(strInputFile);
if (!tiffFile.exists()) {
logger.error("找不到文件【{}】", strInputFile);
throw new FileNotFoundException("文件不存在: " + strInputFile);
}
File outputDir = new File(outputDirPath);
if (!outputDir.exists() && !outputDir.mkdirs()) {
throw new IOException("创建目录失败: " + outputDirPath);
}
// 加载所有图片
List<BufferedImage> images;
try {
images = Imaging.getAllBufferedImages(tiffFile);
logger.info("TIF文件加载完成共{}页,文件: {}", images.size(), strInputFile);
} catch (IOException e) {
handleImagingException(e, strInputFile);
throw e;
}
int pageCount = images.size();
// 根据页面数量决定是否使用并行处理
boolean useParallel = parallelProcessing && pageCount > 5;
if (useParallel) {
return convertParallel(images, outputDirPath, baseUrl, forceUpdatedCache);
} else {
return convertSequentially(images, outputDirPath, baseUrl, forceUpdatedCache);
}
}
/**
* 并行转换
*/
private List<String> convertParallel(List<BufferedImage> images, String outputDirPath,
String baseUrl, boolean forceUpdatedCache) {
int pageCount = images.size();
List<CompletableFuture<String>> futures = new ArrayList<>(pageCount);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger skipCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
// 提交所有页面转换任务
for (int i = 0; i < pageCount; i++) {
final int pageIndex = i;
BufferedImage image = images.get(i);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
String fileName = outputDirPath + File.separator + pageIndex + ".jpg";
File outputFile = new File(fileName);
// 检查是否需要转换
if (forceUpdatedCache || !outputFile.exists()) {
// 使用PNG格式保持更好的质量如果需要JPG可以调整
boolean success = ImageIO.write(image, "png", outputFile);
if (!success) {
logger.error("无法写入图片格式,页号: {}", pageIndex);
return null;
}
logger.debug("并行转换图片页 {} 完成", pageIndex);
successCount.incrementAndGet();
} else {
logger.debug("使用缓存图片页 {}", pageIndex);
skipCount.incrementAndGet();
}
// 构建URL
String relativePath = fileName.replace(FILE_DIR, "");
return baseUrl + WebUtils.encodeFileName(relativePath);
} catch (Exception e) {
logger.error("并行转换页 {} 失败: {}", pageIndex, e.getMessage());
return null;
}
}, tifConversionPool);
futures.add(future);
}
// 等待所有任务完成并收集结果
List<String> imageUrls = futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("TIF并行转换完成: 成功={}, 跳过={}, 总页数={}, 耗时={}ms",
successCount.get(), skipCount.get(), pageCount, elapsedTime);
return imageUrls;
}
/**
* 串行转换
*/
private List<String> convertSequentially(List<BufferedImage> images, String outputDirPath,
String baseUrl, boolean forceUpdatedCache) throws Exception {
List<String> imageUrls = new ArrayList<>(images.size());
for (int i = 0; i < images.size(); i++) {
String fileName = outputDirPath + File.separator + i + ".jpg";
File outputFile = new File(fileName);
try {
if (forceUpdatedCache || !outputFile.exists()) {
BufferedImage image = images.get(i);
boolean success = ImageIO.write(image, "png", outputFile);
if (!success) {
throw new IOException("无法写入JPG格式图片: " + fileName);
}
logger.debug("转换图片页 {} 完成", i);
} else {
logger.debug("使用缓存图片页 {}", i);
}
String relativePath = fileName.replace(FILE_DIR, "");
String url = baseUrl + WebUtils.encodeFileName(relativePath);
imageUrls.add(url);
} catch (IOException e) {
logger.error("转换页 {} 失败: {}", i, e.getMessage());
throw e;
}
}
return imageUrls;
}
/**
* 将JPG图片转换为PDF - 优化版本
*/
public void convertTif2Pdf(String strJpgFile, String strPdfFile) throws Exception {
convertJpg2Pdf(strJpgFile, strPdfFile, true);
}
/**
* 将JPG图片转换为PDF - 支持并行处理图片加载
*/
public void convertJpg2Pdf(String strJpgFile, String strPdfFile, boolean parallelLoad) throws Exception {
Document document = new Document();
FileOutputStream outputStream = null;
RandomAccessFileOrArray rafa = null;
try {
File tiffFile = new File(strJpgFile);
// 修复:使用非弃用的方式创建 RandomAccessFileOrArray
rafa = createRandomAccessFileOrArray(tiffFile);
int pages = TiffImage.getNumberOfPages(rafa);
logger.info("开始转换TIFF到PDF总页数: {}", pages);
outputStream = new FileOutputStream(strPdfFile);
PdfWriter.getInstance(document, outputStream);
document.open();
// 修改为传入File对象而不是RandomAccessFileOrArray
if (parallelLoad && pages > 10) {
convertPagesParallel(document, tiffFile, pages);
} else {
convertPagesSequentially(document, tiffFile, pages);
}
} catch (IOException e) {
handlePdfConversionException(e, strPdfFile);
throw e;
} finally {
// 修复:传入 rafa 以正确关闭资源
closeResources(document, rafa, outputStream);
}
logger.info("PDF转换完成: {}", strPdfFile);
}
/**
* 串行处理页面 - 修复版
*/
private void convertPagesSequentially(Document document, File tiffFile, int pages) throws IOException, DocumentException {
RandomAccessFileOrArray rafa = null;
try {
// 修复:使用非弃用的方式创建 RandomAccessFileOrArray
rafa = createRandomAccessFileOrArray(tiffFile);
for (int i = 1; i <= pages; i++) {
Image image = TiffImage.getTiffImage(rafa, i);
image.scaleToFit(FIT_WIDTH, FIT_HEIGHT);
document.add(image);
if (i % 10 == 0) {
logger.debug("已处理 {} 页", i);
}
}
} finally {
if (rafa != null) {
try {
rafa.close();
} catch (Exception e) {
logger.warn("关闭RandomAccessFileOrArray失败", e);
}
}
}
}
/**
* 并行加载并添加图片到PDF - 修复版
*/
private void convertPagesParallel(Document document, File tiffFile, int pages) {
List<CompletableFuture<Image>> futures = new ArrayList<>();
// 提交所有页面加载任务
for (int i = 1; i <= pages; i++) {
final int pageNum = i;
CompletableFuture<Image> future = CompletableFuture.supplyAsync(() -> {
RandomAccessFileOrArray localRafa = null;
try {
// 为每个线程创建独立的RandomAccessFileOrArray
// 修复:使用非弃用的方式创建 RandomAccessFileOrArray
localRafa = createRandomAccessFileOrArray(tiffFile);
Image image = TiffImage.getTiffImage(localRafa, pageNum);
image.scaleToFit(FIT_WIDTH, FIT_HEIGHT);
logger.debug("并行加载TIFF页 {}", pageNum);
return image;
} catch (Exception e) {
logger.error("加载TIFF页 {} 失败", pageNum, e);
return null;
} finally {
if (localRafa != null) {
try {
localRafa.close();
} catch (Exception e) {
logger.warn("关闭RandomAccessFileOrArray失败", e);
}
}
}
}, tifConversionPool);
futures.add(future);
}
// 按顺序添加到文档(保持页面顺序)
for (int i = 0; i < futures.size(); i++) {
try {
Image image = futures.get(i).get(30, TimeUnit.SECONDS);
if (image != null) {
// 使用专门的锁对象而不是同步document参数
synchronized (documentLock) {
document.add(image);
}
}
} catch (Exception e) {
logger.error("添加页 {} 到PDF失败", i + 1, e);
}
}
}
/**
* 异常处理
*/
private void handleImagingException(IOException e, String filePath) {
if (!e.getMessage().contains("Only sequential, baseline JPEGs are supported at the moment")) {
logger.error("TIF转JPG异常文件路径{}", filePath, e);
} else {
logger.warn("不支持的非基线JPEG格式文件{}", filePath);
}
}
private void handlePdfConversionException(IOException e, String filePath) {
if (!e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)")) {
logger.error("TIF转PDF异常文件路径{}", filePath, e);
} else {
logger.warn("TIFF文件字节顺序标记错误文件{}", filePath);
}
}
/**
* 资源关闭
*/
private void closeResources(Document document, RandomAccessFileOrArray rafa, FileOutputStream outputStream) {
try {
if (document != null && document.isOpen()) {
document.close();
}
} catch (Exception e) {
logger.warn("关闭Document失败", e);
}
try {
if (rafa != null) {
rafa.close();
}
} catch (Exception e) {
logger.warn("关闭RandomAccessFileOrArray失败", e);
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (Exception e) {
logger.warn("关闭FileOutputStream失败", e);
}
}
/**
* 优雅关闭
*/
public static void shutdown() {
if (tifConversionPool != null && !tifConversionPool.isShutdown()) {
tifConversionPool.shutdown();
try {
if (!tifConversionPool.awaitTermination(30, TimeUnit.SECONDS)) {
tifConversionPool.shutdownNow();
}
} catch (InterruptedException e) {
tifConversionPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}

View File

@@ -7,6 +7,7 @@ import cn.keking.service.CadToPdfService;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils; import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils; import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter; import cn.keking.web.filter.BaseUrlFilter;
@@ -16,6 +17,9 @@ import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** /**
* @author chenjh * @author chenjh
@@ -33,7 +37,13 @@ public class CadFilePreviewImpl implements FilePreview {
private final OtherFilePreviewImpl otherFilePreview; private final OtherFilePreviewImpl otherFilePreview;
private final OfficeFilePreviewImpl officefilepreviewimpl; private final OfficeFilePreviewImpl officefilepreviewimpl;
public CadFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, CadToPdfService cadtopdfservice,OfficeFilePreviewImpl officefilepreviewimpl ) { // 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
public CadFilePreviewImpl(FileHandlerService fileHandlerService,
OtherFilePreviewImpl otherFilePreview,
CadToPdfService cadtopdfservice,
OfficeFilePreviewImpl officefilepreviewimpl) {
this.fileHandlerService = fileHandlerService; this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.cadtopdfservice = cadtopdfservice; this.cadtopdfservice = cadtopdfservice;
@@ -43,41 +53,109 @@ public class CadFilePreviewImpl implements FilePreview {
@Override @Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
// 预览Type参数传了就取参数的没传取系统默认 // 预览Type参数传了就取参数的没传取系统默认
String officePreviewType = fileAttribute.getOfficePreviewType() == null ? ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType(); String officePreviewType = fileAttribute.getOfficePreviewType() == null ?
ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
String baseUrl = BaseUrlFilter.getBaseUrl(); String baseUrl = BaseUrlFilter.getBaseUrl();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String fileName = fileAttribute.getName(); String fileName = fileAttribute.getName();
String cadPreviewType = ConfigConstants.getCadPreviewType(); String cadPreviewType = ConfigConstants.getCadPreviewType();
String cacheName = fileAttribute.getCacheName(); String cacheName = fileAttribute.getCacheName();
String outFilePath = fileAttribute.getOutFilePath(); String outFilePath = fileAttribute.getOutFilePath();
// 查询转换状态
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(fileName);
if (status != null) {
if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) {
// 正在转换中,返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("message", status.getRealTimeMessage());
return WAITING_FILE_PREVIEW_PAGE;
} else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) {
// 超时状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
} else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) {
// 失败状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换");
}
}
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换 // 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName)
|| !ConfigConstants.isCacheEnabled()) {
// 检查是否已在转换中
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} }
String filePath = response.getContent(); String filePath = response.getContent();
boolean imageUrls = false;
if (StringUtils.hasText(outFilePath)) { if (StringUtils.hasText(outFilePath)) {
try { try {
imageUrls = cadtopdfservice.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute); // 启动异步转换,并添加回调处理
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("message", "文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to convert CAD file: {}", filePath, e); logger.error("Failed to start CAD conversion: {}", filePath, e);
}
if (!imageUrls) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员"); return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员");
} }
//是否保留CAD源文件
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
} }
} }
cacheName= WebUtils.encodeFileName(cacheName); // 如果已有缓存,直接渲染预览
return renderPreview(model, cacheName, outFilePath, officePreviewType, cadPreviewType, fileAttribute);
}
/**
* 启动异步转换,并在转换完成后处理后续操作
*/
private void startAsyncConversion(String filePath, String outFilePath,
String cacheName, FileAttribute fileAttribute) {
// 启动异步转换
CompletableFuture<Boolean> conversionFuture = cadtopdfservice.cadToPdfAsync(
filePath,
outFilePath,
ConfigConstants.getCadPreviewType(),
fileAttribute
);
// 添加转换完成后的回调
conversionFuture.whenCompleteAsync((success, throwable) -> {
if (success != null && success) {
try {
// 1. 是否保留CAD源文件只在转换成功后才删除
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
// 2. 加入缓存(只在转换成功后才添加)
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(cacheName,
fileHandlerService.getRelativePath(outFilePath));
}
} catch (Exception e) {
logger.error("CAD转换后续处理失败: {}", filePath, e);
}
} else {
// 转换失败,保留源文件供排查问题
logger.error("CAD转换失败保留源文件: {}", filePath);
if (throwable != null) {
logger.error("转换失败原因: ", throwable);
}
}
}, callbackExecutor);
}
/**
* 渲染预览页面
*/
private String renderPreview(Model model, String cacheName, String outFilePath,
String officePreviewType, String cadPreviewType,
FileAttribute fileAttribute) {
cacheName = WebUtils.encodeFileName(cacheName);
String baseUrl = BaseUrlFilter.getBaseUrl();
if ("tif".equalsIgnoreCase(cadPreviewType)) { if ("tif".equalsIgnoreCase(cadPreviewType)) {
model.addAttribute("currentUrl", cacheName); model.addAttribute("currentUrl", cacheName);
return TIFF_FILE_PREVIEW_PAGE; return TIFF_FILE_PREVIEW_PAGE;
@@ -85,10 +163,15 @@ public class CadFilePreviewImpl implements FilePreview {
model.addAttribute("currentUrl", cacheName); model.addAttribute("currentUrl", cacheName);
return SVG_FILE_PREVIEW_PAGE; return SVG_FILE_PREVIEW_PAGE;
} }
if (baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview); if (baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) ||
OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType,
cacheName, outFilePath, fileHandlerService,
OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
} }
model.addAttribute("pdfUrl", cacheName); model.addAttribute("pdfUrl", cacheName);
return PDF_FILE_PREVIEW_PAGE; return PDF_FILE_PREVIEW_PAGE;
} }
} }

View File

@@ -6,23 +6,19 @@ import cn.keking.model.FileType;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.service.Mediatomp4Service;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.io.IOException;
import java.util.Map; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit;
import java.util.concurrent.Future; import java.util.concurrent.TimeoutException;
/** /**
* @author : kl * @author : kl
@@ -36,14 +32,6 @@ public class MediaFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class); private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
private final FileHandlerService fileHandlerService; private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; private final OtherFilePreviewImpl otherFilePreview;
private static final String mp4 = "mp4";
// 添加线程池管理视频转换任务
private static final ExecutorService videoConversionExecutor =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 添加转换任务缓存,避免重复转换
private static final Map<String, Future<String>> conversionTasks = new HashMap<>();
public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) { public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService; this.fileHandlerService = fileHandlerService;
@@ -61,235 +49,189 @@ public class MediaFilePreviewImpl implements FilePreview {
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式 String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式
boolean mediaTypes = false; boolean mediaTypes = false;
for (String temp : mediaTypesConvert) { for (String temp : mediaTypesConvert) {
if (suffix.equals(temp)) { if (suffix.equalsIgnoreCase(temp)) {
mediaTypes = true; mediaTypes = true;
break; break;
} }
} }
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的 // 非HTTP协议或需要转换的文件
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存 if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName); // 检查缓存
if (response.isFailure()) { File outputFile = new File(outFilePath);
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); if (outputFile.exists() && !forceUpdatedCache && ConfigConstants.isCacheEnabled()) {
String relativePath = fileHandlerService.getRelativePath(outFilePath);
if (fileHandlerService.listConvertedFiles().containsKey(cacheName)) {
model.addAttribute("mediaUrl", relativePath);
logger.info("使用已缓存的视频文件: {}", cacheName);
return MEDIA_FILE_PREVIEW_PAGE;
} }
String filePath = response.getContent();
String convertedUrl = null;
try {
if (mediaTypes) {
// 检查是否已有正在进行的转换任务
Future<String> conversionTask = conversionTasks.get(cacheName);
if (conversionTask != null && !conversionTask.isDone()) {
// 等待现有转换任务完成
convertedUrl = conversionTask.get();
} else {
// 提交新的转换任务
conversionTask = videoConversionExecutor.submit(() -> {
return convertToMp4(filePath, outFilePath, fileAttribute);
});
conversionTasks.put(cacheName, conversionTask);
convertedUrl = conversionTask.get();
}
} else {
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
}
} catch (Exception e) {
logger.error("Failed to convert media file: {}", filePath, e);
// 清理失败的任务
conversionTasks.remove(cacheName);
}
if (convertedUrl == null) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
// 转换完成后清理任务缓存
conversionTasks.remove(cacheName);
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
} else {
model.addAttribute("mediaUrl", fileHandlerService.listConvertedFiles().get(cacheName));
} }
return MEDIA_FILE_PREVIEW_PAGE;
// 下载文件
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
try {
if (mediaTypes) {
// 检查文件大小限制
if (isFileSizeExceeded(filePath)) {
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制禁止转换");
}
// 使用改进的转换方法
String convertedPath = convertVideoWithImprovedTimeout(filePath, outFilePath, fileAttribute);
if (convertedPath != null) {
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(convertedPath));
// 缓存转换结果
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(convertedPath));
}
return MEDIA_FILE_PREVIEW_PAGE;
} else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换失败,请联系管理员");
}
} else {
// 不需要转换的文件
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
return MEDIA_FILE_PREVIEW_PAGE;
}
} catch (Exception e) {
logger.error("处理媒体文件失败: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频处理异常: " + getErrorMessage(e));
}
} }
if (type.equals(FileType.MEDIA)) { // 支持输出 只限默认格式
// HTTP协议的媒体文件直接播放
if (type.equals(FileType.MEDIA)) {
model.addAttribute("mediaUrl", url); model.addAttribute("mediaUrl", url);
return MEDIA_FILE_PREVIEW_PAGE; return MEDIA_FILE_PREVIEW_PAGE;
} }
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览"); return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览");
} }
/** /**
* 检查视频文件转换是否已开启,以及当前文件是否需要转换 * 检查文件大小是否超过限制
* */
* @return private boolean isFileSizeExceeded(String filePath) {
try {
File inputFile = new File(filePath);
if (inputFile.exists()) {
long fileSizeMB = inputFile.length() / (1024 * 1024);
int maxSizeMB = ConfigConstants.getMediaConvertMaxSize();
if (fileSizeMB > maxSizeMB) {
logger.warn("视频文件大小超过限制: {}MB > {}MB", fileSizeMB, maxSizeMB);
return true;
}
}
} catch (Exception e) {
logger.error("检查文件大小时出错: {}", filePath, e);
}
return false;
}
/**
* 改进的转换方法
*/
private String convertVideoWithImprovedTimeout(String filePath, String outFilePath,
FileAttribute fileAttribute) {
try {
// 检查文件是否存在
File outputFile = new File(outFilePath);
if (outputFile.exists() && !fileAttribute.forceUpdatedCache()) {
logger.info("输出文件已存在且非强制更新模式,跳过转换!");
return outFilePath;
}
// 使用改进的异步转换方法
CompletableFuture<Boolean> future =
Mediatomp4Service.convertToMp4Async(filePath, outFilePath, fileAttribute);
// 计算超时时间
File inputFile = new File(filePath);
long fileSizeMB = inputFile.length() / (1024 * 1024);
int timeoutSeconds = calculateTimeout(fileSizeMB);
try {
boolean result = future.get(timeoutSeconds, TimeUnit.SECONDS);
if (result) {
// 验证输出文件
File convertedFile = new File(outFilePath);
if (!convertedFile.exists() || convertedFile.length() == 0) {
throw new IOException("转换完成但输出文件无效");
}
return outFilePath;
} else {
throw new Exception("转换返回失败状态");
}
} catch (TimeoutException e) {
// 超时后尝试获取任务ID并取消
logger.error("视频转换超时: {}, 文件大小: {}MB, 超时: {}秒",
filePath, fileSizeMB, timeoutSeconds);
throw new RuntimeException("视频转换超时,文件可能过大");
}
} catch (Exception e) {
logger.error("视频转换异常: {}", filePath, e);
throw new RuntimeException("视频转换失败: " + getErrorMessage(e), e);
}
}
/**
* 计算超时时间 - 从配置文件读取
*/
public int calculateTimeout(long fileSizeMB) {
// 如果超时功能被禁用,返回一个非常大的值
if (!ConfigConstants.isMediaTimeoutEnabled()) {
return Integer.MAX_VALUE;
}
// 根据文件大小从配置文件读取超时时间
if (fileSizeMB < 10) return ConfigConstants.getMediaSmallFileTimeout(); // 小文件
if (fileSizeMB < 50) return ConfigConstants.getMediaMediumFileTimeout(); // 中等文件
if (fileSizeMB < 200) return ConfigConstants.getMediaLargeFileTimeout(); // 较大文件
if (fileSizeMB < 500) return ConfigConstants.getMediaXLFileTimeout(); // 大文件
if (fileSizeMB < 1024) return ConfigConstants.getMediaXXLFileTimeout(); // 超大文件
return ConfigConstants.getMediaXXXLFileTimeout(); // 极大文件
}
/**
* 检查是否需要转换
*/ */
private boolean checkNeedConvert(boolean mediaTypes) { private boolean checkNeedConvert(boolean mediaTypes) {
//1.检查开关是否开启 // 1.检查开关是否开启
if ("true".equals(ConfigConstants.getMediaConvertDisable())) { if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
return mediaTypes; return mediaTypes;
} }
return false; return false;
} }
private static String convertToMp4(String filePath, String outFilePath, FileAttribute fileAttribute) throws Exception {
FFmpegFrameGrabber frameGrabber = null;
FFmpegFrameRecorder recorder = null;
try {
File desFile = new File(outFilePath);
//判断一下防止重复转换
if (desFile.exists()) {
return outFilePath;
}
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
int index = outFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
String folder = outFilePath.substring(0, index);
File path = new File(folder);
//目录不存在 创建新的目录
if (!path.exists()) {
path.mkdirs();
}
}
frameGrabber = FFmpegFrameGrabber.createDefault(filePath);
frameGrabber.start();
// 优化:使用更快的编码预设
recorder = new FFmpegFrameRecorder(outFilePath,
frameGrabber.getImageWidth(),
frameGrabber.getImageHeight(),
frameGrabber.getAudioChannels());
// 设置快速编码参数
recorder.setFormat(mp4);
recorder.setFrameRate(frameGrabber.getFrameRate());
recorder.setSampleRate(frameGrabber.getSampleRate());
// 视频编码属性配置 - 使用快速编码预设
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setVideoOption("preset", "veryfast"); // 添加快速编码预设
recorder.setVideoOption("tune", "zerolatency"); // 降低延迟
recorder.setVideoBitrate(frameGrabber.getVideoBitrate());
recorder.setAspectRatio(frameGrabber.getAspectRatio());
// 音频编码设置
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
recorder.setAudioBitrate(frameGrabber.getAudioBitrate());
recorder.setAudioChannels(frameGrabber.getAudioChannels());
// 优化当源文件已经是h264/aac编码时尝试直接复制流
if (isCompatibleCodec(frameGrabber)) {
recorder.setVideoOption("c:v", "copy");
recorder.setAudioOption("c:a", "copy");
}
recorder.start();
// 批量处理帧,提高处理效率
Frame capturedFrame;
int batchSize = 100; // 批量处理的帧数
int frameCount = 0;
Frame[] frameBatch = new Frame[batchSize];
while (true) {
capturedFrame = frameGrabber.grabFrame();
if (capturedFrame == null) {
break;
}
frameBatch[frameCount % batchSize] = capturedFrame;
frameCount++;
// 批量记录帧
if (frameCount % batchSize == 0 || capturedFrame == null) {
for (int i = 0; i < Math.min(batchSize, frameCount); i++) {
if (frameBatch[i] != null) {
recorder.record(frameBatch[i]);
frameBatch[i] = null; // 释放引用
}
}
}
}
// 记录剩余的帧
for (int i = 0; i < frameBatch.length; i++) {
if (frameBatch[i] != null) {
recorder.record(frameBatch[i]);
}
}
logger.info("视频转码完成: {} -> {}", filePath, outFilePath);
return outFilePath;
} catch (Exception e) {
logger.error("Failed to convert video file to mp4: {}", filePath, e);
// 删除可能已创建的失败文件
try {
File failedFile = new File(outFilePath);
if (failedFile.exists()) {
failedFile.delete();
}
} catch (SecurityException ex) {
logger.warn("无法删除失败的转换文件: {}", outFilePath, ex);
}
throw e;
} finally {
// 确保资源被正确释放
if (recorder != null) {
try {
recorder.stop();
recorder.close();
} catch (Exception e) {
logger.warn("关闭recorder时发生异常", e);
}
}
if (frameGrabber != null) {
try {
frameGrabber.stop();
frameGrabber.close();
} catch (Exception e) {
logger.warn("关闭frameGrabber时发生异常", e);
}
}
// 强制垃圾回收释放FFmpeg相关资源
System.gc();
}
}
/** /**
* 检查源文件是否已经是兼容的编码格式H264/AAC * 获取友好的错误信息
*/ */
private static boolean isCompatibleCodec(FFmpegFrameGrabber grabber) { private String getErrorMessage(Exception e) {
try { if (e instanceof CancellationException) {
String videoCodec = grabber.getVideoCodecName(); return "转换被取消";
String audioCodec = grabber.getAudioCodecName(); } else if (e instanceof TimeoutException) {
return "转换超时";
boolean videoCompatible = videoCodec != null && } else if (e.getMessage() != null) {
(videoCodec.toLowerCase().contains("h264") || // 截取主要错误信息
videoCodec.toLowerCase().contains("h.264")); String msg = e.getMessage();
if (msg.length() > 100) {
boolean audioCompatible = audioCodec != null && msg = msg.substring(0, 100) + "...";
(audioCodec.toLowerCase().contains("aac") || }
audioCodec.toLowerCase().contains("mp3")); return msg;
return videoCompatible && audioCompatible;
} catch (Exception e) {
logger.debug("无法获取编解码器信息", e);
return false;
} }
} return "未知错误";
/**
* 清理所有转换任务(应用关闭时调用)
*/
public static void shutdown() {
if (!CollectionUtils.isEmpty(conversionTasks)) {
conversionTasks.values().forEach(task -> task.cancel(true));
conversionTasks.clear();
}
videoConversionExecutor.shutdownNow();
} }
} }

View File

@@ -5,7 +5,7 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.service.TifToService; import cn.keking.service.TifToPdfService;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import cn.keking.utils.KkFileUtils; import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils; import cn.keking.utils.WebUtils;
@@ -16,7 +16,6 @@ import java.util.List;
/** /**
* tiff 图片文件处理 * tiff 图片文件处理
*
* @author kl (http://kailing.pub) * @author kl (http://kailing.pub)
* @since 2021/2/8 * @since 2021/2/8
*/ */
@@ -25,8 +24,8 @@ public class TiffFilePreviewImpl implements FilePreview {
private final FileHandlerService fileHandlerService; private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; private final OtherFilePreviewImpl otherFilePreview;
private final TifToService tiftoservice; private final TifToPdfService tiftoservice;
public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview,TifToService tiftoservice) { public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview,TifToPdfService tiftoservice) {
this.fileHandlerService = fileHandlerService; this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.tiftoservice = tiftoservice; this.tiftoservice = tiftoservice;
@@ -47,7 +46,7 @@ public class TiffFilePreviewImpl implements FilePreview {
String filePath = response.getContent(); String filePath = response.getContent();
if ("pdf".equalsIgnoreCase(tifPreviewType)) { if ("pdf".equalsIgnoreCase(tifPreviewType)) {
try { try {
tiftoservice.convertTif2Pdf(filePath, outFilePath); tiftoservice.convertTif2Pdf(filePath, outFilePath,forceUpdatedCache);
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) { if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) {
model.addAttribute("imgUrls", url); model.addAttribute("imgUrls", url);
@@ -59,7 +58,7 @@ public class TiffFilePreviewImpl implements FilePreview {
} }
//是否保留TIFF源文件 //是否保留TIFF源文件
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) { if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
// KkFileUtils.deleteFileByPath(filePath); KkFileUtils.deleteFileByPath(filePath);
} }
if (ConfigConstants.isCacheEnabled()) { if (ConfigConstants.isCacheEnabled()) {
// 加入缓存 // 加入缓存

View File

@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
import io.mola.galimatias.GalimatiasParseException; import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -41,7 +42,7 @@ public class DownloadUtils {
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding"; private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final String URL_PARAM_FTP_PORT = "ftp.control.port"; private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private static final RestTemplate restTemplate = new RestTemplate(); private static final RestTemplate restTemplate = new RestTemplate();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
@@ -51,12 +52,12 @@ public class DownloadUtils {
* @return 本地文件绝对路径 * @return 本地文件绝对路径
*/ */
public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) { public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) {
// 忽略ssl证书
String urlStr = null; String urlStr = null;
try { try {
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20"); urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
} catch (Exception e) { } catch (Exception e) {
logger.error("忽略SSL证书异常:", e); logger.error("处理URL异常:", e);
} }
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", ""); ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
String realPath = getRelFilePath(fileName, fileAttribute); String realPath = getRelFilePath(fileName, fileAttribute);
@@ -90,7 +91,10 @@ public class DownloadUtils {
if (!fileAttribute.getSkipDownLoad()) { if (!fileAttribute.getSkipDownLoad()) {
if (isHttpUrl(url)) { if (isHttpUrl(url)) {
File realFile = new File(realPath); File realFile = new File(realPath);
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
// 创建配置好的HttpClient
CloseableHttpClient httpClient = createConfiguredHttpClient();
factory.setHttpClient(httpClient); factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory); restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> { RequestCallback requestCallback = request -> {
@@ -111,10 +115,25 @@ public class DownloadUtils {
return null; return null;
}); });
} catch (Exception e) { } catch (Exception e) {
// 如果是SSL证书错误给出建议
if (e.getMessage() != null &&
(e.getMessage().contains("SSL") ||
e.getMessage().contains("证书") ||
e.getMessage().contains("certificate")) &&
!ConfigConstants.isIgnoreSSL()) {
logger.warn("SSL证书验证失败建议启用SSL忽略功能或检查证书");
}
response.setCode(1); response.setCode(1);
response.setContent(null); response.setContent(null);
response.setMsg("下载失败:" + e); response.setMsg("下载失败:" + e);
return response; return response;
} finally {
// 确保HttpClient被关闭
try {
httpClient.close();
} catch (IOException e) {
logger.warn("关闭HttpClient失败", e);
}
} }
} else if (isFtpUrl(url)) { } else if (isFtpUrl(url)) {
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME); String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
@@ -147,6 +166,38 @@ public class DownloadUtils {
} }
} }
/**
* 创建根据配置定制的HttpClient
*/
private static CloseableHttpClient createConfiguredHttpClient() throws Exception {
org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder = HttpClients.custom();
// 配置SSL
if (ConfigConstants.isIgnoreSSL()) {
logger.debug("创建忽略SSL验证的HttpClient");
// 如果SslUtils有创建builder的方法就更好了这里假设我们直接使用SslUtils
// 或者我们可以创建一个新的方法来返回配置了忽略SSL的builder
return createHttpClientWithConfig();
} else {
logger.debug("创建标准HttpClient");
}
// 配置重定向
if (!ConfigConstants.isEnableRedirect()) {
logger.debug("禁用HttpClient重定向");
builder.disableRedirectHandling();
}
return builder.build();
}
/**
* 创建配置了忽略SSL的HttpClient
*/
private static CloseableHttpClient createHttpClientWithConfig() throws Exception {
return SslUtils.createHttpClientIgnoreSsl();
}
// 处理file协议的文件下载 // 处理file协议的文件下载
private static void handleFileProtocol(URL url, String targetPath) throws IOException { private static void handleFileProtocol(URL url, String targetPath) throws IOException {
@@ -229,4 +280,4 @@ public class DownloadUtils {
return realPath; return realPath;
} }
} }

View File

@@ -0,0 +1,257 @@
package cn.keking.utils;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 文件转换状态管理器(增强版)
* 支持实时状态跟踪和状态锁定机制
*/
public class FileConvertStatusManager {
// 存储转换状态key=文件名value=转换状态对象
private static final ConcurrentMap<String, ConvertState> STATUS_MAP = new ConcurrentHashMap<>();
// 记录最终状态(超时或异常),防止重复转换
private static final ConcurrentMap<String, Status> FINAL_STATUS_MAP = new ConcurrentHashMap<>();
/**
* 开始转换,创建初始状态
* @param fileName 文件名
*/
public static void startConvert(String fileName) {
STATUS_MAP.putIfAbsent(fileName, new ConvertState(Status.CONVERTING, "等待转换", 0));
// 清除可能存在的最终状态,因为要开始新的转换
FINAL_STATUS_MAP.remove(fileName);
}
/**
* 更新转换进度
* @param fileName 文件名
* @param message 状态消息
* @param progress 进度百分比(0-100)
*/
public static void updateProgress(String fileName, String message, int progress) {
STATUS_MAP.computeIfPresent(fileName, (key, state) -> {
state.update(message, progress);
logger.debug("更新转换进度: {} -> {} ({}%)", fileName, message, progress);
return state;
});
}
/**
* 标记转换超时 - 记录为最终状态
* @param fileName 文件名
*/
public static void markTimeout(String fileName) {
STATUS_MAP.put(fileName, new ConvertState(Status.TIMEOUT, "转换超时,请重试", 0));
// 记录为最终状态
FINAL_STATUS_MAP.put(fileName, Status.TIMEOUT);
logger.warn("标记文件转换超时: {}", fileName);
}
/**
* 标记转换失败 - 记录为最终状态
* @param fileName 文件名
* @param errorMessage 错误信息
*/
public static void markError(String fileName, String errorMessage) {
STATUS_MAP.put(fileName, new ConvertState(Status.FAILED, errorMessage, 0));
// 记录为最终状态
FINAL_STATUS_MAP.put(fileName, Status.FAILED);
logger.warn("标记文件转换失败: {}, 错误: {}", fileName, errorMessage);
}
/**
* 查询文件转换状态
* @param fileName 文件名
* @return 转换状态对象如果不存在返回null
*/
public static ConvertStatus getConvertStatus(String fileName) {
// 先检查是否有最终状态
Status finalStatus = FINAL_STATUS_MAP.get(fileName);
if ((finalStatus == Status.TIMEOUT || finalStatus == Status.FAILED)) {
ConvertState state = STATUS_MAP.get(fileName);
if (state == null) {
// 如果STATUS_MAP中没有创建一个最终状态
if (finalStatus == Status.TIMEOUT) {
return new ConvertStatus(Status.TIMEOUT, "转换超时,请重试", 0, 0);
} else {
return new ConvertStatus(Status.FAILED, "转换失败", 0, 0);
}
}
// 返回最终状态
return new ConvertStatus(state.status, state.message, state.progress, 0);
}
ConvertState state = STATUS_MAP.get(fileName);
if (state == null) {
return null;
}
// 如果是转换中状态,计算已等待时间
long waitingSeconds = 0;
if (state.status == Status.CONVERTING) {
waitingSeconds = (System.currentTimeMillis() - state.startTime) / 1000;
}
return new ConvertStatus(
state.status,
state.message,
state.progress,
waitingSeconds
);
}
/**
* 转换成功
* @param fileName 文件名
*/
public static void convertSuccess(String fileName) {
STATUS_MAP.remove(fileName);
// 清除最终状态,允许重新转换
FINAL_STATUS_MAP.remove(fileName);
}
/**
* 清理状态(强制重置,允许重新转换)
* @param fileName 文件名
* @return true: 清理成功; false: 清理失败
*/
public static boolean clearStatus(String fileName) {
boolean removed1 = STATUS_MAP.remove(fileName) != null;
boolean removed2 = FINAL_STATUS_MAP.remove(fileName) != null;
logger.info("清理文件状态: {}, STATUS_MAP: {}, FINAL_STATUS_MAP: {}",
fileName, removed1, removed2);
return removed1 || removed2;
}
/**
* 清理过期状态(长时间未清理的状态)
* @param expireHours 过期时间(小时)
* @return 清理的数量
*/
public static int cleanupExpiredStatus(int expireHours) {
long expireMillis = expireHours * 3600 * 1000L;
long currentTime = System.currentTimeMillis();
// 清理STATUS_MAP中的过期状态
int count1 = (int) STATUS_MAP.entrySet().stream()
.filter(entry -> {
ConvertState state = entry.getValue();
if (state.status == Status.CONVERTING) {
return false; // 转换中的不清理
}
long elapsed = currentTime - state.startTime;
return elapsed > expireMillis;
})
.count();
// 清理FINAL_STATUS_MAP中的过期状态
// 注意FINAL_STATUS_MAP没有时间戳无法基于时间清理
// 如果需要清理,可以设置一个独立的过期机制
logger.info("清理了 {} 个过期的转换状态", count1);
return count1;
}
/**
* 转换状态枚举
*/
public enum Status {
CONVERTING("转换中"),
FAILED("转换失败"),
TIMEOUT("转换超时"),
QUEUED("排队中");
private final String description;
Status(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 内部状态存储类
*/
private static class ConvertState {
private final Status status;
private String message;
private int progress; // 0-100
private final long startTime;
public ConvertState(Status status, String message, int progress) {
this.status = status;
this.message = message;
this.progress = Math.max(0, Math.min(100, progress));
this.startTime = System.currentTimeMillis();
}
public void update(String message, int progress) {
this.message = message;
this.progress = Math.max(0, Math.min(100, progress));
}
}
/**
* 对外暴露的转换状态封装类
*/
public static class ConvertStatus {
private final Status status;
private final String message;
private final int progress;
private final long waitingSeconds;
private final long timestamp;
public ConvertStatus(Status status, String message, int progress, long waitingSeconds) {
this.status = status;
this.message = message;
this.progress = progress;
this.waitingSeconds = waitingSeconds;
this.timestamp = System.currentTimeMillis();
}
// 获取实时状态信息
public String getRealTimeMessage() {
if (status == Status.CONVERTING) {
if (progress > 0) {
return String.format("%s: %s (进度: %d%%,已等待 %d 秒)",
status.getDescription(), message, progress, waitingSeconds);
} else {
return String.format("%s: %s已等待 %d 秒",
status.getDescription(), message, waitingSeconds);
}
}
return message;
}
// Getters
public Status getStatus() { return status; }
public String getMessage() { return message; }
public int getProgress() { return progress; }
public long getTimestamp() { return timestamp; }
@Override
public String toString() {
return "ConvertStatus{" +
"status=" + status +
", message='" + message + '\'' +
", progress=" + progress +
", waitingSeconds=" + waitingSeconds +
", timestamp=" + timestamp +
'}';
}
}
// 日志记录器
private static final org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(FileConvertStatusManager.class);
}

View File

@@ -1,8 +1,8 @@
package cn.keking.utils; package cn.keking.utils;
import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
@@ -11,7 +11,8 @@ import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout; import org.apache.hc.core5.util.Timeout;
import javax.net.ssl.*; import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
/** /**
@@ -23,40 +24,58 @@ public class SslUtils {
* 创建忽略SSL验证的HttpClient适用于HttpClient 5.6 * 创建忽略SSL验证的HttpClient适用于HttpClient 5.6
*/ */
public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception { public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
// 创建自定义的SSL上下文 return configureHttpClientBuilder(HttpClients.custom(), true, true).build();
SSLContext sslContext = createIgnoreVerifySSL(); }
// 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂 /**
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy( * 配置HttpClientBuilder支持SSL和重定向配置
sslContext, NoopHostnameVerifier.INSTANCE); * @param builder HttpClientBuilder
* @param ignoreSSL 是否忽略SSL验证
* @param enableRedirect 是否启用重定向
* @return 配置好的HttpClientBuilder
*/
public static HttpClientBuilder configureHttpClientBuilder(HttpClientBuilder builder,
boolean ignoreSSL,
boolean enableRedirect) throws Exception {
// 配置SSL
if (ignoreSSL) {
// 创建自定义的SSL上下文
SSLContext sslContext = createIgnoreVerifySSL();
// 使用新的PoolingHttpClientConnectionManagerBuilder构建连接管理器 // 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
// 使用连接管理器构建器 DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() sslContext, NoopHostnameVerifier.INSTANCE);
.setTlsSocketStrategy(tlsStrategy)
.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.ofSeconds(10))
.build())
.build();
// 配置连接池参数 // 使用连接管理器构建器
connectionManager.setMaxTotal(200); PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
connectionManager.setDefaultMaxPerRoute(20); .setTlsSocketStrategy(tlsStrategy)
.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.ofSeconds(10))
.build())
.build();
// 配置连接池参数
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
builder.setConnectionManager(connectionManager);
}
// 配置请求参数 // 配置请求参数
RequestConfig requestConfig = RequestConfig.custom() RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(10)) .setConnectionRequestTimeout(Timeout.ofSeconds(10))
.setResponseTimeout(Timeout.ofSeconds(72)) .setResponseTimeout(Timeout.ofSeconds(72))
.setConnectionRequestTimeout(Timeout.ofSeconds(2)) .setConnectTimeout(Timeout.ofSeconds(2))
.setRedirectsEnabled(true) .setRedirectsEnabled(enableRedirect)
.setMaxRedirects(5) .setMaxRedirects(5)
.build(); .build();
builder.setDefaultRequestConfig(requestConfig);
return HttpClients.custom() if (!enableRedirect) {
.setConnectionManager(connectionManager) builder.disableRedirectHandling();
.setDefaultRequestConfig(requestConfig) }
.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
.build(); return builder;
} }
/** /**
@@ -84,7 +103,7 @@ public class SslUtils {
} }
}; };
sc.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom()); sc.init(null, new javax.net.ssl.TrustManager[]{trustManager}, new java.security.SecureRandom());
return sc; return sc;
} }
} }

View File

@@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
import fr.opensagres.xdocreport.core.io.IOUtils; import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -63,8 +64,6 @@ public class OnlinePreviewController {
private final CacheService cacheService; private final CacheService cacheService;
private final FileHandlerService fileHandlerService; private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; private final OtherFilePreviewImpl otherFilePreview;
private static final RestTemplate restTemplate = new RestTemplate();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) { public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
@@ -181,12 +180,18 @@ public class OnlinePreviewController {
InputStream inputStream = null; InputStream inputStream = null;
logger.info("读取跨域pdf文件url{}", urlPath); logger.info("读取跨域pdf文件url{}", urlPath);
if (!isFtpUrl(url)) { if (!isFtpUrl(url)) {
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl(); // 根据配置创建HttpClient
CloseableHttpClient httpClient = createConfiguredHttpClient();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient); factory.setHttpClient(httpClient);
// restTemplate.setRequestFactory(factory);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(factory); restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> { RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
WebUtils.applyBasicAuthHeaders(request.getHeaders(), fileAttribute);
String proxyAuthorization = fileAttribute.getKkProxyAuthorization(); String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){ if(StringUtils.hasText(proxyAuthorization)){
Map<String, String> proxyAuthorizationMap = mapper.readValue( Map<String, String> proxyAuthorizationMap = mapper.readValue(
@@ -202,9 +207,24 @@ public class OnlinePreviewController {
return null; return null;
}); });
} catch (Exception e) { } catch (Exception e) {
System.out.println(e); // 如果是SSL证书错误给出建议
if (e.getMessage() != null &&
(e.getMessage().contains("SSL") ||
e.getMessage().contains("证书") ||
e.getMessage().contains("certificate")) &&
!ConfigConstants.isIgnoreSSL()) {
logger.warn("SSL证书验证失败建议启用SSL忽略功能或检查证书");
}
logger.error("获取跨域文件失败", e);
} finally {
// 确保HttpClient被关闭
try {
httpClient.close();
} catch (IOException e) {
logger.warn("关闭HttpClient失败", e);
}
} }
}else{ } else {
try { try {
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1); String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
String contentType = WebUtils.getContentTypeByFilename(filename); String contentType = WebUtils.getContentTypeByFilename(filename);
@@ -225,6 +245,20 @@ public class OnlinePreviewController {
} }
} }
/**
* 创建根据配置定制的HttpClient
*/
private CloseableHttpClient createConfiguredHttpClient() throws Exception {
org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder = HttpClients.custom();
// 配置SSL和重定向
return SslUtils.configureHttpClientBuilder(
builder,
ConfigConstants.isIgnoreSSL(),
ConfigConstants.isEnableRedirect()
).build();
}
/** /**
* 通过api接口入队 * 通过api接口入队
* *
@@ -260,4 +294,4 @@ public class OnlinePreviewController {
cacheService.addQueueTask(fileUrls); cacheService.addQueueTask(fileUrls);
return "success"; return "success";
} }
} }

View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${fileName}文件转换中</title>
<style>
:root{--primary:#3498db;--primary-dark:#2980b9;--secondary:#2c3e50;--light:#ecf0f1;--warning:#f39c12;--gray:#95a5a6;--shadow:0 10px 30px rgba(0,0,0,0.1);--radius:12px;--transition:all 0.3s ease;}*{margin:0;padding:0;box-sizing:border-box;font-family:'Segoe UI','Microsoft YaHei',sans-serif;}
body{background:linear-gradient(135deg,#f5f7fa 0%,#c3cfe2 100%);min-height:100vh;display:flex;justify-content:center;align-items:center;padding:20px;color:var(--secondary);}.container{max-width:600px;width:100%;background-color:white;border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;padding:40px;text-align:center;animation:fadeIn 0.8s ease-out;}@keyframes fadeIn{from{opacity:0;transform:translateY(20px);}
to{opacity:1;transform:translateY(0);}}.header{margin-bottom:30px;}.header h1{color:var(--secondary);font-size:28px;margin-bottom:10px;}.subtitle{color:var(--gray);font-size:16px;}.spinner{border:8px solid rgba(52,152,219,0.1);border-top:8px solid var(--primary);border-radius:50%;width:80px;height:80px;animation:spin 1.5s linear infinite;margin:0 auto 30px;}@keyframes spin{0%{transform:rotate(0deg);}
100%{transform:rotate(360deg);}}.file-info{background-color:#f8f9fa;border-radius:var(--radius);padding:20px;margin-bottom:30px;text-align:left;border-left:4px solid var(--primary);}.file-info h3{margin-bottom:10px;color:var(--secondary);}.file-name{font-weight:bold;color:var(--primary);word-break:break-all;}.message{font-size:18px;margin-bottom:25px;color:var(--secondary);padding:15px;background-color:#f8f9fa;border-radius:var(--radius);line-height:1.5;}.countdown-section{margin:30px 0;padding:20px;background:linear-gradient(to right,#f8f9fa,#e9ecef);border-radius:var(--radius);}.countdown-text{font-size:16px;margin-bottom:10px;}#countdown{font-weight:bold;font-size:28px;color:var(--primary);display:inline-block;min-width:40px;}.controls{display:flex;justify-content:center;gap:20px;margin-top:30px;flex-wrap:wrap;}.btn{padding:14px 28px;border:none;border-radius:50px;font-weight:600;font-size:16px;cursor:pointer;transition:var(--transition);min-width:180px;}.btn-primary{background-color:var(--primary);color:white;}.btn-primary:hover{background-color:var(--primary-dark);}.btn-secondary{background-color:var(--light);color:var(--secondary);border:2px solid var(--gray);}.btn-secondary:hover{background-color:#e0e0e0;}.footer{margin-top:40px;color:var(--gray);font-size:14px;padding-top:20px;border-top:1px solid#eee;}.tips{background-color:#fff8e1;border-radius:var(--radius);padding:15px;margin-top:25px;font-size:14px;text-align:left;border-left:4px solid var(--warning);}.tips h4{margin-bottom:8px;color:var(--secondary);}.tips ul{padding-left:20px;margin-bottom:0;}.tips li{margin-bottom:5px;}@media(max-width:576px){.container{padding:25px 20px;}.header h1{font-size:24px;}.btn{min-width:100%;}.controls{flex-direction:column;}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>文件转换中</h1>
<p class="subtitle">请稍等,我们正在处理您的文件</p>
</div>
<div class="spinner"></div>
<div class="file-info">
<h3>正在处理的文件</h3>
<p class="file-name" id="fileName">${fileName}</p>
</div>
<div class="message" id="message">
${message}...
</div>
<div class="countdown-section">
<p class="countdown-text">页面将在<span id="countdown">5</span>秒后自动刷新</p>
</div>
<div class="controls">
<button class="btn btn-primary" id="refreshBtn">立即刷新</button>
</div>
<div class="tips">
<h4>提示</h4>
<ul>
<li>文件转换时间取决于文件大小和服务器负载</li>
<li>转换完成后,页面将自动跳转到预览页面</li>
<li>您也可以点击&quot;立即刷新&quot;按钮手动检查转换状态</li>
</ul>
</div>
<div class="footer">
<p>预计剩余时间:<span id="estimatedTime">约 1 分钟</span></p>
<p style="margin-top: 5px;">如有问题,请联系技术支持</p>
</div>
</div><script>
let countdown = 5;
let countdownInterval;
// 删除forceUpdatedCache参数并更新URL
function cleanForceUpdateParam() {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
if (params.has('forceUpdatedCache')) {
params.delete('forceUpdatedCache');
// 构建新的URL
const newSearch = params.toString();
const newUrl = url.origin + url.pathname + (newSearch ? '?' + newSearch : '');
// 使用history.replaceState更新URL而不刷新页面
window.history.replaceState({}, document.title, newUrl);
console.log('已移除forceUpdatedCache参数当前URL:', newUrl);
}
}
function startCountdown() {
const countdownElement = document.getElementById('countdown');
countdownInterval = setInterval(() => {
if (countdown > 0) {
countdown--;
countdownElement.textContent = countdown;
} else {
clearInterval(countdownInterval);
window.location.reload();
}
}, 1000);
}
document.getElementById('refreshBtn').addEventListener('click', function() {
window.location.reload();
});
// 页面加载后执行
window.addEventListener('load', function() {
// 先清理URL参数
cleanForceUpdateParam();
// 然后开始倒计时
startCountdown();
});
</script>
</body>
</html>