package dual_check import ( "errors" "net/http" "strings" "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 } if err := validateNickname(req.Nickname); err != nil { response.BadRequest(c, err.Error()) return } resp, err := h.service.CreateSession(c.Request.Context(), req.Nickname, req.BrowserID) 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 err := validateNickname(req.Nickname); err != nil { response.BadRequest(c, err.Error()) 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": 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 } if err := validateNickname(req.Nickname); err != nil { response.BadRequest(c, err.Error()) return } sess, actualNick, err := h.service.JoinParticipant(c.Request.Context(), req.SessionID, req.Nickname, req.BrowserID) 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 } h.hub.Broadcast(req.SessionID, response.WSParticipantJoined(actualNick)) if actualNick != req.Nickname { response.Success(c, gin.H{ "session": sess, "actual_nickname": actualNick, "renamed": true, }) return } response.Success(c, sess) } // GetSession POST /api/v1/dual-check/session func (h *Handler) GetSession(c *gin.Context) { var req SessionRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, ErrMessage(ErrCodeInvalidParam)) return } sess, err := h.service.GetSession(c.Request.Context(), req.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 POST /api/v1/dual-check/check func (h *Handler) CheckSession(c *gin.Context) { var req SessionRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, ErrMessage(ErrCodeInvalidParam)) return } info, err := h.service.CheckSession(c.Request.Context(), req.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/resubmit func (h *Handler) SetResubmit(c *gin.Context) { var req SetResubmitRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, ErrMessage(ErrCodeInvalidParam)) return } sess, err := h.service.SetResubmit(c.Request.Context(), req.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(req.SessionID, response.WSResubmitChanged(req.Allow)) response.Success(c, sess) } // SetIgnoreWs POST /api/v1/dual-check/ignore-ws func (h *Handler) SetIgnoreWs(c *gin.Context) { var req SetIgnoreWsRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, ErrMessage(ErrCodeInvalidParam)) return } sess, err := h.service.SetIgnoreWs(c.Request.Context(), req.SessionID, req.Ignore) if err != nil { var be *BusinessError if errors.As(err, &be) { response.Fail(c, be.Code, be.Error()) return } h.logger.Error("set ignore_ws error", zap.Error(err)) response.InternalError(c, "set ignore_ws failed") return } h.hub.Broadcast(req.SessionID, response.WSIgnoreWsChanged(req.Ignore)) response.Success(c, sess) } // EndSession POST /api/v1/dual-check/end func (h *Handler) EndSession(c *gin.Context) { var req SessionRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, ErrMessage(ErrCodeInvalidParam)) return } // 先广播,让所有参与方 UI 都弹提示 h.hub.Broadcast(req.SessionID, response.WSSessionEnded()) // 再删除数据 if err := h.service.EndSession(c.Request.Context(), req.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}) } // validateNickname 统一的昵称校验:字符数 1-32(按 Unicode rune) func validateNickname(n string) error { n = strings.TrimSpace(n) if utf8.RuneCountInString(n) == 0 { return errors.New("nickname is required") } if utf8.RuneCountInString(n) > 32 { return errors.New("nickname must be 1-32 characters") } return nil } var _ = http.StatusOK