用PHP+MySQL实现12306购票和退票以及余票查询逻辑

原创
2017/03/17 07:09
阅读数 8.5K

普通商品的库存之间没有关联性,库存量都是确定的. 火车票跟普通商品不同,同一车次不同路段的车票的库存可能会相互影响. 所以数据库中不应存储某个车次某个路段的余票数量,而应存储该车次该路段已售的车票数量. 也就是普通商品存的是剩下的,而火车票存的是已售的.

车票表:
(车次, 路段(区间), 已售, 限售, 已售座位, 出发日)
(1024, [1,2],     0)
(1024, [1,2,3],   0)
(1024, [1,2,3,4], 0)
(1024, [2,3],     0)
(1024, [2,3,4],   0)
(1024, [3,4],     0)

主键是自增字段,"车次"+"路段"这2个字段组成联合索引,添加唯一约束.
优化并发时,可以考虑根据"出发日"或"车次"进行分表分库.
路段[1,2,3]的含义是:从车站1上车,经过车站2(停站),到车站3下车,编号有序.
如果要给不同的路段配置不同的出票限额,可以添加一个"限售"字段.

购票逻辑:

例如用户购买路段[2,3,4]的车票时,
程序找出跟目标路段[2,3,4]有交集(相交元素>=2)的路段,算法如下:

<?php
function section(array $arr, array &$sub) {
	$size = count($arr);
	if ($size == 1) return;
	for ($i = 2; $i <= $size; $i++) {
		// 从位置0开始,取$i个元素,不影响输入的数组.
		$sub[] = array_slice($arr, 0, $i);
	}
	array_shift($arr); // 删除数组开头元素
	section($arr, $sub); // 递归
}

$all = array(1,2,3,4);
$sub = array();
section($all, $sub); // 找到该车次包含的所有路段
$target = array(2,3,4); // 目标路段
$related = array(); // 目标路段的相关路段(相交路段)
foreach ($sub as $v) {
	$intersect = array_intersect($target, $v);
	if (count($intersect) >= 2) {
		$related[] = $v;
	}
}
var_export($related);
// 得到
[1,2,3]
[1,2,3,4]
[2,3]
[2,3,4]
[3,4]

如果相关路段的票合计小于500(这里假设列车满载为500人),
则用户能够购票,即[2,3,4]这个路段已售的票数+1.

SQL购票逻辑如下(以MySQL为例):

SET AUTOCOMMIT=0;
START TRANSACTION; 或  BEGIN; --开启事务
SELECT 已售 FROM 车票表 WHERE 车次=1024 
AND 路段 IN ('1,2,3', '1,2,3,4', '2,3', '2,3,4', '3,4') FOR UPDATE;
UPDATE 车票表 SET 已售=(已售+1) WHERE 车次=1024 AND 路段='2,3,4';
COMMIT; --提交事务
SET AUTOCOMMIT=1;

其中 select for update 的作用是读上锁(对读出的行上写锁),依赖事务.

select 区间 from 车票表 where 车次=1024 and 区间 like '%2,3,4%';
在本例中得到:
[1,2,3,4]
function foo(array $arr, array &$tmp) {
	$size = count($arr);
	if($size == 1) return;
	for($i=2;$i<=$size;$i++) {
		$tmp[] = array_slice($arr, 0, $i);
	}
	array_shift($arr);
	foo($arr, $tmp);
}
$arr = array(2,3,4);
$tmp = array();
foo($arr, $tmp);
var_export($tmp);
在本例中得到:
[2,3]
[2,3,4]
[3,4]

退票逻辑比购票逻辑简单得多,直接给对应车次,对应区间的已售车票-1即可:

UPDATE 车票表 SET 已售=(已售-1) WHERE 车次=1024 AND 路段='2,3,4';

余票查询逻辑:
例如查询车次1024上区间[2,3,4]的余票:

SELECT 已售 FROM 车票表 WHERE 车次=1024 
AND 路段 IN ('1,2,3', '1,2,3,4', '2,3', '2,3,4', '3,4');

用500减去上述区间已售车票的和就是区间[2,3,4]上的余票.
这里假设列车满载为500人.

座位分配逻辑:
一列火车,其座位都是固定的,存储每列火车的座位的数据表没什么可说的.
至于座位分配,可以在车票表里增加一个"已售座位"的字段.
用户成功购票时,从"待售座位"中取一个分配给用户并更新"已售座位"字段.
"待售座位"为"所有座位"去掉相关路段"已售座位"后的座位.

SET AUTOCOMMIT=0;
BEGIN;
SELECT 已售,已售座位 FROM 车票表 
WHERE 车次=1024 AND 路段 IN ('1,2,3', '1,2,3,4', '2,3', '2,3,4', '3,4') FOR UPDATE;
UPDATE 车票表 SET 已售=(已售+1), 已售座位=CONCAT(已售座位,分配座位) 
WHERE 车次=1024 AND 路段='2,3,4';
COMMIT;
SET AUTOCOMMIT=1;

其中CONCAT相比直接赋值,能够减少传递给MySQL的数据量.
车票表中"已售座位"字段存储"座位编号".
座位表: 座位编号, 列车, 车厢, 座位

 

展开阅读全文
加载中
点击加入讨论🔥(11) 发布并加入讨论🔥
打赏
11 评论
9 收藏
5
分享
返回顶部
顶部