This commit is contained in:
2026-06-30 09:31:33 +07:00
parent c736326162
commit bbf8336664
77 changed files with 12601 additions and 556 deletions

View File

@@ -3,7 +3,9 @@ package handlers
import (
"strconv"
"strings"
"time"
internalDb "server/internal/db"
"server/internal/models"
"server/internal/qldt"
"server/internal/syncjobs"
@@ -126,31 +128,8 @@ func ListClassesHandler(db *gorm.DB) fiber.Handler {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to query classes: " + err.Error()})
}
// Kéo danh sách Courses cho các lớp
var classIDs []int64
for _, r := range rows {
classIDs = append(classIDs, r.RkID)
}
coursesByClass := map[int64][]courseRefItem{}
if len(classIDs) > 0 {
var links []models.ClassCourseLink
if err := db.Where("class_rk_id IN ?", classIDs).Order("course_order asc").Find(&links).Error; err == nil {
for _, link := range links {
coursesByClass[link.ClassRkID] = append(coursesByClass[link.ClassRkID], courseRefItem{
CourseRkID: link.CourseRkID,
CourseName: link.CourseName,
})
}
}
}
out := make([]classItem, 0, len(rows))
for _, r := range rows {
courses := coursesByClass[r.RkID]
if courses == nil {
courses = []courseRefItem{}
}
out = append(out, classItem{
RkID: r.RkID,
Name: r.Name,
@@ -163,7 +142,7 @@ func ListClassesHandler(db *gorm.DB) fiber.Handler {
SystemCode: r.SystemCode,
SystemName: r.SystemName,
IsStudying: r.IsStudying,
Courses: courses,
Courses: []courseRefItem{},
})
}
@@ -176,6 +155,48 @@ func ListClassesHandler(db *gorm.DB) fiber.Handler {
}
}
// GET /api/classes/:rkId - Lấy thông tin chi tiết một lớp học
func GetClassHandler(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 row models.Class
if err := db.Where("rk_id = ?", rkID).First(&row).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Class not found"})
}
coursesOut := []courseRefItem{}
if cached, err := internalDb.ListClassCourses(db, rkID); err == nil {
for _, co := range cached {
coursesOut = append(coursesOut, courseRefItem{
CourseRkID: co.ID,
CourseName: co.Name,
})
}
}
out := classItem{
RkID: row.RkID,
Name: row.Name,
ClassCode: row.ClassCode,
Type: row.Type,
StudentCount: row.StudentCount,
SpecializeRkID: row.SpecializeRkID,
SpecializeName: row.SpecializeName,
SystemRkID: row.SystemRkID,
SystemCode: row.SystemCode,
SystemName: row.SystemName,
IsStudying: row.IsStudying,
Courses: coursesOut,
}
return c.JSON(out)
}
}
// Handler cập nhật trạng thái đang học (isStudying) của lớp học
func PatchClassStudyingHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
@@ -200,6 +221,45 @@ func PatchClassStudyingHandler(db *gorm.DB) fiber.Handler {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if body.IsStudying {
// 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
if errApp == gorm.ErrRecordNotFound || strings.TrimSpace(appConfig.Keywords) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Không thể kích hoạt trạng thái đang học: Lớp chưa được cấu hình danh sách ứng dụng cho phép."})
}
// 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 {
startMin := parseTimeMinutes(s.StartTime)
endMin := parseTimeMinutes(s.EndTime)
if currMin >= startMin && currMin <= endMin {
inSchedule = true
break
}
}
}
}
if !inSchedule {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Không thể kích hoạt trạng thái đang học: Hiện tại ngoài giờ học theo lịch đã cài đặt."})
}
}
cl.IsStudying = body.IsStudying
if err := db.Save(&cl).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update class: " + err.Error()})
@@ -236,7 +296,32 @@ func ListClassStudentsHandler(db *gorm.DB) fiber.Handler {
}
}
return c.JSON(fiber.Map{"data": students, "total": len(students)})
type studentRow struct {
ID uint `json:"id"`
RkID int64 `json:"rkId"`
StudentCode string `json:"studentCode"`
FullName string `json:"fullName"`
Phone *string `json:"phone"`
Email string `json:"email"`
Avatar string `json:"avatar"`
}
out := make([]studentRow, 0, len(students))
for _, st := range students {
row := studentRow{
ID: st.ID,
RkID: st.RkID,
StudentCode: st.StudentCode,
FullName: st.FullName,
Phone: st.Phone,
Email: st.Email,
}
if st.Avatar != nil {
row.Avatar = *st.Avatar
}
out = append(out, row)
}
return c.JSON(fiber.Map{"data": out, "total": len(out)})
}
}

