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
+35 -13
View File
@@ -2,7 +2,7 @@ local DEFAULT_SERVER_BASE = "http://ticket.fse-media.group"
local DEFAULT_SERVER_PATH = "/api/tickets/check"
local GATE_OPEN_SECONDS = 2
local VERSION = "v1.5.10"
local VERSION_CHECK_INTERVAL = 5
local VERSION_CHECK_INTERVAL = 60
local CONFIG_PATH = "gate_config.json"
@@ -92,6 +92,30 @@ local serverConnected = nil
local serverLastChangeTs = 0
local expectedGateVersion = 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)
if serverConnected == ok then return end
@@ -997,7 +1021,7 @@ local function currentDeviceId()
if label and label ~= "" then
return label
end
return "闸机#" .. tostring(os.getComputerID())
return "#" .. tostring(os.getComputerID())
end
local function syncICCardState(cardId, payload)
@@ -1353,6 +1377,7 @@ local versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
while true do
local ev = pack(os.pullEvent())
if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then
markGateBusy(GATE_OPEN_SECONDS + 2)
processInspectionEvent(ev[1], ev)
os.sleep(0.35)
drawReadyScreen()
@@ -1361,19 +1386,16 @@ while true do
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
end)
if versionMismatch == true then
pcall(function()
clear()
local w, h = termDev.getSize()
centerText(math.floor(h / 2), "Auto Updating...", colors.yellow)
end)
pcall(function()
if shell and type(shell.run) == "function" then
shell.run("update_gate.lua")
else
os.run(getfenv and getfenv() or _ENV, "update_gate.lua")
pendingGateUpdate = true
elseif versionMismatch == false then
pendingGateUpdate = false
end
end)
if pendingGateUpdate and isGateIdleForUpdate() then
local ok, updated = pcall(runSilentGateUpdate)
if ok and updated then
os.reboot()
return
end
end
drawReadyScreen()
versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ticketmachine",
"version": "1.0.17",
"version": "1.0.18",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
+20 -9
View File
@@ -1,6 +1,7 @@
local CURRENT_STATION_CODE = 'Ticket-Machine'
local API_BASE = 'http://ticket.fse-media.group/api'
local VERSION = 'v1.5.12'
local UPDATE_CHECK_INTERVAL = 60
-- ###########################
-- Core HTTP & JSON Utilities
@@ -13,6 +14,8 @@ end
local serverConnected = nil
local versionMismatch = nil
local pendingMachineUpdate = false
local state
local function normalizeVersionTag(v)
local s = tostring(v or ''):gsub('^%s+', ''):gsub('%s+$', '')
@@ -652,19 +655,27 @@ local function clickSound() end
-- ###########################
-- 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()
while true do
sleep(5)
sleep(UPDATE_CHECK_INTERVAL)
pcall(refreshConfigOnce)
if versionMismatch == true then
pcall(function()
if shell and type(shell.run) == "function" then
shell.run("update_machine.lua")
else
os.run(getfenv and getfenv() or _ENV, "update_machine.lua")
pendingMachineUpdate = true
elseif versionMismatch == false then
pendingMachineUpdate = false
end
if pendingMachineUpdate and state and tostring(state.page or 'home') == 'home' then
local ok, updated = pcall(runSilentMachineUpdate)
if ok and updated then
os.reboot()
return
end
end)
-- Removed os.reboot() to prevent interrupting user unexpectedly
end
end
end
@@ -966,7 +977,7 @@ end
-- ###########################
-- Pages & Navigation
-- ###########################
local state = {
state = {
page = 'home',
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,
+22 -11
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 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 mode = binary and "wb" or "w"
@@ -54,25 +61,29 @@ local function httpGet(url)
end
end
if not silent then
term.clear()
term.setCursorPos(1, 1)
print("Ticket Gate Updater")
print("")
print("Downloading gate program...")
log("Ticket Gate Updater")
log("")
end
log("Downloading gate program...")
local ok, code = httpGet(URL_GATE)
if not ok or type(code) ~= "string" or #code == 0 then
print("Download failed: " .. tostring(code or ""))
return
error("Download failed: " .. tostring(code or ""))
end
if not atomicWrite("startup.lua", code, false) then
print("Write failed: startup.lua")
return
error("Write failed: startup.lua")
end
atomicWrite("startup", code, false)
if fs.exists("gate.lua") then atomicWrite("gate.lua", code, false) end
print("")
print("Done.")
print("Reboot the computer to apply the update.")
if not silent then
log("")
log("Done.")
log("Reboot the computer to apply the update.")
end
return true
+22 -11
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 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 mode = binary and "wb" or "w"
@@ -54,25 +61,29 @@ local function httpGet(url)
end
end
if not silent then
term.clear()
term.setCursorPos(1, 1)
print("Ticket Machine Updater")
print("")
print("Downloading ticket machine program...")
log("Ticket Machine Updater")
log("")
end
log("Downloading ticket machine program...")
local ok, code = httpGet(URL_MACHINE)
if not ok or type(code) ~= "string" or #code == 0 then
print("Download failed: " .. tostring(code or ""))
return
error("Download failed: " .. tostring(code or ""))
end
if not atomicWrite("startup.lua", code, false) then
print("Write failed: startup.lua")
return
error("Write failed: startup.lua")
end
atomicWrite("startup", code, false)
if fs.exists("ticketmachine.lua") then atomicWrite("ticketmachine.lua", code, false) end
print("")
print("Done.")
print("Reboot the computer to apply the update.")
if not silent then
log("")
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">
<div class="jr-column-main">
<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 class="jr-section-head">
<div>
@@ -98,21 +75,6 @@
</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-block">
<div class="jr-field-head">
-126
View File
@@ -565,43 +565,9 @@
async function previewPrice(force = false){
const seq = ++previewSeq;
const ticketClass = document.querySelector('input[name="ticketClass"]:checked').value;
const type = document.querySelector('input[name="trainType"]:checked').value;
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 to = (toEl.value||'').trim();
if(!from || !to){ priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择起点与终点</p></div>'; return; }
@@ -679,21 +645,11 @@
// Create Order with auto-preview
async function createOrder(){
const ticketClass = document.querySelector('input[name="ticketClass"]:checked').value;
const type = document.querySelector('input[name="trainType"]:checked').value;
const trips = normalizeTrips();
const ride_date = new Date().toISOString().slice(0, 10);
let payload = null;
if (ticketClass === 'single') {
const lineName = document.getElementById('singleSegmentSelection').value;
const priceStr = document.getElementById('singleSegmentPrice').value;
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; }
@@ -701,7 +657,6 @@
// Auto-fetch price before creating
await previewPrice(true);
payload = { start: from, terminal: to, train_type: type, trips, ride_date };
}
try{
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 => {
el.onchange = () => { if(fromEl.value && toEl.value) previewPrice(); };
});
document.querySelectorAll('input[name="ticketClass"]').forEach(r => r.addEventListener('change', toggleTicketClass));
if (tripsEl) {
tripsEl.oninput = () => { 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);
};
})();