604 lines
21 KiB
Go
604 lines
21 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"time"
|
||
|
||
"fashionapi/internal/dto"
|
||
"fashionapi/internal/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ReviewRepository 审核草稿的数据访问接口。
|
||
//
|
||
// 草稿由爬虫 worker 写入(见 IngestRepository),本接口负责「读 + 晋升」:
|
||
// 列出待审草稿、查看详情、审核前微调字段、通过时晋升到正式表、驳回时标记状态。
|
||
type ReviewRepository interface {
|
||
// ListDrafts 分页列出草稿;status 为空表示全部,否则按状态过滤。
|
||
// 排序:pending 优先,其次按 id 倒序(最新在前)。
|
||
ListDrafts(ctx context.Context, status string, page, size int) ([]model.BrandRunwayDraft, int64, error)
|
||
// DraftDetail 查单条草稿(不含图片)。
|
||
DraftDetail(ctx context.Context, id uint32) (*model.BrandRunwayDraft, error)
|
||
// DraftImages 查草稿的全部图片,按排序值升序。
|
||
DraftImages(ctx context.Context, draftID uint32) ([]model.BrandRunwayDraftImage, error)
|
||
// UpdateDraft 审核前微调草稿字段(白名单,杜绝任意列注入)。
|
||
UpdateDraft(ctx context.Context, id uint32, fields map[string]any) error
|
||
// SetDraftStatus 设置草稿状态(通过/驳回)与审核人/驳回理由。
|
||
SetDraftStatus(ctx context.Context, id uint32, status, reviewer, reason string) error
|
||
// SaveRunwayFromDraft 晋升:按实体键(brand_id + season_code + collection_type)幂等 upsert 到 brand_runway 正式表并替换图片,返回正式表主键。
|
||
SaveRunwayFromDraft(ctx context.Context, draftID uint32) (uint32, error)
|
||
// 街拍草稿镜像方法(与 runway 同构,但对象为 street_snap_draft / street_snap)。
|
||
ListStreetDrafts(ctx context.Context, status string, page, size int) ([]model.StreetSnapDraft, int64, error)
|
||
StreetDraftDetail(ctx context.Context, id uint32) (*model.StreetSnapDraft, error)
|
||
StreetDraftImages(ctx context.Context, draftID uint32) ([]model.StreetSnapDraftImage, error)
|
||
// SoftDeleteRunwayDraftImage 软删单张走秀草稿图片(审核阶段剔除坏图),并同步 image_count。
|
||
SoftDeleteRunwayDraftImage(ctx context.Context, draftID, imgID uint32) error
|
||
// SoftDeleteStreetDraftImage 软删单张街拍草稿图片,并同步 image_count。
|
||
SoftDeleteStreetDraftImage(ctx context.Context, draftID, imgID uint32) error
|
||
// GetDraftImageKey 取单张草稿图片的七牛 key(删除前清理用);不存在返回空串。
|
||
GetDraftImageKey(ctx context.Context, kind string, draftID, imgID uint32) (string, error)
|
||
UpdateStreetDraft(ctx context.Context, id uint32, fields map[string]any) error
|
||
SetStreetDraftStatus(ctx context.Context, id uint32, status, reviewer, reason string) error
|
||
SaveStreetSnapFromDraft(ctx context.Context, draftID uint32) (uint32, error)
|
||
// CountDraftPending 统计某草稿表(dest 传表对应的零值指针,如 &model.BrandRunwayDraft{})中指定状态的草稿数。
|
||
// 供各 ReviewModule 复用,新增模块无需再单独写计数方法。
|
||
CountDraftPending(ctx context.Context, status string, dest any) (int64, error)
|
||
}
|
||
|
||
type reviewRepository struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewReviewRepository 创建审核仓储。
|
||
func NewReviewRepository(db *gorm.DB) ReviewRepository {
|
||
return &reviewRepository{db: db}
|
||
}
|
||
|
||
func (r *reviewRepository) ListDrafts(ctx context.Context, status string, page, size int) ([]model.BrandRunwayDraft, int64, error) {
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if size < 1 || size > 100 {
|
||
size = 20
|
||
}
|
||
q := r.db.WithContext(ctx).Model(&model.BrandRunwayDraft{}).Where("is_deleted = 0")
|
||
if status != "" {
|
||
q = q.Where("status = ?", status)
|
||
}
|
||
var total int64
|
||
if err := q.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if total == 0 {
|
||
return []model.BrandRunwayDraft{}, 0, nil
|
||
}
|
||
var rows []model.BrandRunwayDraft
|
||
if err := q.
|
||
Order("CASE status WHEN '" + model.DraftStatusPending + "' THEN 0 ELSE 1 END ASC, id DESC").
|
||
Offset((page - 1) * size).
|
||
Limit(size).
|
||
Find(&rows).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return rows, total, nil
|
||
}
|
||
|
||
func (r *reviewRepository) DraftDetail(ctx context.Context, id uint32) (*model.BrandRunwayDraft, error) {
|
||
var d model.BrandRunwayDraft
|
||
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = 0", id).First(&d).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, ErrNotFound
|
||
}
|
||
return nil, err
|
||
}
|
||
return &d, nil
|
||
}
|
||
|
||
func (r *reviewRepository) DraftImages(ctx context.Context, draftID uint32) ([]model.BrandRunwayDraftImage, error) {
|
||
var imgs []model.BrandRunwayDraftImage
|
||
err := r.db.WithContext(ctx).
|
||
Where("draft_id = ? AND is_deleted = 0", draftID).
|
||
Order("sort_order ASC, id ASC").
|
||
Find(&imgs).Error
|
||
return imgs, err
|
||
}
|
||
|
||
// draftEditable 审核前可微调的字段白名单(键即列名)。
|
||
var draftEditable = 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,
|
||
}
|
||
|
||
// UpdateDraft 只更新白名单内的字段,其余忽略,杜绝任意列注入。
|
||
func (r *reviewRepository) UpdateDraft(ctx context.Context, id uint32, fields map[string]any) error {
|
||
clean := map[string]any{}
|
||
for k, v := range fields {
|
||
if draftEditable[k] {
|
||
clean[k] = v
|
||
}
|
||
}
|
||
if len(clean) == 0 {
|
||
return nil
|
||
}
|
||
clean["updated_at"] = uint32(time.Now().Unix())
|
||
return r.db.WithContext(ctx).
|
||
Model(&model.BrandRunwayDraft{}).
|
||
Where("id = ?", id).
|
||
Updates(clean).Error
|
||
}
|
||
|
||
func (r *reviewRepository) SetDraftStatus(ctx context.Context, id uint32, status, reviewer, reason string) error {
|
||
return r.db.WithContext(ctx).
|
||
Model(&model.BrandRunwayDraft{}).
|
||
Where("id = ?", id).
|
||
Updates(map[string]any{
|
||
"status": status,
|
||
"reviewer": reviewer,
|
||
"reject_reason": reason,
|
||
"updated_at": uint32(time.Now().Unix()),
|
||
}).Error
|
||
}
|
||
|
||
// SaveRunwayFromDraft 晋升草稿到正式表:
|
||
// 1. 按实体键(brand_id + season_code + collection_type)查找已存在的正式走秀(幂等:重审同一草稿只更新,不重复建行);
|
||
// 2. 存在则更新全部可编辑字段 + 图片数;不存在则新建;
|
||
// 3. 替换图片:软删旧图(is_deleted=1)+ 按草稿图片重建;
|
||
// 4. 返回正式表主键。
|
||
//
|
||
// 在事务内完成,保证草稿与正式表一致性。
|
||
func (r *reviewRepository) SaveRunwayFromDraft(ctx context.Context, draftID uint32) (uint32, error) {
|
||
var draft model.BrandRunwayDraft
|
||
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = 0", draftID).First(&draft).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return 0, ErrNotFound
|
||
}
|
||
return 0, err
|
||
}
|
||
// 多来源聚合:当前草稿图 + 同实体已 approved 的其他草稿图(按 image URL 去重,
|
||
// 七牛 key 已是 sha1,同图 URL 必相同)。当前正被审核的草稿强制纳入,其余只取已审核来源。
|
||
curImgs, err := r.DraftImages(ctx, draftID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
sibImgs, err := r.runwayApprovedSiblingImages(ctx, draftID, draft.BrandID, draft.SeasonCode, draft.CollectionType)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
imgs := unionRunwayDraftImages(curImgs, sibImgs)
|
||
|
||
// image_count 仅计主图(is_detail=0):对外列表卡片「N 张」与详情默认画廊都只展示主图,
|
||
// 细节图作为主图的附属按需展开,不计入主图数。
|
||
mainCount := 0
|
||
for _, im := range imgs {
|
||
if im.IsDetail == 0 {
|
||
mainCount++
|
||
}
|
||
}
|
||
|
||
now := uint32(time.Now().Unix())
|
||
var runwayID uint32
|
||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
// 1) 按实体键查找既有正式走秀
|
||
var existing model.BrandRunway
|
||
if eErr := tx.Where("brand_id = ? AND season_code = ? AND collection_type = ? AND is_deleted = 0", draft.BrandID, draft.SeasonCode, draft.CollectionType).
|
||
Limit(1).Find(&existing).Error; eErr != nil {
|
||
return eErr
|
||
}
|
||
|
||
common := map[string]any{
|
||
"title_en": draft.TitleEn,
|
||
"title_cn": draft.TitleCn,
|
||
"description_en": draft.DescriptionEn,
|
||
"description_cn": draft.DescriptionCn,
|
||
"year": draft.Year,
|
||
"season": draft.Season,
|
||
"collection_type": draft.CollectionType,
|
||
"season_code": draft.SeasonCode,
|
||
"cover": draft.Cover,
|
||
"image_count": uint16(mainCount),
|
||
"updated_at": now,
|
||
}
|
||
|
||
if existing.ID != 0 {
|
||
runwayID = existing.ID
|
||
common["brand_id"] = existing.BrandID // 保留正式表的品牌归属
|
||
if uErr := tx.Model(&model.BrandRunway{}).Where("id = ?", runwayID).Updates(common).Error; uErr != nil {
|
||
return uErr
|
||
}
|
||
} else {
|
||
rw := &model.BrandRunway{
|
||
BrandID: draft.BrandID,
|
||
TitleEn: draft.TitleEn,
|
||
TitleCn: draft.TitleCn,
|
||
DescriptionEn: draft.DescriptionEn,
|
||
DescriptionCn: draft.DescriptionCn,
|
||
Year: draft.Year,
|
||
Season: draft.Season,
|
||
CollectionType: draft.CollectionType,
|
||
SeasonCode: draft.SeasonCode,
|
||
Cover: draft.Cover,
|
||
ImageCount: uint16(mainCount),
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if cErr := tx.Create(rw).Error; cErr != nil {
|
||
return cErr
|
||
}
|
||
runwayID = rw.ID
|
||
}
|
||
|
||
// 2) 替换图片:软删旧 + 重建
|
||
if dErr := tx.Model(&model.BrandRunwayImage{}).
|
||
Where("runway_id = ? AND is_deleted = 0", runwayID).
|
||
Updates(map[string]any{"is_deleted": 1, "updated_at": now}).Error; dErr != nil {
|
||
return dErr
|
||
}
|
||
if len(imgs) > 0 {
|
||
rows := make([]model.BrandRunwayImage, 0, len(imgs))
|
||
for i, im := range imgs {
|
||
rows = append(rows, model.BrandRunwayImage{
|
||
RunwayID: runwayID,
|
||
BrandID: draft.BrandID,
|
||
Image: im.Image,
|
||
Name: im.Name,
|
||
SortOrder: uint32(i + 1),
|
||
LookIndex: im.LookIndex,
|
||
IsDetail: im.IsDetail,
|
||
ContentSha1: im.ContentSha1,
|
||
Phash: im.Phash,
|
||
IsDuplicate: im.IsDuplicate,
|
||
DupOf: im.DupOf,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
})
|
||
}
|
||
if cErr := tx.Create(&rows).Error; cErr != nil {
|
||
return cErr
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return runwayID, nil
|
||
}
|
||
|
||
// CountDraftPending 统计传入表(dest 零值指针)中指定状态的草稿数;供各 ReviewModule 复用。
|
||
func (r *reviewRepository) CountDraftPending(ctx context.Context, status string, dest any) (int64, error) {
|
||
var n int64
|
||
if err := r.db.WithContext(ctx).
|
||
Model(dest).
|
||
Where("status = ? AND is_deleted = 0", status).
|
||
Count(&n).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return n, nil
|
||
}
|
||
|
||
// ---- 街拍草稿镜像实现(与 runway 同构) ----
|
||
|
||
func (r *reviewRepository) ListStreetDrafts(ctx context.Context, status string, page, size int) ([]model.StreetSnapDraft, int64, error) {
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if size < 1 || size > 100 {
|
||
size = 20
|
||
}
|
||
q := r.db.WithContext(ctx).Model(&model.StreetSnapDraft{}).Where("is_deleted = 0")
|
||
if status != "" {
|
||
q = q.Where("status = ?", status)
|
||
}
|
||
var total int64
|
||
if err := q.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if total == 0 {
|
||
return []model.StreetSnapDraft{}, 0, nil
|
||
}
|
||
var rows []model.StreetSnapDraft
|
||
if err := q.
|
||
Order("CASE status WHEN '" + model.DraftStatusPending + "' THEN 0 ELSE 1 END ASC, id DESC").
|
||
Offset((page - 1) * size).
|
||
Limit(size).
|
||
Find(&rows).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return rows, total, nil
|
||
}
|
||
|
||
func (r *reviewRepository) StreetDraftDetail(ctx context.Context, id uint32) (*model.StreetSnapDraft, error) {
|
||
var d model.StreetSnapDraft
|
||
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = 0", id).First(&d).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, ErrNotFound
|
||
}
|
||
return nil, err
|
||
}
|
||
return &d, nil
|
||
}
|
||
|
||
func (r *reviewRepository) StreetDraftImages(ctx context.Context, draftID uint32) ([]model.StreetSnapDraftImage, error) {
|
||
var imgs []model.StreetSnapDraftImage
|
||
err := r.db.WithContext(ctx).
|
||
Where("draft_id = ? AND is_deleted = 0", draftID).
|
||
Order("sort_order ASC, id ASC").
|
||
Find(&imgs).Error
|
||
return imgs, err
|
||
}
|
||
|
||
// streetDraftEditable 街拍草稿可微调字段白名单(title / year / city)。
|
||
var streetDraftEditable = map[string]bool{
|
||
"title": true,
|
||
"year": true,
|
||
"city": true,
|
||
}
|
||
|
||
func (r *reviewRepository) UpdateStreetDraft(ctx context.Context, id uint32, fields map[string]any) error {
|
||
clean := map[string]any{}
|
||
for k, v := range fields {
|
||
if streetDraftEditable[k] {
|
||
clean[k] = v
|
||
}
|
||
}
|
||
if len(clean) == 0 {
|
||
return nil
|
||
}
|
||
clean["updated_at"] = uint32(time.Now().Unix())
|
||
return r.db.WithContext(ctx).
|
||
Model(&model.StreetSnapDraft{}).
|
||
Where("id = ?", id).
|
||
Updates(clean).Error
|
||
}
|
||
|
||
func (r *reviewRepository) SetStreetDraftStatus(ctx context.Context, id uint32, status, reviewer, reason string) error {
|
||
return r.db.WithContext(ctx).
|
||
Model(&model.StreetSnapDraft{}).
|
||
Where("id = ?", id).
|
||
Updates(map[string]any{
|
||
"status": status,
|
||
"reviewer": reviewer,
|
||
"reject_reason": reason,
|
||
"updated_at": uint32(time.Now().Unix()),
|
||
}).Error
|
||
}
|
||
|
||
// SaveStreetSnapFromDraft 晋升街拍草稿到正式表 street_snap(按实体键 city+year 幂等 upsert,替换图片)。
|
||
// 逻辑与 SaveRunwayFromDraft 同构,但无 brand_id、有 city,图片明细落到 street_snap_images。
|
||
// 在事务内完成,保证草稿与正式表一致性。
|
||
func (r *reviewRepository) SaveStreetSnapFromDraft(ctx context.Context, draftID uint32) (uint32, error) {
|
||
var draft model.StreetSnapDraft
|
||
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = 0", draftID).First(&draft).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return 0, ErrNotFound
|
||
}
|
||
return 0, err
|
||
}
|
||
curImgs, err := r.StreetDraftImages(ctx, draftID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
sibImgs, err := r.streetApprovedSiblingImages(ctx, draftID, draft.City, draft.Year)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
imgs := unionStreetDraftImages(curImgs, sibImgs)
|
||
|
||
now := uint32(time.Now().Unix())
|
||
var snapID uint32
|
||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var existing model.StreetSnap
|
||
if eErr := tx.Where("city = ? AND year = ? AND is_deleted = 0", draft.City, draft.Year).
|
||
Limit(1).Find(&existing).Error; eErr != nil {
|
||
return eErr
|
||
}
|
||
|
||
common := map[string]any{
|
||
"title": draft.Title,
|
||
"year": draft.Year,
|
||
"city": draft.City,
|
||
"cover": draft.Cover,
|
||
"image_count": uint16(len(imgs)),
|
||
"updated_at": now,
|
||
}
|
||
|
||
if existing.ID != 0 {
|
||
snapID = existing.ID
|
||
if uErr := tx.Model(&model.StreetSnap{}).Where("id = ?", snapID).Updates(common).Error; uErr != nil {
|
||
return uErr
|
||
}
|
||
} else {
|
||
snap := &model.StreetSnap{
|
||
Title: draft.Title,
|
||
Year: draft.Year,
|
||
City: draft.City,
|
||
Cover: draft.Cover,
|
||
ImageCount: uint16(len(imgs)),
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if cErr := tx.Create(snap).Error; cErr != nil {
|
||
return cErr
|
||
}
|
||
snapID = snap.ID
|
||
}
|
||
|
||
// 替换图片:软删旧 + 重建
|
||
if dErr := tx.Model(&model.StreetSnapImage{}).
|
||
Where("snap_id = ? AND is_deleted = 0", snapID).
|
||
Updates(map[string]any{"is_deleted": 1, "updated_at": now}).Error; dErr != nil {
|
||
return dErr
|
||
}
|
||
if len(imgs) > 0 {
|
||
rows := make([]model.StreetSnapImage, 0, len(imgs))
|
||
for i, im := range imgs {
|
||
rows = append(rows, model.StreetSnapImage{
|
||
SnapID: snapID,
|
||
Image: im.Image,
|
||
Name: im.Name,
|
||
SortOrder: uint32(i + 1),
|
||
ContentSha1: im.ContentSha1,
|
||
Phash: im.Phash,
|
||
IsDuplicate: im.IsDuplicate,
|
||
DupOf: im.DupOf,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
})
|
||
}
|
||
if cErr := tx.Create(&rows).Error; cErr != nil {
|
||
return cErr
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return snapID, nil
|
||
}
|
||
|
||
// runwayApprovedSiblingImages 返回同实体(品牌+季节码+系列)且已 approved 的其他草稿的图片,
|
||
// 用于多来源聚合:当前正被审核的草稿强制纳入,其余仅合并已审核来源,避免未审图泄露进正式表。
|
||
func (r *reviewRepository) runwayApprovedSiblingImages(ctx context.Context, excludeDraftID, brandID uint32, seasonCode, collectionType string) ([]model.BrandRunwayDraftImage, error) {
|
||
var drafts []model.BrandRunwayDraft
|
||
if err := r.db.WithContext(ctx).
|
||
Where("brand_id = ? AND season_code = ? AND collection_type = ? AND status = ? AND is_deleted = 0 AND id <> ?", brandID, seasonCode, collectionType, model.DraftStatusApproved, excludeDraftID).
|
||
Find(&drafts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]model.BrandRunwayDraftImage, 0)
|
||
for _, d := range drafts {
|
||
imgs, err := r.DraftImages(ctx, d.ID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, imgs...)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// unionRunwayDraftImages 按 image URL 去重合并(七牛 key 已是 sha1,同图 URL 必相同)。当前草稿图优先。
|
||
func unionRunwayDraftImages(a, b []model.BrandRunwayDraftImage) []model.BrandRunwayDraftImage {
|
||
seen := make(map[string]struct{}, len(a)+len(b))
|
||
out := make([]model.BrandRunwayDraftImage, 0, len(a)+len(b))
|
||
for _, im := range a {
|
||
if _, ok := seen[im.Image]; ok {
|
||
continue
|
||
}
|
||
seen[im.Image] = struct{}{}
|
||
out = append(out, im)
|
||
}
|
||
for _, im := range b {
|
||
if _, ok := seen[im.Image]; ok {
|
||
continue
|
||
}
|
||
seen[im.Image] = struct{}{}
|
||
out = append(out, im)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// streetApprovedSiblingImages 同 city+year 已 approved 的其他街拍草稿图(多来源聚合)。
|
||
func (r *reviewRepository) streetApprovedSiblingImages(ctx context.Context, excludeDraftID uint32, city string, year uint16) ([]model.StreetSnapDraftImage, error) {
|
||
var drafts []model.StreetSnapDraft
|
||
if err := r.db.WithContext(ctx).
|
||
Where("city = ? AND year = ? AND status = ? AND is_deleted = 0 AND id <> ?", city, year, model.DraftStatusApproved, excludeDraftID).
|
||
Find(&drafts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]model.StreetSnapDraftImage, 0)
|
||
for _, d := range drafts {
|
||
imgs, err := r.StreetDraftImages(ctx, d.ID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, imgs...)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// unionStreetDraftImages 按 image URL 去重合并。当前草稿图优先。
|
||
func unionStreetDraftImages(a, b []model.StreetSnapDraftImage) []model.StreetSnapDraftImage {
|
||
seen := make(map[string]struct{}, len(a)+len(b))
|
||
out := make([]model.StreetSnapDraftImage, 0, len(a)+len(b))
|
||
for _, im := range a {
|
||
if _, ok := seen[im.Image]; ok {
|
||
continue
|
||
}
|
||
seen[im.Image] = struct{}{}
|
||
out = append(out, im)
|
||
}
|
||
for _, im := range b {
|
||
if _, ok := seen[im.Image]; ok {
|
||
continue
|
||
}
|
||
seen[im.Image] = struct{}{}
|
||
out = append(out, im)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ── 草稿图片删除 ────────────────────────────────────────────────────────────────
|
||
|
||
// SoftDeleteRunwayDraftImage 软删单张走秀草稿图片(审核阶段剔除坏图),并同步草稿 image_count。
|
||
// 不删七牛文件(与正式表 DeleteRunwayImage 行为一致:仅 DB 标记,避免误删被引用文件)。
|
||
func (r *reviewRepository) SoftDeleteRunwayDraftImage(ctx context.Context, draftID, imgID uint32) error {
|
||
if err := r.db.WithContext(ctx).
|
||
Model(&model.BrandRunwayDraftImage{}).
|
||
Where("id = ? AND draft_id = ? AND is_deleted = 0", imgID, draftID).
|
||
Updates(map[string]any{"is_deleted": 1, "updated_at": uint32(time.Now().Unix())}).Error; err != nil {
|
||
return err
|
||
}
|
||
return r.db.WithContext(ctx).
|
||
Exec("UPDATE brand_runway_draft SET image_count = GREATEST(image_count - 1, 0) WHERE id = ?", draftID).Error
|
||
}
|
||
|
||
// SoftDeleteStreetDraftImage 软删单张街拍草稿图片,并同步 image_count。
|
||
func (r *reviewRepository) SoftDeleteStreetDraftImage(ctx context.Context, draftID, imgID uint32) error {
|
||
if err := r.db.WithContext(ctx).
|
||
Model(&model.StreetSnapDraftImage{}).
|
||
Where("id = ? AND draft_id = ? AND is_deleted = 0", imgID, draftID).
|
||
Updates(map[string]any{"is_deleted": 1, "updated_at": uint32(time.Now().Unix())}).Error; err != nil {
|
||
return err
|
||
}
|
||
return r.db.WithContext(ctx).
|
||
Exec("UPDATE street_snap_draft SET image_count = GREATEST(image_count - 1, 0) WHERE id = ?", draftID).Error
|
||
}
|
||
|
||
// GetDraftImageKey 取单张草稿图片的七牛 key(删除前清理用);不存在返回空串。
|
||
// kind 决定查哪张草稿图片表(runway / street);未知 kind 返回 ErrNotFound。
|
||
func (r *reviewRepository) GetDraftImageKey(ctx context.Context, kind string, draftID, imgID uint32) (string, error) {
|
||
switch kind {
|
||
case dto.IngestKindRunway:
|
||
var im model.BrandRunwayDraftImage
|
||
err := r.db.WithContext(ctx).Where("id = ? AND draft_id = ?", imgID, draftID).First(&im).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return "", nil
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return im.Image, nil
|
||
case dto.IngestKindStreet:
|
||
var im model.StreetSnapDraftImage
|
||
err := r.db.WithContext(ctx).Where("id = ? AND draft_id = ?", imgID, draftID).First(&im).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return "", nil
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return im.Image, nil
|
||
default:
|
||
return "", ErrNotFound
|
||
}
|
||
}
|