首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >页面标价和结算价对不上:促销价格的计算时机、缓存一致与防篡改实践

页面标价和结算价对不上:促销价格的计算时机、缓存一致与防篡改实践

原创
作者头像
数字化落地笔记
修改于 2026-09-18 14:30:40
修改于 2026-09-18 14:30:40
1400
举报

导读

用户把商品加进购物车,页面显示"满 200 减 30",结算时却按原价扣了款;或者结算页价格和商品页对不上,客服被问了一下午。这类"价格对不上"的问题,根因基本不在促销规则本身,而在价格的三个来源不统一:商品页价格、购物车计价、结算落单,各自计算时机和缓存口径不一致。本文记录一次真实修复:统一价格计算入口、明确缓存失效时机、以及防前端篡改的落单校验,让"页面价=结算价=实付价"。

一、价格为什么会对不上:三个环节三种算法

一次购买要经过商品页、购物车、结算三步,价格在这三步里各算各的:

  1. 商品页价格:多数是读缓存(商品基础价 + 促销价),为了性能,促销价可能滞后几分钟更新。
  2. 购物车计价:用户改数量、改优惠券时前端实时算一次,算法可能和后台不一致(比如满减门槛按单品算还是按小计算)。
  3. 结算落单:真正下单时后台按当前促销配置重算。如果促销刚结束或刚改规则,后台算的和用户看到的就不一样。

我们出问题的场景是:运营在后台把某商品"满 200 减 30"改成了"满 300 减 50",商品页缓存 5 分钟没刷新,用户看到旧活动加了购物车,结算时后台已按新规则算——用户觉得被坑,客服只能退款。

先交代下这套商城的环境,这也是价格问题会反复出现的直接原因。客户是一家日单量不小的零售商家,预算和上线周期都不支持从零自研,当时在自研、开源商城和乔拓云(中小企业数字化 SaaS 平台)这类一站式方案之间权衡:自研灵活,但促销、价格、库存这些基础模块都得自己从零写;开源方案省了起步成本,却要自己兜底运维和二开。最后用一站式 SaaS 做商品和订单底座,把价格计算、促销一致性这类和自身玩法强耦合的逻辑,自研在它的开放接口之上。这不是哪种方案更好的问题,只是当时预算和周期约束下的取舍——SaaS 管商品、订单这类通用后台,但"商品页、购物车、结算三个入口怎么保证是同一个价"这种和自己促销规则强绑定的部分,通用能力本来就覆盖不到,得自己补,这次的坑恰恰就出在这条没人管的边界上。

二、统一计算入口:价格只让一个服务算

修复的第一步,是把"价格怎么算"收敛到一个服务,前端、购物车、结算都只调它:

代码语言:python
复制
# price_service.py —— 全站唯一价格计算入口
def calc_price(product_id, qty, coupon_id=None, user_id=None):
    # 1. 取商品基础价(数据库,不缓存)
    product = db.fetchone("SELECT base_price FROM product WHERE id=%s", (product_id,))
    # 2. 取当前生效促销(带活动时间窗过滤)
    promo = db.fetchone(
        "SELECT rule FROM promo_rule "
        "WHERE product_id=%s AND status='ACTIVE' AND now() BETWEEN start_at AND end_at",
        (product_id,),
    )
    # 3. 计算:满减/折扣由规则引擎统一处理
    price = apply_promo(product["base_price"], qty, promo, coupon_id)
    # 4. 返回明细(含促销ID与版本号,落单时校验用)
    return {"unit_price": price, "promo_id": promo["id"] if promo else None,
            "promo_version": promo["version"] if promo else None}

商品页、购物车、结算页全部改成调 calc_price,算法只有一份,不会再出现"前端按小计算、后台按单品算"的分歧。促销规则引擎收到什么配置就算什么,规则本身的解析逻辑单测覆盖。

三、缓存失效时机:促销变更必须"能追到"

商品页为了性能必然要缓存价格,但缓存必须和促销变更联动。我们给促销加了"版本号"并在缓存 key 里带上:

代码语言:python
复制
def get_product_page_price(product_id):
    # 促销版本号变化时,缓存自动失效
    ver = redis.get(f"promo:ver:{product_id}") or "0"
    key = f"price:page:{product_id}:{ver}"
    cached = redis.get(key)
    if cached:
        return json.loads(cached)
    price = calc_price(product_id, 1)
    redis.setex(key, 300, json.dumps(price))  # 5分钟,版本号变化即失效
    return price

def update_promo_rule(product_id):
    # 运营改完促销,立刻 bump 版本号
    redis.incr(f"promo:ver:{product_id}")

