assertMayEditSignup('status'); } private function currentApplication(Request $request): Application { return $this->participantApplication($request); } /** * @return list */ private function enabledTrackCodes(Competition $competition): array { return $competition->tracks() ->where('is_enabled', true) ->pluck('track_code') ->all(); } /** 报名表 schema 是否包含参赛承诺书勾选(与选手端 key 一致) */ private function signupSchemaRequiresCommitment(Competition $competition): bool { $competition->loadMissing('formSchema'); $rows = $competition->formSchema?->schema_json; if (! is_array($rows) || count($rows) === 0) { return true; } foreach ($rows as $row) { if (is_array($row) && ($row['key'] ?? '') === 'commitment_accepted') { return true; } } return false; } public function show(Request $request): JsonResponse { $app = $this->currentApplication($request); $app->load(['files']); return response()->json($this->transform($app)); } public function update(Request $request): JsonResponse { $competition = $this->resolvePublishedCompetitionFromRequest($request); $app = $this->currentApplication($request); $this->ensureParticipantMayEditSignup($app); $trackCodes = $this->enabledTrackCodes($competition); $degrees = config('contest.degrees', []); $countries = config('contest.location_countries', []); $trackRules = count($trackCodes) ? ['nullable', 'string', Rule::in($trackCodes)] : ['nullable', 'string', 'max:100']; $rules = [ 'player_name' => ['nullable', 'string', 'max:120'], 'school' => ['nullable', 'string', 'max:200'], 'degree' => ['nullable', 'string', Rule::in($degrees)], 'contact_email' => ['nullable', 'email', 'max:255'], 'contact_mobile' => ['nullable', 'regex:/^1[3-9]\d{9}$/'], 'company_name' => ['nullable', 'string', 'max:255'], 'project_name' => ['nullable', 'string', 'max:255'], 'track' => $trackRules, 'location_country' => ['nullable', 'string', Rule::in($countries)], 'location_province' => ['nullable', 'string', 'max:100'], 'location_city' => ['nullable', 'string', 'max:100'], 'oversea_country' => ['nullable', 'string', 'max:100'], 'intro' => ['nullable', 'string', 'max:5000'], ]; if ($this->signupSchemaRequiresCommitment($competition)) { $rules['commitment_accepted'] = ['sometimes', 'boolean']; $rules['promise_signature'] = ['nullable', 'string', 'max:2097152']; } $data = $request->validate($rules); $commitmentAccepted = $data['commitment_accepted'] ?? null; $promiseSignature = $data['promise_signature'] ?? null; unset($data['commitment_accepted'], $data['promise_signature']); $app->fill($data); if ($this->signupSchemaRequiresCommitment($competition)) { if ($commitmentAccepted === true) { if (! is_string($promiseSignature) || trim($promiseSignature) === '') { throw ValidationException::withMessages([ 'promise_signature' => ['请完成手写签名后再保存'], ]); } $app->promise_signed_at = now(); $app->promise_signature = $promiseSignature; } elseif ($commitmentAccepted === false) { $app->promise_signed_at = null; $app->promise_signature = null; } } if (! empty($data['player_name']) && empty($request->user()->name)) { $request->user()->update(['name' => $data['player_name']]); } if (! empty($data['contact_email']) && empty($request->user()->email)) { $request->user()->update(['email' => $data['contact_email']]); } $app->save(); $app->load(['files']); return response()->json($this->transform($app)); } public function submit(Request $request): JsonResponse { $competition = $this->resolvePublishedCompetitionFromRequest($request); $app = $this->currentApplication($request); $this->ensureParticipantMayEditSignup($app); $trackCodes = $this->enabledTrackCodes($competition); if (count($trackCodes) === 0) { throw ValidationException::withMessages([ 'track' => ['本场赛事尚未配置可用赛道,请联系管理员'], ]); } $degrees = config('contest.degrees', []); $countries = config('contest.location_countries', []); $rules = [ 'player_name' => ['required', 'string', 'max:120'], 'school' => ['required', 'string', 'max:200'], 'degree' => ['required', 'string', Rule::in($degrees)], 'contact_email' => ['required', 'email', 'max:255'], 'contact_mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'], 'company_name' => ['nullable', 'string', 'max:255'], 'project_name' => ['required', 'string', 'max:255'], 'track' => ['required', 'string', Rule::in($trackCodes)], 'location_country' => ['required', 'string', Rule::in($countries)], 'location_province' => ['nullable', 'string', 'max:100', 'required_if:location_country,中国'], 'location_city' => ['nullable', 'string', 'max:100', 'required_if:location_country,中国'], 'oversea_country' => ['nullable', 'string', 'max:100', 'required_if:location_country,海外'], 'intro' => ['nullable', 'string', 'max:5000'], ]; if ($this->signupSchemaRequiresCommitment($competition)) { $rules['commitment_accepted'] = ['required', 'accepted']; $rules['promise_signature'] = ['required', 'string', 'max:2097152']; } $data = $request->validate($rules); $promiseSignature = $data['promise_signature'] ?? ''; unset($data['commitment_accepted'], $data['promise_signature']); $planCount = $app->files()->where('kind', 'plan')->count(); if ($planCount < 1) { throw ValidationException::withMessages([ 'files' => ['请至少上传一份商业计划书'], ]); } $app->fill($data); if ($this->signupSchemaRequiresCommitment($competition)) { if (! is_string($promiseSignature) || trim($promiseSignature) === '') { throw ValidationException::withMessages([ 'promise_signature' => ['请完成参赛承诺书手写签名'], ]); } $app->promise_signature = $promiseSignature; $app->promise_signed_at = now(); } $app->status = 'submitted'; $app->submitted_at = now(); $app->save(); $request->user()->update([ 'name' => $data['player_name'], 'email' => $data['contact_email'], ]); $app->load(['files']); return response()->json($this->transform($app)); } private function transform(Application $app): array { return [ 'id' => $app->id, 'competition_id' => $app->competition_id, 'status' => $app->status, 'player_name' => $app->player_name, 'school' => $app->school, 'degree' => $app->degree, 'contact_email' => $app->contact_email, 'contact_mobile' => $app->contact_mobile, 'company_name' => $app->company_name, 'project_name' => $app->project_name, 'track' => $app->track, 'location_country' => $app->location_country, 'location_province' => $app->location_province, 'location_city' => $app->location_city, 'oversea_country' => $app->oversea_country, 'intro' => $app->intro, 'promise_signed_at' => $app->promise_signed_at?->toIso8601String(), 'promise_signature' => $app->promise_signature, 'submitted_at' => $app->submitted_at?->toIso8601String(), 'participant_may_edit' => $app->participantMayEditSignup(), 'files' => $app->files->map(fn ($f) => [ 'id' => $f->id, 'kind' => $f->kind, 'original_name' => $f->original_name, 'size' => $f->size, 'url' => $f->publicUrl(), ])->values(), ]; } }