feat: add hook system, QR code login, and layered cache
- Implement extensible hook system for content moderation with builtin and AI-powered moderation hooks - Add QR code login feature with SSE for real-time status updates and scan/confirm/cancel workflow - Introduce layered cache with local LRU + Redis backend for improved read performance - Refactor post and comment services to use hook-based moderation instead of direct AI service calls - Add local cache configuration options (size, buckets, ttl)
This commit is contained in:
237
internal/handler/qrcode_handler.go
Normal file
237
internal/handler/qrcode_handler.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"carrot_bbs/internal/pkg/response"
|
||||
"carrot_bbs/internal/pkg/sse"
|
||||
"carrot_bbs/internal/service"
|
||||
)
|
||||
|
||||
// QRCodeHandler 二维码登录处理器
|
||||
type QRCodeHandler struct {
|
||||
qrcodeService *service.QRCodeLoginService
|
||||
}
|
||||
|
||||
// NewQRCodeHandler 创建二维码登录处理器
|
||||
func NewQRCodeHandler(qrcodeService *service.QRCodeLoginService) *QRCodeHandler {
|
||||
return &QRCodeHandler{
|
||||
qrcodeService: qrcodeService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetQRCode 获取二维码
|
||||
// GET /api/v1/auth/qrcode
|
||||
func (h *QRCodeHandler) GetQRCode(c *gin.Context) {
|
||||
session, err := h.qrcodeService.CreateSession(c.Request.Context())
|
||||
if err != nil {
|
||||
response.InternalServerError(c, "failed to create qrcode session")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成二维码URL (deeplink格式)
|
||||
qrcodeURL := fmt.Sprintf("carrotbbs://qrcode/login?session_id=%s", session.SessionID)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"session_id": session.SessionID,
|
||||
"qrcode_url": qrcodeURL,
|
||||
"expires_in": 300, // 5分钟
|
||||
"expires_at": session.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
// SSEEvents SSE事件流
|
||||
// GET /api/v1/auth/qrcode/events?session_id=xxx
|
||||
func (h *QRCodeHandler) SSEEvents(c *gin.Context) {
|
||||
sessionID := c.Query("session_id")
|
||||
if sessionID == "" {
|
||||
response.BadRequest(c, "session_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
ch, cancel, replay := h.qrcodeService.GetSSEHub().Subscribe(sessionID, 0)
|
||||
defer cancel()
|
||||
|
||||
w := c.Writer
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
response.InternalServerError(c, "streaming unsupported")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
c.Status(http.StatusOK)
|
||||
flusher.Flush()
|
||||
|
||||
writeEvent := func(ev sse.Event) bool {
|
||||
data, err := sse.EncodeData(ev)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "id: %d\nevent: %s\ndata: %s\n\n", ev.ID, ev.Event, data); err != nil {
|
||||
return false
|
||||
}
|
||||
flusher.Flush()
|
||||
return true
|
||||
}
|
||||
|
||||
// 发送历史事件
|
||||
for _, ev := range replay {
|
||||
if !writeEvent(ev) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳
|
||||
heartbeat := time.NewTicker(25 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
case ev, ok := <-ch:
|
||||
if !ok || !writeEvent(ev) {
|
||||
return
|
||||
}
|
||||
case <-heartbeat.C:
|
||||
if _, err := fmt.Fprint(w, "event: heartbeat\ndata: {}\n\n"); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 扫描二维码
|
||||
// POST /api/v1/auth/qrcode/scan
|
||||
func (h *QRCodeHandler) Scan(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
type ScanRequest struct {
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
|
||||
var req ScanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.qrcodeService.Scan(c.Request.Context(), req.SessionID, userID); err != nil {
|
||||
if err.Error() == "qrcode already scanned" {
|
||||
response.BadRequest(c, "qrcode already scanned by others")
|
||||
return
|
||||
}
|
||||
if err.Error() == "session not found or expired" {
|
||||
response.NotFound(c, "qrcode expired")
|
||||
return
|
||||
}
|
||||
response.InternalServerError(c, "failed to scan qrcode")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息返回
|
||||
user, err := h.qrcodeService.GetUserService().GetUserByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, "failed to get user info")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"nickname": user.Nickname,
|
||||
"avatar": user.Avatar,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Confirm 确认登录
|
||||
// POST /api/v1/auth/qrcode/confirm
|
||||
func (h *QRCodeHandler) Confirm(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
type ConfirmRequest struct {
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
|
||||
var req ConfirmRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
result, err := h.qrcodeService.Confirm(c.Request.Context(), req.SessionID, userID, ip, userAgent)
|
||||
if err != nil {
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Unauthorized(c, "not authorized to confirm this login")
|
||||
return
|
||||
}
|
||||
if err.Error() == "session not found or expired" {
|
||||
response.NotFound(c, "qrcode expired")
|
||||
return
|
||||
}
|
||||
response.InternalServerError(c, "failed to confirm login")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"success": true,
|
||||
"user": gin.H{
|
||||
"id": result.User.ID,
|
||||
"username": result.User.Username,
|
||||
"nickname": result.User.Nickname,
|
||||
"avatar": result.User.Avatar,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Cancel 取消登录
|
||||
// POST /api/v1/auth/qrcode/cancel
|
||||
func (h *QRCodeHandler) Cancel(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
type CancelRequest struct {
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
|
||||
var req CancelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.qrcodeService.Cancel(c.Request.Context(), req.SessionID, userID); err != nil {
|
||||
if err.Error() == "unauthorized" {
|
||||
response.Unauthorized(c, "not authorized to cancel this login")
|
||||
return
|
||||
}
|
||||
response.InternalServerError(c, "failed to cancel login")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"success": true})
|
||||
}
|
||||
Reference in New Issue
Block a user