Răsfoiți Sursa

图片缩略图

xiaoyelj 5 luni în urmă
comite
2f3326452d

+ 36 - 0
.gitignore

@@ -0,0 +1,36 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea
+
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store

+ 10 - 0
.idea/.gitignore

@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+
+.idea

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>

+ 8 - 0
.idea/encodings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/kotlin" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

+ 6 - 0
.idea/kotlinc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinJpsPluginSettings">
+    <option name="version" value="1.9.23" />
+  </component>
+</project>

+ 14 - 0
.idea/misc.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 175 - 0
API-USAGE.md

@@ -0,0 +1,175 @@
+# 缩略图API使用示例
+
+## 接口地址
+
+基础URL: `http://localhost:8080`
+
+## 1. 生成缩略图
+
+### 接口信息
+- **方法**: GET
+- **路径**: `/api/thumbnail/generate`
+- **功能**: 根据文件路径生成指定尺寸的缩略图
+
+### 请求参数
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| path   | String | 是 | - | 相对文件路径 |
+| w      | Integer | 否 | 200 | 缩略图宽度(1-4000) |
+| h      | Integer | 否 | 200 | 缩略图高度(1-4000) |
+
+### 请求示例
+
+```bash
+# 基本用法
+curl "http://localhost:8080/api/thumbnail/generate?path=photos/sunset.jpg&w=300&h=300" \
+  --output thumbnail.jpg
+
+# 使用默认尺寸
+curl "http://localhost:8080/api/thumbnail/generate?path=images/photo.png" \
+  --output thumbnail.jpg
+
+# 生成不同尺寸
+curl "http://localhost:8080/api/thumbnail/generate?path=gallery/image.jpg&w=150&h=150" \
+  --output small_thumbnail.jpg
+```
+
+### 响应说明
+
+**成功响应**:
+- **状态码**: 200
+- **Content-Type**: `image/jpeg`
+- **Cache-Control**: `max-age=3600`
+- **响应体**: JPG格式的图片二进制数据
+
+**错误响应**:
+- **状态码**: 500
+- **Content-Type**: `application/json`
+- **响应体**:
+```json
+{
+  "success": false,
+  "message": "错误信息"
+}
+```
+
+### 常见错误
+
+| 错误信息 | 原因 | 解决方案 |
+|----------|------|----------|
+| "文件路径不能为空" | path参数为空 | 提供有效的文件路径 |
+| "宽度必须在1-4000像素之间" | w参数超出范围 | 设置正确的宽度值 |
+| "高度必须在1-4000像素之间" | h参数超出范围 | 设置正确的高度值 |
+| "源文件不存在" | 文件路径不存在 | 检查文件路径是否正确 |
+| "不支持的文件类型" | 文件格式不支持 | 使用支持的图片格式 |
+
+## 2. 健康检查
+
+### 接口信息
+- **方法**: GET
+- **路径**: `/api/thumbnail/health`
+- **功能**: 检查服务是否正常运行
+
+### 请求示例
+
+```bash
+curl "http://localhost:8080/api/thumbnail/health"
+```
+
+### 响应示例
+
+```json
+{
+  "success": true,
+  "message": "服务正常运行",
+  "data": "OK",
+  "timestamp": 1699123456789
+}
+```
+
+## 3. 获取支持格式
+
+### 接口信息
+- **方法**: GET
+- **路径**: `/api/thumbnail/formats`
+- **功能**: 获取支持的图片格式列表
+
+### 请求示例
+
+```bash
+curl "http://localhost:8080/api/thumbnail/formats"
+```
+
+### 响应示例
+
+```json
+{
+  "success": true,
+  "message": "获取支持格式成功",
+  "data": ["jpg", "jpeg", "png", "webp"],
+  "timestamp": 1699123456789
+}
+```
+
+## 配置说明
+
+### 文件路径配置
+
+在 `application.properties` 中配置文件路径前缀:
+
+```properties
+# 文件路径前缀
+thumbnail.file.prefix=D:/images/
+```
+
+### 缩略图存储
+
+- 缩略图文件保存在原图片相同目录
+- 文件名格式: `原文件名_宽度_高度.jpg`
+- 例如: `sunset_300_300.jpg`
+
+### 缓存机制
+
+- 首次生成缩略图时会保存到磁盘
+- 后续相同参数的请求直接返回已缓存的缩略图
+- 提高响应速度,减少服务器负载
+
+## JavaScript调用示例
+
+```javascript
+// 生成缩略图并显示
+async function generateThumbnail(path, width, height) {
+    try {
+        const url = `/api/thumbnail/generate?path=${encodeURIComponent(path)}&w=${width}&h=${height}`;
+        const response = await fetch(url);
+
+        if (response.ok) {
+            const blob = await response.blob();
+            const imageUrl = URL.createObjectURL(blob);
+
+            // 显示图片
+            const img = document.createElement('img');
+            img.src = imageUrl;
+            document.body.appendChild(img);
+
+            return imageUrl;
+        } else {
+            const error = await response.json();
+            console.error('生成失败:', error.message);
+        }
+    } catch (error) {
+        console.error('请求失败:', error);
+    }
+}
+
+// 使用示例
+generateThumbnail('photos/sunset.jpg', 300, 300);
+```
+
+## 性能特点
+
+1. **直接流输出**: 缩略图直接写入HTTP响应流,无额外内存拷贝
+2. **磁盘缓存**: 生成的缩略图保存到磁盘,避免重复计算
+3. **高效处理**: 基于Thumbnailator库,高性能图片处理
+4. **内存优化**: 避免大图片在内存中的完整加载

+ 183 - 0
ENVIRONMENT-CONFIG.md

