664 lines
29 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.

// 本包实现 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)
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)
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)
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)
SetAgencyJoinEnabled(ctx context.Context, command SetAgencyJoinEnabledCommand) (hostdomain.Agency, error)
GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error)
GetBDProfile(ctx context.Context, userID int64) (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)
HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, 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 结束普通成员关系,保留 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.idGenerator.NewInt64(),
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)
}
// InviteAgency 由 BD 直接创建用户的 Agency owner 身份,不再等待目标用户确认。
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil {
return hostdomain.RoleInvitation{}, err
}
// Agency 名称在邀请动作里固化;持久化层会在同一事务中写 accepted invitation、Agency、owner Host 和 owner membership。
agencyName := strings.TrimSpace(command.AgencyName)
if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 || agencyName == "" {
return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id, target_user_id and agency_name 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,
AgencyID: s.idGenerator.NewInt64(),
MembershipID: s.idGenerator.NewInt64(),
EventID: s.nextEventIDRange(3),
NowMs: nowMs,
})
}
// InviteBD 由 BD Leader 直接创建用户的 BD 身份,不再等待目标用户确认。
func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil {
return hostdomain.RoleInvitation{}, err
}
// 负责人身份、区域、目标是否已有 BD 都交给持久化层在同一事务中校验,并和 accepted invitation 一起提交。
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,
EventID: s.idGenerator.NewInt64(),
NowMs: nowMs,
})
}
// 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),
})
}
// 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-serveruser-service 只维护业务事实一致性。
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 状态Phase 2 只允许 active/disabled 两个稳定状态。
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)
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 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,
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),
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),
EventID: s.idGenerator.NewInt64(),
NowMs: nowMs,
})
}
// 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 || name == "" {
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, owner_user_id and name 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,
})
}
// 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,
})
}
// 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)
}
// GetBDProfile 读取 BD/BD 负责人身份事实。
func (s *Service) GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error) {
if s.repository == nil {
return hostdomain.BDProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
}
if userID <= 0 {
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
return s.repository.GetBDProfile(ctx, userID)
}
// 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
}