diff --git a/pom.xml b/pom.xml
index 1841c289..f7b4c4df 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,6 @@
3.0.6
- 5.5.13.4
1.4.0
@@ -51,7 +50,6 @@
1.4.2
- 5.6
4.5.16
3.12.0
diff --git a/server/pom.xml b/server/pom.xml
index 29f4c7b3..c28ff099 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -23,15 +23,20 @@
-
-
- aspose-maven-repository
- https://repository.aspose.com/repo
-
- false
-
-
-
+
+
+
+
+ aspose-maven-repository
+ https://repository.aspose.com/repo
+
+ true
+
+
+ false
+
+
+
@@ -115,11 +120,6 @@
pdfbox-tools
${pdfbox.version}
-
- com.itextpdf
- itextpdf
- ${itextpdf.version}
-
@@ -303,18 +303,6 @@
spring-boot-starter-test
test
-
- commons-httpclient
- commons-httpclient
- ${httpclient.version}
- test
-
-
- commons-logging
- commons-logging
-
-
-
diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties
index a4161f48..1958b96c 100644
--- a/server/src/main/config/application.properties
+++ b/server/src/main/config/application.properties
@@ -1,18 +1,12 @@
-#######################################不可动态配置,需要重启生效#######################################
+####################################### 一、服务器基础配置(不可动态配置,需重启生效)#######################################
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
-#启用GZIP压缩功能
server.compression.enabled = true
-#允许压缩的响应缓冲区最小字节数,默认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
-# 文件上传限制前端
-spring.servlet.multipart.max-file-size=500MB
-#文件上传限制
-spring.servlet.multipart.max-request-size=500MB
-## Freemarker 配置
+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.freemarker.template-loader-path = classpath:/web/
spring.freemarker.cache = false
spring.freemarker.charset = UTF-8
@@ -22,226 +16,119 @@ spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.request-context-attribute = request
spring.freemarker.suffix = .ftl
-# Spring Boot Actuator 健康检查配置
-# 开启健康检查端点
-management.endpoints.web.exposure.include=health,info,metrics
-# 显示详细的健康检查信息(生产环境建议设置为when-authorized)
-management.endpoint.health.show-details=always
-# 启用健康检查组件
-management.health.defaults.enabled=true
+management.endpoints.web.exposure.include = health,info,metrics
+management.endpoint.health.show-details = always
+management.health.defaults.enabled = true
-
-# office设置
-#openoffice或LibreOffice home路径
-#office.home = C:\\Program Files (x86)\\OpenOffice 4
+####################################### 二、Office文档处理配置(部分支持动态配置)#######################################
office.home = ${KK_OFFICE_HOME:default}
-## office转换服务的端口,默认开启两个进程
office.plugin.server.ports = 2001,2002
-## office 转换服务 task 超时时间,默认五分钟
office.plugin.task.timeout = 5m
-#此属性设置office进程在重新启动之前可以执行的最大任务数。0表示无限数量的任务(永远不会重新启动)
office.plugin.task.maxtasksperprocess = 200
-#此属性设置处理任务所允许的最长时间。如果任务的处理时间长于此超时,则此任务将中止,并处理下一个任务。
office.plugin.task.taskexecutiontimeout = 5m
-#生成限制 默认不限制 使用方法 (1-5)
office.pagerange = ${KK_OFFICE_PAGERANGE:false}
-#生成水印 默认不启用 使用方法 (kkFileView)
-office.watermark = ${KK_OFFICE_WATERMARK:false}
-#OFFICE JPEG图片压缩
+office.watermark = ${KK_OFFICE_WATERMARK:false}
office.quality = ${KK_OFFICE_QUALITY:80}
-#图像分辨率限制
office.maximageresolution = ${KK_OFFICE_MAXIMAGERESOLUTION:150}
-#导出书签
office.exportbookmarks = ${KK_OFFICE_EXPORTBOOKMARKS:true}
-#批注作为PDF的注释
office.exportnotes = ${KK_OFFICE_EXPORTNOTES:true}
-#加密文档 生成的PDF文档 添加密码(密码为加密文档的密码)
office.documentopenpasswords = ${KK_OFFICE_DOCUMENTOPENPASSWORD:true}
-#xlsx格式前端解析
office.type.web = ${KK_OFFICE_TYPE_WEB:web}
-
-# 其他核心设置
-#预览生成资源路径(默认为打包根路径下的file目录下)
-#file.dir = D:\\kkFileview\\
+####################################### 三、文件存储与缓存配置 #######################################
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}
-#是否启用缓存
cache.enabled = ${KK_CACHE_ENABLED:true}
-#缓存实现类型,不配默认为内嵌RocksDB(type = default)实现,可配置为redis(type = redis)实现(需要配置spring.redisson.address等参数)和 JDK 内置对象实现(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,...
+cache.type = ${KK_CACHE_TYPE:jdk}
spring.redisson.mode = single
spring.redisson.address = ${KK_SPRING_REDISSON_ADDRESS:redis://127.0.0.1:6379}
spring.redisson.password = ${KK_SPRING_REDISSON_PASSWORD:}
-#redis 设置库
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 = true时才有用,cron表达式,基于Quartz cron
cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
-#######################################可在运行时动态配置#######################################
-#提供预览服务的地址,默认从请求url读,如果使用nginx等反向代理,需要手动设置
-#base.url = https://file.keking.cn
+
+####################################### 四、安全与访问控制配置(支持动态配置)#######################################
base.url = ${KK_BASE_URL:default}
-
-# ========== 安全配置(重要)==========
-# 信任站点白名单配置,多个用','隔开
-# ⚠️ 安全提示:为防止SSRF攻击,强烈建议配置信任主机白名单
-# ⚠️ 如果不配置,系统将默认拒绝所有外部文件预览请求
-#
-# 配置示例:
-# trust.host = kkview.cn,yourdomain.com,cdn.example.com
-#
-# 如果需要允许所有域名(不推荐,仅用于测试环境),请设置为:
-# trust.host = *
-#
-# 当前配置:
trust.host = *
-
-# 不信任站点黑名单配置,多个用','隔开
-# 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件
-# 建议配置:禁止访问内网地址和本地地址
-# 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-8,Windows一般为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}
-#禁止上传类型
+not.trust.host = ${KK_NOT_TRUST_HOST:default}
prohibit = ${KK_PROHIBIT:exe,dll,dat}
-#启用验证码删除文件 默认关闭
-delete.captcha= ${KK_DELETE_CAPTCHA:false}
-#删除密码
-delete.password = ${KK_DELETE_PASSWORD:123456}
-#删除 转换后OFFICE、CAD、TIFF、压缩包源文件 默认开启 节约磁盘空间
-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}
-
-#Cad类型设置
-#Cad类型图片浏览模式:tif(利用前端js插件浏览);svg(转换为svg显示);pdf(转换为pdf后显示,便于打印)
cad.preview.type = ${KK_CAD_PREVIEW_TYPE:svg}
-#Cad转换超时设置
-cad.timeout =${KK_CAD_TIMEOUT:90}
-#Cad转换线程设置
-cad.thread =${KK_CAD_THREAD:5}
+office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
+office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
+pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
+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
-kk.Getcorsfile=true
-kk.addTask=true
-kk.Key=false
-# 启用AES 接入方法 ase秘钥 必须16位 (接入秘钥必须相同) false (为不启用)
-ase.key= 1234567890123456
+####################################### 六、性能与资源管理配置(支持动态配置)#######################################
+pdf.max.threads = 10
+cad.thread = ${KK_CAD_THREAD:5}
+tif.thread = 5
+media.timeout.enabled = true
+media.small.file.timeout = 30
+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 (域名+用户名+密码 多个接入用,分割)
-basic.name=10.99.1.2:aaa:bbb
+####################################### 七、FTP文件访问配置(支持动态配置)#######################################
+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
\ No newline at end of file
diff --git a/server/src/main/config/application详细.properties b/server/src/main/config/application详细.properties
new file mode 100644
index 00000000..e655f7de
--- /dev/null
+++ b/server/src/main/config/application详细.properties
@@ -0,0 +1,544 @@
+####################################### 一、服务器基础配置(不可动态配置,需重启生效)#######################################
+
+####################### 1.1 服务器网络配置 #######################
+# 服务器端口号,默认8012,可通过环境变量 KK_SERVER_PORT 覆盖
+# 修改端口后需重启服务生效
+server.port = ${KK_SERVER_PORT:8012}
+
+# 服务器上下文路径,默认为根路径,可通过环境变量 KK_CONTEXT_PATH 覆盖
+# 示例:设置为 /preview 后,访问地址为 http://localhost:8012/preview
+server.servlet.context-path = ${KK_CONTEXT_PATH:/}
+
+# 服务器请求和响应编码字符集,设置为UTF-8以支持中文和国际化
+server.servlet.encoding.charset = utf-8
+
+####################### 1.2 服务器性能优化配置 #######################
+# 启用GZIP压缩功能,减少网络传输数据量,提升页面加载速度
+# 建议在生产环境中开启以节省带宽
+server.compression.enabled = true
+
+# 允许压缩的响应缓冲区最小字节数,默认为2048字节
+# 小于此值的响应不压缩,避免小文件压缩反而增加开销
+server.compression.min-response-size = 2048
+
+# 支持GZIP压缩的MIME类型列表,包括常见的前端资源格式
+# 可根据需要添加或删除特定的MIME类型
+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
+
+####################### 1.3 文件上传限制配置 #######################
+# 单个文件上传最大限制,默认为500MB
+# 根据业务需求和服务器内存大小调整
+spring.servlet.multipart.max-file-size = 500MB
+
+# 整个请求体(含多个文件)最大限制,默认为500MB
+# 设置时应大于等于 max-file-size
+spring.servlet.multipart.max-request-size = 500MB
+
+####################### 1.4 FreeMarker模板引擎配置 #######################
+# FreeMarker模板加载路径,默认为classpath下的web目录
+# 模板文件应放置在此目录下
+spring.freemarker.template-loader-path = classpath:/web/
+
+# 是否启用模板缓存,开发环境建议设置为false,生产环境建议设置为true
+# false: 每次请求都重新加载模板,便于调试
+# true: 缓存模板,提升性能
+spring.freemarker.cache = false
+
+# 模板文件编码,设置为UTF-8以支持中文和国际化字符
+spring.freemarker.charset = UTF-8
+
+# 检查模板位置是否存在,启动时验证模板路径
+spring.freemarker.check-template-location = true
+
+# 模板内容类型,设置为HTML格式
+spring.freemarker.content-type = text/html
+
+# 将HTTP请求属性暴露给模板,便于模板访问请求参数、头部等信息
+spring.freemarker.expose-request-attributes = true
+
+# 将HTTP会话属性暴露给模板,便于模板访问会话数据
+spring.freemarker.expose-session-attributes = true
+
+# 设置模板中访问请求上下文的变量名
+# 在模板中使用 ${request} 访问请求对象
+spring.freemarker.request-context-attribute = request
+
+# 模板文件后缀,默认为.ftl
+# 模板文件应命名为 *.ftl
+spring.freemarker.suffix = .ftl
+
+####################### 1.5 Spring Boot健康监控配置 #######################
+# 开启的健康检查端点,包括health、info、metrics
+# health: 应用健康状态
+# info: 应用基本信息
+# metrics: 应用性能指标
+management.endpoints.web.exposure.include = health,info,metrics
+
+# 健康检查端点显示详细信息的策略,默认为always(总是显示)
+# 生产环境建议设置为 when-authorized 或 never,避免敏感信息泄露
+management.endpoint.health.show-details = always
+
+# 启用默认的健康检查组件,监控应用核心组件状态
+management.health.defaults.enabled = true
+
+
+
+####################################### 二、Office文档处理配置(部分支持动态配置)#######################################
+
+####################### 2.1 Office组件安装路径配置 #######################
+# OpenOffice或LibreOffice安装路径,默认为default表示使用系统默认路径
+# Windows示例(注意双反斜杠):C:\\Program Files (x86)\\OpenOffice 4
+# Linux示例:/opt/libreoffice
+# MacOS示例:/Applications/LibreOffice.app/Contents
+office.home = ${KK_OFFICE_HOME:default}
+
+####################### 2.2 Office转换服务配置 #######################
+# Office转换服务监听的端口,默认开启两个进程分别监听2001和2002端口
+# 多个端口用逗号分隔,系统会自动启动多个进程处理转换任务
+office.plugin.server.ports = 2001,2002
+
+# Office转换任务总超时时间,默认为5分钟
+# 超过此时间的任务将被强制终止,释放资源
+office.plugin.task.timeout = 5m
+
+# Office进程在重启前可执行的最大任务数,默认为200
+# 0表示无限,不重启。适当限制可避免内存泄漏
+office.plugin.task.maxtasksperprocess = 200
+
+# Office任务执行超时时间,默认为5分钟
+# 单个任务处理超过此时间将被中止,处理下一个任务
+office.plugin.task.taskexecutiontimeout = 5m
+
+####################### 2.3 Office转换参数配置(支持动态更新)#######################
+# 是否启用Office文档分页转换,默认为false(转换全部页面)
+# 可设置为页面范围,如"1-5"表示只转换前5页
+office.pagerange = ${KK_OFFICE_PAGERANGE:false}
+
+# 是否启用Office文档水印功能,默认为false(不启用)
+# 启用后会在转换的图片或PDF上添加水印
+office.watermark = ${KK_OFFICE_WATERMARK:false}
+
+# Office文档转图片的JPEG压缩质量,范围1-100,默认80
+# 值越高图片质量越好但文件越大,建议80-90之间
+office.quality = ${KK_OFFICE_QUALITY:80}
+
+# Office文档转图片的最大分辨率DPI,默认150
+# 值越高图片越清晰但文件越大,转换时间越长
+# 建议范围:72(屏幕显示)-300(打印质量)
+office.maximageresolution = ${KK_OFFICE_MAXIMAGERESOLUTION:150}
+
+# 是否导出Office文档中的书签到PDF,默认为true(导出)
+# 保留书签可方便PDF文档导航
+office.exportbookmarks = ${KK_OFFICE_EXPORTBOOKMARKS:true}
+
+# 是否将Office文档中的批注作为PDF注释导出,默认为true(导出)
+# 保留批注便于文档审阅
+office.exportnotes = ${KK_OFFICE_EXPORTNOTES:true}
+
+# 加密文档生成的PDF是否添加密码,默认为true(添加)
+# 密码为原始加密文档的密码,增强文档安全性
+office.documentopenpasswords = ${KK_OFFICE_DOCUMENTOPENPASSWORD:true}
+
+# Excel文档(xlsx)的前端解析方式,默认为web(Web端解析)
+# web: 使用前端SheetJS库解析,减轻服务器压力
+# image: 服务器转换为图片,兼容性更好
+office.type.web = ${KK_OFFICE_TYPE_WEB:web}
+
+
+
+####################################### 三、文件存储与缓存配置 #######################################
+
+####################### 3.1 文件存储路径配置 #######################
+# 预览生成资源的存储路径,默认为应用根路径下的file目录
+# Windows示例:D:\\kkFileview\\(注意双反斜杠)
+# Linux示例:/opt/kkfileview/file/
+# 重要:确保应用有该目录的读写权限
+file.dir = ${KK_FILE_DIR:default}
+
+# 允许预览的本地文件夹路径,默认为default(禁止所有本地文件预览)
+# ⚠️ 安全警告:配置此路径可能允许访问系统文件,请谨慎配置
+# Windows示例(注意前面加反斜杠):\D:\\kkFileview\\1\\1.txt
+# Linux示例(注意前面加正斜杠):/opt/1.txt
+# 使用file协议访问:file://d:/1/1.txt(Windows)或 file:/opt/1/1.txt(Linux)
+local.preview.dir = ${KK_LOCAL_PREVIEW_DIR:default}
+
+####################### 3.2 缓存核心配置 #######################
+# 是否启用缓存功能,默认为true(启用)
+# 启用缓存可显著提升重复文件的预览速度
+cache.enabled = ${KK_CACHE_ENABLED:true}
+
+# 缓存实现类型,默认为jdk(使用JDK内置对象实现)
+# 可选值:
+# jdk: JDK内置ConcurrentHashMap,单机部署推荐
+# redis: Redis分布式缓存,集群部署推荐
+# default: 内嵌RocksDB,支持持久化
+cache.type = ${KK_CACHE_TYPE:jdk}
+
+####################### 3.3 Redis缓存详细配置(仅当cache.type=redis时生效)#######################
+# Redis部署模式,默认为single(单机模式)
+# 可选值:
+# single: 单机模式(默认)
+# cluster: 集群模式
+# sentinel: 哨兵模式(高可用)
+# master-slave: 主从模式
+spring.redisson.mode = single
+
+# Redis连接地址,支持多种格式:
+# 单机模式:redis://127.0.0.1:6379
+# 集群模式:redis://node1:6379,redis://node2:6379,redis://node3:6379
+# 哨兵模式:redis://sentinel1:26379,redis://sentinel2:26379
+spring.redisson.address = ${KK_SPRING_REDISSON_ADDRESS:redis://127.0.0.1:6379}
+
+# Redis连接密码,无密码时留空
+# 注意:密码包含特殊字符时需使用URL编码
+spring.redisson.password = ${KK_SPRING_REDISSON_PASSWORD:}
+
+# Redis数据库索引,默认为0(0-15)
+# 不同业务可使用不同数据库隔离
+spring.redisson.database = ${KK_SPRING_REDISSON_DATABASE:0}
+
+####################### 3.4 缓存维护配置 #######################
+# 是否启用缓存自动清理,默认为true(启用)
+# 定期清理过期缓存,避免磁盘空间无限增长
+cache.clean.enabled = ${KK_CACHE_CLEAN_ENABLED:true}
+
+# 缓存自动清理时间,使用Quartz cron表达式,默认为每天凌晨3点执行
+# 表达式格式:秒 分 时 日 月 周 年(可选)
+# 0 0 3 * * ? 表示每天3:00:00执行清理
+cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
+
+
+
+####################################### 四、安全与访问控制配置(支持动态配置)#######################################
+
+####################### 4.1 服务地址配置 #######################
+# 提供预览服务的完整地址,默认从请求URL自动获取
+# 当使用Nginx等反向代理时,必须手动设置此地址
+# 示例:https://file.keking.cn
+# 示例:http://192.168.1.100:8012
+base.url = ${KK_BASE_URL:default}
+
+####################### 4.2 信任主机白名单配置(重要安全配置)#######################
+# 信任站点白名单配置,多个域名用逗号隔开
+# ⚠️ 安全提示:为防止SSRF(服务器端请求伪造)攻击,必须配置信任主机
+# ⚠️ 如果不配置,系统将默认拒绝所有外部文件预览请求
+#
+# 配置示例:
+# trust.host = kkview.cn,yourdomain.com,cdn.example.com,192.168.1.*
+#
+# 支持通配符:
+# * 匹配所有字符
+# ? 匹配单个字符
+# 192.168.* 匹配192.168开头的所有IP
+#
+# 当前配置:允许所有域名(仅建议测试环境使用,生产环境必须修改)
+trust.host = *
+
+####################### 4.3 不信任主机黑名单配置 #######################
+# 不信任站点黑名单配置,多个用逗号隔开
+# 黑名单优先级高于白名单,设置后将禁止预览来自这些站点的文件
+# 建议配置:禁止访问内网地址和本地地址,防止内部信息泄露
+#
+# 配置示例:
+# not.trust.host = localhost,127.0.0.1,0.0.0.0,192.168.*,10.*,172.16.*,172.17.*,172.18.*,172.19.*,172.20.*,172.21.*,172.22.*,172.23.*,172.24.*,172.25.*,172.26.*,172.27.*,172.28.*,172.29.*,172.30.*,172.31.*
+not.trust.host = ${KK_NOT_TRUST_HOST:default}
+
+####################### 4.4 文件类型安全限制 #######################
+# 禁止上传和预览的文件类型,多个用逗号隔开
+# 默认禁止可执行文件和系统文件,防止恶意文件上传
+# exe: Windows可执行文件
+# dll: Windows动态链接库
+# dat: 数据文件(可能包含敏感信息)
+prohibit = ${KK_PROHIBIT:exe,dll,dat}
+
+
+
+####################################### 五、文件格式与预览配置(支持动态配置)#######################################
+
+####################### 5.1 文本文件类型配置 #######################
+# 支持的文本文件类型,多个用逗号隔开
+# 这些文件将直接在前端以文本形式展示,无需转换
+# 默认支持常见编程语言、配置文件和文档格式
+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}
+
+####################### 5.2 多媒体文件类型配置 #######################
+# 支持的媒体文件类型,多个用逗号隔开
+# 这些文件将使用HTML5原生播放器或转换后播放
+# 默认支持主流音频、视频和流媒体格式
+media = ${KK_MEDIA:mp3,wav,mp4,flv,mpd,m3u8,ts,mpeg,m4a}
+
+# 支持格式转换的视频类型,多个用逗号隔开
+# 这些格式的视频将被转换为mp4格式以确保浏览器兼容性
+# ⚠️ 视频转换消耗资源较大,建议根据服务器性能配置
+convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm,mpeg}
+
+####################### 5.3 图片文件类型配置 #######################
+# TIF/TIFF格式图片预览模式,可选值:jpg、pdf
+# tif: 使用前端Tiff.js插件直接浏览(需要浏览器支持)
+# jpg: 服务器转换为JPG格式后显示(兼容性好)
+# pdf: 服务器转换为PDF格式显示(支持多页和打印)
+tif.preview.type = ${KK_TIF_PREVIEW_TYPE:tif}
+
+# CAD设计文件预览模式,可选值:tif、svg、pdf
+# tif: 转换为TIFF格式,使用前端插件浏览
+# svg: 转换为SVG矢量格式(缩放不失真)
+# pdf: 转换为PDF格式(便于打印和标注)
+cad.preview.type = ${KK_CAD_PREVIEW_TYPE:svg}
+
+####################### 5.4 Office文档预览配置 #######################
+# Office类型文档(Word、PPT、Excel)预览样式,可选值:image、pdf
+# image: 转换为图片序列(兼容性好,支持水印)
+# pdf: 转换为PDF格式(支持文本复制、打印)
+# 用户可在预览界面切换模式(除非禁用切换开关)
+office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
+
+# 是否关闭Office预览模式切换开关,默认为false(允许切换)
+# 设置为true时,用户无法在图片和PDF模式间切换
+office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
+
+####################### 5.5 PDF文档预览配置 #######################
+# PDF文件预览安全限制配置
+# 是否禁止PDF演示模式,默认为true(禁止)
+pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
+
+# 是否禁止PDF文件菜单中的"打开文件"选项,默认为true(禁止)
+pdf.openFile.disable = ${KK_PDF_OPEN_FILE_DISABLE:true}
+
+# 是否禁止PDF打印功能,默认为true(禁止)
+pdf.print.disable = ${KK_PDF_PRINT_DISABLE:true}
+
+# 是否禁止PDF下载功能,默认为true(禁止)
+pdf.download.disable = ${KK_PDF_DOWNLOAD_DISABLE:true}
+
+# 是否禁止PDF书签/大纲功能,默认为true(禁止)
+pdf.bookmark.disable = ${KK_PDF_BOOKMARK_DISABLE:true}
+
+# 是否禁止PDF编辑功能(注释、表单等),默认为false(允许编辑)
+pdf.disable.editing = ${KK_PDF_DISABLE_EDITING:false}
+
+####################### 5.6 水印配置 #######################
+# 水印文本内容,为空表示不添加水印
+# 示例:内部文件,严禁外泄
+watermark.txt = ${WATERMARK_TXT:}
+
+# 水印在X轴方向的间隔(像素),默认为10
+# 值越小水印越密集,值越大水印越稀疏
+watermark.x.space = ${WATERMARK_X_SPACE:10}
+
+# 水印在Y轴方向的间隔(像素),默认为10
+watermark.y.space = ${WATERMARK_Y_SPACE:10}
+
+# 水印字体,默认为"微软雅黑"
+# 注意:服务器需要安装对应字体,Linux服务器可能需要安装中文字体
+watermark.font = ${WATERMARK_FONT:微软雅黑}
+
+# 水印字体大小,默认为18px
+# 建议根据水印文本长度调整
+watermark.fontsize = ${WATERMARK_FONTSIZE:18px}
+
+# 水印字体颜色,默认为black(黑色)
+# 支持格式:颜色名(black, white, red等)、十六进制(#000000)、RGB(rgb(0,0,0))
+watermark.color = ${WATERMARK_COLOR:black}
+
+# 水印透明度,范围0.005-1,默认为0.2(20%不透明度)
+# 值越小水印越淡,值越大水印越明显
+watermark.alpha = ${WATERMARK_ALPHA:0.2}
+
+# 单个水印块的宽度(像素),默认为180
+# 需要根据水印文本长度调整
+watermark.width = ${WATERMARK_WIDTH:180}
+
+# 单个水印块的高度(像素),默认为80
+watermark.height = ${WATERMARK_HEIGHT:80}
+
+# 水印倾斜度数,范围0-90,默认为10度
+# 0度:水平水印,90度:垂直水印
+watermark.angle = ${WATERMARK_ANGLE:10}
+
+# CAD文件是否添加水印,默认为true(启用)
+cad.watermark = true
+
+
+
+####################################### 六、性能与资源管理配置(支持动态配置)#######################################
+
+####################### 6.1 线程池配置 #######################
+# PDF转换最大并发线程数,默认为10
+# 根据服务器CPU核心数调整:建议为核心数×2
+pdf.max.threads = 10
+
+# CAD转换线程数,默认为5
+# CAD转换较耗时,不宜设置过高
+cad.thread = ${KK_CAD_THREAD:5}
+
+# TIF转换线程数,默认为5
+tif.thread = 5
+
+####################### 6.2 超时配置(按文件大小分类)#######################
+# 是否启用视频转换超时控制,默认为true(启用)
+media.timeout.enabled = true
+
+# 视频文件大小分类及超时时间(单位:秒)
+# 小文件(<50MB):30秒超时
+media.small.file.timeout = 30
+# 中等文件(50-150MB):60秒超时
+media.medium.file.timeout = 60
+# 大文件(150-300MB):180秒超时(3分钟)
+media.large.file.timeout = 180
+# 超大文件(300-600MB):300秒超时(5分钟)
+media.xl.file.timeout = 300
+# XXL文件(600-1200MB):600秒超时(10分钟)
+media.xxl.file.timeout = 600
+# XXXL文件(>1200MB):1200秒超时(20分钟)
+media.xxxl.file.timeout = 1200
+
+####################### 6.3 超时配置(按PDF页数分类)#######################
+# PDF文件按页数分类的超时配置(单位:秒)
+# 小文件(0-50页):90秒超时
+pdf.timeout.small = 90
+# 中等文件(51-200页):180秒超时(3分钟)
+pdf.timeout.medium = 180
+# 大文件(201-500页):300秒超时(5分钟)
+pdf.timeout.large = 300
+# 超大文件(>500页):600秒超时(10分钟)
+pdf.timeout.xlarge = 600
+
+# CAD转换超时时间,默认为90秒
+cad.timeout = ${KK_CAD_TIMEOUT:90}
+
+# TIF转换超时时间,默认为90秒
+tif.timeout = 90
+
+####################### 6.4 资源限制配置 #######################
+# 视频转换最大文件大小限制(单位:MB),默认为300MB
+# 超过此大小的视频文件禁止转换,避免服务器资源耗尽
+media.convert.max.size = 300
+
+# 是否禁用视频格式转换功能,默认为true(禁用)
+# ⚠️ 重要:视频转换非常消耗CPU和内存资源
+# 启用前确保:1)服务器性能充足 2)配置异步处理 3)增加线程池
+media.convert.disable = ${KK_MEDIA_CONVERT_DISABLE:true}
+
+####################### 6.5 DPI优化配置 #######################
+# 是否启用PDF DPI智能调整,默认为true(启用)
+# 根据PDF页数自动调整DPI,平衡清晰度和性能
+pdf.dpi.enabled = true
+
+# PDF转图片的基准DPI,默认为144
+# 当DPI优化禁用时使用此值
+pdf2jpg.dpi = ${KK_PDF2JPG_DPI:144}
+
+# PDF按页数范围的DPI优化值
+# 小文件(0-50页):150 DPI(高质量)
+pdf.dpi.small = 150
+# 中等文件(50-100页):120 DPI(平衡质量与性能)
+pdf.dpi.medium = 120
+# 大文件(100-200页):96 DPI(优化性能)
+pdf.dpi.large = 96
+# 超大文件(200-500页):72 DPI(快速转换)
+pdf.dpi.xlarge = 72
+# 巨量文件(>500页):72 DPI(最小资源消耗)
+pdf.dpi.xxlarge = 72
+
+
+
+####################################### 七、FTP文件访问配置(支持动态配置)#######################################
+
+####################### 7.1 FTP连接认证配置 #######################
+#FTP模块设置
+#预览源为FTP时,可在ftp url后面加参数?ftp.username=ftpuser&ftp.password=123456&ftp.control.encoding=GBK,指定,不指定默认用配置的 (为了安全我们强烈建议在配置中设置相关信息)
+#ftp.control.encodin (根据FTP服务器操作系统选择,Linux一般为UTF-8,Windows一般为GBK)
+#使用方法,支持,分割第一个是域名或者IP地址后面是用户名在后面是密码,在后面是编码(用户名密码和编码用:分割, 域名用,分割),切记url不需要任何协议头
+#ftp.username = 地址:端口:用户名:密码:编码,192.168.0.2:21:name:123456:UTF-8,www.xxx.com:21:admin:pass:UTF-8 多客户端,分割
+#ftp.username =10.99.1.2:21:666:88888:GBK
+ftp.username = false
+
+
+
+####################################### 八、首页与文件管理配置(支持动态配置)#######################################
+
+####################### 8.1 首页功能配置 #######################
+# 是否禁用首页文件上传功能,默认为false(不禁用)
+# 设置为true可关闭上传功能,仅用于预览
+file.upload.disable = false
+
+# 网站备案信息,显示在首页底部,默认为空
+# 示例:京ICP备12345678号
+beian = ${KK_BEIAN:default}
+
+# 首页初始化加载的页码,默认为1(第一页)
+home.pagenumber = ${DEFAULT_HOME_PAGENUMBER:1}
+
+# 首页是否启用分页功能,默认为true(启用)
+home.pagination = ${DEFAULT_HOME_PAGINATION:true}
+
+# 首页每页显示的文件数量,默认为15
+home.pagesize = ${DEFAULT_HOME_PAGSIZE:15}
+
+# 首页是否显示搜索框,默认为true(显示)
+home.search = ${DEFAULT_HOME_SEARCH:true}
+
+####################### 8.2 文件删除安全配置 #######################
+# 是否启用验证码验证删除文件,默认为false(不启用)
+# 启用后删除文件需要输入验证码,防止误删
+delete.captcha = ${KK_DELETE_CAPTCHA:false}
+
+# 删除文件密码,默认为123456
+# 删除文件时需要验证此密码(如果启用了验证码,两者都需要)
+delete.password = ${KK_DELETE_PASSWORD:123456}
+
+# 是否删除转换后的源文件,默认为true(删除)
+# 启用可节约磁盘空间,但会丢失原始文件
+# 注意:删除后无法重新转换,只能重新上传
+delete.source.file = ${KK_DELETE_SOURCE_FILE:true}
+
+
+
+####################################### 九、权限与认证配置(支持动态配置)#######################################
+
+####################### 9.1 功能权限配置 #######################
+# 是否启用图片预览权限,默认为true(启用)
+# 设置为false可禁用所有图片预览功能
+kk.Picturespreview = true
+
+# 是否启用跨域文件获取权限,默认为true(启用)
+# 设置为false可防止跨站请求伪造(CSRF)
+kk.Getcorsfile = true
+
+# 是否启用添加异步任务权限,默认为true(启用)
+# 大文件转换通常使用异步任务处理
+kk.addTask = true
+
+# API密钥功能,默认为false(禁用)
+# 启用后需要提供密钥才能调用API
+kk.Key = false
+
+####################### 9.2 加密与认证配置 #######################
+# AES加密密钥,必须为16位字符
+# 启用AES加密时,接入方需使用相同的密钥
+# 用于敏感数据传输加密
+ase.key = 1234567890123456
+
+# Basic认证配置,格式:域名:用户名:密码,多个用逗号分隔
+# 用于保护特定域名的访问
+# 示例:192.168.0.1:admin:pass123,example.com:user:pass456
+basic.name = 10.99.1.2:aaa:bbb
+
+# User-Agent验证字符串,默认为99999
+# 可用于简单的客户端验证
+useragent = 99999
+
+
+
+####################################### 十、高级功能与兼容性配置(支持动态配置)#######################################
+
+####################### 10.1 SSL/TLS安全配置 #######################
+# 是否忽略SSL证书验证,默认为true(忽略)
+# 用于开发环境或自签名证书场景
+# 生产环境建议设置为false,启用完整的证书验证
+kk.ignore.ssl = true
+
+####################### 10.2 重定向功能配置 #######################
+# 是否启用URL重定向功能,默认为true(启用)
+# 用于处理文件下载、外部资源引用等场景
+kk.enable.redirect = true
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/config/ConfigConstants.java b/server/src/main/java/cn/keking/config/ConfigConstants.java
index 5c742f4d..9a83f0f6 100644
--- a/server/src/main/java/cn/keking/config/ConfigConstants.java
+++ b/server/src/main/java/cn/keking/config/ConfigConstants.java
@@ -23,40 +23,38 @@ public class ConfigConstants {
}
// ==================================================
- // 常量定义区
- // ==================================================
+// 常量定义区(按功能模块分组)
+// ==================================================
- // 缓存配置常量
+ // 1. 缓存配置常量
public static final String DEFAULT_CACHE_ENABLED = "true";
- // 文件类型配置常量
+ // 2. 文件类型配置常量
public static final String DEFAULT_TXT_TYPE = "txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd,xbrl";
public static final String DEFAULT_MEDIA_TYPE = "mp3,wav,mp4,flv";
public static final String DEFAULT_PROHIBIT = "exe,dll";
public static final String DEFAULT_TIF_PREVIEW_TYPE = "tif";
public static final String DEFAULT_CAD_PREVIEW_TYPE = "pdf";
- // Office配置常量
+ // 3. Office配置常量
public static final String DEFAULT_OFFICE_PREVIEW_TYPE = "image";
public static final String DEFAULT_OFFICE_PREVIEW_SWITCH_DISABLED = "false";
public static final String DEFAULT_OFFICE_TYPE_WEB = "web";
- public static final String DEFAULT_OFFICE_PAQERANQE = "false";
+ public static final String DEFAULT_OFFICE_PAQERANQE = "false"; // 注意:拼写错误,应为PAGERANGE
public static final String DEFAULT_OFFICE_WATERMARK = "false";
public static final String DEFAULT_OFFICE_QUALITY = "80";
- public static final String DEFAULT_OFFICE_MAXIMAQERESOLUTION = "150";
+ public static final String DEFAULT_OFFICE_MAXIMAQERESOLUTION = "150"; // 注意:拼写错误,应为MAXIMAGERESOLUTION
public static final String DEFAULT_OFFICE_EXPORTBOOKMARKS = "true";
public static final String DEFAULT_OFFICE_EXPORTNOTES = "true";
- public static final String DEFAULT_OFFICE_EOCUMENTOPENPASSWORDS = "true";
+ public static final String DEFAULT_OFFICE_EOCUMENTOPENPASSWORDS = "true"; // 注意:拼写错误,应为DOCUMENTOPENPASSWORDS
- // FTP配置常量
+ // 4. FTP配置常量
public static final String DEFAULT_FTP_USERNAME = null;
- public static final String DEFAULT_FTP_PASSWORD = null;
- public static final String DEFAULT_FTP_CONTROL_ENCODING = "UTF-8";
- // 路径配置常量
+ // 5. 路径配置常量
public static final String DEFAULT_VALUE = "default";
- // PDF配置常量
+ // 6. PDF配置常量
public static final String DEFAULT_PDF_PRESENTATION_MODE_DISABLE = "true";
public static final String DEFAULT_PDF_OPEN_FILE_DISABLE = "true";
public static final String DEFAULT_PDF_PRINT_DISABLE = "true";
@@ -64,45 +62,81 @@ public class ConfigConstants {
public static final String DEFAULT_PDF_BOOKMARK_DISABLE = "true";
public static final String DEFAULT_PDF_DISABLE_EDITING = "true";
public static final String DEFAULT_PDF2_JPG_DPI = "105";
- public static final String DEFAULT_PDF_TIMEOUT = "90";
- public static final String DEFAULT_PDF_TIMEOUT80 = "180";
- public static final String DEFAULT_PDF_TIMEOUT200 = "300";
- public static final String DEFAULT_PDF_THREAD = "5";
- // CAD配置常量
+ // 7. CAD配置常量
public static final String DEFAULT_CAD_TIMEOUT = "90";
public static final String DEFAULT_CAD_THREAD = "5";
- // 文件操作配置常量
+ // 8. TIF配置常量
+ public static final String DEFAULT_TIF_TIMEOUT = "90";
+ public static final String DEFAULT_TIF_THREAD = "5";
+
+ // 9. 文件操作配置常量
public static final String DEFAULT_FILE_UPLOAD_DISABLE = "false";
public static final String DEFAULT_DELETE_SOURCE_FILE = "true";
public static final String DEFAULT_DELETE_CAPTCHA = "false";
public static final String DEFAULT_SIZE = "500MB";
public static final String DEFAULT_PASSWORD = "123456";
- // 首页配置常量
+ // 10. 首页配置常量
public static final String DEFAULT_BEIAN = "无";
public static final String DEFAULT_HOME_PAGENUMBER = "1";
public static final String DEFAULT_HOME_PAGINATION = "true";
public static final String DEFAULT_HOME_PAGSIZE = "15";
public static final String DEFAULT_HOME_SEARCH = "true";
- // 权限配置常量
+ // 11. 权限配置常量
public static final String DEFAULT_KEY = "false";
public static final String DEFAULT_PICTURES_PREVIEW = "true";
public static final String DEFAULT_GET_CORS_FILE = "true";
public static final String DEFAULT_ADD_TASK = "true";
- public static final String DEFAULT_AES_KEY= "1234567890123456";
+ public static final String DEFAULT_AES_KEY = "1234567890123456";
- // UserAgent配置常量
+ // 12. UserAgent配置常量
public static final String DEFAULT_USER_AGENT = "false";
- // Basic认证配置常量
+ // 13. Basic认证配置常量
public static final String DEFAULT_BASIC_NAME = "";
+ // 14. 视频转换配置常量
+ public static final String DEFAULT_MEDIA_CONVERT_MAX_SIZE = "300";
+ public static final String DEFAULT_MEDIA_TIMEOUT_ENABLED = "true";
+ public static final String DEFAULT_MEDIA_SMALL_FILE_TIMEOUT = "30";
+ public static final String DEFAULT_MEDIA_MEDIUM_FILE_TIMEOUT = "60";
+ public static final String DEFAULT_MEDIA_LARGE_FILE_TIMEOUT = "180";
+ public static final String DEFAULT_MEDIA_XL_FILE_TIMEOUT = "300";
+ public static final String DEFAULT_MEDIA_XXL_FILE_TIMEOUT = "600";
+ public static final String DEFAULT_MEDIA_XXXL_FILE_TIMEOUT = "1200";
+
+ // 15. PDF DPI配置常量
+ public static final String DEFAULT_PDF_SMALL_DTI = "150"; // 注意:拼写错误,应为DPI
+ public static final String DEFAULT_PDF_MEDIUM_DPI = "120";
+ public static final String DEFAULT_PDF_LARGE_DPI = "96";
+ public static final String DEFAULT_PDF_XLARGE_DPI = "72";
+ public static final String DEFAULT_PDF_XXLARGE_DPI = "72";
+ public static final String DEFAULT_PDF_DPI_ENABLED = "true";
+
+ // 16. PDF超时配置常量(新标准化配置)
+ public static final String DEFAULT_PDF_TIMEOUT_SMALL = "90";
+ public static final String DEFAULT_PDF_TIMEOUT_MEDIUM = "180";
+ public static final String DEFAULT_PDF_TIMEOUT_LARGE = "300";
+ public static final String DEFAULT_PDF_TIMEOUT_XLARGE = "600";
+
+ // 17. PDF线程配置常量
+ public static final String DEFAULT_PDF_MAX_THREADS = "10";
+
+ // 18. CAD水印配置常量
+ public static final String DEFAULT_CAD_WATERMARK = "false";
+
+ // 19. SSL忽略配置常量
+ public static final String DEFAULT_IGNORE_SSL = "true";
+
+ // 20. 重定向启用配置常量
+ public static final String DEFAULT_ENABLE_REDIRECT = "true";
+
// ==================================================
- // 配置变量定义区(按功能分类)
- // ==================================================
+// 配置变量定义区(按功能分类,均为静态变量)
+// ==================================================
// 1. 缓存配置
private static Boolean cacheEnabled;
@@ -130,8 +164,6 @@ public class ConfigConstants {
// 4. FTP配置
private static String ftpUsername;
- private static String ftpPassword;
- private static String ftpControlEncoding;
// 5. 路径配置
private static String fileDir = ConfigUtils.getHomePath() + File.separator + "file" + File.separator;
@@ -150,45 +182,132 @@ public class ConfigConstants {
private static String pdfDownloadDisable;
private static String pdfBookmarkDisable;
private static int pdf2JpgDpi;
- private static int pdfTimeout;
- private static int pdfTimeout80;
- private static int pdfTimeout200;
- private static int pdfThread;
// 8. CAD配置
private static String cadTimeout;
private static int cadThread;
- // 9. 文件操作配置
+ // 9. TIF配置
+ private static String tifTimeout;
+ private static int tifThread;
+
+ // 10. 文件操作配置
private static Boolean fileUploadDisable;
private static String size;
private static String password;
private static Boolean deleteSourceFile;
private static Boolean deleteCaptcha;
- // 10. 首页配置
+ // 11. 首页配置
private static String beian;
private static String homePageNumber;
private static String homePagination;
private static String homePageSize;
private static String homeSearch;
- // 11. 权限配置
+ // 12. 权限配置
private static String key;
private static boolean picturesPreview;
private static boolean getCorsFile;
private static boolean addTask;
private static String aesKey;
- // 12. UserAgent配置
+ // 13. UserAgent配置
private static String userAgent;
- // 13. Basic认证配置
+ // 14. Basic认证配置
private static String basicName;
+ // 15. 视频转换配置
+ private static int mediaConvertMaxSize;
+ private static boolean mediaTimeoutEnabled;
+ private static int mediaSmallFileTimeout;
+ private static int mediaMediumFileTimeout;
+ private static int mediaLargeFileTimeout;
+ private static int mediaXLFileTimeout;
+ private static int mediaXXLFileTimeout;
+ private static int mediaXXXLFileTimeout;
+
+ // 16. PDF DPI配置
+ private static boolean pdfDpiEnabled;
+ private static int pdfSmallDpi;
+ private static int pdfMediumDpi;
+ private static int pdfLargeDpi;
+ private static int pdfXLargeDpi;
+ private static int pdfXXLargeDpi;
+
+ // 17. PDF超时配置(新)
+ private static int pdfTimeoutSmall;
+ private static int pdfTimeoutMedium;
+ private static int pdfTimeoutLarge;
+ private static int pdfTimeoutXLarge;
+
+ // 18. PDF线程配置
+ private static int pdfMaxThreads;
+
+ // 19. CAD水印配置
+ private static Boolean cadwatermark;
+
+ // 20. SSL忽略配置
+ private static Boolean ignoreSSL;
+
+ // 21. 重定向启用配置
+ private static Boolean enableRedirect;
+
+
// ==================================================
- // 获取方法(按功能分类)
- // ==================================================
+// 获取方法(按功能分类)
+// ==================================================
+
+ /**
+ * PDF超时配置获取方法(新)
+ */
+ public static int getPdfTimeoutSmall() {
+ return pdfTimeoutSmall;
+ }
+
+ public static int getPdfTimeoutMedium() {
+ return pdfTimeoutMedium;
+ }
+
+ public static int getPdfTimeoutLarge() {
+ return pdfTimeoutLarge;
+ }
+
+ public static int getPdfTimeoutXLarge() {
+ return pdfTimeoutXLarge;
+ }
+
+ public static int getPdfMaxThreads() {
+ return pdfMaxThreads;
+ }
+
+ /**
+ * 根据页数获取优化的DPI值
+ * 智能DPI调整策略:
+ * - 0-50页: 150 DPI
+ * - 51-100页: 120 DPI
+ * - 101-200页: 96 DPI
+ * - 201-500页: 72 DPI
+ * - >500页: 72 DPI
+ */
+ public static int getOptimizedDpi(int pageCount) {
+ if (!pdfDpiEnabled) {
+ return ConfigConstants.getPdf2JpgDpi();
+ }
+
+ if (pageCount > 500) {
+ return pdfXXLargeDpi;
+ } else if (pageCount > 200) {
+ return pdfXLargeDpi;
+ } else if (pageCount > 100) {
+ return pdfLargeDpi;
+ } else if (pageCount > 50) {
+ return pdfMediumDpi;
+ } else {
+ return pdfSmallDpi;
+ }
+ }
// 1. 缓存配置获取方法
public static Boolean isCacheEnabled() {
@@ -266,14 +385,6 @@ public class ConfigConstants {
return ftpUsername;
}
- public static String getFtpPassword() {
- return ftpPassword;
- }
-
- public static String getFtpControlEncoding() {
- return ftpControlEncoding;
- }
-
// 5. 路径配置获取方法
public static String getBaseUrl() {
return baseUrl;
@@ -325,22 +436,6 @@ public class ConfigConstants {
return pdf2JpgDpi;
}
- public static int getPdfTimeout() {
- return pdfTimeout;
- }
-
- public static int getPdfTimeout80() {
- return pdfTimeout80;
- }
-
- public static int getPdfTimeout200() {
- return pdfTimeout200;
- }
-
- public static int getPdfThread() {
- return pdfThread;
- }
-
// 8. CAD配置获取方法
public static String getCadPreviewType() {
return cadPreviewType;
@@ -354,7 +449,16 @@ public class ConfigConstants {
return cadThread;
}
- // 9. 文件操作配置获取方法
+ // 9. TIF配置获取方法
+ public static String getTifTimeout() {
+ return tifTimeout;
+ }
+
+ public static int getTifThread() {
+ return tifThread;
+ }
+
+ // 10. 文件操作配置获取方法
public static Boolean getFileUploadDisable() {
return fileUploadDisable;
}
@@ -375,7 +479,7 @@ public class ConfigConstants {
return deleteCaptcha;
}
- // 10. 首页配置获取方法
+ // 11. 首页配置获取方法
public static String getBeian() {
return beian;
}
@@ -396,7 +500,7 @@ public class ConfigConstants {
return homeSearch;
}
- // 11. 权限配置获取方法
+ // 12. 权限配置获取方法
public static String getKey() {
return key;
}
@@ -417,19 +521,67 @@ public class ConfigConstants {
return aesKey;
}
- // 12. UserAgent配置获取方法
+ // 13. UserAgent配置获取方法
public static String getUserAgent() {
return userAgent;
}
- // 13. Basic认证配置获取方法
+ // 14. Basic认证配置获取方法
public static String getBasicName() {
return basicName;
}
- // ==================================================
- // Setter方法(按功能分类)
- // ==================================================
+ // 15. 视频转换配置获取方法
+ public static int getMediaConvertMaxSize() {
+ return mediaConvertMaxSize;
+ }
+
+ public static boolean isMediaTimeoutEnabled() {
+ return mediaTimeoutEnabled;
+ }
+
+ public static int getMediaSmallFileTimeout() {
+ return mediaSmallFileTimeout;
+ }
+
+ public static int getMediaMediumFileTimeout() {
+ return mediaMediumFileTimeout;
+ }
+
+ public static int getMediaLargeFileTimeout() {
+ return mediaLargeFileTimeout;
+ }
+
+ public static int getMediaXLFileTimeout() {
+ return mediaXLFileTimeout;
+ }
+
+ public static int getMediaXXLFileTimeout() {
+ return mediaXXLFileTimeout;
+ }
+
+ public static int getMediaXXXLFileTimeout() {
+ return mediaXXXLFileTimeout;
+ }
+
+ // 19. CAD水印配置获取方法
+ public static boolean getCadwatermark() {
+ return cadwatermark;
+ }
+
+ // 20. SSL忽略配置获取方法
+ public static boolean isIgnoreSSL() {
+ return ignoreSSL;
+ }
+
+ // 21. 重定向启用配置获取方法
+ public static boolean isEnableRedirect() {
+ return enableRedirect;
+ }
+
+// ==================================================
+// Setter方法(按功能分类)
+// ==================================================
// 1. 缓存配置Setter方法
@Value("${cache.enabled:true}")
@@ -610,24 +762,6 @@ public class ConfigConstants {
ConfigConstants.ftpUsername = ftpUsername;
}
- @Value("${ftp.password:}")
- public void setFtpPassword(String ftpPassword) {
- setFtpPasswordValue(ftpPassword);
- }
-
- public static void setFtpPasswordValue(String ftpPassword) {
- ConfigConstants.ftpPassword = ftpPassword;
- }
-
- @Value("${ftp.control.encoding:UTF-8}")
- public void setFtpControlEncoding(String ftpControlEncoding) {
- setFtpControlEncodingValue(ftpControlEncoding);
- }
-
- public static void setFtpControlEncodingValue(String ftpControlEncoding) {
- ConfigConstants.ftpControlEncoding = ftpControlEncoding;
- }
-
// 5. 路径配置Setter方法
@Value("${base.url:default}")
public void setBaseUrl(String baseUrl) {
@@ -685,11 +819,15 @@ public class ConfigConstants {
setNotTrustHostSet(getHostValue(notTrustHost));
}
+ /**
+ * 解析主机配置值
+ * 支持格式:host1,host2,host3
+ * 自动转换为小写并移除空格
+ */
private static CopyOnWriteArraySet getHostValue(String trustHost) {
if (DEFAULT_VALUE.equalsIgnoreCase(trustHost)) {
return new CopyOnWriteArraySet<>();
} else {
- // 去除空格并转小写
String[] trustHostArray = trustHost.toLowerCase().replaceAll("\\s+", "").split(",");
return new CopyOnWriteArraySet<>(Arrays.asList(trustHostArray));
}
@@ -767,42 +905,6 @@ public class ConfigConstants {
ConfigConstants.pdf2JpgDpi = pdf2JpgDpi;
}
- @Value("${pdf.timeout:90}")
- public void setPdfTimeout(int pdfTimeout) {
- setPdfTimeoutValue(pdfTimeout);
- }
-
- public static void setPdfTimeoutValue(int pdfTimeout) {
- ConfigConstants.pdfTimeout = pdfTimeout;
- }
-
- @Value("${pdf.timeout80:180}")
- public void setPdfTimeout80(int pdfTimeout80) {
- setPdfTimeout80Value(pdfTimeout80);
- }
-
- public static void setPdfTimeout80Value(int pdfTimeout80) {
- ConfigConstants.pdfTimeout80 = pdfTimeout80;
- }
-
- @Value("${pdf.timeout200:300}")
- public void setPdfTimeout200(int pdfTimeout200) {
- setPdfTimeout200Value(pdfTimeout200);
- }
-
- public static void setPdfTimeout200Value(int pdfTimeout200) {
- ConfigConstants.pdfTimeout200 = pdfTimeout200;
- }
-
- @Value("${pdf.thread:5}")
- public void setPdfThread(int pdfThread) {
- setPdfThreadValue(pdfThread);
- }
-
- public static void setPdfThreadValue(int pdfThread) {
- ConfigConstants.pdfThread = pdfThread;
- }
-
// 8. CAD配置Setter方法
@Value("${cad.timeout:90}")
public void setCadTimeout(String cadTimeout) {
@@ -822,7 +924,26 @@ public class ConfigConstants {
ConfigConstants.cadThread = cadThread;
}
- // 9. 文件操作配置Setter方法
+ // 9. TIF配置Setter方法
+ @Value("${tif.timeout:90}")
+ public void setTifTimeout(String tifTimeout) {
+ setTifTimeoutValue(tifTimeout);
+ }
+
+ public static void setTifTimeoutValue(String tifTimeout) {
+ ConfigConstants.tifTimeout = tifTimeout;
+ }
+
+ @Value("${tif.thread:5}")
+ public void setTifThread(int tifThread) {
+ setTifThreadValue(tifThread);
+ }
+
+ public static void setTifThreadValue(int tifThread) {
+ ConfigConstants.tifThread = tifThread;
+ }
+
+ // 10. 文件操作配置Setter方法
@Value("${file.upload.disable:true}")
public void setFileUploadDisable(Boolean fileUploadDisable) {
setFileUploadDisableValue(fileUploadDisable);
@@ -868,7 +989,7 @@ public class ConfigConstants {
ConfigConstants.deleteCaptcha = deleteCaptcha;
}
- // 10. 首页配置Setter方法
+ // 11. 首页配置Setter方法
@Value("${beian:default}")
public void setBeian(String beian) {
setBeianValue(beian);
@@ -914,7 +1035,7 @@ public class ConfigConstants {
ConfigConstants.homeSearch = homeSearch;
}
- // 11. 权限配置Setter方法
+ // 12. 权限配置Setter方法
@Value("${kk.Key:}")
public void setKey(String key) {
setKeyValue(key);
@@ -960,7 +1081,7 @@ public class ConfigConstants {
ConfigConstants.aesKey = aesKey;
}
- // 12. UserAgent配置Setter方法
+ // 13. UserAgent配置Setter方法
@Value("${useragent:false}")
public void setUserAgent(String userAgent) {
setUserAgentValue(userAgent);
@@ -970,7 +1091,7 @@ public class ConfigConstants {
ConfigConstants.userAgent = userAgent;
}
- // 13. Basic认证配置Setter方法
+ // 14. Basic认证配置Setter方法
@Value("${basic.name:}")
public void setBasicName(String basicName) {
setBasicNameValue(basicName);
@@ -979,4 +1100,209 @@ public class ConfigConstants {
public static void setBasicNameValue(String basicName) {
ConfigConstants.basicName = basicName;
}
+
+ // 15. 视频转换配置Setter方法
+ @Value("${media.convert.max.size:300}")
+ public void setMediaConvertMaxSize(int mediaConvertMaxSize) {
+ setMediaConvertMaxSizeValue(mediaConvertMaxSize);
+ }
+
+ public static void setMediaConvertMaxSizeValue(int mediaConvertMaxSize) {
+ ConfigConstants.mediaConvertMaxSize = mediaConvertMaxSize;
+ }
+
+ @Value("${media.timeout.enabled:true}")
+ public void setMediaTimeoutEnabled(String mediaTimeoutEnabled) {
+ setMediaTimeoutEnabledValue(Boolean.parseBoolean(mediaTimeoutEnabled));
+ }
+
+ public static void setMediaTimeoutEnabledValue(boolean mediaTimeoutEnabled) {
+ ConfigConstants.mediaTimeoutEnabled = mediaTimeoutEnabled;
+ }
+
+ @Value("${media.small.file.timeout:30}")
+ public void setMediaSmallFileTimeout(int mediaSmallFileTimeout) {
+ setMediaSmallFileTimeoutValue(mediaSmallFileTimeout);
+ }
+
+ public static void setMediaSmallFileTimeoutValue(int mediaSmallFileTimeout) {
+ ConfigConstants.mediaSmallFileTimeout = mediaSmallFileTimeout;
+ }
+
+ @Value("${media.medium.file.timeout:60}")
+ public void setMediaMediumFileTimeout(int mediaMediumFileTimeout) {
+ setMediaMediumFileTimeoutValue(mediaMediumFileTimeout);
+ }
+
+ public static void setMediaMediumFileTimeoutValue(int mediaMediumFileTimeout) {
+ ConfigConstants.mediaMediumFileTimeout = mediaMediumFileTimeout;
+ }
+
+ @Value("${media.large.file.timeout:180}")
+ public void setMediaLargeFileTimeout(int mediaLargeFileTimeout) {
+ setMediaLargeFileTimeoutValue(mediaLargeFileTimeout);
+ }
+
+ public static void setMediaLargeFileTimeoutValue(int mediaLargeFileTimeout) {
+ ConfigConstants.mediaLargeFileTimeout = mediaLargeFileTimeout;
+ }
+
+ @Value("${media.xl.file.timeout:300}")
+ public void setMediaXLFileTimeout(int mediaXLFileTimeout) {
+ setMediaXLFileTimeoutValue(mediaXLFileTimeout);
+ }
+
+ public static void setMediaXLFileTimeoutValue(int mediaXLFileTimeout) {
+ ConfigConstants.mediaXLFileTimeout = mediaXLFileTimeout;
+ }
+
+ @Value("${media.xxl.file.timeout:600}")
+ public void setMediaXXLFileTimeout(int mediaXXLFileTimeout) {
+ setMediaXXLFileTimeoutValue(mediaXXLFileTimeout);
+ }
+
+ public static void setMediaXXLFileTimeoutValue(int mediaXXLFileTimeout) {
+ ConfigConstants.mediaXXLFileTimeout = mediaXXLFileTimeout;
+ }
+
+ @Value("${media.xxxl.file.timeout:1200}")
+ public void setMediaXXXLFileTimeout(int mediaXXXLFileTimeout) {
+ setMediaXXXLFileTimeoutValue(mediaXXXLFileTimeout);
+ }
+
+ public static void setMediaXXXLFileTimeoutValue(int mediaXXXLFileTimeout) {
+ ConfigConstants.mediaXXXLFileTimeout = mediaXXXLFileTimeout;
+ }
+
+ // 16. PDF DPI配置Setter方法
+ @Value("${pdf.dpi.enabled:true}")
+ public void setPdfDpiEnabled(String pdfDpiEnabled) {
+ setPdfDpiEnabledValue(Boolean.parseBoolean(pdfDpiEnabled));
+ }
+
+ public static void setPdfDpiEnabledValue(boolean pdfDpiEnabled) {
+ ConfigConstants.pdfDpiEnabled = pdfDpiEnabled;
+ }
+
+ @Value("${pdf.dpi.small:150}")
+ public void setPdfSmallDpi(int pdfSmallDpi) {
+ setPdfSmallDpiValue(pdfSmallDpi);
+ }
+
+ public static void setPdfSmallDpiValue(int pdfSmallDpi) {
+ ConfigConstants.pdfSmallDpi = pdfSmallDpi;
+ }
+
+ @Value("${pdf.dpi.medium:120}")
+ public void setPdfMediumDpi(int pdfMediumDpi) {
+ setPdfMediumDpiValue(pdfMediumDpi);
+ }
+
+ public static void setPdfMediumDpiValue(int pdfMediumDpi) {
+ ConfigConstants.pdfMediumDpi = pdfMediumDpi;
+ }
+
+ @Value("${pdf.dpi.large:96}")
+ public void setPdfLargeDpi(int pdfLargeDpi) {
+ setPdfLargeDpiValue(pdfLargeDpi);
+ }
+
+ public static void setPdfLargeDpiValue(int pdfLargeDpi) {
+ ConfigConstants.pdfLargeDpi = pdfLargeDpi;
+ }
+
+ @Value("${pdf.dpi.xlarge:72}")
+ public void setPdfXLargeDpi(int pdfXLargeDpi) {
+ setPdfXLargeDpiValue(pdfXLargeDpi);
+ }
+
+ public static void setPdfXLargeDpiValue(int pdfXLargeDpi) {
+ ConfigConstants.pdfXLargeDpi = pdfXLargeDpi;
+ }
+
+ @Value("${pdf.dpi.xxlarge:72}")
+ public void setPdfXXLargeDpi(int pdfXXLargeDpi) {
+ setPdfXXLargeDpiValue(pdfXXLargeDpi);
+ }
+
+ public static void setPdfXXLargeDpiValue(int pdfXXLargeDpi) {
+ ConfigConstants.pdfXXLargeDpi = pdfXXLargeDpi;
+ }
+
+ // 17. PDF超时配置Setter方法(新)
+ @Value("${pdf.timeout.small:90}")
+ public void setPdfTimeoutSmall(int pdfTimeoutSmall) {
+ setPdfTimeoutSmallValue(pdfTimeoutSmall);
+ }
+
+ public static void setPdfTimeoutSmallValue(int pdfTimeoutSmall) {
+ ConfigConstants.pdfTimeoutSmall = pdfTimeoutSmall;
+ }
+
+ @Value("${pdf.timeout.medium:180}")
+ public void setPdfTimeoutMedium(int pdfTimeoutMedium) {
+ setPdfTimeoutMediumValue(pdfTimeoutMedium);
+ }
+
+ public static void setPdfTimeoutMediumValue(int pdfTimeoutMedium) {
+ ConfigConstants.pdfTimeoutMedium = pdfTimeoutMedium;
+ }
+
+ @Value("${pdf.timeout.large:300}")
+ public void setPdfTimeoutLarge(int pdfTimeoutLarge) {
+ setPdfTimeoutLargeValue(pdfTimeoutLarge);
+ }
+
+ public static void setPdfTimeoutLargeValue(int pdfTimeoutLarge) {
+ ConfigConstants.pdfTimeoutLarge = pdfTimeoutLarge;
+ }
+
+ @Value("${pdf.timeout.xlarge:600}")
+ public void setPdfTimeoutXLarge(int pdfTimeoutXLarge) {
+ setPdfTimeoutXLargeValue(pdfTimeoutXLarge);
+ }
+
+ public static void setPdfTimeoutXLargeValue(int pdfTimeoutXLarge) {
+ ConfigConstants.pdfTimeoutXLarge = pdfTimeoutXLarge;
+ }
+
+ // 18. PDF线程配置Setter方法
+ @Value("${pdf.max.threads:10}")
+ public void setPdfMaxThreads(int pdfMaxThreads) {
+ setPdfMaxThreadsValue(pdfMaxThreads);
+ }
+
+ public static void setPdfMaxThreadsValue(int pdfMaxThreads) {
+ ConfigConstants.pdfMaxThreads = pdfMaxThreads;
+ }
+
+ // 19. CAD水印配置Setter方法
+ @Value("${cad.watermark:false}")
+ public void setCadwatermark(String cadwatermark) {
+ setCadwatermarkValue(Boolean.parseBoolean(cadwatermark));
+ }
+
+ public static void setCadwatermarkValue(Boolean cadwatermark) {
+ ConfigConstants.cadwatermark = cadwatermark;
+ }
+
+ // 20. SSL忽略配置Setter方法
+ @Value("${kk.ignore.ssl:true}")
+ public void setIgnoreSSL(String ignoreSSL) {
+ setIgnoreSSLValue(Boolean.parseBoolean(ignoreSSL));
+ }
+
+ public static void setIgnoreSSLValue(Boolean ignoreSSL) {
+ ConfigConstants.ignoreSSL = ignoreSSL;
+ }
+
+ // 21. 重定向启用配置Setter方法
+ @Value("${kk.enable.redirect:true}")
+ public void setEnableRedirect(String enableRedirect) {
+ setEnableRedirectValue(Boolean.parseBoolean(enableRedirect));
+ }
+
+ public static void setEnableRedirectValue(Boolean enableRedirect) {
+ ConfigConstants.enableRedirect = enableRedirect;
+ }
}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java
index f6deaa38..c34e3dfe 100644
--- a/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java
+++ b/server/src/main/java/cn/keking/config/ConfigRefreshComponent.java
@@ -15,35 +15,38 @@ import java.util.Properties;
import java.util.concurrent.*;
/**
- * @auther: chenjh
- * @time: 2019/4/10 16:16
- * @description 使用 WatchService 监听配置文件变化,实现事件驱动的配置更新
+ * 配置刷新组件 - 动态配置管理
+ * 功能:监听配置文件变化,实现热更新配置
*/
@Component
public class ConfigRefreshComponent {
-
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class);
+ // 线程池和任务调度器
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService watchServiceExecutor = Executors.newSingleThreadExecutor();
private final Object lock = new Object();
- // 防抖延迟时间(单位:秒)
+ // 防抖延迟时间(秒)
private static final long DEBOUNCE_DELAY_SECONDS = 5;
+ // 任务和状态管理
private ScheduledFuture> scheduledReloadTask;
private WatchService watchService;
private volatile boolean running = true;
+ /**
+ * 初始化方法 - 启动配置监听
+ */
@PostConstruct
void init() {
- // 初始化时立即加载一次配置
loadConfig();
-
- // 启动监听线程
watchServiceExecutor.submit(this::watchConfigFile);
}
+ /**
+ * 销毁方法 - 清理资源
+ */
@PreDestroy
void destroy() {
running = false;
@@ -74,8 +77,6 @@ public class ConfigRefreshComponent {
}
watchService = FileSystems.getDefault().newWatchService();
-
- // 注册监听目录的修改事件
configDir.register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE,
@@ -91,7 +92,6 @@ public class ConfigRefreshComponent {
WatchEvent.Kind> kind = event.kind();
Path changedPath = (Path) event.context();
- // 检查是否是目标配置文件的变化
if (changedPath.equals(configPath.getFileName())) {
handleConfigChange(kind);
}
@@ -131,7 +131,6 @@ public class ConfigRefreshComponent {
if (kind == StandardWatchEventKinds.ENTRY_MODIFY ||
kind == StandardWatchEventKinds.ENTRY_CREATE) {
- // 使用防抖机制:取消之前的任务,重新调度新任务
synchronized (lock) {
if (scheduledReloadTask != null && !scheduledReloadTask.isDone()) {
scheduledReloadTask.cancel(false);
@@ -158,7 +157,6 @@ public class ConfigRefreshComponent {
Properties properties = new Properties();
String configFilePath = ConfigUtils.getCustomizedConfigPath();
- // 检查文件是否存在
Path configPath = Paths.get(configFilePath);
if (!Files.exists(configPath)) {
LOGGER.warn("配置文件不存在: {}", configFilePath);
@@ -169,7 +167,6 @@ public class ConfigRefreshComponent {
properties.load(bufferedReader);
ConfigUtils.restorePropertiesFromEnvFormat(properties);
- // 解析并设置配置项
updateConfigConstants(properties);
setWatermarkConfig(properties);
@@ -181,6 +178,7 @@ public class ConfigRefreshComponent {
}
}
}
+
/**
* 更新配置常量
*/
@@ -213,8 +211,6 @@ public class ConfigRefreshComponent {
// 4. FTP配置
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. 路径配置
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 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 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配置
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 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 homePageNumber = properties.getProperty("home.pagenumber", ConfigConstants.DEFAULT_HOME_PAGENUMBER);
String homePagination = properties.getProperty("home.pagination", ConfigConstants.DEFAULT_HOME_PAGINATION);
String homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
String homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
- // 11. 权限配置
+ // 12. 权限配置
String key = properties.getProperty("kk.Key", ConfigConstants.DEFAULT_KEY);
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 addTask = Boolean.parseBoolean(properties.getProperty("kk.addTask", ConfigConstants.DEFAULT_ADD_TASK));
String aesKey = properties.getProperty("ase.key", ConfigConstants.DEFAULT_AES_KEY);
- // 12. UserAgent配置
+
+ // 13. UserAgent配置
String userAgent = properties.getProperty("useragent", ConfigConstants.DEFAULT_USER_AGENT);
- // 13. Basic认证配置
+ // 14. Basic认证配置
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. 缓存配置
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
@@ -291,8 +324,6 @@ public class ConfigRefreshComponent {
// 4. FTP配置
ConfigConstants.setFtpUsernameValue(ftpUsername);
- ConfigConstants.setFtpPasswordValue(ftpPassword);
- ConfigConstants.setFtpControlEncodingValue(ftpControlEncoding);
// 5. 路径配置
ConfigConstants.setBaseUrlValue(baseUrl);
@@ -309,10 +340,6 @@ public class ConfigRefreshComponent {
ConfigConstants.setPdfBookmarkDisableValue(pdfBookmarkDisable);
ConfigConstants.setPdfDisableEditingValue(pdfDisableEditing);
ConfigConstants.setPdf2JpgDpiValue(pdf2JpgDpi);
- ConfigConstants.setPdfTimeoutValue(pdfTimeout);
- ConfigConstants.setPdfTimeout80Value(pdfTimeout80);
- ConfigConstants.setPdfTimeout200Value(pdfTimeout200);
- ConfigConstants.setPdfThreadValue(pdfThread);
// 8. CAD配置
ConfigConstants.setCadTimeoutValue(cadTimeout);
@@ -325,25 +352,65 @@ public class ConfigRefreshComponent {
ConfigConstants.setDeleteSourceFileValue(deleteSourceFile);
ConfigConstants.setDeleteCaptchaValue(deleteCaptcha);
- // 10. 首页配置
+ // 10. TIF配置
+ ConfigConstants.setTifTimeoutValue(tifTimeout);
+ ConfigConstants.setTifThreadValue(tifThread);
+
+ // 11. 首页配置
ConfigConstants.setBeianValue(beian);
ConfigConstants.setHomePageNumberValue(homePageNumber);
ConfigConstants.setHomePaginationValue(homePagination);
ConfigConstants.setHomePageSizeValue(homePageSize);
ConfigConstants.setHomeSearchValue(homeSearch);
- // 11. 权限配置
+ // 12. 权限配置
ConfigConstants.setKeyValue(key);
ConfigConstants.setPicturesPreviewValue(picturesPreview);
ConfigConstants.setGetCorsFileValue(getCorsFile);
ConfigConstants.setAddTaskValue(addTask);
ConfigConstants.setaesKeyValue(aesKey);
- // 12. UserAgent配置
+ // 13. UserAgent配置
ConfigConstants.setUserAgentValue(userAgent);
- // 13. Basic认证配置
+ // 14. Basic认证配置
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);
}
/**
diff --git a/server/src/main/java/cn/keking/service/FilePreview.java b/server/src/main/java/cn/keking/service/FilePreview.java
index 5a018b4d..a349473a 100644
--- a/server/src/main/java/cn/keking/service/FilePreview.java
+++ b/server/src/main/java/cn/keking/service/FilePreview.java
@@ -34,6 +34,7 @@ public interface FilePreview {
String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
String XLSX_FILE_PREVIEW_PAGE = "officeweb";
String CSV_FILE_PREVIEW_PAGE = "csv";
+ String WAITING_FILE_PREVIEW_PAGE = "waiting";
String filePreviewHandle(String url, Model model, FileAttribute fileAttribute);
}
diff --git a/server/src/main/java/cn/keking/service/Mediatomp4Service.java b/server/src/main/java/cn/keking/service/Mediatomp4Service.java
new file mode 100644
index 00000000..72e577aa
--- /dev/null
+++ b/server/src/main/java/cn/keking/service/Mediatomp4Service.java
@@ -0,0 +1,621 @@
+package cn.keking.service;
+
+import cn.keking.config.ConfigConstants;
+import cn.keking.model.FileAttribute;
+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.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
+
+
+/**
+ * 视频转换服务 - JavaCV最佳方案
+ * 支持:1) 超时控制 2) 进程监控 3) 资源清理 4) 进度反馈
+ */
+@Component
+public class Mediatomp4Service {
+
+ private static final Logger logger = LoggerFactory.getLogger(Mediatomp4Service.class);
+
+ private static final String MP4 = "mp4";
+
+ // 转换任务管理器
+ private static final Map activeTasks = new ConcurrentHashMap<>();
+
+ // 使用带监控的线程池
+ private static final ExecutorService videoConversionExecutor = Executors.newVirtualThreadPerTaskExecutor();
+
+ // 监控线程池 - 用于超时监控
+ private static final ScheduledExecutorService monitorExecutor =
+ Executors.newScheduledThreadPool(2, r -> {
+ Thread t = new Thread(r, "ffmpeg-monitor-" + System.currentTimeMillis());
+ t.setDaemon(true);
+ return t;
+ });
+
+ /**
+ * 转换任务上下文
+ */
+ private static class ConversionContext {
+ final FFmpegFrameGrabber grabber;
+ final FFmpegFrameRecorder recorder;
+ final AtomicBoolean cancelled;
+ final AtomicLong processedFrames;
+ final ReentrantLock lock;
+ volatile boolean completed;
+
+ ConversionContext(FFmpegFrameGrabber grabber, FFmpegFrameRecorder recorder) {
+ this.grabber = grabber;
+ this.recorder = recorder;
+ this.cancelled = new AtomicBoolean(false);
+ this.processedFrames = new AtomicLong(0);
+ this.lock = new ReentrantLock();
+ this.completed = false;
+ }
+ }
+
+ /**
+ * 转换任务包装
+ */
+ private static class ConversionTask {
+ final String taskId;
+ final CompletableFuture future;
+ final ConversionContext context;
+ final Thread conversionThread;
+ volatile ScheduledFuture> timeoutFuture;
+
+ ConversionTask(String taskId, CompletableFuture future,
+ ConversionContext context, Thread conversionThread) {
+ this.taskId = taskId;
+ this.future = future;
+ this.context = context;
+ this.conversionThread = conversionThread;
+ }
+ }
+
+ /**
+ * 异步转换方法(带任务ID和超时控制)
+ */
+ public static CompletableFuture convertToMp4Async(
+ String filePath, String outFilePath, FileAttribute fileAttribute) {
+
+ String taskId = generateTaskId(filePath);
+ CompletableFuture resultFuture = new CompletableFuture<>();
+
+ // 创建转换线程
+ Thread conversionThread = new Thread(() -> {
+ try {
+ boolean result = convertToMp4WithCancellation(filePath, outFilePath,
+ fileAttribute, taskId, resultFuture);
+ resultFuture.complete(result);
+ } catch (Exception e) {
+ resultFuture.completeExceptionally(e);
+ } finally {
+ activeTasks.remove(taskId);
+ }
+ }, "ffmpeg-convert-" + taskId);
+
+ conversionThread.setDaemon(true);
+
+ // 启动任务
+ try {
+ // 先进行预检查
+ preCheckFiles(filePath, outFilePath, fileAttribute);
+
+ // 启动转换线程
+ conversionThread.start();
+
+ // 设置超时监控
+ File inputFile = new File(filePath);
+ long fileSizeMB = inputFile.length() / (1024 * 1024);
+ scheduleTimeoutMonitor(taskId, calculateTimeout(fileSizeMB));
+ return resultFuture;
+
+ } catch (Exception e) {
+ resultFuture.completeExceptionally(e);
+ cleanupFailedFile(outFilePath);
+ return resultFuture;
+ }
+ }
+
+ /**
+ * 带取消支持的同步转换方法(核心改进)
+ */
+ private static boolean convertToMp4WithCancellation(
+ String filePath, String outFilePath, FileAttribute fileAttribute,
+ String taskId, CompletableFuture resultFuture) throws Exception {
+
+ FFmpegFrameGrabber frameGrabber = null;
+ FFmpegFrameRecorder recorder = null;
+ ConversionContext context = null;
+
+ try {
+ File sourceFile = new File(filePath);
+ if (!sourceFile.exists()) {
+ throw new FileNotFoundException("源文件不存在: " + filePath);
+ }
+
+ File desFile = new File(outFilePath);
+ if (desFile.exists() && !fileAttribute.forceUpdatedCache()) {
+ logger.info("目标文件已存在,跳过转换: {}", outFilePath);
+ return true;
+ }
+
+ // 初始化抓取器
+ frameGrabber = new FFmpegFrameGrabber(sourceFile);
+ frameGrabber.setOption("stimeout", "10000000"); // 10秒超时
+ frameGrabber.start();
+
+ // 创建录制器
+ recorder = new FFmpegFrameRecorder(
+ desFile,
+ frameGrabber.getImageWidth(),
+ frameGrabber.getImageHeight(),
+ Math.max(frameGrabber.getAudioChannels(), 0)
+ );
+
+ configureRecorder(recorder, frameGrabber);
+ recorder.start();
+
+ // 创建任务上下文
+ context = new ConversionContext(frameGrabber, recorder);
+
+ // 注册任务
+ ConversionTask task = new ConversionTask(taskId, resultFuture, context,
+ Thread.currentThread());
+ activeTasks.put(taskId, task);
+
+ logger.info("开始转换任务 {}: {} -> {}", taskId, filePath, outFilePath);
+
+ // 核心:使用非阻塞方式读取帧
+ return processFramesWithTimeout(frameGrabber, recorder, context, taskId);
+
+ } catch (Exception e) {
+ // 检查是否是取消操作
+ if (context != null && context.cancelled.get()) {
+ logger.info("转换任务 {} 被取消", taskId);
+ throw new CancellationException("转换被用户取消");
+ }
+ throw e;
+ } finally {
+ // 标记完成
+ if (context != null) {
+ context.completed = true;
+ }
+ // 清理资源
+ closeResources(frameGrabber, recorder);
+ // 清理失败文件
+ if (context != null && context.cancelled.get()) {
+ cleanupFailedFile(outFilePath);
+ }
+ }
+ }
+
+ /**
+ * 带超时控制的帧处理(核心改进)
+ */
+ private static boolean processFramesWithTimeout(
+ FFmpegFrameGrabber grabber, FFmpegFrameRecorder recorder,
+ ConversionContext context, String taskId) throws Exception {
+
+ long frameCount = 0;
+ long startTime = System.currentTimeMillis();
+ long lastFrameTime = startTime;
+ int consecutiveNullFrames = 0; // 连续读取到null帧的次数
+ final int MAX_CONSECUTIVE_NULL_FRAMES = 10; // 最大连续null帧次数
+
+ // 设置读取超时
+ grabber.setTimeout(5000); // 5秒读取超时
+
+ try {
+ Frame frame;
+ while (!context.cancelled.get()) {
+ // 检查超时:如果30秒没有新帧,认为超时
+ if (System.currentTimeMillis() - lastFrameTime > 30000) {
+ logger.warn("任务 {} 帧读取超时,可能文件损坏", taskId);
+ throw new TimeoutException("帧读取超时");
+ }
+
+ // 尝试抓取帧
+ frame = grabber.grabFrame();
+
+ if (frame == null) {
+ consecutiveNullFrames++;
+
+ // 检查是否达到最大连续null帧次数
+ if (consecutiveNullFrames >= MAX_CONSECUTIVE_NULL_FRAMES) {
+ // 检查是否真的结束了
+ if (grabber.getLengthInFrames() > 0 &&
+ frameCount >= grabber.getLengthInFrames()) {
+ logger.debug("任务 {} 正常结束,总帧数: {}", taskId, frameCount);
+ break;
+ } else {
+ logger.warn("任务 {} 连续读取到 {} 个null帧,可能文件已结束或损坏",
+ taskId, consecutiveNullFrames);
+ break;
+ }
+ }
+
+ // 短暂等待后重试,但避免忙等待
+ try {
+ Thread.sleep(50); // 减少sleep时间
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ if (context.cancelled.get()) {
+ throw new CancellationException("转换被取消");
+ }
+ throw e;
+ }
+ continue;
+ }
+
+ // 成功获取到帧,重置计数器
+ consecutiveNullFrames = 0;
+ lastFrameTime = System.currentTimeMillis();
+ frameCount++;
+
+ // 记录帧
+ recorder.record(frame);
+
+ // 更新上下文
+ context.processedFrames.set(frameCount);
+
+ // 定期日志输出
+ if (frameCount % 500 == 0) {
+ long elapsed = System.currentTimeMillis() - startTime;
+ double fps = (frameCount * 1000.0) / elapsed;
+ logger.debug("任务 {}: 已处理 {} 帧, 平均速度: {} fps",
+ taskId, frameCount, String.format("%.2f", fps));
+
+ // 检查是否被取消
+ if (context.cancelled.get()) {
+ logger.info("任务 {} 在处理中被取消", taskId);
+ return false;
+ }
+ }
+
+ // 检查文件大小增长(防止无限循环)
+ if (frameCount % 1000 == 0) {
+ checkProgress(taskId, frameCount, startTime);
+ }
+ }
+
+ // 完成录制
+ recorder.stop();
+ recorder.close();
+
+ long totalTime = System.currentTimeMillis() - startTime;
+ double fps = totalTime > 0 ? (frameCount * 1000.0) / totalTime : 0;
+ logger.info("任务 {} 转换完成: {} 帧, 耗时: {}ms, 平均速度: {} fps",
+ taskId, frameCount, totalTime, String.format("%.2f", fps));
+
+ return true;
+
+ } catch (Exception e) {
+ // 如果是取消操作,不记录为错误
+ if (context.cancelled.get()) {
+ logger.info("任务 {} 在处理中被取消", taskId);
+ throw new CancellationException("转换被取消");
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 安全取消指定的转换任务(不中断线程)
+ */
+ public static void cancelConversion(String taskId) {
+ ConversionTask task = activeTasks.get(taskId);
+ if (task != null) {
+ logger.info("安全取消转换任务: {}", taskId);
+
+ // 如果已经完成,则直接移除任务并返回
+ if (task.context.completed) {
+ logger.info("转换任务 {} 已经完成,无需取消", taskId);
+ activeTasks.remove(taskId);
+ return;
+ }
+
+ // 标记为取消(不要中断线程)
+ task.context.cancelled.set(true);
+
+ // 重要:不要中断线程!让线程自然结束
+ // 中断线程可能导致FFmpeg原生代码崩溃
+
+ // 安全地关闭资源
+ safeCloseResources(task.context);
+
+ // 取消超时监控
+ if (task.timeoutFuture != null && !task.timeoutFuture.isDone()) {
+ task.timeoutFuture.cancel(true);
+ }
+
+ // 从活跃任务中移除
+ activeTasks.remove(taskId);
+
+ logger.info("转换任务 {} 已安全取消", taskId);
+ }
+ }
+
+ /**
+ * 安全关闭资源(替代forceCloseResources)
+ */
+ private static void safeCloseResources(ConversionContext context) {
+ if (context == null) return;
+
+ context.lock.lock();
+ try {
+ // 使用独立的线程进行资源关闭,避免阻塞当前线程
+ Thread cleanupThread = new Thread(() -> {
+ try {
+ // 给转换线程一点时间响应取消标志
+ Thread.sleep(1000);
+
+ // 关闭recorder
+ if (context.recorder != null) {
+ try {
+ if (!context.completed) {
+ try {
+ context.recorder.stop();
+ } catch (Exception e) {
+ // 忽略,可能已经停止
+ }
+ }
+ context.recorder.close();
+ } catch (Exception e) {
+ logger.debug("安全关闭recorder时发生异常", e);
+ }
+ }
+
+ // 关闭grabber
+ if (context.grabber != null) {
+ try {
+ context.grabber.stop();
+ context.grabber.close();
+ } catch (Exception e) {
+ logger.debug("安全关闭grabber时发生异常", e);
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("清理线程异常", e);
+ }
+ }, "ffmpeg-cleanup-" + context.hashCode());
+
+ cleanupThread.setDaemon(true);
+ cleanupThread.start();
+
+ } finally {
+ context.lock.unlock();
+ }
+ }
+
+ /**
+ * 配置超时监控
+ */
+ private static void scheduleTimeoutMonitor(String taskId, long timeoutSeconds) {
+ ScheduledFuture> timeoutFuture = monitorExecutor.schedule(() -> {
+ ConversionTask task = activeTasks.get(taskId);
+ if (task != null && !task.context.completed) {
+ logger.warn("任务 {} 超时 ({}秒),开始强制终止", taskId, timeoutSeconds);
+ cancelConversion(taskId);
+ task.future.completeExceptionally(
+ new TimeoutException("转换超时: " + timeoutSeconds + "秒")
+ );
+ }
+ }, timeoutSeconds, TimeUnit.SECONDS);
+
+ // 保存超时future引用
+ ConversionTask task = activeTasks.get(taskId);
+ if (task != null) {
+ task.timeoutFuture = timeoutFuture;
+ }
+ }
+
+ /**
+ * 计算超时时间(根据文件大小)
+ */
+ public static 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 static void checkProgress(String taskId, long frameCount, long startTime) {
+ long elapsed = System.currentTimeMillis() - startTime;
+ if (elapsed > 0) {
+ double fps = (frameCount * 1000.0) / elapsed;
+ if (fps < 1.0) { // 速度太慢,可能有问题
+ logger.warn("任务 {} 转换速度过慢: {} fps", taskId, String.format("%.2f", fps));
+ }
+ }
+ }
+
+ /**
+ * 生成任务ID
+ */
+ private static String generateTaskId(String filePath) {
+ return "task-" + filePath.hashCode() + "-" + System.currentTimeMillis();
+ }
+
+ /**
+ * 预检查文件
+ */
+ private static void preCheckFiles(String filePath, String outFilePath,
+ FileAttribute fileAttribute) throws Exception {
+ File sourceFile = new File(filePath);
+ if (!sourceFile.exists()) {
+ throw new FileNotFoundException("源文件不存在: " + filePath);
+ }
+
+ File desFile = new File(outFilePath);
+ if (desFile.exists()) {
+ if (fileAttribute.forceUpdatedCache()) {
+ if (!desFile.delete()) {
+ throw new IOException("无法删除已存在的文件: " + outFilePath);
+ }
+ } else {
+ throw new IllegalStateException("目标文件已存在,跳过转换");
+ }
+ }
+ }
+
+ /**
+ * 配置录制器(与原方法保持一致)
+ */
+ private static void configureRecorder(FFmpegFrameRecorder recorder,
+ FFmpegFrameGrabber grabber) {
+ recorder.setFormat(MP4);
+ recorder.setFrameRate(grabber.getFrameRate());
+ recorder.setSampleRate(grabber.getSampleRate());
+ recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
+
+ int videoBitrate = grabber.getVideoBitrate();
+ if (videoBitrate <= 0) {
+ videoBitrate = 2000 * 1000;
+ }
+ recorder.setVideoBitrate(videoBitrate);
+
+ if (grabber.getAudioChannels() > 0) {
+ recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
+ recorder.setAudioBitrate(grabber.getAudioBitrate());
+ recorder.setAudioChannels(grabber.getAudioChannels());
+ }
+ }
+
+ /**
+ * 安全关闭资源(带异常保护)
+ */
+ private static void closeResources(FFmpegFrameGrabber grabber,
+ FFmpegFrameRecorder recorder) {
+ // 先关闭recorder
+ if (recorder != null) {
+ try {
+ // 尝试停止,但忽略可能的异常
+ try {
+ recorder.stop();
+ } catch (Exception e) {
+ // 忽略,可能已经停止
+ }
+ // 关闭
+ recorder.close();
+ } catch (Exception e) {
+ logger.warn("关闭recorder时发生异常(已忽略)", e);
+ }
+ }
+
+ // 再关闭grabber
+ if (grabber != null) {
+ try {
+ // 尝试停止,但忽略可能的异常
+ try {
+ grabber.stop();
+ } catch (Exception e) {
+ // 忽略,可能已经停止
+ }
+ // 关闭
+ grabber.close();
+ } catch (Exception e) {
+ logger.warn("关闭grabber时发生异常(已忽略)", e);
+ }
+ }
+ }
+
+ /**
+ * 清理失败的文件
+ */
+ private static void cleanupFailedFile(String filePath) {
+ try {
+ File failedFile = new File(filePath);
+ if (failedFile.exists() && failedFile.delete()) {
+ logger.debug("已删除失败的转换文件: {}", filePath);
+ }
+ } catch (Exception e) {
+ logger.warn("无法删除失败的转换文件: {}", filePath, e);
+ }
+ }
+
+ /**
+ * 优雅关闭服务(不强制终止)
+ */
+ public static void shutdown() {
+ logger.info("开始优雅关闭视频转换服务...");
+
+ // 标记所有任务为取消,但不强制关闭
+ for (String taskId : activeTasks.keySet()) {
+ ConversionTask task = activeTasks.get(taskId);
+ if (task != null && !task.context.completed) {
+ logger.info("标记任务 {} 为取消状态", taskId);
+ task.context.cancelled.set(true);
+ }
+ }
+
+ // 等待所有任务自然结束(最大等待30秒)
+ long startTime = System.currentTimeMillis();
+ while (!activeTasks.isEmpty() &&
+ (System.currentTimeMillis() - startTime) < 30000) {
+ try {
+ Thread.sleep(1000);
+ logger.info("等待 {} 个转换任务自然结束...", activeTasks.size());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ // 关闭监控线程池
+ if (monitorExecutor != null && !monitorExecutor.isShutdown()) {
+ try {
+ monitorExecutor.shutdown();
+ if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ logger.info("监控线程池未完全关闭,但将继续关闭流程");
+ }
+ } catch (Exception e) {
+ logger.warn("关闭监控线程池时发生异常", e);
+ }
+ }
+
+ // 关闭转换线程池
+ if (videoConversionExecutor != null && !videoConversionExecutor.isShutdown()) {
+ try {
+ videoConversionExecutor.shutdown();
+ if (!videoConversionExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
+ logger.info("转换线程池未完全关闭,但将继续关闭流程");
+ }
+ } catch (Exception e) {
+ logger.warn("关闭转换线程池时发生异常", e);
+ }
+ }
+
+ // 最后清理剩余的活动任务
+ if (!activeTasks.isEmpty()) {
+ logger.warn("仍有 {} 个转换任务未完成,将强制移除", activeTasks.size());
+ activeTasks.clear();
+ }
+
+ logger.info("视频转换服务优雅关闭完成");
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/OfficeToPdfService.java b/server/src/main/java/cn/keking/service/OfficeToPdfService.java
index e82e80d7..50df239c 100644
--- a/server/src/main/java/cn/keking/service/OfficeToPdfService.java
+++ b/server/src/main/java/cn/keking/service/OfficeToPdfService.java
@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.File;
+import java.time.Duration;
+import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -28,6 +30,7 @@ public class OfficeToPdfService {
public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException {
+ Instant startTime = Instant.now();
File outputFile = new File(outputFilePath_end);
// 假如目标路径不存在,则新建该路径
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
@@ -65,7 +68,42 @@ public class OfficeToPdfService {
} else {
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);
}
-}
+}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/PdfToJpgService.java b/server/src/main/java/cn/keking/service/PdfToJpgService.java
index f07a3f33..79a57327 100644
--- a/server/src/main/java/cn/keking/service/PdfToJpgService.java
+++ b/server/src/main/java/cn/keking/service/PdfToJpgService.java
@@ -22,27 +22,27 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;
-import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
- * @author yudian-it
+ * PDF转JPG服务 - 高性能优化版本
*/
@Component
public class PdfToJpgService {
private final FileHandlerService fileHandlerService;
- // PDF转换专用线程池
- private ExecutorService pdfConversionPool;
- private ThreadPoolExecutor pdfThreadPoolExecutor;
-
+ // 使用线程池替代虚拟线程,便于控制并发数
+ private ExecutorService threadPoolExecutor;
private static final Logger logger = LoggerFactory.getLogger(PdfToJpgService.class);
private static final String PDF_PASSWORD_MSG = "password";
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg";
+ private static final int BATCH_SIZE = 20;
+ private static final int PARALLEL_BATCH_THRESHOLD = 100;
- // 最大并行页数阈值
- private static final int MAX_PARALLEL_PAGES = 20;
+ // 性能监控
+ private final AtomicInteger activeTaskCount = new AtomicInteger(0);
+ private final AtomicInteger totalCompletedTasks = new AtomicInteger(0);
public PdfToJpgService(FileHandlerService fileHandlerService) {
this.fileHandlerService = fileHandlerService;
@@ -50,119 +50,46 @@ public class PdfToJpgService {
@PostConstruct
public void init() {
- try {
- int threadCount = getPdfThreadPoolSize();
- int queueCapacity = threadCount * 10;
+ // 使用固定大小的线程池,便于控制并发数
+ int maxThreads = ConfigConstants.getPdfMaxThreads();
+ this.threadPoolExecutor = new ThreadPoolExecutor(
+ maxThreads, // 核心线程数
+ maxThreads, // 最大线程数
+ 60L, TimeUnit.SECONDS, // 空闲线程存活时间
+ new LinkedBlockingQueue<>(100), // 任务队列
+ Executors.defaultThreadFactory(),
+ new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行
+ );
- AtomicInteger threadNum = new AtomicInteger(1);
- pdfThreadPoolExecutor = new ThreadPoolExecutor(
- threadCount,
- threadCount,
- 60L,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(queueCapacity),
- r -> {
- Thread t = new Thread(r);
- t.setName("pdf-convert-pool-" + threadNum.getAndIncrement());
- t.setUncaughtExceptionHandler((thread, throwable) ->
- logger.error("PDF转换线程未捕获异常: {}", thread.getName(), throwable));
- return t;
- },
- new ThreadPoolExecutor.CallerRunsPolicy()
- );
-
- pdfThreadPoolExecutor.allowCoreThreadTimeOut(true);
- pdfConversionPool = pdfThreadPoolExecutor;
-
- logger.info("PDF转换线程池初始化完成,线程数: {}, 队列容量: {}",
- threadCount, queueCapacity);
-
- } catch (Exception e) {
- logger.error("PDF转换线程池初始化失败", e);
- int defaultThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
- pdfConversionPool = Executors.newFixedThreadPool(defaultThreads);
- logger.warn("使用默认PDF线程池配置,线程数: {}", defaultThreads);
- }
- }
-
- private int getPdfThreadPoolSize() {
- try {
- String pdfThreadConfig = System.getProperty("pdf.thread.count");
- int threadCount;
- if (pdfThreadConfig != null) {
- threadCount = Integer.parseInt(pdfThreadConfig);
- } else {
- threadCount = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
- }
-
- if (threadCount <= 0) {
- threadCount = Runtime.getRuntime().availableProcessors();
- logger.warn("PDF线程数配置无效,使用CPU核心数: {}", threadCount);
- }
-
- int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
- if (threadCount > maxThreads) {
- logger.warn("PDF线程数配置过大({}),限制为: {}", threadCount, maxThreads);
- threadCount = maxThreads;
- }
- return threadCount;
- } catch (Exception e) {
- logger.error("获取PDF线程数配置失败", e);
- return Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
- }
+ logger.info("PDF转换线程池初始化完成,最大线程数: {}", maxThreads);
}
@PreDestroy
public void shutdown() {
- if (pdfConversionPool != null && !pdfConversionPool.isShutdown()) {
- gracefulShutdown(pdfConversionPool, getShutdownTimeout());
- }
- }
-
- private long getShutdownTimeout() {
- try {
- String pdfTimeout = System.getProperty("pdf.timeout");
- if (pdfTimeout != null) {
- return Long.parseLong(pdfTimeout);
- }
- return Long.parseLong(ConfigConstants.getCadTimeout());
- } catch (Exception e) {
- logger.warn("获取PDF关闭超时时间失败,使用默认值60秒", e);
- return 60L;
- }
- }
-
- private void gracefulShutdown(ExecutorService executor, long timeoutSeconds) {
- logger.info("开始关闭{}...", "PDF转换线程池");
- executor.shutdown();
-
- try {
- if (!executor.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) {
- logger.warn("{}超时未关闭,尝试强制关闭...", "PDF转换线程池");
- executor.shutdownNow();
-
- if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
- logger.error("{}无法完全关闭", "PDF转换线程池");
- } else {
- logger.info("{}已强制关闭", "PDF转换线程池");
+ if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) {
+ threadPoolExecutor.shutdown();
+ try {
+ if (!threadPoolExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+ threadPoolExecutor.shutdownNow();
}
- } else {
- logger.info("{}已正常关闭", "PDF转换线程池");
+ } catch (InterruptedException e) {
+ threadPoolExecutor.shutdownNow();
+ Thread.currentThread().interrupt();
}
- } catch (InterruptedException e) {
- logger.error("{}关闭时被中断", "PDF转换线程池", e);
- executor.shutdownNow();
- Thread.currentThread().interrupt();
+ logger.info("PDF转换服务已关闭");
}
}
+ /**
+ * PDF转JPG - 高性能主方法
+ */
public List pdf2jpg(String fileNameFilePath, String pdfFilePath,
String pdfName, FileAttribute fileAttribute) throws Exception {
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
- // 1. 检查缓存
+ // 检查缓存
if (!forceUpdatedCache) {
List cacheResult = fileHandlerService.loadPdf2jpgCache(pdfFilePath);
if (!CollectionUtils.isEmpty(cacheResult)) {
@@ -170,14 +97,14 @@ public class PdfToJpgService {
}
}
- // 2. 验证文件存在
+ // 验证文件存在
File pdfFile = new File(fileNameFilePath);
if (!pdfFile.exists()) {
logger.error("PDF文件不存在: {}", fileNameFilePath);
return null;
}
- // 3. 创建输出目录
+ // 创建输出目录
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
File path = new File(folder);
@@ -186,35 +113,282 @@ public class PdfToJpgService {
throw new IOException("无法创建输出目录");
}
- // 4. 加载PDF文档获取页数
- int pageCount = 0;
+ // 加载PDF文档获取页数
+ int pageCount;
try (PDDocument tempDoc = Loader.loadPDF(pdfFile, filePassword)) {
pageCount = tempDoc.getNumberOfPages();
- logger.info("PDF文件总页数: {}, 文件: {}", pageCount, pdfFilePath);
} catch (IOException e) {
handlePdfLoadException(e, pdfFilePath);
throw new Exception("PDF文件加载失败", e);
}
- // 5. 根据页数决定转换策略
+ // 检查线程池负载
+ checkThreadPoolLoad();
+
+ // 根据页数选择最佳转换策略
List imageUrls;
- if (pageCount > MAX_PARALLEL_PAGES) {
- // 大文件使用新方案:每页独立加载PDF
- imageUrls = convertParallelIndependent(pdfFile, filePassword, pdfFilePath, folder, pageCount);
+ long startTime = System.currentTimeMillis();
+
+ if (pageCount <= PARALLEL_BATCH_THRESHOLD) {
+ imageUrls = convertOptimizedParallel(pdfFile, filePassword, pdfFilePath, folder, pageCount);
} else {
- // 小文件使用串行处理(稳定)
- imageUrls = convertSequentially(pdfFile, filePassword, pdfFilePath, folder, pageCount);
+ imageUrls = convertHighPerformance(pdfFile, filePassword, pdfFilePath, folder, pageCount);
}
- // 6. 缓存结果
+ long elapsedTime = System.currentTimeMillis() - startTime;
+
+ // 缓存结果
if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) {
fileHandlerService.addPdf2jpgCache(pdfFilePath, pageCount);
}
- logger.info("PDF转换完成,成功转换{}页,文件: {}", imageUrls.size(), pdfFilePath);
+ // 性能统计
+ logger.info("PDF转换完成: 总页数={}, 耗时={}ms, DPI={}, 文件: {}, 活动任务: {}",
+ pageCount, elapsedTime, ConfigConstants.getOptimizedDpi(pageCount),
+ pdfFilePath, activeTaskCount.get());
+
return imageUrls;
}
+ /**
+ * 检查线程池负载
+ */
+ private void checkThreadPoolLoad() {
+ if (threadPoolExecutor instanceof ThreadPoolExecutor pool) {
+ int activeCount = pool.getActiveCount();
+ long taskCount = pool.getTaskCount();
+ long completedTaskCount = pool.getCompletedTaskCount();
+ int queueSize = pool.getQueue().size();
+
+ logger.debug("线程池状态: 活动线程={}, 队列大小={}, 总任务={}, 已完成={}",
+ activeCount, queueSize, taskCount, completedTaskCount);
+
+ if (queueSize > 50) {
+ logger.warn("PDF转换任务队列堆积,当前队列大小: {}", queueSize);
+ }
+ }
+ }
+
+ /**
+ * 高性能并行转换 - 独立加载每个批次(针对100页以上的大文件)
+ */
+ private List convertHighPerformance(File pdfFile, String filePassword,
+ String pdfFilePath, String folder, int pageCount) {
+ List imageUrls = Collections.synchronizedList(new ArrayList<>(pageCount));
+ AtomicInteger successCount = new AtomicInteger(0);
+ AtomicInteger errorCount = new AtomicInteger(0);
+ int batchCount = (pageCount + BATCH_SIZE - 1) / BATCH_SIZE;
+ long[] totalBatchTime = new long[]{0};
+
+ logger.info("使用高性能独立加载并行转换,总页数: {}, 批次数: {}, DPI: {}, 超时: {}秒",
+ pageCount, batchCount, ConfigConstants.getOptimizedDpi(pageCount),
+ calculateTimeout(pageCount));
+
+ List> batchFutures = new ArrayList<>();
+
+ for (int batchIndex = 0; batchIndex < batchCount; batchIndex++) {
+ final int currentBatch = batchIndex;
+ final int batchStart = batchIndex * BATCH_SIZE;
+ final int batchEnd = Math.min(batchStart + BATCH_SIZE, pageCount);
+
+ CompletableFuture batchFuture = CompletableFuture.runAsync(() -> {
+ activeTaskCount.incrementAndGet();
+ long batchStartTime = System.currentTimeMillis();
+ try {
+ try (PDDocument batchDoc = Loader.loadPDF(pdfFile, filePassword)) {
+ batchDoc.setResourceCache(new NotResourceCache());
+ PDFRenderer renderer = new PDFRenderer(batchDoc);
+ renderer.setSubsamplingAllowed(true);
+
+ // 直接使用配置的DPI值
+ int dpi = ConfigConstants.getOptimizedDpi(pageCount);
+
+ int pagesInBatch = 0;
+ for (int pageIndex = batchStart; pageIndex < batchEnd; pageIndex++) {
+ try {
+ String imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
+ BufferedImage image = renderer.renderImageWithDPI(
+ pageIndex,
+ dpi,
+ ImageType.RGB
+ );
+
+ ImageIOUtil.writeImage(image, imageFilePath, dpi);
+ image.flush();
+
+ String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex);
+ synchronized (imageUrls) {
+ imageUrls.add(imageUrl);
+ }
+
+ successCount.incrementAndGet();
+ pagesInBatch++;
+
+ } catch (Exception e) {
+ errorCount.incrementAndGet();
+ logger.error("转换页 {} 失败: {}", pageIndex, e.getMessage());
+ }
+ }
+
+ long batchTime = System.currentTimeMillis() - batchStartTime;
+ synchronized (this) {
+ totalBatchTime[0] += batchTime;
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("批次{}完成: 转换{}页, 耗时: {}ms",
+ currentBatch, pagesInBatch, batchTime);
+ }
+ }
+ } catch (Exception e) {
+ logger.error("批次{}处理失败: {}", currentBatch, e.getMessage());
+ errorCount.addAndGet(batchEnd - batchStart);
+ } finally {
+ activeTaskCount.decrementAndGet();
+ totalCompletedTasks.incrementAndGet();
+ }
+ }, threadPoolExecutor);
+
+ batchFutures.add(batchFuture);
+ }
+
+ // 等待所有批次完成
+ int timeout = calculateTimeout(pageCount);
+ long waitStartTime = System.currentTimeMillis();
+
+ try {
+ CompletableFuture allBatches = CompletableFuture.allOf(
+ batchFutures.toArray(new CompletableFuture[0])
+ );
+ allBatches.get(timeout, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ logger.warn("PDF转换超时,已转换页数: {},超时时间: {}秒", successCount.get(), timeout);
+ } catch (Exception e) {
+ logger.error("批量转换失败", e);
+ }
+
+ long waitTime = System.currentTimeMillis() - waitStartTime;
+
+ logger.info("批次转换统计: 总批次={}, 成功={}, 失败={}, DPI={}, 等待耗时={}ms",
+ batchCount, successCount.get(), errorCount.get(),
+ ConfigConstants.getOptimizedDpi(pageCount), waitTime);
+
+ // 按页码排序
+ return sortImageUrls(imageUrls);
+ }
+
+ /**
+ * 优化并行转换 - 线程安全的批处理模式(针对100页以内的文件)
+ */
+ private List convertOptimizedParallel(File pdfFile, String filePassword,
+ String pdfFilePath, String folder, int pageCount) {
+ int dpi = ConfigConstants.getOptimizedDpi(pageCount);
+
+ logger.info("使用高性能批处理并行转换,总页数: {}, DPI: {}, 超时: {}秒",
+ pageCount, dpi, calculateTimeout(pageCount));
+
+ // 按CPU核心数划分批次,优化并行度
+ int availableProcessors = Runtime.getRuntime().availableProcessors();
+ int optimalBatchSize = Math.max(1, pageCount / availableProcessors);
+ optimalBatchSize = Math.min(optimalBatchSize, 10); // 每批最多10页
+
+ logger.debug("可用处理器: {}, 推荐批次大小: {}", availableProcessors, optimalBatchSize);
+
+ List>> batchFutures = new ArrayList<>();
+ List allImageUrls = Collections.synchronizedList(new ArrayList<>(pageCount));
+
+ // 分批次并行处理
+ for (int batchStart = 0; batchStart < pageCount; batchStart += optimalBatchSize) {
+ final int startPage = batchStart;
+ final int endPage = Math.min(batchStart + optimalBatchSize, pageCount);
+
+ CompletableFuture> batchFuture = CompletableFuture.supplyAsync(() -> {
+ List batchImageUrls = new ArrayList<>(endPage - startPage);
+ activeTaskCount.incrementAndGet();
+
+ try {
+ // 每个批次独立加载PDF,处理一批页面(而不是一页)
+ try (PDDocument batchDoc = Loader.loadPDF(pdfFile, filePassword)) {
+ batchDoc.setResourceCache(new NotResourceCache());
+ PDFRenderer renderer = new PDFRenderer(batchDoc);
+ renderer.setSubsamplingAllowed(true);
+
+ for (int pageIndex = startPage; pageIndex < endPage; pageIndex++) {
+ try {
+ String imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
+ BufferedImage image = renderer.renderImageWithDPI(
+ pageIndex,
+ dpi,
+ ImageType.RGB
+ );
+
+ ImageIOUtil.writeImage(image, imageFilePath, dpi);
+ image.flush();
+
+ String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex);
+ batchImageUrls.add(imageUrl);
+
+ } catch (Exception e) {
+ logger.error("批次内转换页 {} 失败: {}", pageIndex, e.getMessage());
+ // 添加占位符URL
+ String placeholderUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex);
+ batchImageUrls.add(placeholderUrl);
+ }
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("批次 {}-{} 完成,转换 {} 页",
+ startPage, endPage - 1, batchImageUrls.size());
+ }
+ }
+ } catch (Exception e) {
+ logger.error("批次 {}-{} 加载失败: {}", startPage, endPage - 1, e.getMessage());
+ // 为整个批次添加占位符URL
+ for (int pageIndex = startPage; pageIndex < endPage; pageIndex++) {
+ batchImageUrls.add(fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex));
+ }
+ } finally {
+ activeTaskCount.decrementAndGet();
+ totalCompletedTasks.incrementAndGet();
+ }
+
+ return batchImageUrls;
+ }, threadPoolExecutor);
+
+ batchFutures.add(batchFuture);
+ }
+
+ // 等待所有批次完成并收集结果
+ CompletableFuture allBatches = CompletableFuture.allOf(
+ batchFutures.toArray(new CompletableFuture[0])
+ );
+
+ int timeout = calculateTimeout(pageCount);
+ try {
+ allBatches.get(timeout, TimeUnit.SECONDS);
+
+ // 收集所有批次的结果
+ for (CompletableFuture> future : batchFutures) {
+ try {
+ List batchUrls = future.getNow(null);
+ if (batchUrls != null) {
+ allImageUrls.addAll(batchUrls);
+ }
+ } catch (Exception e) {
+ // 忽略已完成的任务
+ }
+ }
+
+ } catch (TimeoutException e) {
+ logger.warn("PDF转换超时,已转换页数: {},超时时间: {}秒", allImageUrls.size(), timeout);
+ } catch (Exception e) {
+ logger.error("批次并行转换失败", e);
+ }
+
+ // 确保返回正确数量的URL
+ return sortImageUrls(allImageUrls);
+ }
+
/**
* 处理PDF加载异常
*/
@@ -233,167 +407,36 @@ public class PdfToJpgService {
}
/**
- * 串行转换(稳定方案)
+ * 计算超时时间 - 标准化配置,不使用计算
*/
- private List convertSequentially(File pdfFile, String filePassword,
- String pdfFilePath, String folder, int pageCount) {
- List imageUrls = new ArrayList<>(pageCount);
-
- try (PDDocument doc = Loader.loadPDF(pdfFile, filePassword)) {
- doc.setResourceCache(new NotResourceCache());
- PDFRenderer pdfRenderer = new PDFRenderer(doc);
- pdfRenderer.setSubsamplingAllowed(true);
-
- for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
- try {
- String imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
- BufferedImage image = pdfRenderer.renderImageWithDPI(
- pageIndex,
- ConfigConstants.getPdf2JpgDpi(),
- ImageType.RGB
- );
-
- ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
- image.flush();
-
- String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, pageIndex);
- imageUrls.add(imageUrl);
-
- logger.debug("串行转换页 {} 完成", pageIndex);
-
- } catch (Exception e) {
- logger.error("串行转换页 {} 失败: {}", pageIndex, e.getMessage());
- }
- }
- } catch (Exception e) {
- logger.error("串行转换PDF失败", e);
+ private int calculateTimeout(int pageCount) {
+ // 根据页数范围直接返回对应的超时时间配置
+ if (pageCount <= 50) {
+ return ConfigConstants.getPdfTimeoutSmall(); // 小文件:90秒
+ } else if (pageCount <= 200) {
+ return ConfigConstants.getPdfTimeoutMedium(); // 中等文件:180秒
+ } else if (pageCount <= 500) {
+ return ConfigConstants.getPdfTimeoutLarge(); // 大文件:300秒
+ } else {
+ return ConfigConstants.getPdfTimeoutXLarge(); // 超大文件:600秒
}
-
- return imageUrls;
}
/**
- * 并行转换 - 每个线程独立加载PDF(避免线程安全问题)
+ * 按页码排序
*/
- private List convertParallelIndependent(File pdfFile, String filePassword,
- String pdfFilePath, String folder, int pageCount) {
- List imageUrls = Collections.synchronizedList(new ArrayList<>());
- List> futures = new ArrayList<>();
- AtomicInteger successCount = new AtomicInteger(0);
- AtomicInteger errorCount = new AtomicInteger(0);
-
- // 提交页面转换任务
- for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
- final int currentPage = pageIndex;
-
- Future future = pdfConversionPool.submit(() -> {
- try {
- // 每个任务独立加载PDF,确保线程安全
- try (PDDocument pageDoc = Loader.loadPDF(pdfFile, filePassword)) {
- pageDoc.setResourceCache(new NotResourceCache());
- PDFRenderer renderer = new PDFRenderer(pageDoc);
- renderer.setSubsamplingAllowed(true);
-
- String imageFilePath = folder + File.separator + currentPage + PDF2JPG_IMAGE_FORMAT;
- BufferedImage image = renderer.renderImageWithDPI(
- currentPage,
- ConfigConstants.getPdf2JpgDpi(),
- ImageType.RGB
- );
-
- ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
- image.flush();
-
- String imageUrl = fileHandlerService.getPdf2jpgUrl(pdfFilePath, currentPage);
- synchronized (imageUrls) {
- imageUrls.add(imageUrl);
- }
-
- successCount.incrementAndGet();
- logger.debug("并行转换页 {} 完成", currentPage);
- return true;
- }
- } catch (Exception e) {
- errorCount.incrementAndGet();
- logger.error("并行转换页 {} 失败: {}", currentPage, e.getMessage());
- return false;
- }
- });
-
- futures.add(future);
- }
-
- // 等待所有任务完成
- int timeout = calculateTimeout(pageCount);
- long startTime = System.currentTimeMillis();
-
- for (Future future : futures) {
+ private List sortImageUrls(List imageUrls) {
+ List sortedImageUrls = new ArrayList<>(imageUrls);
+ sortedImageUrls.sort((url1, url2) -> {
try {
- future.get(timeout, TimeUnit.SECONDS);
- } catch (TimeoutException e) {
- logger.warn("页面转换任务超时,取消剩余任务");
- for (Future f : futures) {
- if (!f.isDone()) {
- f.cancel(true);
- }
- }
- break;
- } catch (Exception e) {
- logger.error("页面转换任务执行失败", e);
- }
-
- // 检查是否超时
- if (System.currentTimeMillis() - startTime > timeout * 1000L) {
- logger.warn("PDF转换整体超时,取消剩余任务");
- for (Future f : futures) {
- if (!f.isDone()) {
- f.cancel(true);
- }
- }
- break;
- }
- }
-
- long elapsedTime = System.currentTimeMillis() - startTime;
- logger.info("并行转换统计: 成功={}, 失败={}, 总页数={}, 耗时={}ms",
- successCount.get(), errorCount.get(), pageCount, elapsedTime);
-
- // 按页码排序
- imageUrls.sort(Comparator.comparingInt(url -> {
- try {
- String pageStr = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'));
- return Integer.parseInt(pageStr);
+ String pageStr1 = url1.substring(url1.lastIndexOf('/') + 1, url1.lastIndexOf('.'));
+ String pageStr2 = url2.substring(url2.lastIndexOf('/') + 1, url2.lastIndexOf('.'));
+ return Integer.compare(Integer.parseInt(pageStr1), Integer.parseInt(pageStr2));
} catch (Exception e) {
return 0;
}
- }));
-
- return imageUrls;
+ });
+ return sortedImageUrls;
}
- /**
- * 计算超时时间
- */
- private int calculateTimeout(int pageCount) {
- if (pageCount <= 50) {
- return ConfigConstants.getPdfTimeout();
- } else if (pageCount <= 200) {
- return ConfigConstants.getPdfTimeout80();
- } else {
- return ConfigConstants.getPdfTimeout200();
- }
- }
-
- /**
- * 监控线程池状态
- */
- public void monitorThreadPoolStatus() {
- if (pdfThreadPoolExecutor != null) {
- logger.info("PDF线程池状态: 活跃线程={}, 队列大小={}, 完成任务={}, 线程总数={}",
- pdfThreadPoolExecutor.getActiveCount(),
- pdfThreadPoolExecutor.getQueue().size(),
- pdfThreadPoolExecutor.getCompletedTaskCount(),
- pdfThreadPoolExecutor.getPoolSize());
- }
- }
}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/TifToPdfService.java b/server/src/main/java/cn/keking/service/TifToPdfService.java
new file mode 100644
index 00000000..6ee47a96
--- /dev/null
+++ b/server/src/main/java/cn/keking/service/TifToPdfService.java
@@ -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 convertTif2Jpg(String strInputFile, String strOutputFile,
+ boolean forceUpdatedCache) throws Exception {
+ String fileName = new File(strInputFile).getName();
+ Instant startTime = Instant.now();
+
+ try {
+ List 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 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 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 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 convertPagesVirtualThreads(List 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 imageUrls = Collections.synchronizedList(new ArrayList<>(pageCount));
+
+ Instant startTime = Instant.now();
+
+ try {
+ // 使用虚拟线程并行处理所有页面
+ List> 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 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> futures = new ArrayList<>(pageCount);
+
+ // 为每个页面创建处理任务
+ for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
+ final int currentPageIndex = pageIndex;
+ BufferedImage originalImage = images.get(pageIndex);
+
+ CompletableFuture 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 allFutures = CompletableFuture.allOf(
+ futures.toArray(new CompletableFuture[0])
+ );
+
+ // 设置超时
+ allFutures.completeOnTimeout(null, getConversionTimeout(), TimeUnit.SECONDS).join();
+
+ // 按顺序收集处理结果
+ List results = new ArrayList<>(pageCount);
+ for (CompletableFuture 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转换服务已关闭");
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/TifToService.java b/server/src/main/java/cn/keking/service/TifToService.java
deleted file mode 100644
index 8327a62f..00000000
--- a/server/src/main/java/cn/keking/service/TifToService.java
+++ /dev/null
@@ -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 convertTif2Jpg(String strInputFile, String strOutputFile,
- boolean forceUpdatedCache) throws Exception {
- return convertTif2Jpg(strInputFile, strOutputFile, forceUpdatedCache, true);
- }
-
- /**
- * TIF转JPG - 可选择是否启用并行处理
- * @param parallelProcessing 是否启用并行处理
- */
- public List 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 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 convertParallel(List images, String outputDirPath,
- String baseUrl, boolean forceUpdatedCache) {
- int pageCount = images.size();
- List> 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 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 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 convertSequentially(List images, String outputDirPath,
- String baseUrl, boolean forceUpdatedCache) throws Exception {
- List 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> futures = new ArrayList<>();
-
- // 提交所有页面加载任务
- for (int i = 1; i <= pages; i++) {
- final int pageNum = i;
- CompletableFuture 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();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java
index 36304da1..86d4d917 100644
--- a/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java
+++ b/server/src/main/java/cn/keking/service/impl/CadFilePreviewImpl.java
@@ -7,6 +7,7 @@ import cn.keking.service.CadToPdfService;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils;
+import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter;
@@ -16,6 +17,9 @@ import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/**
* @author chenjh
@@ -33,7 +37,13 @@ public class CadFilePreviewImpl implements FilePreview {
private final OtherFilePreviewImpl otherFilePreview;
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.otherFilePreview = otherFilePreview;
this.cadtopdfservice = cadtopdfservice;
@@ -43,41 +53,109 @@ public class CadFilePreviewImpl implements FilePreview {
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
// 预览Type,参数传了就取参数的,没传取系统默认
- String officePreviewType = fileAttribute.getOfficePreviewType() == null ? ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
+ String officePreviewType = fileAttribute.getOfficePreviewType() == null ?
+ ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
String baseUrl = BaseUrlFilter.getBaseUrl();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String fileName = fileAttribute.getName();
String cadPreviewType = ConfigConstants.getCadPreviewType();
String cacheName = fileAttribute.getCacheName();
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 response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
+
String filePath = response.getContent();
- boolean imageUrls = false;
if (StringUtils.hasText(outFilePath)) {
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) {
- logger.error("Failed to convert CAD file: {}", filePath, e);
- }
- if (!imageUrls) {
+ logger.error("Failed to start CAD conversion: {}", filePath, e);
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 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)) {
model.addAttribute("currentUrl", cacheName);
return TIFF_FILE_PREVIEW_PAGE;
@@ -85,10 +163,15 @@ public class CadFilePreviewImpl implements FilePreview {
model.addAttribute("currentUrl", cacheName);
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);
return PDF_FILE_PREVIEW_PAGE;
}
-}
+}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java
index 9409071d..2591e649 100644
--- a/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java
+++ b/server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java
@@ -6,23 +6,19 @@ import cn.keking.model.FileType;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
+import cn.keking.service.Mediatomp4Service;
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.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
-import org.springframework.util.CollectionUtils;
import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
/**
* @author : kl
@@ -36,14 +32,6 @@ public class MediaFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
- private static final String mp4 = "mp4";
-
- // 添加线程池管理视频转换任务
- private static final ExecutorService videoConversionExecutor =
- Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
-
- // 添加转换任务缓存,避免重复转换
- private static final Map> conversionTasks = new HashMap<>();
public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService;
@@ -61,235 +49,189 @@ public class MediaFilePreviewImpl implements FilePreview {
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式
boolean mediaTypes = false;
for (String temp : mediaTypesConvert) {
- if (suffix.equals(temp)) {
+ if (suffix.equalsIgnoreCase(temp)) {
mediaTypes = true;
break;
}
}
- if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的
- if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存
- ReturnResponse response = DownloadUtils.downLoad(fileAttribute, fileName);
- if (response.isFailure()) {
- return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
+ // 非HTTP协议或需要转换的文件
+ if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) {
+ // 检查缓存
+ File outputFile = new File(outFilePath);
+ 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 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 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);
return MEDIA_FILE_PREVIEW_PAGE;
}
+
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 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) {
- //1.检查开关是否开启
+ // 1.检查开关是否开启
if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
return mediaTypes;
}
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) {
- try {
- String videoCodec = grabber.getVideoCodecName();
- String audioCodec = grabber.getAudioCodecName();
-
- boolean videoCompatible = videoCodec != null &&
- (videoCodec.toLowerCase().contains("h264") ||
- videoCodec.toLowerCase().contains("h.264"));
-
- boolean audioCompatible = audioCodec != null &&
- (audioCodec.toLowerCase().contains("aac") ||
- audioCodec.toLowerCase().contains("mp3"));
-
- return videoCompatible && audioCompatible;
- } catch (Exception e) {
- logger.debug("无法获取编解码器信息", e);
- return false;
+ private String getErrorMessage(Exception e) {
+ if (e instanceof CancellationException) {
+ return "转换被取消";
+ } else if (e instanceof TimeoutException) {
+ return "转换超时";
+ } else if (e.getMessage() != null) {
+ // 截取主要错误信息
+ String msg = e.getMessage();
+ if (msg.length() > 100) {
+ msg = msg.substring(0, 100) + "...";
+ }
+ return msg;
}
- }
-
- /**
- * 清理所有转换任务(应用关闭时调用)
- */
- public static void shutdown() {
- if (!CollectionUtils.isEmpty(conversionTasks)) {
- conversionTasks.values().forEach(task -> task.cancel(true));
- conversionTasks.clear();
- }
- videoConversionExecutor.shutdownNow();
+ return "未知错误";
}
}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java b/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java
index d931dd09..66aa5e84 100644
--- a/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java
+++ b/server/src/main/java/cn/keking/service/impl/TiffFilePreviewImpl.java
@@ -5,7 +5,7 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
-import cn.keking.service.TifToService;
+import cn.keking.service.TifToPdfService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
@@ -16,7 +16,6 @@ import java.util.List;
/**
* tiff 图片文件处理
- *
* @author kl (http://kailing.pub)
* @since 2021/2/8
*/
@@ -25,8 +24,8 @@ public class TiffFilePreviewImpl implements FilePreview {
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
- private final TifToService tiftoservice;
- public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview,TifToService tiftoservice) {
+ private final TifToPdfService tiftoservice;
+ public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview,TifToPdfService tiftoservice) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.tiftoservice = tiftoservice;
@@ -47,7 +46,7 @@ public class TiffFilePreviewImpl implements FilePreview {
String filePath = response.getContent();
if ("pdf".equalsIgnoreCase(tifPreviewType)) {
try {
- tiftoservice.convertTif2Pdf(filePath, outFilePath);
+ tiftoservice.convertTif2Pdf(filePath, outFilePath,forceUpdatedCache);
} catch (Exception e) {
if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) {
model.addAttribute("imgUrls", url);
@@ -59,7 +58,7 @@ public class TiffFilePreviewImpl implements FilePreview {
}
//是否保留TIFF源文件
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
- // KkFileUtils.deleteFileByPath(filePath);
+ KkFileUtils.deleteFileByPath(filePath);
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
diff --git a/server/src/main/java/cn/keking/utils/DownloadUtils.java b/server/src/main/java/cn/keking/utils/DownloadUtils.java
index 391cd609..98ab8723 100644
--- a/server/src/main/java/cn/keking/utils/DownloadUtils.java
+++ b/server/src/main/java/cn/keking/utils/DownloadUtils.java
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils;
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.LoggerFactory;
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_PORT = "ftp.control.port";
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();
@@ -51,12 +52,12 @@ public class DownloadUtils {
* @return 本地文件绝对路径
*/
public static ReturnResponse downLoad(FileAttribute fileAttribute, String fileName) {
- // 忽略ssl证书
+
String urlStr = null;
try {
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
} catch (Exception e) {
- logger.error("忽略SSL证书异常:", e);
+ logger.error("处理URL异常:", e);
}
ReturnResponse response = new ReturnResponse<>(0, "下载成功!!!", "");
String realPath = getRelFilePath(fileName, fileAttribute);
@@ -90,7 +91,10 @@ public class DownloadUtils {
if (!fileAttribute.getSkipDownLoad()) {
if (isHttpUrl(url)) {
File realFile = new File(realPath);
- CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
+
+ // 创建配置好的HttpClient
+ CloseableHttpClient httpClient = createConfiguredHttpClient();
+
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
@@ -111,10 +115,25 @@ public class DownloadUtils {
return null;
});
} 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.setContent(null);
response.setMsg("下载失败:" + e);
return response;
+ } finally {
+ // 确保HttpClient被关闭
+ try {
+ httpClient.close();
+ } catch (IOException e) {
+ logger.warn("关闭HttpClient失败", e);
+ }
}
} else if (isFtpUrl(url)) {
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协议的文件下载
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
@@ -229,4 +280,4 @@ public class DownloadUtils {
return realPath;
}
-}
+}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/utils/FileConvertStatusManager.java b/server/src/main/java/cn/keking/utils/FileConvertStatusManager.java
new file mode 100644
index 00000000..63ececc5
--- /dev/null
+++ b/server/src/main/java/cn/keking/utils/FileConvertStatusManager.java
@@ -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 STATUS_MAP = new ConcurrentHashMap<>();
+
+ // 记录最终状态(超时或异常),防止重复转换
+ private static final ConcurrentMap 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);
+}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/utils/SslUtils.java b/server/src/main/java/cn/keking/utils/SslUtils.java
index f28dad6e..824eadc4 100644
--- a/server/src/main/java/cn/keking/utils/SslUtils.java
+++ b/server/src/main/java/cn/keking/utils/SslUtils.java
@@ -1,8 +1,8 @@
package cn.keking.utils;
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.HttpClientBuilder;
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.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.util.Timeout;
-import javax.net.ssl.*;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
@@ -23,40 +24,58 @@ public class SslUtils {
* 创建忽略SSL验证的HttpClient(适用于HttpClient 5.6)
*/
public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
- // 创建自定义的SSL上下文
- SSLContext sslContext = createIgnoreVerifySSL();
+ return configureHttpClientBuilder(HttpClients.custom(), true, true).build();
+ }
- // 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
- DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
- sslContext, NoopHostnameVerifier.INSTANCE);
+ /**
+ * 配置HttpClientBuilder,支持SSL和重定向配置
+ * @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构建连接管理器
- // 使用连接管理器构建器
- PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
- .setTlsSocketStrategy(tlsStrategy)
- .setDefaultSocketConfig(SocketConfig.custom()
- .setSoTimeout(Timeout.ofSeconds(10))
- .build())
- .build();
+ // 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
+ DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
+ sslContext, NoopHostnameVerifier.INSTANCE);
- // 配置连接池参数
- connectionManager.setMaxTotal(200);
- connectionManager.setDefaultMaxPerRoute(20);
+ // 使用连接管理器构建器
+ PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
+ .setTlsSocketStrategy(tlsStrategy)
+ .setDefaultSocketConfig(SocketConfig.custom()
+ .setSoTimeout(Timeout.ofSeconds(10))
+ .build())
+ .build();
+
+ // 配置连接池参数
+ connectionManager.setMaxTotal(200);
+ connectionManager.setDefaultMaxPerRoute(20);
+
+ builder.setConnectionManager(connectionManager);
+ }
// 配置请求参数
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(10))
.setResponseTimeout(Timeout.ofSeconds(72))
- .setConnectionRequestTimeout(Timeout.ofSeconds(2))
- .setRedirectsEnabled(true)
+ .setConnectTimeout(Timeout.ofSeconds(2))
+ .setRedirectsEnabled(enableRedirect)
.setMaxRedirects(5)
.build();
+ builder.setDefaultRequestConfig(requestConfig);
- return HttpClients.custom()
- .setConnectionManager(connectionManager)
- .setDefaultRequestConfig(requestConfig)
- .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
- .build();
+ if (!enableRedirect) {
+ builder.disableRedirectHandling();
+ }
+
+ 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;
}
}
\ No newline at end of file
diff --git a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java
index a295fdf1..6882ba30 100644
--- a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java
+++ b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java
@@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
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.LoggerFactory;
import org.springframework.http.HttpMethod;
@@ -63,8 +64,6 @@ public class OnlinePreviewController {
private final CacheService cacheService;
private final FileHandlerService fileHandlerService;
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();
public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
@@ -181,12 +180,18 @@ public class OnlinePreviewController {
InputStream inputStream = null;
logger.info("读取跨域pdf文件url:{}", urlPath);
if (!isFtpUrl(url)) {
- CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
+ // 根据配置创建HttpClient
+ CloseableHttpClient httpClient = createConfiguredHttpClient();
+
+ HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
- // restTemplate.setRequestFactory(factory);
+
+ RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(factory);
+
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
+ WebUtils.applyBasicAuthHeaders(request.getHeaders(), fileAttribute);
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){
Map proxyAuthorizationMap = mapper.readValue(
@@ -202,9 +207,24 @@ public class OnlinePreviewController {
return null;
});
} 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 {
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
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接口入队
*
@@ -260,4 +294,4 @@ public class OnlinePreviewController {
cacheService.addQueueTask(fileUrls);
return "success";
}
-}
+}
\ No newline at end of file
diff --git a/server/src/main/resources/web/waiting.ftl b/server/src/main/resources/web/waiting.ftl
new file mode 100644
index 00000000..06ef2be0
--- /dev/null
+++ b/server/src/main/resources/web/waiting.ftl
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ ${fileName}文件转换中
+
+
+
+
+
+
+
+
+
+
+
正在处理的文件
+
+
${fileName}
+
+
+
+ ${message}...
+
+
+
+
+
+
+
+
+
+
提示
+
+
+ - 文件转换时间取决于文件大小和服务器负载
+
+ - 转换完成后,页面将自动跳转到预览页面
+
+ - 您也可以点击"立即刷新"按钮手动检查转换状态
+
+
+
+
+
+
+