云传 开发者文档
项目文档

{{H1}}

apk-dist · 通用文件分发平台 · Go 1.26 + Gin

通用文件分发平台,支持 APK、图片、视频、压缩包、文档等多种格式。上传后自动生成短链接与二维码,APK 文件额外解析应用名、包名、版本等元信息并支持多版本管理。类似 betaqr / fir.im 的精简自托管版本,支持 Cloudflare R2 对象存储。

后端 Go 1.26 + Gin存储 Cloudflare R2单文件上限 5 GB生产环境 send.machinaix.com

功能特性

功能说明
多格式文件上传支持 APK、图片(JPG/PNG/GIF/WebP)、视频(MP4/MOV/AVI/MKV)、压缩包(ZIP/RAR/7z)、文档(PDF/Word/Excel/PPT/TXT),最大 5 GB
文件类型安全检测扩展名白名单 + 魔数(magic byte)双重校验,防止文件类型伪造
文件分类展示仪表盘与管理后台按类型分 Tab(全部 / APK / 图片 / 视频 / 压缩包 / 文档),APK 按包名分组,其他文件平铺列表
多用户注册登录账号各自管理自己上传的文件,注册入口默认关闭(内测阶段)
邮箱验证注册后生成 24 小时验证链接展示在页面,点击完成验证后才能登录;无需 SMTP 服务器,适合自托管
忘记密码生成 15 分钟有效的重置链接,无需邮件服务器
拖拽上传支持拖拽 / 点击选择文件,显示上传进度条,最大 5 GB
APK 自动解析自动提取应用名、包名、版本名、版本号、启动图标(失败时降级处理)
短链接生成 /d/{12位随机码} 格式的公开下载链接
二维码上传成功页和下载页均展示二维码,支持 PNG 下载
版本历史同一包名的所有 APK 版本分组管理,下载页显示版本列表(默认 5 条,可展开)
更新说明每个版本可填写描述,下载页展示
密码保护为每个文件单独设置下载密码(可选),验证通过后 Cookie 记录 10 分钟
防暴力破解登录/管理员登录/下载密码均有 IP 限速,超限返回 429
管理后台管理员可查看所有用户的文件,支持编辑、删除、查看下载次数
流式上传/下载非 APK 大文件直接将 multipart.File 流式写入 R2,不在服务器落盘;下载走 R2 预签名 URL 302 跳转,绕过服务器直达 CDN
Cloudflare R2 存储文件存储于 R2 对象存储,下载走 Cloudflare CDN;未配置 R2 时自动降级到本地磁盘
响应式 UI登录/注册页左右分屏布局,列表/详情页自适应桌面和移动端
品牌动画登录/注册/忘记密码页左侧纯 CSS 动画(blob + 浮动文件卡片 + 云上传图标),不依赖 JS
自定义 Favicon云朵+上传箭头 SVG 图标,与品牌配色一致

技术栈

后端:  Go 1.26 + Gin v1.12 + GORM v1.25
数据库:SQLite(glebarez/sqlite,纯 Go,无需 CGO)
APK 解析:shogo82148/androidbinary
二维码:skip2/go-qrcode
密码哈希:golang.org/x/crypto/bcrypt
对象存储:aws/aws-sdk-go-v2(兼容 S3 API,接入 Cloudflare R2)
前端:  HTML + Tailwind CSS(CDN)+ HTMX 1.9(局部更新)
部署:  Docker + Nginx

数据库模型

User(用户)

字段类型说明
IDuint主键,自增
Usernamestring用户名,唯一,最长 32 字符
Emailstring邮箱,注册时必填
EmailVerifiedbool邮箱是否已验证,未验证不可登录
PasswordHashstringbcrypt 哈希,不存明文
CreatedAt / UpdatedAttime.Time自动维护

App(文件记录)

