// +---------------------------------------------------------------------- declare(strict_types=1); namespace app\backend\controller; use think\facade\Db; use ywxapp\controller\BackendBase; use ywxapp\library\AdminLogger; use ywxapp\model\CommonAttachment; use ywxapp\service\FileStorageService; /** * 附件统一管理 * * API 端(/api/v1/common/upload 等)负责收文件,后台这里做全局视角: * 全站附件一览(不分模块/不分上传者)、按条件检索、预览、删除(物理文件 + 软删记录)、容量统计。 * 删除始终走 FileStorageService::delete(),OSS / FTP 等自定义存储由 FileDelete 事件接管, * 后台代码无需关心文件到底存在哪。 * * 权限节点(首次访问自动补录):attachment:index / attachment:delete * * @author ywxapp */ class Attachment extends BackendBase { use EnsuresPower; protected $noNeedLogin = []; protected $noNeedVerify = []; /** * 进程内只补录一次权限 * @var bool */ protected static $powerChecked = false; protected function initialize() { if (!self::$powerChecked) { self::$powerChecked = true; $this->ensureMenus(); } } /** * 列表 + 统计 */ public function index() { if ($this->request->isAjax()) { $page = $this->request->param('page/d', 1); $limit = $this->request->param('limit/d', 20); $kw = trim((string) $this->request->param('keyword', '')); $module = trim((string) $this->request->param('module', '')); $onlyImg = (int) $this->request->param('only_image/d', 0); $query = CommonAttachment::where('delete_at', 0); if ($kw !== '') { $query->where(function ($q) use ($kw) { $q->where('original', 'like', '%' . $kw . '%') ->whereOr('name', 'like', '%' . $kw . '%') ->whereOr('path', 'like', '%' . $kw . '%'); }); } if ($module !== '') { $query->where('module', $module); } if ($onlyImg === 1) { $query->where('is_image', 1); } $total = (int) $query->count(); $list = $query->order('id', 'desc')->page($page, $limit)->select()->toArray(); foreach ($list as &$row) { $row['size_text'] = $this->formatBytes((int) $row['size']); $row['create_at_text'] = $row['create_at'] ? date('Y-m-d H:i:s', (int) $row['create_at']) : '-'; } $this->result->setCount($total)->success($list); } // 概览统计与模块清单(供页面筛选下拉) $stat = [ 'total' => (int) CommonAttachment::where('delete_at', 0)->count(), 'size' => (int) CommonAttachment::where('delete_at', 0)->sum('size'), 'image' => (int) CommonAttachment::where('delete_at', 0)->where('is_image', 1)->count(), ]; $stat['size_text'] = $this->formatBytes($stat['size']); $modules = Db::name('common_attachment') ->where('delete_at', 0) ->field('module, COUNT(*) AS total') ->group('module') ->select() ->toArray(); $this->assign('stat', $stat); $this->assign('modules', $modules); return $this->fetch(); } /** * 删除:物理删文件(可由事件接管)+ 软删记录 */ public function delete() { if (! $this->request->isPost()) { $this->result->error('请求方式错误', 405); } $ids = $this->request->param('ids', ''); $ids = is_array($ids) ? $ids : array_filter(explode(',', (string) $ids)); if (empty($ids)) { $this->result->error('请选择要删除的附件'); } $storage = new FileStorageService(); $ok = 0; $fail = 0; foreach ($ids as $id) { $row = CommonAttachment::find((int) $id); if (! $row) { $fail++; continue; } // 文件删不掉也要删记录,避免磁盘残留变成无法再管的孤儿 $storage->delete((string) $row->path); $row->delete(); $ok++; } AdminLogger::write('attachment/delete', '删除附件 ' . $ok . ' 个' . ($fail ? "({$fail} 个不存在)" : '')); $this->result->success(['ok' => $ok, 'fail' => $fail], '已删除 ' . $ok . ' 个附件'); } protected function formatBytes(int $bytes): string { if ($bytes <= 0) { return '0 B'; } $units = ['B', 'KB', 'MB', 'GB']; $i = (int) min(count($units) - 1, floor(log($bytes, 1024))); return round($bytes / (1024 ** $i), 2) . ' ' . $units[$i]; } }