// Package store 提供会话存储接口 // 接口解耦使得可以在测试中用 mock 替换,生产用 Redis package store import ( "context" "time" ) // Session 存储的会话数据结构(不含敏感字段) type Session struct { ID string `json:"session_id"` ExpiresAt time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` } // ParticipantInfo 参与者在全量快照中的状态 type ParticipantInfo struct { Nickname string `json:"nickname"` BrowserID string `json:"browser_id,omitempty"` Submitted bool `json:"submitted"` Match *bool `json:"match,omitempty"` } // SessionSnapshot Get 返回的全量快照 type SessionSnapshot struct { SessionID string `json:"session_id"` InitiatorHash string `json:"-"` // 发起方标准答案哈希,不对外暴露 InitiatorNickname string `json:"initiator_nickname"` InitiatorBrowserID string `json:"initiator_browser_id,omitempty"` InitiatorSubmitted bool `json:"initiator_submitted"` AllowResubmit bool `json:"allow_resubmit"` IgnoreWs bool `json:"ignore_ws"` // 是否忽略首尾空白 Participants []ParticipantInfo `json:"participants"` ExpiresAt time.Time `json:"expires_at"` } // SessionStore 会话存储接口 type SessionStore interface { // Create 原子创建会话(SET NX EX),同时记录发起方昵称和 browserId Create(ctx context.Context, id, initiatorNickname, initiatorBrowserID string, ttl time.Duration) error // SubmitInitiator 发起方提交标准答案(initiator_hash + initiator_nickname + 原始文本) // 同时立即为所有已提交的参与者计算 match SubmitInitiator(ctx context.Context, sessionID, nickname, hashHex, originalText string) error // SubmitParticipant 参与者提交自己的文本(按 nickname 唯一标识,同时存原始文本) // 返回:initiatorSubmitted(发起方是否已提交,是则 match 已算出)、match SubmitParticipant(ctx context.Context, sessionID, nickname, hashHex, originalText string) ( initiatorSubmitted bool, match *bool, err error) // JoinParticipant 参与者加入会话(只登记昵称,不提交文本) // browserId 用于识别"同一个人",避免跨 tab 昵称冲突 JoinParticipant(ctx context.Context, sessionID, nickname, browserID string) error // SetResubmit 设置是否允许参与者重复提交 SetResubmit(ctx context.Context, sessionID string, allow bool) error // SetIgnoreWs 设置比对时是否忽略首尾空白(1=忽略,0=不忽略) SetIgnoreWs(ctx context.Context, sessionID string, ignore bool) error // GetIgnoreWs 获取当前会话的 ignore_ws 设置 GetIgnoreWs(ctx context.Context, sessionID string) (bool, error) // RecalcMatches 按新的 ignoreWs 规则重新 trim + hash 所有已提交文本并重算 match RecalcMatches(ctx context.Context, sessionID string, ignoreWs bool) error // UpdateParticipantNickname 原子重命名参与者:将 participant:{old} → participant:{new} // oldBrowserID 用于判断是否与冲突方是同一个浏览器(跳过冲突) // 返回业务错误:ErrSessionNotFound / ErrNicknameConflict UpdateParticipantNickname(ctx context.Context, sessionID, oldNick, newNick, oldBrowserID string) error // RemoveParticipant 参与者离开时从 Redis 删除其所有字段(hash/text/match) RemoveParticipant(ctx context.Context, sessionID, nickname string) error // Get 获取会话当前状态全量快照 Get(ctx context.Context, sessionID string) (*SessionSnapshot, error) // Delete 删除会话 Delete(ctx context.Context, sessionID string) error }