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.

280 lines
8.1 KiB

5 months ago
<?php
namespace App\Models;
3 months ago
use App\Support\ProjectCode;
3 months ago
use Illuminate\Database\Eloquent\Builder;
5 months ago
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
3 months ago
use Illuminate\Support\Facades\Schema;
5 months ago
use Illuminate\Validation\ValidationException;
class Application extends Model
{
3 months ago
public const EXCLUDE_SOFT_DELETED_SCOPE = 'excludeSoftDeleted';
5 months ago
protected $fillable = [
3 months ago
'project_code',
5 months ago
'user_id',
'competition_id',
'signup_channel_id',
'signup_channel_code',
'signup_channel_state',
'signup_channel_entered_at',
3 months ago
'recommend',
5 months ago
'status',
1 month ago
'signup_locale',
5 months ago
'player_name',
'school',
'degree',
'contact_email',
'contact_mobile',
'company_name',
'project_name',
'track',
'location_country',
'location_province',
'location_city',
'oversea_country',
1 month ago
'event_location',
'team_members',
5 months ago
'intro',
1 month ago
'answers_json',
5 months ago
'promise_signed_at',
'promise_signature',
'submitted_at',
3 months ago
'review_result',
'review_result_note',
'review_result_at',
2 months ago
'review_eligible',
5 months ago
];
2 months ago
/**
* 非选手报名表单项、由系统维护的 applications 列(其余 fillable 列均可作为 schema 标量字段)。
*
* @var list<string>
*/
public const SIGNUP_SCALAR_FILLABLE_EXCLUDE = [
'project_code',
'user_id',
'competition_id',
'signup_channel_id',
'signup_channel_code',
'signup_channel_state',
'signup_channel_entered_at',
'status',
1 month ago
'signup_locale',
2 months ago
'promise_signed_at',
'promise_signature',
'submitted_at',
'review_result',
'review_result_note',
'review_result_at',
2 months ago
'review_eligible',
1 month ago
'answers_json',
2 months ago
];
/**
* 报名表 schema 可绑定的标量列(与 $fillable 同步,新增列只需迁移 + fillable)。
*
* @return list<string>
*/
public static function signupScalarFillableKeys(): array
{
$exclude = array_flip(self::SIGNUP_SCALAR_FILLABLE_EXCLUDE);
$keys = [];
foreach ((new static)->getFillable() as $key) {
if (! isset($exclude[$key])) {
$keys[] = $key;
}
}
return $keys;
}
5 months ago
protected $casts = [
'promise_signed_at' => 'datetime',
'submitted_at' => 'datetime',
'signup_channel_entered_at' => 'datetime',
3 months ago
'deleted_at' => 'datetime',
3 months ago
'review_result_at' => 'datetime',
2 months ago
'review_eligible' => 'boolean',
1 month ago
'answers_json' => 'array',
5 months ago
];
2 months ago
/** 是否出现在评审端列表(未设置时默认参与) */
public function isReviewEligible(): bool
{
return (bool) ($this->review_eligible ?? true);
}
3 months ago
public static function hasDeletedAtColumn(): bool
{
return Schema::hasColumn((new static)->getTable(), 'deleted_at');
}
3 months ago
protected static function booted(): void
{
3 months ago
static::addGlobalScope(self::EXCLUDE_SOFT_DELETED_SCOPE, function (Builder $builder): void {
if (! static::hasDeletedAtColumn()) {
return;
}
$builder->whereNull($builder->getModel()->qualifyColumn('deleted_at'));
});
3 months ago
static::created(function (Application $application): void {
3 months ago
if (! Schema::hasColumn($application->getTable(), 'project_code')) {
return;
}
3 months ago
if (trim((string) ($application->project_code ?? '')) === '') {
$application->forceFill([
'project_code' => ProjectCode::forApplication($application),
])->saveQuietly();
}
});
}
3 months ago
public static function firstOrCreateDraft(int $userId, int $competitionId): self
{
3 months ago
if (static::hasDeletedAtColumn()) {
$trashed = static::withoutGlobalScope(self::EXCLUDE_SOFT_DELETED_SCOPE)
->where('user_id', $userId)
->where('competition_id', $competitionId)
->whereNotNull('deleted_at')
->first();
if ($trashed !== null) {
$trashed->forceFill([
'deleted_at' => null,
'status' => 'draft',
])->save();
return $trashed->fresh();
}
}
3 months ago
return static::query()->firstOrCreate(
[
'user_id' => $userId,
'competition_id' => $competitionId,
],
['status' => 'draft']
);
}
5 months ago
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function competition(): BelongsTo
{
return $this->belongsTo(Competition::class);
}
public function signupChannel(): BelongsTo
{
return $this->belongsTo(SignupChannel::class);
}
5 months ago
public function files(): HasMany
{
return $this->hasMany(ApplicationFile::class);
}
public function reviewRecords(): HasMany
{
return $this->hasMany(ApplicationReviewRecord::class);
}
public function reviewScores(): HasMany
{
return $this->hasMany(ApplicationReviewScore::class);
}
public function isSubmitted(): bool
{
return $this->status === 'submitted';
}
/** 评委已产生评审记录或打分后,选手端不可再修改报名/附件 */
public function isSignupLockedByReview(): bool
{
return $this->reviewRecords()->exists() || $this->reviewScores()->exists();
}
/** 已超过赛事设置的报名截止时间(未设置截止时间则不据此拦截) */
public function isPastSignupCloseForCompetition(): bool
{
$this->loadMissing('competition');
$close = $this->competition?->signup_close_at;
return $close !== null && now()->isAfter($close);
}
/**
* 选手是否仍可编辑报名表:报名截止时间内且评委尚未评审;草稿与已提交均可继续改报直至截止或评审落库。
*/
public function participantMayEditSignup(): bool
{
if ($this->isSignupLockedByReview()) {
return false;
}
return ! $this->isPastSignupCloseForCompetition();
}
/**
* 选手是否可上传/删除附件:截止前与表单一致;截止后仅当赛事开启补传且登录手机号在白名单。
* 已有评审/打分时始终不可改附件。
*/
public function participantMayEditFiles(): bool
{
if ($this->isSignupLockedByReview()) {
return false;
}
if (! $this->isPastSignupCloseForCompetition()) {
return true;
}
return $this->isAllowlistedForPostCloseFileEdit();
}
public function isAllowlistedForPostCloseFileEdit(): bool
{
$this->loadMissing(['competition', 'user']);
return $this->competition?->allowsPostCloseFileEditForMobile($this->user?->mobile) ?? false;
}
5 months ago
/**
5 months ago
* @param 'status'|'file' $field 422 时 errors 字段名(报名表用 status,附件接口用 file)
5 months ago
*/
public function assertMayEditSignup(string $field = 'status'): void
{
if ($this->isSignupLockedByReview()) {
$msg = $field === 'file'
? '已有评委评审或打分记录,不可再上传或删除附件'
: '已有评委评审或打分记录,报名内容不可再修改';
throw ValidationException::withMessages([$field => [$msg]]);
}
if ($this->isPastSignupCloseForCompetition()) {
if ($field === 'file' && $this->isAllowlistedForPostCloseFileEdit()) {
return;
}
5 months ago
$msg = $field === 'file'
? '报名已截止,不可再上传或删除附件'
: '报名已截止,无法再修改或提交报名';
throw ValidationException::withMessages([$field => [$msg]]);
}
}
}