فهرست منبع

feat(core): 添加文件管理系统扩展
- 实现了文件分类和文件管理的功能
- 添加了文件上传、删除、重命名等操作
- 支持文件类型限制和文件搜索
- 创建了文件管理的数据库迁移和模型
-编写了文件管理的控制器和实体类
- 添加了文件管理的路由和权限控制
- 实现了文件管理的钩子函数,用于扩展和定制

runphp 7 ماه پیش
والد
کامیت
77f942d8b5

+ 1 - 4
.gitignore

@@ -1,6 +1,3 @@
 composer.phar
 /vendor/
-
-# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
-# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
-# composer.lock
+.idea/

+ 35 - 0
README.md

@@ -0,0 +1,35 @@
+# Flysystem Extension
+
+文件管理系统
+
+这是用来管理文件的扩展,主要是展示文件列表,上传文件,下载文件,删除文件,重命名文件等功能。
+
+## 功能说明
+1. **展示文件列表**:列出所有已上传的文件及其基本信息。
+1. **上传文件**:支持从本地或远程仓库上传新的文件。
+1. **删除文件**:移除不再需要的文件。
+1. **重命名文件**:修改文件的名称。
+1. **文件类型限制**:限制上传文件的类型。
+1. **文件分类管理**:支持对文件进行分类管理。
+1. **文件搜索**:支持通过关键字搜索文件。
+1. **文件预览**:支持预览文件内容。
+
+## 使用方法
+1. **访问文件管理页面**:
+   - 登录系统后,导航至“文件管理”模块。
+2. **操作文件**:
+   - 点击“上传”按钮以添加新文件。
+   - 点击“图片”以预览文件。
+   - 点击“删除”按钮以移除文件。
+   - 点击“重命名”按钮以修改文件名称,此处是文件备注名称,不是文件名(因为文件有可能被其他业务数据引用)。
+## 注意事项
+1. **权限要求**:仅管理员用户可进行文件的上传、下载和删除操作。
+1. **备份数据**:在执行删除操作前,请确保已备份相关文件。
+1. **文件类型限制**:上传文件时,请确保其符合系统要求的文件类型。
+
+## HOOKS
+1. **after_flysystem_upload**: 在上传文件后触发
+1. **after_flysystem_delete**: 在删除文件后触发
+
+## 前端说明(管理后台)
+参考:  `frontend/admin/src/components/flysystem/README.md`

+ 31 - 0
composer.json

@@ -0,0 +1,31 @@
+{
+  "name": "six-shop/filesystem",
+  "description": "文件管理系统",
+  "type": "sixshop-extension",
+  "keywords": [
+    "sixshop",
+    "thinkphp"
+  ],
+  "require": {
+    "php": ">=8.3",
+    "six-shop/core": ">=0.4 <1.0"
+  },
+  "authors": [
+    {
+      "name": "hui he",
+      "email": "runphp@qq.com"
+    }
+  ],
+  "license": "MIT",
+  "autoload": {
+    "psr-4": {
+      "SixShop\\Filesystem\\": "src"
+    }
+  },
+  "extra": {
+    "sixshop": {
+      "id": "filesystem",
+      "class": "SixShop\\Filesystem\\Extension"
+    }
+  }
+}

+ 70 - 0
config.php

@@ -0,0 +1,70 @@
+<?php
+declare(strict_types=1);
+
+return json_decode(<<<'JSON'
+[
+  {
+    "type": "radio",
+    "field": "driver",
+    "value": "local",
+    "title": "驱动",
+    "info": "存储类型",
+    "effect": {
+      "fetch": ""
+    },
+    "$required": true,
+    "options": [
+      {
+        "label": "本地",
+        "value": "local"
+      }
+    ],
+    "_fc_id": "id_Fdcomcoutbvualc",
+    "name": "ref_Fb80mcotvpt7adc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "radio"
+  },
+  {
+    "type": "input",
+    "field": "root",
+    "value": "public/uploads",
+    "title": "root",
+    "info": "存储根目录",
+    "$required": true,
+    "props": {
+      "type": "text",
+      "placeholder": "public/uploads"
+    },
+    "_fc_id": "id_F4r4mcou0ptzafc",
+    "name": "ref_Fcm7mcou0ptzagc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+  {
+    "type": "input",
+    "field": "url",
+    "value": "/uploads",
+    "title": "url",
+    "info": "访问url",
+    "$required": true,
+    "props": {
+      "type": "text",
+      "placeholder": "/uploads"
+    },
+    "_fc_id": "id_Fizwmcou0rbbaic",
+    "name": "ref_Fh90mcou0rbbajc",
+    "display": true,
+    "hidden": false,
+    "_fc_drag_tag": "input"
+  },
+  {
+    "type": "input",
+    "field": "domain",
+    "value": "https://sixshop.ddev.site",
+    "title": "domain",
+    "info": "访问域名"
+  }
+]
+JSON, true);

