Files
FSE-Ticket.sys/server/services/logic.js
T

317 lines
10 KiB
JavaScript
Raw Normal View History

2026-06-21 10:00:13 +08:00
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;