简介:这份文档面向软件工程学习者与系统设计入门者,围绕中国铁路窗口售票系统展开需求分析,帮助读者理解从用户购票视角梳理业务流程、明确功能边界并形成规范需求说明的完整思路。压缩包内共1个docx文件,约275KB,为一份结构完整的《车站窗口售票系统》需求说明书,含引言、总体要求、业务需求、辅助模块与结语等章节,并附修订记录与版本信息。正文依次覆盖系统总体目标、功能要求与体系架构,订票、改签、退票三大业务模块流程,基本票价与优惠票价的计算方法,以及火车信息、个人信息等辅助管理内容,可作为课程设计、需求文档写作或系统分析练习的参考模板。目前已有582人学习下载,适合需要了解售票类系统需求建模与文档组织方式的读者查阅借鉴。
1. 窗口售票需求文档:一份能直接拆成开发任务的 V3.0 底稿
窗口售票系统这几年被线上购票挤到边缘,但真到春运、到务工人员集中出行、到证件核验出问题的现场,窗口仍是兜底通道。这份《车站窗口售票系统》需求说明书 V3.0.0 就是围绕这个兜底场景写的:订票、改签、退票三条主业务线,加上票价计算和辅助信息管理,把售票员在柜台前要点的每一个按钮都落到了文字上。它适合两类人:一是要照着做课程设计或毕业设计的同学,二是接手存量 WinForm + WCF 售票客户端、需要快速摸清业务边界的开发者。文档本身不是代码,但它把业务规则写得足够细,细到退票费率的时间窗、改签的次数限制、票价的分段累加公式都能直接翻译成 if-else 和数据库字段。下面按「文档怎么读 → 规则怎么落 → 坑在哪」的顺序拆。
2. 三条主业务线的规则拆解:订票、改签、退票各自卡在哪
这份文档最有价值的部分不是目录结构,而是 3.1 节里对三个业务模块的约束描述。很多需求文档写到「支持退票」就结束了,这份写到了「开车前 15 天不收手续费、48 小时到 15 天收 5%、24 到 48 小时收 10%、不足 24 小时收 20%」。这种颗粒度才是能拿去写代码的。下面把三条线分别拆开,说清楚每条线的状态流转和必须落库的字段。
2.1 订票模块:查询条件、中转查询与订单生成
订票的入口是车票查询,文档明确三个必选条件:出发地、目的地、出发时间。查询结果要带回车次、席别类型和单价。这里有个容易被忽略的点——中转查询。它不是简单的两次单程拼接,而是要在出发地和目的地之间插入一个中转站,查出「出发地→中转站」和「中转站→目的地」两段车次,且两段的时间要能接上。落到实现上,查询接口至少要有两个分支:
-- 直达查询:三条件必填,返回车次+席别+余票+单价 SELECT t.train_no, t.seat_type, t.remain_count, t.unit_price FROM train_inventory t WHERE t.from_station = :fromStation AND t.to_station = :toStation AND t.depart_date = :departDate AND t.remain_count > 0; -- 中转查询:先查第一段,再用第一段到达站作为第二段出发站 SELECT a.train_no AS leg1_train, a.arrive_time AS leg1_arrive, b.train_no AS leg2_train, b.depart_time AS leg2_depart FROM train_inventory a JOIN train_inventory b ON b.from_station = a.to_station AND b.to_station = :toStation AND b.depart_date = a.depart_date WHERE a.from_station = :fromStation AND a.to_station = :transferStation AND a.remain_count > 0 AND b.remain_count > 0 AND b.depart_time > a.arrive_time; -- 换乘时间必须为正第一段 SQL 是直达查询,条件全部来自界面上的三个下拉/日期控件,remain_count > 0过滤掉售罄车次。第二段是中转查询的核心:用JOIN把两段车次按「第一段到达站 = 第二段出发站」串起来,最后那个b.depart_time > a.arrive_time是血泪经验——不加这个条件,系统会把到达时间早于出发时间的荒谬组合也返回给售票员,柜台前当场翻车。参数上,:transferStation由售票员在界面上选,文档没规定中转站怎么选,常见做法是给一个沿线主要站点的下拉列表,或者让售票员手动输入站名做模糊匹配。
订单生成环节,文档写的是「选择车票及对应乘客后,系统统计总金额,旅客完成支付后点击完成订单,生成车票并打印」。这里要落库的实体至少四个:订单主表(订单号、总金额、支付方式、创建时间、状态)、订单明细(车次、席别、乘客证件号、单价)、乘客信息(姓名、证件类型、证件号)、车票记录(票号、打印时间、检票状态)。状态机建议用枚举而不是字符串:待支付 → 已支付 → 已出票 → 已检票 / 已退票 / 已改签。很多课程设计在这里用字符串存状态,后期加一个「已改签」状态就要全表扫一遍改数据,后悔药都来不及吃。
2.2 改签模块:一次限制与差额退补的边界
改签的规则文档写得很密,提炼成可执行约束是四条:第一,变更到站只能在预售期内、开车前 48 小时以上办理,且不收手续费;第二,变更到站只办一次,已办过的车票不再办理改签,已改签的车票也不提供变更到站;第三,批量改签要求同一订单中日期、车次、出发站、目的站、乘坐类型全部相同;第四,到站不变只改时间时,新车票价格低于原票退差额并核收退票费,高于原票补差额同样核收退票费。
第四条是最容易写错的。文档原文是「对差额部分核收退票费并执行现行退票费标准」,意思是退票费只针对差额算,不是针对全款。举个具体例子:原票 300 元,改签后新票 260 元,差额 40 元,退票费率若为 5%,则退给旅客 40 - 40×5% = 38 元,而不是 300 - 300×5%。实现上要单独写一个差额结算函数:
def settle_change_fee(old_price, new_price, refund_rate): """改签差额结算:只对差额部分核收退票费""" diff = old_price - new_price if diff > 0: # 新票便宜,退差额 fee = round(diff * refund_rate, 2) return {"action": "refund", "amount": round(diff - fee, 2), "fee": fee} elif diff < 0: # 新票贵,补差额 fee = round(abs(diff) * refund_rate, 2) return {"action": "charge", "amount": round(abs(diff) + fee, 2), "fee": fee} return {"action": "none", "amount": 0, "fee": 0}refund_rate不能写死,要按改签操作发生时距原车次开车的时间窗动态取,和退票费率共用同一张费率表。round(..., 2)是必须的,票价计算里浮点误差累积到分位,打印出来的车票金额和系统记录对不上,对账时就是黑匣子。另外「只允许改签一次」这个约束,建议在车票记录表加一个change_count字段,改签前先查,大于等于 1 直接拒绝,别指望前端按钮置灰能拦住所有入口。
2.3 退票模块:时间窗费率与交易留痕
退票的费率表文档给得完整:开车前 15 天以上免手续费,48 小时到 15 天收 5%,24 到 48 小时收 10%,不足 24 小时收 20%。还有两条特殊规则:改签或变更到站到 15 天以上其他列车、又在 15 天前退票的,仍收 5%;紧急情况短期改签退票不受影响,特殊情况没赶上车也能在开车后改签当日有余票的其他列车。
费率计算的关键是「以什么时间点为基准」。文档说的是「距开车前」,所以基准是原车次的发车时间,不是退票操作时间,也不是改签后的新车次时间。实现时把发车时间、当前时间、费率表三者喂给一个纯函数,别在业务代码里散落 if-else:
from datetime import datetime REFUND_RULES = [ (15 * 24, 0.00), # 15天以上,免手续费 (48, 0.05), # 48小时~15天,5% (24, 0.10), # 24~48小时,10% (0, 0.20), # 不足24小时,20% ] def calc_refund_fee(price, depart_time, now=None): now = now or datetime.now() hours = (depart_time - now).total_seconds() / 3600 for threshold, rate in REFUND_RULES: if hours >= threshold: fee = round(price * rate, 2) return {"refund": round(price - fee, 2), "fee": fee, "rate": rate} return {"refund": 0, "fee": price, "rate": 1.0}REFUND_RULES按小时阈值从大到小排列,第一个命中的就是适用费率,这样加新档位只改列表不改逻辑。hours为负说明已过发车时间,落到最后返回全额扣费——实际业务里这种情况要走「开车后改签」的单独流程,不能直接按退票处理,这里只是兜底。文档还强调「退票即作废订单信息,但交易记录保留」,所以退票不是DELETE,是把订单状态改成已退票并写一条退款流水,票号作废但可追溯。这条如果做成物理删除,后期审计查不到任何痕迹,属于典型的翻车设计。
3. 票价计算落地:分段累加公式怎么变成可测函数
票价是这份文档里唯一带完整数学公式的部分,也是最能体现「需求能不能直接开发」的试金石。文档给的步骤是:基础票价 = 区段里程 × 区段票价率 + 前各区段累计票价,然后软座翻倍、保险 2%(2013 年已取消)、附加票价按车种和席别乘系数、附加费按条件加收,最后联合票价 =(基本票价 + 附加票价)×(1 + 上浮率)+ 附加费。这套公式看着像会计题,拆开就是一张费率和一张分段表。
3.1 基础票价的分段累加与费率表
文档的例子是里程 2375 公里,计算式(2375 - 1500) × 0.035166 + 71.5042 = 102.27445。这个式子透露了两个信息:1500 公里是一个分段节点,71.5042 是前 1500 公里的累计票价,0.035166 是超出部分的每公里费率。也就是说基础票价不是单一费率乘里程,而是分段累加。合格的做法是把分段表抽成配置:
| 区段起点(km) | 区段终点(km) | 累计票价(元) | 区段费率(元/km) |
|---|---|---|---|
| 0 | 200 | 0 | 按短途表 |
| 200 | 1500 | 累计值 | 按中途表 |
| 1500 | 以上 | 71.5042 | 0.035166 |
实际费率表比这长得多,文档只给了 1500 以上这一段作为示例,实现时按「这个资源场景下合格从业者最可能用的方案」——把完整分段表放数据库或配置文件,代码只负责查段、算差值、累加。函数签名建议base_fare(mileage) -> Decimal,全程用Decimal不用float,票价算到分,浮点误差在几千张票的批量结算里会放大成对不上的账。
3.2 附加票价系数与联合票价组装
基础票价出来后,附加票价按车种和席别乘系数,文档列了:普快 20%、快速(K/T) 40%、空调 25%、硬卧上 110%、硬卧中 120%、硬卧下 130%、软卧上 175%、软卧下 195%。注意这些系数是乘在基础票价上的,且卧铺系数和车种系数可能叠加,具体叠加顺序文档没写死,常见做法是先算车种附加再算席别附加,或者按业务确认的优先级表来。附加费三项是固定规则:客票发展金票价不大于 5 元收 0.5 元、大于 5 元收 1 元;乘车超 200 公里硬席收 1 元候车室空调费;卧铺收 10 元订票费。
组装顺序严格按文档:联合票价 = (基本票价 + 附加票价) × (1 + 上浮率) + 附加费。上浮率是政策参数,默认 0,但要留字段,春运或特定线路调价时不用改代码。这里有个参数说明必须写进注释:保险费 2% 已于 2013 年取消,文档保留这一行是历史沿革,新系统不要再加回去,否则票价整体偏高,测试对账时会被当成 bug 查半天。
4. 避坑与排查:需求文档转开发时最容易翻车的五处
4.1 退票费率基准时间取错
现象:同一张票,测试环境算出的退票费和手工核算差一个档位。原因:代码用了「当前时间」和「改签后新车次发车时间」做差,而规则基准是原车次发车时间。解决:把原车次发车时间作为必传参数固化进退票函数,改签场景下单独传原车次时间,并在单元测试里覆盖 15 天、48 小时、24 小时三个边界的前后各一分钟。
4.2 改签次数限制只做在前端
现象:按钮置灰后,通过接口直接调用仍能二次改签。原因:change_count校验只写在界面层。解决:校验下沉到服务层,改签事务里先SELECT ... FOR UPDATE锁住车票记录再判断次数,并发下也不会漏。
4.3 中转查询返回时间倒挂组合
现象:售票员查到「北京→郑州→广州」但第二段发车早于第一段到达。原因:中转 SQL 漏了换乘时间正数约束。解决:加b.depart_time > a.arrive_time,并给换乘时间设一个最小间隔(常见 30 分钟),避免同站台秒换乘的不可行方案。
4.4 票价用 float 导致分位对不上
现象:批量出票后总金额和逐张相加差几分钱。原因:全程float运算。解决:金额字段数据库用DECIMAL(10,2),代码用Decimal,所有round显式指定两位,费率表也存成字符串再转Decimal。
4.5 退票做成物理删除
现象:审计要查某张票的退票记录,数据库里找不到。原因:退票逻辑写了DELETE。解决:订单状态改已退票,票号标记作废,退款流水单独入表,保留原订单和明细,只追加不覆盖。
5. 把需求文档当契约用:一份可执行的验收清单
需求文档最容易沦为写完就锁进抽屉的摆设,这份 V3.0.0 的价值在于它把规则写到了可以反向生成测试用例的程度。我一般会从文档里抽三类内容做成验收清单:状态流转、金额计算、边界条件。状态流转覆盖订单从待支付到已出票、已退票、已改签的每条边;金额计算把票价公式和退票费率表各做一组参数化用例,输入里程/票价/时间窗,断言输出精确到分;边界条件专挑文档里带「只」「不再」「不足」「以上」字样的句子,每一句都是一个测试点。
举个具体做法,把退票费率表直接转成 pytest 的参数化数据:
import pytest from datetime import datetime, timedelta @pytest.mark.parametrize("hours_before,expected_rate", [ (360, 0.00), # 15天以上 (72, 0.05), # 48小时~15天 (36, 0.10), # 24~48小时 (12, 0.20), # 不足24小时 ]) def test_refund_rate(hours_before, expected_rate): depart = datetime(2024, 1, 20, 8, 0) now = depart - timedelta(hours=hours_before) result = calc_refund_fee(300, depart, now) assert result["rate"] == expected_rate这段测试的意义不是覆盖率数字好看,而是把文档里那段费率文字变成了机器可验证的契约。以后谁改费率表,测试先红,改的人就得回头确认是需求变了还是手滑了。同理,改签的「只办一次」、中转的「换乘时间为正」、票价的「分段累加」都能各写一组。文档里那些「可能全额退款」「要看符合哪条规定」的模糊表述,在验收清单里必须被逼成确定的输入输出,逼不出来的,就是需求还没聊透,得回去找业务确认,而不是带着模糊上线。
从那以后我每次拿到这类需求说明书,第一件事不是看目录,而是把带数字和「只/不再/不足」的句子全部标黄,逐条转成测试用例,转不动的当场记下来当待确认项。这份 V3.0.0 的底子够扎实,三条业务线的规则密度足以支撑一个完整的课程设计或存量系统重构,照着上面的拆法走一遍,能省掉不少对着柜台业务拍脑袋的时间。希望帮到你。
本文还有配套的精品资源,点击获取