# 优惠券模块后端改造方案 > 前端 Vue 改造已完成(paopao-vue),本文档为后端 Java 端对应的改动清单。 --- ## 一、DDL — shop_coupon 表新增字段 ```sql -- ============================================================ -- 优惠券模块升级:新增发放对象控制 + 场地使用券支持 -- 执行前请先备份表数据! -- ============================================================ ALTER TABLE `shop_coupon` -- 发放对象(0全部用户 1仅会员 2仅非会员 3指定用户) ADD COLUMN `receive_target` TINYINT NOT NULL DEFAULT 0 COMMENT '发放对象(0全部用户 1仅会员 2仅非会员 3指定用户)' AFTER `enabled`, -- 指定用户ID列表(JSON数组),receive_target=3 时使用 ADD COLUMN `receive_user_ids` VARCHAR(1000) DEFAULT NULL COMMENT '指定用户ID列表(JSON数组格式),receiveTarget=3时使用' AFTER `receive_target`, -- 场地使用券相关字段 (type=50 时使用) ADD COLUMN `venue_type` TINYINT DEFAULT NULL COMMENT '场地使用券-场地类型' AFTER `receive_user_ids`, ADD COLUMN `venue_id` INT DEFAULT NULL COMMENT '场地使用券-指定场地ID' AFTER `venue_type`, ADD COLUMN `use_count` INT NOT NULL DEFAULT -1 COMMENT '场地使用券-可用次数(-1无限制)' AFTER `venue_id`, ADD COLUMN `use_duration` INT DEFAULT NULL COMMENT '场地使用券-使用时长(分钟)' AFTER `use_count`; ``` ### 字段说明 | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `receive_target` | TINYINT | `0` | 0=全部, 1=仅会员, 2=仅非会员, 3=指定用户 | | `receive_user_ids` | VARCHAR(1000) | NULL | JSON 数组如 `[101,202,303]`, receiveTarget=3 时有值 | | `venue_type` | TINYINT | NULL | 场地类型,type=50 时填写 | | `venue_id` | INT | NULL | 具体场地 ID,type=50 时填写 | | `use_count` | INT | `-1` | 可用次数,-1=不限制 | | `use_duration` | INT | NULL | 使用时长(分钟) | --- ## 二、Java Entity — ShopCoupon.java 新增字段 在实体类中新增以下字段(与前端 model/index.ts 对齐): ```java // ========== 以下是新增字段 ========== /** * 发放对象(0全部用户 1仅会员 2仅非会员 3指定用户) */ @TableField("receive_target") private Integer receiveTarget; /** * 指定用户ID列表(JSON数组格式),receiveTarget=3时使用 * 存储格式: ["101","202","303"] 或 [101,202,303] */ @TableField("receive_user_ids") private String receiveUserIds; /** * 场地使用券-场地类型 (type=50时使用) */ @TableField("venue_type") private Integer venueType; /** * 场地使用券-指定场地ID (type=50时使用) */ @TableField("venue_id") private Integer venueId; /** * 场地使用券-可用次数(-1表示无限制) */ @TableField("use_count") private Integer useCount; /** * 场地使用券-使用时长(分钟) */ @TableField("use_duration") private Integer useDuration; ``` > 注意:如果项目使用了 Lombok `@Data` 或手动 getter/setter,确保新字段也有对应的访问方法。 --- ## 三、领券/用券校验逻辑 ### 3.1 领券接口校验(用户领取优惠券时) 在领券 Controller 或 Service 中增加校验: ```java /** * 校验用户是否有资格领取该优惠券 * * @param coupon 优惠券信息 * @param userId 当前用户ID * @param userGradeId 用户会员等级ID (0=非会员, >0=会员) * @throws BusinessException 校验不通过时抛出业务异常 */ public void validateCouponReceiveTarget(ShopCoupon coupon, Long userId, Integer userGradeId) { Integer target = coupon.getReceiveTarget(); // target=0 全部用户可领,直接通过 if (target == null || target == 0) { return; } // target=1 仅会员可领 if (target == 1) { if (userGradeId == null || userGradeId <= 0) { throw new BusinessException("该优惠券仅限会员领取"); } return; } // target=2 仅非会员可领 if (target == 2) { if (userGradeId != null && userGradeId > 0) { throw new BusinessException("该优惠券仅限非会员领取"); } return; } // target=3 指定用户可领 if (target == 3) { String receiveUserIds = coupon.getReceiveUserIds(); if (StringUtils.isBlank(receiveUserIds)) { throw new BusinessException("该优惠券未设置指定用户"); } // 解析 JSON 数组 List allowedUserIds = JsonUtils.parseArray(receiveUserIds, Long.class); if (allowedUserIds == null || !allowedUserIds.contains(userId)) { throw new BusinessException("您不在该优惠券的发放范围内"); } return; } // 未知的 target 值,默认放行(向后兼容) } ``` ### 3.2 调用位置建议 ``` 用户点击"领取优惠券" │ ▼ ┌──────────────────────┐ │ 1. 检查优惠券是否启用 │ │ 2. 检查是否已过期 │ │ 3. 检查发放数量是否已完 │ │ 4. 检查每人限领数量 │ ◄── 在此步骤之后、实际发券之前插入 │ ★5. 校验发放对象 ★ │ validateCouponReceiveTarget() │ 6. 创建用户优惠券记录 │ └──────────────────────┘ ``` ### 3.3 用券/下单时的校验(可选增强) 下单抵扣时除了常规的金额/时间/商品范围校验外,可追加: ```java /** * 下单使用优惠券时的额外校验 */ public void validateCouponForOrder(ShopCoupon coupon, ShopUser user, OrderContext ctx) { // ... 已有的金额/有效期/适用范围校验 ... // 场地使用券(type=50)特殊校验 if (coupon.getType() != null && coupon.getType() == 50) { // 验证订单是否包含场地服务 if (!ctx.hasVenueItem()) { throw new BusinessException("该券仅可用于场地预订"); } // 如果指定了场地类型/ID,校验是否匹配 if (coupon.getVenueType() != null && !coupon.getVenueType().equals(ctx.getVenueType())) { throw new BusinessException("场地类型不匹配"); } if (coupon.getVenueId() != null && !coupon.getVenueId().equals(ctx.getVenueId())) { throw new BusinessException("指定场地不匹配"); } } } ``` --- ## 四、JSON 工具方法说明 `receive_user_ids` 字段的读写需要 JSON 序列化/反序列化: **写入(保存优惠券时):** ```java // 前端传来的是 JSON 字符串 "[101,202,303]",直接存即可 coupon.setReceiveUserIds(receiveUserIdsJsonString); ``` **读取(校验时解析):** ```java // 推荐使用 Jackson / Fastjson / Gson (项目已有的 JSON 库) // 示例(Jackson): ObjectMapper mapper = new ObjectMapper(); List userIds = mapper.readValue(coupon.getReceiveUserIds(), new TypeReference>() {}); ``` --- ## 五、前端-后端字段对照表 | 前端字段 (TypeScript) | 后端字段 (Java/DB) | 类型 | |------------------------|--------------------|------| | `receiveTarget` | `receive_target` | Integer/TINYINT | | `receiveUserIds` | `receive_user_ids` | String/VARCHAR | | `venueType` | `venue_type` | Integer/TINYINT | | `venueId` | `venue_id` | Integer/INT | | `useCount` | `use_count` | Integer/INT | | `useDuration` | `use_duration` | Integer/INT | --- ## 六、注意事项 1. **向后兼容**:`receive_target` 默认值 `0` 表示全部用户,不影响已有优惠券的领用行为 2. **JSON 格式一致性**:前端提交时已序列化为 JSON 字符串,后端直接存储;读取时按 JSON 解析 3. **场地使用券 (type=50)** 是独立于普通商品券的类型,下单系统需单独适配其核销逻辑 4. **`gradeId` 判断会员**:依赖 `shop_user.grade_id` 字段,`grade_id > 0` 为会员,`=0` 或 `NULL` 为非会员 5. **建议先在测试环境执行 DDL 并验证完整流程后再上生产**