update
This commit is contained in:
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"fashionapi/internal/dto"
|
||||
"fashionapi/internal/model"
|
||||
@ -23,7 +24,7 @@ type MediaCleanupEnqueuer interface {
|
||||
// ArticleService 文章业务接口。
|
||||
type ArticleService interface {
|
||||
List(ctx context.Context, q dto.ArticleQuery) ([]dto.PublicArticle, int64, error)
|
||||
Detail(ctx context.Context, id string, locale string, hd bool) (*dto.PublicArticleDetail, error)
|
||||
Detail(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error)
|
||||
// AdminList 后台管理列表:返回含季节 / 年份 / 系列等管理字段的走秀项。
|
||||
AdminList(ctx context.Context, q dto.ArticleQuery) ([]dto.AdminRunway, int64, error)
|
||||
// GetForEdit 取原始走秀记录与图片,供后台编辑表单预填。
|
||||
@ -49,9 +50,9 @@ type articleService struct {
|
||||
}
|
||||
|
||||
// NewArticleService 创建文章服务。
|
||||
// img 负责把库里存储的七牛 key 拼装为可访问 URL(按 tier 决定普通/高清);
|
||||
// media + deleter 用于删除图集时清理七牛孤儿文件(引用计数归零才真删);
|
||||
// cleaner 把「删七牛」动作异步入队(由入库 worker 处理),避免阻塞删除请求。
|
||||
// img 负责把库里存储的 S4 key 拼装为可访问 URL(统一走展示样式,VIP 与免费同质量);
|
||||
// media + deleter 用于删除图集时清理孤儿文件(引用计数归零才真删);
|
||||
// cleaner 把「删存储」动作异步入队(由入库 worker 处理),避免阻塞删除请求。
|
||||
func NewArticleService(articles repository.ArticleRepository, media repository.MediaRepository, img *imgurl.Composer, deleter storage.Deleter, cleaner MediaCleanupEnqueuer) ArticleService {
|
||||
return &articleService{articles: articles, media: media, img: img, deleter: deleter, cleaner: cleaner}
|
||||
}
|
||||
@ -73,7 +74,7 @@ func (s *articleService) List(ctx context.Context, q dto.ArticleQuery) ([]dto.Pu
|
||||
UID: hashid.EncodeTyped(hashid.TypeRunway, r.ID),
|
||||
BrandUID: hashid.Encode(r.BrandID),
|
||||
Title: pickLocale(r.TitleEn, r.TitleCn, q.Locale),
|
||||
Cover: s.img.Compose(r.Cover, false), // 列表封面统一普通图
|
||||
Cover: s.img.ComposeThumb(r.Cover), // 列表封面走缩略图样式(小尺寸省流量)
|
||||
BrandName: pickLocale(r.BrandNameEn, r.BrandNameCn, "en"), // 品牌名强制优先英文,保持英文观感
|
||||
ImageCount: r.ImageCount,
|
||||
})
|
||||
@ -88,7 +89,7 @@ func (s *articleService) List(ctx context.Context, q dto.ArticleQuery) ([]dto.Pu
|
||||
}
|
||||
|
||||
// AdminList 后台管理列表:复用同一套筛选与排序,但返回含季节 / 年份 / 系列 /
|
||||
// 来源链接的管理字段(对外 PublicArticle 刻意不暴露这些)。不附带缩略图,列表更轻。
|
||||
// 管理字段(对外 PublicArticle 刻意不暴露这些)。不附带缩略图,列表更轻。
|
||||
func (s *articleService) AdminList(ctx context.Context, q dto.ArticleQuery) ([]dto.AdminRunway, int64, error) {
|
||||
q.Normalize()
|
||||
rows, total, err := s.articles.ListAdmin(ctx, q)
|
||||
@ -107,7 +108,6 @@ func (s *articleService) AdminList(ctx context.Context, q dto.ArticleQuery) ([]d
|
||||
CollectionType: r.CollectionType,
|
||||
SeasonCode: r.SeasonCode,
|
||||
ImageCount: r.ImageCount,
|
||||
SourceURL: r.SourceURL,
|
||||
})
|
||||
}
|
||||
return list, total, nil
|
||||
@ -125,8 +125,9 @@ func (s *articleService) attachImages(ctx context.Context, list []dto.PublicArti
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// 列表缩略图统一普通图;再剔除与卡片主图(cover)同 URL 的首图,避免重复展示。
|
||||
imgs := s.toPublicImages(raw, false)
|
||||
// 列表缩略图走缩略图样式(与卡片封面同样式,故去重比较仍成立);
|
||||
// 再剔除与卡片主图(cover)同 URL 的首图,避免重复展示。
|
||||
imgs := s.toThumbImages(raw)
|
||||
imgs = excludeCover(imgs, list[i].Cover)
|
||||
if len(imgs) > limit {
|
||||
imgs = imgs[:limit]
|
||||
@ -152,8 +153,8 @@ func excludeCover(imgs []dto.PublicArticleImage, cover string) []dto.PublicArtic
|
||||
}
|
||||
|
||||
// Detail 查询文章详情,含完整图片集。locale 决定标题/描述/品牌名的展示语言;
|
||||
// hd=true 时返回高清原图(VIP),false 时所有图拼普通样式(免费/未登录)。
|
||||
func (s *articleService) Detail(ctx context.Context, id string, locale string, hd bool) (*dto.PublicArticleDetail, error) {
|
||||
// 图片统一走展示样式(VIP 与免费同质量)。
|
||||
func (s *articleService) Detail(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error) {
|
||||
row, err := s.articles.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
@ -167,16 +168,40 @@ func (s *articleService) Detail(ctx context.Context, id string, locale string, h
|
||||
return nil, internalErr(err.Error())
|
||||
}
|
||||
|
||||
// 默认仅主图(is_detail=0)进 Images;细节图按 look_index 分组进 Details,
|
||||
// 供前端「细节入口」按需展开。这样既满足「默认只展示主图」,又不丢失细节图数据。
|
||||
mains := make([]model.BrandRunwayImage, 0, len(imgs))
|
||||
byLook := make(map[uint32][]model.BrandRunwayImage)
|
||||
for _, im := range imgs {
|
||||
if im.IsDetail == 0 {
|
||||
mains = append(mains, im)
|
||||
} else {
|
||||
byLook[im.LookIndex] = append(byLook[im.LookIndex], im)
|
||||
}
|
||||
}
|
||||
details := make([]dto.RunwayLookDetails, 0, len(byLook))
|
||||
lookIdxs := make([]uint32, 0, len(byLook))
|
||||
for k := range byLook {
|
||||
lookIdxs = append(lookIdxs, k)
|
||||
}
|
||||
sort.Slice(lookIdxs, func(i, j int) bool { return lookIdxs[i] < lookIdxs[j] })
|
||||
for _, k := range lookIdxs {
|
||||
details = append(details, dto.RunwayLookDetails{
|
||||
LookIndex: k,
|
||||
Images: s.toPublicImages(byLook[k]),
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.PublicArticleDetail{
|
||||
UID: hashid.EncodeTyped(hashid.TypeRunway, row.ID),
|
||||
BrandUID: hashid.Encode(row.BrandID),
|
||||
Title: pickLocale(row.TitleEn, row.TitleCn, locale),
|
||||
Description: pickLocale(row.DescriptionEn, row.DescriptionCn, locale),
|
||||
Cover: s.img.Compose(row.Cover, hd),
|
||||
Cover: s.img.Compose(row.Cover),
|
||||
// 品牌名强制优先英文("en"),不随 locale 切换,保持英文观感。
|
||||
BrandName: pickLocale(row.BrandNameEn, row.BrandNameCn, "en"),
|
||||
SourceURL: row.SourceURL,
|
||||
Images: s.toPublicImages(imgs, hd),
|
||||
Images: s.toPublicImages(mains),
|
||||
Details: details,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -241,20 +266,43 @@ func (s *articleService) PurgeGallery(ctx context.Context, id uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDs 返回全部文章的编码 uid(供 SSG 枚举详情页路径使用,对外暴露的即编码串)。
|
||||
// toPublicImages 把图片实体映射为对外结构(url + 名称 + 图片自身的编码 id)。
|
||||
// ID 用 TypeRunwayImage 前缀编码,与文章 uid(r 前缀)区分开,供单图收藏定位。
|
||||
// hd=true 时返回高清原图(VIP),false 时拼普通样式(免费/未登录)。
|
||||
func (s *articleService) toPublicImages(imgs []model.BrandRunwayImage, hd bool) []dto.PublicArticleImage {
|
||||
// Image 走展示样式(high);额外给出 Thumb(缩略图样式),供大图灯箱左栏缩略图条与右下角
|
||||
// 细节缩略图等小尺寸场景使用——那些位置只显示几十到一百多像素,用大图纯属浪费带宽。
|
||||
// 注意:列表用的 toThumbImages 不填 Thumb(列表的 Image 本身就是缩略图)。
|
||||
func (s *articleService) toPublicImages(imgs []model.BrandRunwayImage) []dto.PublicArticleImage {
|
||||
if len(imgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]dto.PublicArticleImage, 0, len(imgs))
|
||||
for _, im := range imgs {
|
||||
out = append(out, dto.PublicArticleImage{
|
||||
ID: hashid.EncodeTyped(hashid.TypeRunwayImage, im.ID),
|
||||
Image: s.img.Compose(im.Image, hd),
|
||||
Name: im.Name,
|
||||
ID: hashid.EncodeTyped(hashid.TypeRunwayImage, im.ID),
|
||||
Image: s.img.Compose(im.Image),
|
||||
Thumb: s.img.ComposeThumb(im.Image),
|
||||
Name: im.Name,
|
||||
IsDetail: im.IsDetail,
|
||||
LookIndex: im.LookIndex,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// toThumbImages 与 toPublicImages 同构,但图片地址走「列表缩略图」样式(ComposeThumb),
|
||||
// 仅供列表卡片 / 图条等小尺寸场景使用;详情页与大图灯箱仍用 toPublicImages(展示样式)。
|
||||
func (s *articleService) toThumbImages(imgs []model.BrandRunwayImage) []dto.PublicArticleImage {
|
||||
if len(imgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]dto.PublicArticleImage, 0, len(imgs))
|
||||
for _, im := range imgs {
|
||||
out = append(out, dto.PublicArticleImage{
|
||||
ID: hashid.EncodeTyped(hashid.TypeRunwayImage, im.ID),
|
||||
Image: s.img.ComposeThumb(im.Image),
|
||||
Name: im.Name,
|
||||
IsDetail: im.IsDetail,
|
||||
LookIndex: im.LookIndex,
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user