保存更多图片元数据并同步更新前端

This commit is contained in:
2026-01-27 13:52:40 +08:00
parent 6dfffe1236
commit 9c2a5d5cd8
11 changed files with 331 additions and 72 deletions

View File

@@ -461,8 +461,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -518,8 +517,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -575,8 +573,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -607,8 +604,7 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -885,6 +881,61 @@ const docTemplate = `{
} }
} }
}, },
"handlers.ImageMetaResp": {
"type": "object",
"properties": {
"copyright": {
"type": "string"
},
"copyrightlink": {
"type": "string"
},
"date": {
"type": "string"
},
"fullstartdate": {
"type": "string"
},
"hsh": {
"type": "string"
},
"quiz": {
"type": "string"
},
"startdate": {
"type": "string"
},
"title": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ImageVariantResp"
}
}
}
},
"handlers.ImageVariantResp": {
"type": "object",
"properties": {
"format": {
"type": "string"
},
"size": {
"type": "integer"
},
"storage_key": {
"type": "string"
},
"url": {
"type": "string"
},
"variant": {
"type": "string"
}
}
},
"handlers.LoginRequest": { "handlers.LoginRequest": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -455,8 +455,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -512,8 +511,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -569,8 +567,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -601,8 +598,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -879,6 +875,61 @@
} }
} }
}, },
"handlers.ImageMetaResp": {
"type": "object",
"properties": {
"copyright": {
"type": "string"
},
"copyrightlink": {
"type": "string"
},
"date": {
"type": "string"
},
"fullstartdate": {
"type": "string"
},
"hsh": {
"type": "string"
},
"quiz": {
"type": "string"
},
"startdate": {
"type": "string"
},
"title": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ImageVariantResp"
}
}
}
},
"handlers.ImageVariantResp": {
"type": "object",
"properties": {
"format": {
"type": "string"
},
"size": {
"type": "integer"
},
"storage_key": {
"type": "string"
},
"url": {
"type": "string"
},
"variant": {
"type": "string"
}
}
},
"handlers.LoginRequest": { "handlers.LoginRequest": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -178,6 +178,42 @@ definitions:
required: required:
- name - name
type: object type: object
handlers.ImageMetaResp:
properties:
copyright:
type: string
copyrightlink:
type: string
date:
type: string
fullstartdate:
type: string
hsh:
type: string
quiz:
type: string
startdate:
type: string
title:
type: string
variants:
items:
$ref: '#/definitions/handlers.ImageVariantResp'
type: array
type: object
handlers.ImageVariantResp:
properties:
format:
type: string
size:
type: integer
storage_key:
type: string
url:
type: string
variant:
type: string
type: object
handlers.LoginRequest: handlers.LoginRequest:
properties: properties:
password: password:
@@ -500,8 +536,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.ImageMetaResp'
type: object
summary: 获取指定日期图片元数据 summary: 获取指定日期图片元数据
tags: tags:
- image - image
@@ -538,8 +573,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.ImageMetaResp'
type: object
summary: 获取随机图片元数据 summary: 获取随机图片元数据
tags: tags:
- image - image
@@ -576,8 +610,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/handlers.ImageMetaResp'
type: object
summary: 获取今日图片元数据 summary: 获取今日图片元数据
tags: tags:
- image - image
@@ -597,8 +630,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
additionalProperties: true $ref: '#/definitions/handlers.ImageMetaResp'
type: object
type: array type: array
summary: 获取图片列表 summary: 获取图片列表
tags: tags:

View File

@@ -16,6 +16,26 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
type ImageVariantResp struct {
Variant string `json:"variant"`
Format string `json:"format"`
Size int64 `json:"size"`
URL string `json:"url"`
StorageKey string `json:"storage_key"`
}
type ImageMetaResp struct {
Date string `json:"date"`
Title string `json:"title"`
Copyright string `json:"copyright"`
CopyrightLink string `json:"copyrightlink"`
Quiz string `json:"quiz"`
StartDate string `json:"startdate"`
FullStartDate string `json:"fullstartdate"`
HSH string `json:"hsh"`
Variants []ImageVariantResp `json:"variants"`
}
// GetToday 获取今日图片 // GetToday 获取今日图片
// @Summary 获取今日图片 // @Summary 获取今日图片
// @Description 根据参数返回今日必应图片流或重定向 // @Description 根据参数返回今日必应图片流或重定向
@@ -39,7 +59,7 @@ func GetToday(c *gin.Context) {
// @Description 获取今日必应图片的标题、版权等元数据 // @Description 获取今日必应图片的标题、版权等元数据
// @Tags image // @Tags image
// @Produce json // @Produce json
// @Success 200 {object} map[string]interface{} // @Success 200 {object} ImageMetaResp
// @Router /image/today/meta [get] // @Router /image/today/meta [get]
func GetTodayMeta(c *gin.Context) { func GetTodayMeta(c *gin.Context) {
img, err := image.GetTodayImage() img, err := image.GetTodayImage()
@@ -73,7 +93,7 @@ func GetRandom(c *gin.Context) {
// @Description 随机获取一张已抓取图片的元数据 // @Description 随机获取一张已抓取图片的元数据
// @Tags image // @Tags image
// @Produce json // @Produce json
// @Success 200 {object} map[string]interface{} // @Success 200 {object} ImageMetaResp
// @Router /image/random/meta [get] // @Router /image/random/meta [get]
func GetRandomMeta(c *gin.Context) { func GetRandomMeta(c *gin.Context) {
img, err := image.GetRandomImage() img, err := image.GetRandomImage()
@@ -110,7 +130,7 @@ func GetByDate(c *gin.Context) {
// @Tags image // @Tags image
// @Param date path string true "日期 (yyyy-mm-dd)" // @Param date path string true "日期 (yyyy-mm-dd)"
// @Produce json // @Produce json
// @Success 200 {object} map[string]interface{} // @Success 200 {object} ImageMetaResp
// @Router /image/date/{date}/meta [get] // @Router /image/date/{date}/meta [get]
func GetByDateMeta(c *gin.Context) { func GetByDateMeta(c *gin.Context) {
date := c.Param("date") date := c.Param("date")
@@ -128,7 +148,7 @@ func GetByDateMeta(c *gin.Context) {
// @Tags image // @Tags image
// @Param limit query int false "限制数量" default(30) // @Param limit query int false "限制数量" default(30)
// @Produce json // @Produce json
// @Success 200 {array} map[string]interface{} // @Success 200 {array} ImageMetaResp
// @Router /images [get] // @Router /images [get]
func ListImages(c *gin.Context) { func ListImages(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "30") limitStr := c.DefaultQuery("limit", "30")
@@ -232,10 +252,14 @@ func formatMeta(img *model.Image) gin.H {
} }
return gin.H{ return gin.H{
"date": img.Date, "date": img.Date,
"title": img.Title, "title": img.Title,
"copyright": img.Copyright, "copyright": img.Copyright,
"quiz": img.Quiz, "copyrightlink": img.CopyrightLink,
"variants": variants, "quiz": img.Quiz,
"startdate": img.StartDate,
"fullstartdate": img.FullStartDate,
"hsh": img.HSH,
"variants": variants,
} }
} }

View File

@@ -7,16 +7,20 @@ import (
) )
type Image struct { type Image struct {
ID uint `gorm:"primaryKey" json:"id"` ID uint `gorm:"primaryKey" json:"id"`
Date string `gorm:"uniqueIndex;type:varchar(10)" json:"date"` // YYYY-MM-DD Date string `gorm:"uniqueIndex;type:varchar(10)" json:"date"` // YYYY-MM-DD
Title string `json:"title"` Title string `json:"title"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
URLBase string `json:"urlbase"` CopyrightLink string `json:"copyrightlink"`
Quiz string `json:"quiz"` URLBase string `json:"urlbase"`
CreatedAt time.Time `json:"created_at"` Quiz string `json:"quiz"`
UpdatedAt time.Time `json:"updated_at"` StartDate string `json:"startdate"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` FullStartDate string `json:"fullstartdate"`
Variants []ImageVariant `gorm:"foreignKey:ImageID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"variants"` HSH string `json:"hsh"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Variants []ImageVariant `gorm:"foreignKey:ImageID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"variants"`
} }
type ImageVariant struct { type ImageVariant struct {

View File

@@ -38,6 +38,7 @@ type BingImage struct {
CopyrightLink string `json:"copyrightlink"` CopyrightLink string `json:"copyrightlink"`
Title string `json:"title"` Title string `json:"title"`
Quiz string `json:"quiz"` Quiz string `json:"quiz"`
HSH string `json:"hsh"`
} }
type Fetcher struct { type Fetcher struct {
@@ -111,11 +112,15 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error {
// 创建 DB 记录 // 创建 DB 记录
dbImg := model.Image{ dbImg := model.Image{
Date: dateStr, Date: dateStr,
Title: bingImg.Title, Title: bingImg.Title,
Copyright: bingImg.Copyright, Copyright: bingImg.Copyright,
URLBase: bingImg.URLBase, CopyrightLink: bingImg.CopyrightLink,
Quiz: bingImg.Quiz, URLBase: bingImg.URLBase,
Quiz: bingImg.Quiz,
StartDate: bingImg.Startdate,
FullStartDate: bingImg.Fullstartdate,
HSH: bingImg.HSH,
} }
if err := repo.DB.Clauses(clause.OnConflict{ if err := repo.DB.Clauses(clause.OnConflict{

View File

@@ -455,8 +455,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -512,8 +511,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -569,8 +567,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -601,8 +598,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "$ref": "#/definitions/handlers.ImageMetaResp"
"additionalProperties": true
} }
} }
} }
@@ -879,6 +875,61 @@
} }
} }
}, },
"handlers.ImageMetaResp": {
"type": "object",
"properties": {
"copyright": {
"type": "string"
},
"copyrightlink": {
"type": "string"
},
"date": {
"type": "string"
},
"fullstartdate": {
"type": "string"
},
"hsh": {
"type": "string"
},
"quiz": {
"type": "string"
},
"startdate": {
"type": "string"
},
"title": {
"type": "string"
},
"variants": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ImageVariantResp"
}
}
}
},
"handlers.ImageVariantResp": {
"type": "object",
"properties": {
"format": {
"type": "string"
},
"size": {
"type": "integer"
},
"storage_key": {
"type": "string"
},
"url": {
"type": "string"
},
"variant": {
"type": "string"
}
}
},
"handlers.LoginRequest": { "handlers.LoginRequest": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -149,6 +149,11 @@ export interface ImageMeta {
date?: string date?: string
title?: string title?: string
copyright?: string copyright?: string
copyrightlink?: string // 图片的详细版权链接(指向 Bing 搜索页面)
quiz?: string // 旧字段,保留向后兼容
startdate?: string // 图片的发布开始日期格式YYYYMMDD
fullstartdate?: string // 图片的完整发布时间格式YYYYMMDDHHMM
hsh?: string // 图片的唯一哈希值
url?: string url?: string
variant?: string variant?: string
format?: string format?: string

View File

@@ -294,7 +294,7 @@
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10"> <div class="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
<p class="text-white/70 mb-4">获取图片的元数据信息(标题、版权、日期等),只返回 JSON 数据不返回图片</p> <p class="text-white/70 mb-4">获取图片的元数据信息(标题、版权、日期等),只返回 JSON 数据不返回图片</p>
<div class="space-y-3"> <div class="space-y-3 mb-6">
<div class="flex items-center gap-3 text-sm"> <div class="flex items-center gap-3 text-sm">
<code class="text-blue-400 font-mono">/image/today/meta</code> <code class="text-blue-400 font-mono">/image/today/meta</code>
<span class="text-white/50">-</span> <span class="text-white/50">-</span>
@@ -316,6 +316,45 @@
<span class="text-white/60">图片列表(支持分页)</span> <span class="text-white/60">图片列表(支持分页)</span>
</div> </div>
</div> </div>
<!-- 元数据字段说明 -->
<div class="mt-6 pt-6 border-t border-white/10">
<h4 class="text-white/80 font-semibold mb-4">响应字段说明</h4>
<div class="space-y-3 text-sm">
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">date</code>
<span class="text-white/60">图片日期格式YYYY-MM-DD</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">title</code>
<span class="text-white/60">图片标题</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">copyright</code>
<span class="text-white/60">版权信息</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">copyrightlink</code>
<span class="text-white/60">版权详情链接(指向 Bing 搜索页面)⭐ 新增</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">startdate</code>
<span class="text-white/60">发布开始日期格式YYYYMMDD⭐ 新增</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">fullstartdate</code>
<span class="text-white/60">完整发布时间格式YYYYMMDDHHMM⭐ 新增</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">hsh</code>
<span class="text-white/60">图片唯一哈希值 ⭐ 新增</span>
</div>
<div class="flex gap-4">
<code class="text-yellow-400 min-w-32">quiz</code>
<span class="text-white/60">必应 quiz 链接(已废弃,建议使用 copyrightlink</span>
</div>
</div>
</div>
</div> </div>
</section> </section>

View File

@@ -40,8 +40,8 @@
查看大图 查看大图
</button> </button>
<button <button
v-if="todayImage.quiz" v-if="todayImage.copyrightlink"
@click="openQuiz(todayImage.quiz)" @click="openCopyrightLink(todayImage.copyrightlink)"
class="px-6 py-3 bg-white/10 backdrop-blur-md text-white rounded-lg font-semibold hover:bg-white/20 transition-all border border-white/30" class="px-6 py-3 bg-white/10 backdrop-blur-md text-white rounded-lg font-semibold hover:bg-white/20 transition-all border border-white/30"
> >
了解更多 了解更多
@@ -145,7 +145,7 @@
</router-link> </router-link>
<a <a
href="https://github.com" href="https://github.com/hanxuanyu/BingPaper"
target="_blank" target="_blank"
class="text-white/60 hover:text-white transition-colors text-sm flex items-center gap-2" class="text-white/60 hover:text-white transition-colors text-sm flex items-center gap-2"
> >
@@ -200,11 +200,10 @@ const viewImage = (date: string) => {
router.push(`/image/${date}`) router.push(`/image/${date}`)
} }
// 打开必应 quiz 链接 // 打开版权详情链接
const openQuiz = (quiz: string) => { const openCopyrightLink = (link: string) => {
// 拼接完整的必应地址 // copyrightlink 是完整的 URL直接打开
const bingUrl = `https://www.bing.com${quiz}` window.open(link, '_blank')
window.open(bingUrl, '_blank')
} }
</script> </script>
@@ -213,6 +212,7 @@ const openQuiz = (quiz: string) => {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
line-clamp: 2;
overflow: hidden; overflow: hidden;
} }
</style> </style>

View File

@@ -49,10 +49,10 @@
{{ image.copyright }} {{ image.copyright }}
</p> </p>
<!-- Quiz 链接 --> <!-- 版权详情链接 -->
<a <a
v-if="image.quiz" v-if="image.copyrightlink"
:href="getBingQuizUrl(image.quiz)" :href="image.copyrightlink"
target="_blank" target="_blank"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 text-white rounded-lg text-sm font-medium transition-all group" class="inline-flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 text-white rounded-lg text-sm font-medium transition-all group"
> >
@@ -175,10 +175,7 @@ const getFullImageUrl = () => {
return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg') return bingPaperApi.getImageUrlByDate(currentDate.value, 'UHD', 'jpg')
} }
// 获取必应 quiz URL // copyrightlink 现在是完整的 URL无需额外处理
const getBingQuizUrl = (quiz: string) => {
return `https://www.bing.com${quiz}`
}
// 返回首页 // 返回首页
const goBack = () => { const goBack = () => {