diff --git a/CONFIG.md b/CONFIG.md index 6071c01..e18f425 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -42,7 +42,7 @@ BingPaper 支持通过配置文件(YAML)和环境变量进行配置。 - `daily_spec`: Cron 表达式,定义每日抓取时间。默认 `"0 10 * * *"` (每日上午 10:00)。 #### retention (数据保留) -- `days`: 图片及元数据保留天数。超过此天数的数据可能会被清理任务处理(需配合 API 触发或未来功能),默认 `30`。 +- `days`: 图片及元数据保留天数。超过此天数的数据可能会被清理任务处理。设置为 `0` 表示永久保留,不进行自动清理。默认 `0`。 #### db (数据库配置) - `type`: 数据库类型,可选 `sqlite`, `mysql`, `postgres`。默认 `sqlite`。 diff --git a/config.example.yaml b/config.example.yaml index 667c678..a1cef63 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -22,7 +22,7 @@ cron: daily_spec: "0 10 * * *" retention: - days: 30 + days: 0 db: type: sqlite # sqlite | mysql | postgres diff --git a/docs/docs.go b/docs/docs.go index 4a1d22b..3d67f9f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -537,7 +537,7 @@ const docTemplate = `{ { "type": "string", "default": "UHD", - "description": "分辨率 (UHD, 1920x1080, 1366x768)", + "description": "分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240)", "name": "variant", "in": "query" }, @@ -581,7 +581,7 @@ const docTemplate = `{ }, "/images": { "get": { - "description": "分页获取已抓取的图片元数据列表", + "description": "分页获取已抓取的图片元数据列表。支持分页(page, page_size)、限制数量(limit)和按月份过滤(month, 格式: YYYY-MM)。", "produces": [ "application/json" ], @@ -593,9 +593,27 @@ const docTemplate = `{ { "type": "integer", "default": 30, - "description": "限制数量", + "description": "限制数量 (如果不使用分页)", "name": "limit", "in": "query" + }, + { + "type": "integer", + "description": "页码 (从1开始)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "按月份过滤 (格式: YYYY-MM)", + "name": "month", + "in": "query" } ], "responses": { diff --git a/docs/swagger.json b/docs/swagger.json index ab04513..674cdc6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -531,7 +531,7 @@ { "type": "string", "default": "UHD", - "description": "分辨率 (UHD, 1920x1080, 1366x768)", + "description": "分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240)", "name": "variant", "in": "query" }, @@ -575,7 +575,7 @@ }, "/images": { "get": { - "description": "分页获取已抓取的图片元数据列表", + "description": "分页获取已抓取的图片元数据列表。支持分页(page, page_size)、限制数量(limit)和按月份过滤(month, 格式: YYYY-MM)。", "produces": [ "application/json" ], @@ -587,9 +587,27 @@ { "type": "integer", "default": 30, - "description": "限制数量", + "description": "限制数量 (如果不使用分页)", "name": "limit", "in": "query" + }, + { + "type": "integer", + "description": "页码 (从1开始)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "按月份过滤 (格式: YYYY-MM)", + "name": "month", + "in": "query" } ], "responses": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 44bbe96..9b4a38e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -582,7 +582,8 @@ paths: description: 根据参数返回今日必应图片流或重定向 parameters: - default: UHD - description: 分辨率 (UHD, 1920x1080, 1366x768) + description: 分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, + 640x480, 640x360, 480x360, 400x240, 320x240) in: query name: variant type: string @@ -616,13 +617,26 @@ paths: - image /images: get: - description: 分页获取已抓取的图片元数据列表 + description: '分页获取已抓取的图片元数据列表。支持分页(page, page_size)、限制数量(limit)和按月份过滤(month, + 格式: YYYY-MM)。' parameters: - default: 30 - description: 限制数量 + description: 限制数量 (如果不使用分页) in: query name: limit type: integer + - description: 页码 (从1开始) + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + - description: '按月份过滤 (格式: YYYY-MM)' + in: query + name: month + type: string produces: - application/json responses: diff --git a/internal/config/config.go b/internal/config/config.go index 9870b6e..408dc23 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -157,7 +157,7 @@ func Init(configPath string) error { v.SetDefault("api.mode", "local") v.SetDefault("cron.enabled", true) v.SetDefault("cron.daily_spec", "0 10 * * *") - v.SetDefault("retention.days", 30) + v.SetDefault("retention.days", 0) v.SetDefault("db.type", "sqlite") v.SetDefault("db.dsn", "data/bing_paper.db") v.SetDefault("storage.type", "local") diff --git a/internal/http/handlers/image.go b/internal/http/handlers/image.go index cc77710..1dcdb09 100644 --- a/internal/http/handlers/image.go +++ b/internal/http/handlers/image.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strconv" "BingPaper/internal/config" "BingPaper/internal/model" @@ -40,7 +41,7 @@ type ImageMetaResp struct { // @Summary 获取今日图片 // @Description 根据参数返回今日必应图片流或重定向 // @Tags image -// @Param variant query string false "分辨率 (UHD, 1920x1080, 1366x768)" default(UHD) +// @Param variant query string false "分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240)" default(UHD) // @Param format query string false "格式 (jpg)" default(jpg) // @Produce image/jpeg // @Success 200 {file} binary @@ -144,19 +145,53 @@ func GetByDateMeta(c *gin.Context) { // ListImages 获取图片列表 // @Summary 获取图片列表 -// @Description 分页获取已抓取的图片元数据列表 +// @Description 分页获取已抓取的图片元数据列表。支持分页(page, page_size)、限制数量(limit)和按月份过滤(month, 格式: YYYY-MM)。 // @Tags image -// @Param limit query int false "限制数量" default(30) +// @Param limit query int false "限制数量 (如果不使用分页)" default(30) +// @Param page query int false "页码 (从1开始)" +// @Param page_size query int false "每页数量" +// @Param month query string false "按月份过滤 (格式: YYYY-MM)" // @Produce json // @Success 200 {array} ImageMetaResp // @Router /images [get] func ListImages(c *gin.Context) { - limitStr := c.DefaultQuery("limit", "30") - var limit int - fmt.Sscanf(limitStr, "%d", &limit) + limitStr := c.Query("limit") + pageStr := c.Query("page") + pageSizeStr := c.Query("page_size") + month := c.Query("month") - images, err := image.GetImageList(limit) + // 记录请求参数,便于排查过滤失效问题 + util.Logger.Debug("ListImages parameters", + zap.String("month", month), + zap.String("page", pageStr), + zap.String("page_size", pageSizeStr), + zap.String("limit", limitStr)) + + var limit, offset int + + if pageStr != "" && pageSizeStr != "" { + page, _ := strconv.Atoi(pageStr) + pageSize, _ := strconv.Atoi(pageSizeStr) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 30 + } + limit = pageSize + offset = (page - 1) * pageSize + } else { + if limitStr == "" { + limit = 30 + } else { + limit, _ = strconv.Atoi(limitStr) + } + offset = 0 + } + + images, err := image.GetImageList(limit, offset, month) if err != nil { + util.Logger.Error("ListImages service call failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } diff --git a/internal/service/fetcher/fetcher.go b/internal/service/fetcher/fetcher.go index f2e6fe5..b9596f3 100644 --- a/internal/service/fetcher/fetcher.go +++ b/internal/service/fetcher/fetcher.go @@ -142,36 +142,47 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error { } // 保存各种分辨率 - variants := []struct { + targetVariants := []struct { name string width int height int }{ - {variantName, 0, 0}, // 原图 (UHD 或 1080p) {"1920x1080", 1920, 1080}, {"1366x768", 1366, 768}, + {"1280x720", 1280, 720}, + {"1024x768", 1024, 768}, + {"800x600", 800, 600}, + {"800x480", 800, 480}, + {"640x480", 640, 480}, + {"640x360", 640, 360}, + {"480x360", 480, 360}, + {"400x240", 400, 240}, + {"320x240", 320, 240}, } - for _, v := range variants { - // 如果是探测到的最高清版本,且我们已经有了数据,直接使用 - var currentImgData []byte - if v.width == 0 { - currentImgData = imgData - } else { - resized := imaging.Fill(srcImg, v.width, v.height, imaging.Center, imaging.Lanczos) - buf := new(bytes.Buffer) - if err := jpeg.Encode(buf, resized, &jpeg.Options{Quality: 90}); err != nil { - util.Logger.Warn("Failed to encode jpeg", zap.String("variant", v.name), zap.Error(err)) - continue - } - currentImgData = buf.Bytes() + // 首先保存原图 (UHD 或 1080p) + if err := f.saveVariant(ctx, &dbImg, variantName, "jpg", imgData); err != nil { + util.Logger.Error("Failed to save original variant", zap.String("variant", variantName), zap.Error(err)) + } + + for _, v := range targetVariants { + // 如果目标分辨率就是原图分辨率,则跳过(已经保存过了) + if v.name == variantName { + continue } + resized := imaging.Fill(srcImg, v.width, v.height, imaging.Center, imaging.Lanczos) + buf := new(bytes.Buffer) + if err := jpeg.Encode(buf, resized, &jpeg.Options{Quality: 100}); err != nil { + util.Logger.Warn("Failed to encode jpeg", zap.String("variant", v.name), zap.Error(err)) + continue + } + currentImgData := buf.Bytes() + // 保存 JPG if err := f.saveVariant(ctx, &dbImg, v.name, "jpg", currentImgData); err != nil { util.Logger.Error("Failed to save variant", zap.String("variant", v.name), zap.Error(err)) } - } // 保存今日额外文件 @@ -241,13 +252,13 @@ func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte) { return } - // daily.jpeg (quality 95) + // daily.jpeg (quality 100) jpegPath := filepath.Join(localRoot, "daily.jpeg") fJpeg, err := os.Create(jpegPath) if err != nil { util.Logger.Error("Failed to create daily.jpeg", zap.Error(err)) } else { - jpeg.Encode(fJpeg, srcImg, &jpeg.Options{Quality: 95}) + jpeg.Encode(fJpeg, srcImg, &jpeg.Options{Quality: 100}) fJpeg.Close() } diff --git a/internal/service/image/image_service.go b/internal/service/image/image_service.go index b5fc3e7..7b0fba0 100644 --- a/internal/service/image/image_service.go +++ b/internal/service/image/image_service.go @@ -86,12 +86,29 @@ func GetImageByDate(date string) (*model.Image, error) { return &img, err } -func GetImageList(limit int) ([]model.Image, error) { +func GetImageList(limit int, offset int, month string) ([]model.Image, error) { var images []model.Image - db := repo.DB.Order("date desc").Preload("Variants") - if limit > 0 { - db = db.Limit(limit) + tx := repo.DB.Model(&model.Image{}) + + if month != "" { + // 增强过滤:确保只处理 YYYY-MM 格式,防止注入或非法字符 + // 这里简单处理:只要不为空就增加 LIKE 过滤 + util.Logger.Debug("Filtering images by month", zap.String("month", month)) + tx = tx.Where("date LIKE ?", month+"%") + } + + tx = tx.Order("date desc").Preload("Variants") + + if limit > 0 { + tx = tx.Limit(limit) + } + if offset > 0 { + tx = tx.Offset(offset) + } + + err := tx.Find(&images).Error + if err != nil { + util.Logger.Error("Failed to get image list", zap.Error(err), zap.String("month", month)) } - err := db.Find(&images).Error return images, err } diff --git a/webapp/doc/swagger.json b/webapp/doc/swagger.json index ab04513..674cdc6 100644 --- a/webapp/doc/swagger.json +++ b/webapp/doc/swagger.json @@ -531,7 +531,7 @@ { "type": "string", "default": "UHD", - "description": "分辨率 (UHD, 1920x1080, 1366x768)", + "description": "分辨率 (UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240)", "name": "variant", "in": "query" }, @@ -575,7 +575,7 @@ }, "/images": { "get": { - "description": "分页获取已抓取的图片元数据列表", + "description": "分页获取已抓取的图片元数据列表。支持分页(page, page_size)、限制数量(limit)和按月份过滤(month, 格式: YYYY-MM)。", "produces": [ "application/json" ], @@ -587,9 +587,27 @@ { "type": "integer", "default": 30, - "description": "限制数量", + "description": "限制数量 (如果不使用分页)", "name": "limit", "in": "query" + }, + { + "type": "integer", + "description": "页码 (从1开始)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "按月份过滤 (格式: YYYY-MM)", + "name": "month", + "in": "query" } ], "responses": { diff --git a/webapp/src/composables/useImages.ts b/webapp/src/composables/useImages.ts index 7ab0cda..8377dee 100644 --- a/webapp/src/composables/useImages.ts +++ b/webapp/src/composables/useImages.ts @@ -1,4 +1,5 @@ -import { ref, onMounted } from 'vue' +import { ref, onMounted, watch } from 'vue' +import type { Ref } from 'vue' import { bingPaperApi } from '@/lib/api-service' import type { ImageMeta } from '@/lib/api-types' @@ -36,27 +37,43 @@ export function useTodayImage() { } /** - * 获取图片列表(支持分页) + * 获取图片列表(支持分页和月份筛选) */ -export function useImageList(initialLimit = 30) { +export function useImageList(pageSize = 30) { const images = ref([]) const loading = ref(false) const error = ref(null) const hasMore = ref(true) + const currentPage = ref(1) + const currentMonth = ref(undefined) - const fetchImages = async (limit = initialLimit) => { + const fetchImages = async (page = 1, month?: string) => { if (loading.value) return loading.value = true error.value = null try { - const newImages = await bingPaperApi.getImages({ limit }) - - if (newImages.length < limit) { - hasMore.value = false + const params: any = { + page, + page_size: pageSize + } + if (month) { + params.month = month } - images.value = [...images.value, ...newImages] + const newImages = await bingPaperApi.getImages(params) + + if (page === 1) { + // 首次加载或重新筛选 + images.value = newImages + } else { + // 加载更多 + images.value = [...images.value, ...newImages] + } + + // 判断是否还有更多数据 + hasMore.value = newImages.length === pageSize + currentPage.value = page } catch (e) { error.value = e as Error console.error('Failed to fetch images:', e) @@ -67,12 +84,19 @@ export function useImageList(initialLimit = 30) { const loadMore = () => { if (!loading.value && hasMore.value) { - fetchImages() + fetchImages(currentPage.value + 1, currentMonth.value) } } + const filterByMonth = (month?: string) => { + currentMonth.value = month + currentPage.value = 1 + hasMore.value = true + fetchImages(1, month) + } + onMounted(() => { - fetchImages() + fetchImages(1) }) return { @@ -81,10 +105,11 @@ export function useImageList(initialLimit = 30) { error, hasMore, loadMore, + filterByMonth, refetch: () => { - images.value = [] + currentPage.value = 1 hasMore.value = true - fetchImages() + fetchImages(1, currentMonth.value) } } } @@ -92,7 +117,7 @@ export function useImageList(initialLimit = 30) { /** * 获取指定日期的图片 */ -export function useImageByDate(date: string) { +export function useImageByDate(dateRef: Ref) { const image = ref(null) const loading = ref(false) const error = ref(null) @@ -101,18 +126,19 @@ export function useImageByDate(date: string) { loading.value = true error.value = null try { - image.value = await bingPaperApi.getImageMetaByDate(date) + image.value = await bingPaperApi.getImageMetaByDate(dateRef.value) } catch (e) { error.value = e as Error - console.error(`Failed to fetch image for date ${date}:`, e) + console.error(`Failed to fetch image for date ${dateRef.value}:`, e) } finally { loading.value = false } } - onMounted(() => { + // 监听日期变化,自动重新获取 + watch(dateRef, () => { fetchImage() - }) + }, { immediate: true }) return { image, diff --git a/webapp/src/lib/api-service.ts b/webapp/src/lib/api-service.ts index 57e9cb0..022b02c 100644 --- a/webapp/src/lib/api-service.ts +++ b/webapp/src/lib/api-service.ts @@ -106,6 +106,9 @@ export class BingPaperApiService { const searchParams = new URLSearchParams() if (params?.limit) searchParams.set('limit', params.limit.toString()) if (params?.offset) searchParams.set('offset', params.offset.toString()) + if (params?.page) searchParams.set('page', params.page.toString()) + if (params?.page_size) searchParams.set('page_size', params.page_size.toString()) + if (params?.month) searchParams.set('month', params.month) const queryString = searchParams.toString() const endpoint = queryString ? `/images?${queryString}` : '/images' diff --git a/webapp/src/lib/api-types.ts b/webapp/src/lib/api-types.ts index 53543ae..c33e288 100644 --- a/webapp/src/lib/api-types.ts +++ b/webapp/src/lib/api-types.ts @@ -161,7 +161,9 @@ export interface ImageMeta { } export interface ImageListParams extends PaginationParams { - // 可以扩展更多筛选参数 + page?: number // 页码(从1开始) + page_size?: number // 每页数量 + month?: string // 按月份过滤(格式:YYYY-MM) } export interface ManualFetchRequest { @@ -170,5 +172,5 @@ export interface ManualFetchRequest { // ===== API 端点类型定义 ===== -export type ImageVariant = 'UHD' | '1920x1080' | '1366x768' +export type ImageVariant = 'UHD' | '1920x1080' | '1366x768' | '1280x720' | '1024x768' | '800x600' | '800x480' | '640x480' | '640x360' | '480x360' | '400x240' | '320x240' export type ImageFormat = 'jpg' \ No newline at end of file diff --git a/webapp/src/views/ApiDocs.vue b/webapp/src/views/ApiDocs.vue index 2594821..cf3acb8 100644 --- a/webapp/src/views/ApiDocs.vue +++ b/webapp/src/views/ApiDocs.vue @@ -94,7 +94,10 @@
variant - 分辨率: UHD, 1920x1080, 1366x768 (默认: UHD) +
+ 分辨率 (默认: UHD) + 可选值: UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240 +
format @@ -170,7 +173,10 @@
variant - 分辨率 (默认: UHD) +
+ 分辨率 (默认: UHD) + 可选值: UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240 +
format @@ -239,7 +245,10 @@
variant - 分辨率 (默认: UHD) +
+ 分辨率 (默认: UHD) + 可选值: UHD, 1920x1080, 1366x768, 1280x720, 1024x768, 800x600, 800x480, 640x480, 640x360, 480x360, 400x240, 320x240 +
format diff --git a/webapp/src/views/Home.vue b/webapp/src/views/Home.vue index c21cba4..a712f4b 100644 --- a/webapp/src/views/Home.vue +++ b/webapp/src/views/Home.vue @@ -62,20 +62,87 @@
-

- 历史精选 -

+
+

+ 历史精选 +

+ + +
+ + + + + + + + +
+
+ + +
+ 当前显示: + + {{ selectedYear }} 年 {{ selectedMonth }} 月 + + 的图片(共 {{ images.length }} 张) +
- + +
+
+
-
-
-
+
+
+
{{ formatDate(image.date) }}
-

+

{{ image.title || '未命名' }}

-

+

{{ image.copyright }}

@@ -162,17 +229,182 @@