Files
backend_v2/internal/service/article_service.go
toom1996 a325e438be update
2026-08-31 00:44:23 +08:00

136 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"errors"
"net/http"
"fashionapi/internal/dto"
"fashionapi/internal/model"
"fashionapi/internal/pkg/hashid"
"fashionapi/internal/repository"
)
// ArticleService 文章业务接口。
type ArticleService interface {
List(ctx context.Context, q dto.ArticleQuery) ([]dto.PublicArticle, int64, error)
Detail(ctx context.Context, id string, locale string) (*dto.PublicArticleDetail, error)
}
type articleService struct {
articles repository.ArticleRepository
}
// NewArticleService 创建文章服务。
func NewArticleService(articles repository.ArticleRepository) ArticleService {
return &articleService{articles: articles}
}
// List 查询文章列表,并按需附带每篇的前 N 张图。
func (s *articleService) List(ctx context.Context, q dto.ArticleQuery) ([]dto.PublicArticle, int64, error) {
q.Normalize()
rows, total, err := s.articles.List(ctx, q)
if err != nil {
return nil, 0, internalErr(err.Error())
}
list := make([]dto.PublicArticle, 0, len(rows))
ids := make([]uint32, 0, len(rows))
for _, r := range rows {
ids = append(ids, r.ID)
list = append(list, dto.PublicArticle{
UID: hashid.EncodeTyped(hashid.TypeRunway, r.ID),
BrandUID: hashid.Encode(r.BrandID),
Title: pickLocale(r.TitleEn, r.TitleCn, q.Locale),
Cover: r.Cover,
BrandName: pickLocale(r.BrandNameEn, r.BrandNameCn, "en"), // 品牌名强制优先英文,保持英文观感
ImageCount: r.ImageCount,
})
}
if q.Images > 0 && len(list) > 0 {
if err := s.attachImages(ctx, list, ids, q.Images); err != nil {
return nil, 0, internalErr(err.Error())
}
}
return list, total, nil
}
// attachImages 用单次 IN 查询为列表批量补齐图片,避免前端逐篇请求详情。
// ids 是与之对应的数字主键(对外已编码为 UID,此处用数字主键查库)。
func (s *articleService) attachImages(ctx context.Context, list []dto.PublicArticle, ids []uint32, limit int) error {
grouped, err := s.articles.ImagesByRunwayIDs(ctx, ids)
if err != nil {
return err
}
for i := range list {
imgs, ok := grouped[ids[i]]
if !ok {
continue
}
// 缩略图条带剔除主图(cover),避免与卡片主图重复。cover 恒等于图片列表首张。
imgs = excludeCover(imgs, list[i].Cover)
if len(imgs) > limit {
imgs = imgs[:limit]
}
list[i].Images = toPublicImages(imgs)
}
return nil
}
// excludeCover 从图片列表中去掉与主图(cover)同 URL 的元素,供列表缩略图条带使用。
// 详情接口(完整图集)不走此函数,封面作为其中一张 look 需保留。
func excludeCover(imgs []model.BrandRunwayImage, cover string) []model.BrandRunwayImage {
if cover == "" {
return imgs
}
kept := imgs[:0]
for _, im := range imgs {
if im.Image != cover {
kept = append(kept, im)
}
}
return kept
}
// Detail 查询文章详情,含完整图片集。locale 决定标题/描述/品牌名的展示语言。
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) {
return nil, ErrArticleNotFound
}
return nil, NewError(http.StatusInternalServerError, err.Error())
}
imgs, err := s.articles.ListImages(ctx, id)
if err != nil {
return nil, internalErr(err.Error())
}
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: row.Cover,
// 品牌名强制优先英文("en"),不随 locale 切换,保持英文观感。
BrandName: pickLocale(row.BrandNameEn, row.BrandNameCn, "en"),
Images: toPublicImages(imgs),
}, nil
}
// IDs 返回全部文章的编码 uid(供 SSG 枚举详情页路径使用,对外暴露的即编码串)。
// toPublicImages 把图片实体映射为对外结构(只保留 url 与名称)。
func 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{Image: im.Image, Name: im.Name})
}
return out
}