id搜索修改

This commit is contained in:
zhx 2026-06-29 14:26:06 +08:00
parent f8ec2118b9
commit a921aadcf8
34 changed files with 501 additions and 78 deletions

View File

@ -49,7 +49,7 @@ func (s *Service) ListBannedUsers(ctx context.Context, query banListQuery) ([]Ap
return nil, 0, fmt.Errorf("user mysql is not configured") return nil, 0, fmt.Errorf("user mysql is not configured")
} }
query = normalizeBanListQuery(query) query = normalizeBanListQuery(query)
adminIDs, err := s.lookupAdminOperatorIDs(ctx, query.OperatorKeyword) adminIDs, err := s.lookupAdminOperatorIDs(ctx, query.operatorAdminLookupKeyword())
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -119,12 +119,20 @@ func banListWhereSQL(ctx context.Context, query banListQuery, adminIDs []int64)
WHERE l.app_code = ? AND target.status IN ('banned', 'disabled')` WHERE l.app_code = ? AND target.status IN ('banned', 'disabled')`
args := []any{appCode, appCode} args := []any{appCode, appCode}
nowMs := nowMillis() nowMs := nowMillis()
if query.TargetKeyword != "" { if !query.TargetFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("target", "target.user_id", query.TargetFilter, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
} else if query.TargetKeyword != "" {
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("target", "target.user_id", query.TargetKeyword, nowMs) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("target", "target.user_id", query.TargetKeyword, nowMs)
whereSQL += " AND " + matchSQL whereSQL += " AND " + matchSQL
args = append(args, matchArgs...) args = append(args, matchArgs...)
} }
if query.OperatorKeyword != "" { if !query.OperatorFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("operator_user", "operator_user.user_id", query.OperatorFilter, nowMs)
whereSQL += " AND (l.operator_type = 'app_user' AND " + matchSQL + ")"
args = append(args, matchArgs...)
} else if query.OperatorKeyword != "" {
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("operator_user", "operator_user.user_id", query.OperatorKeyword, nowMs) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("operator_user", "operator_user.user_id", query.OperatorKeyword, nowMs)
operatorClauses := []string{"(l.operator_type = 'app_user' AND " + matchSQL + ")"} operatorClauses := []string{"(l.operator_type = 'app_user' AND " + matchSQL + ")"}
args = append(args, matchArgs...) args = append(args, matchArgs...)
@ -135,6 +143,15 @@ func banListWhereSQL(ctx context.Context, query banListQuery, adminIDs []int64)
} }
} }
whereSQL += " AND (" + strings.Join(operatorClauses, " OR ") + ")" whereSQL += " AND (" + strings.Join(operatorClauses, " OR ") + ")"
} else if query.OperatorAdminKeyword != "" {
if len(adminIDs) == 0 {
whereSQL += " AND 1 = 0"
} else {
whereSQL += " AND (l.operator_type = 'admin' AND l.operator_user_id IN (" + placeholders(len(adminIDs)) + "))"
for _, id := range adminIDs {
args = append(args, id)
}
}
} }
if query.StartMs > 0 { if query.StartMs > 0 {
whereSQL += " AND l.created_at_ms >= ?" whereSQL += " AND l.created_at_ms >= ?"
@ -276,9 +293,23 @@ func normalizeBanListQuery(query banListQuery) banListQuery {
} }
query.TargetKeyword = strings.TrimSpace(query.TargetKeyword) query.TargetKeyword = strings.TrimSpace(query.TargetKeyword)
query.OperatorKeyword = strings.TrimSpace(query.OperatorKeyword) query.OperatorKeyword = strings.TrimSpace(query.OperatorKeyword)
query.OperatorAdminKeyword = strings.TrimSpace(query.OperatorAdminKeyword)
query.TargetFilter.UserID = strings.TrimSpace(query.TargetFilter.UserID)
query.TargetFilter.DisplayUserID = strings.TrimSpace(query.TargetFilter.DisplayUserID)
query.TargetFilter.Username = strings.TrimSpace(query.TargetFilter.Username)
query.OperatorFilter.UserID = strings.TrimSpace(query.OperatorFilter.UserID)
query.OperatorFilter.DisplayUserID = strings.TrimSpace(query.OperatorFilter.DisplayUserID)
query.OperatorFilter.Username = strings.TrimSpace(query.OperatorFilter.Username)
return query return query
} }
func (query banListQuery) operatorAdminLookupKeyword() string {
if keyword := strings.TrimSpace(query.OperatorAdminKeyword); keyword != "" {
return keyword
}
return strings.TrimSpace(query.OperatorKeyword)
}
func uniquePositiveInt64s(values []int64) []int64 { func uniquePositiveInt64s(values []int64) []int64 {
seen := make(map[int64]struct{}, len(values)) seen := make(map[int64]struct{}, len(values))
out := make([]int64, 0, len(values)) out := make([]int64, 0, len(values))

View File

@ -160,6 +160,7 @@ func parseListQuery(c *gin.Context) listQuery {
PageSize: options.PageSize, PageSize: options.PageSize,
Country: firstQuery(c, "country", "country_code", "countryCode"), Country: firstQuery(c, "country", "country_code", "countryCode"),
Keyword: options.Keyword, Keyword: options.Keyword,
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
RegionID: regionID, RegionID: regionID,
RegionIDSet: regionIDSet, RegionIDSet: regionIDSet,
Status: options.Status, Status: options.Status,
@ -177,6 +178,7 @@ func parseLoginLogQuery(c *gin.Context) loginLogQuery {
Page: options.Page, Page: options.Page,
PageSize: options.PageSize, PageSize: options.PageSize,
Keyword: options.Keyword, Keyword: options.Keyword,
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
UserID: queryInt64(c, "user_id", "userId"), UserID: queryInt64(c, "user_id", "userId"),
DisplayUserID: firstQuery(c, "display_user_id", "displayUserId"), DisplayUserID: firstQuery(c, "display_user_id", "displayUserId"),
IP: firstQuery(c, "ip"), IP: firstQuery(c, "ip"),
@ -198,7 +200,10 @@ func parseBanListQuery(c *gin.Context) banListQuery {
Page: options.Page, Page: options.Page,
PageSize: options.PageSize, PageSize: options.PageSize,
TargetKeyword: firstQuery(c, "target_keyword", "targetKeyword", "keyword"), TargetKeyword: firstQuery(c, "target_keyword", "targetKeyword", "keyword"),
TargetFilter: shared.UserIdentityFilterFromQuery(c, "target"),
OperatorKeyword: firstQuery(c, "operator_keyword", "operatorKeyword"), OperatorKeyword: firstQuery(c, "operator_keyword", "operatorKeyword"),
OperatorFilter: shared.UserIdentityFilterFromQuery(c, "operator"),
OperatorAdminKeyword: firstQuery(c, "operator_admin_keyword", "operatorAdminKeyword"),
StartMs: queryInt64(c, "start_ms", "startMs"), StartMs: queryInt64(c, "start_ms", "startMs"),
EndMs: queryInt64(c, "end_ms", "endMs"), EndMs: queryInt64(c, "end_ms", "endMs"),
}) })

View File

@ -108,6 +108,11 @@ func loginLogWhereSQL(ctx context.Context, query loginLogQuery) (string, []any)
whereSQL += " AND " + matchSQL whereSQL += " AND " + matchSQL
args = append(args, matchArgs...) args = append(args, matchArgs...)
} }
if !query.UserFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "la.user_id", query.UserFilter, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
}
if query.IP != "" { if query.IP != "" {
whereSQL += " AND la.client_ip = ?" whereSQL += " AND la.client_ip = ?"
args = append(args, query.IP) args = append(args, query.IP)
@ -146,7 +151,7 @@ func loginLogWhereSQL(ctx context.Context, query loginLogQuery) (string, []any)
whereSQL += " AND la.created_at_ms <= ?" whereSQL += " AND la.created_at_ms <= ?"
args = append(args, query.EndMs) args = append(args, query.EndMs)
} }
if query.Keyword != "" { if query.Keyword != "" && query.UserFilter.IsEmpty() {
like := "%" + query.Keyword + "%" like := "%" + query.Keyword + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "la.user_id", query.Keyword, nowMs) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "la.user_id", query.Keyword, nowMs)
whereSQL += " AND (" + matchSQL + " OR la.client_ip LIKE ?)" whereSQL += " AND (" + matchSQL + " OR la.client_ip LIKE ?)"
@ -214,6 +219,9 @@ func normalizeLoginLogQuery(query loginLogQuery) loginLogQuery {
} }
query.Keyword = strings.TrimSpace(query.Keyword) query.Keyword = strings.TrimSpace(query.Keyword)
query.DisplayUserID = strings.TrimSpace(query.DisplayUserID) query.DisplayUserID = strings.TrimSpace(query.DisplayUserID)
query.UserFilter.UserID = strings.TrimSpace(query.UserFilter.UserID)
query.UserFilter.DisplayUserID = strings.TrimSpace(query.UserFilter.DisplayUserID)
query.UserFilter.Username = strings.TrimSpace(query.UserFilter.Username)
query.IP = strings.TrimSpace(query.IP) query.IP = strings.TrimSpace(query.IP)
query.IPCountryCode = normalizeCountryCode(query.IPCountryCode) query.IPCountryCode = normalizeCountryCode(query.IPCountryCode)
query.Channel = strings.ToLower(strings.TrimSpace(query.Channel)) query.Channel = strings.ToLower(strings.TrimSpace(query.Channel))

