Compare commits

..

8 Commits

  1. 42
      app/Auth/PassportUserRepository.php
  2. 43
      app/Http/Controllers/Api/ExerciseController.php
  3. 24
      app/Http/Resources/BlankResource.php
  4. 24
      app/Http/Resources/ChoiceResource.php
  5. 27
      app/Http/Resources/ExerciseResource.php
  6. 23
      app/Http/Resources/QuestionGroupResource.php
  7. 25
      app/Http/Resources/QuestionResource.php
  8. 22
      app/Http/Resources/SkillResource.php
  9. 12
      app/Models/Exercise.php
  10. 4
      app/Models/QuestionBlank.php
  11. 2
      app/Models/User.php
  12. 49
      app/Services/ExerciseService.php
  13. 8
      app/Services/QuestionService.php
  14. 9437
      composer.lock
  15. 2
      routes/api.php

@ -3,13 +3,22 @@
namespace App\Auth;
use Laravel\Passport\Bridge\UserRepository;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Laravel\Passport\Bridge\UserRepositoryInterface;
use Laravel\Passport\Bridge\User;
use Illuminate\Support\Facades\Hash;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use App\Models\User as UserModel;
class PassportUserRepository extends UserRepository
{
/**
* OAuth2.
*
* @param string $username
* @param string $password
* @param string $grantType
* @param ClientEntityInterface $clientEntity
* @return User|null
*/
public function getUserEntityByUserCredentials(
$username,
$password,
@ -18,37 +27,14 @@ class PassportUserRepository extends UserRepository
) {
$user = UserModel::where('email', $username)->first();
if (!$user) {
if (! $user) {
return null;
}
// Avoid Hash::check() error with non-bcrypt hashes
if ($this->isBcryptHash($user->password)) {
if (Hash::check($password, $user->password)) {
return new User($user->id);
}
} else {
// If the hash is not bcrypt, check for MD5 manually
if (md5($password) === $user->password) {
// Upgrade password to bcrypt
$user->password = Hash::make($password);
$user->save();
return new User($user->id);
}
if (md5($password) === $user->password) {
return new User($user->getAuthIdentifier());
}
return null;
}
/**
* Check if the given hash uses the bcrypt algorithm.
*
* @param string $hashedPassword
* @return bool
*/
protected function isBcryptHash($hashedPassword): bool
{
return password_get_info($hashedPassword)['algo'] === PASSWORD_BCRYPT;
}
}

@ -6,9 +6,9 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\CreateExerciseRequest;
use App\Services\ExerciseService;
use App\Services\ExerciseSkillService;
use App\Services\QuestionGroupService;
use App\Services\QuestionService;
use App\Services\SkillService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ExerciseController extends Controller
@ -24,7 +24,25 @@ class ExerciseController extends Controller
$this->skillService = $skillService;
$this->exerciseSkillService = $exerciseSkillService;
$this->questionService = $questionService;
}
public function index(Request $request)
{
$exercises = $this->exerciseService->getExercises($request->all());
if ($exercises->isEmpty()) {
return response()->json([
'status' => true,
'data' => $exercises,
'message' => 'Không có dữ liệu.',
]);
}
return response()->json([
'status' => true,
'data' => $exercises,
'message' => 'Lấy danh sách đề thi thành công.',
]);
}
public function create(CreateExerciseRequest $request)
@ -45,16 +63,35 @@ class ExerciseController extends Controller
return response()->json([
'success' => true,
'message' => 'Exercise created successfully',
'message' => 'Tạo đề thi thành công.',
'exercise_id' => $exerciseId
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to create exercise',
'message' => 'Tạo đề thi thất bại.',
'error' => $e->getMessage()
], 500);
}
}
public function detail($id)
{
$exercise = $this->exerciseService->detail($id);
if (empty($exercise)) {
return response()->json([
'status' => true,
'data' => $exercise,
'message' => 'Không có dữ liệu.',
]);
}
return response()->json([
'status' => true,
'data' => $exercise,
'message' => 'Lấy danh sách đề thi thành công.',
]);
}
}

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class BlankResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'position' => $this->position,
'correct_answer' => $this->correct_answer,
'other_answers' => $this->other_answers,
];
}
}

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ChoiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'content' => $this->content,
'is_correct' => $this->is_correct,
'position' => $this->position,
];
}
}

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ExerciseResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->lesson_name,
'description' => $this->description,
'level' => $this->level_label,
'year' => $this->year,
'skills' => SkillResource::collection($this->whenLoaded('skills')),
'question_groups' => QuestionGroupResource::collection($this->whenLoaded('questionGroups')),
];
}
}

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class QuestionGroupResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'content' => $this->content,
'questions' => QuestionResource::collection($this->whenLoaded('questions')),
];
}
}

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class QuestionResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'content' => $this->content,
'score' => $this->score,
'explanation' => $this->explanation,
'hint' => $this->hint,
'type' => [
'code' => $this->type->code ?? null,
'name' => $this->type->name ?? null,
],
'choices' => $this->when($this->type->code === 'multiple_choice', ChoiceResource::collection($this->choices)),
'blanks' => $this->when($this->type->code !== 'multiple_choice', BlankResource::collection($this->blanks)),
];
}
}