+ 70 - 0
database/migrations/20250704030421_extension_filesystem_cagegory.php

@@ -0,0 +1,70 @@
+<?php
+
+use think\migration\Migrator;
+use think\migration\db\Column;
+
+class ExtensionFilesystemCagegory extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $table = $this->table('extension_filesystem_category', [
+            'comment' => '文件分类表',
+            'engine' => 'InnoDB',
+            'collation' => 'utf8mb4_general_ci',
+            'id' => false,
+            'primary_key' => 'id'
+        ]);
+        $table->addColumn('id', 'integer', [
+            'identity' => true,
+            'signed' => false,
+            'comment' => '主键'
+        ])->addColumn('name', 'string', [
+            'limit' => 100,
+            'null' => false,
+            'comment' => '名称'
+        ])->addColumn('parent_id', 'integer', [
+            'signed' => false,
+            'null' => true,
+            'default' => 0,
+            'comment' => '父级ID'
+        ])->addColumn('level', 'integer', [
+            'signed' => false,
+            'null' => false,
+            'default' => 0,
+            'comment' => '层级'
+        ])->addColumn('sort', 'integer', [
+           'signed' => false,
+            'null' => false,
+            'default' => 0,
+            'comment' => '排序'
+        ])->addColumn('file_count', 'integer',[
+            'signed' => false,
+            'null' => false,
+            'default' => 0,
+            'comment' => '文件数量'
+        ])->addTimestamps()
+            ->addSoftDelete()
+            ->create();
+    }
+}

+ 85 - 0
database/migrations/20250704031150_extension_filesystem_file.php

@@ -0,0 +1,85 @@
+<?php
+
+use think\migration\Migrator;
+
+class ExtensionFilesystemFile extends Migrator
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
+     *
+     * The following commands can be used in this method and Phinx will
+     * automatically reverse them when rolling back:
+     *
+     *    createTable
+     *    renameTable
+     *    addColumn
+     *    renameColumn
+     *    addIndex
+     *    addForeignKey
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change()
+    {
+        $table = $this->table('extension_filesystem_file', [
+            'comment' => '文件表',
+            'engine' => 'InnoDB',
+            'collation' => 'utf8mb4_general_ci',
+            'id' => false,
+            'primary_key' => 'id'
+        ]);
+        $table
+            ->addColumn('id', 'integer', [
+                'identity' => true,
+                'signed' => false,
+                'comment' => '主键'
+            ])
+            ->addColumn('category_id', 'integer', [
+                'default' => 0,
+                'signed' => false,
+                'comment' => '分类id'
+            ])
+            ->addColumn('file_hash', 'string', [
+                'default' => '',
+                'comment' => '文件hash'
+            ])->addColumn('name', 'string', [
+                'default' => '',
+                'comment' => '文件备注名称'
+            ])->addColumn('file_name', 'string', [
+                'default' => '',
+                'comment' => '文件名'
+            ])->addColumn('file_path', 'string', [
+                'default' => '',
+                'comment' => '文件路径'
+            ])->addColumn('file_size', 'integer', [
+                'default' => 0,
+                'comment' => '文件大小'
+            ])->addColumn('file_ext', 'string', [
+                'default' => '',
+                'comment' => '文件类型'
+            ])->addColumn('file_mine', 'string', [
+                'default' => '',
+                'comment' => '文件MIME'
+            ])->addColumn('file_url', 'string', [
+                'default' => '',
+                'comment' => '文件URL'
+            ])->addColumn('create_time', 'datetime', [
+                'null' => true,
+                'comment' => '创建时间'
+            ])->addColumn('update_time', 'datetime', [
+                'null' => true,
+                'comment' => '更新时间'
+            ])->addColumn('delete_time', 'datetime', [
+                'null' => true,
+                'comment' => '删除时间'
+            ])->addIndex('file_hash', [
+                'name' => 'file_hash'
+            ])->create();
+    }
+}

