244 lines
7.0 KiB
Go
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
|
|
}
|