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")
}
query = normalizeBanListQuery(query)
adminIDs, err := s.lookupAdminOperatorIDs(ctx, query.OperatorKeyword)
adminIDs, err := s.lookupAdminOperatorIDs(ctx, query.operatorAdminLookupKeyword())
if err != nil {
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')`
args := []any{appCode, appCode}
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)
whereSQL += " AND " + matchSQL
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)
operatorClauses := []string{"(l.operator_type = 'app_user' AND " + matchSQL + ")"}
args = append(args, matchArgs...)
@ -135,6 +143,15 @@ func banListWhereSQL(ctx context.Context, query banListQuery, adminIDs []int64)
}
}
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 {
whereSQL += " AND l.created_at_ms >= ?"
@ -276,9 +293,23 @@ func normalizeBanListQuery(query banListQuery) banListQuery {
}
query.TargetKeyword = strings.TrimSpace(query.TargetKeyword)
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
}
func (query banListQuery) operatorAdminLookupKeyword() string {
if keyword := strings.TrimSpace(query.OperatorAdminKeyword); keyword != "" {
return keyword
}
return strings.TrimSpace(query.OperatorKeyword)
}
func uniquePositiveInt64s(values []int64) []int64 {
seen := make(map[int64]struct{}, len(values))
out := make([]int64, 0, len(values))

View File

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

View File

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

View File

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

View File

@ -771,6 +771,9 @@ func normalizeListQuery(query listQuery) listQuery {
}
query.Country = normalizeCountryCode(query.Country)
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 {
query.RegionID = 0
query.RegionIDSet = false
@ -798,7 +801,11 @@ func appUserListWhereSQLAt(appCode string, query listQuery, nowMs int64) (string
whereSQL += " AND u.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)
whereSQL += " AND " + matchSQL
args = append(args, matchArgs...)

View File

@ -149,6 +149,7 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
Page: options.Page,
PageSize: options.PageSize,
UserKeyword: strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user_id", "userId", "keyword")),
UserFilter: shared.UserIdentityFilterFromQuery(c, ""),
BizType: firstQuery(c, "biz_type", "bizType"),
StartAtMS: startAtMS,
EndAtMS: endAtMS,
@ -186,9 +187,14 @@ func parseCoinSellerLedgerQuery(c *gin.Context) (coinSellerLedgerQuery, bool) {
PageSize: options.PageSize,
SellerUserID: sellerUserID,
SellerKeyword: firstQuery(c, "seller_keyword", "sellerKeyword", "keyword"),
LedgerType: firstQuery(c, "ledger_type", "ledgerType"),
StartAtMS: startAtMS,
EndAtMS: endAtMS,
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"),
StartAtMS: startAtMS,
EndAtMS: endAtMS,
})
if _, err := coinSellerLedgerBizTypes(query.LedgerType); err != nil {
response.BadRequest(c, "流水类型不正确")

View File

@ -1,9 +1,12 @@
package coinledger
import "hyapp-admin-server/internal/modules/shared"
type listQuery struct {
Page int
PageSize int
UserKeyword string
UserFilter shared.UserIdentityFilter
BizType string
StartAtMS int64
EndAtMS int64
@ -14,6 +17,7 @@ type coinSellerLedgerQuery struct {
PageSize int
SellerUserID int64
SellerKeyword string
SellerFilter shared.UserIdentityFilter
LedgerType string
StartAtMS 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")
}
userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword)
userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword, query.UserFilter)
if err != nil {
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")
}
userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword)
userIDs, userFiltered, err := s.resolveUserFilter(ctx, appCode, query.UserKeyword, query.UserFilter)
if err != nil {
return nil, 0, err
}
@ -539,13 +539,17 @@ func (s *Service) CreateCoinAdjustment(ctx context.Context, appCode string, acto
}, 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)
if keyword == "" {
if filter.IsEmpty() && keyword == "" {
return nil, false, nil
}
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)
}
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")
}
// 用户列筛选同时支持长 ID、默认短号、当前短号、active 靓号和昵称;数字关键字直接加入 user_id避免用户资料缺失时查不到账务事实。
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, time.Now().UnixMilli())
var matchSQL string
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...)
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
}
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)
if keyword == "" {
// 未传币商筛选时返回 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 靓号和昵称补充;
// 这里故意不搜索 receiver/counterparty避免“收款用户命中”把其他币商的流水带进当前币商筛选结果。
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, `
SELECT csp.user_id
FROM coin_seller_profiles csp
@ -765,6 +787,9 @@ func normalizeListQuery(query listQuery) listQuery {
query.PageSize = 100
}
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)
return query
}
@ -780,6 +805,9 @@ func normalizeCoinSellerLedgerQuery(query coinSellerLedgerQuery) coinSellerLedge
query.PageSize = 100
}
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)
return query
}

View File

@ -131,12 +131,13 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
func (h *Handler) ListGrants(c *gin.Context) {
options := shared.ListOptions(c)
// 发放记录主表只存 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 {
response.ServerError(c, "查询用户信息失败")
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})
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) {
if keyword == "" {
func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
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 统一维护。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {

View File

@ -137,12 +137,13 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
func (h *Handler) ListClaims(c *gin.Context) {
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 {
response.ServerError(c, "查询用户信息失败")
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})
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) {
if keyword == "" {
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
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。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {

View File

@ -471,12 +471,16 @@ func parseInt64ID(c *gin.Context, name string) (int64, bool) {
func parseListQuery(c *gin.Context) (listQuery, bool) {
options := shared.ListOptions(c)
query := listQuery{
Page: options.Page,
PageSize: options.PageSize,
Keyword: options.Keyword,
Status: options.Status,
SortBy: firstNonBlank(c.Query("sortBy"), c.Query("sort_by")),
SortDirection: firstNonBlank(c.Query("sortDirection"), c.Query("sort_direction"), c.Query("order")),
Page: options.Page,
PageSize: options.PageSize,
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,
SortBy: firstNonBlank(c.Query("sortBy"), c.Query("sort_by")),
SortDirection: firstNonBlank(c.Query("sortDirection"), c.Query("sort_direction"), c.Query("order")),
}
var ok bool
if query.RegionID, ok = parseOptionalInt64Query(c, "regionId", "region_id"); !ok {

View File

@ -134,8 +134,16 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
if query.ParentLeaderUserID > 0 {
whereSQL += " AND bp.parent_leader_user_id = ?"
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 + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli())
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 = ?"
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 + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "bp.user_id", keyword, time.Now().UnixMilli())
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 = ?"
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 + "%"
managerSQL, managerArgs := shared.UserIdentityKeywordSQL("manager", "mp.user_id", keyword, time.Now().UnixMilli())
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 {
whereSQL += " AND a.parent_bd_user_id = ?"
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 + "%"
nowMs := time.Now().UnixMilli()
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 = ?"
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 + "%"
nowMs := time.Now().UnixMilli()
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 = ?"
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 + "%"
matchSQL, matchArgs := shared.UserIdentityKeywordSQL("u", "csp.user_id", keyword, time.Now().UnixMilli())
whereSQL += " AND (" + matchSQL + " OR csp.contact_info LIKE ?)"

View File

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

View File

@ -614,6 +614,18 @@ func normalizeListQuery(query listQuery) listQuery {
query.PageSize = 100
}
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.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy))
switch query.SortBy {

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import (
"strings"
"time"
"hyapp-admin-server/internal/modules/shared"
walletv1 "hyapp.local/api/proto/wallet/v1"
"google.golang.org/grpc"
@ -362,6 +363,15 @@ func (s *Service) List(ctx context.Context, appCode string, req query) ([]settle
return nil, 0, err
}
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)
var total int64
@ -447,6 +457,25 @@ func (s *Service) List(ctx context.Context, appCode string, req query) ([]settle
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) {
conditions := []string{"app_code = ?"}
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) {
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 {
response.ServerError(c, "查询用户信息失败")
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})
return
}
@ -193,12 +194,13 @@ func (h *Handler) ListClaims(c *gin.Context) {
func (h *Handler) ListSummaries(c *gin.Context) {
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 {
response.ServerError(c, "查询用户信息失败")
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})
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) {
if keyword == "" {
func (h *Handler) resolveUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
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 解析器里。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
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) {
options := shared.ListOptions(c)
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 {
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 {
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{
RequestId: middleware.CurrentRequestID(c),
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()})
}
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...))
if keyword == "" {
return 0, true
if filter.IsEmpty() && keyword == "" {
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。
userID, _, err := shared.ResolveExactUserIDOrNumericFallback(c.Request.Context(), h.userDB, appCode, keyword, time.Now().UnixMilli())
if err != nil {
if errors.Is(err, shared.ErrInvalidUserIdentityFilter) {
response.BadRequest(c, invalidMessage)
return 0, false
return 0, false, false
}
response.ServerError(c, "查询账单用户信息失败")
return 0, false
return 0, false, false
}
return userID, true
return userID, true, true
}
// fillRechargeBillUsers 将用户资料写回当前页账单 DTO缺失资料会在 DTO 层保留长 ID避免影响账单事实展示。

View File

@ -31,10 +31,10 @@ func TestListRechargeBillsResolvesDisplayIDsBeforeWalletFilter(t *testing.T) {
router := newPaymentHandlerTestRouter(New(wallet, db, nil, nil, nil))
userID := int64(327702592329093120)
sellerUserID := int64(328453424662192128)
expectUserIdentityLookup(sqlMock, "111", userID)
expectUserIdentityLookup(sqlMock, "167864", sellerUserID)
expectUserDisplayIDLookup(sqlMock, "111", userID)
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()
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))
}
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 {
appCode string
regionID int64

View File

@ -118,12 +118,13 @@ func (h *Handler) ListRegistrationRewardClaims(c *gin.Context) {
options := shared.ListOptions(c)
claimedStartMS := queryInt64(c, "claimed_start_ms", "claimedStartMs", "start_ms", "startMs")
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 {
response.ServerError(c, "查询用户信息失败")
return
}
if options.Keyword != "" && !matched {
if (options.Keyword != "" || !userFilter.IsEmpty()) && !matched {
todayClaimedCount, ok := h.loadTodayClaimedCount(c)
if !ok {
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) {
if keyword == "" {
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
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 同源。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {

View File

@ -674,6 +674,7 @@ func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
func (h *Handler) ListResourceGrants(c *gin.Context) {
options := shared.ListOptions(c)
appCode := appctx.FromContext(c.Request.Context())
targetFilter := shared.UserIdentityFilterFromQuery(c, "target")
targetUserID, _, err := shared.ResolveExactUserIDOrNumericFallback(
c.Request.Context(),
h.userDB,
@ -689,6 +690,18 @@ func (h *Handler) ListResourceGrants(c *gin.Context) {
response.ServerError(c, "查询赠送用户失败")
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{
RequestId: middleware.CurrentRequestID(c),
AppCode: appCode,
@ -745,7 +758,20 @@ func (h *Handler) ListResourceShopPurchaseOrders(c *gin.Context) {
if !ok {
return
}
userFilter := shared.UserIdentityFilterFromQuery(c, "")
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 != "" {
resolvedUserID, matched, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword)
if err != nil {

View File

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

View File

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

View File

@ -77,7 +77,7 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
if err != nil {
return nil, 0, err
}
if strings.TrimSpace(query.OwnerUserKeyword) != "" && !ownerMatched {
if (!query.OwnerFilter.IsEmpty() || strings.TrimSpace(query.OwnerUserKeyword) != "") && !ownerMatched {
return []Room{}, 0, nil
}
roomKeyword := query.Keyword
@ -115,6 +115,9 @@ func (s *Service) resolveListOwnerUserID(ctx context.Context, query listQuery) (
if query.OwnerUserID > 0 {
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)
}
@ -380,6 +383,10 @@ func normalizeListQuery(query listQuery) listQuery {
query.PageSize = 100
}
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))
if status, ok := normalizeRoomStatus(query.Status); ok {
query.Status = status

View File

@ -130,7 +130,8 @@ func (h *Handler) ListSettlements(c *gin.Context) {
// 列表筛选沿用通用 ListOptionsstatus 过滤结算状态keyword 用作 room_id 精确查询,避免后台接口引入模糊扫表。
options := shared.ListOptions(c)
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 {
response.ServerError(c, "查询房主信息失败")
return
@ -138,7 +139,7 @@ func (h *Handler) ListSettlements(c *gin.Context) {
if !ownerMatched {
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})
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) {
if keyword == "" {
func (h *Handler) resolveOwnerUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
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 {
// userDB 缺失时只允许正整数长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。
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) {
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 {
response.ServerError(c, "查询用户信息失败")
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})
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) {
if keyword == "" {
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string, filter shared.UserIdentityFilter) (int64, bool, bool) {
if filter.IsEmpty() && keyword == "" {
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。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
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
}
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 等下游服务的场景。
// 下游只认内部 user_id后台先按短号/靓号解析;解析不到时只接受正整数长 ID兼容用户资料缺失的历史事实记录。
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"
)
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) {
keyword = strings.TrimSpace(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) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {

View File

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

View File

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

View File

@ -14,6 +14,8 @@ import (
"strconv"
"strings"
"time"
"hyapp-admin-server/internal/modules/shared"
)
const (
@ -124,6 +126,15 @@ func (s *Service) ListPending(ctx context.Context, appCode string, req pendingQu
return nil, 0, err
}
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())
if err != nil {
@ -157,6 +168,15 @@ func (s *Service) ListRecords(ctx context.Context, appCode string, req recordQue
return nil, 0, err
}
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)
if err != nil {
return nil, 0, err
@ -210,6 +230,17 @@ func (s *Service) ListRecords(ctx context.Context, appCode string, req recordQue
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。
func (s *Service) Settle(ctx context.Context, appCode string, operatorAdminID int64, req settleRequest) (settleResultDTO, error) {
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
}
}
// 注册投影的主键是 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 {
if _, err := tx.ExecContext(ctx, `
INSERT INTO stat_user_dimension (app_code, user_id, country_id, region_id, updated_at_ms)