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

221 lines
6.0 KiB
Go

package dual_check
import (
"errors"
"net/http"
"unicode/utf8"
pkgws "cancerai.top/toolbox-server/internal/pkg/websocket"
"cancerai.top/toolbox-server/internal/response"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
)
type Handler struct {
service *Service
hub *pkgws.Hub
logger *zap.Logger
maxLen int
validate *validator.Validate
}
func NewHandler(service *Service, hub *pkgws.Hub, logger *zap.Logger, maxTextLen int) *Handler {
return &Handler{
service: service,
hub: hub,
logger: logger,
maxLen: maxTextLen,
validate: validator.New(),
}
}
// Create POST /api/v1/dual-check/create
func (h *Handler) Create(c *gin.Context) {
var req CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, ErrMessage(ErrCodeInvalidParam))
return
}
resp, err := h.service.CreateSession(c.Request.Context(), req.Nickname)
if err != nil {
var be *BusinessError
if errors.As(err, &be) {
response.Fail(c, be.Code, be.Error())
return
}
h.logger.Error("create session error", zap.Error(err))
response.InternalError(c, "create failed")
return
}
response.Success(c, resp)
}
// Submit POST /api/v1/dual-check/submit
func (h *Handler) Submit(c *gin.Context) {
// ContentLength 保护:用 maxLen*4 覆盖 UTF-8 最大 4 字节/字符 + JSON 字段开销
if c.Request.ContentLength > int64(h.maxLen*4+2048) {
response.BadRequest(c, "text too long")
return
}
var req SubmitRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, ErrMessage(ErrCodeInvalidParam))
return
}
if utf8.RuneCountInString(req.Text) > h.maxLen {
response.BadRequest(c, "text too long")
return
}
resp, err := h.service.Submit(c.Request.Context(), &req)
if err != nil {
var be *BusinessError
if errors.As(err, &be) {
response.Fail(c, be.Code, be.Error())
return
}
h.logger.Error("submit error", zap.Error(err),
zap.String("session_id", truncateSessionID(req.SessionID)))
response.InternalError(c, "submit failed")
return
}
// 广播事件到房间内所有连接(包括发起方和其他参与者)
switch req.Role {
case "initiator":
// 发起方刚提交:触发全量快照推送(所有参与者都需要更新)
sess, _ := h.service.GetSession(c.Request.Context(), req.SessionID)
if sess != nil {
h.hub.Broadcast(req.SessionID, response.WSFullState(sess))
}
case "participant":
// 参与者刚提交:推送 participant_submitted 事件
// 发起方需要更新该参与者的提交状态 + 可能的 match 结果
h.hub.Broadcast(req.SessionID, response.WSParticipantSubmitted(req.Nickname, resp))
}
response.Success(c, resp)
}
// Join POST /api/v1/dual-check/join — 参与者加入(登记昵称)
func (h *Handler) Join(c *gin.Context) {
var req JoinRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, ErrMessage(ErrCodeInvalidParam))
return
}
sessionID := c.Param("session_id")
if sessionID == "" {
response.BadRequest(c, "session_id is required")
return
}
sess, err := h.service.JoinParticipant(c.Request.Context(), sessionID, req.Nickname)
if err != nil {
var be *BusinessError
if errors.As(err, &be) {
response.Fail(c, be.Code, be.Error())
return
}
h.logger.Error("join participant error", zap.Error(err))
response.InternalError(c, "join failed")
return
}
// 广播 participant_joined 事件
h.hub.Broadcast(sessionID, response.WSParticipantJoined(req.Nickname))
response.Success(c, sess)
}
// GetSession GET /api/v1/dual-check/session/:session_id
func (h *Handler) GetSession(c *gin.Context) {
sessionID := c.Param("session_id")
sess, err := h.service.GetSession(c.Request.Context(), sessionID)
if err != nil {
var be *BusinessError
if errors.As(err, &be) && be.Code == ErrCodeSessionNotFound {
response.NotFound(c, be.Error())
return
}
response.InternalError(c, "get session failed")
return
}
response.Success(c, sess)
}
// CheckSession GET /api/v1/dual-check/check/:session_id
// 前端加入前预校验
func (h *Handler) CheckSession(c *gin.Context) {
sessionID := c.Param("session_id")
info, err := h.service.CheckSession(c.Request.Context(), sessionID)
if err != nil {
response.InternalError(c, "check session failed")
return
}
if !info.Exists {
response.Fail(c, ErrCodeSessionNotFound, ErrMessage(ErrCodeSessionNotFound))
return
}
response.Success(c, info)
}
// SetResubmit POST /api/v1/dual-check/session/:session_id/resubmit
// 发起方切换"允许参与者重新提交"开关
func (h *Handler) SetResubmit(c *gin.Context) {
sessionID := c.Param("session_id")
if sessionID == "" {
response.BadRequest(c, "session_id is required")
return
}
var req SetResubmitRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, ErrMessage(ErrCodeInvalidParam))
return
}
sess, err := h.service.SetResubmit(c.Request.Context(), sessionID, req.Allow)
if err != nil {
var be *BusinessError
if errors.As(err, &be) {
response.Fail(c, be.Code, be.Error())
return
}
h.logger.Error("set resubmit error", zap.Error(err))
response.InternalError(c, "set resubmit failed")
return
}
// 广播开关变化给所有参与者
h.hub.Broadcast(sessionID, response.WSResubmitChanged(req.Allow))
response.Success(c, sess)
}
// EndSession POST /api/v1/dual-check/session/:session_id/end
// 发起方主动结束会话:先广播 session_ended,再 Delete Redis key
func (h *Handler) EndSession(c *gin.Context) {
sessionID := c.Param("session_id")
if sessionID == "" {
response.BadRequest(c, "session_id is required")
return
}
// 先广播,让所有参与方 UI 都弹提示
h.hub.Broadcast(sessionID, response.WSSessionEnded())
// 再删除数据
if err := h.service.EndSession(c.Request.Context(), sessionID); err != nil {
h.logger.Error("end session error", zap.Error(err))
response.InternalError(c, "end session failed")
return
}
response.Success(c, gin.H{"ended": true})
}
// 占位,避免 http.StatusOK 未使用
var _ = http.StatusOK