[exam] schedule

This commit is contained in:
xiaomlove
2021-04-29 19:18:13 +08:00
parent a1972ea288
commit 164bc80c4e
14 changed files with 315 additions and 92 deletions

View File

@@ -42,7 +42,7 @@
</el-menu> </el-menu>
</el-aside> </el-aside>
<el-container class="content"> <el-container class="content">
<Header /> <Header :router-name="state.routerName"/>
<div class="main"> <div class="main">
<router-view /> <router-view />
</div> </div>
@@ -56,7 +56,7 @@
</template> </template>
<script> <script>
import { reactive } from 'vue' import { reactive, onMounted } from 'vue'
import Header from './components/Header.vue' import Header from './components/Header.vue'
import Footer from './components/Footer.vue' import Footer from './components/Footer.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -76,7 +76,11 @@ export default {
currentPath: '/dashboard', currentPath: '/dashboard',
count: { count: {
number: 1 number: 1
} },
routerName: router.name
})
onMounted(() => {
console.log(router)
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
console.log("App beforeEach to", to) console.log("App beforeEach to", to)

View File

@@ -31,38 +31,42 @@
<script> <script>
import {computed, onMounted, reactive, toRefs, watch} from 'vue' import {computed, onMounted, reactive, toRefs, watch} from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { localRemove, pathMap } from '../utils' import {localGet, localSet, localRemove, pathMap} from '../utils'
import api from "../utils/api"; import api from "../utils/api";
export default { export default {
name: 'Header', name: 'Header',
props: {
userInfo: {
type: Object
}
},
setup(props, context) { setup(props, context) {
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const userInfoKey = 'userInfo'
const state = reactive({ const state = reactive({
name: 'dashboard', name: 'dashboard',
userInfo: null, userInfo: null,
hasBack: false hasBack: false
}) })
onMounted(() => { onMounted(async () => {
console.log("Head onMounted!") console.log("Head onMounted!")
console.log(props, context) console.log(props)
// let userInfo = localGet(userInfoKey);
// if (userInfo) {
// state.userInfo = userInfo;
// } else {
// let res = await api.getUserBase()
// state.userInfo = res.data
// localSet(userInfoKey, res.data)
// }
}) })
watch(
() => route,
(newValue, oldValue) => {
console.log(newValue, 'new')
console.log(oldValue, 'old')
}
)
const getUserInfo = async () => {
const userInfo = await api.getUserBase()
console.log(userInfo)
state.userInfo = userInfo.data
}
const logout = () => { const logout = () => {
api.logout().then(() => { api.logout().then(() => {
localRemove('token') localRemove('token')
localRemove(userInfoKey)
router.push({ name: 'login' }) router.push({ name: 'login' })
}) })
} }

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Repositories\ExamRepository;
use Illuminate\Console\Command;
class ExamAssign extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'exam:assign {--uid=} {--exam_id=} {--begin=} {--end=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Assign exam to user, options: --uid, --exam_id, --begin, --end';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$examRep = new ExamRepository();
$uid = $this->option('uid');
$examId = $this->option('exam_id');
$begin = $this->option('begin');
$end = $this->option('end');
$this->info(sprintf('uid: %s, examId: %s, begin: %s, end: %s', $uid, $examId, $begin, $end));
$result = $examRep->assignToUser($uid, $examId, $begin, $end);
$this->info(sprintf('%s, [assignToUser], result: %s, request_id: %s', __METHOD__, var_export($result, true), REQUEST_ID));
return 0;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use App\Repositories\ExamRepository;
use Illuminate\Console\Command;
class ExamAssignCronjob extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'exam:assign_cronjob';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Assign exam cronjob';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$examRep = new ExamRepository();
$result = $examRep->cronjonAssign();
$this->info(sprintf('%s, [cronjonAssign], result: %s, request_id: %s', __METHOD__, var_export($result, true), REQUEST_ID));
return 0;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use App\Repositories\ExamRepository;
use Illuminate\Console\Command;
class ExamCheckoutCronjob extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'exam:checkout_cronjob {--ignore-time-range}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Checkout exam cronjob, options: --ignore-time-range';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$examRep = new ExamRepository();
$ignoreTimeRange = $this->option('ignore-time-range');
$this->info('ignore-time-range: ' . var_export($ignoreTimeRange, true));
$result = $examRep->cronjobCheckout($ignoreTimeRange);
$this->info(sprintf('%s, [cronjobCheckout], result: %s, request_id: %s', __METHOD__, var_export($result, true), REQUEST_ID));
return 0;
}
}

View File

@@ -44,12 +44,19 @@ class Test extends Command
*/ */
public function handle() public function handle()
{ {
$user = User::query()->findOrFail(1);
dd(nexus_trans('exam.checkout_pass_message_content', ['exam_name' => '年中考核', 'begin' => 1, 'end' => 2]));
$rep = new ExamRepository(); $rep = new ExamRepository();
// $r = $rep->assignToUser(1, 1); // $r = $rep->assignToUser(1, 1);
// $r = $rep->addProgress(27, 4, 1025, 1); $r = $rep->addProgress(3, 1, [
// dd($r); 1 => 25*1024*1024*1024,
2 => 55*3600,
3 => 100*1024*1024*1024,
4 => 1252
]);
dd($r);
// $rep->assignCronjob(); // $rep->assignCronjob();
$rep->cronjobCheckout(); // $rep->cronjobCheckout();
} }
} }

