增加bd leader

This commit is contained in:
zhx 2026-06-04 20:01:55 +08:00
parent 0fdaec9ef9
commit 6d4648bf8d
14 changed files with 1335 additions and 299 deletions

File diff suppressed because it is too large Load Diff

View File

@ -221,6 +221,28 @@ message InviteBDResponse {
RoleInvitation invitation = 1; RoleInvitation invitation = 1;
} }
message ListBDLeaderBDsRequest {
RequestMeta meta = 1;
int64 leader_user_id = 2;
string status = 3;
int32 page_size = 4;
}
message ListBDLeaderBDsResponse {
repeated BDProfile bd_profiles = 1;
}
message ListBDLeaderAgenciesRequest {
RequestMeta meta = 1;
int64 leader_user_id = 2;
string status = 3;
int32 page_size = 4;
}
message ListBDLeaderAgenciesResponse {
repeated Agency agencies = 1;
}
message ProcessRoleInvitationRequest { message ProcessRoleInvitationRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string command_id = 2; string command_id = 2;
@ -438,6 +460,8 @@ service UserHostService {
rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse); rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse);
rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse); rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse);
rpc InviteBD(InviteBDRequest) returns (InviteBDResponse); rpc InviteBD(InviteBDRequest) returns (InviteBDResponse);
rpc ListBDLeaderBDs(ListBDLeaderBDsRequest) returns (ListBDLeaderBDsResponse);
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse); rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse); rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse); rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);

View File

@ -25,6 +25,8 @@ const (
UserHostService_KickAgencyHost_FullMethodName = "/hyapp.user.v1.UserHostService/KickAgencyHost" UserHostService_KickAgencyHost_FullMethodName = "/hyapp.user.v1.UserHostService/KickAgencyHost"
UserHostService_InviteAgency_FullMethodName = "/hyapp.user.v1.UserHostService/InviteAgency" UserHostService_InviteAgency_FullMethodName = "/hyapp.user.v1.UserHostService/InviteAgency"
UserHostService_InviteBD_FullMethodName = "/hyapp.user.v1.UserHostService/InviteBD" UserHostService_InviteBD_FullMethodName = "/hyapp.user.v1.UserHostService/InviteBD"
UserHostService_ListBDLeaderBDs_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderBDs"
UserHostService_ListBDLeaderAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderAgencies"
UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation" UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation"
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile" UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile" UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
@ -49,6 +51,8 @@ type UserHostServiceClient interface {
KickAgencyHost(ctx context.Context, in *KickAgencyHostRequest, opts ...grpc.CallOption) (*KickAgencyHostResponse, error) KickAgencyHost(ctx context.Context, in *KickAgencyHostRequest, opts ...grpc.CallOption) (*KickAgencyHostResponse, error)
InviteAgency(ctx context.Context, in *InviteAgencyRequest, opts ...grpc.CallOption) (*InviteAgencyResponse, error) InviteAgency(ctx context.Context, in *InviteAgencyRequest, opts ...grpc.CallOption) (*InviteAgencyResponse, error)
InviteBD(ctx context.Context, in *InviteBDRequest, opts ...grpc.CallOption) (*InviteBDResponse, error) InviteBD(ctx context.Context, in *InviteBDRequest, opts ...grpc.CallOption) (*InviteBDResponse, error)
ListBDLeaderBDs(ctx context.Context, in *ListBDLeaderBDsRequest, opts ...grpc.CallOption) (*ListBDLeaderBDsResponse, error)
ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error)
ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error) ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error)
GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error) GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error)
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error) GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error)
@ -129,6 +133,26 @@ func (c *userHostServiceClient) InviteBD(ctx context.Context, in *InviteBDReques
return out, nil 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)
err := c.cc.Invoke(ctx, UserHostService_ListBDLeaderBDs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userHostServiceClient) ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListBDLeaderAgenciesResponse)
err := c.cc.Invoke(ctx, UserHostService_ListBDLeaderAgencies_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) { func (c *userHostServiceClient) ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ProcessRoleInvitationResponse) out := new(ProcessRoleInvitationResponse)
@ -241,6 +265,8 @@ type UserHostServiceServer interface {
KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error) KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error)
InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error) InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error)
InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error)
ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error)
ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error)
ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error)
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error) GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error)
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error) GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
@ -279,6 +305,12 @@ func (UnimplementedUserHostServiceServer) InviteAgency(context.Context, *InviteA
func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) { func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) {
return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented") return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented")
} }
func (UnimplementedUserHostServiceServer) ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
}
func (UnimplementedUserHostServiceServer) ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderAgencies not implemented")
}
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) { func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented") return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
} }
@ -438,6 +470,42 @@ func _UserHostService_InviteBD_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) 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 {
return nil, err
}
if interceptor == nil {
return srv.(UserHostServiceServer).ListBDLeaderBDs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserHostService_ListBDLeaderBDs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserHostServiceServer).ListBDLeaderBDs(ctx, req.(*ListBDLeaderBDsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserHostService_ListBDLeaderAgencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListBDLeaderAgenciesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserHostServiceServer).ListBDLeaderAgencies(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserHostService_ListBDLeaderAgencies_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserHostServiceServer).ListBDLeaderAgencies(ctx, req.(*ListBDLeaderAgenciesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserHostService_ProcessRoleInvitation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserHostService_ProcessRoleInvitation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ProcessRoleInvitationRequest) in := new(ProcessRoleInvitationRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -649,6 +717,14 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
MethodName: "InviteBD", MethodName: "InviteBD",
Handler: _UserHostService_InviteBD_Handler, Handler: _UserHostService_InviteBD_Handler,
}, },
{
MethodName: "ListBDLeaderBDs",
Handler: _UserHostService_ListBDLeaderBDs_Handler,
},
{
MethodName: "ListBDLeaderAgencies",
Handler: _UserHostService_ListBDLeaderAgencies_Handler,
},
{ {
MethodName: "ProcessRoleInvitation", MethodName: "ProcessRoleInvitation",
Handler: _UserHostService_ProcessRoleInvitation_Handler, Handler: _UserHostService_ProcessRoleInvitation_Handler,

View File

@ -69,6 +69,10 @@ type UserCountryQueryClient interface {
type UserHostClient interface { type UserHostClient interface {
SearchAgencies(ctx context.Context, req *userv1.SearchAgenciesRequest) (*userv1.SearchAgenciesResponse, error) SearchAgencies(ctx context.Context, req *userv1.SearchAgenciesRequest) (*userv1.SearchAgenciesResponse, error)
ApplyToAgency(ctx context.Context, req *userv1.ApplyToAgencyRequest) (*userv1.ApplyToAgencyResponse, error) 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)
ListBDLeaderBDs(ctx context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error)
ListBDLeaderAgencies(ctx context.Context, req *userv1.ListBDLeaderAgenciesRequest) (*userv1.ListBDLeaderAgenciesResponse, error)
GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error)
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error) GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error)
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error) GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
@ -315,6 +319,22 @@ func (c *grpcUserHostClient) ApplyToAgency(ctx context.Context, req *userv1.Appl
return c.client.ApplyToAgency(ctx, req) return c.client.ApplyToAgency(ctx, req)
} }
func (c *grpcUserHostClient) InviteAgency(ctx context.Context, req *userv1.InviteAgencyRequest) (*userv1.InviteAgencyResponse, error) {
return c.client.InviteAgency(ctx, req)
}
func (c *grpcUserHostClient) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*userv1.InviteBDResponse, error) {
return c.client.InviteBD(ctx, req)
}
func (c *grpcUserHostClient) ListBDLeaderBDs(ctx context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error) {
return c.client.ListBDLeaderBDs(ctx, req)
}
func (c *grpcUserHostClient) ListBDLeaderAgencies(ctx context.Context, req *userv1.ListBDLeaderAgenciesRequest) (*userv1.ListBDLeaderAgenciesResponse, error) {
return c.client.ListBDLeaderAgencies(ctx, req)
}
func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) { func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
return c.client.GetHostProfile(ctx, req) return c.client.GetHostProfile(ctx, req)
} }

