用户靓号收回
This commit is contained in:
parent
a186f2b0d4
commit
6a783ec487
File diff suppressed because it is too large
Load Diff
@ -1278,6 +1278,13 @@ message SetPrettyDisplayIDStatusRequest {
|
||||
string reason = 5;
|
||||
}
|
||||
|
||||
message RecyclePrettyDisplayIDRequest {
|
||||
RequestMeta meta = 1;
|
||||
string pretty_id = 2;
|
||||
string reason = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message PrettyDisplayIDResponse {
|
||||
PrettyDisplayID item = 1;
|
||||
}
|
||||
@ -1414,5 +1421,6 @@ service UserPrettyDisplayIDAdminService {
|
||||
rpc GeneratePrettyDisplayIDs(GeneratePrettyDisplayIDsRequest) returns (GeneratePrettyDisplayIDsResponse);
|
||||
rpc ListPrettyDisplayIDs(ListPrettyDisplayIDsRequest) returns (ListPrettyDisplayIDsResponse);
|
||||
rpc SetPrettyDisplayIDStatus(SetPrettyDisplayIDStatusRequest) returns (PrettyDisplayIDResponse);
|
||||
rpc RecyclePrettyDisplayID(RecyclePrettyDisplayIDRequest) returns (PrettyDisplayIDResponse);
|
||||
rpc AdminGrantPrettyDisplayID(AdminGrantPrettyDisplayIDRequest) returns (AdminGrantPrettyDisplayIDResponse);
|
||||
}
|
||||
|
||||
@ -3171,6 +3171,7 @@ const (
|
||||
UserPrettyDisplayIDAdminService_GeneratePrettyDisplayIDs_FullMethodName = "/hyapp.user.v1.UserPrettyDisplayIDAdminService/GeneratePrettyDisplayIDs"
|
||||
UserPrettyDisplayIDAdminService_ListPrettyDisplayIDs_FullMethodName = "/hyapp.user.v1.UserPrettyDisplayIDAdminService/ListPrettyDisplayIDs"
|
||||
UserPrettyDisplayIDAdminService_SetPrettyDisplayIDStatus_FullMethodName = "/hyapp.user.v1.UserPrettyDisplayIDAdminService/SetPrettyDisplayIDStatus"
|
||||
UserPrettyDisplayIDAdminService_RecyclePrettyDisplayID_FullMethodName = "/hyapp.user.v1.UserPrettyDisplayIDAdminService/RecyclePrettyDisplayID"
|
||||
UserPrettyDisplayIDAdminService_AdminGrantPrettyDisplayID_FullMethodName = "/hyapp.user.v1.UserPrettyDisplayIDAdminService/AdminGrantPrettyDisplayID"
|
||||
)
|
||||
|
||||
@ -3186,6 +3187,7 @@ type UserPrettyDisplayIDAdminServiceClient interface {
|
||||
GeneratePrettyDisplayIDs(ctx context.Context, in *GeneratePrettyDisplayIDsRequest, opts ...grpc.CallOption) (*GeneratePrettyDisplayIDsResponse, error)
|
||||
ListPrettyDisplayIDs(ctx context.Context, in *ListPrettyDisplayIDsRequest, opts ...grpc.CallOption) (*ListPrettyDisplayIDsResponse, error)
|
||||
SetPrettyDisplayIDStatus(ctx context.Context, in *SetPrettyDisplayIDStatusRequest, opts ...grpc.CallOption) (*PrettyDisplayIDResponse, error)
|
||||
RecyclePrettyDisplayID(ctx context.Context, in *RecyclePrettyDisplayIDRequest, opts ...grpc.CallOption) (*PrettyDisplayIDResponse, error)
|
||||
AdminGrantPrettyDisplayID(ctx context.Context, in *AdminGrantPrettyDisplayIDRequest, opts ...grpc.CallOption) (*AdminGrantPrettyDisplayIDResponse, error)
|
||||
}
|
||||
|
||||
@ -3257,6 +3259,16 @@ func (c *userPrettyDisplayIDAdminServiceClient) SetPrettyDisplayIDStatus(ctx con
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userPrettyDisplayIDAdminServiceClient) RecyclePrettyDisplayID(ctx context.Context, in *RecyclePrettyDisplayIDRequest, opts ...grpc.CallOption) (*PrettyDisplayIDResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PrettyDisplayIDResponse)
|
||||
err := c.cc.Invoke(ctx, UserPrettyDisplayIDAdminService_RecyclePrettyDisplayID_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userPrettyDisplayIDAdminServiceClient) AdminGrantPrettyDisplayID(ctx context.Context, in *AdminGrantPrettyDisplayIDRequest, opts ...grpc.CallOption) (*AdminGrantPrettyDisplayIDResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminGrantPrettyDisplayIDResponse)
|
||||
@ -3279,6 +3291,7 @@ type UserPrettyDisplayIDAdminServiceServer interface {
|
||||
GeneratePrettyDisplayIDs(context.Context, *GeneratePrettyDisplayIDsRequest) (*GeneratePrettyDisplayIDsResponse, error)
|
||||
ListPrettyDisplayIDs(context.Context, *ListPrettyDisplayIDsRequest) (*ListPrettyDisplayIDsResponse, error)
|
||||
SetPrettyDisplayIDStatus(context.Context, *SetPrettyDisplayIDStatusRequest) (*PrettyDisplayIDResponse, error)
|
||||
RecyclePrettyDisplayID(context.Context, *RecyclePrettyDisplayIDRequest) (*PrettyDisplayIDResponse, error)
|
||||
AdminGrantPrettyDisplayID(context.Context, *AdminGrantPrettyDisplayIDRequest) (*AdminGrantPrettyDisplayIDResponse, error)
|
||||
mustEmbedUnimplementedUserPrettyDisplayIDAdminServiceServer()
|
||||
}
|
||||
@ -3308,6 +3321,9 @@ func (UnimplementedUserPrettyDisplayIDAdminServiceServer) ListPrettyDisplayIDs(c
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) SetPrettyDisplayIDStatus(context.Context, *SetPrettyDisplayIDStatusRequest) (*PrettyDisplayIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetPrettyDisplayIDStatus not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) RecyclePrettyDisplayID(context.Context, *RecyclePrettyDisplayIDRequest) (*PrettyDisplayIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecyclePrettyDisplayID not implemented")
|
||||
}
|
||||
func (UnimplementedUserPrettyDisplayIDAdminServiceServer) AdminGrantPrettyDisplayID(context.Context, *AdminGrantPrettyDisplayIDRequest) (*AdminGrantPrettyDisplayIDResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGrantPrettyDisplayID not implemented")
|
||||
}
|
||||
@ -3441,6 +3457,24 @@ func _UserPrettyDisplayIDAdminService_SetPrettyDisplayIDStatus_Handler(srv inter
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserPrettyDisplayIDAdminService_RecyclePrettyDisplayID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RecyclePrettyDisplayIDRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserPrettyDisplayIDAdminServiceServer).RecyclePrettyDisplayID(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserPrettyDisplayIDAdminService_RecyclePrettyDisplayID_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserPrettyDisplayIDAdminServiceServer).RecyclePrettyDisplayID(ctx, req.(*RecyclePrettyDisplayIDRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserPrettyDisplayIDAdminService_AdminGrantPrettyDisplayID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminGrantPrettyDisplayIDRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -3490,6 +3524,10 @@ var UserPrettyDisplayIDAdminService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SetPrettyDisplayIDStatus",
|
||||
Handler: _UserPrettyDisplayIDAdminService_SetPrettyDisplayIDStatus_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RecyclePrettyDisplayID",
|
||||
Handler: _UserPrettyDisplayIDAdminService_RecyclePrettyDisplayID_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminGrantPrettyDisplayID",
|
||||
Handler: _UserPrettyDisplayIDAdminService_AdminGrantPrettyDisplayID_Handler,
|
||||
|
||||
@ -25,6 +25,7 @@ type Client interface {
|
||||
GeneratePrettyDisplayIDs(ctx context.Context, req GeneratePrettyDisplayIDsRequest) (*PrettyDisplayIDGenerationBatch, error)
|
||||
ListPrettyDisplayIDs(ctx context.Context, req ListPrettyDisplayIDsRequest) ([]*PrettyDisplayID, int64, error)
|
||||
SetPrettyDisplayIDStatus(ctx context.Context, req SetPrettyDisplayIDStatusRequest) (*PrettyDisplayID, error)
|
||||
RecyclePrettyDisplayID(ctx context.Context, req RecyclePrettyDisplayIDRequest) (*PrettyDisplayID, error)
|
||||
AdminGrantPrettyDisplayID(ctx context.Context, req AdminGrantPrettyDisplayIDRequest) (*AdminGrantPrettyDisplayIDResult, error)
|
||||
ListCountries(ctx context.Context, req ListCountriesRequest) ([]*Country, error)
|
||||
UpdateCountry(ctx context.Context, req UpdateCountryRequest) (*Country, error)
|
||||
|
||||
@ -113,6 +113,14 @@ type SetPrettyDisplayIDStatusRequest struct {
|
||||
OperatorAdminID int64
|
||||
}
|
||||
|
||||
type RecyclePrettyDisplayIDRequest struct {
|
||||
RequestID string
|
||||
Caller string
|
||||
PrettyID string
|
||||
Reason string
|
||||
OperatorAdminID int64
|
||||
}
|
||||
|
||||
type AdminGrantPrettyDisplayIDRequest struct {
|
||||
RequestID string
|
||||
Caller string
|
||||
@ -238,6 +246,20 @@ func (c *GRPCClient) SetPrettyDisplayIDStatus(ctx context.Context, req SetPretty
|
||||
return fromProtoPrettyDisplayID(resp.GetItem()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) RecyclePrettyDisplayID(ctx context.Context, req RecyclePrettyDisplayIDRequest) (*PrettyDisplayID, error) {
|
||||
// 回收必须走 user-service 的后台 RPC,由 owner service 同事务恢复默认短号并释放靓号池记录。
|
||||
resp, err := c.prettyClient.RecyclePrettyDisplayID(ctx, &userv1.RecyclePrettyDisplayIDRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
PrettyId: req.PrettyID,
|
||||
Reason: req.Reason,
|
||||
OperatorAdminId: req.OperatorAdminID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromProtoPrettyDisplayID(resp.GetItem()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) AdminGrantPrettyDisplayID(ctx context.Context, req AdminGrantPrettyDisplayIDRequest) (*AdminGrantPrettyDisplayIDResult, error) {
|
||||
// 后台发放返回当前用户身份快照,admin-server 原样转给前端,方便页面确认最终展示号。
|
||||
resp, err := c.prettyClient.AdminGrantPrettyDisplayID(ctx, &userv1.AdminGrantPrettyDisplayIDRequest{
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -68,6 +69,10 @@ type statusRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type recycleRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (h *Handler) ListPools(c *gin.Context) {
|
||||
page, pageSize, ok := pageQuery(c)
|
||||
if !ok {
|
||||
@ -256,6 +261,29 @@ func (h *Handler) SetStatus(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Recycle(c *gin.Context) {
|
||||
var req recycleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||
response.BadRequest(c, "回收参数不正确")
|
||||
return
|
||||
}
|
||||
// 回收只能以路径 pretty_id 定位,不能相信前端传用户 ID 或 lease ID;真实占用关系由 user-service 事务重新锁表校验。
|
||||
prettyID := strings.TrimSpace(c.Param("pretty_id"))
|
||||
item, err := h.user.RecyclePrettyDisplayID(c.Request.Context(), userclient.RecyclePrettyDisplayIDRequest{
|
||||
RequestID: middleware.CurrentRequestID(c),
|
||||
Caller: "admin-server",
|
||||
PrettyID: prettyID,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
OperatorAdminID: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLogWithResourceID(c, h.audit, "recycle-pretty-id", "pretty_display_ids", prettyID, "success", "status="+item.Status)
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func pageQuery(c *gin.Context) (int32, int32, bool) {
|
||||
// 分页参数在 admin-server 先收敛到 1..100,避免后台页面传错值造成 user-service 大分页查询。
|
||||
page := int32(1)
|
||||
|
||||
@ -17,5 +17,6 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
protected.POST("/admin/users/pretty-id-pools/:pool_id/generate", middleware.RequirePermission("pretty-id:generate"), h.Generate)
|
||||
protected.GET("/admin/users/pretty-ids", middleware.RequirePermission("pretty-id:view"), h.ListIDs)
|
||||
protected.POST("/admin/users/pretty-ids/grant", middleware.RequirePermission("pretty-id:grant"), h.Grant)
|
||||
protected.POST("/admin/users/pretty-ids/:pretty_id/recycle", middleware.RequirePermission("pretty-id:update"), h.Recycle)
|
||||
protected.POST("/admin/users/pretty-ids/:pretty_id/status", middleware.RequirePermission("pretty-id:update"), h.SetStatus)
|
||||
}
|
||||
|
||||
@ -569,6 +569,16 @@ type PrettyDisplayIDStatusCommand struct {
|
||||
NowMs int64
|
||||
}
|
||||
|
||||
// PrettyDisplayIDRecycleCommand 描述后台强制回收用户当前靓号的输入。
|
||||
type PrettyDisplayIDRecycleCommand struct {
|
||||
AppCode string
|
||||
PrettyID string
|
||||
Reason string
|
||||
OperatorAdminID int64
|
||||
RequestID string
|
||||
NowMs int64
|
||||
}
|
||||
|
||||
// PrettyDisplayIDPoolApplyCommand 描述 App 靓号池申请事务输入。
|
||||
type PrettyDisplayIDPoolApplyCommand struct {
|
||||
AppCode string
|
||||
|
||||
@ -1634,3 +1634,66 @@ func TestPasswordLoginAllowsAlphanumericPrettyDisplayID(t *testing.T) {
|
||||
t.Fatalf("unexpected alphanumeric pretty login token: %+v", prettyToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecycleAdminGrantedPrettyDisplayIDRestoresDefaultDisplayID(t *testing.T) {
|
||||
// 后台回收必须能处理 duration_ms=0 的长期靓号;这类靓号不能复用到期恢复接口,否则会被未到期校验拒绝。
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
seedCountry(t, repository, "SG")
|
||||
now := time.UnixMilli(1000)
|
||||
authSvc := newAuthService(repository, &now, []int64{900001}, []string{"100001"})
|
||||
userSvc := newUserService(repository, userservice.WithClock(func() time.Time { return now }))
|
||||
|
||||
token, _, err := authSvc.LoginThirdParty(ctx, "wechat", "openid-pretty-recycle", thirdPartyRegistration("ios"), authservice.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoginThirdParty failed: %v", err)
|
||||
}
|
||||
if err := authSvc.SetPassword(ctx, token.UserID, "secret-pass", authservice.Meta{}); err != nil {
|
||||
t.Fatalf("SetPassword failed: %v", err)
|
||||
}
|
||||
identity, _, err := userSvc.AdminGrantPrettyDisplayID(ctx, userdomain.AdminGrantPrettyDisplayIDCommand{
|
||||
TargetUserID: token.UserID,
|
||||
DisplayUserID: "RECYCLE2026",
|
||||
OperatorAdminID: 1,
|
||||
RequestID: "req-admin-pretty-recycle-grant",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("AdminGrantPrettyDisplayID failed: %v", err)
|
||||
}
|
||||
if identity.PrettyID == "" || identity.DisplayUserID != "RECYCLE2026" {
|
||||
t.Fatalf("unexpected grant identity: %+v", identity)
|
||||
}
|
||||
|
||||
now = time.UnixMilli(2000)
|
||||
recycled, err := userSvc.RecyclePrettyDisplayID(ctx, userdomain.PrettyDisplayIDRecycleCommand{
|
||||
PrettyID: identity.PrettyID,
|
||||
Reason: "manual test recycle",
|
||||
OperatorAdminID: 2,
|
||||
RequestID: "req-admin-pretty-recycle",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RecyclePrettyDisplayID failed: %v", err)
|
||||
}
|
||||
if recycled.Status != userdomain.PrettyDisplayIDStatusReleased || recycled.AssignedUserID != 0 || recycled.AssignedLeaseID != "" {
|
||||
// 后台自由发放号回收后保留 released 历史态,不回到池内可申请。
|
||||
t.Fatalf("unexpected recycled pretty item: %+v", recycled)
|
||||
}
|
||||
|
||||
userAfterRecycle, err := repository.GetUser(ctx, token.UserID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser after recycle failed: %v", err)
|
||||
}
|
||||
if userAfterRecycle.CurrentDisplayUserID != token.DefaultDisplayUserID || userAfterRecycle.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindDefault {
|
||||
t.Fatalf("user must recover default display id after recycle: %+v", userAfterRecycle)
|
||||
}
|
||||
defaultToken, err := authSvc.LoginPassword(ctx, token.DefaultDisplayUserID, "secret-pass", "ios", authservice.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("default display_user_id login after recycle failed: %v", err)
|
||||
}
|
||||
if defaultToken.DisplayUserID != token.DefaultDisplayUserID || defaultToken.DisplayUserIDKind != string(userdomain.DisplayUserIDKindDefault) {
|
||||
t.Fatalf("unexpected default token after recycle: %+v", defaultToken)
|
||||
}
|
||||
if _, err := authSvc.LoginPassword(ctx, "RECYCLE2026", "secret-pass", "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.AuthFailed) {
|
||||
t.Fatalf("recycled pretty display_user_id must not login, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,6 +270,22 @@ func (s *Service) SetPrettyDisplayIDStatus(ctx context.Context, command userdoma
|
||||
return s.identityRepository.SetPrettyDisplayIDStatus(ctx, command)
|
||||
}
|
||||
|
||||
// RecyclePrettyDisplayID 后台强制回收用户正在占用的靓号,并恢复该用户默认短号。
|
||||
func (s *Service) RecyclePrettyDisplayID(ctx context.Context, command userdomain.PrettyDisplayIDRecycleCommand) (userdomain.PrettyDisplayID, error) {
|
||||
if s.identityRepository == nil {
|
||||
return userdomain.PrettyDisplayID{}, xerr.New(xerr.Unavailable, "identity repository is not configured")
|
||||
}
|
||||
// 回收是后台写操作,只接受 pretty_id 定位当前占用记录,用户、lease 和来源都在 repository 事务内重新读取校验。
|
||||
command.AppCode = appcode.FromContext(ctx)
|
||||
command.PrettyID = strings.TrimSpace(command.PrettyID)
|
||||
command.Reason = strings.TrimSpace(command.Reason)
|
||||
command.NowMs = s.now().UnixMilli()
|
||||
if command.PrettyID == "" || command.OperatorAdminID <= 0 {
|
||||
return userdomain.PrettyDisplayID{}, xerr.New(xerr.InvalidArgument, "pretty display id recycle argument is invalid")
|
||||
}
|
||||
return s.identityRepository.RecyclePrettyDisplayID(ctx, command)
|
||||
}
|
||||
|
||||
// AdminGrantPrettyDisplayID 后台直接发放靓号。
|
||||
func (s *Service) AdminGrantPrettyDisplayID(ctx context.Context, command userdomain.AdminGrantPrettyDisplayIDCommand) (userdomain.Identity, string, error) {
|
||||
if s.identityRepository == nil {
|
||||
|
||||
@ -137,6 +137,8 @@ type IdentityRepository interface {
|
||||
ListPrettyDisplayIDs(ctx context.Context, query userdomain.PrettyDisplayIDQuery) ([]userdomain.PrettyDisplayID, int64, error)
|
||||
// SetPrettyDisplayIDStatus 切换未占用靓号的状态。
|
||||
SetPrettyDisplayIDStatus(ctx context.Context, command userdomain.PrettyDisplayIDStatusCommand) (userdomain.PrettyDisplayID, error)
|
||||
// RecyclePrettyDisplayID 后台强制回收用户当前占用的靓号。
|
||||
RecyclePrettyDisplayID(ctx context.Context, command userdomain.PrettyDisplayIDRecycleCommand) (userdomain.PrettyDisplayID, error)
|
||||
// AdminGrantPrettyDisplayID 后台直接发放靓号并覆盖当前展示号。
|
||||
AdminGrantPrettyDisplayID(ctx context.Context, command userdomain.AdminGrantPrettyDisplayIDCommand) (userdomain.Identity, string, error)
|
||||
// ExpirePrettyDisplayUserID 原子恢复默认短号。
|
||||
|
||||
@ -29,6 +29,23 @@ type prettyAssignment struct {
|
||||
NowMs int64
|
||||
}
|
||||
|
||||
type prettyReleaseCommand struct {
|
||||
// LeaseID 为空时按用户当前 active lease 释放;非空时必须匹配当前 active lease,防止旧请求误释放新靓号。
|
||||
LeaseID string
|
||||
// Reason 写入 lease、展示号、靓号池记录和变更日志,用于区分到期、替换和后台回收。
|
||||
Reason string
|
||||
// ChangeType 是 display_user_id_change_logs 的业务类型。
|
||||
ChangeType string
|
||||
// LeaseStatus 表示租约释放后的生命周期状态。
|
||||
LeaseStatus userdomain.PrettyLeaseStatus
|
||||
// DisplayStatus 表示当前展示号占用释放后的状态,到期用 expired,后台回收用 released。
|
||||
DisplayStatus userdomain.DisplayUserIDStatus
|
||||
// OperatorAdminID 只有后台回收这类管理动作存在;到期任务为 0。
|
||||
OperatorAdminID int64
|
||||
RequestID string
|
||||
NowMs int64
|
||||
}
|
||||
|
||||
// ApplyPrettyDisplayUserID 兼容旧付费靓号接口,仍要求租期和支付回执,但允许覆盖旧靓号。
|
||||
func (r *Repository) ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) {
|
||||
// 旧接口按展示号申请,没有 pretty_id;先懒过期同名到期靓号,避免已过期记录继续占用唯一性。
|
||||
@ -289,20 +306,99 @@ func (r *Repository) expirePrettyInTx(ctx context.Context, tx *sql.Tx, user user
|
||||
return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not expired")
|
||||
}
|
||||
|
||||
// 锁 active lease 后再释放,lease_id 传入时作为精确校验,避免旧定时任务误释放后来替换的新租约。
|
||||
activeLeaseID, _, err := lockActivePrettyLease(ctx, tx, appcode.FromContext(ctx), user.UserID)
|
||||
return r.releaseCurrentPrettyInTx(ctx, tx, user, prettyReleaseCommand{
|
||||
LeaseID: leaseID,
|
||||
Reason: "pretty_expire",
|
||||
ChangeType: "pretty_expire",
|
||||
LeaseStatus: userdomain.PrettyLeaseStatusExpired,
|
||||
DisplayStatus: userdomain.DisplayUserIDStatusExpired,
|
||||
RequestID: requestID,
|
||||
NowMs: nowMs,
|
||||
})
|
||||
}
|
||||
|
||||
// RecyclePrettyDisplayID 强制回收后台列表中仍处于 assigned 的靓号,并把用户恢复到默认短号。
|
||||
func (r *Repository) RecyclePrettyDisplayID(ctx context.Context, command userdomain.PrettyDisplayIDRecycleCommand) (userdomain.PrettyDisplayID, error) {
|
||||
appCode := appcode.Normalize(command.AppCode)
|
||||
reason := strings.TrimSpace(command.Reason)
|
||||
if reason == "" {
|
||||
// 默认原因必须和到期恢复区分,方便审计和后续排查运营手动动作。
|
||||
reason = "admin_recycle_pretty"
|
||||
}
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return userdomain.PrettyDisplayID{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
item, err := queryPrettyDisplayIDForUpdate(ctx, tx, appCode, strings.TrimSpace(command.PrettyID))
|
||||
if err == sql.ErrNoRows {
|
||||
return userdomain.PrettyDisplayID{}, xerr.New(xerr.NotFound, "pretty display id not found")
|
||||
}
|
||||
if err != nil {
|
||||
return userdomain.PrettyDisplayID{}, err
|
||||
}
|
||||
if item.Status != userdomain.PrettyDisplayIDStatusAssigned || item.AssignedUserID <= 0 || item.AssignedLeaseID == "" {
|
||||
// 只有真实占用中的号码才能回收;可用、禁用、预留和已释放状态仍走普通状态管理。
|
||||
return userdomain.PrettyDisplayID{}, xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display id is not assigned")
|
||||
}
|
||||
|
||||
user, err := userstorage.QueryUser(ctx, tx, "WHERE user_id = ? FOR UPDATE", item.AssignedUserID)
|
||||
if err == sql.ErrNoRows {
|
||||
return userdomain.PrettyDisplayID{}, xerr.New(xerr.NotFound, "assigned user not found")
|
||||
}
|
||||
if err != nil {
|
||||
return userdomain.PrettyDisplayID{}, err
|
||||
}
|
||||
if user.CurrentDisplayUserIDKind != userdomain.DisplayUserIDKindPretty || user.CurrentDisplayUserID != item.DisplayUserID {
|
||||
// 列表记录和 users 当前展示号不一致时不能盲目清表,交给后续数据修复处理,避免回收错用户的新靓号。
|
||||
return userdomain.PrettyDisplayID{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display id lease is not active")
|
||||
}
|
||||
|
||||
if _, err := r.releaseCurrentPrettyInTx(ctx, tx, user, prettyReleaseCommand{
|
||||
LeaseID: item.AssignedLeaseID,
|
||||
Reason: reason,
|
||||
ChangeType: "admin_recycle_pretty",
|
||||
LeaseStatus: userdomain.PrettyLeaseStatusCancelled,
|
||||
DisplayStatus: userdomain.DisplayUserIDStatusReleased,
|
||||
OperatorAdminID: command.OperatorAdminID,
|
||||
RequestID: command.RequestID,
|
||||
NowMs: command.NowMs,
|
||||
}); err != nil {
|
||||
return userdomain.PrettyDisplayID{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return userdomain.PrettyDisplayID{}, err
|
||||
}
|
||||
|
||||
return queryPrettyDisplayID(ctx, r.db, appCode, item.PrettyID)
|
||||
}
|
||||
|
||||
func (r *Repository) releaseCurrentPrettyInTx(ctx context.Context, tx *sql.Tx, user userdomain.User, command prettyReleaseCommand) (userdomain.Identity, error) {
|
||||
appCode := appcode.FromContext(ctx)
|
||||
// 先锁 active lease,再按调用方传入的 lease_id 做精确校验;后台回收必须命中列表行里的 lease,过期任务可以空 lease 幂等恢复。
|
||||
activeLeaseID, _, err := lockActivePrettyLease(ctx, tx, appCode, user.UserID)
|
||||
if err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
if leaseID != "" && activeLeaseID != "" && leaseID != activeLeaseID {
|
||||
if command.LeaseID != "" && activeLeaseID != command.LeaseID {
|
||||
return userdomain.Identity{}, xerr.New(xerr.DisplayUserIDLeaseExpired, "pretty display_user_id lease is not active")
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE pretty_display_user_id_leases
|
||||
SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ? AND status = ?
|
||||
`, string(userdomain.PrettyLeaseStatusExpired), nowMs, "pretty_expire", nowMs, appcode.FromContext(ctx), user.UserID, string(userdomain.PrettyLeaseStatusActive))
|
||||
if command.LeaseID != "" {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE pretty_display_user_id_leases
|
||||
SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND lease_id = ? AND status = ?
|
||||
`, string(command.LeaseStatus), command.NowMs, command.Reason, command.NowMs, appCode, command.LeaseID, string(userdomain.PrettyLeaseStatusActive))
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE pretty_display_user_id_leases
|
||||
SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ? AND status = ?
|
||||
`, string(command.LeaseStatus), command.NowMs, command.Reason, command.NowMs, appCode, user.UserID, string(userdomain.PrettyLeaseStatusActive))
|
||||
}
|
||||
if err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
@ -311,23 +407,23 @@ func (r *Repository) expirePrettyInTx(ctx context.Context, tx *sql.Tx, user user
|
||||
UPDATE user_display_user_ids
|
||||
SET status = ?, released_at_ms = ?, release_reason = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ?
|
||||
`, string(userdomain.DisplayUserIDStatusExpired), nowMs, "pretty_expire", nowMs, appcode.FromContext(ctx), user.UserID, user.CurrentDisplayUserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive))
|
||||
`, string(command.DisplayStatus), command.NowMs, command.Reason, command.NowMs, appCode, user.UserID, user.CurrentDisplayUserID, string(userdomain.DisplayUserIDKindPretty), string(userdomain.DisplayUserIDStatusActive))
|
||||
if err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
if activeLeaseID != "" {
|
||||
// 池号和后台发放号释放后的状态不同,统一通过 lease 反查池表行处理。
|
||||
if err := releasePrettyDisplayIDByLeaseInTx(ctx, tx, appcode.FromContext(ctx), activeLeaseID, nowMs, "pretty_expire"); err != nil {
|
||||
if err := releasePrettyDisplayIDByLeaseInTx(ctx, tx, appCode, activeLeaseID, command.NowMs, command.Reason); err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// 默认短号在用户使用靓号期间保持 held;过期恢复只把这条默认号重新激活,不重新生成短号。
|
||||
// 默认短号在用户使用靓号期间保持 held;释放时只把原默认号重新激活,不重新生成短号。
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE user_display_user_ids
|
||||
SET status = ?, activated_at_ms = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ? AND display_user_id = ? AND display_user_id_kind = ? AND status = ?
|
||||
`, string(userdomain.DisplayUserIDStatusActive), nowMs, nowMs, appcode.FromContext(ctx), user.UserID, user.DefaultDisplayUserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusHeld))
|
||||
`, string(userdomain.DisplayUserIDStatusActive), command.NowMs, command.NowMs, appCode, user.UserID, user.DefaultDisplayUserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusHeld))
|
||||
if err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
@ -336,29 +432,30 @@ func (r *Repository) expirePrettyInTx(ctx context.Context, tx *sql.Tx, user user
|
||||
UPDATE users
|
||||
SET current_display_user_id = default_display_user_id, current_display_user_id_kind = ?, current_display_user_id_expires_at_ms = NULL, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, string(userdomain.DisplayUserIDKindDefault), nowMs, appcode.FromContext(ctx), user.UserID)
|
||||
`, string(userdomain.DisplayUserIDKindDefault), command.NowMs, appCode, user.UserID)
|
||||
if err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
|
||||
if err := insertDisplayUserIDChangeLog(ctx, tx, displayUserIDLog{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
AppCode: appCode,
|
||||
UserID: user.UserID,
|
||||
OldID: user.CurrentDisplayUserID,
|
||||
NewID: user.DefaultDisplayUserID,
|
||||
OldKind: userdomain.DisplayUserIDKindPretty,
|
||||
NewKind: userdomain.DisplayUserIDKindDefault,
|
||||
ChangeType: "pretty_expire",
|
||||
ChangeType: command.ChangeType,
|
||||
LeaseID: activeLeaseID,
|
||||
Reason: "pretty_expire",
|
||||
RequestID: requestID,
|
||||
CreatedAtMs: nowMs,
|
||||
Reason: command.Reason,
|
||||
OperatorID: command.OperatorAdminID,
|
||||
RequestID: command.RequestID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
}); err != nil {
|
||||
return userdomain.Identity{}, err
|
||||
}
|
||||
|
||||
return userdomain.Identity{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
AppCode: appCode,
|
||||
UserID: user.UserID,
|
||||
DisplayUserID: user.DefaultDisplayUserID,
|
||||
DefaultDisplayUserID: user.DefaultDisplayUserID,
|
||||
|
||||
@ -249,6 +249,11 @@ func (r *Repository) SetPrettyDisplayIDStatus(ctx context.Context, command userd
|
||||
return r.Repository.IdentityRepository().SetPrettyDisplayIDStatus(ctx, command)
|
||||
}
|
||||
|
||||
// RecyclePrettyDisplayID 让测试 wrapper 继续满足后台靓号回收接口。
|
||||
func (r *Repository) RecyclePrettyDisplayID(ctx context.Context, command userdomain.PrettyDisplayIDRecycleCommand) (userdomain.PrettyDisplayID, error) {
|
||||
return r.Repository.IdentityRepository().RecyclePrettyDisplayID(ctx, command)
|
||||
}
|
||||
|
||||
// AdminGrantPrettyDisplayID 让测试 wrapper 继续满足后台发放靓号接口。
|
||||
func (r *Repository) AdminGrantPrettyDisplayID(ctx context.Context, command userdomain.AdminGrantPrettyDisplayIDCommand) (userdomain.Identity, string, error) {
|
||||
return r.Repository.IdentityRepository().AdminGrantPrettyDisplayID(ctx, command)
|
||||
|
||||
@ -1148,6 +1148,22 @@ func (s *Server) SetPrettyDisplayIDStatus(ctx context.Context, req *userv1.SetPr
|
||||
return &userv1.PrettyDisplayIDResponse{Item: toProtoPrettyDisplayID(item)}, nil
|
||||
}
|
||||
|
||||
// RecyclePrettyDisplayID 后台回收用户当前占用靓号并恢复默认短号。
|
||||
func (s *Server) RecyclePrettyDisplayID(ctx context.Context, req *userv1.RecyclePrettyDisplayIDRequest) (*userv1.PrettyDisplayIDResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
// 回收允许处理长期靓号,不能复用 ExpirePrettyDisplayUserID 的到期校验;具体 lease 一致性由 user-service 事务完成。
|
||||
item, err := s.userSvc.RecyclePrettyDisplayID(ctx, userdomain.PrettyDisplayIDRecycleCommand{
|
||||
PrettyID: req.GetPrettyId(),
|
||||
Reason: req.GetReason(),
|
||||
OperatorAdminID: req.GetOperatorAdminId(),
|
||||
RequestID: req.GetMeta().GetRequestId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &userv1.PrettyDisplayIDResponse{Item: toProtoPrettyDisplayID(item)}, nil
|
||||
}
|
||||
|
||||
// AdminGrantPrettyDisplayID 后台直接给用户发放自由靓号。
|
||||
func (s *Server) AdminGrantPrettyDisplayID(ctx context.Context, req *userv1.AdminGrantPrettyDisplayIDRequest) (*userv1.AdminGrantPrettyDisplayIDResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user