Compare commits

...

2 Commits

  1. 60
      app/Http/Controllers/Api/ExerciseController.php
  2. 58
      app/Http/Requests/CreateExerciseRequest.php
  3. 4
      app/Models/Exercise.php
  4. 18
      app/Models/ExerciseSkill.php
  5. 2
      app/Models/User.php
  6. 25
      app/Services/ExerciseService.php
  7. 30
      app/Services/ExerciseSkillService.php
  8. 8
      app/Services/QuestionGroupService.php
  9. 142
      app/Services/QuestionService.php
  10. 21
      app/Services/SkillService.php
  11. 9435
      composer.lock
  12. 30
      database/migrations/2025_07_11_013439_create_se_exercise_skills_table.php
  13. 5
      routes/api.php

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Api;
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\Support\Facades\DB;
class ExerciseController extends Controller
{
public function __construct(
ExerciseService $exerciseService,
ExerciseSkillService $exerciseSkillService,
SkillService $skillService,
QuestionService $questionService,
)
{
$this->exerciseService = $exerciseService;
$this->skillService = $skillService;
$this->exerciseSkillService = $exerciseSkillService;
$this->questionService = $questionService;
}
public function create(CreateExerciseRequest $request)
{
DB::beginTransaction();
try {
$data = $request->all();
$groups = json_decode($data['groups']);
$dataExercises = $request->except('groups','skill');
$dataSkills = $data['skill'];
$exerciseId = $this->exerciseService->createExercise($dataExercises);
$skillIds = $this->skillService->getSkillIds($dataSkills);
$this->exerciseSkillService->handleSkillsForExercise($skillIds, $exerciseId);
$this->questionService->createQuestion($groups, $exerciseId);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Exercise created successfully',
'exercise_id' => $exerciseId
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to create exercise',
'error' => $e->getMessage()
], 500);
}
}
}

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class CreateExerciseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string',
'grade_id' => 'required|integer',
'subject_id' => 'required|integer',
'category_id' => 'required|integer',
'skill' => 'required|array',
'level' => 'required|integer',
'groups' => 'required'
];
}
public function messages(): array
{
return [
'name.required' => 'Vui lòng nhập tên bài.',
'grade_id.required' => 'Vui lòng nhập khối lớp.',
'subject_id.required' => 'Vui lòng nhập môn học.',
'category_id.required' => 'Vui lòng nhập danh mục',
'skill.required' => 'Vui lòng nhập tên kĩ năng.',
'level.required' => 'Vui lòng nhập trình độ.',
'groups.required' => 'Vui lòng nhập danh sách các nhóm câu hỏi.'
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'success' => false,
'message' => 'Dữ liệu không hợp lệ',
'errors' => $validator->errors()
], 422));
}
}

@ -33,9 +33,9 @@ class Exercise extends Model
return $this->belongsTo(Category::class, 'category_id');
}
public function skill()
public function skills()
{
return $this->belongsTo(Skill::class, 'skill_id');
return $this->belongsToMany(Skill::class, 'se_exercise_skills', 'exercise_id', 'skill_id');
}
public function questionGroups()

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ExerciseSkill extends Model
{
use SoftDeletes;
protected $table = 'se_exercise_skills';
protected $fillable = [
'exercise_id',
'skill_id'
];
}

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

@ -0,0 +1,25 @@
<?php
namespace App\Services;
use App\Models\Exercise;
class ExerciseService
{
public function createExercise($dataExercise)
{
$dataCreateExercise = [
'lesson_name' => $dataExercise['name'] ?? null,
'description' => $dataExercise['description'] ?? null,
'subject_id' => $dataExercise['subject_id'] ?? null,
'level' => $dataExercise['level'] ?? null,
'status' => $dataExercise['status'] ?? null,
'category_id' => $dataExercise['category_id'] ?? null,
'year' => $dataExercise['year'] ?? null,
];
$exercise = Exercise::create($dataCreateExercise);
return $exercise->id;
}
}

@ -0,0 +1,30 @@
<?php
namespace App\Services;
use App\Models\ExerciseSkill;
class ExerciseSkillService
{
/**
* Xử lý lưu các kỹ năng (skills) cho một bài tập (exercise)
*
* @param array $skills
* @param int $exerciseId
* @return void
*/
public function handleSkillsForExercise($skillIds, $exerciseId)
{
$data = [];
foreach ($skillIds as $skillId) {
$data[] = [
'exercise_id' => $exerciseId,
'skill_id' => $skillId,
];
}
if (!empty($data)) {
ExerciseSkill::insert($data);
}
}
}

