Merge pull request 'create exercise api' (#2) from feature/api-create-exercise into developer
Reviewed-on: #2pull/3/head
commit
b2f449b7f0
13 changed files with 401 additions and 9437 deletions
@ -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)); |
||||||
|
} |
||||||
|
} |
@ -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' |
||||||
|
]; |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
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'); |
||||||
|
} |
||||||
|
}; |
Loading…
Reference in new issue