checkout_token / 购物车 Redis / global_visit_id

本文说明前台 访问标识global_visit_id / visit_id / checkout_token)的生成与 Cookie 关系,Redis 购物车数据结构checkout 快照 的 type 流转,以及 何时清理。与结账形态的关系见 checkout-flows.md。概念速查见 faq.md

与实现冲突时以代码为准。

目录


1. 三个 ID 的关系(易混)

系统里同时存在三层标识,用途不同:

名称代码入口Cookie 存什么业务里常用值用途
global_visit_idFuncHelper::oemsaasGlobalVisitId()原始 UUID/TraceId(未 md5)模板/Liquid 展示、像素长期访客标识
visit_idRequest::getVisitId()Context->visit_id同上,读入后 md5Redis guestcart:{visit_id}游客购物车、UTM、订单 visitor_id
checkout_tokenRequest::getCheckoutVisitId()Context->checkout_visit_id原始 TraceId(未 md5)md5 后的字符串结账 URL、订单 checkout_token、Redis checkoutcart
flowchart LR
    subgraph cookies [Cookie 层 - 存原始值]
        G["global_visit_id<br/>shop_global_visit_id / _vid"]
        C["checkout_visit_id<br/>_cvid / shop_checkout_visit_id"]
    end

    subgraph md5 [Request 层 - 返回 md5]
        V["visit_id = md5(global raw)"]
        T["checkout_token = md5(checkout raw)"]
    end

    subgraph redis [Redis]
        GC["guestcart:visit_id"]
        CC["customercart:customer_id"]
        CH["checkoutcart:checkout_token"]
    end

    G --> V --> GC
    C --> T --> CH
    CC -.->|cart_token 指针| CH

要点

  • visit_idcheckout_token 不是同一个 md5(来自不同 Cookie)。
  • 订单表 o_order.checkout_token = checkout 侧 md5,创单后一般不变(除非 renew,见 §3.3)。
  • 登录后用 customercart:{customer_id};游客用 guestcart:{visit_id}

2. global_visit_id 生成逻辑

2.1 初始化时机

每个请求在 DomainAnalysis 中间件(common/middlewares/DomainAnalysis.php 106–108 行):

app("Context")->visit_id          = $request->getVisitId();
app("Context")->checkout_visit_id = $request->getCheckoutVisitId();
app('Context')->session_visit_id  = $request->getSessionVisitId();

Visitor::initialize()app/home/Visitor.php)同样写入 app("Visitor")

2.2 getVisitId() 流程

common/Request.php 75–97 行:

顺序步骤
1读 Cookie:CookieService::getGlobalVisitId()
2若无 → getVisitIdFromSocial():按 UTM 广告参数(fbclid、gclid 等)+ IP + 平台生成 UUID 形字符串,写入 Cookie
3仍无 → generateTraceId()(Snowflake + Redis 序列),CookieService::setGlobalVisitId()
4返回 md5($visit_id_raw)

CookieService::getGlobalVisitId() / setGlobalVisitId()

优先级Key
1_{encryptedStoreId}_vidgetStoreUniqueCookieKey('_vid')
2shop_global_visit_idCookieConst::COOKIE_OEMSAAS_GLOBAL_VISIT_ID
3历史 oemsaas_global_visit_id

写入:Cookie::forever(永久,除非店铺 195341/197609 特殊跳过写入)。

2.4 与 checkout_token 的区别

  • global_visit_id:全站访客,加购、浏览、guestcart 绑定。
  • checkout_token:一次结账会话;buynow 可强制新建;与订单 1:1(创单后)。

另有 session_visit_idgetSessionVisitId):Cookie _vs,TTL 24h,用于会话级统计,与购物车 key 无直接绑定。


3. checkout_token 生成逻辑

3.1 getCheckoutVisitId(is_new, renew)

common/Request.php 160–174 行:

参数行为
$is_new = true总是 generateTraceId()不写 Cookie(用于 buynow 全新 token)
$is_new = false读 Cookie → 若 $renew 则清空 → 空则生成并 setCheckoutVisitId
返回值md5($raw_trace_id) = 对外的 checkout_token

Cookie 键:_cvid / shop_checkout_visit_id / oemsaas_checkout_visit_idCookie::forever

3.2 各场景如何得到 token

