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.Encode(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 } if len(imgs) > limit { imgs = imgs[:limit] } list[i].Images = toPublicImages(imgs) } return nil } // 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.Encode(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 }