Files
toolbox/server/internal/tools/dual_check/service.go
T
2026-08-29 23:34:01 +08:00

244 lines
7.0 KiB
Go

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
}