Skip to content

Commit aecf576

Browse files
committed
Merge pull request 'feat(recycle-bin): 实现回收站功能并添加软删除支持' (#6) from huangruihao/magic:feat-回收站v1重建-0319 into master
Reviewed-on: https://git.dtyq.com/magic/magic/pulls/6
2 parents b1fe3de + 9d18ca2 commit aecf576

File tree

48 files changed

+4282
-21
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4282
-21
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
use App\Interfaces\Middleware\Auth\UserAuthMiddleware;
8+
use Dtyq\SuperMagic\Interfaces\RecycleBin\RecycleBinApi;
9+
use Hyperf\HttpServer\Router\Router;
10+
11+
Router::addGroup(
12+
'/api/v1/recycle-bin',
13+
static function () {
14+
// 获取回收站列表
15+
Router::get('/list', [RecycleBinApi::class, 'getRecycleBinList']);
16+
17+
// 检查父级是否存在
18+
Router::post('/check', [RecycleBinApi::class, 'checkParent']);
19+
20+
// 恢复资源
21+
Router::post('/restore', [RecycleBinApi::class, 'restore']);
22+
23+
// 彻底删除(永久删除)
24+
Router::post('/permanent-delete', [RecycleBinApi::class, 'permanentDelete']);
25+
26+
// 移动回收站中的项目(移动+恢复)
27+
Router::post('/move-project', [RecycleBinApi::class, 'moveProject']);
28+
29+
// 批量移动回收站中的项目(批量移动+恢复)
30+
Router::post('/batch-move-project', [RecycleBinApi::class, 'batchMoveProject']);
31+
32+
// 移动回收站中的话题(移动+恢复)
33+
Router::post('/move-topic', [RecycleBinApi::class, 'moveTopic']);
34+
35+
// 批量移动回收站中的话题(批量移动+恢复)
36+
Router::post('/batch-move-topic', [RecycleBinApi::class, 'batchMoveTopic']);
37+
},
38+
['middleware' => [UserAuthMiddleware::class]]
39+
);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
use Hyperf\Database\Migrations\Migration;
8+
use Hyperf\Database\Schema\Blueprint;
9+
use Hyperf\Database\Schema\Schema;
10+
use Hyperf\DbConnection\Db;
11+
12+
return new class extends Migration {
13+
/**
14+
* Run the migrations.
15+
*/
16+
public function up(): void
17+
{
18+
if (Schema::hasTable('magic_recycle_bin')) {
19+
return;
20+
}
21+
22+
Schema::create('magic_recycle_bin', function (Blueprint $table) {
23+
// 主键
24+
$table->bigIncrements('id');
25+
26+
// 资源标识
27+
$table->tinyInteger('resource_type')->unsigned()->comment('资源类型:1=workspace,2=project,3=topic,4=file');
28+
$table->unsignedBigInteger('resource_id')->comment('资源ID,对应各业务表主键');
29+
$table->string('resource_name', 255)->default('')->comment('资源名称,删除时快照');
30+
31+
// 所有权与操作人(与工作区/项目/话题/文件表 user_id 一致,用 string)
32+
$table->string('owner_id', 128)->comment('资源创建者ID');
33+
$table->string('deleted_by', 128)->comment('删除人ID');
34+
35+
// 删除时间与有效期(deleted_at 由业务写入)
36+
$table->timestamp('deleted_at')->comment('删除时间');
37+
$table->unsignedInteger('retain_days')->default(30)->comment('有效期(天)');
38+
39+
// 父级关联(用于恢复时排除用户曾删的子资源)
40+
$table->unsignedBigInteger('parent_id')->nullable()->comment('父级资源ID:file/topic=project_id,project=workspace_id,workspace=NULL');
41+
42+
// 扩展信息(可选,列表也可从原表JOIN)
43+
$table->json('extra_data')->nullable()->comment('扩展信息:父级名称等');
44+
45+
// 审计字段(与项目内 2025_06_23 project、2025_08_27 operation_logs 等保持一致)
46+
$table->timestamp('created_at')->nullable()->comment('创建时间');
47+
$table->timestamp('updated_at')->nullable()->comment('更新时间');
48+
49+
// 第一阶段仅主键,不建业务索引;后续若有性能需求再补 idx_owner_deleted、idx_deleted_at
50+
});
51+
52+
// 添加表注释
53+
Db::statement("ALTER TABLE magic_recycle_bin COMMENT '回收站统一表'");
54+
}
55+
56+
/**
57+
* Reverse the migrations.
58+
*/
59+
public function down(): void
60+
{
61+
Schema::dropIfExists('magic_recycle_bin');
62+
}
63+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
use Hyperf\Database\Migrations\Migration;
8+
use Hyperf\Database\Schema\Blueprint;
9+
use Hyperf\Database\Schema\Schema;
10+
11+
return new class extends Migration {
12+
public function up(): void
13+
{
14+
Schema::table('magic_super_agent_project_members', function (Blueprint $table) {
15+
$table->softDeletes()->comment('软删除时间');
16+
});
17+
}
18+
19+
public function down(): void
20+
{
21+
Schema::table('magic_super_agent_project_members', function (Blueprint $table) {
22+
$table->dropSoftDeletes();
23+
});
24+
}
25+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
8+
namespace Dtyq\SuperMagic\Application\RecycleBin\Command;
9+
10+
use App\Infrastructure\Core\Traits\HasLogger;
11+
use Dtyq\SuperMagic\Application\RecycleBin\Crontab\RecycleBinCleanupCrontab;
12+
use Hyperf\Command\Annotation\Command;
13+
use Hyperf\Command\Command as HyperfCommand;
14+
use Throwable;
15+
16+
/**
17+
* 回收站清理命令(手动触发定时任务).
18+
*/
19+
#[Command]
20+
class RecycleBinCleanupCommand extends HyperfCommand
21+
{
22+
use HasLogger;
23+
24+
public function __construct(
25+
protected RecycleBinCleanupCrontab $recycleBinCleanupCrontab,
26+
) {
27+
parent::__construct('recycle-bin:cleanup');
28+
}
29+
30+
public function configure(): void
31+
{
32+
parent::configure();
33+
$this->setDescription('手动执行回收站过期记录清理任务(测试用)');
34+
}
35+
36+
public function handle(): void
37+
{
38+
$this->info('');
39+
$this->info('🗑️ 回收站过期记录清理任务');
40+
$this->info('==========================================');
41+
$this->info('');
42+
43+
$this->info('🚀 开始执行清理任务...');
44+
$this->info('');
45+
46+
$startTime = microtime(true);
47+
48+
try {
49+
$this->recycleBinCleanupCrontab->execute();
50+
51+
$elapsedTime = round((microtime(true) - $startTime) * 1000, 2);
52+
53+
$this->info('');
54+
$this->info("✅ 清理任务执行完成!耗时:{$elapsedTime}ms");
55+
$this->info('');
56+
$this->info('💡 提示:请查看日志文件获取详细执行结果');
57+
} catch (Throwable $e) {
58+
$this->error('');
59+
$this->error('❌ 清理任务执行失败!');
60+
$this->error('错误信息:' . $e->getMessage());
61+
$this->error('文件:' . $e->getFile() . ':' . $e->getLine());
62+
$this->error('');
63+
}
64+
}
65+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
8+
namespace Dtyq\SuperMagic\Application\RecycleBin\Crontab;
9+
10+
use App\Infrastructure\Core\Traits\HasLogger;
11+
use Dtyq\SuperMagic\Domain\RecycleBin\Service\RecycleBinDomainService;
12+
use Hyperf\Crontab\Annotation\Crontab;
13+
use Throwable;
14+
15+
/**
16+
* 回收站过期记录清理定时任务.
17+
* 每天凌晨3点执行,自动清理超过保留期的回收站记录.
18+
*/
19+
#[Crontab(
20+
rule: '0 3 * * *',
21+
name: 'RecycleBinCleanupCrontab',
22+
singleton: true,
23+
mutexExpires: 3600,
24+
onOneServer: true,
25+
callback: 'execute',
26+
memo: '回收站过期记录清理定时任务'
27+
)]
28+
readonly class RecycleBinCleanupCrontab
29+
{
30+
use HasLogger;
31+
32+
/**
33+
* 单批次处理的记录数量上限.
34+
*/
35+
private const BATCH_SIZE = 1000;
36+
37+
public function __construct(
38+
private RecycleBinDomainService $recycleBinDomainService
39+
) {
40+
}
41+
42+
/**
43+
* 执行清理任务.
44+
*/
45+
public function execute(): void
46+
{
47+
$startTime = microtime(true);
48+
$this->logger->info('回收站过期记录清理任务开始');
49+
50+
try {
51+
// 查询过期记录 ID
52+
$expiredIds = $this->recycleBinDomainService->findExpiredRecordIds(self::BATCH_SIZE);
53+
54+
if (empty($expiredIds)) {
55+
$this->logger->info('回收站过期记录清理任务完成:无过期记录');
56+
return;
57+
}
58+
59+
// 调用系统彻底删除(不校验用户权限)
60+
$result = $this->recycleBinDomainService->permanentDeleteBySystem($expiredIds);
61+
62+
$successCount = count($expiredIds) - count($result['failed']);
63+
$failedCount = count($result['failed']);
64+
$elapsedTime = round((microtime(true) - $startTime) * 1000, 2);
65+
66+
$this->logger->info('回收站过期记录清理任务完成', [
67+
'total' => count($expiredIds),
68+
'success' => $successCount,
69+
'failed' => $failedCount,
70+
'elapsed_ms' => $elapsedTime,
71+
]);
72+
73+
// 如果有失败记录,记录详细信息
74+
if ($failedCount > 0) {
75+
$this->logger->warning('回收站过期记录清理部分失败', [
76+
'failed_items' => $result['failed'],
77+
]);
78+
}
79+
} catch (Throwable $e) {
80+
$this->logger->error('回收站过期记录清理任务失败', [
81+
'error' => $e->getMessage(),
82+
'file' => $e->getFile(),
83+
'line' => $e->getLine(),
84+
'trace' => $e->getTraceAsString(),
85+
]);
86+
}
87+
}
88+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
8+
namespace Dtyq\SuperMagic\Application\RecycleBin\DTO;
9+
10+
use App\Infrastructure\Core\AbstractRequestDTO;
11+
12+
/**
13+
* 批量移动回收站项目请求 DTO.
14+
*
15+
* 参考现有 BatchMoveProjectsRequestDTO 设计.
16+
*/
17+
class BatchMoveProjectInRecycleBinRequestDTO extends AbstractRequestDTO
18+
{
19+
/**
20+
* 项目ID数组(字段名与现有批量接口保持一致).
21+
*/
22+
public array $projectIds = [];
23+
24+
/**
25+
* 目标工作区ID(空字符串表示"无工作区").
26+
*/
27+
public string $targetWorkspaceId = '';
28+
29+
/**
30+
* 获取项目ID数组.
31+
*
32+
* @return array<string> 返回字符串数组(雪花ID保持字符串)
33+
*/
34+
public function getProjectIds(): array
35+
{
36+
return $this->projectIds;
37+
}
38+
39+
/**
40+
* 获取整型项目ID数组(供内部使用).
41+
*
42+
* @return array<int>
43+
*/
44+
public function getProjectIdsAsInt(): array
45+
{
46+
return array_map('intval', $this->projectIds);
47+
}
48+
49+
/**
50+
* 获取目标工作区ID.
51+
*/
52+
public function getTargetWorkspaceId(): ?int
53+
{
54+
if ($this->targetWorkspaceId === '') {
55+
return null;
56+
}
57+
return (int) $this->targetWorkspaceId;
58+
}
59+
60+
/**
61+
* 检查是否移动到"无工作区".
62+
*/
63+
public function isMovingToNoWorkspace(): bool
64+
{
65+
return $this->targetWorkspaceId === '';
66+
}
67+
68+
/**
69+
* 获取验证规则.
70+
*/
71+
protected static function getHyperfValidationRules(): array
72+
{
73+
return [
74+
'project_ids' => 'required|array|min:1|max:20',
75+
'project_ids.*' => 'required|string',
76+
'target_workspace_id' => 'present|string|max:64',
77+
];
78+
}
79+
80+
/**
81+
* 获取自定义错误消息.
82+
*/
83+
protected static function getHyperfValidationMessage(): array
84+
{
85+
return [
86+
'project_ids.required' => '项目ID列表不能为空',
87+
'project_ids.array' => '项目ID必须是数组',
88+
'project_ids.min' => '至少需要选择一个项目',
89+
'project_ids.max' => '最多只能同时移动20个项目',
90+
'project_ids.*.required' => '每个项目ID不能为空',
91+
'project_ids.*.string' => '每个项目ID必须是字符串',
92+
'target_workspace_id.present' => '目标工作区ID字段必填',
93+
'target_workspace_id.string' => '目标工作区ID必须是字符串',
94+
'target_workspace_id.max' => '目标工作区ID不能超过64个字符',
95+
];
96+
}
97+
}

0 commit comments

Comments
 (0)