View File

@@ -24,7 +24,8 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
// $schedule->command('inspire')->hourly(); $schedule->command('exam:assign_cronjob')->everyMinute();
$schedule->command('exam:checkout_cronjob')->everyMinute();
} }
/** /**

View File

@@ -29,6 +29,17 @@ class ExamUser extends NexusModel
return self::$status[$this->status]['text'] ?? ''; return self::$status[$this->status]['text'] ?? '';
} }
public function getBeginAttribute()
{
return $this->begin ?? $this->exam->begin;
}
public function getEndAttribute()
{
return $this->end ?? $this->exam->end;
}
public function exam(): \Illuminate\Database\Eloquent\Relations\BelongsTo public function exam(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{ {
return $this->belongsTo(Exam::class, 'exam_id'); return $this->belongsTo(Exam::class, 'exam_id');

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Http\Middleware\Locale;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -117,6 +118,11 @@ class User extends Authenticatable
return true; return true;
} }
public function getLocaleAttribute()
{
return Locale::$languageMaps[$this->language->site_lang_folder] ?? 'en';
}
public function exams() public function exams()
{ {

View File

@@ -155,7 +155,7 @@ class ExamRepository extends BaseRepository
return false; return false;
} }
if ($added < $registerTimeBegin) { if ($added < $registerTimeBegin) {
do_log("$logPrefix, added: $added not after: " . $registerTimeBegin); do_log("$logPrefix, user added: $added not after: " . $registerTimeBegin);
return false; return false;
} }
@@ -164,7 +164,7 @@ class ExamRepository extends BaseRepository
return false; return false;
} }
if ($added > $registerTimeEnd) { if ($added > $registerTimeEnd) {
do_log("$logPrefix, added: $added not before: " . $registerTimeEnd); do_log("$logPrefix, user added: $added not before: " . $registerTimeEnd);
return false; return false;
} }
return true; return true;
@@ -186,17 +186,20 @@ class ExamRepository extends BaseRepository
if ($examId > 0) { if ($examId > 0) {
$exam = Exam::query()->find($examId); $exam = Exam::query()->find($examId);
} else { } else {
$exams = $this->listMatchExam($uid); $exams = $this->listValid();
if ($exams->isEmpty()) {
throw new NexusException("No valid exam.");
}
if ($exams->count() > 1) { if ($exams->count() > 1) {
do_log(last_query()); do_log(last_query());
throw new NexusException("Match exam more than 1."); throw new NexusException("valid exam more than 1.");
} }
$exam = $exams->first(); $exam = $exams->first();
} }
if (!$exam) {
throw new NexusException("No valid exam.");
}
$user = User::query()->findOrFail($uid); $user = User::query()->findOrFail($uid);
if (!$this->isExamMatchUser($exam, $user)) {
throw new NexusException("Exam: {$exam->id} no match this user.");
}
if ($user->exams()->where('status', ExamUser::STATUS_NORMAL)->exists()) { if ($user->exams()->where('status', ExamUser::STATUS_NORMAL)->exists()) {
throw new NexusException("User: $uid already has exam on the way."); throw new NexusException("User: $uid already has exam on the way.");
} }
@@ -404,7 +407,7 @@ class ExamRepository extends BaseRepository
$requireValueAtomic = $requireValue * 1024 * 1024 * 1024; $requireValueAtomic = $requireValue * 1024 * 1024 * 1024;
break; break;
case Exam::INDEX_SEED_TIME_AVERAGE: case Exam::INDEX_SEED_TIME_AVERAGE:
$currentValueFormatted = mkprettytime($currentValue); $currentValueFormatted = number_format($currentValue / 3600, 2) . " {$index['unit']}";
$requireValueAtomic = $requireValue * 3600; $requireValueAtomic = $requireValue * 3600;
break; break;
default: default:
@@ -437,7 +440,7 @@ class ExamRepository extends BaseRepository
$exams = $this->listValid(); $exams = $this->listValid();
if ($exams->isEmpty()) { if ($exams->isEmpty()) {
do_log("No valid exam."); do_log("No valid exam.");
return true; return false;
} }
if ($exams->count() > 1) { if ($exams->count() > 1) {
do_log("Valid exam more than 1.", "warning"); do_log("Valid exam more than 1.", "warning");
@@ -446,103 +449,118 @@ class ExamRepository extends BaseRepository
$exam = $exams->first(); $exam = $exams->first();
$userTable = (new User())->getTable(); $userTable = (new User())->getTable();
$examUserTable = (new ExamUser())->getTable(); $examUserTable = (new ExamUser())->getTable();
User::query() $baseQuery = User::query()
->leftJoin($examUserTable, function (JoinClause $join) use ($examUserTable, $userTable) { ->leftJoin($examUserTable, function (JoinClause $join) use ($examUserTable, $userTable) {
$join->on("$userTable.id", "=", "$examUserTable.uid") $join->on("$userTable.id", "=", "$examUserTable.uid")
->on("$examUserTable.status", "=", DB::raw(ExamUser::STATUS_NORMAL)); ->on("$examUserTable.status", "=", DB::raw(ExamUser::STATUS_NORMAL));
}) })
->whereRaw("$examUserTable.id is null") ->whereRaw("$examUserTable.id is null")
->selectRaw("$userTable.*") ->selectRaw("$userTable.*")
->chunk(100, function ($users) use ($exam) { ->orderBy("$userTable.id", "asc");
do_log("user count: " . $users->count() . last_query()); $size = 100;
$insert = []; $minId = 0;
$now = Carbon::now()->toDateTimeString(); $result = 0;
foreach ($users as $user) { while (true) {
$logPrefix = sprintf('[assignCronjob] user: %s, exam: %s', $user->id, $exam->id); $logPrefix = sprintf('[%s], exam: %s, size: %s', __FUNCTION__, $exam->id , $size);
if (!$this->isExamMatchUser($exam, $user)) { $users = (clone $baseQuery)->where("$userTable.id", ">", $minId)->limit($size)->get();
do_log("$logPrefix, exam not match user."); if ($users->isEmpty()) {
continue; do_log("$logPrefix, no more data..." . last_query());
} break;
$insert[] = [ }
'uid' => $user->id, $insert = [];
'exam_id' => $exam->id, $now = Carbon::now()->toDateTimeString();
'created_at' => $now, foreach ($users as $user) {
'updated_at' => $now, $minId = $user->id;
]; $currentLogPrefix = sprintf("$logPrefix, user: %s", $user->id);
do_log("$logPrefix, exam assign to user."); if (!$this->isExamMatchUser($exam, $user)) {
do_log("$currentLogPrefix, exam not match this user.");
continue;
} }
$insert[] = [
'uid' => $user->id,
'exam_id' => $exam->id,
'created_at' => $now,
'updated_at' => $now,
];
do_log("$currentLogPrefix, exam will be assigned to this user.");
}
if (!empty($insert)) {
$result += count($insert);
ExamUser::query()->insert($insert); ExamUser::query()->insert($insert);
}); }
return true; }
return $result;
} }
public function cronjobCheckout() public function cronjobCheckout($ignoreTimeRange = false)
{ {
$now = Carbon::now()->toDateTimeString(); $now = Carbon::now()->toDateTimeString();
$examUserTable = (new ExamUser())->getTable(); $examUserTable = (new ExamUser())->getTable();
$examTable = (new Exam())->getTable(); $examTable = (new Exam())->getTable();
$perPage = 100; $baseQuery = ExamUser::query()
$page = 1;
$query = ExamUser::query()
->join($examTable, "$examUserTable.exam_id", "=", "$examTable.id") ->join($examTable, "$examUserTable.exam_id", "=", "$examTable.id")
->where("$examUserTable.status", ExamUser::STATUS_NORMAL) ->where("$examUserTable.status", ExamUser::STATUS_NORMAL)
->whereRaw("if($examUserTable.begin is not null, $examUserTable.begin <= '$now', $examTable.begin <= '$now')")
->whereRaw("if($examUserTable.end is not null, $examUserTable.end >= '$now', $examTable.end >= '$now')")
->selectRaw("$examUserTable.*") ->selectRaw("$examUserTable.*")
->with(['exam', 'user', 'user.language']) ->with(['exam', 'user', 'user.language'])
->orderBy("$examUserTable.id", "asc"); ->orderBy("$examUserTable.id", "asc");
if (!$ignoreTimeRange) {
$baseQuery->whereRaw("if($examUserTable.end is not null, $examUserTable.end < '$now', $examTable.end < '$now')");
}
$size = 100;
$minId = 0;
$result = 0;
while (true) { while (true) {
$logPrefix = sprintf('[%s], page: %s', __FUNCTION__, $page); $logPrefix = sprintf('[%s], size: %s', __FUNCTION__, $size);
$examUsers = $query->forPage($page, $perPage)->get("$examUserTable.*"); $examUsers = (clone $baseQuery)->where("$examUserTable.id", ">", $minId)->limit($size)->get();
if ($examUsers->isEmpty()) { if ($examUsers->isEmpty()) {
do_log("$logPrefix, no more data..." . last_query()); do_log("$logPrefix, no more data..." . last_query());
break; break;
} else { } else {
do_log("$logPrefix, fetch exam users: {$examUsers->count()}, " . last_query()); do_log("$logPrefix, fetch exam users: {$examUsers->count()}");
} }
$result += $examUsers->count();
$now = Carbon::now()->toDateTimeString(); $now = Carbon::now()->toDateTimeString();
$idArr = $uidToDisable = $messageToSend = []; $examUserIdArr = $uidToDisable = $messageToSend = [];
foreach ($examUsers as $examUser) { foreach ($examUsers as $examUser) {
$idArr[] = $examUser->id; $minId = $examUser->id;
$examUserIdArr[] = $examUser->id;
$uid = $examUser->uid; $uid = $examUser->uid;
$currentLogPrefix = sprintf("$logPrefix, user: %s, exam: %s, examUser: %s", $uid, $examUser->exam_id, $examUser->id); $currentLogPrefix = sprintf("$logPrefix, user: %s, exam: %s, examUser: %s", $uid, $examUser->exam_id, $examUser->id);
$locale = $examUser->user->locale;
if ($examUser->is_done) { if ($examUser->is_done) {
do_log("$currentLogPrefix, [is_done]"); do_log("$currentLogPrefix, [is_done]");
$messageToSend[] = [ $subjectTransKey = 'exam.checkout_pass_message_subject';
'receiver' => $uid, $msgTransKey = 'exam.checkout_pass_message_content';
'added' => $now,
'subject' => 'Exam passed!',
'msg' => sprintf(
'Congratulations! You have complete the exam: %s in time(%s ~ %s)!',
$examUser->exam->name, $examUser->begin ?? $examUser->exam->begin, $examUser->end ?? $examUser->exam->end
),
];
} else { } else {
do_log("$currentLogPrefix, [will be banned]"); do_log("$currentLogPrefix, [will be banned]");
$subjectTransKey = 'exam.checkout_not_pass_message_subject';
$msgTransKey = 'exam.checkout_not_pass_message_content';
//ban user //ban user
$uidToDisable[] = $uid; $uidToDisable[] = $uid;
$messageToSend[] = [
'receiver' => $uid,
'added' => $now,
'subject' => 'Exam not passed! And your account is banned!',
'msg' => sprintf(
'You did not complete the exam: %s in time(%s ~ %s), so your account has been banned!',
$examUser->exam->name, $examUser->begin ?? $examUser->exam->begin, $examUser->end ?? $examUser->exam->end
),
];
} }
$subject = nexus_trans($subjectTransKey, [], $locale);
$msg = nexus_trans($msgTransKey, [
'exam_name' => $examUser->exam->name,
'begin' => $examUser->begin,
'end' => $examUser->end
], $locale);
$messageToSend[] = [
'receiver' => $uid,
'added' => $now,
'subject' => $subject,
'msg' => $msg
];
} }
DB::transaction(function () use ($uidToDisable, $messageToSend, $idArr) { DB::transaction(function () use ($uidToDisable, $messageToSend, $examUserIdArr) {
ExamUser::query()->whereIn('id', $idArr)->update(['status' => ExamUser::STATUS_FINISHED]); ExamUser::query()->whereIn('id', $examUserIdArr)->update(['status' => ExamUser::STATUS_FINISHED]);
Message::query()->insert($messageToSend); Message::query()->insert($messageToSend);
if (!empty($uidToDisable)) { if (!empty($uidToDisable)) {
User::query()->whereIn('id', $uidToDisable)->update(['enabled' => User::ENABLED_NO]); User::query()->whereIn('id', $uidToDisable)->update(['enabled' => User::ENABLED_NO]);
} }
}); });
$page++;
} }
return true; return $result;
} }

View File

@@ -540,17 +540,32 @@ function nexus_trans($key, $replace = [], $locale = null)
return trans($key, $replace, $locale); return trans($key, $replace, $locale);
} }
static $translations; static $translations;
if (is_null($translations)) { if (!$locale) {
$lang = get_langfolder_cookie(); $lang = get_langfolder_cookie();
$lang = \App\Http\Middleware\Locale::$languageMaps[$lang] ?? 'en'; $locale = \App\Http\Middleware\Locale::$languageMaps[$lang] ?? 'en';
$dir = ROOT_PATH . 'resources/lang/' . $lang; }
$files = glob($dir . '/*.php'); if (is_null($translations)) {
$langDir = ROOT_PATH . 'resources/lang/';
$files = glob($langDir . '*/*');
foreach ($files as $file) { foreach ($files as $file) {
$basename = basename($file);
$values = require $file; $values = require $file;
$key = strstr($basename, '.php', true); $setKey = substr($file, strlen($langDir));
arr_set($translations, $key, $values); if (substr($setKey, -4) == '.php') {
$setKey = substr($setKey, 0, -4);
}
$setKey = str_replace('/', '.', $setKey);
arr_set($translations, $setKey, $values);
} }
} }
return arr_get($translations, $key); $getKey = $locale . "." . $key;
$result = arr_get($translations, $getKey);
if (empty($result) && $locale != 'en') {
$getKey = "en.$key";
$result = arr_get($translations, $getKey);
}
if (!empty($replace)) {
$search = array_map(function ($value) {return ":$value";}, array_keys($replace));
$result = str_replace($search, array_values($replace), $result);
}
return $result;
} }

