perf(query): 全面治理后台跨表模糊查询慢SQL并升级SQL门禁守护

- 解决 MySQL 8.4 无法跨表 Index Merge 导致的索引失效与全表扫描问题
- 订单/提号/纠纷/用户/商品/会话列表实现智能意图路由分流直达单表唯一索引
- 剥离 COUNT(*) 阶段无关多表关联及标量子查询,实现首页条数快速短路
- check-sql-guard.sh 增加 forbid_pattern 拦截器与核心路由强校验防线
This commit is contained in:
yml2213
2026-09-05 10:37:04 +08:00
parent ba5aa48c8f
commit a9125b1939
10 changed files with 535 additions and 89 deletions
+126 -24
View File
@@ -722,11 +722,7 @@ func (r *Repository) FindByID(ctx context.Context, id uint64) (*PickupDTO, error
return &dto, nil
}
func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*PaginatedResult, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
db := r.baseQuery(ctx)
func applyAdminPickupFilters(db *gorm.DB, query AdminPickupQuery) *gorm.DB {
if status := strings.TrimSpace(query.Status); status != "" {
db = db.Where("p.status = ?", status)
}
@@ -740,16 +736,93 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa
db = db.Where("p.offline_settlement_status = ?", offlineSettlementStatus)
}
if kw := strings.TrimSpace(query.Keyword); kw != "" {
like := "%" + kw + "%"
db = db.Where(
"p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ?",
like,
like,
like,
like,
)
upper := strings.ToUpper(kw)
if len(kw) == 11 && kw[0] == '1' && isAllDigits(kw) {
// 11 位手机号:命中 users(phone) 唯一索引
db = db.Where("owner.phone = ?", kw)
} else if strings.HasPrefix(upper, "PK") || strings.HasPrefix(upper, "P") {
// 提号编号:命中 admin_pickups(pickup_no) 唯一索引
db = db.Where("p.pickup_no = ?", kw)
} else if strings.HasPrefix(upper, "L") {
// 商品编号:命中 rental_listings(listing_no) 唯一索引
db = db.Where("l.listing_no = ?", kw)
} else if id, err := strconv.ParseUint(kw, 10, 64); err == nil && id > 0 && kw[0] != '0' && id < 100000000 {
// 短纯数字 ID(非零开头):命中 admin_pickups 主键及关联 ID
db = db.Where("(p.id = ? OR p.listing_id = ? OR p.account_id = ? OR p.owner_id = ?)", id, id, id, id)
} else if len(kw) >= 12 && isAllDigits(kw) {
// 12位以上纯数字可能为商品编号或提号编号
db = db.Where("(p.pickup_no = ? OR l.listing_no = ?)", kw, kw)
} else {
like := "%" + kw + "%"
db = db.Where(
"p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ?",
like,
like,
like,
like,
)
}
}
return r.paginatePickups(db, query.Page, query.PageSize, false)
return db
}
func (r *Repository) adminPickupCountQuery(ctx context.Context, query AdminPickupQuery) *gorm.DB {
kw := strings.TrimSpace(query.Keyword)
if kw == "" {
return r.db.WithContext(ctx).Table("admin_pickups AS p")
}
upper := strings.ToUpper(kw)
if strings.HasPrefix(upper, "PK") || strings.HasPrefix(upper, "P") {
return r.db.WithContext(ctx).Table("admin_pickups AS p")
}
if id, err := strconv.ParseUint(kw, 10, 64); err == nil && id > 0 && kw[0] != '0' && id < 100000000 {
return r.db.WithContext(ctx).Table("admin_pickups AS p")
}
if len(kw) == 11 && kw[0] == '1' && isAllDigits(kw) {
return r.db.WithContext(ctx).Table("admin_pickups AS p").
Joins("JOIN users AS owner ON owner.id = p.owner_id")
}
if strings.HasPrefix(upper, "L") {
return r.db.WithContext(ctx).Table("admin_pickups AS p").
Joins("JOIN rental_listings AS l ON l.id = p.listing_id")
}
return r.db.WithContext(ctx).Table("admin_pickups AS p").
Joins("JOIN rental_listings AS l ON l.id = p.listing_id").
Joins("JOIN game_accounts AS a ON a.id = p.account_id").
Joins("JOIN users AS owner ON owner.id = p.owner_id")
}
func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*PaginatedResult, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
page, pageSize := normalizePaging(query.Page, query.PageSize)
offset := (page - 1) * pageSize
var rows []pickupRow
db := applyAdminPickupFilters(r.baseQuery(ctx), query)
if err := db.Order("p.id DESC").Offset(offset).Limit(pageSize).Scan(&rows).Error; err != nil {
return nil, err
}
var total int64
// 首页查询且返回条数小于 pageSize 时,总数等于当前返回条数,彻底免除第二次 COUNT(*) 扫描
if page == 1 && len(rows) < pageSize {
total = int64(len(rows))
} else {
countDB := applyAdminPickupFilters(r.adminPickupCountQuery(ctx, query), query)
if err := countDB.Count(&total).Error; err != nil {
return nil, err
}
}
items := make([]PickupDTO, 0, len(rows))
for _, row := range rows {
item := row.toDTO()
item.AccountSnapshot = nil
items = append(items, item)
}
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
// ListShopNames 返回历史提号中已使用的店铺名称,供后台选择和筛选。
@@ -782,14 +855,20 @@ func (r *Repository) ListForSeller(ctx context.Context, ownerID uint64, query Se
func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView bool) (*PaginatedResult, error) {
page, pageSize = normalizePaging(page, pageSize)
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, err
}
var rows []pickupRow
if err := db.Order("p.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
return nil, err
}
var total int64
if page == 1 && len(rows) < pageSize {
total = int64(len(rows))
} else {
// 使用独立干净的 Count 查询,避免触发 baseQuery 中的关联子查询
countDB := db.Session(&gorm.Session{}).Select("p.id")
if err := countDB.Count(&total).Error; err != nil {
return nil, err
}
}
items := make([]PickupDTO, 0, len(rows))
for _, row := range rows {
item := row.toDTO()
@@ -829,8 +908,13 @@ func (r *Repository) ListAvailableListings(ctx context.Context, query AvailableL
Joins("LEFT JOIN listing_uploads AS lu ON lu.id = latest_upload.upload_id").
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false)
if kw := strings.TrimSpace(query.Keyword); kw != "" {
like := "%" + kw + "%"
db = db.Where("l.listing_no LIKE ? OR a.title LIKE ?", like, like)
upper := strings.ToUpper(kw)
if strings.HasPrefix(upper, "L") || (len(kw) >= 12 && isAllDigits(kw)) {
db = db.Where("l.listing_no = ?", kw)
} else {
like := "%" + kw + "%"
db = db.Where("l.listing_no LIKE ? OR a.title LIKE ?", like, like)
}
}
switch strings.TrimSpace(query.SourceType) {
case ListingSourceExternal:
@@ -839,15 +923,20 @@ func (r *Repository) ListAvailableListings(ctx context.Context, query AvailableL
db = db.Where("lu.id IS NULL")
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, err
}
page, pageSize := normalizePaging(query.Page, query.PageSize)
var rows []availableListingRow
if err := db.Order("l.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
return nil, err
}
var total int64
if page == 1 && len(rows) < pageSize {
total = int64(len(rows))
} else {
countDB := db.Session(&gorm.Session{}).Select("l.id")
if err := countDB.Count(&total).Error; err != nil {
return nil, err
}
}
items := make([]AvailableListingDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
@@ -1249,3 +1338,16 @@ func newPickupNo() (string, error) {
randomNum := (int(buf[0])<<8 | int(buf[1])) % 1000
return "PK" + timeStr + strconv.Itoa(1000 + randomNum)[1:], nil
}
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
}
@@ -3,6 +3,7 @@ package pickup
import (
"encoding/json"
"errors"
"strings"
"testing"
"hfb_sys/backend/internal/auditlog"
@@ -702,3 +703,58 @@ func seedPickup(t *testing.T, db *gorm.DB, status string) model.AdminPickup {
}
return pickup
}
func TestApplyAdminPickupFiltersSmartRouting(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DryRun: true})
if err != nil {
t.Fatalf("打开测试数据库失败: %v", err)
}
tests := []struct {
name string
query AdminPickupQuery
wantSQLFrag string
}{
{
name: "11位手机号路由至users(phone)",
query: AdminPickupQuery{Keyword: "13800138000"},
wantSQLFrag: "owner.phone = ?",
},
{
name: "PK前缀提号编号路由至admin_pickups(pickup_no)",
query: AdminPickupQuery{Keyword: "PK202609051000"},
wantSQLFrag: "p.pickup_no = ?",
},
{
name: "L前缀商品编号路由至rental_listings(listing_no)",
query: AdminPickupQuery{Keyword: "L20260905100"},
wantSQLFrag: "l.listing_no = ?",
},
{
name: "短数字ID路由至主键及关联ID",
query: AdminPickupQuery{Keyword: "12345"},
wantSQLFrag: "p.id = ? OR p.listing_id = ? OR p.account_id = ? OR p.owner_id = ?",
},
{
name: "长纯数字路由至提号编号或商品编号",
query: AdminPickupQuery{Keyword: "2026090510001234"},
wantSQLFrag: "p.pickup_no = ? OR l.listing_no = ?",
},
{
name: "普通文字退回多列LIKE模糊匹配",
query: AdminPickupQuery{Keyword: "测试账号"},
wantSQLFrag: "p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ?",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := applyAdminPickupFilters(db.Table("admin_pickups AS p"), tt.query).Find(&[]struct{}{}).Statement
sql := stmt.SQL.String()
if !strings.Contains(sql, tt.wantSQLFrag) {
t.Fatalf("期望 SQL 包含 %q, 实际生成: %s", tt.wantSQLFrag, sql)
}
})
}
}