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.

401 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\Concerns\ResolvesParticipantApplication;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\Competition;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class ApplicationController extends Controller
{
use ResolvesParticipantApplication;
private function ensureParticipantMayEditSignup(Application $app): void
{
$app->assertMayEditSignup('status');
}
private function currentApplication(Request $request): Application
{
return $this->participantApplication($request);
}
/**
* @return list<string>
*/
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;
}
/** 当前赛事报名表 schema 是否包含指定字段 key */
private function signupSchemaHasKey(Competition $competition, string $key): bool
{
$competition->loadMissing('formSchema');
$rows = $competition->formSchema?->schema_json;
if (! is_array($rows) || count($rows) === 0) {
// 与选手端 normalizeSignupSchema([]) 兜底 DEFAULT_SIGNUP_FORM_SCHEMA 一致(含 entry_group
return $key === 'entry_group';
}
foreach ($rows as $row) {
if (is_array($row) && ($row['key'] ?? '') === $key) {
return true;
}
}
return false;
}
/**
* @return list<string>
*/
private function entryGroupOptionValues(): array
{
$raw = config('contest.entry_groups', ['创新组', '创业组']);
if (! is_array($raw) || count($raw) === 0) {
return ['创新组', '创业组'];
}
return array_values(array_filter(array_map('strval', $raw), fn (string $v) => $v !== ''));
}
/**
* 报名表 schema 中 entry_group 下拉允许的「值」(与选项 `value` 一致,如 entry_group1
*
* @return list<string>
*/
private function entryGroupSelectValuesFromSignupSchema(Competition $competition): array
{
$competition->loadMissing('formSchema');
$rows = $competition->formSchema?->schema_json;
if (! is_array($rows)) {
return [];
}
foreach ($rows as $row) {
if (! is_array($row) || ($row['key'] ?? '') !== 'entry_group') {
continue;
}
$opts = $row['options'] ?? [];
if (! is_array($opts)) {
return [];
}
$out = [];
foreach ($opts as $opt) {
if (is_array($opt) && array_key_exists('value', $opt)) {
$s = trim((string) $opt['value']);
if ($s !== '') {
$out[] = $s;
}
} elseif (is_string($opt)) {
$s = trim($opt);
if ($s !== '') {
$out[] = $s;
}
}
}
return array_values(array_unique($out));
}
return [];
}
/**
* 参赛组别取何值时企业名称必填:优先读 company_name 的 required_when否则读 config。
*
* @return list<string>
*/
private function companyNameRequiredWhenEntryGroupValues(Competition $competition): array
{
$competition->loadMissing('formSchema');
$rows = $competition->formSchema?->schema_json;
if (! is_array($rows)) {
$rows = [];
}
foreach ($rows as $row) {
if (! is_array($row) || ($row['key'] ?? '') !== 'company_name') {
continue;
}
$rw = $row['required_when'] ?? null;
if (! is_array($rw) || ($rw['field'] ?? '') !== 'entry_group') {
break;
}
$vals = $rw['values'] ?? [];
if (! is_array($vals)) {
break;
}
$out = [];
foreach ($vals as $v) {
$s = trim((string) $v);
if ($s !== '') {
$out[] = $s;
}
}
if (count($out) > 0) {
return array_values(array_unique($out));
}
break;
}
$cv = trim((string) config('contest.entry_group_company_required_value', '创业组'));
return $cv !== '' ? [$cv] : [];
}
/**
* @return list<string>
*/
private function allowedEntryGroupValues(Competition $competition): array
{
$fromSchema = $this->entryGroupSelectValuesFromSignupSchema($competition);
if (count($fromSchema) > 0) {
return $fromSchema;
}
return $this->entryGroupOptionValues();
}
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', []);
$companyRules = ['nullable', 'string', 'max:255'];
if ($this->signupSchemaHasKey($competition, 'entry_group')) {
$reqVals = $this->companyNameRequiredWhenEntryGroupValues($competition);
if (count($reqVals) > 0) {
$companyRules[] = Rule::requiredIf(function () use ($request, $reqVals): bool {
$eg = (string) $request->input('entry_group', '');
return in_array($eg, $reqVals, true);
});
}
}
$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' => $companyRules,
'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->signupSchemaHasKey($competition, 'entry_group')) {
$rules['entry_group'] = ['required', 'string', Rule::in($this->allowedEntryGroupValues($competition))];
}
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', []);
$companyRules = ['nullable', 'string', 'max:255'];
if ($this->signupSchemaHasKey($competition, 'entry_group')) {
$reqVals = $this->companyNameRequiredWhenEntryGroupValues($competition);
if (count($reqVals) > 0) {
$companyRules[] = Rule::requiredIf(function () use ($request, $reqVals): bool {
$eg = (string) $request->input('entry_group', '');
return in_array($eg, $reqVals, true);
});
}
}
$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' => $companyRules,
'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->signupSchemaHasKey($competition, 'entry_group')) {
$rules['entry_group'] = ['required', 'string', Rule::in($this->allowedEntryGroupValues($competition))];
}
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,
'entry_group' => $app->entry_group,
'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->participantPreviewSignedUrl(),
])->values(),
];
}
}