package host import ( "context" "fmt" "hyapp/pkg/appcode" "hyapp/pkg/xerr" hostdomain "hyapp/services/user-service/internal/domain/host" hostservice "hyapp/services/user-service/internal/service/host" ) // CreateBDLeader 创建后台直属 BD 负责人;admin 鉴权在 admin-server,这里只保证事实一致性。 func (r *Repository) CreateBDLeader(ctx context.Context, command hostservice.CreateBDLeaderCommand) (hostdomain.BDProfile, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.BDProfile{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.BDProfile{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeCreateBDLeader, hostdomain.ResultTypeBDProfile); err != nil { return hostdomain.BDProfile{}, err } return queryBDLeaderProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } regionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE") if err != nil { return hostdomain.BDProfile{}, err } if err := requireActiveRegion(ctx, tx, regionID); err != nil { return hostdomain.BDProfile{}, err } // admin 和经理入口都是强制开通:已有 active Leader 直接复用,disabled Leader 恢复 active。 // 开通 Leader 后再补齐普通 BD 身份;已有 active BD 时绝不覆盖原 parent_leader_user_id。 profile, leaderEvent, err := ensureActiveBDLeaderProfile(ctx, tx, command.TargetUserID, regionID, command.AdminUserID, command.NowMs) if err != nil { return hostdomain.BDProfile{}, err } if _, bdEvent, err := ensureSelfBDProfileForLeader(ctx, tx, command.TargetUserID, regionID, command.AdminUserID, command.NowMs); err != nil { return hostdomain.BDProfile{}, err } else if bdEvent != "" { if err := insertHostOutbox(ctx, tx, command.EventID+1, bdEvent, "bd_profile", command.TargetUserID, command.NowMs); err != nil { return hostdomain.BDProfile{}, err } } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeCreateBDLeader, ResultType: hostdomain.ResultTypeBDProfile, ResultID: profile.UserID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.BDProfile{}, mapHostDuplicateError(err) } if leaderEvent != "" { if err := insertHostOutbox(ctx, tx, command.EventID, leaderEvent, "bd_profile", profile.UserID, command.NowMs); err != nil { return hostdomain.BDProfile{}, err } } if err := tx.Commit(); err != nil { return hostdomain.BDProfile{}, err } return profile, nil } // CreateBD 创建普通 BD;父级 Leader 可选,填入时必须是同区域有效 Leader。 func (r *Repository) CreateBD(ctx context.Context, command hostservice.CreateBDCommand) (hostdomain.BDProfile, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.BDProfile{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.BDProfile{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeCreateBD, hostdomain.ResultTypeBDProfile); err != nil { return hostdomain.BDProfile{}, err } return queryBDProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } regionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE") if err != nil { return hostdomain.BDProfile{}, err } if command.ParentLeaderUserID > 0 { // 独立 BD 不需要父级行锁;只有传入 Leader 时才锁定独立 Leader 表并校验启用状态。 // Leader 的 RegionID 来自其 users 当前区域投影,不再相信 bd_leader_profiles 的历史快照。 leader, err := queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentLeaderUserID) if err != nil { return hostdomain.BDProfile{}, err } if leader.Status != hostdomain.BDStatusActive { return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "parent leader is not active") } if regionID != leader.RegionID { return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "target region does not match leader region") } } if _, ok, err := queryBDProfileByUserMaybe(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.BDProfile{}, err } else if ok { return hostdomain.BDProfile{}, xerr.New(xerr.Conflict, "target already has bd profile") } profile := hostdomain.BDProfile{ UserID: command.TargetUserID, Role: hostdomain.BDRoleBD, RegionID: regionID, ParentLeaderUserID: command.ParentLeaderUserID, Status: hostdomain.BDStatusActive, CreatedByUserID: command.AdminUserID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, } if err := insertBDProfile(ctx, tx, profile); err != nil { return hostdomain.BDProfile{}, mapHostDuplicateError(err) } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeCreateBD, ResultType: hostdomain.ResultTypeBDProfile, ResultID: profile.UserID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.BDProfile{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", profile.UserID, command.NowMs); err != nil { return hostdomain.BDProfile{}, err } if err := tx.Commit(); err != nil { return hostdomain.BDProfile{}, err } return profile, nil } // SetBDStatus 按调用方明确传入的角色只改对应身份状态,不迁移角色、区域和上级负责人。 func (r *Repository) SetBDStatus(ctx context.Context, command hostservice.SetBDStatusCommand) (hostdomain.BDProfile, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.BDProfile{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.BDProfile{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeSetBDStatus, hostdomain.ResultTypeBDProfile); err != nil { return hostdomain.BDProfile{}, err } if command.Role == hostdomain.BDRoleLeader { return queryBDLeaderProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } return queryBDProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } var before hostdomain.BDProfile switch command.Role { case hostdomain.BDRoleBD: // 普通 BD 和 Leader 已经拆表;状态路由传 bd 时只锁普通 BD 行。 before, err = queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID) case hostdomain.BDRoleLeader: // 状态路由传 bd_leader 时只锁 Leader 行,避免同用户的普通 BD 钱包权限被误停。 before, err = queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID) default: err = xerr.New(xerr.InvalidArgument, "bd role is invalid") } if err != nil { return hostdomain.BDProfile{}, err } after := before after.Status = command.Status after.UpdatedAtMs = command.NowMs if command.Role == hostdomain.BDRoleLeader { if err := updateBDLeaderProfileStatus(ctx, tx, after); err != nil { return hostdomain.BDProfile{}, err } } else { if err := updateBDProfileStatus(ctx, tx, after); err != nil { return hostdomain.BDProfile{}, err } } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeSetBDStatus, ResultType: hostdomain.ResultTypeBDProfile, ResultID: after.UserID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.BDProfile{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "BDStatusChanged", "bd_profile", after.UserID, command.NowMs); err != nil { return hostdomain.BDProfile{}, err } if err := tx.Commit(); err != nil { return hostdomain.BDProfile{}, err } return after, nil } // CreateCoinSeller 创建币商身份;币商可以叠加在 Agency/BD/BD Leader 用户上。 func (r *Repository) CreateCoinSeller(ctx context.Context, command hostservice.CreateCoinSellerCommand) (hostdomain.CoinSellerProfile, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.CoinSellerProfile{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.CoinSellerProfile{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeCreateCoinSeller, hostdomain.ResultTypeCoinSellerProfile); err != nil { return hostdomain.CoinSellerProfile{}, err } return queryCoinSellerProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } if _, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE"); err != nil { return hostdomain.CoinSellerProfile{}, err } if _, ok, err := queryCoinSellerProfileByUserMaybe(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.CoinSellerProfile{}, err } else if ok { return hostdomain.CoinSellerProfile{}, xerr.New(xerr.Conflict, "target already has coin_seller profile") } profile := hostdomain.CoinSellerProfile{ UserID: command.TargetUserID, Status: hostdomain.CoinSellerStatusActive, MerchantAssetType: hostdomain.CoinSellerMerchantAssetType, CreatedByUserID: command.AdminUserID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs, } if err := insertCoinSellerProfile(ctx, tx, profile); err != nil { return hostdomain.CoinSellerProfile{}, mapHostDuplicateError(err) } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeCreateCoinSeller, ResultType: hostdomain.ResultTypeCoinSellerProfile, ResultID: profile.UserID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.CoinSellerProfile{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "CoinSellerCreated", "coin_seller_profile", profile.UserID, command.NowMs); err != nil { return hostdomain.CoinSellerProfile{}, err } if err := tx.Commit(); err != nil { return hostdomain.CoinSellerProfile{}, err } return profile, nil } // SetCoinSellerStatus 启用或停用币商身份,不改币商钱包余额。 func (r *Repository) SetCoinSellerStatus(ctx context.Context, command hostservice.SetCoinSellerStatusCommand) (hostdomain.CoinSellerProfile, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.CoinSellerProfile{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.CoinSellerProfile{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeSetCoinSellerStatus, hostdomain.ResultTypeCoinSellerProfile); err != nil { return hostdomain.CoinSellerProfile{}, err } return queryCoinSellerProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } before, err := queryCoinSellerProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID) if err != nil { return hostdomain.CoinSellerProfile{}, err } after := before after.Status = command.Status after.UpdatedAtMs = command.NowMs if err := updateCoinSellerProfileStatus(ctx, tx, after); err != nil { return hostdomain.CoinSellerProfile{}, err } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeSetCoinSellerStatus, ResultType: hostdomain.ResultTypeCoinSellerProfile, ResultID: after.UserID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.CoinSellerProfile{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "CoinSellerStatusChanged", "coin_seller_profile", after.UserID, command.NowMs); err != nil { return hostdomain.CoinSellerProfile{}, err } if err := tx.Commit(); err != nil { return hostdomain.CoinSellerProfile{}, err } return after, nil } // CreateAgency 后台直建 Agency;父级 BD 可选,填入时必须是同区域有效 BD。 func (r *Repository) CreateAgency(ctx context.Context, command hostservice.CreateAgencyCommand) (hostdomain.CreateAgencyResult, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.CreateAgencyResult{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.CreateAgencyResult{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeCreateAgency, hostdomain.ResultTypeAgency); err != nil { return hostdomain.CreateAgencyResult{}, err } return createAgencyResultByAgencyID(ctx, tx, existing.ResultID) } regionID, err := r.userRegion(ctx, tx, command.OwnerUserID, "FOR UPDATE") if err != nil { return hostdomain.CreateAgencyResult{}, err } if command.ParentBDUserID > 0 { // 独立 Agency 不需要父级行锁;传入父级时可挂普通 BD 或 Leader 自身,但必须是有效身份且同区域。 // 父级 RegionID 来自其 users 当前区域投影,避免角色表旧快照影响新 Agency 归属。 parentBD, err := queryActiveAgencyInviterProfile(ctx, tx, command.ParentBDUserID) if err != nil { return hostdomain.CreateAgencyResult{}, err } if regionID != parentBD.RegionID { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.PermissionDenied, "owner region does not match parent bd region") } } if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.OwnerUserID, true); err != nil { return hostdomain.CreateAgencyResult{}, err } else if ok { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.Conflict, "owner already has active agency") } activeMembership, hasActiveMembership, err := queryActiveMembershipByHost(ctx, tx, command.OwnerUserID, true) if err != nil { return hostdomain.CreateAgencyResult{}, err } agencyName, agencyAvatar, err := defaultAgencyProfileFromOwner(ctx, tx, command.OwnerUserID, command.Name) if err != nil { return hostdomain.CreateAgencyResult{}, err } // max_hosts 是历史兼容字段,当前业务不再限制主播数量,新建 Agency 固化为 0。 agency := hostdomain.Agency{AgencyID: command.AgencyID, OwnerUserID: command.OwnerUserID, RegionID: regionID, ParentBDUserID: command.ParentBDUserID, Name: agencyName, Avatar: agencyAvatar, Status: hostdomain.AgencyStatusActive, JoinEnabled: command.JoinEnabled, MaxHosts: 0, CreatedByUserID: command.AdminUserID, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs} if err := insertAgency(ctx, tx, agency); err != nil { return hostdomain.CreateAgencyResult{}, mapHostDuplicateError(err) } var hostProfile hostdomain.HostProfile var membership hostdomain.AgencyMembership var hostProfileChanged bool var membershipCreated bool if hasActiveMembership { // owner 已经是其他 Agency 的 active Host 时,只开启 Agency owner 身份; // Host 当前归属继续指向原 membership,避免覆盖既有 Agency 关系。 hostProfile, hostProfileChanged, err = ensureHostProfileForExistingMembership(ctx, tx, command.OwnerUserID, regionID, activeMembership, hostdomain.HostSourceAdminCreateAgency, command.NowMs) if err != nil { return hostdomain.CreateAgencyResult{}, err } membership = activeMembership } else { hostProfile, hostProfileChanged, err = ensureHostProfileForMembership(ctx, tx, command.OwnerUserID, regionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceAdminCreateAgency, command.NowMs) if err != nil { return hostdomain.CreateAgencyResult{}, err } membership = hostdomain.AgencyMembership{MembershipID: command.MembershipID, AgencyID: agency.AgencyID, HostUserID: command.OwnerUserID, RegionID: regionID, MembershipType: hostdomain.MembershipTypeOwner, Status: hostdomain.MembershipStatusActive, JoinedAtMs: command.NowMs, CreatedAtMs: command.NowMs, UpdatedAtMs: command.NowMs} if err := insertAgencyMembership(ctx, tx, membership); err != nil { return hostdomain.CreateAgencyResult{}, mapHostDuplicateError(err) } membershipCreated = true } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeCreateAgency, ResultType: hostdomain.ResultTypeAgency, ResultID: agency.AgencyID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.CreateAgencyResult{}, mapHostDuplicateError(err) } if hostProfileChanged { if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", hostProfile.UserID, command.NowMs); err != nil { return hostdomain.CreateAgencyResult{}, err } } if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyCreated", "agency", agency.AgencyID, command.NowMs); err != nil { return hostdomain.CreateAgencyResult{}, err } if membershipCreated { if err := insertHostOutbox(ctx, tx, command.EventID+2, "AgencyMembershipChanged", "agency_membership", membership.MembershipID, command.NowMs); err != nil { return hostdomain.CreateAgencyResult{}, err } } if err := tx.Commit(); err != nil { return hostdomain.CreateAgencyResult{}, err } return hostdomain.CreateAgencyResult{Agency: agency, HostProfile: hostProfile, Membership: membership}, nil } // AdminAddAgencyHost 后台直接添加普通 Host 成员;它复用 Agency 成员关系事实,不走 App 申请/邀请收件箱。 func (r *Repository) AdminAddAgencyHost(ctx context.Context, command hostservice.AdminAddAgencyHostCommand) (hostdomain.CreateAgencyResult, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.CreateAgencyResult{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.CreateAgencyResult{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeAdminAddAgencyHost, hostdomain.ResultTypeMembership); err != nil { return hostdomain.CreateAgencyResult{}, err } return agencyHostResultByMembershipID(ctx, tx, existing.ResultID) } // Agency 是被添加成员的归属 owner;锁住它可以和状态切换、删除 Agency、批量结束成员串行。 agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID) if err != nil { return hostdomain.CreateAgencyResult{}, err } if agency.Status != hostdomain.AgencyStatusActive { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.Conflict, "agency is not active") } // 目标用户区域是 user-service 当前事实;直接添加不相信 admin 传入区域,避免跨区挂错团队。 targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE") if err != nil { return hostdomain.CreateAgencyResult{}, err } if targetRegionID != agency.RegionID { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.RegionMismatch, "target region does not match agency") } if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.CreateAgencyResult{}, err } else if ok { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.Conflict, "target already has active agency membership") } if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil { return hostdomain.CreateAgencyResult{}, err } else if ok { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.Conflict, "target already owns active agency") } hostProfile, hostProfileChanged, err := ensureHostProfileForMembership(ctx, tx, command.TargetUserID, targetRegionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceAdminAddHost, command.NowMs) if err != nil { return hostdomain.CreateAgencyResult{}, err } membership := hostdomain.AgencyMembership{ MembershipID: command.MembershipID, AgencyID: agency.AgencyID, HostUserID: command.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 hostdomain.CreateAgencyResult{}, mapHostDuplicateError(err) } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeAdminAddAgencyHost, ResultType: hostdomain.ResultTypeMembership, ResultID: membership.MembershipID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.CreateAgencyResult{}, mapHostDuplicateError(err) } if hostProfileChanged { if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", hostProfile.UserID, command.NowMs); err != nil { return hostdomain.CreateAgencyResult{}, err } } if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyMembershipChanged", "agency_membership", membership.MembershipID, command.NowMs); err != nil { return hostdomain.CreateAgencyResult{}, err } if err := tx.Commit(); err != nil { return hostdomain.CreateAgencyResult{}, err } return hostdomain.CreateAgencyResult{Agency: agency, HostProfile: hostProfile, Membership: membership}, nil } // CloseAgency 关闭 Agency 本体和入会开关,不回写历史 membership。 func (r *Repository) CloseAgency(ctx context.Context, command hostservice.CloseAgencyCommand) (hostdomain.Agency, error) { return r.updateAgencyAdmin(ctx, command.CommandID, hostdomain.CommandTypeCloseAgency, command.AgencyID, command.EventID, command.NowMs, func(agency hostdomain.Agency) hostdomain.Agency { agency.Status = hostdomain.AgencyStatusClosed agency.JoinEnabled = false return agency }) } // DeleteAgency 删除 Agency 身份;历史 Agency、membership 和 host_profile 都保留,只解除当前有效归属。 func (r *Repository) DeleteAgency(ctx context.Context, command hostservice.DeleteAgencyCommand) (hostdomain.Agency, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.Agency{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.Agency{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeDeleteAgency, hostdomain.ResultTypeAgency); err != nil { return hostdomain.Agency{}, err } return queryAgency(ctx, tx, "WHERE agency_id = ?", existing.ResultID) } agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID) if err != nil { return hostdomain.Agency{}, err } if agency.Status == hostdomain.AgencyStatusDeleted { return hostdomain.Agency{}, xerr.New(xerr.Conflict, "agency already deleted") } activeMemberships, err := queryAgencyMemberships(ctx, tx, fmt.Sprintf(` SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ? AND status = ? FOR UPDATE `, agencyMembershipColumns), appcode.FromContext(ctx), agency.AgencyID, hostdomain.MembershipStatusActive) if err != nil { return hostdomain.Agency{}, err } endReason := command.Reason if endReason == "" { endReason = "delete agency" } for _, membership := range activeMemberships { hostProfile, err := queryHostProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", membership.HostUserID) if err != nil { return hostdomain.Agency{}, err } membership.Status = hostdomain.MembershipStatusEnded membership.EndedAtMs = command.NowMs membership.EndedByUserID = command.AdminUserID membership.EndedReason = endReason membership.UpdatedAtMs = command.NowMs if err := endAgencyMembership(ctx, tx, membership); err != nil { return hostdomain.Agency{}, err } if hostProfile.CurrentMembershipID == membership.MembershipID { // 删除 Agency 只释放当前归属指针,主播身份、来源和首次成为主播时间都保留。 hostProfile.CurrentAgencyID = 0 hostProfile.CurrentMembershipID = 0 hostProfile.UpdatedAtMs = command.NowMs if err := updateHostCurrentMembership(ctx, tx, hostProfile); err != nil { return hostdomain.Agency{}, err } } } agency.Status = hostdomain.AgencyStatusDeleted agency.JoinEnabled = false agency.UpdatedAtMs = command.NowMs if err := updateAgencyAdminState(ctx, tx, agency); err != nil { return hostdomain.Agency{}, err } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeDeleteAgency, ResultType: hostdomain.ResultTypeAgency, ResultID: agency.AgencyID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.Agency{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "AgencyDeleted", "agency", agency.AgencyID, command.NowMs); err != nil { return hostdomain.Agency{}, err } if err := tx.Commit(); err != nil { return hostdomain.Agency{}, err } return agency, nil } // SetAgencyStatus 显式切换 Agency 状态;关闭会同步关入会,恢复只恢复组织本体,避免误把申请入口重新放开。 func (r *Repository) SetAgencyStatus(ctx context.Context, command hostservice.SetAgencyStatusCommand) (hostdomain.Agency, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.Agency{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.Agency{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeSetAgencyStatus, hostdomain.ResultTypeAgency); err != nil { return hostdomain.Agency{}, err } return queryAgency(ctx, tx, "WHERE agency_id = ?", existing.ResultID) } agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID) if err != nil { return hostdomain.Agency{}, err } if agency.Status == hostdomain.AgencyStatusDeleted { // deleted 已经解除当前 Agency 归属,是删除事实;重新激活会破坏 host_profile 当前归属和历史 membership 语义。 return hostdomain.Agency{}, xerr.New(xerr.Conflict, "deleted agency cannot be reopened") } agency.Status = command.Status if command.Status == hostdomain.AgencyStatusClosed { // 关闭组织必须同时停止 App 申请入口;重新 active 时保留当前 join_enabled,让管理员显式决定是否开放入会。 agency.JoinEnabled = false } agency.UpdatedAtMs = command.NowMs if err := updateAgencyAdminState(ctx, tx, agency); err != nil { return hostdomain.Agency{}, mapHostDuplicateError(err) } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeSetAgencyStatus, ResultType: hostdomain.ResultTypeAgency, ResultID: agency.AgencyID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.Agency{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "AgencyChanged", "agency", agency.AgencyID, command.NowMs); err != nil { return hostdomain.Agency{}, err } if err := tx.Commit(); err != nil { return hostdomain.Agency{}, err } return agency, nil } // SetAgencyJoinEnabled 调整 Agency 入会开关。 func (r *Repository) SetAgencyJoinEnabled(ctx context.Context, command hostservice.SetAgencyJoinEnabledCommand) (hostdomain.Agency, error) { return r.updateAgencyAdmin(ctx, command.CommandID, hostdomain.CommandTypeSetAgencyJoinEnabled, command.AgencyID, command.EventID, command.NowMs, func(agency hostdomain.Agency) hostdomain.Agency { agency.JoinEnabled = command.JoinEnabled return agency }) } // UpdateAgencyProfile 只允许 active Agency owner 修改组织独立资料。 func (r *Repository) UpdateAgencyProfile(ctx context.Context, command hostservice.UpdateAgencyProfileCommand) (hostdomain.Agency, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.Agency{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil { return hostdomain.Agency{}, err } else if ok { if err := requireCommandResult(existing, hostdomain.CommandTypeUpdateAgencyProfile, hostdomain.ResultTypeAgency); err != nil { return hostdomain.Agency{}, err } return queryAgency(ctx, tx, "WHERE agency_id = ?", existing.ResultID) } agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID) if err != nil { return hostdomain.Agency{}, err } if agency.Status != hostdomain.AgencyStatusActive || agency.OwnerUserID != command.OperatorUserID { // Agency 资料的写权限只属于当前 active owner;普通成员、关闭团队和历史 owner 都不能改。 return hostdomain.Agency{}, xerr.New(xerr.PermissionDenied, "permission denied") } if command.Name != nil { agency.Name = *command.Name } if command.Avatar != nil { agency.Avatar = *command.Avatar } agency.UpdatedAtMs = command.NowMs if err := updateAgencyProfile(ctx, tx, agency); err != nil { return hostdomain.Agency{}, err } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeUpdateAgencyProfile, ResultType: hostdomain.ResultTypeAgency, ResultID: agency.AgencyID, CreatedAtMs: command.NowMs}); err != nil { return hostdomain.Agency{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, command.EventID, "AgencyChanged", "agency", agency.AgencyID, command.NowMs); err != nil { return hostdomain.Agency{}, err } if err := tx.Commit(); err != nil { return hostdomain.Agency{}, err } return agency, nil } func (r *Repository) updateAgencyAdmin(ctx context.Context, commandID string, commandType string, agencyID int64, eventID int64, nowMs int64, mutate func(hostdomain.Agency) hostdomain.Agency) (hostdomain.Agency, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return hostdomain.Agency{}, err } defer tx.Rollback() if existing, ok, err := commandResult(ctx, tx, commandID); err != nil { return hostdomain.Agency{}, err } else if ok { if err := requireCommandResult(existing, commandType, hostdomain.ResultTypeAgency); err != nil { return hostdomain.Agency{}, err } return queryAgency(ctx, tx, "WHERE agency_id = ?", existing.ResultID) } before, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", agencyID) if err != nil { return hostdomain.Agency{}, err } after := mutate(before) after.UpdatedAtMs = nowMs if err := updateAgencyAdminState(ctx, tx, after); err != nil { return hostdomain.Agency{}, err } if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: commandID, CommandType: commandType, ResultType: hostdomain.ResultTypeAgency, ResultID: after.AgencyID, CreatedAtMs: nowMs}); err != nil { return hostdomain.Agency{}, mapHostDuplicateError(err) } if err := insertHostOutbox(ctx, tx, eventID, "AgencyChanged", "agency", after.AgencyID, nowMs); err != nil { return hostdomain.Agency{}, err } if err := tx.Commit(); err != nil { return hostdomain.Agency{}, err } return after, nil } func createAgencyResultByAgencyID(ctx context.Context, q sqlQueryer, agencyID int64) (hostdomain.CreateAgencyResult, error) { agency, err := queryAgency(ctx, q, "WHERE agency_id = ?", agencyID) if err != nil { return hostdomain.CreateAgencyResult{}, err } hostProfile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", agency.OwnerUserID) if err != nil { return hostdomain.CreateAgencyResult{}, err } var membership hostdomain.AgencyMembership if hostProfile.CurrentMembershipID > 0 { // 新规则允许 Agency owner 已经是其他 Agency 的 Host 成员; // 幂等回放返回用户当前 Host 归属,而不是强行要求新 Agency 存在 owner membership。 membership, _ = queryAgencyMembership(ctx, q, "WHERE membership_id = ?", hostProfile.CurrentMembershipID) } return hostdomain.CreateAgencyResult{Agency: agency, HostProfile: hostProfile, Membership: membership}, nil } func agencyHostResultByMembershipID(ctx context.Context, q sqlQueryer, membershipID int64) (hostdomain.CreateAgencyResult, error) { membership, err := queryAgencyMembership(ctx, q, "WHERE membership_id = ?", membershipID) if err != nil { return hostdomain.CreateAgencyResult{}, err } agency, err := queryAgency(ctx, q, "WHERE agency_id = ?", membership.AgencyID) if err != nil { return hostdomain.CreateAgencyResult{}, err } hostProfile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", membership.HostUserID) if err != nil { return hostdomain.CreateAgencyResult{}, err } return hostdomain.CreateAgencyResult{Agency: agency, HostProfile: hostProfile, Membership: membership}, nil }