with([ 'teacher:id,name', 'researchDirections:id,name,sort', ]); if ($kw = $request->query('keyword')) { $query->where(function ($q) use ($kw) { $q->where('name', 'like', "%{$kw}%") ->orWhere('mobile', 'like', "%{$kw}%") ->orWhere('company', 'like', "%{$kw}%") ->orWhere('nickname', 'like', "%{$kw}%"); }); } if ($request->query('converted') === '1') { $query->whereNotNull('teacher_id'); } elseif ($request->query('converted') === '0') { $query->whereNull('teacher_id'); } $paginator = $query ->orderByDesc('id') ->paginate((int) $request->query('page_size', 20)) ->withQueryString(); $paginator->getCollection()->transform(fn (MiniappUser $u) => $this->serializeList($u)); return $this->paginated($paginator); } public function show(int $miniappUser): JsonResponse { $model = MiniappUser::query()->with([ 'teacher:id,name', 'researchDirections:id,name,sort', ])->findOrFail($miniappUser); return $this->ok($this->serializeDetail($model)); } public function convertToTeacher(Request $request, int $miniappUser): JsonResponse { $user = $this->gridScope->userFromRequest($request); $this->gridScope->assertCanMutateTeachers($user); $miniapp = MiniappUser::query() ->with('researchDirections:id') ->findOrFail($miniappUser); if ($miniapp->teacher_id) { return $this->fail('该学员已转入老师库', 422); } $this->ensureTeacherDicts(); $data = $this->validatedConvertTeacher($request); $this->gridScope->assertTeacherDataInScope($user, $data); $directionIds = $data['research_direction_ids'] ?? []; unset($data['research_direction_ids']); if ($directionIds === []) { $directionIds = $miniapp->researchDirections->pluck('id')->all(); } if ($directionIds === []) { return $this->fail('请填写研究方向', 422); } $sourceTypeId = DictType::query()->where('code', 'teacher_source')->where('status', 1)->value('id'); $miniappSourceId = DictItem::query() ->where('dict_type_id', $sourceTypeId) ->where('value', 'miniapp') ->where('status', 1) ->value('id'); if ($miniappSourceId) { $data['source_dict_item_id'] = $miniappSourceId; } if (empty($data['phone']) && $miniapp->mobile) { $data['phone'] = $miniapp->mobile; } if (empty($data['title']) && $miniapp->job_title) { $data['title'] = $miniapp->job_title; } $teacherId = DB::transaction(function () use ($miniapp, $data, $directionIds, $request) { $teacher = new Teacher($data); $teacher->miniapp_user_id = $miniapp->id; $this->syncPartnerFlags($teacher, $data); $this->applyStarFollowDate($teacher, $request->boolean('recalc_next_follow_date', true)); $teacher->save(); $teacher->researchDirections()->sync($directionIds); $miniapp->teacher_id = $teacher->id; $miniapp->converted_at = now(); $miniapp->save(); $this->linkSignupsToMiniappUser($miniapp); return $teacher->id; }); return $this->ok(['teacher_id' => $teacherId], '已转入老师库'); } protected function linkSignupsToMiniappUser(MiniappUser $miniapp): void { if (! $miniapp->mobile) { return; } CourseSignup::query() ->where(function ($q) use ($miniapp) { $q->where('miniapp_user_id', $miniapp->id) ->orWhere(function ($q2) use ($miniapp) { $q2->whereNull('miniapp_user_id')->where('mobile', $miniapp->mobile); }); }) ->update(['miniapp_user_id' => $miniapp->id]); ActivitySignup::query() ->where(function ($q) use ($miniapp) { $q->where('miniapp_user_id', $miniapp->id) ->orWhere(function ($q2) use ($miniapp) { $q2->whereNull('miniapp_user_id')->where('mobile', $miniapp->mobile); }); }) ->update(['miniapp_user_id' => $miniapp->id]); } /** * @return array */ protected function validatedConvertTeacher(Request $request): array { $sourceTypeId = DictType::query()->where('code', 'teacher_source')->where('status', 1)->value('id'); $levelTypeId = DictType::query()->where('code', 'teacher_level')->where('status', 1)->value('id'); $statusTypeId = DictType::query()->where('code', 'teacher_status')->where('status', 1)->value('id'); $rules = [ 'name' => ['required', 'string', 'max:64'], 'university_id' => ['required', 'integer', 'exists:universities,id'], 'city' => ['required', 'string', 'max:64'], 'title' => ['required', 'string', 'max:64'], 'research_direction_ids' => ['required', 'array', 'min:1'], 'research_direction_ids.*' => ['integer', 'distinct', Rule::exists('research_directions', 'id')->where('status', 1)], 'phone' => ['nullable', 'string', 'max:32'], 'email' => ['nullable', 'email', 'max:128'], 'source_dict_item_id' => $this->dictItemRules($sourceTypeId, 'nullable'), 'star_level_dict_item_id' => $this->dictItemRules($levelTypeId, 'nullable'), 'status_dict_item_id' => $this->dictItemRules($statusTypeId, 'required'), 'next_follow_date' => ['nullable', 'date'], 'next_follow_subject' => ['nullable', 'string', 'max:255'], 'remark' => ['nullable', 'string'], 'recalc_next_follow_date' => ['nullable', 'boolean'], ]; return $request->validate($rules); } /** * @param array $data */ protected function syncPartnerFlags(Teacher $teacher, array $data): void { if (! array_key_exists('status_dict_item_id', $data)) { return; } $statusItem = DictItem::query()->find($data['status_dict_item_id']); if ($statusItem && $statusItem->value === 'partner') { $teacher->is_partner = true; $teacher->converted_at = $teacher->converted_at ?? now(); } } protected function applyStarFollowDate(Teacher $teacher, bool $recalc): void { if (! $recalc || ! $teacher->star_level_dict_item_id) { return; } $starItem = DictItem::query()->find($teacher->star_level_dict_item_id); $teacher->next_follow_date = $this->followPlan->nextFollowDateFromStar($starItem); } protected function ensureTeacherDicts(): void { $codes = ['teacher_source', 'teacher_level', 'teacher_status']; foreach ($codes as $code) { if (! DictType::query()->where('code', $code)->where('status', 1)->exists()) { throw new \Illuminate\Http\Exceptions\HttpResponseException( response()->json([ 'message' => '老师库字典未配置,请执行 TeacherDictionarySeeder', 'data' => null, ], 422) ); } } } /** * @return array */ protected function dictItemRules(?int $dictTypeId, string $presence): array { if (! $dictTypeId) { return match ($presence) { 'required' => ['required', '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], default => ['nullable', 'integer', $exists], }; } /** * @return array */ protected function serializeList(MiniappUser $u): array { $enrollments = $this->loadEnrollments($u); return [ 'id' => $u->id, 'name' => $u->name, 'mobile' => $u->mobile, 'company' => $u->company, 'nickname' => $u->nickname, 'avatar_url' => $u->avatar_url, 'job_title' => $u->job_title, 'research_direction' => $u->researchDirections->pluck('name')->join('、') ?: null, 'research_direction_ids' => $u->researchDirections->pluck('id')->values()->all(), 'teacher_id' => $u->teacher_id, 'teacher_name' => $u->teacher?->name, 'converted_at' => $u->converted_at?->toIso8601String(), 'course_titles' => $enrollments['course_titles'], 'activity_titles' => $enrollments['activity_titles'], 'created_at' => $u->created_at?->toIso8601String(), ]; } /** * @return array */ protected function serializeDetail(MiniappUser $u): array { $row = $this->serializeList($u); $enrollments = $this->loadEnrollments($u, detailed: true); $row['courses'] = $enrollments['courses']; $row['activities'] = $enrollments['activities']; return $row; } /** * @return array{course_titles: array, activity_titles: array, courses: array>, activities: array>} */ protected function loadEnrollments(MiniappUser $u, bool $detailed = false): array { $courseQuery = CourseSignup::query() ->with('course:id,title') ->where(function ($q) use ($u) { $q->where('miniapp_user_id', $u->id); if ($u->mobile) { $q->orWhere(function ($q2) use ($u) { $q2->whereNull('miniapp_user_id')->where('mobile', $u->mobile); }); } }) ->orderByDesc('signed_up_at') ->orderByDesc('id'); $activityQuery = ActivitySignup::query() ->with('activity:id,title') ->where(function ($q) use ($u) { $q->where('miniapp_user_id', $u->id); if ($u->mobile) { $q->orWhere(function ($q2) use ($u) { $q2->whereNull('miniapp_user_id')->where('mobile', $u->mobile); }); } }) ->orderByDesc('signed_up_at') ->orderByDesc('id'); $courseSignups = $courseQuery->get(); $activitySignups = $activityQuery->get(); $courseTitles = $courseSignups->map(fn ($s) => $s->course?->title)->filter()->unique()->values()->all(); $activityTitles = $activitySignups->map(fn ($s) => $s->activity?->title)->filter()->unique()->values()->all(); $result = [ 'course_titles' => $courseTitles, 'activity_titles' => $activityTitles, 'courses' => [], 'activities' => [], ]; if ($detailed) { $result['courses'] = $courseSignups->map(fn (CourseSignup $s) => [ 'id' => $s->id, 'course_id' => $s->course_id, 'title' => $s->course?->title, 'signed_up_at' => $s->signed_up_at?->toDateString(), 'company' => $s->company, ])->values()->all(); $result['activities'] = $activitySignups->map(fn (ActivitySignup $s) => [ 'id' => $s->id, 'activity_id' => $s->activity_id, 'title' => $s->activity?->title, 'signed_up_at' => $s->signed_up_at?->toDateString(), 'company' => $s->company, ])->values()->all(); } return $result; } }