Files
YwxAppThink/docs/guide/attachment_storage.md
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0)
- 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、
  ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签
  (ywxapp.framework_package_verify,默认开启)
- JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret()
  校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET
- /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验
- 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流
- TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive
- 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php)
- 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis
- paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG

工程化 / 交付
- 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本
- 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过
- 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障
- .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess

结构
- appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

163 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 附件管理(上传 / 查询 / 删除 / 存储扩展)
统一的上传-附件闭环:上传接口负责收文件,`FileStorageService` 自动写入附件表,提供分页查询与删除接口;存储层通过**事件钩子**可扩展 OSS / FTP 等自定义存储,业务代码无需改动。
## 接口一览(均需登录)
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/v1/common/upload` | POST | 上传文件(multipart,字段 `file`),成功自动落库 |
| `/api/v1/attachment/list` | GET | 我的附件分页(可 `module` 过滤) |
| `/api/v1/attachment/delete` | POST | 删除附件(仅本人),物理删文件 + 软删记录 |
### 1. 上传
```
POST /api/v1/common/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
```
| 参数 | 说明 |
|------|------|
| `file` | 上传的文件(必填) |
默认校验:大小 ≤ 10MB,后缀白名单 `jpg,jpeg,png,gif,webp,bmp,mp4,mov,mp3,wav,zip,rar,doc,docx,xls,xlsx,pdf,txt`。
成功响应:
```json
{
"code": 0,
"msg": "上传成功",
"data": {
"url": "https://demo.ywxapp.cn/storage/2026/08/26/xxxx.jpg",
"path": "2026/08/26/xxxx.jpg",
"name": "xxxx.jpg",
"size": 102400
}
}
```
### 2. 查询(我的附件分页)
```
GET /api/v1/attachment/list?page=1&size=20&module=api
```
| 参数 | 默认 | 说明 |
|------|------|------|
| `page` | 1 | 页码 |
| `size` | 20 | 每页条数(上限 100) |
| `module` | - | 模块过滤(可选,如 `api` / `forum` / `chat`) |
响应:`data.list`(附件数组,含 `id/name/url/path/size/ext/mime/is_image/module/create_at` 等)、`data.total`(总数)。
### 3. 删除
```
POST /api/v1/attachment/delete
body: { "id": 1 }
```
- 仅能删除**自己**的附件(`uid` 归属校验)
- 流程:先物理删除文件(失败不阻断,`data.deleted_file=false` 提示)→ 软删记录(`delete_at`)
- 成功响应:`{ "code": 0, "msg": "删除成功", "data": { "deleted_file": true } }`
## 附件表 `common_attachment`
| 字段 | 说明 |
|------|------|
| `uid` | 上传者(会员 uid,未登录 0) |
| `module` | 模块标识(`api` / `forum` / `chat` …) |
| `name` / `original` | 存储文件名 / 原始文件名 |
| `path` / `url` | 相对路径 / 可访问 URL |
| `size` / `ext` / `mime` | 大小 / 后缀 / MIME |
| `storage` / `driver_info` | 存储驱动标识 / 驱动附加信息(JSON) |
| `is_image` / `width` / `height` | 图片标记与尺寸 |
| `delete_at` | 软删除时间(0=未删,模型启用 SoftDelete) |
表结构自愈:模型 `ensureSchema()` 会自动补 `delete_at` 列(旧库无需手工 ALTER)。
## 事件钩子(存储扩展)
所有上传/删除统一走 `FileStorageService`,提供 4 个事件:
| 事件 | 阶段 | 返回约定 |
|------|------|----------|
| `FileUpload` | 上传前(分发) | 监听并**返回结果数组**即接管上传(OSS/FTP) |
| `FileUploaded` | 上传成功后(通知) | 无需返回,用于缩略图/水印/审计 |
| `FileDelete` | 删除文件前(分发) | 监听并**返回 true** 即接管物理删除 |
| `FileDeleted` | 删除成功后(通知) | 无需返回 |
> 中断式事件(`Event::trigger(..., true)`):必须返回**非空数组 / true** 才视为接管;空数组会被当作未接管,回落内置驱动。
### 接管上传(OSS / FTP 示例)
```php
use think\facade\Event;
Event::listen('FileUpload', function (array $payload) {
$file = $payload['file']; // \think\file\UploadedFile
$path = $payload['path']; // 目标路径
// --- 调用你的 OSS / FTP SDK 上传,拿到远端地址 ---
$remotePath = 'uploads/' . $path;
$oss->put($remotePath, file_get_contents($file->getRealPath()));
// 返回数组即接管成功(键 name/path/url/storage 写入附件表)
return [
'name' => basename($remotePath),
'path' => $remotePath,
'url' => 'https://cdn.example.com/' . $remotePath,
'storage' => 'alioss',
];
});
```
### 上传成功后扩展(缩略图 / 水印)
```php
Event::listen('FileUploaded', function (array $data) {
// $data: {id,module,original,name,path,url,size,ext,mime,storage,is_image,width,height,...}
if (!empty($data['is_image'])) {
// 生成缩略图、加水印、CDN 刷新……
}
});
```
### 接管物理删除
```php
Event::listen('FileDelete', function (array $payload) {
// 删除 OSS / FTP 上的 $payload['path']
$oss->delete($payload['path']);
return true; // 必须返回 true 表示已接管
});
```
## 后台统一管理
系统管理 → **附件管理**(`app/backend/controller/Attachment.php`,权限 `attachment:index|delete`,首次访问自动补录):
- 全站附件一览(不分模块 / 不分上传者),可按文件名、路径、模块检索,可只看图片;
- 顶部概览:附件总数、占用空间、图片数;
- 删除走 `FileStorageService::delete()` → 触发 `FileDelete` 事件(OSS/FTP 在此接管)→ 再软删记录,
所以后台删附件和 API 端删附件走同一条链路,不会出现「记录没了文件还在」。
### 孤儿文件清理
「孤儿」= 磁盘上有、附件表里没有记录的文件(落库失败、记录被硬删、手工拷贝都会留下)。
```bash
php think attachment:clean # 只统计,列出前 20 个(安全)
php think attachment:clean --delete # 真正删除
php think attachment:clean --days=30 # 只处理 30 天前的文件
```
删除同样走 `FileStorageService::delete()`,自定义存储由 `FileDelete` 事件接管。
## 内置存储驱动
默认走 `config/filesystem.php` 的 `storage` 驱动(本地磁盘,落盘 `public/storage/`)。新增驱动:实现 `ywxapp\utils\storage\StorageInterface`(`upload()` 返回 `[name, path, url]`、`delete()` 返回 bool),注册到 `config/filesystem.php` 的 `storages` 列表后即可通过配置切换。