Files
2026-08-30 21:25:22 +08:00

268 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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