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()
mode := a.dashboard.MonitorMode
a.mu.Unlock()
if mode == "" || mode == "not_configured" {
if !shouldRecordViolation(mode) {
a.clearRunLock()
return false
}
@@ -359,6 +359,16 @@ type pendingViolation struct {
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() {
a.mu.Lock()
studentID := int64(0)
@@ -367,7 +377,7 @@ func (a *App) markRunLock() {
studentID = a.student.StudentID
}
a.mu.Unlock()
if studentID <= 0 || mode == "" || mode == "not_configured" {
if studentID <= 0 || !shouldRecordViolation(mode) {
return
}
payload, _ := json.Marshal(map[string]any{
@@ -402,7 +412,7 @@ func (a *App) detectUncleanShutdown() {
}
a.mu.Unlock()
}
if meta.StudentRkID <= 0 {
if meta.StudentRkID <= 0 || !shouldRecordViolation(meta.MonitorMode) {
return
}
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 {
if v.StudentRkID <= 0 {
return true
if v.StudentRkID <= 0 || !shouldRecordViolation(v.MonitorMode) {
return true // bỏ qua — không thi / không học thì không lưu
}
payload := map[string]any{
"studentRkId": v.StudentRkID,
@@ -492,7 +502,7 @@ func (a *App) reportViolation(kind, reason string) {
studentID = a.student.StudentID
}
a.mu.Unlock()
if studentID <= 0 {
if studentID <= 0 || !shouldRecordViolation(mode) {
return
}
v := pendingViolation{
@@ -590,7 +600,7 @@ func (a *App) startLocalServer() {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
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()
w.Write([]byte("ok"))
})
@@ -609,18 +619,18 @@ const examViewHTML = `<!DOCTYPE html>
<title>%s</title>
<style>
*{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}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#0f172a;border-bottom:1px solid #334155;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 button{border:none;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:hover{background:#6d28d9}
.btn-refresh{background:#0ea5e9;color:#fff}
.btn-refresh:hover{background:#0284c7}
.btn-cache{background:#f59e0b;color:#111}
.btn-cache:hover{background:#d97706}
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:#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;color:#1a2332}
.bar button{border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap}
.btn-back{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-back:hover{background:#fde8e9}
.btn-refresh{background:#bb2126;color:#fff;border:1px solid #bb2126}
.btn-refresh:hover{background:#9e1c22;border-color:#9e1c22}
.btn-cache{background:#fff;color:#bb2126;border:1px solid #bb2126}
.btn-cache:hover{background:#fde8e9}
.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}
iframe{width:100%%;height:100%%;border:0;display:block}
</style>
@@ -629,7 +639,7 @@ iframe{width:100%%;height:100%%;border:0;display:block}
<div class="bar">
<button type="button" class="btn-back" onclick="goBack()">← Quay lại</button>
<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-cache" id="btn-cache" onclick="clearExamCache()">🗑 Xóa cache</button>
</div>
@@ -640,58 +650,35 @@ iframe{width:100%%;height:100%%;border:0;display:block}
function goBack(){
fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
}
function refreshExam(){
function reloadFrameSoft(){
var f = document.getElementById('exam-frame');
if (!f) return;
var btn = document.getElementById('btn-refresh');
if (btn) btn.disabled = true;
try {
f.contentWindow.location.reload();
} catch (e) {
var src = f.src;
var src = f.getAttribute('src') || f.src;
f.src = 'about:blank';
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);
}
async function clearExamCache(){
var btn = document.getElementById('btn-cache');
if (btn) btn.disabled = true;
try {
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) {}
// Không xóa localStorage/sessionStorage/cookie — tránh văng đăng nhập trang thi.
try {
await fetch('http://127.0.0.1:34115/exam-clear-cache');
} catch (e) {}
var f = document.getElementById('exam-frame');
if (f) {
try {
var u = new URL(f.src, window.location.href);
u.searchParams.set('_sc_cb', String(Date.now()));
f.src = u.toString();
} catch (e) {
refreshExam();
}
}
setTimeout(function(){ if (btn) btn.disabled = false; }, 1200);
// Chờ một nhịp để disk cache kịp xóa rồi reload cùng URL (không thêm query bust).
setTimeout(function(){
reloadFrameSoft();
if (btn) btn.disabled = false;
}, 400);
}
document.addEventListener('keydown', function(e){
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() {
guard.SuppressFor(8 * time.Second)
a.purgeWebViewDiskCache()
runtime.WindowExecJS(a.ctx, `
(async function(){
(function(){
try {
if (window.caches) {
var keys = await caches.keys();
await Promise.all(keys.map(function(k){ return caches.delete(k); }));
if (typeof window.reloadFrameSoft === 'function') {
window.reloadFrameSoft();
return;
}
} catch (e) {}
try { localStorage.clear(); } catch (e) {}
try { sessionStorage.clear(); } catch (e) {}
try {
var f = document.getElementById('exam-frame');
if (f) {
try { f.contentWindow.localStorage.clear(); } catch (e) {}
try { f.contentWindow.sessionStorage.clear(); } catch (e) {}
try {
var u = new URL(f.src, window.location.href);
u.searchParams.set('_sc_cb', String(Date.now()));
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;
try { f.contentWindow.location.reload(); }
catch (e) {
var s = f.getAttribute('src') || f.src;
f.src = 'about:blank';
setTimeout(function(){ f.src = s; }, 50);
}
return;
}
} catch (e) {}
location.reload();
@@ -1921,12 +1898,11 @@ func (a *App) purgeWebViewDiskCache() {
if a.webviewDataPath == "" {
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{
filepath.Join("EBWebView", "Default", "Cache"),
filepath.Join("EBWebView", "Default", "Code Cache"),
filepath.Join("EBWebView", "Default", "GPUCache"),
filepath.Join("EBWebView", "Default", "Service Worker", "CacheStorage"),
filepath.Join("EBWebView", "Default", "Service Worker", "ScriptCache"),
}
for _, sub := range subs {
path := filepath.Join(a.webviewDataPath, sub)
@@ -1948,17 +1924,12 @@ func isLocalExamURL(raw string) bool {
}
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)
if targetURL == "" {
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(
"http://127.0.0.1:34115/exam-view?url=%s&title=%s",
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))
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)
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", targetURL))
return nil
@@ -2060,8 +2030,9 @@ func (a *App) OpenExamQuiz() error {
if err != nil {
return err
}
// Luôn mở trong khung có nút F5 / Xóa cache để SV tự xử lý khi trang lỗi.
return a.openExamWebViewOpts(viewURL, "Làm trắc nghiệm", true)
// Mở trực tiếp (không iframe) để cookie/localStorage đăng nhập web quiz là first-party.
// 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 {

View File

@@ -413,7 +413,7 @@ function renderExamPanel() {
<div class="card-title-row">
<div class="card-title">📝 ${ex.examName || 'Phòng thi'}</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">
${paperBtn}
${quizBtn}

View File

@@ -5,6 +5,8 @@ export function CheckLoginStatus():Promise<boolean>;
export function ClearChatUnread():Promise<void>;
export function ClearExamBrowserCache():Promise<void>;
export function DownloadExamResource(arg1:number):Promise<string>;
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 HandleBeforeClose():Promise<boolean>;
export function LoadExamViewFile(arg1:string):Promise<Record<string, any>>;
export function Logout():Promise<void>;
@@ -35,6 +39,8 @@ export function OpenExamQuiz():Promise<void>;
export function OpenExamResource(arg1:number):Promise<void>;
export function ReloadExamPage():Promise<void>;
export function ReturnToDashboard():Promise<void>;
export function SendChatMessage(arg1:string):Promise<void>;

View File

@@ -10,82 +10,94 @@ export function ClearChatUnread() {
return ObfuscatedCall(1, []);
}
export function ClearExamBrowserCache() {
return ObfuscatedCall(2, []);
}
export function DownloadExamResource(arg1) {
return ObfuscatedCall(2, [arg1]);
return ObfuscatedCall(3, [arg1]);
}
export function GetChatConversations() {
return ObfuscatedCall(3, []);
return ObfuscatedCall(4, []);
}
export function GetChatMessages(arg1) {
return ObfuscatedCall(4, [arg1]);
return ObfuscatedCall(5, [arg1]);
}
export function GetChatUnread() {
return ObfuscatedCall(5, []);
}
export function GetExamPaperFiles() {
return ObfuscatedCall(6, []);
}
export function GetExamPaperViewURL() {
export function GetExamPaperFiles() {
return ObfuscatedCall(7, []);
}
export function GetExamQuizViewURL() {
export function GetExamPaperViewURL() {
return ObfuscatedCall(8, []);
}
export function GetStats() {
export function GetExamQuizViewURL() {
return ObfuscatedCall(9, []);
}
export function GetStudentInfo() {
export function GetStats() {
return ObfuscatedCall(10, []);
}
export function LoadExamViewFile(arg1) {
return ObfuscatedCall(11, [arg1]);
export function GetStudentInfo() {
return ObfuscatedCall(11, []);
}
export function Logout() {
export function HandleBeforeClose() {
return ObfuscatedCall(12, []);
}
export function NavigateToLogin() {
return ObfuscatedCall(13, []);
export function LoadExamViewFile(arg1) {
return ObfuscatedCall(13, [arg1]);
}
export function OpenExamPaper() {
export function Logout() {
return ObfuscatedCall(14, []);
}
export function OpenExamQuiz() {
export function NavigateToLogin() {
return ObfuscatedCall(15, []);
}
export function OpenExamResource(arg1) {
return ObfuscatedCall(16, [arg1]);
export function OpenExamPaper() {
return ObfuscatedCall(16, []);
}
export function ReturnToDashboard() {
export function OpenExamQuiz() {
return ObfuscatedCall(17, []);
}
export function SendChatMessage(arg1) {
export function OpenExamResource(arg1) {
return ObfuscatedCall(18, [arg1]);
}
export function SendWebcamFrame(arg1) {
return ObfuscatedCall(19, [arg1]);
export function ReloadExamPage() {
return ObfuscatedCall(19, []);
}
export function SubmitExamWork() {
export function ReturnToDashboard() {
return ObfuscatedCall(20, []);
}
export function UnlockChatAudio() {
return ObfuscatedCall(21, []);
export function SendChatMessage(arg1) {
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"})
}
monitorMode := strings.TrimSpace(req.MonitorMode)
// Chỉ lưu khi đang thi hoặc đang học — ngoài giờ / chưa cấu hình bỏ qua
if monitorMode != "exam" && monitorMode != "learning" {
return c.JSON(fiber.Map{"ok": true, "skipped": true, "reason": "not_in_exam_or_learning"})
}
kind := strings.ToLower(strings.TrimSpace(req.Kind))
reason := strings.TrimSpace(req.Reason)
if reason == "" {
@@ -768,7 +774,7 @@ func ReportViolationHandler(db *gorm.DB) fiber.Handler {
ClassRkID: classID,
Kind: kind,
Reason: reason,
MonitorMode: strings.TrimSpace(req.MonitorMode),
MonitorMode: monitorMode,
ClientAt: clientAt,
}
if err := db.Create(&row).Error; err != nil {