feat: 优化自动更新逻辑,移除单程票购票并升级版本至1.0.18

升级package.json版本至1.0.18;重构update_gate.lua和update_machine.lua,支持--silent静默运行参数,统一日志输出逻辑;优化ticketmachine.lua和gate.lua的自动更新触发逻辑,新增设备空闲检测,避免打断用户操作,仅在空闲时执行静默更新后重启设备;修复ticketmachine.lua中state变量重复声明的问题;移除Web端ticket-order相关的单程票购票UI与业务逻辑,简化购票流程;将gate.lua中的版本检查间隔从5秒调整为60秒,优化设备ID显示格式。
This commit is contained in:
2026-07-10 20:51:34 +08:00
parent 1bdf0ca5f2
commit 4daa0b6185
7 changed files with 114 additions and 223 deletions
+37 -15
View File
@@ -2,7 +2,7 @@ local DEFAULT_SERVER_BASE = "http://ticket.fse-media.group"
local DEFAULT_SERVER_PATH = "/api/tickets/check" local DEFAULT_SERVER_PATH = "/api/tickets/check"
local GATE_OPEN_SECONDS = 2 local GATE_OPEN_SECONDS = 2
local VERSION = "v1.5.10" local VERSION = "v1.5.10"
local VERSION_CHECK_INTERVAL = 5 local VERSION_CHECK_INTERVAL = 60
local CONFIG_PATH = "gate_config.json" local CONFIG_PATH = "gate_config.json"
@@ -92,6 +92,30 @@ local serverConnected = nil
local serverLastChangeTs = 0 local serverLastChangeTs = 0
local expectedGateVersion = nil local expectedGateVersion = nil
local versionMismatch = nil local versionMismatch = nil
local pendingGateUpdate = false
local gateBusyUntilTs = 0
local function nowMs()
return (os.epoch and os.epoch("utc")) or (os.time() * 1000)
end
local function markGateBusy(seconds)
local secondsNum = tonumber(seconds) or 0
local untilTs = nowMs() + math.max(1000, math.floor(secondsNum * 1000))
if untilTs > gateBusyUntilTs then
gateBusyUntilTs = untilTs
end
end
local function isGateIdleForUpdate()
return nowMs() >= gateBusyUntilTs
end
local function runSilentGateUpdate()
local baseEnv = (getfenv and getfenv()) or _ENV or _G
local env = setmetatable({ AUTO_UPDATE_SILENT = true }, { __index = baseEnv })
return os.run(env, "update_gate.lua", "--silent")
end
local function setServerConnected(ok) local function setServerConnected(ok)
if serverConnected == ok then return end if serverConnected == ok then return end
@@ -997,7 +1021,7 @@ local function currentDeviceId()
if label and label ~= "" then if label and label ~= "" then
return label return label
end end
return "闸机#" .. tostring(os.getComputerID()) return "#" .. tostring(os.getComputerID())
end end
local function syncICCardState(cardId, payload) local function syncICCardState(cardId, payload)
@@ -1353,6 +1377,7 @@ local versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
while true do while true do
local ev = pack(os.pullEvent()) local ev = pack(os.pullEvent())
if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then
markGateBusy(GATE_OPEN_SECONDS + 2)
processInspectionEvent(ev[1], ev) processInspectionEvent(ev[1], ev)
os.sleep(0.35) os.sleep(0.35)
drawReadyScreen() drawReadyScreen()
@@ -1361,19 +1386,16 @@ while true do
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL)) refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
end) end)
if versionMismatch == true then if versionMismatch == true then
pcall(function() pendingGateUpdate = true
clear() elseif versionMismatch == false then
local w, h = termDev.getSize() pendingGateUpdate = false
centerText(math.floor(h / 2), "Auto Updating...", colors.yellow) end
end) if pendingGateUpdate and isGateIdleForUpdate() then
pcall(function() local ok, updated = pcall(runSilentGateUpdate)
if shell and type(shell.run) == "function" then if ok and updated then
shell.run("update_gate.lua") os.reboot()
else return
os.run(getfenv and getfenv() or _ENV, "update_gate.lua") end
end
end)
os.reboot()
end end
drawReadyScreen() drawReadyScreen()
versionTimer = os.startTimer(VERSION_CHECK_INTERVAL) versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ticketmachine", "name": "ticketmachine",
"version": "1.0.17", "version": "1.0.18",
"main": "server/index.js", "main": "server/index.js",
"scripts": { "scripts": {
"start": "node server/index.js", "start": "node server/index.js",
+21 -10
View File
@@ -1,6 +1,7 @@
local CURRENT_STATION_CODE = 'Ticket-Machine' local CURRENT_STATION_CODE = 'Ticket-Machine'
local API_BASE = 'http://ticket.fse-media.group/api' local API_BASE = 'http://ticket.fse-media.group/api'
local VERSION = 'v1.5.12' local VERSION = 'v1.5.12'
local UPDATE_CHECK_INTERVAL = 60
-- ########################### -- ###########################
-- Core HTTP & JSON Utilities -- Core HTTP & JSON Utilities
@@ -13,6 +14,8 @@ end
local serverConnected = nil local serverConnected = nil
local versionMismatch = nil local versionMismatch = nil
local pendingMachineUpdate = false
local state
local function normalizeVersionTag(v) local function normalizeVersionTag(v)
local s = tostring(v or ''):gsub('^%s+', ''):gsub('%s+$', '') local s = tostring(v or ''):gsub('^%s+', ''):gsub('%s+$', '')
@@ -652,19 +655,27 @@ local function clickSound() end
-- ########################### -- ###########################
-- Background Tasks -- Background Tasks
-- ########################### -- ###########################
local function runSilentMachineUpdate()
local baseEnv = (getfenv and getfenv()) or _ENV or _G
local env = setmetatable({ AUTO_UPDATE_SILENT = true }, { __index = baseEnv })
return os.run(env, 'update_machine.lua', '--silent')
end
local function backgroundSyncTask() local function backgroundSyncTask()
while true do while true do
sleep(5) sleep(UPDATE_CHECK_INTERVAL)
pcall(refreshConfigOnce) pcall(refreshConfigOnce)
if versionMismatch == true then if versionMismatch == true then
pcall(function() pendingMachineUpdate = true
if shell and type(shell.run) == "function" then elseif versionMismatch == false then
shell.run("update_machine.lua") pendingMachineUpdate = false
else end
os.run(getfenv and getfenv() or _ENV, "update_machine.lua") if pendingMachineUpdate and state and tostring(state.page or 'home') == 'home' then
end local ok, updated = pcall(runSilentMachineUpdate)
end) if ok and updated then
-- Removed os.reboot() to prevent interrupting user unexpectedly os.reboot()
return
end
end end
end end
end end
@@ -966,7 +977,7 @@ end
-- ########################### -- ###########################
-- Pages & Navigation -- Pages & Navigation
-- ########################### -- ###########################
local state = { state = {
page = 'home', page = 'home',
stationName = (CFG.current_station and (CFG.current_station.en_name or CFG.current_station.name)) or 'Station', stationName = (CFG.current_station and (CFG.current_station.en_name or CFG.current_station.name)) or 'Station',
stationCode = (CFG.current_station and CFG.current_station.code) or CURRENT_STATION_CODE, stationCode = (CFG.current_station and CFG.current_station.code) or CURRENT_STATION_CODE,
+24 -13
View File
@@ -1,5 +1,12 @@
local URL_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/gate.lua" local URL_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/gate.lua"
local args = { ... }
local silent = AUTO_UPDATE_SILENT == true or args[1] == "--silent"
local function log(msg)
if not silent then
print(msg)
end
end
local function writeFile(path, content, binary) local function writeFile(path, content, binary)
local mode = binary and "wb" or "w" local mode = binary and "wb" or "w"
@@ -54,25 +61,29 @@ local function httpGet(url)
end end
end end
term.clear() if not silent then
term.setCursorPos(1, 1) term.clear()
print("Ticket Gate Updater") term.setCursorPos(1, 1)
print("") log("Ticket Gate Updater")
print("Downloading gate program...") log("")
end
log("Downloading gate program...")
local ok, code = httpGet(URL_GATE) local ok, code = httpGet(URL_GATE)
if not ok or type(code) ~= "string" or #code == 0 then if not ok or type(code) ~= "string" or #code == 0 then
print("Download failed: " .. tostring(code or "")) error("Download failed: " .. tostring(code or ""))
return
end end
if not atomicWrite("startup.lua", code, false) then if not atomicWrite("startup.lua", code, false) then
print("Write failed: startup.lua") error("Write failed: startup.lua")
return
end end
atomicWrite("startup", code, false) atomicWrite("startup", code, false)
if fs.exists("gate.lua") then atomicWrite("gate.lua", code, false) end if fs.exists("gate.lua") then atomicWrite("gate.lua", code, false) end
print("") if not silent then
print("Done.") log("")
print("Reboot the computer to apply the update.") log("Done.")
log("Reboot the computer to apply the update.")
end
return true
+24 -13
View File
@@ -1,5 +1,12 @@
local URL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/ticketmachine.lua" local URL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/ticketmachine.lua"
local args = { ... }
local silent = AUTO_UPDATE_SILENT == true or args[1] == "--silent"
local function log(msg)
if not silent then
print(msg)
end
end
local function writeFile(path, content, binary) local function writeFile(path, content, binary)
local mode = binary and "wb" or "w" local mode = binary and "wb" or "w"
@@ -54,25 +61,29 @@ local function httpGet(url)
end end
end end
term.clear() if not silent then
term.setCursorPos(1, 1) term.clear()
print("Ticket Machine Updater") term.setCursorPos(1, 1)
print("") log("Ticket Machine Updater")
print("Downloading ticket machine program...") log("")
end
log("Downloading ticket machine program...")
local ok, code = httpGet(URL_MACHINE) local ok, code = httpGet(URL_MACHINE)
if not ok or type(code) ~= "string" or #code == 0 then if not ok or type(code) ~= "string" or #code == 0 then
print("Download failed: " .. tostring(code or "")) error("Download failed: " .. tostring(code or ""))
return
end end
if not atomicWrite("startup.lua", code, false) then if not atomicWrite("startup.lua", code, false) then
print("Write failed: startup.lua") error("Write failed: startup.lua")
return
end end
atomicWrite("startup", code, false) atomicWrite("startup", code, false)
if fs.exists("ticketmachine.lua") then atomicWrite("ticketmachine.lua", code, false) end if fs.exists("ticketmachine.lua") then atomicWrite("ticketmachine.lua", code, false) end
print("") if not silent then
print("Done.") log("")
print("Reboot the computer to apply the update.") log("Done.")
log("Reboot the computer to apply the update.")
end
return true
-38
View File
@@ -47,29 +47,6 @@
<section class="jr-content-grid"> <section class="jr-content-grid">
<div class="jr-column-main"> <div class="jr-column-main">
<article class="jr-section-card" id="reservationPanel"> <article class="jr-section-card" id="reservationPanel">
<div class="jr-form-block" style="margin-bottom: 24px;">
<div class="jr-field-head">
<span class="jr-field-label">票种</span>
<span class="jr-field-note">请选择购买的车票类型</span>
</div>
<div class="train-type-group jr-train-type-group">
<label>
<input type="radio" name="ticketClass" value="normal" checked>
<div class="type-card jr-type-card">
<div class="type-title">普通票</div>
<div class="type-desc">按起终点购票</div>
</div>
</label>
<label>
<input type="radio" name="ticketClass" value="single">
<div class="type-card jr-type-card">
<div class="type-title">单程票</div>
<div class="type-desc">限乘一个区间</div>
</div>
</label>
</div>
</div>
<div id="normalTicketView"> <div id="normalTicketView">
<div class="jr-section-head"> <div class="jr-section-head">
<div> <div>
@@ -98,21 +75,6 @@
</div> </div>
</div> </div>
<div id="singleTicketView" style="display: none;">
<div class="jr-section-head">
<div>
<p class="jr-section-label">Single Trip</p>
<h2>选择单程区间</h2>
</div>
<p class="jr-section-note">根据线路和票价分类,购买后可乘坐该线路任意单一区间。</p>
</div>
<div id="singleSegmentList" class="list jr-price-list" style="max-height: 400px; overflow-y: auto;">
<div class="loading">加载区间数据...</div>
</div>
<input type="hidden" id="singleSegmentSelection" value="" />
<input type="hidden" id="singleSegmentPrice" value="" />
</div>
<div class="jr-form-grid" style="margin-top: 24px;"> <div class="jr-form-grid" style="margin-top: 24px;">
<div class="jr-form-block"> <div class="jr-form-block">
<div class="jr-field-head"> <div class="jr-field-head">
+6 -132
View File
@@ -565,43 +565,9 @@
async function previewPrice(force = false){ async function previewPrice(force = false){
const seq = ++previewSeq; const seq = ++previewSeq;
const ticketClass = document.querySelector('input[name="ticketClass"]:checked').value;
const type = document.querySelector('input[name="trainType"]:checked').value; const type = document.querySelector('input[name="trainType"]:checked').value;
const trips = normalizeTrips(); const trips = normalizeTrips();
if (ticketClass === 'single') {
const lineName = document.getElementById('singleSegmentSelection').value;
const priceStr = document.getElementById('singleSegmentPrice').value;
if (!lineName || !priceStr) {
priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择单程区间</p></div>';
return;
}
const cost = Number(priceStr) * trips;
priceBox.innerHTML = `
<div class="list-item jr-result-row">
<span class="k">线路</span>
<span class="v" style="font-weight:700; color:#0ea5e9;">${lineName}</span>
</div>
<div class="list-item jr-result-row">
<span class="k">区间</span>
<span class="v">任意单区间</span>
</div>
<div class="list-item jr-result-row">
<span class="k">车型</span>
<span class="v">${type}</span>
</div>
<div class="list-item jr-result-row">
<span class="k">单价</span>
<span class="v">$${priceStr}</span>
</div>
<div class="list-item jr-result-row">
<span class="k">总价</span>
<span class="v" style="font-size:18px; font-weight:800; color:#0ea5e9;">$${cost}</span>
</div>
`;
return;
}
const from = (fromEl.value||'').trim(); const from = (fromEl.value||'').trim();
const to = (toEl.value||'').trim(); const to = (toEl.value||'').trim();
if(!from || !to){ priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择起点与终点</p></div>'; return; } if(!from || !to){ priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择起点与终点</p></div>'; return; }
@@ -679,29 +645,18 @@
// Create Order with auto-preview // Create Order with auto-preview
async function createOrder(){ async function createOrder(){
const ticketClass = document.querySelector('input[name="ticketClass"]:checked').value;
const type = document.querySelector('input[name="trainType"]:checked').value; const type = document.querySelector('input[name="trainType"]:checked').value;
const trips = normalizeTrips(); const trips = normalizeTrips();
const ride_date = new Date().toISOString().slice(0, 10); const ride_date = new Date().toISOString().slice(0, 10);
let payload = null; let payload = null;
if (ticketClass === 'single') { const from = (fromEl.value||'').trim();
const lineName = document.getElementById('singleSegmentSelection').value; const to = (toEl.value||'').trim();
const priceStr = document.getElementById('singleSegmentPrice').value; if(!from || !to){ alert('请完整填写信息'); return; }
if (!lineName || !priceStr) {
alert('请选择单程区间');
return;
}
payload = { start: lineName, terminal: 'SINGLE_SEGMENT', train_type: 'single', price: Number(priceStr), trips, ride_date };
} else {
const from = (fromEl.value||'').trim();
const to = (toEl.value||'').trim();
if(!from || !to){ alert('请完整填写信息'); return; }
// Auto-fetch price before creating // Auto-fetch price before creating
await previewPrice(true); await previewPrice(true);
payload = { start: from, terminal: to, train_type: type, trips, ride_date }; payload = { start: from, terminal: to, train_type: type, trips, ride_date };
}
try{ try{
const res = await fetch('/api/public/orders', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) }); const res = await fetch('/api/public/orders', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) });
@@ -740,89 +695,8 @@
document.querySelectorAll('input[name="trainType"]').forEach(el => { document.querySelectorAll('input[name="trainType"]').forEach(el => {
el.onchange = () => { if(fromEl.value && toEl.value) previewPrice(); }; el.onchange = () => { if(fromEl.value && toEl.value) previewPrice(); };
}); });
document.querySelectorAll('input[name="ticketClass"]').forEach(r => r.addEventListener('change', toggleTicketClass));
if (tripsEl) { if (tripsEl) {
tripsEl.oninput = () => { if(fromEl.value && toEl.value) previewPrice(); }; tripsEl.oninput = () => { if(fromEl.value && toEl.value) previewPrice(); };
tripsEl.onchange = () => { normalizeTrips(); if(fromEl.value && toEl.value) previewPrice(); }; tripsEl.onchange = () => { normalizeTrips(); if(fromEl.value && toEl.value) previewPrice(); };
} }
function toggleTicketClass() {
const isSingle = document.querySelector('input[name="ticketClass"]:checked').value === 'single';
if (isSingle) {
document.getElementById('normalTicketView').style.display = 'none';
document.getElementById('singleTicketView').style.display = 'block';
document.getElementById('tripsBlock').style.display = 'none';
if (tripsEl) tripsEl.value = 1;
renderSingleSegments();
} else {
document.getElementById('normalTicketView').style.display = 'block';
document.getElementById('singleTicketView').style.display = 'none';
document.getElementById('tripsBlock').style.display = 'block';
}
previewPrice(true);
}
async function renderSingleSegments() {
const listEl = document.getElementById('singleSegmentList');
if (listEl.innerHTML.includes('jr-result-row')) return; // Already rendered
listEl.innerHTML = '<div class="loading">加载区间数据...</div>';
try {
const [lines, fares] = await Promise.all([
fetch('/api/public/lines').then(r=>r.json()),
fetch('/api/public/fares').then(r=>r.json())
]);
const type = document.querySelector('input[name="trainType"]:checked').value;
const segments = [];
lines.forEach(line => {
const stops = Array.isArray(line.stops) ? line.stops : (Array.isArray(line.站点列表) ? line.站点列表 : []);
const lineFares = new Set();
for (let i = 0; i < stops.length - 1; i++) {
const a = stops[i], b = stops[i+1];
const f = fares.find(x => (x.from === a && x.to === b) || (x.from === b && x.to === a));
if (f) {
const cost = type === 'Express' ? (f.cost_express ?? f.cost ?? 0) : (f.cost_regular ?? f.cost ?? 0);
lineFares.add(Number(cost));
}
}
lineFares.forEach(cost => {
segments.push({
lineName: line.name || line.线路名称 || '未知线路',
color: line.color || line.颜色 || '#93a2b7',
cost
});
});
});
if (segments.length === 0) {
listEl.innerHTML = '<div class="empty-state jr-empty-state"><p>暂无单程区间数据</p></div>';
return;
}
let html = '';
segments.forEach((seg, idx) => {
html += `
<label class="list-item jr-result-row" style="cursor: pointer; display: flex; align-items: center; gap: 12px; padding: 12px;">
<input type="radio" name="singleSegmentOption" value="${idx}" onchange="selectSingleSegment('${seg.lineName}', ${seg.cost})" style="margin: 0;">
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${seg.color};"></div>
<div style="flex: 1;">
<div style="font-weight: bold; color: #1e293b;">${seg.lineName}</div>
<div style="font-size: 12px; color: #64748b;">单区间票</div>
</div>
<div style="font-weight: bold; color: #0ea5e9;">$${seg.cost}</div>
</label>
`;
});
listEl.innerHTML = html;
} catch (e) {
listEl.innerHTML = '<div class="error">加载区间数据失败</div>';
}
}
window.selectSingleSegment = (lineName, cost) => {
document.getElementById('singleSegmentSelection').value = lineName;
document.getElementById('singleSegmentPrice').value = cost;
previewPrice(true);
};
})(); })();