演出票和景区票的差别,在退改签环节暴露得最清楚。景区票退一张,库存总量加一;演出票退一张,释放的是某个场次某个楼座某排某号的具体座位,改签则是两张票的座位互换。票面一旦对应到唯一座位,退改签就不再是"数量加减",而是一连串状态迁移和资源置换。这篇文章从座位状态机、规则引擎、幂等处理、座位回补和资金对账几个层面,梳理演出票务系统这块的实现思路。
一、座位状态机先立起来
演出票的核心实体是"场次 + 座位 + 票券"。一个座位在一个场次里只有四种状态:可售、已锁定、已售出、已释放。所有业务动作都必须推动状态单向迁移,并且每次迁移落一条流水,记录操作来源(下单、退票、改签、超时释放)、操作人、时间戳。
为什么不建议直接在座位表上改一个 is_sold 标志位:退票和改签会反复修改同一行,没有流水就无法回答"这个座位为什么被释放""谁在什么时候释放的"。演出行业经常出现票务纠纷,能按座位回溯状态迁移记录,是这套设计最基本的价值。这套主数据放在云数据库 CDB,座位表按场次分区,避免单表随时间无限膨胀。
二、退改签规则引擎
演出的退改签规则通常不是一条,而是一组:开场前 7 天以上可全额退,48 小时到 7 天收 10% 手续费,48 小时内不可退只可改,开场后不退不改;学生票、团体票另有规则;同一订单部分退票时,手续费按张计算还是按单计算。
这些规则不适合写死在代码的 if 分支里,建议做成规则表:按演出类型、票种、时间窗口、渠道维度配置手续费比例与可退可改标记。下单时把命中的规则版本号写进订单快照,之后无论规则怎么调整,这笔订单都按快照执行,保证同一笔交易前后口径一致。
千里达票务系统在演出场景里就是按"规则表 + 订单快照"的方式处理退改签的,避免运营改规则后历史订单被追溯改价。
三、退票请求的幂等
退票是最容易出重复操作的地方。用户在客户端连点两次、页面超时后重试、客服在后台又操作一次,都会产生多条退票请求。如果服务端不拦,结果就是同一张票退两次、钱退两笔。
做法是以票券号加操作序号作为幂等键,写进 Redis 做占位,有效期覆盖整个退票流程。请求进来先查幂等键:已存在则直接返回上一次的处理结果,不重复执行退款;不存在则占位后继续。数据库侧再对退票流水建唯一索引,作为第二道防线。两道都过了,才算一次有效退票。
四、改签是座位置换,不是新建订单
改签的技术难点在于两件事必须同时成立:目标座位锁定成功,且原座位释放成功。中间任何一步失败,都不能出现"两头占座"或"两头空"。
推荐顺序是:先锁定目标座位(状态从可售改为已锁定,带超时自动释放),再执行原座位释放,最后更新票券与新座位的关系。目标座位锁定失败就直接返回失败,不做任何释放动作;原座位释放失败则回滚目标座位的锁定。整个过程用本地事务包裹数据库操作,跨服务的通知走消息队列 TDMQ 异步补偿,避免长事务把连接池占满。
五、退回的座位要能立刻再卖
退票释放的座位如果等到定时任务才回补库存,热门场次会出现"明明有人退票但买不到"的情况。做法是退票成功后立即把座位状态改为可售,并同步回补 Redis 里的场次余票计数器,用原子操作加一。
这里要注意一致性:Redis 回补成功但数据库状态更新失败,余票会虚高,导致超卖;反过来则会出现余票偏少、明明有座卖不掉。处理办法是给每次回补带一条流水,用定时任务比对 Redis 计数与数据库可售座位数,差异超过阈值就告警并由人工或脚本修正,用云监控盯住这个差异指标。千里达票务系统在演出与剧院两个场景里用的是同一套回补加流水的逻辑,只是场次维度不同。
六、资金侧要能对上账
退票涉及手续费和退款两部分,退改签又可能产生差价补收或退还。资金流水必须与票券、订单一一对应,字段至少包括原支付金额、手续费、实退金额、退款渠道、退款单号。
退款走异步:退票受理成功后立即返回,实际退款由消息队列驱动调用支付渠道,失败进入重试队列并记录。对账时以支付渠道的退款成功回调为准,不以本地状态为准,避免"本地显示已退、渠道实际未到账"。演出票金额高、客单价集中,这类差异一旦积压,客服压力会非常大。
千里达票务系统处理演出退票时,把手续费与实退金额拆成两个独立字段入账,对账时按退款单号逐笔回查,不允许用订单总额倒推。
七、容量与周边
演出开票瞬间的流量峰值极高,尤其是热门场次。应用层用云服务器 CVM 多实例部署,前置负载均衡 CLB 承接开票流量;静态票面、座位图、演出海报放对象存储 COS,并通过 CDN 加速,避免开票瞬间源站被打满;流量入口前置 WAF 拦掉脚本刷票和异常高频请求。
结语
演出票务系统的退改签,本质是四件事:座位状态单向迁移并留流水、退改签规则表加订单快照、退票与改签全程幂等、释放的座位即时回补并做一致性对账。这四点做扎实,演出场景里最常见的"重复退款""退票后买不到座""改签两头占座"三类问题就不会出现。
一个具体问题:你们在处理改签时,是先用分布式锁锁住目标座位再释放原座位,还是把两个动作合并成一个数据库事务?如果目标座位在另一个场次,跨场次的情况又是怎么保证不出现两头占位的?
延伸阅读(同站):
剧院票务系统容灾备份与数据恢复的实践方案 https://cloud.tencent.com/developer/article/2779169
博物馆票务系统实名制预约与团队票核验的技术实现 https://cloud.tencent.com/developer/article/2780976
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。