增加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;
}
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 {
RequestMeta meta = 1;
string command_id = 2;
@ -438,6 +460,8 @@ service UserHostService {
rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse);
rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse);
rpc InviteBD(InviteBDRequest) returns (InviteBDResponse);
rpc ListBDLeaderBDs(ListBDLeaderBDsRequest) returns (ListBDLeaderBDsResponse);
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);

View File

@ -25,6 +25,8 @@ 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_ListBDLeaderBDs_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderBDs"
UserHostService_ListBDLeaderAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderAgencies"
UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation"
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
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)
InviteAgency(ctx context.Context, in *InviteAgencyRequest, opts ...grpc.CallOption) (*InviteAgencyResponse, 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)
GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, 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
}
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) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ProcessRoleInvitationResponse)
@ -241,6 +265,8 @@ type UserHostServiceServer interface {
KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error)
InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, 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)
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, 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) {
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) {
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)
}
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) {
in := new(ProcessRoleInvitationRequest)
if err := dec(in); err != nil {
@ -649,6 +717,14 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
MethodName: "InviteBD",
Handler: _UserHostService_InviteBD_Handler,
},
{
MethodName: "ListBDLeaderBDs",
Handler: _UserHostService_ListBDLeaderBDs_Handler,
},
{
MethodName: "ListBDLeaderAgencies",
Handler: _UserHostService_ListBDLeaderAgencies_Handler,
},
{
MethodName: "ProcessRoleInvitation",
Handler: _UserHostService_ProcessRoleInvitation_Handler,

View File

@ -69,6 +69,10 @@ type UserCountryQueryClient interface {
type UserHostClient interface {
SearchAgencies(ctx context.Context, req *userv1.SearchAgenciesRequest) (*userv1.SearchAgenciesResponse, 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)
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, 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)
}
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) {
return c.client.GetHostProfile(ctx, req)
}

View File

@ -79,6 +79,11 @@ type UserHandlers struct {
ListAgencyCenterApplications http.HandlerFunc
ReviewAgencyCenterApplication http.HandlerFunc
RemoveAgencyCenterHost http.HandlerFunc
GetBDLeaderCenterOverview http.HandlerFunc
ListBDLeaderCenterBDs http.HandlerFunc
ListBDLeaderCenterAgencies http.HandlerFunc
InviteBDLeaderBD http.HandlerFunc
InviteBDLeaderAgency http.HandlerFunc
CompleteMyOnboarding http.HandlerFunc
UpdateMyProfile 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/{application_id}/review", http.MethodPost, h.ReviewAgencyCenterApplication)
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.profile("/users/me/profile/update", "", h.UpdateMyProfile)
r.profile("/users/me/country/change", "", h.ChangeMyCountry)

View File

@ -395,6 +395,18 @@ type fakeUserHostClient struct {
lastApplyAgency *userv1.ApplyToAgencyRequest
applyAgencyResp *userv1.ApplyToAgencyResponse
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
profile *userv1.CoinSellerProfile
err error
@ -1082,6 +1094,50 @@ func (f *fakeUserHostClient) ApplyToAgency(_ context.Context, req *userv1.ApplyT
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) {
f.lastHost = req
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,
ReviewAgencyCenterApplication: h.reviewAgencyCenterApplication,
RemoveAgencyCenterHost: h.removeAgencyCenterHost,
GetBDLeaderCenterOverview: h.getBDLeaderCenterOverview,
ListBDLeaderCenterBDs: h.listBDLeaderCenterBDs,
ListBDLeaderCenterAgencies: h.listBDLeaderCenterAgencies,
InviteBDLeaderBD: h.inviteBDLeaderBD,
InviteBDLeaderAgency: h.inviteBDLeaderAgency,
CompleteMyOnboarding: h.completeMyOnboarding,
UpdateMyProfile: h.updateMyProfile,
ChangeMyCountry: h.changeMyCountry,

View File

@ -79,6 +79,9 @@ type InviteAgencyCommand struct {
InviterUserID int64
TargetUserID int64
AgencyName string
AgencyID int64
MembershipID int64
EventID int64
NowMs int64
}
@ -95,9 +98,24 @@ type InviteBDCommand struct {
InvitationID int64
InviterUserID int64
TargetUserID int64
EventID 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 是处理角色邀请的外部输入。
type ProcessRoleInvitationInput struct {
CommandID string

View File

@ -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)
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)
CreateBDLeader(ctx context.Context, command CreateBDLeaderCommand) (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)
}
// InviteAgency 创建邀请用户成为 Agency 拥有者的待处理邀请
// InviteAgency 由 BD 直接创建用户的 Agency owner 身份,不再等待目标用户确认
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil {
return hostdomain.RoleInvitation{}, err
}
// Agency 名称在邀请创建时固化,接受时用这份快照创建 Agency
// Agency 名称在邀请动作里固化;持久化层会在同一事务中写 accepted invitation、Agency、owner Host 和 owner membership
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")
@ -214,16 +216,19 @@ 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 负责人邀请用户成为 BD 的待处理邀请
// InviteBD 由 BD Leader 直接创建用户的 BD 身份,不再等待目标用户确认
func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil {
return hostdomain.RoleInvitation{}, err
}
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验,并和 accepted invitation 一起提交
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")
}
@ -234,10 +239,41 @@ 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,
})
}
// 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 接受、拒绝或取消角色邀请。
func (s *Service) ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationInput) (hostdomain.ProcessRoleInvitationResult, error) {
if err := s.requireWriteDependencies(); err != nil {
@ -576,3 +612,13 @@ func normalizeAction(value string) string {
func normalizeStatus(value string) string {
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 {
t.Fatalf("InviteAgency failed: %v", err)
}
accepted, err := svc.ProcessRoleInvitation(ctx, hostservice.ProcessRoleInvitationInput{
CommandID: "accept-agency-601",
ActorUserID: 601,
InvitationID: invitation.InvitationID,
Action: hostdomain.InvitationActionAccept,
})
if err != nil {
t.Fatalf("accept agency invitation failed: %v", err)
if invitation.Status != hostdomain.InvitationStatusAccepted {
t.Fatalf("agency invitation should be accepted immediately: %+v", invitation)
}
if accepted.Agency.OwnerUserID != 601 || accepted.HostProfile.CurrentAgencyID != accepted.Agency.AgencyID || accepted.Membership.MembershipType != hostdomain.MembershipTypeOwner {
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",
})
summary, err := svc.GetUserRoleSummary(ctx, 601)
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{
UserID: 602,
@ -223,14 +219,14 @@ func TestAcceptAgencyInvitationCreatesOwnerHostAndRejectsActiveHost(t *testing.T
RegionID: 10,
Source: "test",
})
_, err = svc.ProcessRoleInvitation(ctx, hostservice.ProcessRoleInvitationInput{
CommandID: "accept-agency-602",
ActorUserID: 602,
InvitationID: blockedInvitation.InvitationID,
Action: hostdomain.InvitationActionAccept,
_, err = svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
CommandID: "invite-agency-602",
InviterUserID: 501,
TargetUserID: 602,
AgencyName: "Blocked Agency",
})
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 {
t.Fatalf("InviteBD failed: %v", err)
}
accepted, err := svc.ProcessRoleInvitation(ctx, hostservice.ProcessRoleInvitationInput{
CommandID: "accept-bd-702",
ActorUserID: 702,
InvitationID: invitation.InvitationID,
Action: hostdomain.InvitationActionAccept,
})
if err != nil {
t.Fatalf("accept bd invitation failed: %v", err)
if invitation.Status != hostdomain.InvitationStatusAccepted {
t.Fatalf("bd invitation should be accepted immediately: %+v", invitation)
}
if accepted.BDProfile.UserID != 702 || accepted.BDProfile.Role != hostdomain.BDRoleBD || accepted.BDProfile.ParentLeaderUserID != 701 {
t.Fatalf("bd profile mismatch: %+v", accepted.BDProfile)
bdProfile, err := svc.GetBDProfile(ctx, 702)
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 {
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 {
t.Fatalf("role summary mismatch: %+v", summary)
}
pending, err := svc.GetUserRoleSummary(ctx, 932)
directBD, err := svc.GetUserRoleSummary(ctx, 932)
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 {
t.Fatalf("pending role summary mismatch: %+v", pending)
if directBD.PendingRoleInvitations != 0 || directBD.IsHost || !directBD.IsBD || directBD.BDID != 932 {
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,
CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeAgency,
Status: hostdomain.InvitationStatusPending,
Status: hostdomain.InvitationStatusAccepted,
InviterUserID: command.InviterUserID,
InviterBDUserID: inviter.UserID,
TargetUserID: command.TargetUserID,
@ -77,13 +77,42 @@ 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,
}
// 待处理邀请与 command_result 同事务写入,保证重复提交能按 invitation_id 回放
// 新邀请不再等待目标用户确认accepted invitation、Agency、owner Host 和 owner membership 必须同事务提交
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
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{
CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteAgency,
@ -145,19 +174,37 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
InvitationID: command.InvitationID,
CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeBD,
Status: hostdomain.InvitationStatusPending,
Status: hostdomain.InvitationStatusAccepted,
InviterUserID: command.InviterUserID,
InviterBDUserID: inviter.UserID,
TargetUserID: command.TargetUserID,
RegionID: inviter.RegionID,
ParentLeaderUserID: inviter.UserID,
ProcessedByUserID: command.InviterUserID,
ProcessReason: "direct_invite",
ProcessedAtMs: command.NowMs,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
// 这里只创建待处理邀请;真正的 BD 身份行在目标用户接受时再落库
// 新 BD 邀请直接创建 BD 身份accepted invitation 只作为审计事实和幂等回放结果
if err := insertRoleInvitation(ctx, tx, invitation); 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 {
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{
CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteBD,

View File

@ -281,6 +281,50 @@ func (r *Repository) HasActiveAgencyOwner(ctx context.Context, userID int64) (in
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 成员关系列表。
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)
@ -373,6 +417,24 @@ func queryAgency(ctx context.Context, q sqlQueryer, clause string, args ...any)
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) {
clause := "WHERE owner_user_id = ? AND status = ?"
if lock {
@ -461,6 +523,24 @@ func queryBDProfile(ctx context.Context, q sqlQueryer, clause string, args ...an
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) {
clause := "WHERE user_id = ? AND status = ?"
if lock {

View File

@ -98,7 +98,7 @@ func (s *Server) KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostR
}, nil
}
// InviteAgency 创建用户成为 Agency 拥有者的邀请
// 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,50 @@ func (s *Server) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*us
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 邀请。
func (s *Server) ProcessRoleInvitation(ctx context.Context, req *userv1.ProcessRoleInvitationRequest) (*userv1.ProcessRoleInvitationResponse, error) {
if s.hostSvc == nil {