fix: 管理会话自动恢复与访问来源统计优化
- 新增 AdminSessionRecover:后台离线/后端重启时自动重试恢复会话,明确失效才回跳登录 - 认证 cookie 契约调整(SameSite=Lax 与刷新轮转适配),路由与守卫适配 - 访问来源统计查询优化与测试、管理端 analytics sources 页面适配 - 移动端布局修正:覆盖 body min-h-screen 避免内容区高度异常
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
@@ -88,11 +91,11 @@ type VisitSourcesKindDay struct {
|
||||
|
||||
// VisitSources 来源分析 Tab
|
||||
type VisitSources struct {
|
||||
RangeDays int `json:"range_days"`
|
||||
Kinds []VisitNamedCount `json:"kinds"`
|
||||
SearchHost []VisitNamedCount `json:"search_hosts"`
|
||||
External []VisitNamedCount `json:"external"`
|
||||
Trend []VisitSourcesKindDay `json:"trend"`
|
||||
RangeDays int `json:"range_days"`
|
||||
Kinds []VisitNamedCount `json:"kinds"`
|
||||
SearchTerms []VisitNamedCount `json:"search_terms"` // 站内搜索词 Top N(/?q= 结果页浏览)
|
||||
External []VisitNamedCount `json:"external"`
|
||||
Trend []VisitSourcesKindDay `json:"trend"`
|
||||
}
|
||||
|
||||
// VisitPages 受访页面 Tab
|
||||
@@ -336,8 +339,7 @@ func (s *VisitStatsService) Sources(rangeQ string) (*VisitSources, error) {
|
||||
if out.Kinds, err = s.groupCounts(start, "ref_kind", model.VisitKindPageview, "", 8); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.SearchHost, err = s.groupCounts(start, "ref_host", model.VisitKindPageview,
|
||||
"ref_kind = '"+RefKindSearch+"'", 20); err != nil {
|
||||
if out.SearchTerms, err = s.siteSearchTerms(start, 10); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.External, err = s.groupCounts(start, "ref_host", model.VisitKindPageview,
|
||||
@@ -350,6 +352,64 @@ func (s *VisitStatsService) Sources(rangeQ string) (*VisitSources, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// siteSearchTerms 站内搜索词:聚合搜索结果页(/?q=…,path 记录完整 query)的浏览量。
|
||||
// 词以 URL 编码存于 path,按编码串分组后解码展示(站内 encodeURIComponent 编码确定性强)。
|
||||
func (s *VisitStatsService) siteSearchTerms(start time.Time, limit int) ([]VisitNamedCount, error) {
|
||||
const expr = "substring(path from '[?&]q=([^&]*)')"
|
||||
var rows []struct {
|
||||
Key string
|
||||
Count int64
|
||||
}
|
||||
if err := s.db.Model(&model.VisitEvent{}).
|
||||
Select(expr+" AS key, COUNT(*) AS count").
|
||||
Where("created_at >= ? AND kind = ?", start, model.VisitKindPageview).
|
||||
Where(expr + " IS NOT NULL AND " + expr + " <> ''").
|
||||
Group("key").Order("count DESC").Limit(limit).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]VisitNamedCount, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
term := decodeSearchTerm(r.Key)
|
||||
if runes := []rune(term); len(runes) > 30 {
|
||||
term = string(runes[:30]) + "…"
|
||||
}
|
||||
out = append(out, VisitNamedCount{Key: term, Label: term, Count: r.Count})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// decodeSearchTerm 解码 path 里 q 参数值(URL 编码)。path 512 字节截断可能:
|
||||
// 1) 把 %XX 转义拦腰切断(孤 % / 一位十六进制)→ 整串解码失败,剥掉残缺尾部重试;
|
||||
// 2) 恰好切在多字节字符的转义序列中间 → 解码「成功」但尾部是半截字符,去掉非法 UTF-8 尾巴。
|
||||
func decodeSearchTerm(raw string) string {
|
||||
term := strings.ReplaceAll(raw, "+", " ")
|
||||
dec, err := url.QueryUnescape(term)
|
||||
if err != nil {
|
||||
// 残缺转义(如结尾孤 % 或一位十六进制):剥掉最后一个 % 起的尾部重试
|
||||
if i := strings.LastIndex(term, "%"); i > 0 {
|
||||
dec, err = url.QueryUnescape(term[:i])
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return term
|
||||
}
|
||||
return validUTF8Prefix(dec)
|
||||
}
|
||||
|
||||
// validUTF8Prefix 返回 s 的最长合法 UTF-8 前缀(截断产生的半截多字节字符会被去掉;
|
||||
// 真实的 U+FFFD 字符合法保留,与非法字节可区分)。
|
||||
func validUTF8Prefix(s string) string {
|
||||
for i := 0; i < len(s); {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
return s[:i]
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SourcesTrend 来源按日明细(kind 分列行)
|
||||
func (s *VisitStatsService) SourcesTrend(rangeQ string) ([]VisitSourcesKindDay, error) {
|
||||
days := parseRangeDays(rangeQ)
|
||||
|
||||
Reference in New Issue
Block a user