150 lines
4.9 KiB
Go
150 lines
4.9 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/http"
|
||
|
||
"fashionapi/internal/dto"
|
||
"fashionapi/internal/model"
|
||
"fashionapi/internal/pkg/hashid"
|
||
"fashionapi/internal/pkg/textutil"
|
||
"fashionapi/internal/repository"
|
||
)
|
||
|
||
// summaryLength 对外摘要的截断长度(rune 数),与原项目一致。
|
||
const summaryLength = 140
|
||
|
||
// 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)
|
||
// IDs 返回全部文章的编码 uid,供 SSG 路径枚举(getStaticPaths)。
|
||
IDs(ctx context.Context) ([]string, 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),
|
||
Summary: textutil.Summarize(pickLocale(r.DescriptionEn, r.DescriptionCn, q.Locale), summaryLength),
|
||
Cover: r.Cover,
|
||
// 品牌名强制优先英文("en"),仅在英文缺失时回落中文,避免留空;
|
||
// 与 title/description 不同,品牌名不随 locale 切换,保持英文观感。
|
||
BrandName: pickLocale(r.BrandNameEn, r.BrandNameCn, "en"),
|
||
Year: r.Year,
|
||
ImageCount: r.ImageCount,
|
||
CollectionType: r.CollectionType,
|
||
Season: r.Season,
|
||
SeasonCode: r.SeasonCode,
|
||
PublishedAt: r.CreatedAt,
|
||
})
|
||
}
|
||
|
||
if q.WithImages > 0 && len(list) > 0 {
|
||
if err := s.attachImages(ctx, list, ids, q.WithImages); 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),
|
||
Summary: textutil.Summarize(pickLocale(row.DescriptionEn, row.DescriptionCn, locale), summaryLength),
|
||
Description: pickLocale(row.DescriptionEn, row.DescriptionCn, locale),
|
||
Cover: row.Cover,
|
||
// 品牌名强制优先英文("en"),不随 locale 切换,保持英文观感。
|
||
BrandName: pickLocale(row.BrandNameEn, row.BrandNameCn, "en"),
|
||
Year: row.Year,
|
||
ImageCount: row.ImageCount,
|
||
SourceURL: row.SourceURL,
|
||
PublishedAt: row.CreatedAt,
|
||
Images: toPublicImages(imgs),
|
||
}, nil
|
||
}
|
||
|
||
// IDs 返回全部文章的编码 uid(供 SSG 枚举详情页路径使用,对外暴露的即编码串)。
|
||
func (s *articleService) IDs(ctx context.Context) ([]string, error) {
|
||
ids, err := s.articles.IDs(ctx)
|
||
if err != nil {
|
||
return nil, internalErr(err.Error())
|
||
}
|
||
out := make([]string, 0, len(ids))
|
||
for _, id := range ids {
|
||
out = append(out, hashid.Encode(id))
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// 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
|
||
}
|