From 35060cc1fc12ab3d6cfa9ab5cb03132f700f7b92 Mon Sep 17 00:00:00 2001 From: zhx Date: Thu, 25 Jun 2026 17:07:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=AC=E5=85=B1=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cumulativerechargereward/handler.go | 32 +--------- .../modules/firstrechargereward/handler.go | 31 +-------- .../modules/inviteactivityreward/handler.go | 31 +-------- .../admin/internal/modules/payment/handler.go | 31 ++++++++- .../internal/modules/payment/handler_test.go | 63 +++++++++++++++++++ .../modules/registrationreward/handler.go | 31 +-------- .../internal/modules/resource/handler.go | 26 ++++---- .../internal/modules/resource/operator.go | 55 ++-------------- .../modules/roomturnoverreward/handler.go | 35 ++--------- .../modules/sevendaycheckin/handler.go | 31 +-------- .../modules/shared/user_identity_resolver.go | 63 +++++++++++++++++++ 11 files changed, 193 insertions(+), 236 deletions(-) create mode 100644 server/admin/internal/modules/shared/user_identity_resolver.go diff --git a/server/admin/internal/modules/cumulativerechargereward/handler.go b/server/admin/internal/modules/cumulativerechargereward/handler.go index 1f468b34..7d9e1b01 100644 --- a/server/admin/internal/modules/cumulativerechargereward/handler.go +++ b/server/admin/internal/modules/cumulativerechargereward/handler.go @@ -176,38 +176,12 @@ func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string) (int64 if keyword == "" { return 0, false, true } - if h.userDB == nil { - return 0, false, true - } - // keyword 支持真实 user_id、短 ID 和昵称模糊匹配;排序优先精确 ID,避免昵称碰撞查到错误用户。 - appCode := appctx.FromContext(ctx) - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) - rows, err := h.userDB.QueryContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) + // 累计充值奖励按 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 { return 0, false, false } - defer rows.Close() - if !rows.Next() { - return 0, false, rows.Err() == nil - } - var userID int64 - if err := rows.Scan(&userID); err != nil { - return 0, false, false - } - return userID, true, rows.Err() == nil + return userID, matched, true } func (h *Handler) fillGrantUsers(ctx context.Context, grants []grantDTO) error { diff --git a/server/admin/internal/modules/firstrechargereward/handler.go b/server/admin/internal/modules/firstrechargereward/handler.go index 8a80b111..41e1b7fe 100644 --- a/server/admin/internal/modules/firstrechargereward/handler.go +++ b/server/admin/internal/modules/firstrechargereward/handler.go @@ -181,37 +181,12 @@ func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64 if keyword == "" { return 0, false, true } - if h.userDB == nil { - return 0, false, true - } - appCode := appctx.FromContext(ctx) - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) - rows, err := h.userDB.QueryContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) + // 奖励领取记录由 activity-service 按内部 user_id 过滤;后台关键词先统一解析,避免各活动模块复制短号/靓号 SQL。 + userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) if err != nil { return 0, false, false } - defer rows.Close() - if !rows.Next() { - return 0, false, rows.Err() == nil - } - var userID int64 - if err := rows.Scan(&userID); err != nil { - return 0, false, false - } - return userID, true, rows.Err() == nil + return userID, matched, true } func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error { diff --git a/server/admin/internal/modules/inviteactivityreward/handler.go b/server/admin/internal/modules/inviteactivityreward/handler.go index 925ad584..0e31a89a 100644 --- a/server/admin/internal/modules/inviteactivityreward/handler.go +++ b/server/admin/internal/modules/inviteactivityreward/handler.go @@ -237,37 +237,12 @@ func (h *Handler) resolveUserID(ctx context.Context, keyword string) (int64, boo if keyword == "" { return 0, false, true } - if h.userDB == nil { - return 0, false, true - } - appCode := appctx.FromContext(ctx) - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) - rows, err := h.userDB.QueryContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) + // 邀请奖励汇总页只把内部 user_id 交给 activity-service;展示 ID 的兼容规则集中在 shared 解析器里。 + userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) if err != nil { return 0, false, false } - defer rows.Close() - if !rows.Next() { - return 0, false, rows.Err() == nil - } - var userID int64 - if err := rows.Scan(&userID); err != nil { - return 0, false, false - } - return userID, true, rows.Err() == nil + return userID, matched, true } func (h *Handler) fillUsers(ctx context.Context, claims []claimDTO) error { diff --git a/server/admin/internal/modules/payment/handler.go b/server/admin/internal/modules/payment/handler.go index 7b09f6b2..04161b95 100644 --- a/server/admin/internal/modules/payment/handler.go +++ b/server/admin/internal/modules/payment/handler.go @@ -7,6 +7,7 @@ import ( "math" "strconv" "strings" + "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/walletclient" @@ -37,11 +38,19 @@ func New(wallet walletclient.Client, userDB *sql.DB, audit shared.OperationLogge 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") + if !ok { + return + } + sellerUserID, ok := h.resolveRechargeBillUserFilter(c, appCode, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId") + if !ok { + return + } resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appCode, - UserId: queryInt64(c, "user_id", "userId"), - SellerUserId: queryInt64(c, "seller_user_id", "sellerUserId"), + UserId: userID, + SellerUserId: sellerUserID, RegionId: queryInt64(c, "region_id", "regionId"), RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")), Status: options.Status, @@ -68,6 +77,24 @@ 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) { + keyword := strings.TrimSpace(firstQuery(c, keys...)) + if keyword == "" { + return 0, 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 + } + response.ServerError(c, "查询账单用户信息失败") + return 0, false + } + return userID, true +} + // fillRechargeBillUsers 将用户资料写回当前页账单 DTO;缺失资料会在 DTO 层保留长 ID,避免影响账单事实展示。 func (h *Handler) fillRechargeBillUsers(ctx context.Context, appCode string, items []rechargeBillDTO) error { userIDs := collectRechargeBillUserIDs(items) diff --git a/server/admin/internal/modules/payment/handler_test.go b/server/admin/internal/modules/payment/handler_test.go index f812e3f4..5aa813ef 100644 --- a/server/admin/internal/modules/payment/handler_test.go +++ b/server/admin/internal/modules/payment/handler_test.go @@ -13,9 +13,42 @@ import ( "hyapp-admin-server/internal/middleware" walletv1 "hyapp.local/api/proto/wallet/v1" + "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" ) +func TestListRechargeBillsResolvesDisplayIDsBeforeWalletFilter(t *testing.T) { + db, sqlMock, err := sqlmock.New() + if err != nil { + t.Fatalf("create sql mock failed: %v", err) + } + defer db.Close() + + wallet := &mockPaymentWallet{rechargeBillsResp: &walletv1.ListRechargeBillsResponse{}} + router := newPaymentHandlerTestRouter(New(wallet, db, nil)) + userID := int64(327702592329093120) + sellerUserID := int64(328453424662192128) + expectUserIdentityLookup(sqlMock, "111", userID) + expectUserIdentityLookup(sqlMock, "167864", sellerUserID) + + request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills?user_id=111&seller_user_id=167864", nil) + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) + } + if wallet.lastRechargeBills == nil || + wallet.lastRechargeBills.GetUserId() != userID || + wallet.lastRechargeBills.GetSellerUserId() != sellerUserID { + t.Fatalf("recharge bill user filters should be resolved before wallet call: %+v", wallet.lastRechargeBills) + } + if err := sqlMock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations mismatch: %v", err) + } +} + func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.T) { wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{ Channels: []*walletv1.ThirdPartyPaymentChannel{{ @@ -330,6 +363,7 @@ func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine { c.Set(middleware.ContextUsername, "tester") c.Next() }) + router.GET("/admin/payment/recharge-bills", handler.ListRechargeBills) router.GET("/admin/payment/third-party-channels", handler.ListThirdPartyPaymentChannels) router.GET("/admin/payment/temporary-links", handler.ListTemporaryPaymentLinks) router.GET("/admin/payment/temporary-links/:order_id", handler.GetTemporaryPaymentLink) @@ -345,9 +379,30 @@ type adminPaymentTestResponse struct { Data map[string]any `json:"data"` } +func expectUserIdentityLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) { + sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`). + WithArgs( + "lalu", + keyword, + keyword, + keyword, + sqlmock.AnyArg(), + keyword, + "%"+keyword+"%", + keyword, + keyword, + keyword, + sqlmock.AnyArg(), + keyword, + ). + WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID)) +} + type mockPaymentWallet struct { walletclient.Client + lastRechargeBills *walletv1.ListRechargeBillsRequest + rechargeBillsResp *walletv1.ListRechargeBillsResponse lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse lastTemporaryOrders *walletv1.ListTemporaryRechargeOrdersRequest @@ -360,6 +415,14 @@ type mockPaymentWallet struct { lastCreateProduct *walletv1.CreateRechargeProductRequest } +func (m *mockPaymentWallet) ListRechargeBills(_ context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error) { + m.lastRechargeBills = req + if m.rechargeBillsResp != nil { + return m.rechargeBillsResp, nil + } + return &walletv1.ListRechargeBillsResponse{}, nil +} + func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) { m.lastThirdPartyChannels = req if m.thirdPartyChannelsResp != nil { diff --git a/server/admin/internal/modules/registrationreward/handler.go b/server/admin/internal/modules/registrationreward/handler.go index de3c391a..e7f89620 100644 --- a/server/admin/internal/modules/registrationreward/handler.go +++ b/server/admin/internal/modules/registrationreward/handler.go @@ -187,37 +187,12 @@ func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64 if keyword == "" { return 0, false, true } - if h.userDB == nil { - return 0, false, true - } - appCode := appctx.FromContext(ctx) - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) - rows, err := h.userDB.QueryContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) + // 注册奖励领取记录的 owner 是 activity-service;列表关键词先解析成内部 user_id,再保持下游分页和 total 同源。 + userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) if err != nil { return 0, false, false } - defer rows.Close() - if !rows.Next() { - return 0, false, rows.Err() == nil - } - var userID int64 - if err := rows.Scan(&userID); err != nil { - return 0, false, false - } - return userID, true, rows.Err() == nil + return userID, matched, true } func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error { diff --git a/server/admin/internal/modules/resource/handler.go b/server/admin/internal/modules/resource/handler.go index b20fd73f..cdcd6219 100644 --- a/server/admin/internal/modules/resource/handler.go +++ b/server/admin/internal/modules/resource/handler.go @@ -3,6 +3,7 @@ package resource import ( "context" "database/sql" + "errors" "fmt" "sort" "strconv" @@ -664,14 +665,17 @@ func (h *Handler) LookupResourceGrantTarget(c *gin.Context) { func (h *Handler) ListResourceGrants(c *gin.Context) { options := shared.ListOptions(c) - targetUserID, _, badRequest, err := h.resolveResourceGrantListTargetUserID( + appCode := appctx.FromContext(c.Request.Context()) + targetUserID, _, err := shared.ResolveExactUserIDOrNumericFallback( c.Request.Context(), - appctx.FromContext(c.Request.Context()), + h.userDB, + appCode, firstQuery(c, "target_user_id", "targetUserId"), + time.Now().UnixMilli(), ) if err != nil { - if badRequest { - response.BadRequest(c, err.Error()) + if errors.Is(err, shared.ErrInvalidUserIdentityFilter) { + response.BadRequest(c, "target_user_id 参数不正确") return } response.ServerError(c, "查询赠送用户失败") @@ -679,7 +683,7 @@ func (h *Handler) ListResourceGrants(c *gin.Context) { } resp, err := h.wallet.ListResourceGrants(c.Request.Context(), &walletv1.ListResourceGrantsRequest{ RequestId: middleware.CurrentRequestID(c), - AppCode: appctx.FromContext(c.Request.Context()), + AppCode: appCode, TargetUserId: targetUserID, Status: options.Status, Page: int32(options.Page), @@ -735,18 +739,16 @@ func (h *Handler) ListResourceShopPurchaseOrders(c *gin.Context) { } userKeyword := strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user")) if userID == 0 && userKeyword != "" { - resolvedUserID, filtered, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword) + resolvedUserID, matched, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword) if err != nil { - if err == sql.ErrNoRows { - response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) - return - } response.ServerError(c, "查询购买用户失败") return } - if filtered { - userID = resolvedUserID + if !matched { + response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0}) + return } + userID = resolvedUserID } ctx, cancel := h.walletRequestContext(c) defer cancel() diff --git a/server/admin/internal/modules/resource/operator.go b/server/admin/internal/modules/resource/operator.go index 0d0d0c94..726cb299 100644 --- a/server/admin/internal/modules/resource/operator.go +++ b/server/admin/internal/modules/resource/operator.go @@ -2,9 +2,6 @@ package resource import ( "context" - "database/sql" - "fmt" - "strconv" "strings" "time" @@ -117,32 +114,6 @@ func applyGrantTargetUsers(grants []grantDTO, users map[int64]grantUserDTO) { } } -func (h *Handler) resolveResourceGrantListTargetUserID(ctx context.Context, appCode string, raw string) (int64, bool, bool, error) { - raw = strings.TrimSpace(raw) - if raw == "" { - return 0, false, false, nil - } - // 资源赠送记录的最终 owner 是 wallet-service,wallet 只接受内部 user_id 过滤; - // 后台列表输入框展示的是“用户信息”,运营会输入默认短号或靓号,所以这里先按 user-service 身份投影解析。 - // 解析不到时保留旧版数字 target_user_id 行为,避免用户资料缺失时历史赠送记录完全查不到。 - fallbackUserID, parseErr := strconv.ParseInt(raw, 10, 64) - if h != nil && h.userDB != nil { - resolvedUserID, filtered, err := h.resolveResourceShopPurchaseUserID(ctx, appCode, raw) - switch { - case err == nil && filtered: - return resolvedUserID, true, false, nil - case err == sql.ErrNoRows: - // 没有用户身份投影时继续尝试按内部 user_id 过滤,兼容历史孤儿赠送记录。 - case err != nil: - return 0, true, false, err - } - } - if parseErr != nil || fallbackUserID < 0 { - return 0, true, true, fmt.Errorf("target_user_id 参数不正确") - } - return fallbackUserID, true, false, nil -} - func (h *Handler) enrichResourceShopPurchaseUsers(ctx context.Context, orders []resourceShopPurchaseOrderDTO) error { ids := collectResourceShopPurchaseUserIDs(orders) if len(ids) == 0 { @@ -191,29 +162,11 @@ func (h *Handler) resolveResourceShopPurchaseUserID(ctx context.Context, appCode if keyword == "" { return 0, false, nil } - if h == nil || h.userDB == nil { - return 0, false, fmt.Errorf("user mysql is not configured") + if h == nil { + return 0, false, nil } - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) - row := h.userDB.QueryRowContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) - var userID int64 - if err := row.Scan(&userID); err != nil { - return 0, true, err - } - return userID, true, nil + // 资源商店购买记录同样由 wallet-service 按内部 user_id 过滤;后台输入允许短号、靓号和昵称,先解析再下传。 + return shared.ResolveExactUserID(ctx, h.userDB, appCode, keyword, time.Now().UnixMilli()) } func (h *Handler) enrichGrantOperators(ctx context.Context, grants []grantDTO) error { diff --git a/server/admin/internal/modules/roomturnoverreward/handler.go b/server/admin/internal/modules/roomturnoverreward/handler.go index aef970e1..43a1e042 100644 --- a/server/admin/internal/modules/roomturnoverreward/handler.go +++ b/server/admin/internal/modules/roomturnoverreward/handler.go @@ -202,41 +202,16 @@ func (h *Handler) resolveOwnerUserID(ctx context.Context, keyword string) (int64 return 0, false, true } if h.userDB == nil { - // userDB 缺失时只允许长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。 - if parsed, err := strconv.ParseInt(keyword, 10, 64); err == nil && parsed > 0 { - return parsed, true, true - } - return 0, false, true + // userDB 缺失时只允许正整数长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。 + userID, matched, err := shared.ResolveExactUserIDOrNumericFallback(ctx, nil, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) + return userID, matched, err == nil } - appCode := appctx.FromContext(ctx) - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) // 表头筛选先把运营输入解析成唯一房主 user_id,再交给 activity-service 的 owner_user_id 条件,保证分页 total 和列表数据同源。 - rows, err := h.userDB.QueryContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) + userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) if err != nil { return 0, false, false } - defer rows.Close() - if !rows.Next() { - return 0, false, rows.Err() == nil - } - var userID int64 - if err := rows.Scan(&userID); err != nil { - return 0, false, false - } - return userID, true, rows.Err() == nil + return userID, matched, true } func (h *Handler) fillOwnerUsers(ctx context.Context, settlements []settlementDTO) error { diff --git a/server/admin/internal/modules/sevendaycheckin/handler.go b/server/admin/internal/modules/sevendaycheckin/handler.go index 61f3a9df..95ac6b75 100644 --- a/server/admin/internal/modules/sevendaycheckin/handler.go +++ b/server/admin/internal/modules/sevendaycheckin/handler.go @@ -163,37 +163,12 @@ func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64 if keyword == "" { return 0, false, true } - if h.userDB == nil { - return 0, false, true - } - appCode := appctx.FromContext(ctx) - nowMs := time.Now().UnixMilli() - matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) - orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) - args := append([]any{appCode}, matchArgs...) - args = append(args, orderArgs...) - rows, err := h.userDB.QueryContext(ctx, ` - SELECT u.user_id - FROM users u - WHERE u.app_code = ? AND `+matchSQL+` - ORDER BY - `+orderSQL+`, - u.user_id DESC - LIMIT 1`, - args..., - ) + // 签到领取记录按 activity-service 的内部 user_id 查;短号、靓号和昵称先在 admin 侧解析,避免下游误用展示 ID。 + userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli()) if err != nil { return 0, false, false } - defer rows.Close() - if !rows.Next() { - return 0, false, rows.Err() == nil - } - var userID int64 - if err := rows.Scan(&userID); err != nil { - return 0, false, false - } - return userID, true, rows.Err() == nil + return userID, matched, true } func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error { diff --git a/server/admin/internal/modules/shared/user_identity_resolver.go b/server/admin/internal/modules/shared/user_identity_resolver.go new file mode 100644 index 00000000..66a6a78d --- /dev/null +++ b/server/admin/internal/modules/shared/user_identity_resolver.go @@ -0,0 +1,63 @@ +package shared + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strconv" + "strings" +) + +var ErrInvalidUserIdentityFilter = errors.New("invalid user identity filter") + +// ResolveExactUserID 把后台输入框里的用户身份解析成 owner service 使用的内部 user_id。 +// 查询条件复用统一身份 SQL:精确长 ID、当前展示号、默认短号、active 靓号优先,昵称只做最后的模糊兜底。 +func ResolveExactUserID(ctx context.Context, db *sql.DB, appCode string, keyword string, nowMs int64) (int64, bool, error) { + keyword = strings.TrimSpace(keyword) + if keyword == "" || db == nil { + return 0, false, nil + } + matchSQL, matchArgs := UserIdentityExactSQL("u", "u.user_id", keyword, nowMs) + orderSQL, orderArgs := UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs) + args := append([]any{appCode}, matchArgs...) + args = append(args, orderArgs...) + row := db.QueryRowContext(ctx, ` + SELECT u.user_id + FROM users u + WHERE u.app_code = ? AND `+matchSQL+` + ORDER BY + `+orderSQL+`, + 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) { + keyword = strings.TrimSpace(keyword) + if keyword == "" { + return 0, false, nil + } + if db != nil { + userID, matched, err := ResolveExactUserID(ctx, db, appCode, keyword, nowMs) + if err != nil || matched { + return userID, matched, err + } + } + userID, err := strconv.ParseInt(keyword, 10, 64) + if err != nil || userID <= 0 { + return 0, true, fmt.Errorf("%w: %s", ErrInvalidUserIdentityFilter, keyword) + } + return userID, true, nil +}