perf(query): 优化后台手机号精确查询

This commit is contained in:
yml2213
2026-11-02 18:28:12 +08:00
parent 9c379b7e0c
commit 599e22331d
17 changed files with 254 additions and 62 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
.snow
.vscode
.idea
.openidea/
.claude
*.log
backend/logs/
@@ -46,4 +47,4 @@ artifacts/
frontend/coverage/
.zcode/
.workbuddy/
.workbuddy/
@@ -11,7 +11,7 @@ import (
func (r *Repository) Disbursements(ctx context.Context, query DisbursementQuery) (*DisbursementListDTO, error) {
var summary DisbursementListSummaryDTO
summaryDB := r.applyDisbursementFilters(r.disbursementListBaseQuery(ctx), query)
summaryDB := r.applyDisbursementFilters(r.disbursementListBaseQuery(ctx, query.Keyword), query)
if err := summaryDB.Select(`COUNT(*) AS record_count,
COALESCE(SUM(CASE WHEN status = 'paid' THEN actual_amount_cent ELSE 0 END), 0) AS paid_amount_cent,
COALESCE(SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END), 0) AS paid_count,
@@ -30,7 +30,7 @@ func (r *Repository) Disbursements(ctx context.Context, query DisbursementQuery)
orderColumn = "paid_at"
}
offset := (query.Page - 1) * query.PageSize
rowsDB := r.applyDisbursementFilters(r.disbursementListBaseQuery(ctx), query)
rowsDB := r.applyDisbursementFilters(r.disbursementListBaseQuery(ctx, query.Keyword), query)
if err := rowsDB.Order(orderColumn + " DESC").
Order("source_id DESC").
Offset(offset).
@@ -52,7 +52,7 @@ func (r *Repository) Disbursements(ctx context.Context, query DisbursementQuery)
}, nil
}
func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
func (r *Repository) disbursementListBaseQuery(ctx context.Context, phoneKeyword string) *gorm.DB {
db := r.db.WithContext(ctx)
platformManaged := db.Table("rental_orders AS ro").
Select(`'platform_managed' AS source_type,
@@ -213,6 +213,11 @@ func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
Joins("LEFT JOIN users AS u ON u.id = wr.user_id").
Joins("LEFT JOIN admin_users AS operator ON operator.id = COALESCE(wr.paid_by, wr.reviewed_by)")
if phoneKeyword = strings.TrimSpace(phoneKeyword); isPhoneKeyword(phoneKeyword) {
withdrawal = withdrawal.Where("wr.user_id = (SELECT id FROM users WHERE phone = ?)", phoneKeyword)
return db.Table("(?) AS d", withdrawal)
}
manualOffline := db.Table("manual_disbursements AS md").
Select(`'manual_offline' AS source_type,
md.id AS source_id,
@@ -267,9 +272,15 @@ func (r *Repository) applyDisbursementFilters(db *gorm.DB, query DisbursementQue
db = db.Where("d.user_id = ?", query.UserID)
}
if query.Keyword != "" {
like := "%" + query.Keyword + "%"
db = db.Where(`(d.business_no LIKE ? OR d.payee_name LIKE ? OR d.payee_phone LIKE ?
OR d.uploader_name LIKE ? OR d.account_name LIKE ? OR d.account_no LIKE ?)`, like, like, like, like, like, like)
keyword := strings.TrimSpace(query.Keyword)
if isPhoneKeyword(keyword) {
// 完整手机号只可能命中提现收款人,先经 users(phone) 唯一索引解析用户。
db = db.Where("d.source_type = ? AND d.user_id = (SELECT id FROM users WHERE phone = ?)", "withdrawal", keyword)
} else {
like := "%" + keyword + "%"
db = db.Where(`(d.business_no LIKE ? OR d.payee_name LIKE ? OR d.payee_phone LIKE ?
OR d.uploader_name LIKE ? OR d.account_name LIKE ? OR d.account_no LIKE ?)`, like, like, like, like, like, like)
}
}
if query.Operator != "" {
like := "%" + query.Operator + "%"
@@ -288,6 +299,18 @@ func (r *Repository) applyDisbursementFilters(db *gorm.DB, query DisbursementQue
return db
}
func isPhoneKeyword(s string) bool {
if len(s) != 11 || s[0] != '1' {
return false
}
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}
type disbursementItemRow struct {
SourceType string
SourceID uint64
@@ -95,6 +95,22 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
if result.Summary.ReviewPendingAmountCent != 5000 || result.Summary.ReviewPendingCount != 1 {
t.Fatalf("待审核 = %d/%d, want 5000/1", result.Summary.ReviewPendingAmountCent, result.Summary.ReviewPendingCount)
}
phoneFiltered, err := repo.Disbursements(t.Context(), DisbursementQuery{
Keyword: "13900000000",
Page: 1,
PageSize: 20,
})
if err != nil {
t.Fatalf("按完整手机号查询出款失败: %v", err)
}
if phoneFiltered.Total != 3 {
t.Fatalf("按完整手机号查询记录数 = %d, want 3", phoneFiltered.Total)
}
for _, item := range phoneFiltered.Items {
if item.SourceType != "withdrawal" || item.PayeePhone != "13900000000" {
t.Fatalf("完整手机号筛选返回了不匹配记录: %+v", item)
}
}
var platformItem *DisbursementItemDTO
for index := range result.Items {
@@ -105,6 +105,13 @@ func TestListAdminConversationsUsesPersonalStateAndBatchData(t *testing.T) {
if counts.Ownership[adminChatFilterMine] != 1 || counts.Stages[adminChatStageAll] != 1 {
t.Fatalf("聚合统计异常: ownership=%v stages=%v", counts.Ownership, counts.Stages)
}
result, err = repo.ListConversationsWithFilter(t.Context(), Principal{Type: "admin", ID: admin.ID}, 1, 20, adminChatFilterMine, adminChatStageAll, user.Phone)
if err != nil {
t.Fatalf("按手机号查询订单群失败: %v", err)
}
if result.Total != 1 {
t.Fatalf("按手机号匹配订单群数量 = %d, want 1", result.Total)
}
if err := db.Model(&conversation).Update("type", ConversationTypeGeneralSupport).Error; err != nil {
t.Fatalf("切换咨询会话类型失败: %v", err)
}
@@ -183,6 +190,13 @@ func TestListAdminConversationsUsesListingOrderSnapshot(t *testing.T) {
if items[0].LatestOrderID == nil || *items[0].LatestOrderID != order.ID || items[0].LatestOrderStatus != "pending_handoff" {
t.Fatalf("最新订单快照未生效: %+v", items[0])
}
result, err = repo.ListConversationsWithFilter(t.Context(), Principal{Type: "admin", ID: admin.ID}, 1, 20, adminChatFilterMine, adminChatStageAll, owner.Phone)
if err != nil {
t.Fatalf("按号主手机号查询发布群失败: %v", err)
}
if result.Total != 1 {
t.Fatalf("按号主手机号匹配发布群数量 = %d, want 1", result.Total)
}
order.Status = "renting"
if err := db.Save(&order).Error; err != nil {
+20 -4
View File
@@ -307,10 +307,22 @@ func (r *Repository) adminConversationBase(ctx context.Context, principal Princi
Joins("LEFT JOIN rental_listings AS l ON l.id = COALESCE(explicit_lo.listing_id, c.listing_id)")
if keyword != "" {
upper := strings.ToUpper(keyword)
if len(keyword) == 11 && keyword[0] == '1' && isAllDigits(keyword) {
db = db.Joins("LEFT JOIN users AS renter ON renter.id = explicit_lo.renter_id").
Joins("LEFT JOIN users AS owner ON owner.id = COALESCE(explicit_lo.owner_id, l.owner_id)").
Where("(renter.phone = ? OR owner.phone = ?)", keyword, keyword)
if isPhoneKeyword(keyword) {
// 先由 users(phone) 唯一索引解析用户,再分别从订单群和发布群定位会话。
// 保持原有语义:订单群匹配订单号主/租客,发布群仅匹配商品号主。
db = db.Joins(`JOIN (
SELECT c2.id AS conversation_id
FROM chat_conversations AS c2
JOIN rental_orders AS o2 ON o2.id = c2.order_id
WHERE o2.owner_id = (SELECT id FROM users WHERE phone = ?)
OR o2.renter_id = (SELECT id FROM users WHERE phone = ?)
UNION ALL
SELECT c2.id AS conversation_id
FROM chat_conversations AS c2
JOIN rental_listings AS l2 ON l2.id = c2.listing_id
WHERE c2.order_id IS NULL
AND l2.owner_id = (SELECT id FROM users WHERE phone = ?)
) AS matched_phone ON matched_phone.conversation_id = c.id`, keyword, keyword, keyword)
} else if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
db = db.Where("(explicit_lo.order_no = ? OR c.latest_order_no = ?)", keyword, keyword)
} else if strings.HasPrefix(upper, "L") || (len(keyword) >= 12 && isAllDigits(keyword)) {
@@ -492,3 +504,7 @@ func isAllDigits(s string) bool {
}
return true
}
func isPhoneKeyword(s string) bool {
return len(s) == 11 && s[0] == '1' && isAllDigits(s)
}
+13 -7
View File
@@ -134,11 +134,9 @@ func (r *Repository) adminDisputeCountQuery(ctx context.Context, query AdminList
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) {
if isPhoneKeyword(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")
Joins("JOIN rental_orders AS o ON o.id = d.order_id")
}
return r.adminFilterQuery(ctx)
}
@@ -152,9 +150,13 @@ func applyAdminListFilters(db *gorm.DB, query AdminListQuery) *gorm.DB {
}
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)
if isPhoneKeyword(keyword) {
// 先由 users(phone) 唯一索引解析用户,再走订单号主/租客索引。
db = db.Where(
"(o.owner_id = (SELECT id FROM users WHERE phone = ?) OR o.renter_id = (SELECT id FROM users WHERE phone = ?))",
keyword,
keyword,
)
} else if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
// 订单号:命中 rental_orders(order_no) 唯一索引
db = db.Where("o.order_no = ?", keyword)
@@ -190,3 +192,7 @@ func isAllDigits(s string) bool {
}
return true
}
func isPhoneKeyword(s string) bool {
return len(s) == 11 && s[0] == '1' && isAllDigits(s)
}
@@ -770,9 +770,9 @@ func TestApplyAdminListFiltersSmartRouting(t *testing.T) {
wantSQLFrag string
}{
{
name: "11位手机号路由至users(phone)",
name: "11位手机号先解析用户再走订单角色索引",
query: AdminListQuery{Keyword: "13800138000"},
wantSQLFrag: "owner.phone = ? OR renter.phone = ?",
wantSQLFrag: "o.owner_id = (SELECT id FROM users WHERE phone = ?)",
},
{
name: "RO前缀订单号路由至rental_orders(order_no)",
@@ -1138,8 +1138,13 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
base = base.Where("f.settlement_status = ?", financialStatus)
}
if strings.TrimSpace(query.Keyword) != "" {
kw := "%" + strings.TrimSpace(query.Keyword) + "%"
base = base.Where("(ro.order_no LIKE ? OR rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ?)", kw, kw, kw, kw)
keyword := strings.TrimSpace(query.Keyword)
if isPhoneKeyword(keyword) {
base = base.Where("ro.renter_id = (SELECT id FROM users WHERE phone = ?)", keyword)
} else {
kw := "%" + keyword + "%"
base = base.Where("(ro.order_no LIKE ? OR rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ?)", kw, kw, kw, kw)
}
}
switch strings.TrimSpace(query.Kind) {
case "sold":
@@ -3034,8 +3039,13 @@ func (r *Repository) ListReceivedListings(ctx context.Context, adminID, siteID u
base = base.Where("ga.game_name = ?", q.GameName)
}
if q.Keyword != "" {
kw := "%" + strings.TrimSpace(q.Keyword) + "%"
base = base.Where("(rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ? OR u.nickname LIKE ?)", kw, kw, kw, kw)
keyword := strings.TrimSpace(q.Keyword)
if isPhoneKeyword(keyword) {
base = base.Where("rl.owner_id = (SELECT id FROM users WHERE phone = ?)", keyword)
} else {
kw := "%" + keyword + "%"
base = base.Where("(rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ? OR u.nickname LIKE ?)", kw, kw, kw, kw)
}
}
var total int64
@@ -3609,7 +3619,11 @@ func (r *Repository) ListDistributionUsers(ctx context.Context, adminID, siteID
}
if kw := strings.TrimSpace(query.Keyword); kw != "" {
baseQuery = baseQuery.Where("u.phone LIKE ? OR u.nickname LIKE ?", "%"+kw+"%", "%"+kw+"%")
if isPhoneKeyword(kw) {
baseQuery = baseQuery.Where("u.phone = ?", kw)
} else {
baseQuery = baseQuery.Where("u.phone LIKE ? OR u.nickname LIKE ?", "%"+kw+"%", "%"+kw+"%")
}
}
var totalFiltered int64
@@ -3675,3 +3689,15 @@ func (r *Repository) ListDistributionUsers(ctx context.Context, adminID, siteID
OrderUsers: orderUsersCount,
}, nil
}
func isPhoneKeyword(s string) bool {
if len(s) != 11 || s[0] != '1' {
return false
}
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}
@@ -911,6 +911,17 @@ func TestDistributionReceivedListingsAndWallet(t *testing.T) {
if recPage.Total != 1 || len(recPage.Items) != 1 {
t.Fatalf("expected 1 received listing, got %d", recPage.Total)
}
phonePage, err := repo.ListReceivedListings(t.Context(), 0, site.ID, ReceivedListingQuery{
Page: 1,
PageSize: 10,
Keyword: user.Phone,
})
if err != nil {
t.Fatalf("ListReceivedListings by phone failed: %v", err)
}
if phonePage.Total != 1 || phonePage.Items[0].ListingID != listing.ID {
t.Fatalf("expected listing %d by phone, got total=%d items=%+v", listing.ID, phonePage.Total, phonePage.Items)
}
item := recPage.Items[0]
if item.ListingNo != "RL-99001" || item.GameName != "delta_force" || item.OwnerNickname != "seller01" {
t.Fatalf("unexpected received listing: %+v", item)
@@ -1804,6 +1815,18 @@ func TestListOrdersPerformanceAndScoping(t *testing.T) {
if len(allPage.Items) != 2 {
t.Fatalf("expected 2 items, got %d", len(allPage.Items))
}
phonePage, err := repo.ListOrders(t.Context(), DistributionOrderListQuery{
SiteID: 0,
Page: 1,
Size: 10,
Keyword: u.Phone,
})
if err != nil {
t.Fatalf("ListOrders by phone failed: %v", err)
}
if phonePage.Total != 2 {
t.Fatalf("expected 2 distribution orders by renter phone, got %d", phonePage.Total)
}
// B. 分销商1查询其订单 (SiteID = site1.ID)
// 预期:只查出 distOrder1
@@ -55,9 +55,13 @@ func TestListAdminInvitationsFiltersPhoneAndInviteCode(t *testing.T) {
query AdminInvitationQuery
want int64
}{
{name: "通用完整手机号精确匹配邀请人", query: AdminInvitationQuery{Phone: "13812345678"}, want: 1},
{name: "通用完整手机号精确匹配被邀请人", query: AdminInvitationQuery{Phone: "13911112222"}, want: 1},
{name: "通用手机号匹配邀请人", query: AdminInvitationQuery{Phone: "1234"}, want: 1},
{name: "通用手机号匹配被邀请人", query: AdminInvitationQuery{Phone: "139111"}, want: 1},
{name: "邀请码模糊匹配", query: AdminInvitationQuery{InviteCode: "beta"}, want: 1},
{name: "邀请人完整手机号精确匹配", query: AdminInvitationQuery{InviterPhone: "13812345678"}, want: 1},
{name: "被邀请人完整手机号精确匹配", query: AdminInvitationQuery{InviteePhone: "13911112222"}, want: 1},
{name: "邀请人手机号模糊匹配", query: AdminInvitationQuery{InviterPhone: "137"}, want: 1},
{name: "被邀请人手机号模糊匹配", query: AdminInvitationQuery{InviteePhone: "1362"}, want: 1},
}
@@ -476,6 +480,8 @@ func TestListAdminRebateLedgersFiltersPartialPhoneAndOrderNo(t *testing.T) {
want int64
}{
{name: "订单号模糊匹配", query: AdminRebateLedgerQuery{OrderNo: "ALPHA"}, want: 1},
{name: "邀请人完整手机号精确匹配", query: AdminRebateLedgerQuery{InviterPhone: "13812345678"}, want: 1},
{name: "被邀请人完整手机号精确匹配", query: AdminRebateLedgerQuery{InviteePhone: "13911112222"}, want: 1},
{name: "邀请人手机号模糊匹配", query: AdminRebateLedgerQuery{InviterPhone: "1381"}, want: 1},
{name: "被邀请人手机号模糊匹配", query: AdminRebateLedgerQuery{InviteePhone: "1362"}, want: 1},
}
@@ -877,14 +877,30 @@ func (r *Repository) ListAdminInvitations(ctx context.Context, query AdminInvita
Joins("LEFT JOIN users invitee ON invitee.id = ui.invitee_id")
if p := strings.TrimSpace(query.Phone); p != "" {
like := "%" + p + "%"
db = db.Where("(inviter.phone LIKE ? OR invitee.phone LIKE ?)", like, like)
if isPhoneKeyword(p) {
db = db.Where(
"(ui.inviter_id = (SELECT id FROM users WHERE phone = ?) OR ui.invitee_id = (SELECT id FROM users WHERE phone = ?))",
p,
p,
)
} else {
like := "%" + p + "%"
db = db.Where("(inviter.phone LIKE ? OR invitee.phone LIKE ?)", like, like)
}
}
if p := strings.TrimSpace(query.InviterPhone); p != "" {
db = db.Where("inviter.phone LIKE ?", "%"+p+"%")
if isPhoneKeyword(p) {
db = db.Where("ui.inviter_id = (SELECT id FROM users WHERE phone = ?)", p)
} else {
db = db.Where("inviter.phone LIKE ?", "%"+p+"%")
}
}
if p := strings.TrimSpace(query.InviteePhone); p != "" {
db = db.Where("invitee.phone LIKE ?", "%"+p+"%")
if isPhoneKeyword(p) {
db = db.Where("ui.invitee_id = (SELECT id FROM users WHERE phone = ?)", p)
} else {
db = db.Where("invitee.phone LIKE ?", "%"+p+"%")
}
}
if code := strings.TrimSpace(query.InviteCode); code != "" {
db = db.Where("ui.invite_code LIKE ?", "%"+code+"%")
@@ -1008,10 +1024,18 @@ func (r *Repository) adminRebateLedgerBase(ctx context.Context, query AdminRebat
db = db.Where("irl.order_no LIKE ?", "%"+o+"%")
}
if p := strings.TrimSpace(query.InviterPhone); p != "" {
db = db.Where("inviter.phone LIKE ?", "%"+p+"%")
if isPhoneKeyword(p) {
db = db.Where("irl.inviter_id = (SELECT id FROM users WHERE phone = ?)", p)
} else {
db = db.Where("inviter.phone LIKE ?", "%"+p+"%")
}
}
if p := strings.TrimSpace(query.InviteePhone); p != "" {
db = db.Where("invitee.phone LIKE ?", "%"+p+"%")
if isPhoneKeyword(p) {
db = db.Where("irl.invitee_id = (SELECT id FROM users WHERE phone = ?)", p)
} else {
db = db.Where("invitee.phone LIKE ?", "%"+p+"%")
}
}
if t := strings.TrimSpace(query.TradeRole); t != "" {
db = db.Where("irl.trade_role = ?", t)
@@ -1050,6 +1074,18 @@ func parseAdminRebateDate(value string) (time.Time, bool) {
return time.Time{}, false
}
func isPhoneKeyword(s string) bool {
if len(s) != 11 || s[0] != '1' {
return false
}
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}
// ListAdminRebateLedgers 管理后台查询返利流水
func (r *Repository) ListAdminRebateLedgers(ctx context.Context, query AdminRebateLedgerQuery) ([]map[string]any, int64, error) {
if r == nil || r.db == nil {
+13 -4
View File
@@ -241,7 +241,7 @@ func (r *Repository) adminOrderCountQuery(ctx context.Context, query AdminOrderQ
upper := strings.ToUpper(kw)
needsJoin := false
if kw != "" {
if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
if isPhoneKeyword(kw) || strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
needsJoin = false
} else if id, err := strconv.ParseUint(kw, 10, 64); err == nil && id > 0 && kw[0] != '0' && len(kw) < 9 {
needsJoin = false
@@ -334,9 +334,14 @@ func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
}
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)
if isPhoneKeyword(keyword) {
// 先由 users(phone) 唯一索引解析用户,再让订单表 owner_id/renter_id 索引驱动。
// 不能直接写 owner.phone/renter.phone:跨表别名 OR 会让 MySQL 倒序扫描全部订单。
db = db.Where(
"(o.owner_id = (SELECT id FROM users WHERE phone = ?) OR o.renter_id = (SELECT id FROM users WHERE phone = ?))",
keyword,
keyword,
)
} else if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
// 订单号:命中 rental_orders(order_no) 唯一索引
db = db.Where("o.order_no = ?", keyword)
@@ -532,3 +537,7 @@ func isAllDigits(s string) bool {
}
return len(s) > 0
}
func isPhoneKeyword(s string) bool {
return len(s) == 11 && s[0] == '1' && isAllDigits(s)
}
@@ -511,9 +511,9 @@ func TestApplyAdminOrderFiltersSmartRouting(t *testing.T) {
wantSQLFrag string
}{
{
name: "11位手机号路由至users(phone)",
name: "11位手机号先解析用户再走订单角色索引",
query: AdminOrderQuery{Keyword: "13800138000"},
wantSQLFrag: "owner.phone = ? OR renter.phone = ?",
wantSQLFrag: "o.owner_id = (SELECT id FROM users WHERE phone = ?)",
},
{
name: "RO前缀订单号路由至rental_orders(order_no)",
+23 -20
View File
@@ -178,17 +178,17 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint
BizID: &bid,
Meta: meta,
Detail: map[string]any{
"pickup_no": pickupNo,
"listing_id": listing.ID,
"platform": pickup.Platform,
"third_party_order_no": pickup.ThirdPartyOrderNo,
"shop_name": pickup.ShopName,
"operator": pickup.Operator,
"account_source": pickup.AccountSource,
"source_channel": pickup.SourceChannel,
"order_amount_cent": pickup.OrderAmountCent,
"deposit_amount_cent": pickup.DepositAmountCent,
"profit_cent": pickup.ProfitAmountCent,
"pickup_no": pickupNo,
"listing_id": listing.ID,
"platform": pickup.Platform,
"third_party_order_no": pickup.ThirdPartyOrderNo,
"shop_name": pickup.ShopName,
"operator": pickup.Operator,
"account_source": pickup.AccountSource,
"source_channel": pickup.SourceChannel,
"order_amount_cent": pickup.OrderAmountCent,
"deposit_amount_cent": pickup.DepositAmountCent,
"profit_cent": pickup.ProfitAmountCent,
},
}); err != nil {
return err
@@ -534,9 +534,9 @@ func (r *Repository) UpdateThirdPartyOrderNo(ctx context.Context, pickupID uint6
BizID: &bid,
Meta: meta,
Detail: map[string]any{
"pickup_no": pickup.PickupNo,
"old_third_party_order_no": oldThirdPartyOrderNo,
"new_third_party_order_no": pickup.ThirdPartyOrderNo,
"pickup_no": pickup.PickupNo,
"old_third_party_order_no": oldThirdPartyOrderNo,
"new_third_party_order_no": pickup.ThirdPartyOrderNo,
},
})
})
@@ -854,9 +854,9 @@ func applyAdminPickupFilters(db *gorm.DB, query AdminPickupQuery) *gorm.DB {
}
if kw := strings.TrimSpace(query.Keyword); kw != "" {
upper := strings.ToUpper(kw)
if len(kw) == 11 && kw[0] == '1' && isAllDigits(kw) {
// 11 位手机号:命中 users(phone) 唯一索引
db = db.Where("owner.phone = ?", kw)
if isPhoneKeyword(kw) {
// 先由 users(phone) 唯一索引解析用户,再让 admin_pickups(owner_id) 索引驱动。
db = db.Where("p.owner_id = (SELECT id FROM users WHERE phone = ?)", kw)
} else if strings.HasPrefix(upper, "PK") || strings.HasPrefix(upper, "P") {
// 提号编号:命中 admin_pickups(pickup_no) 唯一索引
db = db.Where("p.pickup_no = ?", kw)
@@ -894,9 +894,8 @@ func (r *Repository) adminPickupCountQuery(ctx context.Context, query AdminPicku
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 isPhoneKeyword(kw) {
return r.db.WithContext(ctx).Table("admin_pickups AS p")
}
if strings.HasPrefix(upper, "L") || (len(kw) >= 12 && isAllDigits(kw)) {
return r.db.WithContext(ctx).Table("admin_pickups AS p").
@@ -1974,3 +1973,7 @@ func isAllDigits(s string) bool {
}
return true
}
func isPhoneKeyword(s string) bool {
return len(s) == 11 && s[0] == '1' && isAllDigits(s)
}
@@ -829,9 +829,9 @@ func TestApplyAdminPickupFiltersSmartRouting(t *testing.T) {
wantSQLFrag string
}{
{
name: "11位手机号路由至users(phone)",
name: "11位手机号先解析用户再走提号号主索引",
query: AdminPickupQuery{Keyword: "13800138000"},
wantSQLFrag: "owner.phone = ?",
wantSQLFrag: "p.owner_id = (SELECT id FROM users WHERE phone = ?)",
},
{
name: "PK前缀提号编号路由至admin_pickups(pickup_no)",
@@ -0,0 +1,9 @@
-- +goose Up
-- 后台返利流水按完整被邀请人手机号搜索时,先经 users(phone) 解析 ID,
-- 再由该索引定位流水,避免回退为用户表与流水表的跨表全扫描。
ALTER TABLE invitation_rebate_ledgers
ADD KEY idx_invitation_rebate_invitee (invitee_id, settled_at);
-- +goose Down
ALTER TABLE invitation_rebate_ledgers
DROP KEY idx_invitation_rebate_invitee;
+7 -3
View File
@@ -41,14 +41,15 @@ require_pattern "backend/internal/modules/pickup/repository.go" "if pageSize > 1
# 智能意图路由与单表唯一索引直达防线(防止多表跨表 OR/LIKE 导致 MySQL 放弃索引而退化为全表扫描)
require_pattern "backend/internal/modules/order/queries.go" "o.order_no = ?" "订单列表订单号唯一索引直达"
require_pattern "backend/internal/modules/order/queries.go" "l.listing_no = ?" "订单列表商品号唯一索引直达"
require_pattern "backend/internal/modules/order/queries.go" "owner.phone = ? OR renter.phone = ?" "订单列表手机号索引直达"
require_pattern "backend/internal/modules/order/queries.go" "o.owner_id = (SELECT id FROM users WHERE phone = ?)" "订单列表手机号解析用户索引直达"
require_pattern "backend/internal/modules/order/queries.go" "page == 1 && len(rows) < pageSize" "订单列表首页面单扫描短路"
require_pattern "backend/internal/modules/pickup/repository.go" "p.pickup_no = ?" "提号列表提号单号唯一索引直达"
require_pattern "backend/internal/modules/pickup/repository.go" "owner.phone = ?" "提号列表号主手机号索引直达"
require_pattern "backend/internal/modules/pickup/repository.go" "p.owner_id = (SELECT id FROM users WHERE phone = ?)" "提号列表号主手机号解析用户索引直达"
require_pattern "backend/internal/modules/pickup/repository.go" "page == 1 && len(rows) < pageSize" "提号列表首页面单扫描短路"
require_pattern "backend/internal/modules/dispute/query.go" "o.order_no = ?" "纠纷列表订单号唯一索引直达"
require_pattern "backend/internal/modules/dispute/query.go" "o.owner_id = (SELECT id FROM users WHERE phone = ?)" "纠纷列表手机号解析用户索引直达"
require_pattern "backend/internal/modules/dispute/query.go" "page == 1 && len(rows) < pageSize" "纠纷列表首页面单扫描短路"
require_pattern "backend/internal/modules/adminuser/repository.go" "u.phone = ?" "用户管理手机号唯一索引直达"
@@ -58,7 +59,10 @@ require_pattern "backend/internal/modules/listing/query.go" "l.listing_no = ?" "
require_pattern "backend/internal/modules/listing/query.go" "page == 1 && len(rows) < pageSize" "商品列表首页面单扫描短路"
require_pattern "backend/internal/modules/chat/support.go" "l.listing_no = ?" "客服会话商品号唯一索引直达"
require_pattern "backend/internal/modules/chat/support.go" "renter.phone = ? OR owner.phone = ?" "客服会话手机号唯一索引直达"
require_pattern "backend/internal/modules/chat/support.go" "matched_phone.conversation_id = c.id" "客服会话手机号经订单角色索引直达"
require_pattern "backend/internal/modules/adminfinance/disbursement_list.go" "d.source_type = ? AND d.user_id = (SELECT id FROM users WHERE phone = ?)" "出款列表手机号解析用户索引直达"
require_pattern "backend/internal/modules/adminfinance/disbursement_list.go" "wr.user_id = (SELECT id FROM users WHERE phone = ?)" "出款列表手机号在联合前裁剪来源"
# 禁止核心列表重新引入跨表 6~10 路全扫描反模式
forbid_pattern "backend/internal/modules/dispute/query.go" "o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ? OR d.id = ?" "纠纷列表禁止6表无意图全表扫描"