先预览再提单,为什么要这么设计?

2023年 9月 17日 62.0k 0

把一个大象放进冰箱需要几个步骤?3步,那网购一个大象需要几步呢?

购买一个商品需要几步

电商业务场景中,用户下单流程是核心中的核心,用户下单流程的设计不光影响了用户的购买体验,也极大的影响系统可扩展能力。下单内容比较多,今天重点聊一下,为什么先预览再提单,在此之前先熟悉下用户网购的操作流程。

把大象放进冰箱需要 3 步,网购一个大象则需要 6 步。

步骤 系统做了什么?
1. 浏览商品列表页找到心仪的大象 Feeds流,推荐系统根据你的喜好给你推荐 大象
2. 点击进入大象详情页 查询商品中心,获取大象的详细信息
3. 点击去结算(去购买) App调用预览接口,生成购买详情、可用优惠
4. 商品预览页点击选择收货地址、优惠券 客户端记录订单信息
5. 点击去支付 触发系统提单行为,真正创建订单,锁定优惠、扣库存、15分钟支付超时Timer
6. 支付输密码 进入收银台页面,选择可用支付渠道

为了清晰的展示用户行为,以及背后的系统接口,我画了流程图如下。我发现,几个现象?

  • 用户点击去结算,去购买按钮,实际上系统并没有真正提单。系统调用的是预览接口
  • 预览接口预生成订单详情,用户可在预览页面修改地址和优惠信息。(但不能加购商品)
  • 用户看到去支付,立即支付,实际系统并没有真的调支付接口,系统调用的是提交订单接口。
  • 提单接口需要防重,防非法提单,锁定优惠,生成订单。提单成功,页面跳转支付页、收银台页面
  • sequenceDiagram
    User-)App:浏览商品列表页找到心仪的大象
    User-)App: 点击进入大象商品详情页
    App-)商品中心:查询大象详情
    User-)App: 点击去结算(点击立即购买)
    App-->>User:跳转到大象预览页(提单页)
    App-)订单中心:预览接口生成大象订单详情
    User-)App:选择收货地址,选择优惠
    User-)App: 去支付
    App-->>User: 跳转到支付页
    App-)订单中心: 提交订单(一系列校验)
    User-)App: 选择支付方式,支付
    

    你发现没有?好像用户看到的引导操作(去购买、去支付)并不会触发提单、支付等动作,而是触发操作的上一步接口。
    例如 去购买触发的预览接口,去支付触发的提单接口。

    小伙伴们在熟悉订单业务流程的时候要注意这一点。

    点击去购买,不要真的生成订单数据

    我举一个反面例子,前东家的订单系统踩了坑。用户点击去购买居然真的提交了订单,生成了订单数据。而在提单页用户可以修改订单上的优惠信息,收货地址等。用户下单前可能反复比较价格,反复衡量纠结……,在提单页和商品详情页、购物车之间反复切换,这导致了大量的提单动作,用户修改地址、选择优惠等行为也导致系统需要频繁修改订单数据,系统生成了很多无效订单。 为了减少用户在订单列表看到很多无效订单,就要取消上一笔订单,用户的切换行为给系统带来了非常大的性能压力。所幸的的是公司小,订单量不高。要不然高峰期,订单系统性能问题真的够喝一壶的。

    不得不说,这真的是一个糟糕设计。我们日常沟通时也闹出很多误会。

    关于提单,大家的理解就有不同,有的人认为去购买的行为就是提单,因为点完去购买真的生成了订单数据。但是有的人不那么认为。由于在生成订单数据后,才选择优惠券等行为,点击去支付,才真正锁定优惠券,负责营销的同事就认为用户点击去支付才是提单。因为这个动作真正选中地址、锁定优惠,这个接口包括提单的主要校验逻辑、锁定优惠、扣减库存等逻辑。这才是提单。各有各的道理,每次聊到提单,都要额外确认下,“你说的提单是哪个提单”。沟通成本太高了。

    踩坑的原因我也有所了解,是公司第一代程序员对订单的理解浅尝辄止,不够透彻,调研不充分。看到京东、淘宝、美团等公司人家有去购买,去支付等操作流程,就真的以为 去购买就是提单,就要生成订单数据? 实际大错特错。

    订单的提单接口一定要在用户选中优惠、选中地址等信息后,订单的商品信息、优惠信息、收货地址等板上钉钉以后,才能调用订单接口锁定资源(优惠券、库存),才能真正生成订单数据。

    用户在提单页或者叫订单预览页 可选择优惠、选择地址。确定后,用户点击去支付,系统提交订单,唤起收银台。这才是正确的交互流程。

    用户进入预览页不生成订单数据可以减少很多无效订单数据,那假如用户重复提单怎么办?

    预览如何防止重复提单

    防止用户重复提单,前端可以通过防抖、置灰等操作有效避免用户高频率点击提单按钮导致的重复提单。此外服务端也要通过系统设计,避免用户重复提单。我先介绍预览接口如何防重复提单,再介绍其他方案。

    预览接口除了返回订单预览页展示给用户的商品、优惠信息,还可以返回给前端一个预览Token,唯一ID。前端调用提单时,需要指定预览Token。服务端校验预览Token是否存在,不存在则返回提单失败,前端重新刷新页面,重新预览或者返回商品详情页。

    服务端要保证查询预览Token是否存在和删除预览Token操作的原子性。由于预览接口的并发量高于提单,需要保证预览接口的性能,可以考虑使用Redis方案,且无需存储数据库。Redis集群采用主从方案,可保证数据丢失仅仅在秒级别。假设Redis主节点挂掉,也只会丢失几秒的预览Token,只影响几秒内的提单成功率。所以预览Token校验使用Redis即可。Redis del 命令可以返回是否删除成功,能用DEL命令,直接删除Token吗?

    超时场景不好处理,DEL命令如果调用超时就无法确定预览Token是否存在。重试删除也无法知道,因为超时的请求在Redis端可能删除成功,也可能预览Token不存在。所以重试也没用。

    似乎DEL命令不合适,但是实际上Redis超时概率很低,就我们公司业务场景,分布式锁Redis加锁超时的概率低于0.0004% ,概率很低。可以容忍DEL超时场景导致的提单失败。

    所以通过预览接口生成预览Token,可以避免重复提单的问题。还有其他方案吗?

    使用购买信息生成唯一键防重复提单

    用户在极短时间内提交订单,势必订单信息是相同的。例如同一个商品,购买数量相同等,在短时间内提交,被视为重复提交,可以阻断提单。

    例如使用userId+商品Id+购买数量,作为Redis Key,提单时尝试使用 set key1 value1 nx ex 10"加锁"。 这个方案和分布式锁类似,提单瞬间先尝试加锁,加不上锁,则说明重复提单。不同的是,分布式锁是被释放的,此处的锁,可以不用释放,只需要配置超时时间即可。例如用户5s内对同一个商品购买被视为重复提单,那么超时时间配置5s即可。

    想象下如果加锁超时,如何处理呢?在我的文章中# 【千万级日订单系统】分布式锁翻车了… 解释了,超时可以尝试get 查询,是否加锁成功。但此处因为超时时间只有10s,所以返回提单失败,用户再次提单时,大概率锁已经超时被释放。用户的再次提单不会被阻断。所以场景不同,方案设计也不同。

    这个方案不需要预览接口预生成唯一键,使用业务购买信息进行校验,也是可以的,似乎方案还更简单,但是后续我们会看到使用订单Token扩展性更强,可以基于预览Token做更多的事情。

    如果用户长时间停留在某个页面,为了避免订单信息变化,提单失败,如何处理呢?

    如果用户停留预览页面过久怎么办

    用户长时间停留在订单预览页,库存信息、营销活动信息、优惠券生效期等各种数据发生变化。提单可能就会失败,如何避免预览页长时间停留后的提单呢?

    只需要配置预览Token Redis key的超时时间即可。例如10分钟。

    如果用户篡改提单信息呢?

    预览接口是用户从商品详情、购物车等页面跳转到提单页面时要调用的接口。预览接口从前端接收用户购买的商品信息展示在订单预览页,查询用户优惠资源供用户选择,甚至预览接口会给用户推荐一些加购商品,例如外卖会员卡加量包等。总之提单接口需要的数据都应该通过预览查询出来的。(新增地址除外)为的就是用户提单的数据是基本准确的。

    那万一用户提单的数据不准确怎么办?例如黑产通过刷提单接口,选中了一个 非法商品提交。举个例子,假如运营在不同地区投放的会员卡商品不同,例如卡权益不同,甚至价格不同,一般情况下,用户看不到这个商品,自然无法提单购买。

    但是用户如果通过修改Http报文,例如在浏览器修改购买商品id,提交订单,伪造请求呢?

    有的小伙伴可能会想,提单接口校验一下订单数据呗,重新校验下用户是否应该看见这个商品。 当然这个方案是可以的,但是提单接口的工作量和耗时将大大增加。有没有更加简单的方案呢?

    如果预览接口返回的关键信息,例如营销活动信息、优惠券信息、商品购买信息等关键数据 使用MD5加密呢?

  • 预览接口对关键信息加密生成预览摘要,存储在Redis 中,Key为预览Token。Value为MD5值
  • 提单接口计算关键信息的摘要。使用这个摘要和预览Token关联的摘要对比是否一致。不一致,说明订单信息被篡改了。
  • 还记得使用UserId+商品Id生成Redis Key防止用户重复提单的方案吗? 假设用户可以通过购物车购买,一次性购买10个商品Id,这个Redis key得拼接多长啊!!是否可考虑使用Md5呢?是可以的

    在这个方案中,使用MD5计算购买的商品信息等摘要。无论多少个商品ID,也不用担心长度问题,因为MD5 生成的摘要长度32位。并且不同商品列表计算MD5,出现碰撞的概率极低极低(可自行查阅)。

    不要高兴太早。计算订单信息摘要,需要如下校验方式。

  • 商品Id排序。商品Id+购买数量拼接成字符串后参与结算摘要
  • 如果预览接口返回营销活动数据,也可以计算摘要。提单时摘要数据一致说明数据没有被篡改,再校验用户选中的营销活动id是否在有效营销活动id列表中。就可以校验用户是否伪造营销活动数据。
  • 通过简单的摘要计算方式,可以简化一部分订单校验逻辑。但由于营销活动、商品存在时效性问题,可能还是需要订单校验时间等关键数据。计算购买信息MD5摘要数据,可以有效拦截篡改数据的行为。如果能在提单接口返回数据被篡改等响应结果,能够让黑产意识到“我们是有防数据被篡改能力的,你不要浪费精力刷接口了!”。

    但是预览接口如何防止伪造数据呢?

    预览接口作为C端重要的查询接口,理应对用户行为进行合法性校验,例如如果商品信息存在可见性问题,就应该校验是否可见。所以订单预览接口需要查询商品营销等下游数据,保证用户预览行为的合法性。

    订单提单接口由于存在大量的业务逻辑,包括生成订单数据、锁定优惠、扣减库存等大量耗时较高的写流程,所以如何简化提单接口的流程至关重要。可以考虑把一部分合法性校验放到预览环节。这样不但提单接口耗时降下来,而且提单的业务逻辑也更加简单。

    总结

  • 预览Token 服务端生成,提单前端透传,可有效防止重复提单
  • 预览Token+ 超时时间,可防止用户方式长时间停留预览页后提单。
  • 预览Token+ 订单重要数据MD5加密,防止提单接口数据被篡改
  • 提单接口的一部分数据合法性校验,可以前置到预览环节!!!降低提单接口耗时和简化流程。
  • 最后一个问题? 为什么拼多多上买一个大象只需要三步,你猜到拼多多合并哪几步了吗?

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论