@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class SkillResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name
];
}
}

@ -23,6 +23,18 @@ class Exercise extends Model
'media_object_id',
];
protected $appends = ['level_label'];
public function getLevelLabelAttribute()
{
return match ($this->level) {
0 => 'easy',
1 => 'normal',
2 => 'hard',
default => 'normal',
};
}
public function subject()
{
return $this->belongsTo(Subject::class, 'subject_id');

@ -17,6 +17,10 @@ class QuestionBlank extends Model
'other_answers',
];
protected $casts = [
'other_answers' => 'array', // Laravel tự động json_decode
];
public function question()
{
return $this->belongsTo(Question::class, 'question_id');

@ -15,7 +15,7 @@ class User extends Authenticatable
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable, HasRoles;
protected $table = 'users_laravel';
protected $table = 'users';
/**
* The attributes that are mass assignable.

@ -2,10 +2,41 @@
namespace App\Services;
use App\Http\Resources\ExerciseResource;
use App\Models\Exercise;
class ExerciseService
{
public function getExercises($params)
{
$limit = isset($params['limit']) ? min((int)$params['limit'], 100) : 10;
$offset = isset($params['offset']) ? max((int)$params['offset'], 0) : 0;
$lessonName = $params['search'] ?? null;
$query = Exercise::select('id', 'lesson_name', 'description', 'status', 'level', 'year', 'created_at');
if (!empty($lessonName)) {
$query->where('lesson_name', 'like', '%' . $lessonName . '%');
}
// Lấy dữ liệu chính
$data = $query->orderByDesc('created_at')
->skip($offset)
->take($limit)
->get()
->map(function ($item) {
$item->level = match ((int)$item->level) {
0 => 'easy',
1 => 'normal',
2 => 'hard',
default => 'unknown',
};
return $item;
});
return $data;
}
public function createExercise($dataExercise)
{
$dataCreateExercise = [
@ -22,4 +53,22 @@ class ExerciseService
return $exercise->id;
}
public function detail($id)
{
$exercise = Exercise::with([
'skills:id,name',
'questionGroups' => function ($q) {
$q->select('id', 'exercise_id', 'content');
},
'questionGroups.questions' => function ($q) {
$q->select('id', 'content', 'group_id', 'question_type_id', 'description', 'score', 'explanation', 'hint');
},
'questionGroups.questions.type:id,code,name',
'questionGroups.questions.choices',
'questionGroups.questions.blanks',
])->findOrFail($id);
return new ExerciseResource($exercise);
}
}

@ -6,6 +6,7 @@ use App\Models\Question;
use App\Models\QuestionBlank;
use App\Models\QuestionChoice;
use App\Models\QuestionGroup;
use App\Models\QuestionType;
use Illuminate\Support\Facades\DB;
class QuestionService
@ -59,19 +60,20 @@ class QuestionService
foreach ($data as $groupKey => $value) {
$groupId = $groupIds[$groupKey];
foreach ($value->questions as $qIndex => $question) {
$questionType = QuestionType::where('code', $question->type)->first();
$questionTypeId = $questionType ? $questionType->id : 1;
$dataQuestion[] = [
'exercise_id' => $exerciseId,
'content' => $question->content,
'description' => $question->description ?? null,
'group_id' => $groupId,
'question_type_id' => 1,
'question_type_id' => $questionTypeId,
'level' => 2,
'score' => $question->score ?? null,
'explanation' => $question->explanation ?? null,
'hint' => $question->hint ?? null,
'created_at' => $now,
'updated_at' => $now,
'custom_key' => "{$groupKey}_{$qIndex}", // dùng để map lại
'updated_at' => $now
];
$questionMap["{$groupKey}_{$qIndex}"] = [
'type' => $question->type,

9437
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -13,5 +13,7 @@ Route::middleware(['auth:api', 'role:admin'])->get('/user', function (Request $r
});
Route::middleware('auth:api')->group(function () {
Route::get('/exercises', [ExerciseController::class, 'index'])->name('exercise.index');
Route::post('/exercise/create', [ExerciseController::class, 'create'])->name('exercise.create');
Route::get('/exercise/{id}', [ExerciseController::class, 'detail'])->name('exercise.detail');
});

Loading…
Cancel
Save