first commit

This commit is contained in:
2026-01-23 17:28:21 +07:00
commit 29ff8992b9
331 changed files with 30545 additions and 0 deletions

View 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
View 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();
}
}