View File

@ -1,10 +1,13 @@
package appuser package appuser
import "hyapp-admin-server/internal/modules/shared"
type listQuery struct { type listQuery struct {
Page int Page int
PageSize int PageSize int
Country string Country string
Keyword string Keyword string
UserFilter shared.UserIdentityFilter
RegionID int64 RegionID int64
RegionIDSet bool RegionIDSet bool
Status string Status string
@ -18,6 +21,7 @@ type loginLogQuery struct {
Page int Page int
PageSize int PageSize int
Keyword string Keyword string
UserFilter shared.UserIdentityFilter
UserID int64 UserID int64
DisplayUserID string DisplayUserID string
IP string IP string
@ -36,7 +40,10 @@ type banListQuery struct {
Page int Page int
PageSize int PageSize int
TargetKeyword string TargetKeyword string
TargetFilter shared.UserIdentityFilter
OperatorKeyword string OperatorKeyword string
OperatorFilter shared.UserIdentityFilter
OperatorAdminKeyword string
StartMs int64 StartMs int64
EndMs int64 EndMs int64
} }

View File

@ -771,6 +771,9 @@ func normalizeListQuery(query listQuery) listQuery {
} }
query.Country = normalizeCountryCode(query.Country) query.Country = normalizeCountryCode(query.Country)
query.Keyword = strings.TrimSpace(query.Keyword) query.Keyword = strings.TrimSpace(query.Keyword)
query.UserFilter.UserID = strings.TrimSpace(query.UserFilter.UserID)
query.UserFilter.DisplayUserID = strings.TrimSpace(query.UserFilter.DisplayUserID)
query.UserFilter.Username = strings.TrimSpace(query.UserFilter.Username)
if query.RegionID < 0 { if query.RegionID < 0 {
query.RegionID = 0 query.RegionID = 0
query.RegionIDSet = false query.RegionIDSet = false
@ -798,7 +801,11 @@ func appUserListWhereSQLAt(appCode string, query listQuery, nowMs int64) (string
whereSQL += " AND u.status = ?" whereSQL += " AND u.status = ?"
args = append(args, query.Status) args = append(args, query.Status)
} }
if query.Keyword != "" { if !query.UserFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "u.user_id", query.UserFilter, nowMs)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
} else if query.Keyword != "" {
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "u.user_id", query.Keyword, nowMs) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "u.user_id", query.Keyword, nowMs)
whereSQL += " AND " + matchSQL whereSQL += " AND " + matchSQL
args = append(args, matchArgs...) args = append(args, matchArgs...)

View File