View File

@@ -0,0 +1,462 @@
package handlers
import (
"context"
"fmt"
"strconv"
"time"
internalDb "server/internal/db"
"server/internal/models"
"server/internal/qldt"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var standardShiftTemplate = []struct {
Period int
StartTime string
EndTime string
}{
{1, "07:00", "09:00"},
{2, "09:10", "11:10"},
{3, "12:10", "14:10"},
{4, "14:20", "16:20"},
}
// GET /api/classes/:rkId/courses
func GetClassCoursesHandler(db *gorm.DB, qldtClient *qldt.Client, token string) 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 liveErr error
if token != "" {
res, err := qldtClient.GetClassCourses(context.Background(), token, rkID)
if err == nil && len(res.Data) > 0 {
dtos := qldtCourseItemsToDTO(res.Data)
_ = internalDb.UpsertClassCourses(db, rkID, dtos)
return c.JSON(fiber.Map{"data": dtos, "source": "qldt"})
}
liveErr = err
} else {
liveErr = fmt.Errorf("QLDT_TOKEN chưa cấu hình")
}
cached, err := internalDb.ListClassCourses(db, rkID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if len(cached) > 0 {
resp := fiber.Map{
"data": cached,
"source": "cache",
"cached": true,
}
if liveErr != nil {
resp["warning"] = liveErr.Error()
}
return c.JSON(resp)
}
if liveErr != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{
"error": liveErr.Error(),
"hint": "Cập nhật QLDT_TOKEN trong server/.env rồi chạy Sync lớp học để tải danh sách môn.",
})
}
return c.JSON(fiber.Map{"data": []internalDb.ClassCourseDTO{}})
}
}
func qldtCourseItemsToDTO(items []qldt.CourseItem) []internalDb.ClassCourseDTO {
out := make([]internalDb.ClassCourseDTO, 0, len(items))
for _, it := range items {
if it.ID <= 0 {
continue
}
out = append(out, internalDb.ClassCourseDTO{
ID: it.ID,
Name: it.Name,
CourseCode: it.CourseCode,
})
}
return out
}
// POST /api/classes/:rkId/schedule/apply-template
func ApplyScheduleTemplateHandler(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"})
}
created := 0
skipped := 0
for day := 0; day <= 4; day++ {
for _, slot := range standardShiftTemplate {
var existing int64
db.Model(&models.ClassSchedule{}).
Where("class_rk_id = ? AND day_of_week = ? AND period = ?", rkID, day, slot.Period).
Count(&existing)
if existing > 0 {
skipped++
continue
}
row := models.ClassSchedule{
ClassRkID: rkID,
DayOfWeek: day,
Period: slot.Period,
StartTime: slot.StartTime,
EndTime: slot.EndTime,
CourseID: 0,
CourseName: "",
IsActive: false,
}
if err := db.Create(&row).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
created++
}
}
return c.JSON(fiber.Map{"ok": true, "created": created, "skipped": skipped})
}
}
type attendanceRow struct {
StudentRkID int64 `json:"studentRkId"`
StudentCode string `json:"studentCode"`
FullName string `json:"fullName"`
Email string `json:"email"`
Status int `json:"status"`
StatusLabel string `json:"statusLabel"`
OnlineMinutes int `json:"onlineMinutes"`
StatusEditedByTeacher bool `json:"statusEditedByTeacher"`
}
func resolveScheduleForPeriod(db *gorm.DB, classRkID int64, dayOfWeek, period int) (*models.ClassSchedule, error) {
var row models.ClassSchedule
err := db.Where("class_rk_id = ? AND day_of_week = ? AND period = ?", classRkID, dayOfWeek, period).First(&row).Error
if err == nil {
return &row, nil
}
var slots []models.ClassSchedule
if err := db.Where("class_rk_id = ? AND day_of_week = ?", classRkID, dayOfWeek).
Order("start_time").Find(&slots).Error; err != nil {
return nil, err
}
if period < 1 || period > len(slots) {
return nil, gorm.ErrRecordNotFound
}
return &slots[period-1], nil
}
func recalcAttendanceForPeriod(db *gorm.DB, classRkID int64, date string, period int, dayOfWeek int) error {
sched, err := resolveScheduleForPeriod(db, classRkID, dayOfWeek, period)
if err != nil {
return err
}
shiftMins := qldt.ShiftDurationMinutes(sched.StartTime, sched.EndTime)
var mappings []models.ClassStudent
if err := db.Where("class_rk_id = ?", classRkID).Find(&mappings).Error; err != nil {
return err
}
for _, m := range mappings {
var session models.StudentSession
onlineMins := 0
periods := []int{period}
if period == 1 {
periods = append(periods, 0)
}
err := db.Where("student_rk_id = ? AND class_rk_id = ? AND session_date = ? AND period IN ?",
m.StudentRkID, classRkID, date, periods).
Order("period DESC").First(&session).Error
if err == nil {
onlineMins = session.OnlineSeconds / 60
}
status := qldt.InferStatusFromOnlineMinutes(onlineMins, shiftMins)
label := qldt.AttendanceStatusLabel(status)
row := models.AttendanceResult{
ClassRkID: classRkID,
SessionDate: date,
Period: period,
StudentRkID: m.StudentRkID,
Status: status,
StatusLabel: label,
OnlineMinutes: onlineMins,
}
if err := db.Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "class_rk_id"},
{Name: "session_date"},
{Name: "period"},
{Name: "student_rk_id"},
},
DoUpdates: clause.Assignments(map[string]interface{}{
"online_minutes": gorm.Expr("VALUES(online_minutes)"),
"status": gorm.Expr("IF(status_edited_by_teacher, status, VALUES(status))"),
"status_label": gorm.Expr("IF(status_edited_by_teacher, status_label, VALUES(status_label))"),
"updated_at": time.Now(),
}),
}).Create(&row).Error; err != nil {
return err
}
}
return nil
}
func scheduleDayFromDate(date string) (int, error) {
t, err := time.Parse("2006-01-02", date)
if err != nil {
return 0, err
}
wd := t.Weekday()
if wd == time.Sunday {
return 6, nil
}
return int(wd) - 1, nil
}
// GET /api/classes/:rkId/attendance?date=&period=
func GetClassAttendanceHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
date := c.Query("date", qldt.NowDateVN())
period, _ := strconv.Atoi(c.Query("period", "1"))
if period < 1 {
period = 1
}
dayOfWeek, err := scheduleDayFromDate(date)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid date"})
}
_ = recalcAttendanceForPeriod(db, classRkID, date, period, dayOfWeek)
var mappings []models.ClassStudent
db.Where("class_rk_id = ?", classRkID).Find(&mappings)
studentIDs := make([]int64, 0, len(mappings))
for _, m := range mappings {
studentIDs = append(studentIDs, m.StudentRkID)
}
if len(studentIDs) == 0 {
return c.JSON(fiber.Map{"data": []any{}, "period": period, "date": date})
}
var students []models.Student
db.Where("rk_id IN ?", studentIDs).Find(&students)
var results []models.AttendanceResult
db.Where("class_rk_id = ? AND session_date = ? AND period = ?", classRkID, date, period).Find(&results)
resMap := map[int64]models.AttendanceResult{}
for _, r := range results {
resMap[r.StudentRkID] = r
}
out := make([]attendanceRow, 0, len(students))
for _, st := range students {
r, ok := resMap[st.RkID]
row := attendanceRow{
StudentRkID: st.RkID,
StudentCode: st.StudentCode,
FullName: st.FullName,
Email: st.Email,
}
if ok {
row.Status = r.Status
row.StatusLabel = r.StatusLabel
row.OnlineMinutes = r.OnlineMinutes
row.StatusEditedByTeacher = r.StatusEditedByTeacher
}
out = append(out, row)
}
sched, _ := resolveScheduleForPeriod(db, classRkID, dayOfWeek, period)
shiftInfo := fiber.Map{}
if sched != nil {
shiftInfo = fiber.Map{
"startTime": sched.StartTime,
"endTime": sched.EndTime,
"courseId": sched.CourseID,
"courseName": sched.CourseName,
"isActive": sched.IsActive,
}
}
return c.JSON(fiber.Map{"data": out, "period": period, "date": date, "shift": shiftInfo})
}
}
// PUT /api/classes/:rkId/attendance/status
func UpdateAttendanceStatusHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
var req struct {
Date string `json:"date"`
Period int `json:"period"`
StudentRkID int64 `json:"studentRkId"`
Status int `json:"status"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
if req.Date == "" {
req.Date = qldt.NowDateVN()
}
if req.Period < 1 {
req.Period = 1
}
label := qldt.AttendanceStatusLabel(req.Status)
var row models.AttendanceResult
err := db.Where("class_rk_id = ? AND session_date = ? AND period = ? AND student_rk_id = ?",
classRkID, req.Date, req.Period, req.StudentRkID).First(&row).Error
if err == gorm.ErrRecordNotFound {
row = models.AttendanceResult{
ClassRkID: classRkID,
SessionDate: req.Date,
Period: req.Period,
StudentRkID: req.StudentRkID,
Status: req.Status,
StatusLabel: label,
StatusEditedByTeacher: true,
}
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.Status = req.Status
row.StatusLabel = label
row.StatusEditedByTeacher = true
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/classes/:rkId/attendance/push-qldt
func PushAttendanceToQLDTHandler(db *gorm.DB, qldtClient *qldt.Client, token string) fiber.Handler {
return func(c *fiber.Ctx) error {
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
var req struct {
Date string `json:"date"`
Period int `json:"period"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
if req.Date == "" {
req.Date = qldt.NowDateVN()
}
if req.Period < 1 {
req.Period = 1
}
if token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "QLDT_TOKEN chưa cấu hình"})
}
dayOfWeek, _ := scheduleDayFromDate(req.Date)
sched, err := resolveScheduleForPeriod(db, classRkID, dayOfWeek, req.Period)
if err != nil || sched == nil || sched.CourseID == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Ca học chưa cấu hình môn học (courseId)"})
}
userID, err := qldt.ParseUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Không đọc được users_id từ QLDT_TOKEN: " + err.Error()})
}
var results []models.AttendanceResult
db.Where("class_rk_id = ? AND session_date = ? AND period = ?", classRkID, req.Date, req.Period).Find(&results)
if len(results) == 0 {
_ = recalcAttendanceForPeriod(db, classRkID, req.Date, req.Period, dayOfWeek)
db.Where("class_rk_id = ? AND session_date = ? AND period = ?", classRkID, req.Date, req.Period).Find(&results)
}
students := make([]qldt.AttendanceStudent, 0, len(results))
for _, r := range results {
students = append(students, qldt.AttendanceStudent{
StudentID: r.StudentRkID,
Status: r.Status,
})
}
payload := &qldt.AttendancePayload{
UsersID: userID,
CoursesID: int(sched.CourseID),
Period: req.Period,
Students: students,
ClassID: classRkID,
Date: req.Date,
Type: "OFFLINE",
}
body, code, err := qldtClient.SaveAttendance(context.Background(), token, payload)
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error(), "statusCode": code, "body": string(body)})
}
now := time.Now()
db.Model(&models.AttendanceResult{}).
Where("class_rk_id = ? AND session_date = ? AND period = ?", classRkID, req.Date, req.Period).
Update("pushed_to_qldt_at", now)
return c.JSON(fiber.Map{"ok": true, "message": "Đã đẩy điểm danh lên QLĐT", "studentCount": len(students)})
}
}
// GET /api/classes/:rkId/attendance/shifts?date=
func ListAttendanceShiftsHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
date := c.Query("date", qldt.NowDateVN())
dayOfWeek, err := scheduleDayFromDate(date)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid date"})
}
var slots []models.ClassSchedule
db.Where("class_rk_id = ? AND day_of_week = ?", classRkID, dayOfWeek).
Order("period, start_time").Find(&slots)
if len(slots) == 0 {
for p := 1; p <= 4; p++ {
slots = append(slots, models.ClassSchedule{
Period: p, DayOfWeek: dayOfWeek, IsActive: false,
})
}
}
out := make([]fiber.Map, 0, len(slots))
for _, s := range slots {
period := s.Period
if period == 0 {
period = 1
}
out = append(out, fiber.Map{
"period": period,
"startTime": s.StartTime,
"endTime": s.EndTime,
"courseId": s.CourseID,
"courseName": s.CourseName,
"isActive": s.IsActive,
})
}
return c.JSON(fiber.Map{"data": out, "date": date, "dayOfWeek": dayOfWeek})
}
}

