COD 单页(One Page)链路说明
本文档总结 COD 单页 相关实现:自定义 URL 落页、Liquid 取参、商品维度物流异步接口、无 checkout_token 下单。与当前仓库代码一致;若你本地有二次修改,以实现为准。
目录
- 1. 背景与目标
- 2. 前端入口:
cod_page映射(home) - 3. homeapi:COD One Page 路由与控制器
- 4. 物流接口(GET shippings)
- 5. 下单接口(POST order)
- 6. 下单服务:
CodOnePageOrderHandlerService - 7. 与「带 checkout_token」COD 结账的关系
- 8. 可用优惠(promotion / coupon / diy_offer / order_diy_offer)
- 9. 相关文件索引(维护时从这里追)
- 10. 联调备忘
1. 背景与目标
- 通过
o_diy_file等 type=3 文件映射,将自定义路径映射为 商品维度的 COD 着陆页,在同一页完成选 SKU、国家、数量后 异步拉物流 并 提交 COD 订单。 - 不在 SSR 阶段用 Liquid Tag 算运费(SKU/数量会变),改为 homeapi 按当前选择请求。
- 不为单页单独再拆「税费 / 附属信息」等接口(现阶段以业务需要为准;当前仅物流 + 下单)。
- 下单 复用 既有
CodOrderHandlerService::saveOrder,保证黑名单、限流、写单、优惠券与 COD 成功事件等与旧 COD 链路一致。 saveCheckoutCart(Redischeckoutcart快照)已去掉:单笔下单仅以内存中的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.php:cod_page→cod-one-page-products/detail/{商品id}(与旧版仅products/detail的实现可能不同,以本仓库为准)。app/home/route/route.php:注册cod-one-page-products/detail/:id→codOnePageProduct/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-checkouts(store_random 与现有 checkout 类路由一致)
| 方法 | 路径 | 控制器方法 | 说明 |
|---|---|---|---|
| GET | /{store_random}/cod-one-page-checkouts/shippings | OrderCodOnePage::getShippingMethods | 按 商品 + 变体 + 数量 + 国家 返回可用 COD 物流,不需要 checkout_token |
| POST | /{store_random}/cod-one-page-checkouts/order | OrderCodOnePage::saveOrder | 无 checkout_token 入参的 COD 下单 |
实现原因:与带 checkout_token 的 /:store_random/cod-checkouts/... 分离,避免职责混在一个 Controller 里,文档与限流策略也更清晰。
4. 物流接口(GET shippings)
Query 参数(均必填,number):
product_idvariant_idcountry_idquantity(服务端会做max(1, quantity))
流程简述:
CodCartService::buildCartByProductId($productId, $variantId, $quantity)构造临时CodCartDto。CodShippingZoneHandlerService::getShippingMethodsByCountryId($cartData, $countryId)。
实现原因:运费规则依赖行项(SKU、数量、金额等),必须随用户选择变化而 异步 拉取。
5. 下单接口(POST order)
5.1 请求体(JSON)
-
product_info(对象,必填,校验在CheckoutCodOnePageRequest)product_id:integer,> 0variant_id:integer,> 0quantity:integer,> 0(业务上仍可通过 getter 做max(1, …)兜底)
-
order_info/trans_info/shipping_address:与既有CheckoutCodPageRequest一致(父类校验规则)。- One Page 不要求 请求里带
order_info.checkout_token(子类已去掉该必填项)。
- One Page 不要求 请求里带
实现原因:商品维度单独成 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
主流程:
- 从
CheckoutCodOnePageRequest取product_id / variant_id / quantity。 CodOnePageOrderHandlerService::buildCartByProductId→CodCartDto(构造前将Context与购物车货币固定为店铺主货币,exchange_rate = 1;不可用则视为空购物车)。- 生成临时
checkout_token,$requestData->setCheckoutToken($checkoutToken)。 - 不写入 Redis
checkoutcart(已不再调用saveCheckoutCart)。 - 在本接口内触发与 旧 buynow 漏斗对齐 的两类事件:
AddToCart(使用ProductModel/ProductVariantModel+AddToCartDataStruct)BeginCheckout(BeginCheckoutDataStruct+ UTM)
- 调用
CodOrderHandlerService::saveOrder($cartData, $requestData, $checkoutToken),复用其后事务与 COD 订单事件。 - 返回单页成功页路径
/cod-one-page-checkouts/success/{checkout_token}。
订单货币:单页下单与物流预览均在服务内调用 applyStoreBaseCurrency(),订单 currency_code / currency_rate 及 current_*_price 按店铺主货币写入,与前台 cookie 所选展示货币无关(商品库价本身为主货币)。
6.1 有意不触发的事件
按约定:若现有 COD token 结账流程里没有触发的漏斗事件,单页也不新增。因此当前 不 在此处单独触发:
AddAddressInfoAddShippingInfoAddPaymentInfo
(标准在线结账里这些往往在分步链路里打点;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_offer | ✅ buildCartByProductId 后走 CodCartService 计价链 |
| promotion | ✅ 同上 |
| coupon | ✅ trans_info.coupon_code / 预券 Cache;⚠️ 替换型券风险同 COD token |
| order_diy_offer | ❌ |
顺序:与 COD token 相同;商品来自请求体临时 DTO,不读 checkoutcart Redis。
落库:单次 saveOrder → o_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 配置 Tag | extend/liquidExtend/tags/TagGetCodPageParams.php |
| homeapi 路由 | app/homeapi/route/route.php |
| One Page API 控制器 | app/homeapi/controller/OrderCodOnePage.php |
| One Page 下单 Request | app/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(按项目规范配置)。