结账与优惠 FAQ

常见概念、排障与「为什么这样设计」的速查。细节以专题文档与代码为准。

延伸阅读cart-checkout-token-visit-id.md · checkout-flows.md · promotions-by-checkout-type.md · shopping-cart.md

目录


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 的作用(为什么要有它):

  1. 绑定 URL 与商品来源:仅凭 URL 里的 token,系统不知道商品在 guestcart、buynow 内嵌数据,还是已在 MySQL 订单里——读 checkoutcart 的 type 才知道。
  2. 固定结账入口:从购物车点「去结账」时,会把当时的 cart_token(guestcart/customercart 的完整 key)写进 checkoutcart;即使用户之后继续逛店改购物车,当前这条结账 URL 仍可按规则关联原 cart 或已创单的 order(见下条 FAQ 分 type 说明)。
  3. 标记创单进度:在线创单成功后 type 改为 ordergetList 改从 o_order hydrate,而不是再当纯购物车算。

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"
}

含义:

  • 权威商品与金额在 MySQLo_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 被改)
buynowcart_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 去结账购物车点 Checkouttype=cart + cart_token仍读 guestcart
3 填表刷新/换步结账页不变guestcart + 内存计价
4 创单提交联系信息/邮箱等type=order以 o_order 为主,guestcart 仅校验有单
5 支付选物流/支付type=ordero_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

注意几点

  1. 术语:未创单前没有「订单商品」,只有结账页展示的 cart;创单后才有 o_order_product
  2. 刷新:已打开的结账页不会自动推送;下一次 GET/POST(刷新、换步、前端再拉 cart API)才会读到新数据。
  3. 同一访客:必须改的是 同一份 cart——同一 Cookie 的 visit_id(游客)或同一 customer_id(会员)。换浏览器 / 换账号,cart_token 不同,互不影响。
  4. 与 buynow 对比:buynow 把行写在 cart_data 里,是 冻结 的;type=cart活引用,故意允许「去结账后继续逛店改购物车,回来结账看到最新内容」。

Q:创单后 cart_token 一致才会 reconcile 进订单——什么意思?

A: 创单后 checkoutcart 的 type 变为 order,商品 默认以 MySQL 订单为准o_order_product)。
系统 不会 用「当前这次请求随便算出来的购物车」去覆盖订单,除非能证明:你现在继续结账的浏览器/账号,和创单时绑定的是同一份 Redis 购物车

两个 cart key

名称存在哪含义
checkoutcart 里的 cart_tokenRedis 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:同一浏览器,创单后又改了购物车 ✅ 可能同步

  1. Chrome 加购 A、B → 去结账 → checkoutcart 记下 cart_token = guestcart:Chrome的visit_id
  2. 填地址 创单 → 订单里是 A、B
  3. 回店铺页给购物车 加了 C(或改了数量)
  4. 刷新结账页 / 提交下一步(会再调 getList
  5. 当前 cart key 仍等于 checkoutcart 里的 cart_tokencompareCartData 发现差异 → 可能把 C / 新数量写进订单

场景 B:换浏览器打开同一结账链接 ❌ 不会用新浏览器购物车改订单

  1. Chrome 创单,checkoutcart 里 cart_token = guestcart:aaa
  2. 把 URL 发到 Firefox → Firefox 的 cart key 是 guestcart:bbb
  3. aaa != bbb不同步;结账页仍显示 原订单商品

代码注释也写明意图:跨浏览器访问结算页面不刷新_checkCheckoutCart 1189 行)。

其它注意

情况行为
Redis 购物车被 清空compareCartData 对空数组返回 false(770–771 行),不会因此把订单商品清成空
仅 cart_token 一致还不够;还要行/数量/属性有变,或券/税/运费等触发 refreshProductData,才会 saveProducts
COD / buynowCOD 无在线 reconcile 链;buynow 读 cart_data,改 guestcart 不影响 该次结账

Q:创单后在结账页改数量,会写进订单吗?

A: 有可能,但不是改数量 API 直接写订单——走的是 改 Redis 购物车 → 下次 getList reconcile → saveProducts 写 MySQL 这条链。

实际路径

  1. 改数量的入口通常是购物车 API(如 homeapi Cart::changeCartService::update),写入 guestcart / customercart不直接o_order_product
  2. 结账各步(标准 Liquid GET/POST、渐进式 homeapi 各接口)在业务处理前都会调 getList(checkout_token)
  3. 若已创单且满足 上一节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

步骤动作
1guestcart 商品 merge 进 customercart
2若当前存在 checkoutcart(type=carttype=order)→ 把 cart_token 改成 customercart key(1086–1095 行)
3type=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 对象

