This commit is contained in:
2026-07-13 09:44:34 +07:00
parent 8803cd622a
commit 58c96609b6
5 changed files with 112 additions and 117 deletions

View File

@@ -296,7 +296,7 @@ func (a *App) HandleBeforeClose() (prevent bool) {
a.mu.Lock() a.mu.Lock()
mode := a.dashboard.MonitorMode mode := a.dashboard.MonitorMode
a.mu.Unlock() a.mu.Unlock()
if mode == "" || mode == "not_configured" { if !shouldRecordViolation(mode) {
a.clearRunLock() a.clearRunLock()
return false return false
} }
@@ -359,6 +359,16 @@ type pendingViolation struct {
StudentRkID int64 `json:"studentRkId"` StudentRkID int64 `json:"studentRkId"`
} }
// shouldRecordViolation — chỉ lưu vi phạm khi đang thi hoặc đang học (trong giờ).
func shouldRecordViolation(monitorMode string) bool {
switch strings.TrimSpace(monitorMode) {
case "exam", "learning":
return true
default:
return false
}
}
func (a *App) markRunLock() { func (a *App) markRunLock() {
a.mu.Lock() a.mu.Lock()
studentID := int64(0) studentID := int64(0)
@@ -367,7 +377,7 @@ func (a *App) markRunLock() {
studentID = a.student.StudentID studentID = a.student.StudentID
} }
a.mu.Unlock() a.mu.Unlock()
if studentID <= 0 || mode == "" || mode == "not_configured" { if studentID <= 0 || !shouldRecordViolation(mode) {
return return
} }
payload, _ := json.Marshal(map[string]any{ payload, _ := json.Marshal(map[string]any{
@@ -402,7 +412,7 @@ func (a *App) detectUncleanShutdown() {
} }
a.mu.Unlock() a.mu.Unlock()
} }
if meta.StudentRkID <= 0 { if meta.StudentRkID <= 0 || !shouldRecordViolation(meta.MonitorMode) {
return return
} }
reason := "Ứng dụng bị tắt đột ngột (Task Manager / kill process) khi đang giám sát" reason := "Ứng dụng bị tắt đột ngột (Task Manager / kill process) khi đang giám sát"
@@ -457,8 +467,8 @@ func (a *App) flushPendingViolations() {
} }
func (a *App) postViolation(v pendingViolation) bool { func (a *App) postViolation(v pendingViolation) bool {
if v.StudentRkID <= 0 { if v.StudentRkID <= 0 || !shouldRecordViolation(v.MonitorMode) {
return true return true // bỏ qua — không thi / không học thì không lưu
} }
payload := map[string]any{ payload := map[string]any{
"studentRkId": v.StudentRkID, "studentRkId": v.StudentRkID,
@@ -492,7 +502,7 @@ func (a *App) reportViolation(kind, reason string) {
studentID = a.student.StudentID studentID = a.student.StudentID
} }
a.mu.Unlock() a.mu.Unlock()
if studentID <= 0 { if studentID <= 0 || !shouldRecordViolation(mode) {
return return
} }
v := pendingViolation{ v := pendingViolation{
@@ -590,7 +600,7 @@ func (a *App) startLocalServer() {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
return return
} }
// Chỉ purge disk — JS trên trang exam-view tự clear storage + reload iframe. // Chỉ xóa HTTP disk cache — không đụng cookie / localStorage (giữ phiên đăng nhập trang thi).
go a.purgeWebViewDiskCache() go a.purgeWebViewDiskCache()
w.Write([]byte("ok")) w.Write([]byte("ok"))
}) })
@@ -609,18 +619,18 @@ const examViewHTML = `<!DOCTYPE html>
<title>%s</title> <title>%s</title>
<style> <style>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
body{font-family:Segoe UI,system-ui,sans-serif;background:#1e293b;color:#f8fafc;height:100vh;display:flex;flex-direction:column} body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#0f172a;border-bottom:1px solid #334155;flex-shrink:0} .bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-shrink:0}
.bar-title{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:auto;min-width:0} .bar-title{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:auto;min-width:0;color:#1a2332}
.bar button{border:none;border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap} .bar button{border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap}
.btn-back{background:#7c3aed;color:#fff} .btn-back{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-back:hover{background:#6d28d9} .btn-back:hover{background:#fde8e9}
.btn-refresh{background:#0ea5e9;color:#fff} .btn-refresh{background:#bb2126;color:#fff;border:1px solid #bb2126}
.btn-refresh:hover{background:#0284c7} .btn-refresh:hover{background:#9e1c22;border-color:#9e1c22}
.btn-cache{background:#f59e0b;color:#111} .btn-cache{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-cache:hover{background:#d97706} .btn-cache:hover{background:#fde8e9}
.btn-cache:disabled,.btn-refresh:disabled{opacity:.65;cursor:wait} .btn-cache:disabled,.btn-refresh:disabled{opacity:.65;cursor:wait}
.hint{font-size:11px;color:#94a3b8;flex-shrink:0} .hint{font-size:11px;color:#8b99a8;flex-shrink:0}
.frame-wrap{flex:1;min-height:0;background:#fff} .frame-wrap{flex:1;min-height:0;background:#fff}
iframe{width:100%%;height:100%%;border:0;display:block} iframe{width:100%%;height:100%%;border:0;display:block}
</style> </style>
@@ -629,7 +639,7 @@ iframe{width:100%%;height:100%%;border:0;display:block}
<div class="bar"> <div class="bar">
<button type="button" class="btn-back" onclick="goBack()">← Quay lại</button> <button type="button" class="btn-back" onclick="goBack()">← Quay lại</button>
<span class="bar-title">%s</span> <span class="bar-title">%s</span>
<span class="hint">Nếu trang lỗi / trắng: F5 hoặc Xóa cache</span> <span class="hint">Trang trắng/lỗi: F5 hoặc Xóa cache (giữ đăng nhập)</span>
<button type="button" class="btn-refresh" id="btn-refresh" onclick="refreshExam()">⟳ F5 Làm mới</button> <button type="button" class="btn-refresh" id="btn-refresh" onclick="refreshExam()">⟳ F5 Làm mới</button>
<button type="button" class="btn-cache" id="btn-cache" onclick="clearExamCache()">🗑 Xóa cache</button> <button type="button" class="btn-cache" id="btn-cache" onclick="clearExamCache()">🗑 Xóa cache</button>
</div> </div>
@@ -640,58 +650,35 @@ iframe{width:100%%;height:100%%;border:0;display:block}
function goBack(){ function goBack(){
fetch('http://127.0.0.1:34115/exam-close').catch(function(){}); fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
} }
function refreshExam(){ function reloadFrameSoft(){
var f = document.getElementById('exam-frame'); var f = document.getElementById('exam-frame');
if (!f) return; if (!f) return;
var btn = document.getElementById('btn-refresh');
if (btn) btn.disabled = true;
try { try {
f.contentWindow.location.reload(); f.contentWindow.location.reload();
} catch (e) { } catch (e) {
var src = f.src; var src = f.getAttribute('src') || f.src;
f.src = 'about:blank'; f.src = 'about:blank';
setTimeout(function(){ f.src = src; }, 50); setTimeout(function(){ f.src = src; }, 50);
} }
}
function refreshExam(){
var btn = document.getElementById('btn-refresh');
if (btn) btn.disabled = true;
reloadFrameSoft();
setTimeout(function(){ if (btn) btn.disabled = false; }, 800); setTimeout(function(){ if (btn) btn.disabled = false; }, 800);
} }
async function clearExamCache(){ async function clearExamCache(){
var btn = document.getElementById('btn-cache'); var btn = document.getElementById('btn-cache');
if (btn) btn.disabled = true; if (btn) btn.disabled = true;
try { // Không xóa localStorage/sessionStorage/cookie — tránh văng đăng nhập trang thi.
if (window.caches) {
var keys = await caches.keys();
await Promise.all(keys.map(function(k){ return caches.delete(k); }));
}
} catch (e) {}
try { localStorage.clear(); } catch (e) {}
try { sessionStorage.clear(); } catch (e) {}
try {
var f = document.getElementById('exam-frame');
if (f && f.contentWindow) {
try { f.contentWindow.localStorage.clear(); } catch (e) {}
try { f.contentWindow.sessionStorage.clear(); } catch (e) {}
try {
if (f.contentWindow.caches) {
var ikeys = await f.contentWindow.caches.keys();
await Promise.all(ikeys.map(function(k){ return f.contentWindow.caches.delete(k); }));
}
} catch (e) {}
}
} catch (e) {}
try { try {
await fetch('http://127.0.0.1:34115/exam-clear-cache'); await fetch('http://127.0.0.1:34115/exam-clear-cache');
} catch (e) {} } catch (e) {}
var f = document.getElementById('exam-frame'); // Chờ một nhịp để disk cache kịp xóa rồi reload cùng URL (không thêm query bust).
if (f) { setTimeout(function(){
try { reloadFrameSoft();
var u = new URL(f.src, window.location.href); if (btn) btn.disabled = false;
u.searchParams.set('_sc_cb', String(Date.now())); }, 400);
f.src = u.toString();
} catch (e) {
refreshExam();
}
}
setTimeout(function(){ if (btn) btn.disabled = false; }, 1200);
} }
document.addEventListener('keydown', function(e){ document.addEventListener('keydown', function(e){
if (e.key === 'F5') { if (e.key === 'F5') {
@@ -1880,36 +1867,26 @@ func (a *App) ReloadExamPage() {
`) `)
} }
// ClearExamBrowserCache — xóa cache WebView (storage + disk cache) rồi làm mới trang thi. // ClearExamBrowserCache — chỉ xóa HTTP disk cache, giữ cookie/localStorage (không văng đăng nhập trang thi).
func (a *App) ClearExamBrowserCache() { func (a *App) ClearExamBrowserCache() {
guard.SuppressFor(8 * time.Second) guard.SuppressFor(8 * time.Second)
a.purgeWebViewDiskCache() a.purgeWebViewDiskCache()
runtime.WindowExecJS(a.ctx, ` runtime.WindowExecJS(a.ctx, `
(async function(){ (function(){
try { try {
if (window.caches) { if (typeof window.reloadFrameSoft === 'function') {
var keys = await caches.keys(); window.reloadFrameSoft();
await Promise.all(keys.map(function(k){ return caches.delete(k); })); return;
} }
} catch (e) {}
try { localStorage.clear(); } catch (e) {}
try { sessionStorage.clear(); } catch (e) {}
try {
var f = document.getElementById('exam-frame'); var f = document.getElementById('exam-frame');
if (f) { if (f) {
try { f.contentWindow.localStorage.clear(); } catch (e) {} try { f.contentWindow.location.reload(); }
try { f.contentWindow.sessionStorage.clear(); } catch (e) {} catch (e) {
try { var s = f.getAttribute('src') || f.src;
var u = new URL(f.src, window.location.href); f.src = 'about:blank';
u.searchParams.set('_sc_cb', String(Date.now())); setTimeout(function(){ f.src = s; }, 50);
f.src = u.toString();
return;
} catch (e) {
try { f.contentWindow.location.reload(); } catch (e2) {
var s = f.src; f.src = 'about:blank'; setTimeout(function(){ f.src = s; }, 50);
}
return;
} }
return;
} }
} catch (e) {} } catch (e) {}
location.reload(); location.reload();
@@ -1921,12 +1898,11 @@ func (a *App) purgeWebViewDiskCache() {
if a.webviewDataPath == "" { if a.webviewDataPath == "" {
return return
} }
// Chỉ Cache tải tài nguyên — KHÔNG xóa Cookies / Local Storage / Service Worker (giữ phiên đăng nhập).
subs := []string{ subs := []string{
filepath.Join("EBWebView", "Default", "Cache"), filepath.Join("EBWebView", "Default", "Cache"),
filepath.Join("EBWebView", "Default", "Code Cache"), filepath.Join("EBWebView", "Default", "Code Cache"),
filepath.Join("EBWebView", "Default", "GPUCache"), filepath.Join("EBWebView", "Default", "GPUCache"),
filepath.Join("EBWebView", "Default", "Service Worker", "CacheStorage"),
filepath.Join("EBWebView", "Default", "Service Worker", "ScriptCache"),
} }
for _, sub := range subs { for _, sub := range subs {
path := filepath.Join(a.webviewDataPath, sub) path := filepath.Join(a.webviewDataPath, sub)
@@ -1948,17 +1924,12 @@ func isLocalExamURL(raw string) bool {
} }
func (a *App) openExamWebView(targetURL, title string) error { func (a *App) openExamWebView(targetURL, title string) error {
return a.openExamWebViewOpts(targetURL, title, false)
}
// openExamWebViewOpts mở nội dung trong WebView.
// forceWrap=true luôn dùng khung exam-view (có nút F5 / xóa cache) — dùng cho trắc nghiệm.
func (a *App) openExamWebViewOpts(targetURL, title string, forceWrap bool) error {
targetURL = strings.TrimSpace(targetURL) targetURL = strings.TrimSpace(targetURL)
if targetURL == "" { if targetURL == "" {
return errors.New("không có nội dung để mở") return errors.New("không có nội dung để mở")
} }
if forceWrap || isLocalExamURL(targetURL) { // URL local (PDF/resource): khung exam-view. URL ngoài (quiz): mở top-level giữ first-party cookies/session.
if isLocalExamURL(targetURL) {
wrapper := fmt.Sprintf( wrapper := fmt.Sprintf(
"http://127.0.0.1:34115/exam-view?url=%s&title=%s", "http://127.0.0.1:34115/exam-view?url=%s&title=%s",
url.QueryEscape(targetURL), url.QueryEscape(targetURL),
@@ -1968,7 +1939,6 @@ func (a *App) openExamWebViewOpts(targetURL, title string, forceWrap bool) error
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper)) runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper))
return nil return nil
} }
// Trang thi bên ngoài (đề/tài nguyên): mở trực tiếp để giữ first-party cookies.
guard.SuppressFor(5 * time.Second) guard.SuppressFor(5 * time.Second)
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", targetURL)) runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", targetURL))
return nil return nil
@@ -2060,8 +2030,9 @@ func (a *App) OpenExamQuiz() error {
if err != nil { if err != nil {
return err return err
} }
// Luôn mở trong khung có nút F5 / Xóa cache để SV tự xử lý khi trang lỗi. // Mở trực tiếp (không iframe) để cookie/localStorage đăng nhập web quiz là first-party.
return a.openExamWebViewOpts(viewURL, "Làm trắc nghiệm", true) // F5 / Xóa cache / Về trang chính: menu Simple Care.
return a.openExamWebView(viewURL, "Làm trắc nghiệm")
} }
func (a *App) OpenExamResource(fileID uint) error { func (a *App) OpenExamResource(fileID uint) error {

View File

@@ -413,7 +413,7 @@ function renderExamPanel() {
<div class="card-title-row"> <div class="card-title-row">
<div class="card-title">📝 ${ex.examName || 'Phòng thi'}</div> <div class="card-title">📝 ${ex.examName || 'Phòng thi'}</div>
</div> </div>
<p class="exam-panel-desc">Đề PDF và tài nguyên xem trong app. Trắc nghiệm có nút <strong>F5</strong> / <strong>Xóa cache</strong> trên thanh công cụ. Xong: menu <strong>Simple Care → Về trang chính</strong> (Ctrl+H).</p> <p class="exam-panel-desc">Đề PDF và tài nguyên xem trong app. Trắc nghiệm mở trang quiz trực tiếp (giữ đăng nhập). F5 / Xóa cache / Về trang chính: menu <strong>Simple Care</strong> (F5, Ctrl+Delete, Ctrl+H).</p>
<div class="exam-panel-actions"> <div class="exam-panel-actions">
${paperBtn} ${paperBtn}
${quizBtn} ${quizBtn}

View File

@@ -5,6 +5,8 @@ export function CheckLoginStatus():Promise<boolean>;
export function ClearChatUnread():Promise<void>; export function ClearChatUnread():Promise<void>;
export function ClearExamBrowserCache():Promise<void>;
export function DownloadExamResource(arg1:number):Promise<string>; export function DownloadExamResource(arg1:number):Promise<string>;
export function GetChatConversations():Promise<Array<Record<string, any>>>; export function GetChatConversations():Promise<Array<Record<string, any>>>;
@@ -23,6 +25,8 @@ export function GetStats():Promise<Record<string, any>>;
export function GetStudentInfo():Promise<Record<string, any>>; export function GetStudentInfo():Promise<Record<string, any>>;
export function HandleBeforeClose():Promise<boolean>;
export function LoadExamViewFile(arg1:string):Promise<Record<string, any>>; export function LoadExamViewFile(arg1:string):Promise<Record<string, any>>;
export function Logout():Promise<void>; export function Logout():Promise<void>;
@@ -35,6 +39,8 @@ export function OpenExamQuiz():Promise<void>;
export function OpenExamResource(arg1:number):Promise<void>; export function OpenExamResource(arg1:number):Promise<void>;
export function ReloadExamPage():Promise<void>;
export function ReturnToDashboard():Promise<void>; export function ReturnToDashboard():Promise<void>;
export function SendChatMessage(arg1:string):Promise<void>; export function SendChatMessage(arg1:string):Promise<void>;

View File

@@ -10,82 +10,94 @@ export function ClearChatUnread() {
return ObfuscatedCall(1, []); return ObfuscatedCall(1, []);
} }
export function ClearExamBrowserCache() {
return ObfuscatedCall(2, []);
}
export function DownloadExamResource(arg1) { export function DownloadExamResource(arg1) {
return ObfuscatedCall(2, [arg1]); return ObfuscatedCall(3, [arg1]);
} }
export function GetChatConversations() { export function GetChatConversations() {
return ObfuscatedCall(3, []); return ObfuscatedCall(4, []);
} }
export function GetChatMessages(arg1) { export function GetChatMessages(arg1) {
return ObfuscatedCall(4, [arg1]); return ObfuscatedCall(5, [arg1]);
} }
export function GetChatUnread() { export function GetChatUnread() {
return ObfuscatedCall(5, []);
}
export function GetExamPaperFiles() {
return ObfuscatedCall(6, []); return ObfuscatedCall(6, []);
} }
export function GetExamPaperViewURL() { export function GetExamPaperFiles() {
return ObfuscatedCall(7, []); return ObfuscatedCall(7, []);
} }
export function GetExamQuizViewURL() { export function GetExamPaperViewURL() {
return ObfuscatedCall(8, []); return ObfuscatedCall(8, []);
} }
export function GetStats() { export function GetExamQuizViewURL() {
return ObfuscatedCall(9, []); return ObfuscatedCall(9, []);
} }
export function GetStudentInfo() { export function GetStats() {
return ObfuscatedCall(10, []); return ObfuscatedCall(10, []);
} }
export function LoadExamViewFile(arg1) { export function GetStudentInfo() {
return ObfuscatedCall(11, [arg1]); return ObfuscatedCall(11, []);
} }
export function Logout() { export function HandleBeforeClose() {
return ObfuscatedCall(12, []); return ObfuscatedCall(12, []);
} }
export function NavigateToLogin() { export function LoadExamViewFile(arg1) {
return ObfuscatedCall(13, []); return ObfuscatedCall(13, [arg1]);
} }
export function OpenExamPaper() { export function Logout() {
return ObfuscatedCall(14, []); return ObfuscatedCall(14, []);
} }
export function OpenExamQuiz() { export function NavigateToLogin() {
return ObfuscatedCall(15, []); return ObfuscatedCall(15, []);
} }
export function OpenExamResource(arg1) { export function OpenExamPaper() {
return ObfuscatedCall(16, [arg1]); return ObfuscatedCall(16, []);
} }
export function ReturnToDashboard() { export function OpenExamQuiz() {
return ObfuscatedCall(17, []); return ObfuscatedCall(17, []);
} }
export function SendChatMessage(arg1) { export function OpenExamResource(arg1) {
return ObfuscatedCall(18, [arg1]); return ObfuscatedCall(18, [arg1]);
} }
export function SendWebcamFrame(arg1) { export function ReloadExamPage() {
return ObfuscatedCall(19, [arg1]); return ObfuscatedCall(19, []);
} }
export function SubmitExamWork() { export function ReturnToDashboard() {
return ObfuscatedCall(20, []); return ObfuscatedCall(20, []);
} }
export function UnlockChatAudio() { export function SendChatMessage(arg1) {
return ObfuscatedCall(21, []); return ObfuscatedCall(21, [arg1]);
}
export function SendWebcamFrame(arg1) {
return ObfuscatedCall(22, [arg1]);
}
export function SubmitExamWork() {
return ObfuscatedCall(23, []);
}
export function UnlockChatAudio() {
return ObfuscatedCall(24, []);
} }

View File

@@ -752,6 +752,12 @@ func ReportViolationHandler(db *gorm.DB) fiber.Handler {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId and kind are required"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "studentRkId and kind are required"})
} }
monitorMode := strings.TrimSpace(req.MonitorMode)
// Chỉ lưu khi đang thi hoặc đang học — ngoài giờ / chưa cấu hình bỏ qua
if monitorMode != "exam" && monitorMode != "learning" {
return c.JSON(fiber.Map{"ok": true, "skipped": true, "reason": "not_in_exam_or_learning"})
}
kind := strings.ToLower(strings.TrimSpace(req.Kind)) kind := strings.ToLower(strings.TrimSpace(req.Kind))
reason := strings.TrimSpace(req.Reason) reason := strings.TrimSpace(req.Reason)
if reason == "" { if reason == "" {
@@ -768,7 +774,7 @@ func ReportViolationHandler(db *gorm.DB) fiber.Handler {
ClassRkID: classID, ClassRkID: classID,
Kind: kind, Kind: kind,
Reason: reason, Reason: reason,
MonitorMode: strings.TrimSpace(req.MonitorMode), MonitorMode: monitorMode,
ClientAt: clientAt, ClientAt: clientAt,
} }
if err := db.Create(&row).Error; err != nil { if err := db.Create(&row).Error; err != nil {