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 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 err := requireActiveRegion(ctx, tx, regionID); err != nil { return hostdomain.BDProfile{}, err } 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.BDRoleLeader, RegionID: regionID, 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.CommandTypeCreateBDLeader, 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 } // 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, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentLeaderUserID) if err != nil { return hostdomain.BDProfile{}, err } if leader.Status != hostdomain.BDStatusActive || leader.Role != hostdomain.BDRoleLeader { 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 只改 BD 状态,不迁移角色、区域和上级负责人。 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 } return queryBDProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID) } before, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID) if err != nil { return hostdomain.BDProfile{}, err } after := before after.Status = command.Status after.UpdatedAtMs = command.NowMs 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 时才校验 BD 启用状态和同区域归属。 parentBD, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentBDUserID) if err != nil { return hostdomain.CreateAgencyResult{}, err } if parentBD.Status != hostdomain.BDStatusActive { return hostdomain.CreateAgencyResult{}, xerr.New(xerr.PermissionDenied, "parent bd is not active") } 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") } if _, ok, err := queryActiveMembershipByHost(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 membership") } // max_hosts 是历史兼容字段,当前业务不再限制主播数量,新建 Agency 固化为 0。 agency := hostdomain.Agency{AgencyID: command.AgencyID, OwnerUserID: command.OwnerUserID, RegionID: regionID, ParentBDUserID: command.ParentBDUserID, Name: command.Name, 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) } hostProfile, created, 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) } 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 created { 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 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 } // 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 } // 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 }) } 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 } membership, err := queryAgencyMembership(ctx, q, "WHERE agency_id = ? AND host_user_id = ? AND membership_type = ? AND status = ?", agency.AgencyID, agency.OwnerUserID, hostdomain.MembershipTypeOwner, hostdomain.MembershipStatusActive) if err != nil { return hostdomain.CreateAgencyResult{}, err } return hostdomain.CreateAgencyResult{Agency: agency, HostProfile: hostProfile, Membership: membership}, nil }