From 9263a2f98a99e7f90ab36608db94be2b443a4c0f Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:02:45 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20debug-cdn-backend-unre?= =?UTF-8?q?achable.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- debug-cdn-backend-unreachable.md | 42 -------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 debug-cdn-backend-unreachable.md diff --git a/debug-cdn-backend-unreachable.md b/debug-cdn-backend-unreachable.md deleted file mode 100644 index 269e1ec..0000000 --- a/debug-cdn-backend-unreachable.md +++ /dev/null @@ -1,42 +0,0 @@ -# Debug Session: cdn-backend-unreachable - -- Status: OPEN -- Started: 2026-06-20 -- Symptom: - - After enabling Alibaba Cloud CDN for `ticket.fse-media.group`, ticket price calculation fails. - - The ticket query page does not display ticket items. -- Constraints: - - No business logic changes before runtime evidence. - - First code change in existing files must be instrumentation only. -- Hypotheses: - 1. CDN origin/path forwarding for API routes is incorrect. - 2. Frontend requests use relative URLs and now point at the CDN/static origin instead of backend. - 3. Backend CORS or host validation blocks requests from `ticket.fse-media.group`. - 4. HTTPS, certificate, or mixed-content policy blocks API requests. -- Next steps: - 1. Locate the ticket-order and ticket-query frontend request code. - 2. Identify backend API base URL configuration and deployment assumptions. - 3. Reproduce with browser/network evidence and add instrumentation only if needed. -- Runtime evidence: - 1. Live check on `https://ticket.fse-media.group/order` shows `GET /api/public/lines`, `GET /api/public/stations`, and `GET /api/public/config` all return `200`. - 2. Live check on `https://ticket.fse-media.group/search` shows `GET /api/public/tickets?q=` and `GET /api/public/popular` succeed, and the ticket list renders 23 rows. - 3. Direct fetch on the live site confirms `/api/public/fares/query?from=HC-01&to=HC-02` returns valid fare data. - 4. Frontend code uses same-origin relative API paths, so CDN did not break API origin resolution itself. - 5. Live responses for `/ticket-order.js`, `/ticket-search.js`, and `/public-status.js` return `Cache-Control: max-age=43200`, while the affected HTML pages reference them without version query parameters. -- Hypothesis result: - 1. CDN origin/path forwarding for API routes is incorrect: falsified by live `200` responses. - 2. Frontend requests use relative URLs and now point at the wrong origin: falsified by successful same-origin API responses. - 3. Backend CORS or host validation blocks CDN domain: falsified by successful browser fetches from the production page. - 4. HTTPS/certificate/mixed-content issue on API requests: not supported by runtime API evidence. -- Root cause direction: - 1. The codebase contained many hardcoded `http://ticket.fse-media.group/...` links in public pages and JS. - 2. In a CDN + HTTPS deployment, these hardcoded HTTP jumps can split users onto a different protocol path and create intermittent failures or stale-cache behavior. - 3. The affected public pages loaded critical JS assets without version parameters, while CDN/browser caching allowed 12-hour reuse of older scripts. - 4. The booking page had an independent selection-state bug: after both endpoints were selected, clicking a new station only replaced the destination and never restarted the origin/destination flow, which made fare/path results look "stuck". -- Fix applied: - 1. Replaced all hardcoded `http://ticket.fse-media.group` links in `web/*.html` and `web/*.js` with `https://ticket.fse-media.group`. - 2. This keeps all public navigation, ticket detail, and token detail links on the same HTTPS/CDN path as the working API requests. - 3. Added explicit version query parameters to the critical scripts in `ticket-order.html` and `ticket-search.html` so the CDN fetches a fresh asset URL after deployment. - 4. Updated `ticket-order.js` so that clicking a new station after a full start/end selection restarts the selection flow and clears stale route highlights. -- Pending verification: - 1. User to verify ticket price calculation and ticket search list under the CDN domain after redeploy/cache refresh. From d731b482cb11d52efc9b6a906e6a7d4e9def1793 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:02:59 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20fix4.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fix4.js | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 fix4.js diff --git a/fix4.js b/fix4.js deleted file mode 100644 index b6125d3..0000000 --- a/fix4.js +++ /dev/null @@ -1,39 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const dir = path.join(__dirname, 'web'); -const files = fs.readdirSync(dir).filter(f => f.endsWith('.html')); - -for (const file of files) { - let p = path.join(dir, file); - let c = fs.readFileSync(p, 'utf8'); - - // Fix missing closing quotes for placeholders - c = c.replace(/placeholder="中文名 style/g, 'placeholder="中文名" style'); - c = c.replace(/placeholder="英文名 style/g, 'placeholder="英文名" style'); - - // Fix missing < in closing tags - c = c.replace(/中文名\/label>/g, '中文名'); - c = c.replace(/英文名\/label>/g, '英文名'); - c = c.replace(/特急票价\/label>/g, '特急票价'); - c = c.replace(/可换乘到的站点\/label>/g, '可换乘到的站点'); - c = c.replace(/可换乘\/span>/g, '可换乘'); - c = c.replace(/仪表盘\/div>/g, '仪表盘'); - c = c.replace(/今日售票额\/div>/g, '今日售票额'); - c = c.replace(/站点数\/div>/g, '站点数'); - c = c.replace(/线路与票价\/div>/g, '线路与票价'); - c = c.replace(/控制台\/div>/g, '控制台'); - c = c.replace(/控制台\/title>/g, '控制台'); - c = c.replace(/登录\/title>/g, '登录'); - c = c.replace(/登录\/div>/g, '登录'); - c = c.replace(/加载中\.\.\.\/div>/g, '加载中...'); - c = c.replace(/凭证不存在\/h2>/g, '凭证不存在'); - c = c.replace(/凭证码\/th>/g, '凭证码'); - c = c.replace(/状态\/th>/g, '状态'); - c = c.replace(/文件名\/th>/g, '文件名'); - c = c.replace(/文件夹\/th>/g, '文件夹'); // In case it was replaced to this - c = c.replace(/生成凭证码\/button>/g, '生成凭证码'); - c = c.replace(/复制凭证码\/button>/g, '复制凭证码'); - - fs.writeFileSync(p, c, 'utf8'); -} -console.log('Done'); From 2f425f4eb43776afe686c3d27f73f1d141aa8850 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:03:27 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20update=5Frefillmachine?= =?UTF-8?q?.lua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- update_refillmachine.lua | 81 ---------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 update_refillmachine.lua diff --git a/update_refillmachine.lua b/update_refillmachine.lua deleted file mode 100644 index ff9cf20..0000000 --- a/update_refillmachine.lua +++ /dev/null @@ -1,81 +0,0 @@ -local URL_MACHINE_HTTPS = "https://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" -local URL_MACHINE_HTTP = "http://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function atomicWrite(path, content, binary) - local tmp = path .. ".new" - if fs.exists(tmp) then fs.delete(tmp) end - if not writeFile(tmp, content, binary) then return false end - if fs.exists(path) then fs.delete(path) end - fs.move(tmp, path) - return true -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local errMsg = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - errMsg = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(errMsg or "http_failure") - end - end -end - -term.clear() -term.setCursorPos(1, 1) -print("Refill Machine Updater") -print("") -print("Downloading refill machine program...") - -local ok, code = httpGet(URL_MACHINE_HTTPS) -if not ok then - ok, code = httpGet(URL_MACHINE_HTTP) -end -if not ok or type(code) ~= "string" or #code == 0 then - print("Download failed: " .. tostring(code or "")) - return -end - -if not atomicWrite("startup.lua", code, false) then - print("Write failed: startup.lua") - return -end -atomicWrite("startup", code, false) -atomicWrite("refillmachine.lua", code, false) - -print("") -print("Done.") -print("Reboot the computer to apply the update.") From 6a92d07124d6095553d37bddf31d470a237be2a9 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:04:04 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20FSE-ticket.png?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FSE-ticket.png | Bin 66474 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 FSE-ticket.png diff --git a/FSE-ticket.png b/FSE-ticket.png deleted file mode 100644 index 0d51a0914206a20ff1df6b62dc749ff427852125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66474 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�WZRs&c@$9iO>Ad^E{=2 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 From b26e9a65f713069f210febc608d3a6ab7e346da7 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:04:15 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20install=5Frefillmachin?= =?UTF-8?q?e.lua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install_refillmachine.lua | 81 --------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 install_refillmachine.lua diff --git a/install_refillmachine.lua b/install_refillmachine.lua deleted file mode 100644 index f7d909f..0000000 --- a/install_refillmachine.lua +++ /dev/null @@ -1,81 +0,0 @@ -local URL_MACHINE_HTTPS = "https://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" -local URL_MACHINE_HTTP = "http://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function atomicWrite(path, content, binary) - local tmp = path .. ".new" - if fs.exists(tmp) then fs.delete(tmp) end - if not writeFile(tmp, content, binary) then return false end - if fs.exists(path) then fs.delete(path) end - fs.move(tmp, path) - return true -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local errMsg = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - errMsg = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(errMsg or "http_failure") - end - end -end - -term.clear() -term.setCursorPos(1, 1) -print("Refill Machine Installer") -print("") -print("Downloading refill machine program...") - -local ok, code = httpGet(URL_MACHINE_HTTPS) -if not ok then - ok, code = httpGet(URL_MACHINE_HTTP) -end -if not ok or type(code) ~= "string" or #code == 0 then - print("Download failed: " .. tostring(code or "")) - return -end - -if not atomicWrite("startup", code, false) then - print("Write failed: startup") - return -end -atomicWrite("startup.lua", code, false) -atomicWrite("refillmachine.lua", code, false) - -print("") -print("Done.") -print("Reboot the computer to start the refill machine.") From 8a8ebd0df40bb70ff4a100795085e963a246639e Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:04:20 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20install=5Fticketmachin?= =?UTF-8?q?e.lua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install_ticketmachine.lua | 81 --------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 install_ticketmachine.lua diff --git a/install_ticketmachine.lua b/install_ticketmachine.lua deleted file mode 100644 index 361f019..0000000 --- a/install_ticketmachine.lua +++ /dev/null @@ -1,81 +0,0 @@ -local URL_MACHINE_HTTPS = "https://cloud.fse-media.group/d/API/TicketMachine/ticketmachine.lua?sign=UIiheDcpyzwKdovDZM3G-8IRZqApFgU2Kpnhe9k5ETQ=:0" -local URL_MACHINE_HTTP = "http://cloud.fse-media.group/d/API/TicketMachine/ticketmachine.lua?sign=UIiheDcpyzwKdovDZM3G-8IRZqApFgU2Kpnhe9k5ETQ=:0" - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function atomicWrite(path, content, binary) - local tmp = path .. ".new" - if fs.exists(tmp) then fs.delete(tmp) end - if not writeFile(tmp, content, binary) then return false end - if fs.exists(path) then fs.delete(path) end - fs.move(tmp, path) - return true -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local err = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - err = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(err or "http_failure") - end - end -end - -term.clear() -term.setCursorPos(1, 1) -print("Ticket Machine Installer") -print("") -print("Downloading ticket machine program...") - -local ok, code = httpGet(URL_MACHINE_HTTPS) -if not ok then - ok, code = httpGet(URL_MACHINE_HTTP) -end -if not ok or type(code) ~= "string" or #code == 0 then - print("Download failed: " .. tostring(code or "")) - return -end - -if not atomicWrite("startup", code, false) then - print("Write failed: startup") - return -end -atomicWrite("startup.lua", code, false) -if fs.exists("ticketmachine.lua") then atomicWrite("ticketmachine.lua", code, false) end - -print("") -print("Done.") -print("Reboot the computer to start the ticket machine.") From 585e49823534d13bca0e5d7fd9c67e600bb9a6f2 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:04:26 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20installer=5Fbi.lua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- installer_bi.lua | 164 ----------------------------------------------- 1 file changed, 164 deletions(-) delete mode 100644 installer_bi.lua diff --git a/installer_bi.lua b/installer_bi.lua deleted file mode 100644 index 5c44dfc..0000000 --- a/installer_bi.lua +++ /dev/null @@ -1,164 +0,0 @@ -local URL_ERROR = "http://cloud.fse-media.group/d/API/TicketMachine/error.dfpwm?sign=DvQ39wQDN6Cej4KPhAkM3JyXPM466koan6w9CckPxWU=:0" -local URL_PASS = "http://cloud.fse-media.group/d/API/TicketMachine/pass.dfpwm?sign=ZJfGDYnaxQU4JrGSh4NDzKZtjq3eMjYh9KHmMSAMKZA=:0" -local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0" - -local CONFIG_PATH = "gate_config.json" - -local function trim(s) - return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) -end - -local function splitCsv(s) - local out = {} - s = trim(s) - if #s == 0 then return out end - for part in s:gmatch("[^,/%s]+") do - local v = trim(part) - if #v > 0 then table.insert(out, v) end - end - return out -end - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function prompt(label) - term.write(label) - return trim(read() or "") -end - -local function promptOptionalUrl(label) - local raw = prompt(label) - raw = trim(raw) - if #raw == 0 then return nil end - return raw -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local err = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - err = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(err or "http_failure") - end - end -end - -local function download(url, path, binary) - print("Downloading: " .. path) - local ok, data = httpGet(url) - if not ok then - print("Download failed: " .. tostring(data or "")) - return false - end - if not writeFile(path, data, binary) then - print("Write failed: " .. path) - return false - end - return true -end - -local function normalizeMode(raw) - return (trim(raw):lower() == "exit") and "exit" or "entry" -end - -term.clear() -term.setCursorPos(1, 1) -print("Bidirectional Ticket Gate Installer") -print("") - -local stationsRaw = prompt("Station codes (comma or slash): ") -local stationCodes = splitCsv(stationsRaw) -if #stationCodes == 0 then - print("No station codes provided.") - return -end - -print("") -print("Set mode for each side (front/back).") -local frontRaw = prompt("Front mode (entry/exit): ") -local backRaw = prompt("Back mode (entry/exit): ") -local frontMode = normalizeMode(frontRaw) -local backMode = normalizeMode(backRaw) -local frontStationCode = trim(prompt("Front station code (default first code): ")) -local backStationCode = trim(prompt("Back station code (default first code): ")) -if #frontStationCode == 0 then frontStationCode = stationCodes[1] end -if #backStationCode == 0 then backStationCode = stationCodes[1] end - -print("") -print("Optional server config for ticket / IC card checks.") -local ticketServerUrl = promptOptionalUrl("Ticket check URL (blank=auto): ") -local cardServerUrl = promptOptionalUrl("IC card check URL (blank=same server): ") - -local cfg = { - station_codes = stationCodes, - station_code = stationCodes[1], - side_modes = { - front = frontMode, - back = backMode, - }, - side_station_codes = { - front = frontStationCode, - back = backStationCode, - } -} -if ticketServerUrl then cfg.server_url = ticketServerUrl end -if cardServerUrl then cfg.card_server_url = cardServerUrl end -local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg) -if not okCfg then - print("Config serialize failed.") - return -end -if not writeFile(CONFIG_PATH, cfgJson, false) then - print("Failed to write config.") - return -end - -if not download(URL_PASS, "pass.dfpwm", true) then return end -if not download(URL_ERROR, "error.dfpwm", true) then return end - -local okGate, gateCode = httpGet(URL_GATE) -if not okGate then - print("Download failed: startup") - return -end - -writeFile("startup.lua", gateCode, false) -writeFile("startup", gateCode, false) - -print("") -print("Done.") -print("This gate now supports tickets and IC cards.") -print("Attach ticket_inspection_machine on FRONT and BACK.") -print("Reboot the computer to start the gate.") From d35ae5e75bae0085e2f061be7283433d30ae0528 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:04:32 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20gate.lua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gate.lua | 1288 ------------------------------------------------------ 1 file changed, 1288 deletions(-) delete mode 100644 gate.lua diff --git a/gate.lua b/gate.lua deleted file mode 100644 index 20d2b45..0000000 --- a/gate.lua +++ /dev/null @@ -1,1288 +0,0 @@ -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.7" - -local CONFIG_PATH = "gate_config.json" - -local function readFile(path) - if not fs.exists(path) then return nil end - local f = fs.open(path, "r") - if not f then return nil end - local c = f.readAll() - f.close() - return c -end - -local function writeFile(path, content) - local f = fs.open(path, "w") - if not f then return false end - f.write(content) - f.close() - return true -end - -local function trim(s) - return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) -end - -local function splitCsv(s) - local out = {} - s = trim(s) - if #s == 0 then return out end - for part in s:gmatch("[^,/%s]+") do - local v = trim(part) - if #v > 0 then table.insert(out, v) end - end - return out -end - -local pack = table.pack or function(...) - return { n = select("#", ...), ... } -end - -local function loadConfig() - local def = { mode = "entry", station_codes = {} } - local raw = readFile(CONFIG_PATH) - if not raw or #raw == 0 then return def end - local ok, data = pcall(textutils.unserializeJSON, raw) - if not ok or type(data) ~= "table" then return def end - if type(data.mode) == "string" then def.mode = data.mode end - if type(data.station_codes) == "table" then def.station_codes = data.station_codes end - if type(data.side_modes) == "table" then def.side_modes = data.side_modes end - if type(data.server_url) == "string" then def.server_url = data.server_url end - if type(data.card_server_url) == "string" then def.card_server_url = data.card_server_url end - if type(data.station_code) == "string" then def.station_code = data.station_code end - if type(data.side_station_codes) == "table" then def.side_station_codes = data.side_station_codes end - return def -end - -local function stationSetFromList(list) - local set = {} - if type(list) ~= "table" then return set end - for _, v in ipairs(list) do - local c = trim(v) - if #c > 0 then - local parts = splitCsv(c) - if #parts == 0 then - set[c] = true - else - for _, p in ipairs(parts) do set[p] = true end - end - end - end - return set -end - -local monitor = peripheral.find("monitor") -local speaker = peripheral.find("speaker") -local inspection = peripheral.find("ticket_inspection_machine") - -local serverConnected = nil -local serverLastChangeTs = 0 - -local function setServerConnected(ok) - if serverConnected == ok then return end - serverConnected = ok - serverLastChangeTs = os.epoch("utc") -end - -local termDev = term -if monitor then - pcall(monitor.setTextScale, 0.5) - termDev = monitor -end - -local function clear() - termDev.setBackgroundColor(colors.black) - termDev.setTextColor(colors.white) - termDev.clear() - termDev.setCursorPos(1, 1) -end - -local function centerText(y, text, color) - local w = termDev.getSize() - termDev.setTextColor(color or colors.white) - local x = math.max(1, math.floor((w - #text) / 2) + 1) - termDev.setCursorPos(x, y) - termDev.write(text) -end - -local function drawServerStatusIndicator(w) - if w < 2 then return end - local col = colors.yellow - if serverConnected == true then col = colors.lime - elseif serverConnected == false then col = colors.red end - termDev.setBackgroundColor(colors.black) - termDev.setTextColor(col) - termDev.setCursorPos(w - 1, 1) - termDev.write("S") - termDev.setCursorPos(w, 1) - termDev.write("*") - termDev.setTextColor(colors.white) -end - -local function drawVersionIndicator(w) - local s = tostring(VERSION or "") - if #s == 0 then return end - if w < #s then return end - termDev.setBackgroundColor(colors.black) - termDev.setTextColor(colors.gray) - termDev.setCursorPos(1, 1) - termDev.write(s) - termDev.setTextColor(colors.white) -end - -local function draw(statusLine1, statusLine2, statusColor) - clear() - local w, h = termDev.getSize() - centerText(1, "GATE", colors.cyan) - drawVersionIndicator(w) - drawServerStatusIndicator(w) - if statusLine1 and #statusLine1 > 0 then - centerText(math.max(2, math.floor(h / 2)), statusLine1, statusColor or colors.white) - end - if statusLine2 and #statusLine2 > 0 then - centerText(math.min(h, math.max(3, math.floor(h / 2) + 1)), statusLine2, statusColor or colors.white) - end - termDev.setCursorPos(1, h) - termDev.setTextColor(colors.gray) - termDev.write(string.rep(" ", w)) -end - -local function pulseLeftRedstone(seconds) - seconds = tonumber(seconds) or 1 - if not redstone or type(redstone.setOutput) ~= "function" then return end - pcall(redstone.setOutput, "left", true) - os.sleep(seconds) - pcall(redstone.setOutput, "left", false) -end - -pcall(function() - if redstone and type(redstone.setOutput) == "function" then - redstone.setOutput("left", false) - end -end) - -local function readApiEndpointFile(path) - local s = trim(readFile(path) or "") - if #s == 0 then return nil end - return s -end - -local function resolveServerURL(cfg) - if type(cfg.server_url) == "string" and #trim(cfg.server_url) > 0 then - local u = trim(cfg.server_url) - u = u:gsub("/api/tickets/status%s*$", "/api/tickets/check") - return u - end - - local base = readApiEndpointFile("API_ENDPOINT_GATE.txt") or readApiEndpointFile("API_ENDPOINT.txt") - if base and base:match("/api$") then - base = base:sub(1, -5) - end - if base and #base > 0 then - return base .. DEFAULT_SERVER_PATH - end - return DEFAULT_SERVER_BASE .. DEFAULT_SERVER_PATH -end - -local function guessBaseFromStatusURL(url) - url = trim(url or "") - if #url == 0 then return DEFAULT_SERVER_BASE end - local b = url:gsub("/api/tickets/check.*$", "") - b = b:gsub("/api/.*$", "") - b = trim(b) - if #b == 0 then return DEFAULT_SERVER_BASE end - return b -end - -local function httpRequest(method, url, body, headers) - if not http then - setServerConnected(false) - return false, "HTTP API disabled" - end - headers = headers or {} - local okReq, err = pcall(function() - http.request({ - url = url, - method = method, - headers = headers, - body = body, - }) - end) - if not okReq then - setServerConnected(false) - return false, tostring(err) - end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - setServerConnected(true) - return true, data - end - setServerConnected(false) - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local err = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - err = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - setServerConnected(false) - return false, data - end - setServerConnected(false) - return false, tostring(err or "http_failure") - end - os.queueEvent(ev, p1, p2, p3) - os.sleep(0) - end -end - -local function postCheck(url, payload) - local okBody, body = pcall(textutils.serializeJSON, payload) - if not okBody then return false end - local ok, data = httpRequest("POST", url, body, { ["Content-Type"] = "application/json" }) - if not ok then return false, data end - local okJ, parsed = pcall(textutils.unserializeJSON, data or "") - if not okJ then return false, data end - return true, parsed -end - -local function getJSON(url) - local ok, data = httpRequest("GET", url) - if not ok then return false, data end - local okJ, parsed = pcall(textutils.unserializeJSON, data or "") - if not okJ then return false, data end - return true, parsed -end - -local function resolveCardServerURL(cfg, ticketCheckURL) - if type(cfg.card_server_url) == "string" and #trim(cfg.card_server_url) > 0 then - return trim(cfg.card_server_url) - end - local base = guessBaseFromStatusURL(ticketCheckURL) - return base:gsub("/+$", "") .. "/api/cards/check" -end - -local function resolveCardSyncBaseURL(cfg, ticketCheckURL, cardCheckURL) - if type(cfg.card_sync_url) == "string" and #trim(cfg.card_sync_url) > 0 then - local raw = trim(cfg.card_sync_url) - return raw:gsub("/+$", "") - end - local base = guessBaseFromStatusURL(cardCheckURL or ticketCheckURL) - return base:gsub("/+$", "") .. "/api/ic-cards" -end - -local function resolveFareQueryURL(ticketCheckURL) - local base = guessBaseFromStatusURL(ticketCheckURL) - return base:gsub("/+$", "") .. "/api/public/fares/query" -end - -local function urlEncodeComponent(value) - local s = tostring(value or "") - return (s:gsub("([^%w%-_%.~])", function(c) - return string.format("%%%02X", string.byte(c)) - end)) -end - -local function toMoney(v) - local n = tonumber(v) - if n == nil then return nil end - return math.floor(n * 100 + 0.5) / 100 -end - -local stationNameToCode = {} - -local function normKey(s) - return trim(s):lower() -end - -local function refreshStationNameMap(serverBase) - serverBase = trim(serverBase or "") - if #serverBase == 0 then return false end - local url = serverBase:gsub("/+$", "") .. "/api/stations" - local ok, data = httpRequest("GET", url) - if not ok then return false end - local okJ, parsed = pcall(textutils.unserializeJSON, data or "") - if not okJ then return false end - if type(parsed) == "table" and type(parsed.stations) == "table" then - parsed = parsed.stations - end - if type(parsed) ~= "table" then return false end - - stationNameToCode = {} - for _, st in ipairs(parsed) do - if type(st) == "table" then - local code = trim(st.code) - if #code > 0 then - local en = trim(st.en_name or st.en) - if #en > 0 then stationNameToCode[normKey(en)] = code end - local cn = trim(st.name) - if #cn > 0 then stationNameToCode[normKey(cn)] = code end - end - end - end - return true -end - -local function inferStationCodeFromName(name) - local key = normKey(name or "") - if #key == 0 then return "" end - return stationNameToCode[key] or "" -end - -local function playDfpwm(path) - if not speaker then return end - if not fs.exists(path) then return end - local okD, dfpwm = pcall(require, "cc.audio.dfpwm") - if not okD or not dfpwm then return end - local h = fs.open(path, "rb") - if not h then return end - local decoder = dfpwm.make_decoder() - while true do - local chunk = h.read(16 * 1024) - if not chunk then break end - local buf = decoder(chunk) - while not speaker.playAudio(buf) do - os.pullEvent("speaker_audio_empty") - end - end - h.close() -end - -local function normalizeTicketId(v) - v = tostring(v or "") - v = v:gsub("^%s+", ""):gsub("%s+$", "") - v = v:gsub("%s+", "") - if #v == 0 then return nil end - local prefix, num = v:match("^([A-Za-z][A-Za-z])%-?([0-9]+)$") - if prefix and num then - prefix = prefix:upper() - if #num < 8 then - num = string.rep("0", 8 - #num) .. num - elseif #num > 8 then - num = num:sub(-8) - end - return prefix .. "-" .. num - end - return v:lower() -end - -local function normalizeIcCardId(v) - local s = tostring(v or "") - s = s:gsub("%s+", ""):upper() - if #s == 0 then return "" end - local num = s:match("^IC%-?([0-9]+)$") - if num then - if #num < 6 then - num = string.rep("0", 6 - #num) .. num - elseif #num > 6 then - num = num:sub(-6) - end - return "IC-" .. num - end - return s -end - -local function collectScanTables(scan, includeTicket) - local out = {} - local seen = {} - local function add(v) - if type(v) ~= "table" or seen[v] then return end - seen[v] = true - table.insert(out, v) - end - if type(scan) ~= "table" then return out end - add(scan) - add(scan.data) - add(scan.payload) - add(scan.card) - add(scan.ic_card) - add(scan.wallet) - add(scan.card_data) - add(scan.media_data) - if includeTicket then - add(scan.ticket) - add(scan.ticket_data) - end - return out -end - -local function firstNonEmptyFromTables(tables, keys) - if type(tables) ~= "table" or type(keys) ~= "table" then return "" end - for _, t in ipairs(tables) do - for _, key in ipairs(keys) do - local v = t[key] - if v ~= nil then - local s = trim(v) - if #s > 0 then return s end - end - end - end - return "" -end - -local function firstNumberFromTables(tables, keys) - if type(tables) ~= "table" or type(keys) ~= "table" then return nil end - for _, t in ipairs(tables) do - for _, key in ipairs(keys) do - local n = tonumber(t[key]) - if n ~= nil then return n end - end - end - return nil -end - -local function isTruthy(v) - if v == true then return true end - if type(v) == "number" then return v ~= 0 end - if type(v) == "string" then - local s = v:lower() - return s == "true" or s == "1" or s == "yes" - end - return false -end - -local function firstTruthyFromTables(tables, keys) - if type(tables) ~= "table" or type(keys) ~= "table" then return false end - for _, t in ipairs(tables) do - for _, key in ipairs(keys) do - if t[key] ~= nil and isTruthy(t[key]) then - return true - end - end - end - return false -end - -local function getTicketId(scan) - local tables = collectScanTables(scan, true) - if #tables == 0 then return nil end - local raw = firstNonEmptyFromTables(tables, { - "ticketId", "ticket_id", "id", "ticketNo", "ticket_no", "code" - }) - if #raw == 0 then return nil end - return normalizeTicketId(raw) -end - - - -local function getCardId(scan) - local tables = collectScanTables(scan, false) - if #tables == 0 then return "" end - local raw = firstNonEmptyFromTables(tables, { - "card_id", - "cardId", - "ic_card_id", - "icCardId", - "wallet_id", - "walletId", - "card_uid", - "cardUid", - "uid", - "uuid", - "serial", - "serial_number", - "serialNumber", - "nfc_uid", - "nfcUid", - "rfid_uid", - "rfidUid" - }) - if #raw == 0 then return "" end - return normalizeIcCardId(raw) -end - -local function getCardBalance(scan) - local tables = collectScanTables(scan, false) - return firstNumberFromTables(tables, { - "balance", - "stored_value", - "storedValue", - "wallet_balance", - "walletBalance", - "remaining_balance", - "remainingBalance", - "value", - "amount" - }) -end - -local function isICCardScan(scan) - local tables = collectScanTables(scan, false) - if #tables == 0 then return false end - if #getCardId(scan) > 0 then return true end - if getCardBalance(scan) ~= nil then return true end - if type(scan.card) == "table" or type(scan.ic_card) == "table" or type(scan.wallet) == "table" then - return true - end - local media = firstNonEmptyFromTables(tables, { - "media", - "media_type", - "mediaType", - "product_type", - "productType", - "ticket_type", - "ticketType", - "kind", - "type", - "category" - }):lower() - if media:find("card", 1, true) or media:find("wallet", 1, true) then return true end - if media:find("ic", 1, true) or media:find("nfc", 1, true) or media:find("rfid", 1, true) then return true end - return false -end - -local function getStartStation(scan) - local tables = collectScanTables(scan, true) - local id = firstNonEmptyFromTables(tables, { - "entry", - "start_station", - "startStation", - "start", - "start_station_id", - "start_station_code", - "from_station", - "from", - "startStationId", - "start_stationId", - "entry_station", - "entryStation" - }) - if #id > 0 then return id end - return inferStationCodeFromName(firstNonEmptyFromTables(tables, { - "start_name_en", "startNameEn", "start_name", "fromNameCnU", "fromNameCn", "entry_name", "entryName" - })) -end - -local function getTerminalStation(scan) - local tables = collectScanTables(scan, true) - local id = firstNonEmptyFromTables(tables, { - "exit", - "terminal_station", - "terminalStation", - "terminal", - "end_station", - "endStation", - "terminal_station_id", - "terminal_station_code", - "to_station", - "to", - "endStationId", - "end_stationId", - "exit_station", - "exitStation" - }) - if #id > 0 then return id end - return inferStationCodeFromName(firstNonEmptyFromTables(tables, { - "terminal_name_en", "terminalNameEn", "terminal_name", "toNameCnU", "toNameCn", "exit_name", "exitName" - })) -end - - -local function saveLastScan(scan) - if type(scan) ~= "table" then return end - local t = {} - for k, v in pairs(scan) do t[k] = v end - local startStation = getStartStation(t) - if #startStation > 0 and (t.start_station == nil or trim(t.start_station) == "") then - t.start_station = startStation - end - local terminalStation = getTerminalStation(t) - if #terminalStation > 0 and (t.terminal_station == nil or trim(t.terminal_station) == "") then - t.terminal_station = terminalStation - end - - local ok, s = pcall(textutils.serializeJSON, t) - if not ok or type(s) ~= "string" then - ok, s = pcall(textutils.serialize, t) - end - if ok and type(s) == "string" then - writeFile("last_scan.json", s) - end -end - -local cfg = loadConfig() -local stationSet = stationSetFromList(cfg.station_codes) -local serverURL = resolveServerURL(cfg) -local cardServerURL = resolveCardServerURL(cfg, serverURL) -local cardSyncBaseURL = resolveCardSyncBaseURL(cfg, serverURL, cardServerURL) -local fareQueryURL = resolveFareQueryURL(serverURL) -local mode = (trim(cfg.mode):lower() == "exit") and "exit" or "entry" - -local modeBySide = nil -if type(cfg.side_modes) == "table" then - local tmp = {} - for side, m in pairs(cfg.side_modes) do - if type(side) == "string" then - local s = trim(side):lower() - if #s > 0 then - tmp[s] = (trim(m):lower() == "exit") and "exit" or "entry" - end - end - end - if next(tmp) ~= nil then modeBySide = tmp end -end - -local sideStationCodeBySide = nil -if type(cfg.side_station_codes) == "table" then - local tmp = {} - for side, code in pairs(cfg.side_station_codes) do - if type(side) == "string" then - local s = trim(side):lower() - local c = trim(code) - if #s > 0 and #c > 0 then - tmp[s] = c - end - end - end - if next(tmp) ~= nil then sideStationCodeBySide = tmp end -end - -pcall(function() - refreshStationNameMap(guessBaseFromStatusURL(serverURL)) -end) - -if not inspection then - if modeBySide == nil then - draw("Missing peripheral:", "ticket_inspection_machine", colors.red) - error("ticket_inspection_machine not found") - end -end - -if next(stationSet) == nil then - draw("No station codes set.", "Run installer first.", colors.red) - error("No station codes configured") -end - -local stationListText = table.concat(cfg.station_codes, ",") -local function readyLine1() - if not modeBySide then - return "Ready (" .. mode:upper() .. ")" - end - local f = modeBySide.front and modeBySide.front:upper() or "-" - local b = modeBySide.back and modeBySide.back:upper() or "-" - return "Ready (BI) F:" .. f .. " B:" .. b -end - -draw(readyLine1(), "Station: " .. stationListText, colors.lime) - -local stationCodesPayload = {} -for k, _ in pairs(stationSet) do table.insert(stationCodesPayload, k) end -table.sort(stationCodesPayload) - -local function trimSide(s) - s = trim(s or ""):lower() - if #s == 0 then return nil end - return s -end - -local function defaultStationCode() - local direct = trim(cfg.station_code or "") - if #direct > 0 then return direct end - return trim(stationCodesPayload[1] or "") -end - -local function stationCodeForSide(side) - side = trimSide(side) - if side and type(sideStationCodeBySide) == "table" then - local v = trim(sideStationCodeBySide[side] or "") - if #v > 0 then return v end - end - return defaultStationCode() -end - -local function isInspectionPeripheral(p) - return type(p) == "table" and ( - type(p.getLastScanned) == "function" - or type(p.updateICCard) == "function" - or type(p.updateTicket) == "function" - or type(p.destroyTicket) == "function" - ) -end - -local function resolveInspection(side) - side = trimSide(side) - if side and peripheral and type(peripheral.wrap) == "function" then - local okW, p = pcall(peripheral.wrap, side) - if okW and isInspectionPeripheral(p) then - return p - end - return nil - end - if isInspectionPeripheral(inspection) then return inspection end - return nil -end - -local function validateBidirectional() - if not modeBySide then return true end - for side, _ in pairs(modeBySide) do - if not resolveInspection(side) then - draw("Missing peripheral:", "ticket_inspection_machine@" .. tostring(side), colors.red) - error("ticket_inspection_machine not found on side: " .. tostring(side)) - end - end - return true -end - -validateBidirectional() - -local function inferSideFromScan(scan) - if type(scan) ~= "table" then return nil end - return trimSide( - scan.side - or scan.source_side - or scan.reader_side - or scan.peripheral_side - or scan.peripheralSide - or scan.device_side - or scan.peripheral - or scan.source - or scan.reader - or scan.name - ) -end - -local function isSideName(s) - s = trimSide(s) - if not s then return false end - return s == "front" or s == "back" or s == "left" or s == "right" or s == "top" or s == "bottom" -end - -local function parseTicketScannedArgsPacked(ev) - local side = nil - local scan = nil - if type(ev) ~= "table" then return nil, nil end - local n = tonumber(ev.n) or #ev - for i = 2, n do - local v = ev[i] - if not scan and type(v) == "table" then - scan = v - elseif not side and type(v) == "string" and isSideName(v) then - side = trimSide(v) - end - end - if scan and not side then side = inferSideFromScan(scan) end - return scan, side -end - -local function actionForSide(side) - if not modeBySide then return mode end - side = trimSide(side) - if not side then return mode end - return modeBySide[side] or mode -end - -local function collectInspectionDevices(side, modeBySideRef) - local sideKnown = trimSide(side) ~= nil - local inspectionDevs = {} - local function addDev(dev) - if not dev then return end - table.insert(inspectionDevs, dev) - end - if sideKnown then - addDev(resolveInspection(side)) - elseif modeBySideRef then - for s, _ in pairs(modeBySideRef) do - addDev(resolveInspection(s)) - end - else - addDev(resolveInspection(side)) - end - return inspectionDevs, sideKnown -end - -local function collectInspectionBindings(side, modeBySideRef, fallbackInspection) - local out = {} - local seen = {} - local function addBinding(sideName, dev) - if not dev or seen[dev] then return end - seen[dev] = true - table.insert(out, { side = trimSide(sideName), dev = dev }) - end - - side = trimSide(side) - if side then - addBinding(side, resolveInspection(side)) - elseif modeBySideRef then - for s, _ in pairs(modeBySideRef) do - addBinding(s, resolveInspection(s)) - end - else - addBinding(nil, fallbackInspection or resolveInspection(nil)) - end - return out -end - -local function updateDeviceField(dev, key, value) - if type(dev) ~= "table" or type(dev.updateTicket) ~= "function" then return end - pcall(dev.updateTicket, key, value) -end - -local function updateICCardField(dev, key, value) - if type(dev) ~= "table" or type(dev.updateICCard) ~= "function" then return false end - local okCall, okRes, detail = pcall(dev.updateICCard, key, value) - if not okCall then return false, tostring(okRes) end - if okRes == false then return false, tostring(detail or "update_failed") end - return true -end - -local function updateICCardFields(dev, patch) - local allOk = true - local firstErr = nil - if type(patch) ~= "table" then return false end - for key, value in pairs(patch) do - local okField, errField = updateICCardField(dev, key, value) - if not okField then - allOk = false - if not firstErr then - firstErr = tostring(key) .. ": " .. tostring(errField or "update_failed") - end - end - end - return allOk, firstErr -end - -local function readLastScanned(dev) - if type(dev) ~= "table" or type(dev.getLastScanned) ~= "function" then return nil end - local ok, scan = pcall(dev.getLastScanned) - if not ok or type(scan) ~= "table" then return nil end - return scan -end - -local function getCardEntry(scan) - return firstNonEmptyFromTables(collectScanTables(scan, false), { - "entry", "entry_station", "entryStation", "start_station", "startStation" - }) -end - -local function getCardEntered(scan) - if #getCardEntry(scan) > 0 then return true end - return firstTruthyFromTables(collectScanTables(scan, false), { - "entered", "is_entered", "in_station", "inside_station" - }) -end - -local function getCardExited(scan) - if #getCardEntry(scan) > 0 then return false end - return firstTruthyFromTables(collectScanTables(scan, false), { - "exited", "is_exited", "out_station", "outside_station" - }) -end - -local function getCardOwnerName(scan) - return firstNonEmptyFromTables(collectScanTables(scan, false), { - "ownerName", "owner_name", "holder_name", "card_holder", "passenger" - }) -end - -local function queryFare(fromStation, toStation) - fromStation = trim(fromStation) - toStation = trim(toStation) - if #fromStation == 0 or #toStation == 0 then - return nil, "missing_station" - end - local url = fareQueryURL - .. "?from=" .. urlEncodeComponent(fromStation) - .. "&to=" .. urlEncodeComponent(toStation) - local ok, resp = getJSON(url) - if not ok or type(resp) ~= "table" then - return nil, "net_error" - end - local fare = tonumber( - resp.discounted_regular_fare - or resp.discounted_regular - or resp.regular_fare - or resp["优惠后常规票价"] - or resp["常规票价"] - or resp.regular - ) - if fare == nil then - return nil, tostring(resp.error or resp["错误"] or "fare_not_found") - end - return toMoney(fare), nil -end - -local function denyCard(reason, detail) - draw("DENIED", tostring(detail or reason or "deny"), colors.red) - playDfpwm("error.dfpwm") -end - -local function deductICCardBalance(dev, amount) - if type(dev) ~= "table" or type(dev.deductICCard) ~= "function" then - return false, "unsupported_method" - end - local okCall, okRes, detail = pcall(dev.deductICCard, amount) - if not okCall then return false, tostring(okRes) end - if okRes == true then return true, tonumber(detail) end - return false, tostring(detail or "deduct_failed") -end - -local function syncICCardState(cardId, payload) - local id = trim(cardId) - if #id == 0 then return false, "missing_card_id" end - local url = cardSyncBaseURL .. "/" .. urlEncodeComponent(id) .. "/sync" - payload = payload or {} - payload.card_id = id - return postCheck(url, payload) -end - -local function handleICCardScan(scan, side, scanDev) - saveLastScan(scan) - local inspectionDevs = scanDev and { scanDev } or select(1, collectInspectionDevices(side, modeBySide, inspection)) - local sideKnown = trimSide(side) ~= nil - local action = actionForSide(side) - if modeBySide and not sideKnown then - if getCardEntered(scan) and not getCardExited(scan) then - action = "exit" - else - action = "entry" - end - end - - local cardId = getCardId(scan) - if #cardId == 0 then - draw("Invalid card.", "Missing card_id.", colors.red) - playDfpwm("error.dfpwm") - return - end - - local balance = toMoney(getCardBalance(scan) or 0) or 0 - local entryStation = trim(getCardEntry(scan)) - local exitStation = stationCodeForSide(side) - local fare = 0 - local usedAction = action - - if #exitStation == 0 then - denyCard("missing_station", "Missing gate station") - return - end - - if usedAction == "entry" then - if getCardEntered(scan) and not getCardExited(scan) then - denyCard("already_entered", "Already entered") - return - end - local okWrite = true - local writeErr = nil - for _, dev in ipairs(inspectionDevs) do - local okPatch, errPatch = updateICCardFields(dev, { - entry = exitStation, - }) - okWrite = okPatch and okWrite - if not okPatch and not writeErr then writeErr = errPatch end - end - if not okWrite then - draw("WRITE ERROR", tostring(writeErr or "Failed to update card"), colors.red) - playDfpwm("error.dfpwm") - return - end - entryStation = exitStation - else - if not getCardEntered(scan) then - denyCard("not_entered", "Not entered") - return - end - if getCardExited(scan) then - denyCard("already_exited", "Already exited") - return - end - if #entryStation == 0 then - denyCard("missing_entry_station", "Missing entry station") - return - end - local fareValue, fareErr = queryFare(entryStation, exitStation) - if fareValue == nil then - if fareErr == "net_error" then - draw("NET ERROR", "Fare lookup failed", colors.red) - playDfpwm("error.dfpwm") - else - denyCard("fare_not_found", fareErr) - end - return - end - fare = fareValue - if balance < fare then - denyCard("insufficient_balance", "Fare: " .. tostring(fare) .. " Bal: " .. tostring(balance)) - return - end - local okWrite = true - local writeErr = nil - for _, dev in ipairs(inspectionDevs) do - local okDeduct, newBalanceOrErr = deductICCardBalance(dev, fare) - if okDeduct then - balance = toMoney(newBalanceOrErr or (balance - fare)) or 0 - local okClear, clearErr = updateICCardField(dev, "entry", nil) - local okFare, fareErr = updateICCardField(dev, "last_fare", fare) - if not okClear or not okFare then - okWrite = false - if not okClear and not writeErr then writeErr = "entry: " .. tostring(clearErr or "update_failed") end - if not okFare and not writeErr then writeErr = "last_fare: " .. tostring(fareErr or "update_failed") end - end - else - okWrite = false - if tostring(newBalanceOrErr) == "insufficient" then - denyCard("insufficient_balance", "-" .. tostring(fare) .. " Left: " .. tostring(balance)) - else - draw("WRITE ERROR", tostring(newBalanceOrErr or "deduct_failed"), colors.red) - playDfpwm("error.dfpwm") - end - break - end - end - if not okWrite then - if writeErr then - draw("WRITE ERROR", tostring(writeErr), colors.red) - playDfpwm("error.dfpwm") - end - return - end - end - - for _, dev in ipairs(inspectionDevs) do - if usedAction == "entry" then - updateDeviceField(dev, "entered", true) - updateDeviceField(dev, "exited", false) - if #entryStation > 0 then updateDeviceField(dev, "entry_station", entryStation) end - else - updateDeviceField(dev, "exited", true) - updateDeviceField(dev, "entered", false) - if #exitStation > 0 then updateDeviceField(dev, "exit_station", exitStation) end - updateDeviceField(dev, "last_fare", fare) - end - if balance ~= nil then updateDeviceField(dev, "balance", balance) end - end - - local syncTs = (os.epoch and os.epoch("utc")) or (os.time() * 1000) - local okSync, syncResp = syncICCardState(cardId, { - type = "check", - action = usedAction, - device = "gate", - ts = syncTs, - station_code = exitStation, - entry_station = entryStation, - exit_station = usedAction == "exit" and exitStation or "", - entered = (usedAction == "entry"), - exited = (usedAction == "exit"), - fare = fare, - last_fare = fare, - balance = balance, - result = "pass" - }) - if okSync and type(syncResp) == "table" and type(syncResp.card) == "table" then - balance = toMoney(syncResp.card.balance or balance) or balance - end - - local line2 = nil - if usedAction == "exit" then - line2 = "-" .. tostring(fare) .. " Left: " .. tostring(balance or "?") - else - line2 = "Left: " .. tostring(balance or "?") - end - draw("PASS", line2, colors.lime) - parallel.waitForAll( - function() pulseLeftRedstone(GATE_OPEN_SECONDS) end, - function() playDfpwm("pass.dfpwm") end - ) -end - -local function handleScan(scan, side, scanDev) - saveLastScan(scan) - local inspectionDevs = scanDev and { scanDev } or select(1, collectInspectionDevices(side, modeBySide, inspection)) - local sideKnown = trimSide(side) ~= nil - local action = actionForSide(side) - if modeBySide and not sideKnown then - if isTruthy(scan and scan.entered) and not isTruthy(scan and scan.exited) then - action = "exit" - else - action = "entry" - end - end - - local ticketId = getTicketId(scan) - if not ticketId then - draw("Invalid ticket.", "Missing ticketId.", colors.red) - playDfpwm("error.dfpwm") - return - end - - local hintTripsTotal = tonumber(scan.trips_total or scan.rides_total or scan.trips or scan.rides) - local hintTripsRemaining = tonumber(scan.trips_remaining or scan.rides_remaining) - - local function doCheck(act) - return postCheck(serverURL, { - ticket_id = ticketId, - action = act, - station_codes = stationCodesPayload, - station_code = stationCodeForSide(side), - device = "gate", - ts = os.epoch("utc"), - trips_total = hintTripsTotal, - trips_remaining = hintTripsRemaining, - }) - end - - local ok, resp = doCheck(action) - local usedAction = action - if ok and type(resp) == "table" and resp.result ~= "pass" and tostring(resp.reason) == "wrong_station" and modeBySide and not sideKnown then - local alt = (action == "entry") and "exit" or "entry" - local ok2, resp2 = doCheck(alt) - if ok2 and type(resp2) == "table" then - if resp2.result == "pass" then - ok, resp = ok2, resp2 - usedAction = alt - elseif tostring(resp2.reason) ~= "wrong_station" then - ok, resp = ok2, resp2 - usedAction = alt - end - end - end - - if not ok or type(resp) ~= "table" then - draw("NET ERROR", "Server check failed.", colors.red) - playDfpwm("error.dfpwm") - return - end - - if resp.result ~= "pass" then - draw("DENIED", tostring(resp.reason or "deny"), colors.red) - playDfpwm("error.dfpwm") - return - end - - pcall(function() - if #inspectionDevs == 0 then return end - local newRides = tonumber(resp.trips_remaining) - or tonumber(scan.trips_remaining or scan.rides_remaining) - or tonumber(scan.rides) - for _, dev in ipairs(inspectionDevs) do - if type(dev) == "table" and type(dev.updateTicket) == "function" then - if usedAction == "entry" then - dev.updateTicket("entered", true) - dev.updateTicket("exited", false) - if newRides ~= nil then dev.updateTicket("rides", newRides) end - else - dev.updateTicket("exited", true) - dev.updateTicket("entered", false) - if newRides ~= nil then dev.updateTicket("rides", newRides) end - end - end - end - end) - - local remaining = tonumber(resp.trips_remaining) - if usedAction == "exit" and isTruthy(resp.destroy_ticket) and remaining ~= nil and remaining <= 0 then - for _, dev in ipairs(inspectionDevs) do - if type(dev) == "table" and type(dev.destroyTicket) == "function" then - pcall(dev.destroyTicket) - end - end - end - - local msg = (usedAction == "exit") - and ("Rides left: " .. tostring(resp.trips_remaining or "")) - or "Welcome." - - draw("PASS", msg, colors.lime) - parallel.waitForAll( - function() pulseLeftRedstone(GATE_OPEN_SECONDS) end, - function() playDfpwm("pass.dfpwm") end - ) -end - -local recentScans = {} - -local function buildScanKey(scan, side, eventName) - side = trimSide(side) or "-" - if eventName == "ic_card_scanned" or isICCardScan(scan) then - return table.concat({ - side, - "ic", - getCardId(scan), - tostring(getCardBalance(scan) or ""), - getCardEntry(scan), - getCardOwnerName(scan), - }, "|") - end - return table.concat({ - side, - "ticket", - tostring(getTicketId(scan) or ""), - tostring(scan.timestamp or scan.order_datetime or ""), - tostring(scan.rides or scan.trips or ""), - tostring(scan.entered or ""), - tostring(scan.exited or ""), - }, "|") -end - -local function shouldProcessScan(scan, side, eventName) - local key = buildScanKey(scan, side, eventName) - local now = os.epoch("utc") - local prev = recentScans[key] - recentScans[key] = now - if prev and (now - prev) < 500 then - return false - end - return true -end - -local function matchesEventType(scan, eventName) - if eventName == "ic_card_scanned" then return isICCardScan(scan) end - if eventName == "ticket_scanned" then return not isICCardScan(scan) end - return true -end - -local function processInspectionEvent(eventName, ev) - local payloadScan, payloadSide = parseTicketScannedArgsPacked(ev) - local bindings = collectInspectionBindings(payloadSide, modeBySide, inspection) - local handled = false - - for _, binding in ipairs(bindings) do - local scan = readLastScanned(binding.dev) - if type(scan) == "table" and matchesEventType(scan, eventName) and shouldProcessScan(scan, binding.side, eventName) then - if eventName == "ic_card_scanned" or isICCardScan(scan) then - handleICCardScan(scan, binding.side, binding.dev) - else - handleScan(scan, binding.side, binding.dev) - end - handled = true - end - end - - if not handled and type(payloadScan) == "table" and shouldProcessScan(payloadScan, payloadSide, eventName) then - local payloadDev = nil - if payloadSide then - payloadDev = resolveInspection(payloadSide) - elseif #bindings == 1 then - payloadDev = bindings[1].dev - elseif not modeBySide then - payloadDev = inspection - end - if eventName == "ic_card_scanned" or isICCardScan(payloadScan) then - handleICCardScan(payloadScan, payloadSide, payloadDev) - else - handleScan(payloadScan, payloadSide, payloadDev) - end - end -end - -while true do - local ev = pack(os.pullEvent()) - if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then - processInspectionEvent(ev[1], ev) - os.sleep(0.35) - draw(readyLine1(), "Station: " .. stationListText, colors.lime) - end -end From db1562b83045284bfdec9e4a3feb829193963943 Mon Sep 17 00:00:00 2001 From: Henry_Du <813367384@qq.com> Date: Sun, 21 Jun 2026 10:04:43 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20IC=E5=82=A8=E8=93=84?= =?UTF-8?q?=E5=8D=A1=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IC储蓄卡功能开发.md | 1492 ------------------------------------------- 1 file changed, 1492 deletions(-) delete mode 100644 IC储蓄卡功能开发.md diff --git a/IC储蓄卡功能开发.md b/IC储蓄卡功能开发.md deleted file mode 100644 index 4ba85d1..0000000 --- a/IC储蓄卡功能开发.md +++ /dev/null @@ -1,1492 +0,0 @@ -# IC储蓄卡功能开发 - -> **Workspace:** d:\TaoYuan2023_HSTG\HSTG\Lua\TicketMachine - ---- - -**User** - -现在我们新增了一个IC储蓄卡的功能。MOD那边的API是这样的: - IC 卡系统 Lua API - - 售票机 ticket_vending_machine 新增 - 方法 | 返回 - issueTicket(...) | true, ticketId - issueICCard() | true, cardId - `local ok, cardId = vm.issueICCard()` - - 充值机 ic_refill_machine - 方法 | 参数 | 返回 | 说明 - getCardInfo() | 无 | table |卡号/持有者/余额 - refill(amount) | 金额 | true, 新余额 | 充值 - deduct(amount) | 金额 | true, 新余额 | 扣费 - - 操作流程:手持卡右键方块 → 插入 → CC 操作 → ejectCard() 取出 - - --- - 所以要加: - 1. `d:\TaoYuan2023_HSTG\HSTG\Lua\TicketMachine\ticketmachine.lua` 添加一个“办卡”的页面。点击按钮后开始办卡。首先输入卡的使用者。输入框下面有26键键盘以及符号键盘。确定之后选择首次充值余额。可以自定义,也可以选择预设:5、10、15、20。然后选择之后就像“购票支付” `d:\TaoYuan2023_HSTG\HSTG\Lua\TicketMachine\ticketmachine.lua#L902-902` 那样付钱充值.完成之后就出卡。把使用者名、余额传入出票机。同时也要向网页端记录该卡。 - - 2. 检票模块: - 卡右键检票机,向卡中传入进站的车站编号。出站时通过识别卡中的进站站台编号和当前出站站台编号,计算路费。计算后扣费后向卡中传入新的余额。 - - 其它“已经进站”/“还未进站”等提示照旧 - - 3.网页控制台端。加上新的记录卡信息的/api 地址。然后后台也能看到卡的所有信息(类似车票)。同时也要在公开的页面显示,加入查询的索引。 - - 4. 线上购卡 - 和线上购票类似,通过线上购卡,然后获得凭证,在computercraft中通过凭证兑卡。 - - 请注意,所有网页的css请按照当前的css风格(模仿JR官网 `https://www.jreast.co.jp/` ) - -**Assistant** - -我先梳理现有代码结构,重点看 `ticketmachine.lua` 的购票支付流程、检票模块的进出站逻辑,以及网页端现有的票据 API 和展示页,确认新加 IC 储蓄卡最合适的接入点。 - -我已做了完整检索,覆盖了售票机主程序、线上预定页、凭证详情页,以及闸机核验侧代码;下面按“页面结构、输入交互、支付、出票、接入点”汇总。 - -**主流程** -- 主状态机在 [mainPageLoop](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1429-L1444),页面顺序是 `home -> departure -> terminal -> type -> trips -> order -> done`,线上兑票走 `home -> online -> order -> done` -- 首页模式选择在 [showHome](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L693-L732),只有两个入口:`NEW` 新购票、`ONLINE` 凭证兑票 -- 站点/线路选择页共用 [renderLinesSelection](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L735-L796),数据来自 `CFG.lines / CFG.stations` -- 订单确认与支付页在 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1023-L1154),出票完成页在 [showDone](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1190-L1336) -- 线上凭证输入页在 [showOnlineVoucher](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1338-L1424) - -**页面结构** -- `home`:模式选择页,按钮为 `NEW / ONLINE`,进入不同购票链路,见 [showHome](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L693-L732) -- `departure`:选择出发站,见 [showDeparture](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L798-L816) -- `terminal`:选择到达站,禁止同站,见 [showTerminal](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L818-L848) -- `type`:选择 `Local / Express`,见 [showType](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L850-L877) -- `trips`:选择乘次数量,`+/-` 调整,见 [showTrips](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L879-L900) -- `order`:显示区间、车型、次数、金额、支付进度条、确认按钮,见 [drawOrder](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L962-L1021) 与 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1023-L1154) -- `done`:真正执行出票/上传后展示票据信息,见 [showDone](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1190-L1336) -- `online`:5 位凭证码输入页,见 [showOnlineVoucher](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1338-L1424) - -**输入交互** -- 统一按钮机制在 [addButton / waitButtons](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L582-L600),支持 `mouse_click` 和 `monitor_touch` -- 全局取消弹窗在 [renderConfirmCancel](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L662-L674),各页通过 `addCancelButton` 接入 -- 站点选择通过触摸按钮完成,支持滚动翻页,见 [renderLinesSelection](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L735-L796) -- 线上凭证支持两种输入:屏幕软键盘、实体键盘 `char/backspace/enter`,见 [showOnlineVoucher](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1374-L1420) -- 金额不是文本输入,而是监听右侧红石输入脉冲累加,见 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1114-L1129) - -**支付流程** -- 票价计算核心在 [computeCost](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L902-L960),基于 `CFG.fares` 构造普通/特急邻接图后跑最短路 -- 订单页会先重算价格,避免旧数据残留,见 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1023-L1037) -- 支付状态 UI 在 [drawOrder](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L998-L1020),显示 `Paid / Remaining` 与进度条 -- 实际“收款”逻辑很简单:每次 `redstone.getInput('right')` 从 `false -> true`,`state.paid += 1`,见 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1114-L1129) -- 付满后自动确认;若金额为 0 也会自动确认,见 [confirmAction](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1050-L1082) 与 [auto-confirm](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1108-L1112) -- 当前代码没有接现金/扫码/银行卡 API;支付接入本质上就是把外设回调换成更新 `state.paid`,或重写确认条件 - -**出票流程** -- 本地新购票:支付满足后 `confirmed=true`,跳到 `done`,见 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1050-L1082) -- 线上凭证兑票:确认时先 POST `/public/orders/{code}/consume`,成功后才进入 `done`,见 [consume voucher](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1060-L1074) -- 真正出票发生在 [showDone](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1236-L1249):优先调用外设 `ticket_vending_machine.issueTicket(...)` -- 若外设调用失败,则退化为本地生成票号 [generateTicketId](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1162-L1169) -- 出票后会组装 `ticketDataMod` 并上报 `/tickets/sale`,见 [showDone](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1264-L1308) 与 [uploadTicketRecord](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L223-L246) -- 上传失败会写入 `logs/pending_ticket_upload.jsonl`,后台任务自动补传,见 [enqueueTicketUpload](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L248-L259) 和 [backgroundTicketUploadTask](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L339-L354) - -**线上预定关联文件** -- 预定页面结构: [ticket-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-order.html#L120-L244) -- 预定页逻辑: [ticket-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-order.js#L1-L22) 与 [createOrder](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-order.js#L406-L451) -- 凭证详情页结构: [token.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/token.html#L104-L128) -- 凭证详情页逻辑: [token.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/token.js#L37-L57) -- Web 端创建凭证接口:`POST /api/public/orders`,见 [ticket-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-order.js#L418-L420) -- 机台兑票查询接口:`GET /public/orders/{code}`,见 [showOnlineVoucher](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1348-L1370) - -**建议接入点** -- 支付外设接入:优先改 [showOrderAndAudio](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1114-L1129),把 `redstone right` 替换为投币器/纸币器/扫码回调,再统一累加到 `state.paid` -- 支付校验扩展:改 [confirmAction](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1050-L1082),可在确认前增加第三方支付结果查询、超时撤单、退款判断 -- 出票设备接入:改 [showDone](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1236-L1249),当前已预留 `ticket_vending_machine.issueTicket(...)`,这是最直接硬件接入点 -- 售票上报接入:改 [uploadTicketRecord](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L223-L246),适合补充订单号、支付渠道、设备编号、流水号 -- 线上订单联动:改 [showOnlineVoucher.submitCode](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1348-L1370) 与 [consume voucher](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1060-L1074),适合加预锁定、核销前校验、重复兑票保护 -- 闸机验票联动:下游在 [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua),用于票据进出站核验,适合作为出票后全链路闭环入口 - -**结论** -- `ticketmachine.lua` 已具备完整页面状态机、触摸/键盘交互、基础支付累加、出票外设调用、售票上传与离线补传 -- 当前最薄弱的部分是“支付”,本质仍是右侧红石脉冲计数,不是真正支付网关 -- 最推荐的三个接入点是:`showOrderAndAudio` 的支付监听、`confirmAction` 的核销确认、`showDone` 的 `issueTicket` 外设调用 - -如果你需要,我下一步可以继续把这些流程整理成一张“页面/状态/事件/接口”的接入表。 - -已做一轮完整检索,覆盖 `gate.lua`、售票机 Lua、安装脚本,以及与闸机校验直接对应的服务端路由;结论是:当前 Lua 侧已经有比较清晰的“票卡介质适配层”,可直接扩展为 IC 卡接入。 - -**结论** -- 最核心接入点在 [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L479-L670):闸机不直接操作软盘文件,而是依赖 `ticket_inspection_machine` 外设的事件和方法,这里最适合替换/扩展为 IC 卡读写器。 -- 售票/发卡接入点在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1236-L1308):售票机通过 `ticket_vending_machine.issueTicket(...)` 下发票据,可平移为“写 IC 卡初始化数据/发新卡”。 -- 进出站状态维护分两层:Lua 侧只做事件接收、服务器请求、回写介质;真正的状态判定和扣次在服务端 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L553-L698)。 -- 如果要支持 IC 卡,优先保留现有 HTTP 校验协议不变,只替换 Lua 外设层,改造成本最低。 - -**进出站状态** -- 闸机模式来自 [loadConfig](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L44-L55),支持单向 `mode=entry/exit`,也支持双向 `side_modes`,见 [gate.lua:L422-L471](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L422-L471)。 -- 扫描事件入口是 [ticket_scanned](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L672-L682),参数解析在 [parseTicketScannedArgsPacked](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L530-L545)。 -- 进/出站动作选择在 [handleScan](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L554-L618):优先按闸机侧别决定,双向且无侧别时会根据票内 `entered/exited` 推断当前是进站还是出站。 -- 票状态字段明确使用 `entered`、`exited`、`trips_total`、`trips_remaining`,见 [gate.lua:L559-L600](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L559-L600) 和 [ticketmachine.lua:L1265-L1279](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1265-L1279)。 - -**票据读写** -- 读票不是 Lua 自己读文件,而是等待外设抛出 `ticket_scanned` 事件,并从事件里拿 `scan` 表,见 [gate.lua:L530-L545](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L530-L545)。 -- 票号读取统一走 [getTicketId](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L319-L340),兼容 `ticketId/ticket_id/id` 多种字段。 -- 起终点读取统一走 [getStartStation](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L353-L387),兼容多种字段名,也支持从站名反推站码。 -- 验票通过后,Lua 侧对介质回写在 [gate.lua:L632-L659](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L632-L659):`updateTicket("entered"/"exited"/"rides", ...)`,以及必要时 `destroyTicket()`。 -- 售票写票在 [ticketmachine.lua:L1236-L1249](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1236-L1249):调用 `ticket_vending_machine.issueTicket(...)`,这是发新票/初始化卡片的现成接入点。 - -**扣费与校验** -- `gate.lua` 自己不做本地票价计算,也不直接扣费;它只向 `/api/tickets/check` 提交 `ticket_id + action + station_codes + trips_*`,见 [postCheck](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L250-L258) 与 [doCheck](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L591-L602)。 -- 服务端才是权威校验点:进站校验起点站、是否已进站;出站校验终点站、是否已进站/已出站,并在出站时 `trips_remaining - 1`,见 [api.js:L650-L697](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L650-L697)。 -- 服务端返回 `resp.trips_remaining` 与 `resp.destroy_ticket`,Lua 再把剩余次数写回票介质,见 [gate.lua:L632-L659](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L632-L659)。 -- 售票后会把票据记录上传到后端,见 [enqueueTicketUpload](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L223-L259) 与 [ticketmachine.lua:L1308-L1310](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1308-L1310)。 - -**可扩展为 IC 卡的接入点** -- `ticket_inspection_machine` 外设接口是第一接入点,见 [gate.lua:L479-L493](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L479-L493);只要 IC 卡读卡器实现: -- 抛出 `ticket_scanned` 事件,并提供一个 `scan` 表,至少含 `ticket_id/id`,最好再带 `entered/exited/trips_remaining/start_station/terminal_station`。 -- 提供 `updateTicket(key, value)`,用于进出站后回写卡内状态。 -- 提供 `destroyTicket()`,用于单次票或次数耗尽后的废票/锁卡处理。 -- `ticket_vending_machine.issueTicket(...)` 是第二接入点,见 [ticketmachine.lua:L1236-L1249](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1236-L1249);可替换成 IC 发卡器,把新票初始数据写入空卡。 -- 安装配置层已支持闸机模式和站点下发,见 [installer.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/installer.lua#L90-L107) 与 [installer_bi.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/installer_bi.lua#L108-L140);无需改协议,只需新增 IC 设备部署说明。 - -**建议方案** -- 最小改造:保留 [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua) 与后端 `/tickets/check` 协议不变,实现一个兼容 `ticket_inspection_machine` 的 IC 卡外设。 -- 如果 IC 卡要做脱机校验,可把 [handleScan](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L554-L670) 中 `doCheck()` 前后拆成“本地预校验 + 在线确认 + 回写卡片”三段。 -- 如果要支持储值卡而不是次数票,新增点应放在服务端 `/tickets/check` 与售票记录模型,不建议把扣费逻辑直接塞进 Lua。 -- 当前代码最像“票卡介质抽象层”的其实不是文件读写,而是这两个外设接口:`ticket_inspection_machine` 和 `ticket_vending_machine`。 - -如果你要,我下一步可以直接给你整理一份“IC 卡外设最小接口协议”,按这个项目现有 Lua 调用方式列出函数签名和事件格式。 - -我已做过一轮较完整的代码检查,覆盖了管理控制台、公开查询/预定页、后端路由、MySQL 持久化,以及售票机侧的线上兑票流程;结论如下。 - -**现状总览** -- 网页控制台入口是 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html#L1-L49),已包含“车票记录”“凭证管理”等视图。 -- 控制台前端逻辑在 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js#L12-L27);其数据拉取集中在 [index.js:L546-L560](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js#L546-L560),已直接读取 `/api/tickets`、`/api/orders`。 -- 公开查询页是 [ticket-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-search.html#L1-L93) + [ticket-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-search.js#L8-L20),调用 `/api/public/tickets` 与 `/api/public/tickets/:id`。 -- 线上预定/凭证页是 [ticket-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-order.js#L9-L21) 和 [token.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/token.js#L37-L57),调用 `/api/public/orders`、`/api/public/orders/:code`。 -- 路由挂载统一在 [server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L29-L33);公开页面 URL 在 [server/index.js:L66-L87](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L66-L87)。 -- 数据存储不是 README 里旧的 `web/data/*.json`,而是 MySQL;核心实现是 [data.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L13-L22) 和建表逻辑 [data.js:L47-L60](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L47-L60)。 - -**当前已存在的票据/兑票结构** -- 票据主索引:`ticket_index`,缓存字段见 [data.js:L35-L39](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L35-L39) 与 [data.js:L335-L375](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L335-L375)。 -- 票据事件流:`ticket_events`,写入/读取见 [data.js:L312-L333](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L312-L333)。 -- 线上预定凭证:`orders` + `order_index`,创建/查询/消费在 [public.js:L109-L143](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L109-L143)、[public.js:L146-L176](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L146-L176)、[public.js:L178-L215](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L178-L215)。 -- 售票机已支持“ONLINE”凭证兑票:入口在 [ticketmachine.lua:L693-L731](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L693-L731),凭证输入与校验在 [ticketmachine.lua:L1338-L1424](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1338-L1424),支付确认后调用消费接口在 [ticketmachine.lua:L1057-L1074](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1057-L1074)。 -- 售票后会把票据上传到 `/api/tickets/sale`,离线失败写本地队列 `logs/pending_ticket_upload.jsonl`,见 [ticketmachine.lua:L192-L259](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L192-L259)。 - -**IC 卡现状** -- 项目里没有独立的 “IC 卡记录 / IC 卡索引 / IC 卡事件” 后端结构。 -- 现有模型只有“纸票/电子票 ticket”与“线上凭证 order”;也就是线上兑票已经有,但“兑卡”还没有独立数据层。 - -**新增 IC 卡记录与线上兑卡,最少需要的文件** -- 后端存储扩展:修改 [data.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js) -- 管理端 API:修改 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js) -- 公开端 API:修改 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js) -- 控制台页面:修改 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html) 与 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js) -- 若要给用户公开查卡/看兑卡凭证:复用或新增 [token.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/token.html)、[token.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/token.js) -- 若售票机直接兑卡:修改 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua) - -**建议新增的数据结构** -- `ic_cards` - - `card_id` - - `physical_uid` - - `status` (`active`/`lost`/`disabled`) - - `balance` - - `deposit` - - `holder` - - `bind_order_code` - - `created_ts` - - `updated_ts` -- `ic_card_events` - - `card_id` - - `type` (`issue`/`topup`/`redeem`/`entry`/`exit`/`refund`/`disable`) - - `station_code` - - `device` - - `amount` - - `ticket_id` - - `order_code` - - `ts` -- `ic_order_index` 或复用 `orders` - - 若复用现有 `orders`,建议加字段: - - `fulfill_type: 'ticket' | 'ic_card'` - - `target_card_id` - - `redeem_status` - - `redeemed_ts` - -**建议新增的 API 结构** -- 管理端 - - `GET /api/ic-cards` - - `GET /api/ic-cards/:id` - - `POST /api/ic-cards` - - `PUT /api/ic-cards/:id` - - `GET /api/ic-cards/:id/events` -- 公开端 - - `POST /api/public/ic-orders` 创建线上兑卡订单 - - `GET /api/public/ic-orders/:code` 查询兑卡凭证 - - `POST /api/public/ic-orders/:code/redeem` 售票机/制卡机兑卡 -- 设备端 - - 若沿用现有订单体系,也可直接在 `orders` 上扩展 `redeem_target='ic_card'` - -**落地建议** -- 最稳妥的做法是:不要硬改现有 `ticket_index`;新增 `ic_cards`、`ic_card_events` 两套表。 -- 线上兑卡优先复用现有 `orders/order_index` 流程,只给订单加 `fulfill_type='ic_card'` 和兑卡结果字段。 -- 控制台可在现有 “凭证管理” 基础上增加 “IC卡记录” 视图,避免再开一套后台框架。 -- 售票机侧已具备线上凭证输入/消费骨架,新增“兑卡”只需要在 [ticketmachine.lua:L1338-L1424](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1338-L1424) 的成功分支里把目标从“出票”改为“写卡/绑卡”。 - -如果你要,我下一步可以直接按这个检查结果,继续给你整理一份“IC 卡功能改造清单(逐文件到字段级)”。 - -现有结构已经比较清楚了:Lua 侧有成熟的页面状态机、支付与出票上传链路,后端也有票据与线上凭证模型。接下来我会直接读关键文件,把 IC 卡需要复用和新增的字段、页面、API 精确到函数级,再开始落代码。 - -我改成分段读取,避免一次性拉太多文件导致 IDE 超时。先把将要修改的函数和路由精确定位出来,再逐段展开读取。 - -我已经锁定了关键函数和路由位置,现在开始逐段读取并准备改动方案。先从 `ticketmachine.lua` 的页面流和 `data.js` 的存储接口入手,因为这两处会决定前后端字段是否能对齐。 - -读取工具对这些大文件持续超时,我改用终端按行读取,并顺手检查工作区当前改动,避免误覆盖你已有的内容。 - -这里有一个关键不确定点:你给的 MOD API 里只明确了 `issueICCard()` 和充值机 `getCardInfo/refill/deduct`,但没有看到“在办卡时写入持有者名/初始余额”以及“检票机读写进站站号”的具体 Lua 接口。 -我先继续把现有代码读细,确认哪些地方可以先做;如果最终缺少 MOD 侧读写接口,我会单独把那一处列出来请你补一下函数名。 - -我继续往前做,不过会把缺失的 MOD 接口影响控制在最小范围内:先把服务端、网页端和售票机办卡流程落好,Lua 检票端则按“卡数据同步到服务端、设备侧尽量兼容现有事件格式”的思路实现。等你补到精确接口名时,只需要替换设备读写那一层。 - -先梳理 `TicketMachine` 现有 Lua 工程结构,定位办卡、支付、兑卡、闸机扣费和外设事件相关代码,再基于现有流程做兼容式扩展。 - -先查看 `TicketMachine` 项目的结构和现有网页/后端实现方式,确认应该在什么技术栈和目录下扩展 IC 卡相关功能。 - -已经定位到 `server` 和 `web` 目录,下一步会读现有数据层、路由和票务页面,沿用当前项目的接口风格与样式结构扩展 IC 卡功能。 - -已定位到核心文件主要集中在 `ticketmachine.lua` 和 `gate.lua`,接下来读取主流程并搜索现有卡务、支付、同步、外设事件入口。 - -主流程里目前已有纸票购票和线上券核销基础,但还没有会员卡/IC 卡独立状态与首充支付流程。我在继续查找是否已有服务端卡同步字段或外设读卡事件约定,避免破坏现有事件模型。 - -现有项目已经有票务、订单和公共查询的雏形,我在继续确认后台单页应用的导航与渲染方式,再把 IC 卡页面无缝接进去。 - -我在改动前先补齐后台入口与现有订票页的上下文;刚才直接读大文件超时了,改用检索关键片段定位页面和渲染函数。 - -还需要对齐服务端已有接口字段,尤其是 `/tickets/check` 和线上订单相关返回结构,这样 Lua 侧新增卡务字段才能尽量复用当前同步模型。 - -我已经确认后台是 Vue 单页,接下来直接抓取 `index.js` 里视图状态和票据模块的关键代码片段,避免盲改导航和渲染逻辑。 - -我已经确认仓库里还有 Node 服务端代码,接下来直接读服务端入口,核对 Lua 侧要提交的字段名和可复用接口。 - -当前结构已经够用,我准备开始落地实现:先补数据层和 API,再把后台列表详情与公开查询/购卡页接上现有导航和样式。 - -先获取后台模板和脚本的精确片段,随后会直接修改导航、数据源与新页面文件。 - -服务端文件较大,我改用按路由片段抓取,重点看售票上传、检票、订单查询/核销几段实现。 - -准备按两条线改: -1. `ticketmachine.lua` 增加卡务状态、办卡入口、姓名软键盘、首充金额与支付确认、线上兑卡查询与确认,并把写卡/建卡动作做成“优先调用已存在外设能力,缺失时保留同步与日志”的兼容实现。 -2. `gate.lua` 保持 `ticket_scanned` 事件不变,新增 IC 卡识别与服务端同步扣费分支,优先走卡专用同步接口,失败时不影响原纸票检票逻辑,同时把返回的余额/进站站点等字段尽量写回现有 `updateTicket`。开始改 `ticketmachine.lua`,先补通用请求与卡务辅助函数,再接页面流转和确认逻辑。 - -我在按最小侵入方案设计 IC 卡数据模型:一张卡对应基础信息、状态、余额和事件流;线上购卡会生成订单号并同步落卡,后台直接基于这套记录做列表和详情。 - -辅助函数已补进主文件,现在先回读最新片段,再接首页入口、卡务页面和确认提交流程,避免和刚才的改动冲突。 - -首页和卡务子页面已经接上,下一步改确认页与完成页,让票务和卡务共用支付界面,但按不同产品分支处理。 - -先改服务端数据层,增加 IC 卡主表与事件流,后面的管理和公开页面都会直接复用这套接口。 - -现在补管理 API:提供 IC 卡列表、详情、建卡、编辑和充值,前端后台页面就能直接接这几个接口。 - -再补公开 API:支持线上购卡下单、按卡号或订单号查询,以及给购卡页读取卡种配置。 - -后端数据和公开接口已经补上,接着把新页面路由挂到 Express,保证 IC 卡查询和购卡页可以直接访问。 - -售票机主线基本串起来了,开始处理 `gate.lua`:保持现有 `ticket_scanned` 事件入口不变,只在识别到 IC 卡字段时切到卡务同步分支。 - -我改成最小侵入的后台接入方式:在现有控制台侧边栏增加一个独立的 IC 卡管理页入口,这样不用重写原有 Vue 控制台也能保持一致风格。 - -接下来新增三个页面文件:后台 IC 卡管理页、公开查询页和线上购卡页;页面结构会直接复用现在的深色后台和 JR 风格公开页。 - -代码已经落到两个 Lua 主文件里,我先跑编辑器诊断看有没有明显语法或引用问题,再决定是否需要补一轮修正。 - -诊断没报语法问题,但我在回读 `gate.lua` 时发现一个运行时风险:`getCardId()` 提前引用了后面才声明的 `firstNonEmpty()`,这类 Lua 闭包在本文件写法下可能取到全局空值。我先修这个,再做一次最终检查。 - -语法检查已经过了,环境里没有可直接调用的 `lua/luajit` 解释器,所以验证会以编辑器诊断和静态流程核对为主。我再取一份变更摘要和关键片段行号,方便你对照 review。 - -再做一处稳健性修正:`writeICCard()` 目前把“调用成功但返回 `false`”也当成写卡成功,我把这个分支收紧,避免外设明确失败时误报完成。 - -页面文件已经生成,最后补一组很薄的样式扩展,让 IC 卡表单、卡种卡片和后台详情区能贴合现有视觉体系。 - -我顺手把首页入口补上,让新增的 IC 卡查询和线上购卡页能从门户页直接进入,不会变成隐藏功能。 - -**已修改** -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L915-L1160):首页新增 `CARD` 入口,加入办卡子菜单、姓名输入软键盘、首充金额选择、线上兑卡页。 -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1390-L1669):确认页兼容票务/卡务双模式,首充走现有右侧红石支付,线上兑卡支持免现场支付确认。 -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1705-L1764):新增卡片完成页与 `logs/last_card_issue.json` 快照记录,保留原纸票完成页。 -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L345-L388):补充卡片写卡辅助、卡务同步请求封装、外设方法兼容探测。 -- [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L345-L386):新增 IC 卡识别字段解析,兼容 `card_id`、余额、媒体类型等扫描数据。 -- [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L626-L717):新增 IC 卡进出站同步扣费分支,按服务端返回写回 `entered/exited/balance/entry_station/exit_station/last_fare`。 -- [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L822-L836):主循环继续只监听 `ticket_scanned`,检测到卡数据时切到卡务分支,纸票分支保持原逻辑。 - -**实现说明** -- 办卡流程:`Home -> CARD -> OPEN CARD -> 姓名输入 -> 首充金额 -> 支付确认 -> 写卡完成`。 -- 姓名输入:使用屏幕软键盘与 `char/key` 事件,支持字母、数字、空格,限制 12 字符。 -- 首充支付:复用原 `order` 页面与右侧红石投币/支付事件,尽量不改既有外设触发方式。 -- 线上兑卡:`CARD -> ONLINE REDEEM` 输入 5 位码,读取 `/public/orders/:code`,识别为卡订单后直接进入确认页。 -- 写卡兼容:优先尝试 `issueCard / issueICCard / writeCard / writeICCard / issueTicketData / writeTicketData`,成功后记录快照;明确返回 `false` 时判定写卡失败。 -- 闸机扣费:继续走服务端判定模型,只是把纸票 `/tickets/check` 扩到卡片 `/cards/check`,并保留双向闸机的自动进出站推断逻辑。 - -**假设** -- 服务端提供 `POST /api/cards/open` 用于办卡/线上兑卡建卡,返回可包含 `card_id / balance / deposit / topup`。 -- 服务端提供 `POST /api/cards/check`,返回结构尽量仿照现有 `/api/tickets/check`,至少包含 `result`,可选 `reason / balance / fare / entry_station / exit_station`。 -- IC 卡介质仍通过现有 `ticket_scanned` 事件上报,只是扫描表里会带 `card_id`、`balance`、`media` 等字段。 -- 售票机外设若没有上述写卡方法,则本次实现会停在“写卡失败”而不会伪装成功;同时保留 `logs/last_card_issue.json` 便于后续对接外设。 -- 姓名输入按当前 CC:Tweaked 屏幕与键盘能力处理为 ASCII 软键盘,不做中文输入法。 - -**验证结果** -- 诊断检查:`ticketmachine.lua`、`gate.lua` 的 VS Code 诊断均为 `0` 错误。 -- 运行环境检查:当前环境未发现 `lua` 或 `luajit` 可执行文件,无法做本地解释执行验证。 -- 静态流程核对:已逐段检查办卡首页流转、姓名输入、首充确认、线上兑卡确认、卡完成页,以及闸机 `ticket_scanned -> isICCardScan() -> handleICCardScan()` 分支闭环。 -- 仓库状态:当前目录不是 Git 仓库,无法直接给出 `git diff`/提交号;如果你需要,我可以下一步按这两份文件再整理一版人工 diff 摘要。 - -**建议下一步** -- 先在服务端补齐或确认 `/api/cards/open`、`/api/cards/check` 的字段名。 -- 在真实外设上确认 `ticket_vending_machine` 的写卡方法名;如果你给我外设 API,我可以把兼容探测改成精确调用。 -- 如果你希望,我可以继续补一版服务端 `cards` 路由,和网页后台里的卡订单管理。 - -`home.html` 修改时工具超时了,我改用精确替换的方式更新首页卡片文案和入口,避免影响现有结构。 - -功能已经接上,下面开始做静态检查和实际启动验证,优先排语法错误和明显的页面脚本问题。 - -语法检查已通过。现在启动一次本地服务并打几个新接口,确认路由和页面文件都能被服务器正常挂载。 - -服务正在拉起,我改用更小输出重试状态检查,确认端口和启动日志即可。 - -新页面请求返回了 `404`,我在看服务端运行日志和实际加载的 `server/index.js`,确认是不是需要重启或有路由没生效。 - -端口 `23333` 已被现有进程占用,所以我改在 `23334` 启动修改后的版本做独立验证,这样能确认新增路由确实来自本次代码。 - -新页面路由已经在 `23334` 验证为 `200`。我再补一次接口验证,确认健康检查和新增 IC 卡公开配置接口也可响应。 - -再做一组端到端验证:调用线上购卡接口生成一张测试卡,再用公开查询接口回查,确认关键流程闭环可用。 - -我补一个容错修正:让 IC 卡事件流同时写入内存缓存。这样即使本地没起 MySQL,测试环境下也能正常看到详情时间线,不影响正式有库部署。 - -事件缓存修正已完成,我会重启 `23334` 的验证实例,再确认查询接口能带出事件记录。 - -**已完成** -- 已在项目中补齐 IC 卡数据存储、管理 API、公开 API、后台列表详情页、公开查询页、线上购卡页,并复用现有深色后台与 JR 风格公开页样式。 -- 后端新增 `ic_cards` / `ic_card_events` 数据结构,同时做了内存缓存兜底,便于本地无 MySQL 时继续联调。 -- 后台入口已加到现有控制台侧栏,首页也补了 IC 卡相关服务入口。 - -**修改文件** -- 后端: - - [data.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js) - - [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js) - - [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js) - - [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js) -- 后台页面: - - [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html) - - [ic-card-admin.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.html) - - [ic-card-admin.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.js) -- 公开页面: - - [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html) - - [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html) - - [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js) - - [ic-card-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.html) - - [ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js) -- 样式: - - [style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css) - -**新增能力** -- 管理 API: - - `GET /api/ic-cards` - - `GET /api/ic-cards/:id` - - `POST /api/ic-cards` - - `PUT /api/ic-cards/:id` - - `POST /api/ic-cards/:id/topup` -- 公开 API: - - `GET /api/public/ic-cards/config` - - `GET /api/public/ic-cards/query?q=...` - - `GET /api/public/ic-cards/orders/:code` - - `POST /api/public/ic-cards/orders` -- 页面路由: - - `/admin/ic-card` - - `/ic-card/order` - - `/ic-card/search` - -**验证结果** -- 语法检查通过: - - `node --check server/index.js` - - `node --check server/routes/api.js` - - `node --check server/routes/public.js` - - `node --check server/services/data.js` - - `node --check web/ic-card-admin.js` - - `node --check web/ic-card-order.js` - - `node --check web/ic-card-search.js` -- 编辑后诊断结果: - - `GetDiagnostics` 返回空,无新增诊断错误。 -- 本地服务验证: - - 因现有 `23333` 端口已被占用,验证实例使用 `PORT=23334` - - `GET http://127.0.0.1:23334/ic-card/order` 返回 `200` - - `GET http://127.0.0.1:23334/ic-card/search` 返回 `200` - - `GET http://127.0.0.1:23334/admin/ic-card` 返回 `200` - - `GET http://127.0.0.1:23334/api/public/health` 返回正常 - - `GET http://127.0.0.1:23334/api/public/ic-cards/config` 返回卡种配置 - - `GET http://127.0.0.1:23334/api/ic-cards` 返回空列表后可正常写入 - - `POST /api/public/ic-cards/orders` 已成功生成测试订单与卡号 - - `GET /api/public/ic-cards/query?q=卡号/订单号` 已成功回查,并能返回事件流 - - `POST /api/ic-cards` 与 `GET /api/ic-cards/:id` 已成功验证后台建卡与详情读取 - -**说明** -- 本机 `127.0.0.1:3306` 当前未启动 MySQL,启动日志里有 `ECONNREFUSED`;因此本次运行验证主要依赖新增的内存缓存兜底。 -- 正式环境只要 MySQL 可用,代码已会自动建表并写入 `ic_cards`、`ic_card_events`。 - -两块改动都已经落下来了。我先在主会话里做一次交叉核对,确认新增的 IC 路由、页面入口和 Lua 页面状态都在项目里,并补跑一次诊断,避免子任务改完后还有漏项。 - -我做交叉核对时发现一个需要马上确认的点:Lua 侧现在已经调用了 `/api/cards/open` 和 `/api/cards/check`,但我还要再检查服务端是否真的暴露了这两个设备接口,避免页面改完但机台联不起来。 - -我确认到一个真实问题了:目前后端有 `ic-cards` 的管理和公开页面接口,但还没有机台/闸机直接调用的 `/api/cards/open`、`/api/cards/check` 设备接口。 -这会导致网页能用、Lua 机台却打不到后端。我现在补这一层,让售票机办卡和闸机扣费真正闭环。 - -我要补两个设备端关键点:一是线上兑卡查询改成优先走新的 IC 卡公开接口,避免机台还去查旧票据凭证;二是后端补出 `/api/cards/open` 和 `/api/cards/check`,把办卡、进站、出站扣费都接上。 - -先查看 `api.js` 和 `ticketmachine.lua` 的现状,确认现有卡务接口、扣费流程,以及线上兑卡当前调用链,再进行最小范围修改和验证。 - -已定位到服务端路由和 Lua 客户端文件,接下来读取相关实现,确认 `/public/ic-cards/orders/:code` 现有返回结构以及当前线上兑卡分支如何落到本地逻辑。 - -文件直读超时了,我先用内容检索切出关键片段,定位新增接口应插入的位置,以及 Lua 里当前兑卡查询顺序。 - -已经锁定到关键行号,但编辑前还需要把这些行附近的代码完整取出来。我改用终端分段读取,避免 IDE 读大文件超时。 - -关键片段已经拿到一部分。现在继续补齐 `tickets/check`、`cards/open` 调用方和 `ticketmachine.lua` 的兑卡分支,这样就能准确对齐请求/响应字段。 - -现在已经能看到服务器和终端两端的关键入口。下一步会对齐 `gate.lua` 期望的 `/api/cards/check` 响应格式,然后在 `api.js` 里补足对应卡片状态流转。 - -准备开始改文件前,我先确认工作区是否已有未提交改动,避免覆盖你现有编辑;然后会按最小补丁方式更新 `api.js` 和 `ticketmachine.lua`。 - -准备写入 `api.js`:会新增卡片下发与检票两个接口,并复用现有 `DataService`/`LogicService`,保证返回结构能直接被 `ticketmachine.lua` 和 `gate.lua` 消费。 - -接着修改 `ticketmachine.lua` 的兑卡查询顺序,只动线上兑卡入口,先查卡单接口,再回退旧票券接口,避免影响普通车票兑换流程。 - -代码已写入。现在回读关键片段确认没有插错位置,再做语法/诊断检查与最小验证。 - -语法诊断已经通过。为了补一层验证,我再做一次 JS 语法检查,并确认路由已按现有 `server/index.js` 的 `/api` 挂载方式生效。 - -关键改动和验证都完成了,我再补一遍精确位置,方便你直接点开对应代码。 - -**修改结果** -- 在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L724-L1026) 新增了 `POST /cards/open` 与 `POST /cards/check` 两个接口,实际挂载路径是 `/api/cards/open` 与 `/api/cards/check`,挂载位置见 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L29-L33)。 -- 在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L77-L129) 补了卡单号、金额、车站解析等辅助方法,供新卡务接口复用。 -- 在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1091-L1112) 调整了线上兑卡逻辑,先查 `/public/ic-cards/orders/:code`,失败后再回退 `/public/orders/:code`,并对非 `pending_pickup` 的卡单直接提示 `Already Used!`。 - -**接口行为** -- `POST /api/cards/open` - - 支持本地办卡:生成或激活 `card_id`,落库为 `active`,记录开卡事件。 - - 支持线上兑卡:按 `voucher_code/order_code` 查找 `pending_pickup` 卡单,兑卡成功后改为 `active`,写入兑卡站点、设备、余额等信息。 - - 已兑过的线上卡单会返回 `409`,Lua 端会显示 `Already Used!`。 -- `POST /api/cards/check` - - 支持 IC 卡进站:记录 `entry_station`、进站时间与当前卡状态。 - - 支持 IC 卡出站:按 `entry_station -> 当前站` 调用现有票价逻辑扣费,返回 `fare`、`balance/remaining_balance`、`entry_station`、`exit_station`。 - - 对 `not_found`、`already_entered`、`not_entered`、`already_exited`、`insufficient_balance`、`fare_not_found` 等情况返回 `result: 'deny'`,与 `gate.lua` 当前处理方式兼容。 - -**验证情况** -- `api.js` 通过 `node --check` 语法检查。 -- `GetDiagnostics` 检查 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js) 与 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua) 均无诊断错误。 -- 已确认新接口会被现有 `/api` 路由挂载,不需要额外改 `server/index.js`。 - -**补充说明** -- 这次验证是静态语法与编辑器诊断级别,未做带真实卡数据/站点票价数据的联机跑单。 -- 如果你要,我下一步可以继续帮你补一组最小联调用例,直接给出 `curl` 或 `PowerShell Invoke-RestMethod` 的办卡、兑卡、进站、出站验证脚本。 - -**已完成** -- 已把 IC 储蓄卡主流程接进项目:售票机办卡、首次充值支付、检票机进出站扣费、网页后台记录、公开查询、线上购卡与机台兑卡。 -- 售票机首页新增 `CARD` 入口,支持“办卡”和“线上兑卡”,办卡流程含持有人输入、首充金额选择、支付确认、出卡完成页,主改动在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L930-L970)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L977-L1079)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1474-L1669)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1705-L1733)。 -- 线上兑卡已改为优先查询新的 IC 卡订单接口,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1091-L1112)。 -- 检票机已增加 IC 卡分支,支持根据进站站号和当前站号计算路费并扣减余额,再把结果写回卡数据,主改动在 [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L554-L670) 和 [gate.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/gate.lua#L672-L682)。 -- 后端已补齐 IC 卡存储和设备接口,包括 `/api/cards/open`、`/api/cards/check`、后台 `/api/ic-cards*`、公开 `/api/public/ic-cards*`,主改动在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L524-L668)、[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L724-L1026)、[public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L191-L425)、[data.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L50-L96)、[data.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/services/data.js#L267-L347)。 -- 网页端已新增后台卡管理页、公开购卡页、公开查卡页,并接入现有站点风格与路由,主改动在 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html)、[home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html)、[ic-card-admin.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.html)、[ic-card-admin.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.js)、[ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)、[ic-card-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.html)、[ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js)、[style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css)。 -- 服务端页面路由也已补上,见 [server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L68-L83)。 - -**关键说明** -- 机台写卡这一层做了兼容探测,会按顺序尝试 `issueCard`、`issueICCard`、`writeCard`、`writeICCard`、`issueTicketData`、`writeTicketData`,实现位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L345-L388)。 -- 这是为了兼容你目前给出的 MOD 信息不完整的情况;也就是说,服务端与网页闭环已经补齐,但如果 MOD 最终实际写卡函数名不同,还需要把这层适配精确收口一次。 -- 进出站状态提示仍沿用现有逻辑,`already_entered`、`not_entered`、`already_exited` 这类提示链路没有拆掉,只是新增了卡的余额与扣费分支。 -- 卡数据会同步写入服务端,并记录事件流,后台与公开查询都能查到。 - -**验证** -- 我做了主会话交叉核对,确认新入口和新路由已经落在代码里,关键命中包括 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1096-L1096)、[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L724-L724)、[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L860-L860)。 -- 全局 `GetDiagnostics` 返回空,没有看到新增诊断错误。 -- 之前子任务里也完成了 Node 语法检查和页面/API 基本联通检查。 - -**残余风险** -- 最大风险仍然是 MOD 侧真实的“写卡/回写卡字段”函数签名未最终确认;现在代码已按兼容方式实现,但如果你那边的真实接口名、参数顺序、返回字段不同,需要再做一次精准对接。 -- 目前售票机姓名输入是 ASCII 软键盘,不支持中文输入法;如果你要中文持有人名,需要 MOD 或 CC 侧另给输入方案。 -- 如果你愿意,下一步我可以直接继续做两件事中的任意一件: -- 给你一份“MOD 侧最终应提供的精确接口清单”,你发给做 MOD 的人即可。 -- 给你一组联调脚本,按“办卡 -> 兑卡 -> 进站 -> 出站”完整跑通。 - ---- - -**User** - -现在有好多内容都出现了乱码!请修复 - -**Assistant** - -我先定位哪些文件出现了编码乱码,重点检查刚改过的网页、Lua 和后端文件里的中文文本,再逐个修复成统一编码内容。 - -我先排除了 `node_modules` 里的无关命中。现在重点看项目自身文件,尤其是网页页头有没有缺少 UTF-8 声明,以及哪些中文文本已经被写坏。 - -已经确认至少 `web/home.html` 的中文文本本身被写坏了,不是浏览器编码声明问题;`web/index.html` 只有一处注释/注释尾部残缺。 -我继续扫描公开页面和脚本里是否还有类似 `鍙`、`绗`、`鏈` 这类乱码串,然后统一修复。 - -我已经定位到主要乱码源就在 `web/home.html`,内容像是被错误编码后又保存了一次。我直接把这页的中文文案全部重写为正常 UTF-8,同时顺手修掉几个被截断的标签文本。 - -`home.html` 的主体乱码已经换回正常中文了。我再做一轮扫描,把剩余的残留乱码和注释损坏一并清掉,然后检查有没有因此引入语法问题。 - -现在只剩 `web/index.html` 里一处注释残留乱码,虽然不影响显示,但我把它也修掉,保持文件干净。 - -**已修复** -- 主要乱码来源确认是 [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html),这不是浏览器编码声明缺失,而是文件内容本身已经被错误编码后保存了。 -- 我已经把这页的中文文案全部恢复为正常 UTF-8,包括标题、导航、首页说明、服务卡片、提醒文案、票价图/线路图提示和页脚备案号。 -- 同时清理了 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html) 里残留的一处乱码注释。 - -**修改文件** -- [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html) -- [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html) - -**检查结果** -- 再次扫描 `web` 目录后,项目自身网页文件里已没有同类乱码残留。 -- [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html) 的诊断结果为空。 -- [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html) 的诊断结果也为空。 - -**说明** -- 这次确认到的乱码集中在首页 [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html),其他我本轮扫描到的网页、Lua、JS 文件没有发现同类坏字串。 -- 如果你现在浏览器里还有页面显示乱码,通常是浏览器缓存或服务器还在提供旧文件;建议先强刷一次页面。 - -**建议** -- 先重点刷新首页、线上购卡页、IC 卡查询页确认显示是否正常。 -- 如果你还看到别的页面有乱码,直接把页面路径发我,我可以继续逐页清。 - ---- - -**User** - -还是有乱码字符 `div` - 线上购卡的css失效了 `https://ticket.fse-media.group/ic-card/order` - -**Assistant** - -我先做两件事并行确认:一是继续扫描 `home.html` 是否还有漏掉的乱码文本,二是检查 `ic-card/order` 页面为什么样式没生效,重点看 HTML 引用、CSS 文件、以及线上实际页面加载情况。 - -我已经看到一个很像根因的问题:`ic-card/order` 页面里 CSS 用的是相对路径 `style.css?v=6`,在 `/ic-card/order` 这个地址下会被浏览器解析成 `/ic-card/style.css?v=6`,这很可能直接 404。 -我再用浏览器和线上请求确认一下,然后一并修复相对路径和剩余乱码。 - -问题确认了两点: -1. `线上购卡` 样式失效的根因就是 CSS 路径写成了相对路径,线上实际请求了 `https://ticket.fse-media.group/ic-card/style.css`,而这个地址返回 `Cannot GET`。 -2. `home.html` 里还残留了一处乱码文本 `绾胯矾璧勬簮`。 - -我现在直接修这两个点。 - -我又确认到一个更完整的原因:`线上购卡` 不只是 CSS,相对路径的 `logo.png`、`ic-card-order.js`、`public-status.js` 也都会在 `/ic-card/order` 下解析错。 -所以这次我会把公开页面里所有静态资源统一改成站点根路径,避免同类问题反复出现。 - -我已经把公开页面的静态资源路径统一改成绝对路径了,这样 `/ic-card/order`、`/ic-card/search` 这类带子路径的页面就不会再把 CSS 和脚本请求到错误地址。 -接下来我做一轮快速验证,确认没有残留相对路径和新语法问题。 - -**已修** -- 修掉了 `home.html` 里残留的乱码文案,`绾胯矾璧勬簮` 已恢复为“线路资源”,位置在 [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html)。 -- 修掉了 `线上购卡` 样式失效的根因:`/ic-card/order` 页面原来用的是相对路径 `style.css?v=6`,浏览器实际请求成了 `https://ticket.fse-media.group/ic-card/style.css`,线上返回了 `Cannot GET /ic-card/style.css`。 -- 现在已把公开页面静态资源统一改成根路径,避免带子路径的页面再次把资源请求错位置。 - -**已改文件** -- [home.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/home.html) -- [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html) -- [ic-card-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.html) -- [ticket-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-order.html) -- [ticket-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-search.html) -- [token.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/token.html) - -**修复内容** -- 把 `href="style.css?...` 改为 `href="/style.css?...` -- 把 `src="logo.png"` 改为 `src="/logo.png"` -- 把 `public-status.js`、`ic-card-order.js`、`ic-card-search.js`、`ticket-order.js`、`ticket-search.js`、`token.js` 改为根路径引用 -- 这样 `/ic-card/order` 和 `/ic-card/search` 不会再去请求 `/ic-card/style.css`、`/ic-card/ic-card-order.js` 这类错误地址 - -**验证** -- 我直接验证了线上请求: -- `https://ticket.fse-media.group/ic-card/style.css?v=6` 当前确实返回 `Cannot GET /ic-card/style.css` -- `https://ticket.fse-media.group/style.css?v=6` 可以正常返回 CSS -- 修改后的几个 HTML 文件诊断都为空,没有新增语法错误 - -**说明** -- 你现在浏览器里还能看到那个乱码 `div`,大概率是页面还没重新加载到最新文件,或者线上站点还没重新部署/重载静态文件。 -- 代码层面这两个问题都已经修好了;如果线上 URL 还是旧表现,需要让当前服务重新读到这些改动。 - -**下一步** -- 先强制刷新 `home` 和 `https://ticket.fse-media.group/ic-card/order` -- 如果这是你本机运行的 Node 服务,重启一下站点进程 -- 如果你愿意,我可以继续把其他仍使用相对静态资源路径的页面也一并扫完,例如 `blog.html`、`ticket-route.html`、`login.html`,一次性消掉同类隐患 - ---- - -**User** - -购卡界面去掉卡类型,改成首次充值:5、10、15、20、自定义,去掉备注,去掉联系电话,然后持卡人名改成只能英文+符号,去掉押金 - -**Assistant** - -我先改线上购卡这一套:页面表单、前端校验和后端下单字段一起收口,确保“卡类型/备注/联系电话/押金”都真正去掉,不只是前端隐藏。 - -我已经确认线上购卡页和公开下单接口目前都还带着“卡类型、押金、备注、联系电话”这套旧逻辑。现在把它们一起改成“首次充值”模式,并把姓名限制改成英文+符号校验。 - -我会顺手把公开查询页里和这次规则冲突的“卡种、押金、备注”展示也收一下,避免用户购卡后在查询页又看到旧概念。 - -我把界面结构改成“首次充值按钮 + 自定义金额”,并同步把前后端字段削到只剩 `holder_name` 和 `initial_balance`,这样规则不会前端后端不一致。 - -我已经把公开购卡页和后端下单接口都改成“首次充值”模型了。现在做一轮语法和残留字段检查,确认前端没有还在引用被删掉的 `phone`、`note`、`card_type`。 - -**已修改** -- 线上购卡页去掉了“卡类型”“备注”“联系电话”,改成“首次充值”模式,支持 `5 / 10 / 15 / 20 / 自定义`,改动在 [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html) 和 [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)。 -- 持卡人姓名现在限制为“英文 + 常用符号”,不允许中文和数字;前端和后端都做了校验,后端规则在 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js)。 -- 押金已从线上购卡逻辑中移除,购卡金额现在等于首次充值金额;公开下单接口也不再使用 `phone`、`note`、`card_type` 作为必需/核心字段,见 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L191-L196) 和 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L357-L425)。 -- 公开查询页同步去掉了“押金”和“备注”展示,改为显示“首次充值”,见 [ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js#L47-L65)。 - -**现在的购卡规则** -- 卡片固定按 `IC 储值卡` 处理,不再让用户选卡种。 -- 首次充值可直接点 `5 / 10 / 15 / 20`,也可切到“自定义”手输金额。 -- 持卡人姓名必须匹配英文和常用符号格式,例如 `Alex Smith`、`A.Brown`、`Chris-O'Neil`。 -- 押金固定为 `0`,实付金额就是首次充值金额。 - -**验证** -- [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)、[ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js)、[public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js) 的诊断均为空。 -- `node --check` 校验 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js) 通过。 - -**提醒** -- 线上站点要看到新表单,需要重载当前服务或重新部署静态文件。 -- 如果你还想把后台 `IC 卡管理` 里的“卡种 / 联系电话 / 押金 / 备注”也一起删掉,我可以下一步继续统一那一页。 - ---- - -**User** - -继续统一 - -**Assistant** - -我继续把后台 `IC 卡管理` 一并统一到新规则,重点清掉“卡种、联系电话、押金、备注”相关表单和展示,保留持卡人姓名、状态、余额这几个真正还在使用的字段。 - -我把后台这边也按同一套规则收口:建卡只保留“持卡人姓名 + 初始余额”,详情只保留“姓名 + 状态 + 余额”,其余旧字段全部从界面上撤掉。必要的话,我也会顺手把后台接口默认值改成和公开购卡一致。 - -后台页面和后台 API 已经按新规则收口了。现在做最后一轮残留字段扫描和语法检查,确认没有再引用旧的 `联系电话 / 卡种 / 押金 / 备注` 输入。 - -**已统一** -- 后台 `IC 卡管理` 现在也按同一规则收口了,不再出现“卡种、联系电话、押金、备注”这套旧字段。 -- 快速建卡只保留 `持卡人姓名(仅英文与符号)` 和 `初始余额`,改动在 [ic-card-admin.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.html)。 -- 后台详情页只保留 `持卡人 / 卡片类型(固定显示 IC 储值卡)/ 状态 / 余额`,并把摘要区补成“首次充值 + 购卡金额”,改动在 [ic-card-admin.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.js)。 -- 后台列表里的卡片副标题也统一成 `持卡人 · IC 储值卡`,不再根据旧 `card_type` 显示月票卡/纪念卡。 - -**接口同步** -- 后台 API 现在也统一默认按 `IC 储值卡` 处理,建卡和更新都不再接受前端传入的 `phone / note / deposit / card_type` 作为主逻辑字段,改动在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js)。 -- 后台建卡和编辑同样加入了“持卡人姓名仅支持英文与常用符号”的校验,和公开购卡页保持一致。 -- 后台卡片列表搜索也去掉了对 `phone`、`note` 的依赖。 - -**验证** -- [ic-card-admin.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.html)、[ic-card-admin.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.js)、[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js) 诊断均为空。 -- `node --check` 校验 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js) 已通过。 -- 我也额外扫过后台页面代码,已没有 `createPhone`、`createType`、`detailPhone`、`detailType`、`detailDeposit`、`detailNote` 这类旧字段残留。 - -**当前统一结果** -- 公开购卡页:只填持卡人姓名,选择首次充值金额。 -- 公开查询页:不再展示押金、备注。 -- 后台管理页:不再编辑卡种、联系电话、押金、备注。 -- 后台和公开端:都使用同一套英文姓名校验规则。 - -**如果继续** -- 现在还可以再统一一层:把设备端 `/api/cards/open` 里残留的 `phone / note / deposit / card_type` 兼容字段也进一步收窄成纯“姓名 + 余额”模型。 -- 如果你要,我下一步就继续清这一层。 - ---- - -**User** - -没有显示出充值配置 Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') - at ic-card-order.js:167:22 - at ic-card-order.js:182:3 - -**Assistant** - -我先对照 `ic-card-order.html` 和 `ic-card-order.js` 定位是哪一个元素 ID 没对上,再补一层空值保护,避免单个 DOM 缺失就直接把整页脚本打挂。 - -我确认这更像是“线上实际 HTML 还是旧版、JS 已经是新版”造成的前后不匹配:旧页面没有 `#customInitialBalance`,新版脚本一绑定事件就炸了。 -我现在把 `ic-card-order.js` 做成向后兼容,能同时兼容新旧 DOM,避免线上缓存/灰度状态下直接白屏。 - -我已经把线上购卡脚本改成兼容新旧页面结构了:即使线上还在缓存旧 HTML,也不会因为找不到新 ID 直接抛错。再做一轮诊断确认没有手滑引入新问题。 - -**已修** -- 这个报错的根因是线上页面很可能还是旧版 HTML,而浏览器拿到了新版 `ic-card-order.js`。 -- 新版脚本会找 `#customInitialBalance` 和 `#rechargeOptionList`,但旧版页面只有 `#initialBalance` 和 `#planList`,所以在绑定 `addEventListener` 时直接对 `null` 调用了方法。 - -**这次改动** -- 我把 [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js) 改成了兼容新旧 DOM: -- `#rechargeOptionList` 不存在时,自动回退到旧的 `#planList` -- `#customInitialBalance` 不存在时,自动回退到旧的 `#initialBalance` -- 如果关键节点仍然缺失,会先 `console.error` 并安全退出,不再整页脚本崩掉 -- 旧页面里残留的 `联系电话`、`备注` 输入框会被脚本自动禁用并隐藏,避免旧 HTML 和新逻辑混在一起 - -**验证** -- [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js) 诊断为空,没有新增错误。 - -**你现在应该看到的效果** -- 即使线上还是旧 HTML,页面也不该再报 `Cannot read properties of null (reading 'addEventListener')` -- 充值配置应该能正常渲染出来 -- 旧的“联系电话/备注”如果还在缓存页面里,也会被隐藏掉 - -**说明** -- 这属于典型的“HTML 和 JS 部署版本不一致”问题。 -- 代码侧我已经做了兼容,但如果你想完全干净,还是建议把线上静态页面一起更新并清缓存。 - -**建议** -- 强刷 `https://ticket.fse-media.group/ic-card/order` -- 如果服务端有静态缓存/CDN,也一起清掉 -- 如果刷新后还有问题,我可以继续帮你直接排线上实际返回的 HTML 和 JS 是否版本不一致 - ---- - -**User** - -现在似乎cc端没有正确传给售票方块出卡参数,导致没有正常出卡。 - -**Assistant** - -我先对照售票机 Lua 里“购票正常出票”的调用方式,检查办卡分支到底传了哪些参数给 `ticket_vending_machine`,再和你给的 MOD API `issueICCard()` 对比,找出哪里少传或传错了。 - -我基本锁定根因了:当前 CC 端 `writeICCard()` 是按“带参数写卡”去试 `issueICCard` 的,但你最早给的 MOD API 里 `issueICCard()` 是**无参**。 -这就很可能导致外设方法被调用时参数不匹配,最终根本没成功出卡。我先把这一层改成优先按真实无参 API 调用,再兼容旧的猜测式写法。 - -我发现这块不只是“参数不对”,还有更深一层的问题:如果 MOD 的 `issueICCard()` 真的是**无参并返回真实卡号**,那现在 CC 端是“先向服务端开卡,再尝试往方块写卡”,顺序也反了。 -正确闭环应该更像“先让售票方块实际出卡拿到真实 `cardId`,再把这个 `cardId` 连同姓名/余额同步到服务端”。我先确认 `firstString()` 这类工具函数怎么取返回值,再按这个思路改。 - -我按“真实无参 `issueICCard()` 优先”的方式改:先让方块出卡拿到物理 `cardId`,再把这个 `cardId` 发给服务端建卡/兑卡。旧的“带参数写卡”逻辑保留作兼容后备。 - -我已经把办卡/兑卡的出卡顺序改成“优先真实无参 `issueICCard()` 出物理卡,再把返回的 `cardId` 同步给服务端”。同时保留了旧的参数写卡逻辑作为后备,避免兼容老实现时直接失效。 - -**已修方向** -- 我已经把 CC 端出卡流程改成更符合你给的 MOD API: -- 先调用售票方块的无参 `issueICCard()` 尝试**实际出卡** -- 拿到它返回的真实 `cardId` -- 再把这个 `cardId + 持卡人 + 余额` 发给服务端 `/cards/open` -- 只有当无参出卡接口不存在或失败时,才回退到之前那套“服务端先建卡,再尝试参数写卡”的兼容逻辑 - -**为什么之前会出问题** -- 你给的 API 是 `local ok, cardId = vm.issueICCard()` -- 但原来的 `ticketmachine.lua` 把 `issueICCard` 当成“可带参数写卡”的方法来试,这很容易导致外设调用参数不匹配,最终不出卡 -- 更严重的是,原来顺序是“先服务端生成卡号,再让方块出卡”,如果方块自己返回了一个真实 `cardId`,那物理卡号和服务端卡号还会错位 - -**这次改动** -- 新增了物理出卡 helper,在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L372-L382) -- 调整了办卡确认分支,优先走“先出卡再同步”,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1523-L1592) -- 旧的 `writeICCard()` 仍保留为 fallback,并补了一些兼容字段和无参尝试,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L387-L413) - -**现在行为** -- 如果 `ticket_vending_machine.issueICCard()` 正常返回 `true, cardId` - - 会直接用这个 `cardId` 去服务端开卡/兑卡 - - 这样服务端和实体卡号一致 -- 如果物理卡已经出了,但服务端同步失败 - - 页面会显示 `Issued, sync failed: ...` - - 不会误报“办卡成功” -- 如果方块出了卡但没返回 `cardId` - - 页面会显示 `Card issued without ID` - - 方便你继续判断 MOD 侧返回值是否异常 - -**验证** -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua) 诊断为空,没有新增语法问题。 - -**建议你现在测试** -- 在 CC 端实际点一次办卡 -- 看售票机屏幕提示是否从之前的写卡失败,变成正常 `Card ready` -- 如果还不行,优先看两件事: -- `issueICCard()` 是否真的返回了 `true, cardId` -- 物理卡出了以后,服务端 `/cards/open` 是否返回成功 - -**如果继续排** -- 我下一步可以直接给你加一版更明确的现场调试输出: -- 在 CC 屏幕或 `logs/last_card_issue.json` 里把 `issueICCard()` 的返回值、方法名、最终提交给 `/cards/open` 的 `cardId` 都打印出来 -这样就能马上判断是“方块没出卡”,还是“出了卡但没回传卡号”,还是“服务端同步失败”。 - ---- - -**User** - -还是方块没出卡,你先确定这里的代码没有问题 - -**Assistant** - -我按代码审查方式只看这条链路:`ticketmachine.lua` 里办卡确认 → 调用售票方块 → 同步 `/cards/open`。先把相关代码和服务端接口完整读一遍,再给你结论,优先找“必然导致不出卡/错卡”的问题。 - -我还要核对一件关键事:正常出票 `issueTicket(...)` 是怎么调用的,以及 `ticketVendingMachine` 这个外设对象到底是怎么拿到的。这样才能判断现在 `issueICCard()` 的调用方式是不是和现有外设包装层一致。 - -我已经抓到至少一个会直接影响“没出卡却被当成成功分支处理”的硬问题,现在做第二遍交叉验证,避免把猜测当结论给你。 - -先定位 `ticketmachine.lua` 和 `server/routes/api.js` 中办卡、兑卡与 `/cards/open` 的实现,再沿着调用链核对 `card_id` 的来源、落库逻辑,以及同步失败后的重试/去重控制。 - -先查看 `ticketmachine.lua` 里这两个函数及其调用链,核实返回值约定和失败回退路径,再给出逐条审查结论。 - -已经定位到关键判断:两处都用 `r1 ~= false` 作为成功条件,我继续查卡片开户流程里失败后的补偿/回退逻辑,确认是否会出现“没出卡但流程已成功/已扣款”的后果。 - -回退链路也查过了:当前卡开户分支只有 `submitCardOpen` 正向调用,没有对应撤销/退款/作废分支;接下来我按“是否会把 `nil` 当成功、是否会导致不出卡不回退”分别给出结论和等级。 - -我已经看到终端机分成两条路径:一条先出空白卡再调 `/cards/open`,另一条先调接口再写卡。接下来补看服务端 `upsert` 与订单索引逻辑,确认线上兑卡是否真的保留设备上报的 `card_id`,以及“同步失败后重试”会不会再放一张新卡。 - -我在补读辅助函数时遇到一次读取超时,改成更小范围继续取证。重点会放在 `normalizeIcCardId`、`findIcCardByOrderCode`、`upsertIcCard`,这些能直接决定是否覆盖 `card_id` 和是否允许重复兑卡。 - -补充看了一下服务端路由,`/cards/open` 存在,但暂时没看到与这条开卡调用成对的回滚接口;我再取一小段路由确认一下,避免把“前端无回退”误说成“系统绝对无回退”。 - -**发现** -- `严重`:`writeICCard` 的成功判断会把 `nil` 误判为成功;一旦外设方法“正常返回但首返回值是 nil”,代码仍会走成功分支,直接把订单标记为 `confirmed = true` 并显示 `Card ready`,即使实际没有写卡/出卡。[ticketmachine.lua:L362-L364](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L362-L364) [ticketmachine.lua:L1611-L1617](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1611-L1617) -- `结论`:这里的 `nil` 返回会被误判为成功,且会导致“可能没出卡却不回退”。更严重的是,`submitCardOpen(payload)` 在写卡前已把服务端卡状态创建/激活,当前 `ticketmachine.lua` 中没有对应撤销/退款/回退逻辑。[ticketmachine.lua:L1594-L1617](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1594-L1617) [api.js:L729-L818](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L729-L818) -- `高`:`issueBlankICCard` 的成功判断同样会把 `nil` 误判为成功;但它的后果和 `writeICCard` 不完全一样。这里不会直接把 UI 标成成功,而是进入 `elseif okIssueBlank` 分支,报 `Card issued without ID`,同时跳过后续 `submitCardOpen + writeICCard` 的后备路径。[ticketmachine.lua:L378-L380](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L378-L380) [ticketmachine.lua:L1568-L1592](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1568-L1592) -- `结论`:`issueBlankICCard` 的 `nil` 返回也会被误判为成功;如果这个 `nil` 实际表示“未出空白卡/未拿到有效结果”,当前流程不会回退付款,也不会进入后备写卡路径,结果是订单停在失败态但没有补偿逻辑,属于“支付后可能不出卡且不回退”。[ticketmachine.lua:L1568-L1594](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1568-L1594) - -**流程图** -```mermaid -flowchart TD - A[确认开卡] --> B[issueBlankICCard] - B -->|pcall成功 且 r1=nil| C[被判定为 okIssueBlank=true] - C --> D{blankCardId 是否为空} - D -->|为空| E[显示 Card issued without ID] - E --> F[不走 submitCardOpen/writeICCard] - F --> G[无回退/退款逻辑] - - B -->|返回 false| H[submitCardOpen] - H --> I[writeICCard] - I -->|pcall成功 且 r1=nil| J[被判定为 okWrite=true] - J --> K[confirmed=true + Card ready] - K --> L[即使未实际写卡/出卡也结束交易] - - style C fill:#fff3e0,color:#e65100 - style E fill:#fff3e0,color:#e65100 - style J fill:#ffcdd2,color:#b71c1c - style K fill:#ffcdd2,color:#b71c1c - style G fill:#ffcdd2,color:#b71c1c - style L fill:#ffcdd2,color:#b71c1c -``` - -**审查表** -| No. | Issue Title | Suggestion | Code Link | -|-----|-------------|------------|-----------| -| 1 | `严重` `writeICCard` 经由 `callPeripheralMethods` 将 `nil` 当成功,可能未写卡却显示成功 | 成功条件改为“显式成功值”或“显式拿到有效卡标识/写卡结果”,不要用 `r1 ~= false`;同时在 `submitCardOpen` 成功但写卡失败时增加补偿逻辑 | [ticketmachine.lua:L362-L364](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L362-L364), [ticketmachine.lua:L1611-L1617](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1611-L1617) | -| 2 | `高` `issueBlankICCard` 将 `nil` 当成功,导致跳过后备路径并卡死已支付交易 | `issueBlankICCard` 仅在返回明确成功信号时才置 `okIssueBlank=true`;若无卡号,应继续尝试后备路径或进入补偿/退款流程,而不是停在“issued without ID” | [ticketmachine.lua:L378-L380](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L378-L380), [ticketmachine.lua:L1568-L1592](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1568-L1592) | - -**补充说明** -- `nil` 会被误判为成功,这一点是代码语义上可以直接确认的,因为 Lua 中 `nil ~= false` 为真。 -- 两处问题的区别在于:`issueBlankICCard` 的误判更像“内部成功态误判,阻断后续分支”;`writeICCard` 的误判则是“最终用户可见成功误判”,风险更高。 -- 就当前代码范围看,确实存在“可能不出卡却不回退”的结果;其中 `writeICCard` 路径是最明确、最严重的一条。 - -如果你要,我可以下一步直接把这两处成功判断改成“只接受显式成功返回”,并补一个最小化的失败补偿方案。 - -关键证据已经比较清楚了:线上卡订单在下单时就预生成了 `card_id`,而终端兑卡时又会先从设备拿真实空白卡号再上报。如果 `/cards/open` 不接纳这个真实卡号,就会出现“数据库里是预生成号,实体卡上是设备号”的错位;我再补一眼 `/cards/check` 的查卡逻辑,确认这个错位会不会直接导致刷卡失败。 - -**结论** -```mermaid -flowchart TD - A[线上下单生成待领卡订单] --> B[public.js 预生成 card_id] - B --> C[终端兑卡先从设备拿空白卡真实 card_id] - C --> D[/cards/open 兑卡分支] - D --> E[沿用订单原 card_id] - E --> F[返回旧 card_id 给终端] - F --> G[数据库 card_id 与实体卡真实 card_id 错位] - - H[终端确认办卡/兑卡] --> I[先 issueBlankICCard 出卡] - I --> J[再调用 /cards/open 同步] - J --> K{同步失败?} - K -->|是| L[仅提示 Issued, sync failed] - L --> M[processing=false 且 confirmed=false] - M --> N[CONFIRM 再次可点] - N --> I - - style B fill:#bbdefb,color:#0d47a1 - style E fill:#fff3e0,color:#e65100 - style G fill:#ffcdd2,color:#b71c1c - style I fill:#f3e5f5,color:#7b1fa2 - style L fill:#fff3e0,color:#e65100 - style N fill:#ffcdd2,color:#b71c1c -``` - -- `1. 严重` 线上兑卡会忽略设备返回的 `card_id`。 - 结论:`会`。在线订单在创建时就预生成了一个 `card_id`,[public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L366-L382);终端兑卡时先从设备拿到真实空白卡号并放进 `payload.card_id`,[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1568-L1574)。但 `/cards/open` 的兑卡分支只用 `findIcCardByOrderCode()` 找到原订单后直接 `upsert {...current, ...}`,没有把 `body.card_id` 写回卡记录,因此仍保留订单原来的 `card_id`,[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L743-L800)。随后终端又优先采用服务端返回的 `respData.card_id` 覆盖本地显示值,[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1571-L1578)。而后续验卡是按上报的 `card_id` 直接查索引,[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L865-L878),所以一旦设备真实卡号与订单预生成号不一致,就会形成“实体卡号和后台卡号错位”。 - -- `2. 严重` 同步失败后允许重复出卡。 - 结论:`允许`。无论办新卡还是兑卡,终端在确认时都会先执行 `issueBlankICCard()` 出卡,再调用 `/cards/open` 同步,[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1554-L1590)。如果同步失败,代码只是提示 `Issued, sync failed`,没有落本地待补偿队列,也没有把本次出卡锁死;随后 `processing` 被清回 `false`,`confirmed` 仍是 `false`,`CONFIRM` 按钮重新可点,[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1654-L1680)。同时当前工程的离线补偿队列只覆盖车票上传,不覆盖卡开卡/兑卡,[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L246-L313) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L480-L487)。这意味着操作员重试时会再次先出一张新卡;若第一次其实已被服务端受理但响应丢失,第二次重试在兑卡场景会先出第二张卡再收到 `409 already redeemed`,在普通开卡场景还可能直接生成第二张有效卡。 - -**补充** -- 未看到能够推翻上述两点的保护逻辑。 -- 这两个问题都不是纯日志/展示问题,都会影响线上实体卡与后台状态的一致性。 -- 如需,我可以下一步直接给出修复方案,或按“先修 `card_id` 错位 / 先修重复出卡”分别落代码。 - -我已经确认:这段代码目前**还不能说没问题**,而且有两处是会直接影响实体卡流程的硬问题。 -我先把问题按严重级别列清楚,再由你选是一起修还是先修最影响出卡的那一处。 - -你这边已经选了“全部修复”,我就直接动手。 -这次我会同时修 3 件事:`nil` 误判成功、线上兑卡忽略真实 `card_id`、同步失败后重复点确认会再次出卡。 - -我要顺手确认一下 `DataService.upsertIcCard()` 在“在线兑卡时替换 `card_id`”这个场景下是否会清理旧索引;如果不会,我会一起补上,避免后台里残留旧卡号脏数据。 - -我现在开始改 Lua 主流程,重点是让“重试只重试同步、不再重复出卡”,并把外设成功判断收严成“必须显式成功”。 - -Lua 侧第一轮已改完。接着补服务端 `/cards/open` 的兑卡分支,把设备真实 `card_id` 接住并替换旧预生成卡号,避免实体卡和后台错位。 - -我补一处刚才顺手写错的服务端语法:把误写成 Lua 的 `~=` 改回 JS 比较,并顺便把旧卡号变量提出来,避免重复归一化。 - -**结论** -- 这段代码原来**不是没问题**,我确认到了 3 个会影响实体卡流程的硬问题,并且已经全部修掉。 -- 现在这条链路从静态代码上看已经顺了:外设成功判断更严格、线上兑卡会接住真实 `card_id`、同步失败后不会再次重复出卡。 -- 语法和诊断已通过:`ticketmachine.lua` 与 `api.js` 都无诊断错误,`node --check` 也通过。 - -**本次修复** -- `nil` 误判成功已修:外设调用现在只接受“显式成功返回”,不再把 `nil` 当成功,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L356-L383)。 -- 同步失败重复出卡已修:如果空白卡已经出过但服务端同步失败,后续重试会复用同一张已出的卡号,只重试同步,不会再调用方块重新出卡,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1574-L1633)。 -- 线上兑卡卡号错位已修:`/cards/open` 的兑卡分支现在会采用设备返回的真实 `card_id`,并清理旧的预生成卡号记录,位置在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L755-L805)。 - -**流程图** -```mermaid -flowchart TD - A[CC 确认办卡/兑卡] --> B{已有待同步空白卡号?} - B -->|否| C[调用 issueICCard 出实体卡] - B -->|是| D[复用上次 blank card_id] - C --> E{返回显式成功且有 card_id} - E -->|是| F[/cards/open 同步服务端] - E -->|否| G[停在失败态,不误判成功] - D --> F - F -->|成功| H[Card ready] - F -->|失败| I[保留 pendingBlankCardId,仅重试同步] - style C fill:#bbdefb,color:#0d47a1 - style F fill:#c8e6c9,color:#1a5e20 - style G fill:#ffcdd2,color:#b71c1c - style I fill:#fff3e0,color:#e65100 -``` - -**我确认过的关键点** -- 成功判断:`r1 ~= false` 已改成“`r1` 不能是 `nil/false`”,避免外设没回值时被当成成功。 -- 重试逻辑:同步失败后会记住 `pendingBlankCardId`,下一次 `CONFIRM` 不会再出第二张卡。 -- 在线兑卡:服务端不再死守下单时预生成的卡号,而是跟随设备真实卡号。 - -**现在如果还不出卡,优先怀疑代码外因素** -- `ticket_vending_machine` 外设没有被 `peripheral.find('ticket_vending_machine')` 找到。 -- MOD 实际方法名/返回值和你给的文档不一致,比如 `issueICCard()` 并不返回 `true, cardId`。 -- 售票方块出卡前还有额外前置条件,例如库存、朝向、内部槽位、红石状态或实体卡物品未就绪。 - -**下一步最有效** -- 我建议直接再加一轮运行时日志,把这 4 个值打印出来: -- `ticketVendingMachine` 是否存在 -- `issueICCard` 是否存在 -- `pcall(issueICCard)` 的原始返回值 -- 最终提交到 `/cards/open` 的 `card_id` -- 这样就能马上把问题切成“代码流程问题”还是“MOD/外设行为问题”。 - -**状态** -- 这轮代码我已经改完。 -- 如果你愿意,我下一步直接给你把 CC 端调试日志也补上,专门抓“为什么方块没出卡”。 - ---- - -**User** - -IC卡网页控制台端可以正常收到信息,也能看到卡号、持卡人。但是订单号那里显示“---”。而且页面上还是有联系方式,请删除。然后加一个“删除卡”的按钮。然后把这个IC卡管理的页合并到/admin页 - -**Assistant** - -我先检查后台页面结构和 IC 卡管理页现状,重点看 3 件事:订单号为什么显示 `---`、联系方式残留在哪里、以及现在 `/admin` 和独立 IC 卡页是怎么分开的。 - -我已经确认独立的 IC 卡页就是现在那套后台界面。接下来我会把它并到现有主控制台页,同时补上删除接口和删除按钮,再顺手查订单号为什么总是空。 - -我在看主控制台的 Vue 结构,判断是直接并一块原生区域,还是把现有 IC 卡逻辑迁进 Vue。优先会选改动更小但不别扭的方案。 - -我准备直接把 IC 卡管理迁进主控制台的 Vue 页,不走 iframe。这样 `/admin` 里能原生切标签、共享现有刷新逻辑,也方便加“删除卡”按钮和统一样式。 - -我开始改主控制台模板,把独立的 IC 卡区块直接并进去,同时会让旧地址 `/admin/ic-card` 自动落到这个新视图,不再维持两套后台入口。 - -主控制台里的 IC 卡视图已经嵌进去了。现在我先重新读一遍刚改过的 `index.js`,再补最后几处导出和路由改动,避免在同一文件上踩掉刚才的上下文。 - -我再补两层后端收口:去掉 IC 卡记录里的联系方式字段,并加删除卡接口。顺手把旧 `/admin/ic-card` 入口指向新的 `/admin` 视图。 - -我已经把主界面、后台接口和旧入口路由都串起来了。现在做最后一轮检查,确认 `index.html/index.js/api.js/server/index.js` 没有新报错,也确认后台页里不再残留“联系方式”。 - -最后我再抓一下关键改动的定位行,方便你直接点开看:主控制台入口、IC 卡视图、删除接口和旧路由跳转。 - -**已调整** -- IC 卡管理已并入主控制台 `/admin`,侧边栏不再跳独立页,而是在主控制台内切换到 `IC 卡管理` 视图,改动在 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html#L43-L45) 和 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html#L455-L590)。 -- 订单号显示已改成优先读取 `order_code / voucher_code / code`,如果本来就是现场办卡没有线上订单,则显示“现场办卡”,不再一律是 `---`,逻辑在 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js#L355-L365)。 -- 后台页中的“联系方式”已去掉;我也确认新的 `/admin` 页面代码里已经没有 `联系方式 / 联系电话 / phone` 残留。 -- 卡片详情区新增了“删除卡”按钮,前端逻辑在 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html#L528-L531) 和 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js#L712-L721)。 - -**接口变更** -- 新增后台删除接口 `DELETE /api/ic-cards/:id`,会删除卡记录并记一条删除事件,位置在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L703-L721)。 -- `cards/open` 这条卡开卡接口里不再继续写 `phone`,位置在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L751-L849)。 - -**路由合并** -- 旧入口 `/admin/ic-card` 现在会跳转到 `/admin?view=iccards`,位置在 [server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L71-L72)。 -- 旧兼容入口 `/ic-card-admin` 也同样跳转到新的主控制台视图,位置在 [server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L78-L81)。 -- 主控制台脚本会自动识别 `view=iccards` 并直接打开 IC 卡管理页,位置在 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js#L14-L20)。 - -**验证** -- [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html)、[index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js)、[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js)、[server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js) 诊断均为空。 -- `node --check` 已通过 [index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.js) 和 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js)。 -- 我额外扫过合并后的后台页面代码,没再找到 `联系方式 / 联系电话 / phone`。 - -**说明** -- 独立文件 `ic-card-admin.html/js` 还保留在仓库里,但运行时入口已经并回 `/admin` 了,用户访问旧地址会自动跳到新视图。 -- 如果你愿意,我下一步可以继续把这两个旧文件也彻底清理掉,避免以后误维护两套页面。 - ---- - -**User** - -在出卡的时候持卡人name是否正确传给mod - -**Assistant** - -我直接核对办卡这条调用链里“持卡人姓名”从页面状态到 `ticket_vending_machine` 外设调用的传递路径,重点确认有没有真正作为参数传到 MOD。 - -**结论** -- 按你现在给的 MOD API 来看,**持卡人 `name` 在主流程里没有直接传给 MOD 出卡方法**。 -- 当前代码的“优先路径”是先调无参 `issueICCard()` 出空白卡,这一步**不带姓名参数**,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L372-L385) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1578-L1587)。 -- `holder_name` 目前是先组进本地 `payload`,然后发给你的网站后端 `/cards/open`,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1561-L1573) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1584-L1588)。 - -**现在的实际行为** -- 姓名来源:`state.holderName` 会写进 `payload.holder_name`,见 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1561-L1563)。 -- 主出卡调用:`issueBlankICCard()` 只做 `pcall(fn)`,没有任何参数,见 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L375-L383)。 -- 服务端同步:姓名会跟着 `payload` 提交给 `/cards/open`,所以网页控制台能看到持卡人姓名,这和你观察到的一致。 - -**只有哪种情况会传给 MOD** -- 只有走到后备路径 `writeICCard(finalCard)` 时,代码才会尝试把姓名传给外设。 -- 这里会把 `holder_name` 填进 `payload.owner`、`payload.card_holder`,并尝试多种参数形式调用外设,见 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L391-L412)。 -- 例如它会尝试: -```lua -{ payload } -{ tostring(payload.holder_name or ''), tonumber(payload.initial_balance) or 0 } -{ tostring(payload.card_id or ''), tostring(payload.holder_name or ''), ... } -``` -- 但这只是**兼容性后备**,不是当前你这套 `issueICCard()` API 的主流程。 - -**所以答案是** -- 如果 MOD 真实接口就是你发的: -```lua -local ok, cardId = vm.issueICCard() -``` -- 那么答案是:**没有,姓名没有在“出卡给 MOD”这一步传进去。** -- 目前姓名只进了后端记录,没有确定写进实体 IC 卡的数据区。 - -**建议** -- 如果你希望“出卡时就把持卡人名写进卡里”,MOD 侧还需要提供明确的写卡接口,比如: -- `issueICCard(holderName, balance)` -- 或 `issueICCard()` 后再 `writeCardData(cardId, data)` -- 如果你要,我下一步可以直接把 CC 端改成“只支持你最终确认的 MOD 写卡接口”,把这层彻底收口,不再靠兼容猜测。 - ---- - -**User** - -调用是: - vm.issueICCard("") - -**Assistant** - -我按你刚确认的真实调用签名把 CC 端收口到 `vm.issueICCard("")`,这样主出卡路径会把持卡人姓名直接传给 MOD,不再走无参猜测。 - -我已经把主流程改成按姓名参数出卡了,同时把后备写卡路径也补上了“单参数姓名”这一种调用形式,避免 `issueICCard(name)` 被别的参数组合打偏。现在做一轮语法检查。 - -**已改** -- 现在主出卡调用已经按你给的真实签名改成:`vm.issueICCard("")`。 -- 也就是说,持卡人姓名现在会在“出卡给 MOD”这一步直接传入,不再是之前的无参调用。 - -**具体改动** -- `issueBlankICCard()` 改成接收 `holderName`,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L376-L388)。 -- 主办卡确认分支现在会把 `payload.holder_name` 传给它,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1584-L1585)。 -- 后备写卡路径也补上了单参数调用形式 `{ holder_name }`,这样如果走到兼容分支,也会优先尝试 `issueICCard(name)`,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L404-L413)。 - -**现在的实际行为** -- 办卡页输入的姓名先进入 `payload.holder_name` -- 然后主流程调用: -```lua -ticketVendingMachine.issueICCard(payload.holder_name) -``` -- 如果这种调用失败,代码才会退回到旧的兼容尝试 - -**验证** -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua) 诊断为空,没有新增错误。 - -**结论** -- 现在可以明确说:**持卡人 name 已经会正确传给 MOD 出卡方法**。 -- 如果接下来还是“不出卡”或“卡里名字不对”,问题就更可能在 MOD 侧实现,而不是这里没把参数传过去。 - -**如果你要继续排** -- 我下一步可以再把这次调用的运行时参数直接写到 `logs/last_card_issue.json` 或屏幕调试输出里,确认现场实际传出去的名字字符串到底是什么。 - ---- - -**User** - -办卡充值时不要计算手续费、押金等内容。就直接是充值多少付多少钱。 - - 然后网页查询那里支持查询凭证码 - -**Assistant** - -我先收口两处逻辑:CC 办卡支付金额只按首充金额计算,以及网页查询把“凭证码”也作为 IC 卡查询入口。随后会把前后端一起对齐。 - -我看到查询页目前只认 `card_id / order_code`,还没把线上购卡返回的“凭证码/兑换码”纳入匹配。我再把购卡下单返回字段和服务端存储字段核对一下,一并修成可按凭证码查。 - -我已经定位到 CC 端办卡金额仍然沿用了旧的 `deposit + topup` 计算。我现在把这套金额模型直接改成“首充即实付”,并同步把公开查询文案和后端匹配都加上“凭证码”。 - -金额逻辑和查询入口都已经改完。我现在做检查,确认 Lua 和网页脚本没有新报错,同时抓一下关键位置方便你直接点开看。 - -**已修改** -- 办卡充值现在不再计算手续费、押金等附加项,实付金额就是首充金额,改动在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L775-L790)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L810-L824)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L843-L883)。 -- CC 办卡确认页也去掉了押金显示,只保留 `Top-up / Balance / Order`,并且 `Order` 现在等于首充金额,见 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1457-L1468) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1523-L1531)。 -- 网页 IC 卡查询现在支持按 `卡号 / 订单号 / 凭证码` 查询,后端匹配已补上,见 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L298-L325)。 -- 查询页文案、输入提示和结果展示也已同步加入“凭证码”,见 [ic-card-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.html#L46-L70) 和 [ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js#L47-L64)。 - -**现在的行为** -- 现场办卡:付 `5` 就充 `5`,付 `10` 就充 `10`,不再额外加押金。 -- 线上购卡查询:如果你拿到的是 IC 卡订单生成的凭证码,也可以直接在查询页输入。 -- 查询结果里会显示“凭证码”字段;当前若没有单独存 `voucher_code`,会回退显示已有的 `order_code`。 - -**验证** -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua)、[public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js)、[ic-card-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.html)、[ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js) 诊断均为空。 -- `node --check` 已通过 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js)。 - -**说明** -- 你现在如果在线上页面还看不到新查询提示,通常是静态文件缓存;强刷 `IC 卡查询` 页即可。 -- 如果你还想更彻底一点,我下一步可以把后台 `/admin` 里的 IC 卡搜索框也一起扩成支持“凭证码”检索。 - ---- - -**User** - -把后台 /admin 里的 IC 卡搜索框也一起扩成支持“凭证码”检索。 - -**Assistant** - -我先把 `/admin` 的 IC 卡搜索链路看一遍,确认是前端占位文案要改,还是后台 `/api/ic-cards?q=` 本身还没把“凭证码”纳入检索。 - -后台接口这边确实还没把 `voucher_code/code` 纳入过滤;前端占位文案也还是旧的。我直接一起补上。 - -我再做一轮检查,确认 `/api/ic-cards?q=` 和 `/admin` 的 IC 卡页都没有新问题。 - -**已支持** -- `/admin` 里的 IC 卡搜索框现在也支持按“凭证码”检索。 -- 后台接口 `/api/ic-cards?q=` 已把 `voucher_code` 和 `code` 纳入搜索字段,改动在 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L589-L598)。 -- 前端输入框提示文案也同步改成“搜索卡号 / 订单号 / 凭证码 / 姓名”,位置在 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html#L520-L522)。 - -**验证** -- [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js) 和 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html) 诊断均为空。 -- `node --check` 已通过 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js)。 - -**说明** -- 现在后台 IC 卡列表可按 `card_id / order_code / voucher_code / code / holder_name / source` 命中。 -- 如果线上数据里“凭证码”实际仍是写在 `order_code`,这次改动也兼容,不会影响原有搜索。 - -**建议** -- 强刷一下 `/admin?view=iccards` 后测试一张线上购卡记录的凭证码。 -- 如果你愿意,我还可以继续把后台 IC 卡列表里那行“订单 xxx”文案也改成“订单/凭证 xxx”,避免展示上看起来像只能搜订单号。 - ---- - -**User** - -售票机办卡的时候也要传入余额参数: - issueICCard(ownerName, initialBalance) — 第二参数可选,默认 0。 - vm.issueICCard("张三", 50) -- 开卡,余额 50 - -**Assistant** - -我把售票机办卡主调用再收口一次,改成按你最新确认的签名传 `name + initialBalance`,并同步检查后备分支的参数顺序。 - -我顺手扫到办卡金额选择页里还有一处旧公式残留,仍在用 `押金 + 充值`。这次会一起修掉,避免界面金额和实际出卡参数不一致。 - -我开始改 `ticketmachine.lua`:主出卡改成 `issueICCard(name, initialBalance)`,并把办卡金额页里残留的押金公式和提示一起清干净。 - -主调用已经改完了。我现在做一轮诊断,确认 Lua 没有语法问题,并把关键位置抓出来给你确认。 - -**已修改** -- 售票机办卡主调用现在会把“持卡人姓名 + 初始余额”一起传给 MOD: - - `vm.issueICCard(ownerName, initialBalance)` -- 对应改动在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L376-L394) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1562-L1593)。 - -**现在的实际调用** -- 主流程会调用: -```lua -issueBlankICCard(payload.holder_name, payload.balance) -``` -- 而 `issueBlankICCard()` 内部优先尝试: -```lua -ticketVendingMachine.issueICCard(holderName, initialBalance) -``` -- 位置分别在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L384-L389) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1582-L1585)。 - -**兼容处理** -- 如果 `issueICCard(name, balance)` 失败,代码还会回退尝试: - - `issueICCard(name)` - - `issueICCard()` -- 这样既满足你现在确认的真实签名,也不会把旧兼容路径直接砍掉。 - -**顺手修正** -- 办卡充值页里残留的旧公式也一起修了,不再出现 `押金 + 充值` 的计算。 -- 现在 `Need Pay` 直接等于首充金额,位置在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1077-L1090)。 - -**验证** -- [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua) 诊断为空,没有新增错误。 - -**结论** -- 现在 CC 端已经会按你给的最新接口,把姓名和余额一起传给 MOD 出卡。 -- 如果接下来实体卡余额仍不对,就更像是 MOD 侧没有正确使用第二参数,而不是 CC 端没传。 - ---- - -**User** - -线上购卡这里没有正确出五位凭证码,出了一个ICO-xxxx,请修改。 - 然后一些订单信息的字颜色要醒目一些。 - - 我希望卡片信息也有一个单独的界面,类似电子客票。 - 在ticket.fse-media.group/ic/<卡号> - - cc的屏幕键盘要加上符号的按钮 - -**Assistant** - -我先核对 4 条链路:线上购卡凭证码生成规则、购卡结果页样式、独立卡片详情页路由,以及 CC 办卡软键盘的按键布局。 - -我看到一个关键不一致:CC 线上兑卡当前就只接受 5 位码,但网页购卡后端还在生成 `ICO-xxxxxx`。我会把后端和结果页一起改成统一的 5 位凭证码,并顺手把乱码 HTML 一并清掉。 - -我再看一下现有的 5 位凭证码生成器和“电子客票”样式,尽量复用现成的设计语言,不新造一套。 - -我已经确认这次有三类改动要一起做:凭证码统一为 5 位、独立 IC 卡详情页、CC 符号键盘;同时会把购卡结果卡片的关键信息做成更醒目的高对比展示。 - -我准备开始改后端与页面文件了:先统一生成 5 位凭证码,再补独立卡片详情页和更醒目的结果展示,最后处理 CC 键盘。 - -我先把后端码制改掉:线上购卡返回的主标识改成 5 位凭证码,并把卡数据里也显式保留 `voucher_code/code` 字段,方便展示和查询。 - -我已经开始做页面层改造了。接下来把新卡片详情页路由接上,并新建 `/ic/<卡号>` 页面文件。 - -CC 这边我会把姓名输入键盘做成 `ABC / SYM` 切换,不只补符号按钮,也顺手把输入约束收口到“英文 + 常用符号 + 空格”。 - -我在补最后的细节检查前,先把新详情页脚本做一层转义处理,避免事件详情里有特殊字符时把 DOM 撑坏。 - -我开始做最后一轮检查:后端码制、两个网页脚本、新增详情页和 CC Lua 一起过一遍,避免这波改动互相打架。 - -诊断已经空了。我再做一层语法校验,并抓关键定位行,方便你直接跳到 5 位凭证码、独立卡片页和符号键盘这几处确认。 - -我再抓一下几处精确行号:5 位凭证码、独立卡片页入口、购卡结果链接,以及 CC `ABC/SYM` 键盘按钮,方便你直接点进去看。 - -新页面的行号我再单独补一下,上一轮通配路径没命中。 - -**已修改** -- 线上购卡凭证码已改成统一 `5` 位,不再生成 `ICO-xxxx`;生成逻辑在 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L60-L67),新建卡记录时也会同时写入 `voucher_code/code` 字段,位置在 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L370-L376)。 -- 购卡结果区已改成更醒目的高对比展示,重点强化了 `凭证码 / 卡号 / 首次充值 / 状态` 的颜色和层级,主逻辑在 [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js#L154-L219),样式在 [style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css)。 -- `ic-card-order.html` 这页我也顺手清掉了残留乱码,并把文案统一成“凭证码”口径,文件在 [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html)。 - -**新页面** -- 已新增独立 IC 卡详情页入口 `ticket.fse-media.group/ic/<卡号>`,服务端路由在 [server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js#L68-L72)。 -- 新页面模板在 [ic-card-detail.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.html#L45-L109),走的是类似电子客票的双栏卡片样式。 -- 页面数据通过公开查询接口加载,脚本在 [ic-card-detail.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.js#L82-L123)。 -- 线上购卡成功后,结果卡片里已新增“卡片详情”按钮,会直接跳到 `/ic/<卡号>`,位置在 [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js#L160-L194)。 - -**CC 键盘** -- 办卡姓名输入已新增 `ABC / SYM` 切换按钮,支持常用符号键盘。 -- 现在姓名输入会走“英文 + 常用符号 + 空格”的收口逻辑,相关代码在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1016-L1102)。 -- `SYM` 按钮和字符追加逻辑分别在 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1070-L1076) 和 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1038-L1043)。 - -**验证** -- [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js)、[server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js)、[ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)、[ic-card-detail.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.html)、[ic-card-detail.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.js)、[style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua) 诊断都为空。 -- `node --check` 已通过 [public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js)、[server/index.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/index.js)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)、[ic-card-detail.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.js)。 - -**注意** -- 这次改的是“新生成的线上购卡凭证码”规则;历史上已经生成出来的 `ICO-xxxx` 老记录不会自动改写,但现有查询仍兼容旧值。 -- 线上要看到新页面和 5 位凭证码,需要重新部署或重载当前服务,并强刷 `ic-card/order`。 -- 如果你要,我下一步可以继续把 `IC 卡查询页` 也加一个“查看电子卡片页”按钮,直接从查询结果跳到 `/ic/<卡号>`。 - ---- - -**User** - -线上购卡那里 自定义充值的框放到那个按钮下面,就是把按钮的高度换成现在的一半,然后把输入框放到下面。 - 有个问题:首次充值的卡(未使用),但是卡详情里面显示余额10,首次充值30. 我们直接改成只显示余额就可以了不显示首次充值 - -**Assistant** - -我先改线上购卡页的自定义充值布局,再把 IC 卡详情页里“首次充值”字段去掉,只保留余额,避免和实际卡内余额混淆。 - -我把自定义金额输入框从“持卡人信息”区挪到充值选项区下面,并且会把“自定义”按钮做得更紧凑一些。然后把详情页里的“首次充值”整块删掉。 - -我已经把布局和详情字段都改完了。现在做一轮检查,确认购卡页和详情页没有新增错误。 - -我再抓一下关键位置的行号:自定义充值框的新位置、按钮高度样式,以及详情页去掉“首次充值”后的字段区。 - -**已调整** -- 线上购卡页的“自定义充值”输入框已经移到充值按钮区下面,位置在 [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html#L66-L78)。 -- “自定义”按钮高度已改成更紧凑的一版,并在选中时显示下面的输入框,逻辑在 [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js#L65-L99)。 -- 对应样式在 [style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css#L3463-L3476)。 - -**卡详情页** -- `/ic/<卡号>` 详情页里已经去掉“首次充值”字段,只保留 `当前余额 / 凭证码 / 开卡时间`,模板在 [ic-card-detail.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.html#L76-L81)。 -- 详情页脚本也同步去掉了对“首次充值”的赋值,现在只写余额、凭证码和开卡时间,见 [ic-card-detail.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.js#L96-L103)。 - -**现在效果** -- 充值区:先点 `自定义`,下面才会出现金额输入框。 -- 输入区:持卡人姓名表单不再混着自定义金额输入框。 -- 卡详情页:不会再出现“余额 10、首次充值 30”这种并排冲突展示,只显示当前余额。 - -**验证** -- [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)、[ic-card-detail.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.html)、[ic-card-detail.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.js)、[style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css) 诊断都为空。 - -**提示** -- 线上页面如果暂时还是旧布局,强刷 `ic-card/order` 和 `/ic/<卡号>` 即可。 -- 如果你愿意,我还可以顺手把 `IC 卡查询页` 里的“首次充值”字段也一起去掉,整站统一成只显示余额。 - ---- - -**User** - -没有看到“自定义充值”输入框 - -**Assistant** - -我先核对购卡页当前 DOM 和切换逻辑,确认是“按钮没切到显示状态”,还是输入框实际被隐藏在别处。 - -我确认到问题点了:现在这版是“只有点了自定义才显示输入框”,在缓存或旧静态文件混用时很容易看不到。 -我准备改成更稳妥的方案:输入框固定显示在按钮区下面,点“自定义”时启用并聚焦,选预设金额时禁用但仍可见。 - -我已经把输入框改成固定显示了,现在只是在“自定义”时高亮并可输入。再做一轮检查确认没有新错误。 - -**已修** -- 我把“自定义充值”输入框改成了**固定显示在按钮区下面**,不再依赖“点了才出现”的隐藏逻辑。 -- 现在的行为是: - - 选 `5 / 10 / 15 / 20` 时:输入框可见,但禁用 - - 选 `自定义` 时:输入框可见、启用,并自动聚焦 - -**改动位置** -- 输入框固定放在充值按钮区下方:[ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html#L66-L71) -- 切换逻辑改成“高亮/启用”,不再做隐藏:[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js) -- 样式补成固定可见的盒子:[style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css) - -**验证** -- [ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js)、[style.css](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/style.css) 诊断均为空。 - -**你现在应该看到** -- “自定义”按钮下方始终有一个输入框 -- 点击“自定义”后,这个输入框会变为可输入状态 - -**提醒** -- 如果线上还没看到,先强刷 `ic-card/order`,因为这次改的是静态页面和前端脚本。 - ---- - -**User** - -卡ID要统一啊,现在各个界面显示的都不一样,有00020,有64AE5246 - -**Assistant** - -我先把 IC 卡 `card_id` 的生成和展示链路梳理一下,重点找出哪里在用“预生成卡号”,哪里在用“MOD 实际返回卡号”,再统一成一套口径。 - -我已经定位到根因了: -现在系统里同时存在两种 ID 口径。 - -- 线上购卡下单时,后端先预生成一个占位卡号,比如 `IC-000020` -- 真正出卡时,MOD 又返回实体卡号,比如 `64AE5246` - -所以有些页面拿的是“预生成号”,有些页面拿的是“实体卡号”,就分裂了。 -我再确认一下检票和查卡接口现在是按哪一个字段找卡,避免直接改显示把进出站链路弄断。 - -我已经确认到一个关键点:后台和公开页面很多地方都直接渲染 `card.card_id`,而这个字段在“线上待领卡”和“实体卡已出卡”两个阶段含义不一样。 -我再读一下 `/api/cards/check`,确认它是不是强依赖当前这条主键,避免我统一显示后把检票查卡弄断。 - -我准备把“卡 ID”的显示口径收成一条规则: -只有 MOD/实体卡真正拿到的那个 ID 才当作对外卡号;线上待领卡阶段不再把 `IC-000020` 这种内部占位号当成“卡 ID”展示。 - -具体会做两层: -- 后端统一给卡对象补一个 `display_card_id` -- 前端各页面都改成显示这个字段,不再直接拿 `card_id` 上屏 - -这样至少页面上不会再一会儿 `00020`、一会儿 `64AE5246`。 - -我开始改后端响应和前端展示,把“显示用卡号”抽成一个统一字段,不碰底层实际查卡主键,这样不会影响检票和数据同步。 - -后端已经补好了统一展示字段。现在把前端几个页面都改成只显示 `display_card_id`,不再直接把内部 `card_id` 上屏。 - -我顺手把旧的独立 IC 管理页也一并改一下,避免你本地如果还开着旧文件时继续看到两套卡号。 - -我已经把主后台、公开查询、详情页、购卡结果页和旧后台页的显示都收成统一字段了。现在做一轮检查,确保没有新增报错。 - -我再把关键定位行抓一下,方便你直接点进去核对“显示用卡号”的规则和几个页面的替换点。 - -先继续收口“卡 ID 统一显示”这件事,我会先把代码里所有仍然直接显示 `card_id` 的地方找出来,再核对后端返回的展示字段是否已经统一。 - ---- - -**User** - -IC卡详情页东西不要那么多,就和电子客票上显示的内容差不多就好了。 - -**Assistant** - -我继续把卡号显示统一收口,同时会顺手把 IC 卡详情页精简成接近电子客票的展示,不再堆太多字段。 - -现在看到后端已经在返回 `display_card_id`,但详情页和结果页还保留了不少“真实卡号”直出逻辑;我先把这些展示入口统一掉,再把详情页内容裁成电子客票那种简洁版。 - -我准备直接改两个点:一是把 `ic-card-detail` 页面收成单卡片、少字段的电子客票样式;二是把待领卡状态下的跳转和复制逻辑改成优先用凭证码,避免看到占位卡号。 - -我已经把详情页主体裁掉了“最近记录”等重内容,接下来再扫一遍剩余直接露出原始 `card_id` 的前端代码,避免还有漏网的显示入口。 - -细节页和线上购卡页已经没有新的报错,我再把旧的 `ic-card-admin` 残留提示也顺手统一一下,避免弹窗里还冒出原始占位号。 - -**已调整** -- 已把详情页精简成接近电子客票的单卡片样式,只保留 `卡号 / 持卡人 / 余额 / 凭证码 / 开卡时间 / 状态提示 / 操作按钮`,去掉了“最近记录”等过重内容,见 [ic-card-detail.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.html#L45-L96) -- 已把详情页逻辑改成统一优先显示 `display_card_id`,并根据状态自动切换提示文案;待领卡时按钮会优先按“凭证码”处理,避免又露出占位卡号,见 [ic-card-detail.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.js#L41-L110) -- 已把线上购卡结果页的“查询此卡 / 卡片详情”跳转改成待领卡时优先使用凭证码,避免后续实体卡出卡后链接还绑着旧占位号,见 [ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js#L159-L177) -- 旧后台页里的创建成功提示和充值弹窗也改成走统一展示口径,不再直接弹原始 `card_id`,见 [ic-card-admin.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.js#L240-L288) - -**当前效果** -- 详情页现在会更像电子客票,不再堆很多字段。 -- 待领卡状态下,页面展示继续是统一口径,不会一会儿是 `IC-000020` 一会儿又是别的显示。 -- 已启用实体卡时,仍然显示并复制真实卡号,检票主键逻辑没有动。 - -**检查** -- 已检查 `ic-card-detail.html`、`ic-card-detail.js`、`ic-card-order.js`、`ic-card-admin.js` 的诊断,当前无新增报错。 - -**建议** -- 如果你要再进一步统一,我下一步可以把公开查询页 `ic-card-search` 也继续裁成和电子客票同一套信息密度,这样所有对外页面观感会更一致。 - ---- - -**User** - -去掉购卡的“订单号”吧。 - 然后在兑换卡的时候没有支付就直接出卡了。 - 卡号改成后面几位随机的。 - 控制台无法进入“IC卡管理”页 - `section` 这一块直接拉宽到和上面 `section` 对齐 - -**Assistant** - -我先分五块排查:购卡页文案、兑卡支付校验、卡号生成规则、后台 `/admin` 视图切换、以及详情页宽度布局,然后一起改掉。 - -我已经定位到后台入口和详情页布局点了,接着看卡号生成和 CC 兑卡支付流程,重点查是不是线上兑卡分支直接跳过了付费确认。 - -问题点找到了:`ticketmachine.lua` 的线上兑卡分支把 `state.cost = 0` 且 `payment_mode = 'online'`,所以确认页会自动放行并直接出卡,这里我会改回“到机支付后才能兑卡”。 - -后台进不去的根因也找到了:顶部导航还写着旧地址 `/ic-card-admin.html`,而现在真正可用的是 `/admin?view=iccards`。我会一并改掉。 - -我会顺手把服务端也加一道“兑卡必须已支付”的校验,不只修前端/CC 流程,这样就算有人直接调接口也不能白拿卡。 - -我顺手把公开页文案里的“订单号”一起淡出,保留“凭证码”作为统一说法;后台内部检索能力不动。 - -改动已经落下去了,我先做一轮诊断检查,重点看 `api/public` 路由、后台页和购卡详情页有没有因为这些改动引入新错误。 - -**已修改** -- 去掉公开购卡/查询页里对“订单号”的展示和文案,统一对外强调“凭证码”,见 [ic-card-search.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.js#L47-L66)、[ic-card-search.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-search.html#L46-L70)、[ic-card-order.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.html#L97-L104)、[ic-card-order.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-order.js#L177-L202) -- 修了“兑卡不付钱直接出卡”问题:CC 端线上兑卡现在会按订单金额收款,且上送真实已付金额;服务端也新增了未付款拒绝兑卡的校验,见 [ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1182-L1193)、[ticketmachine.lua](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/ticketmachine.lua#L1590-L1603)、[api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L790-L799) -- 把系统生成的 IC 卡号从顺序号改成 `IC-` 后 6 位随机数,避免再是 `IC-000020` 这种递增格式,见 [api.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/api.js#L33-L40)、[public.js](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/server/routes/public.js#L50-L57) -- 修了后台“IC 卡管理”入口:顶部导航原来还指向旧地址 `/ic-card-admin.html`,现在统一跳到 `/admin?view=iccards`,见 [index.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/index.html#L42-L46)、[ic-card-admin.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-admin.html)、[ticket-route.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ticket-route.html) -- 把 IC 卡详情页那块 `section` 拉宽,和上面的介绍区保持同一列宽,不再单独缩成 760,见 [ic-card-detail.html](file:///d:/TaoYuan2023_HSTG/HSTG/Lua/TicketMachine/web/ic-card-detail.html#L64-L96) - -**检查** -- 已检查 `server/routes/api.js`、`server/routes/public.js`、`web/index.html`、`web/ic-card-order.js`、`web/ic-card-search.js`、`web/ic-card-search.html`、`web/ic-card-detail.html`、`web/ticket-route.html`,当前无新增诊断错误。 -- 这次没有在实际 ComputerCraft 机器上跑兑卡流程,所以设备侧建议你再现场点一次“线上兑卡”确认红石付费后才会出卡。 - -**建议验证** -- 线上购卡新建一单,确认生成的占位卡号已变成随机 6 位。 -- 在 CC 上输入凭证码后,不投币时应不能确认出卡。 -- 后台顶部导航点击 `IC 卡管理`,应直接进入 `/admin?view=iccards`。 -- `/ic/` 详情页中卡片主区域宽度应与上方介绍区对齐。 - ---- -