perf(query): 优化后台手机号精确查询
This commit is contained in:
+2
-1
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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;
|
||||
@@ -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表无意图全表扫描"
|
||||
|
||||
Reference in New Issue
Block a user