+ 17 - 0
info.php

@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+return [
+    'id' => 'filesystem', # 扩展的唯一标识符
+    'name' => '文件管理系统', # 扩展的名称
+    'is_core' => false, # 是否核心扩展'
+    'category' => 'core', # 扩展的分类 core:核心扩展,other:其他扩展
+    'description' => '这是用来管理文件的扩展,主要是展示文件列表,上传文件,下载文件,删除文件,重命名文件等功能', # 扩展的描述
+    'version' => '1.0.0',  # 扩展的版本
+    'core_version' => '^1.0',  # 支持的核心版本
+    'author' => 'runphp', # 作者
+    'email' => 'runphp@qq.com', # 作者的邮箱
+    'website' => '', # 扩展的地址,可以是扩展的仓库地址,帮助用户寻找扩展,安装扩展等网络地址
+    'image' => '', # 扩展的图片,用于展示扩展的图标,或者是扩展的截图等图片地址
+    'license' => 'MIT', # 扩展的开源协议
+];

+ 15 - 0
route/admin.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+
+use SixShop\Filesystem\Controller\{
+    CategoryController,
+    FileController
+};
+use think\facade\Route;
+
+Route::resource('category', CategoryController::class)->middleware([
+    'auth'
+]);
+Route::resource('file', FileController::class)->middleware([
+    'auth'
+]);

+ 63 - 0
src/Controller/CategoryController.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Filesystem\Controller;
+
+
+use SixShop\Core\Helper;
+use SixShop\Filesystem\Model\FilesystemCategoryModel;
+use SixShop\Filesystem\Model\FilesystemFileModel;
+use think\Request;
+use think\response\Json;
+
+class CategoryController
+{
+    public function index(): Json
+    {
+        $list = FilesystemCategoryModel::order(['sort' => 'asc', 'id' => 'asc'])->select();
+        $list = $list->toArray();
+        $list[] = [
+            'id' => 0,
+            'name' => '未分类',
+            'sort' => 0,
+            'file_count' => FilesystemFileModel::where('category_id', 0)->count(),
+        ];
+        $list[] = [
+            'id' => -1,
+            'name' => '所有文件',
+            'sort' => 0,
+            'file_count' => array_sum(array_column($list, 'file_count')),
+        ];
+        return Helper::success_response($list);
+    }
+
+    public function save(Request $request): Json
+    {
+        $data = $request->post([
+            'name/s',
+            'sort/d',
+        ]);
+        $result = FilesystemCategoryModel::create($data);
+
+        return Helper::success_response($result);
+
+    }
+
+    public function update(int $id, Request $request): Json
+    {
+        $data = $request->post([
+            'name/s',
+            'sort/d',
+        ]);
+        $result = FilesystemCategoryModel::findOrFail($id)->save($data);
+
+        return Helper::success_response($result);
+    }
+
+    public function delete(int $id): Json
+    {
+        $result = FilesystemCategoryModel::destroy($id);
+
+        return Helper::success_response($result);
+    }
+}

+ 67 - 0
src/Controller/FileController.php

@@ -0,0 +1,67 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Filesystem\Controller;
+
+use SixShop\Core\Helper;
+use SixShop\Core\Middleware\MacroPageMiddleware;
+use SixShop\Filesystem\Entity\FilesystemFileEntity;
+use think\facade\Event;
+use think\Request;
+use think\Response;
+
+class FileController
+{
+    protected array $middleware = [
+        MacroPageMiddleware::class
+    ];
+
+    public function index(Request $request, FilesystemFileEntity $filesystemFileEntity): Response
+    {
+        $params = $request->get([
+            'category_id/d' => -1,
+            'keyword/s',
+        ]);
+
+        return Helper::page_response($filesystemFileEntity->getPage($params, $request->pageAndLimit()));
+    }
+
+    public function save(Request $request, FilesystemFileEntity $filesystemFileEntity): Response
+    {
+        $categoryId = $request->post('category_id/d', 0);
+        $file = $request->file('file');
+        validate([
+            'category_id' => 'egt:0',
+            'file' => 'require|fileSize:'.(1024*1024*100).',fileExt:png,jpg,jpeg,gif,fileMime:image/png,image/jpg,image/jpeg,image/gif',
+        ], [
+            'category_id.egt' => '分类ID不能小于0',
+            'file.require' => '请选择文件',
+            'file.fileSize' => '文件过大',
+            'file.fileExt' => '文件格式错误',
+            'file.fileMime' => '文件格式错误',
+        ])->check([
+            'category_id' => $categoryId,
+            'file' => $file,
+        ]);
+
+        $result = $filesystemFileEntity->upload($categoryId, $file);
+
+        return Helper::success_response($result);
+    }
+
+    public function delete(int $id, FilesystemFileEntity $filesystemFileEntity): Response
+    {
+        $result = $filesystemFileEntity->destroy($id);
+        Event::trigger('after_filesystem_delete', $id);
+        return Helper::success_response($result);
+    }
+
+    public function update(int $id, Request $request, FilesystemFileEntity $filesystemFileEntity): Response
+    {
+        $data = $request->post([
+            'name/s' => '',
+        ]);
+        $result = $filesystemFileEntity->update($data, ['id' => $id]);
+        return Helper::success_response($result);
+    }
+}

