package dispute import ( "context" "strconv" "strings" "gorm.io/gorm" ) func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) { conditions := r.userVisibleQuery(ctx, userID) var total int64 if err := conditions.Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var rows []disputeRow err := applyUserVisibleFilter(r.baseQuery(ctx), userID). Order("d.id DESC"). Offset(offset).Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) FindForUser(ctx context.Context, userID uint64, id uint64) (*DisputeDTO, error) { var row disputeRow if err := applyUserVisibleFilter(r.baseQuery(ctx).Where("d.id = ?", id), userID). First(&row).Error; err != nil { return nil, err } dto := row.toDTO() return &dto, nil } func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*PaginatedResult, error) { page := query.Page pageSize := query.PageSize if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } offset := (page - 1) * pageSize var rows []disputeRow err := applyAdminListFilters(r.baseQuery(ctx), query). Order("d.id DESC"). Offset(offset). Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } var total int64 // 首页查询且返回条数小于 pageSize 时,总数等于当前返回条数,彻底免除第二次 COUNT(*) 扫描 if page == 1 && len(rows) < pageSize { total = int64(len(rows)) } else { countDB := applyAdminListFilters(r.adminDisputeCountQuery(ctx, query), query) if err := countDB.Count(&total).Error; err != nil { return nil, err } } return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) FindAdminByID(ctx context.Context, id uint64) (*DisputeDTO, error) { var row disputeRow if err := r.baseQuery(ctx).Where("d.id = ?", id).First(&row).Error; err != nil { return nil, err } dto := row.toDTO() return &dto, nil } func (r *Repository) userVisibleQuery(ctx context.Context, userID uint64) *gorm.DB { return applyUserVisibleFilter(r.adminFilterQuery(ctx), userID) } func applyUserVisibleFilter(db *gorm.DB, userID uint64) *gorm.DB { return db.Where( `((d.initiator_type = ? AND d.initiator_id = ?) OR d.target_user_id = ? OR (d.initiator_type = ? AND (o.owner_id = ? OR o.renter_id = ?)))`, disputeInitiatorUser, userID, userID, disputeInitiatorAdmin, userID, userID, ) } func (r *Repository) baseQuery(ctx context.Context) *gorm.DB { return r.adminFilterQuery(ctx). Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status, o.owner_id, o.renter_id, o.extra_item_original_amount_cent, owner.phone AS owner_phone, renter.phone AS renter_phone, l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m, c.consumable_amount_cent AS checkout_consumable_amount_cent, CASE WHEN c.deposit_deduct_amount_cent > 0 THEN c.deposit_deduct_amount_cent ELSE c.other_amount_cent END AS checkout_deposit_deduct_amount_cent`) } func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx).Table("disputes AS d"). Joins("JOIN rental_orders AS o ON o.id = d.order_id"). Joins("JOIN rental_listings AS l ON l.id = o.listing_id"). Joins("JOIN game_accounts AS a ON a.id = o.account_id"). Joins("JOIN users AS owner ON owner.id = o.owner_id"). Joins("JOIN users AS renter ON renter.id = o.renter_id"). Joins("LEFT JOIN order_checkouts AS c ON c.id = d.checkout_id") } func (r *Repository) adminDisputeCountQuery(ctx context.Context, query AdminListQuery) *gorm.DB { kw := strings.TrimSpace(query.Keyword) if kw == "" { return r.db.WithContext(ctx).Table("disputes AS d") } upper := strings.ToUpper(kw) if id, err := strconv.ParseUint(kw, 10, 64); err == nil && id > 0 && kw[0] != '0' && id < 100000000 { return r.db.WithContext(ctx).Table("disputes AS d"). Joins("JOIN rental_orders AS o ON o.id = d.order_id") } if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") { return r.db.WithContext(ctx).Table("disputes AS d"). Joins("JOIN rental_orders AS o ON o.id = d.order_id") } if strings.HasPrefix(upper, "L") || (len(kw) >= 12 && isAllDigits(kw)) { return r.db.WithContext(ctx).Table("disputes AS d"). Joins("JOIN rental_orders AS o ON o.id = d.order_id"). Joins("JOIN rental_listings AS l ON l.id = o.listing_id") } if len(kw) == 11 && kw[0] == '1' && isAllDigits(kw) { return r.db.WithContext(ctx).Table("disputes AS d"). Joins("JOIN rental_orders AS o ON o.id = d.order_id"). Joins("JOIN users AS owner ON owner.id = o.owner_id"). Joins("JOIN users AS renter ON renter.id = o.renter_id") } return r.adminFilterQuery(ctx) } func applyAdminListFilters(db *gorm.DB, query AdminListQuery) *gorm.DB { if query.Status != "" { db = db.Where("d.status = ?", query.Status) } if query.Type != "" { db = db.Where("d.type = ?", query.Type) } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { upper := strings.ToUpper(keyword) if len(keyword) == 11 && keyword[0] == '1' && isAllDigits(keyword) { // 11 位手机号:命中 users(phone) 唯一索引 db = db.Where("(owner.phone = ? OR renter.phone = ?)", keyword, keyword) } else if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") { // 订单号:命中 rental_orders(order_no) 唯一索引 db = db.Where("o.order_no = ?", keyword) } else if strings.HasPrefix(upper, "L") || (len(keyword) >= 12 && isAllDigits(keyword)) { // 商品编号(如 L前缀或时间计算生成的 12位纯数字如 202609040184):命中 rental_listings(listing_no) 唯一索引 db = db.Where("l.listing_no = ?", keyword) } else if id, err := strconv.ParseUint(keyword, 10, 64); err == nil && id > 0 && keyword[0] != '0' && id < 100000000 { // 短纯数字 ID(非零开头):命中纠纷 ID、订单 ID 或关联 ID db = db.Where("(d.id = ? OR d.order_id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)", id, id, id, id, id) } else { like := "%" + keyword + "%" db = db.Where( `(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ?)`, like, like, like, like, like, ) } } return db } func isAllDigits(s string) bool { if s == "" { return false } for i := 0; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return false } } return true }