Files
backend/internal/handler/qrcode_handler.go
lan d9aa4b46c3
All checks were successful
Build Backend / build (push) Successful in 4m55s
Build Backend / build-docker (push) Successful in 10m34s
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00

231 lines
5.3 KiB
Go

package handler
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"with_you/internal/pkg/response"
"with_you/internal/pkg/ws"
"with_you/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,
})
}
// WSEvents WebSocket事件流
// GET /api/v1/auth/qrcode/events?session_id=xxx
func (h *QRCodeHandler) WSEvents(c *gin.Context) {
sessionID := c.Query("session_id")
if sessionID == "" {
response.BadRequest(c, "session_id is required")
return
}
ch, cancel := h.qrcodeService.GetWSHub().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 ws.Event) bool {
data, err := ws.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
}
// 心跳
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})
}