This commit is contained in:
@@ -53,6 +53,7 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.WifiPoolEntry{},
|
||||
&models.AcceptedWifi{},
|
||||
&models.StudentSession{},
|
||||
&models.StudentViolation{},
|
||||
&models.AttendanceResult{},
|
||||
&models.StaffAccount{},
|
||||
&models.EmailDomain{},
|
||||
|
||||
@@ -735,6 +735,227 @@ func ReportBlockedAppHandler(db *gorm.DB) fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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"})
|
||||
}
|
||||
|
||||
kind := strings.ToLower(strings.TrimSpace(req.Kind))
|
||||
reason := strings.TrimSpace(req.Reason)
|
||||
if reason == "" {
|
||||
reason = kind
|
||||
}
|
||||
classID := internalDb.FindActiveClassForStudent(db, req.StudentRkID)
|
||||
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,
|
||||
Kind: kind,
|
||||
Reason: reason,
|
||||
MonitorMode: strings.TrimSpace(req.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,
|
||||
"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})
|
||||
}
|
||||
}
|
||||
|
||||
type studentViolationItem struct {
|
||||
ID uint `json:"id"`
|
||||
StudentRkID int64 `json:"studentRkId"`
|
||||
StudentCode string `json:"studentCode"`
|
||||
FullName string `json:"fullName"`
|
||||
ClassRkID int64 `json:"classRkId"`
|
||||
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{}})
|
||||
}
|
||||
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)
|
||||
|
||||
q := db.Where("student_rk_id IN ? AND created_at >= ? AND created_at < ?", studentIDs, dayStart, dayEnd)
|
||||
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,
|
||||
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{}})
|
||||
}
|
||||
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)
|
||||
|
||||
q := db.Where("student_rk_id IN ? AND created_at >= ? AND created_at < ?", studentIDs, dayStart, dayEnd)
|
||||
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,
|
||||
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 {
|
||||
|
||||
@@ -179,6 +179,20 @@ type StudentSession struct {
|
||||
|
||||
func (StudentSession) TableName() string { return "student_sessions" }
|
||||
|
||||
// StudentViolation — ghi nhận SV tắt app / vi phạm môi trường khi đang giám sát
|
||||
type StudentViolation struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
StudentRkID int64 `gorm:"column:student_rk_id;not null;index" json:"studentRkId"`
|
||||
ClassRkID int64 `gorm:"column:class_rk_id;index" json:"classRkId"`
|
||||
Kind string `gorm:"column:kind;size:64;not null;index" json:"kind"` // app_closed | unclean_shutdown | multi_monitor | user_switch | session_change | virtual_desktop | wifi | guard
|
||||
Reason string `gorm:"column:reason;type:text" json:"reason"`
|
||||
MonitorMode string `gorm:"column:monitor_mode;size:32" json:"monitorMode"`
|
||||
ClientAt time.Time `gorm:"column:client_at" json:"clientAt"`
|
||||
}
|
||||
|
||||
func (StudentViolation) TableName() string { return "student_violations" }
|
||||
|
||||
// SystemSetting lưu các cấu hình hệ thống động
|
||||
type SystemSetting struct {
|
||||
SettingKey string `gorm:"primaryKey;column:setting_key;size:128" json:"settingKey"`
|
||||
|
||||
@@ -13,8 +13,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
wsPingInterval = 30 * time.Second
|
||||
wsPongWait = 60 * time.Second
|
||||
// Heartbeat kiểu game: phát hiện mất kết nối trong ~30–45s.
|
||||
wsPingInterval = 15 * time.Second
|
||||
wsPongWait = 45 * time.Second
|
||||
// Grace khi SV reconnect — tránh nhấp nháy online/offline.
|
||||
studentOfflineGrace = 8 * time.Second
|
||||
)
|
||||
|
||||
type SocketMsg struct {
|
||||
@@ -44,6 +47,10 @@ func (c *SocketClient) WriteRaw(msg []byte) error {
|
||||
return c.Conn.WriteMessage(websocket.TextMessage, msg)
|
||||
}
|
||||
|
||||
type offlineGrace struct {
|
||||
until time.Time
|
||||
classID int64
|
||||
}
|
||||
|
||||
type WsHub struct {
|
||||
mu sync.RWMutex
|
||||
@@ -51,6 +58,8 @@ type WsHub struct {
|
||||
teachers map[string]*SocketClient
|
||||
teachersByStaff map[uint][]string // staffId -> teacher connection addresses
|
||||
subscribers map[int64][]string // studentId -> list of teacher connection addresses
|
||||
grace map[int64]offlineGrace
|
||||
graceTimers map[int64]*time.Timer
|
||||
}
|
||||
|
||||
var Hub = &WsHub{
|
||||
@@ -58,13 +67,18 @@ var Hub = &WsHub{
|
||||
teachers: make(map[string]*SocketClient),
|
||||
teachersByStaff: make(map[uint][]string),
|
||||
subscribers: make(map[int64][]string),
|
||||
grace: make(map[int64]offlineGrace),
|
||||
graceTimers: make(map[int64]*time.Timer),
|
||||
}
|
||||
|
||||
func (h *WsHub) IsStudentOnline(studentRkID int64) bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
_, ok := h.students[studentRkID]
|
||||
return ok
|
||||
if _, ok := h.students[studentRkID]; ok {
|
||||
return true
|
||||
}
|
||||
g, ok := h.grace[studentRkID]
|
||||
return ok && time.Now().Before(g.until)
|
||||
}
|
||||
|
||||
func (h *WsHub) PushExamToStudent(studentRkID int64, data map[string]any) {
|
||||
@@ -102,16 +116,126 @@ func (h *WsHub) PushChatToStaff(staffID uint, data map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WsHub) BroadcastStudentViolation(data map[string]any) {
|
||||
msg := SocketMsg{Event: "teacher:student-violation", Data: data}
|
||||
msgBytes, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for _, t := range h.teachers {
|
||||
if t != nil {
|
||||
_ = t.WriteRaw(msgBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WsHub) broadcastPresence(studentID int64, online bool, classID int64) {
|
||||
msg := SocketMsg{
|
||||
Event: "presence:update",
|
||||
Data: map[string]any{
|
||||
"studentId": studentID,
|
||||
"online": online,
|
||||
"classId": classID,
|
||||
},
|
||||
}
|
||||
msgBytes, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range h.teachers {
|
||||
if t != nil {
|
||||
_ = t.WriteRaw(msgBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ForceStudentOffline — tắt ngay (không chờ grace), dùng khi SV cố ý đóng app / vi phạm.
|
||||
func (h *WsHub) ForceStudentOffline(studentID int64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
classID := int64(0)
|
||||
if client, ok := h.students[studentID]; ok && client != nil {
|
||||
classID = client.ClassID
|
||||
delete(h.students, studentID)
|
||||
}
|
||||
if g, ok := h.grace[studentID]; ok {
|
||||
if classID == 0 {
|
||||
classID = g.classID
|
||||
}
|
||||
}
|
||||
h.cancelGraceLocked(studentID)
|
||||
|
||||
log.Printf("[WS] Student %d force offline (intentional quit/violation)", studentID)
|
||||
h.broadcastPresence(studentID, false, classID)
|
||||
if teachers, exists := h.subscribers[studentID]; exists {
|
||||
for _, tAddr := range teachers {
|
||||
if t, found := h.teachers[tAddr]; found {
|
||||
_ = t.WriteJSON(SocketMsg{
|
||||
Event: "teacher:stream-stopped",
|
||||
Data: map[string]any{"studentId": studentID},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WsHub) cancelGraceLocked(studentID int64) {
|
||||
if t, ok := h.graceTimers[studentID]; ok {
|
||||
t.Stop()
|
||||
delete(h.graceTimers, studentID)
|
||||
}
|
||||
delete(h.grace, studentID)
|
||||
}
|
||||
|
||||
func (h *WsHub) finalizeStudentOffline(studentID int64, classID int64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if _, online := h.students[studentID]; online {
|
||||
return
|
||||
}
|
||||
if _, ok := h.grace[studentID]; !ok {
|
||||
return
|
||||
}
|
||||
delete(h.grace, studentID)
|
||||
delete(h.graceTimers, studentID)
|
||||
|
||||
log.Printf("[WS] Student %d offline after grace", studentID)
|
||||
h.broadcastPresence(studentID, false, classID)
|
||||
|
||||
if teachers, exists := h.subscribers[studentID]; exists {
|
||||
for _, tAddr := range teachers {
|
||||
if t, found := h.teachers[tAddr]; found {
|
||||
_ = t.WriteJSON(SocketMsg{
|
||||
Event: "teacher:stream-stopped",
|
||||
Data: map[string]any{"studentId": studentID},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WsHub) GetOnlineStudentIDs(classID int64) []int64 {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
seen := map[int64]bool{}
|
||||
var ids []int64
|
||||
now := time.Now()
|
||||
for _, client := range h.students {
|
||||
if client.ClassID == classID {
|
||||
seen[client.StudentID] = true
|
||||
ids = append(ids, client.StudentID)
|
||||
}
|
||||
}
|
||||
for sid, g := range h.grace {
|
||||
if now.Before(g.until) && g.classID == classID && !seen[sid] {
|
||||
ids = append(ids, sid)
|
||||
}
|
||||
}
|
||||
if ids == nil {
|
||||
return []int64{}
|
||||
}
|
||||
@@ -124,8 +248,14 @@ func (h *WsHub) Register(c *SocketClient) {
|
||||
|
||||
c.Addr = c.Conn.RemoteAddr().String()
|
||||
if c.Role == "student" {
|
||||
wasInGrace := false
|
||||
if _, ok := h.grace[c.StudentID]; ok {
|
||||
wasInGrace = true
|
||||
}
|
||||
h.cancelGraceLocked(c.StudentID)
|
||||
h.students[c.StudentID] = c
|
||||
log.Printf("[WS] Student %d registered (Address: %s, Class: %d)", c.StudentID, c.Addr, c.ClassID)
|
||||
log.Printf("[WS] Student %d registered (Address: %s, Class: %d, reconnectGrace=%v)", c.StudentID, c.Addr, c.ClassID, wasInGrace)
|
||||
h.broadcastPresence(c.StudentID, true, c.ClassID)
|
||||
if subs, exists := h.subscribers[c.StudentID]; exists && len(subs) > 0 {
|
||||
_ = c.WriteJSON(SocketMsg{Event: "start_screenshot_stream"})
|
||||
_ = c.WriteJSON(SocketMsg{Event: "start_webcam_stream"})
|
||||
@@ -151,19 +281,17 @@ func (h *WsHub) Unregister(c *SocketClient) {
|
||||
return
|
||||
}
|
||||
delete(h.students, c.StudentID)
|
||||
log.Printf("[WS] Student %d disconnected", c.StudentID)
|
||||
classID := c.ClassID
|
||||
studentID := c.StudentID
|
||||
|
||||
// Báo cho các giáo viên đang xem là stream của học sinh đã dừng
|
||||
if teachers, exists := h.subscribers[c.StudentID]; exists {
|
||||
for _, tAddr := range teachers {
|
||||
if t, found := h.teachers[tAddr]; found {
|
||||
_ = t.WriteJSON(SocketMsg{
|
||||
Event: "teacher:stream-stopped",
|
||||
Data: map[string]any{"studentId": c.StudentID},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
h.cancelGraceLocked(studentID)
|
||||
deadline := time.Now().Add(studentOfflineGrace)
|
||||
h.grace[studentID] = offlineGrace{until: deadline, classID: classID}
|
||||
h.graceTimers[studentID] = time.AfterFunc(studentOfflineGrace, func() {
|
||||
h.finalizeStudentOffline(studentID, classID)
|
||||
})
|
||||
log.Printf("[WS] Student %d disconnected — grace %v before offline", studentID, studentOfflineGrace)
|
||||
// Chưa broadcast offline / stream-stopped — chờ grace (reconnect nhanh như game).
|
||||
} else if c.Role == "teacher" {
|
||||
delete(h.teachers, c.Addr)
|
||||
if c.StaffID > 0 {
|
||||
@@ -182,7 +310,6 @@ func (h *WsHub) Unregister(c *SocketClient) {
|
||||
}
|
||||
log.Printf("[WS] Teacher %s disconnected", c.Addr)
|
||||
|
||||
// Dọn dẹp subscriptions của giáo viên này
|
||||
for sID, teachersList := range h.subscribers {
|
||||
newList := []string{}
|
||||
for _, addr := range teachersList {
|
||||
@@ -192,7 +319,6 @@ func (h *WsHub) Unregister(c *SocketClient) {
|
||||
}
|
||||
if len(newList) == 0 {
|
||||
delete(h.subscribers, sID)
|
||||
// Nếu không còn ai xem học sinh này, gửi lệnh tắt camera/screen cho client học sinh
|
||||
if student, exists := h.students[sID]; exists {
|
||||
_ = student.WriteJSON(SocketMsg{Event: "stop_screenshot_stream"})
|
||||
_ = student.WriteJSON(SocketMsg{Event: "stop_webcam_stream"})
|
||||
@@ -204,12 +330,10 @@ func (h *WsHub) Unregister(c *SocketClient) {
|
||||
}
|
||||
}
|
||||
|
||||
// Teacher bắt đầu xem stream của Student
|
||||
func (h *WsHub) Subscribe(teacherAddr string, studentID int64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Thêm giáo viên vào list người xem của học sinh
|
||||
teachersList := h.subscribers[studentID]
|
||||
alreadySubscribed := false
|
||||
for _, addr := range teachersList {
|
||||
@@ -223,14 +347,12 @@ func (h *WsHub) Subscribe(teacherAddr string, studentID int64) {
|
||||
log.Printf("[WS] Teacher %s subscribed to student %d stream", teacherAddr, studentID)
|
||||
}
|
||||
|
||||
// Phát lệnh cho máy học sinh bật stream (nếu học sinh đang online)
|
||||
if student, exists := h.students[studentID]; exists {
|
||||
_ = student.WriteJSON(SocketMsg{Event: "start_screenshot_stream"})
|
||||
_ = student.WriteJSON(SocketMsg{Event: "start_webcam_stream"})
|
||||
}
|
||||
}
|
||||
|
||||
// Teacher dừng xem stream của Student
|
||||
func (h *WsHub) Unsubscribe(teacherAddr string, studentID int64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
@@ -250,8 +372,6 @@ func (h *WsHub) Unsubscribe(teacherAddr string, studentID int64) {
|
||||
if len(newList) == 0 {
|
||||
delete(h.subscribers, studentID)
|
||||
log.Printf("[WS] Student %d has no more proctor subscribers. Stopping streams.", studentID)
|
||||
|
||||
// Báo học sinh tắt camera & screen stream để tiết kiệm mạng và CPU
|
||||
if student, exists := h.students[studentID]; exists {
|
||||
_ = student.WriteJSON(SocketMsg{Event: "stop_screenshot_stream"})
|
||||
_ = student.WriteJSON(SocketMsg{Event: "stop_webcam_stream"})
|
||||
@@ -261,7 +381,6 @@ func (h *WsHub) Unsubscribe(teacherAddr string, studentID int64) {
|
||||
}
|
||||
}
|
||||
|
||||
// Chuyển tiếp frame ảnh từ Student đến các Teacher đã subscribe
|
||||
func (h *WsHub) RelayFrame(studentID int64, event string, data map[string]any) {
|
||||
h.mu.RLock()
|
||||
teachersList, exists := h.subscribers[studentID]
|
||||
@@ -269,7 +388,6 @@ func (h *WsHub) RelayFrame(studentID int64, event string, data map[string]any) {
|
||||
h.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
// Snapshot subscriber addresses while holding read lock
|
||||
addrs := make([]string, len(teachersList))
|
||||
copy(addrs, teachersList)
|
||||
h.mu.RUnlock()
|
||||
@@ -291,18 +409,30 @@ func (h *WsHub) RelayFrame(studentID int64, event string, data map[string]any) {
|
||||
return
|
||||
}
|
||||
|
||||
var dead []string
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for _, addr := range addrs {
|
||||
if t, found := h.teachers[addr]; found {
|
||||
if err := t.WriteRaw(msgBytes); err != nil {
|
||||
log.Printf("[WS] Relay to teacher %s failed: %v", addr, err)
|
||||
dead = append(dead, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
for _, addr := range dead {
|
||||
if t, ok := func() (*SocketClient, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
t, ok := h.teachers[addr]
|
||||
return t, ok
|
||||
}(); ok && t != nil {
|
||||
_ = t.Conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket handler cho Fiber route
|
||||
func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
|
||||
return func(c *websocket.Conn) {
|
||||
role := c.Query("role", "student")
|
||||
@@ -351,7 +481,7 @@ func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
|
||||
return
|
||||
case <-ticker.C:
|
||||
client.writeMu.Lock()
|
||||
err := c.WriteMessage(websocket.PingMessage, nil)
|
||||
err := c.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(5*time.Second))
|
||||
client.writeMu.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -372,14 +502,14 @@ func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Xử lý các sự kiện
|
||||
switch msg.Event {
|
||||
case "client:ping":
|
||||
_ = client.WriteJSON(SocketMsg{Event: "client:pong", Data: map[string]any{"t": time.Now().UnixMilli()}})
|
||||
|
||||
case "screenshot_stream_frame", "webcam_stream_frame":
|
||||
// Nhận frame từ học sinh, chuyển tiếp về các thầy cô
|
||||
Hub.RelayFrame(client.StudentID, msg.Event, msg.Data)
|
||||
|
||||
case "teacher:subscribe":
|
||||
// Giáo viên đăng ký xem học sinh cụ thể
|
||||
if client.Role == "teacher" {
|
||||
if sIDVal, ok := msg.Data["studentId"]; ok {
|
||||
var sID int64
|
||||
@@ -396,7 +526,6 @@ func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
|
||||
}
|
||||
|
||||
case "teacher:unsubscribe":
|
||||
// Giáo viên hủy đăng ký
|
||||
if client.Role == "teacher" {
|
||||
if sIDVal, ok := msg.Data["studentId"]; ok {
|
||||
var sID int64
|
||||
|
||||
@@ -93,6 +93,7 @@ func main() {
|
||||
api.Get("/student/wifi-policy", handlers.GetStudentWifiPolicyHandler(gormDB))
|
||||
api.Post("/student/report-wifi", handlers.ReportWifiHandler(gormDB))
|
||||
api.Post("/student/report-blocked-app", handlers.ReportBlockedAppHandler(gormDB))
|
||||
api.Post("/student/report-violation", handlers.ReportViolationHandler(gormDB))
|
||||
api.Get("/student/chat/messages", handlers.StudentListChatHandler(gormDB))
|
||||
api.Post("/student/chat/messages", handlers.StudentSendChatHandler(gormDB))
|
||||
api.Get("/student/chat/conversations", handlers.StudentListChatConversationsHandler(gormDB))
|
||||
@@ -156,6 +157,7 @@ func main() {
|
||||
staff.Post("/network/accepted-wifis/add", handlers.AddAcceptedWifiHandler(gormDB))
|
||||
staff.Delete("/network/accepted-wifis/:id", handlers.DeleteAcceptedWifiHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/session-logs", handlers.ListClassSessionLogsHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/violations", handlers.ListClassViolationsHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/online-students", handlers.GetOnlineStudentsHandler(gormDB))
|
||||
staff.Get("/classes/:rkId/schedule-conflicts", handlers.GetClassScheduleConflictsHandler(gormDB))
|
||||
|
||||
@@ -187,6 +189,7 @@ func main() {
|
||||
staff.Get("/exam-rooms/search-students", handlers.SearchExamStudentsHandler(gormDB))
|
||||
staff.Get("/exam-rooms/:id", handlers.GetExamRoomHandler(gormDB))
|
||||
staff.Get("/exam-rooms/:id/online-students", handlers.GetExamRoomOnlineStudentsHandler(gormDB))
|
||||
staff.Get("/exam-rooms/:id/violations", handlers.ListExamRoomViolationsHandler(gormDB))
|
||||
staff.Patch("/exam-rooms/:id", handlers.UpdateExamRoomHandler(gormDB))
|
||||
staff.Delete("/exam-rooms/:id", handlers.DeleteExamRoomHandler(gormDB))
|
||||
staff.Post("/exam-rooms/:id/students", handlers.AddExamRoomStudentsHandler(gormDB))
|
||||
|
||||
Reference in New Issue
Block a user