Files
MrleeAdmin/backend/app/adminapi/controller/upload/FilesController.php
T

503 lines
17 KiB
PHP
Raw Normal View History

2026-08-31 11:33:59 +08:00
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace app\adminapi\controller\upload;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\system\UploadQueryRequest;
use app\adminapi\schema\response\system\UploadResponse;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\system\UploadService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use OpenApi\Attributes\RequestBody;
use support\annotation\Middleware;
use support\Request;
use Webman\RedisQueue\Client;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class FilesController extends Crud
{
public function __construct(UploadService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/files',
summary: '列表',
tags: ['附件管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => UploadQueryRequest::class,
]
)]
#[Permission(code: 'upload:files:list')]
#[PageResponse(schema: UploadResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/files/{id}',
summary: '详情',
tags: ['附件管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'upload:files:read')]
#[SimpleResponse(schema: UploadResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Delete(
path: '/files/{id}',
summary: '删除',
tags: ['附件管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'upload:files:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/files',
summary: '批量删除',
tags: ['附件管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'upload:files:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Post(
path: '/files/fetch-and-save-image',
summary: '上传网络图片到服务器',
tags: ['附件管理'],
)]
#[RequestBody(
required: true,
content: new OA\JsonContent(
properties: [
new OA\Property(
property: 'url',
description: '图片URL',
type: 'string',
example: 'https://example.com/image.jpg'
),
]
)
)]
#[Permission(code: 'upload:files:fetch_and_save_image')]
#[SimpleResponse(schema: [], example: [])]
public function downloadNetworkImage(Request $request): \support\Response
{
$url = $request->input('url', '');
$result = $this->service->saveNetworkImage($url);
return Json::success('操作成功', $result);
}
#[OA\Get(
path: '/files/download-by-id/{id}',
summary: '根据ID下载资源',
tags: ['附件管理'],
parameters: [
new OA\Parameter(
name: 'id',
description: '文件ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '123456789012345678')
),
]
)]
#[Permission(code: 'upload:files:download_by_id')]
#[SimpleResponse(schema: [], example: [])]
public function downloadResourceById(Request $request): \support\Response|\Webman\Http\Response
{
try {
$id = $request->route->param('id');
$data = $this->service->get($id);
if (empty($data)) {
throw new AdminException('数据未找到', ['errorCode' => -1]);
}
return response()->download($data->path, $data->filename);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Get(
path: '/files/download-by-hash/{hash}',
summary: '根据hash下载资源',
tags: ['附件管理'],
parameters: [
new OA\Parameter(
name: 'hash',
description: '文件Hash',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '123456789012345678')
),
]
)]
#[Permission(code: 'upload:files:download_by_hash')]
#[SimpleResponse(schema: [], example: [])]
public function downloadResourceByHash(Request $request): \support\Response|\Webman\Http\Response
{
try {
$hash = $request->route->param('hash');
$data = $this->service->get(['hash' => $hash]);
if (empty($data)) {
throw new AdminException('数据未找到', ['errorCode' => -1]);
}
return response()->download($data->path, $data->filename);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
/**
* @throws \Throwable
*/
#[OA\Post(
path: '/files/upload-image',
summary: '上传图片',
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:upload_image')]
#[RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
properties: [
new OA\Property(
property: 'file',
description: '文件',
type: 'string',
format: 'binary'
),
new OA\Property(
property: 'sub_dir',
description: '子目录路径,例如:image/202603',
type: 'string',
example: 'image/202603'
),
]
)
)
)]
#[SimpleResponse(schema: UploadResponse::class, example: [])]
public function uploadImage(Request $request): \support\Response
{
try {
$subDir = $request->input('sub_dir', 'image');
$subDir .= '/' . date('Ym');
$result = $this->service->upload($subDir);
return Json::success('ok', $result->toArray());
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/files/upload-file',
summary: '上传文件',
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:upload_file')]
#[RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
properties: [
new OA\Property(
property: 'file',
description: '文件',
type: 'string',
format: 'binary'
),
new OA\Property(
property: 'sub_dir',
description: '子目录路径,例如:file/202603',
type: 'string',
example: 'file/202603'
),
]
)
)
)]
#[SimpleResponse(schema: UploadResponse::class, example: [])]
public function uploadFile(Request $request): \support\Response
{
try {
$subDir = $request->input('sub_dir', 'file');
$subDir .= '/' . date('Ym');
$result = $this->service->upload($subDir);
return Json::success('ok', $result);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/files/svg-icons',
summary: 'SVG图标列表',
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:upload_file')]
#[SimpleResponse(schema: [], example: [])]
public function svgIcons(Request $request): \support\Response
{
try {
$targetDir = public_path() . '/upload/svg-icons';
if (!is_dir($targetDir)) {
return Json::success('ok', []);
}
$icons = [];
foreach (glob($targetDir . '/*.svg') ?: [] as $file) {
$name = pathinfo($file, PATHINFO_FILENAME);
$content = file_get_contents($file);
if ($content === false) {
continue;
}
$icons[] = [
'icon' => 'svg:' . $name,
'name' => $name,
'filename' => basename($file),
'url' => '/upload/svg-icons/' . basename($file),
'content' => $content,
];
}
return Json::success('ok', $icons);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/files/upload-svg-icon',
summary: '上传SVG图标',
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:upload_file')]
#[RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
properties: [
new OA\Property(
property: 'file',
description: 'SVG图标文件',
type: 'string',
format: 'binary'
),
]
)
)
)]
#[SimpleResponse(schema: [], example: [])]
public function uploadSvgIcon(Request $request): \support\Response
{
try {
$file = $request->file('file');
if (!$file) {
return Json::fail('请选择SVG文件');
}
if ($file->getSize() > 512 * 1024) {
return Json::fail('SVG文件大小不能超过512KB');
}
$extension = strtolower($file->getUploadExtension());
$mime = $file->getUploadMimeType();
$allowedMimeTypes = ['image/svg+xml', 'image/svg', 'text/xml', 'application/xml', 'text/plain', ''];
if ($extension !== 'svg' || !in_array($mime, $allowedMimeTypes, true)) {
return Json::fail('只支持SVG格式文件');
}
$content = file_get_contents($file->getPathname());
if ($content === false || trim($content) === '') {
return Json::fail('SVG文件读取失败');
}
if (!preg_match('/<svg[\s>]/i', $content)) {
return Json::fail('请选择有效的SVG文件');
}
if (preg_match('/<script|<foreignObject|on\w+\s*=|javascript:/i', $content)) {
return Json::fail('SVG文件包含不安全内容');
}
$originalName = pathinfo($file->getUploadName(), PATHINFO_FILENAME);
$iconName = preg_replace('/[^a-zA-Z0-9_-]/', '-', $originalName) ?: 'icon';
$iconName = trim($iconName, '-_') ?: 'icon';
$targetDir = public_path() . '/upload/svg-icons';
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
return Json::fail('SVG图标目录创建失败');
}
$targetPath = $targetDir . '/' . $iconName . '.svg';
if (is_file($targetPath)) {
$iconName .= '-' . date('YmdHis');
$targetPath = $targetDir . '/' . $iconName . '.svg';
}
if (file_put_contents($targetPath, $content, LOCK_EX) === false) {
return Json::fail('SVG图标保存失败');
}
return Json::success('上传成功', [
'icon' => 'svg:' . $iconName,
'name' => $iconName,
'filename' => $iconName . '.svg',
'url' => '/upload/svg-icons/' . $iconName . '.svg',
'path' => str_replace('\\', '/', $targetPath),
'content' => $content,
]);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 上传图片返回base64
* 用于插件开发等需要base64预览的场景
*/
#[OA\Post(
path: '/files/upload-image-base64',
summary: '上传图片返回base64',
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:upload_image_base64')]
#[RequestBody(
required: true,
content: new OA\JsonContent(
properties: [
new OA\Property(
property: 'file',
description: '图片文件',
type: 'string',
format: 'binary'
),
]
)
)]
public function uploadImageBase64(Request $request): \support\Response
{
try {
$file = $request->file('file');
if (!$file) {
return Json::fail('请选择图片文件');
}
// 验证文件类型
$allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
if (!in_array($file->getUploadMimeType(), $allowedTypes)) {
return Json::fail('只支持PNG、JPEG、JPG、GIF格式的图片');
}
// 验证文件大小(最大2MB)
$maxSize = 2 * 1024 * 1024;
if ($file->getSize() > $maxSize) {
return Json::fail('图片大小不能超过2MB');
}
// 读取文件内容并转换为base64
$imageData = file_get_contents($file->getPathname());
if ($imageData === false) {
return Json::fail('图片读取失败');
}
$base64 = 'data:' . $file->getUploadMimeType() . ';base64,' . base64_encode($imageData);
return Json::success('上传成功', [
'base64' => $base64,
'size' => $file->getSize(),
'mime' => $file->getUploadMimeType(),
]);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/export/download-excel',
summary: '下载导出的excel',//下载导出的excel-后期需要优化实现下载成功后删除原文件
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:download_excel')]
public function downloadExcel(Request $request): ?\support\Response
{
$param = $request->all();
$file = $param['file_path'];//文件路径
$downloadName = $param['file'] ?? date('Y-m-d His', time());//文件下载名称
$filePath = runtime_path() . $file;
//生成的文件5分钟后超时自动删除
$queue = 'remove-excel-file';
Client::send($queue, $param, 300);
return response()->download($filePath, $downloadName);
}
#[OA\Post(
path: '/files/common/wangeditor',
summary: '适配wangeditor编辑器上传接口',
tags: ['附件管理'],
)]
#[Permission(code: 'upload:files:wangeditor')]
public function wangeditor(Request $request): \support\Response
{
try {
$subDir = $request->input('sub_dir', 'image');
$subDir .= '/' . date('Ym');
$result = $this->service->upload($subDir);
$data = $result->toArray();
$data['url'] = $data['base_path'] ?? '';
return Json(['errno' => 0, 'data' => $data]);
} catch (\Exception $e) {
return Json([
'errno' => 1,
'message' => $e->getMessage(),
]);
}
}
}