feat(api): 添加图片管理、回收站和系统设置功能
- 在 Grouping 模型和控制器中添加删除和更新方法 - 实现完整的图片上传、删除、预览和批量操作功能 - 添加图片清理功能用于删除冗余图片文件 - 实现 Markdown 文件按分组查询、搜索和标题更新功能 - 添加回收站功能支持软删除和恢复操作 - 实现系统设置和注册码生成功能 - 配置新的 API 路由包括图片、系统和回收站相关接口 - 更新前端开发服务器代理地址从 8084 到 80 端口
This commit is contained in:
@@ -4,6 +4,10 @@ use Slim\Routing\RouteCollectorProxy;
|
|||||||
use App\Controllers\UserController;
|
use App\Controllers\UserController;
|
||||||
use App\Controllers\MarkdownController;
|
use App\Controllers\MarkdownController;
|
||||||
use App\Controllers\GroupingController;
|
use App\Controllers\GroupingController;
|
||||||
|
use App\Controllers\ImageController;
|
||||||
|
use App\Controllers\SystemController;
|
||||||
|
use App\Controllers\TrashController;
|
||||||
|
use App\Controllers\ImageCleanupController;
|
||||||
use App\Middleware\AuthMiddleware;
|
use App\Middleware\AuthMiddleware;
|
||||||
|
|
||||||
// 根路径测试路由
|
// 根路径测试路由
|
||||||
@@ -37,15 +41,19 @@ $app->group('/api/user', function (RouteCollectorProxy $group) {
|
|||||||
$group->delete('/deleteUser', [UserController::class, 'deleteUser']);
|
$group->delete('/deleteUser', [UserController::class, 'deleteUser']);
|
||||||
})->add(new AuthMiddleware());
|
})->add(new AuthMiddleware());
|
||||||
|
|
||||||
// Markdown 相关路由
|
// Markdown 相关路由(无需认证 - 公开阅读)
|
||||||
$app->group('/api/markdown', function (RouteCollectorProxy $group) {
|
$app->group('/api/markdown', function (RouteCollectorProxy $group) {
|
||||||
$group->get('/{id}', [MarkdownController::class, 'getById']);
|
$group->get('/{id}', [MarkdownController::class, 'getById']);
|
||||||
$group->get('', [MarkdownController::class, 'getAll']);
|
$group->get('', [MarkdownController::class, 'getAll']);
|
||||||
|
$group->get('/search', [MarkdownController::class, 'search']);
|
||||||
|
$group->get('/grouping/{groupingId}', [MarkdownController::class, 'getByGroupingId']);
|
||||||
|
$group->get('/recent', [MarkdownController::class, 'getRecent']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Markdown 相关路由(需要认证)
|
// Markdown 相关路由(需要认证)
|
||||||
$app->group('/api/markdown', function (RouteCollectorProxy $group) {
|
$app->group('/api/markdown', function (RouteCollectorProxy $group) {
|
||||||
$group->post('/updateMarkdown', [MarkdownController::class, 'update']);
|
$group->post('/updateMarkdown', [MarkdownController::class, 'update']);
|
||||||
|
$group->post('/{id}/title', [MarkdownController::class, 'updateTitle']);
|
||||||
$group->delete('/{id}', [MarkdownController::class, 'delete']);
|
$group->delete('/{id}', [MarkdownController::class, 'delete']);
|
||||||
})->add(new AuthMiddleware());
|
})->add(new AuthMiddleware());
|
||||||
|
|
||||||
@@ -60,3 +68,40 @@ $app->group('/api/groupings', function (RouteCollectorProxy $group) {
|
|||||||
$group->put('/{id}', [GroupingController::class, 'update']);
|
$group->put('/{id}', [GroupingController::class, 'update']);
|
||||||
$group->delete('/{id}', [GroupingController::class, 'delete']);
|
$group->delete('/{id}', [GroupingController::class, 'delete']);
|
||||||
})->add(new AuthMiddleware());
|
})->add(new AuthMiddleware());
|
||||||
|
|
||||||
|
// 图片相关路由(无需认证 - 公开预览)
|
||||||
|
$app->group('/api/images', function (RouteCollectorProxy $group) {
|
||||||
|
$group->get('/preview/{url}', [ImageController::class, 'preview']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图片相关路由(需要认证)
|
||||||
|
$app->group('/api/images', function (RouteCollectorProxy $group) {
|
||||||
|
$group->post('', [ImageController::class, 'upload']);
|
||||||
|
$group->post('/{id}', [ImageController::class, 'delete']);
|
||||||
|
$group->post('/deleteByUrl', [ImageController::class, 'deleteByUrl']);
|
||||||
|
$group->post('/batch', [ImageController::class, 'batchDelete']);
|
||||||
|
})->add(new AuthMiddleware());
|
||||||
|
|
||||||
|
// 系统设置相关路由(无需认证 - 公开查询注册状态)
|
||||||
|
$app->group('/api/system', function (RouteCollectorProxy $group) {
|
||||||
|
$group->get('/registration/status', [SystemController::class, 'getRegistrationStatus']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 系统设置相关路由(需要认证)
|
||||||
|
$app->group('/api/system', function (RouteCollectorProxy $group) {
|
||||||
|
$group->post('/registration/toggle', [SystemController::class, 'toggleRegistration']);
|
||||||
|
$group->post('/registration/generate-code', [SystemController::class, 'generateRegistrationCode']);
|
||||||
|
})->add(new AuthMiddleware());
|
||||||
|
|
||||||
|
// 回收站相关路由(需要认证)
|
||||||
|
$app->group('/api/trash', function (RouteCollectorProxy $group) {
|
||||||
|
$group->get('', [TrashController::class, 'getTrashItems']);
|
||||||
|
$group->post('/restore/{type}/{id}', [TrashController::class, 'restoreItem']);
|
||||||
|
$group->delete('/permanently/{type}/{id}', [TrashController::class, 'permanentlyDeleteItem']);
|
||||||
|
$group->delete('/clean', [TrashController::class, 'cleanTrash']);
|
||||||
|
})->add(new AuthMiddleware());
|
||||||
|
|
||||||
|
// 管理员相关路由(需要认证 - 图片清理)
|
||||||
|
$app->group('/api/admin', function (RouteCollectorProxy $group) {
|
||||||
|
$group->post('/cleanup-images', [ImageCleanupController::class, 'cleanupImages']);
|
||||||
|
})->add(new AuthMiddleware());
|
||||||
|
|||||||
@@ -39,4 +39,34 @@ class GroupingController
|
|||||||
$response->getBody()->write(json_encode(ApiResponse::success($created), JSON_UNESCAPED_UNICODE));
|
$response->getBody()->write(json_encode(ApiResponse::success($created), JSON_UNESCAPED_UNICODE));
|
||||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
$grouping = $data['grouping'] ?? '';
|
||||||
|
|
||||||
|
if (empty($grouping)) {
|
||||||
|
$response->getBody()->write(json_encode(ApiResponse::fail('分组名称不能为空'), JSON_UNESCAPED_UNICODE));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new Grouping();
|
||||||
|
$model->update($id, $grouping);
|
||||||
|
$updated = $model->findById($id);
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode(ApiResponse::success($updated), JSON_UNESCAPED_UNICODE));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
$model = new Grouping();
|
||||||
|
$model->delete($id);
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode(ApiResponse::success(null, '删除成功'), JSON_UNESCAPED_UNICODE));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
biji-php/src/Controllers/ImageCleanupController.php
Normal file
29
biji-php/src/Controllers/ImageCleanupController.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use App\Models\ImageCleanup;
|
||||||
|
use App\Utils\Response as ApiResponse;
|
||||||
|
|
||||||
|
class ImageCleanupController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 手动触发清理冗余图片(需要管理员权限)
|
||||||
|
*/
|
||||||
|
public function cleanupImages(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$model = new ImageCleanup();
|
||||||
|
$deletedCount = $model->cleanupRedundantImages();
|
||||||
|
|
||||||
|
$message = "成功清理 {$deletedCount} 个冗余图片";
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($message)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('清理失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
biji-php/src/Controllers/ImageController.php
Normal file
212
biji-php/src/Controllers/ImageController.php
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use App\Models\Image;
|
||||||
|
use App\Utils\Response as ApiResponse;
|
||||||
|
|
||||||
|
class ImageController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 上传图片
|
||||||
|
*/
|
||||||
|
public function upload(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$uploadedFiles = $request->getUploadedFiles();
|
||||||
|
$file = $uploadedFiles['file'] ?? null;
|
||||||
|
|
||||||
|
if (!$file || $file->getError() !== UPLOAD_ERR_OK) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('文件上传失败')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadDir = $_ENV['UPLOAD_DIR'] ?? '../uploads';
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $file->getClientFilename();
|
||||||
|
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||||
|
$storedName = uniqid() . '.' . $extension;
|
||||||
|
$filePath = $uploadDir . '/' . $storedName;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$file->moveTo($filePath);
|
||||||
|
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
$markdownId = $params['markdownId'] ?? null;
|
||||||
|
|
||||||
|
$model = new Image();
|
||||||
|
$imageId = $model->create(
|
||||||
|
$originalName,
|
||||||
|
$storedName,
|
||||||
|
$storedName,
|
||||||
|
$file->getSize(),
|
||||||
|
$file->getClientMediaType(),
|
||||||
|
$markdownId
|
||||||
|
);
|
||||||
|
|
||||||
|
$image = $model->findById($imageId);
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($image)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('上传失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID删除图片
|
||||||
|
*/
|
||||||
|
public function delete(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$uploadDir = $_ENV['UPLOAD_DIR'] ?? '../uploads';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$model = new Image();
|
||||||
|
$image = $model->findById($id);
|
||||||
|
|
||||||
|
if (!$image) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('图片不存在')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除物理文件
|
||||||
|
$filePath = $uploadDir . '/' . $image['stored_name'];
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除数据库记录
|
||||||
|
$model->delete($id);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '删除成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('删除失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线预览图片
|
||||||
|
*/
|
||||||
|
public function preview(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$url = $args['url'];
|
||||||
|
$uploadDir = $_ENV['UPLOAD_DIR'] ?? '../uploads';
|
||||||
|
$filePath = $uploadDir . '/' . $url;
|
||||||
|
|
||||||
|
if (empty($url) || !file_exists($filePath)) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('文件不存在')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件 MIME 类型
|
||||||
|
$contentType = $this->getContentTypeFromFileExtension($url);
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
$fileContent = file_get_contents($filePath);
|
||||||
|
$fileSize = filesize($filePath);
|
||||||
|
|
||||||
|
$response->getBody()->write($fileContent);
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', $contentType)
|
||||||
|
->withHeader('Content-Length', (string)$fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据URL删除图片
|
||||||
|
*/
|
||||||
|
public function deleteByUrl(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
$url = $data['url'] ?? '';
|
||||||
|
$uploadDir = $_ENV['UPLOAD_DIR'] ?? '../uploads';
|
||||||
|
|
||||||
|
if (empty($url)) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('URL不能为空')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 删除物理文件
|
||||||
|
$filePath = $uploadDir . '/' . $url;
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除数据库记录
|
||||||
|
$model = new Image();
|
||||||
|
$model->deleteByUrl($url);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '删除成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('删除失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除图片
|
||||||
|
*/
|
||||||
|
public function batchDelete(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
$urls = $data['urls'] ?? [];
|
||||||
|
$uploadDir = $_ENV['UPLOAD_DIR'] ?? '../uploads';
|
||||||
|
|
||||||
|
if (empty($urls) || !is_array($urls)) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('URL列表不能为空')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 删除物理文件
|
||||||
|
foreach ($urls as $url) {
|
||||||
|
$filePath = $uploadDir . '/' . $url;
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除数据库记录
|
||||||
|
$model = new Image();
|
||||||
|
$model->deleteByUrls($urls);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '批量删除成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('批量删除失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件扩展名获取 MIME 类型
|
||||||
|
*/
|
||||||
|
private function getContentTypeFromFileExtension($fileName)
|
||||||
|
{
|
||||||
|
if (empty($fileName) || strpos($fileName, '.') === false) {
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
$mimeTypes = [
|
||||||
|
'jpg' => 'image/jpeg',
|
||||||
|
'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
'bmp' => 'image/bmp',
|
||||||
|
'webp' => 'image/webp',
|
||||||
|
'svg' => 'image/svg+xml'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $mimeTypes[$extension] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,4 +73,59 @@ class MarkdownController
|
|||||||
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '删除成功')));
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '删除成功')));
|
||||||
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getByGroupingId(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$groupingId = $args['groupingId'];
|
||||||
|
$model = new MarkdownFile();
|
||||||
|
$files = $model->getByGroupingId($groupingId);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($files)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$params = $request->getQueryParams();
|
||||||
|
$keyword = $params['keyword'] ?? '';
|
||||||
|
|
||||||
|
$model = new MarkdownFile();
|
||||||
|
$files = $model->searchByTitle($keyword);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($files)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecent(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$model = new MarkdownFile();
|
||||||
|
$files = $model->getRecent(12);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($files)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateTitle(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
$title = $data['title'] ?? '';
|
||||||
|
|
||||||
|
if (empty($title)) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('标题不能为空')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new MarkdownFile();
|
||||||
|
$model->updateTitle($id, $title);
|
||||||
|
$file = $model->findById($id);
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('文件未找到或更新失败')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($file)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
biji-php/src/Controllers/SystemController.php
Normal file
58
biji-php/src/Controllers/SystemController.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use App\Models\SystemSetting;
|
||||||
|
use App\Models\RegistrationCode;
|
||||||
|
use App\Utils\Response as ApiResponse;
|
||||||
|
|
||||||
|
class SystemController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取注册功能状态
|
||||||
|
*/
|
||||||
|
public function getRegistrationStatus(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$model = new SystemSetting();
|
||||||
|
$enabled = $model->isRegistrationEnabled();
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($enabled)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换注册功能状态(需要认证)
|
||||||
|
*/
|
||||||
|
public function toggleRegistration(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
$enabled = $data['enabled'] ?? false;
|
||||||
|
|
||||||
|
$model = new SystemSetting();
|
||||||
|
$model->setRegistrationEnabled($enabled);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '设置成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成注册码(需要认证)
|
||||||
|
*/
|
||||||
|
public function generateRegistrationCode(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$userId = $request->getAttribute('userId');
|
||||||
|
|
||||||
|
if (!$userId) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::fail('未授权')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new RegistrationCode();
|
||||||
|
$code = $model->generateCode($userId);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($code)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
80
biji-php/src/Controllers/TrashController.php
Normal file
80
biji-php/src/Controllers/TrashController.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use App\Models\Trash;
|
||||||
|
use App\Utils\Response as ApiResponse;
|
||||||
|
|
||||||
|
class TrashController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取回收站列表
|
||||||
|
*/
|
||||||
|
public function getTrashItems(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$model = new Trash();
|
||||||
|
$items = $model->getTrashItems();
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success($items)));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复项目
|
||||||
|
*/
|
||||||
|
public function restoreItem(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$type = $args['type'];
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$model = new Trash();
|
||||||
|
$model->restoreItem($id, $type);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '恢复成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('恢复失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 永久删除项目
|
||||||
|
*/
|
||||||
|
public function permanentlyDeleteItem(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$type = $args['type'];
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$model = new Trash();
|
||||||
|
$model->permanentlyDeleteItem($id, $type);
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '永久删除成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('删除失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空回收站
|
||||||
|
*/
|
||||||
|
public function cleanTrash(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$model = new Trash();
|
||||||
|
$model->cleanTrash();
|
||||||
|
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::success(null, '清空成功')));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(ApiResponse::json(ApiResponse::error('清空失败: ' . $e->getMessage())));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json; charset=utf-8')->withStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,14 @@ class Grouping
|
|||||||
return $stmt->execute([$grouping, $id]);
|
return $stmt->execute([$grouping, $id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"UPDATE `grouping` SET is_deleted = 1, deleted_at = NOW() WHERE id = ?"
|
||||||
|
);
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
public function softDelete($id, $userId)
|
public function softDelete($id, $userId)
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
|
|||||||
59
biji-php/src/Models/Image.php
Normal file
59
biji-php/src/Models/Image.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Utils\Database;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class Image
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = Database::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create($originalName, $storedName, $url, $size, $contentType, $markdownId = null)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"INSERT INTO image (original_name, stored_name, url, size, content_type, markdown_id, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, NOW())"
|
||||||
|
);
|
||||||
|
$stmt->execute([$originalName, $storedName, $url, $size, $contentType, $markdownId]);
|
||||||
|
return $this->db->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findById($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM image WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUrl($url)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM image WHERE url = ?");
|
||||||
|
$stmt->execute([$url]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM image WHERE id = ?");
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteByUrl($url)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM image WHERE url = ?");
|
||||||
|
return $stmt->execute([$url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteByUrls($urls)
|
||||||
|
{
|
||||||
|
$placeholders = str_repeat('?,', count($urls) - 1) . '?';
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM image WHERE url IN ($placeholders)");
|
||||||
|
return $stmt->execute($urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
biji-php/src/Models/ImageCleanup.php
Normal file
69
biji-php/src/Models/ImageCleanup.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Utils\Database;
|
||||||
|
|
||||||
|
class ImageCleanup
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = Database::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理冗余图片
|
||||||
|
* 删除数据库中存在但文件系统中不存在的图片记录
|
||||||
|
* 以及文件系统中存在但数据库中没有记录的图片文件
|
||||||
|
*/
|
||||||
|
public function cleanupRedundantImages()
|
||||||
|
{
|
||||||
|
$uploadDir = $_ENV['UPLOAD_DIR'] ?? '../uploads';
|
||||||
|
$deletedCount = 0;
|
||||||
|
|
||||||
|
// 1. 获取数据库中所有图片记录
|
||||||
|
$stmt = $this->db->prepare("SELECT id, stored_name FROM image");
|
||||||
|
$stmt->execute();
|
||||||
|
$dbImages = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$dbImageFiles = [];
|
||||||
|
foreach ($dbImages as $image) {
|
||||||
|
$dbImageFiles[$image['stored_name']] = $image['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取文件系统中所有图片文件
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = scandir($uploadDir);
|
||||||
|
$fsImages = array_filter($files, function($file) use ($uploadDir) {
|
||||||
|
return is_file($uploadDir . '/' . $file) && $file !== '.' && $file !== '..';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 删除数据库中存在但文件系统中不存在的记录
|
||||||
|
foreach ($dbImageFiles as $fileName => $imageId) {
|
||||||
|
$filePath = $uploadDir . '/' . $fileName;
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM image WHERE id = ?");
|
||||||
|
$stmt->execute([$imageId]);
|
||||||
|
$deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 删除文件系统中存在但数据库中没有记录的文件
|
||||||
|
foreach ($fsImages as $file) {
|
||||||
|
if (!isset($dbImageFiles[$file])) {
|
||||||
|
$filePath = $uploadDir . '/' . $file;
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
$deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deletedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,14 @@ class MarkdownFile
|
|||||||
return $stmt->execute($values);
|
return $stmt->execute($values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateTitle($id, $title)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"UPDATE markdown_file SET title = ?, updated_at = NOW() WHERE id = ?"
|
||||||
|
);
|
||||||
|
return $stmt->execute([$title, $id]);
|
||||||
|
}
|
||||||
|
|
||||||
public function softDelete($id, $userId)
|
public function softDelete($id, $userId)
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
|
|||||||
117
biji-php/src/Models/Trash.php
Normal file
117
biji-php/src/Models/Trash.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Utils\Database;
|
||||||
|
|
||||||
|
class Trash
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = Database::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回收站列表
|
||||||
|
*/
|
||||||
|
public function getTrashItems()
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
// 获取已删除的 Markdown 文件
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
'markdown' as type,
|
||||||
|
deleted_at,
|
||||||
|
deleted_by
|
||||||
|
FROM markdown_file
|
||||||
|
WHERE is_deleted = 1
|
||||||
|
ORDER BY deleted_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$markdownItems = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 获取已删除的分组
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
`grouping` as title,
|
||||||
|
'grouping' as type,
|
||||||
|
deleted_at,
|
||||||
|
deleted_by
|
||||||
|
FROM `grouping`
|
||||||
|
WHERE is_deleted = 1
|
||||||
|
ORDER BY deleted_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$groupingItems = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 合并并按删除时间排序
|
||||||
|
$items = array_merge($markdownItems, $groupingItems);
|
||||||
|
usort($items, function($a, $b) {
|
||||||
|
return strtotime($b['deleted_at']) - strtotime($a['deleted_at']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复项目
|
||||||
|
*/
|
||||||
|
public function restoreItem($id, $type)
|
||||||
|
{
|
||||||
|
if ($type === 'markdown') {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
UPDATE markdown_file
|
||||||
|
SET is_deleted = 0, deleted_at = NULL, deleted_by = NULL
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
} elseif ($type === 'grouping') {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
UPDATE `grouping`
|
||||||
|
SET is_deleted = 0, deleted_at = NULL, deleted_by = NULL
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
} else {
|
||||||
|
throw new \Exception('不支持的类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 永久删除项目
|
||||||
|
*/
|
||||||
|
public function permanentlyDeleteItem($id, $type)
|
||||||
|
{
|
||||||
|
if ($type === 'markdown') {
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM markdown_file WHERE id = ?");
|
||||||
|
} elseif ($type === 'grouping') {
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM `grouping` WHERE id = ?");
|
||||||
|
} else {
|
||||||
|
throw new \Exception('不支持的类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空回收站
|
||||||
|
*/
|
||||||
|
public function cleanTrash()
|
||||||
|
{
|
||||||
|
// 删除所有已标记删除的 Markdown 文件
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM markdown_file WHERE is_deleted = 1");
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// 删除所有已标记删除的分组
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM `grouping` WHERE is_deleted = 1");
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8084',
|
target: 'http://127.0.0.1:80',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: path => path.replace(/^\/api/, '') //去掉 api
|
rewrite: path => path.replace(/^\/api/, '') //去掉 api
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user