简介:这份文档面向软件工程学习者、系统分析与设计初学者以及需要撰写课程作业或项目文档的读者,围绕中国铁路窗口售票系统展开需求分析,帮助读者理解从业务目标到功能落地的完整梳理思路。压缩包内共1个docx文件,约275KB,为一份结构完整的需求说明书,涵盖引言、总体要求、业务需求、辅助模块与结语等章节,其中总体要求部分细化了系统总体目标、功能要求与体系架构,业务需求部分则拆解了订票、改签、退票三大模块及票价计算方法,辅助模块还涉及火车信息查看与个人信息管理。已有582人学习下载,适合作为需求分析文档的写作范本,也可用于对照理解售票类系统的模块划分、业务流程描述与文档组织方式,为后续系统设计与开发提供参考依据。
1. 窗口售票系统需求分析:一份能直接交作业的完整方案
很多人做售票系统课设,第一反应是打开 IDE 先画界面,结果做到一半发现改签规则和退票费率根本对不上,返工重来。这份需求分析方案要解决的就是这个问题:先把业务规则、票价算法、模块边界定死,再谈 WinForm 界面和 WCF 通信。它适合正在做软件工程课设、毕业设计开题,或者需要交付一份需求规格说明书的同学和初级工程师。整套文档按引言、总体要求、业务需求、辅助模块四块组织,核心是把订票、改签、退票三条主线和票价计算规则讲透,让后续开发、测试、验收都有据可依。
2. 总体要求与架构选型:WinForm+WCF 的三层落地方式
2.1 系统目标与功能边界怎么定
窗口售票系统的定位很明确:服务不会用线上渠道的旅客,同时给售票员一个高效率的操作台。这个定位决定了功能边界——它不需要做推荐算法,不需要做社交分享,但必须把余票查询、订票、改签、退票、票价查询、中转查询、基础信息管理这几件事做扎实。原文把功能要求归纳为一条主线:以购票为核心,改签退票为延伸,余票和中转查询为支撑,基础信息设置为底座。
我一般会先把功能拆成「必须做」和「可以砍」两档。必须做的是订票全流程(查询→选座→核验证件→支付→出票)、改签一次限制、退票费率计算;可以砍的是统计分析、会员积分这类锦上添花的东西。课设时间有限,先把主流程跑通比堆功能重要得多。
功能清单建议用一张表锁死,避免开发中途加需求:
| 模块 | 核心功能 | 优先级 |
|---|---|---|
| 订票 | 余票查询、中转查询、下单出票 | 高 |
| 改签 | 变更到站/时间、差额结算 | 高 |
| 退票 | 按时间梯度收手续费、退款 | 高 |
| 票价 | 基础票价+附加票价+附加费 | 高 |
| 辅助 | 车次信息、线路查询、个人信息 | 中 |
2.2 为什么选 WinForm+WCF 而不是纯 Web
原文明确写了体系架构基于 WinForm+WCF 与后台逻辑交互。这个选型在窗口售票场景下是合理的:售票员用的是内网桌面终端,WinForm 响应快、打印车票对接本地设备方便;WCF 负责把业务逻辑封装成服务,前端只管调接口,票价计算、余票扣减这些核心逻辑放在服务端,避免客户端被篡改。
三层职责这样划分:
- 表现层(WinForm):查询界面、订单界面、改签退票界面,只做输入校验和结果展示
- 服务层(WCF):订票服务、改签服务、退票服务、票价服务,每个服务暴露明确的方法契约
- 数据层:车次表、余票表、订单表、票价规则表、用户表
WCF 服务契约建议这样定义,把业务方法签名先固定下来:
// 订票服务契约:查询与下单分离,便于单独测试 [ServiceContract] public interface ITicketService { // 余票查询:出发地、目的地、日期为必填 [OperationContract] List<TrainInfo> QueryRemaining(string from, string to, DateTime date); // 中转查询:多一个中转站参数 [OperationContract] List<TransferRoute> QueryTransfer(string from, string to, string via, DateTime date); // 下单:传入乘客证件与车次席别,返回订单号 [OperationContract] OrderResult CreateOrder(OrderRequest request); }这段契约的关键在于把「查询」和「下单」拆成两个方法。查询是只读的,可以缓存;下单涉及余票扣减,必须走事务。很多同学把两者塞进一个方法,结果并发测试时余票扣成负数,这就是没拆开的代价。参数里from、to、date是查询三要素,缺一不可,中转查询额外加via。
2.3 数据库核心表设计
需求阶段不用把字段抠到极致,但主键、外键、关键状态字段必须定下来。订单表的状态机是整个系统的黑匣子,建议用枚举值管理:
-- 订单主表:状态字段决定后续可执行的操作 CREATE TABLE ticket_order ( order_id VARCHAR(32) PRIMARY KEY, -- 订单号 passenger_id VARCHAR(18) NOT NULL, -- 乘客证件号 train_no VARCHAR(10) NOT NULL, -- 车次 seat_type TINYINT NOT NULL, -- 席别:1硬座 2软座 3硬卧 4软卧 from_station VARCHAR(20) NOT NULL, to_station VARCHAR(20) NOT NULL, depart_time DATETIME NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0已支付 1已改签 2已退票 3已出行 create_time DATETIME DEFAULT NOW() );status字段是改签和退票判断的依据:只有status=0的订单才能改签或退票,改签后置 1,退票后置 2。这样一条订单的生命周期就闭环了,不会出现「已退票还能再改签」的玄学问题。
3. 业务需求拆解:订票、改签、退票三条主线的规则落地
3.1 订票模块:查询条件与订单生成
订票是主线中的主线。原文把流程写得很清楚:售票员登录后按旅客要求选业务类型,购票需要旅客提供身份证或公免乘车证,然后按出发地、目的地、日期查余票,有票就添加乘客、核对信息、收款、完成订单、打印车票。
查询条件三要素必须全部选择,这是硬约束。中转查询则是在出发地和目的地之间插入一个中转站,查两段车次。这里有个容易翻车的点:中转查询不是简单查两次,要保证第一段到达时间早于第二段发车时间,且换乘时间留够。我一般会在服务端加一个最小换乘间隔参数,比如 30 分钟,避免查出「到了就开走」的无效方案。
订单生成的核心是余票扣减的原子性。伪代码逻辑如下:
# 下单核心逻辑:先锁余票再扣减,失败回滚 def create_order(train_no, seat_type, passenger): with db.transaction(): # 行级锁锁定余票记录,防止并发超卖 stock = db.query( "SELECT remain FROM seat_stock WHERE train_no=? AND seat_type=? FOR UPDATE", train_no, seat_type) if stock.remain <= 0: raise SoldOutError("该席别余票不足") db.execute("UPDATE seat_stock SET remain=remain-1 WHERE ...") order_id = generate_order_id() db.insert("ticket_order", order_id, passenger, train_no, seat_type) return order_idFOR UPDATE是这段代码的灵魂,没有它,两个窗口同时卖最后一张票就会超卖。参数seat_type区分席别,因为硬座和硬卧是分开的库存池。下单成功后打印车票,打印内容从订单表反查,不要在前端拼字符串,否则改签后票面信息对不上。
3.2 改签模块:一次限制与差额结算
改签的本质是修改订单信息,但规则比订票复杂。原文列了三条硬规则:变更到站需在预售期内、开车前 48 小时以上办理,且只办理一次;已办理变更到站的车票不再办理改签;批量改签要求同一订单中日期、车次、出发站、目的站、乘坐类型完全一致。
差额结算是改签的难点。到站不变只改时间,不收费;到站变了,按新里程重算票价,新车票低于原票退差额并核收退票费,高于原票补差额同样核收退票费。这里的「核收退票费」容易被误解成全额退票费,实际是对差额部分按退票费率收。
改签服务的方法设计要带上原订单号和目标车次:
[OperationContract] ChangeResult ChangeTicket(string orderId, string newTrainNo, DateTime newDate); // 返回结构里必须带差额和手续费,前端才能展示给旅客确认 public class ChangeResult { public bool Success; public decimal PriceDiff; // 正数补款,负数退款 public decimal Fee; // 对差额收取的手续费 public string Message; }PriceDiff和Fee分开返回,是因为旅客需要先看到「补多少、扣多少手续费」再决定是否确认。如果合成一个数字,柜台解释起来就是一笔糊涂账。改签成功后原订单status置 1,同时生成一条改签记录,方便后续追溯——这就是需求文档里说的「交易记录保留」。
3.3 退票模块:时间梯度费率与特殊情况
退票规则是整套方案里最需要精确到小时的部分。原文给的梯度很明确:开车前 15 天以上不收手续费;48 小时到 15 天收 5%;24 到 48 小时收 10%;不足 24 小时收 20%。另外还有两条特殊规则:48 小时到 15 天期间改签或变更到站到 15 天以上列车、又在 15 天前退票的,仍收 5%;因紧急情况短期改签退票、特殊情况未赶上车的,按原文表述有相应宽松处理。
费率计算建议封装成独立函数,输入退票时刻和发车时刻,输出费率:
from datetime import datetime def refund_rate(depart_time, refund_time): hours = (depart_time - refund_time).total_seconds() / 3600 if hours >= 360: # 15天 = 360小时 return 0.0 elif hours >= 48: return 0.05 elif hours >= 24: return 0.10 else: return 0.20这个函数看着简单,但边界条件必须写测试用例:正好 48 小时、正好 24 小时、正好 360 小时各测一遍。血泪经验是,>=和>用错一个,答辩时被问「开车前整 48 小时退票收多少」就答不上来。退票成功后订单status置 2,退款金额 = 原票价 × (1 - 费率),交易记录保留不删除。
3.4 票价计算:分段累加与附加费叠加
票价算法是这份需求文档里技术含量最高的部分,也是最容易在答辩时被追问的。原文给的公式分五步:基础票价按区段里程乘票价率加前段累计;软座是基础票价两倍;保险费已于 2013 年取消;附加票价按车种和席别乘不同比例;最后叠加客票发展金、候车室空调费、卧铺订票费。
基础票价的例子很具体:里程 2375,落在 1500 以上区段,票价率 0.035166,前段累计 71.5042,算得 (2375-1500)×0.035166+71.5042=102.27445 元。这个分段累加的逻辑必须用循环实现,不能只写单段公式:
# 分段累加基础票价:每段里程区间对应不同票价率 SEGMENTS = [(0, 200, 0.05861), (200, 500, 0.05275), (500, 1000, 0.04688), (1000, 1500, 0.04103), (1500, 99999, 0.035166)] def base_price(mileage): total = 0.0 for low, high, rate in SEGMENTS: if mileage > low: seg = min(mileage, high) - low total += seg * rate return round(total, 2)SEGMENTS里的费率是示意值,实际以《铁路旅客票价计算方法》为准,但分段累加的结构不变。附加票价按原文比例:普快 20%、快速 40%、空调 25%、硬卧上中下 110%/120%/130%、软卧上下 175%/195%。附加费三项固定:客票发展金 5 元以下 0.5 元、以上 1 元;超 200 公里硬席收 1 元空调费;卧铺收 10 元订票费。最终联合票价 =(基本票价+附加票价)×(1+上浮率)+附加费。上浮率平时为 0,春运等特殊时期才启用,需求阶段留好参数位即可。
4. 辅助模块与需求文档编制:车次信息、个人信息与版本管理
4.1 车次与线路信息查询
辅助模块看着不起眼,但它是主流程的数据底座。原文要求能查某一列车的类型、车厢数、每节车厢座位数、行驶方向、途经站点;线路查询要能看到站点间距离、起终点、以及跑这条线的列车。这些信息直接支撑订票时的车次展示和中转查询的换乘判断。
车次信息建议做成只读缓存,因为变更频率低。数据结构上,车次表关联线路表,线路表关联站点距离表,三层关系别揉成一张大表。我见过有同学把途经站点存成逗号分隔字符串,结果中转查询要全表扫描拆字符串,性能惨不忍睹。正确做法是单独一张train_station表,存车次、站序、到站时间、发车时间、距起点里程。
4.2 个人信息管理与密码修改
个人信息模块要求查看账号、手机号,修改资料和登录密码。这块的安全要求不高但也不能裸奔:密码存储必须加盐哈希,不能明文;修改密码要先验原密码。需求文档里通常写「用户可修改个人信息」,但实现时得补一句「敏感字段变更需二次验证」,这是负责任的做法。
4.3 需求说明书的版本与修订记录
原文的修订记录从 V1.0.0 到 V4.0.0,日期集中在几天内,说明需求是反复打磨出来的。这个细节值得学:需求文档不是写完就锁死的,每次评审后的修改都要留痕。修订表至少包含版本号、作者、日期、修改说明四列。答辩时老师翻到修订记录,能看出你是迭代过的,不是一次成型糊弄的。
文档结构按原文的引言、总体要求、业务需求、辅助模块四章走,引言里写清编写目的、预期读者、定义和参考资料。定义部分把「用户方」「开发方」「本系统」这些术语锁死,避免后文歧义。参考资料列模板来源和票价计算方法出处,体现需求有据可查。
5. 避坑与排查:需求分析阶段最容易翻车的五个点
5.1 退票费率边界写错导致对账不平
现象:测试时退票金额和手工计算差几块钱,财务对不上。原因:费率判断用了>而不是>=,或者小时换算把 15 天当成 15×24 之外的数。解决:把费率函数单独抽出来,用边界值用例覆盖 360、48、24 小时三个临界点,每个临界点测「刚好等于」和「差一分钟」两种情况。
5.2 改签与退票状态机混乱
现象:已改签的订单还能再退票,或者退票后还能改签。原因:订单状态字段没有在每次操作后正确流转,或者判断条件写成了「状态不等于某值」而不是白名单。解决:状态流转用白名单,只有status=0允许改签和退票,操作后立即更新状态,并在服务层加断言。
5.3 余票并发扣减超卖
现象:压测时同一席别卖出比库存多的票。原因:查询余票和扣减余票之间没有加锁,两个请求读到同一库存。解决:扣减语句用UPDATE ... SET remain=remain-1 WHERE remain>0并检查影响行数,或查询时加FOR UPDATE行锁,事务提交前不释放。
5.4 中转查询忽略换乘时间
现象:查出的中转方案第一段到站时间晚于第二段发车时间,旅客根本赶不上。原因:只做了两段独立查询,没做时间衔接过滤。解决:服务端加最小换乘间隔参数,过滤掉第二段发车 - 第一段到达 < 间隔的组合,间隔值做成配置项。
5.5 票价分段累加漏掉尾段
现象:长里程票价算出来偏低。原因:循环分段时最后一段的high用了固定值,里程超出后没兜住。解决:最后一段的上界设一个足够大的数(如 99999),或者循环结束后单独处理超出部分,并用原文 2375 公里的例子做回归验证。
6. 验收与答辩:怎么证明这份需求分析站得住
验收这份需求方案,我一般不看文档写得多厚,看三件事能不能当场演示:规则算得对、状态转得顺、边界扛得住。答辩前把这三件事做成一张自检表,比背 PPT 管用。
| 验收项 | 验证方法 | 通过标准 |
|---|---|---|
| 票价计算 | 用原文 2375 公里例子跑一遍 | 基础票价 102.27 元,误差≤0.01 |
| 退票费率 | 构造 360/48/24 小时三个边界订单 | 费率分别为 0、5%、10%、20% |
| 改签限制 | 对同一订单连续改签两次 | 第二次被拒绝并提示原因 |
| 余票并发 | 两线程同时抢最后一张票 | 只有一单成功,库存不为负 |
| 状态流转 | 退票后再尝试改签 | 系统拒绝,状态保持已退票 |
答辩时老师最爱问的两个问题,提前准备好答案:一是「你的票价公式里保险费为什么没有」,答 2013 年已取消,文档里注明即可;二是「改签只允许一次,这个一次是按订单算还是按乘客算」,答按订单算,同一订单多次改签请求只放行第一次。这两个点答清楚,基本就稳了。
还有一个技巧:把需求文档里的每条业务规则都对应到一个测试用例编号,答辩时老师指到哪条规则,你直接翻到用例表说「这条我测过,用例编号 TC-07」。这种对应关系在需求阶段就建好,后期开发和测试都省事。
从那以后我每次做需求分析,都强制自己先把状态机和边界条件写成表,再动笔写正文——文档可以润色,规则错了就是返工。希望帮到你。
本文还有配套的精品资源,点击获取