View File

@@ -13,4 +13,8 @@ return [
'result' => 'Result', 'result' => 'Result',
'result_pass' => 'Pass!', 'result_pass' => 'Pass!',
'result_not_pass' => '<bold color="red">Not pass!</bold>', 'result_not_pass' => '<bold color="red">Not pass!</bold>',
'checkout_pass_message_subject' => 'Exam pass!',
'checkout_pass_message_content' => 'Congratulation! You have complete the exam: :exam_name in time(:begin ~ :end)',
'checkout_not_pass_message_subject' => 'Exam not pass, and account is banned!',
'checkout_not_pass_message_content' => 'You did not complete the exam: :exam_name in time(:begin ~ :end), and your account has be banned!',
]; ];

View File

@@ -13,4 +13,8 @@ return [
'result' => '结果', 'result' => '结果',
'result_pass' => '通过!', 'result_pass' => '通过!',
'result_not_pass' => '<bold color="red">未通过!</bold>', 'result_not_pass' => '<bold color="red">未通过!</bold>',
'checkout_pass_message_subject' => '考核通过!',
'checkout_pass_message_content' => '恭喜!你在规定时间内(:begin ~ :end顺利完成了考核:exam_name。',
'checkout_not_pass_message_subject' => '考核未通过,账号被禁用!',
'checkout_not_pass_message_content' => '你在规定时间内(:begin ~ :end未完成考核:exam_name账号已被禁用。',
]; ];

View File

@@ -13,4 +13,8 @@ return [
'result' => '結果', 'result' => '結果',
'result_pass' => '通過!', 'result_pass' => '通過!',
'result_not_pass' => '<bold color="red">未通過!</bold>', 'result_not_pass' => '<bold color="red">未通過!</bold>',
'checkout_pass_message_subject' => '考核通過!',
'checkout_pass_message_content' => '恭喜!你在規定時間內(:begin ~ :end順利完成了考核:exam_name。',
'checkout_not_pass_message_subject' => '考核未通過,賬號被禁用!',
'checkout_not_pass_message_content' => '你在規定時間內(:begin ~ :end未完成考核:exam_name賬號已被禁用。',
]; ];