POS 会员积分

Odoo POS 会员积分为什么不是“前台减几分就行”:优惠券校验、卡券建档与重复发放防线讲透

很多人以为 POS 里的积分、礼品卡、电子钱包只是前台把分数改一改;但 Odoo 真正在处理的是“这张卡是否还有效、积分够不够、同一订单会不会重复发卡、已有卡要不要并到老客户名下”。本文结合 pos_loyalty 源码,把 validate_coupon_programs、confirm_coupon_programs 与 loyalty history 的真实边界讲透。

POS
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多团队第一次做 Odoo POS 会员玩法时,脑子里默认的模型是:

  • 前台顾客用了 100 积分;
  • 系统把积分字段减掉;
  • 如果送了一张券,就新建一张卡。

这个想法太直线了。

/home/ubuntu/odoo-temp/addons/pos_loyalty/models/pos_order.py 看,Odoo 真正在保护的是一整条 卡券生命周期:先校验能不能用,再落订单,再建卡/绑卡/记历史,最后把结果回传给前台缓存。也就是说,POS loyalty 不是“改个余额”,而是“先防错、再记账、再回写状态”的闭环。

结论先说

如果你要理解 Odoo POS 里的积分、礼品卡、电子钱包,先记住三句话:

  1. 前台不能自说自话决定优惠一定成立,后端会再验一次;
  2. 新卡和老卡不是同一类对象,Odoo 会先做映射、去重、合并;
  3. 历史记录不是附属品loyalty.history 才是后续审计、补偿和排错的关键证据。

第一层边界:为什么结账前还要再验一次 coupon

validate_coupon_programs() 做的第一件事,不是发券,而是验券。

它会先把前台带来的 point_changes 转成以后端卡片 ID 为准的数据,再检查:

  • 这些 coupon 是否真的存在;
  • 是否仍属于 active program;
  • 当前卡里积分是否足够抵扣;
  • 前台准备新生成的 code 在数据库里是否已经存在。

这一步非常像收银台前的“最后闸机”。

因为 POS 前端的缓存可能已经旧了:

  • 顾客刚在别的门店用过同一张卡;
  • 这张券对应的 program 已失效;
  • 前台预生成的 code 已被别的终端抢先卖出;
  • 本地缓存还以为有积分,但真实余额已经不够。

所以 Odoo 的态度很明确:

优惠是否成立,最终以后端校验为准,不以前端展示为准。

这也是为什么很多“前台明明能选券,提交时却提示失效”的案例,并不是 bug,而是后端在拦住过期事实。

第二层边界:为什么要先处理“已存在卡”再决定是否建新卡

confirm_coupon_programs() 真正落库前,先调用了三步预处理:

  • _check_existing_loyalty_cards()
  • _remove_duplicate_coupon_data()
  • _process_existing_gift_cards()

这三步特别能体现 Odoo 的业务观。

1)老客户已经有卡,就不要再乱长一张

_check_existing_loyalty_cards() 会检查:如果某个 partner_id 在同一 loyalty / ewallet program 下已经有卡,就把本次前台传来的临时 key 改指向已有卡。

这意味着 Odoo 优先维护的是:

  • 一个客户在同一计划下尽量持续使用同一张卡;
  • 同类权益尽量累计,不轻易碎成多张匿名卡。

这对实施很重要。否则门店越做越久,最后不是积分体系,而是“一个客户名下十几张半死不活的卡”。

2)同一订单重复回放,不应该重复发历史

_remove_duplicate_coupon_data() 会查 loyalty.history 是否已经存在同一订单的历史线。

这一步看似不起眼,实战里却非常关键。因为 POS 本来就存在:

  • 网络抖动后重试;
  • 离线回传重复点击;
  • 同步恢复时订单多次重放。

如果没有这层去重,一张单可能:

  • 重复加积分;
  • 重复发券;
  • 重复生成礼品卡 PDF;
  • 把客户余额冲得越来越离谱。

所以这里的核心思想是:

POS loyalty 必须具备“重复提交幂等防线”,不能把每次重放都当成新业务。

3)礼品卡不是永远新建,很多时候是“补绑定”或“补归属”

_process_existing_gift_cards() 处理 gift card 的方式也很有代表性。

