173 lines
6.0 KiB
Go
173 lines
6.0 KiB
Go
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
|
||
}
|