
简介一份面向软件工程课程设计、系统分析实战与铁路信息化项目的《中国铁路窗口售票系统需求分析》资料以规范的需求说明书形式呈现适合需求分析师、开发人员及高校学生作为撰写售票类系统需求文档的参考蓝本。内容涵盖引言、总体要求、业务需求、票价计算方法及辅助模块对订票、改签、退票三大核心业务和用户管理、支付服务、统计分析等支撑功能均有明确描述并给出总体业务流程与体系架构可帮助读者快速掌握窗口售票系统的功能边界与设计要点。资源包内共1个文件格式为docx包体大小275KB文档结构完整、章节清晰便于直接查阅和二次编辑。目前已有581人学习下载尤其适合需要参考真实需求说明书结构来完善自己作品或项目的用户。1. 售票需求分析窗口售票场景为什么值得单独拆一套规则网上订票铺开之后很多团队把窗口售票当作退化的边缘场景直接套用网页端的用例。但窗口售票面对的是售票员输入效率、异常打断、退改规则的解释口径都跟自助端完全不同。这份需求分析把订票、改签、退票三条业务链的边界规则、票价计算方法和车次基础数据压缩进一个基于 WinFormWCF 的桌面系统正文里最有价值的不是界面原型而是三块改签只能一次的状态约束、退票手续费分档的时间边界、按里程分段累加的票价引擎。适合正在做售票、排班、订票类桌面的开发者也适合刚入门需求分析的从业者拿来当范本拆。2. 架构与选型边界WinFormWCF 在窗口售票场景的取舍2.1 为什么是 WinFormWCF 而不是浏览器方案文档体系架构里写得很明确基于 WinFormWCF 与后台逻辑进行数据交互。放在 2015 年的窗口售票场景这个选型是正确的。车站窗口属于固定工位终端设备由车站统一采购维护不存在远程升级和跨平台诉求WinForm 的强类型绑定和数据网格控件能显著减少售票员敲键盘的次数——输入车次、选乘客、回车打印一次性完成这是浏览器页面做不到的交互密度。WCF 承担的是客户端与后台服务之间的数据通道常见做法是把订单服务、余票查询服务、票价计算服务拆成独立的 ServiceContract客户端通过 net.tcp 或 http 绑定调用。窗口售票的数据量不大但实时性要求高余票的扣减必须在一个事务里完成WCF 的会话能力和事务传播在这里比 Restful API 更顺手。2.2 三个业务模块的功能边界与数据流需求文档把业务切成了订票、改签、退票三个模块外加火车信息和个人信息两个辅助模块。数据流是这样的售票员登录后选择业务类型订票时先按出发地、目的地、出发时间查余票有票则添加乘客、核对信息、收款、完成订单、打印车票改签时校验是否已改签过符合条件则重新计算票价多退少补退票时按开车时间与当前时间的差值确定手续费比例退款走原支付渠道。这里有一个容易被忽略的边界改签的流程图上写的是「修改订单信息」但实际改的是车次和到站而订单号必须保持不变这就意味着订单表和车票表的关联字段要单独设计不能简单地把旧车票删除再新增一条。2.3 从业务流程推导核心表结构需求文档没有给数据库设计但业务规则已经足够推导出核心表。我的习惯是先画出订单的整个生命周期再倒推表结构而不是先建表再凑流程。以下是一组可落地的核心表按需求文档的业务约束推导供参考。-- 车次表存储车次基础信息 CREATE TABLE train_info ( train_no VARCHAR(10) PRIMARY KEY, -- 车次号如 T8349 train_type VARCHAR(10), -- 类型普快/快速/空调 start_station VARCHAR(50), -- 出发站 end_station VARCHAR(50), -- 终点站 start_time DATETIME, -- 出发时间 duration INT, -- 运行时长分钟 seat_layout TEXT -- 车厢及座位布局 JSON ); -- 订单表一个订单包含多张车票 CREATE TABLE orders ( order_id BIGINT PRIMARY KEY IDENTITY, passenger_id VARCHAR(18), -- 身份证号 contact_phone VARCHAR(20), order_status TINYINT, -- 0待支付 1已出票 2已改签 3已退票 total_amount DECIMAL(10,2), create_time DATETIME, modify_time DATETIME ); -- 车票表与订单多对一 CREATE TABLE tickets ( ticket_id BIGINT PRIMARY KEY IDENTITY, order_id BIGINT, train_no VARCHAR(10), travel_date DATE, depart_station VARCHAR(50), arrive_station VARCHAR(50), seat_type VARCHAR(10), -- 硬座/硬卧上/软卧下等 price DECIMAL(10,2), ticket_status TINYINT, -- 0有效 1已改签 2已退票 change_count TINYINT DEFAULT 0, -- 改签次数需求要求最多1次 FOREIGN KEY (order_id) REFERENCES orders(order_id) );表结构里的ticket_status和change_count两个字段直接对应需求文档的硬约束。change_count默认 0每次改签前先查这个字段改签后加 1超过 1 就直接拒绝这是把业务规则下沉到数据库层避免并发场景下两个窗口同时给同一张票改签。orders表和tickets表分离的意义是退票时「交易记录还是会保留下来」需要保留订单的流水痕迹同时允许部分退票。2.4 部署架构上的几个关键参数WCF 服务端一般用 ServiceHost 寄宿在 Windows 服务里客户端配置里需要指定 endpoint 的 binding 和 contract。常见的坑是 net.tcp 绑定的端口没在防火墙放行导致窗口终端连不上服务。我的习惯是用 http 绑定做基础通信端口 8080省去一堆端口开放和权限配置如果对吞吐有硬指标再切 net.tcp默认端口 808同时在服务端配置里显式打开receiveTimeout和sendTimeout防止售票高峰期长事务超时。提示WCF 的InstanceContextMode建议设成PerCall售票系统每个操作都是短事务PerCall 可以避免状态残留代价是每次调用都要重新连接数据库但对窗口售票这种低并发场景完全够用。3. 订票 / 改签 / 退票三个模块的落地细节3.1 订票流程从查询条件到订单生成的状态流转订票是系统的主线。需求文档里写了两个入口直接查询和中转查询。直接查询必须选出发地、目的地、出发时间三个条件查出来的结果是车次列表每个车次下按席别展示余票数和单价。中转查询则是在出发地和目的地之间插入一个中转站查询的其实是两段车程的组合而且中转站可以指定也可以让系统推荐。订票的状态流转建议用一个显式的状态机管理而不是散落在代码里的 if-else。# 订票状态机核心逻辑 class OrderState: DRAFT 0 # 草稿已选车次未支付 PAID 1 # 已支付车票待打印 TICKETED 2 # 已出票打印完成 CHANGED 3 # 已改签车票作废生成新车票 REFUNDED 4 # 已退票 def create_order(train_no, passengers, seat_type): # 1. 预扣余票防止并发超卖 # 2. 生成订单和车票记录初始状态 DRAFT # 3. 支付成功后置 PAID # 4. 打印车票后置 TICKETED pass def query_remaining(train_no, travel_date, seat_type): # 余票 总座位数 - 已售出 - 已锁定时长未支付 # 锁票超过 15 分钟未支付自动释放 pass余票查询和扣减是订票模块最容易出问题的地方。常见做法是查余票时不锁库只读缓存但点下提交订单那一刻必须对tickets表对应车次席别做行级锁或乐观锁扣减。文档里没有明确说锁票时间我一般会设 15 分钟支付超时超时自动释放余票否则会出现大量占票不付款导致窗口售不出票的情况。3.2 改签只能一次状态约束与票价差额处理改签是三个模块里业务规则最密的。文档里总共给了四条约束第一开车前 48 小时以上可以变更到站范围不收手续费第二变更到站只办一次已经办过的不再改签已改签的车票不提供变更到站服务第三同一订单中相同日期、车次、出发站、目的站、乘坐类型的车票才可以批量改签第四票价差额多退少补但差额部分无论高低都核收退票费。把这些规则翻译成代码约束核心是两个检查点。def validate_change(ticket, new_train_no, new_date): # 检查点 1改签次数 if ticket.change_count 1: return False, 该车票已办理过一次改签不再办理 # 检查点 2批量改签时校验同订单车票的一致性 if not is_same_order_group(ticket.order_id, new_train_no, new_date): return False, 批量改签车票的日期、车次、到站必须一致 # 检查点 3开车前 48 小时以上才能变更到站 if hours_to_departure(ticket) 48: return False, 开车前 48 小时内不能变更到站 return True, 这里有一个隐藏的坑文档里说「变更到站只办理一次」系统的change_count只加在车票上如果改签生成了新车票新车票的change_count继承了旧车票的值不能重置为 0。否则旅客可以用旧车票改签一次拿到新车票再用「新车票」再改签一次绕过「只能改签一次」的限制。需求文档把这一条写得很清楚但实现时很容易在生成新车票的时候顺手把计数清零。票价差额的计算逻辑不复杂但要注意方向。新车票价低于原车票退还差额差额部分按退票费标准收新车票高于原车票补收差额差额部分同样收退票费。这个「差额部分也收退票费」的规则和很多人直觉理解的不一样需求方反复确认过在需求分析阶段就明确写进了文档实现时不要省略。3.3 退票手续费分档时间边界的代码化文档退票规则写得最细的部分是手续费分档而且有一个容易被读漏的边角开车前 48 小时到 15 天这个区间内如果你改签或者变更到站到 15 天以上的车次又在 15 天前退票仍然收 5% 的退票费。我一般会把退票费计算抽成一个独立函数用时间段作为入参避免在业务代码里散落魔法数字。def calc_refund_fee(ticket_price, hours_before_departure, changed_flagFalse): 计算退票手续费 :param hours_before_departure: 距离开车时间小时 :param changed_flag: 是否属于 48h~15天 区间改签至15天以上的车票 days_before hours_before_departure / 24 # 距开车 15 天以上 if days_before 15: return 0.0 # 48h~15天区间内改签过改后又退仍然收 5% if 2 days_before 15 and changed_flag: return ticket_price * 0.05 if 2 days_before 15: return ticket_price * 0.05 if 1 days_before 2: # 24h~48h return ticket_price * 0.10 return ticket_price * 0.20 # 不足 24 小时这个函数有三个边界要注意。第一days_before 15时走第一分支不收费第 14 天 23 小时就收 5%跨天瞬间手续费从 0 跳到 5%是测试最容易漏的等值边界。第二改签标记changed_flag需要在改签记录表里持久化不能只在内存里判断否则重启服务后这个标记丢失退票时会漏掉 5% 的手续费。第三「不足 24 小时收 20%」和「开车后仍可改签当日其他列车」是两条独立规则开车后改签不影响退票费率计算但一旦改签成功原车票转为已改签状态不允许再退这条在需求文档中隐藏在特例描述里实现的时候要单独拉出来确认。3.4 辅助模块车次信息和常用信息管理的边界火车信息模块里写了一个具体车次 T8349 的查询场景查出来能知道车型、车厢数、每节车厢座位数、行驶方向和经过站点。这个模块在需求上就是一张车次基础信息表的 CRUD但有个业务细节查看线路时要能看到「每两个站点之间的距离」和「行驶在这条线路上的列车」。这意味着线路上站点之间的距离是基础数据的属性不是算出来的。设计这张表的时候要把站点顺序和距离一并存储单独存一个route_stations表字段为车次号、站序、站名、距上一站里程、到达时间、出发时间。常用信息管理更简单就是账号、手机号、登录密码的修改需求文档里没有额外权限分级窗口售票员的账号应该能查看到所有线路这一点在权限上要和后台管理账号区分开。4. 票价计算引擎里程分段累加与席别附加规则4.1 票价组成的四个层次文档把票价计算拆成五个层次基础票价、软座客票、保险费、附加票价、附加费。2013 年保险费取消后实际计算只剩四个层次但文档保留了完整的算法对理解铁路票价结构反而有帮助。基础票价是核心公式是「该区段里程数 × 该区段票价率 前各区段累计票价」也就是按里程分段累加。文档给了一个具体例子里程 2375 公里取超出 1500 公里的部分计算(2375-1500)×0.035166 71.5042 102.27445 元。这里的 71.5042 就是前面 1500 公里各段的累计票价0.035166 是 1500 公里以上区段的票价率。附加票价按席别和车次类型乘不同的系数再单独取整到元。附加费则是三个固定值客票发展金、候车室空调费、卧铺订票费。4.2 计算引擎实现与取整规则把文档里的计算规则转成一个可执行的价格函数关键是把各席别的附加比例做成配置表。# 席别附加比例表对应文档 3.2 节 SEAT_RATIO { 硬座: 0, # 基础票价 软座: 1.0, # 基础票价 * 2即加100% 普快: 0.20, # 数字头车次 快速: 0.40, # K/T 车次 空调: 0.25, 硬卧上: 1.10, 硬卧中: 1.20, 硬卧下: 1.30, 软卧上: 1.75, 软卧下: 1.95, } def calc_base_price(mileage, seg_table): 按里程分段累加基础票价 total 0.0 last_end 0 for seg_end, rate in seg_table: if mileage last_end: break if mileage seg_end: total (mileage - last_end) * rate return total total (seg_end - last_end) * rate last_end seg_end return total def calc_total_price(mileage, seat_type, train_type, overrate0.0): base calc_base_price(mileage, SEG_TABLE) add_ratio SEAT_RATIO[seat_type] SEAT_RATIO.get(train_type, 0) add_price round(base * add_ratio) # 附加票价取整到元 total (base add_price) * (1 overrate) # 附加费客票发展金 候车室空调费 卧铺订票费 extra 1.0 # over 5 yuan, development fee 1 if mileage 200: extra 1.0 if 卧 in seat_type: extra 10.0 return round(total, 2) extra这个函数里最需要留意的两个取整点一是附加票价单独取整后再参与总票价计算不能先算总和再取整文档里 20.45489 约 21 元就是四舍五入到元这会导致基础票价相同、不同席别的组合产生不同的取整偏差二是软卧的 175%、195% 是附加票价比例不是总票价比例文档原文「软卧上175%」指的是基础票价的 1.75 倍作为附加票价叠加在基础票价之上不是直接乘 1.75 得到总价。4.3 参数表与扩展设计票价计算的参数需要独立于代码存放常见的做法是存配置表或者 JSON 文件。上浮率参数overrate文档里没有给出具体值但公式里保留了这一个乘数春运、暑运等高峰期铁路部门会有浮动价格这个参数必须在配置层预留。参数项值说明基础票价率0.035166 元/km1500km 以上区段保险附加2%2013年取消保留开关位客票发展金≤5元收0.5元5元收1元固定附加费候车室空调费1元乘车超过 200km 的硬席卧铺订票费10元购买卧铺票时收取5. 避坑与排查需求文档里最容易翻车的五个细节5.1 退票费跨天边界算错现象退票费计算函数在开车前 14 天 23 小时和 15 天整返回不同结果测试用例覆盖漏掉上线后旅客投诉扣费不对。原因15 天和 48 小时是硬边界但时间差计算用的是小时数14天23小时 15天天然成立而业务方说的「15天」是按自然日算的开车当天算第 0 天需要统一换算口径。解决以开车日期减退票日期的自然日差为准而不是小时数除 24。我在代码里保留days_before的同时额外传一个calendar_days参数自然日差优先小时只在 48 小时、24 小时这两个档位使用。5.2 里程分段累加的区间归属错位现象手动验算 2375 公里的票价套用(2375-1500)×费率71.5042总是差几毛钱。原因分段表里的前 1500 公里是多个费率段累加得到的 71.5042直接拿总里程减去 1500 再乘尾部费率只做了「尾部区段」的计算没有把中段的费率拐点算进去。文档的公式没有展示前 1500 公里又分了几段直接把 71.5042 作为常数给出实现时如果直接把累加表写错价格就会系统性偏差。解决把分段表的每个区间终点和费率作为独立配置项录入用 4.2 的calc_base_price循环累加不要手工算常数配置表要有版本号每次费率调整跟随版本发布。5.3 改签生成新车票时把改签计数清零现象旅客改签一次后拿着新车票再次改签系统居然放行了。原因新车票的change_count字段在建票时走了默认值 0没有继承旧车票的已改签次数。需求文档明确「变更到站只办理一次」但生成新票的逻辑经常单独维护遗漏了字段传递。解决在生成新车票的构造函数里强制change_count old_ticket.change_count 1并且要求改签和退票共用同一张车票编号链所有派生车票通过parent_ticket_id关联回原件随时可以追溯改签历史。5.4 WCF 服务在部署后连接超时现象开发环境一切正常部署到车站窗口后客户端报连接超时服务端日志显示请求已进入但响应超时。原因窗口终端和服务器之间可能跨交换机WCF 默认的receiveTimeout是 10 分钟太长反向是很多代理设备会在 60 秒后断开空闲连接客户端没配可靠会话服务端第一次响应被截断。解决客户端配置里显式设sendTimeout00:00:30 receiveTimeout00:00:30服务端绑定加maxBufferPoolSize和maxReceivedMessageSize同时关闭reliableSession窗口售票的操作链路短不需要可靠会话反而省掉心跳包的干扰。5.5 已改签车票继续退票造成资金重复退还现象旅客先办理改签然后对原车票发起退款系统两边都退了钱。原因改签的本质是原票作废、生成新票两张票在系统里存在不同票号退票逻辑没有校验车票状态和关联关系把旧票当成有效票来退。解决在退票入口增加前置校验车票状态必须为TICKETED已出票且未改签并且parent_ticket_id为空一张车票在改签后原票状态置为CHANGED并锁定退款入口退款只允许对当前有效车票发起。6. 进阶把需求文档的规则反向生成测试验收清单需求分析文档交出去之后最容易被开发团队忽略的是如何验证实现是否忠实于规则。我的习惯是把文档里每条带数字、带条件的句子抽出来转成等价类测试用例这个转换过程本身就是对需求理解的一次校验。下面是从这份需求文档提炼的验收清单覆盖了三条核心业务链和时间、金额、状态三个维度。用例前置条件输入期望结果退票无手续费距开车 16 天全额退款请求退票费0原路退回退票 5% 档距开车 3 天改签过且退票收 5% 退票费退票 5% 改签特例距开车 10 天改签至 20 天后再退退票请求仍收 5%免手续费不生效改签次数限制车票已改签 1 次再次改签拒绝提示仅可改签一次软卧下计价里程 2375软卧下票价计算基础价102.27 附加199 附加费12变更到站时间窗距开车 30 小时变更到站拒绝必须 ≥48 小时批量改签一致性订单内 3 张票1 张到站不同批量改签全部拒绝需拆单订单状态追溯退票完成查订单历史交易记录保留状态已退票最后说说这套测试清单怎么用。当时带着这份清单去验证一个模拟项目X的售票实现第一轮就测出了五个缺陷其中最隐蔽的是改签新车票计数清零的问题——开发写了非常完整的改签业务逻辑唯独没有检查新车票的初始状态。从那以后我每次接到需求分析文档做的第一件事不是画原型图而是把文档里所有「必须、不得、允许、仍收、不低于」这类规范动词全部圈出来逐条编号转成测试断言。需求分析文档里最贵的就是这些边界约束少测一条上线后就是一笔投诉。希望这条习惯能帮到你。本文还有配套的精品资源点击获取