云传 · 开发者文档
apk-dist · 通用文件分发平台 · Go 1.26 + Gin
通用文件分发平台,支持 APK、图片、视频、压缩包、文档等多种格式。上传后自动生成短链接与二维码,APK 文件额外解析应用名、包名、版本等元信息并支持多版本管理。类似 betaqr / fir.im 的精简自托管版本,支持 Cloudflare R2 对象存储。
功能特性
| 功能 | 说明 |
|---|---|
| 多格式文件上传 | 支持 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(用户)
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| Username | string | 用户名,唯一,最长 32 字符 |
| string | 邮箱,注册时必填 | |
| EmailVerified | bool | 邮箱是否已验证,未验证不可登录 |
| PasswordHash | string | bcrypt 哈希,不存明文 |
| CreatedAt / UpdatedAt | time.Time | 自动维护 |
App(文件记录)
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| FileType | string | 文件类型:apk / image / video / zip / document(默认 apk) |
| Name | string | 文件/应用名称 |
| PackageName | string | 包名(APK 专用,如 com.example.app;其他格式为空) |
| VersionName | string | 版本名(APK 专用) |
| VersionCode | int32 | 版本号(APK 专用) |
| Description | string | 更新说明 / 文件描述 |
| ShortCode | string | 下载短码(唯一,12 位 hex) |
| FileName | string | 原始文件名 |
| FilePath | string | 磁盘存储路径(R2 模式下为对象 Key) |
| FileSize | int64 | 文件大小(字节) |
| HasIcon | bool | 是否已提取到图标(APK 专用) |
| UserID | uint | 所属用户 ID(0 = 历史遗留/管理员创建) |
| Password | string | 下载密码 bcrypt 哈希,空 = 无密码 |
| DownloadCount | int | 累计下载次数 |
| CreatedAt / UpdatedAt | time.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,不会打印密码。
完整配置项
所有配置通过环境变量传入:
| 变量名 | 默认值 | 说明 |
|---|---|---|
PORT | 8080 | 监听端口 |
APP_ENV | development | 环境,production 时关闭 Gin 调试日志 |
BASE_URL | http://localhost:8080 | 对外访问域名(用于生成二维码/重置链接) |
ADMIN_PASSWORD | admin123 | 管理后台登录密码,生产必须修改 |
SESSION_SECRET | please-change-this... | Cookie 签名密钥,生产必须修改 |
UPLOAD_DIR | ./uploads | 本地存储路径(R2 未配置时使用) |
DATA_DIR | ./data | SQLite 数据库存储路径 |
MAX_UPLOAD_MB | 5120 | 单个文件最大上传大小(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 数据库存储在服务器本地磁盘。
首次部署
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-dist3. 修改 docker-compose.yml 中的配置
vim docker-compose.ymlenvironment:
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 |
| 目标 URL | http://127.0.0.1:8080 |
| 发送域名 | $host |
6. 配置 Cloudflare SSL(Full Strict 模式)
- 在 Cloudflare → SSL/TLS → Origin Server 生成 Origin Certificate(覆盖
*.machinaix.com,15 年有效期) - 将证书和私钥粘贴到 aaPanel → 网站 → send.machinaix.com → SSL → 其他证书
- 在 aaPanel 防火墙开放 443 端口
- 在 Cloudflare SSL/TLS 加密模式选择 完整(严格)(Full Strict)
7. 在 Cloudflare 添加子域名 DNS 记录
Cloudflare → machinaix.com → DNS → 添加记录:
| 类型 | 名称 | IPv4 | 代理状态 |
|---|---|---|---|
| A | send | 服务器 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.conf | docker 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) - Cookie:
apk_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 结构)
- APK:
- 视频格式头部多样(MP4/MOV/AVI/MKV),仅做扩展名白名单校验
下载密码与防盗链(download.go)
- 每次访问下载页强制清除旧 cookie,禁用缓存,确保每次都需要重新输入密码
- 验证通过后签发
dl_{shortCode}Cookie:- HMAC-SHA256 签名(
shortCode:timestamp+SESSION_SECRET) - Cookie 有效期 10 分钟,服务端验证也限制在 660 秒内(防止 token 被截获后长期有效)
- R2 模式下每次生成 15 分钟预签名 URL,不暴露固定下载地址
- HMAC-SHA256 签名(
- 短码为 12 位随机 hex(16¹² ≈ 281 万亿种),枚举不现实
防暴力破解(middleware/ratelimit.go)
内存固定窗口限速器,无需外部依赖:
| 端点 | 限制 | Key |
|---|---|---|
POST /login | 10 次 / 5 分钟 | 客户端 IP |
POST /admin/login | 10 次 / 5 分钟 | 客户端 IP |
POST /d/:code/unlock | 15 次 / 10 分钟 | 客户端 IP + 短码 |
超限返回 HTTP 429,前端显示友好提示。
Cookie 安全属性
所有 session / unlock cookie 均设置:
HttpOnly:JavaScript 无法读取,防 XSS 窃取SameSite=Lax:阻断跨站 POST 请求,有效防御 CSRFSecure:BASE_URL 以https://开头时自动启用,生产环境必须开启
关键业务逻辑
文件分类与存储路径(upload.go + utils/filetype.go)
上传时:
DetectFileType(filename)检查扩展名白名单,返回FileTypeInfo(类别、存储目录、MIME 类型)- APK:写入临时文件 → 解析元数据 + 提取图标 → 上传 R2
apks/{shortCode}.apk - 非 APK:直接将
multipart.File(io.Reader)流式写入 R2{type}/{shortCode}.{ext},服务器不落盘 - 未配置 R2 时按原目录结构保存到本地磁盘
应用分组(helpers.go)
buildAppGroups(apps []models.App) []AppGroup — 将 APK 列表按 PackageName 聚合:
- 第一条即为「最新版本」(代表该组的图标、版本号、上传时间)
- 统计
TotalVersions、TotalDownloads、HasPassword(任意版本有密码则为 true) admin.go和user.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 移除
hiddenclass
上传与 UserID 绑定(upload.go)
Upload 从 gin.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 |
性能:所有动画仅使用 transform 和 opacity,全部由 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/ForgotPasswordhandler 中发送邮件而非渲染链接 - 相关配置:
SMTP_HOST、SMTP_PORT、SMTP_USER、SMTP_PASS、SMTP_FROM
下载统计 / 访问日志
- 新增
DownloadLog表(AppID、IP、UA、时间) ServeFile中异步写入- 管理后台增加图表(Chart.js +
/admin/statsAPI)
切换 PostgreSQL
db.go:替换glebarez/sqlite为gorm.io/driver/postgres- 通过
DATABASE_URL环境变量传入连接串