From b55a014c591e783b9ca1bab81db8e00962905d68 Mon Sep 17 00:00:00 2001 From: xiaomlove Date: Sat, 13 Apr 2024 13:55:32 +0800 Subject: [PATCH] exam add recurring --- .../Resources/System/ExamResource.php | 7 ++ app/Models/Exam.php | 80 ++++++++++++++----- app/Repositories/ExamRepository.php | 49 +++++++++--- ...013_add_recurring_field_to_exams_table.php | 32 ++++++++ resources/lang/zh_CN/exam.php | 8 +- 5 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 database/migrations/2024_04_13_042013_add_recurring_field_to_exams_table.php diff --git a/app/Filament/Resources/System/ExamResource.php b/app/Filament/Resources/System/ExamResource.php index 2ce072a33..c0d53b51b 100644 --- a/app/Filament/Resources/System/ExamResource.php +++ b/app/Filament/Resources/System/ExamResource.php @@ -91,6 +91,12 @@ public static function form(Form $form): Form ->columnSpan(['sm' => 2]) ->label(__('label.duration')) ->helperText(__('label.exam.duration_help')), + Forms\Components\Select::make('recurring') + ->options(Exam::listRecurringOptions()) + ->label(__('exam.recurring')) + ->helperText(__('exam.recurring_help')) + ->columnSpan(['sm' => 2]) + , ])->columns(2), Forms\Components\Section::make(__('label.exam.section_target_user'))->schema([ @@ -122,6 +128,7 @@ public static function table(Table $table): Table Tables\Columns\TextColumn::make('begin')->label(__('label.begin')), Tables\Columns\TextColumn::make('end')->label(__('label.end')), Tables\Columns\TextColumn::make('durationText')->label(__('label.duration')), + Tables\Columns\TextColumn::make('recurringText')->label(__('exam.recurring')), Tables\Columns\TextColumn::make('filterFormatted')->label(__('label.exam.filter_formatted'))->html(), Tables\Columns\BooleanColumn::make('is_discovered')->label(__('label.exam.is_discovered')), Tables\Columns\TextColumn::make('priority')->label(__('label.priority')), diff --git a/app/Models/Exam.php b/app/Models/Exam.php index c58a0a421..eaf36b495 100644 --- a/app/Models/Exam.php +++ b/app/Models/Exam.php @@ -9,7 +9,10 @@ class Exam extends NexusModel { - protected $fillable = ['name', 'description', 'begin', 'end', 'duration', 'status', 'is_discovered', 'filters', 'indexes', 'priority']; + protected $fillable = [ + 'name', 'description', 'begin', 'end', 'duration', 'status', 'is_discovered', 'filters', 'indexes', 'priority', + 'recurring', + ]; public $timestamps = true; @@ -62,6 +65,9 @@ class Exam extends NexusModel self::FILTER_USER_REGISTER_DAYS_RANGE => ['name' => 'User register days range'], ]; + const RECURRING_WEEKLY = "Weekly"; + const RECURRING_MONTHLY = "Monthly"; + protected static function booted() { static::saving(function (Model $model) { @@ -84,6 +90,19 @@ public static function listIndex($onlyKeyValue = false): array return $result; } + public static function listRecurringOptions(): array + { + return [ + self::RECURRING_WEEKLY => nexus_trans("exam.recurring_weekly"), + self::RECURRING_MONTHLY => nexus_trans("exam.recurring_monthly"), + ]; + } + protected function getRecurringTextAttribute(): string + { + $options = self::listRecurringOptions(); + return $options[$this->recurring] ?? ''; + } + public function getStatusTextAttribute(): string { return $this->status == self::STATUS_ENABLED ? nexus_trans('label.enabled') : nexus_trans('label.disabled'); @@ -164,28 +183,51 @@ public function getFilterFormattedAttribute(): string return implode("
", $arr); } - protected function beginForUser(): Attribute + public function getBeginForUser(): Carbon { - return new Attribute( - get: fn ($value) => $value ? Carbon::parse($value) : Carbon::now() - ); + if (!empty($this->begin)) { + return Carbon::parse($this->begin); + } + if (!empty($this->recurring)) { + return $this->getRecurringBegin(Carbon::now()); + } + return Carbon::now(); } - protected function endForUser(): Attribute + public function getEndForUser(): Carbon { - return new Attribute( - get: function ($value, $attributes) { - if ($value) { - return Carbon::parse($value); - } - if (!empty($attributes['duration'])) { - /** @var Carbon $begin */ - $begin = $this->begin_for_user; - return $begin->clone()->addDays($attributes['duration']); - } - throw new \RuntimeException("No specific end or duration"); - } - ); + if (!empty($this->end)) { + return Carbon::parse($this->end); + } + if (!empty($this->duration)) { + return $this->getBeginForUser()->clone()->addDays($this->duration); + } + if (!empty($this->recurring)) { + return $this->getRecurringEnd(Carbon::now()); + } + throw new \RuntimeException(nexus_trans("exam.time_condition_invalid")); + } + + public function getRecurringBegin(Carbon $time): Carbon + { + $recurring = $this->recurring; + if ($recurring == self::RECURRING_WEEKLY) { + return $time->startOfWeek(); + } elseif ($recurring == self::RECURRING_MONTHLY) { + return $time->startOfMonth(); + } + throw new \RuntimeException("Invalid recurring: $recurring"); + } + + public function getRecurringEnd(Carbon $time): Carbon + { + $recurring = $this->recurring; + if ($recurring == self::RECURRING_WEEKLY) { + return $time->endOfWeek(); + } elseif ($recurring == self::RECURRING_MONTHLY) { + return $time->endOfMonth(); + } + throw new \RuntimeException("Invalid recurring: $recurring"); } } diff --git a/app/Repositories/ExamRepository.php b/app/Repositories/ExamRepository.php index 517a1effa..2e1977b66 100644 --- a/app/Repositories/ExamRepository.php +++ b/app/Repositories/ExamRepository.php @@ -101,14 +101,29 @@ private function checkIndexes(array $params): bool private function checkBeginEnd(array $params): bool { - if (!empty($params['begin']) && !empty($params['end']) && empty($params['duration'])) { + if ( + !empty($params['begin']) && !empty($params['end']) + && empty($params['duration']) + && empty($params['recurring']) + ) { return true; } - if (empty($params['begin']) && empty($params['end']) && isset($params['duration']) && ctype_digit((string)$params['duration']) && $params['duration'] > 0) { + if ( + empty($params['begin']) && empty($params['end']) + && isset($params['duration']) && ctype_digit((string)$params['duration']) && $params['duration'] > 0 + && empty($params['recurring']) + ) { + return true; + } + if ( + empty($params['begin']) && empty($params['end']) + && empty($params['duration']) + && !empty($params['recurring']) + ) { return true; } - throw new \InvalidArgumentException("Require begin and end or only duration."); + throw new \InvalidArgumentException(nexus_trans("exam.time_condition_invalid")); } private function checkFilters(array $params) @@ -231,7 +246,7 @@ public function listValid($excludeId = null, $isDiscovered = null) $now = Carbon::now(); $query = Exam::query() ->where('status', Exam::STATUS_ENABLED) - ->whereRaw("if(begin is not null and end is not null, begin <= '$now' and end >= '$now', duration > 0)") + ->whereRaw("if(begin is not null and end is not null, begin <= '$now' and end >= '$now', duration > 0 or recurring is not null)") ; if (!is_null($excludeId)) { @@ -330,6 +345,7 @@ private function isExamMatchUser(Exam $exam, $user): bool public function assignToUser(int $uid, int $examId, $begin = null, $end = null) { $logPrefix = "uid: $uid, examId: $examId, begin: $begin, end: $end"; + /** @var Exam $exam */ $exam = Exam::query()->find($examId); $user = User::query()->findOrFail($uid); if (Auth::user()->class <= $user->class) { @@ -349,12 +365,12 @@ public function assignToUser(int $uid, int $examId, $begin = null, $end = null) 'exam_id' => $exam->id, ]; if (empty($begin)) { - $begin = $exam->begin_for_user; + $begin = $exam->getBeginForUser(); } else { $begin = Carbon::parse($begin); } if (empty($end)) { - $end = $exam->end_for_user; + $end = $exam->getEndForUser(); } else { $end = Carbon::parse($end); } @@ -1003,8 +1019,8 @@ public function fetchUserAndDoAssign(Exam $exam): bool|int $size = 1000; $minId = 0; $result = 0; - $begin = $exam->begin_for_user; - $end = $exam->end_for_user; + $begin = $exam->getBeginForUser(); + $end = $exam->getEndForUser(); while (true) { $logPrefix = sprintf('[%s], exam: %s, size: %s', __FUNCTION__, $exam->id , $size); $users = (clone $baseQuery)->where("$userTable.id", ">", $minId)->limit($size)->get(); @@ -1069,10 +1085,12 @@ public function cronjobCheckout($ignoreTimeRange = false): int $result += $examUsers->count(); $now = Carbon::now()->toDateTimeString(); $examUserIdArr = $uidToDisable = $messageToSend = $userBanLog = $userModcommentUpdate = []; + $examUserToInsert = []; foreach ($examUsers as $examUser) { $minId = $examUser->id; $examUserIdArr[] = $examUser->id; $uid = $examUser->uid; + /** @var Exam $exam */ $exam = $examUser->exam; $currentLogPrefix = sprintf("$logPrefix, user: %s, exam: %s, examUser: %s", $uid, $examUser->exam_id, $examUser->id); if (!$examUser->user) { @@ -1124,8 +1142,18 @@ public function cronjobCheckout($ignoreTimeRange = false): int 'subject' => $subject, 'msg' => $msg ]; + if (!empty($exam->recurring) && $this->isExamMatchUser($exam, $examUser->user)) { + $examUserToInsert[] = [ + 'uid' => $examUser->user->id, + 'exam_id' => $exam->id, + 'begin' => $exam->getBeginForUser(), + 'end' => $exam->getEndForUser(), + 'created_at' => $now, + 'updated_at' => $now, + ]; + } } - DB::transaction(function () use ($uidToDisable, $messageToSend, $examUserIdArr, $userBanLog, $userModcommentUpdate, $userTable, $logPrefix) { + DB::transaction(function () use ($uidToDisable, $messageToSend, $examUserIdArr, $examUserToInsert, $userBanLog, $userModcommentUpdate, $userTable, $logPrefix) { ExamUser::query()->whereIn('id', $examUserIdArr)->update(['status' => ExamUser::STATUS_FINISHED]); do { $deleted = ExamProgress::query()->whereIn('exam_user_id', $examUserIdArr)->limit(10000)->delete(); @@ -1144,6 +1172,9 @@ public function cronjobCheckout($ignoreTimeRange = false): int if (!empty($userBanLog)) { UserBanLog::query()->insert($userBanLog); } + if (!empty($examUserToInsert)) { + ExamUser::query()->insert($examUserToInsert); + } }); } return $result; diff --git a/database/migrations/2024_04_13_042013_add_recurring_field_to_exams_table.php b/database/migrations/2024_04_13_042013_add_recurring_field_to_exams_table.php new file mode 100644 index 000000000..81f557928 --- /dev/null +++ b/database/migrations/2024_04_13_042013_add_recurring_field_to_exams_table.php @@ -0,0 +1,32 @@ +string("recurring")->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('exams', function (Blueprint $table) { + $table->dropColumn("recurring"); + }); + } +}; diff --git a/resources/lang/zh_CN/exam.php b/resources/lang/zh_CN/exam.php index caefe846b..287a69372 100644 --- a/resources/lang/zh_CN/exam.php +++ b/resources/lang/zh_CN/exam.php @@ -32,5 +32,11 @@ 'list' => [ 'page_title' => '考核列表' ] - ] + ], + 'recurring' => '周期性', + 'recurring_weekly' => '每周一次', + 'recurring_monthly' => '每月一次', + 'recurring_help' => '如果指定为周期性,考核开始时间为当前周期的开始时间,结束时间为当前周期的结束时间,这里说的都是自然周/月。每个周期结束后,如果用户仍然满足筛选条件,会自动为用户分配下个周期的任务。', + + 'time_condition_invalid' => '时间参数不合理,有且只有三项之一:开始时间+结束时间/时长/周期性', ];