# 营销与优惠模块业务说明 > 本文档描述营销活动与优惠券体系,涵盖活动类型、优惠券生命周期、优惠计算逻辑及叠加规则。 > 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。 --- ## 一、营销活动体系 ### 1.1 三套活动体系 | 体系 | 核心实体 | 管理入口 | 用途 | |------|---------|---------|------| | 营销活动 | MarketingActivity | `MarketingActivityController` | 首单立减/折扣/满减 | | 定时折扣 | TimedDiscountActivity | `TimedDiscountController` | 按时间段自动执行的折扣 | | 邀请有礼 | InviteActivity | `InviteActivityAdminController` | 邀请奖励活动 | **统一活动管理**:`UnifiedActivityService` / `UnifiedActivityController` 提供三套活动的统一查询和管理入口。 #### 为什么有三套活动体系而非统一一套? 三套体系虽同属"营销",但业务差异很大,强行合并会增加复杂度: - **营销活动**(MarketingActivity):面向设备端计算的即时优惠,有时间范围、预算控制,由运营人员配置 - **定时折扣**(TimedDiscountActivity):按每天时段自动执行(如夜场22:00-06:00打折),有库存关联的阶梯折扣逻辑,可自动执行 - **邀请有礼**(InviteActivity):奖励机制完全不同(发放优惠券给邀请人/被邀请人),有每日限额、首单门槛等专属逻辑 `UnifiedActivityService` 在查询层面做统一聚合,但各体系保持独立的 CRUD 和业务逻辑。 ### 1.2 MarketingActivity 实体 | 字段 | 类型 | 说明 | |------|------|------| | 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 | 逻辑删除 | ### 1.3 活动类型与计算规则 #### 类型1:首单立减 (FIRST_ORDER_DISCOUNT) ``` 条件:用户之前无已完成订单(isFirstOrder 判断) 优惠 = min(discountValue, orderAmount) 结果:可能免单(优惠 >= 订单金额) ``` #### 类型2:优惠折扣 (DISCOUNT) ``` 折扣率 discountValue:0 < discountValue <= 1(如0.8表示8折) 优惠 = orderAmount × (1 - discountValue),保留2位小数 如有 maxDiscount:优惠 = min(计算优惠, maxDiscount) ``` #### 类型3:商品满减 (FULL_REDUCTION) ``` 门槛:orderAmount >= minAmount 满足:优惠 = discountValue(固定减免金额) 不满足:优惠 = 0 ``` ### 1.4 活动状态流转 (`ActivityStatusEnum`) ``` DRAFT(0, 草稿) ↓ publish PUBLISHED(1, 已发布) → 到达开始时间自动 → ONGOING(2, 进行中) ↓ pause ↓ pause PAUSED(3, 已暂停) → resume → ONGOING ↓ 到达结束时间 ↓ ↓ ENDED(4, 已结束) ``` 状态操作权限: - 可编辑:仅 DRAFT - 可发布:仅 DRAFT - 可暂停:仅 ONGOING - 可恢复:仅 PAUSED - 可删除:DRAFT 或 ENDED ### 1.5 关联关系表 | 关联表 | 说明 | |--------|------| | t_activity_shop | 活动-门店关联(applyScope=2时) | | t_activity_device | 活动-设备关联(deviceScope=2时) | | t_activity_product | 活动-商品关联(productScope=2时) | #### 营销活动与优惠券的关系设计 **关系模型**:一个营销活动可以关联多个优惠券模板(1:N),优惠券模板的 `activityId` 字段指向所属活动,但优惠券也可以独立存在(`activityId` 为空)。 **设计考量**: - 活动和优惠券解耦,优惠券可独立发放(如日常运营券),也可归属某个活动(如春节大促券) - 活动详情页可展示关联的优惠券列表,便于运营人员统一管理 - `activityId` 为可选字段,不强制绑定 --- ## 二、定时折扣体系 ### 2.1 TimedDiscountActivity 实体 | 字段 | 类型 | 说明 | |------|------|------| | 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 | 执行时是否通知 | ### 2.2 折扣类型 (`DiscountTypeEnum`) | code | 枚举值 | 说明 | |------|--------|------| | 1 | FIXED_DISCOUNT | 固定折扣(如统一8折) | | 2 | TIERED_DISCOUNT | 阶梯折扣(库存越少折扣越大) | | 3 | FIXED_PRICE | 固定价格(如夜场统一3元) | #### 定时折扣的业务场景 定时折扣主要解决**临期/夜间清库存**问题: - **夜场折扣**:22:00-06:00 折扣销售,提高夜间销量 - **临期清仓**:库存低于阈值时自动加大折扣,减少损耗 - **固定价格**:特定时段统一定价(如夜场饮品一律3元),简化计价 **阶梯折扣**是定时折扣独有的机制:根据当前库存量动态调整折扣力度(库存越少折扣越大),由 `minStock` / `maxStock` 字段定义库存区间。 ### 2.3 关联表 | 关联表 | 说明 | |--------|------| | t_timed_discount_device | 折扣-设备关联 | | t_timed_discount_shop | 折扣-门店关联 | | t_timed_discount_product | 折扣-商品关联 | | t_timed_discount_record | 折扣执行记录 | | t_timed_discount_statistics | 折扣统计 | --- ## 三、优惠券体系 ### 3.1 CouponTemplate 实体(优惠券模板) | 字段 | 类型 | 说明 | |------|------|------| | 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-系统发放 | ### 3.2 优惠券类型与计算 (`CouponTypeEnum`) | couponType | 名称 | discountValue含义 | 计算方式 | |-----------|------|-------------------|---------| | 1 | 满减券 | 减免金额(元) | 订单金额 >= minAmount → 优惠 = discountValue | | 2 | 折扣券 | 折扣率(如0.8=8折) | 优惠 = orderAmount × (1 - discountValue/10),有 maxDiscount 上限 | | 3 | 立减券 | 抵扣金额(元) | 优惠 = discountValue(无门槛) | | 4 | 商品券 | 无意义 | 免费兑换指定商品,不用于金额扣减 | ### 3.3 UserCoupon 实体(用户优惠券) | 字段 | 类型 | 说明 | |------|------|------| | 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 | 实际优惠金额(使用时记录) | ### 3.4 优惠券状态 (`CouponStatusEnum`) | code | 枚举值 | 说明 | |------|--------|------| | 0 | UNUSED | 未使用 | | 1 | USED | 已使用 | | 2 | EXPIRED | 已过期 | ### 3.5 关联表 | 关联表 | 说明 | |--------|------| | t_coupon_shop | 优惠券-门店关联 | | t_coupon_product | 优惠券-商品关联 | | t_coupon_distribute | 优惠券发放记录 | #### 优惠券发放类型 (`receiveType`) | 类型 | 说明 | |------|------| | Collect | 主动领取:用户在小程序“领券中心”点击领取 | | Release | 系统发放:系统自动发放(如邀请奖励、新人礼包) | #### 优惠券库存防超卖 高并发领取场景下使用 Redis 原子操作扣减库存: ```java // Redis原子扣减 String key = "coupon:stock:" + templateId; Long remain = redisTemplate.opsForValue().decrement(key); if (remain < 0) { redisTemplate.opsForValue().increment(key); // 回滚 return false; } ``` 同时使用 Redis 分布式锁防止重复领取: ```java String lockKey = "coupon:receive:" + userId + ":" + templateId; Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMinutes(1)); ``` --- ## 四、优惠计算与叠加规则 ### 4.1 优惠叠加规则 | 优惠类型 | 是否可叠加 | 计算顺序 | |---------|-----------|---------| | 多个营销活动 | 可叠加 | 设备端计算 | | 优惠券 | 一次仅用1张 | 服务端计算 | | 营销活动 + 优惠券 | 可叠加 | 先营销活动,后优惠券 | ### 4.2 优惠券自动扣减流程(订单回调时触发) #### 为什么是自动扣减? 智能售卖机没有“确认订单”环节,用户关门即结算。系统无法等待用户选择优惠券,因此采用自动匹配策略:优先使用最先到期的符合条件的券。 **设计考量**: - 优先到期策略避免优惠券过期浪费,符合用户利益 - 一次仅用1张优惠券,避免复杂叠加计算 - 异常不阻断:优惠券计算失败时返回原金额,不影响正常订单流程 ``` HahaCallbackServiceImpl.processCouponDiscount() ↓ 1. 查询用户所有可用优惠券(status=0 且未过期) ↓ 2. 按到期时间升序排序(优先使用最先到期的券) ↓ 3. 遍历优惠券,检查适用条件: - 订单金额 >= 优惠券满减门槛(minAmount) - 适用范围匹配(applyScope) ↓ 4. 使用第一张符合条件的优惠券 ↓ 5. 计算优惠金额(根据 couponType) ↓ 6. 确保优惠金额 <= 订单金额(paidAmount >= 0) ↓ 7. 标记优惠券为已使用(useCoupon) ↓ 8. 更新订单 discountAmount / paidAmount ↓ 9. 返回最终实付金额(用于支付扣费) ``` ### 4.3 优惠券使用校验(UserCouponServiceImpl.useCoupon) ``` 1. 验证优惠券归属(userId 匹配) 2. 检查状态(must be UNUSED) 3. 检查有效期(validEndTime) 4. 更新状态为 USED,写入 orderId / useTime / discountAmount ``` ### 4.4 优惠金额公式 ``` 最终实付 = totalAmount - discountAmount discountAmount = 营销活动优惠(设备端已扣减,不可见) + 优惠券优惠(服务端扣减) 注意:设备端传来的 totalAmount 已包含营销活动优惠 服务端 discountAmount 仅记录优惠券优惠部分 营销活动优惠金额不单独持久化到订单表 ``` #### 为什么营销活动优惠金额不持久化到订单表? 营销活动优惠是**动态计算**的:同一个活动在不同订单中的优惠金额不同(如折扣率取决于订单金额),且活动配置可能随时修改。将计算结果持久化到订单表既不必要也不合理——订单创建时优惠已确定,后续活动修改不影响已有订单。 如果将来需要统计营销活动优惠总额,可通过 `MarketingRuleService.calculateOrderDiscount()` 重新计算或从统计表 `t_marketing_statistics` 获取。 --- ## 五、邀请有礼体系 ### 5.1 InviteActivity 实体 | 字段 | 类型 | 说明 | |------|------|------| | 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 | 适用范围 | ### 5.2 邀请记录 (InviteRecord) | 关键字段 | 说明 | |---------|------| | inviterUserId | 邀请人 | | inviteeUserId | 被邀请人 | | inviteCode | 邀请码 | | status | 0-待激活 1-已激活 2-已失效 3-已奖励 | ### 5.3 邀请奖励 (InviteReward) | 关键字段 | 说明 | |---------|------| | rewardType | 0-邀请人奖励 1-被邀请人奖励 | | triggerType | 0-首单完成触发 1-邀请绑定触发 | | couponTemplateId | 奖励的优惠券模板 | | userCouponId | 发放的用户优惠券ID | | status | 0-待发放 1-已发放 2-发放失败 | ### 5.4 邀请激活触发 订单完成时(`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` | 折扣类型枚举 |