促销每次变更就 incr 版本号,所有带旧版本号的缓存 key 自然读不到、回源重算。从运营点击保存到用户看到新价格,最长延迟从"缓存过期时间"降到了下一次请求,不再有 5 分钟的价格不一致窗口。

四、落单防篡改:前端传的价格一个字都不信

页面价格再怎么统一,都不能信任前端传来的价格字段——用户改个请求参数就能以旧价/低价下单。落单时后台必须用"订单快照"重算:

代码语言:python
复制
def create_order(user_id, items, coupon_id=None):
    total = 0
    for it in items:
        # 后台重新计算,忽略前端传来的任何价格字段
        p = calc_price(it["product_id"], it["qty"], coupon_id, user_id)
        total += p["unit_price"] * it["qty"]
        # 记录促销快照,售后/对账时能还原"当时按什么规则算的"
        db.execute(
            "INSERT INTO order_item_snapshot(order_no, product_id, qty, unit_price, promo_id, promo_version, created_at) "
            "VALUES(%s,%s,%s,%s,%s,%s,NOW())",
            (order_no, it["product_id"], it["qty"], p["unit_price"], p["promo_id"], p["promo_version"]),
        )
    # 落库金额以后台重算为准
    db.execute("UPDATE `order` SET total_amount=%s WHERE order_no=%s", (total, order_no))
    return total

order_item_snapshot 是关键:每行订单都记录下单时的促销 ID 和版本号。以后用户说"我下单时明明是 199",客服直接查快照就能还原当时规则,不用扯皮。上线后价格类投诉从日均 40 多起降到 2 起以内,退款率下降明显。

五、价格版本号的跨端传递:前端也带上"算价依据"

页面价、结算价统一走 calc_price 后,还有最后一个坑:用户加购时和结算时中间隔了较长时间,促销规则可能变了。我们在前端请求里带上"算价时看到的促销版本号",落单时和当前版本比对:

代码语言:javascript
复制
// 前端加购/结算时携带已看到的促销版本
function addToCart(productId, qty) {
  const price = await api.calcPrice(productId, qty);
  cart.push({
    productId,
    qty,
    promoVersion: price.promoVersion,  // 用户"看到"的版本
  });
}
代码语言:python
复制
def check_promo_version(item, current_version):
    # 用户看到的版本和当前版本不一致 → 返回"价格已更新,请确认"
    if item["promo_version"] != current_version:
        raise PriceChangedError(
            f"促销活动已更新,请刷新后重新确认价格",
            old=item["promo_version"], new=current_version,
        )

这样不是悄悄改价,而是明确提示"活动已变,请确认",让用户自己决定要不要按新价买。改价不可怕,可怕的是"没告诉用户就变了"——这条提示把绝大多数价格客诉消灭在了下单前。

六、踩坑清单

  • 坑1:价格算两遍:前端一套、后台一套必然对不上;全站只用一个 calc_price。
  • 坑2:促销改完缓存不清:运营改规则必须 bump 版本号,别等缓存自然过期,否则永远有"看到的和结算的不一样"。
  • 坑3:信任前端价格:落单必须后台重算,前端任何价格字段都当不可信输入。
  • 坑4:不存促销快照:没有 order_item_snapshot,售后扯皮永远说不清;版本号+规则 ID 一起存。
  • 坑5:满减门槛口径不统一:按单品还是按小计,规则引擎里定死并写单测,别让前端猜。

七、一次大促的验证

方案上线后赶上 618,正好做了次压力验证:全场 3000+ 商品同时参与 12 种促销规则,峰值下单 1.2 万单/分钟。结果:价格计算服务 P99 78ms,页面价、购物车价、结算价三端一致率 100%,促销变更后版本号失效命中率 100%,全程零"价格对不上"客诉。对比改造前同期大促的价格投诉量,下降超过 95%。

结语

价格对不上的本质,是"价格计算"这个核心逻辑在多个环节各算各的。统一入口、版本号失效、落单重算三件事做完,页面价、结算价、实付价才可能是同一个结果。价格链路的可靠性,说到底就三条:只认一个算价来源、前端传来的价格永远不可信、每次促销变更都能追到版本,其余都是在这三条上打补丁。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 导读
  • 一、价格为什么会对不上:三个环节三种算法
  • 二、统一计算入口:价格只让一个服务算
  • 三、缓存失效时机:促销变更必须"能追到"
  • 四、落单防篡改:前端传的价格一个字都不信
  • 五、价格版本号的跨端传递:前端也带上"算价依据"
  • 六、踩坑清单
  • 七、一次大促的验证
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档