字段类型说明
IDuint主键,自增
FileTypestring文件类型:apk / image / video / zip / document(默认 apk
Namestring文件/应用名称
PackageNamestring包名(APK 专用,如 com.example.app;其他格式为空)
VersionNamestring版本名(APK 专用)
VersionCodeint32版本号(APK 专用)
Descriptionstring更新说明 / 文件描述
ShortCodestring下载短码(唯一,12 位 hex)
FileNamestring原始文件名
FilePathstring磁盘存储路径(R2 模式下为对象 Key)
FileSizeint64文件大小(字节)
HasIconbool是否已提取到图标(APK 专用)
UserIDuint所属用户 ID(0 = 历史遗留/管理员创建)
Passwordstring下载密码 bcrypt 哈希,空 = 无密码
DownloadCountint累计下载次数
CreatedAt / UpdatedAttime.Time自动维护

本地开发启动

前提

  • Go 已安装(本项目安装在 D:\InstallSoft\Go
  • 已克隆/下载本项目到本地

方式一:直接运行编译好的 exe(推荐)

cd D:\ANDREW\Work\Personal\GoWork\apk-dist

# 可选:设置环境变量(以下为默认值)
$env:PORT           = "8080"
$env:BASE_URL       = "http://localhost:8080"
$env:ADMIN_PASSWORD = "admin123"

.\apk-dist.exe

方式二:源码运行

cd D:\ANDREW\Work\Personal\GoWork\apk-dist
$env:GOROOT = "D:\InstallSoft\Go"
$env:PATH   = "D:\InstallSoft\Go\bin;" + $env:PATH

go run .

方式三:重新编译后运行

cd D:\ANDREW\Work\Personal\GoWork\apk-dist
$env:GOROOT = "D:\InstallSoft\Go"
$env:PATH   = "D:\InstallSoft\Go\bin;" + $env:PATH

go build -o apk-dist.exe .
.\apk-dist.exe

访问地址

页面地址
登录http://localhost:8080/login
上传页(需登录)http://localhost:8080/
我的文件http://localhost:8080/my
管理后台http://localhost:8080/admin
💡

管理员密码默认 admin123,通过 ADMIN_PASSWORD 环境变量修改。 启动日志只打印后台 URL,不会打印密码。

完整配置项

所有配置通过环境变量传入:

变量名默认值说明
PORT8080监听端口
APP_ENVdevelopment环境,production 时关闭 Gin 调试日志
BASE_URLhttp://localhost:8080对外访问域名(用于生成二维码/重置链接)
ADMIN_PASSWORDadmin123管理后台登录密码,生产必须修改
SESSION_SECRETplease-change-this...Cookie 签名密钥,生产必须修改
UPLOAD_DIR./uploads本地存储路径(R2 未配置时使用)
DATA_DIR./dataSQLite 数据库存储路径
MAX_UPLOAD_MB5120单个文件最大上传大小(MB),默认 5 GB
R2_ACCOUNT_ID``Cloudflare Account ID
R2_ACCESS_KEY_ID``R2 API 令牌 Access Key ID
R2_SECRET_ACCESS_KEY``R2 API 令牌 Secret Access Key
R2_BUCKET``R2 Bucket 名称
💡

R2 四个变量全部填写时自动启用 R2 存储;任一为空则降级到本地磁盘。

云端部署(Cloudflare + aaPanel + Docker)

当前生产环境:send.machinaix.com,架构为 Cloudflare CDN → aaPanel Nginx(宝塔)→ Docker 容器(127.0.0.1:8080)。 文件存储在 Cloudflare R2(Bucket:yunchuan),SQLite 数据库存储在服务器本地磁盘。

用户 → Cloudflare(HTTPS/CDN) → 服务器 aaPanel Nginx(:80/:443) → Docker app(:8080) 上传:app → Cloudflare R2(流式写入,服务器不落盘) 下载:app 生成预签名 URL → 302 跳转 → 用户直连 R2 CDN

首次部署

1. 服务器准备

# 安装 Docker
curl -fsSL https://get.docker.com | sh
apt install docker-compose-plugin -y

# 在 aaPanel 防火墙开放 80、443 端口(不暴露 8080)

2. 克隆项目到服务器

cd /www/wwwroot           # aaPanel 默认网站目录
git clone https://github.com/你的用户名/apk-dist.git
cd apk-dist

3. 修改 docker-compose.yml 中的配置

vim docker-compose.yml
environment:
  BASE_URL:              "https://send.machinaix.com"
  ADMIN_PASSWORD:        "your-strong-password"       # ← 必须修改
  SESSION_SECRET:        "your-random-32-char-secret" # ← 必须修改
  MAX_UPLOAD_MB:         "5120"
  R2_ACCOUNT_ID:         "your-account-id"            # ← Cloudflare Dashboard 右侧 Account ID
  R2_ACCESS_KEY_ID:      "your-access-key-id"         # ← R2 API 令牌的 Access Key ID
  R2_SECRET_ACCESS_KEY:  "your-secret-access-key"     # ← R2 API 令牌的 Secret Access Key
  R2_BUCKET:             "yunchuan"                   # ← 你创建的 Bucket 名称
💡

获取 R2 凭证:Cloudflare Dashboard → R2 → 创建 Bucket(如 yunchuan,建议选 APAC 区域)→ 右上角「管理 R2 API 令牌」→ 创建令牌(权限选「对象读和写」,范围选「所有 Bucket」)→ 复制 Access Key ID 和 Secret Access Key(只显示一次,立即保存)。

4. 首次启动容器

docker compose up -d --build

容器启动后监听 127.0.0.1:8080,不对外暴露。

5. 配置 aaPanel 反向代理

在 aaPanel → 网站 → send.machinaix.com → 反向代理,添加:

字段
代理名称apk-dist
目标 URLhttp://127.0.0.1:8080
发送域名$host

6. 配置 Cloudflare SSL(Full Strict 模式)

  1. 在 Cloudflare → SSL/TLS → Origin Server 生成 Origin Certificate(覆盖 *.machinaix.com,15 年有效期)
  2. 将证书和私钥粘贴到 aaPanel → 网站 → send.machinaix.com → SSL → 其他证书
  3. 在 aaPanel 防火墙开放 443 端口
  4. 在 Cloudflare SSL/TLS 加密模式选择 完整(严格)(Full Strict)

7. 在 Cloudflare 添加子域名 DNS 记录

Cloudflare → machinaix.com → DNS → 添加记录:

类型名称IPv4代理状态
Asend服务器 IP橙云(已代理)

更新代码(日常部署流程)

本地修改代码 → 推送到 GitHub → 服务器拉取 → 重新构建容器:

步骤一:本地提交并推送

git add .
git commit -m "描述本次修改"
git push

步骤二:SSH 登录服务器,拉取并重建

cd /www/wwwroot/apk-dist

# docker-compose.yml 含有本地密码/密钥,需先暂存再拉取再还原
git stash
git pull
git stash pop

# 重新构建并启动(--build 会重新编译 Go 代码)
docker compose up -d --build
💡

注意git stash pop 有时会产生冲突(docker-compose.yml 两侧都有修改), 若出现 CONFLICT 提示,执行 git checkout HEAD -- docker-compose.yml 恢复干净版本,再用 vim 重新填入密码和 R2 凭证,参考首次部署第 3 步。

步骤三:确认服务正常

docker compose ps          # 状态应为 Up
docker compose logs -f app # 查看启动日志,Ctrl+C 退出

运维命令速查

场景命令
改了 Go 代码 / 模板 / nginx.confdocker compose up -d --build
只改了 docker-compose.yml 环境变量docker compose up -d
只是重启服务(不重新编译)docker compose restart app
实时查看日志docker compose logs -f app
查看容器状态docker compose ps
进入容器调试docker compose exec app sh
# 备份数据库(SQLite)
docker compose exec app tar czf /tmp/backup.tar.gz /data
docker cp $(docker compose ps -q app):/tmp/backup.tar.gz ./backup.tar.gz

项目结构

apk-dist/
├── main.go                          # 入口:路由注册、模板加载、服务启动
├── go.mod / go.sum
│
├── internal/
│   ├── config/config.go             # 配置加载(环境变量),含 R2 配置
│   ├── db/db.go                     # GORM + SQLite 初始化、自动迁移(User + App)
│   │
│   ├── models/
│   │   ├── user.go                  # User 模型
│   │   └── app.go                   # App 模型(含 FileType、UserID 等字段)
│   │
│   ├── handlers/
│   │   ├── helpers.go               # AppGroup 结构体 + buildAppGroups() + buildTypeCounts()
│   │   ├── auth.go                  # 注册/登录/退出/忘记密码/重置密码(含登录限速)
│   │   ├── user.go                  # 用户文件管理(Dashboard 按类型过滤、版本列表、编辑、删除)
│   │   ├── upload.go                # POST /upload — 多格式上传、APK 解析、流式写入 R2
│   │   ├── download.go              # GET /d/:code — 下载页、密码验证(含限速)、R2 预签名跳转
│   │   └── admin.go                 # /admin/* — 管理后台(含登录限速)
│   │
│   ├── middleware/
│   │   ├── auth.go                  # 管理员会话(内存 session + SameSite Cookie)
│   │   ├── user_auth.go             # 用户会话(随机 token + SameSite Cookie)+ 验证/重置 token
│   │   └── ratelimit.go             # 内存固定窗口限速器
│   │
│   ├── storage/
│   │   └── r2.go                    # Cloudflare R2 客户端(UploadFile io.Reader 接口、GetPresignedURL、DeleteFile)
│   │
│   └── utils/
│       ├── apk.go                   # APK 元数据解析 + 图标提取(多策略降级)
│       ├── filetype.go              # 文件类型检测(扩展名白名单 + 魔数校验)+ FileTypeInfo
│       ├── qr.go                    # 二维码 PNG 生成(Base64 / 文件)
│       └── short.go                 # 短码生成(crypto/rand,12 位 hex)
│
├── templates/
│   ├── layout.html                  # 基础 HTML 框架(Tailwind CDN + HTMX + favicon)
│   │
│   ├── register.html                # 注册页(分屏布局)
│   ├── user_login.html              # 用户登录页(分屏布局,含忘记密码入口)
│   ├── forgot_password.html         # 忘记密码页(输入用户名 → 生成重置链接)
│   ├── reset_password.html          # 重置密码页(输入新密码 + 确认)
│   │
│   ├── index.html                   # 上传页(登录后可见,含用户 NavBar)
│   ├── dashboard.html               # 我的文件列表(APK 分组 + 其他格式平铺,类型 Tab 过滤)
│   ├── user_app_versions.html       # 我的 APK 版本列表
│   ├── user_app_edit.html           # 编辑我的文件(名称/描述/密码)
│   │
│   ├── download.html                # 公开下载页(文件信息 + 二维码 + 下载按钮)
│   │
│   ├── admin_login.html             # 管理员登录页(深色主题分屏)
│   ├── admin_apps.html              # 管理后台文件列表(全用户,类型 Tab 过滤)
│   ├── admin_app_versions.html      # 管理后台 APK 版本列表
│   ├── admin_app_edit.html          # 管理后台编辑文件
│   │
│   └── partials/
│       ├── auth_brand_panel.html    # 登录/注册/忘记密码页左侧品牌动画面板(纯 CSS)
│       ├── upload_result.html       # HTMX 局部:上传成功结果
│       ├── password_form.html       # HTMX 局部:下载密码表单
│       └── download_button.html     # HTMX 局部:下载按钮(密码验证后替换)
│
├── static/
│   └── favicon.svg                  # 品牌图标:云朵+上传箭头,indigo 底色
│
├── uploads/                         # 运行时生成(R2 未配置时):images/ videos/ zips/ docs/ apks/ icons/
├── data/                            # 运行时生成:apk.db(SQLite)
│
├── Dockerfile                       # 多阶段构建(alpine)
├── docker-compose.yml               # 一键编排:app 容器
└── nginx/nginx.conf                 # Cloudflare 真实 IP 还原 + 限速 + 大文件超时(5G / 3600s)

API 路由一览

公开路由(无需登录)

GET  /register                   注册页
POST /register                   提交注册(自动登录后跳转首页)
GET  /login                      用户登录页
POST /login                      提交登录(IP 限速:10次/5min)
GET  /logout                     退出登录
GET  /forgot-password            忘记密码页(输入用户名)
POST /forgot-password            生成重置链接(显示在页面上,有效期 15 分钟)
GET  /reset-password?token=xxx   重置密码页(验证 token)
POST /reset-password             提交新密码(token 一次性,消费后失效)
GET  /verify-email?token=xxx     消费验证 token,标记邮箱已验证并自动登录
GET  /resend-verify              重新发送验证链接页(输入用户名)
POST /resend-verify              生成新验证链接(显示在页面上,有效期 24 小时)

GET  /d/:code                    下载页(文件信息 + 版本历史(APK)+ 二维码)
POST /d/:code/unlock             密码验证(HTMX 局部响应,IP+短码限速:15次/10min)
GET  /d/:code/dl                 文件下载(R2: 302 预签名 URL;本地: 流式传输)
GET  /icons/:file                应用图标(R2 / 本地磁盘)
GET  /static/*                   静态资源(favicon 等)

用户路由(需登录,Cookie: apk_session)

GET  /                           上传页(含用户 NavBar)
POST /upload                     上传文件(multipart,HTMX 响应,绑定 UserID)

GET  /my                         我的文件列表(?type=apk/image/video/zip/document 过滤)
GET  /my/versions?pkg=com.xxx    某 APK 应用的版本历史列表
GET  /my/apps/:id/edit           编辑页(仅限自己的文件)
POST /my/apps/:id/edit           保存编辑
DELETE /my/apps/:id              删除(HTMX 行内移除,仅限自己的文件)

管理员路由(需登录,Cookie: apk_admin)

GET  /admin/login                管理员登录页
POST /admin/login                提交登录(IP 限速:10次/5min)
GET  /admin/logout               退出

GET  /admin                      所有用户文件列表(?type= 过滤)
GET  /admin/versions?pkg=com.xxx 某 APK 应用所有版本(跨用户)
GET  /admin/apps/:id/edit        编辑任意文件
POST /admin/apps/:id/edit        保存编辑
DELETE /admin/apps/:id           删除任意文件(HTMX 行内移除)
GET  /admin/apps/:id/qr.png      下载二维码 PNG

会话与安全机制

用户会话(user_auth.go)

  • 登录态:生成 64 位随机 hex token,存入服务器内存(sync.RWMutex 保护的 map)
  • Cookieapk_session,HttpOnly + SameSite=Lax,7 天有效
  • 中间件
    • LoadUserContext(db):全局运行,从 Cookie 读取 session,有效则将 *models.User 写入 gin.Context,无效则静默跳过
    • RequireUser:保护需登录的路由,无有效 session 则重定向 /login

邮箱验证(user_auth.go)

  • 注册时生成 48 位随机 hex token,TTL 24 小时,存于内存 map
  • ConsumeVerifyToken:验证 + 删除(一次性),防重放;成功后更新 EmailVerified = true 并自动登录
  • 验证链接直接显示在注册成功页(无邮件依赖),适合自托管场景

密码重置(user_auth.go)

  • 生成 48 位随机 hex token,TTL 15 分钟,存于内存 map
  • ConsumeResetToken:验证 + 删除(一次性),防重放
  • PeekResetToken:仅校验有效性(展示 reset 表单时使用,不消费)

管理员会话(auth.go)

  • 独立内存 session map,与用户会话互不干扰
  • Cookie:apk_admin,HttpOnly + SameSite=Lax,24 小时有效
  • 登录凭证:subtle.ConstantTimeCompare 对比 ADMIN_PASSWORD 环境变量(防时序攻击)

文件类型安全(utils/filetype.go)

  • 扩展名白名单:仅接受预定义扩展名,其他一律拒绝
  • 魔数校验:读取文件头字节验证实际格式,防止将 .exe 改名为 .jpg 上传
    • APK:PK\x03\x04(ZIP 结构)
    • 图片:JPEG / PNG / GIF / WebP(RIFF)
    • 压缩包:ZIP / RAR / 7z / GZip
    • 文档:PDF / OLE2(Word/Excel 旧格式)/ OOXML(ZIP 结构)
  • 视频格式头部多样(MP4/MOV/AVI/MKV),仅做扩展名白名单校验

下载密码与防盗链(download.go)

  • 每次访问下载页强制清除旧 cookie,禁用缓存,确保每次都需要重新输入密码
  • 验证通过后签发 dl_{shortCode} Cookie:
    • HMAC-SHA256 签名(shortCode:timestamp + SESSION_SECRET
    • Cookie 有效期 10 分钟,服务端验证也限制在 660 秒内(防止 token 被截获后长期有效)
    • R2 模式下每次生成 15 分钟预签名 URL,不暴露固定下载地址
  • 短码为 12 位随机 hex(16¹² ≈ 281 万亿种),枚举不现实

防暴力破解(middleware/ratelimit.go)

内存固定窗口限速器,无需外部依赖:

端点限制Key
POST /login10 次 / 5 分钟客户端 IP
POST /admin/login10 次 / 5 分钟客户端 IP
POST /d/:code/unlock15 次 / 10 分钟客户端 IP + 短码

超限返回 HTTP 429,前端显示友好提示。

Cookie 安全属性

所有 session / unlock cookie 均设置:

  • HttpOnly:JavaScript 无法读取,防 XSS 窃取
  • SameSite=Lax:阻断跨站 POST 请求,有效防御 CSRF
  • Secure:BASE_URL 以 https:// 开头时自动启用,生产环境必须开启

关键业务逻辑

文件分类与存储路径(upload.go + utils/filetype.go)

上传时:

  1. DetectFileType(filename) 检查扩展名白名单,返回 FileTypeInfo(类别、存储目录、MIME 类型)
  2. APK:写入临时文件 → 解析元数据 + 提取图标 → 上传 R2 apks/{shortCode}.apk
  3. 非 APK:直接将 multipart.Fileio.Reader)流式写入 R2 {type}/{shortCode}.{ext},服务器不落盘
  4. 未配置 R2 时按原目录结构保存到本地磁盘

应用分组(helpers.go)

buildAppGroups(apps []models.App) []AppGroup — 将 APK 列表按 PackageName 聚合:

  • 第一条即为「最新版本」(代表该组的图标、版本号、上传时间)
  • 统计 TotalVersionsTotalDownloadsHasPassword(任意版本有密码则为 true)
  • admin.gouser.go 均复用此函数

buildTypeCounts(apps []models.App) map[string]int64 — 统计各类型数量,用于 Tab 徽章。

仪表盘类型过滤(user.go / admin.go)

?type=apk/image/video/zip/document 查询参数过滤 App 列表:

  • type=apk:仅查 APK,传入 buildAppGroups 展示分组表格
  • 其他类型:平铺列表,展示文件名、类型图标、大小、下载数
  • type=(空):APK 分组表 + 其他文件列表同时展示

版本历史(download.go)

DownloadPage 额外查询同 PackageName 的所有版本传给模板:

  • 模板默认渲染前 5 条({{if lt $i 5}}
  • 超过 5 条显示「查看更多」按钮,点击用 JS 移除 hidden class

上传与 UserID 绑定(upload.go)

Uploadgin.Context 读取 currentUser(由 LoadUserContext 写入),将 user.ID 赋给 app.UserID。历史数据 UserID = 0,管理员后台可统一查看。

所有权校验(user.go)

findOwnedApp 查询时加 WHERE id = ? AND user_id = ?,防止用户操作他人文件。管理员路由不加此条件。

左侧品牌动画说明(auth_brand_panel.html)

所有认证页(登录/注册/忘记密码/重置密码)共享同一个局部模板:

r.AddFromFilesFuncs("user_login", fm, layout, authPanel, "templates/user_login.html")

动画元素:

元素动画关键帧
3 个背景 blob缓慢 scale + translate 脉动blobPulse 8~12 s
2 个同心环正反向缓速旋转spinSlow 16/22 s
云上传 SVG 图标上下浮动 + 发光cloudBob 4 s
4 张文件卡片(图片/视频/压缩包/文档)漂浮(错开延迟)cardFloat 6 s
格式标签行(图片/视频/APK/压缩包/文档/最大5GB)静态展示
功能列表条目入场淡入左移(一次性)fadeInLeft 0.5 s

性能:所有动画仅使用 transformopacity,全部由 GPU 合成层处理,不触发重排(reflow)或重绘(repaint)。

后续扩展方向

开放注册

注册入口已实现,仅注释了模板中的「立即注册」链接。开放时取消 templates/user_login.html 中的注释即可。

支持 IPA(iOS)

  • IPA 是 ZIP 格式,读取 Payload/*.app/Info.plist 解析元数据
  • 安装需要生成 manifest.plist + itms-services:// 协议链接

邮件发送(注册验证 + 找回密码)

  • 当前验证链接和重置链接均直接显示在页面,适合内网/自托管,无 SMTP 依赖
  • 如需真正发送邮件:引入 gopkg.in/gomail.v2,在 Register / ResendVerify / ForgotPassword handler 中发送邮件而非渲染链接
  • 相关配置:SMTP_HOSTSMTP_PORTSMTP_USERSMTP_PASSSMTP_FROM

下载统计 / 访问日志

  • 新增 DownloadLog 表(AppID、IP、UA、时间)
  • ServeFile 中异步写入
  • 管理后台增加图表(Chart.js + /admin/stats API)

切换 PostgreSQL

  • db.go:替换 glebarez/sqlitegorm.io/driver/postgres
  • 通过 DATABASE_URL 环境变量传入连接串