2026-05-02 13:02:38 +08:00

407 lines
18 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"
"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)
}
if err := requireActiveRegion(ctx, tx, command.RegionID); err != nil {
return hostdomain.BDProfile{}, err
}
currentRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
if 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")
}
if currentRegionID != command.RegionID {
if err := updateUserRegion(ctx, tx, command.TargetUserID, command.RegionID, command.NowMs); err != nil {
return hostdomain.BDProfile{}, err
}
}
profile := hostdomain.BDProfile{
UserID: command.TargetUserID,
Role: hostdomain.BDRoleLeader,
RegionID: command.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并把它固定到一个有效 BD 负责人名下。
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)
}
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")
}
regionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
if err != nil {
return hostdomain.BDProfile{}, err
}
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并原子创建 owner 的 Host 身份和拥有者成员关系。
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)
}
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")
}
regionID, err := r.userRegion(ctx, tx, command.OwnerUserID, "FOR UPDATE")
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")
}
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")
}
if _, ok, err := queryHostProfileByUserMaybe(ctx, tx, command.OwnerUserID, true); err != nil {
return hostdomain.CreateAgencyResult{}, err
} else if ok {
// 后台直接创建 Agency 会同时创建 owner 的 Host 身份;已存在 Host 身份时拒绝,避免后台把已有主播静默改挂到新 Agency。
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.Conflict, "owner already has host profile")
}
agency := hostdomain.Agency{AgencyID: command.AgencyID, OwnerUserID: command.OwnerUserID, RegionID: regionID, ParentBDUserID: command.ParentBDUserID, Name: command.Name, Status: hostdomain.AgencyStatusActive, JoinEnabled: command.JoinEnabled, MaxHosts: command.MaxHosts, 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
})
}
// 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
}