COD 带 checkout_token 结账链路说明

本文档说明 checkout_token 的 COD 结账/{store}/cod-checkouts/{checkout_token}):购物车 / buynow 进入 COD 着陆页,在同一页选地址、物流、券后 一次 POST 下单。与 cod-one-page-checkout.md(无 token、商品维单页)并行存在。

与当前仓库代码一致;冲突时以代码为准。

相关:cart-checkout-token-visit-id.mdcod-shipping-zone-plan.mdpromotion-discount-checkout.md(COD 计价差异)。

目录


1. 背景与目标

  • 店铺配置 cod_checkout_type仅 CODCOD 首选 时,购物车 / buynow 入口会落到 cod-checkouts/{checkout_token},而非在线 checkouts/
  • 货到付款:无在线支付网关;订单写入 o_cod_order 系列表,o_order
  • 单 POST 创单:地址 + 物流 + 券 + 备注一次提交;分步 presave / email 创单。
  • 计价在 内存 CodCartDto 完成;o_orderCartService::getList 的订单 reconcile。
  • 不写 o_order_diy_offer(积分/Seel 等订单级插件不支持 COD token 链路)。

2. 入口:checkout_token 与 URL

2.1 token 来源(与在线相同)

入口说明
POST /cartCartService::getCheckoutUrl() → 写 Redis checkoutcart
Buy NowCartService::createBuynow()
InstantGET /express/checkout

token / Redis 结构见 cart-checkout-token-visit-id.md

2.2 URL 如何变成 COD

CheckoutService::generateCheckoutUrlByCodConfig() 读取 storeConfig.cod_checkout_type

含义默认跳转
1 ONLY_ONLINE仅在线在线 checkout URL
2 ONLY_COD仅 COD/cod-checkouts/{token}
3 FIRST_ONLINE在线首选在线 URL;页面可切换 COD
4 FIRST_CODCOD 首选COD URL;页面可切换在线

快捷支付(Express)强制在线 URL(isExpressPayment)。

已存在 COD 单generateCheckoutUrlByCodOrder()o_cod_order 已有该 token → 固定 COD URL。

2.3 URL 形态

/{storeId}-{token前6位}/cod-checkouts/{checkout_token}

成功页:

/cod-checkouts/success/{checkout_token}

(home 路由组,无 store_random 前缀)

2.4 在线 / COD 双入口(可选)

SSR 首屏传入 checkout_url_arrayCheckoutService::generateCheckoutUrlArrayByCodConfig):

{
  "online_checkout_url": "/{store}-{rand}/checkouts/{token}",
  "cod_checkout_url": "/{store}-{rand}/cod-checkouts/{token}"
}

已有 COD 订单后 返回空数组,不允许切换。


3. 业务流程总览

sequenceDiagram
    participant U as 用户
    participant H as home/OrderCodPage
    participant API as homeapi/OrderCodPage
    participant CS as CodCartService
    participant OH as CodOrderHandlerService

    U->>H: GET cod-checkouts/{token}
    H->>CS: getCartList(仅 Redis)
    H-->>U: Liquid checkout 单页

    U->>API: GET shippings?country_id=
    API->>CS: getCartList
    API-->>U: 物流列表

    U->>API: GET tax?country_id=&province_id=
    API-->>U: 税费试算

    U->>API: POST use-coupon(可选)
    API-->>U: 券结果 + Cache 预券

    U->>API: POST /(saveOrder)
    API->>OH: saveOrder
    OH-->>U: checkout_url → success

与在线三种的核心差异

维度COD token在线 standard 等
订单表o_cod_ordero_order
创单一次 POST分步 / presave / email
购物车数据源仅 RedisRedis + 有单时 MySQL reconcile
支付无 gatewaypayment_gateway
order_diy_offershipping / complete 写入

4. home 页面(SSR)

路由GET /:store_random/cod-checkouts/:checkout_token
Controllerapp/home/controller/OrderCodPage.php::index

顺序动作RedisMySQL
1CodCartService::getCartListcheckoutcart + guest/customer cart 或 cart_data不读
2CodOrderModel::getOrderInfoByCheckToken已有 COD 单 → redirect success
3CheckoutService::generateCheckoutUrlArrayByCodConfig
4Liquid 模板 checkout

传入模板cart(CodCartDto toArray)、tip_settingcheckout_tokencheckout_url_array

成功页GET /cod-checkouts/success/:checkout_tokenOrderCodPage::successCodOrderService::orderFetch


5. homeapi 路由与 API

前缀/:store_random/cod-checkouts/:checkout_token
Controllerapp/homeapi/controller/OrderCodPage.php

5.1 路由一览

