up
Some checks failed
Deploy on Master Change / deploy (push) Failing after 1m23s

This commit is contained in:
2026-07-20 12:38:02 +07:00
parent 6ee9773f7f
commit 232f9873a0
8 changed files with 516 additions and 121 deletions

View File

@@ -54,6 +54,11 @@ func ExamRoomCanCancel(room models.ExamRoom, now time.Time) bool {
return ExamRoomIsLive(room, now)
}
// ExamRoomCanExtend — gia hạn giờ kết thúc khi phòng đang thi.
func ExamRoomCanExtend(room models.ExamRoom, now time.Time) bool {
return ExamRoomIsLive(room, now)
}
// ProcessExamRoomLifecycle đánh dấu phòng ready đã quá giờ kết thúc.
func ProcessExamRoomLifecycle(db *gorm.DB) {
now := time.Now()

View File

@@ -14,6 +14,7 @@ import (
"time"
internalDb "server/internal/db"
"server/internal/middleware"
"server/internal/models"
internalWs "server/internal/websocket"
@@ -120,15 +121,23 @@ func deliverExamPaper(db *gorm.DB, enrollmentID uint) {
// GET /api/exam-rooms
func ListExamRoomsHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
staffID := middleware.StaffIDFromCtx(c)
mineOnly := c.Query("mine") == "1" || c.Query("mine") == "true"
q := db.Order("start_time desc")
if mineOnly && staffID > 0 {
q = q.Where("created_by_staff_id = ?", staffID)
}
var rooms []models.ExamRoom
if err := db.Order("start_time desc").Find(&rooms).Error; err != nil {
if err := q.Find(&rooms).Error; err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
type row struct {
models.ExamRoom
StudentCount int `json:"studentCount"`
PaperCount int `json:"paperCount"`
DisplayStatus string `json:"displayStatus"`
StudentCount int `json:"studentCount"`
PaperCount int `json:"paperCount"`
DisplayStatus string `json:"displayStatus"`
CreatedByStaffID uint `json:"createdByStaffId"`
}
out := make([]row, 0, len(rooms))
now := time.Now()
@@ -140,10 +149,11 @@ func ListExamRoomsHandler(db *gorm.DB) fiber.Handler {
_ = db.Model(&models.ExamRoomStudent{}).Where("exam_room_id = ?", r.ID).Count(&sc).Error
_ = db.Model(&models.ExamPaper{}).Where("exam_room_id = ?", r.ID).Count(&pc).Error
out = append(out, row{
ExamRoom: r,
StudentCount: int(sc),
PaperCount: int(pc),
DisplayStatus: internalDb.ExamDisplayStatus(r, now),
ExamRoom: r,
StudentCount: int(sc),
PaperCount: int(pc),
DisplayStatus: internalDb.ExamDisplayStatus(r, now),
CreatedByStaffID: r.CreatedByStaffID,
})
}
return c.JSON(fiber.Map{"data": out})
@@ -177,12 +187,13 @@ func CreateExamRoomHandler(db *gorm.DB) fiber.Handler {
}
apps := strings.TrimSpace(req.AllowedApps)
room := models.ExamRoom{
Name: req.Name,
StartTime: start,
EndTime: end,
AllowedApps: apps,
QuizURL: strings.TrimSpace(req.QuizURL),
Status: models.ExamStatusDraft,
Name: req.Name,
StartTime: start,
EndTime: end,
AllowedApps: apps,
QuizURL: strings.TrimSpace(req.QuizURL),
Status: models.ExamStatusDraft,
CreatedByStaffID: middleware.StaffIDFromCtx(c),
}
if err := db.Create(&room).Error; err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
@@ -259,8 +270,9 @@ func GetExamRoomHandler(db *gorm.DB) fiber.Handler {
"editable": internalDb.ExamRoomEditable(room),
"prepEditable": internalDb.ExamRoomPrepEditable(room, now),
"canPublish": room.Status == models.ExamStatusDraft,
"canUnpublish": internalDb.ExamRoomCanUnpublish(room, time.Now()),
"canCancel": internalDb.ExamRoomCanCancel(room, time.Now()),
"canUnpublish": internalDb.ExamRoomCanUnpublish(room, now),
"canCancel": internalDb.ExamRoomCanCancel(room, now),
"canExtend": internalDb.ExamRoomCanExtend(room, now),
})
}
}
@@ -307,8 +319,15 @@ func UpdateExamRoomHandler(db *gorm.DB) fiber.Handler {
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
}
now := time.Now()
ds := internalDb.ExamDisplayStatus(room, now)
if !internalDb.ExamRoomEditable(room) {
if room.Status == models.ExamStatusReady {
if ds == "active" {
// Đang thi: chỉ cho sửa app được phép + giờ kết thúc (gia hạn)
if req.Name != nil || req.StartTime != nil || req.QuizURL != nil {
return c.Status(403).JSON(fiber.Map{"error": "Khi đang thi chỉ được sửa ứng dụng được phép hoặc gia hạn giờ kết thúc"})
}
} else if room.Status == models.ExamStatusReady {
if req.Name != nil || req.StartTime != nil || req.EndTime != nil || req.QuizURL != nil {
return c.Status(403).JSON(fiber.Map{"error": "Chỉ sửa được cấu hình ứng dụng được phép khi phòng thi đã đẩy hoặc đang thi"})
}
@@ -331,6 +350,14 @@ func UpdateExamRoomHandler(db *gorm.DB) fiber.Handler {
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "endTime không hợp lệ"})
}
if ds == "active" {
if !t.After(now) {
return c.Status(400).JSON(fiber.Map{"error": "Giờ kết thúc mới phải sau thời điểm hiện tại"})
}
if !t.After(room.EndTime) {
return c.Status(400).JSON(fiber.Map{"error": "Gia hạn phải chọn giờ kết thúc muộn hơn giờ hiện tại của phòng"})
}
}
room.EndTime = t
}
if !room.EndTime.After(room.StartTime) {
@@ -420,13 +447,54 @@ func CancelExamRoomHandler(db *gorm.DB) fiber.Handler {
}
}
// POST /api/exam-rooms/:id/extend — gia hạn thêm phút cho phòng đang thi
func ExtendExamRoomHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
id, err := parseUintParam(c, "id")
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid id"})
}
var req struct {
Minutes int `json:"minutes"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
}
if req.Minutes < 1 || req.Minutes > 480 {
return c.Status(400).JSON(fiber.Map{"error": "Số phút gia hạn phải từ 1 đến 480"})
}
var room models.ExamRoom
if err := db.First(&room, id).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
}
now := time.Now()
if !internalDb.ExamRoomCanExtend(room, now) {
return c.Status(403).JSON(fiber.Map{"error": "Chỉ gia hạn được phòng thi đang diễn ra"})
}
base := room.EndTime
if !base.After(now) {
base = now
}
room.EndTime = base.Add(time.Duration(req.Minutes) * time.Minute)
if err := db.Save(&room).Error; err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"ok": true,
"room": room,
"displayStatus": internalDb.ExamDisplayStatus(room, time.Now()),
"addedMinutes": req.Minutes,
"endTime": room.EndTime,
})
}
}
// DELETE /api/exam-rooms/:id
func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
staffID := middleware.StaffIDFromCtx(c)
email, _ := c.Locals("staffEmail").(string)
if email != "phuocntb@rikkeiacademy.com" {
return c.Status(403).JSON(fiber.Map{"error": "Bạn không có quyền thực hiện chức năng này"})
}
isSuperAdmin := email == "phuocntb@rikkeiacademy.com"
id, err := parseUintParam(c, "id")
if err != nil {
@@ -436,6 +504,9 @@ func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
if err := db.First(&room, id).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
}
if !isSuperAdmin && (room.CreatedByStaffID == 0 || room.CreatedByStaffID != staffID) {
return c.Status(403).JSON(fiber.Map{"error": "Chỉ người tạo phòng hoặc admin mới được xóa"})
}
if internalDb.ExamRoomIsLive(room, time.Now()) {
return c.Status(403).JSON(fiber.Map{"error": "Không thể xóa phòng thi đang diễn ra — hãy hủy phòng trước"})
}
@@ -451,6 +522,7 @@ func DeleteExamRoomHandler(db *gorm.DB) fiber.Handler {
_ = db.Where("exam_paper_id = ?", p.ID).Delete(&models.ExamPaperResource{}).Error
}
_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamPaper{}).Error
_ = db.Where("exam_room_id = ?", id).Delete(&models.ExamSeatingLayout{}).Error
_ = db.Delete(&room).Error
return c.JSON(fiber.Map{"ok": true})
}
@@ -593,8 +665,9 @@ func RemoveExamRoomStudentHandler(db *gorm.DB) fiber.Handler {
if err := db.First(&room, roomID).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Không tìm thấy"})
}
if !internalDb.ExamRoomPrepEditable(room, time.Now()) {
return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã bắt đầu — không thể xóa sinh viên"})
ds := internalDb.ExamDisplayStatus(room, time.Now())
if ds == models.ExamStatusEnded {
return c.Status(403).JSON(fiber.Map{"error": "Phòng thi đã kết thúc — không thể xóa sinh viên"})
}
_ = db.Where("exam_room_id = ? AND student_rk_id = ?", roomID, studentRkID).Delete(&models.ExamRoomStudent{}).Error
return c.JSON(fiber.Map{"ok": true})

View File

@@ -11,18 +11,19 @@ const (
// ExamRoom — phòng thi độc lập với lớp học
type ExamRoom struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Name string `gorm:"column:name;size:255;not null" json:"name"`
StartTime time.Time `gorm:"column:start_time;not null;index" json:"startTime"`
EndTime time.Time `gorm:"column:end_time;not null;index" json:"endTime"`
AllowedApps string `gorm:"column:allowed_apps;type:text" json:"allowedApps"`
QuizURL string `gorm:"column:quiz_url;size:1024" json:"quizUrl"`
GitRepoURL string `gorm:"column:git_repo_url;size:512" json:"gitRepoUrl"`
GitBranch string `gorm:"column:git_branch;size:64;default:main" json:"gitBranch"`
GitPublishURL string `gorm:"column:git_publish_url;size:1024" json:"gitPublishUrl,omitempty"`
Status string `gorm:"column:status;size:16;not null;default:draft;index" json:"status"`
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Name string `gorm:"column:name;size:255;not null" json:"name"`
StartTime time.Time `gorm:"column:start_time;not null;index" json:"startTime"`
EndTime time.Time `gorm:"column:end_time;not null;index" json:"endTime"`
AllowedApps string `gorm:"column:allowed_apps;type:text" json:"allowedApps"`
QuizURL string `gorm:"column:quiz_url;size:1024" json:"quizUrl"`
GitRepoURL string `gorm:"column:git_repo_url;size:512" json:"gitRepoUrl"`
GitBranch string `gorm:"column:git_branch;size:64;default:main" json:"gitBranch"`
GitPublishURL string `gorm:"column:git_publish_url;size:1024" json:"gitPublishUrl,omitempty"`
Status string `gorm:"column:status;size:16;not null;default:draft;index" json:"status"`
CreatedByStaffID uint `gorm:"column:created_by_staff_id;index;default:0" json:"createdByStaffId"`
}
func (ExamRoom) TableName() string { return "exam_rooms" }

View File

@@ -213,6 +213,7 @@ func main() {
staff.Post("/exam-rooms/:id/publish", handlers.PublishExamRoomHandler(gormDB))
staff.Post("/exam-rooms/:id/unpublish", handlers.UnpublishExamRoomHandler(gormDB))
staff.Post("/exam-rooms/:id/cancel", handlers.CancelExamRoomHandler(gormDB))
staff.Post("/exam-rooms/:id/extend", handlers.ExtendExamRoomHandler(gormDB))
staff.Post("/exam-rooms/:id/assign-random", handlers.RandomAssignExamPapersHandler(gormDB))
staff.Post("/exam-rooms/:id/assign-papers-batch", handlers.AssignExamPapersBatchHandler(gormDB))
staff.Get("/exam-rooms/:id/seating-layout", handlers.GetExamSeatingLayoutHandler(gormDB))