fix: 管理会话自动恢复与访问来源统计优化

- 新增 AdminSessionRecover:后台离线/后端重启时自动重试恢复会话,明确失效才回跳登录
- 认证 cookie 契约调整(SameSite=Lax 与刷新轮转适配),路由与守卫适配
- 访问来源统计查询优化与测试、管理端 analytics sources 页面适配
- 移动端布局修正:覆盖 body min-h-screen 避免内容区高度异常
This commit is contained in:
2026-09-28 03:33:52 +08:00
parent 0c3b3a710c
commit 3e0689fe98
15 changed files with 548 additions and 85 deletions

View File

@@ -242,13 +242,21 @@ func (h *Handlers) Refresh(c *gin.Context) {
}
accessToken, newRefresh, user, err := h.Auth.RotateRefreshToken(refreshToken, c.ClientIP(), c.Request.UserAgent())
if err != nil {
clearAuthCookies(c, !h.Cfg.DevMode)
if errors.Is(err, service.ErrAccountBanned) {
// 账号已封禁:清 cookie 的同时给出可识别 code,前端弹封禁告知并强制下线
clearAuthCookies(c, !h.Cfg.DevMode)
c.JSON(http.StatusForbidden, gin.H{"error": "账号已被封禁", "code": "account_banned"})
return
}
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
if errors.Is(err, service.ErrRefreshInvalid) || errors.Is(err, service.ErrRefreshExpired) {
// 登录态确实失效:清 cookie 要求重新登录(前端弹"登录已失效")
clearAuthCookies(c, !h.Cfg.DevMode)
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
return
}
// 其他错误(数据库抖动等):服务暂不可用。保留 cookie 等下次刷新,
// 前端按瞬态失败处理(不弹"登录已失效"、不强制重登)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "登录刷新暂不可用,请稍后重试"})
return
}
setAuthCookies(c, accessToken, newRefresh, !h.Cfg.DevMode)

View File

@@ -8,6 +8,7 @@ import (
"net/url"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/freefire/jiang13-bbs/middleware"
@@ -16,13 +17,20 @@ import (
"github.com/gin-gonic/gin"
)
// maxSearchKeywordRunes 帖子搜索关键词长度上限(字符数,与前端搜索框 maxLength 一致)
const maxSearchKeywordRunes = 100
// Posts 获取帖子列表
func (h *Handlers) Posts(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
boardID, _ := strconv.ParseUint(c.Query("board_id"), 10, 64)
sort := c.DefaultQuery("sort", "latest")
keyword := c.Query("keyword")
keyword := strings.TrimSpace(c.Query("keyword"))
// 搜索词上限:前端输入框同限;超长词 ILIKE 必然无意义结果,静默截断
if runes := []rune(keyword); len(runes) > maxSearchKeywordRunes {
keyword = string(runes[:maxSearchKeywordRunes])
}
recommended := c.Query("recommended") == "true"
var viewerID uint