973 lines
45 KiB
Go
973 lines
45 KiB
Go
// 本包实现 user-service 内 Host/Agency/BD 业务用例。
|
||
package host
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/idgen"
|
||
"hyapp/pkg/xerr"
|
||
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||
)
|
||
|
||
// Repository 是 Host 领域的持久化边界。
|
||
// 写方法内部必须开启 MySQL 事务并完成行锁、幂等检查、事实写入和事件箱写入。
|
||
type Repository interface {
|
||
SearchAgencies(ctx context.Context, command SearchAgenciesCommand) ([]hostdomain.Agency, error)
|
||
ApplyToAgency(ctx context.Context, command ApplyToAgencyCommand) (hostdomain.AgencyApplication, error)
|
||
ReviewAgencyApplication(ctx context.Context, command ReviewAgencyApplicationCommand) (hostdomain.ReviewAgencyApplicationResult, error)
|
||
KickAgencyHost(ctx context.Context, command KickAgencyHostCommand) (hostdomain.KickAgencyHostResult, error)
|
||
InviteAgency(ctx context.Context, command InviteAgencyCommand) (hostdomain.RoleInvitation, error)
|
||
InviteBD(ctx context.Context, command InviteBDCommand) (hostdomain.RoleInvitation, error)
|
||
InviteHost(ctx context.Context, command InviteHostCommand) (hostdomain.RoleInvitation, error)
|
||
ListRoleInvitations(ctx context.Context, command ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error)
|
||
GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (hostdomain.RoleInvitation, error)
|
||
ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error)
|
||
ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error)
|
||
ListBDAgencies(ctx context.Context, command ListBDAgenciesCommand) ([]hostdomain.Agency, error)
|
||
ListManagerTeamAgencies(ctx context.Context, command ListManagerTeamAgenciesCommand) (hostdomain.ManagerTeamAgencies, error)
|
||
ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error)
|
||
CreateBDLeader(ctx context.Context, command CreateBDLeaderCommand) (hostdomain.BDProfile, error)
|
||
CreateBD(ctx context.Context, command CreateBDCommand) (hostdomain.BDProfile, error)
|
||
SetBDStatus(ctx context.Context, command SetBDStatusCommand) (hostdomain.BDProfile, error)
|
||
CreateCoinSeller(ctx context.Context, command CreateCoinSellerCommand) (hostdomain.CoinSellerProfile, error)
|
||
SetCoinSellerStatus(ctx context.Context, command SetCoinSellerStatusCommand) (hostdomain.CoinSellerProfile, error)
|
||
CreateSubCoinSeller(ctx context.Context, command CreateSubCoinSellerCommand) (hostdomain.CreateSubCoinSellerResult, error)
|
||
SubmitSubCoinSellerApplication(ctx context.Context, command SubmitSubCoinSellerApplicationCommand) (hostdomain.SubmitSubCoinSellerApplicationResult, error)
|
||
ReviewSubCoinSellerApplication(ctx context.Context, command ReviewSubCoinSellerApplicationCommand) (hostdomain.ReviewSubCoinSellerApplicationResult, error)
|
||
ListSubCoinSellers(ctx context.Context, command ListSubCoinSellersCommand) (hostdomain.ListSubCoinSellersResult, error)
|
||
CheckCoinSellerSubRelation(ctx context.Context, command CheckCoinSellerSubRelationCommand) (hostdomain.CoinSellerListItem, error)
|
||
CreateAgency(ctx context.Context, command CreateAgencyCommand) (hostdomain.CreateAgencyResult, error)
|
||
CloseAgency(ctx context.Context, command CloseAgencyCommand) (hostdomain.Agency, error)
|
||
DeleteAgency(ctx context.Context, command DeleteAgencyCommand) (hostdomain.Agency, error)
|
||
SetAgencyStatus(ctx context.Context, command SetAgencyStatusCommand) (hostdomain.Agency, error)
|
||
SetAgencyJoinEnabled(ctx context.Context, command SetAgencyJoinEnabledCommand) (hostdomain.Agency, error)
|
||
AdminAddAgencyHost(ctx context.Context, command AdminAddAgencyHostCommand) (hostdomain.CreateAgencyResult, error)
|
||
GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error)
|
||
BatchGetHostProfiles(ctx context.Context, userIDs []int64) (map[int64]hostdomain.HostProfile, error)
|
||
GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error)
|
||
GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
|
||
ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error)
|
||
GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
|
||
GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error)
|
||
UpdateAgencyProfile(ctx context.Context, command UpdateAgencyProfileCommand) (hostdomain.Agency, error)
|
||
HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error)
|
||
HasActiveManagerProfile(ctx context.Context, userID int64) (bool, error)
|
||
HasActiveManagerCapability(ctx context.Context, userID int64, capability string) (bool, error)
|
||
ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
|
||
ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
|
||
}
|
||
|
||
// IDGenerator 生成 Host 领域内部事实 ID。
|
||
type IDGenerator interface {
|
||
NewInt64() int64
|
||
}
|
||
|
||
// Service 承载 Host/Agency/BD 用例;它不直接访问数据库,事务语义保留在持久化层。
|
||
type Service struct {
|
||
repository Repository
|
||
idGenerator IDGenerator
|
||
now func() time.Time
|
||
}
|
||
|
||
// Option 调整 Host 业务层的运行时依赖,测试可以注入固定时钟和确定性发号器。
|
||
type Option func(*Service)
|
||
|
||
// New 创建 Host 领域业务层对象。
|
||
func New(repository Repository, options ...Option) *Service {
|
||
svc := &Service{
|
||
repository: repository,
|
||
idGenerator: idgen.NewInt64Generator(0),
|
||
now: time.Now,
|
||
}
|
||
for _, option := range options {
|
||
option(svc)
|
||
}
|
||
|
||
return svc
|
||
}
|
||
|
||
// WithIDGenerator 注入事实 ID 发号器。
|
||
func WithIDGenerator(generator IDGenerator) Option {
|
||
return func(s *Service) {
|
||
if generator != nil {
|
||
s.idGenerator = generator
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithClock 注入业务时钟,避免测试依赖真实时间。
|
||
func WithClock(now func() time.Time) Option {
|
||
return func(s *Service) {
|
||
if now != nil {
|
||
s.now = now
|
||
}
|
||
}
|
||
}
|
||
|
||
// SearchAgencies 返回当前用户同国家可加入的有效 Agency。
|
||
func (s *Service) SearchAgencies(ctx context.Context, userID int64, keyword string, pageSize int32) ([]hostdomain.Agency, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if userID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
limit := int(pageSize)
|
||
if limit <= 0 || limit > 100 {
|
||
// 搜索接口默认收敛到小结果集,避免第一阶段误用成全表导出。
|
||
limit = 50
|
||
}
|
||
|
||
// 业务层只负责输入归一化;用户国家过滤由持久化层读取 users 当前事实完成。
|
||
return s.repository.SearchAgencies(ctx, SearchAgenciesCommand{
|
||
UserID: userID,
|
||
Keyword: strings.TrimSpace(keyword),
|
||
PageSize: limit,
|
||
})
|
||
}
|
||
|
||
// ApplyToAgency 创建用户加入 Agency 的待处理申请。
|
||
func (s *Service) ApplyToAgency(ctx context.Context, command ApplyToAgencyInput) (hostdomain.AgencyApplication, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.AgencyApplication{}, err
|
||
}
|
||
// command_id 来自调用方,是幂等边界;申请 ID 由服务端生成,避免客户端猜测事实主键。
|
||
if strings.TrimSpace(command.CommandID) == "" {
|
||
return hostdomain.AgencyApplication{}, xerr.New(xerr.InvalidArgument, "command_id is required")
|
||
}
|
||
if command.UserID <= 0 || command.AgencyID <= 0 {
|
||
return hostdomain.AgencyApplication{}, xerr.New(xerr.InvalidArgument, "user_id and agency_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
// 业务层不做 Agency、区域、成员关系判定,避免在无事务上下文里读到过期事实。
|
||
return s.repository.ApplyToAgency(ctx, ApplyToAgencyCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
ApplicationID: s.idGenerator.NewInt64(),
|
||
UserID: command.UserID,
|
||
AgencyID: command.AgencyID,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// ReviewAgencyApplication 审核申请;通过时会在同一事务内创建 Host 身份和有效成员关系。
|
||
func (s *Service) ReviewAgencyApplication(ctx context.Context, command ReviewAgencyApplicationInput) (hostdomain.ReviewAgencyApplicationResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.ReviewAgencyApplicationResult{}, err
|
||
}
|
||
// 对外动作统一做空白裁剪和小写化,领域层只接收规范化后的状态机输入。
|
||
decision := normalizeAction(command.Decision)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.ReviewerUserID <= 0 || command.ApplicationID <= 0 {
|
||
return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.InvalidArgument, "command_id, reviewer_user_id and application_id are required")
|
||
}
|
||
if decision != hostdomain.ApplicationDecisionApprove && decision != hostdomain.ApplicationDecisionReject {
|
||
return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.InvalidArgument, "decision is invalid")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
// 成员关系 ID 和事件 ID 对拒绝动作可能不会被使用;提前生成可以保持命令对象完整,
|
||
// 真正是否落库由持久化层事务状态机决定。
|
||
eventID := s.nextEventIDRange(2)
|
||
return s.repository.ReviewAgencyApplication(ctx, ReviewAgencyApplicationCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
ReviewerUserID: command.ReviewerUserID,
|
||
ApplicationID: command.ApplicationID,
|
||
Decision: decision,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
MembershipID: s.idGenerator.NewInt64(),
|
||
EventID: eventID,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// KickAgencyHost 结束普通成员关系,并在目标没有 active Agency owner 身份时停用 Host 身份。
|
||
func (s *Service) KickAgencyHost(ctx context.Context, command KickAgencyHostInput) (hostdomain.KickAgencyHostResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.KickAgencyHostResult{}, err
|
||
}
|
||
// 业务层只校验必填字段;操作者是否为拥有者、Host 是否为普通成员必须在锁内判断。
|
||
if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.AgencyID <= 0 || command.HostUserID <= 0 {
|
||
return hostdomain.KickAgencyHostResult{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id, agency_id and host_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.KickAgencyHost(ctx, KickAgencyHostCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
OperatorUserID: command.OperatorUserID,
|
||
AgencyID: command.AgencyID,
|
||
HostUserID: command.HostUserID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
EventID: s.nextEventIDRange(2),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// GetAgency 读取 Agency 详情;调用方负责根据业务入口校验 owner 或管理员身份。
|
||
func (s *Service) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.Agency{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if agencyID <= 0 {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency_id is required")
|
||
}
|
||
return s.repository.GetAgency(ctx, agencyID)
|
||
}
|
||
|
||
// UpdateAgencyProfile 只修改 Agency 独立组织资料,不回写 owner 用户昵称或头像。
|
||
func (s *Service) UpdateAgencyProfile(ctx context.Context, command UpdateAgencyProfileInput) (hostdomain.Agency, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.Agency{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.AgencyID <= 0 {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id and agency_id are required")
|
||
}
|
||
if command.Name == nil && command.Avatar == nil {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "name or avatar is required")
|
||
}
|
||
var name *string
|
||
if command.Name != nil {
|
||
trimmed := strings.TrimSpace(*command.Name)
|
||
if trimmed == "" {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency name is required")
|
||
}
|
||
name = &trimmed
|
||
}
|
||
var avatar *string
|
||
if command.Avatar != nil {
|
||
trimmed := strings.TrimSpace(*command.Avatar)
|
||
if trimmed == "" {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency avatar is required")
|
||
}
|
||
avatar = &trimmed
|
||
}
|
||
|
||
return s.repository.UpdateAgencyProfile(ctx, UpdateAgencyProfileCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
OperatorUserID: command.OperatorUserID,
|
||
AgencyID: command.AgencyID,
|
||
Name: name,
|
||
Avatar: avatar,
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.nextEventIDRange(1),
|
||
NowMs: s.now().UnixMilli(),
|
||
})
|
||
}
|
||
|
||
// InviteAgency 由 BD 或 BD Leader 创建 Agency owner 邀请;目标用户接受前不创建 Agency 身份。
|
||
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.RoleInvitation{}, err
|
||
}
|
||
// Agency 名称允许邀请方显式覆盖;为空时在目标接受并真正创建 Agency 时复制 owner 当前昵称/短 ID。
|
||
agencyName := strings.TrimSpace(command.AgencyName)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.InviteAgency(ctx, InviteAgencyCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
InvitationID: s.idGenerator.NewInt64(),
|
||
InviterUserID: command.InviterUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
AgencyName: agencyName,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// InviteBD 由 BD Leader 创建 BD 邀请;目标用户接受前不创建普通 BD 身份。
|
||
func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.RoleInvitation{}, err
|
||
}
|
||
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验,避免发出不可处理的待确认消息。
|
||
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.InviteBD(ctx, InviteBDCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
InvitationID: s.idGenerator.NewInt64(),
|
||
InviterUserID: command.InviterUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// InviteHost 由 active Agency owner 邀请用户加入自己的 Agency,接受前不创建 Host 或成员关系。
|
||
func (s *Service) InviteHost(ctx context.Context, command InviteHostInput) (hostdomain.RoleInvitation, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.RoleInvitation{}, err
|
||
}
|
||
// command_id 仍是发送动作的幂等边界;目标处理时会使用新的 command_id 推进邀请终态。
|
||
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.InviteHost(ctx, InviteHostCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
InvitationID: s.idGenerator.NewInt64(),
|
||
InviterUserID: command.InviterUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// ListRoleInvitations 返回当前用户收到的角色/团队邀请收件箱。
|
||
func (s *Service) ListRoleInvitations(ctx context.Context, command ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.TargetUserID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
|
||
}
|
||
return s.repository.ListRoleInvitations(ctx, ListRoleInvitationsCommand{
|
||
TargetUserID: command.TargetUserID,
|
||
Status: normalizeStatus(command.Status),
|
||
PageSize: normalizePageSize(command.PageSize),
|
||
})
|
||
}
|
||
|
||
// GetRoleInvitation 返回单条邀请事实;actor 必须是邀请发起人或目标用户,避免内部接口被误用成任意枚举入口。
|
||
func (s *Service) GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (hostdomain.RoleInvitation, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.RoleInvitation{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if actorUserID <= 0 || invitationID <= 0 {
|
||
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "actor_user_id and invitation_id are required")
|
||
}
|
||
return s.repository.GetRoleInvitation(ctx, actorUserID, invitationID)
|
||
}
|
||
|
||
// ListBDLeaderBDs 返回当前 BD Leader 直属的有效 BD 列表。
|
||
func (s *Service) ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.LeaderUserID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "leader_user_id is required")
|
||
}
|
||
return s.repository.ListBDLeaderBDs(ctx, ListBDLeaderBDsCommand{
|
||
LeaderUserID: command.LeaderUserID,
|
||
Status: normalizeStatus(command.Status),
|
||
PageSize: normalizePageSize(command.PageSize),
|
||
})
|
||
}
|
||
|
||
// ListBDLeaderAgencies 返回当前 BD Leader 团队下的 Agency 列表。
|
||
func (s *Service) ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.LeaderUserID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "leader_user_id is required")
|
||
}
|
||
return s.repository.ListBDLeaderAgencies(ctx, ListBDLeaderAgenciesCommand{
|
||
LeaderUserID: command.LeaderUserID,
|
||
Status: normalizeStatus(command.Status),
|
||
PageSize: normalizePageSize(command.PageSize),
|
||
})
|
||
}
|
||
|
||
// ListBDAgencies 返回当前 BD 直接拓展的 Agency 列表。
|
||
func (s *Service) ListBDAgencies(ctx context.Context, command ListBDAgenciesCommand) ([]hostdomain.Agency, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.BDUserID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "bd_user_id is required")
|
||
}
|
||
return s.repository.ListBDAgencies(ctx, ListBDAgenciesCommand{
|
||
BDUserID: command.BDUserID,
|
||
Status: normalizeStatus(command.Status),
|
||
PageSize: normalizePageSize(command.PageSize),
|
||
})
|
||
}
|
||
|
||
// ListManagerTeamAgencies 返回经理团队树内全部有效 Agency,经理中心团队工资统计据此圈定主播归属。
|
||
func (s *Service) ListManagerTeamAgencies(ctx context.Context, command ListManagerTeamAgenciesCommand) (hostdomain.ManagerTeamAgencies, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.ManagerTeamAgencies{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.ManagerUserID <= 0 {
|
||
return hostdomain.ManagerTeamAgencies{}, xerr.New(xerr.InvalidArgument, "manager_user_id is required")
|
||
}
|
||
// 团队统计要求一次拿全 Agency 集合,防御上限比常规分页列表更高;超限用 Truncated 显式标记而不是静默丢数据。
|
||
pageSize := command.PageSize
|
||
if pageSize <= 0 {
|
||
pageSize = 2000
|
||
}
|
||
if pageSize > 5000 {
|
||
pageSize = 5000
|
||
}
|
||
return s.repository.ListManagerTeamAgencies(ctx, ListManagerTeamAgenciesCommand{
|
||
ManagerUserID: command.ManagerUserID,
|
||
PageSize: pageSize,
|
||
})
|
||
}
|
||
|
||
// ProcessRoleInvitation 接受、拒绝或取消角色邀请。
|
||
func (s *Service) ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationInput) (hostdomain.ProcessRoleInvitationResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.ProcessRoleInvitationResult{}, err
|
||
}
|
||
// 接受、拒绝、取消共用同一入口;先归一化再进入持久化层状态机。
|
||
action := normalizeAction(command.Action)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.ActorUserID <= 0 || command.InvitationID <= 0 {
|
||
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "command_id, actor_user_id and invitation_id are required")
|
||
}
|
||
if action != hostdomain.InvitationActionAccept && action != hostdomain.InvitationActionReject && action != hostdomain.InvitationActionCancel {
|
||
return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "action is invalid")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
// Agency ID、成员关系 ID、事件 ID 只在接受 Agency 邀请时使用;
|
||
// 拒绝、取消、接受 BD 邀请可能消耗不到这些预分配 ID,这是开发阶段可接受的设计。
|
||
eventID := s.nextEventIDRange(3)
|
||
return s.repository.ProcessRoleInvitation(ctx, ProcessRoleInvitationCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
ActorUserID: command.ActorUserID,
|
||
InvitationID: command.InvitationID,
|
||
Action: action,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
AgencyID: s.idGenerator.NewInt64(),
|
||
MembershipID: s.idGenerator.NewInt64(),
|
||
EventID: eventID,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// CreateBDLeader 创建 BD Leader/Admin 身份。
|
||
// 后台 admin 的权限在 admin-server 校验;App 经理中心的权限由 CheckBusinessCapability 基于 manager_profiles 校验。
|
||
// 本方法只负责统一写入 BD Leader 事实,保证幂等、区域和身份冲突规则不会因入口不同而分叉。
|
||
func (s *Service) CreateBDLeader(ctx context.Context, command CreateBDLeaderInput) (hostdomain.BDProfile, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.BDProfile{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.CreateBDLeader(ctx, CreateBDLeaderCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// CreateBD 由后台创建普通 BD;传入父级 Leader 时绑定到该 Leader,未传时创建独立 BD。
|
||
func (s *Service) CreateBD(ctx context.Context, command CreateBDInput) (hostdomain.BDProfile, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.BDProfile{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
|
||
}
|
||
if command.ParentLeaderUserID < 0 {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "parent_leader_user_id must not be negative")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.CreateBD(ctx, CreateBDCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
ParentLeaderUserID: command.ParentLeaderUserID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// SetBDStatus 调整普通 BD 或 BD Leader 状态;调用方必须传 role,避免同一用户两种身份时误改。
|
||
func (s *Service) SetBDStatus(ctx context.Context, command SetBDStatusInput) (hostdomain.BDProfile, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.BDProfile{}, err
|
||
}
|
||
status := normalizeStatus(command.Status)
|
||
role := normalizeBDRole(command.Role)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 || role == "" {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, target_user_id and role are required")
|
||
}
|
||
if status != hostdomain.BDStatusActive && status != hostdomain.BDStatusDisabled {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "bd status is invalid")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.SetBDStatus(ctx, SetBDStatusCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
Role: role,
|
||
Status: status,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// CreateCoinSeller 由后台创建币商身份;该身份可以和 Agency/BD/BD Leader 并存。
|
||
func (s *Service) CreateCoinSeller(ctx context.Context, command CreateCoinSellerInput) (hostdomain.CoinSellerProfile, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.CoinSellerProfile{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.CreateCoinSeller(ctx, CreateCoinSellerCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
CanManageSubCoinSellers: command.CanManageSubCoinSellers,
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// SetCoinSellerStatus 调整币商身份状态;停用后 gateway 不能再放行币商转账。
|
||
func (s *Service) SetCoinSellerStatus(ctx context.Context, command SetCoinSellerStatusInput) (hostdomain.CoinSellerProfile, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.CoinSellerProfile{}, err
|
||
}
|
||
status := normalizeStatus(command.Status)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
|
||
}
|
||
if status != hostdomain.CoinSellerStatusActive && status != hostdomain.CoinSellerStatusDisabled {
|
||
return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "coin_seller status is invalid")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.SetCoinSellerStatus(ctx, SetCoinSellerStatusCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
Status: status,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
CanManageSubCoinSellers: command.CanManageSubCoinSellers,
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// CreateSubCoinSeller 是历史直开 RPC。审批制上线后保留 proto 兼容,但服务端拒绝新直开请求,避免旧调用方绕过后台审核。
|
||
func (s *Service) CreateSubCoinSeller(ctx context.Context, command CreateSubCoinSellerInput) (hostdomain.CreateSubCoinSellerResult, error) {
|
||
if strings.TrimSpace(command.CommandID) == "" || command.ParentUserID <= 0 || command.ChildUserID <= 0 {
|
||
return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.InvalidArgument, "command_id, parent_user_id and child_user_id are required")
|
||
}
|
||
if command.ParentUserID == command.ChildUserID {
|
||
return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.Conflict, "coin seller cannot add self as sub seller")
|
||
}
|
||
return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.PermissionDenied, "sub coin seller invitation requires admin review")
|
||
}
|
||
|
||
// SubmitSubCoinSellerApplication 只创建待审申请;active 直属关系必须等待后台审核通过后再落库。
|
||
func (s *Service) SubmitSubCoinSellerApplication(ctx context.Context, command SubmitSubCoinSellerApplicationInput) (hostdomain.SubmitSubCoinSellerApplicationResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.ParentUserID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.InvalidArgument, "command_id, parent_user_id and target_user_id are required")
|
||
}
|
||
if command.ParentUserID == command.TargetUserID {
|
||
return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.Conflict, "coin seller cannot add self as sub seller")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
return s.repository.SubmitSubCoinSellerApplication(ctx, SubmitSubCoinSellerApplicationCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
ApplicationID: s.idGenerator.NewInt64(),
|
||
ParentUserID: command.ParentUserID,
|
||
TargetUserID: command.TargetUserID,
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// ReviewSubCoinSellerApplication 是后台审批边界;通过时才创建子币商身份与直属关系。
|
||
func (s *Service) ReviewSubCoinSellerApplication(ctx context.Context, command ReviewSubCoinSellerApplicationInput) (hostdomain.ReviewSubCoinSellerApplicationResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
|
||
}
|
||
decision := strings.TrimSpace(command.Decision)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.ApplicationID <= 0 || decision == "" {
|
||
return hostdomain.ReviewSubCoinSellerApplicationResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, application_id and decision are required")
|
||
}
|
||
if decision != hostdomain.CoinSellerSubApplicationStatusApproved && decision != hostdomain.CoinSellerSubApplicationStatusRejected {
|
||
return hostdomain.ReviewSubCoinSellerApplicationResult{}, xerr.New(xerr.InvalidArgument, "decision must be approved or rejected")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
return s.repository.ReviewSubCoinSellerApplication(ctx, ReviewSubCoinSellerApplicationCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
ApplicationID: command.ApplicationID,
|
||
Decision: decision,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
RelationID: s.idGenerator.NewInt64(),
|
||
EventID: s.nextEventIDRange(2),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// ListSubCoinSellers 返回父币商直属子币商列表;权限和 active 身份在存储层按当前事实校验。
|
||
func (s *Service) ListSubCoinSellers(ctx context.Context, command ListSubCoinSellersCommand) (hostdomain.ListSubCoinSellersResult, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.ListSubCoinSellersResult{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.ParentUserID <= 0 {
|
||
return hostdomain.ListSubCoinSellersResult{}, xerr.New(xerr.InvalidArgument, "parent_user_id is required")
|
||
}
|
||
page := command.Page
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
pageSize := command.PageSize
|
||
if pageSize <= 0 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
return s.repository.ListSubCoinSellers(ctx, ListSubCoinSellersCommand{
|
||
ParentUserID: command.ParentUserID,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
})
|
||
}
|
||
|
||
// CheckCoinSellerSubRelation 是转账前的服务端授权边界,避免 H5 隐藏按钮后仍能绕过前端直调。
|
||
func (s *Service) CheckCoinSellerSubRelation(ctx context.Context, command CheckCoinSellerSubRelationCommand) (hostdomain.CoinSellerListItem, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.CoinSellerListItem{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if command.ParentUserID <= 0 || command.ChildUserID <= 0 {
|
||
return hostdomain.CoinSellerListItem{}, xerr.New(xerr.InvalidArgument, "parent_user_id and child_user_id are required")
|
||
}
|
||
if command.ParentUserID == command.ChildUserID {
|
||
return hostdomain.CoinSellerListItem{}, xerr.New(xerr.Conflict, "coin seller sub relation is invalid")
|
||
}
|
||
return s.repository.CheckCoinSellerSubRelation(ctx, command)
|
||
}
|
||
|
||
// CreateAgency 由后台直接创建 Agency;父级 BD 可选,并原子创建或复用 owner 的 host_profile 和 owner membership。
|
||
func (s *Service) CreateAgency(ctx context.Context, command CreateAgencyInput) (hostdomain.CreateAgencyResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.CreateAgencyResult{}, err
|
||
}
|
||
name := strings.TrimSpace(command.Name)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.OwnerUserID <= 0 {
|
||
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and owner_user_id are required")
|
||
}
|
||
if command.ParentBDUserID < 0 {
|
||
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "parent_bd_user_id must not be negative")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
eventID := s.nextEventIDRange(3)
|
||
return s.repository.CreateAgency(ctx, CreateAgencyCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
OwnerUserID: command.OwnerUserID,
|
||
ParentBDUserID: command.ParentBDUserID,
|
||
Name: name,
|
||
JoinEnabled: command.JoinEnabled,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
AgencyID: s.idGenerator.NewInt64(),
|
||
MembershipID: s.idGenerator.NewInt64(),
|
||
EventID: eventID,
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// CloseAgency 关闭 Agency;当前阶段只关闭组织和入会开关,不追溯改历史成员关系。
|
||
func (s *Service) CloseAgency(ctx context.Context, command CloseAgencyInput) (hostdomain.Agency, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.Agency{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.CloseAgency(ctx, CloseAgencyCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
AgencyID: command.AgencyID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// DeleteAgency 删除 Agency 身份;它只解除当前 Agency 关系,不删除主播身份和历史记录。
|
||
func (s *Service) DeleteAgency(ctx context.Context, command DeleteAgencyInput) (hostdomain.Agency, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.Agency{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.DeleteAgency(ctx, DeleteAgencyCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
AgencyID: command.AgencyID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// SetAgencyStatus 显式切换 Agency 可用状态;closed 可恢复为 active,但 deleted 是删除事实,不能复活。
|
||
func (s *Service) SetAgencyStatus(ctx context.Context, command SetAgencyStatusInput) (hostdomain.Agency, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.Agency{}, err
|
||
}
|
||
status := normalizeStatus(command.Status)
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
|
||
}
|
||
if status != hostdomain.AgencyStatusActive && status != hostdomain.AgencyStatusClosed {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency status is invalid")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.SetAgencyStatus(ctx, SetAgencyStatusCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
AgencyID: command.AgencyID,
|
||
Status: status,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// SetAgencyJoinEnabled 设置 Agency 是否允许 App 用户申请加入。
|
||
func (s *Service) SetAgencyJoinEnabled(ctx context.Context, command SetAgencyJoinEnabledInput) (hostdomain.Agency, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.Agency{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
|
||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
return s.repository.SetAgencyJoinEnabled(ctx, SetAgencyJoinEnabledCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
AgencyID: command.AgencyID,
|
||
JoinEnabled: command.JoinEnabled,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
EventID: s.idGenerator.NewInt64(),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// AdminAddAgencyHost 由后台直接把目标用户添加为 Agency 普通 Host。
|
||
func (s *Service) AdminAddAgencyHost(ctx context.Context, command AdminAddAgencyHostInput) (hostdomain.CreateAgencyResult, error) {
|
||
if err := s.requireWriteDependencies(); err != nil {
|
||
return hostdomain.CreateAgencyResult{}, err
|
||
}
|
||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 || command.TargetUserID <= 0 {
|
||
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, agency_id and target_user_id are required")
|
||
}
|
||
nowMs := s.now().UnixMilli()
|
||
|
||
// 一个直接添加动作最多创建/恢复 Host 身份并创建 membership 两类事实,因此一次占用两个连续 outbox ID。
|
||
return s.repository.AdminAddAgencyHost(ctx, AdminAddAgencyHostCommand{
|
||
CommandID: strings.TrimSpace(command.CommandID),
|
||
AdminUserID: command.AdminUserID,
|
||
AgencyID: command.AgencyID,
|
||
TargetUserID: command.TargetUserID,
|
||
Reason: strings.TrimSpace(command.Reason),
|
||
RequestID: strings.TrimSpace(command.RequestID),
|
||
MembershipID: s.idGenerator.NewInt64(),
|
||
EventID: s.nextEventIDRange(2),
|
||
NowMs: nowMs,
|
||
})
|
||
}
|
||
|
||
// GetHostProfile 读取主播身份事实。
|
||
func (s *Service) GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.HostProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if userID <= 0 {
|
||
return hostdomain.HostProfile{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
|
||
return s.repository.GetHostProfile(ctx, userID)
|
||
}
|
||
|
||
// BatchGetHostProfiles 批量读取主播身份事实,缺失用户不返回占位对象。
|
||
func (s *Service) BatchGetHostProfiles(ctx context.Context, userIDs []int64) (map[int64]hostdomain.HostProfile, error) {
|
||
if len(userIDs) == 0 {
|
||
// 空批量请求是合法的无操作,调用方可以直接按空 map 组装结果。
|
||
return map[int64]hostdomain.HostProfile{}, nil
|
||
}
|
||
uniqueUserIDs := make([]int64, 0, len(userIDs))
|
||
seen := make(map[int64]bool, len(userIDs))
|
||
for _, userID := range userIDs {
|
||
if userID <= 0 {
|
||
// 任一非法 ID 都表示调用方组装的批量请求不可信,不能静默跳过。
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_ids contains invalid value")
|
||
}
|
||
if seen[userID] {
|
||
continue
|
||
}
|
||
seen[userID] = true
|
||
uniqueUserIDs = append(uniqueUserIDs, userID)
|
||
}
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
|
||
return s.repository.BatchGetHostProfiles(ctx, uniqueUserIDs)
|
||
}
|
||
|
||
// GetBDProfile 按指定 role 读取普通 BD 或 BD Leader 身份事实。
|
||
func (s *Service) GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
role = normalizeBDRole(role)
|
||
if userID <= 0 || role == "" {
|
||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "user_id and role are required")
|
||
}
|
||
|
||
return s.repository.GetBDProfile(ctx, userID, role)
|
||
}
|
||
|
||
func normalizeBDRole(role string) string {
|
||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||
case hostdomain.BDRoleBD:
|
||
return hostdomain.BDRoleBD
|
||
case hostdomain.BDRoleLeader:
|
||
return hostdomain.BDRoleLeader
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// GetCoinSellerProfile 读取币商身份事实,供 gateway 在发起币商转账前校验。
|
||
func (s *Service) GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.CoinSellerProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if userID <= 0 {
|
||
return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
|
||
return s.repository.GetCoinSellerProfile(ctx, userID)
|
||
}
|
||
|
||
// ListActiveCoinSellersInMyRegion 返回当前用户所在区域的所有启用币商。
|
||
func (s *Service) ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if userID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
|
||
// 当前区域属于 user-service 用户主事实,gateway 不能传入 region_id 来扩大查询范围。
|
||
return s.repository.ListActiveCoinSellersInMyRegion(ctx, userID)
|
||
}
|
||
|
||
// GetUserRoleSummary 读取 App 我的页使用的轻量身份摘要,避免 gateway 为入口显隐发起多个角色 RPC。
|
||
func (s *Service) GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error) {
|
||
if s.repository == nil {
|
||
return hostdomain.UserRoleSummary{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if userID <= 0 {
|
||
return hostdomain.UserRoleSummary{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
|
||
return s.repository.GetUserRoleSummary(ctx, userID)
|
||
}
|
||
|
||
// ListAgencyMembers 读取 Agency 成员关系列表,第一阶段直接读事实表。
|
||
func (s *Service) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if agencyID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "agency_id is required")
|
||
}
|
||
|
||
return s.repository.ListAgencyMembers(ctx, agencyID, normalizeStatus(status))
|
||
}
|
||
|
||
// ListAgencyApplications 读取 Agency 申请列表,第一阶段直接读事实表。
|
||
func (s *Service) ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error) {
|
||
if s.repository == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if agencyID <= 0 {
|
||
return nil, xerr.New(xerr.InvalidArgument, "agency_id is required")
|
||
}
|
||
|
||
return s.repository.ListAgencyApplications(ctx, agencyID, normalizeStatus(status))
|
||
}
|
||
|
||
func (s *Service) requireWriteDependencies() error {
|
||
// 写路径必须具备持久化层、ID 发号器和时钟;缺任何一个都会破坏事务事实或事件时间。
|
||
if s.repository == nil {
|
||
return xerr.New(xerr.Unavailable, "host repository is not configured")
|
||
}
|
||
if s.idGenerator == nil {
|
||
return xerr.New(xerr.Unavailable, "host id generator is not configured")
|
||
}
|
||
if s.now == nil {
|
||
return xerr.New(xerr.Unavailable, "host clock is not configured")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) nextEventIDRange(count int) int64 {
|
||
// 部分 host 事务会在同一命令里写多条 outbox;这里一次性占用连续事件 ID,避免下一条命令拿到 eventID+1 后和本事务内部事件冲突。
|
||
first := s.idGenerator.NewInt64()
|
||
for i := 1; i < count; i++ {
|
||
s.idGenerator.NewInt64()
|
||
}
|
||
return first
|
||
}
|
||
|
||
func normalizeAction(value string) string {
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
}
|
||
|
||
func normalizeStatus(value string) string {
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
}
|
||
|
||
func normalizePageSize(value int) int {
|
||
if value <= 0 {
|
||
return 20
|
||
}
|
||
if value > 100 {
|
||
return 100
|
||
}
|
||
return value
|
||
}
|