538 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package host
import (
"context"
"database/sql"
"hyapp/pkg/xerr"
hostdomain "hyapp/services/user-service/internal/domain/host"
hostservice "hyapp/services/user-service/internal/service/host"
)
// InviteAgency 创建 Agency 角色邀请,校验 inviter BD 身份和目标用户区域。
func (r *Repository) InviteAgency(ctx context.Context, command hostservice.InviteAgencyCommand) (hostdomain.RoleInvitation, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return hostdomain.RoleInvitation{}, err
}
defer tx.Rollback()
// 邀请是可重试写命令;命中幂等结果时返回既有邀请,
// 不重新校验当前 BD/用户状态,保持“已提交结果可回放”的语义。
if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
if err := requireCommandResult(existing, hostdomain.CommandTypeInviteAgency, hostdomain.ResultTypeInvitation); err != nil {
return hostdomain.RoleInvitation{}, err
}
return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
}
// InviteAgency 没有显式角色字段;普通 BD 行优先承载 BD Center 邀请,普通 BD 不可用时再使用 Leader 行承载负责人直属邀请。
// 两种身份都用 FOR UPDATE 串行化停用/换区和邀请创建,避免旧权限在同一事务窗口内继续写入。
inviter, err := queryActiveAgencyInviterProfile(ctx, tx, command.InviterUserID)
if err != nil {
return hostdomain.RoleInvitation{}, err
}
// 目标用户还不是 Host/Agency 时没有领域表行,所以锁 users 行来串行化目标身份变更。
targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
if err != nil {
return hostdomain.RoleInvitation{}, err
}
if targetRegionID != inviter.RegionID {
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "target region does not match inviter region")
}
// Agency 拥有者必须没有当前有效 Agency 归属;已有 Host 身份但未归属 Agency 时可以复用身份。
if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.TargetUserID, true); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active agency membership")
}
// 一个用户只能拥有一个有效 Agency这里先拦截最终仍依赖唯一索引兜底并发。
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")
}
// BD 发出的 Agency 邀请要记录直接上级 BD 和负责人链路;
// 如果本次以负责人身份邀请,则直接 BD 和负责人都指向 Leader 自己。
parentLeaderUserID := inviter.ParentLeaderUserID
if inviter.Role == hostdomain.BDRoleLeader {
parentLeaderUserID = inviter.UserID
}
invitation := hostdomain.RoleInvitation{
InvitationID: command.InvitationID,
CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeAgency,
Status: hostdomain.InvitationStatusPending,
InviterUserID: command.InviterUserID,
InviterBDUserID: inviter.UserID,
TargetUserID: command.TargetUserID,
RegionID: targetRegionID,
AgencyName: command.AgencyName,
ParentBDUserID: inviter.UserID,
ParentLeaderUserID: parentLeaderUserID,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
// 邀请创建只落 pending 消息Agency、owner Host 和 owner membership 必须等目标用户接受时再同事务创建。
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
}
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteAgency,
ResultType: hostdomain.ResultTypeInvitation,
ResultID: invitation.InvitationID,
CreatedAtMs: command.NowMs,
}); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
}
if err := tx.Commit(); err != nil {
return hostdomain.RoleInvitation{}, err
}
return invitation, nil
}
// InviteBD 创建 BD Leader 到 BD 的邀请。
func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDCommand) (hostdomain.RoleInvitation, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return hostdomain.RoleInvitation{}, err
}
defer tx.Rollback()
// BD 邀请也以 command_id 去重;已成功的邀请不会因为负责人后续状态变化而回放失败。
if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
if err := requireCommandResult(existing, hostdomain.CommandTypeInviteBD, hostdomain.ResultTypeInvitation); err != nil {
return hostdomain.RoleInvitation{}, err
}
return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
}
// 只有有效 BD 负责人能发展 BD锁住独立 Leader 表行让停用和邀请创建串行。
inviter, err := queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.InviterUserID)
if err != nil {
return hostdomain.RoleInvitation{}, err
}
if inviter.Status != hostdomain.BDStatusActive {
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter is not active bd leader")
}
// BD 从属于负责人的区域,邀请创建时先锁目标用户当前区域。
targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
if err != nil {
return hostdomain.RoleInvitation{}, err
}
if targetRegionID != inviter.RegionID {
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "target region does not match leader region")
}
// 目标已有普通 BD 身份行时不再创建新邀请,即使已停用也不能再插入第二条普通 BD 事实。
// BD Leader 自邀也必须新增普通 BD 行,后续才能获得 BD_SALARY_USD 钱包鉴权。
if _, ok, err := queryBDProfileByUserMaybe(ctx, tx, command.TargetUserID, true); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has bd profile")
}
invitation := hostdomain.RoleInvitation{
InvitationID: command.InvitationID,
CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeBD,
Status: hostdomain.InvitationStatusPending,
InviterUserID: command.InviterUserID,
InviterBDUserID: inviter.UserID,
TargetUserID: command.TargetUserID,
RegionID: targetRegionID,
ParentLeaderUserID: inviter.UserID,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
// 新 BD 邀请只产生待确认消息BD 身份要等目标用户接受后才创建。
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
}
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteBD,
ResultType: hostdomain.ResultTypeInvitation,
ResultID: invitation.InvitationID,
CreatedAtMs: command.NowMs,
}); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
}
if err := tx.Commit(); err != nil {
return hostdomain.RoleInvitation{}, err
}
return invitation, nil
}
// InviteHost 创建 Agency owner 到目标用户的 Host 入会邀请。
func (r *Repository) InviteHost(ctx context.Context, command hostservice.InviteHostCommand) (hostdomain.RoleInvitation, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return hostdomain.RoleInvitation{}, err
}
defer tx.Rollback()
// 发送邀请是独立写命令;重试只回放同一条 pending/终态邀请,不重新读取 owner 当前权限。
if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
if err := requireCommandResult(existing, hostdomain.CommandTypeInviteHost, hostdomain.ResultTypeInvitation); err != nil {
return hostdomain.RoleInvitation{}, err
}
return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
}
// Agency owner 是邀请 Host 的唯一授权身份;锁 active agency 行可以让关闭、删除、禁入和邀请创建串行。
agency, ok, err := queryActiveAgencyByOwner(ctx, tx, command.InviterUserID, true)
if err != nil {
return hostdomain.RoleInvitation{}, err
}
if !ok || !agency.JoinEnabled {
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter agency is not active or joinable")
}
// 目标用户可能还没有 host_profile所以用 users 行作为目标身份变化的串行化锁。
targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
if err != nil {
return hostdomain.RoleInvitation{}, err
}
if targetRegionID != agency.RegionID {
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "target region does not match agency region")
}
// Host 入会邀请不能绕过“一人一个有效 Agency 归属”的硬约束,接受时还会再次校验当前状态。
if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.TargetUserID, true); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active agency membership")
}
if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil {
return hostdomain.RoleInvitation{}, err
} else if ok {
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already owns active agency")
}
invitation := hostdomain.RoleInvitation{
InvitationID: command.InvitationID,
CommandID: command.CommandID,
InvitationType: hostdomain.InvitationTypeHost,
Status: hostdomain.InvitationStatusPending,
InviterUserID: command.InviterUserID,
InviterBDUserID: agency.ParentBDUserID,
TargetUserID: command.TargetUserID,
RegionID: targetRegionID,
AgencyName: agency.Name,
ParentBDUserID: agency.ParentBDUserID,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
}
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeInviteHost,
ResultType: hostdomain.ResultTypeInvitation,
ResultID: invitation.InvitationID,
CreatedAtMs: command.NowMs,
}); err != nil {
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
}
if err := tx.Commit(); err != nil {
return hostdomain.RoleInvitation{}, err
}
return invitation, nil
}
func queryActiveAgencyInviterProfile(ctx context.Context, tx *sql.Tx, userID int64) (hostdomain.BDProfile, error) {
if profile, ok, err := queryBDProfileMaybe(ctx, tx, userID, true); err != nil || ok {
return profile, err
}
if profile, ok, err := queryBDLeaderProfileMaybe(ctx, tx, userID, true); err != nil || ok {
return profile, err
}
return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "inviter bd is not active")
}
// ProcessRoleInvitation 处理角色邀请,接受路径在同一事务内创建 Host/Agency/BD 角色事实。
func (r *Repository) ProcessRoleInvitation(ctx context.Context, command hostservice.ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
defer tx.Rollback()
// 接受、拒绝、取消共用同一个处理命令类型;重试时通过 invitation_id 重建结果,
// 不重复创建 Agency、成员关系或 BD 身份行。
if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
} else if ok {
if err := requireCommandResult(existing, hostdomain.CommandTypeProcessRoleInvitation, hostdomain.ResultTypeInvitation); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
return processResultByInvitationID(ctx, tx, existing.ResultID)
}
// 邀请行是邀请状态机的唯一锁;待处理状态只能被推进一次。
invitation, err := queryRoleInvitation(ctx, tx, "WHERE invitation_id = ? FOR UPDATE", command.InvitationID)
if err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
if invitation.Status != hostdomain.InvitationStatusPending {
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.Conflict, "invitation is not pending")
}
// 取消属于发起人撤回;接受/拒绝属于目标用户处理。
// 这两个授权分支不要下沉到传输层,否则批处理或内部调用会绕过规则。
if command.Action == hostdomain.InvitationActionCancel && command.ActorUserID != invitation.InviterUserID {
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.PermissionDenied, "only inviter can cancel invitation")
}
if command.Action != hostdomain.InvitationActionCancel && command.ActorUserID != invitation.TargetUserID {
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.PermissionDenied, "only target can process invitation")
}
invitation.ProcessedByUserID = command.ActorUserID
invitation.ProcessReason = command.Reason
invitation.ProcessedAtMs = command.NowMs
invitation.UpdatedAtMs = command.NowMs
result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}
switch command.Action {
case hostdomain.InvitationActionReject:
// 拒绝只改变邀请终态,不创建任何角色事实。
invitation.Status = hostdomain.InvitationStatusRejected
case hostdomain.InvitationActionCancel:
// 取消同样只关闭邀请,用于邀请人在目标处理前撤回。
invitation.Status = hostdomain.InvitationStatusCancelled
case hostdomain.InvitationActionAccept:
invitation.Status = hostdomain.InvitationStatusAccepted
if invitation.InvitationType == hostdomain.InvitationTypeAgency {
// Agency 邀请的接受会一次性创建 Agency 拥有者身份、Agency 和拥有者成员关系。
hostProfileCreated, err := r.acceptAgencyInvitation(ctx, tx, command, &invitation, &result)
if err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
// 事件 ID 在业务层预分配;这里按固定偏移写多条事件箱记录,
// 让同一个接受事务中的派生事件保持可追踪顺序。
if hostProfileCreated {
if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
}
if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyCreated", "agency", command.AgencyID, command.NowMs); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
if err := insertHostOutbox(ctx, tx, command.EventID+2, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
} else if invitation.InvitationType == hostdomain.InvitationTypeBD {
// BD 邀请的接受只创建 bd_profileBD 不需要 host_profile。
if err := r.acceptBDInvitation(ctx, tx, command, &invitation, &result); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
} else if invitation.InvitationType == hostdomain.InvitationTypeHost {
// Host 邀请只创建或复用 Host 身份,并把当前成员关系挂到发起 Agency。
hostProfileCreated, err := r.acceptHostInvitation(ctx, tx, command, &invitation, &result)
if err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
if hostProfileCreated {
if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
}
if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
} else {
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "invitation type is invalid")
}
}
// 邀请终态必须在角色事实之后更新;如果事实创建失败,待处理状态保留给后续重试/处理。
if err := updateRoleInvitationProcessed(ctx, tx, invitation); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
result.Invitation = invitation
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
CommandID: command.CommandID,
CommandType: hostdomain.CommandTypeProcessRoleInvitation,
ResultType: hostdomain.ResultTypeInvitation,
ResultID: invitation.InvitationID,
CreatedAtMs: command.NowMs,
}); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, mapHostDuplicateError(err)
}
if err := tx.Commit(); err != nil {
return hostdomain.ProcessRoleInvitationResult{}, err
}
return result, nil
}
func (r *Repository) acceptAgencyInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) (bool, error) {
// 邀请可能放置一段时间后才被接受,所以目标用户和邀请 BD 都用 users 当前区域重新对齐。
// 邀请表不再保存区域快照,旧库残留的 region_id 也不能参与是否可接受的业务判断。
targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
if err != nil {
return false, err
}
inviterRegionID, err := userRegionValue(ctx, tx, invitation.InviterBDUserID, "")
if err != nil {
return false, err
}
if targetRegionID != inviterRegionID {
return false, xerr.New(xerr.PermissionDenied, "target region no longer matches invitation")
}
// 目标用户在邀请创建后可能已经加入其他 Agency接受前必须再次阻断有效归属。
if _, ok, err := queryActiveMembershipByHost(ctx, tx, invitation.TargetUserID, true); err != nil {
return false, err
} else if ok {
return false, xerr.New(xerr.Conflict, "target already has active agency membership")
}
// 同一用户不能拥有多个有效 Agency这里和唯一索引共同处理并发接受。
if _, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.TargetUserID, true); err != nil {
return false, err
} else if ok {
return false, xerr.New(xerr.Conflict, "target already owns active agency")
}
// Agency、拥有者 host_profile、拥有者成员关系是同一业务事实的三张表投影
// 必须全部创建成功后邀请才能进入已接受状态。
agency := hostdomain.Agency{
AgencyID: command.AgencyID,
OwnerUserID: invitation.TargetUserID,
RegionID: targetRegionID,
ParentBDUserID: invitation.ParentBDUserID,
Name: invitation.AgencyName,
Status: hostdomain.AgencyStatusActive,
JoinEnabled: true,
// max_hosts 是历史兼容字段,当前业务不再限制主播数量。
MaxHosts: 0,
CreatedByUserID: invitation.InviterUserID,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
if err := insertAgency(ctx, tx, agency); err != nil {
return false, mapHostDuplicateError(err)
}
hostProfile, hostProfileCreated, err := ensureHostProfileForMembership(ctx, tx, invitation.TargetUserID, targetRegionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceAgencyInvitation, command.NowMs)
if err != nil {
return false, err
}
membership := hostdomain.AgencyMembership{
MembershipID: command.MembershipID,
AgencyID: agency.AgencyID,
HostUserID: invitation.TargetUserID,
RegionID: targetRegionID,
MembershipType: hostdomain.MembershipTypeOwner,
Status: hostdomain.MembershipStatusActive,
JoinedAtMs: command.NowMs,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
if err := insertAgencyMembership(ctx, tx, membership); err != nil {
return false, mapHostDuplicateError(err)
}
result.HostProfile = hostProfile
result.Agency = agency
result.Membership = membership
return hostProfileCreated, nil
}
func (r *Repository) acceptHostInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) (bool, error) {
// Host 邀请绑定的是发起人当前 active Agency而不是客户端传入的 agency_id
// 这样邀请消息无法被目标用户篡改成加入其他团队。
agency, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.InviterUserID, true)
if err != nil {
return false, err
}
if !ok || !agency.JoinEnabled {
return false, xerr.New(xerr.Conflict, "inviter agency is no longer active or joinable")
}
targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
if err != nil {
return false, err
}
if targetRegionID != agency.RegionID {
return false, xerr.New(xerr.PermissionDenied, "target region no longer matches agency")
}
// 接受时再次检查有效归属和 Agency owner 身份,覆盖邀请发出后用户已经加入或创建团队的情况。
if _, ok, err := queryActiveMembershipByHost(ctx, tx, invitation.TargetUserID, true); err != nil {
return false, err
} else if ok {
return false, xerr.New(xerr.Conflict, "target already has active agency membership")
}
if _, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.TargetUserID, true); err != nil {
return false, err
} else if ok {
return false, xerr.New(xerr.Conflict, "target already owns active agency")
}
hostProfile, hostProfileCreated, err := ensureHostProfileForMembership(ctx, tx, invitation.TargetUserID, targetRegionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceAgencyInvitation, command.NowMs)
if err != nil {
return false, err
}
membership := hostdomain.AgencyMembership{
MembershipID: command.MembershipID,
AgencyID: agency.AgencyID,
HostUserID: invitation.TargetUserID,
RegionID: targetRegionID,
MembershipType: hostdomain.MembershipTypeMember,
Status: hostdomain.MembershipStatusActive,
JoinedAtMs: command.NowMs,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
if err := insertAgencyMembership(ctx, tx, membership); err != nil {
return false, mapHostDuplicateError(err)
}
result.HostProfile = hostProfile
result.Agency = agency
result.Membership = membership
return hostProfileCreated, nil
}
func (r *Repository) acceptBDInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) error {
// BD 邀请同样以目标用户和发起 Leader 的 users 当前区域为准;旧邀请快照不能继续授权跨区接受。
targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
if err != nil {
return err
}
leaderRegionID, err := userRegionValue(ctx, tx, invitation.InviterBDUserID, "")
if err != nil {
return err
}
if targetRegionID != leaderRegionID {
return xerr.New(xerr.PermissionDenied, "target region no longer matches invitation")
}
// BD 身份是独立角色事实,不依赖 host_profile任何已有普通 BD 行都不能重复插入。
if _, ok, err := queryBDProfileByUserMaybe(ctx, tx, invitation.TargetUserID, true); err != nil {
return err
} else if ok {
return xerr.New(xerr.Conflict, "target already has bd profile")
}
bd := hostdomain.BDProfile{
UserID: invitation.TargetUserID,
Role: hostdomain.BDRoleBD,
RegionID: targetRegionID,
ParentLeaderUserID: invitation.ParentLeaderUserID,
Status: hostdomain.BDStatusActive,
CreatedByUserID: invitation.InviterUserID,
CreatedAtMs: command.NowMs,
UpdatedAtMs: command.NowMs,
}
if err := insertBDProfile(ctx, tx, bd); err != nil {
return mapHostDuplicateError(err)
}
result.BDProfile = bd
return nil
}