场景代码位置生成方式
立即购买 buynowCartService::createBuynow 243 行getCheckoutVisitId(true)新 token,写 checkoutcart type=buynow
购物车去结账CartService::getCheckoutUrl 1126 行用当前 Context->checkout_visit_id(Cookie 复用或新建)
订单已付/取消/待付且 cookie 匹配getList 404–407、getCheckoutUrl 1145–1146getCheckoutVisitId(false, true) renew → 新 token
渐进式 saveEmail 创单请求里 URL 的 token路由 {checkout_token},创单写入 o_order.checkout_token
单页 presave同上URL token,preSave 创/更新单

buynow 注释(extend/shoppingProcess/cartLogic.php):流程未走完时 buynow 快照 TTL 较短;不应用同一 token 重复进入未完成 buynow(新 buynow 总是新 token)。

3.3 何时 renew checkout_token

Cookie 中的 checkout id 的 md5 仍等于当前订单 checkout_token 时,若订单已进入终态,则 renew:

  • financial_status = paid / pending
  • status = cancel
  • checkoutcart.cart_token 与当前用户 cart key 不一致(换浏览器/换账号,getCheckoutUrl 1142–1143 行)

renew 后:removeCheckoutVisitId() + getCheckoutVisitId(false, true) 得到 新 checkout_token

3.4 checkout_token 何时「删除」

Redis checkoutcart:{token} 不会主动 delete 键名,而是:

  • TTL 过期(见 §4.3)
  • 支付成功clearCartByCheckoutToken 删除 cart_token 指向的 guest/customer cart,不是删 checkoutcart 键本身(OrderService 1453、saveProducts 空 cart 2028 行)
  • 购物车为空 saveProducts:同上 + removeCheckoutVisitId()

Cookie checkout_visit_id 删除:Request::removeCheckoutVisitId() / CookieService::removeCheckoutVisitId(),触发场景包括:

  • CartService::clearCart
  • 订单支付/取消/回调后 OrderService::clearCart
  • 订单取消 cancelOrder
  • 空 cart 抛 RedirectException

订单行 checkout_token:创单后 持久保留 在 MySQL,不随 Cookie 删除而失效;URL 仍可用 _checkCheckoutCart 校验。


4. Redis 购物车数据结构

4.1 Key 一览

前缀均为 {storeId}:CacheKeyHelper::store())。

Key方法值类型说明
{store}:guestcart:{visit_id}guestcartKeyJSON 数组游客购物车行列表
{store}:customercart:{customer_id}customercartKeyJSON 数组会员购物车行列表
{store}:checkoutcart:{checkout_token}checkoutcartKeyJSON 对象结账快照(type + 指针或内嵌 cart)

visit_id = md5(global_visit_raw)checkout_token = md5(checkout_raw)

4.2 购物车行结构(cart_list 数组元素)

CartService::cartStructBuild()(1452–1481 行)生成,存 Redis 时为 主货币计价前的商品快照(价格在 cartDataComposition / getList 阶段再算):

{
  "product_id": 123,
  "sku_code": "xxx-0-0-0-0-0-0",
  "quantity": 2,
  "create_time": 1710000000,
  "last_time": 1710000000,
  "store_id": 1,
  "data_from": "app_bundlesale",
  "spm": "",
  "property": [],
  "diy_offer_id": 456,
  "diy_offer_name": "活动名",
  "unmodifiable": 0,
  "unavailable": 0,
  "label": "",
  "ends_at": 1710003600
}

getListitems[] 还会扩展 variant、price、final_price、promotion_id 等,见 CartService::cartDataComposition

4.3 checkoutcart 快照结构

通俗理解(分 type 解释「快照到底存了什么」、完整交互时序):见 结账快照

checkoutcart:{checkout_token}对象(非数组):

字段说明
type见下表
cart_tokentype=cart/order 时,指向 guestcartcustomercart完整 Redis key 字符串
cart_datatype=buynow/instant/paypal_ec 时,JSON 字符串的内嵌行数组

type 枚举CartModel):

type含义商品数据来源
cart从购物车结账getData(cart_token) 数组
buynow立即购买cart_data 内嵌
instant_checkoutInstant checkout同 buynow
paypal_ecPayPal EC同 buynow
order已创单o_order + order_product,不再读 cart_token 商品(或仅校验指针)

type 流转

