// Package cp 承载 CP/兄弟/姐妹关系的用例编排。 package cp import ( "context" "strings" "time" "hyapp/pkg/xerr" cpdomain "hyapp/services/user-service/internal/domain/cp" ) // Repository 描述 CP 关系 service 需要的持久化能力,便于后续单元测试替换 MySQL 实现。 type Repository interface { ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32, nowMs int64) ([]cpdomain.Application, int64, error) AcceptApplication(ctx context.Context, userID int64, applicationID string, nowMs int64) (cpdomain.Application, cpdomain.Relationship, error) RejectApplication(ctx context.Context, userID int64, applicationID string, reason string, nowMs int64) (cpdomain.Application, error) ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, error) } // Service 只编排 CP 用例,不直接投递 IM;IM 由 user_outbox 下游消费者按事实事件处理。 type Service struct { repo Repository clock func() time.Time } // New 创建 CP 关系 service。 func New(repo Repository) *Service { return &Service{ repo: repo, clock: func() time.Time { return time.Now().UTC() }, } } // ListApplications 返回用户收到或发出的 CP 申请;过期固化在 repository 中完成。 func (s *Service) ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32) ([]cpdomain.Application, int64, int64, error) { if err := requireUserID(userID); err != nil { return nil, 0, 0, err } nowMs := s.nowMs() applications, total, err := s.repo.ListApplications(ctx, userID, normalizeDirection(direction), normalizeStatus(status), page, pageSize, nowMs) return applications, total, nowMs, err } // AcceptApplication 同意申请并建立关系;同一用户只能有一个 active 关系的互斥由 repository 事务保证。 func (s *Service) AcceptApplication(ctx context.Context, userID int64, applicationID string) (cpdomain.Application, cpdomain.Relationship, error) { if err := requireUserID(userID); err != nil { return cpdomain.Application{}, cpdomain.Relationship{}, err } if strings.TrimSpace(applicationID) == "" { return cpdomain.Application{}, cpdomain.Relationship{}, xerr.New(xerr.InvalidArgument, "application_id is required") } return s.repo.AcceptApplication(ctx, userID, strings.TrimSpace(applicationID), s.nowMs()) } // RejectApplication 拒绝申请;拒绝不阻断同一对用户其他类型的 pending 申请。 func (s *Service) RejectApplication(ctx context.Context, userID int64, applicationID string, reason string) (cpdomain.Application, error) { if err := requireUserID(userID); err != nil { return cpdomain.Application{}, err } if strings.TrimSpace(applicationID) == "" { return cpdomain.Application{}, xerr.New(xerr.InvalidArgument, "application_id is required") } return s.repo.RejectApplication(ctx, userID, strings.TrimSpace(applicationID), reason, s.nowMs()) } // ListRelationships 返回用户当前 active 关系;当前产品固定每个用户同时只能有一条关系。 func (s *Service) ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error) { if err := requireUserID(userID); err != nil { return nil, 0, err } return s.repo.ListRelationships(ctx, userID, normalizeRelationType(relationType), page, pageSize) } // ConsumeGiftEvent 消费 room-service 已扣费并落房间状态的送礼事实。 func (s *Service) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent) (cpdomain.ConsumeResult, error) { event.CPRelationType = normalizeRelationType(event.CPRelationType) if event.CPRelationType == "" { return cpdomain.ConsumeResult{Consumed: false}, nil } // gift_value 是亲密值增量,沿用 room 热度结算后的值;0 值仍可创建申请,但不会增加已有关联亲密值。 if event.GiftValue < 0 { event.GiftValue = 0 } return s.repo.ConsumeGiftEvent(ctx, event, s.nowMs()) } func (s *Service) nowMs() int64 { if s == nil || s.clock == nil { return time.Now().UTC().UnixMilli() } return s.clock().UTC().UnixMilli() } func requireUserID(userID int64) error { if userID <= 0 { return xerr.New(xerr.InvalidArgument, "user_id is required") } return nil } func normalizeDirection(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "outgoing": return "outgoing" default: return "incoming" } } func normalizeStatus(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case cpdomain.ApplicationStatusPending, cpdomain.ApplicationStatusAccepted, cpdomain.ApplicationStatusRejected, cpdomain.ApplicationStatusExpired, cpdomain.ApplicationStatusBlocked: return strings.ToLower(strings.TrimSpace(value)) default: return "" } } func normalizeRelationType(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case cpdomain.RelationTypeCP: return cpdomain.RelationTypeCP case cpdomain.RelationTypeBrother: return cpdomain.RelationTypeBrother case cpdomain.RelationTypeSister: return cpdomain.RelationTypeSister default: return "" } }