View File

@@ -0,0 +1,629 @@
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"})
}
}
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)
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,
})
}
return c.JSON(appConfig)
}
}
// 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})
}
}

View File

@@ -0,0 +1,235 @@
package handlers
import (
"strconv"
"strings"
"time"
"server/internal/models"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
func normalizeSSID(ssid string) string {
return strings.TrimSpace(ssid)
}
func ssidKey(ssid string) string {
return strings.ToLower(strings.TrimSpace(ssid))
}
func normalizeBSSID(bssid string) string {
var hex []rune
for _, c := range strings.ToLower(bssid) {
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
hex = append(hex, c)
}
}
if len(hex) != 12 {
return strings.TrimSpace(bssid)
}
s := string(hex)
return s[0:2] + ":" + s[2:4] + ":" + s[4:6] + ":" + s[6:8] + ":" + s[8:10] + ":" + s[10:12]
}
func bssidKey(bssid string) string {
var hex []rune
for _, c := range strings.ToLower(bssid) {
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
hex = append(hex, c)
}
}
return string(hex)
}
type wifiItemPayload struct {
SSID string `json:"ssid"`
BSSID string `json:"bssid"`
}
// POST /api/student/report-wifi — ghi nhận SSID + BSSID vào kho
func ReportWifiHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var req struct {
StudentRkID int64 `json:"studentRkId"`
SSID string `json:"ssid"`
BSSID string `json:"bssid"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
ssid := normalizeSSID(req.SSID)
bssid := normalizeBSSID(req.BSSID)
sKey := ssidKey(ssid)
bKey := bssidKey(bssid)
if sKey == "" || bKey == "" || len(bKey) != 12 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ssid and valid bssid are required"})
}
now := time.Now()
var row models.WifiPoolEntry
err := db.Where("bssid_key = ?", bKey).First(&row).Error
if err == gorm.ErrRecordNotFound {
row = models.WifiPoolEntry{
SSID: ssid,
SSIDKey: sKey,
BSSID: bssid,
BSSIDKey: bKey,
HitCount: 1,
LastSeenAt: now,
LastStudentRkID: req.StudentRkID,
}
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.SSID = ssid
row.SSIDKey = sKey
row.BSSID = bssid
if req.StudentRkID > 0 {
row.LastStudentRkID = req.StudentRkID
}
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/student/wifi-policy — danh sách điểm phát được phép (theo BSSID)
func GetStudentWifiPolicyHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var rows []models.AcceptedWifi
if err := db.Where("bssid_key <> '' AND LENGTH(bssid_key) = 12").Order("ssid asc").Find(&rows).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
items := make([]wifiItemPayload, 0, len(rows))
for _, r := range rows {
items = append(items, wifiItemPayload{SSID: r.SSID, BSSID: r.BSSID})
}
return c.JSON(fiber.Map{
"enforce": len(items) > 0,
"accepted": items,
"acceptedSsids": nil, // legacy — client mới dùng accepted
})
}
}
// GET /api/wifi-pool?q=&limit=
func ListWifiPoolHandler(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.WifiPoolEntry{}).Where("bssid_key <> '' AND LENGTH(bssid_key) = 12")
if q != "" {
like := "%" + q + "%"
query = query.Where("ssid LIKE ? OR ssid_key LIKE ? OR bssid LIKE ? OR bssid_key LIKE ?", like, like, like, like)
}
var rows []models.WifiPoolEntry
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})
}
}
// GET /api/network/accepted-wifis
func ListAcceptedWifisHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var rows []models.AcceptedWifi
if err := db.Order("ssid asc, bssid asc").Find(&rows).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"data": rows})
}
}
// POST /api/network/accepted-wifis — thay toàn bộ danh sách (cần SSID + BSSID)
func SaveAcceptedWifisHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var req struct {
Items []wifiItemPayload `json:"items"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
if err := db.Where("1 = 1").Delete(&models.AcceptedWifi{}).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
now := time.Now()
count := 0
seen := map[string]bool{}
for _, item := range req.Items {
ssid := normalizeSSID(item.SSID)
bssid := normalizeBSSID(item.BSSID)
sKey := ssidKey(ssid)
bKey := bssidKey(bssid)
if sKey == "" || bKey == "" || len(bKey) != 12 || seen[bKey] {
continue
}
seen[bKey] = true
if err := db.Create(&models.AcceptedWifi{
SSID: ssid,
SSIDKey: sKey,
BSSID: bssid,
BSSIDKey: bKey,
CreatedAt: now,
}).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
count++
}
return c.JSON(fiber.Map{"ok": true, "count": count})
}
}
// POST /api/network/accepted-wifis/add
func AddAcceptedWifiHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var req wifiItemPayload
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
ssid := normalizeSSID(req.SSID)
bssid := normalizeBSSID(req.BSSID)
sKey := ssidKey(ssid)
bKey := bssidKey(bssid)
if sKey == "" || bKey == "" || len(bKey) != 12 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ssid and valid bssid are required"})
}
row := models.AcceptedWifi{
SSID: ssid,
SSIDKey: sKey,
BSSID: bssid,
BSSIDKey: bKey,
CreatedAt: time.Now(),
}
if err := db.Where("bssid_key = ?", bKey).FirstOrCreate(&row).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "data": row})
}
}
// DELETE /api/network/accepted-wifis/:id
func DeleteAcceptedWifiHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := db.Delete(&models.AcceptedWifi{}, id).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
}

