mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-26 13:32:07 +07:00
first commit
This commit is contained in:
45
app/Services/SocialAuthService.php
Normal file
45
app/Services/SocialAuthService.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SocialAccount;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
/**
|
||||
* Revoke the OAuth token for a given social account.
|
||||
* Mandatory for security compliance.
|
||||
*/
|
||||
public function revokeToken(SocialAccount $account): bool
|
||||
{
|
||||
if (!$account->token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($account->provider === 'google') {
|
||||
$response = Http::asForm()->post('https://oauth2.googleapis.com/revoke', [
|
||||
'token' => $account->token,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
Log::info("Successfully revoked Google token for User ID: {$account->user_id}");
|
||||
$account->update([
|
||||
'token' => null,
|
||||
'refresh_token' => null,
|
||||
'expires_at' => null,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::error("Failed to revoke Google token for User ID: {$account->user_id}. Status: " . $response->status());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error during token revocation: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
129
app/Services/SrsService.php
Normal file
129
app/Services/SrsService.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SrsReview;
|
||||
use App\Models\User;
|
||||
use App\Models\Vocabulary;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SrsService
|
||||
{
|
||||
/**
|
||||
* Process a review using SuperMemo-2 Algorithm.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Vocabulary $vocab
|
||||
* @param int $grade (1=Again, 2=Hard, 3=Good, 4=Easy)
|
||||
* Mapped to SM-2 Quality (q):
|
||||
* 1 (Again) -> q=1
|
||||
* 2 (Hard) -> q=3
|
||||
* 3 (Good) -> q=4
|
||||
* 4 (Easy) -> q=5
|
||||
*/
|
||||
public function processReview(User $user, Vocabulary $vocab, int $grade)
|
||||
{
|
||||
// Map UI Grade to SM-2 Quality
|
||||
// Mapping: 1->1, 2->3, 3->4, 4->5
|
||||
$qualityMap = [1 => 1, 2 => 3, 3 => 4, 4 => 5];
|
||||
$q = $qualityMap[$grade] ?? 3;
|
||||
|
||||
// Find existing review or create new
|
||||
$review = SrsReview::firstOrNew([
|
||||
'user_id' => $user->id,
|
||||
'vocabulary_id' => $vocab->id
|
||||
]);
|
||||
|
||||
if (!$review->exists) {
|
||||
// Defaults
|
||||
$review->ease_factor = 2.5;
|
||||
$review->interval = 0;
|
||||
$review->repetitions = 0;
|
||||
$review->status = 'new';
|
||||
}
|
||||
|
||||
// SM-2 Algorithm Implementation
|
||||
if ($q >= 3) {
|
||||
// Correct response
|
||||
if ($review->repetitions == 0) {
|
||||
$review->interval = 1;
|
||||
} elseif ($review->repetitions == 1) {
|
||||
$review->interval = 6;
|
||||
} else {
|
||||
$review->interval = round($review->interval * $review->ease_factor);
|
||||
}
|
||||
$review->repetitions++;
|
||||
$review->status = $review->interval > 21 ? 'graduated' : 'review';
|
||||
} else {
|
||||
// Incorrect response
|
||||
$review->repetitions = 0;
|
||||
$review->interval = 1;
|
||||
$review->status = 'learning';
|
||||
}
|
||||
|
||||
// Update Ease Factor
|
||||
// EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
|
||||
$review->ease_factor = $review->ease_factor + (0.1 - (5 - $q) * (0.08 + (5 - $q) * 0.02));
|
||||
if ($review->ease_factor < 1.3) {
|
||||
$review->ease_factor = 1.3;
|
||||
}
|
||||
|
||||
$review->last_review_at = Carbon::now();
|
||||
$review->next_review_at = Carbon::now()->addDays($review->interval);
|
||||
|
||||
$review->save();
|
||||
|
||||
// Track Activity (XP and Streak)
|
||||
$this->trackActivity($user, $grade);
|
||||
|
||||
return $review;
|
||||
}
|
||||
|
||||
public function trackActivity(User $user, int $grade)
|
||||
{
|
||||
$today = Carbon::today();
|
||||
$lastActivity = $user->last_activity_at ? Carbon::parse($user->last_activity_at)->startOfDay() : null;
|
||||
|
||||
// Award XP (Easy gives slightly more?)
|
||||
// Let's just do 5 XP per card for now.
|
||||
$user->xp_points += 5;
|
||||
|
||||
if (!$lastActivity) {
|
||||
// First activity ever
|
||||
$user->current_streak = 1;
|
||||
} elseif ($today->isSameDay($lastActivity)) {
|
||||
// Already active today, do nothing to streak
|
||||
} elseif ($today->diffInDays($lastActivity) === 1) {
|
||||
// Consecutive day
|
||||
$user->current_streak += 1;
|
||||
} else {
|
||||
// Streak broken
|
||||
$user->current_streak = 1;
|
||||
}
|
||||
|
||||
$user->last_activity_at = Carbon::now();
|
||||
$user->save();
|
||||
}
|
||||
|
||||
public function getDueReviews(User $user, int $limit = 20)
|
||||
{
|
||||
return SrsReview::where('user_id', $user->id)
|
||||
->where('next_review_at', '<=', Carbon::now())
|
||||
->with(['vocabulary'])
|
||||
->orderBy('next_review_at', 'asc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getNewCards(User $user, int $limit = 5)
|
||||
{
|
||||
// Get vocabularies NOT in srs_reviews
|
||||
$reviewedIds = SrsReview::where('user_id', $user->id)->pluck('vocabulary_id');
|
||||
|
||||
return Vocabulary::whereNotIn('id', $reviewedIds)
|
||||
// Ideally filter by User's Level (e.g. N5)
|
||||
// ->where('level', $user->level ?? 'N5')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user