方法路径后缀Controller 方法写库
GET/shippingsgetShippingMethods
GET/taxgetTaxPrice
GET/address/:type/:country_id/:pathgetAddressList
GET/shipping-addongetShippingInfoAddon
POST/(根)saveOrder
POST/use-couponuseCoupon否(写 Cache 预券)
POST/cancel-couponcancelCoupon

5.2 GET shippings(物流列表)

Query(必填):

参数类型说明
country_idint收货国家

流程

CodCartService::getCartList(customerId, checkout_token)
→ CodShippingZoneHandlerService::getShippingMethodsByCountryId(cartDto, countryId)

Redis:读 checkoutcart、guest/customer cart。
MySQL:读 COD 物流区域/方案配置(CodShippingZoneAreaServiceCodShippingZonePlanService),运费计算见 cod-shipping-zone-plan.md

响应outputJson($shippingMethods) — 物流方案数组(含 shipping_id、价格等)。

调用示例

GET /{store_random}/cod-checkouts/{checkout_token}/shippings?country_id=840

5.3 GET tax(税费试算)

Query

参数类型
country_idint
province_idint(可选)

流程

CodCartService::getCartList
→ TaxService::getCartTaxes(cartDto.toArray(), countryId, provinceId)

响应:税费结构(含 tax_price 等)。


5.4 GET address(711 / 全家等三级地址)

路径参数typecountry_idpath
ServiceCodAddressService::getAddressList
响应:经 OrderCodAddressListResponse 格式化。


5.5 GET shipping-addon(附属信息)

Querycountry_id
ServiceCodCheckoutInfoService::getShippingInfoAddon
响应:结账附加字段配置(随国家变化)。


5.6 POST use-coupon(用券)

Body(JSON):

{ "code": "SAVE10" }

流程

CodCartService::getCartList
→ CodCouponService::useCoupon(cartDto, code)
→ Cache::set(customerIdentity, couponCode, 3个月)

注意:COD 用券 走在线的 checkCouponUseWithPromotionStatus;与满减叠加行为见优惠文档排障。

响应:校验成功时含 idprice 等;失败 { code: -1, msg: "..." }


5.7 POST cancel-coupon(取消券)

流程CouponHandlerService::delPreCouponCode() → 返回最新 getCartList().toArray()


5.8 POST saveOrder(下单)

Body(JSON,CheckoutCodPageRequest 校验):

{
  "order_info": {
    "checkout_token": "{必须与 URL token 一致}"
  },
  "shipping_address": {
    "email": "user@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "country_id": 840,
    "province": "NY",
    "address1": "123 Main St",
    "city": "NY",
    "zip": "10001",
    "phone": "+1...",
    "contact": "optional",
    "address_ext": {}
  },
  "trans_info": {
    "shipping_id": 123,
    "coupon_code": "SAVE10",
    "tip_price": 2.0,
    "note": "备注",
    "checkoutinfo": {}
  }
}

幂等:若 o_cod_order 已存在该 checkout_token → 直接返回 success URL,不重复创单。

流程CodOrderHandlerService::saveOrder 32–97 行):

顺序ServiceMySQL
1黑名单 / ReqLimiter
2CodCustomerService::saveCustomero_customer
3CodOrderService::saveOrdero_cod_ordercheckout_type=cod
4OrderService::checkFirstOrder
5CodOrderProductService::saveOrderProductso_cod_order_product, o_cod_order_product_property
6CodOrderShippingAddressService::saveOrderShippingAddresso_cod_order_shipping_address
7CodOrderShippingZoneplanService::saveOrderShippingZonePlano_cod_order_shipping_zone_plan
8CodOrderCustomerSysInfoService::saveCustomerSysInfoo_cod_order_customer_sysinfo
9CodOrderUtmService::saveOrderUtmo_cod_order_utm
10CodOrderTagService::saveOrderTago_cod_order_tag, o_cod_order_tag_rel
11CodOrderCouponService::couponSubtract扣券库存 + del 预券 Cache

事务:上述在 Db::startTrans() 内;失败 rollback + 限流计数 decr。

Redis 清理(下单成功且 cookie checkout_token 匹配):

removeCheckoutVisitId()
CartService::clearCart(customerId)   // 删 guest/customer cart

调用 OrderService::saveCheckoutCartcheckoutcart type(在线创单才改 order)。

事件

  • codOrderSubmitSuccess
  • codOrderPaid

响应

{
  "code": 0,
  "data": {
    "checkout_url": "/cod-checkouts/success/{checkout_token}"
  }
}

6. CodCartService 计价(内存)

getCartList()(20–61 行)只读 Redis,组装 CodCartDto

