# 农资优惠券

> 小程序端领券中心 / 券包 / 下单用券，运营后台模板 / 投放活动 / 券包与核销明细；含鉴权约定、枚举、状态流转与请求响应示例

- 类型：前端对接
- 分类：农资商城
- 端：农资小程序 / 运营后台
- 后端服务：supplier / admin
- 更新：2026-09-27
- 网页版：https://doc.xfun.fun/peasant-coupon-api

两端接口分开列出：先「小程序端」（supplier 服务，供应商登录态），后「运营后台」（admin 服务）。2026-09-27 更新：领券中心 tab、领取记录、sourceDesc、定向发放重试口径。

## 小程序端 · 通用约定

共 9 个接口。

- 路径前缀：`/peasant/coupon`
- 鉴权：与其他 /peasant/* 接口一致：请求头 Authorization 携带登录 JWT（payload 含 supplierId），服务端解析并校验 Redis 在线态得到 supplierId。未登录/过期返回 code=9 msg=当前未登录。
- `code`：int，0 成功，非 0 失败（1 业务失败、2 参数校验失败、9 未登录、999 系统错误）
- `msg`：string，成功固定 success，失败为可直接展示的中文文案
- `data`：业务数据；为 null 时序列化为空字符串 ""，失败响应 data 恒为 ""
- `serialization`：LocalDateTime 序列化为 "yyyy-MM-dd HH:mm:ss"；BigDecimal 序列化为字符串（如 "10.00"）；分页字段 records/total/size/current/pages

### 枚举

| 枚举 | 取值 |
|----|----|
| 券实例状态 status | 10 可使用 · 20 已锁定（被待支付订单占用） · 30 已使用 · 40 已过期 · 50 已作废（运营作废） |
| 领券中心 tab | 1 可领取（默认） · 2 即将开始（开始时间在未来） |
| 我的券包 tab | 1 可使用（status 10/20 且未过期，按 validEnd 升序） · 2 已使用（status 30，按更新时间倒序） · 3 已失效（status 40/50，以及 status 10 但已过期，按更新时间倒序） |
| 优惠类型 discountType | 1 满减券（需 thresholdPrice 门槛） · 2 无门槛券 |
| 适用商品 productScopeType | 0 全场通用 · 1 指定商品（只对范围内商品金额计算门槛与优惠） |
| 获取方式 sourceType | 1 自主领取（sourceDesc=自主领取） · 2 定向发放（sourceDesc=平台发放） · 3 积分兑换（sourceDesc=积分兑换，积分商城上线后出现） |
| 有效期类型 validType（影响 validDesc 文案） | 1 固定有效期："2026.09.18 00:00 至 2026.10.18 23:59" · 2 领取后生效："领取后N天内有效"，validStart=领取时刻，validEnd=领取时刻+N天 |

## 小程序端 · 领券中心

### POST /peasant/coupon/center/list · 领券中心列表（09-23 新增 tab）

按 tab 返回当前供应商在投放范围内的自主领取活动，不分页。请求体可不传或传 {}，等同 tab=1。tab=1 可领取：活动进行中且当前时间在活动起止内、模板进行中且未结束，按开始时间倒序。tab=2 即将开始：活动待生效或进行中但开始时间在未来、活动与模板都未到结束时间，按开始时间升序；此时 claimedCount 恒为 0，remainCount 为预计可领张数，领取按钮应禁用并用 startTime 展示“x月x日 开始”。公共前置：供应商审核通过且类型为种植户/供应商/包花厂，否则返回空数组；供应商需在活动投放范围及模板适用范围内。remainCount = min(活动剩余, 模板剩余, 活动每家限领-已领, 模板每家限领-已领)，为 0 时展示“已领完/已达上限”并禁用领取按钮。注意：活动点过“立即开始”后 startTime 会被改成实际开始时刻。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `tab` | int | 否 | 1-可领取（默认） 2-即将开始 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `activityId` | long | 投放活动 id，领取时传此值 |
| `activityNo` | string | 活动编号 |
| `activityName` | string | 活动名称 |
| `templateId` | long | 券模板 id |
| `couponName` | string | 券名称 |
| `discountType` | int | 1-满减 2-无门槛 |
| `thresholdPrice` | string(BigDecimal) | 满减门槛金额，无门槛券为 0 |
| `discountPrice` | string(BigDecimal) | 优惠金额 |
| `productScopeType` | int | 0-全场 1-指定商品 |
| `ruleDesc` | string | 规则文案，如“满100减10”/“无门槛减5” |
| `validDesc` | string | 有效期文案 |
| `usageDesc` | string | 使用说明 |
| `startTime` | datetime | 活动开始时间 |
| `endTime` | datetime | 活动结束时间（领取截止） |
| `perSupplierLimit` | int | 活动每家限领张数，null 或 0 表示不限 |
| `claimedCount` | int | 我在本活动已领取张数 |
| `remainCount` | int | 我还可领取张数（>=0） |

#### 可能的失败 msg

- 当前未登录

#### 请求示例

```json
{
  "tab": 1
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": [
    {
      "activityId": 12,
      "activityNo": "ACT20260918001",
      "activityName": "九月农资满减",
      "templateId": 5,
      "couponName": "满100减10",
      "discountType": 1,
      "thresholdPrice": "100.00",
      "discountPrice": "10.00",
      "productScopeType": 0,
      "ruleDesc": "满100减10",
      "validDesc": "领取后30天内有效",
      "usageDesc": "仅限农资商城下单使用，每单限用一张",
      "startTime": "2026-09-18 00:00:00",
      "endTime": "2026-09-30 23:59:59",
      "perSupplierLimit": 2,
      "claimedCount": 1,
      "remainCount": 1
    }
  ]
}
```

### POST /peasant/coupon/claim · 领取优惠券

对某个自主领取活动领 1 张券，成功返回新券码。服务端按 supplierId 做 1 秒防重。校验顺序：账号可领 → 活动存在且为自主领取 → 活动进行中且在时间窗内 → 模板进行中 → 供应商在投放范围 → 事务内加锁校验每家上限与库存并落库。成功后刷新领券中心和券包。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `activityId` | long | 是 | 领券中心返回的 activityId |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | string | 新券码，格式 SIM + 8位模板id + 8位序号 |

#### 可能的失败 msg

- 活动id不能为空
- 操作频繁，请稍后重试
- 当前账号不可领取
- 活动不存在
- 活动未开始或已结束
- 优惠券已结束
- 不在活动投放范围内
- 已达领取上限
- 优惠券库存不足
- 当前未登录

#### 请求示例

```json
{
  "activityId": 12
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": "SIM0000000500000012"
}
```

## 小程序端 · 我的券包

### POST /peasant/coupon/my/list · 我的券包列表

按 tab 分页查询当前供应商的券。tab=1 可使用：status 10/20 且未过期，按 validEnd 升序（快过期在前），status=20 展示“订单占用中”并禁止选用；tab=2 已使用；tab=3 已失效（含 status 10 但已过期、定时任务尚未扫到的券）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `tab` | int | 否 | 1-可使用 2-已使用 3-已失效，默认 1 |
| `current` | long | 否 | 页码，默认 1 |
| `size` | long | 否 | 每页条数，默认 20 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `records` | array<MyCouponDto> | 券列表 |
| `records.id` | long | 券实例 id，下单时作为 couponInstanceId 传入 |
| `records.couponCode` | string | 券码 |
| `records.templateId` | long | 券模板 id |
| `records.couponName` | string | 券名称（领取时快照） |
| `records.discountType` | int | 1-满减 2-无门槛 |
| `records.thresholdPrice` | string(BigDecimal) | 满减门槛 |
| `records.discountPrice` | string(BigDecimal) | 优惠金额 |
| `records.productScopeType` | int | 0-全场 1-指定商品 |
| `records.sourceType` | int | 1-自主领取 2-定向发放 3-积分兑换 |
| `records.sourceDesc` | string | 来源文案：自主领取 / 平台发放 / 积分兑换（09-27 新增） |
| `records.status` | int | 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 |
| `records.validStart` | datetime | 生效时间 |
| `records.validEnd` | datetime | 失效时间 |
| `records.orderId` | string | 锁定/核销的订单号，未使用为空字符串 |
| `records.useTime` | datetime | 核销时间，未使用为 null |
| `records.createTime` | datetime | 领取/到账时间 |
| `records.ruleDesc` | string | 规则文案 |
| `records.validDesc` | string | 有效期文案 |
| `records.usageDesc` | string | 使用说明，无则空字符串 |
| `total` | long | 总条数 |
| `size` | long | 每页条数 |
| `current` | long | 当前页 |
| `pages` | long | 总页数 |

#### 可能的失败 msg

- 当前未登录

#### 请求示例

```json
{
  "tab": 1,
  "current": 1,
  "size": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "records": [
      {
        "id": 1001,
        "couponCode": "SIM0000000500000012",
        "templateId": 5,
        "couponName": "满100减10",
        "discountType": 1,
        "thresholdPrice": "100.00",
        "discountPrice": "10.00",
        "productScopeType": 0,
        "sourceType": 1,
        "sourceDesc": "自主领取",
        "status": 10,
        "validStart": "2026-09-18 10:05:32",
        "validEnd": "2026-10-18 10:05:32",
        "orderId": "",
        "useTime": null,
        "createTime": "2026-09-18 10:05:32",
        "ruleDesc": "满100减10",
        "validDesc": "2026.09.18 10:05 至 2026.10.18 10:05",
        "usageDesc": "仅限农资商城下单使用，每单限用一张"
      }
    ],
    "total": 1,
    "size": 20,
    "current": 1,
    "pages": 1
  }
}
```

### POST /peasant/coupon/my/records · 领取记录（09-27 新增）

“我的优惠券 - 领取记录”页。分页返回当前供应商领到的全部券，不分状态（含已使用/已过期/已作废），按到账时间 createTime 倒序。data.total 即“共领取 N 张优惠券”。每行展示 couponName、sourceDesc、createTime；右侧状态文案由前端按 sourceType 决定：2 显示“发放成功”，1 和 3 显示“领取成功”。请求体可不传，默认第 1 页 20 条；tab 字段在本接口无效。返回结构与“我的券包列表”完全一致。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `current` | long | 否 | 页码，默认 1 |
| `size` | long | 否 | 每页条数，默认 20 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `records` | array<MyCouponDto> | 券列表 |
| `records.id` | long | 券实例 id，下单时作为 couponInstanceId 传入 |
| `records.couponCode` | string | 券码 |
| `records.templateId` | long | 券模板 id |
| `records.couponName` | string | 券名称（领取时快照） |
| `records.discountType` | int | 1-满减 2-无门槛 |
| `records.thresholdPrice` | string(BigDecimal) | 满减门槛 |
| `records.discountPrice` | string(BigDecimal) | 优惠金额 |
| `records.productScopeType` | int | 0-全场 1-指定商品 |
| `records.sourceType` | int | 1-自主领取 2-定向发放 3-积分兑换 |
| `records.sourceDesc` | string | 来源文案：自主领取 / 平台发放 / 积分兑换（09-27 新增） |
| `records.status` | int | 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 |
| `records.validStart` | datetime | 生效时间 |
| `records.validEnd` | datetime | 失效时间 |
| `records.orderId` | string | 锁定/核销的订单号，未使用为空字符串 |
| `records.useTime` | datetime | 核销时间，未使用为 null |
| `records.createTime` | datetime | 领取/到账时间 |
| `records.ruleDesc` | string | 规则文案 |
| `records.validDesc` | string | 有效期文案 |
| `records.usageDesc` | string | 使用说明，无则空字符串 |
| `total` | long | 总条数 |
| `size` | long | 每页条数 |
| `current` | long | 当前页 |
| `pages` | long | 总页数 |

#### 可能的失败 msg

- 当前未登录

#### 请求示例

```json
{
  "current": 1,
  "size": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "records": [
      {
        "id": 4305,
        "couponCode": "SIM0000000600000001",
        "templateId": 6,
        "couponName": "农资采购满减券",
        "discountType": 1,
        "thresholdPrice": "200.00",
        "discountPrice": "10.00",
        "productScopeType": 0,
        "sourceType": 1,
        "sourceDesc": "自主领取",
        "status": 10,
        "validStart": "2026-09-23 17:02:29",
        "validEnd": "2026-10-23 17:02:29",
        "orderId": "",
        "useTime": null,
        "createTime": "2026-09-23 17:02:29",
        "ruleDesc": "满200减10",
        "validDesc": "2026.09.23 17:02 至 2026.10.23 17:02",
        "usageDesc": "全场农资商品可用，每单限用一张"
      },
      {
        "id": 1,
        "couponCode": "SIM0000000200000001",
        "templateId": 2,
        "couponName": "测试模板2",
        "discountType": 1,
        "thresholdPrice": "100.00",
        "discountPrice": "10.00",
        "productScopeType": 1,
        "sourceType": 2,
        "sourceDesc": "平台发放",
        "status": 10,
        "validStart": "2026-09-21 08:27:58",
        "validEnd": "2026-09-30 08:28:22",
        "orderId": "",
        "useTime": null,
        "createTime": "2026-09-23 11:13:00",
        "ruleDesc": "满100减10",
        "validDesc": "2026.09.21 08:27 至 2026.09.30 08:28",
        "usageDesc": ""
      }
    ],
    "total": 2,
    "size": 20,
    "current": 1,
    "pages": 1
  }
}
```

### POST /peasant/coupon/my/count · 可使用张数

返回当前供应商 status=10（不含已锁定）且未过期的券数量，用于“我的”页角标。无请求体。

#### 请求参数

无

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | long | 可使用张数 |

#### 可能的失败 msg

- 当前未登录

#### 请求示例

```json
{}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": 3
}
```

## 小程序端 · 下单用券

### POST /peasant/coupon/usable · 下单页可用券

确认订单页调用。购物车下单传 cartIds，商品直接下单传 skuIdPriceList（只用 skuId/buyCount）；cartIds 优先。服务端用 sku 当前价（斗南基地用斗南价）×数量算商品金额，对所有可使用券逐张计算：指定商品券只累计范围内商品金额；满减券需适用金额 >= 门槛；优惠上限为 商品总额-0.01（实付最低 0.01 元）。返回 usable/unusable 两组，各按 discount 降序（可默认选中 usable[0]）。无可用券时直接返回两个空数组（此时不校验商品参数）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `cartIds` | array<long> | 否 | 购物车下单：购物车行 id 列表 |
| `skuIdPriceList` | array<object> | 否 | 商品直接下单：sku 与数量 |
| `skuIdPriceList.skuId` | long | 是 | sku id |
| `skuIdPriceList.buyCount` | int | 是 | 购买数量 |
| `skuIdPriceList.price` | string(BigDecimal) | 否 | 本接口忽略，可不传 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `usable` | array<UsableCouponDto> | 本单可用券，按 discount 降序 |
| `usable.…MyCouponDto 全部字段` | - | 同我的券包 records 元素 |
| `usable.discount` | string(BigDecimal) | 本单可优惠金额 |
| `usable.reason` | string | 可用时为空字符串 |
| `unusable` | array<UsableCouponDto> | 本单不可用券 |
| `unusable.…MyCouponDto 全部字段` | - | 同上 |
| `unusable.discount` | string(BigDecimal) | 固定 0 |
| `unusable.reason` | string | 不可用原因：无适用商品 / 未满100元 / 订单金额过低 / 优惠券未到生效时间 / 优惠券已过期 / 优惠券不可用 |

#### 可能的失败 msg

- 请选择商品
- 供应商不存在
- 当前未登录

#### 请求示例

```json
{
  "cartIds": [
    8801,
    8802
  ]
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "usable": [
      {
        "id": 1001,
        "couponCode": "SIM0000000500000012",
        "couponName": "满100减10",
        "discountType": 1,
        "thresholdPrice": "100.00",
        "discountPrice": "10.00",
        "productScopeType": 0,
        "status": 10,
        "validStart": "2026-09-18 10:05:32",
        "validEnd": "2026-10-18 10:05:32",
        "ruleDesc": "满100减10",
        "validDesc": "2026.09.18 10:05 至 2026.10.18 10:05",
        "usageDesc": "仅限农资商城下单使用，每单限用一张",
        "discount": "10.00",
        "reason": ""
      }
    ],
    "unusable": [
      {
        "id": 1002,
        "couponCode": "SIM0000000700000003",
        "couponName": "指定肥料满300减50",
        "discountType": 1,
        "thresholdPrice": "300.00",
        "discountPrice": "50.00",
        "productScopeType": 1,
        "status": 10,
        "validStart": "2026-09-01 00:00:00",
        "validEnd": "2026-09-30 23:59:59",
        "ruleDesc": "满300减50",
        "validDesc": "2026.09.01 00:00 至 2026.09.30 23:59",
        "usageDesc": "",
        "discount": "0",
        "reason": "未满300元"
      }
    ]
  }
}
```

### POST /peasant/order/cart/create · 购物车创建订单（新增 couponInstanceId）

已有接口，公共下单参数新增 couponInstanceId。price 仍传商品合计（券前金额），服务端先校验 price==商品合计，再按 couponInstanceId 重新计算优惠并锁券(10→20)，实付 = 商品合计 - 优惠；订单 orderPrice=商品合计，totalPrice=券后实付。券不可用时整单失败。待支付取消/超时关单自动释放券；支付成功后券变已使用。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `couponInstanceId` | long | 否 | 新增：使用的券实例 id（可用券接口 usable[].id），不用券不传 |
| `addressId` | long | 是 | 收货地址 id（原有） |
| `price` | string(BigDecimal) | 是 | 商品合计金额（券前，原有口径不变） |
| `payType` | int | 是 | 1-余额支付 2-微信支付（原有） |
| `buyerMemo` | string | 否 | 买家留言（原有） |
| `cartIds 等` | - | 是 | 原有字段不变 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | string | 订单号（原有） |

#### 可能的失败 msg

- 价格有误
- 优惠券不可用：优惠券不存在
- 优惠券不可用：优惠券未到生效时间
- 优惠券不可用：优惠券已过期
- 优惠券不可用：无适用商品
- 优惠券不可用：未满100元
- 优惠券不可用：订单金额过低
- 优惠券不可用，请重新选择（券已被其他订单占用）
- 创建订单失败

#### 请求示例

```json
{
  "addressId": 301,
  "price": "120.00",
  "payType": 2,
  "buyerMemo": "",
  "couponInstanceId": 1001
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": "NZ20260918100532123456"
}
```

### POST /peasant/order/product/create · 商品直接创建订单（新增 couponInstanceId）

已有接口，用券规则与购物车下单完全一致：price 传商品合计（券前），skuIdPriceList 每项 price 需与 sku 当前价一致，couponInstanceId 传所选券实例 id。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `couponInstanceId` | long | 否 | 新增：使用的券实例 id，不用券不传 |
| `addressId` | long | 是 | 收货地址 id |
| `price` | string(BigDecimal) | 是 | 商品合计金额（券前） |
| `payType` | int | 是 | 1-余额 2-微信 |
| `buyerMemo` | string | 否 | 买家留言 |
| `skuIdPriceList` | array<object> | 是 | 商品列表 |
| `skuIdPriceList.skuId` | long | 是 | sku id |
| `skuIdPriceList.price` | string(BigDecimal) | 是 | 单价，需与当前售价一致 |
| `skuIdPriceList.buyCount` | int | 是 | 数量 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | string | 订单号 |

#### 可能的失败 msg

- 价格有误
- 优惠券不可用：xxx（同购物车下单）
- 优惠券不可用，请重新选择
- 创建订单失败

#### 请求示例

```json
{
  "addressId": 301,
  "price": "120.00",
  "payType": 1,
  "buyerMemo": "尽快发货",
  "skuIdPriceList": [
    {
      "skuId": 5001,
      "price": "60.00",
      "buyCount": 2
    }
  ],
  "couponInstanceId": 1001
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": "NZ20260918100532123457"
}
```

### POST /peasant/order/list 、 /peasant/order/detail · 订单列表 / 订单详情（新增券字段）

已有接口，列表元素与详情均新增 4 个券字段。展示口径：orderPrice=商品合计，couponPrice=券优惠（平台承担），totalPrice=实付（=orderPrice-couponPrice）。退款金额为商品口径，用券订单买家实际到账按比例扣减券优惠，最后一笔退完时清零剩余实付。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `-` | - | 否 | 请求参数不变 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `couponInstanceId` | long | 新增：券实例 id，0 表示未用券 |
| `couponCode` | string | 新增：券码，未用券为空 |
| `couponName` | string | 新增：券名称快照 |
| `couponPrice` | string(BigDecimal) | 新增：券优惠金额，未用券为 0 |
| `orderPrice` | string(BigDecimal) | 原有：商品合计（券前） |
| `totalPrice` | string(BigDecimal) | 原有：券后实付金额 |

#### 可能的失败 msg

- 当前未登录

#### 请求示例

```json
{
  "orderSn": "NZ20260918100532123456"
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "orderId": "NZ20260918100532123456",
    "orderPrice": "120.00",
    "totalPrice": "110.00",
    "couponInstanceId": 1001,
    "couponCode": "SIM0000000500000012",
    "couponName": "满100减10",
    "couponPrice": "10.00",
    "payType": 2,
    "orderStatus": 1,
    "createTime": "2026-09-18 10:05:32"
  }
}
```

## 运营后台 · 通用约定

共 26 个接口。

- 路径前缀：`/peasantCoupon`（运营后台服务 xm-bulky-admin，三个控制器同挂 @RequestMapping("/peasantCoupon")，仓库内未配置 server.servlet.context-path（Nacos 配置若有前缀由网关/环境决定）；所有接口均为 POST + application/json 请求体，与相邻 /peasantOrder、/peasantProduct 控制器一致）
- 鉴权：所有接口标注 @AuthToken（默认 PermissionEnum.login）：请求头携带 usertokenxm=<登录返回的 JWT>；缺失/失效时 HTTP 仍为 200，响应体 {"code":2,"msg":"登录失效，请重新登录"}（初始密码未改返回 code 19）；菜单权限走 xm_bulky_auth_permission.perm=peasantCoupon，后端拦截器不按 perm 拦接口，仅控制前端菜单展示
- `code`：Integer，0 成功；1 业务失败（msg 为业务文案）；2 登录超时/失效；4 参数校验失败（msg 形如 "name:券名称不能为空,useType:用途不能为空"）；6 权限不足；19 请先修改初始密码
- `msg`：String，成功固定 "success"，失败为可直接展示的中文文案
- `data`：泛型，成功时的数据；操作类接口成功时 data=true；失败时 data=null
- `dateFormat`：LocalDateTime 统一 "yyyy-MM-dd HH:mm:ss"（请求与响应相同）
- `decimalFormat`：BigDecimal 全局序列化为字符串，如 "20.00"；请求可传数字或字符串

成功示例：

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

失败示例：

```json
{
  "code": 1,
  "msg": "仅进行中的模板可以暂停",
  "data": null
}
```

### 分页约定

| 字段 | 说明 |
|----|----|
| 入参 `index` | Integer，页码，不传默认 1 |
| 入参 `size` | Integer，页大小，不传默认 10 |
| 入参 `searchCount` | Boolean，是否查询总数，默认 true |
| 出参 `index` | Integer，当前页 |
| 出参 `size` | Integer，页大小 |
| 出参 `searchCount` | Boolean，是否查了总数 |
| 出参 `total` | Number，总条数 |
| 出参 `records` | Array，当前页记录 |
| 出参 `other` | Object，扩展数据，本组接口恒为 null |

### 菜单

- **农资优惠券**（route `peasantCoupon`）：docs/sql/2026-09-17-peasant-coupon-menu.sql：插入 xm_bulky_auth_permission(pid=“农资商品管理”所在父组的 pid, name='农资优惠券', perm='peasantCoupon', can_index=1)，即与“农资商品管理”同级的一个菜单项；前端页面内自行用 Tab 划分 概览 / 券模板 / 投放活动 / 券包与核销明细 四块，后端未再拆子菜单

### 枚举

| 枚举 | 取值 |
|----|----|
| 模板/活动状态 status | 0 草稿 · 10 待生效 · 20 进行中 · 30 已暂停 · 40 已结束 · 50 已作废 |
| 券实例状态 status（券包） | 10 可使用 · 20 已锁定（下单占用中） · 30 已使用 · 40 已过期 · 50 已作废 |
| 模板用途 useType | 1 通用券 · 2 积分商城专用券 |
| 优惠类型 discountType | 1 满减券 · 2 无门槛券 |
| 适用商品 productScopeType | 0 全场 · 1 指定商品 |
| 适用供应商 supplierScopeType（模板） | 0 全部 · 1 指定基地 · 2 指定供应商 |
| 有效期类型 validType | 1 固定有效期 · 2 领取后生效 |
| 模板 saveMode | 1 保存草稿 · 2 定时生效（startTime 必填；startTime<=当前时间则直接进行中） |
| 投放方式 issueType | 1 自主领取 · 2 定向发放 |
| 投放对象 targetType（活动） | 0 全部供应商 · 1 指定基地 · 2 指定供应商 |
| 活动 saveMode | 1 仅保存（待生效） · 2 保存并启用（保存后立即调用开始） |
| 券获取方式 sourceType（券实例） | 1 自主领取 · 2 定向发放 · 3 积分兑换 |
| 活动最近记录类型 recordType | 1 领取/发放 · 2 核销 |
| 操作日志 bizType | 1 模板 · 2 活动 · 3 券实例 |
| 操作日志 action | 1 创建 · 2 编辑 · 3 排期/立即开始（模板定时生效或立即生效、活动立即开始） · 5 暂停 · 6 恢复 · 7 手工结束 · 8 自动结束（模板结束/作废联动，或定时任务到期） · 9 作废 |
| 操作日志 operatorType | 0 系统 · 1 平台（运营后台） · 2 供应商 |

### 状态流转

| 对象 | 从 | 动作 | 到 | 接口 |
|----|----|----|----|----|
| 模板 | 无 | 新建 saveMode=1 | 草稿 | `POST /peasantCoupon/template/save` |
| 模板 | 无 | 新建 saveMode=2（startTime>当前时间） | 待生效 | `POST /peasantCoupon/template/save` |
| 模板 | 无 | 新建 saveMode=2（startTime<=当前时间） | 进行中 | `POST /peasantCoupon/template/save` |
| 模板 | 草稿 | 编辑 saveMode=2 | 待生效 或 进行中（按 startTime 与当前时间比较） | `POST /peasantCoupon/template/update` |
| 模板 | 草稿 | 编辑 saveMode=1 | 草稿 | `POST /peasantCoupon/template/update` |
| 模板 | 待生效 | 编辑（服务端强制按 saveMode=2 校验；把 startTime 改到过去） | 进行中 | `POST /peasantCoupon/template/update` |
| 模板 | 待生效 | 到达 startTime（supplier 侧定时任务，admin 无接口） | 进行中 | `定时任务` |
| 模板 | 进行中 | 暂停 | 已暂停 | `POST /peasantCoupon/template/pause` |
| 模板 | 已暂停 | 恢复（要求 endTime>当前时间） | 进行中 | `POST /peasantCoupon/template/resume` |
| 模板 | 待生效/进行中/已暂停 | 结束（其下待生效/进行中/已暂停的活动同步自动结束，活动日志 action=8） | 已结束 | `POST /peasantCoupon/template/end` |
| 模板 | 草稿/待生效/进行中/已暂停 | 作废（要求无任何券实例且 issuedCount=0，需 reason；其下活动同步自动结束） | 已作废 | `POST /peasantCoupon/template/void` |
| 模板 | 进行中/已暂停 | 到达 endTime（定时任务） | 已结束 | `定时任务` |
| 活动 | 无 | 新建 saveMode=1 | 待生效 | `POST /peasantCoupon/activity/save` |
| 活动 | 无 | 新建 saveMode=2（保存后立即开始） | 进行中（开始失败则停留待生效并返回失败文案） | `POST /peasantCoupon/activity/save` |
| 活动 | 草稿/待生效 | 编辑（全量字段） | 状态不变 | `POST /peasantCoupon/activity/update` |
| 活动 | 进行中/已暂停 | 编辑（仅名称、结束时间、自主领取的投放总量） | 状态不变 | `POST /peasantCoupon/activity/update` |
| 活动 | 待生效 | 立即开始（模板须进行中且活动 endTime>当前时间；定向发放桥调 supplier 立即发券，失败回到待生效并写 issueFailMsg） | 进行中 | `POST /peasantCoupon/activity/start` |
| 活动 | 待生效 | 到达 startTime（定时任务） | 进行中 | `定时任务` |
| 活动 | 进行中 | 暂停 | 已暂停 | `POST /peasantCoupon/activity/pause` |
| 活动 | 已暂停 | 恢复（要求 endTime>当前时间） | 进行中 | `POST /peasantCoupon/activity/resume` |
| 活动 | 待生效/进行中/已暂停 | 结束 | 已结束 | `POST /peasantCoupon/activity/end` |
| 活动 | 草稿/待生效/进行中/已暂停 | 作废（要求 issuedCount=0，需 reason） | 已作废 | `POST /peasantCoupon/activity/void` |
| 活动 | 待生效/进行中/已暂停 | 所属模板结束或作废（联动） | 已结束 | `由 /template/end、/template/void 触发` |
| 券实例 | 可使用(10) | 人工作废（需 reason） | 已作废(50) | `POST /peasantCoupon/instance/void` |
| 券实例 | 可使用/已锁定/已使用/已过期 | 下单锁定、支付核销、退款解锁、到期过期（均由 supplier 侧完成，admin 只读） | 20/30/10/40 | `无（admin 不提供）` |

### 公共模型

#### CouponScopeItemDto

| 字段 | 类型 | 说明 |
|----|----|----|
| `targetId` | Long | 对象 id（商品 id / 基地 id / 供应商 id） |
| `targetName` | String | 对象名称快照，保存时服务端补齐，最长 64 字 |

#### XmPeasantCouponOpLog

| 字段 | 类型 | 说明 |
|----|----|----|
| `id` | Long | 日志 id |
| `bizType` | Integer | 1-模板 2-活动 3-券实例 |
| `bizId` | Long | 模板/活动/券实例 id |
| `action` | Integer | 动作，见枚举“操作日志 action” |
| `beforeStatus` | Integer | 操作前状态（创建时为 0） |
| `afterStatus` | Integer | 操作后状态 |
| `content` | String | 内容：创建模板 / 编辑差异“名称：旧 → 新；发行总量：100 → 200” / 暂停 / 恢复 / 手工结束 / 作废 / 立即开始 / 定时生效 2026-09-20T10:00 / 模板结束 等，最长 1024 字 |
| `reason` | String | 作废原因，其他动作为空串 |
| `orderId` | String | 券实例锁定/核销相关订单号，运营端操作为空 |
| `operatorType` | Integer | 0-系统 1-平台 2-供应商 |
| `operatorId` | Long | 操作人 id（平台=运营账号 userId） |
| `operatorName` | String | 操作人昵称 |
| `createTime` | String | 操作时间 yyyy-MM-dd HH:mm:ss |

## 运营后台 · 券模板

### POST /peasantCoupon/template/list · 模板列表

分页查询模板（排除已删除 dataStatus!=0 的数据），按 id 倒序。keyword 对券名称/券编号模糊匹配；useType/discountType/status 精确过滤。列表项不含范围明细与日志，instanceCount 为 null

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |
| `searchCount` | Boolean | 否 | 是否查总数，默认 true |
| `keyword` | String | 否 | 券名称/券编号，模糊匹配 |
| `useType` | Integer | 否 | 用途 1-通用券 2-积分商城专用券 |
| `discountType` | Integer | 否 | 优惠类型 1-满减 2-无门槛 |
| `status` | Integer | 否 | 状态 0/10/20/30/40/50 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `index` | Integer | 当前页 |
| `size` | Integer | 页大小 |
| `searchCount` | Boolean | 是否查总数 |
| `total` | Number | 总条数 |
| `records` | Array<PeasantCouponTemplateDto> | 模板列表 |
| `records.id` | Long | 模板 id |
| `records.templateNo` | String | 券编号，NZQ+yyyyMMdd+6 位随机数，如 NZQ20260918123456 |
| `records.name` | String | 券名称 |
| `records.useType` | Integer | 用途 1-通用券 2-积分商城专用券 |
| `records.discountType` | Integer | 1-满减券 2-无门槛券 |
| `records.thresholdPrice` | String(BigDecimal) | 使用门槛 满 X 元，无门槛券为 0 |
| `records.discountPrice` | String(BigDecimal) | 优惠金额 |
| `records.productScopeType` | Integer | 适用商品 0-全场 1-指定商品 |
| `records.supplierScopeType` | Integer | 适用供应商 0-全部 1-指定基地 2-指定供应商 |
| `records.validType` | Integer | 有效期类型 1-固定有效期 2-领取后生效 |
| `records.validStart` | String | 固定有效期开始，validType=2 时为 null |
| `records.validEnd` | String | 固定有效期结束，validType=2 时为 null |
| `records.validDays` | Integer | 领取后有效天数，validType=1 时为 0 |
| `records.totalCount` | Integer | 发行总量 |
| `records.issuedCount` | Integer | 已发放张数（各活动/积分兑换累计） |
| `records.perSupplierLimit` | Integer | 每家供应商上限，0-不限 |
| `records.startTime` | String | 模板生效时间（可领取/发放期起点），草稿可为 null |
| `records.endTime` | String | 模板结束时间，固定有效期时等于 validEnd |
| `records.status` | Integer | 0-草稿 10-待生效 20-进行中 30-已暂停 40-已结束 50-已作废 |
| `records.usageDesc` | String | 使用说明 |
| `records.creatorId` | Long | 创建人 id |
| `records.creatorName` | String | 创建人昵称 |
| `records.dataStatus` | Integer | 0-正常，固定 0 |
| `records.createTime` | String | 创建时间 |
| `records.updateTime` | String | 更新时间 |
| `records.ruleDesc` | String | 优惠规则文案：满减“满100减20”，无门槛“无门槛减10” |
| `records.validDesc` | String | 有效期文案：固定“2026.09.20 00:00 至 2026.10.20 23:59”，领取后“领取后30天内有效” |
| `records.instanceCount` | Long | 已产生券实例数，列表中为 null，详情才返回 |
| `records.productScopes` | Array<CouponScopeItemDto> | 列表中为 null |
| `records.baseScopes` | Array<CouponScopeItemDto> | 列表中为 null |
| `records.supplierScopes` | Array<CouponScopeItemDto> | 列表中为 null |
| `records.logs` | Array<XmPeasantCouponOpLog> | 列表中为 null |
| `other` | Object | 恒为 null |

#### 可能的失败 msg

- 登录失效，请重新登录

#### 请求示例

```json
{
  "index": 1,
  "size": 10,
  "keyword": "秋种",
  "useType": 1,
  "status": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "index": 1,
    "size": 10,
    "searchCount": true,
    "total": 1,
    "records": [
      {
        "id": 12,
        "templateNo": "NZQ20260918305127",
        "name": "秋种满100减20",
        "useType": 1,
        "discountType": 1,
        "thresholdPrice": "100.00",
        "discountPrice": "20.00",
        "productScopeType": 0,
        "supplierScopeType": 1,
        "validType": 1,
        "validStart": "2026-09-20 00:00:00",
        "validEnd": "2026-10-20 23:59:59",
        "validDays": 0,
        "totalCount": 1000,
        "issuedCount": 236,
        "perSupplierLimit": 2,
        "startTime": "2026-09-18 10:00:00",
        "endTime": "2026-10-20 23:59:59",
        "status": 20,
        "usageDesc": "仅限农资商城下单使用，不可叠加",
        "creatorId": 3,
        "creatorName": "沈浩",
        "dataStatus": 0,
        "createTime": "2026-09-18 09:30:12",
        "updateTime": "2026-09-18 10:00:00",
        "ruleDesc": "满100减20",
        "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
        "instanceCount": null,
        "productScopes": null,
        "baseScopes": null,
        "supplierScopes": null,
        "logs": null
      }
    ],
    "other": null
  }
}
```

### POST /peasantCoupon/template/detail · 模板详情

按 id 查模板，附带范围明细（指定商品 productScopes、指定基地 baseScopes、指定供应商 supplierScopes，未选的为空数组）、已产生券实例数 instanceCount、最近 20 条操作日志 logs（id 倒序）。新建/编辑接口成功后也返回本结构

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id，>=1 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `(模板基础字段)` | PeasantCouponTemplateDto | 与 /template/list records 项字段完全相同 |
| `instanceCount` | Long | 已产生券实例数（含各状态），>0 时不可作废 |
| `productScopes` | Array<CouponScopeItemDto> | productScopeType=1 时的指定商品；targetId=农资商品 id，targetName=商品名 |
| `baseScopes` | Array<CouponScopeItemDto> | supplierScopeType=1 时的指定基地；targetId=xm_bulky_base_config.id，targetName=基地名 |
| `supplierScopes` | Array<CouponScopeItemDto> | supplierScopeType=2 时的指定供应商；targetId=供应商 id，targetName=供应商名 |
| `logs` | Array<XmPeasantCouponOpLog> | 最近 20 条操作日志，字段见 commonModels.XmPeasantCouponOpLog；全部日志用 /template/logList |

#### 可能的失败 msg

- 优惠券模板不存在
- id:ID不能为空
- id:ID最小值不能小于1

#### 请求示例

```json
{
  "id": 12
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 12,
    "templateNo": "NZQ20260918305127",
    "name": "秋种满100减20",
    "useType": 1,
    "discountType": 1,
    "thresholdPrice": "100.00",
    "discountPrice": "20.00",
    "productScopeType": 0,
    "supplierScopeType": 1,
    "validType": 1,
    "validStart": "2026-09-20 00:00:00",
    "validEnd": "2026-10-20 23:59:59",
    "validDays": 0,
    "totalCount": 1000,
    "issuedCount": 236,
    "perSupplierLimit": 2,
    "startTime": "2026-09-18 10:00:00",
    "endTime": "2026-10-20 23:59:59",
    "status": 20,
    "usageDesc": "仅限农资商城下单使用，不可叠加",
    "creatorId": 3,
    "creatorName": "沈浩",
    "dataStatus": 0,
    "createTime": "2026-09-18 09:30:12",
    "updateTime": "2026-09-18 10:00:00",
    "ruleDesc": "满100减20",
    "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
    "instanceCount": 236,
    "productScopes": [],
    "baseScopes": [
      {
        "targetId": 5,
        "targetName": "斗南基地"
      },
      {
        "targetId": 8,
        "targetName": "晋宁基地"
      }
    ],
    "supplierScopes": [],
    "logs": [
      {
        "id": 88,
        "bizType": 1,
        "bizId": 12,
        "action": 3,
        "beforeStatus": 0,
        "afterStatus": 20,
        "content": "立即生效",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 10:00:00"
      },
      {
        "id": 87,
        "bizType": 1,
        "bizId": 12,
        "action": 1,
        "beforeStatus": 0,
        "afterStatus": 0,
        "content": "创建模板",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 10:00:00"
      }
    ]
  }
}
```

### POST /peasantCoupon/template/save · 新建模板

创建模板。saveMode=1 保存为草稿（0）；saveMode=2 定时生效：startTime>当前时间进入待生效（10），startTime<=当前时间直接进行中（20）。范围只传 id，名称由服务端补齐。校验顺序：名称非空且<=64字 → useType∈{1,2} → discountType∈{1,2} → discountPrice 0.01~5000 → 满减券 thresholdPrice 1~10000 且 discountPrice<thresholdPrice → productScopeType=1 须传 productIds → supplierScopeType=1 须传 baseIds、=2 须传 supplierIds → validType∈{1,2} → 积分商城专用券(useType=2) 只能 validType=2 → validType=1 时 validStart<validEnd 且 startTime<=validEnd（endTime 自动取 validEnd，validDays 置 0）；validType=2 时 validDays>=1，saveMode=2 必填 endTime，且 startTime<endTime → saveMode=2 必填 startTime 且结束时间>当前时间 → totalCount>=1 → perSupplierLimit>=0 → usageDesc<=500字。无门槛券 thresholdPrice 服务端强制写 0。券编号 NZQ+日期+6 位随机自动生成。成功返回模板详情

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `name` | String | 是 | 券名称，<=64 字 |
| `useType` | Integer | 是 | 1-通用券 2-积分商城专用券（2 只能领取后生效，且不能被活动选用） |
| `discountType` | Integer | 是 | 1-满减券 2-无门槛券 |
| `thresholdPrice` | BigDecimal | 否 | 满 X 元，满减券必填 1~10000；无门槛券传 0 或不传 |
| `discountPrice` | BigDecimal | 是 | 优惠金额 0.01~5000，满减券须小于门槛 |
| `productScopeType` | Integer | 否 | 0-全场（默认） 1-指定商品 |
| `productIds` | Array<Long> | 否 | 指定商品 id 列表，productScopeType=1 时必填（农资商品 xm_peasant_product.id） |
| `supplierScopeType` | Integer | 否 | 0-全部（默认） 1-指定基地 2-指定供应商 |
| `baseIds` | Array<Long> | 否 | 指定基地 id 列表（xm_bulky_base_config.id），supplierScopeType=1 时必填 |
| `supplierIds` | Array<Long> | 否 | 指定供应商 id 列表，supplierScopeType=2 时必填 |
| `validType` | Integer | 是 | 1-固定有效期 2-领取后生效 |
| `validStart` | String | 否 | yyyy-MM-dd HH:mm:ss，validType=1 必填 |
| `validEnd` | String | 否 | yyyy-MM-dd HH:mm:ss，validType=1 必填，同时作为模板 endTime |
| `validDays` | Integer | 否 | 领取后有效天数，validType=2 必填且>=1 |
| `totalCount` | Integer | 是 | 发行总量 >=1 |
| `perSupplierLimit` | Integer | 否 | 每家上限，0-不限（默认 0） |
| `startTime` | String | 否 | 模板生效时间，saveMode=2 必填；<=当前时间则立即进行中 |
| `endTime` | String | 否 | 模板结束时间，validType=2 且 saveMode=2 时必填；validType=1 时忽略（取 validEnd） |
| `usageDesc` | String | 否 | 使用说明 <=500 字 |
| `saveMode` | Integer | 否 | 1-保存草稿（默认） 2-定时生效 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | PeasantCouponTemplateDto | 同 /template/detail 返回结构 |

#### 可能的失败 msg

- name:券名称不能为空
- useType:用途不能为空
- discountType:优惠类型不能为空
- discountPrice:优惠金额不能为空
- validType:有效期类型不能为空
- totalCount:发行总量不能为空
- 券名称不能为空且不超过64字
- 用途不正确
- 优惠类型不正确
- 优惠金额范围 0.01~5000
- 使用门槛范围 1~10000
- 满减券的优惠金额必须小于使用门槛
- 请选择适用商品
- 请选择适用基地
- 请选择适用供应商
- 有效期类型不正确
- 积分商城专用券只能选择领取后生效
- 固定有效期的开始时间必须早于结束时间
- 生效时间不能晚于有效期结束
- 领取后有效天数必须大于0
- 请填写模板结束时间
- 生效时间必须早于结束时间
- 定时生效必须填写生效时间
- 结束时间必须晚于当前时间
- 发行总量必须大于0
- 每家上限不能为负数
- 使用说明不超过500字

#### 请求示例

```json
{
  "name": "秋种满100减20",
  "useType": 1,
  "discountType": 1,
  "thresholdPrice": 100,
  "discountPrice": 20,
  "productScopeType": 0,
  "supplierScopeType": 1,
  "baseIds": [
    5,
    8
  ],
  "validType": 1,
  "validStart": "2026-09-20 00:00:00",
  "validEnd": "2026-10-20 23:59:59",
  "totalCount": 1000,
  "perSupplierLimit": 2,
  "startTime": "2026-09-18 10:00:00",
  "usageDesc": "仅限农资商城下单使用，不可叠加",
  "saveMode": 2
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 12,
    "templateNo": "NZQ20260918305127",
    "name": "秋种满100减20",
    "status": 20,
    "issuedCount": 0,
    "instanceCount": 0,
    "baseScopes": [
      {
        "targetId": 5,
        "targetName": "斗南基地"
      },
      {
        "targetId": 8,
        "targetName": "晋宁基地"
      }
    ],
    "productScopes": [],
    "supplierScopes": [],
    "logs": [
      {
        "id": 88,
        "action": 3,
        "beforeStatus": 0,
        "afterStatus": 20,
        "content": "立即生效",
        "operatorName": "沈浩",
        "createTime": "2026-09-18 10:00:00"
      },
      {
        "id": 87,
        "action": 1,
        "beforeStatus": 0,
        "afterStatus": 0,
        "content": "创建模板",
        "operatorName": "沈浩",
        "createTime": "2026-09-18 10:00:00"
      }
    ],
    "其余字段": "同详情"
  }
}
```

### POST /peasantCoupon/template/update · 编辑模板

请求体与新建相同，另需 id。已结束/已作废不可编辑。草稿/待生效：全量字段可改，按新建规则完整校验并重写范围明细；待生效模板服务端强制按 saveMode=2 校验（startTime/endTime 必填），且若把 startTime 改到当前时间之前则直接进入进行中；草稿传 saveMode=2 等价于排期生效。进行中/已暂停：只接受 name、usageDesc、totalCount、endTime 四个字段（其余字段忽略），校验 name 非空、totalCount>=已发放数、endTime>当前时间、固定有效期时 endTime 不早于 validStart、endTime 不早于其下未结束活动的最晚结束时间；固定有效期模板改 endTime 会同步改 validEnd（新发的券按新有效期）。更新采用当前状态做乐观条件，状态被定时任务改变会返回“模板状态已变更，请刷新后重试”。字段差异写入操作日志。成功返回模板详情

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id |
| `name` | String | 是 | 券名称（所有状态均可改） |
| `usageDesc` | String | 否 | 使用说明（所有状态均可改） |
| `totalCount` | Integer | 是 | 发行总量，进行中/已暂停时须>=issuedCount |
| `endTime` | String | 否 | 结束时间；进行中/已暂停时必填且>当前时间；草稿/待生效固定有效期时忽略（取 validEnd） |
| `saveMode` | Integer | 否 | 草稿时 1-仍为草稿 2-排期生效；待生效时服务端固定按 2 处理；进行中/已暂停忽略 |
| `(其余字段)` | 同新建 | 否 | useType/discountType/thresholdPrice/discountPrice/productScopeType/productIds/supplierScopeType/baseIds/supplierIds/validType/validStart/validEnd/validDays/perSupplierLimit/startTime 仅草稿/待生效生效；因 @Validated 注解，useType、discountType、discountPrice、validType 即使进行中也必须传值（可原样回传） |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | PeasantCouponTemplateDto | 同 /template/detail 返回结构 |

#### 可能的失败 msg

- ID不能为空
- 优惠券模板不存在
- 已结束/已作废的模板不能编辑
- 模板状态已变更，请刷新后重试
- 券名称不能为空
- 发行总量不能小于已发放数量236
- 结束时间必须晚于当前时间
- 结束时间不得早于有效期开始 2026-09-20T00:00
- 结束时间不得早于其下活动的结束时间 2026-10-15T23:59:59
- （草稿/待生效时）同新建模板的全部校验文案

#### 请求示例

```json
{
  "id": 12,
  "name": "秋种满100减20（加量）",
  "useType": 1,
  "discountType": 1,
  "thresholdPrice": 100,
  "discountPrice": 20,
  "validType": 1,
  "totalCount": 2000,
  "endTime": "2026-10-31 23:59:59",
  "usageDesc": "仅限农资商城下单使用，不可叠加"
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 12,
    "name": "秋种满100减20（加量）",
    "totalCount": 2000,
    "endTime": "2026-10-31 23:59:59",
    "validEnd": "2026-10-31 23:59:59",
    "status": 20,
    "logs": [
      {
        "id": 90,
        "action": 2,
        "beforeStatus": 20,
        "afterStatus": 20,
        "content": "名称：秋种满100减20 → 秋种满100减20（加量）；发行总量：1000 → 2000；结束时间：2026-10-20T23:59:59 → 2026-10-31T23:59:59",
        "operatorName": "沈浩",
        "createTime": "2026-09-18 11:20:05"
      }
    ],
    "其余字段": "同详情"
  }
}
```

### POST /peasantCoupon/template/pause · 暂停模板

进行中(20) → 已暂停(30)，CAS 更新。暂停后供应商端不可领取/发放，已发出的券不受影响

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- 优惠券模板不存在
- 仅进行中的模板可以暂停

#### 请求示例

```json
{
  "id": 12
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/template/resume · 恢复模板

已暂停(30) → 进行中(20)。前置：endTime 必须晚于当前时间（已到期的只能保持暂停或结束）

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- 模板已到结束时间，不能恢复
- 优惠券模板不存在
- 仅已暂停的模板可以恢复

#### 请求示例

```json
{
  "id": 12
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/template/end · 结束模板

待生效/进行中/已暂停 → 已结束(40)，不可逆。其下所有待生效/进行中/已暂停的投放活动同步置为已结束（活动日志 action=8，内容“模板结束”）。已发出的券按各自有效期继续可用。草稿不能结束（请用作废）

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- 优惠券模板不存在
- 当前状态不能结束

#### 请求示例

```json
{
  "id": 12
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/template/void · 作废模板

草稿/待生效/进行中/已暂停 → 已作废(50)，需填写原因。前置：该模板未产生任何券实例（instanceCount=0 且 issuedCount=0，CAS 同时校验以防并发领取）；已有券的请用“结束”。其下未结束活动同步置为已结束（日志内容“模板作废”）。前端建议：detail.instanceCount>0 时隐藏作废按钮

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id |
| `reason` | String | 是 | 作废原因，非空且<=255 字 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- reason:作废原因不能为空
- reason:作废原因不能超过255字
- 已产生券实例的模板不能作废，请使用结束
- 优惠券模板不存在
- 当前状态不能作废或已产生券实例

#### 请求示例

```json
{
  "id": 13,
  "reason": "配置错误，重新建券"
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/template/logList · 模板操作日志

分页查询某模板的全部操作日志（bizType=1），按 id 倒序。详情里只带最近 20 条，需要看全部时用本接口

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 模板 id |
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |
| `searchCount` | Boolean | 否 | 是否查总数，默认 true |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope |
| `records` | Array<XmPeasantCouponOpLog> | 日志列表，字段见 commonModels.XmPeasantCouponOpLog |

#### 可能的失败 msg

- id:ID不能为空

#### 请求示例

```json
{
  "id": 12,
  "index": 1,
  "size": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "index": 1,
    "size": 20,
    "searchCount": true,
    "total": 3,
    "records": [
      {
        "id": 90,
        "bizType": 1,
        "bizId": 12,
        "action": 2,
        "beforeStatus": 20,
        "afterStatus": 20,
        "content": "发行总量：1000 → 2000",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 11:20:05"
      },
      {
        "id": 88,
        "bizType": 1,
        "bizId": 12,
        "action": 3,
        "beforeStatus": 0,
        "afterStatus": 20,
        "content": "立即生效",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 10:00:00"
      },
      {
        "id": 87,
        "bizType": 1,
        "bizId": 12,
        "action": 1,
        "beforeStatus": 0,
        "afterStatus": 0,
        "content": "创建模板",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 10:00:00"
      }
    ],
    "other": null
  }
}
```

### POST /peasantCoupon/template/options · 模板下拉

活动新建/编辑表单的模板选择器数据：只返回通用券(useType=1)且状态为待生效(10)/进行中(20)的模板，id 倒序，不分页。无请求体（可传 {}）。前端可用 totalCount-issuedCount 显示剩余可发、用 startTime/endTime 限制活动时间选择范围、用 supplierScopeType 提示投放对象会与模板适用供应商取交集

#### 请求参数

无

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Array<PeasantCouponTemplateOptionDto> | 模板选项列表 |
| `data.id` | Long | 模板 id（活动 templateId） |
| `data.templateNo` | String | 券编号 |
| `data.name` | String | 券名称 |
| `data.ruleDesc` | String | 优惠规则文案 |
| `data.validDesc` | String | 有效期文案 |
| `data.status` | Integer | 10-待生效 20-进行中（活动只能在模板进行中时开始） |
| `data.startTime` | String | 模板生效时间，活动开始时间不能早于它 |
| `data.endTime` | String | 模板结束时间，活动结束时间不能晚于它 |
| `data.totalCount` | Integer | 发行总量 |
| `data.issuedCount` | Integer | 已发放 |
| `data.perSupplierLimit` | Integer | 模板每家上限，0-不限；定向发放每家张数受其约束 |
| `data.supplierScopeType` | Integer | 模板适用供应商 0-全部 1-指定基地 2-指定供应商 |

#### 请求示例

```json
{}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": [
    {
      "id": 12,
      "templateNo": "NZQ20260918305127",
      "name": "秋种满100减20",
      "ruleDesc": "满100减20",
      "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
      "status": 20,
      "startTime": "2026-09-18 10:00:00",
      "endTime": "2026-10-20 23:59:59",
      "totalCount": 2000,
      "issuedCount": 236,
      "perSupplierLimit": 2,
      "supplierScopeType": 1
    },
    {
      "id": 11,
      "templateNo": "NZQ20260915880213",
      "name": "新客无门槛10元",
      "ruleDesc": "无门槛减10",
      "validDesc": "领取后30天内有效",
      "status": 10,
      "startTime": "2026-09-25 00:00:00",
      "endTime": "2026-12-31 23:59:59",
      "totalCount": 500,
      "issuedCount": 0,
      "perSupplierLimit": 1,
      "supplierScopeType": 0
    }
  ]
}
```

## 运营后台 · 投放活动

### POST /peasantCoupon/activity/list · 活动列表

分页查询投放活动，id 倒序。keyword 对活动名/活动编号/模板名/模板编号模糊匹配；issueType/status/templateId 精确过滤（模板详情页“查看活动”可传 templateId）。列表项带 ruleDesc/validDesc（来自模板），不含范围、效果、记录、日志 【10-10 调整】已作废活动返回 voidReason/voidOperatorName/voidTime（与券包明细字段名一致）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |
| `searchCount` | Boolean | 否 | 是否查总数，默认 true |
| `keyword` | String | 否 | 活动名/券名/券编号/活动编号 |
| `issueType` | Integer | 否 | 投放方式 1-自主领取 2-定向发放 |
| `status` | Integer | 否 | 状态 0/10/20/30/40/50 |
| `templateId` | Long | 否 | 按模板过滤 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope |
| `records` | Array<PeasantCouponActivityDto> | 活动列表 |
| `records.id` | Long | 活动 id |
| `records.activityNo` | String | 活动编号，TFL+yyyyMMdd+6 位随机数 |
| `records.name` | String | 活动名称 |
| `records.templateId` | Long | 模板 id |
| `records.templateNo` | String | 模板券编号快照 |
| `records.templateName` | String | 模板券名称快照 |
| `records.issueType` | Integer | 投放方式 1-自主领取 2-定向发放 |
| `records.targetType` | Integer | 投放对象 0-全部供应商 1-指定基地 2-指定供应商 |
| `records.perSupplierLimit` | Integer | 自主领取=每家限领；定向发放=每家发放张数 |
| `records.totalCount` | Integer | 投放总量；定向发放为 有效对象数×每家张数（服务端重算） |
| `records.issuedCount` | Integer | 已发放/已领取张数 |
| `records.targetCount` | Integer | 有效对象数（定向发放），自主领取为 0 |
| `records.startTime` | String | 活动开始时间 |
| `records.endTime` | String | 活动结束时间 |
| `records.status` | Integer | 0-草稿 10-待生效 20-进行中 30-已暂停 40-已结束 50-已作废（admin 新建直接为 10，不产生草稿） |
| `records.issueTime` | String | 定向发放实际执行时间，未执行为 null |
| `records.issueFailMsg` | String | 定向发放失败原因，成功/未执行为空串；待生效且非空时应在页面提示。业务性失败（库存不足/无有效投放对象）不会自动重试，需运营编辑活动或再点“立即开始” |
| `records.creatorId` | Long | 创建人 id |
| `records.creatorName` | String | 创建人昵称 |
| `records.dataStatus` | Integer | 固定 0 |
| `records.createTime` | String | 创建时间 |
| `records.updateTime` | String | 更新时间 |
| `records.ruleDesc` | String | 券优惠规则文案（取自模板） |
| `records.validDesc` | String | 券有效期文案（取自模板） |
| `records.baseScopes` | Array<CouponScopeItemDto> | 列表中为 null |
| `records.supplierScopes` | Array<CouponScopeItemDto> | 列表中为 null |
| `records.effect` | CouponActivityEffectDto | 列表中为 null |
| `records.recentRecords` | Array<CouponActivityRecordDto> | 列表中为 null |
| `records.logs` | Array<XmPeasantCouponOpLog> | 列表中为 null |
| `voidReason` | String | 【10-10 新增】作废原因，仅已作废(50)有值，其余为空 |
| `voidOperatorName` | String | 【10-10 新增】作废操作人，仅已作废有值 |
| `voidTime` | String | 【10-10 新增】作废时间 yyyy-MM-dd HH:mm:ss，仅已作废有值 |

#### 请求示例

```json
{
  "index": 1,
  "size": 10,
  "issueType": 2,
  "status": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "index": 1,
    "size": 10,
    "searchCount": true,
    "total": 1,
    "records": [
      {
        "id": 31,
        "activityNo": "TFL20260918417702",
        "name": "斗南基地秋种定向发券",
        "templateId": 12,
        "templateNo": "NZQ20260918305127",
        "templateName": "秋种满100减20",
        "issueType": 2,
        "targetType": 1,
        "perSupplierLimit": 2,
        "totalCount": 236,
        "issuedCount": 236,
        "targetCount": 118,
        "startTime": "2026-09-18 12:00:00",
        "endTime": "2026-10-20 23:59:59",
        "status": 20,
        "issueTime": "2026-09-18 12:00:03",
        "issueFailMsg": "",
        "creatorId": 3,
        "creatorName": "沈浩",
        "dataStatus": 0,
        "createTime": "2026-09-18 11:50:00",
        "updateTime": "2026-09-18 12:00:03",
        "ruleDesc": "满100减20",
        "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
        "baseScopes": null,
        "supplierScopes": null,
        "effect": null,
        "recentRecords": null,
        "logs": null
      }
    ],
    "other": null
  }
}
```

### POST /peasantCoupon/activity/detail · 活动详情

活动基础信息 + 投放范围（baseScopes/supplierScopes）+ 效果指标 effect + 最近领取与核销记录 recentRecords（最近 10 条领取/发放 ∪ 最近 10 条核销，按时间倒序取 10）+ 最近 20 条操作日志。新建/编辑成功后同样返回本结构。效果口径：issuedCount 等来自券实例表按状态聚合；订单指标来自已支付订单（order_status∈1,2,3,5 且 pay_time 非空），整单退款(5)计入 orderSupplierCount 但不计入 orderCount/成本/实付 【10-10 调整】已作废活动返回 voidReason/voidOperatorName/voidTime（与券包明细字段名一致）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `(活动基础字段)` | PeasantCouponActivityDto | 与 /activity/list records 项相同 |
| `baseScopes` | Array<CouponScopeItemDto> | targetType=1 的指定基地列表，其他为空数组 |
| `supplierScopes` | Array<CouponScopeItemDto> | targetType=2 的指定供应商列表，其他为空数组 |
| `effect` | CouponActivityEffectDto | 效果指标 |
| `effect.issuedCount` | Long | 已发放/领取张数（券实例总数） |
| `effect.totalCount` | Integer | 投放总量 |
| `effect.issueRate` | String(BigDecimal) | 投放达成率 % = issuedCount/totalCount×100，两位小数，如 "100.00" |
| `effect.supplierCount` | Long | 覆盖供应商数（去重） |
| `effect.usedCount` | Long | 已核销张数（status=30） |
| `effect.useRate` | String(BigDecimal) | 核销转化率 % = usedCount/issuedCount×100 |
| `effect.usableCount` | Long | 可使用张数（10） |
| `effect.lockedCount` | Long | 已锁定张数（20） |
| `effect.expiredCount` | Long | 已过期张数（40） |
| `effect.voidCount` | Long | 已作废张数（50） |
| `effect.couponCost` | String(BigDecimal) | 估算优惠成本 = Σ关联订单 coupon_price（剔除整单退款） |
| `effect.paidAmount` | String(BigDecimal) | 关联订单实付金额合计 |
| `effect.driveRatio` | String(BigDecimal)\|null | 消费带动比 = paidAmount/couponCost，成本为 0 时 null，前端显示“—” |
| `effect.avgOrderAmount` | String(BigDecimal) | 关联订单客单价 = paidAmount/orderCount，无订单为 0 |
| `effect.orderCount` | Long | 关联订单数（剔除整单退款） |
| `effect.orderCoverRate` | String(BigDecimal) | 订单关联覆盖率 % = 下单供应商数/覆盖供应商数×100 |
| `recentRecords` | Array<CouponActivityRecordDto> | 最近领取与核销 10 条 |
| `recentRecords.instanceId` | Long | 券实例 id，可跳转 /instance/detail |
| `recentRecords.couponCode` | String | 券码 |
| `recentRecords.supplierName` | String | 供应商名 |
| `recentRecords.recordType` | Integer | 1-领取/发放 2-核销 |
| `recentRecords.status` | Integer | 券当前状态 10/20/30/40/50 |
| `recentRecords.time` | String | 领取/发放时间（券创建时间）或核销时间 |
| `recentRecords.orderId` | String | 锁定/核销订单号，未使用为 null |
| `logs` | Array<XmPeasantCouponOpLog> | 最近 20 条操作日志（bizType=2） |
| `voidReason` | String | 【10-10 新增】作废原因，仅已作废(50)有值，其余为空 |
| `voidOperatorName` | String | 【10-10 新增】作废操作人，仅已作废有值 |
| `voidTime` | String | 【10-10 新增】作废时间 yyyy-MM-dd HH:mm:ss，仅已作废有值 |

#### 可能的失败 msg

- 投放活动不存在

#### 请求示例

```json
{
  "id": 31
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 31,
    "activityNo": "TFL20260918417702",
    "name": "斗南基地秋种定向发券",
    "templateId": 12,
    "templateNo": "NZQ20260918305127",
    "templateName": "秋种满100减20",
    "issueType": 2,
    "targetType": 1,
    "perSupplierLimit": 2,
    "totalCount": 236,
    "issuedCount": 236,
    "targetCount": 118,
    "startTime": "2026-09-18 12:00:00",
    "endTime": "2026-10-20 23:59:59",
    "status": 20,
    "issueTime": "2026-09-18 12:00:03",
    "issueFailMsg": "",
    "creatorId": 3,
    "creatorName": "沈浩",
    "dataStatus": 0,
    "createTime": "2026-09-18 11:50:00",
    "updateTime": "2026-09-18 12:00:03",
    "ruleDesc": "满100减20",
    "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
    "baseScopes": [
      {
        "targetId": 5,
        "targetName": "斗南基地"
      }
    ],
    "supplierScopes": [],
    "effect": {
      "issuedCount": 236,
      "totalCount": 236,
      "issueRate": "100.00",
      "supplierCount": 118,
      "usedCount": 41,
      "useRate": "17.37",
      "usableCount": 180,
      "lockedCount": 3,
      "expiredCount": 0,
      "voidCount": 12,
      "couponCost": "820.00",
      "paidAmount": "9865.50",
      "driveRatio": "12.03",
      "avgOrderAmount": "246.64",
      "orderCount": 40,
      "orderCoverRate": "28.81"
    },
    "recentRecords": [
      {
        "instanceId": 5120,
        "couponCode": "NZ2609180000512",
        "supplierName": "云南丰花种植",
        "recordType": 2,
        "status": 30,
        "time": "2026-09-18 15:32:10",
        "orderId": "PO2609181532001"
      },
      {
        "instanceId": 5236,
        "couponCode": "NZ2609180000628",
        "supplierName": "晋宁兴农合作社",
        "recordType": 1,
        "status": 10,
        "time": "2026-09-18 12:00:03",
        "orderId": null
      }
    ],
    "logs": [
      {
        "id": 102,
        "bizType": 2,
        "bizId": 31,
        "action": 3,
        "beforeStatus": 10,
        "afterStatus": 20,
        "content": "立即开始",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 12:00:00"
      },
      {
        "id": 101,
        "bizType": 2,
        "bizId": 31,
        "action": 1,
        "beforeStatus": 0,
        "afterStatus": 10,
        "content": "创建活动",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 11:50:00"
      }
    ]
  }
}
```

### POST /peasantCoupon/activity/targetCount · 有效对象数

定向发放表单联动接口：按当前选择实时计算有效对象数与投放总量（=对象数×每家张数），表单里模板/投放对象/基地/供应商/每家张数变化时调用。口径：审核通过的种植户/供应商/包花厂 ∩ 活动投放范围 ∩ 模板适用供应商范围 ∩ 未达模板每家上限（模板 perSupplierLimit>0 时，已获得数+本次张数 <= 上限）∩ 未在该活动获得过（编辑时传 activityId 排除）。自主领取表单不需要调用

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `templateId` | Long | 是 | 模板 id |
| `targetType` | Integer | 否 | 0-全部供应商（默认） 1-指定基地 2-指定供应商 |
| `baseIds` | Array<Long> | 否 | targetType=1 时的基地 id 列表（为空则对象数为 0） |
| `supplierIds` | Array<Long> | 否 | targetType=2 时的供应商 id 列表（为空则对象数为 0） |
| `perSupplierLimit` | Integer | 否 | 每家发放张数，默认 1，<1 按 1 计 |
| `activityId` | Long | 否 | 编辑已有活动时传，排除该活动已发放过的对象 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `targetCount` | Integer | 有效对象数 |
| `totalCount` | Integer | 投放总量 = targetCount × 每家张数，用于表单只读回显 |

#### 可能的失败 msg

- templateId:请选择优惠券模板
- 优惠券模板不存在

#### 请求示例

```json
{
  "templateId": 12,
  "targetType": 1,
  "baseIds": [
    5
  ],
  "perSupplierLimit": 2
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "targetCount": 118,
    "totalCount": 236
  }
}
```

### POST /peasantCoupon/activity/save · 新建活动（10-10 调整：支持草稿）

创建投放活动。【10-10 调整】saveMode=1 存为草稿(0)：不会自动开始、供应商端不可见，需编辑时传 saveMode=2 或调 /activity/enable 启用；saveMode=2 存为待生效(10)。校验顺序：模板存在 → 模板为通用券 → 模板状态待生效/进行中 → 名称非空<=64字 → issueType∈{1,2} → targetType=1 须传 baseIds、=2 须传 supplierIds → perSupplierLimit>=1 → startTime<endTime → startTime>=模板 startTime → endTime<=模板 endTime → endTime>当前时间 → 自主领取 totalCount>=perSupplierLimit。定向发放的 totalCount 忽略前端值，服务端按有效对象数×每家张数重算并写 targetCount。saveMode=2 在保存事务提交后：开始时间在未来则保持待生效(10)、startTime 不变，到点由定时任务自动开始；开始时间已到则自动调用“立即开始”，成功返回进行中的详情，失败返回 code=1、msg=“活动已保存为待生效，但启用失败：<原因>”，此时活动已存在（前端应刷新列表而非重复提交）。活动编号 TFL+日期+6 位随机

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `name` | String | 是 | 活动名称 <=64 字 |
| `templateId` | Long | 是 | 模板 id，取自 /template/options |
| `issueType` | Integer | 是 | 1-自主领取 2-定向发放 |
| `targetType` | Integer | 否 | 0-全部供应商（默认） 1-指定基地 2-指定供应商 |
| `baseIds` | Array<Long> | 否 | targetType=1 必填 |
| `supplierIds` | Array<Long> | 否 | targetType=2 必填 |
| `perSupplierLimit` | Integer | 是 | 自主领取=每家限领；定向发放=每家发放张数；>=1 |
| `totalCount` | Integer | 否 | 投放总量，自主领取必填且>=perSupplierLimit；定向发放忽略（服务端重算） |
| `startTime` | String | 是 | 活动开始 yyyy-MM-dd HH:mm:ss，不早于模板生效时间（逻辑校验必填） |
| `endTime` | String | 是 | 活动结束，不晚于模板结束时间且晚于当前时间（逻辑校验必填） |
| `saveMode` | Integer | 否 | 1-仅保存草稿（默认，状态 0，不会自动开始） 2-保存并启用（开始时间未到为待生效 10，已到立即开始 20） |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | PeasantCouponActivityDto | 同 /activity/detail 返回结构 |

#### 可能的失败 msg

- name:活动名称不能为空
- templateId:请选择优惠券模板
- issueType:投放方式不能为空
- perSupplierLimit:每家张数不能为空
- 优惠券模板不存在
- 只能选择通用券模板
- 只能选择待生效或进行中的模板
- 活动名称不能为空且不超过64字
- 投放方式不正确
- 请选择投放基地
- 请选择投放供应商
- 每家张数必须大于0
- 活动开始时间必须早于结束时间
- 活动开始时间不能早于模板生效时间 2026-09-18T10:00
- 活动结束时间不能晚于模板结束时间 2026-10-20T23:59:59
- 活动结束时间必须晚于当前时间
- 投放总量不能小于每家限领
- 活动已保存为待生效，但启用失败：优惠券模板尚未生效
- 活动已保存为待生效，但启用失败：发放失败
- 活动已保存为待生效，但启用失败：发放调用异常

#### 请求示例

```json
{
  "name": "斗南基地秋种定向发券",
  "templateId": 12,
  "issueType": 2,
  "targetType": 1,
  "baseIds": [
    5
  ],
  "perSupplierLimit": 2,
  "startTime": "2026-09-18 12:00:00",
  "endTime": "2026-10-20 23:59:59",
  "saveMode": 2
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 31,
    "activityNo": "TFL20260918417702",
    "name": "斗南基地秋种定向发券",
    "status": 20,
    "issueType": 2,
    "targetType": 1,
    "perSupplierLimit": 2,
    "targetCount": 118,
    "totalCount": 236,
    "issuedCount": 236,
    "issueTime": "2026-09-18 12:00:03",
    "issueFailMsg": "",
    "baseScopes": [
      {
        "targetId": 5,
        "targetName": "斗南基地"
      }
    ],
    "effect": {
      "issuedCount": 236,
      "totalCount": 236,
      "issueRate": "100.00",
      "supplierCount": 118,
      "usedCount": 0,
      "useRate": "0.00",
      "usableCount": 236,
      "lockedCount": 0,
      "expiredCount": 0,
      "voidCount": 0,
      "couponCost": "0",
      "paidAmount": "0",
      "driveRatio": null,
      "avgOrderAmount": "0",
      "orderCount": 0,
      "orderCoverRate": "0.00"
    },
    "其余字段": "同详情"
  }
}
```

### POST /peasantCoupon/activity/update · 编辑活动

请求体同新建，另需 id。已结束/已作废不可编辑。草稿/待生效：全量字段可改（可换模板），按新建规则完整校验，重写范围明细，定向发放重算 targetCount/totalCount；更新以当前状态为乐观条件，状态被定时任务改变返回“活动状态已变更，请刷新后重试”。进行中/已暂停：只接受 name、endTime、（自主领取时）totalCount，校验 name 非空、endTime>当前时间且<=模板 endTime、totalCount>=issuedCount；定向发放进行中不能改总量。【10-10 调整】草稿编辑时传 saveMode=2 则保存后启用（效果同 /activity/enable），传 1 或不传仍为草稿；非草稿状态忽略 saveMode。字段差异写日志。成功返回详情。【09-23 调整】待生效活动编辑成功会清空 issueFailMsg，已到开始时间的定向发放活动会在下一分钟由定时任务自动重发一次

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |
| `name` | String | 是 | 活动名称 |
| `templateId` | Long | 是 | 模板 id（@Validated 必传；进行中/已暂停时忽略，原样回传） |
| `issueType` | Integer | 是 | 投放方式（@Validated 必传；进行中/已暂停时忽略） |
| `perSupplierLimit` | Integer | 是 | 每家张数（@Validated 必传；进行中/已暂停时忽略） |
| `endTime` | String | 是 | 结束时间，进行中/已暂停时必填且>当前时间、<=模板结束时间 |
| `totalCount` | Integer | 否 | 自主领取的投放总量，进行中/已暂停时须>=issuedCount |
| `targetType/baseIds/supplierIds/startTime` | 同新建 | 否 | 仅草稿/待生效生效 |
| `saveMode` | Integer | 否 | 仅草稿生效：2-保存并启用；1/不传-仍为草稿 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | PeasantCouponActivityDto | 同 /activity/detail 返回结构 |

#### 可能的失败 msg

- ID不能为空
- 投放活动不存在
- 已结束/已作废的活动不能编辑
- 活动状态已变更，请刷新后重试
- 活动名称不能为空
- 结束时间必须晚于当前时间
- 结束时间不能晚于模板结束时间 2026-10-20T23:59:59
- 投放总量不能小于已发放数量120
- （草稿/待生效时）同新建活动的全部校验文案

#### 请求示例

```json
{
  "id": 32,
  "name": "全场自主领取（延期）",
  "templateId": 12,
  "issueType": 1,
  "targetType": 0,
  "perSupplierLimit": 1,
  "totalCount": 800,
  "startTime": "2026-09-19 00:00:00",
  "endTime": "2026-10-15 23:59:59"
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 32,
    "name": "全场自主领取（延期）",
    "totalCount": 800,
    "endTime": "2026-10-15 23:59:59",
    "status": 20,
    "logs": [
      {
        "id": 110,
        "action": 2,
        "beforeStatus": 20,
        "afterStatus": 20,
        "content": "名称：全场自主领取 → 全场自主领取（延期）；结束时间：2026-10-10T23:59:59 → 2026-10-15T23:59:59；投放总量：500 → 800",
        "operatorName": "沈浩",
        "createTime": "2026-09-18 14:02:11"
      }
    ],
    "其余字段": "同详情"
  }
}
```

### POST /peasantCoupon/activity/enable · 启用草稿（10-10 新增）

草稿(0) → 待生效(10)。前置：活动为草稿；活动 endTime>当前时间；所属模板为待生效或进行中。启用后：开始时间在未来则停在待生效，到点由定时任务自动开始；开始时间已到则立即执行“立即开始”（startTime 改为当前时间，定向发放同步发券，可能耗时数秒，前端加 loading），开始失败返回 code=1 msg=“活动已保存为待生效，但启用失败：<原因>”，此时活动已是待生效。成功返回活动详情，按返回的 status 刷新行。列表操作建议：草稿显示“启用/编辑/作废”，待生效显示“立即开始/编辑/结束/作废”。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | PeasantCouponActivityDto | 同 /activity/detail 返回结构 |

#### 可能的失败 msg

- 投放活动不存在
- 仅草稿状态的活动可以启用
- 活动结束时间已过，请先修改活动时间
- 优惠券模板不是待生效或进行中，不能启用
- 活动状态已变更，请刷新
- 活动已保存为待生效，但启用失败：<原因>

#### 请求示例

```json
{
  "id": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 20,
    "status": 10,
    "startTime": "2026-10-10 20:00:00"
  }
}
```

### POST /peasantCoupon/activity/start · 立即开始

待生效(10) → 进行中(20)，并把 startTime 改为当前时间。前置：活动待生效；所属模板必须是进行中(20)（待生效模板需先到生效时间或编辑生效时间）；活动 endTime>当前时间。自主领取：直接进行中，供应商端开始可领。定向发放：置进行中后桥调 supplier 侧 /peasant/coupon/activity/issue 同步发券（可能耗时数秒，前端加 loading）；发放失败时活动回到待生效并写 issueFailMsg，接口返回 code=1 msg=失败原因，前端提示后刷新详情可见 issueFailMsg。成功时 data 为 true（自主领取）或 supplier 返回的数据。【09-23 调整】发放失败后的自动重试：库存不足、无有效投放对象等业务性失败不再自动重试，活动停在待生效并保留 issueFailMsg，需运营处理后再次点“立即开始”，或编辑活动（编辑会清空 issueFailMsg，定时任务下一分钟自动重发一次）；只改模板总量不会清活动的 issueFailMsg。仅“发放异常…”这类系统错误会每 10 分钟自动重试

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean\|Object | 自主领取 true；定向发放为 supplier 发放结果数据（前端可忽略，重新拉详情） |

#### 可能的失败 msg

- 投放活动不存在
- 仅待生效的活动可以开始
- 优惠券模板尚未生效
- 活动结束时间已过
- 活动状态已变更，请刷新
- 发放失败
- 发放调用异常
- （supplier 侧返回的具体发放失败原因，如 无有效发放对象 / 模板剩余数量不足）

#### 请求示例

```json
{
  "id": 31
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/activity/pause · 暂停活动

进行中(20) → 已暂停(30)。自主领取暂停后供应商端不可领取；定向发放已发完的暂停无实际影响

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- 投放活动不存在
- 仅进行中的活动可以暂停

#### 请求示例

```json
{
  "id": 32
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/activity/resume · 恢复活动

已暂停(30) → 进行中(20)。前置：活动 endTime 晚于当前时间

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- 活动已到结束时间，不能恢复
- 投放活动不存在
- 仅已暂停的活动可以恢复

#### 请求示例

```json
{
  "id": 32
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/activity/end · 结束活动

待生效/进行中/已暂停 → 已结束(40)，不可逆；已发出的券不受影响

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- 投放活动不存在
- 当前状态不能结束

#### 请求示例

```json
{
  "id": 32
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/activity/void · 作废活动

草稿/待生效/进行中/已暂停 → 已作废(50)，需原因。前置：issuedCount=0（从未发放/领取过，CAS 同时校验防并发领取）；已有发放的请用“结束”。前端建议：issuedCount>0 时隐藏作废按钮

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |
| `reason` | String | 是 | 作废原因，非空且<=255 字 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- reason:作废原因不能为空
- reason:作废原因不能超过255字
- 已发放的活动不能作废，请使用结束
- 投放活动不存在
- 当前状态不能作废或已有发放

#### 请求示例

```json
{
  "id": 33,
  "reason": "投放对象选错"
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/activity/logList · 活动操作日志

分页查询某活动的全部操作日志（bizType=2），id 倒序

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 活动 id |
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |
| `searchCount` | Boolean | 否 | 是否查总数，默认 true |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope |
| `records` | Array<XmPeasantCouponOpLog> | 日志列表，字段见 commonModels.XmPeasantCouponOpLog |

#### 可能的失败 msg

- id:ID不能为空

#### 请求示例

```json
{
  "id": 31,
  "index": 1,
  "size": 20
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "index": 1,
    "size": 20,
    "searchCount": true,
    "total": 2,
    "records": [
      {
        "id": 102,
        "bizType": 2,
        "bizId": 31,
        "action": 3,
        "beforeStatus": 10,
        "afterStatus": 20,
        "content": "立即开始",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 12:00:00"
      },
      {
        "id": 101,
        "bizType": 2,
        "bizId": 31,
        "action": 1,
        "beforeStatus": 0,
        "afterStatus": 10,
        "content": "创建活动",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 11:50:00"
      }
    ],
    "other": null
  }
}
```

## 运营后台 · 券包与核销明细

### POST /peasantCoupon/instance/list · 券包列表

分页查询券实例（供应商券包），id 倒序。keyword：券码/券名称/供应商名模糊匹配，来源单号/订单号精确匹配；sourceType/status/templateId/activityId/supplierId 精确过滤。活动页“已发放 N 张”跳转时传 activityId，模板页跳转传 templateId。列表项 logs 为 null

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |
| `searchCount` | Boolean | 否 | 是否查总数，默认 true |
| `keyword` | String | 否 | 券码/券名称/供应商名（模糊）、来源单号/订单号（精确） |
| `sourceType` | Integer | 否 | 获取方式 1-自主领取 2-定向发放 3-积分兑换 |
| `status` | Integer | 否 | 券状态 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 |
| `templateId` | Long | 否 | 按模板过滤 |
| `activityId` | Long | 否 | 按活动过滤 |
| `supplierId` | Long | 否 | 按供应商过滤 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope |
| `records` | Array<PeasantCouponInstanceDto> | 券实例列表 |
| `records.id` | Long | 券实例 id |
| `records.couponCode` | String | 券码 |
| `records.templateId` | Long | 模板 id |
| `records.templateNo` | String | 券编号 |
| `records.couponName` | String | 券名称快照 |
| `records.useType` | Integer | 1-通用券 2-积分商城专用券 |
| `records.discountType` | Integer | 1-满减 2-无门槛 |
| `records.thresholdPrice` | String(BigDecimal) | 使用门槛 |
| `records.discountPrice` | String(BigDecimal) | 优惠金额 |
| `records.productScopeType` | Integer | 适用商品 0-全场 1-指定商品 |
| `records.supplierId` | Long | 持有供应商 id |
| `records.supplierName` | String | 供应商名 |
| `records.baseName` | String | 供应商所属基地名 |
| `records.sourceType` | Integer | 获取方式 1-自主领取 2-定向发放 3-积分兑换 |
| `records.activityId` | Long | 来源活动 id，积分兑换为 null/0 |
| `records.sourceNo` | String | 来源单号（活动编号或积分兑换单号） |
| `records.status` | Integer | 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 |
| `records.validStart` | String | 本券有效期开始 |
| `records.validEnd` | String | 本券有效期结束 |
| `records.orderId` | String | 当前锁定/核销订单号 |
| `records.lockTime` | String | 锁定时间 |
| `records.useTime` | String | 核销时间 |
| `records.voidReason` | String | 作废原因 |
| `records.voidOperatorId` | Long | 作废操作人 id |
| `records.voidOperatorName` | String | 作废操作人 |
| `records.voidTime` | String | 作废时间 |
| `records.dataStatus` | Integer | 固定 0 |
| `records.createTime` | String | 领取/发放时间 |
| `records.updateTime` | String | 更新时间 |
| `records.ruleDesc` | String | 优惠规则文案 |
| `records.validDesc` | String | 有效期文案“2026.09.20 00:00 至 2026.10.20 23:59” |
| `records.logs` | Array<XmPeasantCouponOpLog> | 列表中为 null |

#### 请求示例

```json
{
  "index": 1,
  "size": 10,
  "activityId": 31,
  "status": 30
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "index": 1,
    "size": 10,
    "searchCount": true,
    "total": 41,
    "records": [
      {
        "id": 5120,
        "couponCode": "NZ2609180000512",
        "templateId": 12,
        "templateNo": "NZQ20260918305127",
        "couponName": "秋种满100减20",
        "useType": 1,
        "discountType": 1,
        "thresholdPrice": "100.00",
        "discountPrice": "20.00",
        "productScopeType": 0,
        "supplierId": 2088,
        "supplierName": "云南丰花种植",
        "baseName": "斗南基地",
        "sourceType": 2,
        "activityId": 31,
        "sourceNo": "TFL20260918417702",
        "status": 30,
        "validStart": "2026-09-20 00:00:00",
        "validEnd": "2026-10-20 23:59:59",
        "orderId": "PO2609181532001",
        "lockTime": "2026-09-18 15:30:02",
        "useTime": "2026-09-18 15:32:10",
        "voidReason": null,
        "voidOperatorId": null,
        "voidOperatorName": null,
        "voidTime": null,
        "dataStatus": 0,
        "createTime": "2026-09-18 12:00:03",
        "updateTime": "2026-09-18 15:32:10",
        "ruleDesc": "满100减20",
        "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
        "logs": null
      }
    ],
    "other": null
  }
}
```

### POST /peasantCoupon/instance/detail · 券详情

单张券的全部字段 + 该券的全部业务历史 logs（bizType=3，不限条数，id 倒序），包括供应商端领取、下单锁定、核销、退款解锁、过期、运营作废等，operatorType 区分系统/平台/供应商，orderId 为相关订单号（supplier 侧写入的日志 action/content 以 supplier-core 为准，示例仅示意）

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 券实例 id |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `(券实例字段)` | PeasantCouponInstanceDto | 与 /instance/list records 项相同 |
| `logs` | Array<XmPeasantCouponOpLog> | 全部业务历史，字段见 commonModels.XmPeasantCouponOpLog |

#### 可能的失败 msg

- 优惠券不存在

#### 请求示例

```json
{
  "id": 5120
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 5120,
    "couponCode": "NZ2609180000512",
    "couponName": "秋种满100减20",
    "supplierName": "云南丰花种植",
    "status": 30,
    "orderId": "PO2609181532001",
    "useTime": "2026-09-18 15:32:10",
    "ruleDesc": "满100减20",
    "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59",
    "logs": [
      {
        "id": 3302,
        "bizType": 3,
        "bizId": 5120,
        "action": 0,
        "beforeStatus": 20,
        "afterStatus": 30,
        "content": "支付核销",
        "reason": "",
        "orderId": "PO2609181532001",
        "operatorType": 0,
        "operatorId": 0,
        "operatorName": "",
        "createTime": "2026-09-18 15:32:10"
      },
      {
        "id": 3290,
        "bizType": 3,
        "bizId": 5120,
        "action": 0,
        "beforeStatus": 10,
        "afterStatus": 20,
        "content": "下单锁定",
        "reason": "",
        "orderId": "PO2609181532001",
        "operatorType": 2,
        "operatorId": 2088,
        "operatorName": "云南丰花种植",
        "createTime": "2026-09-18 15:30:02"
      },
      {
        "id": 3001,
        "bizType": 3,
        "bizId": 5120,
        "action": 1,
        "beforeStatus": 0,
        "afterStatus": 10,
        "content": "定向发放",
        "reason": "",
        "orderId": null,
        "operatorType": 1,
        "operatorId": 3,
        "operatorName": "沈浩",
        "createTime": "2026-09-18 12:00:03"
      }
    ],
    "其余字段": "同列表项"
  }
}
```

### POST /peasantCoupon/instance/void · 作废券

可使用(10) → 已作废(50)，CAS 更新，需填写原因；记录作废人/时间/原因并写券日志（action=9，内容“人工作废”）。已锁定/已使用/已过期/已作废的券不可作废。前端建议只在 status=10 时显示作废按钮

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 券实例 id |
| `reason` | String | 是 | 作废原因，非空且<=255 字 |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `data` | Boolean | true |

#### 可能的失败 msg

- reason:作废原因不能为空
- reason:作废原因不能超过255字
- 优惠券不存在
- 仅可作废可使用状态的优惠券

#### 请求示例

```json
{
  "id": 5236,
  "reason": "供应商账号异常，收回优惠券"
}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": true
}
```

### POST /peasantCoupon/overview · 顶部概览

农资优惠券页面顶部指标卡，全局统计（不分时间段）。无请求体（可传 {}）。口径：templateCount=未删除模板总数（含各状态）；runningActivityCount=进行中(20)活动数；instanceCount=券实例总数；usedCount=已使用(30)券数；useRate=usedCount/instanceCount×100；couponCost=已支付未整单退款订单(order_status∈1,2,3)的 coupon_price 合计

#### 请求参数

无

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `templateCount` | Long | 优惠券（模板）总数 |
| `runningActivityCount` | Long | 有效（进行中）活动数 |
| `instanceCount` | Long | 已进入券包的券总数 |
| `usedCount` | Long | 累计核销张数 |
| `useRate` | String(BigDecimal) | 核销率 %，两位小数 |
| `couponCost` | String(BigDecimal) | 平台优惠成本（元） |

#### 请求示例

```json
{}
```

#### 响应示例

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "templateCount": 6,
    "runningActivityCount": 2,
    "instanceCount": 1320,
    "usedCount": 187,
    "useRate": "14.17",
    "couponCost": "3560.00"
  }
}
```

