feat: 站点访问统计与可配置侧边栏组件

- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
2026-09-24 18:53:45 +08:00
parent 3e55b5d230
commit 7b327bc8cc
60 changed files with 6539 additions and 572 deletions

View File

@@ -0,0 +1,63 @@
package service
import (
"net/url"
"strings"
)
// 来源分类常量
const (
RefKindDirect = "direct"
RefKindInternal = "internal"
RefKindSearch = "search"
RefKindSocial = "social"
RefKindExternal = "external"
)
// searchHosts 搜索引擎域名(按「等于或以 .子域结尾」匹配)
var searchHosts = []string{"google.com", "bing.com", "baidu.com", "sogou.com", "so.com", "sm.cn", "duckduckgo.com", "yandex.com", "yandex.ru", "yahoo.com"}
// socialHosts 社交/社区域名特征
var socialHosts = []string{
"weibo.com", "weibo.cn", "zhihu.com", "zhuanlan.zhihu.com", "twitter.com", "x.com",
"t.me", "telegram.me", "douyin.com", "xiaohongshu.com", "bilibili.com", "b23.tv",
"qq.com", "tieba.baidu.com", "v2ex.com", "juejin.cn", "csdn.net", "segmentfault.com",
"github.com", "gitee.com", "reddit.com", "facebook.com", "instagram.com", "discord.com",
}
// hostMatches 按「等于或以 .子域结尾」匹配 host 与域名列表
func hostMatches(host string, list []string) bool {
for _, d := range list {
if host == d || strings.HasSuffix(host, "."+d) {
return true
}
}
return false
}
// ClassifyReferrer 解析并归类 Referer:返回 (来源 host, 来源类别)。
// 空 → direct;本站 → internal;搜索引擎 → search;社交 → social;其余 → external;解析失败按 direct。
func ClassifyReferrer(raw, siteHost string) (string, string) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", RefKindDirect
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return "", RefKindDirect
}
host := strings.ToLower(u.Host)
if i := strings.LastIndex(host, ":"); i >= 0 && !strings.Contains(host, "]") {
host = host[:i] // 去端口
}
if siteHost != "" && (host == siteHost || siteHost == host) {
return host, RefKindInternal
}
if hostMatches(host, searchHosts) {
return host, RefKindSearch
}
if hostMatches(host, socialHosts) {
return host, RefKindSocial
}
return host, RefKindExternal
}