getAttendance($uid); $now = Carbon::now(); $today = Carbon::today(); $settings = Setting::get('bonus'); $initialBonus = $settings['attendance_initial'] ?? Attendance::INITIAL_BONUS; $isUpdated = 1; $initialData = [ 'uid' => $uid, 'added' => $now, 'points' => $initialBonus, 'days' => 1, 'total_days' => 1, ]; $update = $initialData; if (!$attendance) { //first time do_log("[DO_INSERT]: " . nexus_json_encode($initialData)); $attendance = Attendance::query()->create($initialData); } else { $added = $attendance->added->startOfDay(); do_log("[ORIGINAL_DATA]: " . $attendance->toJson()); if ($added->gte($today)) { //already attended today, do nothing $isUpdated = 0; } else { $diffDays = $today->diffInDays($added, true); if ($diffDays == 1) { //yesterday do it, it's continuous $continuousDays = $this->getContinuousDays($attendance, Carbon::yesterday()); $points = $this->getContinuousPoints($continuousDays + 1); do_log("[CONTINUOUS] continuous days from yesterday: $continuousDays, points: $points"); $update = [ 'added' => $now, 'points' => $points, 'days' => $continuousDays + 1, 'total_days' => $attendance->total_days + 1, ]; } else { //not continuous do_log("[NOT_CONTINUOUS]"); $update['total_days'] = $attendance->total_days + 1; } do_log("[DO_UPDATE]: " . nexus_json_encode($update)); $attendance->update($update); } } if ($isUpdated) { User::query()->where('id', $uid)->increment('seedbonus', $update['points']); $attendanceLog = [ 'uid' => $attendance->uid, 'points' => $update['points'], 'date' => $now->format('Y-m-d'), 'created_at' => $now, 'updated_at' => $now, ]; AttendanceLog::query()->insert($attendanceLog); } $attendance->added_time = $now->toTimeString(); $attendance->is_updated = $isUpdated; $baseQuery = AttendanceLog::query()->where('date', $today->format('Y-m-d')); $attendance->today_counts = (clone $baseQuery)->count(); $myId = (clone $baseQuery)->where('uid', $uid)->first(['id'])->id; $attendance->my_ranking = (clone $baseQuery)->where('id', '<=', $myId)->count(); do_log("[FINAL_ATTENDANCE]: " . $attendance->toJson()); return $attendance; } public function getAttendance($uid, $date = '') { $query = Attendance::query() ->where('uid', $uid) ->orderBy('id', 'desc'); if (!empty($date)) { $query->where('added', '>=', Carbon::parse($date)->startOfDay()) ->where('added', '<=', Carbon::parse($date)->endOfDay()); } return $query->first(); } public function getContinuousPoints($days) { $settings = Setting::get('bonus'); $initial = $settings['attendance_initial'] ?? Attendance::INITIAL_BONUS; $step = $settings['attendance_step'] ?? Attendance::STEP_BONUS; $max = $settings['attendance_max'] ?? Attendance::MAX_BONUS; $extraAwards = $settings['attendance_continuous'] ?? Attendance::CONTINUOUS_BONUS; $points = min($initial + ($days - 1) * $step, $max); krsort($extraAwards); foreach ($extraAwards as $key => $value) { if ($days == $key) { $points += $value; break; } } return $points; } /** * 将旧的 1 人 1 天 1 条迁移到新版 1 人一条 * * @return int */ public function migrateAttendance(): int { $page = 1; $size = 10000; $caseWhens = []; $idArr = []; $table = 'attendance'; while (true) { $logPrefix = "[MIGRATE_ATTENDANCE], page: $page, size: $size"; //as soon as possible, don't use eloquent $result = NexusDB::table($table) ->groupBy(['uid']) ->selectRaw('uid, max(id) as id, count(*) as counts') ->forPage($page, $size) ->get(); do_log("$logPrefix, " . last_query() . ", count: " . $result->count()); if ($result->isEmpty()) { do_log("$logPrefix, no more data..."); break; } foreach ($result as $row) { $caseWhens[] = sprintf('when %s then %s', $row->id, $row->counts); $idArr[] = $row->id; do_log(sprintf( "$logPrefix, update user: %s(ID: %s) => %s", $row->uid, $row->id, $row->counts )); } $page++; } if (empty($caseWhens)) { do_log("no data to update..."); return 0; } $caseWhenStr = sprintf('case id %s end', implode(' ', $caseWhens)); $result = NexusDB::table($table) ->whereIn('id', $idArr) ->update(['total_days' => NexusDB::raw($caseWhenStr)]); do_log("[MIGRATE_ATTENDANCE] DONE! $caseWhenStr, result: " . var_export($result, true)); return count($idArr); } /** * 清理签到记录,每人只保留一条 * * @return int */ public function cleanup(): int { $query = Attendance::query()->groupBy('uid')->havingRaw("count(*) > 1")->selectRaw('uid, max(id) as max_id'); $page = 1; $size = 10000; $deleteCounts = 0; while (true) { $rows = $query->forPage($page, $size)->get(); $log = "sql: " . last_query() . ", count: " . $rows->count(); do_log($log, 'info', isRunningInConsole()); if ($rows->isEmpty()) { $log = "no more data...."; do_log($log, 'info', isRunningInConsole()); break; } foreach ($rows as $row) { do { $deleted = Attendance::query() ->where('uid', $row->uid) ->where('id', '<', $row->max_id) ->limit(10000) ->delete(); $log = "delete: $deleted by sql: " . last_query(); $deleteCounts += $deleted; do_log($log, 'info', isRunningInConsole()); } while ($deleted > 0); } $page++; } return $deleteCounts; } /** * 为 1.7 新的补签功能回写当前连续签到记录 * * @param int $uid * @return int */ public function migrateAttendanceLogs($uid = 0): int { $cleanUpCounts = $this->cleanup(); do_log("cleanup count: $cleanUpCounts", 'info', isRunningInConsole()); $page = 1; $size = 10000; $insert = []; $table = 'attendance_logs'; while (true) { $logPrefix = "[MIGRATE_ATTENDANCE_LOGS], page: $page, size: $size"; $query = Attendance::query() ->where('added', '>=', Carbon::yesterday()) ->forPage($page, $size); if ($uid) { $query->where('uid', $uid); } $result = $query->get(); do_log("$logPrefix, " . last_query() . ", count: " . $result->count(), 'info', isRunningInConsole()); if ($result->isEmpty()) { do_log("$logPrefix, no more data..."); break; } foreach ($result as $row) { $interval =\DateInterval::createFromDateString("-1 day"); $period = new \DatePeriod($row->added->addDay(1), $interval, $row->days, \DatePeriod::EXCLUDE_START_DATE); $i = 0; foreach ($period as $periodValue) { $insert[] = sprintf( "(%d, %d, '%s')", $row->uid, $i == 0 ? $row->points : 0, $periodValue->format('Y-m-d') ); $i++; } } $page++; } if (empty($insert)) { do_log("no data to insert...", 'info', isRunningInConsole()); return 0; } $sql = sprintf( "insert into `%s` (`uid`, `points`, `date`) values %s on duplicate key update `uid` = values(`uid`)", $table, implode(',', $insert) ); NexusDB::statement($sql); $insertCount = count($insert); do_log("[MIGRATE_ATTENDANCE_LOGS] DONE! insert sql: " . $sql, 'info', isRunningInConsole()); return $insertCount; } public function getContinuousDays(Attendance $attendance, $start): int { $start = Carbon::parse($start); $logQuery = $attendance->logs()->where('date', '<=', $start->format('Y-m-d'))->orderBy('date', 'desc'); $attendanceLogs = $logQuery->get(['date'])->keyBy('date'); $counts = $attendanceLogs->count(); do_log(sprintf('user: %s, log counts: %s from query: %s', $attendance->uid, $counts, last_query())); if ($counts == 0) { return 0; } $interval =\DateInterval::createFromDateString("-1 day"); $period = new \DatePeriod($start->clone()->addDay(1), $interval, $counts, \DatePeriod::EXCLUDE_START_DATE); $days = 0; foreach ($period as $value) { $checkDate = $value->format('Y-m-d'); if ($attendanceLogs->has($checkDate)) { $days++; do_log(sprintf('user: %s, date: %s, [HAS_ATTENDANCE], now days: %s', $attendance->uid, $checkDate, $days)); } else { do_log(sprintf('user: %s, date: %s, [NOT_ATTENDANCE], now days: %s', $attendance->uid, $checkDate, $days)); break; } } return $days; } public function retroactive($user, $dateStr) { if (!$user instanceof User) { $user = User::query()->findOrFail((int)$user); } $attendance = $this->getAttendance($user->id); if (!$attendance) { throw new \LogicException(nexus_trans('attendance.have_not_attendance_yet')); } $date = Carbon::parse($dateStr); $now = Carbon::now(); if ($date->gte($now) || $now->diffInDays($date, true) > Attendance::MAX_RETROACTIVE_DAYS) { throw new \LogicException(nexus_trans('attendance.target_date_can_no_be_retroactive', ['date' => $date->format('Y-m-d')])); } return NexusDB::transaction(function () use ($user, $attendance, $date) { if (AttendanceLog::query()->where('uid', $user->id)->where('date', $date->format('Y-m-d'))->exists()) { throw new \RuntimeException(nexus_trans('attendance.already_attendance')); } if ($user->attendance_card < 1) { throw new \RuntimeException(nexus_trans('attendance.card_not_enough')); } $log = sprintf('user: %s, card: %s, retroactive date: %s', $user->id, $user->attendance_card, $date->format('Y-m-d')); $continuousDays = $this->getContinuousDays($attendance, $date->clone()->subDays(1)); $log .= ", continuousDays from prev day: $continuousDays"; $points = $this->getContinuousPoints($continuousDays + 1); $log .= ", points: $points"; do_log($log); $userUpdates = [ 'attendance_card' => NexusDB::raw('attendance_card - 1'), 'seedbonus' => NexusDB::raw("seedbonus + $points"), ]; $affectedRows = User::query() ->where('id', $user->id) ->where('attendance_card', $user->attendance_card) ->update($userUpdates); $msg = "Decrement user attendance_card and increment bonus"; if ($affectedRows != 1) { do_log("$msg fail, query: " . last_query()); throw new \RuntimeException("$msg fail"); } do_log("$msg success, query: " . last_query()); $insert = [ 'uid' => $user->id, 'points' => $points, 'date' => $date, 'is_retroactive' => 1, ]; $attendanceLog = AttendanceLog::query()->create($insert); //Increment total days and update days. $attendance->update([ 'total_days' => NexusDB::raw('total_days + 1'), 'days' => $this->getContinuousDays($attendance, Carbon::today()), ]); return $attendanceLog; }); } }