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

5.9 KiB
Raw Blame History

附件管理(上传 / 查询 / 删除 / 存储扩展)

统一的上传-附件闭环:上传接口负责收文件,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。

成功响应:

{
  "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 示例)

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',
    ];
});

上传成功后扩展(缩略图 / 水印)

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 刷新……
    }
});

接管物理删除

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 端删附件走同一条链路,不会出现「记录没了文件还在」。

孤儿文件清理

「孤儿」= 磁盘上有、附件表里没有记录的文件(落库失败、记录被硬删、手工拷贝都会留下)。

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 列表后即可通过配置切换。