# 优惠券模块后端改造方案

> 前端 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<Long> 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<Long> userIds = mapper.readValue(coupon.getReceiveUserIds(),
    new TypeReference<List<Long>>() {});
```

---

## 五、前端-后端字段对照表

| 前端字段 (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 并验证完整流程后再上生产**
