visibility !== 1) { $projectOwner = ProjectUser::whereProjectId($task->project_id)->whereUserid($userid) ->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])->exists(); $participant = ProjectTaskUser::whereUserid($userid)->where(function ($query) use ($taskId) { $query->where('task_id', $taskId)->orWhere('task_pid', $taskId); })->exists(); if (!$projectOwner && !$participant && !ProjectTaskVisibilityUser::whereTaskId($taskId)->whereUserid($userid)->exists()) { throw new ApiException('无任务权限'); } } return $task; } /** * 校验指派权限,计算候选人员和不可移除的负责人。 */ public static function policy(ProjectTask $task): array { $settings = ProjectTaskHandoffSettings::get(); $project = Project::find($task->project_id); if ($settings['project_task_handoff'] !== 'open' || !$project || $project->archived_at || $task->archived_at) { throw new ApiException('当前任务不可指派'); } $userid = Doo::userId(); $owners = ProjectTaskHandoffRecord::owners($task->id); $members = ProjectUser::whereProjectId($task->project_id) ->pluck('userid') ->map(fn ($id) => (int)$id) ->all(); $ordinary = false; if (in_array($userid, $members, true)) { $authProject = Project::userProject($project->id); if ($owners) { ProjectPermission::userTaskPermission($authProject, ProjectPermission::TASK_UPDATE, $task); } $ordinary = true; } $protected = []; $candidates = $members; if (!$ordinary) { $context = UserDepartment::ownerViewContext(User::auth(), true); if ($settings['project_task_handoff_role'] === 'close' || !UserDepartment::isDepartmentReadonlyProject($context, $project->id) || (int)$task->visibility !== 1) { throw new ApiException('无指派权限'); } $departments = UserDepartment::getManagedDepartments($userid); if ($settings['project_task_handoff_role'] === 'owner') { $departments = $departments->where('owner_userid', $userid); } $selectedScope = UserDepartment::getManagedDepartmentScopeIds( $userid, request()->input('department_owner_ids', request()->input('department_ids')) ); $roots = $departments->pluck('id')->map(fn ($id) => (int)$id)->all(); $scope = $roots ? array_values(array_intersect( UserDepartment::getManagedDepartmentScopeIds($userid, $roots), $selectedScope )) : []; if (!$scope) { throw new ApiException('无指派权限'); } $managed = User::where(function ($query) use ($scope) { foreach ($scope as $id) { $query->orWhere('department', 'like', "%,{$id},%"); } })->pluck('userid')->map(fn ($id) => (int)$id)->all(); // 项目访问权限必须来自符合指派设置的部门,不能借用其他部门的管理员身份。 if (!array_intersect($managed, $members)) { throw new ApiException('无指派权限'); } if ($settings['project_task_handoff_candidates'] === 'department') { $candidates = array_values(array_intersect($members, $managed)); } if ($settings['project_task_handoff_adjust'] === 'department') { $protected = array_values(array_diff($owners, $managed)); } } $candidates = User::whereIn('userid', $candidates) ->whereNull('disable_at') ->where('bot', 0) ->pluck('userid') ->map(fn ($id) => (int)$id) ->all(); return [ 'owners' => $owners, 'protected' => $protected, 'candidates' => $candidates, 'note_required' => $settings['project_task_handoff_note'] === 'required', 'version' => self::version($task), ]; } public static function version(ProjectTask $task): string { return hash('sha256', json_encode([ $task->project_id, ProjectTaskHandoffRecord::owners($task->id), ProjectTaskHandoff::where('task_id', $task->id)->max('id'), ])); } public static function options(ProjectTask $task): array { $policy = self::policy($task); $policy['users'] = User::select(['userid', 'email', 'nickname', 'userimg']) ->whereIn('userid', array_unique(array_merge($policy['candidates'], $policy['owners']))) ->orderBy('nickname') ->get() ->makeHidden(['email']) ->toArray(); return $policy; } public static function validateOwners(array $owners, array $policy, string $version, string $note): array { $owners = array_values(array_unique(array_map('intval', $owners))); sort($owners); if (!hash_equals($policy['version'], $version)) { throw new ApiException('负责人已发生变化,请刷新后重试'); } if (count($owners) > 10) { throw new ApiException('任务负责人最多不能超过10个'); } if (array_diff($policy['protected'], $owners)) { throw new ApiException('不能移除管理范围外的负责人'); } $added = array_diff($owners, $policy['owners']); if (array_diff($added, $policy['candidates'])) { throw new ApiException('所选负责人不在可指派范围内'); } if ($owners === $policy['owners']) { throw new ApiException('负责人未发生变化'); } if ($policy['note_required'] && $note === '') { throw new ApiException('请填写指派留言'); } return $owners; } /** * 在同一事务中更新负责人、记录留言,并复用任务通知。 */ public static function assign(ProjectTask $task, array $owners, string $version, string $note): array { return ProjectTaskHandoffRecord::track($task, 'assign', function () use ($task, $owners, $version, $note) { $authorized = self::task($task->id); $policy = self::policy($authorized); $owners = self::validateOwners($owners, $policy, $version, $note); $projectMembers = ProjectUser::whereProjectId($task->project_id)->whereIn('userid', $owners)->pluck('userid')->all(); if (array_diff($owners, $projectMembers)) { throw new ApiException('所选负责人不在可指派范围内'); } $marking = []; $task->updateTask(['owner' => $owners], $marking); $data = ProjectTask::oneTask($task->id)->toArray(); $data['update_marking'] = $marking; $data['visibility_appointor'] = ProjectTaskVisibilityUser::whereTaskId($task->id)->pluck('userid')->all(); $task->pushMsg('update', $data); $removed = array_values(array_diff($policy['owners'], $owners)); $visibilityTasks = [$task]; if ($task->parent_id && ($parent = ProjectTask::find($task->parent_id))) { $visibilityTasks[] = $parent; } foreach ($visibilityTasks as $visibleTask) { if (!$removed || (int)$visibleTask->visibility === 1) { continue; } $remaining = ProjectTaskUser::where(function ($query) use ($visibleTask) { $query->where('task_id', $visibleTask->id)->orWhere('task_pid', $visibleTask->id); })->pluck('userid')->all(); $appointed = ProjectTaskVisibilityUser::whereTaskId($visibleTask->id)->pluck('userid')->all(); $lostAccess = array_values(array_diff($removed, $remaining, $appointed)); if ($lostAccess) { $visibleTask->pushMsgVisibleRemove($lostAccess); } } return $data; }, $note); } }