公共组件

This commit is contained in:
zhx 2026-06-25 17:07:01 +08:00
parent 72ec2bfcc9
commit 35060cc1fc
11 changed files with 193 additions and 236 deletions

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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()

View File

@ -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-servicewallet 只接受内部 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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}