作用速记:

  1. 把 URL 里的 token 绑到商品来源(guestcart / 内嵌 cart_data / MySQL 订单)。
  2. type 区分购物车结账、立即购买、已创单。
  3. 在线创单后 type=ordergetListo_order 补全并对账。
type含义商品从哪来
cart从购物车来cart_token → guestcart / customercart
buynow / instant_checkout / paypal_ec立即购买等内嵌 cart_data
order已创在线单o_order + o_order_product

它不是订单表;创单后真实订单在 MySQL。
写入:getCheckoutUrlcreateBuynow 等;创单成功:OrderService::saveCheckoutCart 改 type。

→ 字段级说明见 cart-checkout-token-visit-id.md §4.3 · 购物车全链路 shopping-cart.md


Q:guestcart / customercartcheckoutcart 什么关系?

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_tokenvisit_idglobal_visit_id 有什么区别?

A: 三个不同概念,不要混用

名称Cookie业务常用值典型用途
global_visit_id原始 TraceIdLiquid/像素读 原始值长期访客
visit_id同上md5(原始)guestcart、订单 visitor_id
checkout_token另一套 Cookie _cvidmd5(原始)结账 URL、订单 checkout_tokencheckoutcart

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 cartclearCartByCheckoutToken),并可能删 checkout Cookie,但 checkoutcart 键本身多半仍在直到过期。


五种结账形态

Q:标准 / 单页 / 渐进式 / COD token / COD 单页 怎么选文档?

A:

形态文档
标准多步 Liquidstandard-checkout-flow.md
单页 one_page(presave + complete)one-page-checkout-flow.md
渐进式 single_page(email 创单 + 分步 API)single-page-checkout-flow.md
COD 带 tokencod-checkout-flow.md
COD 单页无 tokencod-one-page-checkout.md

Q:各形态 什么时候创单(写 MySQL)?

A:

形态创单时机订单表
标准第一步 contact_information POSTo_order
单页presave 首次成功(占位单);complete _finalizeo_order
渐进式email POSTo_order
COD tokensaveOrder 一次 POSTo_cod_order
COD 单页order 一次 POSTo_cod_order

COD 从不o_order(除非店铺另有在线单流程)。


Q:checkout_processcheckout_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=promotiono_promotion
是什么App 插件限时/单品促销店铺 满减活动(7 种 rule)
怎么生效加购绑 diy_offer_id,改 行价扫 cart 范围,出 discount
订单字段diy_offer_iddata_frompromotion_id(多在行上)
汇总多在 subtotalcurrent_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_idcartDiyOffers 按它分组,合并加购 不更新 已有 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/Cachepre_use_coupon 相关 key,按顾客 identity)。创单或 savePreCoupon 时才正式绑到订单;下单成功后 delPreCouponCode


Q:为什么结账页价格和下单瞬间不一致?

A: 常见原因:

  1. 有在线单后 getList 会 reconcile 写回 o_order / o_order_product(券、税、满减变化)。
  2. 单页:price API 是内存算,complete 才全量落盘——中间字段(shipping_id、offer_from_name)未传齐。
  3. ES/MySQL 延迟:列表/报表查 ES 可能落后主库(见下条)。
  4. COD:展示靠内存 DTO,最终以 saveOrder 写入 o_cod_order 为准。

Q:刚写的订单/购物车为什么查不到?

A: 优先排查两类延迟(持久架构前提):

  1. MySQL 主从:读从库可能落后写主库。
  2. ES 同步MySQL → Canal → MQ → ES,列表/搜索有最终一致延迟。

代码路径以 MySQL 主库 + Redis 为准排障。


常见报错与现象

Q:结账页提示 order not found / 跳回购物车

A: 排查顺序:

  1. checkoutcart:{token} 是否过期(TTL)。
  2. 对应 guestcart / cart_data 是否被 clearCart 删掉。
  3. 在线:o_order 是否存在该 checkout_tokengetList 在 checkoutcart 空时会尝试按 order 加载。
  4. token 与 Cookie checkout id 是否不一致(renew 后 URL 仍是旧 token)。

Q:Cart is empty(COD)

A:

  1. checkoutcart 不存在或 type 与数据不匹配。
  2. 商品下架、变体删除、productDetailByIds 查不到。
  3. buynow cart_data JSON 损坏或过期。

COD 单页无 token:查 product_id / variant_id 是否属当前店铺。


Q:加购了但另一浏览器看不到购物车

A: 正常。guestcartvisit_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:

  1. 未创单:看 checkout_process、smart Cookie。
  2. 已创单:看 o_order.checkout_type
  3. 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 物流 paramcod-shipping-zone-plan.md

有新的高频问题可补在本文件末尾,并酌情在专题长文里加交叉链接。

此文件夹下有1条笔记。