id搜索修改
This commit is contained in:
parent
f8ec2118b9
commit
a921aadcf8
@ -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))
|
||||
|
||||
@ -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"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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...)
|
||||
|
||||
@ -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, "流水类型不正确")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 ?)"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,避免影响账单事实展示。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -130,7 +130,8 @@ func (h *Handler) ListSettlements(c *gin.Context) {
|
||||
// 列表筛选沿用通用 ListOptions:status 过滤结算状态,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())
|
||||
|
||||
@ -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 {
|
||||
|
||||
39
server/admin/internal/modules/shared/user_identity_query.go
Normal file
39
server/admin/internal/modules/shared/user_identity_query.go
Normal 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:]
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user