View File

@@ -0,0 +1,212 @@
package handlers
import (
"fmt"
"strconv"
"strings"
"time"
internalDb "server/internal/db"
"server/internal/models"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type studentShiftItem struct {
Period int `json:"period"`
CourseName string `json:"courseName"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
IsActiveNow bool `json:"isActiveNow"`
OnlineSeconds int `json:"onlineSeconds"`
OfflineSeconds int `json:"offlineSeconds"`
AttendanceStatus int `json:"attendanceStatus"`
AttendanceLabel string `json:"attendanceLabel"`
}
// GET /api/student/status — trạng thái giám sát, lớp/ca hiện tại, tích lũy & điểm danh hôm nay
func GetStudentStatusHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
studentID, err := strconv.ParseInt(c.Query("studentRkId", "0"), 10, 64)
if err != nil || studentID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId is required"})
}
now := time.Now()
today := now.Format("2006-01-02")
dayIdx := weekdayIndex(now)
currMin := now.Hour()*60 + now.Minute()
classID, activePeriod := internalDb.FindActiveClassAndPeriodForStudent(db, studentID)
if classID <= 0 {
var link models.ClassStudent
if err := db.Where("student_rk_id = ?", studentID).First(&link).Error; err == nil {
classID = link.ClassRkID
}
}
className, classCode := "", ""
if classID > 0 {
var cl models.Class
if err := db.Where("rk_id = ?", classID).First(&cl).Error; err == nil {
className = cl.Name
classCode = cl.ClassCode
}
}
var todaySchedules []models.ClassSchedule
if classID > 0 {
_ = db.Where("class_rk_id = ? AND day_of_week = ? AND is_active = ?", classID, dayIdx, true).
Order("period asc, start_time asc").Find(&todaySchedules).Error
}
inScheduleNow := false
currentPeriod := 0
currentCourse, currentStart, currentEnd := "", "", ""
for _, s := range todaySchedules {
startMin := parseTimeMinutes(s.StartTime)
endMin := parseTimeMinutes(s.EndTime)
if currMin >= startMin && currMin <= endMin {
inScheduleNow = true
currentPeriod = s.Period
if currentPeriod < 1 {
currentPeriod = 1
}
currentCourse = s.CourseName
currentStart = s.StartTime
currentEnd = s.EndTime
break
}
}
if !inScheduleNow && activePeriod > 0 {
currentPeriod = activePeriod
}
hasApps := false
keywords := ""
if classID > 0 {
var appConfig models.ClassAllowedApp
if err := db.Where("class_rk_id = ?", classID).First(&appConfig).Error; err == nil {
keywords = strings.TrimSpace(appConfig.Keywords)
hasApps = keywords != ""
}
}
monitorMode := "outside_schedule"
monitorLabel := "Ngoài giờ học"
if classID <= 0 {
monitorMode = "not_configured"
monitorLabel = "Chưa xác định được lớp học"
} else if !hasApps {
monitorMode = "not_configured"
monitorLabel = "Lớp chưa cấu hình ứng dụng được phép"
} else if inScheduleNow {
monitorMode = "learning"
if currentCourse != "" {
monitorLabel = fmt.Sprintf("Ca %d — %s (%s%s)", currentPeriod, currentCourse, currentStart, currentEnd)
} else {
monitorLabel = fmt.Sprintf("Đang giám sát — Ca %d", currentPeriod)
}
}
// monitorMode = "exam" — dành cho phòng thi (sẽ bổ sung sau)
sessionMap := map[int]models.StudentSession{}
attMap := map[int]models.AttendanceResult{}
if classID > 0 {
var sessions []models.StudentSession
_ = db.Where("student_rk_id = ? AND class_rk_id = ? AND session_date = ?", studentID, classID, today).Find(&sessions).Error
for _, s := range sessions {
p := s.Period
if p < 1 {
p = 1
}
sessionMap[p] = s
}
var attendances []models.AttendanceResult
_ = db.Where("student_rk_id = ? AND class_rk_id = ? AND session_date = ?", studentID, classID, today).Find(&attendances).Error
for _, a := range attendances {
p := a.Period
if p < 1 {
p = 1
}
attMap[p] = a
}
}
shifts := make([]studentShiftItem, 0, len(todaySchedules))
for _, sch := range todaySchedules {
period := sch.Period
if period < 1 {
period = 1
}
sess := sessionMap[period]
att := attMap[period]
startMin := parseTimeMinutes(sch.StartTime)
endMin := parseTimeMinutes(sch.EndTime)
isActiveNow := currMin >= startMin && currMin <= endMin
attStatus := -1
attLabel := "Chưa tính"
if att.ID > 0 {
attStatus = att.Status
attLabel = att.StatusLabel
if attLabel == "" {
attLabel = attendanceLabelFallback(att.Status)
}
}
shifts = append(shifts, studentShiftItem{
Period: period,
CourseName: sch.CourseName,
StartTime: sch.StartTime,
EndTime: sch.EndTime,
IsActiveNow: isActiveNow,
OnlineSeconds: sess.OnlineSeconds,
OfflineSeconds: sess.OfflineSeconds,
AttendanceStatus: attStatus,
AttendanceLabel: attLabel,
})
}
return c.JSON(fiber.Map{
"sessionDate": today,
"monitorMode": monitorMode,
"monitorLabel": monitorLabel,
"classRkId": classID,
"className": className,
"classCode": classCode,
"currentPeriod": currentPeriod,
"currentCourseName": currentCourse,
"currentShiftStart": currentStart,
"currentShiftEnd": currentEnd,
"inScheduleNow": inScheduleNow,
"blockerActive": hasApps && inScheduleNow,
"allowedKeywords": keywords,
"shifts": shifts,
})
}
}
func weekdayIndex(t time.Time) int {
wd := t.Weekday()
if wd == time.Sunday {
return 6
}
return int(wd) - 1
}
func attendanceLabelFallback(status int) string {
switch status {
case 1:
return "Nghỉ có phép"
case 2:
return "Nghỉ nửa buổi"
case 3:
return "Đi học muộn"
case 4:
return "Đi học đầy đủ"
default:
return "Nghỉ không phép"
}
}