268 lines
6.9 KiB
Go
268 lines
6.9 KiB
Go
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
|