简介:这份文档面向软件工程学习者、系统分析与设计初学者以及需要撰写课程作业或项目文档的读者,围绕中国铁路窗口售票系统展开需求分析,帮助读者理解从用户视角梳理售票业务流程、明确系统边界与功能要求的完整思路。压缩包内共1个docx文件,约275KB,为一份结构完整的需求说明书,涵盖引言、总体要求、业务需求、辅助模块与结语等章节,其中总体要求部分细化了系统总体目标、功能要求与体系架构,业务需求部分则拆解了订票、改签、退票三大模块及票价计算方法,辅助模块还涉及火车信息查看与个人信息管理。已有582人学习下载,适合作为需求分析文档的写作范本,也可用于对照梳理售票系统的功能清单、模块划分与业务流程,为后续设计与开发提供参考依据。
1. 窗口售票系统需求分析:一份能直接落地的 V3.0.0 规格书
窗口售票这个场景,很多人以为早被线上购票取代了,但真到春运或者小站,售票员面前那台机器才是主力。这份《车站窗口售票系统》需求说明书 V3.0.0 就是围绕这个场景写的——它不聊虚的,直接把订票、改签、退票三条主线的业务规则、票价公式、体系架构(WinForm + WCF)全部钉死。适合两类人:一是要照着做课程设计或毕设的开发者,二是接手老售票系统二次开发、需要一份能对得上代码的规格底稿的人。下面我按“这文档说了什么 → 怎么把它变成能跑的东西 → 哪里容易翻车 → 怎么改”的顺序拆一遍。
2. 体系架构与模块边界:WinForm + WCF 这套组合怎么落地
2.1 为什么是 WinForm + WCF,而不是 Web
文档 2.3 节写得很直白:基于 WinForm + WCF 与后台逻辑做数据交互。这个选型放在窗口售票场景里其实很合理——售票员用的是内网固定工位机,不需要浏览器兼容,WinForm 拖控件快、打印车票调本地打印机直接;WCF 负责把业务逻辑收在服务端,客户端只做界面和调用,避免每台机器都连数据库。常见做法是把解决方案拆成三层:Ticketing.Client(WinForm 界面)、Ticketing.Contract(WCF 服务契约与数据传输对象)、Ticketing.Service(业务实现 + 数据访问)。契约层单独抽出来是关键,客户端和服务端都引用同一份接口定义,改字段时编译期就能发现不一致,比手写 XML 靠谱得多。
服务契约我一般这样起头,把订票、改签、退票三个入口先定出来:
// Ticketing.Contract/ITicketService.cs [ServiceContract] public interface ITicketService { [OperationContract] QueryResult QueryTrains(QueryCondition cond); // 余票/中转查询 [OperationContract] OrderResult CreateOrder(OrderRequest req); // 订票 [OperationContract] ChangeResult ChangeTicket(ChangeRequest req); // 改签 [OperationContract] RefundResult RefundTicket(RefundRequest req); // 退票 }QueryCondition里至少要有出发地、目的地、出发日期三个必填字段,对应文档 3.1.1 里“这些条件都是必须选择”。OrderRequest要带乘客证件类型和证件号(身份证、公免乘车证),因为文档明确要求核验有效证件。ChangeRequest里必须有一个HasChanged标志位——改签只允许一次,这个状态得在服务端判,不能信客户端传上来的“我没改过”。
2.2 目录结构:照着这个骨架建工程
需求文档本身不给代码,但按它的模块划分,工程目录基本能反推出来。我习惯这样组织,和文档章节一一对应,后期对需求方便:
Ticketing.sln ├─ Ticketing.Contract/ # WCF 契约、DTO │ ├─ ITicketService.cs │ └─ Dto/ (QueryCondition, OrderRequest, ...) ├─ Ticketing.Service/ # 服务实现 │ ├─ TicketService.cs # 订票/改签/退票入口 │ ├─ Pricing/PriceCalculator.cs # 3.2 票价计算 │ ├─ Refund/RefundRule.cs # 3.1.3 退票费率 │ └─ Data/ (Repository, DbContext) ├─ Ticketing.Client/ # WinForm │ ├─ Forms/ (QueryForm, OrderForm, ChangeForm, RefundForm) │ └─ ServiceProxy/ # WCF 客户端代理 └─ Ticketing.Tests/ # 票价与退票规则单测把Pricing和Refund单独拎成目录是有原因的:这两块规则最密、最容易改,文档 3.2 的票价公式和 3.1.3 的退票费率都是纯计算,抽出来就能写单元测试,不用起界面、不用连服务。血泪经验是,票价和退票费如果散在TicketService里,后面调一次费率就要通读几百行,改错一个分支就是真金白银的差额。
2.3 服务端启动与客户端连接
WCF 服务端用控制台或 Windows 服务宿主都行,开发阶段控制台最省事,App.config里配一个basicHttpBinding就够内网用:
<!-- Ticketing.Service/App.config --> <system.serviceModel> <services> <service name="Ticketing.Service.TicketService"> <endpoint address="" binding="basicHttpBinding" contract="Ticketing.Contract.ITicketService" /> <host> <baseAddresses> <add baseAddress="http://localhost:8733/Ticketing" /> </baseAddresses> </host> </service> </services> </system.serviceModel>客户端在ServiceProxy里用ChannelFactory<ITicketService>建通道,地址从配置文件读,别硬编码。这样换测试环境只改配置。注意basicHttpBinding默认消息大小有限制,中转查询返回的车次列表如果一次几百条,容易超限,把maxReceivedMessageSize调到 1MB 以上比较稳。
3. 三大业务模块拆解:订票、改签、退票的规则怎么写成代码
3.1 订票模块:查询 → 选座 → 下单 → 打印
文档 3.1.1 把订票拆成“车票查询”和“完成订单”两步,查询里还带一个中转查询。落到代码上,查询接口返回的是车次 + 席别 + 单价 + 余票数的组合,不是单纯的车次列表。中转查询的本质是两段行程拼接:出发地→中转站、中转站→目的地,两段都要有余票且时间能接上。常见做法是先查第一段,拿到达时间作为第二段的最早出发时间再查,避免拼出时间倒挂的行程。
下单这块,文档强调“核对信息、旅客给车票费后完成订单,打印车票”。服务端要做的是:校验余票(防并发超卖)、生成订单号、扣减库存、返回车票数据。余票扣减一定要在事务里做,并且用带条件的更新,别先查再改:
-- 扣减余票:只有余票足够才更新成功,返回受影响行数判断 UPDATE TrainSeat SET Remain = Remain - @count WHERE TrainId = @trainId AND SeatType = @seatType AND TravelDate = @date AND Remain >= @count;受影响行数为 0 就说明余票不够,直接回滚并提示“该席别已售完”,对应文档里“也有可能会出现所有车票都已经售完的情况”。这一步是窗口售票最容易翻车的地方——两个窗口同时卖最后一张票,先查后改必然超卖,条件更新是后悔药。
3.2 改签模块:一次限制与差额处理
改签在文档 3.1.2 里规则很细,核心有三条:变更到站只办一次、已改签的不再改签、差额按退票费标准核收。代码上我把它拆成“资格校验”和“差额结算”两段。资格校验先查原票的ChangeCount,大于等于 1 直接拒;再判断是否变更到站,变更到站和普通改签共用这个计数。
差额结算的逻辑要小心:新车票便宜退差额、贵了补差额,但差额部分要按退票费标准收费。文档写的是“对差额部分核收退票费并执行现行退票费标准”,所以不是简单相减,而是把差额当成一次退票来算手续费:
// Pricing/ChangeSettlement.cs public decimal SettleDifference(decimal oldFare, decimal newFare, DateTime departTime) { var diff = newFare - oldFare; if (diff == 0) return 0m; // 只改时间不改到站,不收费 var fee = RefundRule.CalcFee(Math.Abs(diff), departTime); // 差额按退票费率 return diff > 0 ? diff + fee : diff + fee; // 补差额加费 / 退差额扣费 }RefundRule.CalcFee就是下一节退票模块复用的那个费率函数,改签和退票共用一套费率,避免两处规则漂移。参数departTime传原票开车时间,因为费率是按“距开车多久”分档的。
3.3 退票模块:四档费率与特殊情形
文档 3.1.3 给了明确的四档:开车前 15 天以上不收,48 小时~15 天收 5%,24~48 小时收 10%,不足 24 小时收 20%。这个用switch按小时差落档就行,但有两个特殊条款必须写进代码:一是“改签或变更到站至 15 天以上、又在 15 天前退票的仍收 5%”,二是“紧急情况短期改签退票不受影响、未赶上车可在开车后改签当日其他列车”。前者意味着费率函数不能只看当前票的开车时间,还要看这张票是否经历过“改签到远期再退”的操作,得在订单上留一个OriginalDepartTime字段做判断。
// Refund/RefundRule.cs public static decimal CalcFee(decimal fare, DateTime departTime, DateTime? originalDepart = null) { var hours = (departTime - DateTime.Now).TotalHours; if (hours >= 360) return 0m; // 15 天以上 if (originalDepart.HasValue && // 改签到远期再退,仍按 5% (originalDepart.Value - DateTime.Now).TotalHours >= 360) return fare * 0.05m; if (hours >= 48) return fare * 0.05m; if (hours >= 24) return fare * 0.10m; return fare * 0.20m; }退票本身是“作废订单但保留交易记录”,所以数据库里订单状态改成Refunded,不要物理删除,退款金额和手续费单独记一条流水,后面对账全靠它。
3.4 票价计算:分段累进 + 附加费
文档 3.2 的公式是这份需求里最“硬”的部分,必须原样实现。基础票价按区段累进:里程落在哪个区段就用哪个票价率,再加上前各区段累计票价。文档给的例子是 2375 公里,(2375-1500)*0.035166+71.5042=102.27445,这个数要能对上,说明区段表和累计值都得维护成配置,不能写死。
// Pricing/PriceCalculator.cs public decimal BaseFare(int mileage) { // 区段表:上限里程、该段费率、前段累计 var seg = Segments.Last(s => mileage <= s.MaxMileage); return (mileage - seg.PrevMileage) * seg.Rate + seg.Accumulated; }附加票价按比例乘基础票价:普快 20%、快速 40%、空调 25%、硬卧上中下 110%/120%/130%、软卧上下 175%/195%。附加费三项——客票发展金(票价≤5 元收 0.5,否则 1 元)、候车室空调费(超 200 公里硬席收 1 元)、卧铺订票费(卧铺收 10 元)。最后联合票价 =(基本票价+附加票价)×(1+上浮率)+附加费。上浮率做成参数,平时传 0,特殊时期再调。这套公式建议直接对着文档例子写单测,算出来 102.27 才算过。
4. 避坑与排查:这份需求落地时最容易翻车的五处
4.1 余票超卖:先查后改的经典坑
现象是同一张票被两个窗口卖出,库存变负数。原因是查询和扣减分两步、中间没有锁。解决就是 3.1 里那条带Remain >= @count条件的UPDATE,用受影响行数判断成败,整个下单包在事务里。别用SELECT ... FOR UPDATE再改,窗口并发不高时条件更新更轻。
4.2 改签次数判断失效
现象是同一张票被改了两次。原因是ChangeCount只在客户端判断,或者服务端更新时没加条件。解决是把“计数+1”和“校验<1”合成一条带条件的更新,和余票扣减一个思路,服务端是唯一裁判。
4.3 退票费率算错档
现象是 47 小时退票按 5% 收了。原因是边界用了>而不是>=,或者小时差用日期相减丢了时分。解决是统一用DateTime.Now到开车时间的TotalHours做浮点比较,边界值写进单测:47.9 小时必须是 10% 档。
4.4 WCF 调用超时或消息过大
现象是查询车次多的时候客户端报“最大消息配额已超出”。原因是basicHttpBinding默认maxReceivedMessageSize只有 64KB。解决是在服务端和客户端配置里同时调大maxReceivedMessageSize和maxBufferSize,两边必须一致,只改一边照样报错。
4.5 打印车票与订单数据不一致
现象是界面显示的和打出来的票席别对不上。原因是打印时重新查了一次库,而订单在打印前被改签了。解决是下单成功后把车票数据快照随订单一起返回并缓存,打印只用这份快照,打印前再校验一次订单状态是否为Paid。
5. 二次开发改造:把需求文档变成可验证的代码
拿到这份 V3.0.0 规格书做二次开发,我的习惯是先别急着写界面,而是把 3.2 票价公式和 3.1.3 退票费率这两块纯计算先落成带单测的类,用文档里的 2375 公里例子和四档费率当验收标准。这两块过了,业务主干就稳了一半,因为订票、改签、退票最终都绕不开算钱。
改造时优先动配置而不是动代码。区段票价表、附加票价比例、退票费率档位、上浮率,全部抽到数据库配置表或appsettings里,代码只读配置做计算。这样调价、调费率不用重新发版,测试也能直接改配置跑不同场景。下面这张表是我一般会建的配置结构,字段名可以直接抄:
| 配置项 | 示例值 | 说明 |
|---|---|---|
SegmentRate | 0.035166 | 区段票价率 |
SegmentAccumulated | 71.5042 | 前段累计票价 |
RefundRate15d | 0 | 15 天以上费率 |
RefundRate48h | 0.05 | 48 小时~15 天 |
RefundRate24h | 0.10 | 24~48 小时 |
RefundRateUnder24h | 0.20 | 不足 24 小时 |
FloatRate | 0 | 上浮率,平时为 0 |
验证方法上,我建议给每个业务模块留一个“规则回归”测试集:订票测超卖并发、改签测二次改签被拒、退票测四个边界小时、票价测文档例子。跑通这套再上界面,比界面点半天发现算错钱强得多。另外文档 4.1 的火车信息、4.2 的个人信息属于辅助模块,改造时可以最后做,它们不影响主交易链路,先把三条主线跑顺。
从那以后我每次接这类需求文档,都强制先把“算钱”和“状态流转”两块单独抽出来写测试,再动 UI——因为界面错了顶多重画,钱算错了和状态错了,是要一条条对账去补的。希望这份拆解能帮你把这份规格书真正跑起来。
本文还有配套的精品资源,点击获取