All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m45s
1020 lines
33 KiB
Go
1020 lines
33 KiB
Go
package handlers
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
internalDb "server/internal/db"
|
|
"server/internal/auth"
|
|
"server/internal/models"
|
|
"server/internal/util"
|
|
internalWs "server/internal/websocket"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type classWithScheduleItem struct {
|
|
RkID int64 `json:"rkId"`
|
|
Name string `json:"name"`
|
|
ClassCode string `json:"classCode"`
|
|
Type string `json:"type"`
|
|
StudentCount int `json:"studentCount"`
|
|
SpecializeName string `json:"specializeName"`
|
|
SystemName string `json:"systemName"`
|
|
Schedules []models.ClassSchedule `json:"schedules"`
|
|
AllowedApps string `json:"allowedApps"`
|
|
}
|
|
|
|
// GET /api/classes/schedules - Lấy các lớp có lịch học trong tuần
|
|
func ListActiveSchedulesHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
// Tìm các classRkId có lịch học
|
|
var activeClassRkIDs []int64
|
|
if err := db.Model(&models.ClassSchedule{}).Distinct("class_rk_id").Pluck("class_rk_id", &activeClassRkIDs).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
if len(activeClassRkIDs) == 0 {
|
|
return c.JSON(fiber.Map{"data": []any{}})
|
|
}
|
|
|
|
// Lấy thông tin lớp học
|
|
var classes []models.Class
|
|
if err := db.Where("rk_id IN ?", activeClassRkIDs).Find(&classes).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
// Lấy toàn bộ lịch học của các lớp này
|
|
var schedules []models.ClassSchedule
|
|
if err := db.Where("class_rk_id IN ?", activeClassRkIDs).Order("day_of_week, start_time").Find(&schedules).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
// Lấy cấu hình allowed apps của các lớp này
|
|
var allowedApps []models.ClassAllowedApp
|
|
if err := db.Where("class_rk_id IN ?", activeClassRkIDs).Find(&allowedApps).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
// Map dữ liệu
|
|
schedulesMap := map[int64][]models.ClassSchedule{}
|
|
for _, s := range schedules {
|
|
schedulesMap[s.ClassRkID] = append(schedulesMap[s.ClassRkID], s)
|
|
}
|
|
|
|
allowedAppsMap := map[int64]string{}
|
|
for _, a := range allowedApps {
|
|
allowedAppsMap[a.ClassRkID] = a.Keywords
|
|
}
|
|
|
|
out := make([]classWithScheduleItem, 0, len(classes))
|
|
for _, cl := range classes {
|
|
sList := schedulesMap[cl.RkID]
|
|
if sList == nil {
|
|
sList = []models.ClassSchedule{}
|
|
}
|
|
specName := ""
|
|
if cl.SpecializeName != nil {
|
|
specName = *cl.SpecializeName
|
|
}
|
|
sysName := ""
|
|
if cl.SystemName != nil {
|
|
sysName = *cl.SystemName
|
|
}
|
|
|
|
out = append(out, classWithScheduleItem{
|
|
RkID: cl.RkID,
|
|
Name: cl.Name,
|
|
ClassCode: cl.ClassCode,
|
|
Type: cl.Type,
|
|
StudentCount: cl.StudentCount,
|
|
SpecializeName: specName,
|
|
SystemName: sysName,
|
|
Schedules: sList,
|
|
AllowedApps: allowedAppsMap[cl.RkID],
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"data": out})
|
|
}
|
|
}
|
|
|
|
// GET /api/classes/:rkId/schedule - Lấy lịch học của lớp
|
|
func GetClassScheduleHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
var schedules []models.ClassSchedule
|
|
if err := db.Where("class_rk_id = ?", rkID).Order("day_of_week, period, start_time").Find(&schedules).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"data": schedules})
|
|
}
|
|
}
|
|
|
|
// POST /api/classes/:rkId/schedule - Lưu lịch học của lớp
|
|
func SaveClassScheduleHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
type reqSchedule struct {
|
|
DayOfWeek int `json:"dayOfWeek"`
|
|
Period int `json:"period"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
CourseID int64 `json:"courseId"`
|
|
CourseName string `json:"courseName"`
|
|
IsActive bool `json:"isActive"`
|
|
}
|
|
|
|
var req struct {
|
|
Schedules []reqSchedule `json:"schedules"`
|
|
}
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload format"})
|
|
}
|
|
|
|
tx := db.Begin()
|
|
// Xóa lịch cũ
|
|
if err := tx.Where("class_rk_id = ?", rkID).Delete(&models.ClassSchedule{}).Error; err != nil {
|
|
tx.Rollback()
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to clear old schedules"})
|
|
}
|
|
|
|
// Insert lịch mới
|
|
if len(req.Schedules) > 0 {
|
|
var newSchedules []models.ClassSchedule
|
|
for _, s := range req.Schedules {
|
|
if strings.TrimSpace(s.StartTime) == "" || strings.TrimSpace(s.EndTime) == "" {
|
|
continue
|
|
}
|
|
period := s.Period
|
|
if period < 1 {
|
|
period = 1
|
|
}
|
|
if period > 4 {
|
|
period = 4
|
|
}
|
|
newSchedules = append(newSchedules, models.ClassSchedule{
|
|
ClassRkID: rkID,
|
|
DayOfWeek: s.DayOfWeek,
|
|
Period: period,
|
|
StartTime: s.StartTime,
|
|
EndTime: s.EndTime,
|
|
CourseID: s.CourseID,
|
|
CourseName: s.CourseName,
|
|
IsActive: s.IsActive,
|
|
})
|
|
}
|
|
if err := tx.Create(&newSchedules).Error; err != nil {
|
|
tx.Rollback()
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to insert new schedules"})
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Transaction commit failed"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"ok": true, "message": "Schedule saved successfully"})
|
|
}
|
|
}
|
|
|
|
// DELETE /api/classes/:rkId/schedule - Xóa lịch học của lớp (rút lớp khỏi active list)
|
|
func DeleteClassScheduleHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
if err := db.Where("class_rk_id = ?", rkID).Delete(&models.ClassSchedule{}).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"ok": true, "message": "Schedule deleted successfully"})
|
|
}
|
|
}
|
|
|
|
// GET /api/classes/:rkId/schedule-conflicts - Kiểm tra sinh viên trùng với lớp đang giám sát có lịch học
|
|
func GetClassScheduleConflictsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
// Lấy danh sách sinh viên của lớp muốn kích hoạt
|
|
var newClassStudents []int64
|
|
if err := db.Model(&models.ClassStudent{}).Where("class_rk_id = ?", rkID).Pluck("student_rk_id", &newClassStudents).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if len(newClassStudents) == 0 {
|
|
return c.JSON(fiber.Map{"conflicts": []any{}, "ok": true})
|
|
}
|
|
|
|
// Tìm các lớp đang giám sát CÓ lịch học (không bao gồm lớp đang được kiểm tra)
|
|
var activeClassIDs []int64
|
|
if err := db.Model(&models.ClassSchedule{}).Distinct("class_rk_id").Pluck("class_rk_id", &activeClassIDs).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
// Lọc bỏ chính lớp đang kiểm tra
|
|
var otherActiveIDs []int64
|
|
for _, id := range activeClassIDs {
|
|
if id != rkID {
|
|
otherActiveIDs = append(otherActiveIDs, id)
|
|
}
|
|
}
|
|
if len(otherActiveIDs) == 0 {
|
|
return c.JSON(fiber.Map{"conflicts": []any{}, "ok": true})
|
|
}
|
|
|
|
// Tìm sinh viên trùng
|
|
var overlappingStudentIDs []int64
|
|
if err := db.Model(&models.ClassStudent{}).
|
|
Where("class_rk_id IN ? AND student_rk_id IN ?", otherActiveIDs, newClassStudents).
|
|
Pluck("student_rk_id", &overlappingStudentIDs).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if len(overlappingStudentIDs) == 0 {
|
|
return c.JSON(fiber.Map{"conflicts": []any{}, "ok": true})
|
|
}
|
|
|
|
// Lấy thông tin sinh viên và lớp bị trùng
|
|
type conflictItem struct {
|
|
StudentRkID int64 `json:"studentRkId"`
|
|
FullName string `json:"fullName"`
|
|
StudentCode string `json:"studentCode"`
|
|
ConflictClassRkID int64 `json:"conflictClassRkId"`
|
|
ConflictClassName string `json:"conflictClassName"`
|
|
ConflictClassCode string `json:"conflictClassCode"`
|
|
}
|
|
|
|
var conflicts []conflictItem
|
|
for _, sid := range overlappingStudentIDs {
|
|
var st models.Student
|
|
if err := db.Where("rk_id = ?", sid).First(&st).Error; err != nil {
|
|
continue
|
|
}
|
|
var classIDs []int64
|
|
_ = db.Model(&models.ClassStudent{}).Where("class_rk_id IN ? AND student_rk_id = ?", otherActiveIDs, sid).Pluck("class_rk_id", &classIDs)
|
|
for _, cid := range classIDs {
|
|
var cls models.Class
|
|
if err := db.Where("rk_id = ?", cid).First(&cls).Error; err != nil {
|
|
continue
|
|
}
|
|
conflicts = append(conflicts, conflictItem{
|
|
StudentRkID: sid,
|
|
FullName: st.FullName,
|
|
StudentCode: st.StudentCode,
|
|
ConflictClassRkID: cid,
|
|
ConflictClassName: cls.Name,
|
|
ConflictClassCode: cls.ClassCode,
|
|
})
|
|
}
|
|
}
|
|
if conflicts == nil {
|
|
conflicts = []conflictItem{}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"conflicts": conflicts, "ok": len(conflicts) == 0})
|
|
}
|
|
}
|
|
|
|
func parseTimeMinutes(tStr string) int {
|
|
parts := strings.Split(tStr, ":")
|
|
if len(parts) != 2 {
|
|
return 0
|
|
}
|
|
hours, _ := strconv.Atoi(parts[0])
|
|
mins, _ := strconv.Atoi(parts[1])
|
|
return hours*60 + mins
|
|
}
|
|
|
|
// GET /api/classes/:rkId/allowed-apps - Lấy danh sách app được phép
|
|
func GetAllowedAppsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
studentIDStr := c.Query("studentId", "0")
|
|
studentID, _ := strconv.ParseInt(studentIDStr, 10, 64)
|
|
|
|
// Phòng thi ưu tiên hơn lịch học
|
|
if studentID > 0 {
|
|
if examInfo := internalDb.FindActiveExamForStudent(db, studentID); examInfo != nil {
|
|
keywords := strings.TrimSpace(examInfo.Room.AllowedApps)
|
|
if keywords == "" {
|
|
keywords = systemAllowedApps
|
|
} else {
|
|
keywords = keywords + "," + systemAllowedApps
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"classRkId": rkID,
|
|
"keywords": keywords,
|
|
"exit": false,
|
|
"examMode": true,
|
|
"examRoomId": examInfo.Room.ID,
|
|
})
|
|
}
|
|
}
|
|
|
|
if studentID > 0 {
|
|
resolvedClassID := internalDb.FindActiveClassForStudent(db, studentID)
|
|
if resolvedClassID > 0 {
|
|
rkID = resolvedClassID
|
|
}
|
|
}
|
|
|
|
// 1. Kiểm tra xem lớp đã cấu hình app được phép chưa
|
|
var appConfig models.ClassAllowedApp
|
|
errApp := db.Where("class_rk_id = ?", rkID).First(&appConfig).Error
|
|
hasAppsConfigured := (errApp == nil && strings.TrimSpace(appConfig.Keywords) != "")
|
|
|
|
// Nếu là request từ Staff (có Authorization Bearer token hợp lệ), trả về trực tiếp danh sách keywords đã lưu
|
|
authHeader := c.Get("Authorization")
|
|
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
|
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
|
if _, err := auth.ParseToken(token); err == nil {
|
|
keywords := ""
|
|
if errApp == nil {
|
|
keywords = appConfig.Keywords
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"classRkId": rkID,
|
|
"keywords": keywords,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 2. Kiểm tra xem có trong giờ học không
|
|
var schedules []models.ClassSchedule
|
|
errSched := db.Where("class_rk_id = ?", rkID).Find(&schedules).Error
|
|
inSchedule := false
|
|
if errSched == nil && len(schedules) > 0 {
|
|
now := time.Now()
|
|
goWeekday := now.Weekday()
|
|
var scheduleDay int
|
|
if goWeekday == time.Sunday {
|
|
scheduleDay = 6
|
|
} else {
|
|
scheduleDay = int(goWeekday) - 1
|
|
}
|
|
currMin := now.Hour()*60 + now.Minute()
|
|
for _, s := range schedules {
|
|
if s.DayOfWeek == scheduleDay && s.IsActive {
|
|
startMin := parseTimeMinutes(s.StartTime)
|
|
endMin := parseTimeMinutes(s.EndTime)
|
|
if currMin >= startMin && currMin <= endMin {
|
|
inSchedule = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nếu lớp chưa cấu hình app được phép, hoặc không trong giờ học
|
|
if !hasAppsConfigured || !inSchedule {
|
|
// Tắt trạng thái đang học của lớp trong DB (nếu đang bật)
|
|
var cl models.Class
|
|
if errClass := db.Where("rk_id = ?", rkID).First(&cl).Error; errClass == nil {
|
|
if cl.IsStudying {
|
|
cl.IsStudying = false
|
|
db.Save(&cl)
|
|
}
|
|
}
|
|
|
|
reason := ""
|
|
if !hasAppsConfigured {
|
|
reason = "Lớp chưa được xét danh sách ứng dụng được phép."
|
|
} else {
|
|
reason = "Hiện tại ngoài giờ học theo lịch đã cài đặt."
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"classRkId": rkID,
|
|
"keywords": "",
|
|
"exit": true,
|
|
"reason": reason,
|
|
})
|
|
}
|
|
|
|
keywords := strings.TrimSpace(appConfig.Keywords)
|
|
if keywords != "" {
|
|
keywords = keywords + "," + systemAllowedApps
|
|
} else {
|
|
keywords = systemAllowedApps
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"classRkId": appConfig.ClassRkID,
|
|
"keywords": keywords,
|
|
"exit": false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// POST /api/classes/:rkId/allowed-apps - Lưu danh sách app được phép
|
|
func SaveAllowedAppsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
rkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
var req struct {
|
|
Keywords string `json:"keywords"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload format"})
|
|
}
|
|
|
|
appConfig := models.ClassAllowedApp{
|
|
ClassRkID: rkID,
|
|
Keywords: req.Keywords,
|
|
}
|
|
|
|
if err := db.Save(&appConfig).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"ok": true, "data": appConfig})
|
|
}
|
|
}
|
|
|
|
// POST /api/student/sync-log - Nhận log học tập (online/offline time, wifi SSID) tích lũy từ client
|
|
func SyncStudentSessionLogHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
var req struct {
|
|
StudentRkID int64 `json:"studentRkId"`
|
|
ClassRkID int64 `json:"classRkId"`
|
|
SessionDate string `json:"sessionDate"` // YYYY-MM-DD
|
|
AddOnlineSecs int `json:"addOnlineSeconds"`
|
|
AddOfflineSecs int `json:"addOfflineSeconds"`
|
|
WifiSSID string `json:"wifiSsid"`
|
|
Avatar string `json:"avatar"`
|
|
}
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload format"})
|
|
}
|
|
|
|
if req.StudentRkID == 0 || req.SessionDate == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing key parameters"})
|
|
}
|
|
|
|
// Dynamically resolve active class + ca theo lịch học
|
|
resolvedClassID, resolvedPeriod := internalDb.FindActiveClassAndPeriodForStudent(db, req.StudentRkID)
|
|
if resolvedClassID > 0 {
|
|
req.ClassRkID = resolvedClassID
|
|
}
|
|
period := resolvedPeriod
|
|
if period < 1 {
|
|
period = 1
|
|
}
|
|
|
|
if req.ClassRkID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Student does not belong to any class"})
|
|
}
|
|
|
|
if avatar := util.NormalizeAvatarURL(req.Avatar); avatar != "" {
|
|
_ = db.Model(&models.Student{}).Where("rk_id = ?", req.StudentRkID).Update("avatar", avatar).Error
|
|
}
|
|
|
|
var session models.StudentSession
|
|
err := db.Where("student_rk_id = ? AND class_rk_id = ? AND session_date = ? AND period = ?",
|
|
req.StudentRkID, req.ClassRkID, req.SessionDate, period).First(&session).Error
|
|
|
|
now := time.Now()
|
|
if gorm.ErrRecordNotFound == err {
|
|
// Tạo record mới
|
|
session = models.StudentSession{
|
|
StudentRkID: req.StudentRkID,
|
|
ClassRkID: req.ClassRkID,
|
|
SessionDate: req.SessionDate,
|
|
Period: period,
|
|
OnlineSeconds: req.AddOnlineSecs,
|
|
OfflineSeconds: req.AddOfflineSecs,
|
|
WifiSSIDs: req.WifiSSID,
|
|
LastActiveAt: now,
|
|
}
|
|
if err := db.Create(&session).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create student session: " + err.Error()})
|
|
}
|
|
} else if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
} else {
|
|
// Cộng dồn thời gian
|
|
session.OnlineSeconds += req.AddOnlineSecs
|
|
session.OfflineSeconds += req.AddOfflineSecs
|
|
session.LastActiveAt = now
|
|
|
|
// Thêm Wifi SSID nếu chưa tồn tại trong danh sách
|
|
if req.WifiSSID != "" {
|
|
ssids := strings.Split(session.WifiSSIDs, ",")
|
|
exists := false
|
|
for _, s := range ssids {
|
|
if strings.TrimSpace(s) == req.WifiSSID {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
if session.WifiSSIDs == "" {
|
|
session.WifiSSIDs = req.WifiSSID
|
|
} else {
|
|
session.WifiSSIDs = session.WifiSSIDs + "," + req.WifiSSID
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := db.Save(&session).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update student session: " + err.Error()})
|
|
}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"ok": true, "session": session})
|
|
}
|
|
}
|
|
|
|
type studentSessionLogItem struct {
|
|
ID uint `json:"id"`
|
|
StudentRkID int64 `json:"studentRkId"`
|
|
StudentCode string `json:"studentCode"`
|
|
FullName string `json:"fullName"`
|
|
Email string `json:"email"`
|
|
Period int `json:"period"`
|
|
OnlineSeconds int `json:"onlineSeconds"`
|
|
OfflineSeconds int `json:"offlineSeconds"`
|
|
WifiSSIDs string `json:"wifiSsids"`
|
|
LastActiveAt time.Time `json:"lastActiveAt"`
|
|
}
|
|
|
|
// GET /api/classes/:rkId/session-logs - Lấy danh sách logs điểm danh của lớp trong ngày
|
|
func ListClassSessionLogsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
classRkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
date := c.Query("date")
|
|
if date == "" {
|
|
date = time.Now().Format("2006-01-02")
|
|
}
|
|
period := atoiDefault(c.Query("period"), 0)
|
|
if period <= 0 {
|
|
period = internalDb.ResolveSessionPeriodForDate(db, classRkID, date, 0)
|
|
}
|
|
|
|
// Lấy danh sách học sinh của lớp từ class_students mapping
|
|
var mappings []models.ClassStudent
|
|
if err := db.Where("class_rk_id = ?", classRkID).Find(&mappings).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
if len(mappings) == 0 {
|
|
return c.JSON(fiber.Map{"data": []any{}})
|
|
}
|
|
|
|
var studentRkIDs []int64
|
|
for _, m := range mappings {
|
|
studentRkIDs = append(studentRkIDs, m.StudentRkID)
|
|
}
|
|
|
|
var students []models.Student
|
|
if err := db.Where("rk_id IN ?", studentRkIDs).Find(&students).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
// Lấy log session theo ca trong ngày
|
|
var sessions []models.StudentSession
|
|
periods := []int{period}
|
|
if period == 1 {
|
|
periods = append(periods, 0) // dữ liệu cũ gộp vào ca 1
|
|
}
|
|
if err := db.Where("class_rk_id = ? AND session_date = ? AND student_rk_id IN ? AND period IN ?",
|
|
classRkID, date, studentRkIDs, periods).Find(&sessions).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
sessionsMap := map[int64]models.StudentSession{}
|
|
for _, s := range sessions {
|
|
existing, ok := sessionsMap[s.StudentRkID]
|
|
if !ok || s.Period > existing.Period {
|
|
sessionsMap[s.StudentRkID] = s
|
|
}
|
|
}
|
|
|
|
out := make([]studentSessionLogItem, 0, len(students))
|
|
for _, st := range students {
|
|
sess, ok := sessionsMap[st.RkID]
|
|
onlineSec := 0
|
|
offlineSec := 0
|
|
wifi := "—"
|
|
var lastAct time.Time
|
|
if ok {
|
|
onlineSec = sess.OnlineSeconds
|
|
offlineSec = sess.OfflineSeconds
|
|
wifi = sess.WifiSSIDs
|
|
lastAct = sess.LastActiveAt
|
|
}
|
|
|
|
out = append(out, studentSessionLogItem{
|
|
ID: sess.ID,
|
|
StudentRkID: st.RkID,
|
|
StudentCode: st.StudentCode,
|
|
FullName: st.FullName,
|
|
Email: st.Email,
|
|
Period: period,
|
|
OnlineSeconds: onlineSec,
|
|
OfflineSeconds: offlineSec,
|
|
WifiSSIDs: wifi,
|
|
LastActiveAt: lastAct,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"data": out, "period": period, "date": date})
|
|
}
|
|
}
|
|
|
|
// GET /api/classes/:rkId/online-students - Lấy danh sách ID sinh viên đang online
|
|
func GetOnlineStudentsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
classRkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
|
|
onlineIDs := internalWs.Hub.GetOnlineStudentIDs(classRkID)
|
|
return c.JSON(fiber.Map{"onlineStudentIds": onlineIDs})
|
|
}
|
|
}
|
|
|
|
func processKeyword(processName string) string {
|
|
name := strings.ToLower(strings.TrimSpace(processName))
|
|
return strings.TrimSuffix(name, ".exe")
|
|
}
|
|
|
|
func processKey(processName string) string {
|
|
return strings.ToLower(strings.TrimSpace(processName))
|
|
}
|
|
|
|
// POST /api/student/report-blocked-app — client báo app vừa bị chặn → kho global
|
|
func ReportBlockedAppHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
var req struct {
|
|
StudentRkID int64 `json:"studentRkId"`
|
|
ProcessName string `json:"processName"`
|
|
WindowTitle string `json:"windowTitle"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload format"})
|
|
}
|
|
if req.StudentRkID <= 0 || strings.TrimSpace(req.ProcessName) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId and processName are required"})
|
|
}
|
|
|
|
processName := strings.TrimSpace(req.ProcessName)
|
|
pKey := processKey(processName)
|
|
keyword := processKeyword(processName)
|
|
if pKey == "" || keyword == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid processName"})
|
|
}
|
|
|
|
lastClassID := internalDb.FindActiveClassForStudent(db, req.StudentRkID)
|
|
now := time.Now()
|
|
|
|
var row models.AppPoolEntry
|
|
err := db.Where("process_key = ?", pKey).First(&row).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
row = models.AppPoolEntry{
|
|
ProcessName: processName,
|
|
ProcessKey: pKey,
|
|
WindowTitle: req.WindowTitle,
|
|
Keyword: keyword,
|
|
HitCount: 1,
|
|
LastSeenAt: now,
|
|
LastStudentRkID: req.StudentRkID,
|
|
LastClassRkID: lastClassID,
|
|
}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
} else if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
} else {
|
|
row.HitCount++
|
|
row.LastSeenAt = now
|
|
row.WindowTitle = req.WindowTitle
|
|
row.ProcessName = processName
|
|
if req.StudentRkID > 0 {
|
|
row.LastStudentRkID = req.StudentRkID
|
|
}
|
|
if lastClassID > 0 {
|
|
row.LastClassRkID = lastClassID
|
|
}
|
|
if err := db.Save(&row).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{"ok": true, "data": row})
|
|
}
|
|
}
|
|
|
|
// POST /api/student/report-violation — SV tắt app / vi phạm khi đang giám sát
|
|
func ReportViolationHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
var req struct {
|
|
StudentRkID int64 `json:"studentRkId"`
|
|
Kind string `json:"kind"`
|
|
Reason string `json:"reason"`
|
|
MonitorMode string `json:"monitorMode"`
|
|
ClientAt string `json:"clientAt"` // RFC3339 optional
|
|
ClassRkID int64 `json:"classRkId"`
|
|
ExamRoomID uint `json:"examRoomId"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload format"})
|
|
}
|
|
if req.StudentRkID <= 0 || strings.TrimSpace(req.Kind) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId and kind are required"})
|
|
}
|
|
|
|
monitorMode := strings.TrimSpace(req.MonitorMode)
|
|
// Chỉ lưu khi đang thi hoặc đang học — ngoài giờ / chưa cấu hình bỏ qua
|
|
if monitorMode != "exam" && monitorMode != "learning" {
|
|
return c.JSON(fiber.Map{"ok": true, "skipped": true, "reason": "not_in_exam_or_learning"})
|
|
}
|
|
|
|
kind := strings.ToLower(strings.TrimSpace(req.Kind))
|
|
reason := strings.TrimSpace(req.Reason)
|
|
if reason == "" {
|
|
reason = kind
|
|
}
|
|
|
|
classID := req.ClassRkID
|
|
if classID <= 0 {
|
|
classID = internalDb.FindActiveClassForStudent(db, req.StudentRkID)
|
|
}
|
|
|
|
examRoomID := req.ExamRoomID
|
|
if examRoomID == 0 && monitorMode == "exam" {
|
|
if examInfo := internalDb.FindActiveExamForStudent(db, req.StudentRkID); examInfo != nil {
|
|
examRoomID = examInfo.Room.ID
|
|
}
|
|
}
|
|
|
|
clientAt := time.Now()
|
|
if t, err := time.Parse(time.RFC3339, strings.TrimSpace(req.ClientAt)); err == nil {
|
|
clientAt = t
|
|
}
|
|
|
|
row := models.StudentViolation{
|
|
StudentRkID: req.StudentRkID,
|
|
ClassRkID: classID,
|
|
ExamRoomID: examRoomID,
|
|
Kind: kind,
|
|
Reason: reason,
|
|
MonitorMode: monitorMode,
|
|
ClientAt: clientAt,
|
|
}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
studentName := ""
|
|
studentCode := ""
|
|
var st models.Student
|
|
if err := db.Where("rk_id = ?", req.StudentRkID).First(&st).Error; err == nil {
|
|
studentName = st.FullName
|
|
studentCode = st.StudentCode
|
|
}
|
|
|
|
// Cố ý tắt / crash / môi trường → offline ngay, không dùng grace 8s
|
|
switch kind {
|
|
case "app_closed", "unclean_shutdown", "wifi",
|
|
"guard", "multi_monitor", "user_switch", "session_change", "virtual_desktop":
|
|
internalWs.Hub.ForceStudentOffline(req.StudentRkID)
|
|
}
|
|
|
|
internalWs.Hub.BroadcastStudentViolation(map[string]any{
|
|
"id": row.ID,
|
|
"studentId": req.StudentRkID,
|
|
"studentName": studentName,
|
|
"studentCode": studentCode,
|
|
"classId": classID,
|
|
"examRoomId": examRoomID,
|
|
"kind": kind,
|
|
"reason": reason,
|
|
"monitorMode": row.MonitorMode,
|
|
"clientAt": clientAt.Format(time.RFC3339),
|
|
"createdAt": row.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
|
|
return c.JSON(fiber.Map{"ok": true, "id": row.ID, "classRkId": classID, "examRoomId": examRoomID})
|
|
}
|
|
}
|
|
|
|
type studentViolationItem struct {
|
|
ID uint `json:"id"`
|
|
StudentRkID int64 `json:"studentRkId"`
|
|
StudentCode string `json:"studentCode"`
|
|
FullName string `json:"fullName"`
|
|
ClassRkID int64 `json:"classRkId"`
|
|
ExamRoomID uint `json:"examRoomId"`
|
|
Kind string `json:"kind"`
|
|
Reason string `json:"reason"`
|
|
MonitorMode string `json:"monitorMode"`
|
|
ClientAt time.Time `json:"clientAt"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
}
|
|
|
|
// GET /api/classes/:rkId/violations?date=YYYY-MM-DD&kind=
|
|
func ListClassViolationsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
classRkID, err := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid class rkId"})
|
|
}
|
|
date := c.Query("date")
|
|
if date == "" {
|
|
date = time.Now().Format("2006-01-02")
|
|
}
|
|
kindFilter := strings.TrimSpace(c.Query("kind", ""))
|
|
|
|
var mappings []models.ClassStudent
|
|
if err := db.Where("class_rk_id = ?", classRkID).Find(&mappings).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if len(mappings) == 0 {
|
|
return c.JSON(fiber.Map{"data": []any{}, "date": date})
|
|
}
|
|
studentIDs := make([]int64, 0, len(mappings))
|
|
for _, m := range mappings {
|
|
studentIDs = append(studentIDs, m.StudentRkID)
|
|
}
|
|
|
|
var students []models.Student
|
|
if err := db.Where("rk_id IN ?", studentIDs).Find(&students).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
nameByID := map[int64]models.Student{}
|
|
for _, st := range students {
|
|
nameByID[st.RkID] = st
|
|
}
|
|
|
|
dayStart, err := time.ParseInLocation("2006-01-02", date, time.Local)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid date"})
|
|
}
|
|
dayEnd := dayStart.Add(24 * time.Hour)
|
|
|
|
// Ưu tiên vi phạm gắn đúng lớp; fallback bản ghi cũ (class_rk_id=0) của SV trong lớp khi đang học
|
|
q := db.Where(
|
|
"created_at >= ? AND created_at < ? AND ((class_rk_id = ?) OR (class_rk_id = 0 AND monitor_mode = ? AND student_rk_id IN ?))",
|
|
dayStart, dayEnd, classRkID, "learning", studentIDs,
|
|
)
|
|
if kindFilter != "" {
|
|
q = q.Where("kind = ?", kindFilter)
|
|
}
|
|
var rows []models.StudentViolation
|
|
if err := q.Order("created_at desc").Limit(500).Find(&rows).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
out := make([]studentViolationItem, 0, len(rows))
|
|
for _, r := range rows {
|
|
st := nameByID[r.StudentRkID]
|
|
if st.RkID == 0 {
|
|
_ = db.Where("rk_id = ?", r.StudentRkID).First(&st).Error
|
|
}
|
|
out = append(out, studentViolationItem{
|
|
ID: r.ID,
|
|
StudentRkID: r.StudentRkID,
|
|
StudentCode: st.StudentCode,
|
|
FullName: st.FullName,
|
|
ClassRkID: r.ClassRkID,
|
|
ExamRoomID: r.ExamRoomID,
|
|
Kind: r.Kind,
|
|
Reason: r.Reason,
|
|
MonitorMode: r.MonitorMode,
|
|
ClientAt: r.ClientAt,
|
|
CreatedAt: r.CreatedAt,
|
|
})
|
|
}
|
|
return c.JSON(fiber.Map{"data": out, "date": date})
|
|
}
|
|
}
|
|
|
|
// GET /api/exam-rooms/:id/violations?date=YYYY-MM-DD
|
|
func ListExamRoomViolationsHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid exam room id"})
|
|
}
|
|
date := c.Query("date")
|
|
if date == "" {
|
|
date = time.Now().Format("2006-01-02")
|
|
}
|
|
kindFilter := strings.TrimSpace(c.Query("kind", ""))
|
|
|
|
var roster []models.ExamRoomStudent
|
|
if err := db.Where("exam_room_id = ?", id).Find(&roster).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if len(roster) == 0 {
|
|
return c.JSON(fiber.Map{"data": []any{}, "date": date})
|
|
}
|
|
studentIDs := make([]int64, 0, len(roster))
|
|
for _, s := range roster {
|
|
studentIDs = append(studentIDs, s.StudentRkID)
|
|
}
|
|
|
|
var students []models.Student
|
|
_ = db.Where("rk_id IN ?", studentIDs).Find(&students)
|
|
nameByID := map[int64]models.Student{}
|
|
for _, st := range students {
|
|
nameByID[st.RkID] = st
|
|
}
|
|
|
|
dayStart, err := time.ParseInLocation("2006-01-02", date, time.Local)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid date"})
|
|
}
|
|
dayEnd := dayStart.Add(24 * time.Hour)
|
|
|
|
// Ưu tiên exam_room_id; fallback bản ghi cũ (exam_room_id=0, mode=exam) của SV trong phòng
|
|
q := db.Where(
|
|
"created_at >= ? AND created_at < ? AND ((exam_room_id = ?) OR (exam_room_id = 0 AND monitor_mode = ? AND student_rk_id IN ?))",
|
|
dayStart, dayEnd, id, "exam", studentIDs,
|
|
)
|
|
if kindFilter != "" {
|
|
q = q.Where("kind = ?", kindFilter)
|
|
}
|
|
var rows []models.StudentViolation
|
|
if err := q.Order("created_at desc").Limit(500).Find(&rows).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
out := make([]studentViolationItem, 0, len(rows))
|
|
for _, r := range rows {
|
|
st := nameByID[r.StudentRkID]
|
|
out = append(out, studentViolationItem{
|
|
ID: r.ID,
|
|
StudentRkID: r.StudentRkID,
|
|
StudentCode: st.StudentCode,
|
|
FullName: st.FullName,
|
|
ClassRkID: r.ClassRkID,
|
|
ExamRoomID: r.ExamRoomID,
|
|
Kind: r.Kind,
|
|
Reason: r.Reason,
|
|
MonitorMode: r.MonitorMode,
|
|
ClientAt: r.ClientAt,
|
|
CreatedAt: r.CreatedAt,
|
|
})
|
|
}
|
|
return c.JSON(fiber.Map{"data": out, "date": date})
|
|
}
|
|
}
|
|
|
|
// GET /api/app-pool — kho app bị chặn toàn hệ thống (?q= tìm kiếm, limit= mặc định 50)
|
|
func ListAppPoolHandler(db *gorm.DB) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
q := strings.TrimSpace(c.Query("q", ""))
|
|
limit := 50
|
|
if l, err := strconv.Atoi(c.Query("limit", "50")); err == nil && l > 0 && l <= 200 {
|
|
limit = l
|
|
}
|
|
|
|
query := db.Model(&models.AppPoolEntry{})
|
|
if q != "" {
|
|
like := "%" + q + "%"
|
|
query = query.Where(
|
|
"keyword LIKE ? OR process_name LIKE ? OR window_title LIKE ? OR process_key LIKE ?",
|
|
like, like, like, like,
|
|
)
|
|
}
|
|
|
|
var rows []models.AppPoolEntry
|
|
if err := query.Order("hit_count DESC, last_seen_at DESC").Limit(limit).Find(&rows).Error; err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"data": rows, "q": q, "limit": limit})
|
|
}
|
|
}
|