package handlers import ( "strconv" "strings" "time" internalDb "server/internal/db" "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) != "") // 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}) } } // 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}) } }