如果系统能通过 codecoupon_id 找到已有 gift card,它并不会急着新建,而是优先做这些补全:

  • 卡还没绑客户,但这次订单有客户,就把 partner_id 补上;
  • 卡还没挂 source order,就把 source_pos_order_id 补上;
  • 如果本次实际消费/充值与卡内 points 不一致,就更新 points 并写历史。

这说明在 Odoo 看来,礼品卡不是一次性打印凭证,而是一个会持续演化的权益载体。

第三层边界:为什么前台负 ID 能变成后端真实卡 ID

confirm_coupon_programs() 里,前台尚未存在的 coupon 常常先用负 ID 占位。

后端落库后,会建立 coupon_new_id_map,把:

  • 前台临时 ID
  • 后端新建真实 ID

对应起来,再回传 coupon_updates 给前台。

这解决的是 POS 场景里一个很现实的问题:

前台在一张订单里,可能先“假想”出若干将要创建的卡券,但只有订单真的落库后,它们才配拥有数据库里的正式身份。

如果你忽略这层映射,前端会很容易出现:

  • 订单行绑了不存在的 coupon;
  • 小票打印拿不到真实 code;
  • 后续继续消费时还指着旧临时 ID。

reward line 为什么还要回填 coupon_id

源码里会把订单行按 reward_identifier_code 归组,再把对应的 coupon_id 回填到真正的 reward line 上。

这一步的意义不是“字段填完整”这么简单,而是把三层对象串起来:

  • 顾客用了哪个奖励;
  • 这条奖励来源于哪张卡;
  • 这张卡属于哪个 program。

没有这层链路,你后面就很难回答:

  • 这次优惠到底消耗了谁的积分;
  • 赠券是哪张订单发出去的;
  • 某张 gift card 为什么余额会变。

loyalty history 为什么是整个机制的审计中轴

add_loyalty_history_lines() 会把每张卡在这张单上的 spent / won 分别记成 history line。

它不是可有可无的日志,而是后续最关键的解释层。因为真实门店里最常见的问题不是“系统会不会发券”,而是:

  • 为什么这个客户积分少了;
  • 为什么这张卡多了 50;
  • 为什么礼品卡突然绑定到某个客户;
  • 为什么同一订单打印了券但看起来没加积分。

这些问题最后都要回到历史线去追。

你可以把它理解成:

余额是结果,history 才是证据。

最容易误解的四件事

误解一:POS loyalty 就是改 points

不对。

points 只是余额表象。真正完整的流程包括校验、建卡、映射、绑订单、绑订单行、记历史、回写前端缓存。

误解二:新券一定新建卡

不对。

对 loyalty / ewallet,Odoo 会优先复用同客户同计划已有卡;对 gift card,也会先找已有卡并补归属。

误解三:同步重试无所谓,最多重复发一次

这恰恰很危险。

POS 场景最怕的就是“看起来只是重试,实际上把权益重复发放”。_remove_duplicate_coupon_data() 就是在挡这种事。

误解四:打印出来就说明后端都落对了

不一定。

正确判断应该看:

  • loyalty.card 是否正确创建/更新;
  • coupon_updates 是否回传前台;
  • loyalty.history 是否形成证据链。

实战排错顺序

遇到“积分扣错、礼品卡重复发、优惠券失效、客户卡余额不对”时,建议按这个顺序查:

  1. 订单提交前,前台使用的 coupon/code 是否还是最新状态;
  2. validate_coupon_programs() 返回的是哪类失败:无效券、积分不足,还是 code 冲突;
  3. 同一个 partner 在对应 program 下是否已有卡;
  4. 是否存在订单重复同步,导致同单多次尝试发券;
  5. loyalty.history 是否已经存在同一订单的历史,触发了去重;
  6. gift card 是应新建,还是其实命中了已有卡;
  7. reward line 有没有正确回填 coupon_id
  8. 前端是否拿到了新的 coupon_updates 并刷新本地缓存。

最后的结论

Odoo POS 的 loyalty 机制,本质上解决的不是“积分数字怎么变”,而是:

  • 顾客权益能不能被安全使用;
  • 新旧卡怎样在一次结账里正确衔接;
  • 重试、离线、重复提交时怎样避免把权益体系冲坏。

所以这套设计最值得记住的一句话是:

在 Odoo POS 里,积分、礼品卡和优惠券都不是前台临时 UI 状态,而是有校验、有身份、有历史证据的长期对象。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。