结账与优惠 FAQ
常见概念、排障与「为什么这样设计」的速查。细节以专题文档与代码为准。
延伸阅读:cart-checkout-token-visit-id.md · checkout-flows.md · promotions-by-checkout-type.md · shopping-cart.md
目录
- Redis 与标识
- Q:
checkoutcartKey是什么?和checkout_token什么关系? - Q:「结账快照」到底快照了什么?
- Q:从加购到创单,checkoutcart 数据怎么流转?
- Q:
type=cart未创单前改购物车,结账页商品会变吗? - Q:创单后
cart_token一致才会 reconcile 进订单——什么意思? - Q:创单后在结账页改数量,会写进订单吗?
- Q:登录前后
cart_token会变吗?会导致订单不同步吗? - Q:
checkoutcart:{checkout_token}是干啥的?(速查) - Q:
guestcart/customercart和checkoutcart什么关系? - Q:
checkout_token、visit_id、global_visit_id有什么区别? - Q:buynow 为什么每次是新 checkout_token?
- Q:checkout_token 什么时候会 renew(换新)?
- Q:付完款 checkoutcart 还在吗?
- Q:
- 五种结账形态
- 优惠与计价
- 常见报错与现象
- 设计取舍(简短)
- 快速索引
Redis 与标识
Q:checkoutcartKey 是什么?和 checkout_token 什么关系?
A: 可以拆成三个层次理解,不要混为一谈:
| 层次 | 是什么 | 例子 |
|---|---|---|
| checkout_token | 这一次结账的 会话 ID(业务值) | a1b2c3d4e5f6...(32 位 md5 字符串) |
| checkoutcartKey | 上面这个 ID 在 Redis 里的 完整键名 | 34626:checkoutcart:a1b2c3d4e5f6... |
| checkoutcart 的值 | 该键里存的 JSON 对象(下文叫「结账快照」) | {"type":"cart","cart_token":"34626:guestcart:..."} |
代码对应:
- 键名:
CacheKeyHelper::checkoutcartKey($checkoutToken)→{storeId}:checkoutcart:{checkout_token} - 读写:
CartService::getCheckoutCartKey()/getData($checkoutCartKey)/saveData(...)
和 URL 的关系:用户打开的结账地址形如 /checkouts/{checkout_token}/...。
后端拿到 URL 里的 token → 拼出 checkoutcartKey → 读 Redis 里这条「结账会话说明」→ 再决定商品从哪读。
和 guestcart 的区别(最容易混):
guestcart / customercart → 购物车抽屉里「正在逛」的商品列表(数组,会随加购改数量变)
checkoutcartKey 指向的值 → 「这一次结账」的说明书(对象,告诉系统怎么找商品)
checkoutcartKey 的作用(为什么要有它):
- 绑定 URL 与商品来源:仅凭 URL 里的 token,系统不知道商品在 guestcart、buynow 内嵌数据,还是已在 MySQL 订单里——读 checkoutcart 的
type才知道。 - 固定结账入口:从购物车点「去结账」时,会把当时的
cart_token(guestcart/customercart 的完整 key)写进 checkoutcart;即使用户之后继续逛店改购物车,当前这条结账 URL 仍可按规则关联原 cart 或已创单的 order(见下条 FAQ 分 type 说明)。 - 标记创单进度:在线创单成功后
type改为order,getList改从o_orderhydrate,而不是再当纯购物车算。
Q:「结账快照」到底快照了什么?
A: 「快照」在文档里指 checkoutcart 这条 Redis 记录,但 不是每种 type 都复制了一份完整商品列表。按 type 理解才准确:
type = cart(从购物车结账)—— 指针型快照,不是商品副本
写入时机:用户点「去结账」→ CartService::getCheckoutUrl()(约 1152–1169 行)。
Redis 里实际只有 两行信息:
{
"type": "cart",
"cart_token": "34626:guestcart:8139f46e8c2a1b0d..."
}含义:
- 快照的是「绑定关系」:这次结账 token 对应哪一份 guestcart/customercart。
- 商品行仍在
guestcart/customercart里,checkoutcart 不存 product_id、quantity 等。 - 每次结账页
getList都会:先读 checkoutcart → 再getData(cart_token)拉行数据 → 内存里算价。
类比:checkoutcart 像 取号单上的「请到 3 号窗口取货」;货在 3 号窗口(guestcart),不在取号单上。
type = buynow / instant_checkout / paypal_ec —— 真·商品快照
写入时机:createBuynow / batchbuynow 等(约 1582–1586、1636–1640 行)。
{
"type": "buynow",
"cart_data": "[{\"product_id\":123,\"sku_code\":\"...\",\"quantity\":1,...}]"
}含义:
- 商品行直接嵌在 checkoutcart 里(
cart_data是 JSON 字符串)。 - 不经过 guestcart;用户继续逛店加购 不会 改这次 buynow 结账里的商品。
- TTL 较短(7 天),且 buynow 常强制新 token,未完成流程不易用旧 URL 重进。
类比:像 外卖预订单:菜名数量写在订单上,和堂食购物车无关。
type = order(已创在线单)—— 状态标记 + 可选 cart 指针
写入时机:创单成功 → OrderService::saveCheckoutCart() 把 type 改成 order(5379–5387 行)。
{
"type": "order",
"cart_token": "34626:customercart:9527"
}含义:
- 权威商品与金额在 MySQL(
o_order+o_order_product)。 - checkoutcart 告诉
getList:别当纯购物车算了,先加载订单(getOrderByToken,约 399–484 行)。 - 仍保留
cart_token用于:当前浏览器 cart 与创单时 cart 是否一致、是否 reconcile 行商品等(504 行附近)。
若 checkoutcart 键过期但订单还在:getCheckoutUrl / getList 会 重建 {type:order, cart_token}(1160–1164 行)。
一张表总结「快照了什么」
| type | 快照里有没有商品行 | 商品真正在哪 | 会不会被继续逛店改掉 |
|---|---|---|---|
cart | ❌ 只有 cart_token 指针 | guestcart / customercart | 会(同一 cart key 被改) |
buynow 等 | ✅ cart_data 内嵌 | checkoutcart 自身 | 不会 |
order | ❌ 以 MySQL 为准 | o_order_product | 订单 reconcile 逻辑决定 |
Q:从加购到创单,checkoutcart 数据怎么流转?
A: 下面用 「购物车结账」(最常见)走一遍;buynow 差异在括号里注明。
sequenceDiagram participant U as 用户浏览器 participant API as 后台 CartService participant R as Redis participant DB as MySQL o_order Note over U,R: 阶段 1:逛店加购(还没有 checkout_token URL) U->>API: POST 加购 API->>R: SET guestcart:visit_id = [行1, 行2] Note over U,R: 阶段 2:点「去结账」 U->>API: POST /cart → getCheckoutUrl() API->>R: SET checkoutcart:token = {type:cart, cart_token:guestcart:...} API-->>U: 302 /checkouts/{token}/... Note over U,R: 阶段 3:打开结账页(可能多次 GET/POST) U->>API: GET checkout?token=... API->>R: GET checkoutcart:token API->>R: GET guestcart:...(按 cart_token) API->>API: getList 算价、满减、券 API-->>U: 结账页 HTML/JSON Note over U,DB: 阶段 4:第一步提交创单(如 contact_information) U->>API: POST 地址等信息 API->>DB: INSERT o_order + order_product API->>R: checkoutcart.type = order API-->>U: 下一步 URL 仍带同一 token Note over U,DB: 阶段 5:后续步骤 / 支付 U->>API: GET/POST shipping、payment... API->>R: GET checkoutcart → type=order API->>DB: 读 o_order 补全地址/运费/支付状态 API->>API: getList reconcile 写回订单
分阶段说明:
| 阶段 | 用户动作 | checkoutcartKey 里 | 商品数据从哪读 | MySQL |
|---|---|---|---|---|
| 1 加购 | 商品页加购 | 还不存在 | guestcart | 无 |
| 2 去结账 | 购物车点 Checkout | type=cart + cart_token | 仍读 guestcart | 无 |
| 3 填表 | 刷新/换步结账页 | 不变 | guestcart + 内存计价 | 无 |
| 4 创单 | 提交联系信息/邮箱等 | type=order | 以 o_order 为主,guestcart 仅校验 | 有单 |
| 5 支付 | 选物流/支付 | type=order | o_order + reconcile | 更新 |
buynow 差异:阶段 2 写入的是 type=buynow + cart_data,不读 guestcart;其余创单后同样变 order。
COD 带 token:阶段 3–4 仍 读 checkoutcart 取商品,但创单写 o_cod_order,一般 不改 checkoutcart 为 order(无在线单 reconcile 链)。
COD 单页:不写 checkoutcart;商品来自请求体临时 DTO,token 只作订单字段与成功页 URL。
getList 分支逻辑(代码 374–493 行)—— 打开任意带 token 的结账页都会走:
有 checkout_token?
├─ 读 checkoutcartKey
├─ type=cart → getData(cart_token) 得行数组
├─ type=buynow… → json_decode(cart_data)
├─ type=order → getOrderByToken → 用 order.products + 订单金额字段
└─ checkoutcart 空但订单存在 → 当作 order 处理(379–381 行兜底)
无 checkout_token → 直接读当前用户 guestcart/customercart(购物车页)
Q:type=cart 未创单前改购物车,结账页商品会变吗?
A: 会变。 这是 type=cart 指针型设计的直接结果——checkoutcart 没有复制商品,每次 getList 都是 实时读 cart_token 指向的那份 guestcart/customercart:
case CartModel::CHECKOUT_CART_TYPE_CART:
$cart_list_data = $this->getData($checkout_cart['cart_token'], true);因此:
| 阶段 | 有没有 MySQL 订单 | 改 guestcart/customercart | 结账页下次请求看到的商品 |
|---|---|---|---|
type=cart | ❌ 还没有 | 加购 / 删行 / 改数量 | 跟着变(重新 getList 即生效) |
type=order | ✅ 已有 | 同上 | 默认以订单为准;仅当 checkoutcart 里 cart_token 与当前浏览器 cart key 一致时,才可能 reconcile 把 Redis 变更同步进订单(504–518 行) |
type=buynow | ❌ | 改 guestcart | 不变(读内嵌 cart_data) |
注意几点:
- 术语:未创单前没有「订单商品」,只有结账页展示的 cart;创单后才有
o_order_product。 - 刷新:已打开的结账页不会自动推送;下一次 GET/POST(刷新、换步、前端再拉 cart API)才会读到新数据。
- 同一访客:必须改的是 同一份 cart——同一 Cookie 的
visit_id(游客)或同一customer_id(会员)。换浏览器 / 换账号,cart_token不同,互不影响。 - 与 buynow 对比:buynow 把行写在
cart_data里,是 冻结 的;type=cart是 活引用,故意允许「去结账后继续逛店改购物车,回来结账看到最新内容」。
Q:创单后 cart_token 一致才会 reconcile 进订单——什么意思?
A: 创单后 checkoutcart 的 type 变为 order,商品 默认以 MySQL 订单为准(o_order_product)。
系统 不会 用「当前这次请求随便算出来的购物车」去覆盖订单,除非能证明:你现在继续结账的浏览器/账号,和创单时绑定的是同一份 Redis 购物车。
两个 cart key
| 名称 | 存在哪 | 含义 |
|---|---|---|
checkoutcart 里的 cart_token | Redis checkoutcart:{token} | 去结账 / 创单时 记下 的购物车 Redis 完整 key,如 34626:guestcart:aaa... |
| 当前浏览器的 cart key | 每次请求现场计算 | 本请求 Cookie 的 guestcart:{visit_id},或登录用户的 customercart:{customer_id} |
代码在做什么(CartService::getList 495–518、627–628 行)
已有订单(orderInfo 非空)
└─ 当前 cart key == checkoutcart.cart_token ?
├─ 否 → 跳过;订单商品保持 MySQL 原样(跨浏览器/换账号防护)
└─ 是 → 读 Redis 购物车,compareCartData 与订单行比对
├─ 有差异 → 用 Redis 行重算,必要时 saveProducts 写回 o_order_product
└─ 无差异 → 不动行,仅可能更新券/税/运费等金额字段
reconcile = 把 Redis 购物车 与 MySQL 订单商品/金额 重新对齐并落库(OrderService::saveProducts)。
场景 A:同一浏览器,创单后又改了购物车 ✅ 可能同步
- Chrome 加购 A、B → 去结账 → checkoutcart 记下
cart_token = guestcart:Chrome的visit_id - 填地址 创单 → 订单里是 A、B
- 回店铺页给购物车 加了 C(或改了数量)
- 刷新结账页 / 提交下一步(会再调
getList) - 当前 cart key 仍等于 checkoutcart 里的
cart_token→compareCartData发现差异 → 可能把 C / 新数量写进订单
场景 B:换浏览器打开同一结账链接 ❌ 不会用新浏览器购物车改订单
- Chrome 创单,checkoutcart 里
cart_token = guestcart:aaa - 把 URL 发到 Firefox → Firefox 的 cart key 是
guestcart:bbb aaa != bbb→ 不同步;结账页仍显示 原订单商品
代码注释也写明意图:跨浏览器访问结算页面不刷新(_checkCheckoutCart 1189 行)。
其它注意
| 情况 | 行为 |
|---|---|
| Redis 购物车被 清空 | compareCartData 对空数组返回 false(770–771 行),不会因此把订单商品清成空 |
| 仅 cart_token 一致 | 还不够;还要行/数量/属性有变,或券/税/运费等触发 refreshProductData,才会 saveProducts |
| COD / buynow | COD 无在线 reconcile 链;buynow 读 cart_data,改 guestcart 不影响 该次结账 |
Q:创单后在结账页改数量,会写进订单吗?
A: 有可能,但不是改数量 API 直接写订单——走的是 改 Redis 购物车 → 下次 getList reconcile → saveProducts 写 MySQL 这条链。
实际路径
- 改数量的入口通常是购物车 API(如 homeapi
Cart::change→CartService::update),写入 guestcart / customercart,不直接改o_order_product。 - 结账各步(标准 Liquid GET/POST、渐进式 homeapi 各接口)在业务处理前都会调
getList(checkout_token)。 - 若已创单且满足 上一节 的 cart_token 一致:
compareCartData发现 quantity(或 SKU、属性)变化 →refreshProductData = true- 重算满减/券/税后 →
saveProducts写回订单行与金额
按操作方式
| 用户操作 | Redis 会变吗 | 订单会变吗 |
|---|---|---|
| 在 购物车页 改数量,再 刷新/继续 结账页 | ✅ | ✅(cart_token 一致且 compare 有差异时) |
| 主题在 结账侧栏 提供改数量且调用 cart update API,再触发 getList | ✅ | ✅ 同上 |
| 只在已打开的结账页改数量,没有新请求 | ✅(若调了 API) | ❌ 页面不刷新则看不到;下一次 getList 才进订单 |
| 换浏览器 打开同一结账 URL 后改自己的购物车 | ✅(改的是别人的 cart key) | ❌ cart_token 不一致,不动原订单 |
| buynow 已创单 | 改 guestcart | ❌ 商品读 cart_data,与 guestcart 无关 |
和「未创单」对比
| 阶段 | 改数量后谁变 |
|---|---|
type=cart 未创单 | 结账页 直接跟 Redis 走,无订单可写 |
type=order 已创单 | 先满足 cart_token 一致,再通过 reconcile 间接写订单 |
排障:用户说「结账页改了数量但订单没变」→ 查是否 刷新了页面/提交了下一步、是否 同一浏览器/账号、是否 buynow。
Q:登录前后 cart_token 会变吗?会导致订单不同步吗?
A: 会变——游客用 guestcart:{visit_id},登录后用 customercart:{customer_id},key 本来就不是同一个。
但系统在 登录后的每个已登录请求 会跑 _checkGuestCart,在合并购物车的同事 更新 checkoutcart 里的 cart_token,正常情况下 刻意避免 登录导致 reconcile 断链。
登录时发生了什么(HomeBaseController::customerHandle → _checkGuestCart)
| 步骤 | 动作 |
|---|---|
| 1 | 把 guestcart 商品 merge 进 customercart |
| 2 | 若当前存在 checkoutcart(type=cart 或 type=order)→ 把 cart_token 改成 customercart key(1086–1095 行) |
| 3 | type=order 时顺带把订单 customer_id 绑到刚登录会员(1096–1099 行) |
| 4 | 删除 guestcart(1102 行) |
因此:在结账流程中登录 → checkoutcart 从指向 guestcart:xxx 变为 customercart:{id} → 与登录后「当前 cart key」对齐,后续 reconcile 仍可用(比的是合并后的 customercart)。
sequenceDiagram participant U as 用户 participant API as 后台 participant R as Redis Note over U,R: 游客去结账 U->>API: getCheckoutUrl API->>R: checkoutcart.cart_token = guestcart:aaa Note over U,R: 结账途中登录 U->>API: 任意已登录请求 API->>R: merge guestcart → customercart:9527 API->>R: checkoutcart.cart_token = customercart:9527 API->>R: DEL guestcart:aaa Note over U,R: 继续结账 getList API->>R: 当前 key customercart:9527 == checkoutcart.cart_token ✅ API->>API: reconcile 可用
什么时候会「不同步」?
| 场景 | cart_token 是否一致 | 订单能否被 Redis 变更影响 |
|---|---|---|
结账途中 正常登录(走 _checkGuestCart) | ✅ 会更新为 customercart | ✅ 可以 |
| 换浏览器 打开结账链接 | ❌ guestcart 不同 | ❌ 不会 |
| 换账号登录(A 创单,B 登录同一浏览器) | ❌ customercart 不同 | ❌ 不会;且 getCheckoutUrl 对已付/已取消单可能 renew 新 token(1141–1147 行) |
| 游客创单后 长期未登录,checkoutcart 仍指向已删的 guestcart | ❌ 登录前若 guestcart 已清 | ❌ 直到登录 merge 并更新 cart_token |
| buynow | 不依赖 guest/customer cart_token 读商品 | 登录 merge 不影响 buynow 的 cart_data |
和「未创单 type=cart」的关系
- 登录前未创单:checkoutcart 指向 guestcart;登录后
_checkGuestCart改为 customercart,下次getList读 会员购物车(含 merge 结果)。 - 登录后才点去结账:
getCheckoutUrl直接写cart_token = customercart:{id}。
排障:「登录后订单商品和购物车不一致」→ 先确认 checkoutcart 里 cart_token 是否已变为 customercart、是否在同一账号下刷新过结账页。
Q:checkoutcart:{checkout_token} 是干啥的?(速查)
A: 即 checkoutcartKey 存的那条 结账会话说明(见上两节详解)。Key = {storeId}:checkoutcart:{checkout_token},值 = JSON 对象。
作用速记:
- 把 URL 里的 token 绑到商品来源(guestcart / 内嵌 cart_data / MySQL 订单)。
- 用
type区分购物车结账、立即购买、已创单。 - 在线创单后
type=order,getList从o_order补全并对账。
| type | 含义 | 商品从哪来 |
|---|---|---|
cart | 从购物车来 | cart_token → guestcart / customercart |
buynow / instant_checkout / paypal_ec | 立即购买等 | 内嵌 cart_data |
order | 已创在线单 | o_order + o_order_product |
它不是订单表;创单后真实订单在 MySQL。
写入:getCheckoutUrl、createBuynow 等;创单成功:OrderService::saveCheckoutCart 改 type。
→ 字段级说明见 cart-checkout-token-visit-id.md §4.3 · 购物车全链路 shopping-cart.md
Q:guestcart / customercart 和 checkoutcart 什么关系?
A: 三把 Redis key,职责不同(详见 § 从加购到创单的数据流转):
| Key | 存什么 | 谁在用 |
|---|---|---|
guestcart:{visit_id} | 游客购物车 行数组 | 加购、改数量 |
customercart:{customer_id} | 会员购物车 行数组 | 同上 |
checkoutcart:{checkout_token} | 结账会话说明(type + cart_token 或 cart_data) | 结账 URL、getList |
关系:type=cart 时,checkoutcart 里的 cart_token 是完整 Redis key 字符串,例如 34626:guestcart:8139f46e...。商品行在 guestcart,不在 checkoutcart 里。
Q:checkout_token、visit_id、global_visit_id 有什么区别?
A: 三个不同概念,不要混用:
| 名称 | Cookie | 业务常用值 | 典型用途 |
|---|---|---|---|
| global_visit_id | 原始 TraceId | Liquid/像素读 原始值 | 长期访客 |
| visit_id | 同上 | md5(原始) | guestcart、订单 visitor_id |
| checkout_token | 另一套 Cookie _cvid | md5(原始) | 结账 URL、订单 checkout_token、checkoutcart |
visit_id 的 md5 ≠ checkout_token 的 md5。
→ 详见 cart-checkout-token-visit-id.md §1
Q:buynow 为什么每次是新 checkout_token?
A: createBuynow 调用 getCheckoutVisitId(true):强制新 TraceId且不写 Cookie,避免用户用旧 token 重复进入未完成的 buynow 漏斗。checkoutcart 里直接内嵌 cart_data,TTL 较短(7 天)。
Q:checkout_token 什么时候会 renew(换新)?
A: 当 Cookie 里 checkout id 的 md5 仍等于当前订单 token,且:
- 订单已 paid / pending,或 cancel;或
checkoutcart.cart_token与当前用户 cart key 不一致(换浏览器/账号);
会 removeCheckoutVisitId() 再生成新 token。MySQL 里 旧订单 checkout_token 不变。
Q:付完款 checkoutcart 还在吗?
A: 键通常 不会主动 delete,靠 TTL 过期(cart 型约 90 天,buynow 约 7 天)。支付成功会 清 guest/customer cart(clearCartByCheckoutToken),并可能删 checkout Cookie,但 checkoutcart 键本身多半仍在直到过期。
五种结账形态
Q:标准 / 单页 / 渐进式 / COD token / COD 单页 怎么选文档?
A:
| 形态 | 文档 |
|---|---|
| 标准多步 Liquid | standard-checkout-flow.md |
| 单页 one_page(presave + complete) | one-page-checkout-flow.md |
| 渐进式 single_page(email 创单 + 分步 API) | single-page-checkout-flow.md |
| COD 带 token | cod-checkout-flow.md |
| COD 单页无 token | cod-one-page-checkout.md |
Q:各形态 什么时候创单(写 MySQL)?
A:
| 形态 | 创单时机 | 订单表 |
|---|---|---|
| 标准 | 第一步 contact_information POST | o_order |
| 单页 | presave 首次成功(占位单);complete _finalize | o_order |
| 渐进式 | email POST | o_order |
| COD token | saveOrder 一次 POST | o_cod_order |
| COD 单页 | order 一次 POST | o_cod_order |
COD 从不写 o_order(除非店铺另有在线单流程)。
Q:checkout_process 和 checkout_type 有什么区别?
A:
checkout_process(店铺配置):未创单前决定跳哪种结账 URL(standard / one_page / single_page / smart)。checkout_type(订单字段):创单后锁定该单属于哪种形态;之后 URL 按generateCheckoutUrlByOrder生成,不再跟店铺默认走。
例外:部分场景 payment_gateway 会强制 standard URL。
→ 详见 checkout-flows-detail.md §6
Q:cod_checkout_type 是干啥的?
A: 控制购物车入口跳 在线结账 还是 /cod-checkouts/{token}:
| 值 | 行为 |
|---|---|
| 仅在线 | 永远在线 URL |
| 仅 COD | 永远 COD URL |
| 在线首选 / COD 首选 | 默认一种,页面可通过 checkout_url_array 切换(未创单前) |
快捷支付强制在线。已有 COD 单则 URL 固定 COD。
优惠与计价
Q:cart.diy_offers[] 和 cart.diy_offer_price 是一回事吗?
A: 不是。
diy_offers[]:来自o_diy_offer(加购活动、bundlesale、gift 等),在 cart 行或 cart 级计算。diy_offer_price:来自o_order_diy_offer(积分、Seel、deliveryprotec 等 订单级 插件),在选物流/complete 等步骤写入。
COD token 链路 不支持 o_order_diy_offer(无积分抵扣等)。
→ 详见 promotion-discount-checkout.md
Q:diy_offer 的 promotion 和标准满减 o_promotion 有什么区别?
A: 名字相似,表、算法、落库字段都不同:
diy_offer type=promotion | o_promotion | |
|---|---|---|
| 是什么 | App 插件限时/单品促销 | 店铺 满减活动(7 种 rule) |
| 怎么生效 | 加购绑 diy_offer_id,改 行价 | 扫 cart 范围,出 discount |
| 订单字段 | diy_offer_id、data_from | promotion_id(多在行上) |
| 汇总 | 多在 subtotal | current_promotion_price |
二者 可同时存在(另有替换型券会清满减的特殊分支)。
→ 详表 promotion-discount-checkout.md §4.1.1
Q:diy_offer 怎么传参?可以一次用多个吗?
A: 在 加购 时绑在 购物车行 上,主 API 为 POST /cart/diyoffers:
{
"action": "addtocart",
"products": [
{ "product_id": 123, "sku_code": "...", "quantity": 1, "diy_offer_id": 456, "data_from": "app_bundlesale" },
{ "product_id": 789, "sku_code": "...", "quantity": 2, "diy_offer_id": 999, "data_from": "app_limitedtimeoffer", "second": 3600 }
]
}- 多个活动:
products[]里 每行一个diy_offer_id,或 cart 里多行各绑不同活动;计价时cart.diy_offers[]可 多条。 - 不是 结账步再传一个总 JSON(那是
offer_from_name/ order_diy_offer)。 - minmaxoffer 店铺级自动,一般 不传 id;激活后 其它 diy_offer 不算。
- 普通
POST /cart/add不带diy_offer_id。
→ diy-offer-types-reference.md §5
Q:只有 POST /cart/diyoffers 才能写 diy_offer_id 吗?
A: 前台显式传 id 的主路径是它,但不是唯一会动 diy_offer_id 的方式。
| 路径 | 说明 |
|---|---|
POST /cart/diyoffers | ✅ 主路径;products[].diy_offer_id 写 Redis 或 buynow cart_data |
POST /cart/buynow、/cart/add、batched | ❌ 无 diy_offer_id 参数,默认为 0 |
getList 计价 | minmax 自动 formatCartItemInfo 写 id;gift 重建行;bundlesale 不满足时 清 id;可能 resetCartListData 回写 Redis |
| 创单 | saveProducts 把 cart 行 id 拷到 o_order_product |
→ diy-offer-types-reference.md §5.2.1
Q:同一行能绑多个 diy_offer_id 吗?
A: 不能(数据结构上不支持)。 Redis 行只有 一个 标量 diy_offer_id;cartDiyOffers 按它分组,合并加购 不更新 已有 id。
同一 商品 若要参与多个 diy 活动:靠 不同 property 拆成两行、gift 加赠品行、或与 o_promotion 满减叠加;minmax 激活则 独占 其它 diy_offer。
→ diy-offer-types-reference.md §5.5–5.6
Q:offer_from_name 可以一次传多个吗?怎么传?
A: 可以。 传 JSON 对象字符串,key 为 from_name,value 为各 Handler 参数:
offer_from_name={"customer_points":1,"app_seel":1}
- 积分:
customer_points=1使用 /0退还 - Seel / 运输保障:传
1;且后端 总会 尝试处理这两种(即使 POST 未带) - 单页:放在
trans_info.offer_from_name,仍是字符串
多种 offer 各写一行 o_order_diy_offer,合计进 current_offer_price。
→ order-diy-offer-handlers.md §5.1
Q:优惠券「预存」存在哪?
A: 购物车阶段 POST /coupons 或 COD use-coupon 会把码写入 Redis/Cache(pre_use_coupon 相关 key,按顾客 identity)。创单或 savePreCoupon 时才正式绑到订单;下单成功后 delPreCouponCode。
Q:为什么结账页价格和下单瞬间不一致?
A: 常见原因:
- 有在线单后
getList会 reconcile 写回o_order/o_order_product(券、税、满减变化)。 - 单页:price API 是内存算,complete 才全量落盘——中间字段(shipping_id、offer_from_name)未传齐。
- ES/MySQL 延迟:列表/报表查 ES 可能落后主库(见下条)。
- COD:展示靠内存 DTO,最终以 saveOrder 写入
o_cod_order为准。
Q:刚写的订单/购物车为什么查不到?
A: 优先排查两类延迟(持久架构前提):
- MySQL 主从:读从库可能落后写主库。
- ES 同步:
MySQL → Canal → MQ → ES,列表/搜索有最终一致延迟。
代码路径以 MySQL 主库 + Redis 为准排障。
常见报错与现象
Q:结账页提示 order not found / 跳回购物车
A: 排查顺序:
checkoutcart:{token}是否过期(TTL)。- 对应
guestcart/cart_data是否被clearCart删掉。 - 在线:
o_order是否存在该checkout_token;getList在 checkoutcart 空时会尝试按 order 加载。 - token 与 Cookie checkout id 是否不一致(renew 后 URL 仍是旧 token)。
Q:Cart is empty(COD)
A:
checkoutcart不存在或 type 与数据不匹配。- 商品下架、变体删除、
productDetailByIds查不到。 - buynow
cart_dataJSON 损坏或过期。
COD 单页无 token:查 product_id / variant_id 是否属当前店铺。
Q:加购了但另一浏览器看不到购物车
A: 正常。guestcart 绑 visit_id(Cookie global_visit_id),不同浏览器 Cookie 不同,不共享。
Q:积分没扣 / Seel 没生效
A:
| 形态 | 检查 |
|---|---|
| 标准 | shipping_method POST 是否传 offer_from_name |
| 单页 | complete 的 trans_info.offer_from_name |
| 渐进式 | shipping-method POST 的 offer_from_name |
| COD | 不支持 order_diy_offer |
Q:用了替换型优惠券还叠了满减
A: 在线应走 checkCouponUseWithPromotionStatus 清满减;COD 用券路径 可能未走 同一逻辑,存在双计风险(已知排障点)。
Q:跳错结账页(例如 one_page 单却打开 standard)
A:
- 未创单:看
checkout_process、smart Cookie。 - 已创单:看
o_order.checkout_type。 - COD:看
cod_checkout_type与是否已有o_cod_order。
设计取舍(简短)
Q:为什么 COD 单页不写 Redis checkoutcart?
A: 单页 COD 商品来自请求体临时构造 CodCartDto,token 仅用于订单字段与成功页 URL;不必维护 checkoutcart 快照。带 token 的 COD 仍 读 checkoutcart 取商品。
Q:为什么在线创单后要改 checkoutcart type=order?
A: 让 getList(token) 知道「已有订单」,从 MySQL hydrate 价格、地址、支付状态,并做 reconcile;URL 仍用同一 checkout_token。
Q:COD 和在线能共用同一个 checkout_token 吗?
A: token 是结账 会话 id;在线写 o_order、COD 写 o_cod_order,互斥——同一 token 一般只完成一种下单;已有 COD 单则 COD 页直接 success,不会再去创在线单。
快速索引
| 我想查… | 去看 |
|---|---|
| checkoutcartKey / 结账快照详解 | 本文 § checkoutcartKey · § 快照含义 · § 数据流转 |
| 创单后 reconcile / 改数量 / 登录 | § reconcile · § 改数量 · § 登录 cart_token |
| checkoutcart / guestcart 结构 | cart-checkout-token-visit-id.md |
| 四种形态对比 | checkout-flows.md |
| 优惠计算顺序 | promotion-discount-checkout.md |
| 落库时机表 | checkout-flows-detail.md §5 |
| COD 物流 param | cod-shipping-zone-plan.md |
有新的高频问题可补在本文件末尾,并酌情在专题长文里加交叉链接。