@ -0,0 +1,8 @@
<?php
namespace App\Services;
class QuestionGroupService
{
}

@ -0,0 +1,142 @@
<?php
namespace App\Services;
use App\Models\Question;
use App\Models\QuestionBlank;
use App\Models\QuestionChoice;
use App\Models\QuestionGroup;
use Illuminate\Support\Facades\DB;
class QuestionService
{
public function __construct(
QuestionGroupService $questionGroupService
)
{
$this->questionGroupService = $questionGroupService;
}
public function createQuestion($data, $exerciseId)
{
$now = now();
$groupData = [];
$dataQuestion = [];
$dataQuestionChoices = [];
$dataQuestionBlank = [];
$questionMap = [];
DB::beginTransaction();
try {
// 1. Chuẩn bị groupData
foreach ($data as $groupKey => $value) {
$groupData[] = [
'content' => $value->title,
'exercise_id' => $exerciseId,
'is_question_order_fixed' => $value->is_question_order_fixed,
'is_option_order_fixed' => $value->is_option_order_fixed,
'position' => $groupKey + 1,
'paragraph' => $value->paragraph,
'created_at' => $now,
'updated_at' => $now,
];
}
// 2. Insert group & lấy lại id
QuestionGroup::insert($groupData);
$groupIds = QuestionGroup::where('exercise_id', $exerciseId)
->orderByDesc('id')
->take(count($groupData))
->get()
->reverse()
->pluck('id')
->values()
->toArray();
// 3. Chuẩn bị question
$questionIndex = 0;
foreach ($data as $groupKey => $value) {
$groupId = $groupIds[$groupKey];
foreach ($value->questions as $qIndex => $question) {
$dataQuestion[] = [
'exercise_id' => $exerciseId,
'content' => $question->content,
'description' => $question->description ?? null,
'group_id' => $groupId,
'question_type_id' => 1,
'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
];
$questionMap["{$groupKey}_{$qIndex}"] = [
'type' => $question->type,
'options' => $question->options ?? [],
'answers' => $question->answers ?? [],
];
$questionIndex++;
}
}
// 4. Insert question & lấy lại theo custom_key
Question::insert($dataQuestion);
$questions = Question::where('exercise_id', $exerciseId)->get();
// $questionsByKey = $questions->keyBy(function ($item) {
// return "{$item->group_id}_{$item->position}"; // hoặc dùng custom_key nếu có
// });
// 5. Mapping lại options/answers
$qIndex = 0;
foreach ($questionMap as $key => $meta) {
$questionId = $questions[$qIndex]->id ?? null;
if (!$questionId) continue;
if ($meta['type'] == 'multiple_choice') {
foreach ($meta['options'] as $optKey => $option) {
$dataQuestionChoices[] = [
'question_id' => $questionId,
'label' => $option->label,
'content' => $option->content,
'is_correct' => $option->is_correct,
'position' => $optKey + 1,
'created_at' => $now,
'updated_at' => $now,
];
}
} else {
foreach ($meta['answers'] as $ansKey => $answer) {
$dataQuestionBlank[] = [
'question_id' => $questionId,
'correct_answer' => $answer->answer_true,
'other_answers' => json_encode($answer->other_answers),
'position' => $ansKey + 1,
'created_at' => $now,
'updated_at' => $now,
];
}
}
$qIndex++;
}
if (!empty($dataQuestionChoices)) {
QuestionChoice::insert($dataQuestionChoices);
}
if (!empty($dataQuestionBlank)) {
QuestionBlank::insert($dataQuestionBlank);
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
}
}

@ -0,0 +1,21 @@
<?php
namespace App\Services;
use App\Models\Skill;
use Illuminate\Support\Str;
class SkillService
{
public function getSkillIds($skills)
{
$skillIds = [];
foreach ($skills as $value) {
$skill = Skill::where('code', $value)->first();
if ($skill) {
$skillIds[] = $skill->id;
}
}
return $skillIds;
}
}

9435
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('se_exercise_skills', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('exercise_id');
$table->unsignedBigInteger('skill_id');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('se_exercise_skills');
}
};

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\ExerciseController;
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use Psr\Http\Message\ServerRequestInterface;
@ -10,3 +11,7 @@ use Laravel\Passport\Passport;
Route::middleware(['auth:api', 'role:admin'])->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware('auth:api')->group(function () {
Route::post('/exercise/create', [ExerciseController::class, 'create'])->name('exercise.create');
});

Loading…
Cancel
Save