@ -149,6 +149,7 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
Page: options.Page, Page: options.Page,
PageSize: options.PageSize, PageSize: options.PageSize,
UserKeyword: strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user_id", "userId", "keyword")), UserKeyword: strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user_id", "userId", "keyword")),
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
BizType: firstQuery(c, "biz_type", "bizType"), BizType: firstQuery(c, "biz_type", "bizType"),
StartAtMS: startAtMS, StartAtMS: startAtMS,
EndAtMS: endAtMS, EndAtMS: endAtMS,
@ -186,6 +187,11 @@ func parseCoinSellerLedgerQuery(c *gin.Context) (coinSellerLedgerQuery, bool) {
PageSize: options.PageSize, PageSize: options.PageSize,
SellerUserID: sellerUserID, SellerUserID: sellerUserID,
SellerKeyword: firstQuery(c, "seller_keyword", "sellerKeyword", "keyword"), SellerKeyword: firstQuery(c, "seller_keyword", "sellerKeyword", "keyword"),
SellerFilter: shared.UserIdentityFilter{
DisplayUserID: strings.TrimSpace(firstQuery(c, "seller_display_user_id", "sellerDisplayUserId")),
UserID: strings.TrimSpace(firstQuery(c, "seller_filter_user_id", "sellerFilterUserId")),
Username: strings.TrimSpace(firstQuery(c, "seller_username", "sellerUsername")),
},
LedgerType: firstQuery(c, "ledger_type", "ledgerType"), LedgerType: firstQuery(c, "ledger_type", "ledgerType"),
StartAtMS: startAtMS, StartAtMS: startAtMS,
EndAtMS: endAtMS, EndAtMS: endAtMS,

View File

@ -1,9 +1,12 @@
package coinledger package coinledger
import "hyapp-admin-server/internal/modules/shared"
type listQuery struct { type listQuery struct {
Page int Page int
PageSize int PageSize int
UserKeyword string UserKeyword string
UserFilter shared.UserIdentityFilter
BizType string BizType string
StartAtMS int64 StartAtMS int64
EndAtMS int64 EndAtMS int64
@ -14,6 +17,7 @@ type coinSellerLedgerQuery struct {
PageSize int PageSize int
SellerUserID int64 SellerUserID int64
SellerKeyword string SellerKeyword string
SellerFilter shared.UserIdentityFilter
LedgerType string LedgerType string
StartAtMS int64 StartAtMS int64
EndAtMS int64 EndAtMS int64

View File

@ -63,7 +63,7 @@ func (s *Service) ListCoinLedger(ctx context.Context, appCode string, query list
return nil, 0, fmt.Errorf("wallet mysql is not configured") return nil, 0, fmt.Errorf("wallet mysql is not configured")
} }
userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword) userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword, query.UserFilter)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -342,7 +342,7 @@ func (s *Service) ListCoinAdjustments(ctx context.Context, appCode string, query
return nil, 0, fmt.Errorf("wallet mysql is not configured") return nil, 0, fmt.Errorf("wallet mysql is not configured")
} }
userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword) userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword, query.UserFilter)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -539,13 +539,17 @@ func (s *Service) CreateCoinAdjustment(ctx context.Context, appCode string, acto
}, nil }, nil
} }
func (s *Service) resolveUserFilter(ctx context.Context, appCode string, keyword string) ([]int64, bool, error) { func (s *Service) resolveUserFilter(ctx context.Context, appCode string, keyword string, filter shared.UserIdentityFilter) ([]int64, bool, error) {
keyword = strings.TrimSpace(keyword) keyword = strings.TrimSpace(keyword)
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return nil, false, nil return nil, false, nil
} }
directUserIDs := make([]int64, 0, 1) directUserIDs := make([]int64, 0, 1)
if numeric, err := strconv.ParseInt(keyword, 10, 64); err == nil && numeric > 0 { numericKeyword := keyword
if strings.TrimSpace(filter.UserID) != "" {
numericKeyword = filter.UserID
}
if numeric, err := strconv.ParseInt(numericKeyword, 10, 64); err == nil && numeric > 0 {
directUserIDs = append(directUserIDs, numeric) directUserIDs = append(directUserIDs, numeric)
} }
if s == nil || s.userDB == nil { if s == nil || s.userDB == nil {
@ -555,8 +559,15 @@ func (s *Service) resolveUserFilter(ctx context.Context, appCode string, keyword
return nil, true, fmt.Errorf("user mysql is not configured") return nil, true, fmt.Errorf("user mysql is not configured")
} }
// 用户列筛选同时支持长 ID、默认短号、当前短号、active 靓号和昵称;数字关键字直接加入 user_id避免用户资料缺失时查不到账务事实。 var matchSQL string
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, time.Now().UnixMilli()) var matchArgs []any
if !filter.IsEmpty() {
// 拆分字段来自表头明确输入:短 ID、长 ID 和昵称分别构造精确约束,避免数字短号被昵称或长 ID LIKE 扩大。
matchSQL, matchArgs = shared.UserIdentityFieldsSQL("u", "u.user_id", filter, time.Now().UnixMilli())
} else {
// 旧 keyword 入口保留兼容;数字关键字直接加入 user_id避免用户资料缺失时查不到账务事实。
matchSQL, matchArgs = shared.UserIdentityExactSQL("u", "u.user_id", keyword, time.Now().UnixMilli())
}
args := append([]any{appCode}, matchArgs...) args := append([]any{appCode}, matchArgs...)
rows, err := s.userDB.QueryContext(ctx, ` rows, err := s.userDB.QueryContext(ctx, `
@ -589,6 +600,13 @@ func (s *Service) resolveCoinSellerFilter(ctx context.Context, appCode string, q
// 即使用户资料暂时查不到,也允许账本事实被查出来,避免抽屉入口被资料缺失阻断。 // 即使用户资料暂时查不到,也允许账本事实被查出来,避免抽屉入口被资料缺失阻断。
return []int64{query.SellerUserID}, true, nil return []int64{query.SellerUserID}, true, nil
} }
if !query.SellerFilter.IsEmpty() {
if s == nil || s.userDB == nil {
return nil, true, fmt.Errorf("user mysql is not configured")
}
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "csp.user_id", query.SellerFilter, time.Now().UnixMilli())
return s.resolveCoinSellerIDs(ctx, appCode, matchSQL, matchArgs)
}
keyword := strings.TrimSpace(query.SellerKeyword) keyword := strings.TrimSpace(query.SellerKeyword)
if keyword == "" { if keyword == "" {
// 未传币商筛选时返回 userFiltered=false调用方会查询所有币商流水但仍被 biz_type、asset_type 和分页限制住。 // 未传币商筛选时返回 userFiltered=false调用方会查询所有币商流水但仍被 biz_type、asset_type 和分页限制住。
@ -601,8 +619,12 @@ func (s *Service) resolveCoinSellerFilter(ctx context.Context, appCode string, q
// 币商关键字只命中 coin_seller_profiles 里的币商本人LEFT JOIN users 只用于短 ID、active 靓号和昵称补充; // 币商关键字只命中 coin_seller_profiles 里的币商本人LEFT JOIN users 只用于短 ID、active 靓号和昵称补充;
// 这里故意不搜索 receiver/counterparty避免“收款用户命中”把其他币商的流水带进当前币商筛选结果。 // 这里故意不搜索 receiver/counterparty避免“收款用户命中”把其他币商的流水带进当前币商筛选结果。
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "csp.user_id", keyword, time.Now().UnixMilli()) matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "csp.user_id", keyword, time.Now().UnixMilli())
args := append([]any{appCode}, matchArgs...)
return s.resolveCoinSellerIDs(ctx, appCode, matchSQL, matchArgs)
}
func (s *Service) resolveCoinSellerIDs(ctx context.Context, appCode string, matchSQL string, matchArgs []any) ([]int64, bool, error) {
args := append([]any{appCode}, matchArgs...)
rows, err := s.userDB.QueryContext(ctx, ` rows, err := s.userDB.QueryContext(ctx, `
SELECT csp.user_id SELECT csp.user_id
FROM coin_seller_profiles csp FROM coin_seller_profiles csp
@ -765,6 +787,9 @@ func normalizeListQuery(query listQuery) listQuery {
query.PageSize = 100 query.PageSize = 100
} }
query.UserKeyword = strings.TrimSpace(query.UserKeyword) query.UserKeyword = strings.TrimSpace(query.UserKeyword)
query.UserFilter.UserID = strings.TrimSpace(query.UserFilter.UserID)
query.UserFilter.DisplayUserID = strings.TrimSpace(query.UserFilter.DisplayUserID)
query.UserFilter.Username = strings.TrimSpace(query.UserFilter.Username)
query.BizType = strings.TrimSpace(query.BizType) query.BizType = strings.TrimSpace(query.BizType)
return query return query
} }
@ -780,6 +805,9 @@ func normalizeCoinSellerLedgerQuery(query coinSellerLedgerQuery) coinSellerLedge
query.PageSize = 100 query.PageSize = 100
} }
query.SellerKeyword = strings.TrimSpace(query.SellerKeyword) query.SellerKeyword = strings.TrimSpace(query.SellerKeyword)
query.SellerFilter.UserID = strings.TrimSpace(query.SellerFilter.UserID)
query.SellerFilter.DisplayUserID = strings.TrimSpace(query.SellerFilter.DisplayUserID)
query.SellerFilter.Username = strings.TrimSpace(query.SellerFilter.Username)
query.LedgerType = strings.TrimSpace(query.LedgerType) query.LedgerType = strings.TrimSpace(query.LedgerType)
return query return query
} }

View File

@ -131,12 +131,13 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
func (h *Handler) ListGrants(c *gin.Context) { func (h *Handler) ListGrants(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
// 发放记录主表只存 user_id后台 keyword 先在 users 表解析成精确用户,再下推给 activity-service 查询。 // 发放记录主表只存 user_id后台 keyword 先在 users 表解析成精确用户,再下推给 activity-service 查询。
userID, matched, ok := h.resolveGrantUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveGrantUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok { if !ok {
response.ServerError(c, "查询用户信息失败") response.ServerError(c, "查询用户信息失败")
return return
} }
if options.Keyword != "" && !matched { if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
response.OK(c, response.Page{Items: []grantDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) response.OK(c, response.Page{Items: []grantDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return return
} }
@ -172,10 +173,14 @@ func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
} }
} }
func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string) (int64, bool, bool) { func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, false, true return 0, false, true
} }
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
// 累计充值奖励按 activity-service 的内部 user_id 过滤keyword 的长 ID、短号、靓号、昵称优先级由 shared 统一维护。 // 累计充值奖励按 activity-service 的内部 user_id 过滤keyword 的长 ID、短号、靓号、昵称优先级由 shared 统一维护。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil { if err != nil {

View File

@ -137,12 +137,13 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
func (h *Handler) ListClaims(c *gin.Context) { func (h *Handler) ListClaims(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok { if !ok {
response.ServerError(c, "查询用户信息失败") response.ServerError(c, "查询用户信息失败")
return return
} }
if options.Keyword != "" && !matched { if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return return
} }
@ -177,10 +178,14 @@ func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
} }
} }
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64, bool, bool) { func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, false, true return 0, false, true
} }
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
// 奖励领取记录由 activity-service 按内部 user_id 过滤;后台关键词先统一解析,避免各活动模块复制短号/靓号 SQL。 // 奖励领取记录由 activity-service 按内部 user_id 过滤;后台关键词先统一解析,避免各活动模块复制短号/靓号 SQL。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil { if err != nil {

View File

@ -474,6 +474,10 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
Page: options.Page, Page: options.Page,
PageSize: options.PageSize, PageSize: options.PageSize,
Keyword: options.Keyword, Keyword: options.Keyword,
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
OwnerFilter: shared.UserIdentityFilterFromQuery(c, "owner"),
ParentLeaderFilter: shared.UserIdentityFilterFromQuery(c, "parent_leader"),
ParentBDFilter: shared.UserIdentityFilterFromQuery(c, "parent_bd"),
Status: options.Status, Status: options.Status,
SortBy: firstNonBlank(c.Query("sortBy"), c.Query("sort_by")), SortBy: firstNonBlank(c.Query("sortBy"), c.Query("sort_by")),
SortDirection: firstNonBlank(c.Query("sortDirection"), c.Query("sort_direction"), c.Query("order")), SortDirection: firstNonBlank(c.Query("sortDirection"), c.Query("sort_direction"), c.Query("order")),

