COD 单页(One Page)链路说明

本文档总结 COD 单页 相关实现:自定义 URL 落页、Liquid 取参、商品维度物流异步接口、无 checkout_token 下单。与当前仓库代码一致;若你本地有二次修改,以实现为准。

目录


1. 背景与目标

  • 通过 o_diy_filetype=3 文件映射,将自定义路径映射为 商品维度的 COD 着陆页,在同一页完成选 SKU、国家、数量后 异步拉物流提交 COD 订单
  • 在 SSR 阶段用 Liquid Tag 算运费(SKU/数量会变),改为 homeapi 按当前选择请求。
  • 为单页单独再拆「税费 / 附属信息」等接口(现阶段以业务需要为准;当前仅物流 + 下单)。
  • 下单 复用 既有 CodOrderHandlerService::saveOrder,保证黑名单、限流、写单、优惠券与 COD 成功事件等与旧 COD 链路一致。
  • saveCheckoutCart(Redis checkoutcart 快照)已去掉:单笔下单仅以内存中的 CodCartDto + 订单持久化的 checkout_token 为准,避免误认为「必须用 Redis 才能完成下单」。若日后需要「按 token 排障」,可再加入写入逻辑。

2. 前端入口:cod_page 映射(home)

2.1 Diy 文件映射与 Context

当 home 路由未命中并走 Diy 文件映射,且解析出的 JSON 满足 type = cod_page、有效 value 时:

  • 将整条 map 配置写入:app('Context')->cod_page_params(文件名与历史对话中的 cod_page 上下文一致,以实现为准)。
  • 再通过 DiyFileService::handlerPathByFileType('cod_page', $mapValue) 得到真实 ThinkPHP path 并二次 dispatch。

实现原因:不落独立「假路由」controller 堆叠;配置跟着 Diy 映射走,模板与 API 都只依赖 Context。

2.2 落地路径(当前)

  • common/services/DiyFileService.phpcod_pagecod-one-page-products/detail/{商品id}(与旧版仅 products/detail 的实现可能不同,以本仓库为准)。
  • app/home/route/route.php:注册 cod-one-page-products/detail/:idcodOnePageProduct/detail(继承详情逻辑)。

2.3 专用模板:codOnePageProduct

app/home/controller/codOnePageProduct.php 覆盖 fetch()

  • Context->cod_page_params['template'] 取 Liquid 模板名,缺省为 product_detail
  • 传入 $data['template'],由 HomeBaseController 侧逻辑决定最终渲染模板。

实现原因:复用商品详情数据与行为,同时允许 Diy 里配置 另一套主题模板,解决「同商品 id、不同落地视觉」的问题。

2.4 Liquid:读取单页配置

  • extend/liquidExtend/tags/TagGetCodPageParams.php:把 Context->cod_page_params 解码/归一后 merge 到模板变量(默认变量名 cod_page_params,可用 tag 参数覆盖)。

其他可能用到的 Tag(以仓库为准):

  • TagGetRecentOrders:按配置条数从 ES 拉最近在线单 + COD 单(高并发场景避免直查 MySQL);缓存 key 在 CacheKeyHelper::tagRecentOrders,空数据/有数据 TTL 策略见项目持久规则。

3. homeapi:COD One Page 路由与控制器

前缀/:store_random/cod-one-page-checkoutsstore_random 与现有 checkout 类路由一致)

方法路径控制器方法说明
GET/{store_random}/cod-one-page-checkouts/shippingsOrderCodOnePage::getShippingMethods商品 + 变体 + 数量 + 国家 返回可用 COD 物流,不需要 checkout_token
POST/{store_random}/cod-one-page-checkouts/orderOrderCodOnePage::saveOrder checkout_token 入参的 COD 下单

实现原因:与带 checkout_token/:store_random/cod-checkouts/... 分离,避免职责混在一个 Controller 里,文档与限流策略也更清晰。


4. 物流接口(GET shippings)

