package externaladmin import ( "errors" "fmt" "strconv" "strings" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/response" "github.com/gin-gonic/gin" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type externalInviteRequest struct { TargetUserID FlexibleString `json:"targetUserId" binding:"required"` ParentBDUserID FlexibleString `json:"parentBdUserId"` AgencyID FlexibleString `json:"agencyId"` AgencyName string `json:"agencyName"` } func (h *Handler) ListExternalTeamUsers(client userclient.ExternalTeamClient, fixedRole string) gin.HandlerFunc { return func(c *gin.Context) { scope, ok := CurrentScope(c) if !ok || client == nil { response.ServerError(c, "团队服务暂不可用") return } role := strings.ToLower(strings.TrimSpace(fixedRole)) if role == "" { role = strings.ToLower(strings.TrimSpace(c.Query("role"))) } if !externalTeamRoleAllowed(c, role) { response.Forbidden(c, "没有该团队列表权限") return } page := positiveQueryInt(c.Query("page"), 1) pageSize := positiveQueryInt(c.Query("pageSize"), 20) if pageSize > 100 { pageSize = 100 } result, err := client.ListExternalTeamUsers(c.Request.Context(), userclient.ListExternalTeamUsersRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external", ScopeOwnerUserIDs: scope.OwnerUserIDs, RegionID: scope.RegionID, Role: role, Status: strings.ToLower(strings.TrimSpace(c.DefaultQuery("status", "active"))), Keyword: strings.TrimSpace(c.Query("keyword")), Page: page, PageSize: pageSize, }) if err != nil { writeExternalOwnerError(c, err, "获取团队用户失败") return } if result == nil { response.ServerError(c, "团队服务返回空结果") return } response.OK(c, response.Page{Items: result.Users, Page: result.Page, PageSize: result.PageSize, Total: result.Total}) } } func (h *Handler) SearchExternalInvitationCandidate(client userclient.ExternalTeamClient) gin.HandlerFunc { return func(c *gin.Context) { scope, ok := CurrentScope(c) if !ok || client == nil { response.ServerError(c, "邀请服务暂不可用") return } invitationType := strings.ToLower(strings.TrimSpace(c.Query("invitationType"))) if !externalInvitePermissionAllowed(c, invitationType) { response.Forbidden(c, "没有该邀请权限") return } item, err := client.SearchExternalInvitationCandidate(c.Request.Context(), userclient.SearchExternalInvitationCandidateRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external", OperatorUserID: scope.LinkedAppUserID, ExpectedRegionID: scope.RegionID, InvitationType: invitationType, Keyword: strings.TrimSpace(c.Query("keyword")), }) if err != nil { writeExternalOwnerError(c, err, "未找到符合条件的同区域用户") return } response.OK(c, item) } } func (h *Handler) CreateExternalBDInvitation(client userclient.ExternalTeamClient) gin.HandlerFunc { return h.createExternalInvitation(client, "bd") } func (h *Handler) CreateExternalAgencyInvitation(client userclient.ExternalTeamClient) gin.HandlerFunc { return h.createExternalInvitation(client, "agency") } func (h *Handler) CreateExternalHostInvitation(client userclient.ExternalTeamClient) gin.HandlerFunc { return h.createExternalInvitation(client, "host") } func (h *Handler) createExternalInvitation(client userclient.ExternalTeamClient, invitationType string) gin.HandlerFunc { return func(c *gin.Context) { scope, ok := CurrentScope(c) if !ok || client == nil { response.ServerError(c, "邀请服务暂不可用") return } var request externalInviteRequest if err := c.ShouldBindJSON(&request); err != nil { response.BadRequest(c, "邀请参数不正确") return } targetUserID, err := positiveFlexibleID(request.TargetUserID) if err != nil { response.BadRequest(c, "目标用户 ID 不正确") return } requestID := middleware.CurrentRequestID(c) commandID := fmt.Sprintf("external-%s:%s:%d", invitationType, requestID, targetUserID) var invitation *userclient.ExternalRoleInvitation switch invitationType { case "bd": invitation, err = client.ExternalInviteBD(c.Request.Context(), userclient.ExternalInviteBDRequest{ RequestID: requestID, Caller: "hyapp-admin-external", CommandID: commandID, OperatorUserID: scope.LinkedAppUserID, TargetUserID: targetUserID, ExpectedRegionID: scope.RegionID, }) case "agency": parentBDUserID, parseErr := positiveFlexibleID(request.ParentBDUserID) if parseErr != nil || strings.TrimSpace(request.AgencyName) == "" { response.BadRequest(c, "请选择团队 BD 并填写 Agency 名称") return } invitation, err = client.ExternalInviteAgency(c.Request.Context(), userclient.ExternalInviteAgencyRequest{ RequestID: requestID, Caller: "hyapp-admin-external", CommandID: commandID, OperatorUserID: scope.LinkedAppUserID, ParentBDUserID: parentBDUserID, TargetUserID: targetUserID, AgencyName: strings.TrimSpace(request.AgencyName), ScopeOwnerUserIDs: scope.OwnerUserIDs, ExpectedRegionID: scope.RegionID, }) case "host": agencyID, parseErr := positiveFlexibleID(request.AgencyID) if parseErr != nil { response.BadRequest(c, "请选择团队 Agency") return } invitation, err = client.ExternalInviteHost(c.Request.Context(), userclient.ExternalInviteHostRequest{ RequestID: requestID, Caller: "hyapp-admin-external", CommandID: commandID, OperatorUserID: scope.LinkedAppUserID, AgencyID: agencyID, TargetUserID: targetUserID, ScopeOwnerUserIDs: scope.OwnerUserIDs, ExpectedRegionID: scope.RegionID, }) } if err != nil { writeExternalOwnerError(c, err, "创建邀请失败") return } response.Created(c, invitation) } } func (h *Handler) RequireExternalTeamTarget(client userclient.ExternalTeamClient, param string) gin.HandlerFunc { return func(c *gin.Context) { scope, ok := CurrentScope(c) targetUserID, err := strconv.ParseInt(strings.TrimSpace(c.Param(param)), 10, 64) if !ok || client == nil || err != nil || targetUserID <= 0 { response.Forbidden(c, "目标用户不在当前团队") c.Abort() return } allowed := false for _, userStatus := range []string{"active", "banned", "disabled"} { result, lookupErr := client.ListExternalTeamUsers(c.Request.Context(), userclient.ListExternalTeamUsersRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external-target-check", ScopeOwnerUserIDs: scope.OwnerUserIDs, RegionID: scope.RegionID, Status: userStatus, Keyword: strconv.FormatInt(targetUserID, 10), Page: 1, PageSize: 10, }) if lookupErr != nil { writeExternalOwnerError(c, lookupErr, "团队范围校验失败") c.Abort() return } for _, item := range result.Users { if item.UserID == targetUserID && item.RegionID == scope.RegionID { allowed = true break } } if allowed { break } } if !allowed { response.Forbidden(c, "目标用户不在当前团队或区域") c.Abort() return } c.Next() } } // HydrateExternalRoomScope resolves business team members before entering roomadmin. // Room owners are BD/Agency/Host users, not portal Local/经理/SuperAdmin account roots. func (h *Handler) HydrateExternalRoomScope(client userclient.ExternalTeamClient) gin.HandlerFunc { return func(c *gin.Context) { scope, ok := CurrentScope(c) if !ok || client == nil { response.ServerError(c, "团队服务暂不可用") c.Abort() return } const maxRoomScopeUsers = 5000 seen := make(map[int64]struct{}) teamUserIDs := make([]int64, 0) for _, userStatus := range []string{"active", "banned", "disabled"} { for page := 1; ; page++ { result, err := client.ListExternalTeamUsers(c.Request.Context(), userclient.ListExternalTeamUsersRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external-room-scope", ScopeOwnerUserIDs: scope.OwnerUserIDs, RegionID: scope.RegionID, Status: userStatus, Page: page, PageSize: 200, }) if err != nil { writeExternalOwnerError(c, err, "房间团队范围校验失败") c.Abort() return } if result == nil { response.ServerError(c, "团队服务返回空结果") c.Abort() return } for _, item := range result.Users { if item.UserID <= 0 || item.RegionID != scope.RegionID { continue } if _, exists := seen[item.UserID]; exists { continue } seen[item.UserID] = struct{}{} teamUserIDs = append(teamUserIDs, item.UserID) if len(teamUserIDs) > maxRoomScopeUsers { response.Forbidden(c, "当前团队规模超过房间管理上限") c.Abort() return } } if len(result.Users) == 0 || result.PageSize <= 0 || int64(page*result.PageSize) >= result.Total { break } } } requestScope := appctx.ExternalScope{ AccountID: scope.AccountID, LinkedAppUserID: scope.LinkedAppUserID, IdentityType: scope.IdentityType, RegionID: scope.RegionID, OwnerUserIDs: teamUserIDs, } c.Request = c.Request.WithContext(appctx.WithExternalScope(c.Request.Context(), requestScope)) c.Next() } } func positiveFlexibleID(value FlexibleString) (int64, error) { parsed, err := strconv.ParseInt(strings.TrimSpace(string(value)), 10, 64) if err != nil || parsed <= 0 { return 0, ErrInvalidInput } return parsed, nil } func positiveQueryInt(raw string, fallback int) int { value, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || value <= 0 { return fallback } return value } func externalTeamRoleAllowed(c *gin.Context, role string) bool { switch role { case "": return principalHasAnyPermission(c, "user:list", "user:update", "user:ban", "user-ban:list", "user:unban", "user-level:grant") case "bd": // Agency invitations must select an existing BD even when the account was // intentionally granted agency:invite without the standalone BD list menu. return principalHasAnyPermission(c, "bd:list", "bd:invite", "agency:invite") case "agency": // Host invitations have the same bounded parent-selector dependency on Agency. return principalHasAnyPermission(c, "agency:list", "agency:invite", "host:invite") case "host": return principalHasAnyPermission(c, "host:list", "host:invite") default: return false } } func externalInvitePermissionAllowed(c *gin.Context, invitationType string) bool { return principalHasAnyPermission(c, invitationType+":invite") } func principalHasAnyPermission(c *gin.Context, permissions ...string) bool { principal, ok := CurrentPrincipal(c) if !ok { return false } assigned := make(map[string]struct{}, len(principal.Permissions)) for _, permission := range principal.Permissions { assigned[permission] = struct{}{} } for _, permission := range permissions { if _, exists := assigned[permission]; exists { return true } } return false } func writeExternalOwnerError(c *gin.Context, err error, fallback string) { if err == nil { return } code := status.Code(err) switch { case code == codes.InvalidArgument: response.BadRequest(c, status.Convert(err).Message()) case code == codes.NotFound: response.NotFound(c, status.Convert(err).Message()) case code == codes.PermissionDenied || code == codes.FailedPrecondition: response.Forbidden(c, status.Convert(err).Message()) case errors.Is(err, ErrInvalidInput): response.BadRequest(c, fallback) default: response.ServerError(c, fallback) } }