This commit introduces several significant architectural improvements to enhance system performance, scalability, and reliability:
- **Performance Optimization (N+1 Problem Resolution)**: Implemented batching mechanisms across `MessageHandler`, `ChatService`, `GroupService`, `MessageService`, and `UserService`. This replaces multiple individual database/cache queries with single batch operations for fetching unread counts, last messages, participants, and user information.
- **Enhanced Unread Count Management**: Migrated unread count tracking to a Redis Hash-based approach (`unread#️⃣{userID}`). This allows for atomic increments/decrements and efficient retrieval of both individual conversation unread counts and total unread counts for a user.
- **Improved Message Sequencing**: Integrated Redis-based sequence (`seq`) pre-allocation for messages to ensure strict ordering and reduce database contention during high-concurrency message creation.
- **Push Service Reliability**: Refactored the push notification system to include persistent `PushRecord` tracking. Added a recovery mechanism (`recoverPendingPushes`) to reload pending notifications from the database upon service startup, ensuring better delivery guarantees.
- **WebSocket Reliability**: Updated the WebSocket hub to move away from in-memory history replay in favor of a client-driven synchronization model (`sync_required` event and `ack` handling), reducing memory overhead and improving connection stability.
- **Cache Layer Enhancements**: Added `HIncrBy` and `IncrBySeq` to the `Cache` interface and its implementations (`RedisCache`, `LayeredCache`) to support the new unread and sequence management logic.
231 lines
5.3 KiB
Go
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})
|
|
}
|