图片上传与存储
已确认的实施方案
管理员配置本地、S3 兼容存储或自部署 Lsky Pro,启用一套默认上传目标;用户在头像、应用图标、商品图片字段直接选择、拖拽、粘贴图片。存储凭据仅后端加密保存。现有外部链接兼容保留。
图片经后端鉴权、格式与像素校验后上传,保持 PNG 透明通道。每张图片记录上传时的存储配置,对外使用稳定 /api/v1/media/{id} 地址。切换默认只影响新图;停用不影响旧图。已有配置的存储目标不可变,密钥可轮换。工单与发票保持私有本地附件流程。
本地指后端服务器磁盘。旧附件保持原 UPLOAD_DIR,新增存储可指定绝对目录。S3 使用私有 Bucket,由服务端读取。Lsky 首先适配开源 v2 API(Bearer Token、/api/v1/upload);其他版本不得标称兼容,需真实接口验证。
管理能力包括配置管理、实际上传/读取/删除测试、图片列表、引用保护与未引用图片清理。上传不等于业务保存;取消编辑后临时图片延迟 24 小时清理。清理失败保留记录重试。
验证:应用直接本机运行;数据库只使用已检查状态及映射的现有 Postgres18 / RedisPwd。禁止创建或重建容器。后端 ./scripts/run-local.ps1 -Mode test,前端 npm run build,再进行三种存储真实联调。没有配置的真实服务标记未验证。
发布验收记录
2026-09-09 本机验证:
./scripts/run-local.ps1 -Mode test:Go 测试、go vet 通过;其中没有配置 TEST_DATABASE_URL 的其他集成用例会跳过。go test ./internal/app -run '^TestImageStorageIntegration$' -count=1 -v:显式连接已检查的 Postgres18(127.0.0.1:5432),在临时 schema 执行全部迁移及图片上传、PNG 透明度、头像关联、引用保护、默认存储切换、旧图读取和删除测试,通过后清理临时 schema。本用例不使用 Redis;RedisPwd 已确认健康且映射 6379。npm --prefix web run build:通过。node --test scripts/api-refresh-tests.cjs:7 项通过。node scripts/image-storage-ui-check.cjs:本机前端 13140,使用 API fixtures 验证 1440/390 宽度三种配置弹窗、图片上传并保存应用图标、剪贴板粘贴,通过。脚本需要 Playwright 和 Edge;本机依赖置于忽略目录.local/ui-tools,通过 NODE_PATH 指定。- 本地目录读写、S3 签名请求及上传读取删除协议、Lsky Bearer/strategy/multipart/返回解析、错误与重定向拒绝:自动测试通过。S3 / Lsky 使用本机 HTTP 测试服务;没有真实 S3 Bucket 和 Lsky 实例配置,真实服务联调未执行,不能据此宣称所有图床版本兼容。
发布目标按现有 GitHub 仓库处理;当前未提供线上服务器和部署流程,不将源码发布描述为服务器已部署。
使用与升级
- 备份数据库、DATA_KEY_FILE 和原本地上传目录,执行
./scripts/run-local.ps1 -Mode migrate应用008_image_storage.sql,然后重启 API。迁移只增加图片配置及附件元数据,不移动旧文件。 - 本机 API 启动:
./scripts/run-local.ps1 -Mode api;前端在 web 目录执行npm run dev,访问 13140。升级期间不创建、替换任何容器。 - 管理后台 → 系统设置 → 图片存储,新增配置、测试上传/读取/删除、设为默认。没有默认存储时禁止新图片上传;旧图片仍按原 UPLOAD_DIR 读取。
- S3 桶保持私有,授予配置前缀(默认
images/*)的 PutObject/GetObject/DeleteObject 权限;无需浏览器 CORS。Lsky 填写站点地址和专用账号 Token;第一版只支持开源 v2 API。 - PNG/JPEG 由后端校验并重新编码;浏览器接收 WebP 后转 PNG。头像最大 2MB,其他图片最大 10MB,宽高均最多 4096 像素。头像可选择居中方形裁剪。进度为上传忙碌状态,不显示虚假的百分比。
- 新图片 24 小时未引用会清理,旧附件不自动清理;后台清理失败每分钟重试。Lsky 若远端已成功但网络中断且未返回图片标识,无法精确删除未知远端对象,需要在图床侧核对清理。测试删除失败会显示待清理标识。
管理 API
前缀 /api/v1,图片配置和管理需 settings:write;写操作沿用 Cookie、Origin、CSRF 校验。普通头像、商品和应用图片上传分别沿用用户身份、products:write、applications:write 权限;不会开放工单/发票附件。
| 接口 | 用途 |
|---|---|
| GET /image-policy | 获取当前图片规则 |
| PUT /admin/image-policy | 修改图片规则和外链开关 |
| GET /admin/image-storages | 列出配置,不返回密钥 |
| POST /admin/image-storages | 创建 local/s3/lsky 配置 |
| PUT /admin/image-storages/:id | 修改名称、密钥及 active;目标不能变 |
| POST /admin/image-storages/:id/test | 测试上传、图片读取、删除;分别返回状态 |
| DELETE /admin/image-storages/:id | 删除没有图片的停用配置 |
| GET /admin/images?page=1 | 每页 30 张图片及引用状态 |
| DELETE /admin/images/:id | 删除未引用图片;失败保留记录 |
| POST /attachments | multipart 的 purpose(avatar/product/application)和 file |
| GET /media/:id | 稳定展示地址,头像须已绑定 |
| GET /attachments/:id | 上传者或图片管理员预览;私有附件维持原权限 |
管理员可配置 S3 路径前缀及 CDN 域名、图片大小和尺寸、每用户图片总配额、外链开关;图片列表可按用途、存储、上传者 ID 筛选。默认路径前缀 images/、默认每用户配额 200MB,并保留既有每 IP 每小时 30 次上传限制。图片用量含未清理附件;降低配额不会删除旧图,但会阻止超额上传。门户首屏、图文板块和项目卡片使用同一上传链路与引用保护;图片上传后仍需保存对应门户方案才会成为长期引用。