|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
|
|
|
|
|
|
|
|
class Competition extends Model
|
|
|
|
|
|
{
|
|
|
|
|
|
use HasFactory;
|
|
|
|
|
|
|
|
|
|
|
|
public const STATUSES = [
|
|
|
|
|
|
'draft',
|
|
|
|
|
|
'published',
|
|
|
|
|
|
'signup_open',
|
|
|
|
|
|
'signup_closed',
|
|
|
|
|
|
'reviewing',
|
|
|
|
|
|
'ended',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
|
'slug',
|
|
|
|
|
|
'name',
|
|
|
|
|
|
'description',
|
|
|
|
|
|
'pledge_title',
|
|
|
|
|
|
'pledge_title_en',
|
|
|
|
|
|
'pledge_subtitle',
|
|
|
|
|
|
'pledge_subtitle_en',
|
|
|
|
|
|
'pledge_content_html',
|
|
|
|
|
|
'pledge_content_html_en',
|
|
|
|
|
|
'status',
|
|
|
|
|
|
'published',
|
|
|
|
|
|
'signup_open_at',
|
|
|
|
|
|
'signup_close_at',
|
|
|
|
|
|
'form_schema_id',
|
|
|
|
|
|
'review_form_schema_id',
|
|
|
|
|
|
'audience_form_schema_id',
|
|
|
|
|
|
'branding_json',
|
|
|
|
|
|
'settings',
|
|
|
|
|
|
'scoring_rules_json',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @var array<string, string>
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected $casts = [
|
|
|
|
|
|
'published' => 'boolean',
|
|
|
|
|
|
'signup_open_at' => 'datetime',
|
|
|
|
|
|
'signup_close_at' => 'datetime',
|
|
|
|
|
|
'branding_json' => 'array',
|
|
|
|
|
|
'settings' => 'array',
|
|
|
|
|
|
'scoring_rules_json' => 'array',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
public function tracks(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(CompetitionTrack::class)->orderBy('sort');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function signupChannels(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(SignupChannel::class)->orderByDesc('id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function audienceSessions(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(AudienceSession::class)->orderBy('sort')->orderBy('id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function competitionAdmins(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(CompetitionAdmin::class);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 嵌套路由 competitions.admins 的绑定关系名 */
|
|
|
|
|
|
public function admins(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->competitionAdmins();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function audienceRegistrations(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(AudienceRegistration::class);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function formSchemaDefinitions(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(FormSchemaDefinition::class, 'competition_id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 嵌套路由参数 `{form_schema}` 的隐式绑定约定关系名为 formSchemas(见 Model::childRouteBindingRelationshipName)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function formSchemas(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->formSchemaDefinitions();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function formSchema(): BelongsTo
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->belongsTo(FormSchemaDefinition::class, 'form_schema_id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function reviewFormSchema(): BelongsTo
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->belongsTo(FormSchemaDefinition::class, 'review_form_schema_id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function audienceFormSchema(): BelongsTo
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->belongsTo(FormSchemaDefinition::class, 'audience_form_schema_id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 评审端是否开放登录/访问。settings.review_portal.enabled 缺省或非 false 时视为开启。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function isReviewPortalEnabled(): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
$portal = $this->reviewPortalSettings();
|
|
|
|
|
|
if ($portal === null || ! array_key_exists('enabled', $portal)) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $portal['enabled'] !== false
|
|
|
|
|
|
&& $portal['enabled'] !== 0
|
|
|
|
|
|
&& $portal['enabled'] !== '0'
|
|
|
|
|
|
&& $portal['enabled'] !== 'false';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关闭评审端时展示给评审员的说明(登录弹窗 / 接口提示)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function reviewPortalClosedMessage(): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$portal = $this->reviewPortalSettings();
|
|
|
|
|
|
$message = is_array($portal) ? trim((string) ($portal['message'] ?? '')) : '';
|
|
|
|
|
|
if ($message === '') {
|
|
|
|
|
|
return '评审通道暂未开放,请稍后再试';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return mb_substr($message, 0, 2000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 公开接口用的评审端开关摘要。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array{enabled: bool, message: string}
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function reviewPortalPublicPayload(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$enabled = $this->isReviewPortalEnabled();
|
|
|
|
|
|
$portal = $this->reviewPortalSettings();
|
|
|
|
|
|
$stored = is_array($portal) ? trim((string) ($portal['message'] ?? '')) : '';
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'enabled' => $enabled,
|
|
|
|
|
|
'message' => $enabled
|
|
|
|
|
|
? mb_substr($stored, 0, 2000)
|
|
|
|
|
|
: $this->reviewPortalClosedMessage(),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @return array<string, mixed>|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function reviewPortalSettings(): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
$settings = is_array($this->settings) ? $this->settings : [];
|
|
|
|
|
|
$portal = $settings['review_portal'] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
return is_array($portal) ? $portal : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 报名截止后是否开启「指定用户仅可补传/删除附件」。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function isPostCloseFileEditEnabled(): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
$cfg = $this->postCloseFileEditSettings();
|
|
|
|
|
|
if ($cfg === null || ! array_key_exists('enabled', $cfg)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $cfg['enabled'] === true
|
|
|
|
|
|
|| $cfg['enabled'] === 1
|
|
|
|
|
|
|| $cfg['enabled'] === '1'
|
|
|
|
|
|
|| $cfg['enabled'] === 'true';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @return list<string> 规范化后的登录手机号白名单
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function postCloseFileEditMobiles(): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$cfg = $this->postCloseFileEditSettings();
|
|
|
|
|
|
$raw = is_array($cfg) ? ($cfg['user_mobiles'] ?? null) : null;
|
|
|
|
|
|
if (! is_array($raw)) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out = [];
|
|
|
|
|
|
foreach ($raw as $item) {
|
|
|
|
|
|
$mobile = self::normalizeAllowlistMobile($item);
|
|
|
|
|
|
// 勿用手机号做关联数组 key:PHP 会把纯数字字符串键转成 int,导致严格比较失败
|
|
|
|
|
|
if ($mobile !== '' && ! in_array($mobile, $out, true)) {
|
|
|
|
|
|
$out[] = $mobile;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function allowsPostCloseFileEditForMobile(?string $mobile): bool
|
|
|
|
|
|
{
|
|
|
|
|
|
if (! $this->isPostCloseFileEditEnabled()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$normalized = self::normalizeAllowlistMobile($mobile);
|
|
|
|
|
|
if ($normalized === '') {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return in_array($normalized, $this->postCloseFileEditMobiles(), true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static function normalizeAllowlistMobile(mixed $mobile): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$s = preg_replace('/\s+/', '', trim((string) $mobile)) ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
return $s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @return array<string, mixed>|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function postCloseFileEditSettings(): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
$settings = is_array($this->settings) ? $this->settings : [];
|
|
|
|
|
|
$cfg = $settings['post_close_file_edit'] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
return is_array($cfg) ? $cfg : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|