# 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 双重防护 [![Version](https://img.shields.io/badge/version-v1.0.0-4f7cff?style=for-the-badge)](https://gitee.com/chenzhanxu/PicNest) [![Go](https://img.shields.io/badge/Go-1.27+-00ADD8?style=for-the-badge&logo=go&logoColor=white)](https://go.dev) [![Gin](https://img.shields.io/badge/Gin-Web_Framework-009688?style=for-the-badge&logo=gin&logoColor=white)](https://gin-gonic.com) [![GORM](https://img.shields.io/badge/GORM-ORM-00ADD8?style=for-the-badge&logo=gorm&logoColor=white)](https://gorm.io) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-可选_SQLite-4169E1?style=for-the-badge&logo=postgresql&logoColor=white)](https://www.postgresql.org) [![Vue](https://img.shields.io/badge/Vue_3-Composition-42b883?style=for-the-badge&logo=vue.js&logoColor=white)](https://cn.vuejs.org) [![Vite](https://img.shields.io/badge/Vite-8-646CFF?style=for-the-badge&logo=vite&logoColor=white)](https://vite.dev) [![Element Plus](https://img.shields.io/badge/Element_Plus-UI-409EFF?style=for-the-badge&logo=element-plus&logoColor=white)](https://element-plus.org) [![License](https://img.shields.io/badge/License-MIT-22c55e?style=for-the-badge)](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)** | 首页 | 图库 | |:---:|:---:| | ![前台首页](docs/screenshots/front-home-light.jpg) | ![前台图库](docs/screenshots/front-gallery-light.jpg) | **后台(5011)** | 登录 | 仪表盘 | |:---:|:---:| | ![后台登录](docs/screenshots/admin-login-light.jpg) | ![后台仪表盘](docs/screenshots/admin-dashboard-light.jpg) | | 图片管理 | IP 访问控制 | |:---:|:---:| | ![图片管理](docs/screenshots/admin-images-light.jpg) | ![IP 访问控制](docs/screenshots/admin-ip-access-light.jpg) | | 标签管理 | 算法与相似 | |:---:|:---:| | ![标签管理](docs/screenshots/admin-tags-light.jpg) | ![算法与相似](docs/screenshots/admin-algorithm-light.jpg) | | 用户与安全 | 元数据与归档 | |:---:|:---:| | ![用户与安全](docs/screenshots/admin-users-light.jpg) | ![元数据与归档](docs/screenshots/admin-metadata-light.jpg) | | 回收站 | 站点设置 | |:---:|:---:| | ![回收站](docs/screenshots/admin-trash-light.jpg) | ![站点设置](docs/screenshots/admin-settings-light.jpg) | | 相册 | 相册详情 | |:---:|:---:| | ![相册](docs/screenshots/admin-albums-light.jpg) | ![相册详情](docs/screenshots/admin-album-detail-light.jpg) | | 任务中心(M1) | 收藏与评分(M2) | |:---:|:---:| | ![任务中心](docs/screenshots/admin-tasks-light.jpg) | ![收藏与评分](docs/screenshots/admin-library-light.jpg) | | 备份与恢复(M3) | 恢复确认 | |:---:|:---:| | ![备份与恢复](docs/screenshots/admin-backups-light.jpg) | ![恢复确认](docs/screenshots/admin-backups-restore-light.jpg) | > 逐页功能说明、设计系统落点与实测对比度数据:[**docs/admin-pages.md**](docs/admin-pages.md) **语义检索(M6)** | 前台语义检索(含相似度与命中来源角标) | 未启用时的回退提示 | |:---:|:---:| | ![前台语义检索](docs/screenshots/front-gallery-semantic-light.jpg) | ![语义未启用回退](docs/screenshots/front-gallery-semantic-off-light.jpg) | **地理与地图(M7)** | | | |---|---| | ![前台地图](docs/screenshots/front-map-light.jpg) | ![图库地理筛选](docs/screenshots/front-gallery-geo-light.jpg) | **公开相册(M2 分享闭环)** | | | |---|---| | ![公开相册亮色](docs/screenshots/front-album-light.jpg) | ![公开相册暗色](docs/screenshots/front-album-dark.jpg) | **隐私与限流(M5)** | 公开详情:位置信息已隐藏 | 上传限流提示 | |:---:|:---:| | ![公开详情位置隐藏](docs/screenshots/front-detail-privacy-light.jpg) | ![上传限流](docs/screenshots/admin-upload-rate-limit.jpg) | **暗色主题** | 前台首页 | 后台仪表盘 | |:---:|:---:| | ![前台首页暗色](docs/screenshots/front-home-dark.jpg) | ![后台仪表盘暗色](docs/screenshots/admin-dashboard-dark.jpg) | | 任务中心 | 元数据与归档 | |:---:|:---:| | ![任务中心暗色](docs/screenshots/admin-tasks-dark.jpg) | ![元数据与归档暗色](docs/screenshots/admin-metadata-dark.jpg) | | 相册 | 相册详情 | |:---:|:---:| | ![相册暗色](docs/screenshots/admin-albums-dark.jpg) | ![相册详情暗色](docs/screenshots/admin-album-detail-dark.jpg) | | 备份与恢复 | 任务中心 | |:---:|:---:| | ![备份与恢复暗色](docs/screenshots/admin-backups-dark.jpg) | ![任务中心暗色](docs/screenshots/admin-tasks-dark.jpg) | | 图片管理 | IP 访问控制 | |:---:|:---:| | ![图片管理暗色](docs/screenshots/admin-images-dark.jpg) | ![IP 访问控制暗色](docs/screenshots/admin-ip-access-dark.jpg) | | ![前台地图暗色](docs/screenshots/front-map-dark.jpg) | ![后台地图暗色](docs/screenshots/admin-map-dark.jpg) | > 截图由无头 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