单页结账(One Page Checkout)链路说明

本文档说明 单页结账checkout_type = one_page)的完整业务流程:单屏 UI + homeapi JSON API 驱动,含每步 Service、MySQL / Redis、接口调用方式。与当前仓库代码一致。

相关:standard-checkout-flow.md(多步对比)、cart-checkout-token-visit-id.md

目录


1. 背景与目标

  • 店铺配置 checkout_process = one_page(或 smart Cookie 命中 one_page)时,用户进入 单页结账
  • 前端在同一页面内多次调用 homeapi;presave 在邮箱/地址变化时增量写库;complete 一次性落盘并进入支付。
  • 创单时机:presave 首次成功(占位 order_number#prvsave#);complete _finalize 全量字段。
  • 专用 MQ 事件:OrderOnePageCreate(presave 创单时)。

2. 入口与页面

2.1 checkout_token 来源

与标准相同:购物车 POST /cart、buynow、instant 等 → CartService::getCheckoutUrl() / createBuynow()

2.2 URL

/{storeId}-{random}/one-page-checkouts/{checkout_token}

CheckoutService::generateCheckoutUrlByConfigcheckout_process=one_page 时生成。

2.3 SSR 首屏(home)

路由GET /:store_random/one-page-checkouts/:checkout_token
Controllerapp/home/controller/OrderOnePage.php::index

动作ServiceRedisMySQL
权限checkCheckoutPermission
校验CartService::_checkCheckoutCart读 checkoutcart读 order
计价CheckoutOnePageService::getCart()读 cart有单 reconcile
渲染Liquid 模板 checkout

首屏传入:carttip_settingcustomer_addressescheckout_tokenorder_offer_list 等。

支付网关页POST /:store_random/one-page-checkouts/:checkout_token → 复用 Order::checkout(payment_gateway 逻辑)。


3. 业务流程总览

sequenceDiagram
    participant FE as 单页前端
    participant API as homeapi/OrderOnePage
    participant S as CheckoutOnePageService
    participant OS as OrderService

    FE->>API: GET home 首屏(SSR cart)
    FE->>API: POST presave(邮箱 debounce)
    API->>S: preSave()
    S->>OS: saveOrder + saveProducts + 可选地址/物流
    FE->>API: POST price / shippings / payments ...
    FE->>API: POST complete
    API->>S: complete()
    S->>OS: 全量 save + handlerOrderDiyOffer + savePaymentMethod
    API-->>FE: checkout_url(网关或 success)
    FE->>API: POST one-page-checkouts/{token}(支付)

4. homeapi 路由一览

前缀/:store_random/one-page-checkouts/:checkout_token
Controllerapp/homeapi/controller/OrderOnePage.php
请求体:统一 JSON,结构见 §5(CheckoutOnePageRequest 校验)。

方法路径后缀ControllerService是否写库
POST/presavepreSaveCheckoutOnePageService::preSave()
POST/pricecalPricegetCart()否(内存计价)
POST/shippingsgetShippinggetShippingMethods()
POST/paymentsgetPaymentsgetPaymentList()
POST/insurancegetInsuranceSettinggetInsuranceSetting()
POST/addongetShippingInfoAddongetShippingInfoAddon()
POST/paymentformgetPaymentFormgetPaymentForm()
POST/precompletepreCompletepreComplete()(不触发部分事件)
POST/completecompletecomplete() + 事件

5. 请求体结构(CheckoutOnePageRequest)

Content-Typeapplication/json

{
  "order_info": {
    "customer_email": "user@example.com"
  },
  "shipping_address": {
    "first_name": "John",
    "last_name": "Doe",
    "country_id": 840,
    "address1": "123 Main St",
    "city": "NY",
    "province": "NY",
    "zip": "10001",
    "phone": "+1..."
  },
  "bill_address": { },
  "trans_info": {
    "shipping_id": 123,
    "payment_id": 456,
    "coupon_code": "SAVE10",
    "insurance": true,
    "insurance_fee_amount": 1.99,
    "ordertip_checkbox": true,
    "tip_price": 2.00
  },
  "checkoutinfo": { },
  "admin_id": 0
}
  • 各 API 按已传块校验;presave 至少需 order_info.customer_email
  • complete 需全量:shipping_address.country_idtrans_info.shipping_idtrans_info.payment_id 等。
  • trans_info.offer_from_name:JSON 字符串,complete 时写 o_order_diy_offer(积分/Seel 等)。

6. 各 API 逐步说明

6.1 POST presave(预保存)

何时调:用户填邮箱/地址时 debounce(失焦或节流)。

流程CheckoutOnePageService::preSave 72–169 行):

