本文档描述营销活动与优惠券体系,涵盖活动类型、优惠券生命周期、优惠计算逻辑及叠加规则。 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。
| 体系 | 核心实体 | 管理入口 | 用途 |
|---|---|---|---|
| 营销活动 | MarketingActivity | MarketingActivityController |
首单立减/折扣/满减 |
| 定时折扣 | TimedDiscountActivity | TimedDiscountController |
按时间段自动执行的折扣 |
| 邀请有礼 | InviteActivity | InviteActivityAdminController |
邀请奖励活动 |
统一活动管理:UnifiedActivityService / UnifiedActivityController 提供三套活动的统一查询和管理入口。
三套体系虽同属"营销",但业务差异很大,强行合并会增加复杂度:
UnifiedActivityService 在查询层面做统一聚合,但各体系保持独立的 CRUD 和业务逻辑。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 雪花算法主键 |
| activityName | String | 活动名称 |
| activityType | Integer | 活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼 |
| activityDesc | String | 活动描述 |
| startTime / endTime | LocalDateTime | 活动有效期 |
| status | Integer | 状态:0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束 |
| discountValue | BigDecimal | 优惠值(立减金额/折扣率/减免金额) |
| minAmount | BigDecimal | 最低消费金额(满减门槛) |
| maxDiscount | BigDecimal | 最大优惠金额(折扣活动上限) |
| applyScope | Integer | 适用范围:1-全部门店 2-指定门店 |
| deviceScope | Integer | 设备范围:1-全部设备 2-指定设备 |
| productScope | Integer | 商品范围:1-全部商品 2-指定商品 |
| totalBudget / usedBudget | BigDecimal | 预算控制 |
| deleted | Integer | 逻辑删除 |
条件:用户之前无已完成订单(isFirstOrder 判断)
优惠 = min(discountValue, orderAmount)
结果:可能免单(优惠 >= 订单金额)
折扣率 discountValue:0 < discountValue <= 1(如0.8表示8折)
优惠 = orderAmount × (1 - discountValue),保留2位小数
如有 maxDiscount:优惠 = min(计算优惠, maxDiscount)
门槛:orderAmount >= minAmount
满足:优惠 = discountValue(固定减免金额)
不满足:优惠 = 0
ActivityStatusEnum)DRAFT(0, 草稿)
↓ publish
PUBLISHED(1, 已发布) → 到达开始时间自动 → ONGOING(2, 进行中)
↓ pause ↓ pause
PAUSED(3, 已暂停) → resume → ONGOING ↓ 到达结束时间
↓ ↓
ENDED(4, 已结束)
状态操作权限:
| 关联表 | 说明 |
|---|---|
| t_activity_shop | 活动-门店关联(applyScope=2时) |
| t_activity_device | 活动-设备关联(deviceScope=2时) |
| t_activity_product | 活动-商品关联(productScope=2时) |
关系模型:一个营销活动可以关联多个优惠券模板(1:N),优惠券模板的 activityId 字段指向所属活动,但优惠券也可以独立存在(activityId 为空)。
设计考量:
activityId 为可选字段,不强制绑定| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 雪花算法主键 |
| activityName | String | 活动名称 |
| startTime / endTime | LocalTime | 折扣时段(精确到时分秒,如 22:00:00 - 06:00:00) |
| discountType | Integer | 折扣类型:1-固定折扣 2-阶梯折扣 3-固定价格 |
| discountValue | BigDecimal | 折扣值 |
| minDiscountPrice | BigDecimal | 最低折扣价(底线价格) |
| applyScope / deviceScope / productScope | Integer | 适用范围 |
| productTypes | String | 商品分类(逗号分隔) |
| priorityProductTypes | String | 优先折扣商品分类 |
| minStock / maxStock | Integer | 库存范围筛选 |
| status | Integer | 状态 |
| autoExecute | Integer | 是否自动执行 |
| notifyOnExecute | Integer | 执行时是否通知 |
DiscountTypeEnum)| code | 枚举值 | 说明 |
|---|---|---|
| 1 | FIXED_DISCOUNT | 固定折扣(如统一8折) |
| 2 | TIERED_DISCOUNT | 阶梯折扣(库存越少折扣越大) |
| 3 | FIXED_PRICE | 固定价格(如夜场统一3元) |
定时折扣主要解决临期/夜间清库存问题:
阶梯折扣是定时折扣独有的机制:根据当前库存量动态调整折扣力度(库存越少折扣越大),由 minStock / maxStock 字段定义库存区间。
| 关联表 | 说明 |
|---|---|
| t_timed_discount_device | 折扣-设备关联 |
| t_timed_discount_shop | 折扣-门店关联 |
| t_timed_discount_product | 折扣-商品关联 |
| t_timed_discount_record | 折扣执行记录 |
| t_timed_discount_statistics | 折扣统计 |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 雪花算法主键 |
| couponName | String | 优惠券名称 |
| couponType | Integer | 类型:1-满减券 2-折扣券 3-立减券 4-商品券 |
| couponDesc | String | 描述 |
| discountValue | BigDecimal | 优惠值(金额/折扣率) |
| minAmount | BigDecimal | 最低消费金额(满减门槛) |
| maxDiscount | BigDecimal | 最大优惠金额(折扣券上限) |
| totalCount | Integer | 发放总量 |
| remainCount | Integer | 剩余库存 |
| receiveLimit | Integer | 每人限领数量 |
| validType | Integer | 有效期类型:1-固定时间 2-相对时间 |
| validStartTime / validEndTime | LocalDateTime | 固定有效期 |
| validDays | Integer | 相对有效天数(领取后N天有效) |
| applyScope | Integer | 适用范围:1-全部门店 2-指定门店 |
| productScope | Integer | 商品范围:1-全部商品 2-指定商品 |
| activityId | Long | 关联营销活动ID(可选) |
| status | Integer | 状态:0-禁用 1-启用 |
| receiveType | String | 发放类型:Collect-主动领取,Release-系统发放 |
CouponTypeEnum)| couponType | 名称 | discountValue含义 | 计算方式 |
|---|---|---|---|
| 1 | 满减券 | 减免金额(元) | 订单金额 >= minAmount → 优惠 = discountValue |
| 2 | 折扣券 | 折扣率(如0.8=8折) | 优惠 = orderAmount × (1 - discountValue/10),有 maxDiscount 上限 |
| 3 | 立减券 | 抵扣金额(元) | 优惠 = discountValue(无门槛) |
| 4 | 商品券 | 无意义 | 免费兑换指定商品,不用于金额扣减 |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 雪花算法主键 |
| couponCode | String | 优惠券编码(唯一) |
| templateId | Long | 优惠券模板ID |
| userId | Long | 用户ID |
| orderId | Long | 使用订单ID(使用后写入) |
| status | Integer | 状态:0-未使用 1-已使用 2-已过期 |
| receiveTime | LocalDateTime | 领取时间 |
| useTime | LocalDateTime | 使用时间 |
| validStartTime / validEndTime | LocalDateTime | 有效期 |
| discountAmount | BigDecimal | 实际优惠金额(使用时记录) |
CouponStatusEnum)| code | 枚举值 | 说明 |
|---|---|---|
| 0 | UNUSED | 未使用 |
| 1 | USED | 已使用 |
| 2 | EXPIRED | 已过期 |
| 关联表 | 说明 |
|---|---|
| t_coupon_shop | 优惠券-门店关联 |
| t_coupon_product | 优惠券-商品关联 |
| t_coupon_distribute | 优惠券发放记录 |
receiveType)| 类型 | 说明 |
|---|---|
| Collect | 主动领取:用户在小程序“领券中心”点击领取 |
| Release | 系统发放:系统自动发放(如邀请奖励、新人礼包) |
高并发领取场景下使用 Redis 原子操作扣减库存:
// Redis原子扣减
String key = "coupon:stock:" + templateId;
Long remain = redisTemplate.opsForValue().decrement(key);
if (remain < 0) {
redisTemplate.opsForValue().increment(key); // 回滚
return false;
}
同时使用 Redis 分布式锁防止重复领取:
String lockKey = "coupon:receive:" + userId + ":" + templateId;
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMinutes(1));
| 优惠类型 | 是否可叠加 | 计算顺序 |
|---|---|---|
| 多个营销活动 | 可叠加 | 设备端计算 |
| 优惠券 | 一次仅用1张 | 服务端计算 |
| 营销活动 + 优惠券 | 可叠加 | 先营销活动,后优惠券 |
智能售卖机没有“确认订单”环节,用户关门即结算。系统无法等待用户选择优惠券,因此采用自动匹配策略:优先使用最先到期的符合条件的券。
设计考量:
异常不阻断:优惠券计算失败时返回原金额,不影响正常订单流程
HahaCallbackServiceImpl.processCouponDiscount()
↓
1. 查询用户所有可用优惠券(status=0 且未过期)
↓
2. 按到期时间升序排序(优先使用最先到期的券)
↓
3. 遍历优惠券,检查适用条件:
- 订单金额 >= 优惠券满减门槛(minAmount)
- 适用范围匹配(applyScope)
↓
4. 使用第一张符合条件的优惠券
↓
5. 计算优惠金额(根据 couponType)
↓
6. 确保优惠金额 <= 订单金额(paidAmount >= 0)
↓
7. 标记优惠券为已使用(useCoupon)
↓
8. 更新订单 discountAmount / paidAmount
↓
9. 返回最终实付金额(用于支付扣费)
1. 验证优惠券归属(userId 匹配)
2. 检查状态(must be UNUSED)
3. 检查有效期(validEndTime)
4. 更新状态为 USED,写入 orderId / useTime / discountAmount
最终实付 = totalAmount - discountAmount
discountAmount = 营销活动优惠(设备端已扣减,不可见) + 优惠券优惠(服务端扣减)
注意:设备端传来的 totalAmount 已包含营销活动优惠
服务端 discountAmount 仅记录优惠券优惠部分
营销活动优惠金额不单独持久化到订单表
营销活动优惠是动态计算的:同一个活动在不同订单中的优惠金额不同(如折扣率取决于订单金额),且活动配置可能随时修改。将计算结果持久化到订单表既不必要也不合理——订单创建时优惠已确定,后续活动修改不影响已有订单。
如果将来需要统计营销活动优惠总额,可通过 MarketingRuleService.calculateOrderDiscount() 重新计算或从统计表 t_marketing_statistics 获取。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 雪花算法主键 |
| activityName / activityDesc | String | 活动名称/描述 |
| startTime / endTime | LocalDateTime | 活动有效期 |
| status | Integer | 状态:0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束 |
| couponTemplateId | Long | 邀请人奖励优惠券模板ID |
| inviteeCouponTemplateId | Long | 被邀请人奖励优惠券模板ID(双向奖励) |
| dailyLimit | Integer | 每人每日最大邀请数 |
| totalLimit | Integer | 每人活动期间最大邀请总数 |
| requireFirstOrder | Integer | 是否要求完成首单:0-否 1-是 |
| minOrderAmount | BigDecimal | 首单最低金额要求 |
| inviteCodePrefix | String | 邀请码前缀 |
| applyScope / productScope | Integer | 适用范围 |
| 关键字段 | 说明 |
|---|---|
| inviterUserId | 邀请人 |
| inviteeUserId | 被邀请人 |
| inviteCode | 邀请码 |
| status | 0-待激活 1-已激活 2-已失效 3-已奖励 |
| 关键字段 | 说明 |
|---|---|
| rewardType | 0-邀请人奖励 1-被邀请人奖励 |
| triggerType | 0-首单完成触发 1-邀请绑定触发 |
| couponTemplateId | 奖励的优惠券模板 |
| userCouponId | 发放的用户优惠券ID |
| status | 0-待发放 1-已发放 2-发放失败 |
订单完成时(payStatus = PAID),InviteActivityServiceImpl 检查被邀请人是否有待激活的邀请记录,满足条件后发放奖励优惠券。
| 文件 | 职责 |
|---|---|
haha-entity/.../MarketingActivity.java |
营销活动实体 |
haha-entity/.../TimedDiscountActivity.java |
定时折扣活动实体 |
haha-entity/.../InviteActivity.java |
邀请有礼活动实体 |
haha-entity/.../CouponTemplate.java |
优惠券模板实体 |
haha-entity/.../UserCoupon.java |
用户优惠券实体 |
haha-service/.../MarketingRuleServiceImpl.java |
营销规则计算(首单/折扣/满减) |
haha-service/.../MarketingActivityServiceImpl.java |
营销活动CRUD |
haha-service/.../UserCouponServiceImpl.java |
优惠券领取/使用/查询 |
haha-service/.../CouponTemplateServiceImpl.java |
优惠券模板管理 |
haha-service/.../TimedDiscountServiceImpl.java |
定时折扣管理 |
haha-service/.../InviteActivityServiceImpl.java |
邀请有礼管理 |
haha-service/.../UnifiedActivityServiceImpl.java |
统一活动管理 |
haha-service/.../HahaCallbackServiceImpl.java |
订单回调中优惠券自动扣减 |
haha-admin/.../MarketingActivityController.java |
运营平台-营销活动API |
haha-admin/.../CouponController.java |
运营平台-优惠券管理API |
haha-admin/.../TimedDiscountController.java |
运营平台-定时折扣API |
haha-admin/.../InviteActivityAdminController.java |
运营平台-邀请有礼API |
haha-admin/.../UnifiedActivityController.java |
运营平台-统一活动API |
haha-miniapp/.../AppCouponController.java |
小程序-优惠券API |
haha-miniapp/.../InviteController.java |
小程序-邀请API |
haha-common/.../enums/ActivityTypeEnum.java |
活动类型枚举 |
haha-common/.../enums/ActivityStatusEnum.java |
活动状态枚举 |
haha-common/.../enums/CouponTypeEnum.java |
优惠券类型枚举 |
haha-common/.../enums/CouponStatusEnum.java |
优惠券状态枚举 |
haha-common/.../enums/DiscountTypeEnum.java |
折扣类型枚举 |