fix
This commit is contained in:
147
client/app.go
147
client/app.go
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
6
client/frontend/wailsjs/go/main/App.d.ts
vendored
6
client/frontend/wailsjs/go/main/App.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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, []);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user