fix(admin): resolve pretty id grant target aliases
This commit is contained in:
parent
9df03674d1
commit
4b5c2931ea
@ -1,6 +1,10 @@
|
|||||||
package prettyid
|
package prettyid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -10,8 +14,22 @@ import (
|
|||||||
"hyapp-admin-server/internal/response"
|
"hyapp-admin-server/internal/response"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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 {
|
type Handler struct {
|
||||||
user userclient.Client
|
user userclient.Client
|
||||||
audit shared.OperationLogger
|
audit shared.OperationLogger
|
||||||
@ -191,12 +209,12 @@ func (h *Handler) Grant(c *gin.Context) {
|
|||||||
response.BadRequest(c, "发放参数不正确")
|
response.BadRequest(c, "发放参数不正确")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetUserID, ok := parseFlexibleInt64(req.TargetUserID)
|
targetUserID, targetKind, err := resolveGrantTargetUserID(c.Request.Context(), h.user, middleware.CurrentRequestID(c), req.TargetUserID)
|
||||||
if !ok {
|
if err != nil {
|
||||||
response.BadRequest(c, "target_user_id 参数不正确")
|
response.BadRequest(c, err.Error())
|
||||||
return
|
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{
|
result, err := h.user.AdminGrantPrettyDisplayID(c.Request.Context(), userclient.AdminGrantPrettyDisplayIDRequest{
|
||||||
RequestID: middleware.CurrentRequestID(c),
|
RequestID: middleware.CurrentRequestID(c),
|
||||||
Caller: "admin-server",
|
Caller: "admin-server",
|
||||||
@ -210,7 +228,7 @@ func (h *Handler) Grant(c *gin.Context) {
|
|||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
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)
|
response.Created(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,18 +290,86 @@ func optionalInt64Query(c *gin.Context, name string) (int64, bool) {
|
|||||||
return value, err == nil && value > 0
|
return value, err == nil && value > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlexibleInt64(raw any) (int64, bool) {
|
func resolveGrantTargetUserID(ctx context.Context, resolver grantTargetResolver, requestID string, raw any) (int64, string, error) {
|
||||||
// JSON 大整数在前端经常以字符串提交,数字提交时也必须是精确整数,不能接受小数或负数。
|
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) {
|
switch value := raw.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
if value <= 0 || value != float64(int64(value)) {
|
if value <= 0 || value != math.Trunc(value) || value > float64(math.MaxInt64) {
|
||||||
return 0, false
|
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:
|
case string:
|
||||||
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
text := strings.TrimSpace(value)
|
||||||
return parsed, err == nil && parsed > 0
|
return text, text != ""
|
||||||
default:
|
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
|
||||||
|
}
|
||||||
|
|||||||
127
server/admin/internal/modules/prettyid/handler_test.go
Normal file
127
server/admin/internal/modules/prettyid/handler_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user