@@ -0,0 +1,183 @@
+# 环境配置说明
+
+## 配置文件结构
+
+项目使用YAML格式的配置文件,支持多环境配置:
+
+```
+src/main/resources/
+├── application.yml          # 主配置文件
+├── application-dev.yml      # 开发环境配置
+├── application-test.yml     # 测试环境配置
+└── application-prod.yml     # 生产环境配置
+```
+
+## 环境切换
+
+### 1. 通过启动脚本
+
+```bash
+# 开发环境
+start-dev.bat
+
+# 测试环境
+start-test.bat
+
+# 生产环境
+start-prod.bat
+```
+
+### 2. 通过命令行参数
+
+```bash
+# 开发环境
+java -jar -Dspring.profiles.active=dev app.jar
+
+# 测试环境
+java -jar -Dspring.profiles.active=test app.jar
+
+# 生产环境
+java -jar -Dspring.profiles.active=prod app.jar
+```
+
+### 3. 通过环境变量
+
+```bash
+# Windows
+set SPRING_PROFILES_ACTIVE=dev
+
+# Linux/Mac
+export SPRING_PROFILES_ACTIVE=dev
+```
+
+## 各环境配置详情
+
+### 开发环境 (dev)
+
+**特点**:
+- 端口:8080
+- 文件路径:`D:/dev/images/`
+- 日志级别:DEBUG/TRACE
+- 缓存时间:1小时
+- 启用调试模式
+
+**适用场景**:
+- 本地开发
+- 功能调试
+- 单元测试
+
+### 测试环境 (test)
+
+**特点**:
+- 端口:8081
+- 文件路径:`/app/test/images/`
+- 日志级别:INFO/DEBUG
+- 缓存时间:2小时
+- 启用性能监控
+- 日志文件输出
+
+**适用场景**:
+- 集成测试
+- 性能测试
+- 预发布验证
+
+### 生产环境 (prod)
+
+**特点**:
+- 端口:80
+- 文件路径:`/data/images/`
+- 日志级别:WARN/INFO
+- 缓存时间:24小时
+- 性能优化配置
+- 安全限制
+- 监控配置
+
+**适用场景**:
+- 正式生产环境
+- 高并发场景
+- 稳定服务
+
+## 配置项说明
+
+### 核心配置
+
+| 配置项 | 开发环境 | 测试环境 | 生产环境 | 说明 |
+|--------|----------|----------|----------|------|
+| server.port | 8080 | 8081 | 80 | 服务端口 |
+| thumbnail.file.prefix | D:/dev/images/ | /app/test/images/ | /data/images/ | 文件路径前缀 |
+| thumbnail.quality | 0.8 | 0.7 | 0.9 | 图片质量 |
+| thumbnail.cache.max-age | 3600 | 7200 | 86400 | 缓存时间(秒) |
+
+### 日志配置
+
+| 环境 | 根日志级别 | 应用日志级别 | 文件输出 |
+|------|------------|--------------|----------|
+| dev | DEBUG | TRACE | 否 |
+| test | INFO | DEBUG | 是 |
+| prod | WARN | INFO | 是 |
+
+### 性能配置
+
+| 配置项 | 生产环境值 | 说明 |
+|--------|------------|------|
+| performance.thread-pool.core-size | 10 | 线程池核心大小 |
+| performance.thread-pool.max-size | 50 | 线程池最大大小 |
+| performance.thread-pool.queue-capacity | 1000 | 队列容量 |
+
+### 安全配置
+
+| 配置项 | 生产环境值 | 说明 |
+|--------|------------|------|
+| security.rate-limit.enabled | true | 是否启用速率限制 |
+| security.rate-limit.requests-per-minute | 100 | 每分钟请求限制 |
+
+## 自定义配置
+
+### 添加新的配置项
+
+1. 在相应的 `application-{profile}.yml` 文件中添加配置
+2. 在 `AppConfig.kt` 中添加对应的获取方法
+3. 在需要使用的地方注入 `AppConfig` 并调用相应方法
+
+示例:
+
+```yaml
+# application-dev.yml
+custom:
+  feature:
+    enabled: true
+    timeout: 30
+```
+
+```kotlin
+// AppConfig.kt
+fun isCustomFeatureEnabled(): Boolean {
+    return appContext.cfg().getBool("custom.feature.enabled") ?: false
+}
+
+fun getCustomTimeout(): Int {
+    return appContext.cfg().getInt("custom.feature.timeout") ?: 30
+}
+```
+
+## 配置文件优先级
+
+1. 命令行参数
+2. 环境变量
+3. application-{profile}.yml
+4. application.yml
+5. 默认值
+
+## 常见问题
+
+### Q: 如何查看当前使用的配置文件?
+A: 查看启动日志,会显示当前激活的profile和加载的配置文件。
+
+### Q: 配置修改后需要重启吗?
+A: 是的,配置文件修改后需要重启应用才能生效。
+
+### Q: 如何在运行时获取当前环境?
+A: 可以通过 `AppConfig.getActiveProfile()` 方法获取。
+
+### Q: 可以同时激活多个环境吗?
+A: 可以,但建议只激活一个主环境,避免配置冲突。

+ 261 - 0
README.md