getCartListDataByCheckoutToken     // checkoutcart → cart_token 或 cart_data
→ codCartDataComposition           // 商品详情、变体、属性价
→ defaultDiyOffer(minmaxoffer)
→ if !has_minmaxoffer: cartDiyOffers(promotion, bundlesale, skubundlesale, gift)  // 同轮
→ PromotionHandlerService::getCartPromotion
→ CodCouponService::calPreCouponPrice   // 读 Cache 预券
→ cartBuildEnding

与在线 CartService::getList 差异

  • 不读 o_order、无 reconcile 写库
  • gift 与 bundlesale 同轮(在线 gift 单独第二轮)
  • updateDiyOfferInfo、无税费写库(税费在 saveOrder 时 orderInitTaxPriceHandler 算入 o_cod_order

7. Redis 交互汇总

Key时机
{store}:checkoutcart:{token}进入 COD 页、所有 GET/POST 读商品来源
{store}:guestcart:{visit_id} / customercart:{id}type=cart 时经 cart_token 读
buynow/instantcheckoutcart 内 cart_data JSON
预券 Cache(CouponHandlerService::getCustomerIdentity()use-coupon 写;calPreCouponPrice 读;下单后 del
{store}:coupons:id:{id}券详情
ReqLimiter order_codsaveOrder 限流
customer cache注册/更新顾客后 del

TTL:与在线 cart 相同(guest/customer 90 天,buynow checkoutcart 7 天)。


8. MySQL 表汇总

阶段
saveOrdero_cod_order
商品o_cod_order_product, o_cod_order_product_property
地址o_cod_order_shipping_address
物流o_cod_order_shipping_zone_plan
顾客o_customer
系统信息o_cod_order_customer_sysinfo
UTM / Tago_cod_order_utm, o_cod_order_tag, o_cod_order_tag_rel
o_coupon 扣减(via CouponModel::useCoupon

不存在o_ordero_order_diy_offero_order_billing_address(COD 无在线账单地址表)。


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

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

体系COD 带 token
diy_offerCodCartService::getCartList(gift 与 bundlesale 同轮
promotiongetCartPromotion
couponPOST use-coupon → Cache;⚠️ 无 checkCouponUseWithPromotionStatus
order_diy_offer不支持(无积分/Seel)

顺序(内存 DTO,不写 o_order):行价 → minmaxoffer → diy_offer+ gift → 满减 → CodCouponService::calPreCouponPricesaveOrder 一次落库 o_cod_order / o_cod_order_product

落库字段current_promotion_pricecurrent_coupon_pricecurrent_offer_price(cart 级 diy 折扣, o_order_diy_offer 表)。


9. 与 COD 单页(无 token)对比

维度COD token(本文)COD 单页 cod-one-page-checkout.md
入口购物车 / buynow + tokenDiy cod_page 商品着陆
API 前缀/cod-checkouts/{token}/cod-one-page-checkouts/shippings/order
商品来源Redis checkoutcart请求 product_info 临时构造
checkout_token进入结账前已有服务端 getCheckoutVisitId(true) 生成
下单 ServiceCodOrderHandlerService::saveOrder同核心,经 CodOnePageOrderHandlerService 编排
漏斗事件codOrderSubmitSuccess 为准额外 AddToCartBeginCheckout

两套 并行,按需选用。


10. 相关文件索引

模块路径
home 页面app/home/controller/OrderCodPage.php
homeapi APIapp/homeapi/controller/OrderCodPage.php
下单编排common/services/CodOrderHandlerService.php
COD 订单写common/services/CodOrderService.php
购物车 DTOcommon/services/CodCartService.php
物流common/services/CodShippingZoneHandlerService.php
common/services/CodCouponService.php
Requestapp/homeapi/req/CheckoutCodPageRequest.php
URL / 双入口common/services/CheckoutService.php
home 路由app/home/route/route.php(131–137 行)
homeapi 路由app/homeapi/route/route.php(165–173 行)

11. 联调备忘

  • order_info.checkout_token 必填且与 URL 一致(CheckoutCodPageRequest)。
  • 返回「Cart is empty」:查 Redis checkoutcart 是否过期、商品是否下架、checkoutcart.type 与 cart_token 是否有效。
  • 已有 COD 单时 GET 页与 POST 均 redirect / 返回 success,不可用同一 token 再下单。
  • 税费:先 GET /tax 试算展示;最终以 saveOrder 内 orderInitTaxPriceHandler 写入 o_cod_order.current_tax_price 为准。
  • 切换在线/COD:仅未创单时 checkout_url_array 有效;创单后 URL 锁定。
  • 限流 key:order_cod(IP + 店铺维度 firewall)。

在线结账对比:standard-checkout-flow.md