dual check v0.1
This commit is contained in:
1 parent
78bad1fa3d
commit
c137299016
36 files changed
+3727
-81
No files matched your search
@@ -0,0 +1,243 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user