@@ -0,0 +1,261 @@
+# 缩略图生成器
+
+基于 Kotlin + Solon Boot + Thumbnailator 的高性能缩略图生成服务。
+
+## 功能特性
+
+- ✅ 基于文件路径的缩略图生成
+- ✅ 自动缓存检查(缩略图已存在则直接返回)
+- ✅ 直接流输出,高性能无内存拷贝
+- ✅ 批量多尺寸缩略图生成
+- ✅ 支持多种图片格式 (JPG, PNG, WebP, BMP, GIF)
+- ✅ 固定JPG输出格式,优化性能
+- ✅ 文件大小限制和类型验证
+- ✅ RESTful API 接口
+- ✅ Web 测试界面
+- ✅ 可配置的文件路径前缀
+
+## 技术栈
+
+- **后端框架**: Solon Boot 2.7.6
+- **编程语言**: Kotlin 1.9.21
+- **图片处理**: Thumbnailator 0.4.20
+- **构建工具**: Maven
+- **JDK版本**: 17
+
+## 快速开始
+
+### 1. 环境要求
+
+- JDK 17+
+- Maven 3.6+
+
+### 2. 运行项目
+
+#### 开发环境
+```bash
+# 使用启动脚本(推荐)
+start-dev.bat
+
+# 或者使用 Maven 命令
+mvn clean compile
+mvn exec:java -Dexec.mainClass="com.yujin.thumbnail.ThumbnailApplicationKt" -Dspring.profiles.active=dev
+```
+
+#### 测试环境
+```bash
+# 使用启动脚本
+start-test.bat
+
+# 或者使用 Maven 命令
+mvn exec:java -Dexec.mainClass="com.yujin.thumbnail.ThumbnailApplicationKt" -Dspring.profiles.active=test
+```
+
+#### 生产环境
+```bash
+# 使用启动脚本
+start-prod.bat
+
+# 或者使用 Maven 命令
+mvn exec:java -Dexec.mainClass="com.yujin.thumbnail.ThumbnailApplicationKt" -Dspring.profiles.active=prod
+```
+
+### 3. 访问测试页面
+
+- **开发环境**: http://localhost:8080/index.html
+- **测试环境**: http://localhost:8081/index.html
+- **生产环境**: http://localhost/index.html
+
+## 环境配置
+
+项目支持多环境配置,使用YAML格式:
+
+- `application.yml` - 主配置文件
+- `application-dev.yml` - 开发环境配置
+- `application-test.yml` - 测试环境配置
+- `application-prod.yml` - 生产环境配置
+
+详细配置说明请参考:[环境配置文档](ENVIRONMENT-CONFIG.md)
+
+## API 接口
+
+### 1. 生成单个缩略图
+
+**接口**: `GET /api/thumbnail/generate`
+
+**参数**:
+- `path`: 相对文件路径 (必需)
+- `w`: 目标宽度,默认 200px
+- `h`: 目标高度,默认 200px
+
+**响应**: 返回生成的缩略图文件 (JPG格式)
+
+**功能特性**:
+- 自动检查缩略图是否已存在
+- 如存在则直接返回,不存在则生成后返回
+- 缩略图文件名格式: `原文件名_w_h.jpg`
+- 缩略图保存在与原文件相同目录下
+- 直接流输出,无内存拷贝,高性能
+
+**示例**:
+```bash
+# 生成 300x300 的缩略图
+curl "http://localhost:8080/api/thumbnail/generate?path=photos/sunset.jpg&w=300&h=300" \
+  --output thumbnail_300_300.jpg
+```
+
+### 2. 批量生成缩略图
+
+**接口**: `POST /api/thumbnail/batch`
+
+**参数**:
+- `file`: 上传的图片文件 (form-data)
+- 请求体 (JSON):
+```json
+{
+  "sizes": [
+    {"width": 100, "height": 100},
+    {"width": 200, "height": 200},
+    {"width": 300, "height": 300}
+  ],
+  "quality": 0.8,
+  "format": "jpg"
+}
+```
+
+**响应**: 返回包含所有尺寸缩略图的 ZIP 文件
+
+### 3. 获取支持格式
+
+**接口**: `GET /api/thumbnail/formats`
+
+**响应**:
+```json
+{
+  "success": true,
+  "message": "获取支持格式成功",
+  "data": ["jpg", "jpeg", "png", "webp"],
+  "timestamp": 1699123456789
+}
+```
+
+### 4. 健康检查
+
+**接口**: `GET /api/thumbnail/health`
+
+**响应**:
+```json
+{
+  "success": true,
+  "message": "服务正常运行",
+  "data": "OK",
+  "timestamp": 1699123456789
+}
+```
+
+## 配置说明
+
+项目使用YAML格式的配置文件,支持多环境配置。
+
+### 主配置文件: `application.yml`
+```yaml
+spring:
+  profiles:
+    active: dev  # 默认激活开发环境
+
+server:
+  host: 0.0.0.0
+
+# 通用配置...
+```
+
+### 环境特定配置
+
+#### 开发环境 (`application-dev.yml`)
+- 端口: 8080
+- 文件路径: `D:/dev/images/`
+- 日志级别: DEBUG
+- 缓存时间: 1小时
+
+#### 测试环境 (`application-test.yml`)
+- 端口: 8081
+- 文件路径: `/app/test/images/`
+- 日志级别: INFO
+- 缓存时间: 2小时
+
+#### 生产环境 (`application-prod.yml`)
+- 端口: 80
+- 文件路径: `/data/images/`
+- 日志级别: WARN
+- 缓存时间: 24小时
+
+## 限制说明
+
+- 图片尺寸范围: 1-4000px
+- 批量生成最多支持 10 种尺寸
+- 支持的输入图片格式: JPG, PNG, WebP, BMP, GIF
+- 输出格式固定为: JPG
+- 图片质量固定为: 0.8
+- 单个文件最大 50MB
+- 文件路径必须在配置的前缀目录下
+
+## 项目结构
+
+```
+src/main/kotlin/com/yujin/thumbnail/
+├── ThumbnailApplication.kt          # 主应用程序类
+├── controller/
+│   └── ThumbnailController.kt       # REST 控制器
+├── service/
+│   └── ThumbnailService.kt          # 缩略图生成服务
+├── dto/
+│   └── ThumbnailDto.kt              # 数据传输对象
+└── config/
+    └── GlobalExceptionHandler.kt    # 全局异常处理器
+
+src/main/resources/
+├── application.properties           # 配置文件
+└── static/
+    └── index.html                   # 测试页面
+```
+
+## 错误处理
+
+所有 API 接口都包含统一的错误处理:
+
+```json
+{
+  "success": false,
+  "message": "错误信息",
+  "data": null,
+  "timestamp": 1699123456789
+}
+```
+
+常见错误类型:
+- 参数验证错误
+- 文件类型不支持
+- 文件大小超限
+- 图片尺寸超限
+- 服务器内部错误
+
+## 性能优化建议
+
+1. **内存优化**: 对于大量图片处理,建议增加 JVM 堆内存
+2. **并发处理**: Solon Boot 支持异步处理,可配置线程池
+3. **缓存策略**: 可添加 Redis 缓存常用尺寸的缩略图
+4. **文件存储**: 生产环境建议使用对象存储服务
+
+## 扩展功能
+
+可以考虑添加的功能:
+- 图片裁剪
+- 图片旋转
+- 图片滤镜
+- 批量压缩
+- 异步处理队列
+- 缩略图缓存
+
+## License
+
+MIT License

