fix(admin): resolve pretty id grant target aliases

This commit is contained in:
zhx 2026-06-12 15:50:22 +08:00
parent 9df03674d1
commit 4b5c2931ea
2 changed files with 226 additions and 13 deletions

View File

@ -1,6 +1,10 @@
package prettyid
import (
"context"
"encoding/json"
"errors"
"math"
"strconv"
"strings"
@ -10,8 +14,22 @@ import (
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const internalUserIDMinDigits = 15
var (
errGrantTargetInvalid = errors.New("请输入用户 ID 或短号")
errGrantTargetNotFound = errors.New("用户不存在")
)
type grantTargetResolver interface {
GetUser(ctx context.Context, req userclient.GetUserRequest) (*userclient.User, error)
ResolveDisplayUserID(ctx context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error)
}
type Handler struct {
user userclient.Client
audit shared.OperationLogger
@ -191,12 +209,12 @@ func (h *Handler) Grant(c *gin.Context) {
response.BadRequest(c, "发放参数不正确")
return
}
targetUserID, ok := parseFlexibleInt64(req.TargetUserID)
if !ok {
response.BadRequest(c, "target_user_id 参数不正确")
targetUserID, targetKind, err := resolveGrantTargetUserID(c.Request.Context(), h.user, middleware.CurrentRequestID(c), req.TargetUserID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 后台发放允许 target_user_id 由 JS 以数字或字符串提交,这里转成 int64 后把业务校验交给 user-service
// 后台发放最终仍只把内部 user_id 传给 user-service短号/靓号只作为后台输入别名,避免运营把 6 位短号误当成内部 ID 导致 NotFound
result, err := h.user.AdminGrantPrettyDisplayID(c.Request.Context(), userclient.AdminGrantPrettyDisplayIDRequest{
RequestID: middleware.CurrentRequestID(c),
Caller: "admin-server",
@ -210,7 +228,7 @@ func (h *Handler) Grant(c *gin.Context) {
response.BadRequest(c, err.Error())
return
}
shared.OperationLogWithResourceID(c, h.audit, "grant-pretty-id", "pretty_display_ids", result.PrettyID, "success", "target_user_id="+strconv.FormatInt(targetUserID, 10))
shared.OperationLogWithResourceID(c, h.audit, "grant-pretty-id", "pretty_display_ids", result.PrettyID, "success", "target_user_id="+strconv.FormatInt(targetUserID, 10)+" target_kind="+targetKind)
response.Created(c, result)
}
@ -272,18 +290,86 @@ func optionalInt64Query(c *gin.Context, name string) (int64, bool) {
return value, err == nil && value > 0
}
func parseFlexibleInt64(raw any) (int64, bool) {
// JSON 大整数在前端经常以字符串提交,数字提交时也必须是精确整数,不能接受小数或负数。
func resolveGrantTargetUserID(ctx context.Context, resolver grantTargetResolver, requestID string, raw any) (int64, string, error) {
keyword, ok := grantTargetKeyword(raw)
if !ok || resolver == nil {
return 0, "", errGrantTargetInvalid
}
numericID, numericOK := parsePositiveInt64(keyword)
// 内部 user_id 是雪花长整型,运营常用的短 ID 是 6 位左右;短输入优先走展示号解析,长输入优先保持旧的内部 ID 语义。
userIDChecked := false
if numericOK && len(strings.TrimLeft(keyword, "0")) >= internalUserIDMinDigits {
userIDChecked = true
if found, err := grantTargetByUserID(ctx, resolver, requestID, numericID); err != nil {
return 0, "", err
} else if found {
return numericID, "user_id", nil
}
}
if identity, err := resolver.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{
RequestID: requestID,
Caller: "admin-server",
DisplayUserID: keyword,
}); err != nil {
if !isGRPCNotFound(err) {
return 0, "", err
}
} else if identity != nil && identity.UserID > 0 {
return identity.UserID, "display_user_id", nil
}
if numericOK && !userIDChecked {
if found, err := grantTargetByUserID(ctx, resolver, requestID, numericID); err != nil {
return 0, "", err
} else if found {
return numericID, "user_id", nil
}
}
return 0, "", errGrantTargetNotFound
}
func grantTargetByUserID(ctx context.Context, resolver grantTargetResolver, requestID string, userID int64) (bool, error) {
// GetUser 只用于确认内部 ID 存在;真实发放仍在 AdminGrantPrettyDisplayID 事务里完成,避免这里承担写语义。
user, err := resolver.GetUser(ctx, userclient.GetUserRequest{
RequestID: requestID,
Caller: "admin-server",
UserID: userID,
})
if err != nil {
if isGRPCNotFound(err) {
return false, nil
}
return false, err
}
return user != nil && user.UserID > 0, nil
}
func grantTargetKeyword(raw any) (string, bool) {
// JSON 入口可能收到字符串、数字或测试里的 json.Number统一转成精确文本后再决定按内部 ID 还是短号解析。
switch value := raw.(type) {
case float64:
if value <= 0 || value != float64(int64(value)) {
return 0, false
if value <= 0 || value != math.Trunc(value) || value > float64(math.MaxInt64) {
return "", false
}
return int64(value), true
return strconv.FormatInt(int64(value), 10), true
case json.Number:
text := strings.TrimSpace(value.String())
if _, ok := parsePositiveInt64(text); !ok {
return "", false
}
return text, true
case string:
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
return parsed, err == nil && parsed > 0
text := strings.TrimSpace(value)
return text, text != ""
default:
return 0, false
return "", false
}
}
func parsePositiveInt64(text string) (int64, bool) {
parsed, err := strconv.ParseInt(strings.TrimSpace(text), 10, 64)
return parsed, err == nil && parsed > 0
}
func isGRPCNotFound(err error) bool {
return status.Code(err) == codes.NotFound
}

View File

@ -0,0 +1,127 @@
package prettyid
import (
"context"
"reflect"
"strconv"
"testing"
"hyapp-admin-server/internal/integration/userclient"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type fakeGrantTargetResolver struct {
users map[int64]*userclient.User
displays map[string]*userclient.UserIdentity
calls []string
}
func (f *fakeGrantTargetResolver) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) {
f.calls = append(f.calls, "get:"+strconv.FormatInt(req.UserID, 10))
if user := f.users[req.UserID]; user != nil {
return user, nil
}
return nil, status.Error(codes.NotFound, "user not found")
}
func (f *fakeGrantTargetResolver) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) {
f.calls = append(f.calls, "resolve:"+req.DisplayUserID)
if identity := f.displays[req.DisplayUserID]; identity != nil {
return identity, nil
}
return nil, status.Error(codes.NotFound, "display_user_id not found")
}
func TestResolveGrantTargetUserIDUsesShortDisplayID(t *testing.T) {
resolver := &fakeGrantTargetResolver{
users: map[int64]*userclient.User{},
displays: map[string]*userclient.UserIdentity{
"163001": {UserID: 312899006709637120, DisplayUserID: "163001"},
},
}
userID, kind, err := resolveGrantTargetUserID(context.Background(), resolver, "req", "163001")
if err != nil {
t.Fatalf("resolveGrantTargetUserID returned error: %v", err)
}
if userID != 312899006709637120 || kind != "display_user_id" {
t.Fatalf("target mismatch: userID=%d kind=%s", userID, kind)
}
if want := []string{"resolve:163001"}; !reflect.DeepEqual(resolver.calls, want) {
t.Fatalf("calls mismatch: got %v want %v", resolver.calls, want)
}
}
func TestResolveGrantTargetUserIDKeepsLongInternalID(t *testing.T) {
resolver := &fakeGrantTargetResolver{
users: map[int64]*userclient.User{
312899006709637120: {UserID: 312899006709637120, DisplayUserID: "163001"},
},
displays: map[string]*userclient.UserIdentity{},
}
userID, kind, err := resolveGrantTargetUserID(context.Background(), resolver, "req", "312899006709637120")
if err != nil {
t.Fatalf("resolveGrantTargetUserID returned error: %v", err)
}
if userID != 312899006709637120 || kind != "user_id" {
t.Fatalf("target mismatch: userID=%d kind=%s", userID, kind)
}
if want := []string{"get:312899006709637120"}; !reflect.DeepEqual(resolver.calls, want) {
t.Fatalf("calls mismatch: got %v want %v", resolver.calls, want)
}
}
func TestResolveGrantTargetUserIDFallsBackForLongDisplayID(t *testing.T) {
resolver := &fakeGrantTargetResolver{
users: map[int64]*userclient.User{},
displays: map[string]*userclient.UserIdentity{
"312899006709637120": {UserID: 411, DisplayUserID: "312899006709637120"},
},
}
userID, kind, err := resolveGrantTargetUserID(context.Background(), resolver, "req", "312899006709637120")
if err != nil {
t.Fatalf("resolveGrantTargetUserID returned error: %v", err)
}
if userID != 411 || kind != "display_user_id" {
t.Fatalf("target mismatch: userID=%d kind=%s", userID, kind)
}
if want := []string{"get:312899006709637120", "resolve:312899006709637120"}; !reflect.DeepEqual(resolver.calls, want) {
t.Fatalf("calls mismatch: got %v want %v", resolver.calls, want)
}
}
func TestResolveGrantTargetUserIDAcceptsPrettyDisplayID(t *testing.T) {
resolver := &fakeGrantTargetResolver{
users: map[int64]*userclient.User{},
displays: map[string]*userclient.UserIdentity{
"VIP2026": {UserID: 88, DisplayUserID: "VIP2026"},
},
}
userID, kind, err := resolveGrantTargetUserID(context.Background(), resolver, "req", "VIP2026")
if err != nil {
t.Fatalf("resolveGrantTargetUserID returned error: %v", err)
}
if userID != 88 || kind != "display_user_id" {
t.Fatalf("target mismatch: userID=%d kind=%s", userID, kind)
}
if want := []string{"resolve:VIP2026"}; !reflect.DeepEqual(resolver.calls, want) {
t.Fatalf("calls mismatch: got %v want %v", resolver.calls, want)
}
}
func TestResolveGrantTargetUserIDReportsMissingTarget(t *testing.T) {
resolver := &fakeGrantTargetResolver{
users: map[int64]*userclient.User{},
displays: map[string]*userclient.UserIdentity{},
}
_, _, err := resolveGrantTargetUserID(context.Background(), resolver, "req", "163001")
if err != errGrantTargetNotFound {
t.Fatalf("error mismatch: got %v want %v", err, errGrantTargetNotFound)
}
}