# PicNest
**Repository Path**: chenzhanxu/PicNest
## Basic Information
- **Project Name**: PicNest
- **Description**: 个人图片库系统 · 开源 | Go + Gin + GORM + PostgreSQL(可选 SQLite) + 本地文件存储 + imaging/bimg + JWT | Vue 3 + Element Plus + Pinia + Vite | Docker Compose(仅数据库)
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-10
- **Last Updated**: 2026-09-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 🪺 PicNest
**自建个人图片库 · 图床 · 相册管理**
一站式管理你的图片资产:本地存储、内容去重、感知哈希查重、外链分享、IP 白名单与 JWT 双重防护
[](https://gitee.com/chenzhanxu/PicNest)
[](https://go.dev)
[](https://gin-gonic.com)
[](https://gorm.io)
[](https://www.postgresql.org)
[](https://cn.vuejs.org)
[](https://vite.dev)
[](https://element-plus.org)
[](LICENSE)
[📖 快速开始](#-快速开始) · [⚙️ 配置项](#️-配置项) · [📡 API](#-api-一览) · [⚡ 图像引擎](#-图像引擎imaging-与-bimglibvips) · [❓ 常见问题](#-常见问题)
---
## 📌 一句话认识 PicNest
> 把图片放在自己的服务器上,用一套顺手的界面管理起来:**上传即去重、大图自动出缩略图、外链随时开关、谁能访问由 IP 与账号共同决定。**
| 🖼️ 图片管理 | 🧠 智能算法 | 🔐 安全防护 | 🎨 交互体验 |
|:---:|:---:|:---:|:---:|
| 上传 / 去重 / 标签
缩略图 / 外链 / 批量删除 | dHash 感知哈希
BK 树相似检索 / 主色 / 清晰度 | IP 白名单 + JWT
角色分级 / 失败锁定 / 审计 | 毛玻璃 + 呼吸灯
明暗双主题 / 图标卡片化 |
## 📖 目录
- [✅ 发布前验收报告](#-发布前验收报告)
- [📋 项目总览与验收清单](#-项目总览与验收清单)
- [🖼️ 界面预览](#️-界面预览)
- [✨ 核心特性](#-核心特性)
- [🧱 技术栈](#-技术栈)
- [🏗️ 架构总览](#️-架构总览)
- [📁 目录结构](#-目录结构)
- [🔌 端口约定](#-端口约定)
- [🚀 快速开始](#-快速开始)
- [⚙️ 配置项](#️-配置项)
- [📡 API 一览](#-api-一览)
- [⚡ 图像引擎:imaging 与 bimg(libvips)](#-图像引擎imaging-与-bimglibvips)
- [🐳 部署建议](#-部署建议)
- [🧪 测试与基准](#-测试与基准)
- [❓ 常见问题](#-常见问题)
- [🤝 参与贡献](#-参与贡献)
---
## ✅ 发布前验收报告
发布前核对已执行完毕:后端 **25 个包 / 185 个测试函数**全绿,前端类型检查与构建通过,
自动化验收 **204 项通过 · 0 失败 · 1 项按设计跳过**(`scripts/acceptance.sh` 140/141(含恢复演练与前端段、M2 公开相册页)+ `scripts/geo-check.sh` 47/47 + `scripts/map-check.mjs` 17/17),恢复演练已在含 M7 schema 的库上跑通,
反向代理下 M5 脱敏与限流均生效,非法配置 fail-closed。
完整结果、生产配置核对清单与冻结结论见 **[docs/release-readiness.md](docs/release-readiness.md)**。
## 📋 项目总览与验收清单
功能、接口、配置、测试与验证方式逐项可核对的验收清单见 **[docs/project-overview.md](docs/project-overview.md)**。
| 指标 | 数值 |
|---|---|
| 后端包 / 测试函数 | 25 个包 / **185 个测试函数**(`go test ./...` 全绿) |
| 前端页面 / 设计组件 | 18 个页面 / 12 个组件(毛玻璃 + 呼吸灯 + 图标卡片化,亮暗双主题) |
| 数据库表 | 14 张(图片/标签/相册/收藏/评分/任务/备份/用户/IP 访问/设置) |
| 任务类型 / 事件主题 | 8 种任务 / 20+ 事件主题(事件驱动 + 持久化队列) |
| 专项文档 / 实测截图 | 5 篇 + 37 张(逐页对比度与设计系统实测) |
已完成的模块:Phase 1–6 主体能力 · **M1** 任务队列与事件底座 · **M2** 相册/收藏/评分 · **M3** 备份与恢复 · **M5** 安全与隐私 · **M6** 检索增强 · **M7** 地理/地图(前后端均已完成并验收)。
## 🖼️ 界面预览
前后台共用同一套设计语言:**毛玻璃面板 + 图标卡片 + 状态呼吸灯 + 明暗双主题**,
正文对比度经无头浏览器逐元素测量,亮暗两套均 ≥ 4.5:1(WCAG AA)。
**前台(5010)**
| 首页 | 图库 |
|:---:|:---:|
|  |  |
**后台(5011)**
| 登录 | 仪表盘 |
|:---:|:---:|
|  |  |
| 图片管理 | IP 访问控制 |
|:---:|:---:|
|  |  |
| 标签管理 | 算法与相似 |
|:---:|:---:|
|  |  |
| 用户与安全 | 元数据与归档 |
|:---:|:---:|
|  |  |
| 回收站 | 站点设置 |
|:---:|:---:|
|  |  |
| 相册 | 相册详情 |
|:---:|:---:|
|  |  |
| 任务中心(M1) | 收藏与评分(M2) |
|:---:|:---:|
|  |  |
| 备份与恢复(M3) | 恢复确认 |
|:---:|:---:|
|  |  |
> 逐页功能说明、设计系统落点与实测对比度数据:[**docs/admin-pages.md**](docs/admin-pages.md)
**语义检索(M6)**
| 前台语义检索(含相似度与命中来源角标) | 未启用时的回退提示 |
|:---:|:---:|
|  |  |
**地理与地图(M7)**
| | |
|---|---|
|  |  |
**公开相册(M2 分享闭环)**
| | |
|---|---|
|  |  |
**隐私与限流(M5)**
| 公开详情:位置信息已隐藏 | 上传限流提示 |
|:---:|:---:|
|  |  |
**暗色主题**
| 前台首页 | 后台仪表盘 |
|:---:|:---:|
|  |  |
| 任务中心 | 元数据与归档 |
|:---:|:---:|
|  |  |
| 相册 | 相册详情 |
|:---:|:---:|
|  |  |
| 备份与恢复 | 任务中心 |
|:---:|:---:|
|  |  |
| 图片管理 | IP 访问控制 |
|:---:|:---:|
|  |  |
|  |  |
> 截图由无头 Chromium 对本地实例(5010 / 5011 / 5012)真实渲染生成,位于 `docs/screenshots/`。
---
## ✨ 核心特性
### 🖼️ 图片管理
| | 能力 | 说明 |
|:--:|---|---|
| 📤 | **上传与校验** | multipart 多选 / 拖拽上传;扩展名白名单 + 文件头嗅探双重校验,防改后缀绕过 |
| ♻️ | **内容寻址存储** | 原图按 `originals/{hash前2}/{hash第3-4}/{hash}.{ext}` 落盘:同内容同路径、天然幂等;SHA-256 唯一索引兜底并发去重,重复上传直接返回已有资产 |
| 📥 | **流式上传** | 边接收边写临时区边算哈希(内存与文件大小无关)→ 多层校验(大小/扩展名/MIME 嗅探/解码/路径安全)→ **原子提交**;失败自动清理临时文件,可重试 |
| ⚡ | **秒传** | 小文件(≤64MB)浏览器端先算 SHA-256,命中已有内容直接跳过上传;非 HTTPS 环境自动降级为普通上传(服务端仍去重) |
| 🗑️ | **回收站** | 删除 = 软删除(只改元数据、保留文件,可恢复);同内容再次上传走**恢复策略**复用旧记录;到期由定时任务彻底删除(`TRASH_RETENTION_DAYS`) |
| 🧹 | **一致性维护** | 定时清理临时区、扫描孤儿文件与缺失记录;后台「站点设置 → 存储维护」可查看统计并手动巡检 |
| 🗂️ | **分片存储** | 原图按 `年/月/日/ID前两位/` 分片落盘,避免单目录文件过多 |
| 🖼️ | **自动缩略图** | 上传后由任务队列异步生成(EXIF 自动旋转、保持比例、不放大),可重试、可重算,列表加载飞快 |
| 🔗 | **外链分享** | 16 位随机 token 短链 `/i/:token`,可随时重置失效;支持 `?size=` 直接取缩略图 |
| 🏷️ | **标签体系** | 标签 CRUD / 重命名查重 / 合并(关联自动去重)/ 前缀树联想输入 |
| 🔍 | **检索与排序** | 关键词(大小写不敏感)/ 标签 / 格式 / 外链状态筛选,支持按时间、体积、自定义排序 |
| 🗑️ | **删除与批量** | 单张删除(连带磁盘文件与标签关联)与多选批量删除 |
| 🗂️ | **相册(手动/智能)** | 手动相册人工挑选 + 手动排序(成员顺序持久化,封面自动取首图并自动回退);智能相册用规则动态解析(标签/格式/机身/年份/时间区间/GPS/国家城市地点与附近范围 + 收藏/评分),成员永不落库、不漂移;相册可独立公开分享(令牌短链 `PUBLIC_BASE_URL/a/`,**前台有独立公开相册页**,只暴露公开图片,后台可一键预览) |
| ⭐ | **收藏** | 用户维度星标:单张/批量/切换,收藏列表与 ID 集合,可作为图片列表筛选条件;重复收藏幂等 |
| ⭐⭐⭐⭐⭐ | **评分** | 1–5 星(0 = 清除),我的评分 + 全站平均分/评分人数(批量聚合,无 N+1),按分区间浏览与排序,评分分布统计 |
| 🔎 | **组合筛选** | 收藏状态、我的评分区间可与标签/格式/拍摄信息/关键词叠加,列表响应自动补全 star 与评分字段 |
| 💾 | **备份与恢复** | 数据库一致性快照(SQLite VACUUM INTO / pg_dump / mysqldump)+ 可选原图 → 单个 tar.gz(含 manifest 清单);SHA-256 校验可检出损坏;恢复前自动生成安全备份,可选还原原图,全程留痕;支持保留策略与定时自动备份 |
| 🛟 | **崩溃恢复** | 服务被 kill 后,残留的"执行中"任务在启动时自动放回队列重跑;备份任务幂等,重跑安全 |
| 🛡️ | **公开访问脱敏** | 公开/外链访问自动隐藏精确 GPS 与地点文本、设备序列号、作者版权,并丢弃 raw EXIF/XMP/IPTC 排查数据;管理端与登录浏览始终可见完整信息;公开详情页显示「位置信息已隐藏」提示,响应带 `location_redacted` / `redacted` 标记 |
| 🚦 | **上传限流** | 每键令牌桶(登录按用户、未登录按 IP),超限 429 + `Retry-After`,并返回 `X-RateLimit-*` 头;空闲桶自动回收;前端上传队列串行执行,命中限流时提示剩余数量与等待时长并进入冷却 |
| 🔍 | **语义检索(M6)** | 自然语言找图:可插拔向量化(`local` 离线占位 / `http` 对接 CLIP 与 OpenAI 兼容服务)+ 内置向量索引与纯 Go 余弦检索;支持 `semantic` / `hybrid`(语义 ∪ 关键词)两种模式,可与标签/年份/机型/时间/地点/收藏/评分条件叠加;公开入口仅公开图片并按 M5 策略脱敏 |
| 🗺️ | **地理 / 地图(M7)** | 地图打点 + 网格聚合(网格边长可调)+ 地点档案(地点/城市/国家 + 覆盖规模与首末时间 + 封面)+ 国家/城市/地点分面 + 行程轨迹(按时间与距离切分,仅管理端);图库支持 `country/city/place` 与「附近 N 公里」筛选,智能相册可加地理规则;地点文本提升为 `images` 索引列,历史数据可一键回填;公开地图只显示公开图片并把坐标模糊到 2 位小数(网格约 1.1km、误差半径约 557m)且隐藏地点文本(延续 M5) |
### 🧠 智能算法
| | 能力 | 说明 |
|:--:|---|---|
| 🧬 | **感知哈希** | dHash(9×8 差分 → 64 位),对缩放、亮度、重新编码不敏感 |
| 🌳 | **相似检索** | BK 树 + 汉明距离剪枝,毫秒级找出视觉重复图片(阈值默认 ≤ 10) |
| 🎨 | **主色提取** | 4 位/通道量化取最高频色桶平均色,列表可直接按主色渲染占位 |
| 🔎 | **清晰度评分** | 拉普拉斯方差归一化到 0-100,快速识别糊图 |
| 🌲 | **标签前缀树** | 中文友好的 Trie,按词频降序补全,标签变动实时同步 |
| 🧾 | **指纹回填** | 启动自动重建索引,后台异步补齐历史图片指纹,不阻塞服务 |
### 🔐 安全防护
| | 能力 | 说明 |
|:--:|---|---|
| 🌐 | **IP 访问控制** | 作用域(`share` 外链 / `api` 接口)+ CIDR **最长前缀匹配** + `none/read/write/admin` 权限分级 |
| 🛡️ | **账号体系** | JWT(HS256)+ bcrypt 口令;令牌版本机制,改密或登出后旧令牌立即失效 |
| ⛔ | **失败锁定** | 连续失败达上限即锁定账号,锁定期间即使口令正确也拒绝,管理员可一键解锁 |
| 👥 | **角色分级** | `viewer`(只读)< `editor`(图片/标签/外链)< `admin`(用户与 IP 规则) |
| 🧱 | **纵深防御** | 管理接口 = IP 白名单 + 有效 JWT + 角色校验,三层都过才放行 |
| 📝 | **审计日志** | 登录记录(成功/失败原因/IP/UA)、IP 拒绝记录、五类分类日志文件 |
### 🎨 交互体验
| | 能力 | 说明 |
|:--:|---|---|
| 🪟 | **设计系统** | 设计令牌 + 组件库(GlassCard / AppIcon / BreathingDot / StatCard …),一套令牌驱动明暗主题 |
| 💡 | **呼吸灯状态** | 服务、数据库、账号锁定、IP 权限统一用呼吸光点表达状态 |
| 🧊 | **毛玻璃质感** | `backdrop-filter` 玻璃层 + 顶部高光 + 悬浮动效,Element Plus 全组件玻璃化对齐 |
| 🌗 | **明暗主题** | 跟随系统 / 亮色 / 暗色三态切换,暗色下重新校准文字对比度,保证明亮可读 |
| 📊 | **可视化后台** | 仪表盘(图库统计 / 算法索引 / 缓存命中 / 拒绝审计)、图片管理、标签、算法、IP 规则、用户 |
---
## 🧱 技术栈
| 分层 | 选型 |
|:--:|---|
| 🐹 **后端** | Go 1.27 · [Gin](https://gin-gonic.com) · [GORM](https://gorm.io) · JWT(golang-jwt)· bcrypt |
| 🗄️ **数据库** | PostgreSQL(默认)· MySQL · SQLite(零配置,改环境变量即切,含方言隔离) |
| 💾 **存储** | 本地文件分片存储(`originals/` + `thumbnails/`),接口抽象便于后续接对象存储 |
| 🎛️ **图像引擎** | [imaging](https://github.com/disintegration/imaging)(默认,纯 Go 无 cgo)· [bimg](https://github.com/h2non/bimg)+libvips(可选,cgo,大图 **2~2.6×** 加速) |
| 💻 **前端** | Vue 3 · Vite 8 · TypeScript · Element Plus · Pinia · vue-router |
| 🐳 **部署** | Docker Compose(仅数据库);应用本体可本地二进制 / 容器运行 |
---
## 🏗️ 架构总览
```
┌──────────────────────── 浏览器 ────────────────────────┐
│ 5010 前台(首页 / 图库 / 详情) │
│ 5011 后台(/admin:仪表盘·图片·标签·算法·IP·用户) │
└───────────────────────┬────────────────────────────────┘
│ HTTP(S) /api · /i/:token · /uploads
┌───────────────────────▼────────────────────────────────┐
│ 5012 PicNest API(Go + Gin) │
│ │
IP 白名单 ──────▶ │ ipaccess 作用域 share/api · CIDR 最长前缀 · 权限分级 │
JWT 令牌 ──────▶ │ auth bcrypt 口令 · 令牌版本 · 失败锁定 · 角色 │
│ │
│ image 上传·去重·缩略图·外链·检索 ─┐ │
│ tag 标签 CRUD / 合并 / 计数重算 │ 事件总线 │
│ share 外链 token 与公开访问 │ eventbus │
│ algorithm dHash · BK 树 · 主色 · 清晰度 ◀┘ │
│ imageproc 图像引擎抽象(imaging ⇄ bimg/libvips) │
│ storage / cache / database / logger / health │
└───────┬────────────────────────────┬───────────────────┘
│ │
┌───────▼────────┐ ┌────────▼─────────┐
│ PostgreSQL │ │ originals/ │
│ (或 SQLite) │ │ thumbnails/ │
└────────────────┘ └──────────────────┘
```
> 💡 模块之间通过**事件总线**解耦:`image.uploaded` → 算法模块计算指纹并入库入树;`image.deleted` → 移出索引;`tag.changed` → 前缀树失效。
---
## 📁 目录结构
```
PicNest/
├── backend/ # 🐹 Go + Gin API(模块化,单一职责)
│ ├── cmd/main.go # 入口:装配配置→日志→事件→缓存→数据库→引擎→路由
│ ├── internal/module/
│ │ ├── config/ # 环境变量加载与启动校验
│ │ ├── logger/ # 五类日志(访问/应用/错误/审计/慢查询)+ 脱敏 + 轮转
│ │ ├── eventbus/ # 事件总线(异步分发 + panic 隔离 + 上下文解耦)
│ │ ├── cache/ # 泛型 LRU + 命名缓存管理器
│ │ ├── database/ # 多库连接 · 方言隔离 · AutoMigrate / SQL 迁移
│ │ ├── storage/ # 存储抽象与本地分片实现(支持 Range 请求)
│ │ ├── imageproc/ # 🎛️ 图像引擎抽象:imaging(纯 Go)⇄ bimg(libvips)
│ │ ├── image/ # 图片上传 / 去重 / 缩略图 / 外链 / 检索 / 删除
│ │ ├── tag/ # 标签 CRUD / 合并 / 前缀补全
│ │ ├── share/ # 外链 token 与公开访问
│ │ ├── algorithm/ # dHash · BK 树 · 主色 · 清晰度 · 标签前缀树
│ │ ├── ipaccess/ # IP 规则 · 最长前缀匹配 · 判定缓存 · 审计
│ │ ├── auth/ # JWT · bcrypt · 失败锁定 · 角色 · 用户管理
│ │ ├── middleware/ # 请求 ID / 访问日志 / panic 恢复
│ │ ├── response/ health/ shared/ # 统一响应 / 健康检查 / 基础模型
│ │ └── testutil/ # 测试环境(临时 SQLite + 临时存储)
│ ├── scripts/build-bimg.sh # 一键构建带 libvips 加速的后端
│ ├── migrations/{postgres,mysql,sqlite}/
│ ├── data/{originals,thumbnails}/ # 运行时图片数据
│ └── logs/ # 运行时日志
├── frontend/ # 💻 Vue 3 + Vite + TS + Element Plus + Pinia
│ └── src/
│ ├── design/ # 🎨 设计系统
│ │ ├── tokens.css # 明暗两套设计令牌
│ │ ├── components/ # GlassCard · AppIcon · BreathingDot · StatCard · …
│ │ └── composables/ # useTheme(跟随系统 / 亮 / 暗)
│ ├── layouts/ # 前台 FrontLayout · 后台 AdminLayout
│ ├── views/front/ # 首页 · 图库 · 图片详情
│ ├── views/admin/ # 登录 · 仪表盘 · 图片 · 标签 · 算法 · IP · 用户
│ ├── router/ # 路由与鉴权守卫(5011 根路径自动进后台)
│ ├── stores/ # Pinia:登录态与 IP 权限 · UI 状态
│ └── api/ # axios 封装(Bearer 自动携带 · 401/403/423 差异化)
├── docker-compose.yml # 🐳 仅数据库(PostgreSQL + 数据卷)
├── .env.example # 环境变量样例
└── README.md
```
---
## 🔌 端口约定
| 端口 | 服务 | 说明 |
|:--:|---|---|
| **5010** | 🖥️ 前端 · 前台 | 首页、图库、图片详情(`npm run dev:front`) |
| **5011** | 🛠️ 前端 · 后台 | 同一 SPA,根路径自动跳转 `/admin/dashboard`(`npm run dev:admin`) |
| **5012** | ⚙️ 后端 API | Gin:`/api/*`、`/i/:token`、`/uploads/*` |
| 5432 | 🗄️ PostgreSQL | 仅数据库容器使用;SQLite 模式无需容器 |
> 前端开发服务器通过 `PICNEST_API`(默认 `http://localhost:5012`)代理 `/api` 与 `/uploads`。
---
## 🚀 快速开始
### 1️⃣ 启动数据库
```bash
cp .env.example .env # 按需修改数据库账号密码
docker compose up -d # 只启动 PostgreSQL(数据卷持久化)
```
> 🪶 想零依赖试用?跳过这一步,直接用 SQLite:`DB_DRIVER=sqlite DB_DSN=./data/picnest.db`
### 2️⃣ 启动后端
```bash
cd backend
go mod download
go run ./cmd # 默认监听 http://localhost:5012
curl http://localhost:5012/api/health
```
首次启动会自动建表,并在 `users` 表为空时创建管理员:
- 设置了 `ADMIN_PASSWORD` → 用它作为管理员口令(**≥ 8 位**)
- 未设置 → 生成 16 位随机口令,**打印在启动日志里**(请登录后立即修改)
### 3️⃣ 启动前端
```bash
cd frontend
npm install
npm run dev:front # 前台 http://localhost:5010
npm run dev:admin # 后台 http://localhost:5011
```
浏览器打开 `http://localhost:5011`,用管理员账号登录即可开始上传。
> 🔧 生产构建:`npm run build`(产物在 `frontend/dist/`,交给 Nginx 等静态服务器托管,并把 `/api` 反代到 5012)。
---
## ⚙️ 配置项
全部通过环境变量配置,均有默认值;完整样例见 [`.env.example`](.env.example)。
### 🧩 基础
| 变量 | 说明 | 默认 |
|---|---|:--:|
| `PORT` | 后端 API 监听端口 | `5012` |
| `DB_DRIVER` | `postgres` / `mysql` / `sqlite` | `postgres` |
| `DB_DSN` | 连接串(SQLite 时为文件路径) | `postgres://picnest:picnest@127.0.0.1:5432/picnest?sslmode=disable` |
| `DATA_DIR` | 图片存储根目录(原图 / 缩略图) | `./data` |
| `PUBLIC_BASE_URL` | 站点对外地址,用于拼接外链(后台「站点设置」可覆盖,改域名无需重启) | 空 |
| `MAX_FILE_SIZE` | 单文件上限(字节,未设时取 `MAX_UPLOAD_MB`×1MB) | `20971520` |
### 🔐 认证与安全
| 变量 | 说明 | 默认 |
|---|---|:--:|
| `JWT_SECRET` | 令牌签名密钥(**≥ 32 字符**,不满足直接拒绝启动) | `picnest-dev-secret` |
| `ADMIN_USERNAME` | 首启管理员用户名 | `admin` |
| `ADMIN_PASSWORD` | 首启管理员口令(≥ 8 位;留空则随机生成并打印日志) | 空 |
| `TOKEN_TTL_HOURS` | 登录令牌有效期(小时) | `168` |
| `LOGIN_MAX_ATTEMPTS` | 连续登录失败上限,达到即锁定账号 | `5` |
| `LOGIN_LOCK_MINUTES` | 账号锁定时长(分钟) | `15` |
### 🌐 IP 访问控制
| 变量 | 说明 | 默认 |
|---|---|:--:|
| `ALLOWED_IPS` | 初始 IP 白名单,首启导入 `api` 作用域并授予 `admin`(逗号分隔,支持 CIDR) | 空 |
| `TRUSTED_PROXIES` | 可信反向代理 IP/CIDR,用于取真实客户端 IP(**用 Nginx 反代时必须配**) | 空 |
### 🎛️ 图像处理
| 变量 | 说明 | 默认 |
|---|---|:--:|
| `THUMB_SIZE` | 缩略图最长边(像素,32–4096) | `400` |
| `TRASH_RETENTION_DAYS` | 回收站保留天数,到期由定时任务彻底删除 | `30` |
| `THUMB_QUALITY` | 缩略图 JPEG 质量(1–100) | `82` |
| `IMAGE_ENGINE` | 图像引擎:`auto` / `imaging` / `bimg`(`bimg` 需 `-tags bimg` 编译 + libvips) | `auto` |
### ⚙️ 任务队列(M1)
| 变量 | 说明 | 默认 |
|---|---|---|
| `TASK_WORKERS` | 并发 worker 数(1–64) | `2` |
| `TASK_POLL_INTERVAL` | 轮询间隔(`500ms` / `5s` 或纯数字秒,10ms–1m) | `500ms` |
| `TASK_MAX_ATTEMPTS` | 默认最大尝试次数(失败按指数退避重试,1–10) | `3` |
| `TASK_BACKOFF_BASE` | 退避基数(指数增长,上限 10 分钟,100ms–1h) | `5s` |
| `TASK_KEEP_DAYS` | 终态任务保留天数(`task-prune` 定时任务清理,1–3650) | `7` |
### 💾 备份与恢复(M3)
| 变量 | 说明 | 默认 |
|---|---|---|
| `BACKUP_DIR` | 归档目录 | `/../backups` |
| `BACKUP_ENABLED` | 是否启用定时自动备份 | `false` |
| `BACKUP_INTERVAL_HOURS` | 自动备份间隔(1–8760) | `24` |
| `BACKUP_KEEP_DAYS` | 备份保留天数(0 = 不按时间清理) | `14` |
| `BACKUP_KEEP_LATEST` | 最多保留成功备份数(0 = 不限) | `10` |
| `BACKUP_INCLUDE_FILES` | 自动备份是否包含原图 | `false` |
> 检索增强:[语义检索与向量索引](docs/semantic-search.md)(Provider 可插拔 + 内置向量索引 + 语义/混合检索)· [M6 验收报告](docs/m6-acceptance.md)
> 地理 / 地图:[地理与地图模块](docs/geo-map.md)(空间打点 + 聚合 + 地点档案 + 轨迹 + 地理筛选 + 隐私模糊;`scripts/geo-check.sh` 47 项后端 + `scripts/map-check.mjs` 17 项前端验收;前端零依赖自绘底图 + 可配置瓦片)
> 发布验收:[发布前验收报告](docs/release-readiness.md)(204 项通过实测 + 恢复演练 + 反向代理与信任边界 + 生产配置核对 + 冻结结论 + 上线前必做清单)
> 契约文档:[项目总览与验收清单](docs/project-overview.md) · [任务与事件底座](docs/task-queue.md) · [相册 / 收藏 / 评分](docs/albums-interaction.md) · [备份与恢复](docs/backup-restore.md) · [安全与隐私](docs/security-privacy.md) · [地理 / 地图](docs/geo-map.md)
### 🛡️ 安全与隐私(M5)
| 变量 | 说明 | 默认 |
|---|---|---|
| `PRIVACY_PUBLIC_GPS` | 公开访问隐藏精确位置与地点文本 | `true` |
| `PRIVACY_PUBLIC_SERIAL` | 公开访问隐藏设备/镜头序列号 | `true` |
| `PRIVACY_PUBLIC_AUTHOR` | 公开访问隐藏作者/版权/拍摄者 | `true` |
| `PRIVACY_PUBLIC_SOFTWARE` | 公开访问隐藏处理软件 | `false` |
| `PRIVACY_PUBLIC_RAW` | 公开访问返回 raw EXIF/XMP/IPTC | `false` |
| `UPLOAD_RATE_PER_MINUTE` | 单客户端每分钟上传次数(0 = 不限流) | `60` |
| `UPLOAD_RATE_BURST` | 上传突发容量(桶容量) | `15` |
### 🔍 语义检索(M6)
| 变量 | 说明 | 默认 |
|---|---|---|
| `EMBEDDING_PROVIDER` | 向量化提供者:`none` / `local`(离线占位,零依赖)/ `http`(CLIP 或 OpenAI 兼容) | `none` |
| `EMBEDDING_LOCAL_DIM` | 本地占位维度(8–4096) | `128` |
| `EMBEDDING_BATCH_SIZE` | 单批向量化图片数(1–64) | `8` |
| `EMBEDDING_HTTP_URL` | 推理服务地址(`http` 时必填) | 空 |
| `EMBEDDING_HTTP_MODEL` | 模型标识(写入索引,变更需重建) | `clip-vit-b-32` |
| `EMBEDDING_HTTP_API_KEY` | 可选 Bearer 令牌 | 空 |
| `EMBEDDING_HTTP_DIM` | 期望维度(0 = 自动探测) | `0` |
| `EMBEDDING_HTTP_TIMEOUT_SECONDS` | 单次请求超时(1–600) | `30` |
| `EMBEDDING_HTTP_PROTOCOL` | 协议:`auto` / `openai` / `clip` | `auto` |
| `SEARCH_DEFAULT_LIMIT` | 检索默认返回条数(1–100) | `20` |
| `SEARCH_MIN_SCORE` | 默认最低相似度(-1..1) | `0.1` |
| `SEARCH_CANDIDATE_FACTOR` | 候选倍数(1–20) | `4` |
### 🗺️ 地理 / 地图(M7)
| 变量 | 说明 | 默认 |
|---|---|---|
| `MAP_ENABLED` | 启用地图接口与定时聚合(关闭后 `/api/geo/*` 返回 503) | `true` |
| `GEO_PUBLIC_BLUR_DECIMALS` | 公开坐标模糊小数位(0–6;2 → 网格约 1.1km、误差半径约 557m) | `2` |
| `GEO_CLUSTER_CELL_KM` | 地图聚合网格边长(公里,0.5–2000) | `25` |
| `GEO_MIN_CLUSTER_IMAGES` | 聚合点最少图片数 | `1` |
| `GEO_TRACK_SPLIT_HOURS` | 轨迹时间切分阈值(小时) | `6` |
| `GEO_TRACK_SPLIT_KM` | 轨迹距离跳跃阈值(公里) | `30` |
| `MAP_TILE_URL` | 瓦片模板(`{z}/{x}/{y}`);留空用前端零依赖自绘底图 | 空 |
| `MAP_ATTRIBUTION` | 瓦片版权署名 | 空 |
| `MAP_MAX_ZOOM` | 瓦片最大缩放级别(1–22) | `18` |
### 📝 日志与运行模式
| 变量 | 说明 | 默认 |
|---|---|:--:|
| `LOG_DIR` | 日志目录(留空则只输出到 stdout) | `./logs` |
| `LOG_LEVEL` | `debug` / `info` / `warn` / `error` | `info` |
| `IS_DEV` | 开发模式(日志同时输出 stdout,Gin debug) | `false` |
---
## 📡 API 一览
统一响应体:`{"code": 0, "message": "success", "data": …}`(`code != 0` 表示业务错误)。
### 🌍 公开接口(`api` 作用域需 `read` 及以上)
| 方法 | 路径 | 说明 |
|:--:|---|---|
| `GET` | `/api/health` | 健康检查(服务 / 数据库 / 存储 / 缓存 / 图像引擎) |
| `GET` | `/api/images` | 公开图片列表(分页、关键词、标签、格式、排序) |
| `GET` | `/api/images/:id` | 图片详情(含主色、清晰度、缩略图地址) |
| `GET` | `/api/images/:id/thumb` | 缩略图(未生成时回退原图) |
| `GET` | `/api/images/:id/file` | 原图(`?download=1` 带原始文件名下载) |
| `GET` | `/api/images/:id/preview` | 预览图(`?w=1600` 按需生成并缓存;超大原图的浏览器友好版本) |
| `GET` | `/api/images/:id/similar` | 相似图片(BK 树近邻) |
| `GET` | `/api/images/:id/metadata` | 拍摄信息(EXIF/XMP/IPTC:时间、设备、曝光、GPS) |
| `GET` | `/api/albums/:token` | 公开相册(令牌短链,仅返回其中的公开图片) |
| `GET` | `/api/search?q=&mode=&limit=` | 公开语义检索(仅公开图片,位置按 M5 策略脱敏) |
| `GET` | `/api/metadata/facets` | 设备/镜头/年份分面(前台筛选) |
| `GET` | `/api/settings/public` | 站点对外地址(前端拼直链用) |
| `GET` | `/api/tags` | 标签列表(含图片计数) |
| `GET` | `/api/ip-access/me` | 当前来源 IP 在各作用域的权限 |
| `POST` | `/api/auth/login` | 登录获取 JWT |
| `GET` | `/api/auth/meta` | 角色 / 令牌有效期 / 锁定策略 |
| `GET` | `/i/:token` | 图片外链(`?size=400` 返回缩略图) |
| `GET` | `/uploads/*` | 静态文件访问 |
### 🔑 登录态接口(需有效 JWT)
| 方法 | 路径 | 说明 |
|:--:|---|---|
| `GET` | `/api/auth/me` | 当前登录用户 |
| `POST` | `/api/auth/refresh` | 续签令牌 |
| `POST` | `/api/auth/logout` | 全端登出(令牌版本 +1) |
| `PUT` | `/api/auth/password` | 修改自己的密码(改后需重新登录) |
### ✏️ 编辑接口(角色需 `editor` 及以上)
| 方法 | 路径 | 说明 |
|:--:|---|---|
| `POST` | `/api/admin/images/upload` | 上传图片(multipart:`file`、`tags`、`is_public`);返回 `duplicate`(内容去重)/ `restored`(回收站恢复) |
| `POST` | `/api/admin/images/exists` | 秒传预检:按 SHA-256 数组查询已存在内容(返回 `can_skip`) |
| `PUT` | `/api/admin/images/:id` | 修改文件名 / 标签 / 排序 / 外链开关 |
| `PUT` | `/api/admin/images/:id/visibility` | 单独切换外链可见性 |
| `POST` | `/api/admin/images/:id/thumbnail` | 重新生成缩略图 |
| `DELETE` | `/api/admin/images/:id` | 移入回收站(软删除,可恢复;彻底删除走 `/api/admin/trash/:id`) |
| `POST` | `/api/admin/images/batch-delete` | 批量移入回收站 |
| `GET` | `/api/admin/trash` | 回收站列表(`?page=&size=`) |
| `POST` | `/api/admin/trash/:id/restore` | 从回收站恢复 |
| `DELETE` | `/api/admin/trash/:id` | 彻底删除(清理物理文件) |
| `DELETE` | `/api/admin/trash` | 清空回收站 |
| `GET` | `/api/admin/storage/stats` | 存储统计(原图/缩略图/临时区/回收站/去重节省估算) |
| `POST` | `/api/admin/storage/gc` | 一致性维护:`?dry_run=&orphans=&temp_minutes=`(临时区清理 + 孤儿文件扫描 + 缺失检查) |
| `GET` | `/api/admin/images` / `…/stats` | 管理列表(含未公开)/ 图库统计 |
| `GET` | `/api/admin/tags/autocomplete` | 标签前缀补全 |
| `POST` `PUT` `DELETE` | `/api/admin/tags`、`/api/admin/tags/:id`、`/api/admin/tags/merge` | 标签增删改与合并 |
| `GET` | `/api/admin/algorithm/stats` | 算法状态(索引规模 / 指纹覆盖 / 阈值) |
| `POST` | `/api/admin/algorithm/rebuild` | 重建索引(`?backfill=true` 补齐历史指纹) |
| `POST` | `/api/admin/algorithm/images/:id/fingerprint` | 重算单图指纹 |
| `GET` | `/api/admin/algorithm/images/:id/similar` | 相似检索(`?distance=&limit=`) |
| `GET` | `/api/admin/algorithm/tags/suggest` | 标签前缀树补全(`?prefix=&limit=`) |
| `GET` | `/api/admin/tasks` / `…/stats` / `…/types` | 任务列表(`?status=&type=`)/ 统计(含积压时长)/ 已注册类型与调度 |
| `POST` | `/api/admin/tasks` | 手动入队(`{type,payload,key,force,priority,max_attempts}`) |
| `POST` | `/api/admin/tasks/:id/retry` / `…/cancel` | 重试 / 取消任务 |
| `DELETE` | `/api/admin/tasks/finished` | 清理终态任务(`?older_than_days=7`,`0` = 立即清理) |
| `GET` `POST` | `/api/admin/albums` | 相册列表(`?kind=&keyword=`)/ 创建(手动或智能 + 规则) |
| `GET` `PUT` `DELETE` | `/api/admin/albums/:id` | 相册详情 / 更新(名称/描述/规则/排序/封面)/ 删除 |
| `GET` `POST` `DELETE` | `/api/admin/albums/:id/images` | 相册内图片(智能相册实时解析)/ 批量加图 / 批量移出 |
| `PUT` | `/api/admin/albums/:id/images/order` `/cover` `/share` | 手动排序 / 设置封面(空=自动首图)/ 公开开关与 token 重置 |
| `GET` | `/api/admin/albums/stats` | 相册统计(类型/公开数/成员总数) |
| `GET` | `/api/admin/favorites` `/favorites/ids` | 我的收藏列表(回填图片明细)/ 收藏 ID 集合 |
| `POST` `PUT` `DELETE` | `/api/admin/favorites/batch`、`/api/admin/images/:id/favorite` | 批量收藏/取消 / 单张收藏与取消(含 `/toggle` 切换) |
| `GET` | `/api/admin/ratings?min=&max=&sort=` | 我的评分列表(分数区间 + 排序) |
| `PUT` `DELETE` | `/api/admin/images/:id/rating` | 设置评分(1–5,`0` = 清除)/ 清除 |
| `GET` | `/api/admin/interaction/stats` | 收藏数 / 评分分布 / 我的与全站平均分 |
| `POST` | `/api/admin/images/decorate` | 批量补全收藏与评分(列表增量刷新) |
| `GET` | `/api/admin/search?q=&mode=&limit=&min_score=&keyword=…` | 语义/混合检索(可与标签、年份、机型、时间、地点、收藏、评分条件叠加) |
| `GET` | `/api/admin/embeddings/stats` | 向量索引统计(覆盖率 / provider / model / dim / by_model) |
| `POST` | `/api/admin/embeddings/backfill?limit=N` | 回填未向量化图片(0 = 全部) |
| `POST` | `/api/admin/images/:id/embedding` | 重新向量化单张图片 |
| `GET` `POST` | `/api/admin/backups` | 备份列表 / 创建备份(`{label,kind:db\|full,include_files}`,入队异步执行) |
| `GET` `DELETE` | `/api/admin/backups/:id` | 备份详情(含恢复次数)/ 删除记录与归档文件 |
| `POST` | `/api/admin/backups/:id/verify` `/restore` | 重算 SHA-256 校验 / 恢复(`{confirm,restore_files}`,自动安全备份) |
| `GET` | `/api/admin/backups/:id/download` | 下载 tar.gz 归档 |
| `GET` | `/api/admin/backups/stats` `/restores` | 备份统计(含保留策略与目录)/ 恢复历史 |
| `DELETE` | `/api/admin/backups/finished` | 清理已结束备份(`?keep_days=7`,`0` = 立即清理全部) |
### 🛡️ 管理接口(角色需 `admin`)
| 方法 | 路径 | 说明 |
|:--:|---|---|
| `GET` `POST` | `/api/admin/users` | 用户列表 / 新建用户 |
| `PUT` `DELETE` | `/api/admin/users/:id` | 局部更新(昵称/角色/启停/重置密码)/ 删除 |
| `POST` | `/api/admin/users/:id/unlock` | 解除账号锁定 |
| `GET` | `/api/admin/auth/logs` | 登录审计日志(`?limit=`) |
| `POST` | `/api/admin/metadata/backfill` | 批量回填历史图片的拍摄信息(`?limit=`) |
| `GET` | `/api/admin/metadata/stats` | 元数据解析统计(总数/已解析/有 GPS/有设备) |
| `GET` | `/api/admin/metadata/facets` | 设备 / 镜头 / 年份分面 |
| `GET` | `/api/admin/metadata/archive` | 自动归档:`?dim=event\|date\|camera\|place` |
| `POST` | `/api/admin/images/:id/metadata` | 重新解析该图拍摄信息 |
| `GET` `PUT` | `/api/admin/settings` | 站点设置(对外域名 / 图片直链域名) |
| `GET` `POST` | `/api/admin/ip-access` | IP 规则列表(`?scope=share\|api`)/ 新建规则 |
| `PUT` `DELETE` | `/api/admin/ip-access/:id` | 更新(含启停)/ 删除规则 |
| `POST` | `/api/admin/ip-access/check` | 判定调试(任意 IP + 作用域 + 所需权限) |
| `GET` | `/api/admin/ip-access/meta` | 作用域 / 权限 / 默认权限 / 缓存时长 |
| `GET` `DELETE` | `/api/admin/ip-access/logs` | 拒绝审计日志 / 清空 |
> 🧱 **纵深防御**:管理接口需先过 `api` 作用域 IP 白名单(`admin` 权限,本机回环恒为 `admin`),再过 JWT 与角色校验。
> 🔢 **状态码约定**:口令错误 `401` · 账号锁定 `423` · 账号停用 `403` · 角色不足 `403` · IP 未放行 `403` · 规则重复 `409`。
---
## ⚡ 图像引擎:imaging 与 bimg(libvips)
缩略图生成与指纹解码都经过 `internal/module/imageproc` 抽象层,两套实现**按构建标签二选一**:
| 引擎 | 实现 | 依赖 | 适用场景 |
|:--:|---|:--:|---|
| `imaging` | [disintegration/imaging](https://github.com/disintegration/imaging) | 纯 Go,无 cgo | 中小图库、跨平台、单文件分发(**默认**) |
| `bimg` | [h2non/bimg](https://github.com/h2non/bimg) + libvips | cgo + libvips | 大图 / 高并发上传 / 批量导入 |
### 开启 bimg
```bash
# 1️⃣ 安装 libvips 与 C 编译器
sudo apt-get install -y libvips-dev # Debian / Ubuntu
# apk add vips-dev # Alpine
# brew install vips # macOS
# 2️⃣ 带 build tag 编译(脚本会先校验 pkg-config 能找到 vips)
cd backend && bash scripts/build-bimg.sh # 产物:bin/picnest
# 3️⃣ 选择引擎(auto 表示「编译了 bimg 就用 bimg」)
IMAGE_ENGINE=bimg ./bin/picnest
```
> 🛟 未带 `-tags bimg` 编译却配置了 `IMAGE_ENGINE=bimg` **不会启动失败**:会打印 `image_engine_unavailable` 告警并自动回退 `imaging`。当前生效的引擎可在 `/api/health` 的 `image_engine` 字段与后台「算法与相似」页看到。
### 📊 实测加速(Xeon E5-2690 v4)
单元基准(`go test -tags bimg ./internal/module/imageproc/ -bench .`,4000×3000 JPEG):
| 操作 | imaging | bimg | 提升 |
|---|---|:--:|:--:|
| 生成 400px 缩略图 | 278.2 ms(25.8 MB / 356 allocs) | **108.1 ms(24.6 KB / 10 allocs)** | **2.6×**,内存分配 ↓ 约 1000× |
| 解码到 256px(指纹用) | 275.4 ms(23.7 MB / 302 allocs) | **126.9 ms(340 KB / 203 allocs)** | **2.2×** |
HTTP 端到端 A/B(同机两实例,3840×2060 / 963 KB JPEG):
| 接口 | imaging | bimg | 提升 |
|---|---|:--:|:--:|
| `POST /api/admin/images/upload`(含缩略图 + 指纹 + 落盘) | 172 ms | **65 ms** | **2.6×** |
| 重算指纹(解码 + dHash + 主色 + 清晰度) | 137–184 ms | **73–75 ms** | **≈ 2.1×** |
### ⚠️ 边界与取舍
- **JPEG 收益最大**:libvips 走 libjpeg-turbo + shrink-on-load(解码时按 DCT 比例直接缩小),相机与手机大图提升明显;
- **PNG 提升有限**(实测 1920×1030 PNG:109 ms vs 110 ms,瓶颈在 zlib 解压而非缩放),此时 bimg 的意义主要是**内存占用**与多线程;
- **清晰度评分一致性**:bimg 路径先用 libvips 缩到 ≤256px,再交给纯 Go 解码,输出 PNG 保持无损(避免 JPEG 高频噪声抬高拉普拉斯方差);两引擎在平滑图上 dHash 距离 ≤ 2,阈值 10 下判定完全一致;
- **部署提醒**:bimg 二进制依赖 libvips 动态库,容器里请安装运行库(如 `libvips42`),构建阶段需要 `libvips-dev` + gcc。
---
## 🐳 部署建议
**仅数据库用 Compose,应用本体自由部署**,几种常见组合:
| 场景 | 做法 |
|---|---|
| 🧪 本地/单机 | `docker compose up -d` 起数据库 → `go run ./cmd` + `npm run dev:*` |
| 🚀 生产(推荐) | 后端编译为二进制(systemd / supervisor 托管)或用容器镜像;前端 `npm run build` 后交给 Nginx 托管,并把 `/api`、`/i/`、`/uploads/` 反代到 5012 |
| 🎛️ 面板部署 | 1Panel / 宝塔等面板:数据库用面板自带的 PostgreSQL 或 MySQL;网站配置反向代理到 5012,别忘了设置 `TRUSTED_PROXIES` |
生产环境务必检查:
- 🔑 `JWT_SECRET` 换成随机长串(≥ 32 字符)
- 🔒 `ADMIN_PASSWORD` 设置强口令,或首启后立即修改随机口令
- 🌐 `TRUSTED_PROXIES` 填 Nginx/面板网关的 IP,否则 IP 白名单判定的是代理地址
- 🧭 `PUBLIC_BASE_URL` 填对外域名,外链才会是完整地址
- 💾 定期备份 `DATA_DIR`(图片)与数据库
---
## 🧪 测试与基准
```bash
# 后端:单元测试(默认纯 Go 构建)
cd backend && go vet ./... && go test ./...
# 后端:libvips 引擎下的全量测试
source <(echo 'export CGO_ENABLED=1 CC=gcc PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig')
go test -tags bimg ./...
# 图像引擎基准对比(imaging vs bimg)
go test -tags bimg ./internal/module/imageproc/ -run XXX -bench . -benchtime 20x
# 想用自己的照片复现对比
PICNEST_REAL_IMAGE=/path/to/your/photo.jpg \
go test -tags bimg ./internal/module/imageproc/ -run TestRealPhotoCompare -v
# 前端:类型检查 + 生产构建
cd frontend && npx vue-tsc -b && npm run build
```
---
## ❓ 常见问题
登录报 423 / 提示账号已锁定怎么办?
连续失败达到 `LOGIN_MAX_ATTEMPTS`(默认 5 次)会锁定账号 `LOGIN_LOCK_MINUTES`(默认 15 分钟)。等待解锁,或用管理员账号在「用户与安全」页点击「解锁」;封禁信息会带上解锁时间。
接口报 403「IP 未放行」/「角色不足」怎么区分?
响应体会给出原因与作用域。IP 相关:到「IP 访问控制」页给当前来源 IP 加一条 `api` 作用域 `admin` 规则的记录;角色相关:当前账号权限不够(编辑类需 `editor`,用户/IP 规则需 `admin`)。本机回环地址恒为 `admin`,可用于紧急自救。
外链打不开(403)?
外链走 `share` 作用域。检查该作用域是否存在 `* → none` 之类的兜底拒绝规则,给访客 IP 或网段加一条 `read` 及以上的允许规则即可(前缀越长优先级越高,所以 `/24` 的允许规则能覆盖 `*` 的拒绝规则)。
忘记管理员密码了?
删除/清空数据库中的 `users` 表重启,会按 `ADMIN_USERNAME` + `ADMIN_PASSWORD` 重新引导管理员;或直接改库:用 bcrypt 生成新摘要写入 `users.password_hash`。
缩略图 404 或列表显示原图?
缩略图只在生成成功时存在,未生成会自动回退原图(不算错误)。可在「图片管理」里对该图执行「重生成缩略图」,或到「算法与相似」页重建索引。
历史图片没有相似检索结果?
老图片可能还没计算感知哈希。到「算法与相似」页点击**重建索引**(默认勾选补齐历史指纹),或调用 `POST /api/admin/algorithm/rebuild?backfill=true`;服务启动时也会后台自动补齐。
bimg 不生效 / 启动日志有 image_engine_unavailable?
说明当前二进制没有编译 bimg。用 `bash backend/scripts/build-bimg.sh` 重新构建(需 libvips-dev + gcc),再设置 `IMAGE_ENGINE=bimg`;`/api/health` 里的 `image_engine` 字段会告诉你是 `imaging` 还是 `bimg`。
为什么 PNG 换 bimg 几乎没变快?
bimg 的优势来自 JPEG 的 DCT 缩放与 libjpeg-turbo;PNG 主要成本是 zlib 解压,两者差别不大。若图库以 PNG 为主,收益主要是内存占用与线程化。
SQLite 与 PostgreSQL 怎么选?
个人自用、单机部署选 SQLite 最省事(零依赖、单文件备份);并发上传较多或需要多实例选 PostgreSQL/MySQL。切换只需改 `DB_DRIVER` 与 `DB_DSN`,建表与迁移会自动完成。
---
## 🤝 参与贡献
欢迎 Issue / PR!提交前请确保:
```bash
cd backend && go vet ./... && go test ./... # 后端
cd frontend && npx vue-tsc -b && npm run build # 前端
```
代码组织遵循「一个模块一个目录、单一职责、通过接口与事件解耦」的原则,新增能力建议放进 `backend/internal/module//`。
## 📜 许可证
[MIT](LICENSE) © 2026 陈占旭
---
**如果 PicNest 对你有帮助,欢迎点个 ⭐ Star 支持一下!**
Built with ❤️ using Go · Gin · GORM · Vue 3 · Element Plus