feat(ticket): 新增单程票购票与检票功能,优化日志与接口
- 前端新增单程票购票界面,支持票种切换与区间选择、价格预览 - 完善后端检票接口,添加单程票的校验与处理逻辑 - 优化售票机脚本的车票类型映射,新增单程车票支持 - 简化前端售票日志输出,移除冗余站点信息字段 - 新增服务层付费区间计算字段,优化公共接口返回数据
This commit is contained in:
+23
-9
@@ -1336,18 +1336,23 @@ router.post('/tickets/check', async (req, res) => {
|
||||
toFiniteNumberOrUndef(hintTripsRemaining);
|
||||
const tripsRemainEffective = (tripsRemain0 == null) ? tripsTotal : tripsRemain0;
|
||||
|
||||
const isSingleTicket = String(cur.train_type || '').toLowerCase() === 'single';
|
||||
|
||||
if (action === 'entry') {
|
||||
if (!matchesStation(start)) return deny('wrong_station');
|
||||
if (!isSingleTicket && !matchesStation(start)) return deny('wrong_station');
|
||||
if (entered && !exited) return deny('already_entered');
|
||||
|
||||
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: start, station_name: resolveStationName(start), device };
|
||||
const entryCode = isSingleTicket ? (station_code_raw || start) : start;
|
||||
|
||||
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: entryCode, station_name: resolveStationName(entryCode), device };
|
||||
await DataService.appendTicketEvent(ev);
|
||||
await DataService.upsertTicketIndex({
|
||||
ticket_id,
|
||||
entered: true,
|
||||
exited: false,
|
||||
last_action: 'entry',
|
||||
last_station_code: start,
|
||||
last_station_code: entryCode,
|
||||
start: isSingleTicket ? entryCode : cur.start,
|
||||
last_result: 'pass',
|
||||
last_reason: '',
|
||||
last_event: 'check',
|
||||
@@ -1357,22 +1362,31 @@ router.post('/tickets/check', async (req, res) => {
|
||||
});
|
||||
io.emit('ticket:check', ev);
|
||||
appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
|
||||
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: start, trips_remaining: tripsRemainEffective, destroy_ticket: false });
|
||||
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: entryCode, trips_remaining: tripsRemainEffective, destroy_ticket: false });
|
||||
}
|
||||
|
||||
const exitCode = station_code_raw || terminal;
|
||||
if (isSingleTicket) {
|
||||
const fareInfo = LogicService.computeFareBoth(cur.start, exitCode);
|
||||
if (!fareInfo || fareInfo.paid_segments !== 1) {
|
||||
return deny('非单区间');
|
||||
}
|
||||
} else {
|
||||
if (!matchesStation(terminal)) return deny('wrong_station');
|
||||
}
|
||||
|
||||
if (!matchesStation(terminal)) return deny('wrong_station');
|
||||
if (!entered) return deny('not_entered');
|
||||
if (exited) return deny('already_exited');
|
||||
|
||||
const newTripsRemaining = Math.max(0, (Number(tripsRemainEffective) || 0) - 1);
|
||||
const shouldSetUsed = newTripsRemaining <= 0;
|
||||
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: terminal, station_name: resolveStationName(terminal), device, trips_remaining: newTripsRemaining };
|
||||
const shouldSetUsed = isSingleTicket ? true : (newTripsRemaining <= 0);
|
||||
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: exitCode, station_name: resolveStationName(exitCode), device, trips_remaining: newTripsRemaining };
|
||||
await DataService.appendTicketEvent(ev);
|
||||
await DataService.upsertTicketIndex({
|
||||
ticket_id,
|
||||
exited: true,
|
||||
last_action: 'exit',
|
||||
last_station_code: terminal,
|
||||
last_station_code: exitCode,
|
||||
last_result: 'pass',
|
||||
last_reason: '',
|
||||
last_event: 'check',
|
||||
@@ -1383,7 +1397,7 @@ router.post('/tickets/check', async (req, res) => {
|
||||
});
|
||||
io.emit('ticket:check', ev);
|
||||
appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
|
||||
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: terminal, trips_remaining: newTripsRemaining, destroy_ticket: shouldSetUsed });
|
||||
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: exitCode, trips_remaining: newTripsRemaining, destroy_ticket: shouldSetUsed });
|
||||
});
|
||||
|
||||
router.post('/tickets/status', async (req, res) => {
|
||||
|
||||
@@ -714,13 +714,19 @@ router.get('/tickets/:id', async (req, res, next) => {
|
||||
action: e.action,
|
||||
ts: e.ts,
|
||||
ticket_id: e.ticket_id,
|
||||
station_name: nameFor(e.station_code),
|
||||
station_name: e.station_name || nameFor(e.station_code),
|
||||
station_code: e.station_code || '',
|
||||
station_en: e.station_code ? enNameFor(e.station_code) : (e.station_en || ''),
|
||||
trips_remaining: e.trips_remaining ?? e.rides_remaining ?? null
|
||||
};
|
||||
} else if (e.type === 'check') {
|
||||
return {
|
||||
...e,
|
||||
station_name: e.station_name || nameFor(e.station_code),
|
||||
station_en: e.station_code ? enNameFor(e.station_code) : (e.station_en || '')
|
||||
};
|
||||
}
|
||||
return { raw: e };
|
||||
return e;
|
||||
});
|
||||
|
||||
appendReqLog(req, { category: 'public', type: 'ticket_detail', detail: { ticket_id: id, ok: true } });
|
||||
|
||||
@@ -231,7 +231,8 @@ const LogicService = {
|
||||
regular_path: [src],
|
||||
express_path: [src],
|
||||
regular_transfers: [],
|
||||
express_transfers: []
|
||||
express_transfers: [],
|
||||
paid_segments: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,6 +246,7 @@ const LogicService = {
|
||||
const totals = accumulateFareAlongPath(graph, result.path);
|
||||
if (!totals) return null;
|
||||
const transfers = computeTransfersAlongPath(graph, result.path);
|
||||
const dstCost = result.dist.get(dst);
|
||||
|
||||
return {
|
||||
regular: Math.round(Number(totals.regular ?? 0) || 0),
|
||||
@@ -253,6 +255,7 @@ const LogicService = {
|
||||
express_path: result.path,
|
||||
regular_transfers: transfers,
|
||||
express_transfers: transfers,
|
||||
paid_segments: dstCost ? dstCost.paidSegments : Infinity
|
||||
};
|
||||
} catch (_) { return null; }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user