tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 探索SpringBoot

  • 常用功能实现

  • Security认证授权

  • 扩展

  • 实战与注意事项

    • Spring声明式事务,请正确处理
    • 实现微信支付功能
    • 对接多个票务系统存在差异化解决方案
      • 问题描述
        • 购票规则复杂 / 票型多样处理难
        • 一个例子
        • 太乙仙山:
        • 神农架景区:
        • 天门山景区:
        • 问题点:
        • 问题本质
        • 设计目标或解决思路
        • 解决方案-参考
        • 抽象统一票结构
        • 适配器-将不同票转为通用模式
        • 策略模式-封装票务处理逻辑
        • 组合使用
        • 整合
  • 其它

  • 《SpringBoot》笔记
  • 实战与注意事项
EffectTang
2025-07-24
目录

对接多个票务系统存在差异化解决方案

# 对接多个票务系统存在差异化解决方案

# 问题描述

# 购票规则复杂 / 票型多样处理难

  • 问题原因:不同供应商支持票型、规则差异化大
  • 解决方案:
    • 抽象统一票型结构,使用策略模式封装不同规则
    • 对接层适配器模式屏蔽供应商差异

旅游电商、票务中台中非常典型的一个难点:不同供应商的票型设计和业务规则五花八门,系统如何统一适配和维护?

这个问题看似只是“多几个 if”,但实际上涉及到系统解耦性、扩展性、配置灵活性等核心架构能力。

# 一个例子

系统需要对接多个景区或供应商的票务系统,例如:

# 太乙仙山:

  • 支持:单人票 / 团体票 / 学生票
  • 实名制:仅身份证,购票即绑定游客
  • 时间规则:限时当天使用

# 神农架景区:

  • 支持:家庭套票 / 三日票
  • 实名制:1张票可绑定多人,出票后可修改一次
  • 时间规则:购票后三日内任意一天使用

# 天门山景区:

  • 票类型复杂:门票+索道组合、门票+接送车、夜游票
  • 需要动态选择入园时间段,价格随时段变化
  • 有赠票规则(如买2赠1)

# 问题点:

  • 同一张“成人票”,在不同供应商侧含义不同、字段不同、限制不同
  • 每对接一个新供应商,系统都可能要“大改票务处理逻辑”
  • 核验票、出票、退款等流程也不统一

# 问题本质

  • 供应商票型结构异构
  • 票务规则高度差异化(实名、限制、有效期)
  • 处理逻辑容易耦合,维护成本高

# 设计目标或解决思路

  • 用 统一结构/接口 管理多种票型
  • 新增票型或供应商不改旧逻辑,做到“可插拔”
  • 系统能快速扩展,适应业务变化

# 解决方案-参考

解决方案当然不止一个,越思考总是会有更合适更好用的。下面是一个参考方案:

# 抽象统一票结构

将各家票型转换为系统内部标准结构:

public class CommonTicket {
    private String ticketId;         // 系统内部统一票ID
    private String ticketName;
    private TicketCategory category; // 成人票 / 学生票 / 套票
    private boolean realNameRequired;
    private int maxPerson;
    private LocalDate validFrom;
    private LocalDate validTo;
    private PricingRule pricingRule; // 动态定价规则
    private SupplierType supplier;   // 供应商类型
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12

各家供应商的原始字段由对接层适配器转换为 CommonTicket。

# 适配器-将不同票转为通用模式

public interface TicketAdapter {
    CommonTicket convert(SupplierTicket ticket);
    OrderResponse placeOrder(CommonOrder order);
    RefundResponse refundOrder(String orderId);
    // ...
}
# 示例
public class TaiYiAdapter implements TicketAdapter {
    public CommonTicket convert(SupplierTicket ticket) {
        // 把太乙仙山的格式转换成CommonTicket
    }

