尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

餐饮 SaaS 优惠券系统架构演进(三):优惠计算引擎——商品级计价、冲突策略与优惠分摊

餐饮 SaaS 优惠券系统架构演进(三):优惠计算引擎——商品级计价、冲突策略与优惠分摊 这一篇进入优惠券系统真正的“算法核心”。当前代码已经不是简单的orderAmount - couponAmount它会读取用户券快照、匹配门店/场景/商品/规格、计算三类券、处理券与商品活动的冲突策略并把商品级优惠稳定分摊到购物车行。1. 为什么优惠券最终会进入 Pricing Engine订单价格通常同时受到商品活动、会员价、优惠券、加料、打包费和场景规则影响。如果优惠券独立拿“订单总价”计算就无法回答一个基本问题这 20 元到底优惠了哪一行、哪一个规格、是否与商品活动冲突图 1当前营销计价的核心组件关系2. CouponCalculationService统一入口三类算法图 2三类优惠券从 user_claim 快照计算商品级优惠统一入口会根据couponType读取对应的用户领取实例只接受status0、未删除的 claim并优先使用couponSnapshot。结果不是一个总优惠金额而是MapproductId, BigDecimal兑换券还会返回productSpecLimitMap限定具体规格。券类型核心计算基数关键限制折扣券适用商品金额 × (1 - discountRate)threshold、scene/store、商品范围、maxDiscountQuantity、maxDiscountAmount满减券适用商品金额 可计入门槛的额外金额thresholdAmount 达标后减 discountAmount商品兑换券目标 productId/specId 的单件规格价门槛、兑换目标必须在当前商品和券范围内加料/打包费不作为兑换优惠本身3. 活动冲突不是 if/else系统同时构造两个候选图 3CouponActivityConflictPolicyEngine 的候选策略策略常量明确支持COUPON_FIRST1、ACTIVITY_FIRST2、BEST_PRICE3。BEST_PRICE 的实现不是简单比较“券优惠金额”和“活动优惠金额”而是分别计算两个完整候选的finalTotal再选择用户最终支付更低的结果。chooseBest(activityFirst, couponFirst): if couponFirst.finalTotal activityFirst.finalTotal: choose couponFirst if couponFirst.finalTotal activityFirst.finalTotal: choose activityFirst if finalTotal equal: compare totalCouponDiscount4. 为什么优惠分摊不能“平均除”图 4CouponDiscountAllocator 的稳定分摊逻辑当前 Allocator 维护 remainingCouponDiscount、remainingSubtotal 和 remainingItemCount。非最后一行按小计比例计算最后一行直接拿剩余优惠因此可以吸收四舍五入尾差同时每行优惠不会超过自身 subtotal。这个细节对退款、对账、行级金额展示非常重要。5. 一个真实代码里很容易被忽略的 ID 问题图 5couponId 多表命名空间冲突与兼容逻辑源码中有一条非常关键的注释couponId 在三类券表内不是全局唯一。因此旧接口如果没有传 couponType只靠 couponId 可能同时在三类 user_claim 表命中。当前兼容逻辑会从候选里按最近领取时间与 claimId 选择。6. Troubleshooting / RCA用户选择 A 券计价却像用了 B 券Symptoms订单前端只上传 couponId服务端日志显示同一个 couponId 在多个券类型中都找到了可用 claim最终计价券类型与用户界面选择不一致。Investigation检查数据库发现三类券分别自增主键因此 discount_coupon.id12 与 full_reduction_coupon.id12 可以同时存在。继续追踪resolveSelectedCandidate()确认旧调用在未传 couponType 时存在兼容选择逻辑。Root Cause问题不是优惠算法本身而是业务标识不完整couponId 只在各自表内唯一却被旧调用当成全局券标识。Resolution订单与客户端优先传claimId couponTypecouponId 只作为模板关联键新接口避免让服务端靠“猜测候选”定位用户真正选择的权益。Verification构造三类表相同 couponId 的测试数据分别传 claimIdcouponType验证命中唯一用户券同时保留旧接口回归确保兼容逻辑不会中断历史客户端。Lessons Learned数据库主键唯一不等于领域 ID 全局唯一。多表多态模型必须明确“ID 的命名空间”跨服务传参尤其要带类型或使用真正全局唯一的权益实例 ID。7. 当前计算引擎最值得保留的设计设计点价值从 user_claim 快照计算历史用户权益不被模板修改污染商品级 discountMap为订单行展示、退款、分摊和活动冲突提供基础productSpecLimitMap兑换券可以精确约束规格不把同商品其他规格误优惠ConflictPolicyEngine把“券优先 / 活动优先 / 最优价”从业务 if/else 中抽离CouponDiscountAllocator保证总优惠与行级优惠对得上控制舍入误差8. 架构演进建议当前CouponCalculationServiceImpl仍注入了大量 Mapper并同时承担“查用户券、范围匹配、券算法、兼容候选选择”等职责。下一步更适合做模块内解耦而不是立刻拆微服务CouponClaimResolver → CouponRuleCalculator → ScopeMatcher → ConflictPolicy → DiscountAllocator。下一篇《餐饮 SaaS 优惠券系统架构演进四高并发与最终一致性——库存模型、消息、补偿与批量任务》
返回列表