View File

@ -134,8 +134,16 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
if query.ParentLeaderUserID > 0 { if query.ParentLeaderUserID > 0 {
whereSQL += " AND bp.parent_leader_user_id = ?" whereSQL += " AND bp.parent_leader_user_id = ?"
args = append(args, query.ParentLeaderUserID) args = append(args, query.ParentLeaderUserID)
} else if !query.ParentLeaderFilter.IsEmpty() {
parentSQL, parentArgs := shared.UserIdentityFieldsSQL("parent_leader", "bp.parent_leader_user_id", query.ParentLeaderFilter, time.Now().UnixMilli())
whereSQL += " AND " + parentSQL
args = append(args, parentArgs...)
} }
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if !query.UserFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "bp.user_id", query.UserFilter, time.Now().UnixMilli())
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli()) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli())
whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ?)" whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ?)"
@ -249,7 +257,11 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
whereSQL += " AND user_region.region_id = ?" whereSQL += " AND user_region.region_id = ?"
args = append(args, query.RegionID) args = append(args, query.RegionID)
} }
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if !query.UserFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "bp.user_id", query.UserFilter, time.Now().UnixMilli())
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli()) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli())
whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ?)" whereSQL += " AND (" + matchSQL + " OR user_region.name LIKE ?)"
@ -704,7 +716,11 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
whereSQL += " AND manager_region.region_id = ?" whereSQL += " AND manager_region.region_id = ?"
args = append(args, query.RegionID) args = append(args, query.RegionID)
} }
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if !query.UserFilter.IsEmpty() {
managerSQL, managerArgs := shared.UserIdentityFieldsSQL("manager", "mp.user_id", query.UserFilter, time.Now().UnixMilli())
whereSQL += " AND " + managerSQL
args = append(args, managerArgs...)
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
managerSQL, managerArgs := shared.UserIdentityKeywordSQL("manager", "mp.user_id", keyword, time.Now().UnixMilli()) managerSQL, managerArgs := shared.UserIdentityKeywordSQL("manager", "mp.user_id", keyword, time.Now().UnixMilli())
whereSQL += " AND (" + managerSQL + " OR manager_region.name LIKE ?)" whereSQL += " AND (" + managerSQL + " OR manager_region.name LIKE ?)"
@ -798,8 +814,16 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
if query.ParentBDUserID > 0 { if query.ParentBDUserID > 0 {
whereSQL += " AND a.parent_bd_user_id = ?" whereSQL += " AND a.parent_bd_user_id = ?"
args = append(args, query.ParentBDUserID) args = append(args, query.ParentBDUserID)
} else if !query.ParentBDFilter.IsEmpty() {
parentSQL, parentArgs := shared.UserIdentityFieldsSQL("parent_bd", "a.parent_bd_user_id", query.ParentBDFilter, time.Now().UnixMilli())
whereSQL += " AND " + parentSQL
args = append(args, parentArgs...)
} }
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if !query.OwnerFilter.IsEmpty() {
ownerSQL, ownerArgs := shared.UserIdentityFieldsSQL("owner", "a.owner_user_id", query.OwnerFilter, time.Now().UnixMilli())
whereSQL += " AND " + ownerSQL
args = append(args, ownerArgs...)
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
nowMs := time.Now().UnixMilli() nowMs := time.Now().UnixMilli()
ownerSQL, ownerArgs := shared.UserIdentityKeywordSQL("owner", "a.owner_user_id", keyword, nowMs) ownerSQL, ownerArgs := shared.UserIdentityKeywordSQL("owner", "a.owner_user_id", keyword, nowMs)
@ -905,7 +929,11 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
whereSQL += " AND hp.current_agency_id = ?" whereSQL += " AND hp.current_agency_id = ?"
args = append(args, query.AgencyID) args = append(args, query.AgencyID)
} }
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if !query.UserFilter.IsEmpty() {
hostSQL, hostArgs := shared.UserIdentityFieldsSQL("u", "hp.user_id", query.UserFilter, time.Now().UnixMilli())
whereSQL += " AND " + hostSQL
args = append(args, hostArgs...)
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
nowMs := time.Now().UnixMilli() nowMs := time.Now().UnixMilli()
hostSQL, hostArgs := shared.UserIdentityKeywordSQL("u", "hp.user_id", keyword, nowMs) hostSQL, hostArgs := shared.UserIdentityKeywordSQL("u", "hp.user_id", keyword, nowMs)
@ -1021,7 +1049,11 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS
whereSQL += " AND u.region_id = ?" whereSQL += " AND u.region_id = ?"
args = append(args, query.RegionID) args = append(args, query.RegionID)
} }
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if !query.UserFilter.IsEmpty() {
matchSQL, matchArgs := shared.UserIdentityFieldsSQL("u", "csp.user_id", query.UserFilter, time.Now().UnixMilli())
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)
} else if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "csp.user_id", keyword, time.Now().UnixMilli()) matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "csp.user_id", keyword, time.Now().UnixMilli())
whereSQL += " AND (" + matchSQL + " OR csp.contact_info LIKE ?)" whereSQL += " AND (" + matchSQL + " OR csp.contact_info LIKE ?)"

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"hyapp-admin-server/internal/modules/shared"
) )
// flexibleInt64 只用于 admin HTTP 请求入参:前端新合约会按字符串提交用户长 ID旧页面或脚本可能仍提交 JSON number。 // flexibleInt64 只用于 admin HTTP 请求入参:前端新合约会按字符串提交用户长 ID旧页面或脚本可能仍提交 JSON number。
@ -44,6 +46,10 @@ type listQuery struct {
Page int Page int
PageSize int PageSize int
Keyword string Keyword string
UserFilter shared.UserIdentityFilter
OwnerFilter shared.UserIdentityFilter
ParentLeaderFilter shared.UserIdentityFilter
ParentBDFilter shared.UserIdentityFilter
Status string Status string
RegionID int64 RegionID int64
AgencyID int64 AgencyID int64

View File

@ -614,6 +614,18 @@ func normalizeListQuery(query listQuery) listQuery {
query.PageSize = 100 query.PageSize = 100
} }
query.Keyword = strings.TrimSpace(query.Keyword) query.Keyword = strings.TrimSpace(query.Keyword)
query.UserFilter.UserID = strings.TrimSpace(query.UserFilter.UserID)
query.UserFilter.DisplayUserID = strings.TrimSpace(query.UserFilter.DisplayUserID)
query.UserFilter.Username = strings.TrimSpace(query.UserFilter.Username)
query.OwnerFilter.UserID = strings.TrimSpace(query.OwnerFilter.UserID)
query.OwnerFilter.DisplayUserID = strings.TrimSpace(query.OwnerFilter.DisplayUserID)
query.OwnerFilter.Username = strings.TrimSpace(query.OwnerFilter.Username)
query.ParentLeaderFilter.UserID = strings.TrimSpace(query.ParentLeaderFilter.UserID)
query.ParentLeaderFilter.DisplayUserID = strings.TrimSpace(query.ParentLeaderFilter.DisplayUserID)
query.ParentLeaderFilter.Username = strings.TrimSpace(query.ParentLeaderFilter.Username)
query.ParentBDFilter.UserID = strings.TrimSpace(query.ParentBDFilter.UserID)
query.ParentBDFilter.DisplayUserID = strings.TrimSpace(query.ParentBDFilter.DisplayUserID)
query.ParentBDFilter.Username = strings.TrimSpace(query.ParentBDFilter.Username)
query.Status = strings.ToLower(strings.TrimSpace(query.Status)) query.Status = strings.ToLower(strings.TrimSpace(query.Status))
query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy)) query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy))
switch query.SortBy { switch query.SortBy {

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"hyapp-admin-server/internal/modules/shared"
) )
type settlementUserID int64 type settlementUserID int64
@ -126,7 +128,9 @@ type query struct {
SettlementType string SettlementType string
Status string Status string
UserID int64 UserID int64
UserFilter shared.UserIdentityFilter
AgencyOwnerUserID int64 AgencyOwnerUserID int64
AgencyOwnerFilter shared.UserIdentityFilter
PolicyID uint64 PolicyID uint64
StartAtMS int64 StartAtMS int64
EndAtMS int64 EndAtMS int64

View File

@ -120,7 +120,9 @@ func parseQuery(c *gin.Context) (query, bool) {
SettlementType: normalizeSettlementType(firstQuery(c, "settlement_type", "settlementType")), SettlementType: normalizeSettlementType(firstQuery(c, "settlement_type", "settlementType")),
Status: normalizeStatus(firstQuery(c, "status")), Status: normalizeStatus(firstQuery(c, "status")),
UserID: userID, UserID: userID,
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
AgencyOwnerUserID: agencyOwnerUserID, AgencyOwnerUserID: agencyOwnerUserID,
AgencyOwnerFilter: shared.UserIdentityFilterFromQuery(c, "agency_owner"),
PolicyID: policyID, PolicyID: policyID,
StartAtMS: startAtMS, StartAtMS: startAtMS,
EndAtMS: endAtMS, EndAtMS: endAtMS,

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"time" "time"
"hyapp-admin-server/internal/modules/shared"
walletv1 "hyapp.local/api/proto/wallet/v1" walletv1 "hyapp.local/api/proto/wallet/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -362,6 +363,15 @@ func (s *Service) List(ctx context.Context, appCode string, req query) ([]settle
return nil, 0, err return nil, 0, err
} }
req = normalizeQuery(req) req = normalizeQuery(req)
var matched bool
var err error
req, matched, err = s.resolveListUserFilters(ctx, appCode, req)
if err != nil {
return nil, 0, err
}
if !matched {
return []settlementDTO{}, 0, nil
}
whereSQL, args := settlementWhere(appCode, req) whereSQL, args := settlementWhere(appCode, req)
var total int64 var total int64
@ -447,6 +457,25 @@ func (s *Service) List(ctx context.Context, appCode string, req query) ([]settle
return items, total, nil return items, total, nil
} }
func (s *Service) resolveListUserFilters(ctx context.Context, appCode string, req query) (query, bool, error) {
nowMS := time.Now().UnixMilli()
if req.UserID == 0 && !req.UserFilter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, s.userDB, strings.TrimSpace(appCode), req.UserFilter, nowMS)
if err != nil || !matched {
return req, matched, err
}
req.UserID = userID
}
if req.AgencyOwnerUserID == 0 && !req.AgencyOwnerFilter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, s.userDB, strings.TrimSpace(appCode), req.AgencyOwnerFilter, nowMS)
if err != nil || !matched {
return req, matched, err
}
req.AgencyOwnerUserID = userID
}
return req, true, nil
}
func settlementWhere(appCode string, req query) (string, []any) { func settlementWhere(appCode string, req query) (string, []any) {
conditions := []string{"app_code = ?"} conditions := []string{"app_code = ?"}
args := []any{strings.TrimSpace(appCode)} args := []any{strings.TrimSpace(appCode)}

View File

@ -158,12 +158,13 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
func (h *Handler) ListClaims(c *gin.Context) { func (h *Handler) ListClaims(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
userID, matched, ok := h.resolveUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok { if !ok {
response.ServerError(c, "查询用户信息失败") response.ServerError(c, "查询用户信息失败")
return return
} }
if options.Keyword != "" && !matched { if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return return
} }
@ -193,12 +194,13 @@ func (h *Handler) ListClaims(c *gin.Context) {
func (h *Handler) ListSummaries(c *gin.Context) { func (h *Handler) ListSummaries(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
userID, matched, ok := h.resolveUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok { if !ok {
response.ServerError(c, "查询用户信息失败") response.ServerError(c, "查询用户信息失败")
return return
} }
if options.Keyword != "" && !matched { if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
response.OK(c, response.Page{Items: []summaryDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) response.OK(c, response.Page{Items: []summaryDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return return
} }
@ -233,10 +235,14 @@ func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
} }
} }
func (h *Handler) resolveUserID(ctx context.Context, keyword string) (int64, bool, bool) { func (h *Handler) resolveUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, false, true return 0, false, true
} }
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
// 邀请奖励汇总页只把内部 user_id 交给 activity-service展示 ID 的兼容规则集中在 shared 解析器里。 // 邀请奖励汇总页只把内部 user_id 交给 activity-service展示 ID 的兼容规则集中在 shared 解析器里。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil { if err != nil {

View File

@ -41,14 +41,20 @@ func New(wallet walletclient.Client, userDB *sql.DB, walletDB *sql.DB, store *re
func (h *Handler) ListRechargeBills(c *gin.Context) { func (h *Handler) ListRechargeBills(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
appCode := appctx.FromContext(c.Request.Context()) appCode := appctx.FromContext(c.Request.Context())
userID, ok := h.resolveRechargeBillUserFilter(c, appCode, "user_id 参数不正确", "user_id", "userId") userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
if !ok { if !ok {
return return
} }
sellerUserID, ok := h.resolveRechargeBillUserFilter(c, appCode, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId") sellerFilter := shared.UserIdentityFilterFromQuery(c, "seller")
sellerUserID, sellerMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, sellerFilter, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId")
if !ok { if !ok {
return return
} }
if (!userFilter.IsEmpty() && !userMatched) || (!sellerFilter.IsEmpty() && !sellerMatched) {
response.OK(c, response.Page{Items: []rechargeBillDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{ resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
RequestId: middleware.CurrentRequestID(c), RequestId: middleware.CurrentRequestID(c),
AppCode: appCode, AppCode: appCode,
@ -80,22 +86,30 @@ func (h *Handler) ListRechargeBills(c *gin.Context) {
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()}) response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
} }
func (h *Handler) resolveRechargeBillUserFilter(c *gin.Context, appCode string, invalidMessage string, keys ...string) (int64, bool) { func (h *Handler) resolveRechargeBillUserFilter(c *gin.Context, appCode string, filter shared.UserIdentityFilter, invalidMessage string, keys ...string) (int64, bool, bool) {
keyword := strings.TrimSpace(firstQuery(c, keys...)) keyword := strings.TrimSpace(firstQuery(c, keys...))
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, true return 0, false, true
}
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(c.Request.Context(), h.userDB, appCode, filter, time.Now().UnixMilli())
if err != nil {
response.ServerError(c, "查询账单用户信息失败")
return 0, false, false
}
return userID, matched, true
} }
// 充值账单事实在 wallet-service筛选字段只能传内部 user_id后台先把长 ID、默认短号、当前展示号和 active 靓号解析成同一个稳定 ID。 // 充值账单事实在 wallet-service筛选字段只能传内部 user_id后台先把长 ID、默认短号、当前展示号和 active 靓号解析成同一个稳定 ID。
userID, _, err := shared.ResolveExactUserIDOrNumericFallback(c.Request.Context(), h.userDB, appCode, keyword, time.Now().UnixMilli()) userID, _, err := shared.ResolveExactUserIDOrNumericFallback(c.Request.Context(), h.userDB, appCode, keyword, time.Now().UnixMilli())
if err != nil { if err != nil {
if errors.Is(err, shared.ErrInvalidUserIdentityFilter) { if errors.Is(err, shared.ErrInvalidUserIdentityFilter) {
response.BadRequest(c, invalidMessage) response.BadRequest(c, invalidMessage)
return 0, false return 0, false, false
} }
response.ServerError(c, "查询账单用户信息失败") response.ServerError(c, "查询账单用户信息失败")
return 0, false return 0, false, false
} }
return userID, true return userID, true, true
} }
// fillRechargeBillUsers 将用户资料写回当前页账单 DTO缺失资料会在 DTO 层保留长 ID避免影响账单事实展示。 // fillRechargeBillUsers 将用户资料写回当前页账单 DTO缺失资料会在 DTO 层保留长 ID避免影响账单事实展示。

View File

@ -31,10 +31,10 @@ func TestListRechargeBillsResolvesDisplayIDsBeforeWalletFilter(t *testing.T) {
router := newPaymentHandlerTestRouter(New(wallet, db, nil, nil, nil)) router := newPaymentHandlerTestRouter(New(wallet, db, nil, nil, nil))
userID := int64(327702592329093120) userID := int64(327702592329093120)
sellerUserID := int64(328453424662192128) sellerUserID := int64(328453424662192128)
expectUserIdentityLookup(sqlMock, "111", userID) expectUserDisplayIDLookup(sqlMock, "111", userID)
expectUserIdentityLookup(sqlMock, "167864", sellerUserID) expectUserDisplayIDLookup(sqlMock, "167864", sellerUserID)
request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills?user_id=111&seller_user_id=167864", nil) request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills?display_user_id=111&seller_display_user_id=167864", nil)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request) router.ServeHTTP(recorder, request)
@ -600,6 +600,18 @@ func expectUserIdentityLookup(sqlMock sqlmock.Sqlmock, keyword string, userID in
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID)) WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
} }
func expectUserDisplayIDLookup(sqlMock sqlmock.Sqlmock, displayUserID string, userID int64) {
sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
WithArgs(
"lalu",
displayUserID,
displayUserID,
sqlmock.AnyArg(),
displayUserID,
).
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
}
type moneyScopeFixture struct { type moneyScopeFixture struct {
appCode string appCode string
regionID int64 regionID int64

View File

@ -118,12 +118,13 @@ func (h *Handler) ListRegistrationRewardClaims(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
claimedStartMS := queryInt64(c, "claimed_start_ms", "claimedStartMs", "start_ms", "startMs") claimedStartMS := queryInt64(c, "claimed_start_ms", "claimedStartMs", "start_ms", "startMs")
claimedEndMS := queryInt64(c, "claimed_end_ms", "claimedEndMs", "end_ms", "endMs") claimedEndMS := queryInt64(c, "claimed_end_ms", "claimedEndMs", "end_ms", "endMs")
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok { if !ok {
response.ServerError(c, "查询用户信息失败") response.ServerError(c, "查询用户信息失败")
return return
} }
if options.Keyword != "" && !matched { if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
todayClaimedCount, ok := h.loadTodayClaimedCount(c) todayClaimedCount, ok := h.loadTodayClaimedCount(c)
if !ok { if !ok {
return return
@ -183,10 +184,14 @@ func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
} }
} }
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64, bool, bool) { func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, false, true return 0, false, true
} }
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
// 注册奖励领取记录的 owner 是 activity-service列表关键词先解析成内部 user_id再保持下游分页和 total 同源。 // 注册奖励领取记录的 owner 是 activity-service列表关键词先解析成内部 user_id再保持下游分页和 total 同源。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil { if err != nil {

View File

@ -674,6 +674,7 @@ func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
func (h *Handler) ListResourceGrants(c *gin.Context) { func (h *Handler) ListResourceGrants(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
appCode := appctx.FromContext(c.Request.Context()) appCode := appctx.FromContext(c.Request.Context())
targetFilter := shared.UserIdentityFilterFromQuery(c, "target")
targetUserID, _, err := shared.ResolveExactUserIDOrNumericFallback( targetUserID, _, err := shared.ResolveExactUserIDOrNumericFallback(
c.Request.Context(), c.Request.Context(),
h.userDB, h.userDB,
@ -689,6 +690,18 @@ func (h *Handler) ListResourceGrants(c *gin.Context) {
response.ServerError(c, "查询赠送用户失败") response.ServerError(c, "查询赠送用户失败")
return return
} }
if targetUserID == 0 && !targetFilter.IsEmpty() {
var matched bool
targetUserID, matched, err = shared.ResolveUserIdentityFilter(c.Request.Context(), h.userDB, appCode, targetFilter, time.Now().UnixMilli())
if err != nil {
response.ServerError(c, "查询赠送用户失败")
return
}
if !matched {
response.OK(c, response.Page{Items: []grantDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
}
resp, err := h.wallet.ListResourceGrants(c.Request.Context(), &walletv1.ListResourceGrantsRequest{ resp, err := h.wallet.ListResourceGrants(c.Request.Context(), &walletv1.ListResourceGrantsRequest{
RequestId: middleware.CurrentRequestID(c), RequestId: middleware.CurrentRequestID(c),
AppCode: appCode, AppCode: appCode,
@ -745,7 +758,20 @@ func (h *Handler) ListResourceShopPurchaseOrders(c *gin.Context) {
if !ok { if !ok {
return return
} }
userFilter := shared.UserIdentityFilterFromQuery(c, "")
userKeyword := strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user")) userKeyword := strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user"))
if userID == 0 && !userFilter.IsEmpty() {
resolvedUserID, matched, err := shared.ResolveUserIdentityFilter(c.Request.Context(), h.userDB, appCode, userFilter, time.Now().UnixMilli())
if err != nil {
response.ServerError(c, "查询购买用户失败")
return
}
if !matched {
response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
userID = resolvedUserID
}
if userID == 0 && userKeyword != "" { if userID == 0 && userKeyword != "" {
resolvedUserID, matched, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword) resolvedUserID, matched, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword)
if err != nil { if err != nil {

View File

@ -252,6 +252,7 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
PageSize: options.PageSize, PageSize: options.PageSize,
Keyword: options.Keyword, Keyword: options.Keyword,
OwnerUserKeyword: firstQueryValue(c, "ownerUserKeyword", "owner_user_keyword"), OwnerUserKeyword: firstQueryValue(c, "ownerUserKeyword", "owner_user_keyword"),
OwnerFilter: shared.UserIdentityFilterFromQuery(c, "owner"),
Status: options.Status, Status: options.Status,
SortBy: firstQueryValue(c, "sortBy", "sort_by"), SortBy: firstQueryValue(c, "sortBy", "sort_by"),
SortDirection: firstQueryValue(c, "sortDirection", "sort_direction", "order"), SortDirection: firstQueryValue(c, "sortDirection", "sort_direction", "order"),

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"hyapp-admin-server/internal/modules/shared"
) )
type listQuery struct { type listQuery struct {
@ -13,6 +15,7 @@ type listQuery struct {
Keyword string Keyword string
OwnerUserID int64 OwnerUserID int64
OwnerUserKeyword string OwnerUserKeyword string
OwnerFilter shared.UserIdentityFilter
Status string Status string
RegionID int64 RegionID int64
SortBy string SortBy string

View File

@ -77,7 +77,7 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
if strings.TrimSpace(query.OwnerUserKeyword) != "" && !ownerMatched { if (!query.OwnerFilter.IsEmpty() || strings.TrimSpace(query.OwnerUserKeyword) != "") && !ownerMatched {
return []Room{}, 0, nil return []Room{}, 0, nil
} }
roomKeyword := query.Keyword roomKeyword := query.Keyword
@ -115,6 +115,9 @@ func (s *Service) resolveListOwnerUserID(ctx context.Context, query listQuery) (
if query.OwnerUserID > 0 { if query.OwnerUserID > 0 {
return query.OwnerUserID, true, nil return query.OwnerUserID, true, nil
} }
if !query.OwnerFilter.IsEmpty() {
return shared.ResolveUserIdentityFilter(ctx, s.userDB, appctx.FromContext(ctx), query.OwnerFilter, time.Now().UnixMilli())
}
return s.resolveOwnerUserID(ctx, query.OwnerUserKeyword) return s.resolveOwnerUserID(ctx, query.OwnerUserKeyword)
} }
@ -380,6 +383,10 @@ func normalizeListQuery(query listQuery) listQuery {
query.PageSize = 100 query.PageSize = 100
} }
query.Keyword = strings.TrimSpace(query.Keyword) query.Keyword = strings.TrimSpace(query.Keyword)
query.OwnerUserKeyword = strings.TrimSpace(query.OwnerUserKeyword)
query.OwnerFilter.UserID = strings.TrimSpace(query.OwnerFilter.UserID)
query.OwnerFilter.DisplayUserID = strings.TrimSpace(query.OwnerFilter.DisplayUserID)
query.OwnerFilter.Username = strings.TrimSpace(query.OwnerFilter.Username)
query.Status = strings.ToLower(strings.TrimSpace(query.Status)) query.Status = strings.ToLower(strings.TrimSpace(query.Status))
if status, ok := normalizeRoomStatus(query.Status); ok { if status, ok := normalizeRoomStatus(query.Status); ok {
query.Status = status query.Status = status

View File

@ -130,7 +130,8 @@ func (h *Handler) ListSettlements(c *gin.Context) {
// 列表筛选沿用通用 ListOptionsstatus 过滤结算状态keyword 用作 room_id 精确查询,避免后台接口引入模糊扫表。 // 列表筛选沿用通用 ListOptionsstatus 过滤结算状态keyword 用作 room_id 精确查询,避免后台接口引入模糊扫表。
options := shared.ListOptions(c) options := shared.ListOptions(c)
ownerKeyword := strings.TrimSpace(firstQuery(c, "owner_user_keyword", "ownerUserKeyword")) ownerKeyword := strings.TrimSpace(firstQuery(c, "owner_user_keyword", "ownerUserKeyword"))
ownerUserID, ownerMatched, ownerOK := h.resolveOwnerUserID(c.Request.Context(), ownerKeyword) ownerFilter := shared.UserIdentityFilterFromQuery(c, "owner")
ownerUserID, ownerMatched, ownerOK := h.resolveOwnerUserID(c.Request.Context(), ownerKeyword, ownerFilter)
if !ownerOK { if !ownerOK {
response.ServerError(c, "查询房主信息失败") response.ServerError(c, "查询房主信息失败")
return return
@ -138,7 +139,7 @@ func (h *Handler) ListSettlements(c *gin.Context) {
if !ownerMatched { if !ownerMatched {
ownerUserID = queryInt64(c, "owner_user_id", "ownerUserId") ownerUserID = queryInt64(c, "owner_user_id", "ownerUserId")
} }
if !ownerMatched && ownerKeyword != "" { if !ownerMatched && (ownerKeyword != "" || !ownerFilter.IsEmpty()) {
response.OK(c, response.Page{Items: []settlementDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) response.OK(c, response.Page{Items: []settlementDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return return
} }
@ -197,10 +198,14 @@ func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
} }
} }
func (h *Handler) resolveOwnerUserID(ctx context.Context, keyword string) (int64, bool, bool) { func (h *Handler) resolveOwnerUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, false, true return 0, false, true
} }
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
if h.userDB == nil { if h.userDB == nil {
// userDB 缺失时只允许正整数长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。 // userDB 缺失时只允许正整数长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。
userID, matched, err := shared.ResolveExactUserIDOrNumericFallback(ctx, nil, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) userID, matched, err := shared.ResolveExactUserIDOrNumericFallback(ctx, nil, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())

View File

@ -112,12 +112,13 @@ func (h *Handler) UpdateSevenDayCheckInConfig(c *gin.Context) {
func (h *Handler) ListSevenDayCheckInClaims(c *gin.Context) { func (h *Handler) ListSevenDayCheckInClaims(c *gin.Context) {
options := shared.ListOptions(c) options := shared.ListOptions(c)
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword)) userFilter := shared.UserIdentityFilterFromQuery(c, "")
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword), userFilter)
if !ok { if !ok {
response.ServerError(c, "查询用户信息失败") response.ServerError(c, "查询用户信息失败")
return return
} }
if options.Keyword != "" && !matched { if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return return
} }
@ -159,10 +160,14 @@ func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
} }
} }
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64, bool, bool) { func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if keyword == "" { if filter.IsEmpty() && keyword == "" {
return 0, false, true return 0, false, true
} }
if !filter.IsEmpty() {
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, h.userDB, appctx.FromContext(ctx), filter, time.Now().UnixMilli())
return userID, matched, err == nil
}
// 签到领取记录按 activity-service 的内部 user_id 查;短号、靓号和昵称先在 admin 侧解析,避免下游误用展示 ID。 // 签到领取记录按 activity-service 的内部 user_id 查;短号、靓号和昵称先在 admin 侧解析,避免下游误用展示 ID。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil { if err != nil {

View File

@ -0,0 +1,39 @@
package shared
import (
"strings"
"github.com/gin-gonic/gin"
)
func UserIdentityFilterFromQuery(c *gin.Context, prefix string) UserIdentityFilter {
prefix = strings.TrimSpace(prefix)
return UserIdentityFilter{
UserID: firstQuery(c, queryNames(prefix, "user_id", "userId")...),
DisplayUserID: firstQuery(c, queryNames(prefix, "display_user_id", "displayUserId")...),
Username: firstQuery(c, queryNames(prefix, "username", "username")...),
}
}
func firstQuery(c *gin.Context, names ...string) string {
for _, name := range names {
if value := strings.TrimSpace(c.Query(name)); value != "" {
return value
}
}
return ""
}
func queryNames(prefix string, snake string, camel string) []string {
if prefix == "" {
return []string{snake, camel}
}
return []string{prefix + "_" + snake, prefix + upperFirst(camel)}
}
func upperFirst(value string) string {
if value == "" {
return ""
}
return strings.ToUpper(value[:1]) + value[1:]
}

View File

@ -42,6 +42,30 @@ func ResolveExactUserID(ctx context.Context, db *sql.DB, appCode string, keyword
return userID, true, nil return userID, true, nil
} }
func ResolveUserIdentityFilter(ctx context.Context, db *sql.DB, appCode string, filter UserIdentityFilter, nowMs int64) (int64, bool, error) {
if filter.IsEmpty() || db == nil {
return 0, false, nil
}
matchSQL, matchArgs := UserIdentityFieldsSQL("u", "u.user_id", filter, nowMs)
args := append([]any{appCode}, matchArgs...)
row := db.QueryRowContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY u.updated_at_ms DESC, u.user_id DESC
LIMIT 1`,
args...,
)
var userID int64
if err := row.Scan(&userID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, false, nil
}
return 0, false, err
}
return userID, true, nil
}
// ResolveExactUserIDOrNumericFallback 用于列表 owner 在 wallet/activity 等下游服务的场景。 // ResolveExactUserIDOrNumericFallback 用于列表 owner 在 wallet/activity 等下游服务的场景。
// 下游只认内部 user_id后台先按短号/靓号解析;解析不到时只接受正整数长 ID兼容用户资料缺失的历史事实记录。 // 下游只认内部 user_id后台先按短号/靓号解析;解析不到时只接受正整数长 ID兼容用户资料缺失的历史事实记录。
func ResolveExactUserIDOrNumericFallback(ctx context.Context, db *sql.DB, appCode string, keyword string, nowMs int64) (int64, bool, error) { func ResolveExactUserIDOrNumericFallback(ctx context.Context, db *sql.DB, appCode string, keyword string, nowMs int64) (int64, bool, error) {

View File

@ -5,6 +5,18 @@ import (
"strings" "strings"
) )
type UserIdentityFilter struct {
UserID string
DisplayUserID string
Username string
}
func (filter UserIdentityFilter) IsEmpty() bool {
return strings.TrimSpace(filter.UserID) == "" &&
strings.TrimSpace(filter.DisplayUserID) == "" &&
strings.TrimSpace(filter.Username) == ""
}
func UserIdentityKeywordSQL(userAlias string, userIDExpr string, keyword string, nowMs int64) (string, []any) { func UserIdentityKeywordSQL(userAlias string, userIDExpr string, keyword string, nowMs int64) (string, []any) {
keyword = strings.TrimSpace(keyword) keyword = strings.TrimSpace(keyword)
if keyword == "" { if keyword == "" {
@ -27,6 +39,29 @@ func UserIdentityKeywordSQL(userAlias string, userIDExpr string, keyword string,
} }
} }
func UserIdentityFieldsSQL(userAlias string, userIDExpr string, filter UserIdentityFilter, nowMs int64) (string, []any) {
clauses := make([]string, 0, 3)
args := make([]any, 0, 8)
if userID := strings.TrimSpace(filter.UserID); userID != "" {
clauses = append(clauses, fmt.Sprintf("CAST(%s AS CHAR) = ?", userIDExpr))
args = append(args, userID)
}
if displayUserID := strings.TrimSpace(filter.DisplayUserID); displayUserID != "" {
displaySQL, displayArgs := UserDisplayIDSQL(userAlias, displayUserID, nowMs)
clauses = append(clauses, displaySQL)
args = append(args, displayArgs...)
}
if username := strings.TrimSpace(filter.Username); username != "" {
clauses = append(clauses, userAlias+".username LIKE ?")
args = append(args, "%"+username+"%")
}
if len(clauses) == 0 {
return "1 = 1", nil
}
// 字段拆分后每个输入都是一个明确约束;同时输入多个字段时取交集,防止短 ID 和昵称互相扩大结果集。
return "(" + strings.Join(clauses, " AND ") + ")", args
}
func UserIdentityExactSQL(userAlias string, userIDExpr string, keyword string, nowMs int64) (string, []any) { func UserIdentityExactSQL(userAlias string, userIDExpr string, keyword string, nowMs int64) (string, []any) {
keyword = strings.TrimSpace(keyword) keyword = strings.TrimSpace(keyword)
if keyword == "" { if keyword == "" {

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"hyapp-admin-server/internal/modules/shared"
) )
// settlementUserID 只用于批量结算 HTTP 入参,兼容前端新合约的字符串 ID 和旧页面残留的数字 ID。 // settlementUserID 只用于批量结算 HTTP 入参,兼容前端新合约的字符串 ID 和旧页面残留的数字 ID。
@ -106,6 +108,7 @@ type pendingQuery struct {
CountryID int64 CountryID int64
CountryCode string CountryCode string
UserID int64 UserID int64
UserFilter shared.UserIdentityFilter
} }
type recordQuery struct { type recordQuery struct {
@ -119,6 +122,7 @@ type recordQuery struct {
CountryID int64 CountryID int64
CountryCode string CountryCode string
UserID int64 UserID int64
UserFilter shared.UserIdentityFilter
PolicyID uint64 PolicyID uint64
StartAtMS int64 StartAtMS int64
EndAtMS int64 EndAtMS int64

View File

@ -97,6 +97,7 @@ func parsePendingQuery(c *gin.Context) (pendingQuery, bool) {
TriggerMode: normalizeTriggerMode(firstQuery(c, "trigger_mode", "triggerMode")), TriggerMode: normalizeTriggerMode(firstQuery(c, "trigger_mode", "triggerMode")),
CycleKey: strings.TrimSpace(firstQuery(c, "cycle_key", "cycleKey")), CycleKey: strings.TrimSpace(firstQuery(c, "cycle_key", "cycleKey")),
CountryCode: strings.ToUpper(strings.TrimSpace(firstQuery(c, "country_code", "countryCode"))), CountryCode: strings.ToUpper(strings.TrimSpace(firstQuery(c, "country_code", "countryCode"))),
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
} }
var ok bool var ok bool
if req.RegionID, ok = optionalInt64(c, "region_id", "regionId"); !ok { if req.RegionID, ok = optionalInt64(c, "region_id", "regionId"); !ok {
@ -133,6 +134,7 @@ func parseRecordQuery(c *gin.Context) (recordQuery, bool) {
CycleKey: strings.TrimSpace(firstQuery(c, "cycle_key", "cycleKey")), CycleKey: strings.TrimSpace(firstQuery(c, "cycle_key", "cycleKey")),
Status: normalizeStatus(firstQuery(c, "status")), Status: normalizeStatus(firstQuery(c, "status")),
CountryCode: strings.ToUpper(strings.TrimSpace(firstQuery(c, "country_code", "countryCode"))), CountryCode: strings.ToUpper(strings.TrimSpace(firstQuery(c, "country_code", "countryCode"))),
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
} }
var ok bool var ok bool
if req.RegionID, ok = optionalInt64(c, "region_id", "regionId"); !ok { if req.RegionID, ok = optionalInt64(c, "region_id", "regionId"); !ok {

View File

@ -14,6 +14,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"hyapp-admin-server/internal/modules/shared"
) )
const ( const (
@ -124,6 +126,15 @@ func (s *Service) ListPending(ctx context.Context, appCode string, req pendingQu
return nil, 0, err return nil, 0, err
} }
req = normalizePendingQuery(req) req = normalizePendingQuery(req)
var matched bool
var err error
req.UserID, matched, err = s.resolveFilterUserID(ctx, appCode, req.UserID, req.UserFilter)
if err != nil {
return nil, 0, err
}
if !matched {
return []pendingDTO{}, 0, nil
}
// 待结算列表复用结算候选计算,保证运营看到的金额与真正点击结算时的金额口径一致。 // 待结算列表复用结算候选计算,保证运营看到的金额与真正点击结算时的金额口径一致。
candidates, err := s.collectCandidates(ctx, strings.TrimSpace(appCode), req.PolicyType, req.TriggerMode, req.CycleKey, req.RegionID, req.CountryID, req.CountryCode, req.UserID, time.Now().UTC().UnixMilli()) candidates, err := s.collectCandidates(ctx, strings.TrimSpace(appCode), req.PolicyType, req.TriggerMode, req.CycleKey, req.RegionID, req.CountryID, req.CountryCode, req.UserID, time.Now().UTC().UnixMilli())
if err != nil { if err != nil {
@ -157,6 +168,15 @@ func (s *Service) ListRecords(ctx context.Context, appCode string, req recordQue
return nil, 0, err return nil, 0, err
} }
req = normalizeRecordQuery(req) req = normalizeRecordQuery(req)
var matched bool
var err error
req.UserID, matched, err = s.resolveFilterUserID(ctx, appCode, req.UserID, req.UserFilter)
if err != nil {
return nil, 0, err
}
if !matched {
return []recordDTO{}, 0, nil
}
whereSQL, args, err := s.recordWhere(ctx, strings.TrimSpace(appCode), req) whereSQL, args, err := s.recordWhere(ctx, strings.TrimSpace(appCode), req)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -210,6 +230,17 @@ func (s *Service) ListRecords(ctx context.Context, appCode string, req recordQue
return items, total, nil return items, total, nil
} }
func (s *Service) resolveFilterUserID(ctx context.Context, appCode string, currentUserID int64, filter shared.UserIdentityFilter) (int64, bool, error) {
if currentUserID > 0 || filter.IsEmpty() {
return currentUserID, true, nil
}
userID, matched, err := shared.ResolveUserIdentityFilter(ctx, s.userDB, strings.TrimSpace(appCode), filter, time.Now().UnixMilli())
if err != nil || !matched {
return 0, matched, err
}
return userID, true, nil
}
// Settle 执行手动或自动结算;它只关心“触发方式”和“用户集合”,差值与幂等统一落在 settleCandidate。 // Settle 执行手动或自动结算;它只关心“触发方式”和“用户集合”,差值与幂等统一落在 settleCandidate。
func (s *Service) Settle(ctx context.Context, appCode string, operatorAdminID int64, req settleRequest) (settleResultDTO, error) { func (s *Service) Settle(ctx context.Context, appCode string, operatorAdminID int64, req settleRequest) (settleResultDTO, error) {
if err := s.ensureReady(ctx); err != nil { if err := s.ensureReady(ctx); err != nil {

View File

@ -903,6 +903,15 @@ func stageAndMaybeApply(ctx context.Context, db *sql.DB, state *rebuildState, ap
return err return err
} }
} }
// 注册投影的主键是 app_code+stat_tz+user_id不包含 registered_day。历史投影如果曾用错误时区写入
// 仅按日期窗口删除会漏掉同一 user_id 的旧行;本次重算的用户集合必须先清掉,保证补数可重复执行。
if _, err := tx.ExecContext(ctx, `
DELETE FROM stat_user_registration
WHERE app_code = ? AND stat_tz = ?
AND user_id IN (SELECT user_id FROM tmp_stat_backfill_registration)`,
app, statTZ); err != nil {
return err
}
for userID, dim := range state.userDims { for userID, dim := range state.userDims {
if _, err := tx.ExecContext(ctx, ` if _, err := tx.ExecContext(ctx, `
INSERT INTO stat_user_dimension (app_code, user_id, country_id, region_id, updated_at_ms) INSERT INTO stat_user_dimension (app_code, user_id, country_id, region_id, updated_at_ms)