with(['teacher.university', 'typeItem', 'statusItem']) ->withCount('handleLogs'); if ($kw = $request->query('keyword')) { $query->where(function ($q) use ($kw) { $q->where('title', 'like', "%{$kw}%") ->orWhere('contact_name', 'like', "%{$kw}%") ->orWhere('company', 'like', "%{$kw}%") ->orWhere('content', 'like', "%{$kw}%") ->orWhereHas('typeItem', fn ($tq) => $tq->where('label', 'like', "%{$kw}%")); }); } if ($request->filled('teacher_id')) { $query->where('teacher_id', (int) $request->query('teacher_id')); } if ($request->filled('type_dict_item_id')) { $query->where('type_dict_item_id', (int) $request->query('type_dict_item_id')); } if ($request->filled('status_dict_item_id')) { $query->where('status_dict_item_id', (int) $request->query('status_dict_item_id')); } $paginator = $query ->orderByDesc('submitted_at') ->orderByDesc('id') ->paginate((int) $request->query('page_size', 20)) ->withQueryString(); $paginator->getCollection()->transform(fn (Demand $d) => $this->serializeList($d)); return $this->paginated($paginator); } public function store(Request $request): JsonResponse { $data = $this->validatedDemand($request); $teacher = null; if (! empty($data['teacher_id'])) { $teacher = Teacher::query()->with('university')->findOrFail($data['teacher_id']); } $row = Demand::query()->create([ 'teacher_id' => $data['teacher_id'] ?? null, 'type_dict_item_id' => $data['type_dict_item_id'], 'status_dict_item_id' => $data['status_dict_item_id'], 'title' => $data['title'], 'content' => $data['content'], 'contact_name' => $data['contact_name'] ?? $teacher?->name, 'company' => $data['company'] ?? $teacher?->university?->name, 'submitted_at' => now(), ]); return $this->ok(['id' => $row->id], '已发布需求'); } public function show(int $demand): JsonResponse { $model = Demand::query() ->with(['teacher.university', 'typeItem', 'statusItem']) ->withCount('handleLogs') ->findOrFail($demand); return $this->ok($this->serializeDetail($model)); } public function update(Request $request, int $demand): JsonResponse { $model = Demand::query()->findOrFail($demand); $data = $this->validatedDemand($request, partial: true); $model->fill($data); $model->save(); return $this->ok(null, '已保存'); } public function destroy(int $demand): JsonResponse { Demand::query()->findOrFail($demand)->delete(); return $this->ok(null, '已删除'); } public function handleLogs(int $demand): JsonResponse { Demand::query()->findOrFail($demand); $items = DemandHandleLog::query() ->with(['adminUser', 'statusItem']) ->where('demand_id', $demand) ->orderByDesc('handled_at') ->orderByDesc('id') ->get() ->map(fn (DemandHandleLog $log) => $this->serializeHandleLog($log)); return $this->ok(['items' => $items]); } public function storeHandleLog(Request $request, int $demand): JsonResponse { $model = Demand::query()->findOrFail($demand); $statusTypeId = DictType::query()->where('code', 'demand_status')->where('status', 1)->value('id'); $data = $request->validate([ 'handled_at' => ['required', 'date'], 'admin_user_id' => ['nullable', 'integer', 'exists:admin_users,id'], 'status_dict_item_id' => $this->dictItemRules($statusTypeId, 'required'), 'content' => ['required', 'string'], 'next_plan' => ['nullable', 'string', 'max:255'], 'next_follow_date' => ['nullable', 'date'], ]); $log = DemandHandleLog::query()->create([ 'demand_id' => $model->id, 'admin_user_id' => $data['admin_user_id'] ?? $request->user()?->id, 'status_dict_item_id' => $data['status_dict_item_id'], 'content' => $data['content'], 'next_plan' => $data['next_plan'] ?? null, 'next_follow_date' => $data['next_follow_date'] ?? null, 'handled_at' => $data['handled_at'], ]); $model->status_dict_item_id = $data['status_dict_item_id']; $model->save(); return $this->ok(['id' => $log->id], '已保存跟进'); } /** * @return array */ protected function validatedDemand(Request $request, bool $partial = false): array { $typeTypeId = DictType::query()->where('code', 'demand_type')->where('status', 1)->value('id'); $statusTypeId = DictType::query()->where('code', 'demand_status')->where('status', 1)->value('id'); if (! $partial && (! $typeTypeId || ! $statusTypeId)) { throw new \Illuminate\Http\Exceptions\HttpResponseException( response()->json([ 'message' => '需求字典未配置,请执行 DemandDictionarySeeder', 'data' => null, ], 422) ); } $activeStatusId = $statusTypeId ? DictItem::query()->where('dict_type_id', $statusTypeId)->where('value', 'active')->where('status', 1)->value('id') : null; $rules = [ 'teacher_id' => ['nullable', 'integer', 'exists:teachers,id'], 'type_dict_item_id' => $this->dictItemRules($typeTypeId, $partial ? 'sometimes' : 'required'), 'status_dict_item_id' => $this->dictItemRules($statusTypeId, $partial ? 'sometimes' : 'nullable'), 'title' => [$partial ? 'sometimes' : 'required', 'string', 'max:255'], 'content' => [$partial ? 'sometimes' : 'required', 'string'], 'contact_name' => ['nullable', 'string', 'max:64'], 'company' => ['nullable', 'string', 'max:128'], ]; $data = $request->validate($rules); if (! $partial && empty($data['status_dict_item_id']) && $activeStatusId) { $data['status_dict_item_id'] = $activeStatusId; } return $data; } /** * @return array */ protected function dictItemRules(?int $dictTypeId, string $presence): array { if (! $dictTypeId) { return match ($presence) { 'required' => ['required', 'integer'], 'sometimes' => ['sometimes', 'nullable', 'integer'], default => ['nullable', 'integer'], }; } $exists = Rule::exists('dict_items', 'id')->where( fn ($q) => $q->where('dict_type_id', $dictTypeId)->where('status', 1) ); return match ($presence) { 'required' => ['required', 'integer', $exists], 'sometimes' => ['sometimes', 'nullable', 'integer', $exists], default => ['nullable', 'integer', $exists], }; } /** * @return array */ protected function serializeList(Demand $d): array { return [ 'id' => $d->id, 'teacher_id' => $d->teacher_id, 'teacher_name' => $d->teacher?->name, 'type_dict_item_id' => $d->type_dict_item_id, 'type_item' => $this->serializeDictItem($d->typeItem), 'status_dict_item_id' => $d->status_dict_item_id, 'status_item' => $this->serializeDictItem($d->statusItem), 'title' => $d->title, 'content' => $d->content, 'contact_name' => $d->contact_name, 'company' => $d->company, 'submitted_at' => $d->submitted_at?->toDateString(), 'handle_logs_count' => (int) ($d->handle_logs_count ?? 0), ]; } /** * @return array */ protected function serializeDetail(Demand $d): array { $row = $this->serializeList($d); $row['university_name'] = $d->teacher?->university?->name; return $row; } /** * @return array */ protected function serializeHandleLog(DemandHandleLog $log): array { return [ 'id' => $log->id, 'handled_at' => $log->handled_at?->toDateString(), 'operator_name' => $log->adminUser?->real_name ?: $log->adminUser?->username, 'status_item' => $this->serializeDictItem($log->statusItem), 'content' => $log->content, 'next_plan' => $log->next_plan, 'next_follow_date' => $log->next_follow_date?->toDateString(), ]; } /** * @return array{id:int,label:string,value:string}|null */ protected function serializeDictItem(?DictItem $item): ?array { if (! $item) { return null; } return [ 'id' => $item->id, 'label' => $item->label, 'value' => $item->value, ]; } }