    public OrderResponse placeOrder(CommonOrder order) {
        // 实现太乙仙山下单规则
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 策略模式-封装票务处理逻辑

不同票型对应不同的处理策略,例如:

public interface TicketRuleStrategy {
    void validate(CommonOrder order);
    BigDecimal calculatePrice(CommonTicket ticket, LocalDate useDate);
}

public class FixedDateStrategy implements TicketRuleStrategy {
    public void validate(CommonOrder order) {
        // 校验是否选择了当天日期
    }
}

public class DateRangeStrategy implements TicketRuleStrategy {
    public void validate(CommonOrder order) {
        // 校验是否在有效区间内
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

系统通过票型属性自动匹配策略类:

TicketRuleStrategy strategy = strategyFactory.getStrategy(ticket.getPricingRule());
strategy.validate(order);
1
2

# 组合使用

// 下单前流程
CommonTicket ticket = adapter.convert(supplierTicket);
strategy = strategyFactory.getStrategy(ticket.getPricingRule());
strategy.validate(userOrder);  // 验证规则
price = strategy.calculatePrice(ticket, userOrder.getUseDate());

// 下单调用供应商接口
adapter.placeOrder(userOrder);

1
2
3
4
5
6
7
8
9

# 整合

以下是整合后的代码,且假定供应商是2个:

太乙仙山-供应商

票型:成人票(固定日期)

规则:

  • 必须实名(需身份证)
  • 只可当天使用

处理策略:

  • 使用 FixedDateStrategy
  • 下单时强制实名验证

神农架-供应商

票型:家庭套票(三日票)

规则:

  • 无需实名
  • 任意三天内使用
  • 最多绑定 5 人

处理策略:

  • 使用 DateRangeStrategy
  • 下单时校验人数限制
// TicketCategory.java
public enum TicketCategory {
    ADULT, STUDENT, FAMILY, COMBO
}

// SupplierType.java
public enum SupplierType {
    TAIYI, SHENNONGJIA
}

// CommonTicket.java
public class CommonTicket {
    private String ticketId;
    private String ticketName;
    private TicketCategory category;
    private boolean realNameRequired;
    private int maxPerson;
    private LocalDate validFrom;
    private LocalDate validTo;
    private PricingRule pricingRule;
    private SupplierType supplier;
    // getter/setter 省略
}

// PricingRule.java
public enum PricingRule {
    FIXED_DATE, DATE_RANGE
}

// TicketRuleStrategy.java
public interface TicketRuleStrategy {
    /**
     * 校验用户选择的使用时间是否合法
     */
    void validate(CommonTicket ticket, LocalDate useDate);

    /**
     * 计算票价(可扩展为动态定价)
     */
    BigDecimal calculatePrice(CommonTicket ticket, LocalDate useDate);
}

// FixedDateStrategy.java
public class FixedDateStrategy implements TicketRuleStrategy {
    public void validate(CommonTicket ticket, LocalDate useDate) {
        if (!useDate.equals(ticket.getValidFrom())) {
            throw new IllegalArgumentException("票只能在指定日期使用");
        }
    }

    public BigDecimal calculatePrice(CommonTicket ticket, LocalDate useDate) {
        return new BigDecimal("100.00"); // 固定价
    }
}

// DateRangeStrategy.java
public class DateRangeStrategy implements TicketRuleStrategy {
    public void validate(CommonTicket ticket, LocalDate useDate) {
        if (useDate.isBefore(ticket.getValidFrom()) || useDate.isAfter(ticket.getValidTo())) {
            throw new IllegalArgumentException("使用日期不在有效期内");
        }
    }

    public BigDecimal calculatePrice(CommonTicket ticket, LocalDate useDate) {
        return new BigDecimal("90.00"); // 范围票略便宜
    }
}

// TicketStrategyFactory.java
public class TicketStrategyFactory {
    public static TicketRuleStrategy getStrategy(PricingRule rule) {
        switch (rule) {
            case FIXED_DATE: return new FixedDateStrategy();
            case DATE_RANGE: return new DateRangeStrategy();
            default: throw new UnsupportedOperationException("不支持的规则类型");
        }
    }
}

// TicketAdapter.java
public interface TicketAdapter {
    /**
     * 将供应商提供的票型结构转换为系统通用结构
     */
    CommonTicket convert(SupplierTicket ticket);

    /**
     * 提交下单请求到供应商系统
     */
    OrderResponse placeOrder(CommonOrder order);

    /**
     * 校验是否需要验票或预处理流程
     */
    void preCheck(CommonTicket ticket);
}

// TaiYiAdapter.java(太乙仙山:实名制、固定日期、强制验证)
public class TaiYiAdapter implements TicketAdapter {
    public CommonTicket convert(SupplierTicket ticket) {
        CommonTicket ct = new CommonTicket();
        ct.setTicketId(ticket.getId());
        ct.setTicketName(ticket.getName());
        ct.setCategory(TicketCategory.ADULT);
        ct.setRealNameRequired(true); // 强实名
        ct.setValidFrom(ticket.getUseDate());
        ct.setValidTo(ticket.getUseDate());
        ct.setPricingRule(PricingRule.FIXED_DATE);
        ct.setSupplier(SupplierType.TAIYI);
        return ct;
    }

    public void preCheck(CommonTicket ticket) {
        // 实名验证机制
        if (!ticket.isRealNameRequired()) {
            throw new IllegalArgumentException("太乙必须实名");
        }
    }

    public OrderResponse placeOrder(CommonOrder order) {
        // 模拟太乙接口逻辑:必须包含身份证,指定日期出票
        return new OrderResponse("success", "太乙出票成功");
    }
}

// ShenNongJiaAdapter.java(神农架:支持多人套票、非实名、三日票)
public class ShenNongJiaAdapter implements TicketAdapter {
    public CommonTicket convert(SupplierTicket ticket) {
        CommonTicket ct = new CommonTicket();
        ct.setTicketId(ticket.getId());
        ct.setTicketName(ticket.getName());
        ct.setCategory(TicketCategory.FAMILY);
        ct.setRealNameRequired(false); // 无需实名
        ct.setValidFrom(LocalDate.now());
        ct.setValidTo(LocalDate.now().plusDays(2)); // 三日票
        ct.setPricingRule(PricingRule.DATE_RANGE);
        ct.setSupplier(SupplierType.SHENNONGJIA);
        return ct;
    }

    public void preCheck(CommonTicket ticket) {
        // 可选验证:最多5人,不实名
        if (ticket.getMaxPerson() > 5) {
            throw new IllegalArgumentException("最多可绑定5人");
        }
    }

    public OrderResponse placeOrder(CommonOrder order) {
        // 模拟接口:下单时传人数即可
        return new OrderResponse("success", "神农架出票成功");
    }
}

// 使用样例:下单流程
public class TicketService {
    public void handleOrder(SupplierTicket ticket, LocalDate useDate, TicketAdapter adapter) {
        // 1. 转换为统一结构
        CommonTicket commonTicket = adapter.convert(ticket);

        // 2. 根据票型选择策略并校验使用时间
        TicketRuleStrategy strategy = TicketStrategyFactory.getStrategy(commonTicket.getPricingRule());
        strategy.validate(commonTicket, useDate);

        // 3. 执行供应商特有的预检查
        adapter.preCheck(commonTicket);

        // 4. 计算价格(可以动态定价)
        BigDecimal price = strategy.calculatePrice(commonTicket, useDate);
        System.out.println("最终价格: " + price);

        // 5. 发起下单
        OrderResponse res = adapter.placeOrder(new CommonOrder());
        System.out.println(res.getMessage());
    }
}

// SupplierTicket、OrderResponse、CommonOrder 等为数据类,实际开发中可根据 JSON 对接格式定义字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
上次更新: 2025/07/24, 15:25:26
实现微信支付功能
IDEA_SpringBoot搭建多模块项目

← 实现微信支付功能 IDEA_SpringBoot搭建多模块项目→

最近更新
01
Spring中Bean的生命周期
09-03
02
数据不丢失与准确类
09-01
03
线程池与任务调度
08-31
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式