const DataService = require('./data'); const buildRouteGraph = (stations, lines, fares, cfg) => { const valid = new Set((stations || []).map(s => String(s?.code || '').trim()).filter(Boolean)); const graph = new Map(); const segKey = (a, b) => [String(a || '').trim(), String(b || '').trim()].sort().join('|'); const ensureNode = (code) => { if (!graph.has(code)) graph.set(code, new Map()); }; const addEdge = (u, v, edge) => { if (!u || !v || u === v) return; if (!valid.has(u) || !valid.has(v)) return; ensureNode(u); ensureNode(v); const prevUV = graph.get(u).get(v); const prevVU = graph.get(v).get(u); const next = (() => { if (!prevUV) return edge; if ((edge?.paid_segments ?? 0) !== (prevUV?.paid_segments ?? 0)) { return (edge.paid_segments < prevUV.paid_segments) ? edge : prevUV; } if ((edge?.transfer_count ?? 0) !== (prevUV?.transfer_count ?? 0)) { return (edge.transfer_count < prevUV.transfer_count) ? edge : prevUV; } const prevFare = (prevUV?.regular ?? 0) + (prevUV?.express ?? 0); const nextFare = (edge?.regular ?? 0) + (edge?.express ?? 0); return nextFare < prevFare ? edge : prevUV; })(); graph.get(u).set(v, next); graph.get(v).set(u, prevVU && prevVU !== prevUV ? prevVU : next); }; for (const code of valid) ensureNode(code); const fareBySegment = new Map(); for (const f of (fares || [])) { const a = String(f?.from || '').trim(); const b = String(f?.to || '').trim(); if (!a || !b) continue; fareBySegment.set(segKey(a, b), { regular: Number(f?.cost_regular ?? f?.cost ?? 0) || 0, express: Number(f?.cost_express ?? f?.cost ?? (f?.cost_regular ?? f?.cost ?? 0)) || 0 }); } let segmentEdgeCount = 0; for (const line of (lines || [])) { const stops = Array.isArray(line?.stations) ? line.stations : (Array.isArray(line?.stops) ? line.stops : []); for (let i = 0; i < stops.length - 1; i++) { const a = String(stops[i] || '').trim(); const b = String(stops[i + 1] || '').trim(); if (!a || !b || a === b) continue; const fare = fareBySegment.get(segKey(a, b)); if (!fare) continue; addEdge(a, b, { type: 'segment', regular: fare.regular, express: fare.express, paid_segments: 1, transfer_count: 0 }); segmentEdgeCount += 1; } } if (segmentEdgeCount === 0) { for (const [key, fare] of fareBySegment.entries()) { const [a, b] = key.split('|'); addEdge(a, b, { type: 'segment', regular: fare.regular, express: fare.express, paid_segments: 1, transfer_count: 0 }); } } const addTransferEdge = (u, v) => { addEdge(u, v, { type: 'transfer', regular: 0, express: 0, paid_segments: 0, transfer_count: 1 }); }; const transfers = Array.isArray(cfg?.transfers) ? cfg.transfers : []; for (const p of transfers) { const u = String(p?.[0] || '').trim(); const v = String(p?.[1] || '').trim(); if (!u || !v) continue; addTransferEdge(u, v); } for (const s of (stations || [])) { if (!s?.transfer_enabled) continue; const from = String(s?.code || '').trim(); if (!from) continue; const tos = Array.isArray(s.transfer_to) ? s.transfer_to : []; for (const t of tos) { const to = String((typeof t === 'string') ? t : (t?.code || t?.station || t?.id || t?.[0] || '')).trim(); if (!to) continue; addTransferEdge(from, to); } } return graph; }; const makeRouteCost = (paidSegments = Infinity, transfers = Infinity, hops = Infinity) => ({ paidSegments, transfers, hops }); const compareRouteCost = (a, b) => { if ((a?.paidSegments ?? Infinity) !== (b?.paidSegments ?? Infinity)) { return (a?.paidSegments ?? Infinity) - (b?.paidSegments ?? Infinity); } if ((a?.transfers ?? Infinity) !== (b?.transfers ?? Infinity)) { return (a?.transfers ?? Infinity) - (b?.transfers ?? Infinity); } return (a?.hops ?? Infinity) - (b?.hops ?? Infinity); }; const addRouteCost = (base, edge) => makeRouteCost( (base?.paidSegments ?? Infinity) + (edge?.paid_segments ?? 0), (base?.transfers ?? Infinity) + (edge?.transfer_count ?? 0), (base?.hops ?? Infinity) + 1 ); const findRoutePath = (graph, src, dst) => { const dist = new Map(); const prev = new Map(); const visited = new Set(); for (const n of graph.keys()) dist.set(n, makeRouteCost()); if (!graph.has(src)) return { dist, path: null, prev }; dist.set(src, makeRouteCost(0, 0, 0)); while (true) { let u = null; let best = makeRouteCost(); for (const n of graph.keys()) { if (visited.has(n)) continue; const d = dist.get(n); if (u == null || compareRouteCost(d, best) < 0) { best = d; u = n; } } if (u == null || !Number.isFinite(best.paidSegments)) break; if (u === dst) break; visited.add(u); for (const [v, edge] of graph.get(u).entries()) { const nd = addRouteCost(best, edge); if (compareRouteCost(nd, dist.get(v)) < 0) { dist.set(v, nd); prev.set(v, u); } } } const dstCost = dist.get(dst); if (!dstCost || !Number.isFinite(dstCost.paidSegments)) return { dist, path: null, prev }; const path = []; let cur = dst; while (cur != null) { path.push(cur); if (cur === src) break; cur = prev.get(cur); } if (path[path.length - 1] !== src) return { dist, path: null, prev }; path.reverse(); return { dist, path, prev }; }; const accumulateFareAlongPath = (graph, path) => { if (!Array.isArray(path) || path.length < 2) return { regular: 0, express: 0 }; let regular = 0; let express = 0; for (let i = 0; i < path.length - 1; i++) { const edge = graph.get(path[i])?.get(path[i + 1]); if (!edge) return null; if (edge.type === 'transfer') continue; regular += Number(edge.regular ?? 0) || 0; express += Number(edge.express ?? 0) || 0; } return { regular, express }; }; const computeTransfersAlongPath = (graph, path) => { if (!Array.isArray(path) || path.length < 2) return []; const out = new Set(); for (let i = 0; i < path.length - 1; i++) { const a = path[i]; const b = path[i + 1]; const edge = graph.get(a)?.get(b); if (edge?.type === 'transfer') { out.add(a); out.add(b); } } return Array.from(out); }; const LogicService = { // Compute ticket price based on fares and lines computePrice: (fromCode, toCode, trainType) => { try { const cfg = DataService.getConfig(); const baseBoth = LogicService.computeFareBoth(fromCode, toCode); const baseReg = Number(baseBoth?.regular ?? 0) || 0; const baseExp = Number(baseBoth?.express ?? 0) || 0; const discount = Number(cfg?.promotion?.discount ?? 1); const base = (trainType === 'Express') ? baseExp : baseReg; return Math.floor(base * (discount > 0 ? discount : 1)); } catch (_) { return 0; } }, computeFareBoth: (fromCode, toCode) => { try { const src = String(fromCode || '').trim(); const dst = String(toCode || '').trim(); if (!src || !dst) return null; if (src === dst) { return { regular: 0, express: 0, regular_path: [src], express_path: [src], regular_transfers: [], express_transfers: [] }; } const cfg = DataService.getConfig(); const stations = DataService.getStations(); const lines = DataService.getLines(); const fares = DataService.getFares(); const graph = buildRouteGraph(stations, lines, fares, cfg); const result = findRoutePath(graph, src, dst); if (!Array.isArray(result.path)) return null; const totals = accumulateFareAlongPath(graph, result.path); if (!totals) return null; const transfers = computeTransfersAlongPath(graph, result.path); return { regular: Math.round(Number(totals.regular ?? 0) || 0), express: Math.round(Number(totals.express ?? 0) || 0), regular_path: result.path, express_path: result.path, regular_transfers: transfers, express_transfers: transfers, }; } catch (_) { return null; } }, accumulateLineFare: (fromCode, toCode, fares, lines) => { if (!Array.isArray(lines)) return null; const line = lines.find(l => { const stations = Array.isArray(l.stations) ? l.stations : (Array.isArray(l.stops) ? l.stops : []); return stations.includes(fromCode) && stations.includes(toCode); }); if (!line) return null; const arr = Array.isArray(line.stations) ? line.stations : (Array.isArray(line.stops) ? line.stops : []); const i = arr.indexOf(fromCode); const j = arr.indexOf(toCode); if (i === -1 || j === -1) return null; const start = Math.min(i, j); const end = Math.max(i, j); let regular = 0, express = 0; for (let k = start; k < end; k++) { const a = arr[k], b = arr[k + 1]; const f = fares.find(x => (x.from === a && x.to === b) || (x.from === b && x.to === a)); if (!f) return null; const vr = Number((f.cost_regular ?? f.cost ?? 0)) || 0; const ve = Number((f.cost_express ?? f.cost ?? 0)) || 0; regular += vr; express += ve; } return { regular, express }; }, genVoucherCode: () => { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789'; const pick = (n) => Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); let code = pick(5); const idx = DataService.getOrderIndex(); while (idx[code]) { code = pick(5); } return code; }, // Helper for SVG generation buildStationNameMap: () => { try { const arr = DataService.getStations(); const map = {}; for (const s of arr) { map[s.code] = s.name || s.cn_name || s.station_name || s.code; } return map; } catch (_) { return {}; } }, generateLightFareMapSVG: () => { const stations = DataService.getStations(); const lines = DataService.getLines(); const fares = DataService.getFares(); const nameByCode = LogicService.buildStationNameMap(); return require('./svg-generator').generate(stations, lines, fares, nameByCode); } }; module.exports = LogicService;