This commit is contained in:
toom1996
2026-09-07 00:01:48 +08:00
parent b049104a0b
commit 74ba700598
31 changed files with 1113 additions and 606 deletions

View File

@ -3,6 +3,8 @@ package repository
import (
"context"
"errors"
"strconv"
"time"
"fashionapi/internal/dto"
"fashionapi/internal/model"
@ -14,12 +16,28 @@ import (
type ArticleRepository interface {
// List 按筛选条件分页查询文章,同时返回符合条件的总数。
List(ctx context.Context, q dto.ArticleQuery) ([]model.RunwayRow, int64, error)
// ListAdmin 后台管理列表:投影列含 season/year/collection_type/season_code/source_url 等管理字段。
ListAdmin(ctx context.Context, q dto.ArticleQuery) ([]model.RunwayRow, int64, error)
// FindByID 查询单篇文章(含 JOIN 出的品牌名)。不存在时返回 ErrNotFound。
FindByID(ctx context.Context, id string) (*model.RunwayRow, error)
// ListImages 查询某篇文章的全部图片,按排序值升序。
ListImages(ctx context.Context, runwayID string) ([]model.BrandRunwayImage, error)
// ImagesByRunwayIDs 批量查询多篇文章的图片,单次 IN 查询避免 N+1。
ImagesByRunwayIDs(ctx context.Context, ids []uint32) (map[uint32][]model.BrandRunwayImage, error)
// GetForEdit 取原始走秀记录(含全部 en/cn 字段)与图片,供后台编辑表单预填。
GetForEdit(ctx context.Context, id uint32) (*model.BrandRunway, []model.BrandRunwayImage, error)
// UpdateRunway 审核/编辑后更新字段(白名单,杜绝任意列注入)。
UpdateRunway(ctx context.Context, id uint32, fields map[string]any) error
// DeleteRunwayImage 软删单张走秀图片。
DeleteRunwayImage(ctx context.Context, imageID uint32) error
// GetRunwayImageKey 取单张走秀图片的七牛 key(删除前清理用);不存在返回空串。
GetRunwayImageKey(ctx context.Context, imageID uint32) (string, error)
// SoftDeleteRunwayImages 软删某走秀的全部图片(级联下架用),返回被删图片的 key 列表。
SoftDeleteRunwayImages(ctx context.Context, runwayID uint32) ([]string, error)
// SetRunwayCover 更新走秀封面(同时回到图片列表首张)。
SetRunwayCover(ctx context.Context, runwayID uint32, image string) error
// SetRunwayDeleted 上下架:deleted=1 下架(对外不可见),0 上架。
SetRunwayDeleted(ctx context.Context, id uint32, deleted uint8) error
}
type articleRepository struct {
@ -43,6 +61,15 @@ const articleDetailColumns = `brand_runway.id, brand_runway.title_en, brand_runw
brand_runway.description_en, brand_runway.description_cn,
brand_runway.cover, b.name_en AS brand_name_en, b.name_cn AS brand_name_cn`
// articleAdminColumns 后台管理列表投影列:在列表列基础上补回 season / year /
// collection_type / season_code / source_url 等管理字段(前端列表不展示这些)。
const articleAdminColumns = `brand_runway.id, brand_runway.brand_id,
brand_runway.title_en, brand_runway.title_cn,
brand_runway.cover, brand_runway.image_count, brand_runway.year,
brand_runway.collection_type, brand_runway.season, brand_runway.season_code,
brand_runway.source_url,
b.name_en AS brand_name_en, b.name_cn AS brand_name_cn`
// brandJoin 关联品牌表取品牌名;LEFT JOIN 保证品牌被软删时文章依然可见。
const brandJoin = "LEFT JOIN brand b ON b.id = brand_runway.brand_id AND b.is_deleted = 0"
@ -140,6 +167,36 @@ func (r *articleRepository) List(ctx context.Context, q dto.ArticleQuery) ([]mod
return rows, total, nil
}
// ListAdmin 后台管理列表:与 List 共用筛选/排序,但投影列含管理字段。
func (r *articleRepository) ListAdmin(ctx context.Context, q dto.ArticleQuery) ([]model.RunwayRow, int64, error) {
scope := filterScope(q)
var total int64
if err := r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Scopes(scope).
Count(&total).Error; err != nil {
return nil, 0, err
}
if total == 0 {
return []model.RunwayRow{}, 0, nil
}
var rows []model.RunwayRow
if err := r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Scopes(scope).
Select(articleAdminColumns).
Joins(brandJoin).
Order(orderBy(q.Sort)).
Offset(q.Offset()).
Limit(q.Size).
Scan(&rows).Error; err != nil {
return nil, 0, err
}
return rows, total, nil
}
func (r *articleRepository) FindByID(ctx context.Context, id string) (*model.RunwayRow, error) {
var row model.RunwayRow
err := r.db.WithContext(ctx).
@ -187,4 +244,106 @@ func (r *articleRepository) ImagesByRunwayIDs(ctx context.Context, ids []uint32)
return result, nil
}
// IDs 返回全部未删除文章的 id(升序),供 SSG 构建期枚举详情页路径。
// ── 后台编辑支持 ──────────────────────────────────────────────────────────────
// runwayEditable 后台可编辑的走秀字段白名单(键即列名)。
var runwayEditable = map[string]bool{
"title_en": true,
"title_cn": true,
"description_en": true,
"description_cn": true,
"year": true,
"season": true,
"collection_type": true,
"season_code": true,
}
func (r *articleRepository) GetForEdit(ctx context.Context, id uint32) (*model.BrandRunway, []model.BrandRunwayImage, error) {
var rw model.BrandRunway
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = 0", id).First(&rw).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, ErrNotFound
}
return nil, nil, err
}
imgs, err := r.ListImages(ctx, strconv.FormatUint(uint64(id), 10))
if err != nil {
return nil, nil, err
}
return &rw, imgs, nil
}
func (r *articleRepository) UpdateRunway(ctx context.Context, id uint32, fields map[string]any) error {
clean := map[string]any{}
for k, v := range fields {
if runwayEditable[k] {
clean[k] = v
}
}
if len(clean) == 0 {
return nil
}
clean["updated_at"] = uint32(time.Now().Unix())
return r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Where("id = ?", id).
Updates(clean).Error
}
func (r *articleRepository) DeleteRunwayImage(ctx context.Context, imageID uint32) error {
return r.db.WithContext(ctx).
Model(&model.BrandRunwayImage{}).
Where("id = ? AND is_deleted = 0", imageID).
Updates(map[string]any{"is_deleted": 1, "updated_at": uint32(time.Now().Unix())}).Error
}
func (r *articleRepository) GetRunwayImageKey(ctx context.Context, imageID uint32) (string, error) {
var img model.BrandRunwayImage
err := r.db.WithContext(ctx).Where("id = ?", imageID).First(&img).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
if err != nil {
return "", err
}
return img.Image, nil
}
// SoftDeleteRunwayImages 软删某走秀的全部图片(级联下架用)。
// 先取非删除图片的 key 列表返回给调用方(用于后续七牛孤儿清理),再统一置 is_deleted=1。
func (r *articleRepository) SoftDeleteRunwayImages(ctx context.Context, runwayID uint32) ([]string, error) {
var imgs []model.BrandRunwayImage
if err := r.db.WithContext(ctx).
Select("image").
Where("runway_id = ? AND is_deleted = 0", runwayID).
Find(&imgs).Error; err != nil {
return nil, err
}
keys := make([]string, 0, len(imgs))
for _, im := range imgs {
if im.Image != "" {
keys = append(keys, im.Image)
}
}
if err := r.db.WithContext(ctx).
Model(&model.BrandRunwayImage{}).
Where("runway_id = ? AND is_deleted = 0", runwayID).
Updates(map[string]any{"is_deleted": 1, "updated_at": uint32(time.Now().Unix())}).Error; err != nil {
return nil, err
}
return keys, nil
}
func (r *articleRepository) SetRunwayCover(ctx context.Context, runwayID uint32, image string) error {
return r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Where("id = ?", runwayID).
Updates(map[string]any{"cover": image, "updated_at": uint32(time.Now().Unix())}).Error
}
func (r *articleRepository) SetRunwayDeleted(ctx context.Context, id uint32, deleted uint8) error {
return r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Where("id = ?", id).
Updates(map[string]any{"is_deleted": deleted, "updated_at": uint32(time.Now().Unix())}).Error
}