diff --git a/server/routes/api.js b/server/routes/api.js index 01389fb..317d40b 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -1191,6 +1191,29 @@ router.get('/export', (req, res) => { res.json(DataService.buildExportPayload()); }); +// Issues +router.post('/report_issue', async (req, res) => { + const r = req.body || {}; + const item = { + ts: Number(r.ts || Date.now()), + device: String(r.device || 'unknown'), + machine_id: String(r.machine_id || ''), + station_code: String(r.station_code || ''), + error_msg: String(r.error_msg || ''), + detail: String(r.detail || ''), + type: 'issue' + }; + await DataService.appendIssue(item); + io.emit('issue:new', item); + appendReqLog(req, { category: 'device', type: 'report_issue', detail: item }); + res.json({ ok: true }); +}); + +router.get('/issues', (req, res) => { + const issues = DataService.getIssues(); + res.json({ ok: true, issues }); +}); + // Ticket Operations router.post('/tickets/sale', async (req, res) => { const { start, terminal, train_type, cost, station_code, device, trips_total, trips_remaining } = req.body || {}; diff --git a/server/services/data.js b/server/services/data.js index 78ac335..366e739 100644 --- a/server/services/data.js +++ b/server/services/data.js @@ -64,7 +64,8 @@ const cache = { icCardIndex: {}, icCardEvents: [], statsTicket: [], - statsGate: [] + statsGate: [], + issues: [] }; const DataService = { @@ -88,6 +89,7 @@ const DataService = { await conn.query(`CREATE TABLE IF NOT EXISTS ticket_events (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`); await conn.query(`CREATE TABLE IF NOT EXISTS stats_ticket (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`); await conn.query(`CREATE TABLE IF NOT EXISTS stats_gate (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`); + await conn.query(`CREATE TABLE IF NOT EXISTS issues (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`); // Load Cache const [configs] = await conn.query('SELECT v FROM kv_store WHERE k = ?', ['config']); @@ -126,6 +128,9 @@ const DataService = { const [statsG] = await conn.query('SELECT data FROM stats_gate'); cache.statsGate = statsG.map(r => r.data); + const [issues] = await conn.query('SELECT data FROM issues'); + cache.issues = issues.map(r => r.data); + console.log('DataService initialized with MySQL'); } finally { conn.release(); @@ -511,6 +516,15 @@ const DataService = { }, getStatsGate: () => cache.statsGate, + // Issues + appendIssue: async (item) => { + cache.issues.push(item); + try { + await pool.query('INSERT INTO issues (data) VALUES (?)', [JSON.stringify(item)]); + } catch(e) { console.error('appendIssue error', e); } + }, + getIssues: () => cache.issues, + // Export buildExportPayload: () => ({ config: DataService.getConfig(), diff --git a/ticketmachine.lua b/ticketmachine.lua index 647932c..fff32ef 100644 --- a/ticketmachine.lua +++ b/ticketmachine.lua @@ -327,6 +327,19 @@ local function enqueueTicketUpload(ticketData) return false end +local function reportIssue(errMsg, detail) + local url = API_BASE .. '/report_issue' + local payload = { + device = currentDeviceId(), + machine_id = currentDeviceId(), + station_code = state.stationCode or CURRENT_STATION_CODE, + error_msg = tostring(errMsg or ''), + detail = tostring(detail or ''), + ts = (os.epoch and os.epoch('utc')) or (os.time() * 1000) + } + postJSONResult(url, payload) +end + -- ########################### -- Peripheral discovery -- ########################### @@ -1286,14 +1299,22 @@ local function renderConfirmCancel() addButton(bx+4+bw, by+4, 'NO', bw, 1, {colors.green, colors.white}, function() ui_cancel_confirmed = false; ui_cancel_request = false end) end -local function showAlert(msg) +local function showAlert(msg, detail) + if detail then + reportIssue(msg, detail) + end local boxW, boxH = math.max(24, math.min(32, w-4)), 6 local bx, by = math.floor((w-boxW)/2)+1, math.floor((h-boxH)/2)+1 termDev.setBackgroundColor(colors.gray) for i=0,boxH-1 do termDev.setCursorPos(bx, by+i); termDev.write(string.rep(' ', boxW)) end - msg = tostring(msg or '') - if #msg > boxW-4 then msg = msg:sub(1, boxW-4) end - termDev.setTextColor(colors.white); termDev.setCursorPos(bx+2, by+2); termDev.write(msg) + local displayMsg = tostring(msg or '') + if #displayMsg > boxW-4 then displayMsg = displayMsg:sub(1, boxW-4) end + termDev.setTextColor(colors.white); termDev.setCursorPos(bx+2, by+2); termDev.write(displayMsg) + if detail then + local displayDetail = tostring(detail) + if #displayDetail > boxW-4 then displayDetail = displayDetail:sub(1, boxW-4) end + termDev.setCursorPos(bx+2, by+3); termDev.write(displayDetail) + end Buttons = {} addButton(bx+math.floor((boxW-6)/2), by+4, 'OK', 6, 1, {colors.green, colors.white}, function() end) waitButtons() @@ -1815,7 +1836,7 @@ local function showTerminal() invalidUntil = os.epoch('utc') + 1500 sel = nil state.terminal = nil - showAlert('Same station') + showAlert('Same station', 'Dep and Term are identical') else state.terminal = sel end @@ -2073,7 +2094,7 @@ local function showOrderAndAudio() else local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local') if not unit then - showAlert('No route') + showAlert('No route', 'Cannot find fare path') state.page = 'terminal' return end @@ -2084,7 +2105,7 @@ local function showOrderAndAudio() end if state.productMode ~= 'card' and state.trainType ~= 'single' and sameLogicalStation(state.departure, state.terminal) then - showAlert('Same station') + showAlert('Same station', 'Cannot purchase to same station') state.page = state.voucher_code and 'online' or 'terminal' return end @@ -2097,7 +2118,7 @@ local function showOrderAndAudio() local function confirmAction() if processing or confirmed then return end if state.productMode ~= 'card' and sameLogicalStation(state.departure, state.terminal) then - showAlert('Same station') + showAlert('Same station', 'Dep and Term are identical on confirm') if render then render() end return end @@ -2411,7 +2432,7 @@ local function showDone() local issueError = tostring(issueMethod or 'ticket_issue_failed') print('Ticket issue failed: ' .. issueError) _G.TICKET_MACHINE_LAST_TICKET.ticket_issue_error = issueError - showAlert('Ticket issue failed') + showAlert('Ticket issue failed', issueError) resetTicketFlow() state.page = 'home' return @@ -2608,4 +2629,12 @@ local function mainPageLoop() end end -parallel.waitForAny(mainPageLoop, backgroundSyncTask, backgroundTicketUploadTask, backgroundPeripheralTask) +local function safeMain() + local ok, err = pcall(parallel.waitForAny, mainPageLoop, backgroundSyncTask, backgroundTicketUploadTask, backgroundPeripheralTask) + if not ok then + reportIssue("Fatal Error", tostring(err)) + error(err) + end +end + +safeMain() diff --git a/web/index.html b/web/index.html index 106d23e..ba93d05 100644 --- a/web/index.html +++ b/web/index.html @@ -73,6 +73,9 @@ + @@ -893,6 +896,7 @@ +
@@ -947,6 +951,49 @@
+ +
+
+
+

问题汇总

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
时间设备车站错误信息详细信息
{{ formatTime(issue.ts) }}{{ issue.device || issue.machine_id }}{{ issue.station_code }}{{ issue.error_msg }} +
+ 查看详情 +
{{ issue.detail }}
+
+ 无 +
+
+ +

暂无问题上报记录

+
+
+
+