package host import ( "context" "database/sql" "fmt" "strconv" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" hostdomain "hyapp/services/user-service/internal/domain/host" hostservice "hyapp/services/user-service/internal/service/host" ) // ExternalInviteBD 创建“外管操作者 -> BD”的待确认邀请。 // Portal 身份不属于 user-service,但操作者和目标用户的 active/region 事实必须在本事务内锁定重验。 func (r *Repository) ExternalInviteBD(ctx context.Context, command hostservice.ExternalInviteBDCommand) (hostdomain.RoleInvitation, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.RoleInvitation{}, err } defer tx.Rollback() if replayed, ok, err := replayExternalInvitation(ctx, tx, command.CommandID, hostdomain.CommandTypeExternalInviteBD); err != nil || ok { return replayed, err } if err := requireExternalOperatorAndTargetRegion(ctx, tx, command.OperatorUserID, command.TargetUserID, command.ExpectedRegionID); err != nil { return hostdomain.RoleInvitation{}, err } if _, ok, err := queryBDProfileMaybe(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active bd profile") } if pending, ok, err := queryPendingRoleInvitationByTarget(ctx, tx, hostdomain.InvitationTypeExternalBD, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { if pending.ExternalOperatorUserID != command.OperatorUserID || pending.ParentOwnerUserID != command.OperatorUserID { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has pending bd invitation") } if err := bindExternalCommandReplay(ctx, tx, command.CommandID, hostdomain.CommandTypeExternalInviteBD, pending.InvitationID, command.NowMs); err != nil { return hostdomain.RoleInvitation{}, err } if err := tx.Commit(); err != nil { return hostdomain.RoleInvitation{}, err } return pending, nil } invitation := hostdomain.RoleInvitation{ InvitationID: command.InvitationID, CommandID: command.CommandID, InvitationType: hostdomain.InvitationTypeExternalBD, Status: hostdomain.InvitationStatusPending, InviterUserID: command.OperatorUserID, TargetUserID: command.TargetUserID, ExternalOperatorUserID: command.OperatorUserID, RegionID: command.ExpectedRegionID, ParentOwnerUserID: command.OperatorUserID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, } if err := persistExternalInvitation(ctx, tx, invitation, hostdomain.CommandTypeExternalInviteBD, command.NowMs); err != nil { return hostdomain.RoleInvitation{}, err } if err := tx.Commit(); err != nil { return hostdomain.RoleInvitation{}, err } return invitation, nil } // ExternalInviteAgency 为 scope 内明确选择的 BD 创建 Agency 邀请。 // BD 的 parent_owner_user_id 是授权索引;不能用 created_by_user_id 或 parent_leader_user_id 猜外管归属。 func (r *Repository) ExternalInviteAgency(ctx context.Context, command hostservice.ExternalInviteAgencyCommand) (hostdomain.RoleInvitation, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.RoleInvitation{}, err } defer tx.Rollback() if replayed, ok, err := replayExternalInvitation(ctx, tx, command.CommandID, hostdomain.CommandTypeExternalInviteAgency); err != nil || ok { return replayed, err } if err := requireExternalOperatorAndTargetRegion(ctx, tx, command.OperatorUserID, command.TargetUserID, command.ExpectedRegionID); err != nil { return hostdomain.RoleInvitation{}, err } bd, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentBDUserID) if err != nil { return hostdomain.RoleInvitation{}, err } if bd.Status != hostdomain.BDStatusActive || bd.Role != hostdomain.BDRoleBD || bd.RegionID != command.ExpectedRegionID || bd.ParentLeaderUserID != 0 || bd.ParentOwnerUserID <= 0 || !containsExternalScopeOwner(command.ScopeOwnerUserIDs, bd.ParentOwnerUserID) { return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "parent bd is outside external team scope") } if _, err := requireExternalScopeOwnerRegion(ctx, tx, bd.ParentOwnerUserID, command.ExpectedRegionID); err != nil { return hostdomain.RoleInvitation{}, err } if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already owns active agency") } if pending, ok, err := queryPendingRoleInvitationByTarget(ctx, tx, hostdomain.InvitationTypeExternalAgency, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { if pending.ExternalOperatorUserID != command.OperatorUserID || pending.ParentBDUserID != bd.UserID || pending.ParentOwnerUserID != bd.ParentOwnerUserID { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has pending agency invitation") } if err := bindExternalCommandReplay(ctx, tx, command.CommandID, hostdomain.CommandTypeExternalInviteAgency, pending.InvitationID, command.NowMs); err != nil { return hostdomain.RoleInvitation{}, err } if err := tx.Commit(); err != nil { return hostdomain.RoleInvitation{}, err } return pending, nil } invitation := hostdomain.RoleInvitation{ InvitationID: command.InvitationID, CommandID: command.CommandID, InvitationType: hostdomain.InvitationTypeExternalAgency, Status: hostdomain.InvitationStatusPending, InviterUserID: command.OperatorUserID, InviterBDUserID: bd.UserID, TargetUserID: command.TargetUserID, ExternalOperatorUserID: command.OperatorUserID, RegionID: command.ExpectedRegionID, AgencyName: command.AgencyName, ParentBDUserID: bd.UserID, ParentLeaderUserID: bd.ParentLeaderUserID, ParentOwnerUserID: bd.ParentOwnerUserID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, } if err := persistExternalInvitation(ctx, tx, invitation, hostdomain.CommandTypeExternalInviteAgency, command.NowMs); err != nil { return hostdomain.RoleInvitation{}, err } if err := tx.Commit(); err != nil { return hostdomain.RoleInvitation{}, err } return invitation, nil } // ExternalInviteHost 为 scope 内明确选择的 Agency 创建 Host 邀请。 func (r *Repository) ExternalInviteHost(ctx context.Context, command hostservice.ExternalInviteHostCommand) (hostdomain.RoleInvitation, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.RoleInvitation{}, err } defer tx.Rollback() if replayed, ok, err := replayExternalInvitation(ctx, tx, command.CommandID, hostdomain.CommandTypeExternalInviteHost); err != nil || ok { return replayed, err } if err := requireExternalOperatorAndTargetRegion(ctx, tx, command.OperatorUserID, command.TargetUserID, command.ExpectedRegionID); err != nil { return hostdomain.RoleInvitation{}, err } agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID) if err != nil { return hostdomain.RoleInvitation{}, err } if agency.Status != hostdomain.AgencyStatusActive || !agency.JoinEnabled || agency.RegionID != command.ExpectedRegionID { return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "agency is not active in external region") } bd, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", agency.ParentBDUserID) if err != nil { return hostdomain.RoleInvitation{}, err } if bd.Status != hostdomain.BDStatusActive || bd.ParentLeaderUserID != 0 || bd.ParentOwnerUserID <= 0 || !containsExternalScopeOwner(command.ScopeOwnerUserIDs, bd.ParentOwnerUserID) { return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "agency is outside external team scope") } if _, err := requireExternalScopeOwnerRegion(ctx, tx, bd.ParentOwnerUserID, command.ExpectedRegionID); err != nil { return hostdomain.RoleInvitation{}, err } if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active agency membership") } if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already owns active agency") } if pending, ok, err := queryPendingRoleInvitationByTarget(ctx, tx, hostdomain.InvitationTypeExternalHost, command.TargetUserID, true); err != nil { return hostdomain.RoleInvitation{}, err } else if ok { if pending.ExternalOperatorUserID != command.OperatorUserID || pending.ParentAgencyID != agency.AgencyID || pending.ParentOwnerUserID != bd.ParentOwnerUserID { return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has pending host invitation") } if err := bindExternalCommandReplay(ctx, tx, command.CommandID, hostdomain.CommandTypeExternalInviteHost, pending.InvitationID, command.NowMs); err != nil { return hostdomain.RoleInvitation{}, err } if err := tx.Commit(); err != nil { return hostdomain.RoleInvitation{}, err } return pending, nil } invitation := hostdomain.RoleInvitation{ InvitationID: command.InvitationID, CommandID: command.CommandID, InvitationType: hostdomain.InvitationTypeExternalHost, Status: hostdomain.InvitationStatusPending, InviterUserID: command.OperatorUserID, InviterBDUserID: bd.UserID, TargetUserID: command.TargetUserID, ExternalOperatorUserID: command.OperatorUserID, RegionID: command.ExpectedRegionID, AgencyName: agency.Name, ParentBDUserID: bd.UserID, ParentLeaderUserID: bd.ParentLeaderUserID, ParentOwnerUserID: bd.ParentOwnerUserID, ParentAgencyID: agency.AgencyID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, } if err := persistExternalInvitation(ctx, tx, invitation, hostdomain.CommandTypeExternalInviteHost, command.NowMs); err != nil { return hostdomain.RoleInvitation{}, err } if err := tx.Commit(); err != nil { return hostdomain.RoleInvitation{}, err } return invitation, nil } func replayExternalInvitation(ctx context.Context, tx *sql.Tx, commandID string, commandType string) (hostdomain.RoleInvitation, bool, error) { existing, ok, err := commandResult(ctx, tx, commandID) if err != nil || !ok { return hostdomain.RoleInvitation{}, false, err } if err := requireCommandResult(existing, commandType, hostdomain.ResultTypeInvitation); err != nil { return hostdomain.RoleInvitation{}, true, err } invitation, err := queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID) return invitation, true, err } func bindExternalCommandReplay(ctx context.Context, tx *sql.Tx, commandID string, commandType string, invitationID int64, nowMs int64) error { if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{ CommandID: commandID, CommandType: commandType, ResultType: hostdomain.ResultTypeInvitation, ResultID: invitationID, CreatedAtMs: nowMs, }); err != nil { return mapHostDuplicateError(err) } return nil } func persistExternalInvitation(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation, commandType string, nowMs int64) error { if err := insertExternalRoleInvitation(ctx, tx, invitation); err != nil { return mapHostDuplicateError(err) } if err := bindExternalCommandReplay(ctx, tx, invitation.CommandID, commandType, invitation.InvitationID, nowMs); err != nil { return err } return insertRoleInvitationCreatedOutbox(ctx, tx, invitation, nowMs) } func requireExternalOperatorAndTargetRegion(ctx context.Context, tx *sql.Tx, operatorUserID int64, targetUserID int64, expectedRegionID int64) error { operatorRegionID, err := currentUserRegion(ctx, tx, operatorUserID, "FOR UPDATE") if err != nil { return err } if operatorRegionID <= 0 || operatorRegionID != expectedRegionID { return xerr.New(xerr.RegionMismatch, "external operator region has changed") } targetRegionID, err := currentUserRegion(ctx, tx, targetUserID, "FOR UPDATE") if err != nil { return err } if targetRegionID != expectedRegionID { return xerr.New(xerr.RegionMismatch, "target region does not match external operator region") } return nil } func requireExternalScopeOwnerRegion(ctx context.Context, tx *sql.Tx, ownerUserID int64, expectedRegionID int64) (int64, error) { regionID, err := currentUserRegion(ctx, tx, ownerUserID, "FOR UPDATE") if err != nil { return 0, err } if regionID != expectedRegionID { return 0, xerr.New(xerr.RegionMismatch, "external team owner region has changed") } return regionID, nil } func containsExternalScopeOwner(ownerUserIDs []int64, ownerUserID int64) bool { for _, candidate := range ownerUserIDs { if candidate == ownerUserID { return true } } return false } func validateExternalBDInvitationAcceptance(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation, targetRegionID int64) error { if invitation.InvitationType != hostdomain.InvitationTypeExternalBD || invitation.ExternalOperatorUserID <= 0 || invitation.InviterBDUserID != 0 || invitation.ParentLeaderUserID != 0 { return xerr.New(xerr.InvalidArgument, "external bd invitation metadata is invalid") } operatorRegionID, err := currentUserRegion(ctx, tx, invitation.ExternalOperatorUserID, "FOR UPDATE") if err != nil { return err } if invitation.ParentOwnerUserID != invitation.ExternalOperatorUserID || invitation.InviterUserID != invitation.ExternalOperatorUserID || invitation.RegionID <= 0 || operatorRegionID != invitation.RegionID || targetRegionID != invitation.RegionID { return xerr.New(xerr.RegionMismatch, "external bd invitation scope has changed") } return nil } func validateExternalAgencyInvitationAcceptance(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation, targetRegionID int64) error { if invitation.InvitationType != hostdomain.InvitationTypeExternalAgency || invitation.ExternalOperatorUserID <= 0 || invitation.InviterUserID != invitation.ExternalOperatorUserID || invitation.InviterBDUserID != invitation.ParentBDUserID { return xerr.New(xerr.InvalidArgument, "external agency invitation metadata is invalid") } operatorRegionID, err := currentUserRegion(ctx, tx, invitation.ExternalOperatorUserID, "FOR UPDATE") if err != nil { return err } if operatorRegionID != invitation.RegionID || targetRegionID != invitation.RegionID || invitation.ParentOwnerUserID <= 0 { return xerr.New(xerr.RegionMismatch, "external agency invitation region has changed") } bd, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", invitation.ParentBDUserID) if err != nil { return err } if bd.Status != hostdomain.BDStatusActive || bd.RegionID != invitation.RegionID || bd.ParentLeaderUserID != 0 || bd.ParentOwnerUserID != invitation.ParentOwnerUserID { return xerr.New(xerr.Conflict, "external agency parent bd ownership has changed") } _, err = requireExternalScopeOwnerRegion(ctx, tx, bd.ParentOwnerUserID, invitation.RegionID) return err } func validateExternalHostInvitationAcceptance(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation, targetRegionID int64) (hostdomain.Agency, error) { if invitation.InvitationType != hostdomain.InvitationTypeExternalHost || invitation.ExternalOperatorUserID <= 0 || invitation.InviterUserID != invitation.ExternalOperatorUserID || invitation.InviterBDUserID != invitation.ParentBDUserID { return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "external host invitation metadata is invalid") } operatorRegionID, err := currentUserRegion(ctx, tx, invitation.ExternalOperatorUserID, "FOR UPDATE") if err != nil { return hostdomain.Agency{}, err } if operatorRegionID != invitation.RegionID || targetRegionID != invitation.RegionID || invitation.ParentAgencyID <= 0 || invitation.ParentOwnerUserID <= 0 { return hostdomain.Agency{}, xerr.New(xerr.RegionMismatch, "external host invitation region has changed") } agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", invitation.ParentAgencyID) if err != nil { return hostdomain.Agency{}, err } if agency.Status != hostdomain.AgencyStatusActive || !agency.JoinEnabled || agency.RegionID != invitation.RegionID || agency.ParentBDUserID != invitation.ParentBDUserID { return hostdomain.Agency{}, xerr.New(xerr.Conflict, "external host parent agency has changed") } bd, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", invitation.ParentBDUserID) if err != nil { return hostdomain.Agency{}, err } if bd.Status != hostdomain.BDStatusActive || bd.ParentLeaderUserID != 0 || bd.ParentOwnerUserID != invitation.ParentOwnerUserID || bd.RegionID != invitation.RegionID { return hostdomain.Agency{}, xerr.New(xerr.Conflict, "external host parent bd ownership has changed") } if _, err := requireExternalScopeOwnerRegion(ctx, tx, bd.ParentOwnerUserID, invitation.RegionID); err != nil { return hostdomain.Agency{}, err } return agency, nil } // ListExternalTeamUsers 通过 parent_owner 索引先收敛 BD,再向下连接 Agency/Host。 // region/status 条件在 users 权威表上执行;scope 绝不允许为空。 func (r *Repository) ListExternalTeamUsers(ctx context.Context, command hostservice.ListExternalTeamUsersCommand) (hostdomain.ExternalTeamUsers, error) { if len(command.ScopeOwnerUserIDs) == 0 || command.RegionID <= 0 { return hostdomain.ExternalTeamUsers{}, xerr.New(xerr.PermissionDenied, "external team scope is empty") } cte, cteArgs := externalTeamCTE(ctx, command.ScopeOwnerUserIDs, command.RegionID) filter, filterArgs := externalTeamFilter(command) outerArgs := []any{appcode.FromContext(ctx), command.RegionID, command.Status} outerArgs = append(outerArgs, filterArgs...) countArgs := append(append([]any{}, cteArgs...), outerArgs...) var total int64 if err := r.db.QueryRowContext(ctx, cte+` SELECT COUNT(DISTINCT tr.user_id) FROM team_relations tr INNER JOIN users u ON u.app_code = ? AND u.user_id = tr.user_id WHERE u.region_id = ? AND u.status = ?`+filter, countArgs..., ).Scan(&total); err != nil { return hostdomain.ExternalTeamUsers{}, err } offset := (command.Page - 1) * command.PageSize listArgs := append(append([]any{}, cteArgs...), outerArgs...) listArgs = append(listArgs, command.PageSize, offset) rows, err := r.db.QueryContext(ctx, cte+` SELECT u.user_id, COALESCE(u.default_display_user_id, ''), COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''), u.status, COALESCE(u.region_id, 0), GROUP_CONCAT(DISTINCT tr.role ORDER BY FIELD(tr.role, 'bd', 'agency', 'host') SEPARATOR ','), MAX(tr.parent_owner_user_id), MAX(tr.parent_bd_user_id), MAX(tr.agency_id) FROM team_relations tr INNER JOIN users u ON u.app_code = ? AND u.user_id = tr.user_id WHERE u.region_id = ? AND u.status = ?`+filter+` GROUP BY u.user_id, u.default_display_user_id, u.current_display_user_id, u.username, u.avatar, u.status, u.region_id ORDER BY MAX(u.updated_at_ms) DESC, u.user_id DESC LIMIT ? OFFSET ?`, listArgs..., ) if err != nil { return hostdomain.ExternalTeamUsers{}, err } defer rows.Close() users := make([]hostdomain.ExternalTeamUser, 0, command.PageSize) for rows.Next() { var user hostdomain.ExternalTeamUser var roles string if err := rows.Scan( &user.UserID, &user.DefaultDisplayUserID, &user.CurrentDisplayUserID, &user.Username, &user.Avatar, &user.Status, &user.RegionID, &roles, &user.ParentOwnerUserID, &user.ParentBDUserID, &user.AgencyID, ); err != nil { return hostdomain.ExternalTeamUsers{}, err } if roles != "" { user.Roles = strings.Split(roles, ",") } users = append(users, user) } if err := rows.Err(); err != nil { return hostdomain.ExternalTeamUsers{}, err } return hostdomain.ExternalTeamUsers{Users: users, Total: total, Page: command.Page, PageSize: command.PageSize}, nil } func externalTeamCTE(ctx context.Context, ownerUserIDs []int64, regionID int64) (string, []any) { args := make([]any, 0, len(ownerUserIDs)+9) args = append(args, regionID, regionID, appcode.FromContext(ctx)) for _, userID := range ownerUserIDs { args = append(args, userID) } args = append(args, hostdomain.BDStatusActive, appcode.FromContext(ctx), hostdomain.AgencyStatusActive, appcode.FromContext(ctx), hostdomain.MembershipStatusActive, ) query := ` WITH scoped_bd AS ( SELECT b.user_id AS bd_user_id, b.parent_owner_user_id FROM bd_profiles b INNER JOIN users scope_owner ON scope_owner.app_code = b.app_code AND scope_owner.user_id = b.parent_owner_user_id AND scope_owner.status = 'active' AND scope_owner.region_id = ? INNER JOIN users bd_user ON bd_user.app_code = b.app_code AND bd_user.user_id = b.user_id AND bd_user.region_id = ? WHERE b.app_code = ? AND b.parent_owner_user_id IN (` + placeholderList(len(ownerUserIDs)) + `) AND b.parent_leader_user_id IS NULL AND b.status = ? ), scoped_agencies AS ( SELECT a.agency_id, a.owner_user_id, a.parent_bd_user_id, sb.parent_owner_user_id FROM scoped_bd sb INNER JOIN agencies a ON a.app_code = ? AND a.parent_bd_user_id = sb.bd_user_id AND a.status = ? ), team_relations AS ( SELECT bd_user_id AS user_id, 'bd' AS role, parent_owner_user_id, bd_user_id AS parent_bd_user_id, 0 AS agency_id FROM scoped_bd UNION ALL SELECT owner_user_id, 'agency', parent_owner_user_id, parent_bd_user_id, agency_id FROM scoped_agencies UNION ALL SELECT m.host_user_id, 'host', sa.parent_owner_user_id, sa.parent_bd_user_id, sa.agency_id FROM scoped_agencies sa INNER JOIN agency_memberships m ON m.app_code = ? AND m.agency_id = sa.agency_id AND m.status = ? AND m.membership_type = 'member' )` return query, args } func externalTeamFilter(command hostservice.ListExternalTeamUsersCommand) (string, []any) { filter := "" args := make([]any, 0, 5) if command.Role != "" { filter += " AND tr.role = ?" args = append(args, command.Role) } if command.Keyword != "" { filter += " AND (u.user_id = ? OR u.current_display_user_id = ? OR u.default_display_user_id = ? OR u.username LIKE ?)" args = append(args, int64FromKeyword(command.Keyword), command.Keyword, command.Keyword, "%"+command.Keyword+"%") } return filter, args } // SearchExternalInvitationCandidate 只执行最多四个有唯一索引支撑的精确查询; // 不对 username 做 LIKE,避免候选接口演变成区域用户目录。 func (r *Repository) SearchExternalInvitationCandidate(ctx context.Context, command hostservice.SearchExternalInvitationCandidateCommand) (hostdomain.ExternalTeamUser, error) { operatorRegionID, err := currentUserRegion(ctx, r.db, command.OperatorUserID, "") if err != nil { return hostdomain.ExternalTeamUser{}, err } if operatorRegionID <= 0 || operatorRegionID != command.ExpectedRegionID { return hostdomain.ExternalTeamUser{}, xerr.New(xerr.RegionMismatch, "external operator region has changed") } user, err := r.findExactExternalCandidate(ctx, command.ExpectedRegionID, command.Keyword, command.NowMs) if err != nil { return hostdomain.ExternalTeamUser{}, err } if err := r.requireCandidateRoleAvailable(ctx, command.InvitationType, user.UserID); err != nil { return hostdomain.ExternalTeamUser{}, err } return user, nil } func (r *Repository) findExactExternalCandidate(ctx context.Context, regionID int64, keyword string, nowMs int64) (hostdomain.ExternalTeamUser, error) { type lookup struct { clause string args []any } lookups := make([]lookup, 0, 3) if userID, err := strconv.ParseInt(keyword, 10, 64); err == nil && userID > 0 { lookups = append(lookups, lookup{clause: "user_id = ?", args: []any{userID}}) } lookups = append(lookups, lookup{ // users.current_display_user_id 是读取快照;过期 pretty 快照不能继续命中候选人。 clause: `current_display_user_id = ? AND (current_display_user_id_kind <> 'pretty' OR current_display_user_id_expires_at_ms IS NULL OR current_display_user_id_expires_at_ms = 0 OR current_display_user_id_expires_at_ms > ?)`, args: []any{keyword, nowMs}, }, lookup{clause: "default_display_user_id = ?", args: []any{keyword}}, ) for _, candidate := range lookups { var user hostdomain.ExternalTeamUser args := []any{appcode.FromContext(ctx), regionID} args = append(args, candidate.args...) err := r.db.QueryRowContext(ctx, ` SELECT user_id, COALESCE(default_display_user_id, ''), COALESCE(current_display_user_id, ''), COALESCE(username, ''), COALESCE(avatar, ''), status, COALESCE(region_id, 0) FROM users WHERE app_code = ? AND region_id = ? AND status = 'active' AND `+candidate.clause+` LIMIT 1`, args..., ).Scan( &user.UserID, &user.DefaultDisplayUserID, &user.CurrentDisplayUserID, &user.Username, &user.Avatar, &user.Status, &user.RegionID, ) if err == nil { return user, nil } if err != sql.ErrNoRows { return hostdomain.ExternalTeamUser{}, err } } // active lease 是靓号归属事实,不能只依赖 users.current_display_user_id 快照; // active_display_user_id 生成列使该精确查询命中租约唯一索引,不扫描区域用户。 var user hostdomain.ExternalTeamUser err := r.db.QueryRowContext(ctx, ` SELECT u.user_id, COALESCE(u.default_display_user_id, ''), COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''), u.status, COALESCE(u.region_id, 0) FROM pretty_display_user_id_leases lease FORCE INDEX (uk_pretty_display_user_id_active) INNER JOIN users u ON u.user_id = lease.user_id AND u.app_code = lease.app_code WHERE lease.app_code = ? AND lease.active_display_user_id = ? AND (lease.expires_at_ms = 0 OR lease.expires_at_ms > ?) AND u.region_id = ? AND u.status = 'active' LIMIT 1`, appcode.FromContext(ctx), keyword, nowMs, regionID, ).Scan( &user.UserID, &user.DefaultDisplayUserID, &user.CurrentDisplayUserID, &user.Username, &user.Avatar, &user.Status, &user.RegionID, ) if err == nil { return user, nil } if err != sql.ErrNoRows { return hostdomain.ExternalTeamUser{}, err } return hostdomain.ExternalTeamUser{}, xerr.New(xerr.NotFound, "external invitation candidate not found") } func (r *Repository) requireCandidateRoleAvailable(ctx context.Context, invitationType string, userID int64) error { var query string switch invitationType { case hostdomain.InvitationTypeBD: query = `SELECT ( EXISTS(SELECT 1 FROM bd_profiles WHERE app_code = ? AND user_id = ? AND status = 'active') OR EXISTS(SELECT 1 FROM role_invitations WHERE app_code = ? AND target_user_id = ? AND status = 'pending' AND invitation_type IN ('bd', 'external_bd')) )` case hostdomain.InvitationTypeAgency: query = `SELECT ( EXISTS(SELECT 1 FROM agencies WHERE app_code = ? AND owner_user_id = ? AND status = 'active') OR EXISTS(SELECT 1 FROM role_invitations WHERE app_code = ? AND target_user_id = ? AND status = 'pending' AND invitation_type IN ('agency', 'external_agency')) )` case hostdomain.InvitationTypeHost: query = `SELECT ( EXISTS(SELECT 1 FROM agencies WHERE app_code = ? AND owner_user_id = ? AND status = 'active') OR EXISTS(SELECT 1 FROM agency_memberships WHERE app_code = ? AND host_user_id = ? AND status = 'active') OR EXISTS(SELECT 1 FROM role_invitations WHERE app_code = ? AND target_user_id = ? AND status = 'pending' AND invitation_type IN ('host', 'external_host')) )` default: return xerr.New(xerr.InvalidArgument, "invitation_type is invalid") } var exists bool args := []any{appcode.FromContext(ctx), userID, appcode.FromContext(ctx), userID} if invitationType == hostdomain.InvitationTypeHost { args = append(args, appcode.FromContext(ctx), userID) } if err := r.db.QueryRowContext(ctx, query, args...).Scan(&exists); err != nil { return err } if exists { return xerr.New(xerr.Conflict, fmt.Sprintf("target already has active %s identity", invitationType)) } return nil }