data ?: [], $fields); if ($name !== '') return $name; if ($sign->exists && trim((string) $sign->company_name_snapshot) !== '') return trim($sign->company_name_snapshot); return trim((string) (User::find($sign->user_id)->company_name ?? '')); } public static function identity(CourseSign $sign): string { if ($sign->company_key) return $sign->company_key; $name = self::resolvedCompanyName($sign, $sign->form_snapshot['fields'] ?? []); return $name !== '' ? 'name:'.hash('sha256', self::canonical($name)) : ''; } public static function occupants(CourseSign $sign) { $identity = self::identity($sign); return CourseSign::where('course_id', $sign->course_id)->whereIn('status', self::ACTIVE) ->when($sign->exists, fn ($query) => $query->where('id', '!=', $sign->id)) ->orderBy('id')->lockForUpdate()->get()->filter(fn ($other) => self::identity($other) === $identity)->values(); } /** Called only under the course lock by CourseSign::save(). */ public static function guard(CourseSign $sign, Course $course): void { if (!(int) $course->one_person_per_company) return; if ($sign->exists) { $current = CourseSign::whereKey($sign->id)->lockForUpdate()->firstOrFail(); if ((int) $current->status !== (int) $sign->getRawOriginal('status')) throw new InvalidArgumentException('报名状态已变化,请刷新后重试'); if ($current->replaced_by_sign_id && ($sign->isDirty('data') || $sign->isDirty('fee_file_ids'))) throw new InvalidArgumentException('该报名已被更换,原报名信息只读,请重新报名'); if ($current->replaced_by_sign_id && in_array((int) $sign->status, self::ACTIVE, true)) throw new InvalidArgumentException('该报名已被更换,请重新提交报名,不可恢复原报名'); } if (!in_array((int) ($sign->status ?? 0), self::ACTIVE, true)) return; if (!$sign->exists || $sign->isDirty('data') || !$sign->company_key) { $name = self::resolvedCompanyName($sign, $sign->form_snapshot['fields'] ?? CourseFormSchema::definitions($course->id)); if ($name === '') throw new InvalidArgumentException('请先完善个人信息中的公司名称,或在报名表中填写公司名称'); $sign->company_name_snapshot = $name; $sign->company_key = 'name:'.hash('sha256', self::canonical($name)); } if (CourseSign::where('course_id', $course->id)->where('user_id', $sign->user_id)->where('status', 6)->lockForUpdate()->first()) throw new InvalidArgumentException('您当前无法报名该课程,请联系管理员'); $self = CourseSign::where('course_id', $course->id)->where('user_id', $sign->user_id)->whereIn('status', self::ACTIVE) ->when($sign->exists, fn ($query) => $query->where('id', '!=', $sign->id))->lockForUpdate()->first(); if ($self) throw new InvalidArgumentException('请勿重复报名'); if (self::occupants($sign)->isNotEmpty()) throw new InvalidArgumentException('该公司已有有效报名,请先处理原报名后再操作'); } private static function replacementError(CourseSign $old): void { if ((int) $old->fee_status === 1 || !empty($old->fee_file_ids)) throw new InvalidArgumentException('贵公司已有报名已缴费或提交缴费凭证,请联系管理员办理更换'); if (ThirdAppointmentLog::where('course_sign_id', $old->id)->where(function ($query) { $query->where('plate_status', 1)->orWhere(function ($query) { $query->where('finally', 1)->whereNotNull('door_name')->where(function ($query) { $query->whereNull('end_time')->orWhere('end_time', '>', now()); }); }); })->exists()) throw new InvalidArgumentException('贵公司已有报名已开通门禁或停车权限,请联系管理员办理更换'); } private static function fingerprint(CourseSign $old, CourseSign $new): string { return hash('sha256', json_encode([$old->id, $old->getRawOriginal(), $new->course_id, $new->user_id, self::identity($new), $new->data], JSON_UNESCAPED_UNICODE)); } /** Read-only on conflict until the exact current occupant has been confirmed. */ public static function submit(array $attributes, ?string $token = null): array { return DB::transaction(function () use ($attributes, $token) { $course = Course::whereKey($attributes['course_id'])->lockForUpdate()->firstOrFail(); $new = new CourseSign($attributes); $new->status = 0; if (!(int) $course->one_person_per_company) { if (CourseSign::where('course_id', $course->id)->where('user_id', $new->user_id)->exists()) throw new InvalidArgumentException('请勿重复报名'); $new->save(); return ['sign' => $new]; } $fields = $attributes['form_snapshot']['fields'] ?? CourseFormSchema::definitions($course->id); $name = self::resolvedCompanyName($new, $fields); if ($name === '') throw new InvalidArgumentException('请先完善个人信息中的公司名称,或在报名表中填写公司名称'); $new->company_name_snapshot = $name; $new->company_key = 'name:'.hash('sha256', self::canonical($name)); if (CourseSign::where('course_id', $course->id)->where('user_id', $new->user_id)->whereIn('status', [0, 1, 3, 6])->lockForUpdate()->first()) throw new InvalidArgumentException('您已报名该课程,请查看原报名记录'); $occupants = self::occupants($new); if ($occupants->count() > 1) throw new InvalidArgumentException('贵公司存在多条历史有效报名,请联系管理员处理后再报名'); $old = $occupants->first(); if (!$old) { $new->save(); return ['sign' => $new]; } self::replacementError($old); $fingerprint = self::fingerprint($old, $new); $confirmed = false; if ($token) try { $payload = json_decode(Crypt::decryptString($token), true); $confirmed = ($payload['expires'] ?? 0) >= time() && hash_equals($fingerprint, (string) ($payload['fingerprint'] ?? '')); } catch (\Throwable $e) { /* Return a fresh confirmation; never replace a changed occupant. */ } $oldName = $old->participantName(); if (!$confirmed) return ['replacement_required' => true, 'replacement_name' => $oldName, 'replacement_token' => Crypt::encryptString(json_encode(['fingerprint' => $fingerprint, 'expires' => time() + 600])), 'message' => '贵公司'.$oldName.'已提交报名,请确认是否更换为您报名参训。确认后,'.$oldName.'的报名将调整为“主动放弃”,您的报名需重新审核。']; $newName = $new->participantName(); $old->status = 5; $old->giveup_reason = '因同公司更换参训人员,已由'.$newName.'替换报名。'; $old->replaced_at = now(); $old->save(); $new->replaces_sign_id = $old->id; $new->save(); $old->replaced_by_sign_id = $new->id; $old->save(); CourseAppointmentTotal::reduce($old->user_id, $old->id); return ['sign' => $new]; }); } /** Serialize queued grants with replacement so a delayed job cannot restore old access. */ public static function grantAccess(int $signId, callable $grant): void { DB::transaction(function () use ($signId, $grant) { $sign = CourseSign::find($signId); if (!$sign) return; Course::whereKey($sign->course_id)->lockForUpdate()->first(); $sign = CourseSign::find($signId); if ($sign && (int) $sign->status === 1) $grant($sign); }); } public static function duplicates(int $courseId): array { return CourseSign::where('course_id', $courseId)->whereIn('status', self::ACTIVE)->get()->groupBy(fn ($sign) => self::identity($sign)) ->filter(fn ($signs, $key) => $key !== '' && $signs->count() > 1)->map(fn ($signs) => ['company' => self::resolvedCompanyName($signs->first(), $signs->first()->form_snapshot['fields'] ?? []), 'ids' => $signs->pluck('id')->all()])->values()->all(); } }