package dual_check import ( "context" "errors" "fmt" "time" "cancerai.top/toolbox-server/internal/pkg/hash" "cancerai.top/toolbox-server/internal/pkg/sessionid" "cancerai.top/toolbox-server/internal/store" "go.uber.org/zap" ) var ( ErrSessionNotFound = &BusinessError{Code: ErrCodeSessionNotFound} ErrSessionCreate = &BusinessError{Code: ErrCodeSessionCreate} ErrDuplicateSubmit = &BusinessError{Code: ErrCodeDuplicateSubmit} ) // Service 双验业务逻辑层 type Service struct { store store.SessionStore logger *zap.Logger ttl time.Duration } func NewService(s store.SessionStore, logger *zap.Logger, ttl time.Duration) *Service { return &Service{store: s, logger: logger, ttl: ttl} } // ── Create ── func (s *Service) CreateSession(ctx context.Context, nickname string) (*CreateResponse, error) { var sid string var err error for i := 0; i < 3; i++ { sid, err = sessionid.Generate() if err == nil { err = s.store.Create(ctx, sid, s.ttl) if err == nil { break } } } if err != nil { s.logger.Error("create session failed", zap.Error(err)) return nil, ErrSessionCreate } expiresAt := time.Now().Add(s.ttl) s.logger.Info("session created", zap.String("session_id", truncateSessionID(sid)), zap.String("nickname", nickname), zap.Time("expires_at", expiresAt), ) return &CreateResponse{ SessionID: sid, Nickname: nickname, ExpiresAt: expiresAt, TTL: int(s.ttl.Seconds()), }, nil } // ── Submit(发起方 or 参与方) ── func (s *Service) Submit(ctx context.Context, req *SubmitRequest) (*SubmitResponse, error) { hashHex := hash.SHA256(req.Text) req.Text = "" // 显式释放明文引用 switch req.Role { case "initiator": return s.submitInitiator(ctx, req.SessionID, req.Nickname, hashHex) case "participant": return s.submitParticipant(ctx, req.SessionID, req.Nickname, hashHex) default: return nil, &BusinessError{Code: ErrCodeInvalidRole} } } func (s *Service) submitInitiator(ctx context.Context, sessionID, nickname, hashHex string) (*SubmitResponse, error) { if err := s.store.SubmitInitiator(ctx, sessionID, nickname, hashHex); err != nil { if errors.Is(err, store.ErrSessionNotFound) { return nil, ErrSessionNotFound } s.logger.Error("submit initiator failed", zap.Error(err), zap.String("session_id", truncateSessionID(sessionID))) return nil, fmt.Errorf("submit initiator: %w", err) } s.logger.Info("initiator submitted", zap.String("session_id", truncateSessionID(sessionID)), zap.String("nickname", nickname), ) return &SubmitResponse{Submitted: true, InitiatorReady: true}, nil } func (s *Service) submitParticipant(ctx context.Context, sessionID, nickname, hashHex string) (*SubmitResponse, error) { initReady, match, err := s.store.SubmitParticipant(ctx, sessionID, nickname, hashHex) if err != nil { switch { case errors.Is(err, store.ErrSessionNotFound): return nil, ErrSessionNotFound case errors.Is(err, store.ErrDuplicateSubmit): return nil, ErrDuplicateSubmit default: s.logger.Error("submit participant failed", zap.Error(err), zap.String("session_id", truncateSessionID(sessionID)), zap.String("nickname", nickname), ) return nil, fmt.Errorf("submit participant: %w", err) } } s.logger.Info("participant submitted", zap.String("session_id", truncateSessionID(sessionID)), zap.String("nickname", nickname), zap.Bool("initiator_ready", initReady), ) return &SubmitResponse{ Submitted: true, InitiatorReady: initReady, Match: match, }, nil } // ── Join ── func (s *Service) JoinParticipant(ctx context.Context, sessionID, nickname string) (*SessionResponse, error) { // 检查昵称是否等于发起方昵称(不允许用发起方的昵称作为参与者) snap, err := s.store.Get(ctx, sessionID) if err != nil { if errors.Is(err, store.ErrSessionNotFound) { return nil, ErrSessionNotFound } return nil, err } if snap.InitiatorNickname != "" && snap.InitiatorNickname == nickname { return nil, &BusinessError{Code: ErrCodeNicknameExists} } if err := s.store.JoinParticipant(ctx, sessionID, nickname); err != nil { if errors.Is(err, store.ErrSessionNotFound) { return nil, ErrSessionNotFound } return nil, err } // 返回加入后的最新状态 return s.GetSession(ctx, sessionID) } // ── GetSession 返回全量状态 ── func (s *Service) GetSession(ctx context.Context, sessionID string) (*SessionResponse, error) { snap, err := s.store.Get(ctx, sessionID) if err != nil { if errors.Is(err, store.ErrSessionNotFound) { return nil, ErrSessionNotFound } return nil, err } ttl := time.Until(snap.ExpiresAt) parts := make([]ParticipantResp, 0, len(snap.Participants)) for _, p := range snap.Participants { parts = append(parts, ParticipantResp{ Nickname: p.Nickname, Joined: true, Submitted: p.Submitted, Match: p.Match, }) } return &SessionResponse{ SessionID: sessionID, InitiatorNickname: snap.InitiatorNickname, InitiatorReady: snap.InitiatorSubmitted, AllowResubmit: snap.AllowResubmit, Participants: parts, TTLSeconds: int64(ttl.Seconds()), }, nil } // ── CheckSession(加入前预检查) ── func (s *Service) CheckSession(ctx context.Context, sessionID string) (*CheckResponse, error) { snap, err := s.store.Get(ctx, sessionID) if err != nil { if errors.Is(err, store.ErrSessionNotFound) { return &CheckResponse{Exists: false}, nil } return nil, err } ttl := time.Until(snap.ExpiresAt) if ttl < 0 { return &CheckResponse{Exists: false}, nil } return &CheckResponse{ Exists: true, InitiatorReady: snap.InitiatorSubmitted, AllowResubmit: snap.AllowResubmit, ParticipantCount: len(snap.Participants), TTLSeconds: int64(ttl.Seconds()), }, nil } // ── SetResubmit 发起方切换"允许参与者重新提交"开关 ── func (s *Service) SetResubmit(ctx context.Context, sessionID string, allow bool) (*SessionResponse, error) { if err := s.store.SetResubmit(ctx, sessionID, allow); err != nil { if errors.Is(err, store.ErrSessionNotFound) { return nil, ErrSessionNotFound } s.logger.Error("set resubmit failed", zap.Error(err), zap.String("session_id", truncateSessionID(sessionID))) return nil, fmt.Errorf("set resubmit: %w", err) } s.logger.Info("allow_resubmit changed", zap.String("session_id", truncateSessionID(sessionID)), zap.Bool("allow", allow), ) return s.GetSession(ctx, sessionID) } // ── EndSession 发起方结束会话(删除 Redis key,WS 广播由 handler 完成) ── func (s *Service) EndSession(ctx context.Context, sessionID string) error { if err := s.store.Delete(ctx, sessionID); err != nil { s.logger.Error("end session failed", zap.Error(err), zap.String("session_id", truncateSessionID(sessionID))) return fmt.Errorf("end session: %w", err) } s.logger.Info("session ended", zap.String("session_id", truncateSessionID(sessionID)), ) return nil } func truncateSessionID(id string) string { if len(id) > 8 { return id[:8] + "..." } return id }