首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >线上商城到店自提、同城配送的订单状态怎么设计?一套双履约状态机实现

线上商城到店自提、同城配送的订单状态怎么设计?一套双履约状态机实现

原创
作者头像
用户5598620
发布于 2026-09-17 14:00:27
发布于 2026-09-17 14:00:27
1460
举报

导读:商城上线自提和同城配送后,最典型的故障是"订单状态乱了":用户自提码还没核销,订单却显示已完成;配送超时没人管,骑手到了系统还停在待配送;同一个自提码被两个店员核销成功。根子在于把"自提"和"配送"两条履约线塞进了一个订单状态字段。本文给一套双履约状态机的设计:主状态与履约状态分层、超时自动流转、核销幂等,附可直接抄的建表与代码。

一、一个订单状态字段为什么不够用:自提和配送是两条履约线

只用一个 status 字段管订单,自提和配送场景会互相打架。自提订单"已支付→待自提→已完成"三步走;配送订单则是"已支付→待接单→配送中→已完成",中间还要支持超时未接、配送异常、用户取消。如果把两种路径写进同一枚举,状态机要么过度膨胀,要么无法表达"支付已完成、但配送还没开始"这种中间态。

  • 订单状态表示交易进度:待支付、已支付、已取消、已完成、已售后;
  • 履约状态表示交付进度:待备货、待自提、配送中、已送达、超时未取。

两者分离,交易和交付才能独立演进。支付完成不意味着履约开始,退款也不一定要求履约必须结束。

二、订单状态机怎么建模:主状态与履约状态分开存

用一张订单表存主状态,一张履约表存履约明细,主外键关联。这样同一个订单既能挂自提核销记录,也能挂配送轨迹。

代码语言:sql
复制
-- 订单主表:只表达交易进度
CREATE TABLE orders (
  id            BIGINT PRIMARY KEY,
  order_no      VARCHAR(32) NOT NULL UNIQUE,
  status        VARCHAR(16) NOT NULL,  -- WAIT_PAY/PAID/CANCELED/FINISHED/REFUNDING
  pay_amount    DECIMAL(10,2) NOT NULL,
  version       INT NOT NULL DEFAULT 0,
  updated_at    DATETIME NOT NULL
);

-- 履约表:一次履约一行,履约类型区分自提/配送
CREATE TABLE order_fulfillment (
  id            BIGINT PRIMARY KEY,
  order_id      BIGINT NOT NULL,
  type          VARCHAR(8)  NOT NULL, -- PICKUP/DELIVERY
  status        VARCHAR(16) NOT NULL, -- PREPARING/PENDING_PICKUP/OUT_FOR_DELIVERY/DELIVERED/TIMEOUT
  store_id      BIGINT,               -- 自提门店
  pickup_code   VARCHAR(16),          -- 自提码
  pickup_at     DATETIME,             -- 实际核销时间
  timeout_at    DATETIME,             -- 超时判定时间点
  version       INT NOT NULL DEFAULT 0,
  created_at    DATETIME NOT NULL
);

状态流转必须带乐观锁版本号,防止两个终端同时推进状态:

代码语言:java
复制
// 自提核销:带 version 条件更新,影响行数为 0 说明状态已被别人推进
int rows = jdbc.update(
  "UPDATE order_fulfillment SET status='DELIVERED', pickup_at=NOW(), version=version+1 " +
  "WHERE id=? AND status='PENDING_PICKUP' AND version=?",
  fulfillmentId, currentVersion);
if (rows == 0) {
  throw new IllegalStateException("核销失败:该自提码状态已变更");
}

三、订单超时未取、配送超时未送达,状态怎么自动流转

超时是状态机里最容易被漏掉的分支。自提订单到点没人来取,需要从"待自提"流转到"超时未取"并触发通知;配送订单商家超时未接单,要么自动取消、要么转人工。实现上不要靠定时任务扫全表,用延时消息按单触发。

代码语言:java
复制
// 支付成功后,为履约单注册一个超时任务:自提单 24h、配送单 30min
TimeoutTask task = TimeoutTask.builder()
    .fulfillmentId(f.getId())
    .triggerAt(now.plus(f.getType().equals("PICKUP") ? 24 : 1, HOURS))
    .build();
delayQueue.publish(task);
代码语言:java
复制
// 超时回调:仍是条件更新,避免超时通知与正常核销竞态
int rows = jdbc.update(
  "UPDATE order_fulfillment SET status='TIMEOUT', version=version+1 " +
  "WHERE id=? AND status IN ('PENDING_PICKUP','OUT_FOR_DELIVERY') AND version=?",
  fId, version);
