单页结账(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. 背景与目标
- 2. 入口与页面
- 3. 业务流程总览
- 4. homeapi 路由一览
- 5. 请求体结构(CheckoutOnePageRequest)
- 6. 各 API 逐步说明
- 7. 优惠券 API(与标准共用)
- 8. Redis 交互
- 9. 可用优惠(promotion / coupon / diy_offer / order_diy_offer)
- 10. 与标准 / 渐进式差异
- 11. 相关文件索引
- 12. 联调备忘
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::generateCheckoutUrlByConfig 在 checkout_process=one_page 时生成。
2.3 SSR 首屏(home)
路由:GET /:store_random/one-page-checkouts/:checkout_token
Controller:app/home/controller/OrderOnePage.php::index
| 动作 | Service | Redis | MySQL |
|---|---|---|---|
| 权限 | checkCheckoutPermission | — | — |
| 校验 | CartService::_checkCheckoutCart | 读 checkoutcart | 读 order |
| 计价 | CheckoutOnePageService::getCart() | 读 cart | 有单 reconcile |
| 渲染 | Liquid 模板 checkout | — | — |
首屏传入:cart、tip_setting、customer_addresses、checkout_token、order_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
Controller:app/homeapi/controller/OrderOnePage.php
请求体:统一 JSON,结构见 §5(CheckoutOnePageRequest 校验)。
| 方法 | 路径后缀 | Controller | Service | 是否写库 |
|---|---|---|---|---|
| POST | /presave | preSave | CheckoutOnePageService::preSave() | 是 |
| POST | /price | calPrice | getCart() | 否(内存计价) |
| POST | /shippings | getShipping | getShippingMethods() | 否 |
| POST | /payments | getPayments | getPaymentList() | 否 |
| POST | /insurance | getInsuranceSetting | getInsuranceSetting() | 否 |
| POST | /addon | getShippingInfoAddon | getShippingInfoAddon() | 否 |
| POST | /paymentform | getPaymentForm | getPaymentForm() | 否 |
| POST | /precomplete | preComplete | preComplete() | 是(不触发部分事件) |
| POST | /complete | complete | complete() | 是 + 事件 |
5. 请求体结构(CheckoutOnePageRequest)
Content-Type:application/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_id、trans_info.shipping_id、trans_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 行):
| 顺序 | 动作 | MySQL | Redis |
|---|---|---|---|
| 0 | OnePageCheckoutPresaveLockKey 自旋锁(10s) | — | lock key |
| 1 | 读已有 order by checkout_token | o_order | — |
| 2 | 黑名单 / checkDeny | — | — |
| 3 | saveOrder(占位单,checkout_type=one_page) | insert/update o_order | — |
| 4 | savePreCoupon | o_order.coupon_* | pre_use_coupon |
| 5 | saveOrderProducts | o_order_product | — |
| 6 | 可选地址 / 账单 / 物流 | o_order_shipping_address 等 | — |
| 7 | saveCheckoutCart(首次创单) | — | checkoutcart type=order |
| 8 | eventHandler | — | — |
响应:
{ "code": 0, "data": { "data": "ORDER_NUMBER_OR_EMPTY" } }事件:首次创单 → OrderCreate、OrderOnePageCreate、recoveryForAdmin。
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、各 *_price、order 等),与 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 | 黑名单 | — |
| 2 | saveOrder(正式 order_number) | o_order |
| 3 | checkFirstOrder | o_order |
| 4 | savePreCoupon | coupon 字段 |
| 5 | saveOrderProducts | o_order_product |
| 6 | eventHandler | — |
| 7 | 收货/账单地址 | o_order_shipping_address, o_order_billing_address |
| 8 | saveShippingMethod | shipping + zone_plan |
| 9 | 顾客 / UTM / tag / 附加信息 | o_customer, o_order_* |
| 10 | handlerOtherInfo(小费、券校验等) | o_order |
| 11 | handlerOrderDiyOffer | o_order_diy_offer |
| 12 | savePaymentMethod | payment + billing |
| 13 | orderInfo->save() | 汇总 current_* |
| 14 | orderZeroPayment 或 refreshToken | — |
响应:
{
"code": 0,
"data": {
"checkout_url": "/{store}-{rand}/one-page-checkouts/{token}?step=payment_gateway"
}
}0 元单时 checkout_url 为 success 页 URL。
事件:AddAddressInfo、AddShippingInfo、AddPaymentInfo(complete 阶段)。
7. 优惠券 API(与标准共用)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /coupon/use/:checkout_token?code=&email= | 用券;之后调 price 刷新 |
| GET | /coupon/check/:checkout_token | 仅校验 |
| DELETE | /coupon/cancel/:checkout_token | 取消 |
也可在 complete 的 trans_info.coupon_code 中传券,由 savePreCoupon 绑定。
8. Redis 交互
| Key | 时机 |
|---|---|
{store}:checkoutcart:{token} | 全程;创单后 type=order |
{store}:guestcart / customercart | getList / 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_offer | ✅ getCart() → 底层 getList 同在线链 |
| promotion | ✅ 同上 |
| coupon | ✅ calCartCouponPrice(CouponHandlerService);complete/presave 的 trans_info.coupon_code + savePreCoupon |
| order_diy_offer | ✅ complete / preComplete → handlerOrderDiyOffer(trans_info.offer_from_name) |
顺序:getList 商品栈(§3)→ getCart 再算券/税/运费/险/小费/支付费 → complete 最后 写 order_diy_offer。
落库:presave/complete 的 saveOrderProducts 写行价;handlerOrderDiyOffer 写 o_order_diy_offer 与 current_offer_price;promotion_price/coupon_price 经 orderPreData / save 写入 o_order.current_*。
10. 与标准 / 渐进式差异
| 维度 | One Page |
|---|---|
| 交互 | 单屏 + 全量 JSON body 重复提交 |
| 创单 | presave 邮箱即占位单 |
| diy_offer | complete 最后 handlerOrderDiyOffer |
| 专用事件 | OrderOnePageCreate |
| 专用锁 | presave spin lock |
| 网关 | complete 返回 URL → POST one-page-checkouts/{token} |
11. 相关文件索引
| 模块 | 路径 |
|---|---|
| homeapi Controller | app/homeapi/controller/OrderOnePage.php |
| home 页面 | app/home/controller/OrderOnePage.php |
| 核心 Service | common/services/CheckoutOnePageService.php |
| Request 校验 | app/homeapi/req/CheckoutOnePageRequest.php |
| 路由 | app/homeapi/route/route.php(130–142 行) |
| URL | common/services/CheckoutService.php |
12. 联调备忘
- 所有 homeapi 接口 body 为 JSON,不是 form-data。
- presave 并发:依赖 Redis 锁;锁内会 重新读 order 防双写。
price与complete金额不一致:查 complete 是否传齐trans_info(shipping_id、insurance、tip、offer_from_name)。- 积分未扣:确认 complete 请求带
trans_info.offer_from_name。 - 支付前必须
complete成功且refreshToken刷新网关金额缓存。