diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55271d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# IDE +.idea/ + +# 运行产物 +run/ +*.log + +# 本地配置(含密钥,不入库) +config.yaml +# 生产部署配置模板(含数据库密码,不入库;部署时复制到服务器后按需修改) +deploy/linux/config.prod.yaml + +# 依赖与构建产物 +node_modules/ +dist/ +dist-ssr/ + +# 系统文件 +.DS_Store +Thumbs.db diff --git a/deploy/linux/nginx-past.conf b/deploy/linux/nginx-past.conf new file mode 100644 index 0000000..a741623 --- /dev/null +++ b/deploy/linux/nginx-past.conf @@ -0,0 +1,18 @@ +# 宝塔网站 → 设置 → 配置文件,把下面整段放到 server { } 内(或改用宝塔“反向代理”UI) +# 作用:把域名的所有请求反代到本地 Go 后端(:8080),由宝塔 Nginx 负责 HTTPS/SSL。 + +# 允许大文件上传(与 config.yaml 的 max_upload_mb 保持一致,默认 50MB) +client_max_body_size 50m; + +location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # 大文件/慢请求超时,与后端 ReadTimeout 对应 + proxy_connect_timeout 60s; + proxy_read_timeout 180s; + proxy_send_timeout 180s; +} diff --git a/deploy/linux/past.service b/deploy/linux/past.service new file mode 100644 index 0000000..24eccf2 --- /dev/null +++ b/deploy/linux/past.service @@ -0,0 +1,21 @@ +[Unit] +Description=past-go backend service +After=network.target mysql.service +Wants=mysql.service + +[Service] +Type=simple +# 运行用户:宝塔默认站点用户为 www,需对该用户有 data_dir/uploads 写权限 +User=www +Group=www +# 改为你实际上传二进制与 config.yaml 的目录 +WorkingDirectory=/www/wwwroot/past +ExecStart=/www/wwwroot/past/past-server -config /www/wwwroot/past/config.yaml +Restart=always +RestartSec=3 +# 标准输出/错误会进 journalctl;程序自身也会写 data/go-server.log +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/past-go/internal/handler/admin.go b/past-go/internal/handler/admin.go index 57506b8..08e6ad1 100644 --- a/past-go/internal/handler/admin.go +++ b/past-go/internal/handler/admin.go @@ -114,6 +114,7 @@ func adminStatsDetail(c *httpapi.Ctx) error { "date_contents_total": total, "total_users": tableCount(c, "users"), "total_couples": tableCount(c, "couples"), + "total_storage_used": allStorageUsedBytes(), }) } diff --git a/past-go/internal/handler/storage.go b/past-go/internal/handler/storage.go index ae81738..ef1ab38 100644 --- a/past-go/internal/handler/storage.go +++ b/past-go/internal/handler/storage.go @@ -9,6 +9,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "time" "past/internal/config" @@ -99,6 +100,41 @@ func dirSize(root string) int64 { return total } +// storageUsedTotalCache 缓存全站已用存储总量,避免后台统计页频繁整目录遍历。 +var storageUsedTotalCache = struct { + sync.RWMutex + bytes int64 + at time.Time +}{} + +// allStorageUsedBytes 统计存储目录所有文件占用字节数,即全站所有用户已使用的存储空间总量。 +// 注意:按文件系统实际占用计算(情侣共享空间只计一次),更准确反映磁盘消耗。结果缓存 60 秒。 +func allStorageUsedBytes() int64 { + storageUsedTotalCache.RLock() + if !storageUsedTotalCache.at.IsZero() && time.Since(storageUsedTotalCache.at) < time.Minute { + v := storageUsedTotalCache.bytes + storageUsedTotalCache.RUnlock() + return v + } + storageUsedTotalCache.RUnlock() + + root := up.Dir + var total int64 + if root != "" { + _ = filepath.Walk(root, func(_ string, info os.FileInfo, err error) error { + if err == nil && info != nil && !info.IsDir() { + total += info.Size() + } + return nil + }) + } + storageUsedTotalCache.Lock() + storageUsedTotalCache.bytes = total + storageUsedTotalCache.at = time.Now() + storageUsedTotalCache.Unlock() + return total +} + // storageUsedBytes 统计当前用户(或其情侣空间)已用存储字节数。 // 组成:情侣空间共享文件(couples/{space})+ 相关用户(本人,或情侣双方)的头像(avatars/{user_id})与个人上传文件(users/{id})。 func storageUsedBytes(c *httpapi.Ctx, uid, coupleID int64) int64 { diff --git a/past-go/web/admin/src/pages/Dashboard.tsx b/past-go/web/admin/src/pages/Dashboard.tsx index a91fb74..7a02eb2 100644 --- a/past-go/web/admin/src/pages/Dashboard.tsx +++ b/past-go/web/admin/src/pages/Dashboard.tsx @@ -28,6 +28,15 @@ const CONTENT_LINK: Record = { timeline: "timeline", diaries: "diary", albums: "album", photos: "photo", songs: "song", checkins: "checkin", checkin_logs: "checkin", } +function formatBytes(n?: number): string { + if (!n || n <= 0) return "0 B" + const units = ["B", "KB", "MB", "GB", "TB"] + let i = 0 + let v = n + while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ } + return `${v >= 100 || i === 0 ? Math.round(v) : v.toFixed(1)} ${units[i]}` +} + export default function Dashboard() { const navigate = useNavigate() const stats = useQuery({ queryKey: ["admin_stats"], queryFn: () => api("adminStats", null, "GET") }) @@ -42,6 +51,21 @@ export default function Dashboard() { return (
+ + 用户注册统计 + + {today.isPending ? : today.isError ? : ( +
+ + + + + +
+ )} +
+
+
空间动态

用户、关系与生活记录,一览即知。

{stats.isPending ? : stats.isError ? : ( @@ -68,20 +92,6 @@ export default function Dashboard() {
)} - - 用户注册统计 - - {today.isPending ? : today.isError ? : ( -
- - - - -
- )} -
-
- 按日期查询