if (rows > 0) {
  notifyService.timeout(fId); // 给用户发提醒,自提订单可重新预约
}
  • 超时时间不要写死在代码里,放进履约类型配置,便于门店按节假日调整;
  • 超时流转与用户手动取消共用同一套条件更新,谁先执行谁生效;
  • 超时后订单主状态不直接变已完成,留给运营在后台确认,避免"超时=完成"误判。

四、自提码怎么保证只能用一次:核销的幂等设计

自提码核销是典型的"重复请求"场景:店员连点、扫码枪重发、多端同时核销。幂等的关键不是"查一下状态再更新",而是让更新本身带状态约束,配合唯一约束兜底。

代码语言:sql
复制
-- 核销流水表:一次核销一行,order_id + type 唯一,重复核销自然插入失败
CREATE TABLE fulfillment_pickup_log (
  id              BIGINT PRIMARY KEY AUTO_INCREMENT,
  fulfillment_id  BIGINT NOT NULL,
  operator_id     BIGINT NOT NULL,
  created_at      DATETIME NOT NULL,
  UNIQUE KEY uk_fulfillment (fulfillment_id)
);
代码语言:java
复制
// 先写流水(唯一键兜底),成功才推进状态;流水失败说明已核销过
try {
  jdbc.update("INSERT INTO fulfillment_pickup_log(fulfillment_id,operator_id) VALUES (?,?)",
      fId, operatorId);
} catch (DuplicateKeyException e) {
  throw new IllegalStateException("该自提码已完成核销,不能重复使用");
}
jdbc.update("UPDATE order_fulfillment SET status='DELIVERED', pickup_at=NOW(), " +
    "version=version+1 WHERE id=? AND status='PENDING_PICKUP' AND version=?",
    fId, version);

要点:流水表唯一键是最后一道闸,状态条件更新是第一道闸,两层都挡不住才需要分布式锁(单门店场景通常不需要)。配送履约同理,配送轨迹按 fulfillment_id + 节点 唯一,避免回调重复落库。

踩坑清单

  • 把自提和配送塞进同一个 status 枚举,后续每个新履约方式都要改枚举,分支爆炸;
  • 超时用定时任务全表扫,数据量大时任务积压,超时判定晚数小时;
  • 核销只"查状态再更新"不写流水,并发下两个请求都能读到 PENDING_PICKUP,双双放行;
  • 订单状态与履约状态混用,导致"已完成"出现在自提码未核销时;
  • 状态更新不带 version 条件,覆盖式 update 把别人的状态推进吞掉;
  • 超时时间写死,节假日门店无法调整,用户深夜被"超时未取"误伤。

工程落地建议

订单状态机先画清楚"主状态 + 履约状态"两张图再建表;自提单和配送单各自独立流转,共用订单主表的支付/取消/完成语义。状态变更统一走一个方法并带乐观锁,超时走延时消息。这类能力在商业 SaaS 产品(如乔拓云的网上商城)中通常作为内置模块提供。上线时先接自提单、再放开配送,逐步验证状态流转。

上线复盘清单

  • 自提单:支付→待自提→核销完成,全链路状态符合预期
  • 配送单:支付→待接单→配送中→已送达,轨迹落库不重复
  • 同一自提码连续核销两次,第二次被唯一键挡住
  • 超时订单触发提醒,主状态保持"已支付"未误改
  • 用户取消与超时流转同时发生时,只有一个分支生效

常见问题

Q:线上商城到店自提、同城配送的订单状态怎么设计?

A:拆成两层:订单主表只管交易进度(待支付/已支付/已完成),履约表按自提/配送分别记录交付进度,两张表通过订单号关联,状态各自流转,互不干扰。

Q:自提订单用户一直不来取,系统怎么处理?

A:支付成功后为履约单注册延时超时任务,到点执行条件更新把状态置为"超时未取"并通知用户;订单主状态保持已支付,由运营在后台决定改期或取消,不自动变成已完成。

Q:自提码被重复核销怎么办?

A:核销流水表对履约单建唯一索引,先写流水再推进状态;重复请求会因唯一键插入失败被拦截,配合状态的条件更新,两层防护保证核销只能成功一次。

结语

订单状态设计的目标不是状态多,而是每种履约方式都有清晰的流转边界。主状态与履约状态分离、超时与核销走条件更新,自提和配送共存也不会乱。方案按自身业务取舍,以上仅作技术分享。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、一个订单状态字段为什么不够用:自提和配送是两条履约线
  • 二、订单状态机怎么建模:主状态与履约状态分开存
  • 三、订单超时未取、配送超时未送达,状态怎么自动流转
  • 四、自提码怎么保证只能用一次:核销的幂等设计
  • 踩坑清单
  • 工程落地建议
  • 上线复盘清单
  • 常见问题
    • Q:线上商城到店自提、同城配送的订单状态怎么设计?
    • Q:自提订单用户一直不来取,系统怎么处理?
    • Q:自提码被重复核销怎么办?
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档