顺序动作MySQLRedis
0OnePageCheckoutPresaveLockKey 自旋锁(10s)lock key
1读已有 order by checkout_tokeno_order
2黑名单 / checkDeny
3saveOrder(占位单,checkout_type=one_pageinsert/update o_order
4savePreCoupono_order.coupon_*pre_use_coupon
5saveOrderProductso_order_product
6可选地址 / 账单 / 物流o_order_shipping_address
7saveCheckoutCart(首次创单)checkoutcart type=order
8eventHandler

响应

{ "code": 0, "data": { "data": "ORDER_NUMBER_OR_EMPTY" } }

事件:首次创单 → OrderCreateOrderOnePageCreaterecoveryForAdmin
presave 内 saveShippingMethod 不触发 AddShippingInfo(action 抑制)。


6.2 POST price(计价)

何时调:任意字段变化后刷新右侧订单摘要。

流程getCart()(722 行起):

CartService::getList
→ calCartCouponPrice
→ calCartTaxPrice
→ calCartShippingPrice
→ calCartInsurancePrice
→ calCartTipPrice
→ calCartPaymentPrice
→ calCartTotalPrice

MySQL:仅当 getList reconcile 已有订单时写 o_order / o_order_product
Redis:读 cart + checkoutcart。

响应:完整 cart 结构(items、各 *_priceorder 等),与 outputJson($cart) 一致。


6.3 POST shippings(物流列表)

流程getShippingMethods()ShippingZoneHandlerService 按当前 cart + shipping_address.country_id 算可用方案。

响应

{ "code": 0, "data": { "shipping_list": [ ... ] } }

6.4 POST payments(支付方式)

响应

{ "code": 0, "data": { "payment_list": [ ... ] } }

6.5 POST insurance / addon

  • insurance:运费险开关与金额配置。
  • addon:结账附加信息(CheckoutInfoService)。

只读,不写库。


6.6 POST paymentform(支付表单)

何时调:用户选支付方式后,complete 之前或之后拉 iframe/跳转 URL。

流程getPaymentForm()CheckoutService::getPaymentForm / 支付 Script。

响应:支付表单 HTML 或 redirect URL 结构(依支付通道)。


6.7 POST precomplete(预完成)

complete() 类似落盘,但 shouldTriggerEvents = false(277 行):

  • 写地址、物流、支付、diy_offer 等
  • 不触发 AddShippingInfo / AddPaymentInfo

用于「先落库再展示支付 UI、但不结束漏斗」的场景。

响应

{ "code": 0, "data": { "success": true, "data": { /* cart */ } } }

6.8 POST complete(完成下单)

何时调:用户点击「Place order」。

落盘顺序complete() 181–267 行):

顺序操作MySQL
1黑名单
2saveOrder(正式 order_number)o_order
3checkFirstOrdero_order
4savePreCouponcoupon 字段
5saveOrderProductso_order_product
6eventHandler
7收货/账单地址o_order_shipping_address, o_order_billing_address
8saveShippingMethodshipping + zone_plan
9顾客 / UTM / tag / 附加信息o_customer, o_order_*
10handlerOtherInfo(小费、券校验等)o_order
11handlerOrderDiyOffero_order_diy_offer
12savePaymentMethodpayment + billing
13orderInfo->save()汇总 current_*
14orderZeroPaymentrefreshToken

响应

{
  "code": 0,
  "data": {
    "checkout_url": "/{store}-{rand}/one-page-checkouts/{token}?step=payment_gateway"
  }
}

0 元单时 checkout_url 为 success 页 URL。

事件AddAddressInfoAddShippingInfoAddPaymentInfo(complete 阶段)。


7. 优惠券 API(与标准共用)

方法路径说明
GET/coupon/use/:checkout_token?code=&email=用券;之后调 price 刷新
GET/coupon/check/:checkout_token仅校验
DELETE/coupon/cancel/:checkout_token取消

也可在 completetrans_info.coupon_code 中传券,由 savePreCoupon 绑定。


8. Redis 交互

Key时机
{store}:checkoutcart:{token}全程;创单后 type=order
{store}:guestcart / customercartgetList / getCart 读
{store}:oemsaas:one_page_checkout:create:{token}presave 锁(10s TTL)
{store}:pre_use_coupon_*
{store}:customer:{id}顾客更新后 del
{store}:paypalcompletepayments_method:{orderId}PayPal Complete

9. 可用优惠(promotion / coupon / diy_offer / order_diy_offer)

总对照见 promotions-by-checkout-type.md

体系单页 one_page
diy_offergetCart() → 底层 getList 同在线链
promotion✅ 同上
couponcalCartCouponPriceCouponHandlerService);complete/presavetrans_info.coupon_code + savePreCoupon
order_diy_offercomplete / preCompletehandlerOrderDiyOffer(trans_info.offer_from_name)

顺序getList 商品栈(§3)→ getCart 再算券/税/运费/险/小费/支付费 → complete 最后 写 order_diy_offer。

落库:presave/complete 的 saveOrderProducts 写行价;handlerOrderDiyOffero_order_diy_offercurrent_offer_pricepromotion_price/coupon_priceorderPreData / save 写入 o_order.current_*


10. 与标准 / 渐进式差异

维度One Page
交互单屏 + 全量 JSON body 重复提交
创单presave 邮箱即占位单
diy_offercomplete 最后 handlerOrderDiyOffer
专用事件OrderOnePageCreate
专用锁presave spin lock
网关complete 返回 URL → POST one-page-checkouts/{token}

11. 相关文件索引

模块路径
homeapi Controllerapp/homeapi/controller/OrderOnePage.php
home 页面app/home/controller/OrderOnePage.php
核心 Servicecommon/services/CheckoutOnePageService.php
Request 校验app/homeapi/req/CheckoutOnePageRequest.php
路由app/homeapi/route/route.php(130–142 行)
URLcommon/services/CheckoutService.php

12. 联调备忘

  • 所有 homeapi 接口 body 为 JSON,不是 form-data。
  • presave 并发:依赖 Redis 锁;锁内会 重新读 order 防双写。
  • pricecomplete 金额不一致:查 complete 是否传齐 trans_info(shipping_id、insurance、tip、offer_from_name)。
  • 积分未扣:确认 complete 请求带 trans_info.offer_from_name
  • 支付前必须 complete 成功且 refreshToken 刷新网关金额缓存。

对比:standard-checkout-flow.md · single-page-checkout-flow.md