4. 票务规则引擎:票价策略、优惠政策、时段折扣的动态配置

好,咱们来聊聊票务规则引擎。说实话,这是整个票务系统里最“烧脑”的部分之一。为什么?因为业务规则太容易变了。今天搞个暑期促销,明天来个周末折扣,后天又可能针对特定人群推出新优惠。你要是把这些规则写死在代码里,那运维同学得天天陪你加班改代码、发版本。

我个人习惯,是把规则和代码彻底分离。规则引擎,说白了就是一个“规则容器”。它不关心业务逻辑怎么算,只负责把规则存起来、匹配上、执行掉。这样,业务人员改个折扣率,就像改个Excel单元格一样简单。

核心思想: 规则即数据。把“怎么算钱”这件事,从代码里抽出来,变成可配置、可热加载的配置项。

4.1 规则引擎的架构分层

我一般把规则引擎拆成三层。你想想看,这样分层后,每一层只干一件事,出了问题也好定位。

  • 规则定义层: 负责规则的存储和版本管理。我用的是关系型数据库 + 缓存。规则存成JSON或YAML格式,方便解析。
  • 规则匹配层: 这是核心。根据乘客的行程信息(起点、终点、时间、票种等),找到所有适用的规则。
  • 规则执行层: 把匹配到的规则按优先级排序,然后依次计算,最终得出票价。

嗯,这里要注意一点:规则的优先级非常重要。我曾经在项目里遇到过,因为两条优惠规则的优先级没排好,导致乘客同时享受了“学生折扣”和“早鸟折扣”,结果票价算出来是负数……这显然不合理。

4.2 票价策略:基础计费模型

票价策略是规则引擎的“地基”。没有这个,后面的优惠、折扣都无从谈起。常见的计费模型有这么几种:

计费模型 说明 适用场景
里程计费 按实际乘坐距离计算,每公里单价固定或分段 地铁、城际铁路
区间计费 按站点区间收费,同一区间内价格固定 公交、部分地铁
封顶计费 设定单日或单次最高票价,超出部分不再收费 日票、月票场景
混合计费 以上几种模型的组合,比如“里程+封顶” 大多数城市轨道交通

举个例子,我参与过的一个项目,用的是“分段里程计费”。0-6公里收2元,6-12公里收3元,以此类推。这个规则在数据库里存成一张表:

{
  "fare_rules": [
    {"min_km": 0, "max_km": 6, "price": 2.0},
    {"min_km": 6, "max_km": 12, "price": 3.0},
    {"min_km": 12, "max_km": 20, "price": 4.0},
    {"min_km": 20, "max_km": 999, "price": 5.0}
  ],
  "version": "v2.1",
  "effective_date": "2024-06-01"
}

你看,这个结构很简单。但实际生产环境里,规则会复杂得多。比如,有的城市会区分“工作日”和“非工作日”的票价,有的会区分“高峰时段”和“平峰时段”。

4.3 优惠政策:多维度叠加与互斥

优惠政策是规则引擎里最“花样百出”的部分。我见过最复杂的场景,是一个乘客同时满足:学生身份、使用储值卡、在非高峰时段乘车。这三条优惠能不能叠加?怎么叠加?

我的做法是,给每条规则打上“叠加类型”标签:

  • 可叠加: 多条规则可以同时生效,比如“学生折扣”和“储值卡折扣”。
  • 互斥: 多条规则只能选一条生效,比如“早鸟折扣”和“晚鸟折扣”。
  • 覆盖: 高优先级规则覆盖低优先级规则,比如“免费乘车”覆盖所有其他折扣。
避坑指南: 我曾经因为没处理好“互斥”规则,导致一个乘客同时享受了“老年卡免费”和“残疾人优惠”,系统直接报错。后来我加了一个“规则冲突检测”模块,在规则生效前先做一次逻辑校验。

优惠政策的数据结构,我一般这样设计:

{
  "promotion_id": "STU_2024",
  "name": "学生暑期优惠",
  "type": "percentage",  // 折扣类型:百分比、固定金额、免费
  "value": 0.8,          // 打8折
  "conditions": {
    "user_type": "student",
    "time_range": ["09:00", "17:00"],
    "day_type": "weekday"
  },
  "stack_type": "stackable",  // 可叠加
  "priority": 10
}

4.4 时段折扣:时间维度的动态配置

时段折扣,说白了就是“不同时间,不同价格”。这个在轨道交通里很常见,比如早高峰贵一点,鼓励错峰出行;晚平峰便宜点,吸引客流。

我建议把时段规则设计成“时间片”的形式。每个时间片可以独立配置折扣率。举个例子:

{
  "time_slots": [
    {"start": "07:00", "end": "09:00", "discount": 1.0},   // 早高峰,不打折
    {"start": "09:00", "end": "17:00", "discount": 0.9},   // 平峰,9折
    {"start": "17:00", "end": "19:00", "discount": 1.0},   // 晚高峰,不打折
    {"start": "19:00", "end": "23:00", "discount": 0.8}    // 夜间,8折
  ],
  "special_dates": [
    {"date": "2024-10-01", "discount": 0.7}  // 国庆节,7折
  ]
}

这里有个细节:时间片的边界处理。比如,乘客在08:59进站,09:01出站,该按哪个时段算?我一般按“进站时间”为准,因为进站时间更容易获取,也更容易被乘客理解。

注意: 时段折扣的生效时间,一定要和票务系统的时钟同步。我见过一个案例,因为服务器时间慢了5分钟,导致早高峰的乘客享受了平峰折扣,一天下来损失了好几万。所以,NTP时间同步是必须的。

4.5 规则引擎的性能与热加载

规则引擎的性能,直接影响到闸机的通行效率。你想想看,如果每个乘客过闸都要查数据库、算规则,那高峰期闸机前得排多长的队?

我的做法是:规则缓存 + 预计算。把常用的规则加载到内存里,比如Redis。乘客进站时,直接从缓存里取规则计算,毫秒级响应。

另外,规则引擎要支持“热加载”。也就是说,业务人员改了规则后,不需要重启服务,规则就能自动生效。我一般用“版本号+定时刷新”的方式:

// 伪代码示例
public class RuleEngine {
    private Map<String, Rule> ruleCache;
    private int currentVersion = 0;

    public void refreshRules() {
        int latestVersion = getLatestVersionFromDB();
        if (latestVersion > currentVersion) {
            ruleCache = loadRulesFromDB();
            currentVersion = latestVersion;
            System.out.println("规则已更新,版本号: " + currentVersion);
        }
    }
}

嗯,这里要提醒一下:热加载虽然方便,但一定要做好“灰度发布”。先在小范围试点,确认没问题了再全量生效。我曾经因为一次规则更新没做灰度,导致某个站点的票价全部算错,运维同学差点没把我拉黑。

4.6 小结

票务规则引擎,说白了就是“把复杂留给自己,把简单留给业务”。通过规则定义、匹配、执行三层架构,配合灵活的优惠政策配置和时段折扣,你可以让业务人员像搭积木一样,快速响应市场变化。

下一章,咱们聊聊“清分结算系统”。这可是票务系统的“财务大脑”,怎么把钱分得清清楚楚、明明白白,里面门道可不少。