+ 68 - 0
src/Entity/FilesystemFileEntity.php

@@ -0,0 +1,68 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Filesystem\Entity;
+
+use SixShop\Core\Entity\BaseEntity;
+use SixShop\Filesystem\Model\FilesystemFileModel;
+use think\facade\Event;
+use think\facade\Filesystem;
+use think\File;
+use think\file\UploadedFile;
+use think\Paginator;
+
+/**
+ * @mixin FilesystemFileModel
+ */
+class FilesystemFileEntity extends BaseEntity
+{
+    public function upload(int $categoryId, array|UploadedFile|null $file): FilesystemFileModel
+    {
+        $relativeDir = $categoryId . '/' . date('Ym') . '/' . date('d');
+        $fileHash = $file->hash();
+        $fileEntity = $this->withTrashed()->where([
+            'file_hash' => $fileHash,
+            'category_id' => $categoryId
+        ])->findOrEmpty();
+        if ($fileEntity->isEmpty()) {
+            $fileEntity = $this->withTrashed()->where([
+                'file_hash' => $fileHash,
+            ])->findOrEmpty();
+        }
+        if ($fileEntity->isEmpty()) {
+            $fileName = date('YmdHis') . '_' . $fileHash;
+            $filePath = Filesystem::putfile($relativeDir, $file, fn(File $file) => $fileName);
+            $fileExt = $file->extension();
+            $fileName .= '.' . $fileExt;
+            $data = [
+                'category_id' => $categoryId,
+                'file_hash' => $fileHash,
+                'file_name' => $fileName,
+                'file_path' => $filePath,
+                'file_size' => $file->getSize(),
+                'file_ext' => $fileExt,
+                'file_mine' => $file->getMime(),
+                'file_url' => Filesystem::url($filePath),
+            ];
+        } else {
+            $data = $fileEntity->toArray();
+            // 删除相同文件
+            /*$this->destroy([
+                'category_id' => $categoryId,
+                'file_hash' => $fileHash
+            ]);*/
+            $data['category_id'] = $categoryId;
+            unset($data['id'], $data['name'], $data['create_time'], $data['update_time'], $data['delete_time']);
+        }
+        $result = $this->create($data);
+        Event::trigger('after_filesystem_upload', $result->id);
+        return $result;
+    }
+
+    public function getPage(array $params, array $page): Paginator
+    {
+        return $this->withSearch(['category_id', 'keyword'], $params)
+            ->order('id', 'desc')
+            ->paginate($page);
+    }
+}

+ 24 - 0
src/Extension.php

@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Filesystem;
+
+
+use SixShop\Core\ExtensionAbstract;
+use SixShop\Filesystem\Hook\FilesystemHook;
+
+class Extension extends ExtensionAbstract
+{
+
+    public function getHooks(): array
+    {
+        return [
+            FilesystemHook::class
+        ];
+    }
+
+    protected function getBaseDir(): string
+    {
+        return dirname(__DIR__);
+    }
+}

+ 66 - 0
src/Hook/FilesystemHook.php