stateDiagram-v2
    [*] --> cart: getCheckoutUrl 从购物车
    [*] --> buynow: createBuynow
    cart --> order: createOrder + saveCheckoutCart
    buynow --> order: createOrder + saveCheckoutCart
    order --> order: 继续支付/改地址
  • 创单成功:OrderService::saveCheckoutCart()(5379 行)把 checkoutcart.type 设为 order
  • getList 时若 checkoutcart 键已过期但订单存在:重建 {type:order, cart_token}(1160–1164 行)。

4.4 TTL(过期时间)

数据常量TTL
guestcartGUEST_DATA_EXPIRE90 天
customercartCUSTOMER_DATA_EXPIRE90 天
checkoutcart(cart 型)同上90 天(随 customer/guest)
checkoutcart(buynow)BUYNOW_DATA_EXPIRE7 天

过期后:getList 可能走「checkoutcart 失效重建」或订单不存在异常。


5. 何时清理购物车

5.1 clearCart(customer_id)

CartService::clearCart(958 行):

  1. delCart → 删除 guestcartcustomercart Redis 键
  2. removeCheckoutVisitId() → 删 checkout Cookie

自动删 checkoutcart:{旧token}

5.2 clearCartByCheckoutToken(checkout_token)

978 行:若 checkoutcart 存在且含 cart_token,则 delete 该 cart_token 指向的 guest/customer cart

用于:支付成功、saveProducts 发现 item_count=0、Admin 删单等。

5.3 典型触发场景

场景动作
支付成功 / 更新已付clearCartByCheckoutToken + 可能 clearCart + renew checkout cookie
订单 paid/pending/cancel 且 cookie checkout=tokengetList / OrderService::clearCart → clearCart + renew token
用户清空购物车 APIclearCart
登录合并游客 cart合并到 customercart,delete guestcart(1102 行)
saveProducts 购物车空clearCartByCheckoutToken + removeCheckoutVisitId + 跳转 /cart
COD 下单成功CodOrderService::clearCart 同类逻辑

5.4 不会清理的情况

  • 仅关闭浏览器:依赖 Redis TTL(90 天 / 7 天)。
  • 未创单的 checkout 浏览:checkoutcart 与 guestcart 仍保留。
  • 已创单未支付:type=order,商品以 MySQL order_product 为准;Redis cart 可能仍被 cart_token 引用用于 _checkCheckoutCart 校验。

6. getList 如何选数据源

CartService::getList($customer_id, $checkout_token)(355 行起):

有 checkout_token
  → 读 checkoutcart:{token}
  → switch type:
       cart     → 读 cart_token 指向的 cart 数组
       buynow/… → json_decode(cart_data)
       order    → OrderService::getOrderByToken + products
  → cartDataComposition → diy_offer → promotion → coupon → …

checkoutcart 为空:type 默认当作 order(379–380 行),尝试按订单加载。


7. 与订单字段的对应

订单字段来源
checkout_token创单时 URL/Context 的 md5 checkout id
visitor_idContext->visit_id(global 侧 md5)
submit_typecart / buynow / instant 等,来自 checkoutcart.type
customer_id登录或 saveContactInformation 注册

8. 代码索引

主题路径
visit_id / checkout_tokencommon/Request.php
Cookie 读写common/services/CookieService.php
模板用 global_visit_idextend/helper/FuncHelper.php::oemsaasGlobalVisitId
中间件注入 Contextcommon/middlewares/DomainAnalysis.php
购物车 CRUD / checkout URLcommon/services/CartService.php
checkoutcart → ordercommon/services/OrderService.php::saveCheckoutCart
Redis Keyextend/helper/CacheKeyHelper.php
设计注释extend/shoppingProcess/cartLogic.php

9. 排障速查

现象排查
结账页 order not foundcheckoutcart 过期且订单不存在;或 token 与 Cookie 不一致
加购后另一浏览器看不到 cartguestcart 绑 visit_id(Cookie),跨浏览器不共享
buynow 重复进旧单buynow 应用新 token;检查是否误复用 Cookie checkout
付完款还能用旧 token 加购正常;renew 后是新 token,旧 token 仍关联历史订单
guestcart 键对不上确认 visit_id 是否为 md5(global cookie raw)
checkoutcart 有 type 无商品type=cart 时检查 cart_token 是否被 clearCart 删掉

相关:checkout-flows.md · promotion-discount-checkout.md