feat: add role invitation confirmation flow
This commit is contained in:
parent
4b5c2931ea
commit
b810bae232
File diff suppressed because it is too large
Load Diff
@ -222,6 +222,17 @@ message InviteBDResponse {
|
||||
RoleInvitation invitation = 1;
|
||||
}
|
||||
|
||||
message InviteHostRequest {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
int64 inviter_user_id = 3;
|
||||
int64 target_user_id = 4;
|
||||
}
|
||||
|
||||
message InviteHostResponse {
|
||||
RoleInvitation invitation = 1;
|
||||
}
|
||||
|
||||
message ListBDLeaderBDsRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 leader_user_id = 2;
|
||||
@ -255,6 +266,17 @@ message ListBDAgenciesResponse {
|
||||
repeated Agency agencies = 1;
|
||||
}
|
||||
|
||||
message ListRoleInvitationsRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 target_user_id = 2;
|
||||
string status = 3;
|
||||
int32 page_size = 4;
|
||||
}
|
||||
|
||||
message ListRoleInvitationsResponse {
|
||||
repeated RoleInvitation invitations = 1;
|
||||
}
|
||||
|
||||
message ProcessRoleInvitationRequest {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
@ -486,9 +508,11 @@ service UserHostService {
|
||||
rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse);
|
||||
rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse);
|
||||
rpc InviteBD(InviteBDRequest) returns (InviteBDResponse);
|
||||
rpc InviteHost(InviteHostRequest) returns (InviteHostResponse);
|
||||
rpc ListBDLeaderBDs(ListBDLeaderBDsRequest) returns (ListBDLeaderBDsResponse);
|
||||
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
|
||||
rpc ListBDAgencies(ListBDAgenciesRequest) returns (ListBDAgenciesResponse);
|
||||
rpc ListRoleInvitations(ListRoleInvitationsRequest) returns (ListRoleInvitationsResponse);
|
||||
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
|
||||
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
|
||||
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);
|
||||
|
||||
@ -25,9 +25,11 @@ const (
|
||||
UserHostService_KickAgencyHost_FullMethodName = "/hyapp.user.v1.UserHostService/KickAgencyHost"
|
||||
UserHostService_InviteAgency_FullMethodName = "/hyapp.user.v1.UserHostService/InviteAgency"
|
||||
UserHostService_InviteBD_FullMethodName = "/hyapp.user.v1.UserHostService/InviteBD"
|
||||
UserHostService_InviteHost_FullMethodName = "/hyapp.user.v1.UserHostService/InviteHost"
|
||||
UserHostService_ListBDLeaderBDs_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderBDs"
|
||||
UserHostService_ListBDLeaderAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderAgencies"
|
||||
UserHostService_ListBDAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDAgencies"
|
||||
UserHostService_ListRoleInvitations_FullMethodName = "/hyapp.user.v1.UserHostService/ListRoleInvitations"
|
||||
UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation"
|
||||
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
|
||||
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
|
||||
@ -52,9 +54,11 @@ type UserHostServiceClient interface {
|
||||
KickAgencyHost(ctx context.Context, in *KickAgencyHostRequest, opts ...grpc.CallOption) (*KickAgencyHostResponse, error)
|
||||
InviteAgency(ctx context.Context, in *InviteAgencyRequest, opts ...grpc.CallOption) (*InviteAgencyResponse, error)
|
||||
InviteBD(ctx context.Context, in *InviteBDRequest, opts ...grpc.CallOption) (*InviteBDResponse, error)
|
||||
InviteHost(ctx context.Context, in *InviteHostRequest, opts ...grpc.CallOption) (*InviteHostResponse, error)
|
||||
ListBDLeaderBDs(ctx context.Context, in *ListBDLeaderBDsRequest, opts ...grpc.CallOption) (*ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(ctx context.Context, in *ListBDAgenciesRequest, opts ...grpc.CallOption) (*ListBDAgenciesResponse, error)
|
||||
ListRoleInvitations(ctx context.Context, in *ListRoleInvitationsRequest, opts ...grpc.CallOption) (*ListRoleInvitationsResponse, error)
|
||||
ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error)
|
||||
GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error)
|
||||
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error)
|
||||
@ -135,6 +139,16 @@ func (c *userHostServiceClient) InviteBD(ctx context.Context, in *InviteBDReques
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) InviteHost(ctx context.Context, in *InviteHostRequest, opts ...grpc.CallOption) (*InviteHostResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(InviteHostResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_InviteHost_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListBDLeaderBDs(ctx context.Context, in *ListBDLeaderBDsRequest, opts ...grpc.CallOption) (*ListBDLeaderBDsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListBDLeaderBDsResponse)
|
||||
@ -165,6 +179,16 @@ func (c *userHostServiceClient) ListBDAgencies(ctx context.Context, in *ListBDAg
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListRoleInvitations(ctx context.Context, in *ListRoleInvitationsRequest, opts ...grpc.CallOption) (*ListRoleInvitationsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListRoleInvitationsResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_ListRoleInvitations_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ProcessRoleInvitationResponse)
|
||||
@ -277,9 +301,11 @@ type UserHostServiceServer interface {
|
||||
KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error)
|
||||
InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error)
|
||||
InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error)
|
||||
InviteHost(context.Context, *InviteHostRequest) (*InviteHostResponse, error)
|
||||
ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error)
|
||||
ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error)
|
||||
ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error)
|
||||
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error)
|
||||
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
|
||||
@ -318,6 +344,9 @@ func (UnimplementedUserHostServiceServer) InviteAgency(context.Context, *InviteA
|
||||
func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) InviteHost(context.Context, *InviteHostRequest) (*InviteHostResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method InviteHost not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
|
||||
}
|
||||
@ -327,6 +356,9 @@ func (UnimplementedUserHostServiceServer) ListBDLeaderAgencies(context.Context,
|
||||
func (UnimplementedUserHostServiceServer) ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoleInvitations not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
|
||||
}
|
||||
@ -486,6 +518,24 @@ func _UserHostService_InviteBD_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_InviteHost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InviteHostRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).InviteHost(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_InviteHost_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).InviteHost(ctx, req.(*InviteHostRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListBDLeaderBDs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListBDLeaderBDsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -540,6 +590,24 @@ func _UserHostService_ListBDAgencies_Handler(srv interface{}, ctx context.Contex
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListRoleInvitations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListRoleInvitationsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).ListRoleInvitations(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_ListRoleInvitations_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).ListRoleInvitations(ctx, req.(*ListRoleInvitationsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ProcessRoleInvitation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ProcessRoleInvitationRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -751,6 +819,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "InviteBD",
|
||||
Handler: _UserHostService_InviteBD_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InviteHost",
|
||||
Handler: _UserHostService_InviteHost_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListBDLeaderBDs",
|
||||
Handler: _UserHostService_ListBDLeaderBDs_Handler,
|
||||
@ -763,6 +835,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListBDAgencies",
|
||||
Handler: _UserHostService_ListBDAgencies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRoleInvitations",
|
||||
Handler: _UserHostService_ListRoleInvitations_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ProcessRoleInvitation",
|
||||
Handler: _UserHostService_ProcessRoleInvitation_Handler,
|
||||
|
||||
@ -94,9 +94,12 @@ type UserHostClient interface {
|
||||
ApplyToAgency(ctx context.Context, req *userv1.ApplyToAgencyRequest) (*userv1.ApplyToAgencyResponse, error)
|
||||
InviteAgency(ctx context.Context, req *userv1.InviteAgencyRequest) (*userv1.InviteAgencyResponse, error)
|
||||
InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*userv1.InviteBDResponse, error)
|
||||
InviteHost(ctx context.Context, req *userv1.InviteHostRequest) (*userv1.InviteHostResponse, error)
|
||||
ListBDLeaderBDs(ctx context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(ctx context.Context, req *userv1.ListBDLeaderAgenciesRequest) (*userv1.ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(ctx context.Context, req *userv1.ListBDAgenciesRequest) (*userv1.ListBDAgenciesResponse, error)
|
||||
ListRoleInvitations(ctx context.Context, req *userv1.ListRoleInvitationsRequest) (*userv1.ListRoleInvitationsResponse, error)
|
||||
ProcessRoleInvitation(ctx context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error)
|
||||
GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error)
|
||||
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error)
|
||||
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
|
||||
@ -433,6 +436,10 @@ func (c *grpcUserHostClient) InviteBD(ctx context.Context, req *userv1.InviteBDR
|
||||
return c.client.InviteBD(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) InviteHost(ctx context.Context, req *userv1.InviteHostRequest) (*userv1.InviteHostResponse, error) {
|
||||
return c.client.InviteHost(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) ListBDLeaderBDs(ctx context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error) {
|
||||
return c.client.ListBDLeaderBDs(ctx, req)
|
||||
}
|
||||
@ -445,6 +452,14 @@ func (c *grpcUserHostClient) ListBDAgencies(ctx context.Context, req *userv1.Lis
|
||||
return c.client.ListBDAgencies(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) ListRoleInvitations(ctx context.Context, req *userv1.ListRoleInvitationsRequest) (*userv1.ListRoleInvitationsResponse, error) {
|
||||
return c.client.ListRoleInvitations(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) ProcessRoleInvitation(ctx context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error) {
|
||||
return c.client.ProcessRoleInvitation(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
||||
return c.client.GetHostProfile(ctx, req)
|
||||
}
|
||||
|
||||
@ -84,6 +84,7 @@ type UserHandlers struct {
|
||||
ListAgencyCenterApplications http.HandlerFunc
|
||||
ReviewAgencyCenterApplication http.HandlerFunc
|
||||
RemoveAgencyCenterHost http.HandlerFunc
|
||||
InviteAgencyCenterHost http.HandlerFunc
|
||||
GetBDLeaderCenterOverview http.HandlerFunc
|
||||
ListBDLeaderCenterBDs http.HandlerFunc
|
||||
ListBDLeaderCenterAgencies http.HandlerFunc
|
||||
@ -92,6 +93,8 @@ type UserHandlers struct {
|
||||
GetBDCenterOverview http.HandlerFunc
|
||||
ListBDCenterAgencies http.HandlerFunc
|
||||
InviteBDAgency http.HandlerFunc
|
||||
ListMyRoleInvitations http.HandlerFunc
|
||||
ProcessMyRoleInvitation http.HandlerFunc
|
||||
CompleteMyOnboarding http.HandlerFunc
|
||||
UpdateMyProfile http.HandlerFunc
|
||||
ChangeMyCountry http.HandlerFunc
|
||||
@ -397,6 +400,7 @@ func (r routes) registerUserRoutes() {
|
||||
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
|
||||
r.profile("/agency-center/applications/{application_id}/review", http.MethodPost, h.ReviewAgencyCenterApplication)
|
||||
r.profile("/agency-center/hosts/{host_user_id}/remove", http.MethodPost, h.RemoveAgencyCenterHost)
|
||||
r.profile("/agency-center/invitations/host", http.MethodPost, h.InviteAgencyCenterHost)
|
||||
r.profile("/bd-leader-center/overview", http.MethodGet, h.GetBDLeaderCenterOverview)
|
||||
r.profile("/bd-leader-center/bds", http.MethodGet, h.ListBDLeaderCenterBDs)
|
||||
r.profile("/bd-leader-center/agencies", http.MethodGet, h.ListBDLeaderCenterAgencies)
|
||||
@ -405,6 +409,8 @@ func (r routes) registerUserRoutes() {
|
||||
r.profile("/bd-center/overview", http.MethodGet, h.GetBDCenterOverview)
|
||||
r.profile("/bd-center/agencies", http.MethodGet, h.ListBDCenterAgencies)
|
||||
r.profile("/bd/invitations/agency", http.MethodPost, h.InviteBDAgency)
|
||||
r.profile("/role-invitations", http.MethodGet, h.ListMyRoleInvitations)
|
||||
r.profile("/role-invitations/{invitation_id}/process", http.MethodPost, h.ProcessMyRoleInvitation)
|
||||
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
||||
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
||||
|
||||
@ -415,6 +415,9 @@ type fakeUserHostClient struct {
|
||||
lastInviteBD *userv1.InviteBDRequest
|
||||
inviteBDResp *userv1.InviteBDResponse
|
||||
inviteBDErr error
|
||||
lastInviteHost *userv1.InviteHostRequest
|
||||
inviteHostResp *userv1.InviteHostResponse
|
||||
inviteHostErr error
|
||||
lastLeaderBDs *userv1.ListBDLeaderBDsRequest
|
||||
leaderBDsResp *userv1.ListBDLeaderBDsResponse
|
||||
leaderBDsErr error
|
||||
@ -424,6 +427,12 @@ type fakeUserHostClient struct {
|
||||
lastBDAgencies *userv1.ListBDAgenciesRequest
|
||||
bdAgenciesResp *userv1.ListBDAgenciesResponse
|
||||
bdAgenciesErr error
|
||||
lastRoleInvitations *userv1.ListRoleInvitationsRequest
|
||||
roleInvitationsResp *userv1.ListRoleInvitationsResponse
|
||||
roleInvitationsErr error
|
||||
lastProcessInvite *userv1.ProcessRoleInvitationRequest
|
||||
processInviteResp *userv1.ProcessRoleInvitationResponse
|
||||
processInviteErr error
|
||||
last *userv1.GetCoinSellerProfileRequest
|
||||
profile *userv1.CoinSellerProfile
|
||||
err error
|
||||
@ -1297,6 +1306,17 @@ func (f *fakeUserHostClient) InviteBD(_ context.Context, req *userv1.InviteBDReq
|
||||
return &userv1.InviteBDResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) InviteHost(_ context.Context, req *userv1.InviteHostRequest) (*userv1.InviteHostResponse, error) {
|
||||
f.lastInviteHost = req
|
||||
if f.inviteHostErr != nil {
|
||||
return nil, f.inviteHostErr
|
||||
}
|
||||
if f.inviteHostResp != nil {
|
||||
return f.inviteHostResp, nil
|
||||
}
|
||||
return &userv1.InviteHostResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) ListBDLeaderBDs(_ context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error) {
|
||||
f.lastLeaderBDs = req
|
||||
if f.leaderBDsErr != nil {
|
||||
@ -1330,6 +1350,28 @@ func (f *fakeUserHostClient) ListBDAgencies(_ context.Context, req *userv1.ListB
|
||||
return &userv1.ListBDAgenciesResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) ListRoleInvitations(_ context.Context, req *userv1.ListRoleInvitationsRequest) (*userv1.ListRoleInvitationsResponse, error) {
|
||||
f.lastRoleInvitations = req
|
||||
if f.roleInvitationsErr != nil {
|
||||
return nil, f.roleInvitationsErr
|
||||
}
|
||||
if f.roleInvitationsResp != nil {
|
||||
return f.roleInvitationsResp, nil
|
||||
}
|
||||
return &userv1.ListRoleInvitationsResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) ProcessRoleInvitation(_ context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error) {
|
||||
f.lastProcessInvite = req
|
||||
if f.processInviteErr != nil {
|
||||
return nil, f.processInviteErr
|
||||
}
|
||||
if f.processInviteResp != nil {
|
||||
return f.processInviteResp, nil
|
||||
}
|
||||
return &userv1.ProcessRoleInvitationResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
||||
f.lastHost = req
|
||||
if f.hostErr != nil {
|
||||
@ -4392,14 +4434,14 @@ func TestBDCenterOverviewUsesBDSalaryAndDirectAgencies(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBDCenterInviteAgencyDirectlyForwardsInviteAgency(t *testing.T) {
|
||||
func TestBDCenterInviteAgencyCreatesPendingInvitation(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
bdProfile: &userv1.BDProfile{UserId: 42, Role: "bd", Status: "active", RegionId: 1001},
|
||||
inviteAgencyResp: &userv1.InviteAgencyResponse{Invitation: &userv1.RoleInvitation{
|
||||
InvitationId: 9001,
|
||||
CommandId: "cmd-bd-invite-agency",
|
||||
InvitationType: "agency",
|
||||
Status: "accepted",
|
||||
Status: "pending",
|
||||
InviterUserId: 42,
|
||||
TargetUserId: 99,
|
||||
AgencyName: "Target Agency",
|
||||
@ -4429,7 +4471,7 @@ func TestBDCenterInviteAgencyDirectlyForwardsInviteAgency(t *testing.T) {
|
||||
}
|
||||
data := response.Data.(map[string]any)
|
||||
invitation := data["invitation"].(map[string]any)
|
||||
if invitation["status"] != "accepted" || invitation["target_user_id"] != "99" {
|
||||
if invitation["status"] != "pending" || invitation["target_user_id"] != "99" {
|
||||
t.Fatalf("bd invite response mismatch: %+v", data)
|
||||
}
|
||||
}
|
||||
@ -4441,7 +4483,7 @@ func TestBDLeaderInviteBDForwardsSelfInvite(t *testing.T) {
|
||||
InvitationId: 9002,
|
||||
CommandId: "cmd-bd-self",
|
||||
InvitationType: "bd",
|
||||
Status: "accepted",
|
||||
Status: "pending",
|
||||
InviterUserId: 42,
|
||||
InviterBdUserId: 42,
|
||||
TargetUserId: 42,
|
||||
@ -4470,11 +4512,103 @@ func TestBDLeaderInviteBDForwardsSelfInvite(t *testing.T) {
|
||||
}
|
||||
data := response.Data.(map[string]any)
|
||||
invitation := data["invitation"].(map[string]any)
|
||||
if invitation["status"] != "accepted" || invitation["target_user_id"] != "42" || invitation["parent_leader_user_id"] != "42" {
|
||||
if invitation["status"] != "pending" || invitation["target_user_id"] != "42" || invitation["parent_leader_user_id"] != "42" {
|
||||
t.Fatalf("bd self invite response mismatch: %+v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterInviteHostCreatesPendingInvitation(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
|
||||
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{AgencyId: 7001, OwnerUserId: 42, Status: "active"}},
|
||||
inviteHostResp: &userv1.InviteHostResponse{Invitation: &userv1.RoleInvitation{
|
||||
InvitationId: 9010,
|
||||
CommandId: "cmd-host-invite",
|
||||
InvitationType: "host",
|
||||
Status: "pending",
|
||||
InviterUserId: 42,
|
||||
TargetUserId: 100,
|
||||
AgencyName: "Voice Agency",
|
||||
}},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetUserHostClient(hostClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/agency-center/invitations/host", bytes.NewReader([]byte(`{"command_id":"cmd-host-invite","target_user_id":"100"}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
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 hostClient.lastInviteHost == nil || hostClient.lastInviteHost.GetInviterUserId() != 42 || hostClient.lastInviteHost.GetTargetUserId() != 100 {
|
||||
t.Fatalf("host invite request mismatch: %+v", hostClient.lastInviteHost)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
invitation := response.Data.(map[string]any)["invitation"].(map[string]any)
|
||||
if invitation["status"] != "pending" || invitation["invitation_type"] != "host" {
|
||||
t.Fatalf("host invite response mismatch: %+v", response.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleInvitationsListAndProcessForwardCurrentUser(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
roleInvitationsResp: &userv1.ListRoleInvitationsResponse{Invitations: []*userv1.RoleInvitation{{
|
||||
InvitationId: 9011,
|
||||
InvitationType: "agency",
|
||||
Status: "pending",
|
||||
InviterUserId: 77,
|
||||
TargetUserId: 42,
|
||||
AgencyName: "Target Agency",
|
||||
CreatedAtMs: 1700000000000,
|
||||
}}},
|
||||
processInviteResp: &userv1.ProcessRoleInvitationResponse{
|
||||
Invitation: &userv1.RoleInvitation{
|
||||
InvitationId: 9011,
|
||||
InvitationType: "agency",
|
||||
Status: "accepted",
|
||||
InviterUserId: 77,
|
||||
TargetUserId: 42,
|
||||
},
|
||||
Agency: &userv1.Agency{AgencyId: 7008, OwnerUserId: 42, Status: "active", Name: "Target Agency"},
|
||||
Membership: &userv1.AgencyMembership{MembershipId: 3008, AgencyId: 7008, HostUserId: 42, MembershipType: "owner", Status: "active"},
|
||||
},
|
||||
}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
77: {UserId: 77, DisplayUserId: "163077", Username: "Leader Rose"},
|
||||
}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||
handler.SetUserHostClient(hostClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
|
||||
listRequest := httptest.NewRequest(http.MethodGet, "/api/v1/role-invitations?status=pending", nil)
|
||||
listRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
listRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(listRecorder, listRequest)
|
||||
if listRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("list status mismatch: got %d body=%s", listRecorder.Code, listRecorder.Body.String())
|
||||
}
|
||||
if hostClient.lastRoleInvitations == nil || hostClient.lastRoleInvitations.GetTargetUserId() != 42 || hostClient.lastRoleInvitations.GetStatus() != "pending" {
|
||||
t.Fatalf("list invitations request mismatch: %+v", hostClient.lastRoleInvitations)
|
||||
}
|
||||
|
||||
processRequest := httptest.NewRequest(http.MethodPost, "/api/v1/role-invitations/9011/process", bytes.NewReader([]byte(`{"command_id":"cmd-accept-9011","action":"accept"}`)))
|
||||
processRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
processRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(processRecorder, processRequest)
|
||||
if processRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("process status mismatch: got %d body=%s", processRecorder.Code, processRecorder.Body.String())
|
||||
}
|
||||
if hostClient.lastProcessInvite == nil || hostClient.lastProcessInvite.GetActorUserId() != 42 || hostClient.lastProcessInvite.GetInvitationId() != 9011 || hostClient.lastProcessInvite.GetAction() != "accept" {
|
||||
t.Fatalf("process invitation request mismatch: %+v", hostClient.lastProcessInvite)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterOverviewRejectsNonOwner(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: false, AgencyId: 7001}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
@ -4645,6 +4779,7 @@ func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", bytes.NewReader(body))
|
||||
request.Header.Set("X-Request-ID", "req-auth-third")
|
||||
request.Header.Set("X-Forwarded-For", "203.0.113.10, 10.0.0.1")
|
||||
request.Header.Set("X-Real-IP", "129.226.91.24")
|
||||
request.Header.Set("User-Agent", "hyapp-test/1.0")
|
||||
request.Header.Set("CF-IPCountry", "sg")
|
||||
recorder := httptest.NewRecorder()
|
||||
@ -4684,6 +4819,17 @@ func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthClientIPPrefersForwardedPublicIPOverProxyRealIP(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", nil)
|
||||
request.RemoteAddr = "10.0.0.9:54123"
|
||||
request.Header.Set("X-Forwarded-For", "114.8.228.33, 10.0.0.1")
|
||||
request.Header.Set("X-Real-IP", "129.226.91.24")
|
||||
|
||||
if got := clientIP(request); got != "114.8.228.33" {
|
||||
t.Fatalf("clientIP must use the first public X-Forwarded-For address before proxy X-Real-IP, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThirdPartyLoginRateLimitByProviderAndIP(t *testing.T) {
|
||||
userClient := &fakeUserAuthClient{}
|
||||
handler := NewHandler(&fakeRoomClient{}, userClient)
|
||||
|
||||
@ -332,6 +332,37 @@ func (h *Handler) removeAgencyCenterHost(writer http.ResponseWriter, request *ht
|
||||
httpkit.WriteOK(writer, request, map[string]any{"membership": agencyMembershipData(resp.GetMembership())})
|
||||
}
|
||||
|
||||
func (h *Handler) inviteAgencyCenterHost(writer http.ResponseWriter, request *http.Request) {
|
||||
if _, ok := h.resolveCurrentOwnerAgency(writer, request); !ok {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
CommandID string `json:"command_id"`
|
||||
TargetUserID string `json:"target_user_id"`
|
||||
}
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
targetUserID := int64JSONValue(body.TargetUserID)
|
||||
if strings.TrimSpace(body.CommandID) == "" || targetUserID <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
// Agency owner 只负责发送邀请;目标用户接受后 user-service 再原子创建 Host 身份和成员关系。
|
||||
resp, err := h.userHostClient.InviteHost(request.Context(), &userv1.InviteHostRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
CommandId: strings.TrimSpace(body.CommandID),
|
||||
InviterUserId: auth.UserIDFromContext(request.Context()),
|
||||
TargetUserId: targetUserID,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, map[string]any{"invitation": roleInvitationData(resp.GetInvitation())})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveCurrentOwnerAgency(writer http.ResponseWriter, request *http.Request) (*userv1.Agency, bool) {
|
||||
if h.userHostClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
|
||||
@ -372,7 +372,7 @@ func (h *Handler) inviteBDAgency(writer http.ResponseWriter, request *http.Reque
|
||||
target := users[targetUserID]
|
||||
agencyName = firstBDLeaderNonBlank(target.Username, target.DisplayUserID, int64String(targetUserID))
|
||||
}
|
||||
// InviteAgency is already a direct-accept transaction in user-service, so this HTTP endpoint does not create a pending approval step.
|
||||
// InviteAgency 只发送 pending 邀请;Agency 身份必须由目标用户在消息中心接受后才创建。
|
||||
resp, err := h.userHostClient.InviteAgency(request.Context(), &userv1.InviteAgencyRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
CommandId: commandID,
|
||||
@ -457,6 +457,7 @@ func roleInvitationData(invitation *userv1.RoleInvitation) map[string]any {
|
||||
"invitation_type": invitation.GetInvitationType(),
|
||||
"status": invitation.GetStatus(),
|
||||
"inviter_user_id": int64String(invitation.GetInviterUserId()),
|
||||
"inviter_bd_user_id": int64String(invitation.GetInviterBdUserId()),
|
||||
"target_user_id": int64String(invitation.GetTargetUserId()),
|
||||
"region_id": invitation.GetRegionId(),
|
||||
"agency_name": invitation.GetAgencyName(),
|
||||
|
||||
@ -68,6 +68,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||
ListAgencyCenterApplications: h.listAgencyCenterApplications,
|
||||
ReviewAgencyCenterApplication: h.reviewAgencyCenterApplication,
|
||||
RemoveAgencyCenterHost: h.removeAgencyCenterHost,
|
||||
InviteAgencyCenterHost: h.inviteAgencyCenterHost,
|
||||
GetBDLeaderCenterOverview: h.getBDLeaderCenterOverview,
|
||||
ListBDLeaderCenterBDs: h.listBDLeaderCenterBDs,
|
||||
ListBDLeaderCenterAgencies: h.listBDLeaderCenterAgencies,
|
||||
@ -76,6 +77,8 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||
GetBDCenterOverview: h.getBDCenterOverview,
|
||||
ListBDCenterAgencies: h.listBDCenterAgencies,
|
||||
InviteBDAgency: h.inviteBDAgency,
|
||||
ListMyRoleInvitations: h.listMyRoleInvitations,
|
||||
ProcessMyRoleInvitation: h.processMyRoleInvitation,
|
||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||
UpdateMyProfile: h.updateMyProfile,
|
||||
ChangeMyCountry: h.changeMyCountry,
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
package userapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
func (h *Handler) listMyRoleInvitations(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.userHostClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
status := strings.TrimSpace(request.URL.Query().Get("status"))
|
||||
if status == "" {
|
||||
status = "pending"
|
||||
}
|
||||
|
||||
// 收件箱只按当前登录用户读取 target_user_id,前端不能传入任意用户 ID 枚举邀请消息。
|
||||
resp, err := h.userHostClient.ListRoleInvitations(request.Context(), &userv1.ListRoleInvitationsRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
TargetUserId: userID,
|
||||
Status: status,
|
||||
PageSize: int32(queryPageSize(request, 50)),
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make([]int64, 0, len(resp.GetInvitations())*2)
|
||||
for _, invitation := range resp.GetInvitations() {
|
||||
if invitation.GetInviterUserId() > 0 {
|
||||
userIDs = append(userIDs, invitation.GetInviterUserId())
|
||||
}
|
||||
if invitation.GetTargetUserId() > 0 {
|
||||
userIDs = append(userIDs, invitation.GetTargetUserId())
|
||||
}
|
||||
}
|
||||
users, err := h.userProfiles(request, userIDs)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]any, 0, len(resp.GetInvitations()))
|
||||
for _, invitation := range resp.GetInvitations() {
|
||||
items = append(items, map[string]any{
|
||||
"invitation": roleInvitationData(invitation),
|
||||
"inviter": users[invitation.GetInviterUserId()],
|
||||
"target": users[invitation.GetTargetUserId()],
|
||||
})
|
||||
}
|
||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) processMyRoleInvitation(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.userHostClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
invitationID, ok := httpkit.PositiveInt64PathValue(request, "invitation_id")
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid invitation_id")
|
||||
return
|
||||
}
|
||||
var body map[string]any
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
commandID := stringJSONValue(body["command_id"])
|
||||
action := strings.ToLower(strings.TrimSpace(stringJSONValue(body["action"])))
|
||||
if action == "agree" {
|
||||
action = "accept"
|
||||
} else if action == "refuse" {
|
||||
action = "reject"
|
||||
}
|
||||
if commandID == "" || (action != "accept" && action != "reject") {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
// 接受/拒绝入口只使用当前登录用户作为 actor_user_id;user-service 会再次校验他是否是邀请目标。
|
||||
resp, err := h.userHostClient.ProcessRoleInvitation(request.Context(), &userv1.ProcessRoleInvitationRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
CommandId: commandID,
|
||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||
InvitationId: invitationID,
|
||||
Action: action,
|
||||
Reason: strings.TrimSpace(stringJSONValue(body["reason"])),
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, map[string]any{
|
||||
"invitation": roleInvitationData(resp.GetInvitation()),
|
||||
"host_profile": hostProfileData(resp.GetHostProfile()),
|
||||
"agency": hostAgencyFromProto(resp.GetAgency()),
|
||||
"membership": agencyMembershipData(resp.GetMembership()),
|
||||
"bd_profile": bdLeaderProfileFromProto(resp.GetBdProfile()),
|
||||
})
|
||||
}
|
||||
|
||||
func hostProfileData(profile *userv1.HostProfile) map[string]any {
|
||||
if profile == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"user_id": int64String(profile.GetUserId()),
|
||||
"status": profile.GetStatus(),
|
||||
"region_id": profile.GetRegionId(),
|
||||
"current_agency_id": int64String(profile.GetCurrentAgencyId()),
|
||||
"current_membership_id": int64String(profile.GetCurrentMembershipId()),
|
||||
"source": profile.GetSource(),
|
||||
"first_became_host_at_ms": profile.GetFirstBecameHostAtMs(),
|
||||
"created_at_ms": profile.GetCreatedAtMs(),
|
||||
"updated_at_ms": profile.GetUpdatedAtMs(),
|
||||
"current_agency_owner_user_id": int64String(profile.GetCurrentAgencyOwnerUserId()),
|
||||
}
|
||||
}
|
||||
@ -68,6 +68,8 @@ const (
|
||||
InvitationTypeAgency = "agency"
|
||||
// InvitationTypeBD 表示邀请用户成为 BD。
|
||||
InvitationTypeBD = "bd"
|
||||
// InvitationTypeHost 表示 Agency owner 邀请用户成为本 Agency 的 Host 成员。
|
||||
InvitationTypeHost = "host"
|
||||
|
||||
// InvitationStatusPending 表示等待目标用户处理。
|
||||
InvitationStatusPending = "pending"
|
||||
@ -97,6 +99,8 @@ const (
|
||||
CommandTypeInviteAgency = "invite_agency"
|
||||
// CommandTypeInviteBD 记录 BD 邀请命令。
|
||||
CommandTypeInviteBD = "invite_bd"
|
||||
// CommandTypeInviteHost 记录 Agency owner 邀请 Host 加入团队的命令。
|
||||
CommandTypeInviteHost = "invite_host"
|
||||
// CommandTypeProcessRoleInvitation 记录邀请处理命令。
|
||||
CommandTypeProcessRoleInvitation = "process_role_invitation"
|
||||
// CommandTypeCreateBDLeader 记录后台创建 BD Leader 命令。
|
||||
|
||||
@ -102,6 +102,29 @@ type InviteBDCommand struct {
|
||||
NowMs int64
|
||||
}
|
||||
|
||||
// InviteHostInput 是 Agency owner 邀请用户成为 Host 成员的外部输入。
|
||||
type InviteHostInput struct {
|
||||
CommandID string
|
||||
InviterUserID int64
|
||||
TargetUserID int64
|
||||
}
|
||||
|
||||
// InviteHostCommand 是 Agency 邀请 Host 事务所需的完整命令。
|
||||
type InviteHostCommand struct {
|
||||
CommandID string
|
||||
InvitationID int64
|
||||
InviterUserID int64
|
||||
TargetUserID int64
|
||||
NowMs int64
|
||||
}
|
||||
|
||||
// ListRoleInvitationsCommand 描述当前用户收件箱读取条件。
|
||||
type ListRoleInvitationsCommand struct {
|
||||
TargetUserID int64
|
||||
Status string
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ListBDLeaderBDsCommand 描述 BD Leader 查询直属 BD 列表的条件。
|
||||
type ListBDLeaderBDsCommand struct {
|
||||
LeaderUserID int64
|
||||
|
||||
@ -20,6 +20,8 @@ type Repository interface {
|
||||
KickAgencyHost(ctx context.Context, command KickAgencyHostCommand) (hostdomain.KickAgencyHostResult, error)
|
||||
InviteAgency(ctx context.Context, command InviteAgencyCommand) (hostdomain.RoleInvitation, error)
|
||||
InviteBD(ctx context.Context, command InviteBDCommand) (hostdomain.RoleInvitation, error)
|
||||
InviteHost(ctx context.Context, command InviteHostCommand) (hostdomain.RoleInvitation, error)
|
||||
ListRoleInvitations(ctx context.Context, command ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error)
|
||||
ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error)
|
||||
ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error)
|
||||
ListBDAgencies(ctx context.Context, command ListBDAgenciesCommand) ([]hostdomain.Agency, error)
|
||||
@ -200,12 +202,12 @@ func (s *Service) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Age
|
||||
return s.repository.GetAgency(ctx, agencyID)
|
||||
}
|
||||
|
||||
// InviteAgency 由 BD 直接创建用户的 Agency owner 身份,不再等待目标用户确认。
|
||||
// InviteAgency 由 BD 或 BD Leader 创建 Agency owner 邀请;目标用户接受前不创建 Agency 身份。
|
||||
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
|
||||
if err := s.requireWriteDependencies(); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
// Agency 名称在邀请动作里固化;持久化层会在同一事务中写 accepted invitation、Agency、owner Host 和 owner membership。
|
||||
// Agency 名称在邀请动作里固化,接受时按这份快照创建 Agency,避免目标确认后前端再传入不同名称。
|
||||
agencyName := strings.TrimSpace(command.AgencyName)
|
||||
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 || agencyName == "" {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id, target_user_id and agency_name are required")
|
||||
@ -218,19 +220,16 @@ func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (
|
||||
InviterUserID: command.InviterUserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
AgencyName: agencyName,
|
||||
AgencyID: s.idGenerator.NewInt64(),
|
||||
MembershipID: s.idGenerator.NewInt64(),
|
||||
EventID: s.nextEventIDRange(3),
|
||||
NowMs: nowMs,
|
||||
})
|
||||
}
|
||||
|
||||
// InviteBD 由 BD Leader 直接创建用户的 BD 身份,不再等待目标用户确认。
|
||||
// InviteBD 由 BD Leader 创建 BD 邀请;目标用户接受前不创建普通 BD 身份。
|
||||
func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) {
|
||||
if err := s.requireWriteDependencies(); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验,并和 accepted invitation 一起提交。
|
||||
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验,避免发出不可处理的待确认消息。
|
||||
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
|
||||
}
|
||||
@ -241,11 +240,45 @@ func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdoma
|
||||
InvitationID: s.idGenerator.NewInt64(),
|
||||
InviterUserID: command.InviterUserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
EventID: s.idGenerator.NewInt64(),
|
||||
NowMs: nowMs,
|
||||
})
|
||||
}
|
||||
|
||||
// InviteHost 由 active Agency owner 邀请用户加入自己的 Agency,接受前不创建 Host 或成员关系。
|
||||
func (s *Service) InviteHost(ctx context.Context, command InviteHostInput) (hostdomain.RoleInvitation, error) {
|
||||
if err := s.requireWriteDependencies(); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
// command_id 仍是发送动作的幂等边界;目标处理时会使用新的 command_id 推进邀请终态。
|
||||
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
|
||||
}
|
||||
nowMs := s.now().UnixMilli()
|
||||
|
||||
return s.repository.InviteHost(ctx, InviteHostCommand{
|
||||
CommandID: strings.TrimSpace(command.CommandID),
|
||||
InvitationID: s.idGenerator.NewInt64(),
|
||||
InviterUserID: command.InviterUserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
NowMs: nowMs,
|
||||
})
|
||||
}
|
||||
|
||||
// ListRoleInvitations 返回当前用户收到的角色/团队邀请收件箱。
|
||||
func (s *Service) ListRoleInvitations(ctx context.Context, command ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error) {
|
||||
if s.repository == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||||
}
|
||||
if command.TargetUserID <= 0 {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
|
||||
}
|
||||
return s.repository.ListRoleInvitations(ctx, ListRoleInvitationsCommand{
|
||||
TargetUserID: command.TargetUserID,
|
||||
Status: normalizeStatus(command.Status),
|
||||
PageSize: normalizePageSize(command.PageSize),
|
||||
})
|
||||
}
|
||||
|
||||
// ListBDLeaderBDs 返回当前 BD Leader 直属的有效 BD 列表。
|
||||
func (s *Service) ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
|
||||
if s.repository == nil {
|
||||
|
||||
@ -65,7 +65,7 @@ func (r *Repository) InviteAgency(ctx context.Context, command hostservice.Invit
|
||||
InvitationID: command.InvitationID,
|
||||
CommandID: command.CommandID,
|
||||
InvitationType: hostdomain.InvitationTypeAgency,
|
||||
Status: hostdomain.InvitationStatusAccepted,
|
||||
Status: hostdomain.InvitationStatusPending,
|
||||
InviterUserID: command.InviterUserID,
|
||||
InviterBDUserID: inviter.UserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
@ -73,42 +73,13 @@ func (r *Repository) InviteAgency(ctx context.Context, command hostservice.Invit
|
||||
AgencyName: command.AgencyName,
|
||||
ParentBDUserID: inviter.UserID,
|
||||
ParentLeaderUserID: parentLeaderUserID,
|
||||
ProcessedByUserID: command.InviterUserID,
|
||||
ProcessReason: "direct_invite",
|
||||
ProcessedAtMs: command.NowMs,
|
||||
CreatedAtMs: command.NowMs,
|
||||
UpdatedAtMs: command.NowMs,
|
||||
}
|
||||
// 新邀请不再等待目标用户确认:accepted invitation、Agency、owner Host 和 owner membership 必须同事务提交。
|
||||
// 邀请创建只落 pending 消息;Agency、owner Host 和 owner membership 必须等目标用户接受时再同事务创建。
|
||||
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}
|
||||
hostProfileCreated, err := r.acceptAgencyInvitation(ctx, tx, hostservice.ProcessRoleInvitationCommand{
|
||||
CommandID: command.CommandID,
|
||||
ActorUserID: command.InviterUserID,
|
||||
InvitationID: command.InvitationID,
|
||||
Action: hostdomain.InvitationActionAccept,
|
||||
Reason: "direct_invite",
|
||||
AgencyID: command.AgencyID,
|
||||
MembershipID: command.MembershipID,
|
||||
EventID: command.EventID,
|
||||
NowMs: command.NowMs,
|
||||
}, &invitation, &result)
|
||||
if err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if hostProfileCreated {
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
}
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyCreated", "agency", command.AgencyID, command.NowMs); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID+2, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
|
||||
CommandID: command.CommandID,
|
||||
CommandType: hostdomain.CommandTypeInviteAgency,
|
||||
@ -171,47 +142,105 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
|
||||
InvitationID: command.InvitationID,
|
||||
CommandID: command.CommandID,
|
||||
InvitationType: hostdomain.InvitationTypeBD,
|
||||
Status: hostdomain.InvitationStatusAccepted,
|
||||
Status: hostdomain.InvitationStatusPending,
|
||||
InviterUserID: command.InviterUserID,
|
||||
InviterBDUserID: inviter.UserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
RegionID: targetRegionID,
|
||||
ParentLeaderUserID: inviter.UserID,
|
||||
ProcessedByUserID: command.InviterUserID,
|
||||
ProcessReason: "direct_invite",
|
||||
ProcessedAtMs: command.NowMs,
|
||||
CreatedAtMs: command.NowMs,
|
||||
UpdatedAtMs: command.NowMs,
|
||||
}
|
||||
// 新 BD 邀请直接创建 BD 身份;accepted invitation 只作为审计事实和幂等回放结果。
|
||||
// 新 BD 邀请只产生待确认消息;BD 身份要等目标用户接受后才创建。
|
||||
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
recordCommandResult := func() error {
|
||||
return insertCommandResult(ctx, tx, hostdomain.CommandResult{
|
||||
CommandID: command.CommandID,
|
||||
CommandType: hostdomain.CommandTypeInviteBD,
|
||||
ResultType: hostdomain.ResultTypeInvitation,
|
||||
ResultID: invitation.InvitationID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
})
|
||||
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
|
||||
CommandID: command.CommandID,
|
||||
CommandType: hostdomain.CommandTypeInviteBD,
|
||||
ResultType: hostdomain.ResultTypeInvitation,
|
||||
ResultID: invitation.InvitationID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
}); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}
|
||||
if err := r.acceptBDInvitation(ctx, tx, hostservice.ProcessRoleInvitationCommand{
|
||||
CommandID: command.CommandID,
|
||||
ActorUserID: command.InviterUserID,
|
||||
InvitationID: command.InvitationID,
|
||||
Action: hostdomain.InvitationActionAccept,
|
||||
Reason: "direct_invite",
|
||||
EventID: command.EventID,
|
||||
NowMs: command.NowMs,
|
||||
}, &invitation, &result); err != nil {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
|
||||
|
||||
return invitation, nil
|
||||
}
|
||||
|
||||
// InviteHost 创建 Agency owner 到目标用户的 Host 入会邀请。
|
||||
func (r *Repository) InviteHost(ctx context.Context, command hostservice.InviteHostCommand) (hostdomain.RoleInvitation, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if err := recordCommandResult(); err != nil {
|
||||
defer tx.Rollback()
|
||||
|
||||
// 发送邀请是独立写命令;重试只回放同一条 pending/终态邀请,不重新读取 owner 当前权限。
|
||||
if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
} else if ok {
|
||||
if err := requireCommandResult(existing, hostdomain.CommandTypeInviteHost, hostdomain.ResultTypeInvitation); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
|
||||
}
|
||||
|
||||
// Agency owner 是邀请 Host 的唯一授权身份;锁 active agency 行可以让关闭、删除、禁入和邀请创建串行。
|
||||
agency, ok, err := queryActiveAgencyByOwner(ctx, tx, command.InviterUserID, true)
|
||||
if err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if !ok || !agency.JoinEnabled {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter agency is not active or joinable")
|
||||
}
|
||||
// 目标用户可能还没有 host_profile,所以用 users 行作为目标身份变化的串行化锁。
|
||||
targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
|
||||
if err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if targetRegionID != agency.RegionID {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "target region does not match agency region")
|
||||
}
|
||||
// Host 入会邀请不能绕过“一人一个有效 Agency 归属”的硬约束,接受时还会再次校验当前状态。
|
||||
if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.TargetUserID, true); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
} else if ok {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active agency membership")
|
||||
}
|
||||
if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
} else if ok {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already owns active agency")
|
||||
}
|
||||
|
||||
invitation := hostdomain.RoleInvitation{
|
||||
InvitationID: command.InvitationID,
|
||||
CommandID: command.CommandID,
|
||||
InvitationType: hostdomain.InvitationTypeHost,
|
||||
Status: hostdomain.InvitationStatusPending,
|
||||
InviterUserID: command.InviterUserID,
|
||||
InviterBDUserID: agency.ParentBDUserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
RegionID: targetRegionID,
|
||||
AgencyName: agency.Name,
|
||||
ParentBDUserID: agency.ParentBDUserID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
UpdatedAtMs: command.NowMs,
|
||||
}
|
||||
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
|
||||
CommandID: command.CommandID,
|
||||
CommandType: hostdomain.CommandTypeInviteHost,
|
||||
ResultType: hostdomain.ResultTypeInvitation,
|
||||
ResultID: invitation.InvitationID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
}); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
@ -231,7 +260,7 @@ func queryActiveAgencyInviterProfile(ctx context.Context, tx *sql.Tx, userID int
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "inviter bd is not active")
|
||||
}
|
||||
|
||||
// ProcessRoleInvitation 处理角色邀请,接受路径在同一事务内创建 Agency/BD 角色事实。
|
||||
// ProcessRoleInvitation 处理角色邀请,接受路径在同一事务内创建 Host/Agency/BD 角色事实。
|
||||
func (r *Repository) ProcessRoleInvitation(ctx context.Context, command hostservice.ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
@ -309,6 +338,20 @@ func (r *Repository) ProcessRoleInvitation(ctx context.Context, command hostserv
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
|
||||
return hostdomain.ProcessRoleInvitationResult{}, err
|
||||
}
|
||||
} else if invitation.InvitationType == hostdomain.InvitationTypeHost {
|
||||
// Host 邀请只创建或复用 Host 身份,并把当前成员关系挂到发起 Agency。
|
||||
hostProfileCreated, err := r.acceptHostInvitation(ctx, tx, command, &invitation, &result)
|
||||
if err != nil {
|
||||
return hostdomain.ProcessRoleInvitationResult{}, err
|
||||
}
|
||||
if hostProfileCreated {
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
|
||||
return hostdomain.ProcessRoleInvitationResult{}, err
|
||||
}
|
||||
}
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
|
||||
return hostdomain.ProcessRoleInvitationResult{}, err
|
||||
}
|
||||
} else {
|
||||
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "invitation type is invalid")
|
||||
}
|
||||
@ -404,6 +447,59 @@ func (r *Repository) acceptAgencyInvitation(ctx context.Context, tx *sql.Tx, com
|
||||
return hostProfileCreated, nil
|
||||
}
|
||||
|
||||
func (r *Repository) acceptHostInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) (bool, error) {
|
||||
// Host 邀请绑定的是发起人当前 active Agency,而不是客户端传入的 agency_id;
|
||||
// 这样邀请消息无法被目标用户篡改成加入其他团队。
|
||||
agency, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.InviterUserID, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok || !agency.JoinEnabled {
|
||||
return false, xerr.New(xerr.Conflict, "inviter agency is no longer active or joinable")
|
||||
}
|
||||
targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if targetRegionID != agency.RegionID {
|
||||
return false, xerr.New(xerr.PermissionDenied, "target region no longer matches agency")
|
||||
}
|
||||
// 接受时再次检查有效归属和 Agency owner 身份,覆盖邀请发出后用户已经加入或创建团队的情况。
|
||||
if _, ok, err := queryActiveMembershipByHost(ctx, tx, invitation.TargetUserID, true); err != nil {
|
||||
return false, err
|
||||
} else if ok {
|
||||
return false, xerr.New(xerr.Conflict, "target already has active agency membership")
|
||||
}
|
||||
if _, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.TargetUserID, true); err != nil {
|
||||
return false, err
|
||||
} else if ok {
|
||||
return false, xerr.New(xerr.Conflict, "target already owns active agency")
|
||||
}
|
||||
|
||||
hostProfile, hostProfileCreated, err := ensureHostProfileForMembership(ctx, tx, invitation.TargetUserID, targetRegionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceAgencyInvitation, command.NowMs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
membership := hostdomain.AgencyMembership{
|
||||
MembershipID: command.MembershipID,
|
||||
AgencyID: agency.AgencyID,
|
||||
HostUserID: invitation.TargetUserID,
|
||||
RegionID: targetRegionID,
|
||||
MembershipType: hostdomain.MembershipTypeMember,
|
||||
Status: hostdomain.MembershipStatusActive,
|
||||
JoinedAtMs: command.NowMs,
|
||||
CreatedAtMs: command.NowMs,
|
||||
UpdatedAtMs: command.NowMs,
|
||||
}
|
||||
if err := insertAgencyMembership(ctx, tx, membership); err != nil {
|
||||
return false, mapHostDuplicateError(err)
|
||||
}
|
||||
result.HostProfile = hostProfile
|
||||
result.Agency = agency
|
||||
result.Membership = membership
|
||||
return hostProfileCreated, nil
|
||||
}
|
||||
|
||||
func (r *Repository) acceptBDInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) error {
|
||||
// BD 邀请同样以目标用户和发起 Leader 的 users 当前区域为准;旧邀请快照不能继续授权跨区接受。
|
||||
targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
|
||||
|
||||
@ -353,6 +353,20 @@ func (r *Repository) ListBDAgencies(ctx context.Context, command hostservice.Lis
|
||||
return queryAgencies(ctx, r.db, clause, args...)
|
||||
}
|
||||
|
||||
// ListRoleInvitations 读取目标用户收到的角色邀请消息;默认由业务层传入 pending 状态用于消息中心。
|
||||
func (r *Repository) ListRoleInvitations(ctx context.Context, command hostservice.ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error) {
|
||||
query := fmt.Sprintf(`SELECT %s FROM role_invitations WHERE app_code = ? AND target_user_id = ?`, roleInvitationColumns)
|
||||
args := []any{appcode.FromContext(ctx), command.TargetUserID}
|
||||
if command.Status != "" {
|
||||
query += " AND status = ?"
|
||||
args = append(args, command.Status)
|
||||
}
|
||||
// 消息中心优先展示最新邀请;同毫秒内用 invitation_id 保持稳定排序。
|
||||
query += " ORDER BY created_at_ms DESC, invitation_id DESC LIMIT ?"
|
||||
args = append(args, command.PageSize)
|
||||
return queryRoleInvitations(ctx, r.db, query, args...)
|
||||
}
|
||||
|
||||
// ListAgencyMembers 读取 Agency 成员关系列表。
|
||||
func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
|
||||
query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns)
|
||||
@ -812,6 +826,23 @@ func queryRoleInvitation(ctx context.Context, q sqlQueryer, clause string, args
|
||||
return invitation, err
|
||||
}
|
||||
|
||||
func queryRoleInvitations(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.RoleInvitation, error) {
|
||||
rows, err := q.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
invitations := make([]hostdomain.RoleInvitation, 0)
|
||||
for rows.Next() {
|
||||
invitation, err := scanRoleInvitation(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invitations = append(invitations, invitation)
|
||||
}
|
||||
return invitations, rows.Err()
|
||||
}
|
||||
|
||||
func scanRoleInvitation(scanner rowScanner) (hostdomain.RoleInvitation, error) {
|
||||
var invitation hostdomain.RoleInvitation
|
||||
err := scanner.Scan(
|
||||
@ -877,7 +908,10 @@ func processResultByInvitationID(ctx context.Context, q sqlQueryer, invitationID
|
||||
if invitation.Status != hostdomain.InvitationStatusAccepted {
|
||||
return result, nil
|
||||
}
|
||||
if invitation.InvitationType == hostdomain.InvitationTypeAgency {
|
||||
|
||||
// 只有接受动作会创建后续身份事实;按邀请类型回填前端需要立即刷新的最新关系。
|
||||
switch invitation.InvitationType {
|
||||
case hostdomain.InvitationTypeAgency:
|
||||
if profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil {
|
||||
result.HostProfile = profile
|
||||
if profile.CurrentMembershipID > 0 {
|
||||
@ -889,10 +923,20 @@ func processResultByInvitationID(ctx context.Context, q sqlQueryer, invitationID
|
||||
} else if ok {
|
||||
result.Agency = agency
|
||||
}
|
||||
} else if invitation.InvitationType == hostdomain.InvitationTypeBD {
|
||||
case hostdomain.InvitationTypeBD:
|
||||
if bd, err := queryBDProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil {
|
||||
result.BDProfile = bd
|
||||
}
|
||||
case hostdomain.InvitationTypeHost:
|
||||
if profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil {
|
||||
result.HostProfile = profile
|
||||
if profile.CurrentMembershipID > 0 {
|
||||
result.Membership, _ = queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID)
|
||||
}
|
||||
if profile.CurrentAgencyID > 0 {
|
||||
result.Agency, _ = queryAgency(ctx, q, "WHERE agency_id = ?", profile.CurrentAgencyID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ func TestBDLeaderSelfInviteFlowThroughGRPCUsesSplitIdentityTables(t *testing.T)
|
||||
t.Fatalf("InviteBD self RPC failed: %v", err)
|
||||
}
|
||||
invitation := inviteResp.GetInvitation()
|
||||
if invitation.GetStatus() != hostdomain.InvitationStatusAccepted || invitation.GetTargetUserId() != 10001 || invitation.GetParentLeaderUserId() != 10001 {
|
||||
if invitation.GetStatus() != hostdomain.InvitationStatusPending || invitation.GetTargetUserId() != 10001 || invitation.GetParentLeaderUserId() != 10001 {
|
||||
t.Fatalf("self invite response mismatch: %+v", invitation)
|
||||
}
|
||||
retryInviteResp, err := server.InviteBD(ctx, &userv1.InviteBDRequest{
|
||||
@ -82,9 +82,39 @@ func TestBDLeaderSelfInviteFlowThroughGRPCUsesSplitIdentityTables(t *testing.T)
|
||||
t.Fatalf("self invite retry must replay same invitation: first=%+v retry=%+v", invitation, retryInviteResp.GetInvitation())
|
||||
}
|
||||
|
||||
if _, err := server.GetBDProfile(ctx, &userv1.GetBDProfileRequest{Meta: meta, UserId: 10001, Role: hostdomain.BDRoleBD}); xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
||||
t.Fatalf("pending self invite must not create ordinary bd before accept: err=%v", err)
|
||||
}
|
||||
listResp, err := server.ListRoleInvitations(ctx, &userv1.ListRoleInvitationsRequest{
|
||||
Meta: meta,
|
||||
TargetUserId: 10001,
|
||||
Status: hostdomain.InvitationStatusPending,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoleInvitations RPC failed: %v", err)
|
||||
}
|
||||
if len(listResp.GetInvitations()) != 1 || listResp.GetInvitations()[0].GetInvitationId() != invitation.GetInvitationId() {
|
||||
t.Fatalf("pending invitation list mismatch: %+v", listResp.GetInvitations())
|
||||
}
|
||||
processResp, err := server.ProcessRoleInvitation(ctx, &userv1.ProcessRoleInvitationRequest{
|
||||
Meta: meta,
|
||||
CommandId: "rpc-accept-self-bd-10001",
|
||||
ActorUserId: 10001,
|
||||
InvitationId: invitation.GetInvitationId(),
|
||||
Action: hostdomain.InvitationActionAccept,
|
||||
Reason: "accept self bd",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessRoleInvitation accept RPC failed: %v", err)
|
||||
}
|
||||
if processResp.GetInvitation().GetStatus() != hostdomain.InvitationStatusAccepted || processResp.GetBdProfile().GetRole() != hostdomain.BDRoleBD {
|
||||
t.Fatalf("accepted self invite response mismatch: %+v bd=%+v", processResp.GetInvitation(), processResp.GetBdProfile())
|
||||
}
|
||||
|
||||
bdResp, err := server.GetBDProfile(ctx, &userv1.GetBDProfileRequest{Meta: meta, UserId: 10001, Role: hostdomain.BDRoleBD})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBDProfile role=bd RPC failed after self invite: %v", err)
|
||||
t.Fatalf("GetBDProfile role=bd RPC failed after accepting self invite: %v", err)
|
||||
}
|
||||
if bdResp.GetBdProfile().GetRole() != hostdomain.BDRoleBD || bdResp.GetBdProfile().GetParentLeaderUserId() != 10001 {
|
||||
t.Fatalf("ordinary bd profile mismatch: %+v", bdResp.GetBdProfile())
|
||||
|
||||
@ -98,7 +98,7 @@ func (s *Server) KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostR
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InviteAgency 直接创建用户成为 Agency owner 的关系事实,不再等待目标用户确认。
|
||||
// InviteAgency 创建用户成为 Agency owner 的待确认邀请,目标接受后才创建关系事实。
|
||||
func (s *Server) InviteAgency(ctx context.Context, req *userv1.InviteAgencyRequest) (*userv1.InviteAgencyResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
@ -118,7 +118,7 @@ func (s *Server) InviteAgency(ctx context.Context, req *userv1.InviteAgencyReque
|
||||
return &userv1.InviteAgencyResponse{Invitation: toProtoRoleInvitation(invitation)}, nil
|
||||
}
|
||||
|
||||
// InviteBD 直接创建用户成为 BD 的关系事实,只有 BD 负责人可调用。
|
||||
// InviteBD 创建用户成为普通 BD 的待确认邀请,只有 BD 负责人可调用。
|
||||
func (s *Server) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*userv1.InviteBDResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
@ -137,6 +137,25 @@ func (s *Server) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*us
|
||||
return &userv1.InviteBDResponse{Invitation: toProtoRoleInvitation(invitation)}, nil
|
||||
}
|
||||
|
||||
// InviteHost 创建用户加入 Agency 的待确认邀请,只有 active Agency owner 可调用。
|
||||
func (s *Server) InviteHost(ctx context.Context, req *userv1.InviteHostRequest) (*userv1.InviteHostResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
}
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
// 发起人是否拥有 active Agency、目标是否同区和是否已有归属都在持久化事务内锁定判断。
|
||||
invitation, err := s.hostSvc.InviteHost(ctx, hostservice.InviteHostInput{
|
||||
CommandID: req.GetCommandId(),
|
||||
InviterUserID: req.GetInviterUserId(),
|
||||
TargetUserID: req.GetTargetUserId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
return &userv1.InviteHostResponse{Invitation: toProtoRoleInvitation(invitation)}, nil
|
||||
}
|
||||
|
||||
// ListBDLeaderBDs 返回当前 BD Leader 直属 BD 列表。
|
||||
func (s *Server) ListBDLeaderBDs(ctx context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
@ -203,7 +222,30 @@ func (s *Server) ListBDAgencies(ctx context.Context, req *userv1.ListBDAgenciesR
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ProcessRoleInvitation 处理 Agency 或 BD 邀请。
|
||||
// ListRoleInvitations 返回目标用户收到的角色邀请消息。
|
||||
func (s *Server) ListRoleInvitations(ctx context.Context, req *userv1.ListRoleInvitationsRequest) (*userv1.ListRoleInvitationsResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
}
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
// 查询只接受 target_user_id,不允许调用方按 inviter 任意枚举别人的消息。
|
||||
invitations, err := s.hostSvc.ListRoleInvitations(ctx, hostservice.ListRoleInvitationsCommand{
|
||||
TargetUserID: req.GetTargetUserId(),
|
||||
Status: req.GetStatus(),
|
||||
PageSize: int(req.GetPageSize()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
resp := &userv1.ListRoleInvitationsResponse{Invitations: make([]*userv1.RoleInvitation, 0, len(invitations))}
|
||||
for _, invitation := range invitations {
|
||||
resp.Invitations = append(resp.Invitations, toProtoRoleInvitation(invitation))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ProcessRoleInvitation 处理 Host、Agency 或 BD 邀请。
|
||||
func (s *Server) ProcessRoleInvitation(ctx context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user