713 lines
32 KiB
Go
713 lines
32 KiB
Go
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
|
||
}
|