PAYATHON 2026

如何为 Automated Ticketing System 绘制 State machine diagram?

支付阿杰

结论

这套流程可以画成 State machine diagram,但主线应该是一笔交易的生命周期,不必把每个操作或数据字段都画成状态。

完整模型至少应包含以下内容:

  • seat-number、destination 和应付金额等属于交易数据,不是状态。
  • cash 和 card 是由 guard condition 控制的分支。
  • cash 付款需要循环累加投入金额。
  • card 验证需要用 fork/join 表示并行任务。
  • card 无效或授权失败时,流程返回 payment option。
  • cash 付款完成后,根据 change 是否大于 0 判断是否找零。
  • 打印、取票和交易完成应分别建模,以免混淆系统动作和游客动作。

如果只是描述业务步骤,Activity diagram 通常更直观。如果题目指定使用 State machine diagram,重点应放在事件发生前后 ATS 交易对象所处的状态。

状态与转换的划分

一笔交易可以划分为以下主要状态:

  1. WaitingForTripSelection
  2. DisplayingTripDetails
  3. CalculatingPayment
  4. WaitingForPaymentOption
  5. AcceptingCash
  6. ValidatingCard
  7. WaitingForBankAuthorization
  8. PaymentCompleted
  9. PrintingDocuments
  10. DispensingChange
  11. WaitingForCollection
  12. TransactionCompleted

转换可以按下面的形式标注:

event [guard condition] / action

例如:

cashInserted(amount) [cashTotal < amountDue] / display("Insert More Cash")

这里:

  • event 是触发转换的事件;
  • [guard condition] 用来判断该转换能否执行;
  • / action 是转换过程中执行的动作。

可直接使用的 PlantUML 示例

@startuml
hide empty description

state "Waiting for trip selection" as TripSelection
state "Displaying trip details" as DisplayTrip
state "Calculating payment" as CalculatePayment
state "Waiting for payment option" as PaymentOption

state "Accepting cash" as AcceptCash
state "Checking inserted cash" as CashDecision <<choice>>

state "Check expiration date" as CheckExpiration
state "Check credit balance" as CheckBalance
state "Card validation fork" as CardFork <<fork>>
state "Card validation join" as CardJoin <<join>>
state "Evaluating card" as CardDecision <<choice>>

state "Waiting for bank authorization" as BankAuthorization
state "Payment completed" as PaymentCompleted
state "Printing ticket and receipt" as Printing
state "Dispensing change" as DispenseChange
state "Waiting for collection" as WaitingForCollection
state "Transaction completed" as TransactionCompleted

[*] --> TripSelection

TripSelection --> DisplayTrip : tripSelected(trip)
DisplayTrip : entry / display(seatNumber, destination)
DisplayTrip --> CalculatePayment : detailsDisplayed

CalculatePayment : entry / amountDue = calculatePayment(trip)
CalculatePayment --> PaymentOption : calculationCompleted

PaymentOption --> AcceptCash : selectCash / cashTotal = 0
PaymentOption --> CardFork : selectCard

AcceptCash --> CashDecision : cashInserted(amount) / cashTotal += amount

CashDecision --> AcceptCash : [cashTotal < amountDue]\n/ display("Insert More Cash")
CashDecision --> PaymentCompleted : [cashTotal >= amountDue]\n/ change = cashTotal - amountDue

CardFork --> CheckExpiration
CardFork --> CheckBalance

CheckExpiration --> CardJoin : expirationChecked
CheckBalance --> CardJoin : balanceChecked

CardJoin --> CardDecision

CardDecision --> BankAuthorization : [expirationValid && balanceSufficient]\n/ requestAuthorization()
CardDecision --> PaymentOption : [!expirationValid || !balanceSufficient]\n/ display("Select Payment Option")

BankAuthorization --> PaymentCompleted : authorizationApproved
BankAuthorization --> PaymentOption : authorizationDeclined\n/ display("Select Payment Option")

PaymentCompleted --> Printing : paymentRecorded
Printing : entry / printTicket(); printReceipt()

Printing --> DispenseChange : [paymentMethod == CASH && change > 0]
Printing --> WaitingForCollection : [paymentMethod == CARD || change == 0]

DispenseChange : entry / dispenseChange(change)
DispenseChange --> WaitingForCollection : changeDispensed

WaitingForCollection --> TransactionCompleted : itemsCollected
TransactionCompleted : entry / display("Transaction Complete")

TransactionCompleted --> [*] : messageDisplayed
@enduml

cash 循环如何表示

首先要明确“金额不正确”的具体含义。题目后面提到可能需要找零,因此更合理的判断条件是:

cashTotal < amountDue

这表示当前金额不足,需要继续等待游客投入现金,而不是要求投入金额必须与应付金额完全相等。

每次收到现金时执行:

cashTotal = cashTotal + amount

然后判断累计金额:

[cashTotal < amountDue]
    显示 "Insert More Cash"
    返回 AcceptingCash

[cashTotal >= amountDue]
    change = cashTotal - amountDue
    进入 PaymentCompleted

这种写法既能表示循环,也能处理超额投入和找零。

如果 ATS 不接受超额现金,还需要增加一条单独的规则,例如:

[cashTotal > amountDue] / rejectCash(amount)

不过题目没有给出这项限制,因此不应直接把它加入最终模型。

card 并行验证如何表示

expiration date 检查和 credit balance 检查是两个并行任务,应表示为:

fork -> 两个并行状态 -> join

fork 同时启动:

CheckExpiration
CheckBalance

join 等待两个检查全部完成,再根据组合结果进行判断:

[expirationValid && balanceSufficient]

只有两个条件都成立,ATS 才能向 bank 请求授权。只要有一个条件失败,流程就返回:

WaitingForPaymentOption

card 本地验证通过不等于支付成功。完整模型还应区分:

  • 卡片验证通过;
  • bank authorization approved;
  • bank authorization declined。

因此,授权被拒绝时也应返回 payment option,不能直接进入 PaymentCompleted。

bank 更新 account 的表示方式

如果这张图只描述 ATS,就不应在其状态机中展开 bank 内部更新游客账户的过程。ATS 通常只能直接观察到:

requestAuthorization()
authorizationApproved
authorizationDeclined

游客账户扣款是 bank 的职责,可以将其视为授权成功产生的外部效果,也可以放在另一张 bank 状态图或 Sequence diagram 中描述。

如果必须在当前图中标注,可以写成:

authorizationApproved / bank updates account

这条标注只用于说明外部效果,不应让人误以为 ATS 会直接修改银行账户。

打印和找零是否并行

题目只明确要求 card 的两个检查并行,并没有说明打印 ticket、打印 receipt 和找零是否同时进行。因此,较稳妥的顺序是:

  1. 支付完成;
  2. 打印 ticket 和 receipt;
  3. 使用 cash 且有 change 时执行找零;
  4. 等待游客取走物品;
  5. 显示完成消息。

如果实际设备可以同时打印和找零,也可以增加一个 fork:

PaymentCompleted
        |
       fork
      /    \
 Printing  DispensingChange
      \    /
       join
        |
WaitingForCollection

这种设计还需要处理 card 付款没有 change 的情况,例如提供一条不执行找零的分支,否则 join 可能一直等待并不存在的找零任务。

容易遗漏的建模细节

1. 数据不应画成状态

下面这些内容属于交易上下文数据:

seatNumber
destination
amountDue
paymentMethod
cashTotal
change
expirationValid
balanceSufficient

通常没有必要把 Displaying seat-number 和 Displaying destination 分别画成状态。只有当 ATS 会长时间停留在这些页面,并在各页面中响应不同的用户事件时,才需要这样处理。

2. 游客动作和 ATS 状态要分开

“游客取得 ticket 和 change”是一个外部事件,可以表示为:

itemsCollected

在这个事件发生前,ATS 处于:

WaitingForCollection

这样可以明确区分“系统已经输出物品”和“游客已经取走物品”。

3. join 与 choice 的含义不同

  • fork:启动多个并行区域;
  • join:等待所有并行区域完成;
  • choice:根据 guard condition 选择一条路径。

card 检查结束后不能只有 join,还需要使用 choice 判断两个验证结果。

4. 完成消息的顺序需要明确

如果要求游客取走 ticket 和 change 后才显示 "Transaction Complete",转换应写成:

WaitingForCollection
    --> TransactionCompleted : itemsCollected

如果设备在输出完成后立即显示消息,则可以改由 printingCompleted 或 changeDispensed 触发。这两种设计表达的含义不同,应按实际需求选择。

5. 异常流程应按需求补充

实际 ATS 通常还需要处理:

  • 用户取消交易;
  • 操作超时;
  • card reader 读取失败;
  • bank 请求超时;
  • printer 缺纸或故障;
  • cash acceptor 拒绝纸币;
  • change 不足;
  • ticket 或 change 未被取走。

题目没有说明这些条件,因此不能把它们当作已经确定的需求。用于真实系统设计时,应先确认需求,再将相应情况建模为错误状态、重试转换或退款流程。

备注:内容仅供参考。