Query 参数(均必填,number):

  • product_id
  • variant_id
  • country_id
  • quantity(服务端会做 max(1, quantity)

流程简述

  1. CodCartService::buildCartByProductId($productId, $variantId, $quantity) 构造临时 CodCartDto
  2. CodShippingZoneHandlerService::getShippingMethodsByCountryId($cartData, $countryId)

实现原因:运费规则依赖行项(SKU、数量、金额等),必须随用户选择变化而 异步 拉取。


5. 下单接口(POST order)

5.1 请求体(JSON)

  • product_info(对象,必填,校验在 CheckoutCodOnePageRequest

    • product_id:integer,> 0
    • variant_id:integer,> 0
    • quantity:integer,> 0(业务上仍可通过 getter 做 max(1, …) 兜底)
  • order_info / trans_info / shipping_address:与既有 CheckoutCodPageRequest 一致(父类校验规则)。

    • One Page 不要求 请求里带 order_info.checkout_token(子类已去掉该必填项)。

实现原因:商品维度单独成 product_info,与地址、物流方案、备注等分层;下单主体仍复用原 COD 字段,减少重复造 Request。

5.2 请求对象:CheckoutCodOnePageRequest

  • 文件:app/homeapi/req/CheckoutCodOnePageRequest.php
  • 提供:getProductId()getVariantId()getQuantity()setCheckoutToken() / getCheckoutToken()(服务端注入临时 token,供下游仍读 token 的代码使用,如异常路径里对 token 的引用)。

实现原因:参数校验与语义化读取集中在 Request,Controller 只负责调 Service 与 outputJson

5.3 响应

成功时统一 outputJson 包装,data 示例:

{
  "checkout_url": "/cod-one-page-checkouts/success/{checkout_token}"
}

checkout_token 由服务端 $this->request->getCheckoutVisitId(true) 生成,前端不必传、也不必提前 hold


6. 下单服务:CodOnePageOrderHandlerService

文件:common/services/CodOnePageOrderHandlerService.php

主流程

  1. CheckoutCodOnePageRequestproduct_id / variant_id / quantity
  2. CodOnePageOrderHandlerService::buildCartByProductIdCodCartDto(构造前将 Context 与购物车货币固定为店铺主货币exchange_rate = 1;不可用则视为空购物车)。
  3. 生成临时 checkout_token$requestData->setCheckoutToken($checkoutToken)
  4. 不写入 Redis checkoutcart(已不再调用 saveCheckoutCart)。
  5. 在本接口内触发与 旧 buynow 漏斗对齐 的两类事件:
    • AddToCart(使用 ProductModel / ProductVariantModel + AddToCartDataStruct
    • BeginCheckoutBeginCheckoutDataStruct + UTM)
  6. 调用 CodOrderHandlerService::saveOrder($cartData, $requestData, $checkoutToken),复用其后事务与 COD 订单事件。
  7. 返回单页成功页路径 /cod-one-page-checkouts/success/{checkout_token}

订单货币:单页下单与物流预览均在服务内调用 applyStoreBaseCurrency(),订单 currency_code / currency_ratecurrent_*_price店铺主货币写入,与前台 cookie 所选展示货币无关(商品库价本身为主货币)。

6.1 有意不触发的事件

按约定:若现有 COD token 结账流程里没有触发的漏斗事件,单页也不新增。因此当前 在此处单独触发:

  • AddAddressInfo
  • AddShippingInfo
  • AddPaymentInfo

(标准在线结账里这些往往在分步链路里打点;COD 旧链路若以 codOrderSubmitSuccess 等为准,则单页对齐该集合即可。)

6.2 购物车构造:CodCartService::buildCartByProductId

  • 优先按 variant_id 在变体列表中命中;不行再退回默认变体 / 第一个变体。
  • quantity 下限为 1。

7. 与「带 checkout_token」COD 结账的关系

  • 旧链路(带 token)POST /{store_random}/cod-checkouts/{checkout_token},购物车来自 Redis 与该 token 绑定。逐步说明见 cod-checkout-flow.md
  • 单页链路一个 POST 完成创单所需的商品信息 + 地址 + trans_info;token 仅服务端生成,用于订单表字段与成功页 URL,不要求先有 buynow Redis 快照。

两套接口 并行存在,按需选用。


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

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

体系COD 单页
diy_offerbuildCartByProductId 后走 CodCartService 计价链
promotion✅ 同上
coupontrans_info.coupon_code / 预券 Cache;⚠️ 替换型券风险同 COD token
order_diy_offer

顺序:与 COD token 相同;商品来自请求体临时 DTO,不读 checkoutcart Redis。

落库:单次 saveOrdero_cod_order + o_cod_order_product;无结账中途 reconcile。


9. 相关文件索引(维护时从这里追)

模块路径
home 映射注入app/home/ExceptionHandle.php
路径映射common/services/DiyFileService.php
home 路由app/home/route/route.php
单页详情控制器app/home/controller/codOnePageProduct.php
Liquid 配置 Tagextend/liquidExtend/tags/TagGetCodPageParams.php
homeapi 路由app/homeapi/route/route.php
One Page API 控制器app/homeapi/controller/OrderCodOnePage.php
One Page 下单 Requestapp/homeapi/req/CheckoutCodOnePageRequest.php
COD 结账 Request(父类)app/homeapi/req/CheckoutCodPageRequest.php
One Page 下单编排common/services/CodOnePageOrderHandlerService.php
COD 下单核心(复用)common/services/CodOrderHandlerService.php
临时购物车构造common/services/CodCartService.php

更多与 Liquid / 上下文相关的说明可参考:home-liquid-rendering-and-cod-one-page-product.md(若与本文件冲突,以实现代码为准)。


10. 联调备忘

  • 物流与下单请求的 store_random 需与网关/前置约定一致(与同模块其他 checkout 接口相同)。
  • 若返回「Cart is empty」,优先查:product_id/variant_id 是否归属当前店铺、ProductService::productDetail 是否可查、变体是否被删或未上架。
  • 漏斗事件以 单次 POST order 为边界触发;若需防刷,可在路由或控制器上挂与 homeapi 一致的 ReqLimiter(按项目规范配置)。