// +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\articles\controller\backend; use addon\articles\library\ArticleSchema; use addon\articles\model\ArticleAttachment; use think\facade\Request; use think\facade\View; use ywxapp\controller\BackendBase; use ywxapp\service\FileStorageService; /** * 附件管理 * * 上传走框架 FileStorageService(与 /upload 同源),这里额外做: * - 后台专用:类型白名单 + 大小限制 * - 落库记录,便于统一查看与清理 */ class Attachment extends BackendBase { protected $noNeedVerify = []; /** 允许的文件类型 */ private const ALLOW_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', 'txt', 'csv']; /** 单文件上限(字节) */ private const MAX_SIZE = 10485760; // 10MB protected function initialize() { parent::initialize(); ArticleSchema::ensure(); } public function index() { if (Request::isAjax()) { $page = Request::param('page', 1); $limit = Request::param('limit', 15); $aid = (int) Request::param('aid', 0); $query = ArticleAttachment::order('id', 'desc'); if ($aid > 0) { $query->where('aid', $aid); } $paginator = $query->paginate(['list_rows' => $limit, 'page' => $page]); $rows = []; foreach ($paginator->items() as $f) { $rows[] = [ 'id' => $f->id, 'name' => $f->name, 'url' => $f->url, 'ext' => $f->ext, 'size_text' => $f->size_text, 'aid' => (int) $f->aid, 'create_at' => $f->create_at, ]; } return json(['code' => 0, 'msg' => '', 'count' => $paginator->total(), 'data' => $rows]); } return View::fetch('attachment/index'); } /** * 上传并记录 */ public function upload() { $file = Request::file('file'); if (!$file || !$file->isValid()) { return json(['code' => 0, 'msg' => '未获取到上传文件']); } $ext = strtolower($file->extension()); if (!in_array($ext, self::ALLOW_EXT, true)) { return json(['code' => 0, 'msg' => '不支持的文件类型:' . $ext]); } if ($file->getSize() > self::MAX_SIZE) { return json(['code' => 0, 'msg' => '文件不能超过 ' . (self::MAX_SIZE / 1048576) . 'MB']); } // 真实 MIME 二次校验(防改扩展名) $mime = (string) $file->getMime(); if ($mime !== '' && preg_match('#^text/(x-)?php#i', $mime)) { return json(['code' => 0, 'msg' => '危险文件类型']); } try { $storage = new FileStorageService(); $result = $storage->upload($file, 'articles/' . date('Ymd')); $record = ArticleAttachment::record( is_array($result) ? $result : ['url' => (string) $result], (int) ($this->auth->info['uid'] ?? 0), (int) Request::post('aid', 0) ); return json([ 'code' => 1, 'msg' => '上传成功', 'data' => ['id' => $record->id, 'url' => $record->url], ]); } catch (\Throwable $e) { return json(['code' => 0, 'msg' => '上传失败:' . $e->getMessage()]); } } /** * 删除记录并尝试清理物理文件 */ public function delete($id = 0) { $id = (int) ($id ?: Request::post('id', 0)); $att = ArticleAttachment::find($id); if (!$att) { return json(['code' => 0, 'msg' => '记录不存在']); } // 删除物理文件(限定在 public 目录内,防目录穿越) $rel = ltrim((string) $att->path, '/'); $url = ltrim((string) $att->url, '/'); $root = rtrim((string) public_path(), DIRECTORY_SEPARATOR); foreach ([$rel, $url] as $candidate) { if ($candidate === '' || strpos($candidate, '..') !== false) { continue; } $full = $root . DIRECTORY_SEPARATOR . $candidate; if (is_file($full)) { @unlink($full); break; } } $att->delete(); return json(['code' => 1, 'msg' => '已删除']); } }