+ 304 - 0
logs/solon.log

@@ -0,0 +1,304 @@
+INFO  2025-08-07 13:14:07.688 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:14:07.691 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:14:07.693 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:14:07.698 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:14:07.970 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:14:07.970 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @214ms
+INFO  2025-08-07 13:14:07.972 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=945ms pid=19436 v=3.2.0
+INFO  2025-08-07 13:14:10.989 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:14:10.993 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:15:40.323 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:15:40.326 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:15:40.329 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:15:40.334 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:15:40.606 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:15:40.606 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @209ms
+INFO  2025-08-07 13:15:40.607 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=970ms pid=20760 v=3.2.0
+INFO  2025-08-07 13:16:56.311 [-smarthttp-3][*][c.y.t.c.ThumbnailController]: 
+开始生成缩略图: 路径=a, 尺寸=0x0
+WARN  2025-08-07 13:16:56.329 [-smarthttp-3][*][o.noear.solon.Solon]: 
+SolonApp tryHandle failed!
+java.lang.IllegalArgumentException: 宽度必须在1-4000像素之间
+	at com.yujin.thumbnail.controller.ThumbnailController.generateThumbnail(ThumbnailController.kt:29)
+	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
+	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
+	at org.noear.solon.core.wrap.MethodWrap.invoke(MethodWrap.java:269)
+	at org.noear.solon.core.wrap.MethodWrap.doIntercept(MethodWrap.java:258)
+	at org.noear.solon.core.aspect.InterceptorEntity.doIntercept(InterceptorEntity.java:55)
+	at org.noear.solon.core.aspect.Invocation.invoke(Invocation.java:115)
+	at org.noear.solon.core.wrap.MethodWrap.invokeByAspect(MethodWrap.java:284)
+	at org.noear.solon.core.mvc.ActionDefault.executeDo(ActionDefault.java:391)
+	at org.noear.solon.core.mvc.ActionDefault.invokeMethodDo(ActionDefault.java:327)
+	at org.noear.solon.core.mvc.ActionDefault.invokeHandleDo(ActionDefault.java:304)
+	at org.noear.solon.core.handle.FilterChainImpl.doFilter(FilterChainImpl.java:51)
+	at org.noear.solon.core.mvc.ActionDefault.invokeFilterDo(ActionDefault.java:296)
+	at org.noear.solon.core.handle.FilterChainImpl.doFilter(FilterChainImpl.java:51)
+	at org.noear.solon.core.mvc.ActionDefault.invoke(ActionDefault.java:270)
+	at org.noear.solon.core.mvc.ActionDefault.handle(ActionDefault.java:247)
+	at org.noear.solon.core.route.RouterHandler.handleMain(RouterHandler.java:51)
+	at org.noear.solon.core.route.RouterHandler.handle1(RouterHandler.java:71)
+	at org.noear.solon.core.route.RouterInterceptorChainImpl.doIntercept(RouterInterceptorChainImpl.java:54)
+	at org.noear.solon.core.ChainManager.doIntercept(ChainManager.java:199)
+	at org.noear.solon.core.route.RouterHandler.handle(RouterHandler.java:102)
+	at org.noear.solon.core.handle.HandlerPipeline.handle(HandlerPipeline.java:53)
+	at org.noear.solon.core.handle.FilterChainImpl.doFilter(FilterChainImpl.java:51)
+	at org.noear.solon.logging.integration.LoggingPlugin.lambda$start$1(LoggingPlugin.java:71)
+	at org.noear.solon.core.handle.FilterChainImpl.doFilter(FilterChainImpl.java:49)
+	at org.noear.solon.core.ChainManager.doFilter(ChainManager.java:106)
+	at org.noear.solon.SolonApp.tryHandle(SolonApp.java:526)
+	at org.noear.solon.boot.smarthttp.http.SmHttpContextHandler.handleDo(SmHttpContextHandler.java:123)
+	at org.noear.solon.boot.smarthttp.http.SmHttpContextHandler.handle0(SmHttpContextHandler.java:101)
+	at org.noear.solon.boot.smarthttp.http.SmHttpContextHandler.lambda$handle$0(SmHttpContextHandler.java:86)
+	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
+	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
+	at java.base/java.lang.Thread.run(Thread.java:833)
+INFO  2025-08-07 13:28:51.481 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:28:51.484 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:28:55.024 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:28:55.027 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:28:55.029 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:28:55.034 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:28:55.294 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:28:55.294 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @193ms
+INFO  2025-08-07 13:28:55.296 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=874ms pid=10136 v=3.2.0
+INFO  2025-08-07 13:29:25.442 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:29:25.445 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:29:29.088 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:29:29.091 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:29:29.094 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:29:29.099 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:29:29.344 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:29:29.345 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @187ms
+INFO  2025-08-07 13:29:29.346 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=1012ms pid=22620 v=3.2.0
+INFO  2025-08-07 13:31:51.115 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:31:51.119 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:31:54.704 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:31:54.708 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:31:54.712 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:31:54.717 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:31:54.999 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:31:54.999 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @215ms
+INFO  2025-08-07 13:31:55.000 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=1001ms pid=22336 v=3.2.0
+INFO  2025-08-07 13:37:13.067 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:37:13.070 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:37:15.530 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:37:15.532 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:37:15.534 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:37:15.540 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:37:15.815 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:37:15.816 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @211ms
+INFO  2025-08-07 13:37:15.818 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=863ms pid=18516 v=3.2.0
+INFO  2025-08-07 13:38:11.018 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:38:11.021 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:38:51.903 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:38:51.905 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:38:51.908 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:38:51.914 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:38:52.188 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:38:52.189 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @211ms
+INFO  2025-08-07 13:38:52.191 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=851ms pid=21628 v=3.2.0
+INFO  2025-08-07 13:40:01.453 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:40:01.456 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:40:04.873 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:40:04.875 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:40:04.878 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:40:04.882 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:40:05.145 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:40:05.146 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @202ms
+INFO  2025-08-07 13:40:05.147 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=883ms pid=12672 v=3.2.0
+INFO  2025-08-07 13:43:23.575 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:43:23.575 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:43:27.022 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:43:27.025 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:43:27.027 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:43:27.033 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:43:27.342 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:43:27.342 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @234ms
+INFO  2025-08-07 13:43:27.343 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=994ms pid=12616 v=3.2.0
+INFO  2025-08-07 13:44:46.418 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:44:46.422 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:44:50.042 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:44:50.046 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:44:50.050 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:44:50.060 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:44:50.410 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:44:50.410 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @222ms
+INFO  2025-08-07 13:44:50.412 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=1056ms pid=23428 v=3.2.0
+INFO  2025-08-07 13:45:09.792 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:45:09.795 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:45:13.135 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:45:13.138 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:45:13.142 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:45:13.150 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:45:13.443 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:45:13.444 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @220ms
+INFO  2025-08-07 13:45:13.447 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=939ms pid=7492 v=3.2.0
+INFO  2025-08-07 13:45:20.158 [-smarthttp-1][*][c.y.t.c.ThumbnailController]: 
+开始生成缩略图: 路径=generate, 尺寸=100x100
+INFO  2025-08-07 13:45:43.401 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 13:45:43.404 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 13:45:46.945 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:45:46.947 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 13:45:46.951 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 13:45:46.958 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 13:45:47.256 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 13:45:47.256 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @223ms
+INFO  2025-08-07 13:45:47.259 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=1000ms pid=17924 v=3.2.0
+INFO  2025-08-07 14:53:13.349 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 14:53:13.353 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 14:53:17.244 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 14:53:17.247 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 14:53:17.250 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 14:53:17.254 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 14:53:17.551 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 14:53:17.552 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @239ms
+INFO  2025-08-07 14:53:17.554 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=891ms pid=19788 v=3.2.0
+INFO  2025-08-07 14:54:07.693 [-smarthttp-1][*][c.y.t.c.ThumbnailController]: 
+开始生成缩略图: 路径=/api/thumbnail/generate/133, 尺寸=100x100
+INFO  2025-08-07 14:54:36.739 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 14:54:36.742 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped
+INFO  2025-08-07 14:54:40.327 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @json=StringSerializerRender#snack3-json
+INFO  2025-08-07 14:54:40.330 [-main][*][o.noear.solon.Solon]: 
+Render mapping: @type_json=StringSerializerRender#snack3-json
+INFO  2025-08-07 14:54:40.333 [-main][*][o.noear.solon.Solon]: 
+Session: Local session state plugin is loaded
+INFO  2025-08-07 14:54:40.339 [-main][*][o.noear.solon.Solon]: 
+App: Bean scanning
+INFO  2025-08-07 14:54:40.644 [-main][*][o.noear.solon.Solon]: 
+solon.connector:main: smarthttp: Started ServerConnector@{HTTP/1.1,[http/1.1]}{http://localhost:8080}
+INFO  2025-08-07 14:54:40.645 [-main][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Started (smart http 2.5/3.2.0) @227ms
+INFO  2025-08-07 14:54:40.647 [-main][*][o.noear.solon.Solon]: 
+App: End loading elapsed=991ms pid=23452 v=3.2.0
+INFO  2025-08-07 14:54:53.563 [-smarthttp-2][*][c.y.t.c.ThumbnailController]: 
+开始生成缩略图: 路径=/generate/133, 尺寸=100x100
+INFO  2025-08-07 14:55:22.308 [-smarthttp-4][*][c.y.t.c.ThumbnailController]: 
+开始生成缩略图: 路径=/微信截图_20240820173307.png, 尺寸=100x100
+INFO  2025-08-07 14:55:44.025 [-smarthttp-4][*][c.y.t.s.ThumbnailService]: 
+生成新的缩略图: C:/Users/xiaoyelj/Pictures/千寻图片/微信截图_20240820173307.png -> C:\Users\xiaoyelj\Pictures\千寻图片\微信截图_20240820173307_100_100.jpg
+INFO  2025-08-07 14:56:07.303 [-smarthttp-4][*][c.y.t.c.ThumbnailController]: 
+缩略图生成并输出成功
+INFO  2025-08-07 15:06:50.909 [-Thread-0][*][o.noear.solon.Solon]: 
+Server:main: smarthttp: Has Stopped (smart http 2.5/3.2.0)
+INFO  2025-08-07 15:06:50.912 [-Thread-0][*][o.noear.solon.Solon]: 
+App: Stopped

+ 118 - 0
pom.xml

@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.yujin</groupId>
+    <artifactId>yujin-humbnailator</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <kotlin.version>1.9.23</kotlin.version>
+    </properties>
+
+    <parent>
+        <groupId>org.noear</groupId>
+        <artifactId>solon-parent</artifactId>
+        <version>3.2.0</version>
+    </parent>
+
+    <dependencies>
+        <!-- Solon Boot Web -->
+        <dependency>
+            <groupId>org.noear</groupId>
+            <artifactId>solon-web</artifactId>
+        </dependency>
+
+        <!-- Solon Boot Jackson -->
+        <dependency>
+            <groupId>org.noear</groupId>
+            <artifactId>solon-logging-logback</artifactId>
+        </dependency>
+
+        <!-- YAML Configuration Support -->
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>
+
+        <!-- Thumbnailator -->
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <version>0.4.20</version>
+        </dependency>
+
+        <!-- Kotlin Standard Library -->
+        <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-stdlib</artifactId>
+            <version>${kotlin.version}</version>
+        </dependency>
+
+
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>5.10.0</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.noear</groupId>
+            <artifactId>solon-test</artifactId>
+            <version>${solon.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <sourceDirectory>src/main/kotlin</sourceDirectory>
+        <testSourceDirectory>src/test/kotlin</testSourceDirectory>
+
+        <plugins>
+            <!-- Kotlin Compiler Plugin -->
+            <plugin>
+                <groupId>org.jetbrains.kotlin</groupId>
+                <artifactId>kotlin-maven-plugin</artifactId>
+                <version>${kotlin.version}</version>
+                <executions>
+                    <execution>
+                        <id>compile</id>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>test-compile</id>
+                        <phase>test-compile</phase>
+                        <goals>
+                            <goal>test-compile</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <jvmTarget>17</jvmTarget>
+                </configuration>
+            </plugin>
+
+            <!-- Maven Compiler Plugin -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <source>17</source>
+                    <target>17</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 14 - 0
src/main/kotlin/com/yujin/thumbnail/ThumbnailApplication.kt

@@ -0,0 +1,14 @@
+package com.yujin.thumbnail
+
+import org.noear.solon.Solon
+
+/**
+ * 缩略图生成应用程序主类
+ */
+fun main(args: Array<String>) {
+    Solon.start(ThumbnailApplication::class.java, args) {
+        // 配置文件上传大小限制
+    }
+}
+
+class ThumbnailApplication

+ 41 - 0
src/main/kotlin/com/yujin/thumbnail/config/AppConfig.kt

@@ -0,0 +1,41 @@
+package com.yujin.synctool.config
+
+import org.noear.solon.annotation.Bean
+import org.noear.solon.annotation.Configuration
+import org.noear.solon.annotation.Inject
+import org.noear.solon.serialization.snack3.SnackActionExecutor
+import org.noear.solon.serialization.snack3.SnackRenderFactory
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.util.*
+
+
+@Configuration
+class WebConfiguration {
+
+    @Bean
+    fun jsonInit(@Inject factory: SnackRenderFactory, @Inject executor: SnackActionExecutor?) {
+        //示例1:通过转换器,做简单类型的定制
+        factory.addConvertor(Date::class.java, { s: Date -> s.getTime() })
+        factory.addConvertor(LocalDate::class.java) { s: LocalDate ->
+            s.format(
+                DateTimeFormatter.ofPattern(
+                    "yyyy-MM-dd"
+                )
+            )
+        }
+
+        factory.addConvertor(
+            Double::class.java
+        ) { s: Double -> s.toString() }
+
+        factory.addConvertor(
+            BigDecimal::class.java
+        ) { s: BigDecimal -> s.toPlainString() }
+
+    }
+
+
+}
+

+ 22 - 0
src/main/kotlin/com/yujin/thumbnail/config/AppFilter.kt

@@ -0,0 +1,22 @@
+package com.yujin.thumbnail.config
+
+import org.noear.solon.annotation.Component
+import org.noear.solon.core.exception.StatusException
+import org.noear.solon.core.handle.Context
+import org.noear.solon.core.handle.Filter
+import org.noear.solon.core.handle.FilterChain
+
+
+@Component//index 为顺序位(不加,则默认为0)
+class AppFilter : Filter {
+    @Throws(Throwable::class)
+    override fun doFilter(ctx: Context, chain: FilterChain) {
+        try {
+            chain.doFilter(ctx)
+        } catch (e: StatusException) {
+            ctx.render(mapOf("code" to 200, "msg" to e.message))
+        } catch (e: Throwable) {
+            ctx.render(mapOf("code" to 200, "msg" to e.message))
+        }
+    }
+}

+ 8 - 0
src/main/kotlin/com/yujin/thumbnail/config/LogExtend.kt

@@ -0,0 +1,8 @@
+package com.yujin.synctool.config
+
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+
+val <T : Any> T.logger: Logger
+    get() = LoggerFactory.getLogger(this.javaClass)

+ 38 - 0
src/main/kotlin/com/yujin/thumbnail/controller/ThumbnailController.kt

@@ -0,0 +1,38 @@
+package com.yujin.thumbnail.controller
+
+import com.yujin.synctool.config.logger
+import com.yujin.thumbnail.service.ThumbnailService
+import org.noear.solon.annotation.*
+import org.noear.solon.core.handle.Context
+
+/**
+ * 缩略图生成控制器
+ */
+@Controller
+@Mapping("/api/thumbnail")
+class ThumbnailController {
+
+    @Inject
+    private lateinit var thumbnailService: ThumbnailService
+
+    /**
+     * 生成单个缩略图
+     * GET /api/thumbnail/generate
+     */
+    @Get
+    @Mapping("/**")
+    fun generateThumbnail(
+        @Param("w") w: Int = 200,
+        @Param("h") h: Int = 200,
+    ) {
+        var path = Context.current().path().substring("/api/thumbnail".length)
+        logger.info("开始生成缩略图: 路径={}, 尺寸={}x{}", path, w, h) // 验证参数
+        require(path.isNotBlank()) { "文件路径不能为空" }
+        require(w > 0 && w <= 4000) { "宽度必须在1-4000像素之间" }
+        require(h > 0 && h <= 4000) { "高度必须在1-4000像素之间" } // 直接将缩略图写入响应流
+        thumbnailService.generateThumbnailToStream(path, w, h)
+        logger.info("缩略图生成并输出成功")
+
+    }
+
+}

+ 198 - 0
src/main/kotlin/com/yujin/thumbnail/service/ThumbnailService.kt

@@ -0,0 +1,198 @@
+package com.yujin.thumbnail.service
+
+import com.yujin.synctool.config.logger
+import net.coobird.thumbnailator.Thumbnails
+import net.coobird.thumbnailator.geometry.Positions
+import org.noear.solon.annotation.Component
+import org.noear.solon.annotation.Inject
+import org.noear.solon.core.handle.Context
+import java.awt.Color
+import java.io.*
+import java.nio.file.Files
+
+/**
+ * 缩略图生成服务
+ */
+@Component
+class ThumbnailService {
+
+
+    @Inject("\${thumbnai.quality}")
+    private var quality: Double? = null
+
+    @Inject("\${thumbnai.filePrefix}")
+    private lateinit var filePrefix: String
+
+
+    /**
+     * 基于文件路径生成缩略图并直接写入输出流
+     * @param relativePath 相对文件路径
+     * @param width 目标宽度
+     * @param height 目标高度
+     * @param outputStream 输出流
+     */
+    fun generateThumbnailToStream(relativePath: String, width: Int, height: Int) { // 构建完整的文件路径
+        val fullPath = filePrefix + relativePath
+        val sourceFile = File(fullPath)
+
+        // 验证源文件是否存在
+        require(sourceFile.exists()) { "源文件不存在: $fullPath" }
+        require(sourceFile.isFile) { "路径不是一个文件: $fullPath" }
+
+        // 构建缩略图文件路径
+        val thumbnailPath = buildThumbnailPath(fullPath, width, height)
+        val thumbnailFile = File(thumbnailPath)
+
+        // 如果缩略图已存在,直接复制到输出流
+        if (!thumbnailFile.exists()) { // 生成缩略图
+            logger.info("生成新的缩略图: {} -> {}", fullPath, thumbnailPath) // 确保缩略图目录存在
+            thumbnailFile.parentFile?.mkdirs() // 使用配置的质量生成缩略图
+            val thumbnailBuilder = Thumbnails.of(sourceFile)
+                .size(width,height)
+                .outputFormat("jpg").outputQuality(quality!!) // 先保存到文件(用于缓存)
+            thumbnailBuilder.toFile(thumbnailFile)
+        }
+        var context = Context.current()
+        context.headerAdd("content-Type", "application/")
+        context.outputStream().use {
+            Files.copy(thumbnailFile.toPath(), it)
+        }
+    }
+
+
+    private fun buildThumbnailPath(originalPath: String, width: Int, height: Int): String {
+        val file = File(originalPath)
+        val directory = file.parent
+        val nameWithoutExt = file.nameWithoutExtension
+        val extension = "jpg"  // 固定为jpg格式
+
+        val thumbnailName = "${nameWithoutExt}_${width}_${height}.${extension}"
+        return File(directory, thumbnailName).absolutePath
+    }
+
+    /**
+     * 验证图片文件路径
+     */
+    private fun validateImagePath(path: String) {
+        val file = File(path)
+        require(file.exists()) { "文件不存在: $path" }
+        require(file.isFile) { "路径不是一个文件: $path" }
+        require(file.length() <= 50 * 1024 * 1024) { "文件大小不能超过50MB" }
+
+        val allowedExtensions = setOf("jpg", "jpeg", "png", "webp", "bmp", "gif")
+        val extension = file.extension.lowercase()
+        require(extension in allowedExtensions) {
+            "不支持的文件类型: $extension,支持的类型: ${allowedExtensions.joinToString()}"
+        }
+    }
+
+    /**
+     * 生成缩略图
+     * @param inputStream 原始图片输入流
+     * @param width 目标宽度
+     * @param height 目标高度
+     * @param quality 图片质量(0.0-1.0)
+     * @param keepAspectRatio 是否保持宽高比
+     * @param format 输出格式(jpg, png, webp等)
+     * @return 缩略图字节数组
+     */
+    fun generateThumbnail(
+        inputStream: InputStream,
+        width: Int,
+        height: Int,
+        quality: Double = 0.8,
+        keepAspectRatio: Boolean = true,
+        format: String = "jpg",
+    ): ByteArray {
+        val outputStream = ByteArrayOutputStream()
+
+        val thumbnailBuilder = Thumbnails.of(inputStream).outputFormat(format).outputQuality(quality.coerceIn(0.0, 1.0))
+
+        if (keepAspectRatio) { // 保持宽高比,可能会产生不完全匹配目标尺寸的图片
+            thumbnailBuilder.size(width, height)
+        } else { // 强制调整为指定尺寸,可能会拉伸图片
+            thumbnailBuilder.forceSize(width, height)
+        }
+
+        thumbnailBuilder.toOutputStream(outputStream)
+        return outputStream.toByteArray()
+    }
+
+    /**
+     * 生成带水印的缩略图
+     * @param inputStream 原始图片输入流
+     * @param width 目标宽度
+     * @param height 目标高度
+     * @param watermarkText 水印文字
+     * @param quality 图片质量
+     * @param format 输出格式
+     * @return 缩略图字节数组
+     */
+    fun generateThumbnailWithWatermark(
+        inputStream: InputStream,
+        width: Int,
+        height: Int,
+        watermarkText: String? = null,
+        quality: Double = 0.8,
+        format: String = "jpg",
+    ): ByteArray {
+        val outputStream = ByteArrayOutputStream()
+
+        val thumbnailBuilder = Thumbnails.of(inputStream).size(width, height).outputFormat(format).outputQuality(quality.coerceIn(0.0, 1.0))
+
+        // 如果有水印文字,添加水印
+        if (!watermarkText.isNullOrBlank()) { // 创建一个简单的文字水印图片
+            val watermarkImage = createTextWatermark(watermarkText, width, height)
+            thumbnailBuilder.watermark(Positions.BOTTOM_RIGHT, watermarkImage, 0.5f // 透明度
+            )
+        }
+
+        thumbnailBuilder.toOutputStream(outputStream)
+        return outputStream.toByteArray()
+    }
+
+    /**
+     * 批量生成多种尺寸的缩略图
+     * @param inputStream 原始图片输入流
+     * @param sizes 尺寸列表,格式为 [宽度, 高度]
+     * @param quality 图片质量
+     * @param format 输出格式
+     * @return 缩略图映射表,key为"宽度x高度",value为字节数组
+     */
+    fun generateMultipleThumbnails(
+        inputStream: InputStream,
+        sizes: List<Pair<Int, Int>>,
+        quality: Double = 0.8,
+        format: String = "jpg",
+    ): Map<String, ByteArray> {
+        val result = mutableMapOf<String, ByteArray>()
+
+        // 读取原始图片到字节数组,以便多次使用
+        val originalBytes = inputStream.readBytes()
+
+        sizes.forEach { (width, height) ->
+            val thumbnail = generateThumbnail(ByteArrayInputStream(originalBytes), width, height, quality, true, format)
+            result["${width}x${height}"] = thumbnail
+        }
+
+        return result
+    }
+
+    /**
+     * 创建文字水印图片
+     */
+    private fun createTextWatermark(text: String, maxWidth: Int, maxHeight: Int): java.awt.image.BufferedImage {
+        val watermarkWidth = maxWidth / 4
+        val watermarkHeight = 30
+
+        val bufferedImage = java.awt.image.BufferedImage(watermarkWidth, watermarkHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB)
+
+        val graphics = bufferedImage.createGraphics()
+        graphics.color = Color(255, 255, 255, 128) // 半透明白色
+        graphics.font = java.awt.Font("Arial", java.awt.Font.BOLD, 12)
+        graphics.drawString(text, 5, 20)
+        graphics.dispose()
+
+        return bufferedImage
+    }
+}

+ 8 - 0
src/main/resources/app-dev.yml

@@ -0,0 +1,8 @@
+solon.logging:
+  appender:
+    console:
+      level: DEBUG
+
+thumbnai:
+  filePrefix: C:/Users/xiaoyelj/Pictures/千寻图片
+  quality: 0.4

+ 4 - 0
src/main/resources/app-pro.yml

@@ -0,0 +1,4 @@
+solon.logging:
+  appender:
+    console:
+      level: INFO

+ 4 - 0
src/main/resources/app-test.yml

@@ -0,0 +1,4 @@
+solon.logging:
+  appender:
+    console:
+      level: DEBUG

+ 22 - 0
src/main/resources/app.yml

@@ -0,0 +1,22 @@
+solon:
+  env: dev
+
+server:
+  port: 8080
+
+solon.serialization.json:
+  dateAsFormat: 'yyyy-MM-dd HH:mm:ss' #配置日期格式(默认输出为时间戳,对 Date、LocalDateTime 有效)
+  dateAsTimeZone: 'GMT+8' #配置时区
+  dateAsTicks: false #将date转为毫秒数(和 dateAsFormat 二选一)
+  longAsString: true #将long型转为字符串输出 (默认为false)
+  boolAsInt: false   #将bool型转为字符串输出 (默认为false)
+  nullStringAsEmpty: false
+  nullBoolAsFalse: false
+  nullNumberAsZero: false
+  nullArrayAsEmpty: false
+  nullAsWriteable: false #输出所有null值
+  enumAsName: false #枚举使用名字(v2.2.1 后支持)
+
+thumbnai:
+  filePrefix: /
+  quality: 0.4

+ 19 - 0
src/test/kotlin/com/yujin/thumbnail/ThumbnailServiceTest.kt

@@ -0,0 +1,19 @@
+package com.yujin.thumbnail
+
+import org.noear.solon.test.SolonTest
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+
+/**
+ * 缩略图服务测试
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@SolonTest
+class ThumbnailServiceTest {
+
+    @Test
+    fun testHealthCheck() {
+        // 这是一个简单的测试,确保项目能够正常编译和运行
+        println("项目编译成功")
+    }
+}

+ 26 - 0
start-dev.bat

@@ -0,0 +1,26 @@
+@echo off
+echo ======================================
+echo 缩略图生成器 - 开发环境启动脚本
+echo ======================================
+
+echo 正在编译项目...
+call mvn clean compile
+
+if %ERRORLEVEL% neq 0 (
+    echo 编译失败,请检查错误信息
+    pause
+    exit /b 1
+)
+
+echo 编译成功,正在启动开发环境...
+echo 环境: development
+echo 端口: 8080
+echo 配置文件: application-dev.yml
+echo 访问地址: http://localhost:8080
+echo 测试页面: http://localhost:8080/index.html
+echo.
+echo 按 Ctrl+C 停止应用
+echo ======================================
+
+set SPRING_PROFILES_ACTIVE=dev
+call mvn exec:java -Dexec.mainClass="com.yujin.thumbnail.ThumbnailApplicationKt" -Dspring.profiles.active=dev

+ 26 - 0
start-prod.bat

@@ -0,0 +1,26 @@
+@echo off
+echo ======================================
+echo 缩略图生成器 - 生产环境启动脚本
+echo ======================================
+
+echo 正在编译项目...
+call mvn clean compile
+
+if %ERRORLEVEL% neq 0 (
+    echo 编译失败,请检查错误信息
+    pause
+    exit /b 1
+)
+
+echo 编译成功,正在启动生产环境...
+echo 环境: production
+echo 端口: 80
+echo 配置文件: application-prod.yml
+echo 访问地址: http://localhost
+echo.
+echo 注意:生产环境配置请确保端口80可用
+echo 按 Ctrl+C 停止应用
+echo ======================================
+
+set SPRING_PROFILES_ACTIVE=prod
+call mvn exec:java -Dexec.mainClass="com.yujin.thumbnail.ThumbnailApplicationKt" -Dspring.profiles.active=prod

+ 26 - 0
start-test.bat

@@ -0,0 +1,26 @@
+@echo off
+echo ======================================
+echo 缩略图生成器 - 测试环境启动脚本
+echo ======================================
+
+echo 正在编译项目...
+call mvn clean compile
+
+if %ERRORLEVEL% neq 0 (
+    echo 编译失败,请检查错误信息
+    pause
+    exit /b 1
+)
+
+echo 编译成功,正在启动测试环境...
+echo 环境: test
+echo 端口: 8081
+echo 配置文件: application-test.yml
+echo 访问地址: http://localhost:8081
+echo 测试页面: http://localhost:8081/index.html
+echo.
+echo 按 Ctrl+C 停止应用
+echo ======================================
+
+set SPRING_PROFILES_ACTIVE=test
+call mvn exec:java -Dexec.mainClass="com.yujin.thumbnail.ThumbnailApplicationKt" -Dspring.profiles.active=test

+ 14 - 0
start.bat

@@ -0,0 +1,14 @@
+@echo off
+echo ======================================
+echo 缩略图生成器启动脚本(默认开发环境)
+echo ======================================
+echo.
+echo 可用的启动脚本:
+echo   start-dev.bat  - 开发环境 (端口: 8080)
+echo   start-test.bat - 测试环境 (端口: 8081)
+echo   start-prod.bat - 生产环境 (端口: 80)
+echo.
+echo 正在启动开发环境...
+echo ======================================
+
+call start-dev.bat