checkout_token / 购物车 Redis / global_visit_id
本文说明前台 访问标识(global_visit_id / visit_id / checkout_token)的生成与 Cookie 关系,Redis 购物车数据结构、checkout 快照 的 type 流转,以及 何时清理。与结账形态的关系见 checkout-flows.md。概念速查见 faq.md。
与实现冲突时以代码为准。
目录
- 1. 三个 ID 的关系(易混)
- 2. global_visit_id 生成逻辑
- 3. checkout_token 生成逻辑
- 4. Redis 购物车数据结构
- 5. 何时清理购物车
- 6. getList 如何选数据源
- 7. 与订单字段的对应
- 8. 代码索引
- 9. 排障速查
1. 三个 ID 的关系(易混)
系统里同时存在三层标识,用途不同:
| 名称 | 代码入口 | Cookie 存什么 | 业务里常用值 | 用途 |
|---|---|---|---|---|
| global_visit_id | FuncHelper::oemsaasGlobalVisitId() | 原始 UUID/TraceId(未 md5) | 模板/Liquid 展示、像素 | 长期访客标识 |
| visit_id | Request::getVisitId() → Context->visit_id | 同上,读入后 md5 | Redis guestcart:{visit_id} | 游客购物车、UTM、订单 visitor_id |
| checkout_token | Request::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_id与checkout_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) |
2.3 Cookie 键名(兼容多套)
CookieService::getGlobalVisitId() / setGlobalVisitId():
| 优先级 | Key |
|---|---|
| 1 | _{encryptedStoreId}_vid(getStoreUniqueCookieKey('_vid')) |
| 2 | shop_global_visit_id(CookieConst::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_id(getSessionVisitId):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_id,Cookie::forever。
3.2 各场景如何得到 token
| 场景 | 代码位置 | 生成方式 |
|---|---|---|
| 立即购买 buynow | CartService::createBuynow 243 行 | getCheckoutVisitId(true) → 新 token,写 checkoutcart type=buynow |
| 购物车去结账 | CartService::getCheckoutUrl 1126 行 | 用当前 Context->checkout_visit_id(Cookie 复用或新建) |
| 订单已付/取消/待付且 cookie 匹配 | getList 404–407、getCheckoutUrl 1145–1146 | getCheckoutVisitId(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 / pendingstatus= cancel- 或
checkoutcart.cart_token与当前用户 cart key 不一致(换浏览器/换账号,getCheckoutUrl1142–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 键本身(OrderService1453、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} | guestcartKey | JSON 数组 | 游客购物车行列表 |
{store}:customercart:{customer_id} | customercartKey | JSON 数组 | 会员购物车行列表 |
{store}:checkoutcart:{checkout_token} | checkoutcartKey | JSON 对象 | 结账快照(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
}getList 后 items[] 还会扩展 variant、price、final_price、promotion_id 等,见 CartService::cartDataComposition。
4.3 checkoutcart 快照结构
通俗理解(分 type 解释「快照到底存了什么」、完整交互时序):见 结账快照。
checkoutcart:{checkout_token} 为 对象(非数组):
| 字段 | 说明 |
|---|---|
type | 见下表 |
cart_token | type=cart/order 时,指向 guestcart 或 customercart 的 完整 Redis key 字符串 |
cart_data | type=buynow/instant/paypal_ec 时,JSON 字符串的内嵌行数组 |
type 枚举(CartModel):
| type | 含义 | 商品数据来源 |
|---|---|---|
cart | 从购物车结账 | getData(cart_token) 数组 |
buynow | 立即购买 | cart_data 内嵌 |
instant_checkout | Instant checkout | 同 buynow |
paypal_ec | PayPal 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 |
|---|---|---|
| guestcart | GUEST_DATA_EXPIRE | 90 天 |
| customercart | CUSTOMER_DATA_EXPIRE | 90 天 |
| checkoutcart(cart 型) | 同上 | 90 天(随 customer/guest) |
| checkoutcart(buynow) | BUYNOW_DATA_EXPIRE | 7 天 |
过期后:getList 可能走「checkoutcart 失效重建」或订单不存在异常。
5. 何时清理购物车
5.1 clearCart(customer_id)
CartService::clearCart(958 行):
delCart→ 删除guestcart或customercartRedis 键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=token | getList / OrderService::clearCart → clearCart + renew token |
| 用户清空购物车 API | clearCart |
| 登录合并游客 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_id | Context->visit_id(global 侧 md5) |
submit_type | cart / buynow / instant 等,来自 checkoutcart.type |
customer_id | 登录或 saveContactInformation 注册 |
8. 代码索引
| 主题 | 路径 |
|---|---|
| visit_id / checkout_token | common/Request.php |
| Cookie 读写 | common/services/CookieService.php |
| 模板用 global_visit_id | extend/helper/FuncHelper.php::oemsaasGlobalVisitId |
| 中间件注入 Context | common/middlewares/DomainAnalysis.php |
| 购物车 CRUD / checkout URL | common/services/CartService.php |
| checkoutcart → order | common/services/OrderService.php::saveCheckoutCart |
| Redis Key | extend/helper/CacheKeyHelper.php |
| 设计注释 | extend/shoppingProcess/cartLogic.php |
9. 排障速查
| 现象 | 排查 |
|---|---|
| 结账页 order not found | checkoutcart 过期且订单不存在;或 token 与 Cookie 不一致 |
| 加购后另一浏览器看不到 cart | guestcart 绑 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 删掉 |