Files
backend_v2/internal/repository/admin_repository.go
toom1996 42f6316125 update
2026-09-25 11:31:52 +08:00

173 lines
6.0 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 repository
import (
"context"
"strings"
"gorm.io/gorm"
)
// AdminRecordCard 是「文章管理」混合列表的统一卡片:跨 brand_runways 与 street_snaps
// 两张表,字段取两者并集的最小可读子集(类型由 Kind 区分)。
//
// 为什么是 UNION 而非分别查两张表再在 Go 里拼:混合列表需要按(待审优先 + id 倒序)
// 做跨表统一排序与分页,分别查两表再分页会得到错误的边界;一条 UNION 查询在数据库侧
// 完成排序与 LIMIT/OFFSET,分页语义才正确,也避免把整张表拉进内存。
type AdminRecordCard struct {
Kind string // "runway" | "street"
ID uint32 // 内部数值主键(供通过/驳回/批量接口使用)
Title string
Subtitle string // 走秀=品牌名;街拍=城市
ImageCount int
Status string // pending / published / rejected
}
// DupStat 某记录的图片去重统计(仅统计 is_deleted=0 的存活行)。
type DupStat struct {
Total int // 存活图片总数(含主图与细节图)
Dup int // is_duplicate=1 的张数
}
// AdminRepository 文章管理页所需的跨表只读查询。
type AdminRepository interface {
// ListMixed 按状态 + 类型过滤(status 为空表示全部;kind 为空表示走秀+街拍,
// "runway"/"street" 则只取对应一类),跨表统一排序后分页返回。
// 排序:pending 优先,其次 id 倒序(最新待审排最前)。
ListMixed(ctx context.Context, status, kind string, page, size int) ([]AdminRecordCard, int64, error)
// CountMixed 统计某状态 + 类型下的记录总数(用于 tab 角标)。
CountMixed(ctx context.Context, status, kind string) (int64, error)
// DupRateByRecords 批量统计若干记录的图片去重情况(仅存活行)。
// 返回 owner id → {Total 存活图片总数, Dup is_duplicate=1 张数};不存在的 id 不出现在 map 中。
DupRateByRecords(ctx context.Context, kind string, ids []uint32) (map[uint32]DupStat, error)
}
type adminRepository struct {
db *gorm.DB
}
// NewAdminRepository 创建文章管理仓储。
func NewAdminRepository(db *gorm.DB) AdminRepository {
return &adminRepository{db: db}
}
// legWhere 单条腿(带表别名)的过滤条件:is_deleted=0 是后台可见性的硬约束,
// status 过滤可选。别名必须显式指定——两表 LEFT JOIN 时 is_deleted 会歧义。
func legWhere(alias, status string) (string, []interface{}) {
where := alias + ".is_deleted = 0"
args := []interface{}{}
if status != "" {
where += " AND " + alias + ".status = ?"
args = append(args, status)
}
return where, args
}
func (r *adminRepository) ListMixed(ctx context.Context, status, kind string, page, size int) ([]AdminRecordCard, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 24
}
rw, rargs := legWhere("r", status)
sw, sargs := legWhere("s", status)
// kind 决定本次跨表 UNION 取哪几腿:""=两表都取,"street"=只取街拍,"runway"=只取走秀。
var legs []string
var args []interface{}
if kind != "street" {
legs = append(legs, `
SELECT 'runway' AS kind, r.id, COALESCE(r.title_en, '') AS title, COALESCE(b.name_en, '') AS subtitle,
CAST(r.image_count AS BIGINT) AS image_count, r.status
FROM brand_runways r
LEFT JOIN brands b ON b.id = r.brand_id
WHERE `+rw)
args = append(args, rargs...)
}
if kind != "runway" {
legs = append(legs, `
SELECT 'street' AS kind, s.id, COALESCE(s.title, '') AS title, COALESCE(s.city, '') AS subtitle,
CAST(s.image_count AS BIGINT) AS image_count, s.status
FROM street_snaps s
WHERE `+sw)
args = append(args, sargs...)
}
q := `
SELECT * FROM (` + strings.Join(legs, " UNION ALL ") + `) sub
ORDER BY (status = 'pending') DESC, id DESC
LIMIT ? OFFSET ?`
listArgs := append(append([]interface{}{}, args...), size, (page-1)*size)
var cards []AdminRecordCard
if err := r.db.WithContext(ctx).Raw(q, listArgs...).Scan(&cards).Error; err != nil {
return nil, 0, err
}
total, err := r.CountMixed(ctx, status, kind)
if err != nil {
return nil, 0, err
}
return cards, total, nil
}
// DupRateByRecords 批量统计若干记录的图片去重情况。
// kind=runway 查 brand_runway_images(外键 runway_id);kind=street 查 street_snap_images(外键 snap_id)。
// 只统计 is_deleted=0 的存活行;命中阈值为 phash.DefaultThreshold(当前 4)。
func (r *adminRepository) DupRateByRecords(ctx context.Context, kind string, ids []uint32) (map[uint32]DupStat, error) {
out := make(map[uint32]DupStat)
if len(ids) == 0 {
return out, nil
}
var table, fk string
switch kind {
case "runway":
table, fk = "brand_runway_images", "runway_id"
case "street":
table, fk = "street_snap_images", "snap_id"
default:
return out, nil
}
type agg struct {
Owner uint32 `gorm:"column:owner"`
Total int `gorm:"column:total"`
Dup int `gorm:"column:dup"`
}
var rows []agg
if err := r.db.WithContext(ctx).
Table(table).
Select(fk + " AS owner, COUNT(*) AS total, COALESCE(SUM(CASE WHEN is_duplicate = 1 THEN 1 ELSE 0 END), 0) AS dup").
Where(fk+" IN ? AND is_deleted = 0", ids).
Group(fk).
Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
out[row.Owner] = DupStat{Total: row.Total, Dup: row.Dup}
}
return out, nil
}
func (r *adminRepository) CountMixed(ctx context.Context, status, kind string) (int64, error) {
rw, rargs := legWhere("r", status)
sw, sargs := legWhere("s", status)
var legs []string
var args []interface{}
if kind != "street" {
legs = append(legs, `SELECT COUNT(*) AS c FROM brand_runways r WHERE `+rw)
args = append(args, rargs...)
}
if kind != "runway" {
legs = append(legs, `SELECT COUNT(*) AS c FROM street_snaps s WHERE `+sw)
args = append(args, sargs...)
}
q := `
SELECT COALESCE(SUM(c), 0) FROM (` + strings.Join(legs, " UNION ALL ") + `) t`
var total int64
if err := r.db.WithContext(ctx).Raw(q, args...).Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}