View File

@ -79,6 +79,11 @@ type UserHandlers struct {
ListAgencyCenterApplications http.HandlerFunc ListAgencyCenterApplications http.HandlerFunc
ReviewAgencyCenterApplication http.HandlerFunc ReviewAgencyCenterApplication http.HandlerFunc
RemoveAgencyCenterHost http.HandlerFunc RemoveAgencyCenterHost http.HandlerFunc
GetBDLeaderCenterOverview http.HandlerFunc
ListBDLeaderCenterBDs http.HandlerFunc
ListBDLeaderCenterAgencies http.HandlerFunc
InviteBDLeaderBD http.HandlerFunc
InviteBDLeaderAgency http.HandlerFunc
CompleteMyOnboarding http.HandlerFunc CompleteMyOnboarding http.HandlerFunc
UpdateMyProfile http.HandlerFunc UpdateMyProfile http.HandlerFunc
ChangeMyCountry http.HandlerFunc ChangeMyCountry http.HandlerFunc
@ -310,6 +315,11 @@ func (r routes) registerUserRoutes() {
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications) 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/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/hosts/{host_user_id}/remove", http.MethodPost, h.RemoveAgencyCenterHost)
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)
r.profile("/bd-leader/invitations/bd", http.MethodPost, h.InviteBDLeaderBD)
r.profile("/bd-leader/invitations/agency", http.MethodPost, h.InviteBDLeaderAgency)
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding) r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
r.profile("/users/me/profile/update", "", h.UpdateMyProfile) r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
r.profile("/users/me/country/change", "", h.ChangeMyCountry) r.profile("/users/me/country/change", "", h.ChangeMyCountry)

View File

