mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 07:19:33 +08:00
增加多地区每日图片抓取能力
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"BingPaper/internal/config"
|
||||
"BingPaper/internal/cron"
|
||||
@@ -52,10 +53,11 @@ func Init(webFS embed.FS, configPath string) *gin.Engine {
|
||||
|
||||
// 输出配置信息
|
||||
util.Logger.Info("Application configuration loaded")
|
||||
util.Logger.Info("├─ Config file", zap.String("path", config.GetRawViper().ConfigFileUsed()))
|
||||
util.Logger.Info("├─ Database ", zap.String("type", cfg.DB.Type))
|
||||
util.Logger.Info("├─ Storage ", zap.String("type", cfg.Storage.Type))
|
||||
util.Logger.Info("└─ Server ", zap.Int("port", cfg.Server.Port))
|
||||
util.Logger.Info("├─ Config file ", zap.String("path", config.GetRawViper().ConfigFileUsed()))
|
||||
util.Logger.Info("├─ Database ", zap.String("type", cfg.DB.Type))
|
||||
util.Logger.Info("├─ Storage ", zap.String("type", cfg.Storage.Type))
|
||||
util.Logger.Info("├─ Server ", zap.Int("port", cfg.Server.Port))
|
||||
util.Logger.Info("└─ Active Mkt ", zap.Strings("regions", cfg.Fetcher.Regions))
|
||||
|
||||
// 根据存储类型输出更多信息
|
||||
switch cfg.Storage.Type {
|
||||
@@ -147,5 +149,6 @@ func LogWelcomeInfo() {
|
||||
fmt.Printf(" - 管理后台: %s/admin\n", baseURL)
|
||||
fmt.Printf(" - API 文档: %s/swagger/index.html\n", baseURL)
|
||||
fmt.Printf(" - 今日图片: %s/api/v1/image/today\n", baseURL)
|
||||
fmt.Printf(" - 激活地区: %s\n", strings.Join(cfg.Fetcher.Regions, ", "))
|
||||
fmt.Println("---------------------------------------------------------")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"BingPaper/internal/util"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -25,6 +27,7 @@ type Config struct {
|
||||
Token TokenConfig `mapstructure:"token" yaml:"token"`
|
||||
Feature FeatureConfig `mapstructure:"feature" yaml:"feature"`
|
||||
Web WebConfig `mapstructure:"web" yaml:"web"`
|
||||
Fetcher FetcherConfig `mapstructure:"fetcher" yaml:"fetcher"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -57,7 +60,8 @@ func (c LogConfig) GetShowDBLog() bool { return c.ShowDBLog }
|
||||
func (c LogConfig) GetDBLogLevel() string { return c.DBLogLevel }
|
||||
|
||||
type APIConfig struct {
|
||||
Mode string `mapstructure:"mode" yaml:"mode"` // local | redirect
|
||||
Mode string `mapstructure:"mode" yaml:"mode"` // local | redirect
|
||||
EnableMktFallback bool `mapstructure:"enable_mkt_fallback" yaml:"enable_mkt_fallback"` // 当请求的地区不存在时,是否回退到默认地区
|
||||
}
|
||||
|
||||
type CronConfig struct {
|
||||
@@ -118,6 +122,10 @@ type WebConfig struct {
|
||||
Path string `mapstructure:"path" yaml:"path"`
|
||||
}
|
||||
|
||||
type FetcherConfig struct {
|
||||
Regions []string `mapstructure:"regions" yaml:"regions"`
|
||||
}
|
||||
|
||||
// Bing 默认配置 (内置)
|
||||
const (
|
||||
BingMkt = "zh-CN"
|
||||
@@ -157,6 +165,7 @@ func Init(configPath string) error {
|
||||
v.SetDefault("log.show_db_log", false)
|
||||
v.SetDefault("log.db_log_level", "info")
|
||||
v.SetDefault("api.mode", "local")
|
||||
v.SetDefault("api.enable_mkt_fallback", true)
|
||||
v.SetDefault("cron.enabled", true)
|
||||
v.SetDefault("cron.daily_spec", "20 8-23/4 * * *")
|
||||
v.SetDefault("retention.days", 0)
|
||||
@@ -167,6 +176,13 @@ func Init(configPath string) error {
|
||||
v.SetDefault("token.default_ttl", "168h")
|
||||
v.SetDefault("feature.write_daily_files", true)
|
||||
v.SetDefault("web.path", "web")
|
||||
|
||||
// 默认抓取所有支持的地区
|
||||
var defaultRegions []string
|
||||
for _, r := range util.AllRegions {
|
||||
defaultRegions = append(defaultRegions, r.Value)
|
||||
}
|
||||
v.SetDefault("fetcher.regions", defaultRegions)
|
||||
v.SetDefault("admin.password_bcrypt", "$2a$10$fYHPeWHmwObephJvtlyH1O8DIgaLk5TINbi9BOezo2M8cSjmJchka") // 默认密码: admin123
|
||||
|
||||
// 绑定环境变量
|
||||
@@ -311,3 +327,11 @@ func GetTokenTTL() time.Duration {
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
// GetDefaultMkt 返回生效的默认地区编码
|
||||
func (c *Config) GetDefaultMkt() string {
|
||||
if len(c.Fetcher.Regions) > 0 {
|
||||
return c.Fetcher.Regions[0]
|
||||
}
|
||||
return BingMkt
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(), ®ions)
|
||||
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"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func SetupRouter(webFS embed.FS) *gin.Engine {
|
||||
img.GET("/date/:date/meta", handlers.GetByDateMeta)
|
||||
}
|
||||
api.GET("/images", handlers.ListImages)
|
||||
api.GET("/regions", handlers.GetRegions)
|
||||
|
||||
// 管理接口
|
||||
admin := api.Group("/admin")
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
|
||||
type Image struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Date string `gorm:"uniqueIndex;type:varchar(10)" json:"date"` // YYYY-MM-DD
|
||||
Date string `gorm:"uniqueIndex:idx_date_mkt;type:varchar(10)" json:"date"` // YYYY-MM-DD
|
||||
Mkt string `gorm:"uniqueIndex:idx_date_mkt;type:varchar(10)" json:"mkt"` // zh-CN, en-US etc.
|
||||
Title string `json:"title"`
|
||||
Copyright string `json:"copyright"`
|
||||
CopyrightLink string `json:"copyrightlink"`
|
||||
|
||||
@@ -55,7 +55,30 @@ func NewFetcher() *Fetcher {
|
||||
|
||||
func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
||||
util.Logger.Info("Starting fetch task", zap.Int("n", n))
|
||||
url := fmt.Sprintf("%s?format=js&idx=0&n=%d&uhd=1&mkt=%s", config.BingAPIBase, n, config.BingMkt)
|
||||
regions := config.GetConfig().Fetcher.Regions
|
||||
if len(regions) == 0 {
|
||||
regions = []string{config.GetConfig().GetDefaultMkt()}
|
||||
}
|
||||
|
||||
for _, mkt := range regions {
|
||||
util.Logger.Info("Fetching images for region", zap.String("mkt", mkt))
|
||||
// 调用两次 API 获取最多两周的数据
|
||||
// 第一次 idx=0&n=8 (今天起往回数 8 张)
|
||||
if err := f.fetchByMkt(ctx, mkt, 0, 8); err != nil {
|
||||
util.Logger.Error("Failed to fetch images", zap.String("mkt", mkt), zap.Int("idx", 0), zap.Error(err))
|
||||
}
|
||||
// 第二次 idx=7&n=8 (7天前起往回数 8 张,与第一次有重叠,确保不漏)
|
||||
if err := f.fetchByMkt(ctx, mkt, 7, 8); err != nil {
|
||||
util.Logger.Error("Failed to fetch images", zap.String("mkt", mkt), zap.Int("idx", 7), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
util.Logger.Info("Fetch task completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) fetchByMkt(ctx context.Context, mkt string, idx int, n int) error {
|
||||
url := fmt.Sprintf("%s?format=js&idx=%d&n=%d&uhd=1&mkt=%s", config.BingAPIBase, idx, n, mkt)
|
||||
util.Logger.Debug("Requesting Bing API", zap.String("url", url))
|
||||
resp, err := f.httpClient.Get(url)
|
||||
if err != nil {
|
||||
@@ -70,29 +93,28 @@ func (f *Fetcher) Fetch(ctx context.Context, n int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
util.Logger.Info("Fetched images from Bing", zap.Int("count", len(bingResp.Images)))
|
||||
util.Logger.Info("Fetched images from Bing", zap.String("mkt", mkt), zap.Int("count", len(bingResp.Images)))
|
||||
|
||||
for _, bingImg := range bingResp.Images {
|
||||
if err := f.processImage(ctx, bingImg); err != nil {
|
||||
util.Logger.Error("Failed to process image", zap.String("date", bingImg.Enddate), zap.Error(err))
|
||||
if err := f.processImage(ctx, bingImg, mkt); err != nil {
|
||||
util.Logger.Error("Failed to process image", zap.String("date", bingImg.Enddate), zap.String("mkt", mkt), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
util.Logger.Info("Fetch task completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
||||
func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage, mkt string) error {
|
||||
dateStr := fmt.Sprintf("%s-%s-%s", bingImg.Enddate[0:4], bingImg.Enddate[4:6], bingImg.Enddate[6:8])
|
||||
|
||||
// 幂等检查
|
||||
var existing model.Image
|
||||
if err := repo.DB.Where("date = ?", dateStr).First(&existing).Error; err == nil {
|
||||
util.Logger.Info("Image already exists, skipping", zap.String("date", dateStr))
|
||||
if err := repo.DB.Where("date = ? AND mkt = ?", dateStr, mkt).First(&existing).Error; err == nil {
|
||||
util.Logger.Info("Image already exists, skipping", zap.String("date", dateStr), zap.String("mkt", mkt))
|
||||
return nil
|
||||
}
|
||||
|
||||
util.Logger.Info("Processing new image", zap.String("date", dateStr), zap.String("title", bingImg.Title))
|
||||
util.Logger.Info("Processing new image", zap.String("date", dateStr), zap.String("mkt", mkt), zap.String("title", bingImg.Title))
|
||||
|
||||
// UHD 探测
|
||||
imgURL, variantName := f.probeUHD(bingImg.URLBase)
|
||||
@@ -113,6 +135,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
||||
// 创建 DB 记录
|
||||
dbImg := model.Image{
|
||||
Date: dateStr,
|
||||
Mkt: mkt,
|
||||
Title: bingImg.Title,
|
||||
Copyright: bingImg.Copyright,
|
||||
CopyrightLink: bingImg.CopyrightLink,
|
||||
@@ -124,7 +147,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
||||
}
|
||||
|
||||
if err := repo.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "date"}},
|
||||
Columns: []clause.Column{{Name: "date"}, {Name: "mkt"}},
|
||||
DoNothing: true,
|
||||
}).Create(&dbImg).Error; err != nil {
|
||||
util.Logger.Error("Failed to create image record", zap.Error(err))
|
||||
@@ -134,7 +157,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
||||
// 再次检查 dbImg.ID 是否被填充,如果没有被填充(说明由于冲突未插入),则需要查询出已有的 ID
|
||||
if dbImg.ID == 0 {
|
||||
var existing model.Image
|
||||
if err := repo.DB.Where("date = ?", dateStr).First(&existing).Error; err != nil {
|
||||
if err := repo.DB.Where("date = ? AND mkt = ?", dateStr, mkt).First(&existing).Error; err != nil {
|
||||
util.Logger.Error("Failed to query existing image record after conflict", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
@@ -188,7 +211,7 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
|
||||
// 保存今日额外文件
|
||||
today := time.Now().Format("2006-01-02")
|
||||
if dateStr == today && config.GetConfig().Feature.WriteDailyFiles {
|
||||
f.saveDailyFiles(srcImg, imgData)
|
||||
f.saveDailyFiles(srcImg, imgData, mkt)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -213,7 +236,7 @@ func (f *Fetcher) downloadImage(url string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) saveVariant(ctx context.Context, img *model.Image, variant, format string, data []byte) error {
|
||||
key := fmt.Sprintf("%s/%s_%s.%s", img.Date, img.Date, variant, format)
|
||||
key := fmt.Sprintf("%s/%s/%s_%s.%s", img.Mkt, img.Date, img.Date, variant, format)
|
||||
contentType := "image/jpeg"
|
||||
if format == "webp" {
|
||||
contentType = "image/webp"
|
||||
@@ -239,20 +262,21 @@ func (f *Fetcher) saveVariant(ctx context.Context, img *model.Image, variant, fo
|
||||
}).Create(&vRecord).Error
|
||||
}
|
||||
|
||||
func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte) {
|
||||
util.Logger.Info("Saving daily files")
|
||||
func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte, mkt string) {
|
||||
util.Logger.Info("Saving daily files", zap.String("mkt", mkt))
|
||||
localRoot := config.GetConfig().Storage.Local.Root
|
||||
if localRoot == "" {
|
||||
localRoot = "data"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(localRoot, 0755); err != nil {
|
||||
util.Logger.Error("Failed to create directory", zap.String("path", localRoot), zap.Error(err))
|
||||
mktDir := filepath.Join(localRoot, mkt)
|
||||
if err := os.MkdirAll(mktDir, 0755); err != nil {
|
||||
util.Logger.Error("Failed to create directory", zap.String("path", mktDir), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// daily.jpeg (quality 100)
|
||||
jpegPath := filepath.Join(localRoot, "daily.jpeg")
|
||||
jpegPath := filepath.Join(mktDir, "daily.jpeg")
|
||||
fJpeg, err := os.Create(jpegPath)
|
||||
if err != nil {
|
||||
util.Logger.Error("Failed to create daily.jpeg", zap.Error(err))
|
||||
@@ -262,8 +286,21 @@ func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte) {
|
||||
}
|
||||
|
||||
// original.jpeg (quality 100)
|
||||
originalPath := filepath.Join(localRoot, "original.jpeg")
|
||||
originalPath := filepath.Join(mktDir, "original.jpeg")
|
||||
if err := os.WriteFile(originalPath, originalData, 0644); err != nil {
|
||||
util.Logger.Error("Failed to write original.jpeg", zap.Error(err))
|
||||
}
|
||||
|
||||
// 同时也保留一份在根目录下(兼容旧逻辑,且作为默认地区图片)
|
||||
// 如果是默认地区或者是第一个抓取的地区,可以覆盖根目录的文件
|
||||
if mkt == config.GetConfig().GetDefaultMkt() {
|
||||
jpegPathRoot := filepath.Join(localRoot, "daily.jpeg")
|
||||
fJpegRoot, err := os.Create(jpegPathRoot)
|
||||
if err == nil {
|
||||
jpeg.Encode(fJpegRoot, srcImg, &jpeg.Options{Quality: 100})
|
||||
fJpegRoot.Close()
|
||||
}
|
||||
originalPathRoot := filepath.Join(localRoot, "original.jpeg")
|
||||
os.WriteFile(originalPathRoot, originalData, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,43 +50,97 @@ func CleanupOldImages(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTodayImage() (*model.Image, error) {
|
||||
func GetTodayImage(mkt string) (*model.Image, error) {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var img model.Image
|
||||
err := repo.DB.Where("date = ?", today).Preload("Variants").First(&img).Error
|
||||
tx := repo.DB.Where("date = ?", today)
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
err := tx.Preload("Variants").First(&img).Error
|
||||
if err != nil {
|
||||
// 如果今天没有,尝试获取最近的一张
|
||||
err = repo.DB.Order("date desc").Preload("Variants").First(&img).Error
|
||||
tx = repo.DB.Order("date desc")
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
err = tx.Preload("Variants").First(&img).Error
|
||||
}
|
||||
|
||||
// 兜底逻辑:如果指定地区没找到,且开启了兜底开关,则尝试获取默认地区的图片
|
||||
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||
defaultMkt := config.GetConfig().GetDefaultMkt()
|
||||
if mkt != defaultMkt {
|
||||
return GetTodayImage(defaultMkt)
|
||||
}
|
||||
return GetTodayImage("")
|
||||
}
|
||||
|
||||
return &img, err
|
||||
}
|
||||
|
||||
func GetRandomImage() (*model.Image, error) {
|
||||
func GetRandomImage(mkt string) (*model.Image, error) {
|
||||
var img model.Image
|
||||
// SQLite 使用 RANDOM(), MySQL/Postgres 使用 RANDOM() 或 RAND()
|
||||
// 简单起见,先查总数再 Offset
|
||||
var count int64
|
||||
repo.DB.Model(&model.Image{}).Count(&count)
|
||||
tx := repo.DB.Model(&model.Image{})
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
tx.Count(&count)
|
||||
if count == 0 {
|
||||
return nil, fmt.Errorf("no images found")
|
||||
}
|
||||
|
||||
// 这种方法不适合海量数据,但对于 30 天的数据没问题
|
||||
err := repo.DB.Order("RANDOM()").Preload("Variants").First(&img).Error
|
||||
tx = repo.DB.Order("RANDOM()")
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
err := tx.Preload("Variants").First(&img).Error
|
||||
if err != nil {
|
||||
// 适配 MySQL
|
||||
err = repo.DB.Order("RAND()").Preload("Variants").First(&img).Error
|
||||
tx = repo.DB.Order("RAND()")
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
err = tx.Preload("Variants").First(&img).Error
|
||||
}
|
||||
|
||||
// 兜底逻辑
|
||||
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||
defaultMkt := config.GetConfig().GetDefaultMkt()
|
||||
if mkt != defaultMkt {
|
||||
return GetRandomImage(defaultMkt)
|
||||
}
|
||||
return GetRandomImage("")
|
||||
}
|
||||
|
||||
return &img, err
|
||||
}
|
||||
|
||||
func GetImageByDate(date string) (*model.Image, error) {
|
||||
func GetImageByDate(date string, mkt string) (*model.Image, error) {
|
||||
var img model.Image
|
||||
err := repo.DB.Where("date = ?", date).Preload("Variants").First(&img).Error
|
||||
tx := repo.DB.Where("date = ?", date)
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
err := tx.Preload("Variants").First(&img).Error
|
||||
|
||||
// 兜底逻辑
|
||||
if err != nil && mkt != "" && config.GetConfig().API.EnableMktFallback {
|
||||
defaultMkt := config.GetConfig().GetDefaultMkt()
|
||||
if mkt != defaultMkt {
|
||||
return GetImageByDate(date, defaultMkt)
|
||||
}
|
||||
return GetImageByDate(date, "")
|
||||
}
|
||||
|
||||
return &img, err
|
||||
}
|
||||
|
||||
func GetImageList(limit int, offset int, month string) ([]model.Image, error) {
|
||||
func GetImageList(limit int, offset int, month string, mkt string) ([]model.Image, error) {
|
||||
var images []model.Image
|
||||
tx := repo.DB.Model(&model.Image{})
|
||||
|
||||
@@ -97,6 +151,10 @@ func GetImageList(limit int, offset int, month string) ([]model.Image, error) {
|
||||
tx = tx.Where("date LIKE ?", month+"%")
|
||||
}
|
||||
|
||||
if mkt != "" {
|
||||
tx = tx.Where("mkt = ?", mkt)
|
||||
}
|
||||
|
||||
tx = tx.Order("date desc").Preload("Variants")
|
||||
|
||||
if limit > 0 {
|
||||
|
||||
26
internal/util/regions.go
Normal file
26
internal/util/regions.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package util
|
||||
|
||||
type Region struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
var AllRegions = []Region{
|
||||
{Value: "zh-CN", Label: "中国 (zh-CN)"},
|
||||
{Value: "en-US", Label: "美国 (en-US)"},
|
||||
{Value: "ja-JP", Label: "日本 (ja-JP)"},
|
||||
{Value: "en-AU", Label: "澳大利亚 (en-AU)"},
|
||||
{Value: "en-GB", Label: "英国 (en-GB)"},
|
||||
{Value: "de-DE", Label: "德国 (de-DE)"},
|
||||
{Value: "en-NZ", Label: "新西兰 (en-NZ)"},
|
||||
{Value: "en-CA", Label: "加拿大 (en-CA)"},
|
||||
{Value: "fr-FR", Label: "法国 (fr-FR)"},
|
||||
{Value: "it-IT", Label: "意大利 (it-IT)"},
|
||||
{Value: "es-ES", Label: "西班牙 (es-ES)"},
|
||||
{Value: "pt-BR", Label: "巴西 (pt-BR)"},
|
||||
{Value: "ko-KR", Label: "韩国 (ko-KR)"},
|
||||
{Value: "en-IN", Label: "印度 (en-IN)"},
|
||||
{Value: "ru-RU", Label: "俄罗斯 (ru-RU)"},
|
||||
{Value: "zh-HK", Label: "中国香港 (zh-HK)"},
|
||||
{Value: "zh-TW", Label: "中国台湾 (zh-TW)"},
|
||||
}
|
||||
Reference in New Issue
Block a user