增加多地区每日图片抓取能力

This commit is contained in:
2026-01-30 13:33:40 +08:00
parent b69db53f0a
commit 93690e10d3
24 changed files with 980 additions and 149 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"sort"
"strconv"
"BingPaper/internal/config"
@@ -27,6 +28,7 @@ type ImageVariantResp struct {
type ImageMetaResp struct {
Date string `json:"date"`
Mkt string `json:"mkt"`
Title string `json:"title"`
Copyright string `json:"copyright"`
CopyrightLink string `json:"copyrightlink"`
@@ -41,13 +43,15 @@ type ImageMetaResp struct {
// @Summary 获取今日图片
// @Description 根据参数返回今日必应图片流或重定向
// @Tags image
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @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
// @Router /image/today [get]
func GetToday(c *gin.Context) {
img, err := image.GetTodayImage()
mkt := c.Query("mkt")
img, err := image.GetTodayImage(mkt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -59,11 +63,13 @@ func GetToday(c *gin.Context) {
// @Summary 获取今日图片元数据
// @Description 获取今日必应图片的标题、版权等元数据
// @Tags image
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @Produce json
// @Success 200 {object} ImageMetaResp
// @Router /image/today/meta [get]
func GetTodayMeta(c *gin.Context) {
img, err := image.GetTodayImage()
mkt := c.Query("mkt")
img, err := image.GetTodayImage(mkt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -76,13 +82,15 @@ func GetTodayMeta(c *gin.Context) {
// @Summary 获取随机图片
// @Description 随机返回一张已抓取的图片流或重定向
// @Tags image
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @Param variant query string false "分辨率" default(UHD)
// @Param format query string false "格式" default(jpg)
// @Produce image/jpeg
// @Success 200 {file} binary
// @Router /image/random [get]
func GetRandom(c *gin.Context) {
img, err := image.GetRandomImage()
mkt := c.Query("mkt")
img, err := image.GetRandomImage(mkt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -94,11 +102,13 @@ func GetRandom(c *gin.Context) {
// @Summary 获取随机图片元数据
// @Description 随机获取一张已抓取图片的元数据
// @Tags image
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @Produce json
// @Success 200 {object} ImageMetaResp
// @Router /image/random/meta [get]
func GetRandomMeta(c *gin.Context) {
img, err := image.GetRandomImage()
mkt := c.Query("mkt")
img, err := image.GetRandomImage(mkt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -112,6 +122,7 @@ func GetRandomMeta(c *gin.Context) {
// @Description 根据日期返回图片流或重定向 (yyyy-mm-dd)
// @Tags image
// @Param date path string true "日期 (yyyy-mm-dd)"
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @Param variant query string false "分辨率" default(UHD)
// @Param format query string false "格式" default(jpg)
// @Produce image/jpeg
@@ -119,7 +130,8 @@ func GetRandomMeta(c *gin.Context) {
// @Router /image/date/{date} [get]
func GetByDate(c *gin.Context) {
date := c.Param("date")
img, err := image.GetImageByDate(date)
mkt := c.Query("mkt")
img, err := image.GetImageByDate(date, mkt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -132,12 +144,14 @@ func GetByDate(c *gin.Context) {
// @Description 根据日期获取图片元数据 (yyyy-mm-dd)
// @Tags image
// @Param date path string true "日期 (yyyy-mm-dd)"
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @Produce json
// @Success 200 {object} ImageMetaResp
// @Router /image/date/{date}/meta [get]
func GetByDateMeta(c *gin.Context) {
date := c.Param("date")
img, err := image.GetImageByDate(date)
mkt := c.Query("mkt")
img, err := image.GetImageByDate(date, mkt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -154,6 +168,7 @@ func GetByDateMeta(c *gin.Context) {
// @Param page query int false "页码 (从1开始)"
// @Param page_size query int false "每页数量"
// @Param month query string false "按月份过滤 (格式: YYYY-MM)"
// @Param mkt query string false "地区编码 (如 zh-CN, en-US)"
// @Produce json
// @Success 200 {array} ImageMetaResp
// @Router /images [get]
@@ -162,10 +177,12 @@ func ListImages(c *gin.Context) {
pageStr := c.Query("page")
pageSizeStr := c.Query("page_size")
month := c.Query("month")
mkt := c.Query("mkt")
// 记录请求参数,便于排查过滤失效问题
util.Logger.Debug("ListImages parameters",
zap.String("month", month),
zap.String("mkt", mkt),
zap.String("page", pageStr),
zap.String("page_size", pageSizeStr),
zap.String("limit", limitStr))
@@ -192,7 +209,7 @@ func ListImages(c *gin.Context) {
offset = 0
}
images, err := image.GetImageList(limit, offset, month)
images, err := image.GetImageList(limit, offset, month, mkt)
if err != nil {
util.Logger.Error("ListImages service call failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@@ -291,7 +308,7 @@ func formatMeta(img *model.Image) gin.H {
if url == "" && cfg.API.Mode == "redirect" && img.URLBase != "" {
url = fmt.Sprintf("https://www.bing.com%s_%s.jpg", img.URLBase, v.Variant)
} else if cfg.API.Mode == "local" || url == "" {
url = fmt.Sprintf("%s/api/v1/image/date/%s?variant=%s&format=%s", cfg.Server.BaseURL, img.Date, v.Variant, v.Format)
url = fmt.Sprintf("%s/api/v1/image/date/%s?variant=%s&format=%s&mkt=%s", cfg.Server.BaseURL, img.Date, v.Variant, v.Format, img.Mkt)
}
variants = append(variants, gin.H{
"variant": v.Variant,
@@ -304,6 +321,7 @@ func formatMeta(img *model.Image) gin.H {
return gin.H{
"date": img.Date,
"mkt": img.Mkt,
"title": img.Title,
"copyright": img.Copyright,
"copyrightlink": img.CopyrightLink,
@@ -314,3 +332,46 @@ func formatMeta(img *model.Image) gin.H {
"variants": variants,
}
}
// GetRegions 获取支持的地区列表
// @Summary 获取支持的地区列表
// @Description 返回系统支持的所有必应地区编码及标签。如果配置中指定了抓取地区,这些地区将排在列表最前面(置顶)。
// @Tags image
// @Produce json
// @Success 200 {array} util.Region
// @Router /regions [get]
func GetRegions(c *gin.Context) {
cfg := config.GetConfig()
pinned := cfg.Fetcher.Regions
// 创建副本以避免修改原始全局变量
all := make([]util.Region, len(util.AllRegions))
copy(all, util.AllRegions)
if len(pinned) > 0 {
// 创建一个 Map 用于快速查找置顶地区及其顺序
pinnedMap := make(map[string]int)
for i, v := range pinned {
pinnedMap[v] = i
}
// 对列表进行稳定排序,使置顶地区排在前面
sort.SliceStable(all, func(i, j int) bool {
idxI, okI := pinnedMap[all[i].Value]
idxJ, okJ := pinnedMap[all[j].Value]
if okI && okJ {
return idxI < idxJ
}
if okI {
return true
}
if okJ {
return false
}
return false // 保持非置顶地区的原有相对顺序
})
}
c.JSON(http.StatusOK, all)
}

View File

@@ -1,6 +1,7 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
@@ -66,3 +67,27 @@ func TestHandleImageResponseRedirect(t *testing.T) {
assert.Contains(t, variants[0]["url"].(string), "/api/v1/image/date/")
})
}
func TestGetRegions(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("GetRegions should respect pinned order", func(t *testing.T) {
// Setup config with custom pinned regions
config.Init("")
config.GetConfig().Fetcher.Regions = []string{"en-US", "ja-JP"}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
GetRegions(c)
assert.Equal(t, http.StatusOK, w.Code)
var regions []map[string]string
err := json.Unmarshal(w.Body.Bytes(), &regions)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(regions), 2)
assert.Equal(t, "en-US", regions[0]["value"])
assert.Equal(t, "ja-JP", regions[1]["value"])
})
}