You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
337 lines
12 KiB
337 lines
12 KiB
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ActivitySignup;
|
|
use App\Models\CourseSignup;
|
|
use App\Models\DictItem;
|
|
use App\Models\DictType;
|
|
use App\Models\MiniappUser;
|
|
use App\Models\Teacher;
|
|
use App\Services\GridMemberScopeService;
|
|
use App\Services\TeacherFollowPlanService;
|
|
use App\Support\ApiResponse;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class MiniappUserController extends Controller
|
|
{
|
|
use ApiResponse;
|
|
|
|
public function __construct(
|
|
protected TeacherFollowPlanService $followPlan,
|
|
protected GridMemberScopeService $gridScope
|
|
) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$query = MiniappUser::query()->with('teacher:id,name');
|
|
|
|
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')->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()->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']);
|
|
|
|
$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;
|
|
}
|
|
|
|
$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<string, mixed>
|
|
*/
|
|
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<string, mixed> $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<int, \Illuminate\Contracts\Validation\Rule|string>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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,
|
|
'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<string, mixed>
|
|
*/
|
|
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<int, string>, activity_titles: array<int, string>, courses: array<int, array<string, mixed>>, activities: array<int, array<string, mixed>>}
|
|
*/
|
|
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;
|
|
}
|
|
}
|