diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 4dfb44282..1527cbb22 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -30,6 +30,7 @@ use App\Models\WebSocketDialog; use App\Models\UserCheckinRecord; use App\Models\WebSocketDialogMsg; use App\Models\UserTaskBrowse; +use App\Models\UserFavorite; use Illuminate\Support\Facades\DB; use App\Models\UserEmailVerification; use App\Module\AgoraIO\AgoraTokenGenerator; @@ -2825,4 +2826,170 @@ class UsersController extends AbstractController // return Base::retSuccess('清理完成', ['deleted_count' => $deletedCount]); } + + /** + * @api {get} api/users/favorites 46. 获取用户收藏列表 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName favorites + * + * @apiParam {String} [type] 收藏类型过滤 (task/project/file) + * @apiParam {Number} [page=1] 页码 + * @apiParam {Number} [pagesize=20] 每页数量 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function favorites() + { + $user = User::auth(); + // + $type = Request::input('type'); + $page = intval(Request::input('page', 1)); + $pageSize = min(intval(Request::input('pagesize', 20)), 100); + // + // 验证收藏类型 + $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; + if ($type && !in_array($type, $allowedTypes)) { + return Base::retError('无效的收藏类型'); + } + // + $result = UserFavorite::getUserFavorites($user->userid, $type, $page, $pageSize); + // + return Base::retSuccess('success', $result); + } + + /** + * @api {post} api/users/favorite/toggle 47. 切换收藏状态 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName favorite__toggle + * + * @apiParam {String} type 收藏类型 (task/project/file) + * @apiParam {Number} id 收藏对象ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function favorite__toggle() + { + $user = User::auth(); + // + $type = trim(Request::input('type')); + $id = intval(Request::input('id')); + // + if (!$type || $id <= 0) { + return Base::retError('参数错误'); + } + // + // 验证收藏类型 + $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; + if (!in_array($type, $allowedTypes)) { + return Base::retError('无效的收藏类型'); + } + // + // 验证对象是否存在(简化验证,实际应该加上权限检查) + switch ($type) { + case UserFavorite::TYPE_TASK: + $object = ProjectTask::whereId($id)->first(); + if (!$object) { + return Base::retError('任务不存在'); + } + break; + case UserFavorite::TYPE_PROJECT: + $object = Project::whereId($id)->first(); + if (!$object) { + return Base::retError('项目不存在'); + } + break; + case UserFavorite::TYPE_FILE: + $object = File::whereId($id)->first(); + if (!$object) { + return Base::retError('文件不存在'); + } + break; + } + // + $result = UserFavorite::toggleFavorite($user->userid, $type, $id); + // + $message = $result['favorited'] ? '收藏成功' : '取消收藏成功'; + return Base::retSuccess($message, $result); + } + + /** + * @api {post} api/users/favorites/clean 48. 清理用户收藏 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName favorites__clean + * + * @apiParam {String} [type] 收藏类型 (task/project/file),不传则清理全部 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function favorites__clean() + { + $user = User::auth(); + // + $type = trim(Request::input('type')); + // + // 验证收藏类型 + if ($type) { + $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; + if (!in_array($type, $allowedTypes)) { + return Base::retError('无效的收藏类型'); + } + } + // + $deletedCount = UserFavorite::cleanUserFavorites($user->userid, $type); + // + $message = $type ? "清理{$type}收藏成功" : '清理全部收藏成功'; + return Base::retSuccess($message, ['deleted_count' => $deletedCount]); + } + + /** + * @api {get} api/users/favorite/check 49. 检查收藏状态 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName favorite__check + * + * @apiParam {String} type 收藏类型 (task/project/file) + * @apiParam {Number} id 收藏对象ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function favorite__check() + { + $user = User::auth(); + // + $type = trim(Request::input('type')); + $id = intval(Request::input('id')); + // + if (!$type || $id <= 0) { + return Base::retError('参数错误'); + } + // + // 验证收藏类型 + $allowedTypes = [UserFavorite::TYPE_TASK, UserFavorite::TYPE_PROJECT, UserFavorite::TYPE_FILE]; + if (!in_array($type, $allowedTypes)) { + return Base::retError('无效的收藏类型'); + } + // + $isFavorited = UserFavorite::isFavorited($user->userid, $type, $id); + // + return Base::retSuccess('success', ['favorited' => $isFavorited]); + } } diff --git a/app/Models/UserFavorite.php b/app/Models/UserFavorite.php new file mode 100644 index 000000000..4fe06e826 --- /dev/null +++ b/app/Models/UserFavorite.php @@ -0,0 +1,256 @@ +belongsTo(User::class, 'userid', 'userid'); + } + + /** + * 多态关联 + */ + public function favoritable() + { + return $this->morphTo(); + } + + /** + * 切换收藏状态 + * @param int $userid 用户ID + * @param string $type 收藏类型 + * @param int $id 收藏对象ID + * @return array ['favorited' => bool, 'action' => 'added'|'removed'] + */ + public static function toggleFavorite($userid, $type, $id) + { + $favorite = self::whereUserid($userid) + ->whereFavoritableType($type) + ->whereFavoritableId($id) + ->first(); + + if ($favorite) { + // 取消收藏 + $favorite->delete(); + return ['favorited' => false, 'action' => 'removed']; + } else { + // 添加收藏 + self::create([ + 'userid' => $userid, + 'favoritable_type' => $type, + 'favoritable_id' => $id, + ]); + return ['favorited' => true, 'action' => 'added']; + } + } + + /** + * 检查是否已收藏 + * @param int $userid 用户ID + * @param string $type 收藏类型 + * @param int $id 收藏对象ID + * @return bool + */ + public static function isFavorited($userid, $type, $id) + { + return self::whereUserid($userid) + ->whereFavoritableType($type) + ->whereFavoritableId($id) + ->exists(); + } + + /** + * 获取用户收藏列表 + * @param int $userid 用户ID + * @param string|null $type 收藏类型过滤 + * @param int $page 页码 + * @param int $pageSize 每页数量 + * @return array + */ + public static function getUserFavorites($userid, $type = null, $page = 1, $pageSize = 20) + { + $query = self::whereUserid($userid)->orderByDesc('created_at'); + + if ($type) { + $query->whereFavoritableType($type); + } + + $favorites = $query->paginate($pageSize, ['*'], 'page', $page); + + $data = [ + 'tasks' => [], + 'projects' => [], + 'files' => [] + ]; + + // 分组收集ID + $taskIds = []; + $projectIds = []; + $fileIds = []; + + foreach ($favorites->items() as $favorite) { + switch ($favorite->favoritable_type) { + case self::TYPE_TASK: + $taskIds[] = $favorite->favoritable_id; + break; + case self::TYPE_PROJECT: + $projectIds[] = $favorite->favoritable_id; + break; + case self::TYPE_FILE: + $fileIds[] = $favorite->favoritable_id; + break; + } + } + + // 批量查询具体数据 + if (!empty($taskIds)) { + $tasks = ProjectTask::select([ + 'project_tasks.id', + 'project_tasks.name', + 'project_tasks.project_id', + 'project_tasks.complete_at', + 'project_tasks.created_at', + 'project_tasks.flow_item_id', + 'project_tasks.flow_item_name', + 'projects.name as project_name' + ]) + ->leftJoin('projects', 'project_tasks.project_id', '=', 'projects.id') + ->whereIn('project_tasks.id', $taskIds) + ->get() + ->keyBy('id'); + + foreach ($favorites->items() as $favorite) { + if ($favorite->favoritable_type === self::TYPE_TASK && isset($tasks[$favorite->favoritable_id])) { + $task = $tasks[$favorite->favoritable_id]; + + // 解析 flow_item_name 字段(格式:status|name|color) + $flowItemParts = explode('|', $task->flow_item_name ?: ''); + $flowItemStatus = $flowItemParts[0] ?? ''; + $flowItemName = $flowItemParts[1] ?? $task->flow_item_name; + $flowItemColor = $flowItemParts[2] ?? ''; + + $data['tasks'][] = [ + 'id' => $task->id, + 'name' => $task->name, + 'project_id' => $task->project_id, + 'project_name' => $task->project_name, + 'complete_at' => $task->complete_at, + 'flow_item_id' => $task->flow_item_id, + 'flow_item_name' => $flowItemName, + 'flow_item_status' => $flowItemStatus, + 'flow_item_color' => $flowItemColor, + 'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'), + ]; + } + } + } + + if (!empty($projectIds)) { + $projects = Project::select([ + 'id', 'name', 'desc', 'archived_at', 'created_at' + ])->whereIn('id', $projectIds)->get()->keyBy('id'); + + foreach ($favorites->items() as $favorite) { + if ($favorite->favoritable_type === self::TYPE_PROJECT && isset($projects[$favorite->favoritable_id])) { + $project = $projects[$favorite->favoritable_id]; + $data['projects'][] = [ + 'id' => $project->id, + 'name' => $project->name, + 'desc' => $project->desc, + 'archived_at' => $project->archived_at, + 'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'), + ]; + } + } + } + + if (!empty($fileIds)) { + $files = File::select([ + 'id', 'name', 'ext', 'size', 'created_at' + ])->whereIn('id', $fileIds)->get()->keyBy('id'); + + foreach ($favorites->items() as $favorite) { + if ($favorite->favoritable_type === self::TYPE_FILE && isset($files[$favorite->favoritable_id])) { + $file = $files[$favorite->favoritable_id]; + $data['files'][] = [ + 'id' => $file->id, + 'name' => $file->name, + 'ext' => $file->ext, + 'size' => $file->size, + 'favorited_at' => Carbon::parse($favorite->created_at)->format('Y-m-d H:i:s'), + ]; + } + } + } + + return [ + 'data' => $data, + 'total' => $favorites->total(), + 'current_page' => $favorites->currentPage(), + 'per_page' => $favorites->perPage(), + 'last_page' => $favorites->lastPage(), + ]; + } + + /** + * 清理用户收藏 + * @param int $userid 用户ID + * @param string|null $type 收藏类型,null表示全部类型 + * @return int 删除的记录数 + */ + public static function cleanUserFavorites($userid, $type = null) + { + $query = self::whereUserid($userid); + + if ($type) { + $query->whereFavoritableType($type); + } + + return $query->delete(); + } +} diff --git a/database/migrations/2025_09_22_102000_create_user_favorites_table.php b/database/migrations/2025_09_22_102000_create_user_favorites_table.php new file mode 100644 index 000000000..21c88446e --- /dev/null +++ b/database/migrations/2025_09_22_102000_create_user_favorites_table.php @@ -0,0 +1,42 @@ +bigIncrements('id'); + $table->bigInteger('userid')->index()->nullable()->default(0)->comment('用户ID'); + $table->string('favoritable_type', 50)->index()->nullable()->default('')->comment('收藏类型(task/project/file)'); + $table->bigInteger('favoritable_id')->index()->nullable()->default(0)->comment('收藏对象ID'); + $table->timestamps(); + + // 复合索引:用户ID + 收藏类型(用于按类型获取收藏列表) + $table->index(['userid', 'favoritable_type']); + // 唯一索引:用户ID + 收藏类型 + 收藏对象ID(防止重复收藏) + $table->unique(['userid', 'favoritable_type', 'favoritable_id'], 'user_favorites_unique'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_favorites'); + } +} diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 2607bf777..60586e235 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -18,10 +18,11 @@ -