@@ -0,0 +1,66 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Filesystem\Hook;
+
+use SixShop\Core\Attribute\Hook;
+use SixShop\Core\Helper;
+use SixShop\Filesystem\Model\FilesystemCategoryModel;
+use SixShop\Filesystem\Model\FilesystemFileModel;
+use think\App;
+use think\exception\ErrorException;
+
+class FilesystemHook
+{
+    private bool $configInit = false;
+
+    /**
+     * 检查数据库里面是否存在数据
+     */
+    #[Hook("before_uninstall_filesystem_extension")]
+    public function checkDatabase(): void
+    {
+        FilesystemCategoryModel::withTrashed()->count() > 0 && abort(500, '请先删除分类数据');
+        FilesystemFileModel::withTrashed()->count() > 0 && abort(500, '请先删除文件数据');
+    }
+
+    #[Hook(["after_filesystem_upload", "after_filesystem_delete"])]
+    public function updateCategoryFileCount(int $id): void
+    {
+        $file = FilesystemFileModel::withTrashed()->find($id);
+        $count = FilesystemFileModel::where('category_id', $file->category_id)->count();
+        FilesystemCategoryModel::where('id', $file->category_id)->update([
+            'file_count' => $count
+        ]);
+    }
+
+    #[Hook("hook_init")]
+    public function init(App $app): void
+    {
+        $app->config->hook(function ($name, $value) use ($app) {
+            if (str_starts_with($name, 'filesystem') && !$this->configInit) {
+                $config = extension_config('filesystem');
+                try {
+                    $app->config->set([
+                        // 默认磁盘
+                        'default' => $config['driver'],
+                        // 磁盘列表
+                        'disks' => [
+                            $config['driver'] => [
+                                'type' => $config['driver'],
+                                'root' => app()->getRootPath() . $config['root'],
+                                'url' => $config['url'],
+                                'visibility' => 'public',
+                            ],
+                        ]
+                    ], 'filesystem');
+                    $this->configInit = true;
+                } catch (ErrorException $e) {
+                    Helper::throw_logic_exception('filesystem配置错误');
+                }
+                return $app->config->get($name);
+            }
+            return $value;
+        }, 'filesystem');
+    }
+}

+ 29 - 0
src/Model/FilesystemCategoryModel.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Filesystem\Model;
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class SixShop\Filesystem\Model\FilesystemCategoryModel
+ *
+ * @property int $file_count 文件数量
+ * @property int $id 主键
+ * @property int $level 层级
+ * @property int $parent_id 父级ID
+ * @property int $sort 排序
+ * @property string $create_time 创建时间
+ * @property string $delete_time 删除时间
+ * @property string $name 名称
+ * @property string $update_time 更新时间
+ * @method static \think\db\Query onlyTrashed()
+ * @method static \think\db\Query withTrashed()
+ */
+class FilesystemCategoryModel extends Model
+{
+    protected $name = 'extension_filesystem_category';
+    protected $pk = 'id';
+
+    use SoftDelete;
+}

+ 48 - 0
src/Model/FilesystemFileModel.php

@@ -0,0 +1,48 @@
+<?php
+declare(strict_types=1);
+
+namespace SixShop\Filesystem\Model;
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * Class SixShop\Filesystem\Model\FilesystemFileModel
+ *
+ * @property int $category_id 分类id
+ * @property int $file_size 文件大小
+ * @property int $id 主键
+ * @property string $create_time 创建时间
+ * @property string $delete_time 删除时间
+ * @property string $file_ext 文件类型
+ * @property string $file_hash 文件hash
+ * @property string $file_mine 文件MIME
+ * @property string $file_name 文件名
+ * @property string $file_path 文件路径
+ * @property string $file_url 文件URL
+ * @property string $name 文件备注名称
+ * @property string $update_time 更新时间
+ * @method static \think\db\Query onlyTrashed()
+ * @method static \think\db\Query withTrashed()
+ */
+class FilesystemFileModel extends Model
+{
+    protected $name = 'extension_filesystem_file';
+    protected $pk = 'id';
+
+    use SoftDelete;
+
+    public function searchKeywordAttr($query, $value): void
+    {
+        if ($value) {
+            $query->whereLike('file_name|name', '%' . $value . '%');
+        }
+    }
+
+    public function searchCategoryIdAttr($query, int $value): void
+    {
+        if ($value >= 0) {
+            $query->where('category_id', $value);
+        }
+    }
+}

+ 15 - 0
src/helper.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+
+
+if (!function_exists('file_url')) {
+    /**
+     * 获取文件的URL
+     * @param string $path 文件路径
+     * @return string 文件URL
+     */
+    function file_url(string $path): string
+    {
+        return extension_config('filesystem', 'domain').'/'.$path;
+    }
+}