fix(ops): resolve lucky gift profile short IDs

This commit is contained in:
zhx 2026-07-16 18:40:50 +08:00
parent 98d7743d7a
commit dba3cada59
11 changed files with 1064 additions and 739 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1545,6 +1545,18 @@ message ListAppsResponse {
repeated App apps = 1; repeated App apps = 1;
} }
// ResolveAdminUserIdentifierRequest
// user_identifier user_id
message ResolveAdminUserIdentifierRequest {
RequestMeta meta = 1;
string user_identifier = 2;
}
// ResolveAdminUserIdentifierResponse
message ResolveAdminUserIdentifierResponse {
UserIdentity identity = 1;
}
// AppRegistryService gateway package_name -> app_code // AppRegistryService gateway package_name -> app_code
service AppRegistryService { service AppRegistryService {
rpc ResolveApp(ResolveAppRequest) returns (ResolveAppResponse); rpc ResolveApp(ResolveAppRequest) returns (ResolveAppResponse);
@ -1577,6 +1589,8 @@ service RegionAdminService {
service UserIdentityService { service UserIdentityService {
rpc GetUserIdentity(GetUserIdentityRequest) returns (GetUserIdentityResponse); rpc GetUserIdentity(GetUserIdentityRequest) returns (GetUserIdentityResponse);
rpc ResolveDisplayUserID(ResolveDisplayUserIDRequest) returns (ResolveDisplayUserIDResponse); rpc ResolveDisplayUserID(ResolveDisplayUserIDRequest) returns (ResolveDisplayUserIDResponse);
// ResolveAdminUserIdentifier active
rpc ResolveAdminUserIdentifier(ResolveAdminUserIdentifierRequest) returns (ResolveAdminUserIdentifierResponse);
rpc ChangeDisplayUserID(ChangeDisplayUserIDRequest) returns (ChangeDisplayUserIDResponse); rpc ChangeDisplayUserID(ChangeDisplayUserIDRequest) returns (ChangeDisplayUserIDResponse);
rpc ApplyPrettyDisplayUserID(ApplyPrettyDisplayUserIDRequest) returns (ApplyPrettyDisplayUserIDResponse); rpc ApplyPrettyDisplayUserID(ApplyPrettyDisplayUserIDRequest) returns (ApplyPrettyDisplayUserIDResponse);
rpc ListAvailablePrettyDisplayIDs(ListAvailablePrettyDisplayIDsRequest) returns (ListAvailablePrettyDisplayIDsResponse); rpc ListAvailablePrettyDisplayIDs(ListAvailablePrettyDisplayIDsRequest) returns (ListAvailablePrettyDisplayIDsResponse);

View File

@ -3175,6 +3175,7 @@ var RegionAdminService_ServiceDesc = grpc.ServiceDesc{
const ( const (
UserIdentityService_GetUserIdentity_FullMethodName = "/hyapp.user.v1.UserIdentityService/GetUserIdentity" UserIdentityService_GetUserIdentity_FullMethodName = "/hyapp.user.v1.UserIdentityService/GetUserIdentity"
UserIdentityService_ResolveDisplayUserID_FullMethodName = "/hyapp.user.v1.UserIdentityService/ResolveDisplayUserID" UserIdentityService_ResolveDisplayUserID_FullMethodName = "/hyapp.user.v1.UserIdentityService/ResolveDisplayUserID"
UserIdentityService_ResolveAdminUserIdentifier_FullMethodName = "/hyapp.user.v1.UserIdentityService/ResolveAdminUserIdentifier"
UserIdentityService_ChangeDisplayUserID_FullMethodName = "/hyapp.user.v1.UserIdentityService/ChangeDisplayUserID" UserIdentityService_ChangeDisplayUserID_FullMethodName = "/hyapp.user.v1.UserIdentityService/ChangeDisplayUserID"
UserIdentityService_ApplyPrettyDisplayUserID_FullMethodName = "/hyapp.user.v1.UserIdentityService/ApplyPrettyDisplayUserID" UserIdentityService_ApplyPrettyDisplayUserID_FullMethodName = "/hyapp.user.v1.UserIdentityService/ApplyPrettyDisplayUserID"
UserIdentityService_ListAvailablePrettyDisplayIDs_FullMethodName = "/hyapp.user.v1.UserIdentityService/ListAvailablePrettyDisplayIDs" UserIdentityService_ListAvailablePrettyDisplayIDs_FullMethodName = "/hyapp.user.v1.UserIdentityService/ListAvailablePrettyDisplayIDs"
@ -3190,6 +3191,8 @@ const (
type UserIdentityServiceClient interface { type UserIdentityServiceClient interface {
GetUserIdentity(ctx context.Context, in *GetUserIdentityRequest, opts ...grpc.CallOption) (*GetUserIdentityResponse, error) GetUserIdentity(ctx context.Context, in *GetUserIdentityRequest, opts ...grpc.CallOption) (*GetUserIdentityResponse, error)
ResolveDisplayUserID(ctx context.Context, in *ResolveDisplayUserIDRequest, opts ...grpc.CallOption) (*ResolveDisplayUserIDResponse, error) ResolveDisplayUserID(ctx context.Context, in *ResolveDisplayUserIDRequest, opts ...grpc.CallOption) (*ResolveDisplayUserIDResponse, error)
// ResolveAdminUserIdentifier 仅供内部后台精确搜索;不得替代客户端 active 展示号解析语义。
ResolveAdminUserIdentifier(ctx context.Context, in *ResolveAdminUserIdentifierRequest, opts ...grpc.CallOption) (*ResolveAdminUserIdentifierResponse, error)
ChangeDisplayUserID(ctx context.Context, in *ChangeDisplayUserIDRequest, opts ...grpc.CallOption) (*ChangeDisplayUserIDResponse, error) ChangeDisplayUserID(ctx context.Context, in *ChangeDisplayUserIDRequest, opts ...grpc.CallOption) (*ChangeDisplayUserIDResponse, error)
ApplyPrettyDisplayUserID(ctx context.Context, in *ApplyPrettyDisplayUserIDRequest, opts ...grpc.CallOption) (*ApplyPrettyDisplayUserIDResponse, error) ApplyPrettyDisplayUserID(ctx context.Context, in *ApplyPrettyDisplayUserIDRequest, opts ...grpc.CallOption) (*ApplyPrettyDisplayUserIDResponse, error)
ListAvailablePrettyDisplayIDs(ctx context.Context, in *ListAvailablePrettyDisplayIDsRequest, opts ...grpc.CallOption) (*ListAvailablePrettyDisplayIDsResponse, error) ListAvailablePrettyDisplayIDs(ctx context.Context, in *ListAvailablePrettyDisplayIDsRequest, opts ...grpc.CallOption) (*ListAvailablePrettyDisplayIDsResponse, error)
@ -3225,6 +3228,16 @@ func (c *userIdentityServiceClient) ResolveDisplayUserID(ctx context.Context, in
return out, nil return out, nil
} }
func (c *userIdentityServiceClient) ResolveAdminUserIdentifier(ctx context.Context, in *ResolveAdminUserIdentifierRequest, opts ...grpc.CallOption) (*ResolveAdminUserIdentifierResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResolveAdminUserIdentifierResponse)
err := c.cc.Invoke(ctx, UserIdentityService_ResolveAdminUserIdentifier_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userIdentityServiceClient) ChangeDisplayUserID(ctx context.Context, in *ChangeDisplayUserIDRequest, opts ...grpc.CallOption) (*ChangeDisplayUserIDResponse, error) { func (c *userIdentityServiceClient) ChangeDisplayUserID(ctx context.Context, in *ChangeDisplayUserIDRequest, opts ...grpc.CallOption) (*ChangeDisplayUserIDResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ChangeDisplayUserIDResponse) out := new(ChangeDisplayUserIDResponse)
@ -3283,6 +3296,8 @@ func (c *userIdentityServiceClient) ExpirePrettyDisplayUserID(ctx context.Contex
type UserIdentityServiceServer interface { type UserIdentityServiceServer interface {
GetUserIdentity(context.Context, *GetUserIdentityRequest) (*GetUserIdentityResponse, error) GetUserIdentity(context.Context, *GetUserIdentityRequest) (*GetUserIdentityResponse, error)
ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error) ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error)
// ResolveAdminUserIdentifier 仅供内部后台精确搜索;不得替代客户端 active 展示号解析语义。
ResolveAdminUserIdentifier(context.Context, *ResolveAdminUserIdentifierRequest) (*ResolveAdminUserIdentifierResponse, error)
ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error) ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error)
ApplyPrettyDisplayUserID(context.Context, *ApplyPrettyDisplayUserIDRequest) (*ApplyPrettyDisplayUserIDResponse, error) ApplyPrettyDisplayUserID(context.Context, *ApplyPrettyDisplayUserIDRequest) (*ApplyPrettyDisplayUserIDResponse, error)
ListAvailablePrettyDisplayIDs(context.Context, *ListAvailablePrettyDisplayIDsRequest) (*ListAvailablePrettyDisplayIDsResponse, error) ListAvailablePrettyDisplayIDs(context.Context, *ListAvailablePrettyDisplayIDsRequest) (*ListAvailablePrettyDisplayIDsResponse, error)
@ -3304,6 +3319,9 @@ func (UnimplementedUserIdentityServiceServer) GetUserIdentity(context.Context, *
func (UnimplementedUserIdentityServiceServer) ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error) { func (UnimplementedUserIdentityServiceServer) ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResolveDisplayUserID not implemented") return nil, status.Error(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
} }
func (UnimplementedUserIdentityServiceServer) ResolveAdminUserIdentifier(context.Context, *ResolveAdminUserIdentifierRequest) (*ResolveAdminUserIdentifierResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResolveAdminUserIdentifier not implemented")
}
func (UnimplementedUserIdentityServiceServer) ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error) { func (UnimplementedUserIdentityServiceServer) ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ChangeDisplayUserID not implemented") return nil, status.Error(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
} }
@ -3376,6 +3394,24 @@ func _UserIdentityService_ResolveDisplayUserID_Handler(srv interface{}, ctx cont
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _UserIdentityService_ResolveAdminUserIdentifier_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResolveAdminUserIdentifierRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserIdentityServiceServer).ResolveAdminUserIdentifier(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserIdentityService_ResolveAdminUserIdentifier_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserIdentityServiceServer).ResolveAdminUserIdentifier(ctx, req.(*ResolveAdminUserIdentifierRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserIdentityService_ChangeDisplayUserID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserIdentityService_ChangeDisplayUserID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ChangeDisplayUserIDRequest) in := new(ChangeDisplayUserIDRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -3481,6 +3517,10 @@ var UserIdentityService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ResolveDisplayUserID", MethodName: "ResolveDisplayUserID",
Handler: _UserIdentityService_ResolveDisplayUserID_Handler, Handler: _UserIdentityService_ResolveDisplayUserID_Handler,
}, },
{
MethodName: "ResolveAdminUserIdentifier",
Handler: _UserIdentityService_ResolveAdminUserIdentifier_Handler,
},
{ {
MethodName: "ChangeDisplayUserID", MethodName: "ChangeDisplayUserID",
Handler: _UserIdentityService_ChangeDisplayUserID_Handler, Handler: _UserIdentityService_ChangeDisplayUserID_Handler,

View File

@ -177,6 +177,13 @@ type ResolveDisplayUserIDRequest struct {
DisplayUserID string DisplayUserID string
} }
// ResolveAdminUserIdentifierRequest 仅供后台精确搜索永久默认短号、当前有效展示号或系统长 user_id。
type ResolveAdminUserIdentifierRequest struct {
RequestID string
Caller string
UserIdentifier string
}
type UserIdentity struct { type UserIdentity struct {
UserID int64 `json:"userId,string"` UserID int64 `json:"userId,string"`
DisplayUserID string `json:"displayUserId"` DisplayUserID string `json:"displayUserId"`
@ -437,6 +444,19 @@ func (c *GRPCClient) ResolveDisplayUserID(ctx context.Context, req ResolveDispla
return fromProtoUserIdentity(resp.GetIdentity()), nil return fromProtoUserIdentity(resp.GetIdentity()), nil
} }
// ResolveAdminUserIdentifier 调用 user-service owner 的后台专用解析契约admin-server 不读取用户库,
// 也不会为了搜索默认短号而改变客户端 ResolveDisplayUserID 的 active-only 语义。
func (c *GRPCClient) ResolveAdminUserIdentifier(ctx context.Context, req ResolveAdminUserIdentifierRequest) (*UserIdentity, error) {
resp, err := c.identityClient.ResolveAdminUserIdentifier(ctx, &userv1.ResolveAdminUserIdentifierRequest{
Meta: requestMeta(ctx, req.RequestID, req.Caller),
UserIdentifier: req.UserIdentifier,
})
if err != nil {
return nil, err
}
return fromProtoUserIdentity(resp.GetIdentity()), nil
}
func fromProtoRegisterRiskConfig(config *userv1.RegisterRiskConfig) *RegisterRiskConfig { func fromProtoRegisterRiskConfig(config *userv1.RegisterRiskConfig) *RegisterRiskConfig {
if config == nil { if config == nil {
return nil return nil

View File

@ -22,11 +22,18 @@ import (
type Handler struct { type Handler struct {
luckyGift luckygiftadmin.Client luckyGift luckygiftadmin.Client
users userclient.Client users luckyGiftUserClient
requestTimeout time.Duration requestTimeout time.Duration
audit shared.OperationLogger audit shared.OperationLogger
} }
// luckyGiftUserClient 只暴露画像页面需要的 owner 能力。后台搜索必须走专用精确解析 RPC
// 用户资料仍批量读取,避免给全局 userclient.Client 增加画像模块专属依赖。
type luckyGiftUserClient interface {
BatchGetUsers(ctx context.Context, req userclient.BatchGetUsersRequest) (map[int64]*userclient.User, error)
ResolveAdminUserIdentifier(ctx context.Context, req userclient.ResolveAdminUserIdentifierRequest) (*userclient.UserIdentity, error)
}
func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler { func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
if requestTimeout <= 0 { if requestTimeout <= 0 {
requestTimeout = 3 * time.Second requestTimeout = 3 * time.Second
@ -36,7 +43,7 @@ func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit sh
// BindUserClient 注入 user-service 主数据读取能力。幸运礼物 owner 只保存不可变的内部 user_id // BindUserClient 注入 user-service 主数据读取能力。幸运礼物 owner 只保存不可变的内部 user_id
// 短号、默认号、靓号和用户资料必须在 admin-server 展示边界实时解析,不能复制进幸运礼物画像表。 // 短号、默认号、靓号和用户资料必须在 admin-server 展示边界实时解析,不能复制进幸运礼物画像表。
func (h *Handler) BindUserClient(users userclient.Client) *Handler { func (h *Handler) BindUserClient(users luckyGiftUserClient) *Handler {
h.users = users h.users = users
return h return h
} }

View File

@ -335,38 +335,21 @@ func (h *Handler) resolveLuckyGiftProfileKeyword(ctx context.Context, appCode, i
if h.users == nil { if h.users == nil {
return 0, externalUserID, false, fmt.Errorf("lucky gift user profile resolver is not configured") return 0, externalUserID, false, fmt.Errorf("lucky gift user profile resolver is not configured")
} }
identity, err := h.users.ResolveDisplayUserID(appctx.WithContext(ctx, appCode), userclient.ResolveDisplayUserIDRequest{ identity, err := h.users.ResolveAdminUserIdentifier(appctx.WithContext(ctx, appCode), userclient.ResolveAdminUserIdentifierRequest{
RequestID: requestID, Caller: "admin-server", DisplayUserID: keyword, RequestID: requestID, Caller: "admin-server", UserIdentifier: keyword,
}) })
if err == nil && identity != nil && identity.UserID > 0 { if err == nil && identity != nil && identity.UserID > 0 {
return identity.UserID, externalUserID, true, nil return identity.UserID, externalUserID, true, nil
} }
if err != nil { if err != nil {
switch grpcstatus.Code(err) { switch grpcstatus.Code(err) {
case codes.NotFound: case codes.NotFound, codes.InvalidArgument:
// 继续尝试把纯数字解释为系统长 user_id。
case codes.InvalidArgument:
// all 模式下 keyword 也可能是任意 external_user_id内部展示号格式不合法只代表内部未命中不能阻断外部精确搜索。 // all 模式下 keyword 也可能是任意 external_user_id内部展示号格式不合法只代表内部未命中不能阻断外部精确搜索。
return 0, externalUserID, false, nil return 0, externalUserID, false, nil
default: default:
return 0, externalUserID, false, err return 0, externalUserID, false, err
} }
} }
// 纯数字先按展示号解析;只有展示号确实不存在时才回退为 64 位长 user_id避免短号与长号碰撞时查错人。
longUserID, parseErr := strconv.ParseInt(keyword, 10, 64)
if parseErr != nil || longUserID <= 0 {
return 0, externalUserID, false, nil
}
user, getErr := h.users.GetUser(appctx.WithContext(ctx, appCode), userclient.GetUserRequest{
RequestID: requestID, Caller: "admin-server", UserID: longUserID,
})
if getErr == nil && user != nil {
return user.UserID, externalUserID, true, nil
}
if getErr != nil && grpcstatus.Code(getErr) != codes.NotFound {
return 0, externalUserID, false, getErr
}
return 0, externalUserID, false, nil return 0, externalUserID, false, nil
} }

View File

@ -2,6 +2,7 @@ package user
import ( import (
"context" "context"
"strings"
"hyapp/pkg/appcode" "hyapp/pkg/appcode"
"hyapp/pkg/xerr" "hyapp/pkg/xerr"
@ -51,6 +52,26 @@ func (s *Service) ResolveDisplayUserID(ctx context.Context, displayUserID string
return s.identityRepository.ResolveDisplayUserID(ctx, displayUserID, s.now().UnixMilli()) return s.identityRepository.ResolveDisplayUserID(ctx, displayUserID, s.now().UnixMilli())
} }
// ResolveAdminUserIdentifier 供内部后台按默认短号、当前有效展示号或系统长 user_id 精确查人。
// 它与客户端 ResolveDisplayUserID 分离,避免把 held 默认号意外恢复成登录或普通业务可解析身份。
func (s *Service) ResolveAdminUserIdentifier(ctx context.Context, userIdentifier string) (userdomain.Identity, error) {
userIdentifier = strings.TrimSpace(userIdentifier)
if !userdomain.ValidResolvableDisplayUserID(userIdentifier) {
return userdomain.Identity{}, xerr.New(xerr.InvalidArgument, "user_identifier format is invalid")
}
if s.identityRepository == nil {
return userdomain.Identity{}, xerr.New(xerr.Unavailable, "identity repository is not configured")
}
userID, err := s.identityRepository.ResolveAdminUserIdentifier(ctx, userIdentifier, s.now().UnixMilli())
if err != nil {
return userdomain.Identity{}, err
}
// repository 只决定搜索别名归属;最终复用 GetUserIdentity 返回当前 active 投影,
// 因此用 held 默认号搜索到有靓号的用户时,响应仍展示当前有效靓号。
return s.GetUserIdentity(ctx, userID)
}
// ChangeDisplayUserID 修改用户默认短号,并由 repository 在事务内维护当前展示快照和审计。 // ChangeDisplayUserID 修改用户默认短号,并由 repository 在事务内维护当前展示快照和审计。
func (s *Service) ChangeDisplayUserID(ctx context.Context, userID int64, newDisplayUserID string, reason string, operatorUserID int64, requestID string) (userdomain.Identity, error) { func (s *Service) ChangeDisplayUserID(ctx context.Context, userID int64, newDisplayUserID string, reason string, operatorUserID int64, requestID string) (userdomain.Identity, error) {
if userID <= 0 { if userID <= 0 {

View File

@ -124,6 +124,8 @@ type IdentityRepository interface {
GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error) GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error)
// ResolveDisplayUserID 把短号解析成 user_id并按 nowMs 触发懒过期。 // ResolveDisplayUserID 把短号解析成 user_id并按 nowMs 触发懒过期。
ResolveDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error) ResolveDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error)
// ResolveAdminUserIdentifier 为内部后台精确解析永久默认短号、当前有效展示号或系统长 user_id。
ResolveAdminUserIdentifier(ctx context.Context, userIdentifier string, nowMs int64) (int64, error)
// ChangeDisplayUserID 原子修改默认短号并写变更日志。 // ChangeDisplayUserID 原子修改默认短号并写变更日志。
ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error)
// ApplyPrettyDisplayUserID 原子创建靓号租约并覆盖当前展示号。 // ApplyPrettyDisplayUserID 原子创建靓号租约并覆盖当前展示号。

View File

@ -3,6 +3,8 @@ package identity
import ( import (
"context" "context"
"database/sql" "database/sql"
"strconv"
"strings"
"hyapp/pkg/appcode" "hyapp/pkg/appcode"
"hyapp/pkg/xerr" "hyapp/pkg/xerr"
@ -52,6 +54,97 @@ func (r *Repository) ResolveDisplayUserID(ctx context.Context, displayUserID str
return identity, nil return identity, nil
} }
// ResolveAdminUserIdentifier 是后台精确查人的 owner 入口。它刻意不复用 ResolveDisplayUserID
// 客户端只能解析当前 active 展示号,而运营搜索还必须能用被有效靓号覆盖的永久默认短号定位同一用户。
func (r *Repository) ResolveAdminUserIdentifier(ctx context.Context, userIdentifier string, nowMs int64) (int64, error) {
userIdentifier = strings.TrimSpace(userIdentifier)
appCode := appcode.FromContext(ctx)
// 输入本身是已过期靓号时先执行既有懒过期流程;这样过期靓号不会继续作为后台搜索别名,
// 同时用户的默认短号会恢复 active后续 GetUserIdentity 返回的始终是当前身份。
if err := r.expirePrettyByDisplayUserID(ctx, userIdentifier, nowMs, "lazy_admin_resolve"); err != nil {
return 0, err
}
var userID int64
// live_display_user_id 是 active/held 事实的生成列并有 app 级唯一索引。active 表示当前有效展示号;
// held 只接受仍等于 users.default_display_user_id 的永久默认号,避免 reserved/blocked 或历史释放号被解析。
err := r.db.QueryRowContext(ctx, `
SELECT du.user_id
FROM user_display_user_ids du FORCE INDEX (uk_user_display_user_ids_live)
INNER JOIN users u
ON u.app_code = du.app_code
AND u.user_id = du.user_id
WHERE du.app_code = ?
AND du.live_display_user_id = ?
AND (
(du.status = ? AND (du.expires_at_ms IS NULL OR du.expires_at_ms = 0 OR du.expires_at_ms > ?))
OR (du.status = ? AND du.display_user_id = u.default_display_user_id)
)
LIMIT 1
`, appCode, userIdentifier, string(userdomain.DisplayUserIDStatusActive), nowMs, string(userdomain.DisplayUserIDStatusHeld)).Scan(&userID)
if err == nil {
return userID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
// 兼容 identity 事实表补齐前的旧用户:两次独立等值查询分别命中 current/default 唯一索引,
// 不使用 OR 扫描 app 用户集合。过期 pretty 快照不能作为当前展示号命中。
err = r.db.QueryRowContext(ctx, `
SELECT user_id
FROM users FORCE INDEX (uk_users_current_display_user_id)
WHERE app_code = ?
AND current_display_user_id = ?
AND (
current_display_user_id_kind <> ?
OR current_display_user_id_expires_at_ms IS NULL
OR current_display_user_id_expires_at_ms = 0
OR current_display_user_id_expires_at_ms > ?
)
LIMIT 1
`, appCode, userIdentifier, string(userdomain.DisplayUserIDKindPretty), nowMs).Scan(&userID)
if err == nil {
return userID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
err = r.db.QueryRowContext(ctx, `
SELECT user_id
FROM users FORCE INDEX (uk_users_default_display_user_id)
WHERE app_code = ? AND default_display_user_id = ?
LIMIT 1
`, appCode, userIdentifier).Scan(&userID)
if err == nil {
return userID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
// 展示号优先于长 user_id保持既有后台搜索的碰撞决策只有所有展示别名都未命中时才查主键。
parsedUserID, parseErr := strconv.ParseInt(userIdentifier, 10, 64)
if parseErr == nil && parsedUserID > 0 {
err = r.db.QueryRowContext(ctx, `
SELECT user_id
FROM users FORCE INDEX (PRIMARY)
WHERE user_id = ? AND app_code = ?
LIMIT 1
`, parsedUserID, appCode).Scan(&userID)
if err == nil {
return userID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
}
return 0, xerr.New(xerr.DisplayUserIDNotFound, "user identifier not found")
}
func (r *Repository) queryActiveIdentity(ctx context.Context, clause string, args ...any) (userdomain.Identity, error) { func (r *Repository) queryActiveIdentity(ctx context.Context, clause string, args ...any) (userdomain.Identity, error) {
// user_display_user_ids 的 active 行是展示号归属事实users.current_display_user_id 只是高频读取快照。 // user_display_user_ids 的 active 行是展示号归属事实users.current_display_user_id 只是高频读取快照。
// 解析入口优先读取事实表,可以容忍历史脚本或异常中断导致 users 快照暂时没有同步到最新靓号。 // 解析入口优先读取事实表,可以容忍历史脚本或异常中断导致 users 快照暂时没有同步到最新靓号。

View File

@ -214,6 +214,10 @@ func (r *Repository) ResolveDisplayUserID(ctx context.Context, displayUserID str
return r.Repository.IdentityRepository().ResolveDisplayUserID(ctx, displayUserID, nowMs) return r.Repository.IdentityRepository().ResolveDisplayUserID(ctx, displayUserID, nowMs)
} }
func (r *Repository) ResolveAdminUserIdentifier(ctx context.Context, userIdentifier string, nowMs int64) (int64, error) {
return r.Repository.IdentityRepository().ResolveAdminUserIdentifier(ctx, userIdentifier, nowMs)
}
// ChangeDisplayUserID 让测试 wrapper 继续满足短号接口。 // ChangeDisplayUserID 让测试 wrapper 继续满足短号接口。
func (r *Repository) ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) { func (r *Repository) ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) {
return r.Repository.IdentityRepository().ChangeDisplayUserID(ctx, command) return r.Repository.IdentityRepository().ChangeDisplayUserID(ctx, command)

View File

@ -1116,6 +1116,18 @@ func (s *Server) ResolveDisplayUserID(ctx context.Context, req *userv1.ResolveDi
return &userv1.ResolveDisplayUserIDResponse{Identity: toProtoIdentity(identity)}, nil return &userv1.ResolveDisplayUserIDResponse{Identity: toProtoIdentity(identity)}, nil
} }
// ResolveAdminUserIdentifier 为内部后台精确解析默认短号、当前有效展示号或系统长 user_id。
func (s *Server) ResolveAdminUserIdentifier(ctx context.Context, req *userv1.ResolveAdminUserIdentifierRequest) (*userv1.ResolveAdminUserIdentifierResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
// 后台搜索语义由独立 service 用例承载,禁止扩大客户端 ResolveDisplayUserID 的 active-only 边界。
identity, err := s.userSvc.ResolveAdminUserIdentifier(ctx, req.GetUserIdentifier())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.ResolveAdminUserIdentifierResponse{Identity: toProtoIdentity(identity)}, nil
}
// ChangeDisplayUserID 修改当前 active 短号并返回新绑定。 // ChangeDisplayUserID 修改当前 active 短号并返回新绑定。
func (s *Server) ChangeDisplayUserID(ctx context.Context, req *userv1.ChangeDisplayUserIDRequest) (*userv1.ChangeDisplayUserIDResponse, error) { func (s *Server) ChangeDisplayUserID(ctx context.Context, req *userv1.ChangeDisplayUserIDRequest) (*userv1.ChangeDisplayUserIDResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta()) ctx = contextWithApp(ctx, req.GetMeta())