@ -395,6 +395,18 @@ type fakeUserHostClient struct {
lastApplyAgency *userv1.ApplyToAgencyRequest lastApplyAgency *userv1.ApplyToAgencyRequest
applyAgencyResp *userv1.ApplyToAgencyResponse applyAgencyResp *userv1.ApplyToAgencyResponse
applyAgencyErr error applyAgencyErr error
lastInviteAgency *userv1.InviteAgencyRequest
inviteAgencyResp *userv1.InviteAgencyResponse
inviteAgencyErr error
lastInviteBD *userv1.InviteBDRequest
inviteBDResp *userv1.InviteBDResponse
inviteBDErr error
lastLeaderBDs *userv1.ListBDLeaderBDsRequest
leaderBDsResp *userv1.ListBDLeaderBDsResponse
leaderBDsErr error
lastLeaderAgencies *userv1.ListBDLeaderAgenciesRequest
leaderAgenciesResp *userv1.ListBDLeaderAgenciesResponse
leaderAgenciesErr error
last *userv1.GetCoinSellerProfileRequest last *userv1.GetCoinSellerProfileRequest
profile *userv1.CoinSellerProfile profile *userv1.CoinSellerProfile
err error err error
@ -1082,6 +1094,50 @@ func (f *fakeUserHostClient) ApplyToAgency(_ context.Context, req *userv1.ApplyT
return &userv1.ApplyToAgencyResponse{}, nil return &userv1.ApplyToAgencyResponse{}, nil
} }
func (f *fakeUserHostClient) InviteAgency(_ context.Context, req *userv1.InviteAgencyRequest) (*userv1.InviteAgencyResponse, error) {
f.lastInviteAgency = req
if f.inviteAgencyErr != nil {
return nil, f.inviteAgencyErr
}
if f.inviteAgencyResp != nil {
return f.inviteAgencyResp, nil
}
return &userv1.InviteAgencyResponse{}, nil
}
func (f *fakeUserHostClient) InviteBD(_ context.Context, req *userv1.InviteBDRequest) (*userv1.InviteBDResponse, error) {
f.lastInviteBD = req
if f.inviteBDErr != nil {
return nil, f.inviteBDErr
}
if f.inviteBDResp != nil {
return f.inviteBDResp, nil
}
return &userv1.InviteBDResponse{}, nil
}
func (f *fakeUserHostClient) ListBDLeaderBDs(_ context.Context, req *userv1.ListBDLeaderBDsRequest) (*userv1.ListBDLeaderBDsResponse, error) {
f.lastLeaderBDs = req
if f.leaderBDsErr != nil {
return nil, f.leaderBDsErr
}
if f.leaderBDsResp != nil {
return f.leaderBDsResp, nil
}
return &userv1.ListBDLeaderBDsResponse{}, nil
}
func (f *fakeUserHostClient) ListBDLeaderAgencies(_ context.Context, req *userv1.ListBDLeaderAgenciesRequest) (*userv1.ListBDLeaderAgenciesResponse, error) {
f.lastLeaderAgencies = req
if f.leaderAgenciesErr != nil {
return nil, f.leaderAgenciesErr
}
if f.leaderAgenciesResp != nil {
return f.leaderAgenciesResp, nil
}
return &userv1.ListBDLeaderAgenciesResponse{}, nil
}
func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) { func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
f.lastHost = req f.lastHost = req
if f.hostErr != nil { if f.hostErr != nil {

View File

@ -0,0 +1,363 @@
package userapi
import (
"net/http"
"strconv"
"strings"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
const (
adminSalaryAssetType = "ADMIN_SALARY_USD"
bdStatusActive = "active"
)
type bdLeaderProfileData struct {
UserID string `json:"user_id"`
Role string `json:"role"`
RegionID int64 `json:"region_id"`
ParentLeaderUserID string `json:"parent_leader_user_id,omitempty"`
Status string `json:"status"`
CreatedByUserID string `json:"created_by_user_id,omitempty"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
func (h *Handler) getBDLeaderCenterOverview(writer http.ResponseWriter, request *http.Request) {
userID := auth.UserIDFromContext(request.Context())
profile, ok := h.resolveActiveBDLeader(writer, request, userID)
if !ok {
return
}
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
users, err := h.userProfiles(request, []int64{userID})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
salary, err := h.salaryBalance(request, userID, adminSalaryAssetType)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
bdResp, err := h.userHostClient.ListBDLeaderBDs(request.Context(), &userv1.ListBDLeaderBDsRequest{
Meta: httpkit.UserMeta(request, ""),
LeaderUserId: userID,
Status: bdStatusActive,
PageSize: 100,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
agencyResp, err := h.userHostClient.ListBDLeaderAgencies(request.Context(), &userv1.ListBDLeaderAgenciesRequest{
Meta: httpkit.UserMeta(request, ""),
LeaderUserId: userID,
Status: agencyStatusActive,
PageSize: 100,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"profile": users[userID],
"bd_profile": bdLeaderProfileFromProto(profile),
"salary": salary,
"overview": map[string]any{
"total_bd": len(bdResp.GetBdProfiles()),
"total_agency": len(agencyResp.GetAgencies()),
},
})
}
func (h *Handler) listBDLeaderCenterBDs(writer http.ResponseWriter, request *http.Request) {
userID := auth.UserIDFromContext(request.Context())
if _, ok := h.resolveActiveBDLeader(writer, request, userID); !ok {
return
}
pageSize := queryPageSize(request, 20)
resp, err := h.userHostClient.ListBDLeaderBDs(request.Context(), &userv1.ListBDLeaderBDsRequest{
Meta: httpkit.UserMeta(request, ""),
LeaderUserId: userID,
Status: bdStatusActive,
PageSize: int32(pageSize),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
agencyResp, err := h.userHostClient.ListBDLeaderAgencies(request.Context(), &userv1.ListBDLeaderAgenciesRequest{
Meta: httpkit.UserMeta(request, ""),
LeaderUserId: userID,
Status: agencyStatusActive,
PageSize: 100,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
agencyCounts := make(map[int64]int)
for _, agency := range agencyResp.GetAgencies() {
agencyCounts[agency.GetParentBdUserId()]++
}
userIDs := make([]int64, 0, len(resp.GetBdProfiles()))
for _, profile := range resp.GetBdProfiles() {
userIDs = append(userIDs, profile.GetUserId())
}
users, err := h.userProfiles(request, userIDs)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
items := make([]map[string]any, 0, len(resp.GetBdProfiles()))
for _, profile := range resp.GetBdProfiles() {
user := users[profile.GetUserId()]
items = append(items, map[string]any{
"user_id": int64String(profile.GetUserId()),
"display_user_id": user.DisplayUserID,
"username": user.Username,
"avatar": user.Avatar,
"role": profile.GetRole(),
"status": profile.GetStatus(),
"region_id": profile.GetRegionId(),
"agency_count": agencyCounts[profile.GetUserId()],
})
}
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}
func (h *Handler) listBDLeaderCenterAgencies(writer http.ResponseWriter, request *http.Request) {
userID := auth.UserIDFromContext(request.Context())
if _, ok := h.resolveActiveBDLeader(writer, request, userID); !ok {
return
}
pageSize := queryPageSize(request, 20)
resp, err := h.userHostClient.ListBDLeaderAgencies(request.Context(), &userv1.ListBDLeaderAgenciesRequest{
Meta: httpkit.UserMeta(request, ""),
LeaderUserId: userID,
Status: agencyStatusActive,
PageSize: int32(pageSize),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
userIDs := make([]int64, 0, len(resp.GetAgencies()))
for _, agency := range resp.GetAgencies() {
userIDs = append(userIDs, agency.GetOwnerUserId(), agency.GetParentBdUserId())
}
users, err := h.userProfiles(request, userIDs)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
items := make([]map[string]any, 0, len(resp.GetAgencies()))
for _, agency := range resp.GetAgencies() {
membersResp, err := h.userHostClient.GetAgencyMembers(request.Context(), &userv1.GetAgencyMembersRequest{
Meta: httpkit.UserMeta(request, ""),
AgencyId: agency.GetAgencyId(),
Status: membershipStatusActive,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
owner := users[agency.GetOwnerUserId()]
parentBD := users[agency.GetParentBdUserId()]
items = append(items, map[string]any{
"agency": hostAgencyFromProto(agency),
"owner": owner,
"parent_bd": parentBD,
"host_count": len(membersResp.GetMemberships()),
"created_at_ms": agency.GetCreatedAtMs(),
})
}
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}
func (h *Handler) inviteBDLeaderBD(writer http.ResponseWriter, request *http.Request) {
h.inviteBDLeaderRole(writer, request, "bd")
}
func (h *Handler) inviteBDLeaderAgency(writer http.ResponseWriter, request *http.Request) {
h.inviteBDLeaderRole(writer, request, "agency")
}
func (h *Handler) inviteBDLeaderRole(writer http.ResponseWriter, request *http.Request, role string) {
userID := auth.UserIDFromContext(request.Context())
if _, ok := h.resolveActiveBDLeader(writer, request, userID); !ok {
return
}
var body map[string]any
if !httpkit.Decode(writer, request, &body) {
return
}
commandID := stringJSONValue(body["command_id"])
targetUserID := int64JSONValue(body["target_user_id"])
if commandID == "" || targetUserID <= 0 {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
if role == "bd" {
resp, err := h.userHostClient.InviteBD(request.Context(), &userv1.InviteBDRequest{
Meta: httpkit.UserMeta(request, ""),
CommandId: commandID,
InviterUserId: userID,
TargetUserId: targetUserID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{"invitation": roleInvitationData(resp.GetInvitation())})
return
}
agencyName := stringJSONValue(body["agency_name"])
if agencyName == "" {
users, err := h.userProfiles(request, []int64{targetUserID})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
target := users[targetUserID]
agencyName = firstBDLeaderNonBlank(target.Username, target.DisplayUserID, int64String(targetUserID))
}
resp, err := h.userHostClient.InviteAgency(request.Context(), &userv1.InviteAgencyRequest{
Meta: httpkit.UserMeta(request, ""),
CommandId: commandID,
InviterUserId: userID,
TargetUserId: targetUserID,
AgencyName: agencyName,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{"invitation": roleInvitationData(resp.GetInvitation())})
}
func (h *Handler) resolveActiveBDLeader(writer http.ResponseWriter, request *http.Request, userID int64) (*userv1.BDProfile, bool) {
if h.userHostClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return nil, false
}
resp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: userID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return nil, false
}
profile := resp.GetBdProfile()
if profile == nil || profile.GetRole() != "bd_leader" || profile.GetStatus() != bdStatusActive {
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
return nil, false
}
return profile, true
}
func bdLeaderProfileFromProto(profile *userv1.BDProfile) bdLeaderProfileData {
if profile == nil {
return bdLeaderProfileData{}
}
return bdLeaderProfileData{
UserID: int64String(profile.GetUserId()),
Role: profile.GetRole(),
RegionID: profile.GetRegionId(),
ParentLeaderUserID: int64String(profile.GetParentLeaderUserId()),
Status: profile.GetStatus(),
CreatedByUserID: int64String(profile.GetCreatedByUserId()),
CreatedAtMS: profile.GetCreatedAtMs(),
UpdatedAtMS: profile.GetUpdatedAtMs(),
}
}
func roleInvitationData(invitation *userv1.RoleInvitation) map[string]any {
if invitation == nil {
return map[string]any{}
}
return map[string]any{
"invitation_id": int64String(invitation.GetInvitationId()),
"command_id": invitation.GetCommandId(),
"invitation_type": invitation.GetInvitationType(),
"status": invitation.GetStatus(),
"inviter_user_id": int64String(invitation.GetInviterUserId()),
"target_user_id": int64String(invitation.GetTargetUserId()),
"region_id": invitation.GetRegionId(),
"agency_name": invitation.GetAgencyName(),
"parent_bd_user_id": int64String(invitation.GetParentBdUserId()),
"parent_leader_user_id": int64String(invitation.GetParentLeaderUserId()),
"processed_by_user_id": int64String(invitation.GetProcessedByUserId()),
"process_reason": invitation.GetProcessReason(),
"processed_at_ms": invitation.GetProcessedAtMs(),
"created_at_ms": invitation.GetCreatedAtMs(),
"updated_at_ms": invitation.GetUpdatedAtMs(),
}
}
func queryPageSize(request *http.Request, fallback int) int {
value, _ := strconv.Atoi(strings.TrimSpace(request.URL.Query().Get("page_size")))
if value <= 0 {
value = fallback
}
if value > 100 {
return 100
}
return value
}
func stringJSONValue(value any) string {
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
case float64:
return strconv.FormatInt(int64(v), 10)
case int64:
return strconv.FormatInt(v, 10)
case int:
return strconv.Itoa(v)
default:
return ""
}
}
func int64JSONValue(value any) int64 {
switch v := value.(type) {
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
return parsed
case float64:
return int64(v)
case int64:
return v
case int:
return int64(v)
default:
return 0
}
}
func firstBDLeaderNonBlank(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return "-"
}

View File

@ -63,6 +63,11 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
ListAgencyCenterApplications: h.listAgencyCenterApplications, ListAgencyCenterApplications: h.listAgencyCenterApplications,
ReviewAgencyCenterApplication: h.reviewAgencyCenterApplication, ReviewAgencyCenterApplication: h.reviewAgencyCenterApplication,
RemoveAgencyCenterHost: h.removeAgencyCenterHost, RemoveAgencyCenterHost: h.removeAgencyCenterHost,
GetBDLeaderCenterOverview: h.getBDLeaderCenterOverview,
ListBDLeaderCenterBDs: h.listBDLeaderCenterBDs,
ListBDLeaderCenterAgencies: h.listBDLeaderCenterAgencies,
InviteBDLeaderBD: h.inviteBDLeaderBD,
InviteBDLeaderAgency: h.inviteBDLeaderAgency,
CompleteMyOnboarding: h.completeMyOnboarding, CompleteMyOnboarding: h.completeMyOnboarding,
UpdateMyProfile: h.updateMyProfile, UpdateMyProfile: h.updateMyProfile,
ChangeMyCountry: h.changeMyCountry, ChangeMyCountry: h.changeMyCountry,

View File

@ -79,6 +79,9 @@ type InviteAgencyCommand struct {
InviterUserID int64 InviterUserID int64
TargetUserID int64 TargetUserID int64
AgencyName string AgencyName string
AgencyID int64
MembershipID int64
EventID int64
NowMs int64 NowMs int64
} }
@ -95,9 +98,24 @@ type InviteBDCommand struct {
InvitationID int64 InvitationID int64
InviterUserID int64 InviterUserID int64
TargetUserID int64 TargetUserID int64
EventID int64
NowMs int64 NowMs int64
} }
// ListBDLeaderBDsCommand 描述 BD Leader 查询直属 BD 列表的条件。
type ListBDLeaderBDsCommand struct {
LeaderUserID int64
Status string
PageSize int
}
// ListBDLeaderAgenciesCommand 描述 BD Leader 查询下级 Agency 列表的条件。
type ListBDLeaderAgenciesCommand struct {
LeaderUserID int64
Status string
PageSize int
}
// ProcessRoleInvitationInput 是处理角色邀请的外部输入。 // ProcessRoleInvitationInput 是处理角色邀请的外部输入。
type ProcessRoleInvitationInput struct { type ProcessRoleInvitationInput struct {
CommandID string CommandID string

View File

@ -20,6 +20,8 @@ type Repository interface {
KickAgencyHost(ctx context.Context, command KickAgencyHostCommand) (hostdomain.KickAgencyHostResult, error) KickAgencyHost(ctx context.Context, command KickAgencyHostCommand) (hostdomain.KickAgencyHostResult, error)
InviteAgency(ctx context.Context, command InviteAgencyCommand) (hostdomain.RoleInvitation, error) InviteAgency(ctx context.Context, command InviteAgencyCommand) (hostdomain.RoleInvitation, error)
InviteBD(ctx context.Context, command InviteBDCommand) (hostdomain.RoleInvitation, error) InviteBD(ctx context.Context, command InviteBDCommand) (hostdomain.RoleInvitation, error)
ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error)
ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error)
ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error) ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error)
CreateBDLeader(ctx context.Context, command CreateBDLeaderCommand) (hostdomain.BDProfile, error) CreateBDLeader(ctx context.Context, command CreateBDLeaderCommand) (hostdomain.BDProfile, error)
CreateBD(ctx context.Context, command CreateBDCommand) (hostdomain.BDProfile, error) CreateBD(ctx context.Context, command CreateBDCommand) (hostdomain.BDProfile, error)
@ -196,12 +198,12 @@ func (s *Service) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Age
return s.repository.GetAgency(ctx, agencyID) return s.repository.GetAgency(ctx, agencyID)
} }
// InviteAgency 创建邀请用户成为 Agency 拥有者的待处理邀请 // InviteAgency 由 BD 直接创建用户的 Agency owner 身份,不再等待目标用户确认
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) { func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil { if err := s.requireWriteDependencies(); err != nil {
return hostdomain.RoleInvitation{}, err return hostdomain.RoleInvitation{}, err
} }
// Agency 名称在邀请创建时固化,接受时用这份快照创建 Agency // Agency 名称在邀请动作里固化;持久化层会在同一事务中写 accepted invitation、Agency、owner Host 和 owner membership
agencyName := strings.TrimSpace(command.AgencyName) agencyName := strings.TrimSpace(command.AgencyName)
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 || 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") return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id, target_user_id and agency_name are required")
@ -214,16 +216,19 @@ func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (
InviterUserID: command.InviterUserID, InviterUserID: command.InviterUserID,
TargetUserID: command.TargetUserID, TargetUserID: command.TargetUserID,
AgencyName: agencyName, AgencyName: agencyName,
AgencyID: s.idGenerator.NewInt64(),
MembershipID: s.idGenerator.NewInt64(),
EventID: s.nextEventIDRange(3),
NowMs: nowMs, NowMs: nowMs,
}) })
} }
// InviteBD 创建 BD 负责人邀请用户成为 BD 的待处理邀请 // InviteBD 由 BD Leader 直接创建用户的 BD 身份,不再等待目标用户确认
func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) { func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil { if err := s.requireWriteDependencies(); err != nil {
return hostdomain.RoleInvitation{}, err return hostdomain.RoleInvitation{}, err
} }
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验 // 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验,并和 accepted invitation 一起提交
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 { 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") return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
} }
@ -234,10 +239,41 @@ func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdoma
InvitationID: s.idGenerator.NewInt64(), InvitationID: s.idGenerator.NewInt64(),
InviterUserID: command.InviterUserID, InviterUserID: command.InviterUserID,
TargetUserID: command.TargetUserID, TargetUserID: command.TargetUserID,
EventID: s.idGenerator.NewInt64(),
NowMs: nowMs, NowMs: nowMs,
}) })
} }
// ListBDLeaderBDs 返回当前 BD Leader 直属的有效 BD 列表。
func (s *Service) ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
if s.repository == nil {
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
}
if command.LeaderUserID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "leader_user_id is required")
}
return s.repository.ListBDLeaderBDs(ctx, ListBDLeaderBDsCommand{
LeaderUserID: command.LeaderUserID,
Status: normalizeStatus(command.Status),
PageSize: normalizePageSize(command.PageSize),
})
}
// ListBDLeaderAgencies 返回当前 BD Leader 团队下的 Agency 列表。
func (s *Service) ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) {
if s.repository == nil {
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
}
if command.LeaderUserID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "leader_user_id is required")
}
return s.repository.ListBDLeaderAgencies(ctx, ListBDLeaderAgenciesCommand{
LeaderUserID: command.LeaderUserID,
Status: normalizeStatus(command.Status),
PageSize: normalizePageSize(command.PageSize),
})
}
// ProcessRoleInvitation 接受、拒绝或取消角色邀请。 // ProcessRoleInvitation 接受、拒绝或取消角色邀请。
func (s *Service) ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationInput) (hostdomain.ProcessRoleInvitationResult, error) { func (s *Service) ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationInput) (hostdomain.ProcessRoleInvitationResult, error) {
if err := s.requireWriteDependencies(); err != nil { if err := s.requireWriteDependencies(); err != nil {
@ -576,3 +612,13 @@ func normalizeAction(value string) string {
func normalizeStatus(value string) string { func normalizeStatus(value string) string {
return strings.ToLower(strings.TrimSpace(value)) return strings.ToLower(strings.TrimSpace(value))
} }
func normalizePageSize(value int) int {
if value <= 0 {
return 20
}
if value > 100 {
return 100
}
return value
}

View File

@ -195,27 +195,23 @@ func TestAcceptAgencyInvitationCreatesOwnerHostAndRejectsActiveHost(t *testing.T
if err != nil { if err != nil {
t.Fatalf("InviteAgency failed: %v", err) t.Fatalf("InviteAgency failed: %v", err)
} }
accepted, err := svc.ProcessRoleInvitation(ctx, hostservice.ProcessRoleInvitationInput{ if invitation.Status != hostdomain.InvitationStatusAccepted {
CommandID: "accept-agency-601", t.Fatalf("agency invitation should be accepted immediately: %+v", invitation)
ActorUserID: 601,
InvitationID: invitation.InvitationID,
Action: hostdomain.InvitationActionAccept,
})
if err != nil {
t.Fatalf("accept agency invitation failed: %v", err)
} }
if accepted.Agency.OwnerUserID != 601 || accepted.HostProfile.CurrentAgencyID != accepted.Agency.AgencyID || accepted.Membership.MembershipType != hostdomain.MembershipTypeOwner { summary, err := svc.GetUserRoleSummary(ctx, 601)
t.Fatalf("owner agency facts mismatch: %+v", accepted)
}
blockedInvitation, err := svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
CommandID: "invite-agency-602",
InviterUserID: 501,
TargetUserID: 602,
AgencyName: "Blocked Agency",
})
if err != nil { if err != nil {
t.Fatalf("InviteAgency for later active host failed: %v", err) t.Fatalf("GetUserRoleSummary failed: %v", err)
}
agency, err := svc.GetAgency(ctx, summary.AgencyID)
if err != nil {
t.Fatalf("GetAgency failed: %v", err)
}
hostProfile, err := svc.GetHostProfile(ctx, 601)
if err != nil {
t.Fatalf("GetHostProfile failed: %v", err)
}
if !summary.IsAgency || !summary.IsHost || agency.OwnerUserID != 601 || hostProfile.CurrentAgencyID != agency.AgencyID {
t.Fatalf("owner agency facts mismatch: summary=%+v agency=%+v host=%+v", summary, agency, hostProfile)
} }
repository.PutHostProfile(hostdomain.HostProfile{ repository.PutHostProfile(hostdomain.HostProfile{
UserID: 602, UserID: 602,
@ -223,14 +219,14 @@ func TestAcceptAgencyInvitationCreatesOwnerHostAndRejectsActiveHost(t *testing.T
RegionID: 10, RegionID: 10,
Source: "test", Source: "test",
}) })
_, err = svc.ProcessRoleInvitation(ctx, hostservice.ProcessRoleInvitationInput{ _, err = svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
CommandID: "accept-agency-602", CommandID: "invite-agency-602",
ActorUserID: 602, InviterUserID: 501,
InvitationID: blockedInvitation.InvitationID, TargetUserID: 602,
Action: hostdomain.InvitationActionAccept, AgencyName: "Blocked Agency",
}) })
if got := xerr.CodeOf(err); got != xerr.Conflict { if got := xerr.CodeOf(err); got != xerr.Conflict {
t.Fatalf("active host must not accept agency invitation: got %s err=%v", got, err) t.Fatalf("active host must not become agency owner: got %s err=%v", got, err)
} }
} }
@ -255,17 +251,15 @@ func TestAcceptBDInvitationDoesNotCreateHostProfile(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("InviteBD failed: %v", err) t.Fatalf("InviteBD failed: %v", err)
} }
accepted, err := svc.ProcessRoleInvitation(ctx, hostservice.ProcessRoleInvitationInput{ if invitation.Status != hostdomain.InvitationStatusAccepted {
CommandID: "accept-bd-702", t.Fatalf("bd invitation should be accepted immediately: %+v", invitation)
ActorUserID: 702,
InvitationID: invitation.InvitationID,
Action: hostdomain.InvitationActionAccept,
})
if err != nil {
t.Fatalf("accept bd invitation failed: %v", err)
} }
if accepted.BDProfile.UserID != 702 || accepted.BDProfile.Role != hostdomain.BDRoleBD || accepted.BDProfile.ParentLeaderUserID != 701 { bdProfile, err := svc.GetBDProfile(ctx, 702)
t.Fatalf("bd profile mismatch: %+v", accepted.BDProfile) if err != nil {
t.Fatalf("GetBDProfile failed: %v", err)
}
if bdProfile.UserID != 702 || bdProfile.Role != hostdomain.BDRoleBD || bdProfile.ParentLeaderUserID != 701 {
t.Fatalf("bd profile mismatch: %+v", bdProfile)
} }
if _, err := svc.GetHostProfile(ctx, 702); xerr.CodeOf(err) != xerr.NotFound { if _, err := svc.GetHostProfile(ctx, 702); xerr.CodeOf(err) != xerr.NotFound {
t.Fatalf("becoming bd must not create host profile: err=%v", err) t.Fatalf("becoming bd must not create host profile: err=%v", err)
@ -710,11 +704,11 @@ func TestGetUserRoleSummaryAggregatesRolesAndPendingInvitations(t *testing.T) {
if !summary.IsHost || !summary.IsAgency || !summary.IsManager || !summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 { if !summary.IsHost || !summary.IsAgency || !summary.IsManager || !summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 {
t.Fatalf("role summary mismatch: %+v", summary) t.Fatalf("role summary mismatch: %+v", summary)
} }
pending, err := svc.GetUserRoleSummary(ctx, 932) directBD, err := svc.GetUserRoleSummary(ctx, 932)
if err != nil { if err != nil {
t.Fatalf("GetUserRoleSummary pending failed: %v", err) t.Fatalf("GetUserRoleSummary direct bd failed: %v", err)
} }
if pending.PendingRoleInvitations != 1 || pending.IsHost || pending.IsBD { if directBD.PendingRoleInvitations != 0 || directBD.IsHost || !directBD.IsBD || directBD.BDID != 932 {
t.Fatalf("pending role summary mismatch: %+v", pending) t.Fatalf("direct bd role summary mismatch: %+v", directBD)
} }
} }

View File

@ -69,7 +69,7 @@ func (r *Repository) InviteAgency(ctx context.Context, command hostservice.Invit
InvitationID: command.InvitationID, InvitationID: command.InvitationID,
CommandID: command.CommandID, CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeAgency, InvitationType: hostdomain.InvitationTypeAgency,
Status: hostdomain.InvitationStatusPending, Status: hostdomain.InvitationStatusAccepted,
InviterUserID: command.InviterUserID, InviterUserID: command.InviterUserID,
InviterBDUserID: inviter.UserID, InviterBDUserID: inviter.UserID,
TargetUserID: command.TargetUserID, TargetUserID: command.TargetUserID,
@ -77,13 +77,42 @@ func (r *Repository) InviteAgency(ctx context.Context, command hostservice.Invit
AgencyName: command.AgencyName, AgencyName: command.AgencyName,
ParentBDUserID: inviter.UserID, ParentBDUserID: inviter.UserID,
ParentLeaderUserID: parentLeaderUserID, ParentLeaderUserID: parentLeaderUserID,
ProcessedByUserID: command.InviterUserID,
ProcessReason: "direct_invite",
ProcessedAtMs: command.NowMs,
CreatedAtMs: command.NowMs, CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs,
} }
// 待处理邀请与 command_result 同事务写入,保证重复提交能按 invitation_id 回放 // 新邀请不再等待目标用户确认accepted invitation、Agency、owner Host 和 owner membership 必须同事务提交
if err := insertRoleInvitation(ctx, tx, invitation); err != nil { if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err) return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
} }
result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}
created, 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 created {
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{ if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
CommandID: command.CommandID, CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteAgency, CommandType: hostdomain.CommandTypeInviteAgency,
@ -145,19 +174,37 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
InvitationID: command.InvitationID, InvitationID: command.InvitationID,
CommandID: command.CommandID, CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeBD, InvitationType: hostdomain.InvitationTypeBD,
Status: hostdomain.InvitationStatusPending, Status: hostdomain.InvitationStatusAccepted,
InviterUserID: command.InviterUserID, InviterUserID: command.InviterUserID,
InviterBDUserID: inviter.UserID, InviterBDUserID: inviter.UserID,
TargetUserID: command.TargetUserID, TargetUserID: command.TargetUserID,
RegionID: inviter.RegionID, RegionID: inviter.RegionID,
ParentLeaderUserID: inviter.UserID, ParentLeaderUserID: inviter.UserID,
ProcessedByUserID: command.InviterUserID,
ProcessReason: "direct_invite",
ProcessedAtMs: command.NowMs,
CreatedAtMs: command.NowMs, CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs,
} }
// 这里只创建待处理邀请;真正的 BD 身份行在目标用户接受时再落库 // 新 BD 邀请直接创建 BD 身份accepted invitation 只作为审计事实和幂等回放结果
if err := insertRoleInvitation(ctx, tx, invitation); err != nil { if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err) 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 {
return hostdomain.RoleInvitation{}, err
}
if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
return hostdomain.RoleInvitation{}, err
}
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{ if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
CommandID: command.CommandID, CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteBD, CommandType: hostdomain.CommandTypeInviteBD,

View File

@ -281,6 +281,50 @@ func (r *Repository) HasActiveAgencyOwner(ctx context.Context, userID int64) (in
return agency.AgencyID, true, nil return agency.AgencyID, true, nil
} }
// ListBDLeaderBDs 读取 BD Leader 直属 BD调用前先校验 leader 当前仍有效,避免停用后继续读取团队数据。
func (r *Repository) ListBDLeaderBDs(ctx context.Context, command hostservice.ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
leader, err := queryBDProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
if err != nil {
return nil, err
}
if leader.Status != hostdomain.BDStatusActive || leader.Role != hostdomain.BDRoleLeader {
return nil, xerr.New(xerr.PermissionDenied, "leader is not active")
}
status := command.Status
if status == "" {
status = hostdomain.BDStatusActive
}
clause, args := appScopedClause(ctx, "WHERE role = ? AND parent_leader_user_id = ? AND status = ? ORDER BY created_at_ms DESC, user_id DESC LIMIT ?", hostdomain.BDRoleBD, command.LeaderUserID, status, command.PageSize)
return queryBDProfiles(ctx, r.db, clause, args...)
}
// ListBDLeaderAgencies 读取 BD Leader 团队 Agency包含直属 leader 自己创建的 Agency 和下属 BD 创建的 Agency。
func (r *Repository) ListBDLeaderAgencies(ctx context.Context, command hostservice.ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) {
leader, err := queryBDProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
if err != nil {
return nil, err
}
if leader.Status != hostdomain.BDStatusActive || leader.Role != hostdomain.BDRoleLeader {
return nil, xerr.New(xerr.PermissionDenied, "leader is not active")
}
status := command.Status
if status == "" {
status = hostdomain.AgencyStatusActive
}
clause, args := appScopedClause(ctx, `WHERE status = ? AND (
parent_bd_user_id = ?
OR parent_bd_user_id IN (
SELECT user_id
FROM bd_profiles
WHERE app_code = agencies.app_code
AND role = ?
AND parent_leader_user_id = ?
AND status = ?
)
) ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?`, status, command.LeaderUserID, hostdomain.BDRoleBD, command.LeaderUserID, hostdomain.BDStatusActive, command.PageSize)
return queryAgencies(ctx, r.db, clause, args...)
}
// ListAgencyMembers 读取 Agency 成员关系列表。 // ListAgencyMembers 读取 Agency 成员关系列表。
func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) { 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) query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns)
@ -373,6 +417,24 @@ func queryAgency(ctx context.Context, q sqlQueryer, clause string, args ...any)
return agency, err return agency, err
} }
func queryAgencies(ctx context.Context, q sqlQueryer, clause string, args ...any) ([]hostdomain.Agency, error) {
rows, err := q.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...)
if err != nil {
return nil, err
}
defer rows.Close()
agencies := make([]hostdomain.Agency, 0)
for rows.Next() {
agency, err := scanAgency(rows)
if err != nil {
return nil, err
}
agencies = append(agencies, agency)
}
return agencies, rows.Err()
}
func queryActiveAgencyByOwner(ctx context.Context, q sqlQueryer, ownerUserID int64, lock bool) (hostdomain.Agency, bool, error) { func queryActiveAgencyByOwner(ctx context.Context, q sqlQueryer, ownerUserID int64, lock bool) (hostdomain.Agency, bool, error) {
clause := "WHERE owner_user_id = ? AND status = ?" clause := "WHERE owner_user_id = ? AND status = ?"
if lock { if lock {
@ -461,6 +523,24 @@ func queryBDProfile(ctx context.Context, q sqlQueryer, clause string, args ...an
return profile, err return profile, err
} }
func queryBDProfiles(ctx context.Context, q sqlQueryer, clause string, args ...any) ([]hostdomain.BDProfile, error) {
rows, err := q.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...)
if err != nil {
return nil, err
}
defer rows.Close()
profiles := make([]hostdomain.BDProfile, 0)
for rows.Next() {
profile, err := scanBDProfile(rows)
if err != nil {
return nil, err
}
profiles = append(profiles, profile)
}
return profiles, rows.Err()
}
func queryBDProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) { func queryBDProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) {
clause := "WHERE user_id = ? AND status = ?" clause := "WHERE user_id = ? AND status = ?"
if lock { if lock {

View File

@ -98,7 +98,7 @@ func (s *Server) KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostR
}, nil }, nil
} }
// InviteAgency 创建用户成为 Agency 拥有者的邀请 // InviteAgency 直接创建用户成为 Agency owner 的关系事实,不再等待目标用户确认
func (s *Server) InviteAgency(ctx context.Context, req *userv1.InviteAgencyRequest) (*userv1.InviteAgencyResponse, error) { func (s *Server) InviteAgency(ctx context.Context, req *userv1.InviteAgencyRequest) (*userv1.InviteAgencyResponse, error) {
if s.hostSvc == nil { if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured")) 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 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) { func (s *Server) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*userv1.InviteBDResponse, error) {
if s.hostSvc == nil { if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured")) return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
@ -137,6 +137,50 @@ func (s *Server) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*us
return &userv1.InviteBDResponse{Invitation: toProtoRoleInvitation(invitation)}, nil return &userv1.InviteBDResponse{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 {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
}
ctx = contextWithApp(ctx, req.GetMeta())
profiles, err := s.hostSvc.ListBDLeaderBDs(ctx, hostservice.ListBDLeaderBDsCommand{
LeaderUserID: req.GetLeaderUserId(),
Status: req.GetStatus(),
PageSize: int(req.GetPageSize()),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListBDLeaderBDsResponse{BdProfiles: make([]*userv1.BDProfile, 0, len(profiles))}
for _, profile := range profiles {
resp.BdProfiles = append(resp.BdProfiles, toProtoBDProfile(profile))
}
return resp, nil
}
// ListBDLeaderAgencies 返回当前 BD Leader 团队下的 Agency 列表。
func (s *Server) ListBDLeaderAgencies(ctx context.Context, req *userv1.ListBDLeaderAgenciesRequest) (*userv1.ListBDLeaderAgenciesResponse, error) {
if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
}
ctx = contextWithApp(ctx, req.GetMeta())
agencies, err := s.hostSvc.ListBDLeaderAgencies(ctx, hostservice.ListBDLeaderAgenciesCommand{
LeaderUserID: req.GetLeaderUserId(),
Status: req.GetStatus(),
PageSize: int(req.GetPageSize()),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.ListBDLeaderAgenciesResponse{Agencies: make([]*userv1.Agency, 0, len(agencies))}
for _, agency := range agencies {
resp.Agencies = append(resp.Agencies, toProtoAgency(agency))
}
return resp, nil
}
// ProcessRoleInvitation 处理 Agency 或 BD 邀请。 // ProcessRoleInvitation 处理 Agency 或 BD 邀请。
func (s *Server) ProcessRoleInvitation(ctx context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error) { func (s *Server) ProcessRoleInvitation(ctx context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error) {
if s.hostSvc == nil { if s.hostSvc == nil {