This commit is contained in:
CodeBuddy
2026-10-30 23:46:55 +08:00
parent bf99e52937
commit 0530523750
6 changed files with 120 additions and 14 deletions
+20
View File
@@ -0,0 +1,20 @@
# IDE
.idea/
# 运行产物
run/
*.log
# 本地配置(含密钥,不入库)
config.yaml
# 生产部署配置模板(含数据库密码,不入库;部署时复制到服务器后按需修改)
deploy/linux/config.prod.yaml
# 依赖与构建产物
node_modules/
dist/
dist-ssr/
# 系统文件
.DS_Store
Thumbs.db
+18
View File
@@ -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;
}
+21
View File
@@ -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
+1
View File
@@ -114,6 +114,7 @@ func adminStatsDetail(c *httpapi.Ctx) error {
"date_contents_total": total, "date_contents_total": total,
"total_users": tableCount(c, "users"), "total_users": tableCount(c, "users"),
"total_couples": tableCount(c, "couples"), "total_couples": tableCount(c, "couples"),
"total_storage_used": allStorageUsedBytes(),
}) })
} }
+36
View File
@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"past/internal/config" "past/internal/config"
@@ -99,6 +100,41 @@ func dirSize(root string) int64 {
return total 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 统计当前用户(或其情侣空间)已用存储字节数。 // storageUsedBytes 统计当前用户(或其情侣空间)已用存储字节数。
// 组成:情侣空间共享文件(couples/{space})+ 相关用户(本人,或情侣双方)的头像(avatars/{user_id})与个人上传文件(users/{id})。 // 组成:情侣空间共享文件(couples/{space})+ 相关用户(本人,或情侣双方)的头像(avatars/{user_id})与个人上传文件(users/{id})。
func storageUsedBytes(c *httpapi.Ctx, uid, coupleID int64) int64 { func storageUsedBytes(c *httpapi.Ctx, uid, coupleID int64) int64 {
+24 -14
View File
@@ -28,6 +28,15 @@ const CONTENT_LINK: Record<string, string> = {
timeline: "timeline", diaries: "diary", albums: "album", photos: "photo", songs: "song", checkins: "checkin", checkin_logs: "checkin", 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() { export default function Dashboard() {
const navigate = useNavigate() const navigate = useNavigate()
const stats = useQuery({ queryKey: ["admin_stats"], queryFn: () => api("adminStats", null, "GET") }) const stats = useQuery({ queryKey: ["admin_stats"], queryFn: () => api("adminStats", null, "GET") })
@@ -42,6 +51,21 @@ export default function Dashboard() {
return ( return (
<div className="space-y-5"> <div className="space-y-5">
<Card>
<CardHeader><CardTitle className="text-base">用户注册统计</CardTitle></CardHeader>
<CardContent>
{today.isPending ? <Loading rows={1} /> : today.isError ? <ErrorBox error={today.error} /> : (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
<StatCard label="今日新注册" value={today.data.today_registers} tone="pink" />
<StatCard label="本月新注册" value={today.data.month_registers} tone="violet" />
<StatCard label="总用户数" value={today.data.total_users} tone="green" />
<StatCard label="总情侣数" value={today.data.total_couples} tone="blue" />
<StatCard label="所有用户已用存储" value={formatBytes(today.data.total_storage_used)} tone="amber" />
</div>
)}
</CardContent>
</Card>
<div className="dashboard-intro"><div><PageTitle>空间动态</PageTitle><p>用户、关系与生活记录,一览即知。</p></div><Button variant="outline" onClick={() => navigate("/unbind")}>查看解绑请求 <ArrowUpRight size={16}/></Button></div> <div className="dashboard-intro"><div><PageTitle>空间动态</PageTitle><p>用户、关系与生活记录,一览即知。</p></div><Button variant="outline" onClick={() => navigate("/unbind")}>查看解绑请求 <ArrowUpRight size={16}/></Button></div>
{stats.isPending ? <Loading rows={2} /> : stats.isError ? <ErrorBox error={stats.error} /> : ( {stats.isPending ? <Loading rows={2} /> : stats.isError ? <ErrorBox error={stats.error} /> : (
@@ -68,20 +92,6 @@ export default function Dashboard() {
</div> </div>
)} )}
<Card>
<CardHeader><CardTitle className="text-base">用户注册统计</CardTitle></CardHeader>
<CardContent>
{today.isPending ? <Loading rows={1} /> : today.isError ? <ErrorBox error={today.error} /> : (
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<StatCard label="今日新注册" value={today.data.today_registers} tone="pink" />
<StatCard label="本月新注册" value={today.data.month_registers} tone="violet" />
<StatCard label="总用户数" value={today.data.total_users} tone="green" />
<StatCard label="总情侣数" value={today.data.total_couples} tone="blue" />
</div>
)}
</CardContent>
</Card>
<Card> <Card>
<CardHeader><CardTitle className="text-base">按日期查询</CardTitle></CardHeader> <CardHeader><CardTitle className="text-base">按日期查询</CardTitle></CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">