Skill: zhangpeng319/wechatpay-product-coupon (version 1.0.3)

--- name: wechatpay-product-coupon description: 微信支付商品券接入解决方案，覆盖券类型选型、发券/核销/查询/退券/回调全链路，提供选型/示例代码/业务速查/质量评估/排障五大能力。Use when user mentions "商品券", "发券", "核销", "创建商品券", "商品券代码", "商品券回调", "商品券退券", or asks to "接入商品券", "生成发券代码", "排查商品券问题". author: wechatpay version: "1.0" --- # 微信支付商品券接入指引 ## 能力概览 1. **匹配券类型** — 用户描述业务场景、不确定用哪种券时，智能匹配对应的券类型（10种） 2. **示例代码** — 用户需要接入代码时，检索对应的 API 请求示例代码（含签名），仅替换参数 3. **业务知识速查** — 开发参数获取、参数校验、业务知识问答 4. **接入质量评估** — 上线前检查签名验签、业务逻辑完整性和回调处理规范 5. **接口排障** — 接口报错、回调异常、签名失败等问题的诊断与排查 > **接入模式**：使用任何能力前需先确认——品牌直连（品牌方自行接入，在品牌经营平台操作）或 服务商（服务商代品牌方接入，在商户平台操作）。 ## 全局交互规范 > ‼️ 以下规则适用于本技能所有能力、所有对话轮次，优先级高于各能力的局部规则。 1. **所有问题必须得到用户明确回答后才能继续。** 如果一次提出了多个问题，必须逐一检查每个问题是否都已获得用户的明确答复。对于未回答的问题，必须再次追问，**严禁对未回答的问题自行假设、推断或使用默认值**。 2. **分步确认协议**（简单知识问答除外，需要帮用户排查、分析或执行操作时必须遵守）： - **① 明确需求**：先理解用户问题，给出初步判断或原因分析，不要一上来就堆参数清单。 - **② 征得同意**：主动提出下一步能做什么，**等用户明确同意后**才继续，严禁用户没表态就开始收集参数或执行操作。 - **③ 收集信息**：用户同意后再告知需要哪些信息并逐项收集，收齐才能执行。 - **④ 执行前确认**：准备执行操作前，简要说明即将做什么，确认用户同意后再执行；涉及线上环境须额外提示风险。 ## 能力1：匹配券类型 确认接入模式后，按适用范围、优惠类型、使用模式三个维度引导用户选定券类型。已明确券类型的可直接进入能力2或能力3。 - [📄 券类型选型.md](./references/3-品牌与服务商通用/券类型选型/券类型选型.md) ## 能力2：示例代码 > ‼️ **只检索、不生成（Java/Go）。** Java 和 Go 由官方维护，必须从代码示例文件中检索获取，严禁从零编写。依赖的公共库（SDK工具类 + HTTP客户端）需提醒用户一并集成，引导用户主动查看公共库代码。 > > ‼️ **只展示、不写入。** 代码示例仅用于讲解 API 调用结构和签名流程，严禁直接写入用户项目（禁止调用 write_to_file、replace_in_file 等工具创建或修改项目文件）。在对话中展示代码，让用户自行复制适配。 > > ‼️ **先交互、后输出。** 提供代码前必须先确认接入模式、开发语言、具体接口，每次只输出一个接口；提供完代码后主动推荐接入质量评估。 > > ‼️ **券类型三要素仅「创建商品券」接口需确认，其他接口无需询问券类型。** 用户请求查询、核销、发券、退券、失效、批次管理等其他接口时，只需确认接入模式和开发语言，无需询问券类型。 > > ‼️ **用户语言非 Java/Go 时**（本 skill 仅维护 Java/Go 示例）：**禁止**直接生成跨语言代码。流程： > 1. 用 `AskQuestion` 获明确同意（文案需明示「参考实现 / 非官方维护 / 须自行 review 与测试」），未同意只发官方 Java/Go 原文。 > 2. 同意后以官方 Java 示例为基准翻译生成业务代码「参考实现」；再用纯文字问是否翻 Java 公库（SDK 工具类 + HTTP 客户端），未明确要不贴。每段代码前附下方免责块。 > > > ⚠️ 以下代码为**跨语言参考实现**，由 AI 参考官方 Java 示例翻译生成，并非微信支付官方维护。 > > - 请**逐行 review** 签名构造、HTTP 调用、字段命名、回调解密等关键逻辑。 > > - 上线前必须在测试环境完整验证，建议先以官方 Java/Go 示例打通主链路作为对照。 > > - 出现接入问题时以官方 Java/Go 示例为准。 - 输出代码前必读，定义了检索流程、允许/禁止操作、输出格式要求、公共库（SDK工具类 + HTTP客户端）索引 → [📄 代码示例使用规范.md](./references/3-品牌与服务商通用/接入规范/代码示例使用规范.md) - 官方 **Java / Go** 时，按接入模式查阅下表索引，定位目标代码文件： - 品牌直连 → [Java](./references/1-品牌/代码示例/Java/接口索引.md) | [Go](./references/1-品牌/代码示例/Go/接口索引.md) - 服务商 → [Java](./references/2-服务商/代码示例/Java/接口索引.md) | [Go](./references/2-服务商/代码示例/Go/接口索引.md) - 涉及回调地址配置、回调解密、IP白名单等通用开发规范时查阅 → [📄 回调处理.md](./references/3-品牌与服务商通用/接入规范/回调处理.md) ## 能力3：业务知识速查 当用户询问接入步骤、参数获取、实体关系、发券/核销流程、券状态流转或遇到创券/发券/核销踩坑问题时，查阅对应文档： - 品牌直连 → [📄 品牌直连商品券业务知识.md](./references/1-品牌/接入指南/品牌直连商品券业务知识.md) - 服务商 → [📄 服务商商品券业务知识.md](./references/2-服务商/接入指南/服务商商品券业务知识.md) - brand_id、mchid、appid 三者关系 → [📄 MAB关系说明文档.md](./references/3-品牌与服务商通用/接入规范/MAB关系说明文档.md) - 券状态相关问题（状态流转、双状态同步、异常兜底等） → [📄 券状态流转说明.md](./references/3-品牌与服务商通用/接入规范/券状态流转说明.md) ## 能力4：接入质量评估 当用户完成开发准备上线、提供了代码希望检查是否有隐患，或遇到接口报错时，触发接入质量评估： - 签名验签是否按照标准实现，参考：[📄 签名验签规范与排查.md](./references/3-品牌与服务商通用/接入规范/签名验签规范与排查.md) - 业务逻辑是否完整，参考：[📄 业务逻辑完整性检查.md](./references/3-品牌与服务商通用/接入规范/业务逻辑完整性检查.md) - 回调处理是否规范，参考：[📄 回调处理.md](./references/3-品牌与服务商通用/接入规范/回调处理.md) ## 能力5：接口排障 > ‼️ **禁止自行猜测报错原因。** 必须先阅读下方排查说明，严格按其中的排障流程执行，严禁直接分析代码。 > > ‼️ **排障完成后，必须在回复末尾主动推荐接入质量评估**（趁排障契机一次性排查其他潜在问题）。 > > ‼️ **排障推荐示例代码时，必须先确认开发语言，只推荐对应的示例。** 排障手册中每个错误码的「示例代码推荐」列出了 Java/Go 两种语言的示例，但输出时**只输出匹配的示例**。开发语言尚未确认时，先在推荐示例代码时自然地询问用户。 > > ‼️ **用户语言非 Java/Go 时按能力 2 的跨语言确认流程处理**（弹框确认 → 参考生成 + 免责块 + 公库分步）。先用文字说明 Java/Go 示例中的关键修复点（签名、字段、流程），再走完整流程后再生成对应语言的"参考修复代码"。 > 💡 **错误码提取**：用户提供 `Request-Id` 后，截取最后一个 `-` 后面的数字即为错误码（如 `...CF05-268578704` → `268578704`），在对应的排障手册中匹配排查方案。 - 品牌直连 → [📄 品牌排障手册.md](./references/1-品牌/问题排查/排障手册.md) - 服务商 → [📄 服务商排障手册.md](./references/2-服务商/问题排查/排障手册.md) - 排障涉及业务逻辑理解时，也可参考能力3「业务知识速查」中的文档：[品牌直连](./references/1-品牌/接入指南/品牌直连商品券业务知识.md) | [服务商](./references/2-服务商/接入指南/服务商商品券业务知识.md) --- > 以下信息与技能能力无关，仅供查阅。 ## 💬 社区与反馈 在使用过程中遇到问题、有改进建议，或者想和其他开发者交流接入经验，欢迎扫码添加企业微信进群，与官方团队和社区开发者一起讨论： ![微信支付 Skills 交流群二维码](https://raw.githubusercontent.com/wechatpay-apiv3/wechatpay-skills/main/wechatpay-product-coupon/assets/qrcode.jpg)

path: references/3-品牌与服务商通用/券类型选型/券类型选型.md
content: # 商品券类型选型（品牌直连 & 服务商通用）

商品券由三个维度组合，品牌直连和服务商均为 **10 种**。


| 维度                | 枚举值                                          | 含义             |
| ----------------- | -------------------------------------------- | -------------- |
| 优惠范围 (scope)      | `ALL`（全场）/ `SINGLE`（单品）                      | 全场通用 vs 指定单品   |
| 券类型 (type)        | `NORMAL`（满减）/ `DISCOUNT`（折扣）/ `EXCHANGE`（兑换） | 三种优惠形式         |
| 使用模式 (usage_mode) | `SINGLE`（单券）/ `PROGRESSIVE_BUNDLE`（多次优惠）     | 一次性 vs 可用3-15次 |


**约束**：兑换券 (EXCHANGE) 仅限单品券 (scope=SINGLE)。

## 10 种券类型


| #   | scope  | type     | usage_mode         | 简称        | 典型场景       |
| --- | ------ | -------- | ------------------ | --------- | ---------- |
| 1   | ALL    | DISCOUNT | SINGLE             | 全场折扣-单券   | 全场七折促销     |
| 2   | ALL    | NORMAL   | SINGLE             | 全场满减-单券   | 全场满100减20  |
| 3   | SINGLE | DISCOUNT | SINGLE             | 单品折扣-单券   | 指定商品8折     |
| 4   | SINGLE | NORMAL   | SINGLE             | 单品满减-单券   | 指定商品满50减10 |
| 5   | SINGLE | EXCHANGE | SINGLE             | 单品兑换-单券   | 免费兑换指定商品   |
| 6   | ALL    | DISCOUNT | PROGRESSIVE_BUNDLE | 全场折扣-多次优惠 | 月卡全场折扣     |
| 7   | ALL    | NORMAL   | PROGRESSIVE_BUNDLE | 全场满减-多次优惠 | 满减月卡       |
| 8   | SINGLE | DISCOUNT | PROGRESSIVE_BUNDLE | 单品折扣-多次优惠 | 单品折扣月卡     |
| 9   | SINGLE | NORMAL   | PROGRESSIVE_BUNDLE | 单品满减-多次优惠 | 单品满减月卡     |
| 10  | SINGLE | EXCHANGE | PROGRESSIVE_BUNDLE | 单品兑换-多次优惠 | 多次兑换权益卡    |


## 选型决策路径

按以下三个维度逐一确认，**每个维度都需要明确选择后才能确定最终券类型**：

### 第一步：优惠范围 — 全场 or 单品？

- 全场通用 → `ALL`；指定商品 → `SINGLE`
- 不确定时可参考场景举例判断："活动更接近哪种？A）新会员全场9折券、双11全场满200减50（→ 全场）；B）指定新品拿铁7折、招牌奶茶立减3元（→ 单品）"

### 第二步：优惠形式 — 满减 / 折扣 / 兑换？

- 满减 → `NORMAL`；折扣 → `DISCOUNT`；兑换 → `EXCHANGE`（仅限单品）
- 不确定时可参考场景举例判断："想做哪种优惠？A）满100减20、开业立减10元（→ 满减）；B）全场8折、指定商品7折（→ 折扣）；C）会员生日免费兑换指定商品（→ 兑换，仅限单品）"

### 第三步：使用模式 — 单券（一次性）or 多次优惠（月卡）？

- 一次性 → `SINGLE`；多次/月卡/复购 → `PROGRESSIVE_BUNDLE`（3-15次）
- 不确定时可参考场景举例判断："这张券用一次就失效（如双11满减券），还是可以反复使用多次、类似月卡（如8折月卡可用5次）？"

### 选型原则

- **三个维度必须逐一与用户确认，禁止跳过或默认**：即使用户未提及某个维度（如未提到"月卡"或"多次使用"），也不能默认选择单券（SINGLE），必须主动向用户提问确认后才能确定该维度的值
- 某个维度不确定时，**不要仅根据行业猜测**，应结合上述场景举例来判断
- 行业常见组合仅作为参考案例，不能替代实际业务需求的判断
- 三个维度全部明确后，才能确定最终的券类型

## 行业常见组合


| 行业    | 常用组合      | 说明                |
| ----- | --------- | ----------------- |
| 餐饮/茶饮 | 全场折扣-多次优惠 | 门店复购，月卡全场8折       |
| 零售/快消 | 单品满减-单券   | 爆款单品促销            |
| 美妆/护肤 | 单品兑换-单券   | 新品试用/小样兑换         |
| 连锁/品牌 | 全场满减-多次优惠 | 会员权益，月卡满100减20用5次 |
| 生鲜    | 全场满减-单券   | 短期满减，提升客单价        |
| 数码3C  | 单品折扣-单券   | 指定型号折扣/清库存        |


## 产品级对比（商品券 vs 商家券）

> **重要提示**：商家券后续不再维护新功能，建议新接入统一使用商品券。


| 维度   | 商品券（单券）        | 商品券（多次优惠）      | 商家券（存量，不再维护） |
| ---- | -------------- | -------------- | ------------ |
| 适用主体 | 品牌方            | 品牌方            | 单商户          |
| 优惠类型 | 满减/折扣/兑换       | 满减/折扣/兑换       | 满减/折扣        |
| 投放渠道 | 摇一摇/API/领券组件   | 摇一摇/API/领券组件   | 摇一摇/API/自有渠道 |
| 多次使用 | 不支持            | 3-15次          | 不支持          |
| 确认发放 | 必须             | 必须             | 不需要          |


**选型结论**：新接入一律推荐商品券。多次优惠/复购/月卡场景 → 商品券（PROGRESSIVE_BUNDLE）。商家券已停止新功能迭代，仅存量商户继续使用。

## 关键结构差异

**单券 vs 多次优惠**：


| 维度   | 单券 (SINGLE) | 多次优惠 (PROGRESSIVE_BUNDLE) |
| ---- | ----------- | ------------------------- |
| 使用次数 | 1次          | 3-15次                     |
| 批次字段 | `stock`     | `stock_bundle`            |
| 优惠规则 | 单一规则        | `coupon_rule_list` 数组     |
| 递进优惠 | 不支持         | 支持（如第1次8折→第2次7折→第3次6折）    |


**全场券 vs 单品券**：


| 维度       | 全场券 (scope=ALL)           | 单品券 (scope=SINGLE)                    |
| -------- | ------------------------- | ------------------------------------- |
| 优惠配置位置   | 商品券级别 `single_usage_info` | 批次级别 `stock.single_usage_rule`        |
| 额外必填字段   | 无                         | `original_price`、`combo_package_list` |
| 支持的 type | NORMAL、DISCOUNT           | NORMAL、DISCOUNT、EXCHANGE              |


**三种券类型**：


| 维度       | NORMAL（满减）                              | DISCOUNT（折扣）                               | EXCHANGE（兑换） |
| -------- | --------------------------------------- | ------------------------------------------ | ------------ |
| 优惠字段     | `transaction_minimum` + `coupon_amount` | `transaction_minimum` + `discount_percent` | 无门槛          |
| 含义       | 满X元减Y元                                  | 满X元打Z折                                     | 免费/低价兑换      |
| 适用 scope | ALL / SINGLE                            | ALL / SINGLE                               | 仅 SINGLE     |
| 金额单位     | 分                                       | 万分比（8折=8000）                               | -            |


## 摇一摇注意事项

- 仅支持 `coupon_code_mode` 为 `WECHATPAY` 或 `UPLOAD` 的批次
- `API_ASSIGN` 模式**不能**在摇一摇投放
- 推荐使用 `WECHATPAY` 模式



path: references/3-品牌与服务商通用/接入规范/回调处理.md
content: # 回调处理

> 本文档适用于品牌直连和服务商两种接入模式。签名、验签等模式差异部分请参考：
> - 品牌直连 → [📄 品牌/签名验签/签名与验签规则.md](../../1-品牌/签名验签/签名与验签规则.md)
> - 服务商 → [📄 服务商/签名验签/签名与验签规则.md](../../2-服务商/签名验签/签名与验签规则.md)

## 一、回调处理

### 前提条件

1. **必须设置 APIv3 密钥**（32字节），未设置不会收到任何回调
2. **必须调用「设置商品券事件通知地址」API** 配置 `notify_url`
   - 品牌直连：`POST /brand/marketing/product-coupon/notify-config`
   - 服务商：`POST /v3/marketing/product-coupon/notify-config`（服务商维度，所有品牌共用）
3. 回调地址要求：HTTPS + 域名已 ICP 备案 + 公网可访问
4. 不能使用内网地址（127.0.0.1 / 192.168.x.x / localhost）

### 回调 IP 白名单

商户侧对微信支付回调 IP 有防火墙策略限制的，需要对以下 IP 段开通白名单：

| 出口 | 网段/IP |
|------|---------|
| 上海电信出口网段 | 101.226.103.0/25 |
| 上海联通出口网段 | 140.207.54.0/25 |
| 上海CAP出口网段 | 121.51.58.128/25 |
| 深圳电信出口网段 | 183.3.234.0/25 |
| 深圳联通出口网段 | 58.251.80.0/25 |
| 深圳CAP出口网段 | 121.51.30.128/25 |
| 香港出口网段 | 203.205.219.128/25 |
| 广州腾讯云出口IP | 81.71.199.64, 81.71.198.25, 81.71.199.59 |
| 退款结果通知、分账动账通知IP | 175.24.214.208, 175.24.211.24, 175.24.213.135, 109.244.180.23, 114.132.203.119, 43.139.43.69 |

同时关闭 WAF/CC 防护对回调 URL 的拦截，避免误将微信支付回调请求判定为恶意请求。

### 回调事件类型

| event_type | 说明 | 处理动作 |
|-----------|------|---------|
| PRODUCT_COUPON.SEND | 发券通知 | 记录 + **调用确认发放API** |
| PRODUCT_COUPON.USE | 核销通知 | 更新核销状态 |
| PRODUCT_COUPON.RETURN | 退券通知 | 更新退还状态 |

> ‼️ **收到 PRODUCT_COUPON.SEND 回调后必须调用确认发放接口，否则券不生效。** 这是最常见的接入遗漏问题。静态代码扫描时需检查用户代码中是否包含以下接口路径：
> - 品牌：`/brand/marketing/product-coupon/users/{openid}/coupons/{coupon_code}/confirm`
> - 服务商：`/v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/confirm`

### 回调报文与解密

```json
{
  "id": "通知唯一ID",
  "create_time": "2025-08-02T00:00:00+08:00",
  "event_type": "PRODUCT_COUPON.SEND",
  "resource_type": "encrypt-resource",
  "resource": {
    "algorithm": "AEAD_AES_256_GCM",
    "ciphertext": "Base64编码的密文",
    "nonce": "随机串",
    "associated_data": "product_coupon"
  }
}
```

- 算法: `AEAD_AES_256_GCM`，密钥: APIv3 密钥（32字节）
- 从 `resource` 取 `nonce`、`ciphertext`、`associated_data` 进行解密

### 解密后关键字段

`coupon_code`、`product_coupon_id`、`stock_id`、`openid`、`unionid`、`send_channel`、`send_request_no`、`receive_time`、`valid_begin_time`/`valid_end_time`

### 回调处理要求

1. **必须返回 HTTP 2XX**（200 或 204），否则微信支付会重试
2. **必须在 5 秒内应答**
3. **必须做幂等处理**（按 coupon_code + event_type 去重）
4. **必须验签**，防止伪造通知（品牌直连用微信支付公钥，服务商用微信支付平台证书）
5. 签名探测流量以 `WECHATPAY/SIGNTEST/` 开头，需正确处理
6. 即使业务处理异常，也建议返回 200，通过告警系统人工介入

### 回调收不到的常见排查场景

#### 一、前置配置缺失

1. **未设置 APIv3 密钥** — 微信支付不会发送回调通知
2. **未配置 `notify_url`** — 接口或商户平台未配置回调通知接收地址，微信支付不会发送回调通知

#### 二、回调地址配置类问题

1. **地址格式错误**
   - `notify_url` 未以 `https://` 或 `http://` 开头
   - URL 中只有域名，缺少具体路径（如 `http://www.weixin.qq.com`）
   - URL 携带了参数
   - 使用了内网地址（`127.0.0.1`、`192.168.x.x`、`localhost`）
2. **域名未备案或解析异常**
   - 域名未完成工信部 ICP 备案（国内服务器必须）
   - DNS 解析失效（解析记录过期、未配置正确的 A/AAAA 记录）

#### 三、网络与服务器连通性问题

1. **防火墙/安全组拦截** — 未对上方「回调 IP 白名单」中的 IP 段开通入站规则
2. **WAF/CC 防护误拦** — 安全策略将微信支付回调请求误判为恶意请求
3. **网络链路故障** — 丢包或延迟过高（超过 3 秒）导致请求超时
4. **CDN/反向代理配置异常** — Nginx、Cloudflare 等未将回调请求正确转发至后端服务

#### 四、回调处理逻辑问题

1. **登录态校验** — `notify_url` 的代码逻辑不能做登录态校验
2. **未在 5 秒内应答** — 微信支付会认为通知失败并重复发送
3. **未做幂等** — 同一通知可能多次发送，必须按 `coupon_code` + `event_type` 去重

### 服务商回调特殊说明

- 回调地址是**服务商维度**配置的，所有品牌共用同一个回调地址
- 通过回调内容中的 `brand_id` 区分不同品牌

## 二、错误处理策略

| 错误类型 | 处理策略 |
|---------|---------|
| 500 SYSTEM_ERROR | 使用相同 out_request_no 重试（指数退避） |
| 400 参数错误 | 修正参数后重试 |
| 401 签名错误 | 检查证书和签名方案是否与接入模式匹配 |
| 回调超时 | 返回200，异步补偿处理 |
| 解密失败 | 检查 APIv3 密钥是否正确 |

## 三、幂等设计

- 所有写操作使用唯一的 `out_request_no` / `send_request_no`
- 相同请求号重复请求不会创建重复资源
- 建议格式：`{业务前缀}_{日期}_{序号}`，如 `SEND_20250801_000001`

## 四、请求域名

- 主域名: `https://api.mch.weixin.qq.com`
- 备域名: `https://api2.mch.weixin.qq.com`


path: references/3-品牌与服务商通用/接入规范/券状态流转说明.md
content: # 微信支付商品券 —— 券状态流转说明

## 背景：为什么有"两份状态"？

商品券从发放到核销，涉及**微信支付平台**和**品牌商户系统**两个独立系统的协作。用户在微信卡包看到的券状态由微信侧管理，券的实际核销、退券等业务逻辑由商家侧管理。两边各自维护一份状态，不能直接改对方的数据，必须通过「回调通知 + 接口调用」保持同步——这就是本文要解释的**双状态同步机制**。

| 系统 | 角色 | 职责 |
|------|------|------|
| **微信侧**（微信支付平台） | 券的展示方 | 管理用户卡包中的券展示、券的生命周期规则（过期等） |
| **商家侧**（品牌/商户系统） | 券的使用方 | 管理券的实际核销、退券等业务逻辑 |

两个系统各自维护一份券状态，**互相不能直接修改对方的数据**，必须通过「回调通知 + 接口调用」进行同步。这就是**双状态同步机制**。

> 🔴 **最常见的接入错误：** 品牌收到发券回调（`PRODUCT_COUPON.SEND`）后忘记调用「确认发券」接口，导致用户在卡包看到券但无法使用。接入开发和代码审查时务必重点检查此项。


### 同步规则：谁先动，谁跟进

| 操作 | 谁先动 | 同步方式 | 为什么这样设计 |
|------|--------|---------|--------------|
| 发券（摇优惠） | 微信侧先动（用户主动领券触发） | 微信发回调 → 品牌调「确认发券」 | 用户在微信端领券，微信先创建券记录 |
| 发券（主动塞券） | 商家侧先动（品牌调API触发） | 品牌调「向用户发放商品券」→ 微信创建券并发回调 → 品牌调「确认发券」 | 品牌主动推送，但券仍由微信先创建，需双方确认 |
| 核销 | 商家侧先动 | 品牌调用「核销」接口通知微信 | 用户在商家处消费，商家最先知道券被使用了 |
| 退券 | 商家侧先动 | 品牌调用「退券」接口通知微信 | 退券是商家的业务决策，微信跟进恢复券状态 |

### 关于「确认发券」

「确认发券」的本质**不是真的在发券**，而是品牌告诉微信：**"我这边也把这张券记录好了，双方状态对齐。"**

- 如果品牌不调用确认发券 → 微信侧知道品牌还没准备好 → 用户在卡包能看到券，但标记为不可用
- 只有品牌确认后 → 微信才会让这张券变为可用状态

---

## 场景一：摇优惠（用户主动领券）

> **触发方式：** 用户在微信「摇一摇」等入口主动领取优惠券。
> **品牌不需要调用发券接口**，因为用户自己来领了。

### 完整流程

| 步骤 | 发生了什么 | 微信侧状态 | 商家侧状态 |
|:---:|----------|:---------:|:---------:|
| ① | 用户在微信中主动领券 | → **待商家发券** | — |
| ② | 微信发送「领券回调通知」给品牌 | 待商家发券 | → **待发券** |
| ③ | 品牌调用「确认发券」接口（告诉微信：我记好了） | → **待使用** ✅ | → **待使用** ✅ |
| ④ | 用户到店消费，品牌在本地完成核销 | 待使用 | → **已核销** |
| ⑤ | 品牌调用「核销」接口通知微信 | → **已核销** ✅ | 已核销 ✅ |

> ⚠️ 如果品牌没有调用「确认发券」：微信侧停留在"待商家发券"，用户在卡包看得到券但**用不了**。

---

## 场景二：主动塞券（品牌主动发券）

> **触发方式：** 品牌在自有渠道（APP、公众号等）主动给用户发券。
> **用户无感知**，不需要手动领取。
> 比摇优惠**多一步**：品牌需要先调用接口告诉微信"我要给这个用户发券"。

### 完整流程

| 步骤 | 发生了什么 | 微信侧状态 | 商家侧状态 |
|:---:|----------|:---------:|:---------:|
| ① | 品牌调用「向用户发放商品券」接口（告诉微信：请给该用户创建一张券） | → **待商家发券** | — |
| ② | 微信发送「领券回调通知」给品牌（微信：券已创建好） | 待商家发券 | → **待发券** |
| ③ | 品牌调用「确认发券」接口（告诉微信：我也记好了） | → **待使用** ✅ | → **待使用** ✅ |
| ④ | 用户到店消费，品牌在本地完成核销 | 待使用 | → **已核销** |
| ⑤ | 品牌调用「核销」接口通知微信 | → **已核销** ✅ | 已核销 ✅ |

> 💡 **为什么品牌自己发的券还要等回调再确认？**
> 因为品牌调用「向用户发放商品券」只是请求微信创建券，微信创建完成后通过回调通知品牌"创建好了"，品牌再确认"我也同步好了"——这是一个**双方握手**过程，确保两边数据一致。

---

## 两种场景对比

| 对比维度 | 摇优惠 | 主动塞券 |
|---------|--------|---------|
| **谁触发** | 用户主动领券 | 品牌主动调接口 |
| **用户感知** | 有，用户手动操作 | 无，用户无需操作 |
| **品牌是否需要调发券接口** | ❌ 不需要，用户自己领了 | ✅ 需要调「向用户发放商品券」 |
| **到"待使用"的步数** | 2 步（回调 → 确认） | 3 步（塞券 → 回调 → 确认） |
| **核销 / 退券流程** | 完全一致 | 完全一致 |

---

## 异常兜底（两种场景通用）

| 异常场景 | 微信侧 | 商家侧 | 后果 |
|---------|:------:|:------:|------|
| 品牌未调用「确认发券」 | 待商家发券 | 待发券 | 用户看得到券但**用不了** |
| 核券通知失败 | 待使用 | 已核销 | 微信侧券仍显示可用，需**重试**核销通知 |
| 退券通知失败 | 已核销 | 待使用 | 微信侧券已核销，商家此时券待使用，需**重试**退券通知 |


path: references/3-品牌与服务商通用/接入规范/MAB关系说明文档.md
content: # MAB关系说明文档 — brand_id、mchid、appid

> 本文档适用于品牌直连和服务商两种接入模式。
> 理解三者关系是正确传参的前提，尤其在服务商模式下容易混淆。

## 使用指引

当用户遇到以下场景时，应参考本文档回答：
- 询问 brand_id、mchid、appid 三者的关系或区别
- 不清楚品牌直连与服务商模式下分别该传哪些参数
- 遇到 `INVALID_REQUEST: AppID非法`、`NO_AUTH` 等参数关联类报错
- 询问 appid 如何绑定/关联品牌
- 询问服务商如何获得品牌授权

## 目录

1. [三者关系概览](#三者关系概览) — brand_id、mchid、appid 的多对多/一对多关系
2. [品牌直连创券前必须完成的前置条件](#品牌直连创券前必须完成的前置条件) — AppID 关联品牌（不关联 → 报 `AppID非法`）、签名参数配置
3. [服务商创券前必须完成的前置条件](#服务商创券前必须完成的前置条件) — 品牌授权服务商（不授权 → 报 `NO_AUTH`）、AppID 关联品牌（不关联 → 报 `AppID非法`）、签名参数配置


## 三者关系概览

```
appid1 ──┐                          ┌── appid1
appid2 ──┤                          ├── appid2
  ...  ──┤                          ├──  ...
         │                          │
      mchid 1 ──╮      ╭── brandid 1
         │       ╲    ╱        │
         │        ╲  ╱         │
         │         ╳           │
         │        ╱  ╲         │
         │       ╱    ╲        │
      mchid 2 ──╯      ╰── brandid 2
         │                          │
appid1 ──┤                          ├── appid1
appid2 ──┤                          ├── appid2
  ...  ──┘                          └──  ...

(服务商侧appid)  (服务商)  (品牌)  (品牌侧appid)
```

| 关系 | 类型 | 说明 |
|------|------|------|
| **mchid ↔ brand_id** | 多对多 | 一个服务商可服务多个品牌，一个品牌也可授权给多个服务商 |
| **mchid ↔ appid** | 一对多 | 一个服务商下挂多个 appid（服务商自己的小程序、公众号等） |
| **brand_id ↔ appid** | 一对多 | 一个品牌下挂多个 appid（品牌自己的小程序、公众号等） |

> ⚠️ 服务商侧的 appid 和品牌侧的 appid 是**两套独立的 appid**，分属不同主体。

---

## 品牌直连创券前必须完成的前置条件

品牌直连模式下，只涉及 **brand_id** 和 **品牌 appid**，不涉及 mchid。

### ① AppID 关联品牌（brand_id ↔ appid）

创券时 `stock.notify_config.notify_appid` 字段必须传一个已关联品牌的 AppID。

**如何关联**（二选一）：

| 绑定路径 | 操作位置 |
|---------|---------|
| 路径一 | 品牌经营平台 → 账号管理 → 下单 appid 管理 |
| 路径二 | 品牌经营平台 → 商家名片 → 交易连接名片 |

**不关联会怎样**：创券接口返回 **`INVALID_REQUEST: AppID非法`**（错误码 `268578704`），无法创建商品券。

### ② 签名参数配置

| 参数 | 在请求中的位置 | 说明 |
|------|-------------|------|
| brand_id | Authorization 签名头 `brand_id="xxx"` | 品牌身份标识，**不需要**在请求体中传 |
| 品牌 appid | 请求体 `notify_config.notify_appid` | 需已完成上述关联 |

签名方案：`WECHATPAY-BRAND-SHA256-RSA2048`

---

## 服务商创券前必须完成的前置条件

服务商模式下，brand_id、mchid、appid 三者都涉及，**且有两项前置条件必须同时满足，缺一不可**。

### ① 品牌授权服务商（brand_id ↔ mchid）

品牌方必须在品牌经营平台授予服务商「商品券」产品功能权限。

**如何授权**：品牌方登录品牌经营平台，在合作管理中对服务商授权「商品券」产品功能。授权流程见 [服务商申请品牌授权](https://pay.weixin.qq.com/doc/v3/partner/4016026183)。

**不授权会怎样**：服务商调用任何商品券 API 均返回 **`NO_AUTH: 服务商没有此接口权限`**（错误码 `268579803`），所有接口不可用。

### ② AppID 关联品牌（brand_id ↔ appid）

创券时 `stock.notify_config.notify_appid` 字段必须传一个已关联品牌的 AppID（可以是服务商 appid 或品牌 appid，但必须由品牌方在品牌经营平台完成授权关联）。

**如何关联**（二选一，均需品牌方在品牌经营平台操作）：

| 绑定路径 | 操作位置 |
|---------|---------|
| 路径一 | 品牌经营平台 → 账号管理 → 下单 appid 管理 |
| 路径二 | 品牌经营平台 → 商家名片 → 交易连接名片 |

> 💡 服务商可通过 API `POST /v3/brand/partner/cardtemplate/traderelation` 创建交易连接名片，但 AppID 关联品牌仍需品牌方在品牌经营平台确认。

**不关联会怎样**：创券接口返回 **`INVALID_REQUEST: AppID非法`**（错误码 `268578704`），无法创建商品券。

### ③ 签名参数配置

| 参数 | 在请求中的位置 | 说明 |
|------|-------------|------|
| mchid | Authorization 签名头 `mchid="xxx"` | 服务商身份标识 |
| brand_id | **请求体**中传入 | 指定为哪个品牌操作，**服务商必填** |
| appid | 请求体 `notify_config.notify_appid` | 服务商 appid 或品牌 appid，需已完成上述关联 |

签名方案：`WECHATPAY2-SHA256-RSA2048`

---


path: references/3-品牌与服务商通用/接入规范/代码示例使用规范.md
content: # 代码示例使用规范

> 本规范适用于使用本 skill 提供代码示例时的所有场景。

## 核心原则：只检索、不生成

所有代码必须从本 skill 的代码示例文件中检索获取，严禁从零编写任何代码。

**示例代码的定位**：代码示例是伪代码性质，用于讲解业务结构和 API 调用流程，不能直接复制运行。向用户提供代码时，需明确提醒：示例仅展示接口调用结构和签名流程，用户需结合自身项目框架、依赖管理和配置方式进行适配后才能运行。

## 允许的操作

- 从代码示例文件中**读取完整代码**并原样提供给用户
- **替换初始化参数**：`brand_id`、`mchid`、证书序列号、私钥路径、微信支付公钥ID、微信支付公钥路径、回调地址等配置值
- 根据用户实际场景**调整业务参数**：商品券名称、面额、有效期、商品信息等创建券时的业务字段
- 对代码中已有的逻辑添加**注释说明**

## 输出代码时的必要提醒

每次向用户提供代码示例时，必须在代码前或后附带以下提醒：

> ⚠️ 以下代码为结构示例，用于展示接口调用流程和签名逻辑，不能直接复制运行。请结合您的项目框架、依赖管理和实际配置进行适配。

## 禁止的操作

> ⚠️ 违反以下任何一条都可能导致用户接入失败，因为自行编写的代码在参数结构、字段名称、API路径、签名算法等细节上极易出错。

- ❌ **禁止**从零编写 API 调用代码、构造请求体、拼接签名逻辑
- ❌ **禁止**凭记忆或推理编写业务代码，即使用户明确要求"帮我写一个xx"
- ❌ **禁止**拼接或修改示例中的 API 路径、请求方法、Header 结构、签名算法
- ❌ **禁止**将多个示例文件的代码片段拼接组合成新代码
- ❌ **未经用户明确同意前禁止**为示例中未覆盖的语言（如 Python、Node.js、PHP、C# 等）编写等价实现。处理流程见 SKILL.md 能力 2「用户语言非 Java/Go 时」段落。

## 未覆盖的接口

当用户请求的 API 操作在 Java/Go 示例中均未覆盖时（如某个新接口）：

1. 明确告知用户"当前代码示例未覆盖该接口"
2. 提供对应的 API 接口文档链接供用户参考：品牌 → [📄 品牌/接入指南/API接口清单.md](../../1-品牌/接入指南/API接口清单.md)；服务商 → [📄 服务商/接入指南/API接口清单.md](../../2-服务商/接入指南/API接口清单.md)
3. **不要**自行编写代码。

> **未覆盖的开发语言**（如 Python / Node.js / PHP / C#）的处理流程见 SKILL.md 能力 2「用户语言非 Java/Go 时」段落（弹框确认 → 参考生成 + 免责块 + 公库分步）。商品券特有的高风险点：**品牌直连签名前缀 `WECHATPAY-BRAND-SHA256-RSA2048`** vs **服务商签名前缀 `WECHATPAY2-SHA256-RSA2048`**，翻译时不能混用，详见 [📄 签名验签规范与排查.md](./签名验签规范与排查.md)。

## 交互方式约束

> 向用户确认信息时，须遵守 SKILL.md「全局交互规范」：所有问题必须得到用户明确回答后才能继续，禁止自行假设或使用默认值。
>
> 一般信息收集（接入模式、开发语言、接口名称、券类型三要素等）使用纯文本问答，**禁止**使用按钮选择组件。`AskQuestion` 仅在 SKILL.md 能力 2 跨语言生成确认场景使用。

## 定位代码示例的步骤

> ‼️ **严格按步骤顺序执行，禁止跨步骤提问。** 每个步骤只确认该步骤要求的信息，禁止在当前步骤中提前询问后续步骤的问题。例如：步骤1只问接入模式和语言，不能同时问接口名称或券类型。
>
> ‼️ **每次用户提出新的接口需求时，必须重新从当前用户消息中提取目标接口，独立执行定位流程。** 严禁将上一轮对话中已定位的接口名称沿用到新请求中。例如：上一轮定位到"查询用户商品券详情"，用户新消息说"我要发券代码"，必须重新识别为「商品券发放」模块，而不是继续使用"查询用户商品券详情"。

### 步骤1：确认接入模式和开发语言

向用户确认两个信息，选择对应的代码目录：**接入模式**（品牌直连 / 服务商）；**开发语言**（官方支持 Java / Go；其他语言 Python、Node.js、PHP、C# 等需走「无匹配示例时的处理 - 场景 B」流程）。

> ⚠️ 开发语言**必须向用户确认**，禁止根据项目文件或上下文自行推断。
>
> ⚠️ 本步骤**只确认以上两个信息**。禁止在此步骤中同时询问具体接口、券类型（适用范围/优惠类型/使用模式）等后续步骤的内容。
>
> ‼️ **券类型三要素（适用范围、优惠类型、使用模式）只有「创建商品券」接口才需要确认。** 用户请求查询、核销、发券、退券、失效、批次管理等其他任何接口时，**绝对不要在本步骤或任何步骤中询问券类型**。
>
> ‼️ **当用户指定的开发语言不是 Java / Go 时**，立即按 SKILL.md 能力 2「用户语言非 Java/Go 时」段落执行（弹框确认 → 参考生成 + 免责块 + 公库分步）。**禁止**在未经弹框确认前就开始询问接口名称或直接生成代码。

### 步骤2：定位模块和接口

读取对应的接口索引.md，按「**先定位模块，再定位接口**」的原则匹配。每次只输出一个接口的代码示例。根据用户输入的明确程度，按下表处理：

| 用户输入类型 | 示例 | 处理方式 |
|---|---|---|
| **指定了具体接口名称** | "核销接口代码"、"查询用户商品券详情" | 直接定位到对应模块和文件，**跳过步骤3，直接进入步骤4输出代码** |
| **关键词可唯一映射到接口** | "创批次"、"查批次"、"上传券码"、"退券" | 确认模块归属后直接定位，无需二次选择，**跳过步骤3，直接进入步骤4输出代码** |
| **提到模块名称或功能类别** | "批次管理"、"券管理"、"发券" | 匹配到模块后，**列出该模块下所有接口让用户选择** |
| **模糊或宽泛** | "给我代码"、"接入代码" | **列出所有可用模块和接口，让用户指定** |

**唯一映射的判断标准**：关键词包含明确的「操作动词 + 操作对象」，且在该模块下只有一个接口与之匹配。

> ‼️ **列出接口时，必须使用接口索引.md 中的原始接口说明文字，禁止自行编造、改写或添加额外的补充描述。** 错误的补充描述可能误导用户对接口用途的理解。例如：接口索引中写的是"确认发放用户商品券"，就只展示"确认发放用户商品券"，不要自行添加"配合预发放使用"等不准确的说明。
>
> ⚠️ **常见易错场景**："发券"→ 属于「商品券发放」模块，但该模块下有 3 个接口（发放 / 预发放 / 确认发放），**不能唯一映射**，必须列出让用户选择；"创批次"→ 属于「批次管理」模块下的"创建商品券批次"，不要误判为「商品券管理」模块下的"创建商品券"。
>
> ‼️ **关键判断：是否需要进入步骤3？** 定位到的接口是「商品券管理」模块中的「创建商品券」→ **需要**进入步骤3确认券类型三要素；定位到的接口是**其他任何接口**（查询、核销、发券、退券、失效、批次管理、回调配置、图片上传等）→ **直接跳到步骤4输出代码，不要询问券类型**。

### 步骤3：创建商品券的额外确认（⚠️ 仅限「创建商品券」这一个接口）

> ‼️ **只有「商品券管理」模块中的「创建商品券」这一个接口需要执行本步骤。**
>
> ‼️ **以下接口一律跳过本步骤，直接进入步骤4：** 修改商品券、查询商品券详情、失效商品券、创建批次、查询批次、修改批次、失效批次、关联门店、上传券码、发放商品券、预发放商品券、确认发放、核销、查询用户商品券详情、查询用户商品券列表、失效用户商品券、退券、设置通知地址、获取通知地址、上传图片等所有非「创建商品券」的接口。
>
> ‼️ **自检规则**：在执行步骤3前，必须确认当前接口名称是否**精确等于**「创建商品券」。如果不是，立即跳到步骤4。

**仅当**定位到「商品券管理」模块中的「创建商品券」接口时，需额外确认券类型三要素：

| 要素 | 选项 |
|---|---|
| 适用范围 | 全场（All）/ 单品（Single） |
| 优惠类型 | 满减（Normal）/ 折扣（Discount）/ 兑换（Exchange） |
| 使用模式 | 单券 / 多次优惠（Sequential/Many） |

> ⚠️ 三个要素都**必须向用户确认**，不能从业务描述中自行推断。
>
> ‼️ **品牌直连与服务商的创建商品券文件命名规则不同，定位文件时必须按对应模式的命名规则查找：**
>
> | 接入模式 | 命名规则 | 单券示例 | 多次优惠示例 |
> |---------|---------|---------|------------|
> | **品牌直连** | `{范围}{类型}[Sequential]{语言}` | `AllNormalJava.java` | `AllNormalSequentialJava.java` |
> | **服务商** | `{范围}{类型}{Single/Many}{语言}` | `AllNormalSingleJava.java` | `AllNormalManyJava.java` |
>
> 品牌直连：**无后缀** = 单券，带 `Sequential` = 多次优惠；服务商：`Single` 后缀 = 单券，`Many` 后缀 = 多次优惠。详细文件列表见各接口索引.md。

### 步骤4：输出代码

读取目标文件的完整代码，将初始化参数替换为用户的实际值。

> ‼️ **与「跨语言 / 非 Java·Go」的关系**：本步骤在官方 Java/Go 时直接输出从索引定位到的**文件原文**。若用户语言为 Python、Node.js 等，按 SKILL.md 能力 2「用户语言非 Java/Go 时」段落执行，并遵守该段落「公库分步」原则——**不得**在未经确认时把 SDK 工具类、HTTP 客户端大段硬塞给同一则回复。

## 主动推荐其他接口示例

创建商品券的代码已包含完整业务流程（创建券 → 添加批次 → 发券 → 确认 → 核销），用户拿到后即可跑通主链路。提供完创建商品券代码后，**主动告知用户**还可以提供以下模块的独立接口代码示例：

> "创建商品券的代码已包含从创建到核销的完整流程。此外我还可以提供以下模块的代码示例：
> - 商品券管理（修改、查询、失效）
> - 批次管理（创建、查询、修改、失效、关联门店、上传券码等）
> - 发券（API发放、小程序预发放、确认发放）
> - 核销、查询券状态、失效与退券
> - 回调通知地址配置、上传图片
>
> 需要哪个接口告诉我即可。"

## 主动推荐代码扫描

> ‼️ **触发时机**：每次向用户提供代码示例后（无论是创建商品券还是其他接口），除了推荐其他接口外，**必须**在回复末尾紧接着追加以下提示，主动询问用户是否需要对项目代码进行接入质量扫描（不需要用户主动询问）：

> "另外，我还可以帮你对项目代码做一次**接入质量扫描**，自动检查常见的接入隐患（如验签遗漏、回调处理不规范、必接接口缺失等），帮你在上线前排除隐患。需要的话随时告诉我。"

## 代码示例索引

- 品牌直连 Java → [📄 接口索引.md](../../1-品牌/代码示例/Java/接口索引.md)
- 品牌直连 Go → [📄 接口索引.md](../../1-品牌/代码示例/Go/接口索引.md)
- 服务商 Java → [📄 接口索引.md](../../2-服务商/代码示例/Java/接口索引.md)
- 服务商 Go → [📄 接口索引.md](../../2-服务商/代码示例/Go/接口索引.md)

## 公共库输出策略

业务代码依赖两个公共库文件（SDK工具类 + HTTP客户端）。输出代码时，**不要一次性把公共库代码全部展示给用户**，而是采用分步交互方式（避免首屏信息过载、引导按需查阅公共库）：

1. **首次输出**：只展示业务代码（如 `SingleDiscountJava.java`），同时告知用户该代码依赖以下两个公共库文件，需要集成到项目中才能运行：SDK工具类（签名、验签、回调解密）、HTTP客户端（请求/响应模型、枚举、HTTP调用）。
2. **用户主动索取时**：当用户明确要求查看公共库代码时，再从下方索引中读取并提供对应的完整代码。

> 跨语言场景（如 Python 等）的公库分步交付遵守 SKILL.md 能力 2 跨语言流程的「公库分步」原则。

## 签名验签工具类（公共库）

编码前先引入官方工具类，处理签名、验签、回调解密。品牌直连和服务商使用不同的工具类，**不能混用**。工具类差异、签名方案对比见 [📄 签名验签规范与排查.md](./签名验签规范与排查.md) 第一章。

### SDK工具类

- 品牌直连 Java → [📄 WXPayBrandUtility.java](../../../1-品牌/代码示例/Java/9-SDK工具类/WXPayBrandUtility.java)（签名前缀 `WECHATPAY-BRAND-SHA256-RSA2048`，身份标识用 `brand_id`）
- 品牌直连 Go → [📄 wxpay_brand_utility.go](../../../1-品牌/代码示例/Go/9-SDK工具类/wxpay_brand_utility.go)（签名前缀 `WECHATPAY-BRAND-SHA256-RSA2048`，身份标识用 `brand_id`）
- 服务商 Java → [📄 WXPayUtility.java](../../../2-服务商/代码示例/Java/9-SDK工具类/WXPayUtility.java)（签名前缀 `WECHATPAY2-SHA256-RSA2048`，身份标识用 `mchid`）
- 服务商 Go → [📄 wxpay_utility.go](../../../2-服务商/代码示例/Go/9-SDK工具类/wxpay_utility.go)（签名前缀 `WECHATPAY2-SHA256-RSA2048`，身份标识用 `mchid`）

### HTTP客户端

- 品牌直连 Java → [📄 BrandModelsAndClient.java](../../../1-品牌/代码示例/Java/9-SDK工具类/BrandModelsAndClient.java)
- 品牌直连 Go → 同上 `brand_models_and_client.go`（Go 版 SDK工具类与 HTTP客户端合并在同一文件中）
- 服务商 Java → [📄 ServiceModelsAndClient.java](../../../2-服务商/代码示例/Java/9-SDK工具类/ServiceModelsAndClient.java)
- 服务商 Go → 同上 `service_models_and_client.go`（Go 版 SDK工具类与 HTTP客户端合并在同一文件中）


path: references/3-品牌与服务商通用/接入规范/业务逻辑完整性检查.md
content: # 业务逻辑完整性检查

> 本文档适用于品牌直连和服务商两种接入模式，包含两部分：
> 1. **必接接口清单** — 发券核销流程中必须接入的接口，缺少任何一个都可能导致流程不完整
> 2. **业务规则完整性检查** — 静态代码扫描时逐项检查的 7 项业务规则，每项必须独立判定（通过/未实现/不涉及）

---

## 第一部分：必接接口清单

以下接口必须接入，缺少任何一个都可能导致发券核销流程不完整。

### 设置商品券事件通知地址

接收回调的前提，必须先设置通知地址。

- 品牌：`POST /brand/marketing/product-coupon/notify-config`
- 服务商：`POST /v3/marketing/partner/product-coupon/notify-configs`

### 创建商品券

- 品牌：`POST /brand/marketing/product-coupon/product-coupons`
- 服务商：`POST /v3/marketing/partner/product-coupon/product-coupons`

### 商品券回调通知

接收发券/核销/退券事件，回调验签解密规范见 [📄 回调处理.md](./回调处理.md)

- 品牌直连 → [📄 品牌/签名验签/签名与验签规则.md](../../1-品牌/签名验签/签名与验签规则.md)
- 服务商 → [📄 服务商/签名验签/签名与验签规则.md](../../2-服务商/签名验签/签名与验签规则.md)

### 确认发放用户商品券

收到发券回调后必须调用，否则券不生效。

- 品牌：`POST /brand/marketing/product-coupon/product-coupons/{product_coupon_id}/users/{openid}/coupons/{code}/confirm`
- 服务商：`POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/confirm`

### 核销用户商品券

- 品牌：`POST /brand/marketing/product-coupon/product-coupons/{product_coupon_id}/users/{openid}/coupons/{code}/use`
- 服务商：`POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/use`

### 查询用户商品券详情

- 品牌：`GET /brand/marketing/product-coupon/product-coupons/{product_coupon_id}/users/{openid}/coupons/{code}`
- 服务商：`GET /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}`

### 选接接口

**向用户发放商品券（预发放）** — 仅在商户主动塞券场景下使用

- 品牌：`POST /brand/marketing/product-coupon/product-coupons/{product_coupon_id}/users/{openid}/pre-send-coupon`
- 服务商：`POST /v3/marketing/partner/product-coupon/users/{openid}/coupons`
- ⚠️ 摇优惠会场领券场景不需要调用此接口，用户在会场领券后只需调用**确认发放**接口即可

**券Code管理（预上传券Code）** — 仅在品牌方使用自定义券码时接入

- 品牌方希望使用自己的券码（如已有会员系统券码、线下印刷券码等）时需要接入，发券前先调用此接口上传自定义 Code，Code 不足会影响发券，需及时补充
- 如果没有自定义券码的需求，创建批次时选择微信支付自动生成券码即可，无需接入此接口

### 完整 API 路径

- 品牌 → [📄 品牌/接入指南/API接口清单.md](../../1-品牌/接入指南/API接口清单.md)
- 服务商 → [📄 服务商/接入指南/API接口清单.md](../../2-服务商/接入指南/API接口清单.md)

---

## 第二部分：业务规则完整性检查

> **静态代码扫描重点检查项**：扫描用户代码时，必须逐项检查以下 7 项是否实现。每一项必须独立执行搜索并独立给出判定（通过/未实现/不涉及），禁止将多项合并为一条结论——
> 1. 收到发券回调后是否调用了确认发放接口（第一项）
> 2. coupon_code_mode 为 UPLOAD 时是否调用了预上传券 Code 接口（第二项）
> 3. image_url 是否来自图片上传 API 而非外部链接（第三项）
> 4. store_scope 为 SPECIFIC 时是否调用了关联门店接口（第四项）
> 5. 核销券后是否调用了核销用户商品券接口通知微信（第五项）
> 6. 失效券后是否调用了失效用户商品券接口通知微信（第六项）
> 7. 退券后是否调用了退券接口通知微信（第七项）

### 一、发券确认

收到 `PRODUCT_COUPON.SEND` 回调后**必须调用确认发放接口**，否则券不生效。这是最常见的接入遗漏问题。

#### 确认发放接口路径

| 接入模式 | 接口路径 |
|---------|---------|
| 品牌直连 | `POST /brand/marketing/product-coupon/users/{openid}/coupons/{coupon_code}/confirm` |
| 服务商 | `POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/confirm` |

#### 静态扫描检查方式

分两步搜索：
1. **搜索触发条件**：在整个项目中搜索 `PRODUCT_COUPON.SEND` 字符串，确认用户代码中是否存在发券回调分支
2. **搜索接口调用**：在整个项目中搜索确认发放接口路径关键字（品牌直连搜 `confirm`、服务商搜 `confirm`），确认是否存在对确认发放接口的调用

**判定规则**：如果步骤 1 找到了发券回调分支，但步骤 2 在整个项目中未找到确认发放接口的调用，则判定为接入遗漏。注意：不要只看回调分支内部的代码是否为空，而要搜索整个项目。

### 二、创建商品券批次 — 预上传券 Code

创券时 `stock.coupon_code_mode` 选择 `UPLOAD`（品牌方预上传 Code）时，品牌方需先调用**预上传券 Code 接口**将自定义 Code 上传到微信支付系统。

- 微信支付系统发券时从预存 Code 池中**随机选取**；当预存 Code 不足时**可能影响发券**，品牌应及时补充。
- 可通过应答中 `stock.coupon_code_count_info` 的 `available_count` 字段监控剩余可用 Code 数量。
- 此模式的商品批次**只可由品牌方自行发券**（即通过 API 发券），无法在"摇一摇有优惠"等微信支付渠道投放。

#### 静态扫描检查方式

如果用户代码中创建批次请求的 `coupon_code_mode` 值为 `UPLOAD`，检查是否存在对预上传券 Code 接口的调用。若缺失则提示：创券模式为 UPLOAD 时必须先调用预上传券 Code 接口，否则发券时无可用 Code。

### 三、商品券图片 — image_url 来源限制

`display_info.image_url`（商品券图片）**仅支持通过【图片上传 API】获取的图片 URL 地址**，不可使用外部链接或其他云存储地址。

#### 静态扫描检查方式

检查用户代码中设置 `image_url` 的值是否来自图片上传 API 的返回结果。若直接硬编码了非微信支付域名的图片地址，则提示：image_url 仅支持通过图片上传 API 获取的 URL。

### 四、可用门店范围 — store_scope 为 SPECIFIC 时关联门店

创建批次时 `stock.store_scope` 选择 `SPECIFIC`（特定门店可用）时，品牌需调用**关联门店接口**将门店与该批次关联，关联后该批次仅在关联的门店可用。

#### 静态扫描检查方式

如果用户代码中创建批次请求的 `store_scope` 值为 `SPECIFIC`，检查是否存在对关联门店接口（`associate-stores`）的调用。若缺失则提示：门店范围为 SPECIFIC 时必须调用关联门店接口，否则该批次无可用门店。

### 五、核销券后通知微信

品牌在自有系统完成券核销后，**必须调用核销用户商品券接口通知微信支付**，使微信侧券状态同步变更为「已核销」。如果不通知，微信侧券仍为「待使用」状态，用户在微信卡包中看到的券状态与实际不一致。

#### 核销用户商品券接口路径

| 接入模式 | 接口路径 |
|---------|---------|
| 品牌直连 | `POST /brand/marketing/product-coupon/product-coupons/{product_coupon_id}/users/{openid}/coupons/{code}/use` |
| 服务商 | `POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/use` |

#### 静态扫描检查方式

分两步搜索：
1. **搜索触发条件**：在整个项目中搜索 `PRODUCT_COUPON.USE` 字符串，确认用户代码中是否存在核销回调分支
2. **搜索接口调用**：在整个项目中搜索核销接口路径关键字（品牌直连路径含 `coupons/` + `/use`，服务商路径含 `coupons/` + `/use`），确认是否存在对核销用户商品券接口的调用

**判定规则**：如果步骤 1 找到了核销回调分支，但步骤 2 在整个项目中未找到核销接口的调用，则判定为接入遗漏。注意：不要只看回调分支内部的代码是否为空，而要搜索整个项目。

### 六、失效券后通知微信

品牌将用户商品券失效后，**必须调用失效用户商品券接口通知微信支付**，使微信侧券状态同步变更为「已失效」。如果不通知，微信侧券仍保持原状态，用户可能误以为券仍可使用。

#### 失效用户商品券接口路径

| 接入模式 | 接口路径 |
|---------|---------|
| 品牌直连 | `POST /brand/marketing/product-coupon/users/{openid}/coupons/{coupon_code}/deactivate` |
| 服务商 | `POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/deactivate` |

#### 静态扫描检查方式

分两步搜索：
1. **搜索触发条件**：在整个项目中搜索 `deactivate` 关键字，确认用户代码中是否存在失效相关的业务逻辑（如方法调用、接口定义或业务分支）
2. **搜索接口调用**：在整个项目中搜索失效接口路径关键字（路径含 `coupons/` + `/deactivate`），确认是否存在对失效用户商品券接口的调用

**判定规则**：如果步骤 1 找到了失效相关业务逻辑，但步骤 2 在整个项目中未找到失效接口的调用，则判定为接入遗漏。注意：不要只看某个方法体内部的代码是否为空，而要搜索整个项目。

### 七、退券后通知微信

品牌完成退券后，**必须调用退券接口通知微信支付**，使微信侧券状态同步更新。如果不通知，微信侧不知道券已退回，可能影响用户重新领取和批次库存回收。

#### 退券接口路径

| 接入模式 | 接口路径 |
|---------|---------|
| 品牌直连 | `POST /brand/marketing/product-coupon/users/{openid}/coupons/{coupon_code}/return` |
| 服务商 | `POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/return` |

#### 静态扫描检查方式

分两步搜索：
1. **搜索触发条件**：在整个项目中搜索 `PRODUCT_COUPON.RETURN` 字符串，确认用户代码中是否存在退券回调分支
2. **搜索接口调用**：在整个项目中搜索退券接口路径关键字（品牌直连路径含 `coupons/` + `/return`，服务商路径含 `coupons/` + `/return`），确认是否存在对退券接口的调用

**判定规则**：如果步骤 1 找到了退券回调分支，但步骤 2 在整个项目中未找到退券接口的调用，则判定为接入遗漏。注意：不要只看回调分支内部的代码是否为空，而要搜索整个项目。


path: references/3-品牌与服务商通用/接入规范/签名验签规范与排查.md
content: # 公共 — 签名验签规范与排查

> 本文档适用于品牌直连和服务商两种接入模式。
> **注意：排查过程中无需提供私钥文件**，通过核对签名流程即可定位问题。
>
> **静态代码扫描重点检查项**：扫描用户代码时，必须逐项检查以下 4 项是否实现。每一项必须独立执行搜索并独立给出判定（通过/未实现/不涉及），禁止将多项合并为一条结论——
> 1. 是否对 HTTP 响应做了验签（第一章）
> 2. 是否对回调通知做了验签（第二章）
> 3. 是否实现了幂等去重和并发锁控制（第三章）
> 4. （可选）是否未对探测流量做特殊跳过处理（第四章）

## 一、签名方案与工具类差异

品牌直连和服务商使用不同的签名方案和工具类，**不能混用**：

| 项目 | 品牌直连 | 服务商 |
|------|----------|--------|
| 签名方案 | `WECHATPAY-BRAND-SHA256-RSA2048` | `WECHATPAY2-SHA256-RSA2048` |
| Authorization 身份标识 | `brand_id="xxx"` | `mchid="xxx"` |
| 签名用的私钥 | 品牌API证书私钥 | 商户API证书私钥 |
| 验签用的公钥 | 微信支付公钥 | 微信支付公钥（推荐）或平台证书 |
| API路径前缀 | `/brand/` | `/v3/` |
| Java 工具类 | `WXPayBrandUtility` | `WXPayUtility` |
| Go 工具类 | `wxpay_brand_utility` | `wxpay_utility` |
| 配置对象 | `BrandConfig`（传入 `brand_id`） | `MchConfig`（传入 `mchid`） |

**最常见的错误**：用了服务商的 `WXPayUtility` 去调品牌直连接口，或签名方案写错（漏了 `BRAND`），都会导致 401 SIGN_ERROR。

## 二、响应验签与回调验签

### 响应验签

接收微信支付 HTTP 响应时，商户**必须对响应进行签名验证**，确认响应确实来自微信支付，防止中间人篡改。

**验签步骤**：

1. 从响应 HTTP 头中获取 `Wechatpay-Timestamp`、`Wechatpay-Nonce`、`Wechatpay-Signature`、`Wechatpay-Serial`
2. 构造验签串（3 行，每行以 `\n` 结尾）：

```
应答时间戳\n
应答随机串\n
应答报文主体\n
```

3. 使用对应的公钥对验签串和签名值进行 SHA256 with RSA 验证
4. 验证 `Wechatpay-Serial` 与本地持有的公钥 ID / 证书序列号是否匹配

**静态扫描检查方式**：

分两步搜索：
1. **搜索 HTTP 客户端代码**：定位发送 HTTP 请求并处理响应的代码（如 OkHttp 的 `execute()`、HttpClient 的 `send()` 等）
2. **搜索验签调用**：在处理 2XX 成功响应的代码分支中，检查是否调用了响应验签方法（如 `validateResponse`、`verify` 等，或直接实现了上述验签步骤）

**判定规则**：如果 HTTP 客户端在收到 2XX 响应后直接解析返回数据，中间没有任何验签逻辑，则判定为未实现响应验签。

### 回调通知验签

商户系统对于回调通知的内容**必须做签名验证**，并校验通知的信息是否与商户侧的信息一致，防止数据泄露导致出现"假通知"，造成资金损失。

回调通知的验签方式与响应验签一致，从回调请求的 HTTP 头中获取签名信息，构造验签串并验证。

**静态扫描检查方式**：

分两步搜索：
1. **搜索回调处理代码**：定位处理微信支付回调通知的代码（如包含 `PRODUCT_COUPON.SEND`/`USE`/`RETURN` 等事件类型字符串的代码）
2. **搜索验签调用**：在回调处理流程中，检查在解密业务数据**之前**是否调用了通知验签方法（如 `validateNotification`、`verify` 等）

**判定规则**：如果回调处理代码在收到通知后直接解密或解析业务数据，中间没有任何验签逻辑，则判定为未实现回调验签。特别注意：如果使用了 `parseNotification` 等封装方法，需检查该方法内部是否包含验签步骤，而非只做了解密。

## 三、幂等与并发控制

同样的通知可能会多次发送给商户系统。商户系统**必须能够正确处理重复的通知**。

### 推荐做法

1. 收到通知后，先检查对应业务数据的状态，判断该通知是否已经处理
2. 如果未处理，则进行业务处理
3. 如果已处理，则直接返回成功结果

### 并发控制

在对业务数据进行状态检查和处理之前，**必须采用数据锁进行并发控制**，以避免函数重入造成的数据混乱。

建议使用分布式锁（如 Redis 锁）或数据库行锁，锁的粒度为 `coupon_code` + `event_type`。

**静态扫描检查方式**：

在回调处理代码中搜索以下特征：
1. **幂等去重**：是否有按 `coupon_code` + `event_type`（或等价的唯一键）查询已处理记录的逻辑，如数据库查询、缓存查询等
2. **并发锁控制**：是否有加锁逻辑，如 Redis 分布式锁（`setnx`/`RedisLock`）、数据库行锁（`SELECT ... FOR UPDATE`）、`synchronized`、`ReentrantLock` 等

**判定规则**：如果回调处理代码中既没有去重查询逻辑也没有加锁逻辑，则判定为未实现幂等去重和并发控制。

## 四、探测流量处理

微信支付会定期向商户回调地址发送探测流量，用于验证商户系统的连通性和签名验证逻辑。

### 处理要求

- 商户系统**不应对探测流量进行特殊处理**，而应将其视为正常的应答或通知回调，对其签名进行验证
- 探测请求的签名值以 `WECHATPAY/SIGNTEST/` 为前缀，验签时需正确处理该前缀
- 排查问题时，可以通过签名值中的 `WECHATPAY/SIGNTEST/` 前缀快速判断是否为探测流量

### 注意事项

- 所有用于探测目的的签名值都会包含 `WECHATPAY/SIGNTEST/` 前缀
- 如果商户系统对探测流量返回错误，微信支付可能会认为商户回调地址不可用

## 五、签名串格式

签名串固定 5 行，每行以 `\n` 结尾（包括最后一行）：

```
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n
```

### 自查要点

1. **必须严格 5 行**，缺行或多行都会导致签名不一致
2. **每行末尾必须有 `\n`**，包括最后一行空行
3. URL 是**去掉域名的绝对路径**（不含 `https://api.mch.weixin.qq.com`）
4. 请求报文主体为空时（如 GET 请求），该行为空字符串，仍需附加 `\n`

## 六、四种参数类型的签名构造

### 6.1 Path 参数（路径参数）

**特征**：URL 中包含 `{参数名}`，如 `/brand/marketing/product-coupon/product-coupons/{product_coupon_id}`

**构造规则**：URL 中的 `{参数名}` 需替换为实际值。

示例 — 查询商品券（GET，无请求体）：

```
GET\n
/brand/marketing/product-coupon/product-coupons/200000001\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
\n
```

**常见错误**：
- URL 中保留了 `{product_coupon_id}` 占位符未替换
- URL 末尾多了或少了 `/`

### 6.2 Body 参数（包体参数）

**特征**：POST/PUT/PATCH 请求，请求体为 JSON

**构造规则**：签名串的第 5 行为完整的请求体 JSON 字符串。**签名时的 body 和实际发送的 body 必须完全一致**（包括字段顺序、空格、换行）。

示例 — 入会组件预授权（POST）：

```
POST\n
/brand/card-member/pre-auth-tokens\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
{"card_id":"pbLatjvWOibDc5-TBnbUk1pD12o0","openid":"obLatjnx9gnqzS4myYGmLZ7LgLBA"}\n
```

**常见错误**：
- 签名时 body 放一行，但实际请求发送时 body 做了格式化（多行/加空格），导致签名不匹配
- 签名时用了格式化的 JSON，但请求发送时压缩成一行
- body 中字段顺序与签名时不一致

### 6.3 Query 参数（查询参数）

**特征**：GET 请求带 `?key=value&key2=value2`

**构造规则**：URL 必须包含完整的查询字符串（`?` 和所有参数）。如果参数值包含特殊字符，需先 URL Encode。

示例 — 查询用户在品牌下所有会员卡（GET）：

```
GET\n
/brand/card-member/user-cards?openid=obLatjnx9gnqzS4myYGmLZ7LgLBA&user_card_state=EFFECTIVE&offset=0&limit=20\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
\n
```

**常见错误**：
- URL 中漏掉了 Query 参数部分（只签了路径，没签查询字符串）
- 含特殊字符的参数值未做 URL Encode（如 JSON 格式的参数值）
- 参数顺序与实际请求不一致

### 6.4 图片上传接口

**特征**：POST，Content-Type 为 `multipart/form-data`

**构造规则**：签名串的第 5 行为 `meta` 字段的 JSON（包含 `filename` 和 `sha256`），**不是整个 multipart body**。

示例 — 营销图片上传：

```
POST\n
/brand/card-member/media/image-upload\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
{"filename":"wechatpay_logo.png","sha256":"d2973a45b1d528c21ebb77792ef3fcea40fa9a4e04a17e35369102ba9c84c8b1"}\n
```

**常见错误**：
- 用整个 multipart body 参与签名，而非 meta 的 JSON
- sha256 值计算错误（应为文件内容的 SHA256 摘要）
- HTTP 头未设置 `Content-Type: multipart/form-data`

## 七、Authorization 头格式

```
[REDACTED_SECRET] brand_id="100XX",nonce_str="随机串",signature="签名值",timestamp="时间戳",serial_no="证书序列号"
```

### 自查要点

| 检查项 | 正确做法 |
|--------|----------|
| 认证类型 | 品牌直连用 `WECHATPAY-BRAND-SHA256-RSA2048`，服务商用 `WECHATPAY2-SHA256-RSA2048` |
| 身份标识 | 品牌直连用 `brand_id`，服务商用 `mchid` |
| nonce_str | 必须与签名串中的随机串一致 |
| timestamp | 必须与签名串中的时间戳一致，且不能太旧 |
| serial_no | 必须是签名所用私钥对应的证书序列号 |
| Wechatpay-Serial | 请求头中需传微信支付公钥ID（用于验签） |
| 整行不换行 | Authorization 值必须在一行，不能换行 |

## 八、完整自查清单

遇到 401 SIGN_ERROR 时，无需提供私钥文件，按以下顺序逐项确认：

1. **签名方案是否正确**：品牌直连是否用了 `WECHATPAY-BRAND-SHA256-RSA2048`？服务商是否用了 `WECHATPAY2-SHA256-RSA2048`？
2. **Authorization 身份标识是否正确**：品牌直连是否用 `brand_id`？服务商是否用 `mchid`？
3. **证书序列号是否匹配**：`serial_no` 是否是当前签名私钥对应的证书序列号？
4. **签名串是否为 5 行**：HTTP方法、URL、时间戳、随机串、请求体，每行末尾 `\n`。可打印签名串（不含私钥内容）核对格式
5. **URL 是否正确**：
   - 去掉域名部分
   - Path 参数是否已替换为实际值
   - Query 参数是否包含在 URL 中
6. **请求体是否一致**：签名时的 body 和实际请求发送的 body 是否完全一致（含字段顺序、空格、换行）
7. **时间戳和随机串是否一致**：签名串中的值和 Authorization 头中的值是否相同
8. **时间戳是否过期**：系统时间是否准确，时间戳是否为当前时间
9. **图片上传**（如适用）：签名串的 body 部分是否只用了 meta 的 JSON？
10. **以上都正确时**：参照上方各参数类型的签名构造示例，用测试公私钥验证签名计算逻辑


path: references/2-服务商/接入指南/服务商商品券业务知识.md
content: # 服务商商品券业务知识

## 目录

1. [接入流程](#一接入流程) — 接入全流程、参数获取与签名配置、实体关系、发券、投放计划、状态流转
2. [高频踩坑点](#二高频踩坑点) — 创券/发券/核销踩坑、参数与配置踩坑

## 一、接入流程

### 接入全流程（8步）

| 步骤 | 操作 | 操作方式 | 说明 |
|------|------|---------|------|
| 1. 品牌入驻 | API注册品牌，提交品牌资质审核 | **API** | 服务商代品牌方完成入驻 |
| 2. 创品牌门店 | API创建门店，等待平台审核 | **API**（可选，如有线下门店） | 有线下门店场景时需创建 |
| 3. 创交易连接名片 | API创建交易连接名片，关联支付场景 | **API** | 关联AppID的关键步骤，**不关联则后续创券报错** |
| 4. 创券 | API创建商品券，配置券面和规则 | **API** | 请求体中**必须传 `brand_id`**；配置券的scope/type/usage_mode等核心属性 |
| 5. 创批次/批次组 | API添加批次或批次组，设置发放数量和使用规则 | **API** | 设置库存、有效期、发放规则等 |
| 6. 创投放计划 | API创建投放计划，提交审核后生效 | **API** | 需人工审核，约2个工作日 |
| 7. 发券 | 服务商主动发券或用户主动领取，调用确认接口后券生效 | **API** | 预发放→token→领券→回调→确认发放 |
| 8. 核销 | 扫码核销或用户主动出示，验证后完成券核销 | **API** | 必须关联订单，券状态需为EFFECTIVE |

> **服务商优势**：全部 8 步都有 API，可实现全链路自动化对接。品牌直连的步骤1（入驻）、步骤3（交易连接名片）、步骤6（创活动/投放计划）没有对应API。

### 参数获取与签名配置（服务商平台）

服务商模式下，参数在**服务商平台**（pay.weixin.qq.com）获取，brand_id 由品牌方提供：

| 参数 | 获取路径 | 注意事项 |
|------|---------|---------|
| mchid（服务商商户号） | 服务商平台 → 账户中心 → 个人信息 | 所有接口必传，服务商唯一标识 |
| brand_id | 品牌方提供，或合作伙伴平台 → 品牌经营中心 → 合作品牌管理 | 服务商模式**必须在请求体传 `brand_id`** |
| 品牌授权 | 品牌方在品牌经营平台授予服务商「商品券」产品功能权限 | **未授权则调用 API 报 `NO_AUTH`** |
| 签名方案 | — | `WECHATPAY2-SHA256-RSA2048`，Authorization 头用 `mchid="xxx"` |
| 商户API证书序列号 | 服务商平台 → API安全 → 管理证书，或 `openssl x509 -in cert.pem -noout -serial` | 需与签名使用的商户API证书对应 |
| 商户API证书私钥文件 | 服务商平台 → API安全 → 申请证书 → 下载 `apiclient_key.pem` | **只能下载一次，丢失需重新申请；勿上传至 GitHub 等公开场所** |
| APIv3密钥 | 服务商平台 → API安全 → 设置密钥 | 32字节，**设置后无法查看，未设置则收不到回调** |
| 微信支付公钥ID | 服务商平台 → API安全 → 管理公钥 | — |
| 微信支付公钥文件 | 服务商平台 → API安全 → 微信支付公钥 → 申请后下载 | 用于**验签**，长期有效，无需定期更换 |
| appid | 服务商平台 → 产品中心 → APPID账号管理 → 我关联的APPID账号 | 未绑定需由超级管理员在 APPID账号管理中关联 |
| AppID关联品牌 | API `POST /v3/brand/partner/cardtemplate/traderelation` 或品牌方在品牌经营平台关联 | **必须先关联，否则创券报 `INVALID_REQUEST: AppID非法`** |
| 回调通知地址 | 调用「设置商品券事件通知地址」API 配置 | HTTPS + 公网可访问，服务商维度配置，所有品牌共用 |

### 核心实体关系

```
品牌（brand_id）← 服务商代操作
└── 商品券（product_coupon_id）
    ├── scope / type / usage_mode
    ├── 【单券 usage_mode=SINGLE】
    │   └── 批次（stock_id）← 同一商品券下最多200个生效中的批次
    │       └── 用户券（coupon_code）
    └── 【多次优惠 usage_mode=PROGRESSIVE_BUNDLE】
        └── 批次组（stock_bundle）
            ├── count: 3-15（可用次数）
            ├── coupon_rule_list: 递进式优惠规则
            └── 用户券（coupon_code）
```

### 发券流程

1. 调用「预发放商品券」API → 获取 token（有效期 5 分钟）
2. 前端使用 token 调起领券组件 → 用户领券
3. 接收回调通知（PRODUCT_COUPON.SEND）
4. 验签（微信支付公钥）+ 解密回调内容
5. 调用「确认发放」API → 券状态变为 EFFECTIVE

### 投放计划（摇一摇场景）

`POST /v3/marketing/partner/delivery-plan/delivery-plans`

- `reuse_coupon_config=true` 可复用商品券和批次信息
- `total_count` 需 ≥ 10000，投放持续时间 ≥ 5 天
- 配置到投放计划的批次，用户通过摇一摇领券由微信侧发放，无需调发券API，但**仍需调确认发放API**

### 用户券状态流转

```
发券 → CONFIRMING（待确认）
        ├── 调用确认API
        │   ├── 未到可用时间 → PENDING → 到达可用时间 → EFFECTIVE
        │   └── 已在可用时间 → EFFECTIVE
        ├── EFFECTIVE → 核销 → USED
        ├── EFFECTIVE → 过期 → EXPIRED
        ├── EFFECTIVE → 用户删除 → DELETED
        └── EFFECTIVE → 品牌失效 → DEACTIVATED
```

## 二、高频踩坑点

### 创建商品券踩坑

`POST /v3/marketing/partner/product-coupon/product-coupons`

- **券选型易混淆**：全场券（scope=ALL）适合拉新推广，单品券（scope=SINGLE）适合爆品精准营销；不确定时推荐从全场满减券开始
- **优惠字段易搞反**：全场券优惠在 `single_usage_info`，单品券在 `stock.single_usage_rule`，搞反会报 INVALID_REQUEST
- 单券用 `stock` 字段，多次优惠用 `stock_bundle` 字段
- 服务商模式**必须在请求体传 `brand_id`**
- 批次的开始时间与结束时间间隔必须 > 5天，否则无法创建投放计划；同一商品券最多 200 个生效中的批次
- 商品券图片（`image_url`）**仅支持通过图片上传 API 获取的 URL**，不可用外部链接
- 商品券详情图：JPG/PNG ≤ 2M，宽高比 65:77（推荐 1170×1326）；图标：透明 PNG ≤ 2M，1:1（建议 1080×1080，留 120px 透明边距）
- 商品券名称等文本字段支持 emoji，按 1 个字符计入长度限制
- `coupon_code_mode` 为 UPLOAD 时需先调预上传券 Code 接口；无自定义券码需求用默认 WECHATPAY 模式即可

### 发券踩坑

- **无论哪个渠道发券（API / 摇一摇 / 领券组件），都必须调用确认发放API**
- 未确认发放前**不要向用户展示"券已到账"**

### 核销踩坑

`POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/use`

- **必须关联真实订单**：`associated_order_info`（微信支付订单）或 `associated_pay_score_order_info`（支付分订单），二选一必传
- 核销前券必须为 `EFFECTIVE` 状态（未确认发放的券无法核销）
- `use_time` 格式 RFC3339，`out_request_no` 6-40字符（幂等标识，必须做幂等处理）
- **必须传 `brand_id`**


path: references/2-服务商/接入指南/API接口清单.md
content: # 服务商 — API 接口

请求域名：
- 主域名: `https://api.mch.weixin.qq.com`
- 备域名: `https://api2.mch.weixin.qq.com`

## 一、API 路径

服务商商品券 API 路径以 [REDACTED_SECRET] 开头，门店/名片 API 以 `/v3/brand/partner/` 开头。

| 操作 | 方法与路径 |
|------|-----------|
| 品牌入驻 | `POST /v3/brand/applyments` |
| 创建品牌门店 | `POST /v3/brand/partner/store/brandstores` |
| 创建商家名片 | `POST /v3/brand/partner/cardtemplate/brandcards` |
| 创建交易连接名片 | `POST /v3/brand/partner/cardtemplate/traderelation` |
| 设置回调地址 | `POST /v3/marketing/product-coupon/notify-config` |
| 创建商品券（单券） | `POST /v3/marketing/partner/product-coupon/product-coupons` |
| 创建商品券（多次优惠） | `POST /v3/marketing/partner/product-coupon/product-coupons` |
| 修改批次（单券） | `PATCH /v3/marketing/partner/product-coupon/product-coupons/{product_coupon_id}/stocks/{stock_id}` |
| 修改批次组（多次优惠） | `PATCH /v3/marketing/partner/product-coupon/product-coupons/{product_coupon_id}/stock-bundles/{bundle_id}` |
| 创建投放计划（摇优惠） | `POST /v3/marketing/partner/delivery-plan/delivery-plans` |
| 预发放商品券 | `POST /v3/marketing/partner/product-coupon/users/{openid}/pre-send-coupon` |
| 确认发放 | `POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/confirm` |
| 核销商品券 | `POST /v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/use` |

> 路径中的 `{product_coupon_id}` 为创建商品券时返回的商品券 ID。

### 服务商独有 API

以下操作在品牌直连中是 UI 操作，服务商模式**全部有 API**：

| 操作 | API | 说明 |
|------|-----|------|
| 品牌入驻 | `POST /v3/brand/applyments` | 帮品牌方提交入驻申请 |
| 创建商家名片 | `POST /v3/brand/partner/cardtemplate/brandcards` | 服务商独有 |
| 创建交易连接名片 | `POST /v3/brand/partner/cardtemplate/traderelation` | 关联AppID的关键步骤 |
| 创建投放计划 | `POST /v3/marketing/partner/delivery-plan/delivery-plans` | 摇优惠投放 |

## 二、支持的券类型

| # | 券类型 |
|---|--------|
| 1 | 全场折扣-单券 |
| 2 | 全场满减-单券 |
| 3 | 单品折扣-单券 |
| 4 | 单品满减-单券 |
| 5 | 单品兑换-单券 |
| 6 | 全场折扣-多次优惠 |
| 7 | 全场满减-多次优惠 |
| 8 | 单品折扣-多次优惠 |
| 9 | 单品满减-多次优惠 |
| 10 | 单品兑换-多次优惠 |

## 三、创建商品券关键参数约束

> 完整参数说明见官方文档：[服务商-创建商品券接口文档](https://pay.weixin.qq.com/doc/v3/partner/4015781289)

### 顶层参数

| 参数 | 必填 | 类型 | 约束 |
|------|------|------|------|
| out_request_no | 是 | string(40) | 6-40个字符，品牌侧唯一，支持数字、大小写字母、`_`、`-` |
| scope | 是 | string | `ALL`（全场，仅 NORMAL/DISCOUNT）或 `SINGLE`（单品，支持 NORMAL/DISCOUNT/EXCHANGE） |
| type | 是 | string | `NORMAL`（满减）、`DISCOUNT`（折扣）、`EXCHANGE`（兑换，仅 scope=SINGLE） |
| usage_mode | 是 | string | `SINGLE`（单券） |
| out_product_no | 否 | string(40) | 品牌侧商品标识，不校验唯一性 |
| brand_id | 是 | string | **服务商必填**，微信支付为品牌方分配的唯一标识，需与服务商存在授权关系 |

### display_info（展示信息）

| 参数 | 必填 | 类型 | 约束 |
|------|------|------|------|
| name | 是 | string(15) | **3-15 个 UTF-8 字符** |
| image_url | 是 | string | 仅支持图片上传API获取的URL，宽高比1:1，建议1080×1080，≤2M |
| background_url | 是 | string | 仅支持图片上传API获取的URL，宽高比65:77，建议1170×1326，≤2M |
| detail_image_url_list | 否 | array[string] | 最多 **8** 张 |
| original_price | scope=SINGLE 时必填 | integer | 单位：分 |
| combo_package_list | scope=SINGLE 时必填 | array[object] | 最多 **50** 个组合 |

### combo_package_list 内部

| 参数 | 必填 | 类型 | 约束 |
|------|------|------|------|
| name | 是 | string(15) | ≤15 个 UTF-8 字符 |
| pick_count | 是 | integer | 用户可选单品数量 |
| choice_list[].name | 是 | string(15) | ≤15 个 UTF-8 字符 |
| choice_list[].price | 是 | integer | 单位：分 |
| choice_list[].count | 是 | integer | 最多 99 |
| choice_list[].mini_program_appid | 否 | string | 单品跳转小程序AppID，需与品牌方或服务商存在绑定关系 |
| choice_list[].mini_program_path | 否 | string | 单品跳转小程序路径，需和 mini_program_appid 一同填写 |
| choice_list | 是 | array[object] | 最多 **99** 个单品 |

### single_usage_info（全场券优惠规则，scope=ALL 时必填）

| 条件 | 参数 | 必填字段 |
|------|------|---------|
| type=NORMAL | normal_coupon | threshold（分，0=无门槛）、discount_amount（分） |
| type=DISCOUNT | discount_coupon | threshold（分，0=无门槛）、percent_off（如 30=减30%即打7折） |

### stock（批次信息）

| 参数 | 必填 | 约束 |
|------|------|------|
| remark | 否 | ≤20 个 UTF-8 字符，仅品牌可见 |
| coupon_code_mode | 是 | `WECHATPAY` / `UPLOAD` / `API_ASSIGN` |
| stock_send_rule.max_count | 是 | ≤100,000,000 |
| stock_send_rule.max_count_per_day | 否 | ≤100,000,000，默认不限 |
| stock_send_rule.max_count_per_user | 是 | ≤100 |
| store_scope | 是 | `NONE` / `ALL` / `SPECIFIC` |

### stock.single_usage_rule（单券使用规则）

| 参数 | 必填 | 约束 |
|------|------|------|
| coupon_available_period.available_begin_time | 是 | RFC3339 格式，批次有效期最长 365 天，摇优惠场景建议 ≥5 天 |
| coupon_available_period.available_end_time | 是 | RFC3339 格式 |
| coupon_available_period.available_days | 否 | 券生效后 N 天内可用，最多 365 天，不可与 irregular_available_period_list 同时配置 |
| coupon_available_period.wait_days_after_receive | 否 | 领券后等待 N 天生效，最多 30 天，需配合 available_days，不可单独填写 |
| coupon_available_period.weekly_available_period | 否 | 每周固定可用时间，不可与 irregular_available_period_list 同时配置 |
| coupon_available_period.irregular_available_period_list | 否 | 指定可用时间段，最多 2 个区间，不可与 available_days/wait_days_after_receive/weekly_available_period 同时配置 |
| normal_coupon | scope=SINGLE 且 type=NORMAL 时必填 | threshold + discount_amount |
| discount_coupon | scope=SINGLE 且 type=DISCOUNT 时必填 | threshold + percent_off |
| exchange_coupon | scope=SINGLE 且 type=EXCHANGE 时必填 | threshold + exchange_price |

> **注意**：全场券（scope=ALL）的优惠规则放在顶层 `single_usage_info` 中，单品券（scope=SINGLE）的优惠规则放在 `stock.single_usage_rule` 中。

### stock.usage_rule_display_info

| 参数 | 必填 | 约束 |
|------|------|------|
| coupon_usage_method_list | 是 | `OFFLINE` / `MINI_PROGRAM` / `APP` / `PAYMENT_CODE`，可多选 |
| mini_program_appid | 含 MINI_PROGRAM 时必填 | 品牌方或服务商小程序AppID，需和 mini_program_path 一同填写 |
| app_path | 含 APP 时必填 | 品牌方或服务商APP跳转路径 |
| usage_description | 是 | ≤1000 个 UTF-8 字符 |
| coupon_available_store_info.description | 是 | ≤1000 个 UTF-8 字符 |

### stock.coupon_display_info

| 参数 | 必填 | 约束 |
|------|------|------|
| code_display_mode | 是 | `INVISIBLE` / `BARCODE` / `QRCODE` |
| background_color | 否 | 默认 Color020，可选 10 种颜色 |
| entrance_mini_program.entrance_wording | 是 | **≤5 个 UTF-8 字符** |
| entrance_mini_program.guidance_wording | 是 | **≤6 个 UTF-8 字符** |

### stock.notify_config

| 参数 | 必填 | 约束 |
|------|------|------|
| notify_appid | 是 | 服务商或品牌的AppID，支持小程序/服务号/公众号/APP 类型，需与服务商或品牌存在绑定关系 |

### 常见错误码

| 状态码 | 错误码 | 描述 | 解决方案 |
|--------|--------|------|---------|
| 400 | PARAM_ERROR | 参数错误 | 根据错误提示修正参数 |
| 400 | INVALID_REQUEST | 请求不符合业务规则 | 参考文档中字段要求及组合要求 |
| 400 | ALREADY_EXISTS | 商品券已存在 | 确认 out_request_no 是否重复 |
| 401 | SIGN_ERROR | 验证不通过 | 检查签名方案是否使用 WECHATPAY2-SHA256-RSA2048 |
| 429 | RATELIMIT_EXCEEDED | 超过频率限制（20/s） | 稍后使用原参数重试 |
| 500 | SYSTEM_ERROR | 系统异常 | 使用相同 out_request_no 重试 |

## 四、请求体差异（vs 品牌直连）

服务商模式**必须在请求体中传 `brand_id`** 字段，指定品牌方。


path: references/2-服务商/代码示例/Go/7-商品券回调通知/SetNotifyConfig.go
content: package main

import (
	"bytes"
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &SetNotifyConfigRequest{
		NotifyUrl: wxpay_utility.String("https://www.example.com/notify"),
	}

	response, err := SetNotifyConfig(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func SetNotifyConfig(config *wxpay_utility.MchConfig, request *SetNotifyConfigRequest) (response *SetNotifyConfigResponse, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "POST"
		path   = "/v3/marketing/partner/product-coupon/notify-configs"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	reqBody, err := json.Marshal(request)
	if err != nil {
		return nil, err
	}
	httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	httpRequest.Header.Set("Content-Type", "application/json")
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		// 2XX 成功，验证应答签名
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &SetNotifyConfigResponse{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}

		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type SetNotifyConfigRequest struct {
	NotifyUrl *string `json:"notify_url,omitempty"`
}

type SetNotifyConfigResponse struct {
	NotifyUrl  *string    `json:"notify_url,omitempty"`
	UpdateTime *time.Time `json:"update_time,omitempty"`
}


path: references/2-服务商/代码示例/Go/7-商品券回调通知/GetNotifyConfig.go
content: package main

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	response, err := GetNotifyConfig(config)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func GetNotifyConfig(config *wxpay_utility.MchConfig) (response *GetNotifyConfigResponse, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "GET"
		path   = "/v3/marketing/partner/product-coupon/notify-configs"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		// 2XX 成功，验证应答签名
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &GetNotifyConfigResponse{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}

		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type GetNotifyConfigResponse struct {
	NotifyUrl  *string    `json:"notify_url,omitempty"`
	UpdateTime *time.Time `json:"update_time,omitempty"`
}


path: references/2-服务商/代码示例/Go/6-商品券失效与退券/ReturnUserProductCoupon.go
content: package main

import (
	"bytes"
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &ReturnUserProductCouponRequest{
		ProductCouponId: wxpay_utility.String("1000000013"),
		StockId:         wxpay_utility.String("1000000013001"),
		CouponCode:      wxpay_utility.String("Code_123456"),
		Appid:           wxpay_utility.String("wx233544546545989"),
		Openid:          wxpay_utility.String("oh-394z-6CGkNoJrsDLTTUKiAnp4"),
		OutRequestNo:    wxpay_utility.String("MCHRETURN202003101234"),
		BrandId:         wxpay_utility.String("120344"),
	}

	response, err := ReturnUserProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func ReturnUserProductCoupon(config *wxpay_utility.MchConfig, request *ReturnUserProductCouponRequest) (response *UserProductCouponEntity, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "POST"
		path   = "/v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/return"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	reqUrl.Path = strings.Replace(reqUrl.Path, "{coupon_code}", url.PathEscape(*request.CouponCode), -1)
	reqUrl.Path = strings.Replace(reqUrl.Path, "{openid}", url.PathEscape(*request.Openid), -1)
	reqBody, err := json.Marshal(request)
	if err != nil {
		return nil, err
	}
	httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	httpRequest.Header.Set("Content-Type", "application/json")
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		// 2XX 成功，验证应答签名
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &UserProductCouponEntity{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}

		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type ReturnUserProductCouponRequest struct {
	ProductCouponId *string `json:"product_coupon_id,omitempty"`
	StockId         *string `json:"stock_id,omitempty"`
	CouponCode      *string `json:"coupon_code,omitempty"`
	Appid           *string `json:"appid,omitempty"`
	Openid          *string `json:"openid,omitempty"`
	OutRequestNo    *string `json:"out_request_no,omitempty"`
	BrandId         *string `json:"brand_id,omitempty"`
}

func (o *ReturnUserProductCouponRequest) MarshalJSON() ([]byte, error) {
	type Alias ReturnUserProductCouponRequest
	a := &struct {
		CouponCode *string `json:"coupon_code,omitempty"`
		Openid     *string `json:"openid,omitempty"`
		*Alias
	}{
		// 序列化时移除非 Body 字段
		CouponCode: nil,
		Openid:     nil,
		Alias:      (*Alias)(o),
	}
	return json.Marshal(a)
}

type UserProductCouponEntity struct {
	CouponCode                   *string                       `json:"coupon_code,omitempty"`
	CouponState                  *UserProductCouponState       `json:"coupon_state,omitempty"`
	ValidBeginTime               *time.Time                    `json:"valid_begin_time,omitempty"`
	ValidEndTime                 *time.Time                    `json:"valid_end_time,omitempty"`
	ReceiveTime                  *string                       `json:"receive_time,omitempty"`
	SendRequestNo                *string                       `json:"send_request_no,omitempty"`
	SendChannel                  *UserProductCouponSendChannel `json:"send_channel,omitempty"`
	ConfirmRequestNo             *string                       `json:"confirm_request_no,omitempty"`
	ConfirmTime                  *time.Time                    `json:"confirm_time,omitempty"`
	DeactivateRequestNo          *string                       `json:"deactivate_request_no,omitempty"`
	DeactivateTime               *string                       `json:"deactivate_time,omitempty"`
	DeactivateReason             *string                       `json:"deactivate_reason,omitempty"`
	SingleUsageDetail            *CouponUsageDetail            `json:"single_usage_detail,omitempty"`
	ProgressiveBundleUsageDetail *CouponUsageDetail            `json:"progressive_bundle_usage_detail,omitempty"`
	UserProductCouponBundleInfo  *UserProductCouponBundleInfo  `json:"user_product_coupon_bundle_info,omitempty"`
	ProductCoupon                *ProductCouponEntity          `json:"product_coupon,omitempty"`
	Stock                        *StockEntity                  `json:"stock,omitempty"`
	Attach                       *string                       `json:"attach,omitempty"`
	ChannelCustomInfo            *string                       `json:"channel_custom_info,omitempty"`
	CouponTagInfo                *CouponTagInfo                `json:"coupon_tag_info,omitempty"`
	BrandId                      *string                       `json:"brand_id,omitempty"`
}

type UserProductCouponState string

func (e UserProductCouponState) Ptr() *UserProductCouponState {
	return &e
}

const (
	USERPRODUCTCOUPONSTATE_CONFIRMING  UserProductCouponState = "CONFIRMING"
	USERPRODUCTCOUPONSTATE_PENDING     UserProductCouponState = "PENDING"
	USERPRODUCTCOUPONSTATE_EFFECTIVE   UserProductCouponState = "EFFECTIVE"
	USERPRODUCTCOUPONSTATE_USED        UserProductCouponState = "USED"
	USERPRODUCTCOUPONSTATE_EXPIRED     UserProductCouponState = "EXPIRED"
	USERPRODUCTCOUPONSTATE_DELETED     UserProductCouponState = "DELETED"
	USERPRODUCTCOUPONSTATE_DEACTIVATED UserProductCouponState = "DEACTIVATED"
)

type UserProductCouponSendChannel string

func (e UserProductCouponSendChannel) Ptr() *UserProductCouponSendChannel {
	return &e
}

const (
	USERPRODUCTCOUPONSENDCHANNEL_BRAND_MANAGE      UserProductCouponSendChannel = "BRAND_MANAGE"
	USERPRODUCTCOUPONSENDCHANNEL_API               UserProductCouponSendChannel = "API"
	USERPRODUCTCOUPONSENDCHANNEL_RECEIVE_COMPONENT UserProductCouponSendChannel = "RECEIVE_COMPONENT"
)

type CouponUsageDetail struct {
	UseRequestNo                *string                                       `json:"use_request_no,omitempty"`
	UseTime                     *time.Time                                    `json:"use_time,omitempty"`
	ReturnRequestNo             *string                                       `json:"return_request_no,omitempty"`
	ReturnTime                  *time.Time                                    `json:"return_time,omitempty"`
	AssociatedOrderInfo         *UserProductCouponAssociatedOrderInfo         `json:"associated_order_info,omitempty"`
	AssociatedPayScoreOrderInfo *UserProductCouponAssociatedPayScoreOrderInfo `json:"associated_pay_score_order_info,omitempty"`
}

type UserProductCouponBundleInfo struct {
	UserCouponBundleId    *string `json:"user_coupon_bundle_id,omitempty"`
	UserCouponBundleIndex *int64  `json:"user_coupon_bundle_index,omitempty"`
	TotalCount            *int64  `json:"total_count,omitempty"`
	UsedCount             *int64  `json:"used_count,omitempty"`
}

type ProductCouponEntity struct {
	ProductCouponId            *string                     `json:"product_coupon_id,omitempty"`
	Scope                      *ProductCouponScope         `json:"scope,omitempty"`
	Type                       *ProductCouponType          `json:"type,omitempty"`
	UsageMode                  *UsageMode                  `json:"usage_mode,omitempty"`
	SingleUsageInfo            *SingleUsageInfo            `json:"single_usage_info,omitempty"`
	ProgressiveBundleUsageInfo *ProgressiveBundleUsageInfo `json:"progressive_bundle_usage_info,omitempty"`
	DisplayInfo                *ProductCouponDisplayInfo   `json:"display_info,omitempty"`
	OutProductNo               *string                     `json:"out_product_no,omitempty"`
	State                      *ProductCouponState         `json:"state,omitempty"`
	DeactivateRequestNo        *string                     `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *string                     `json:"deactivate_time,omitempty"`
	DeactivateReason           *string                     `json:"deactivate_reason,omitempty"`
	BrandId                    *string                     `json:"brand_id,omitempty"`
}

type StockEntity struct {
	ProductCouponId            *string               `json:"product_coupon_id,omitempty"`
	StockId                    *string               `json:"stock_id,omitempty"`
	Remark                     *string               `json:"remark,omitempty"`
	CouponCodeMode             *CouponCodeMode       `json:"coupon_code_mode,omitempty"`
	CouponCodeCountInfo        *CouponCodeCountInfo  `json:"coupon_code_count_info,omitempty"`
	StockSendRule              *StockSendRule        `json:"stock_send_rule,omitempty"`
	SingleUsageRule            *SingleUsageRule      `json:"single_usage_rule,omitempty"`
	ProgressiveBundleUsageRule *StockUsageRule       `json:"progressive_bundle_usage_rule,omitempty"`
	StockBundleInfo            *StockBundleInfo      `json:"stock_bundle_info,omitempty"`
	UsageRuleDisplayInfo       *UsageRuleDisplayInfo `json:"usage_rule_display_info,omitempty"`
	CouponDisplayInfo          *CouponDisplayInfo    `json:"coupon_display_info,omitempty"`
	NotifyConfig               *NotifyConfig         `json:"notify_config,omitempty"`
	StoreScope                 *StockStoreScope      `json:"store_scope,omitempty"`
	SentCountInfo              *StockSentCountInfo   `json:"sent_count_info,omitempty"`
	State                      *StockState           `json:"state,omitempty"`
	DeactivateRequestNo        *string               `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *time.Time            `json:"deactivate_time,omitempty"`
	DeactivateReason           *string               `json:"deactivate_reason,omitempty"`
	BrandId                    *string               `json:"brand_id,omitempty"`
}

type CouponTagInfo struct {
	CouponTagList []UserProductCouponTag `json:"coupon_tag_list,omitempty"`
	MemberTagInfo *MemberTagInfo         `json:"member_tag_info,omitempty"`
}

type UserProductCouponAssociatedOrderInfo struct {
	TransactionId *string `json:"transaction_id,omitempty"`
	OutTradeNo    *string `json:"out_trade_no,omitempty"`
	Mchid         *string `json:"mchid,omitempty"`
	SubMchid      *string `json:"sub_mchid,omitempty"`
}

type UserProductCouponAssociatedPayScoreOrderInfo struct {
	OrderId    *string `json:"order_id,omitempty"`
	OutOrderNo *string `json:"out_order_no,omitempty"`
	Mchid      *string `json:"mchid,omitempty"`
	SubMchid   *string `json:"sub_mchid,omitempty"`
}

type ProductCouponScope string

func (e ProductCouponScope) Ptr() *ProductCouponScope { return &e }

const (
	PRODUCTCOUPONSCOPE_ALL    ProductCouponScope = "ALL"
	PRODUCTCOUPONSCOPE_SINGLE ProductCouponScope = "SINGLE"
)

type ProductCouponType string

func (e ProductCouponType) Ptr() *ProductCouponType { return &e }

const (
	PRODUCTCOUPONTYPE_NORMAL   ProductCouponType = "NORMAL"
	PRODUCTCOUPONTYPE_DISCOUNT ProductCouponType = "DISCOUNT"
	PRODUCTCOUPONTYPE_EXCHANGE ProductCouponType = "EXCHANGE"
)

type UsageMode string

func (e UsageMode) Ptr() *UsageMode { return &e }

const (
	USAGEMODE_SINGLE             UsageMode = "SINGLE"
	USAGEMODE_PROGRESSIVE_BUNDLE UsageMode = "PROGRESSIVE_BUNDLE"
)

type SingleUsageInfo struct {
	NormalCoupon   *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
}

type ProgressiveBundleUsageInfo struct {
	Count        *int64 `json:"count,omitempty"`
	IntervalDays *int64 `json:"interval_days,omitempty"`
}

type ProductCouponDisplayInfo struct {
	Name               *string        `json:"name,omitempty"`
	ImageUrl           *string        `json:"image_url,omitempty"`
	BackgroundUrl      *string        `json:"background_url,omitempty"`
	DetailImageUrlList []string       `json:"detail_image_url_list,omitempty"`
	OriginalPrice      *int64         `json:"original_price,omitempty"`
	ComboPackageList   []ComboPackage `json:"combo_package_list,omitempty"`
}

type ProductCouponState string

func (e ProductCouponState) Ptr() *ProductCouponState { return &e }

const (
	PRODUCTCOUPONSTATE_AUDITING    ProductCouponState = "AUDITING"
	PRODUCTCOUPONSTATE_EFFECTIVE   ProductCouponState = "EFFECTIVE"
	PRODUCTCOUPONSTATE_DEACTIVATED ProductCouponState = "DEACTIVATED"
)

type CouponCodeMode string

func (e CouponCodeMode) Ptr() *CouponCodeMode { return &e }

const (
	COUPONCODEMODE_WECHATPAY  CouponCodeMode = "WECHATPAY"
	COUPONCODEMODE_UPLOAD     CouponCodeMode = "UPLOAD"
	COUPONCODEMODE_API_ASSIGN CouponCodeMode = "API_ASSIGN"
)

type CouponCodeCountInfo struct {
	TotalCount     *int64 `json:"total_count,omitempty"`
	AvailableCount *int64 `json:"available_count,omitempty"`
}

type StockSendRule struct {
	MaxCount        *int64 `json:"max_count,omitempty"`
	MaxCountPerDay  *int64 `json:"max_count_per_day,omitempty"`
	MaxCountPerUser *int64 `json:"max_count_per_user,omitempty"`
}

type SingleUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type StockUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type StockBundleInfo struct {
	StockBundleId    *string `json:"stock_bundle_id,omitempty"`
	StockBundleIndex *int64  `json:"stock_bundle_index,omitempty"`
}

type UsageRuleDisplayInfo struct {
	CouponUsageMethodList    []CouponUsageMethod       `json:"coupon_usage_method_list,omitempty"`
	MiniProgramAppid         *string                   `json:"mini_program_appid,omitempty"`
	MiniProgramPath          *string                   `json:"mini_program_path,omitempty"`
	AppPath                  *string                   `json:"app_path,omitempty"`
	UsageDescription         *string                   `json:"usage_description,omitempty"`
	CouponAvailableStoreInfo *CouponAvailableStoreInfo `json:"coupon_available_store_info,omitempty"`
}

type CouponDisplayInfo struct {
	CodeDisplayMode         *CouponCodeDisplayMode   `json:"code_display_mode,omitempty"`
	BackgroundColor         *string                  `json:"background_color,omitempty"`
	EntranceMiniProgram     *EntranceMiniProgram     `json:"entrance_mini_program,omitempty"`
	EntranceOfficialAccount *EntranceOfficialAccount `json:"entrance_official_account,omitempty"`
	EntranceFinder          *EntranceFinder          `json:"entrance_finder,omitempty"`
}

type NotifyConfig struct {
	NotifyAppid *string `json:"notify_appid,omitempty"`
}

type StockStoreScope string

func (e StockStoreScope) Ptr() *StockStoreScope { return &e }

const (
	STOCKSTORESCOPE_NONE     StockStoreScope = "NONE"
	STOCKSTORESCOPE_ALL      StockStoreScope = "ALL"
	STOCKSTORESCOPE_SPECIFIC StockStoreScope = "SPECIFIC"
)

type StockSentCountInfo struct {
	TotalCount *int64 `json:"total_count,omitempty"`
	TodayCount *int64 `json:"today_count,omitempty"`
}

type StockState string

func (e StockState) Ptr() *StockState { return &e }

const (
	STOCKSTATE_AUDITING    StockState = "AUDITING"
	STOCKSTATE_SENDING     StockState = "SENDING"
	STOCKSTATE_PAUSED      StockState = "PAUSED"
	STOCKSTATE_STOPPED     StockState = "STOPPED"
	STOCKSTATE_DEACTIVATED StockState = "DEACTIVATED"
)

type UserProductCouponTag string

func (e UserProductCouponTag) Ptr() *UserProductCouponTag { return &e }

const (
	USERPRODUCTCOUPONTAG_MEMBER UserProductCouponTag = "MEMBER"
)

type MemberTagInfo struct {
	MemberCardId *string `json:"member_card_id,omitempty"`
}

type NormalCouponUsageRule struct {
	Threshold      *int64 `json:"threshold,omitempty"`
	DiscountAmount *int64 `json:"discount_amount,omitempty"`
}

type DiscountCouponUsageRule struct {
	Threshold  *int64 `json:"threshold,omitempty"`
	PercentOff *int64 `json:"percent_off,omitempty"`
}

type ComboPackage struct {
	Name       *string              `json:"name,omitempty"`
	PickCount  *int64               `json:"pick_count,omitempty"`
	ChoiceList []ComboPackageChoice `json:"choice_list,omitempty"`
}

type CouponAvailablePeriod struct {
	AvailableBeginTime           *string          `json:"available_begin_time,omitempty"`
	AvailableEndTime             *string          `json:"available_end_time,omitempty"`
	AvailableDays                *int64           `json:"available_days,omitempty"`
	WaitDaysAfterReceive         *int64           `json:"wait_days_after_receive,omitempty"`
	WeeklyAvailablePeriod        *FixedWeekPeriod `json:"weekly_available_period,omitempty"`
	IrregularAvailablePeriodList []TimePeriod     `json:"irregular_available_period_list,omitempty"`
}

type ExchangeCouponUsageRule struct {
	Threshold     *int64 `json:"threshold,omitempty"`
	ExchangePrice *int64 `json:"exchange_price,omitempty"`
}

type CouponUsageMethod string

func (e CouponUsageMethod) Ptr() *CouponUsageMethod { return &e }

const (
	COUPONUSAGEMETHOD_OFFLINE      CouponUsageMethod = "OFFLINE"
	COUPONUSAGEMETHOD_MINI_PROGRAM CouponUsageMethod = "MINI_PROGRAM"
	COUPONUSAGEMETHOD_APP          CouponUsageMethod = "APP"
	COUPONUSAGEMETHOD_PAYMENT_CODE CouponUsageMethod = "PAYMENT_CODE"
)

type CouponAvailableStoreInfo struct {
	Description      *string `json:"description,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type CouponCodeDisplayMode string

func (e CouponCodeDisplayMode) Ptr() *CouponCodeDisplayMode { return &e }

const (
	COUPONCODEDISPLAYMODE_INVISIBLE CouponCodeDisplayMode = "INVISIBLE"
	COUPONCODEDISPLAYMODE_BARCODE   CouponCodeDisplayMode = "BARCODE"
	COUPONCODEDISPLAYMODE_QRCODE    CouponCodeDisplayMode = "QRCODE"
)

type EntranceMiniProgram struct {
	Appid           *string `json:"appid,omitempty"`
	Path            *string `json:"path,omitempty"`
	EntranceWording *string `json:"entrance_wording,omitempty"`
	GuidanceWording *string `json:"guidance_wording,omitempty"`
}

type EntranceOfficialAccount struct {
	Appid *string `json:"appid,omitempty"`
}

type EntranceFinder struct {
	FinderId                 *string `json:"finder_id,omitempty"`
	FinderVideoId            *string `json:"finder_video_id,omitempty"`
	FinderVideoCoverImageUrl *string `json:"finder_video_cover_image_url,omitempty"`
}

type ComboPackageChoice struct {
	Name             *string `json:"name,omitempty"`
	Price            *int64  `json:"price,omitempty"`
	Count            *int64  `json:"count,omitempty"`
	ImageUrl         *string `json:"image_url,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type FixedWeekPeriod struct {
	DayList       []WeekEnum       `json:"day_list,omitempty"`
	DayPeriodList []PeriodOfTheDay `json:"day_period_list,omitempty"`
}

type TimePeriod struct {
	BeginTime *string `json:"begin_time,omitempty"`
	EndTime   *string `json:"end_time,omitempty"`
}

type WeekEnum string

func (e WeekEnum) Ptr() *WeekEnum { return &e }

const (
	WEEKENUM_MONDAY    WeekEnum = "MONDAY"
	WEEKENUM_TUESDAY   WeekEnum = "TUESDAY"
	WEEKENUM_WEDNESDAY WeekEnum = "WEDNESDAY"
	WEEKENUM_THURSDAY  WeekEnum = "THURSDAY"
	WEEKENUM_FRIDAY    WeekEnum = "FRIDAY"
	WEEKENUM_SATURDAY  WeekEnum = "SATURDAY"
	WEEKENUM_SUNDAY    WeekEnum = "SUNDAY"
)

type PeriodOfTheDay struct {
	BeginTime *int64 `json:"begin_time,omitempty"`
	EndTime   *int64 `json:"end_time,omitempty"`
}


path: references/2-服务商/代码示例/Go/6-商品券失效与退券/DeactivateUserProductCoupon.go
content: package main

import (
	"bytes"
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &DeactivateUserProductCouponRequest{
		ProductCouponId:  wxpay_utility.String("1000000013"),
		StockId:          wxpay_utility.String("1000000013001"),
		CouponCode:       wxpay_utility.String("Code_123456"),
		Appid:            wxpay_utility.String("wx233544546545989"),
		Openid:           wxpay_utility.String("oh-394z-6CGkNoJrsDLTTUKiAnp4"),
		OutRequestNo:     wxpay_utility.String("MCHDEACTIVATE202003101234"),
		DeactivateReason: wxpay_utility.String("商品已下线，使用户商品券失效"),
		BrandId:          wxpay_utility.String("120344"),
	}

	response, err := DeactivateUserProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func DeactivateUserProductCoupon(config *wxpay_utility.MchConfig, request *DeactivateUserProductCouponRequest) (response *UserProductCouponEntity, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "POST"
		path   = "/v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}/deactivate"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	reqUrl.Path = strings.Replace(reqUrl.Path, "{coupon_code}", url.PathEscape(*request.CouponCode), -1)
	reqUrl.Path = strings.Replace(reqUrl.Path, "{openid}", url.PathEscape(*request.Openid), -1)
	reqBody, err := json.Marshal(request)
	if err != nil {
		return nil, err
	}
	httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	httpRequest.Header.Set("Content-Type", "application/json")
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &UserProductCouponEntity{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}
		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type DeactivateUserProductCouponRequest struct {
	ProductCouponId  *string `json:"product_coupon_id,omitempty"`
	StockId          *string `json:"stock_id,omitempty"`
	CouponCode       *string `json:"coupon_code,omitempty"`
	Appid            *string `json:"appid,omitempty"`
	Openid           *string `json:"openid,omitempty"`
	OutRequestNo     *string `json:"out_request_no,omitempty"`
	DeactivateReason *string `json:"deactivate_reason,omitempty"`
	BrandId          *string `json:"brand_id,omitempty"`
}

func (o *DeactivateUserProductCouponRequest) MarshalJSON() ([]byte, error) {
	type Alias DeactivateUserProductCouponRequest
	a := &struct {
		CouponCode *string `json:"coupon_code,omitempty"`
		Openid     *string `json:"openid,omitempty"`
		*Alias
	}{
		CouponCode: nil,
		Openid:     nil,
		Alias:      (*Alias)(o),
	}
	return json.Marshal(a)
}

type UserProductCouponEntity struct {
	CouponCode          *string                       `json:"coupon_code,omitempty"`
	CouponState         *UserProductCouponState       `json:"coupon_state,omitempty"`
	ValidBeginTime      *time.Time                    `json:"valid_begin_time,omitempty"`
	ValidEndTime        *time.Time                    `json:"valid_end_time,omitempty"`
	ReceiveTime         *string                       `json:"receive_time,omitempty"`
	SendRequestNo       *string                       `json:"send_request_no,omitempty"`
	SendChannel         *UserProductCouponSendChannel `json:"send_channel,omitempty"`
	ConfirmRequestNo    *string                       `json:"confirm_request_no,omitempty"`
	ConfirmTime         *time.Time                    `json:"confirm_time,omitempty"`
	DeactivateRequestNo *string                       `json:"deactivate_request_no,omitempty"`
	DeactivateTime      *string                       `json:"deactivate_time,omitempty"`
	DeactivateReason    *string                       `json:"deactivate_reason,omitempty"`
	SingleUsageDetail   *CouponUsageDetail            `json:"single_usage_detail,omitempty"`
	ProductCoupon       *ProductCouponEntity          `json:"product_coupon,omitempty"`
	Stock               *StockEntity                  `json:"stock,omitempty"`
	Attach              *string                       `json:"attach,omitempty"`
	ChannelCustomInfo   *string                       `json:"channel_custom_info,omitempty"`
	CouponTagInfo       *CouponTagInfo                `json:"coupon_tag_info,omitempty"`
	BrandId             *string                       `json:"brand_id,omitempty"`
}

type UserProductCouponState string

func (e UserProductCouponState) Ptr() *UserProductCouponState { return &e }

const (
	USERPRODUCTCOUPONSTATE_CONFIRMING  UserProductCouponState = "CONFIRMING"
	USERPRODUCTCOUPONSTATE_PENDING     UserProductCouponState = "PENDING"
	USERPRODUCTCOUPONSTATE_EFFECTIVE   UserProductCouponState = "EFFECTIVE"
	USERPRODUCTCOUPONSTATE_USED        UserProductCouponState = "USED"
	USERPRODUCTCOUPONSTATE_EXPIRED     UserProductCouponState = "EXPIRED"
	USERPRODUCTCOUPONSTATE_DELETED     UserProductCouponState = "DELETED"
	USERPRODUCTCOUPONSTATE_DEACTIVATED UserProductCouponState = "DEACTIVATED"
)

type UserProductCouponSendChannel string

func (e UserProductCouponSendChannel) Ptr() *UserProductCouponSendChannel { return &e }

const (
	USERPRODUCTCOUPONSENDCHANNEL_BRAND_MANAGE      UserProductCouponSendChannel = "BRAND_MANAGE"
	USERPRODUCTCOUPONSENDCHANNEL_API               UserProductCouponSendChannel = "API"
	USERPRODUCTCOUPONSENDCHANNEL_RECEIVE_COMPONENT UserProductCouponSendChannel = "RECEIVE_COMPONENT"
)

type CouponUsageDetail struct {
	UseRequestNo                *string                                       `json:"use_request_no,omitempty"`
	UseTime                     *time.Time                                    `json:"use_time,omitempty"`
	ReturnRequestNo             *string                                       `json:"return_request_no,omitempty"`
	ReturnTime                  *time.Time                                    `json:"return_time,omitempty"`
	AssociatedOrderInfo         *UserProductCouponAssociatedOrderInfo         `json:"associated_order_info,omitempty"`
	AssociatedPayScoreOrderInfo *UserProductCouponAssociatedPayScoreOrderInfo `json:"associated_pay_score_order_info,omitempty"`
}

type ProductCouponEntity struct {
	ProductCouponId            *string                     `json:"product_coupon_id,omitempty"`
	Scope                      *ProductCouponScope         `json:"scope,omitempty"`
	Type                       *ProductCouponType          `json:"type,omitempty"`
	UsageMode                  *UsageMode                  `json:"usage_mode,omitempty"`
	SingleUsageInfo            *SingleUsageInfo            `json:"single_usage_info,omitempty"`
	ProgressiveBundleUsageInfo *ProgressiveBundleUsageInfo `json:"progressive_bundle_usage_info,omitempty"`
	DisplayInfo                *ProductCouponDisplayInfo   `json:"display_info,omitempty"`
	OutProductNo               *string                     `json:"out_product_no,omitempty"`
	State                      *ProductCouponState         `json:"state,omitempty"`
	DeactivateRequestNo        *string                     `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *string                     `json:"deactivate_time,omitempty"`
	DeactivateReason           *string                     `json:"deactivate_reason,omitempty"`
	BrandId                    *string                     `json:"brand_id,omitempty"`
}

type StockEntity struct {
	ProductCouponId      *string               `json:"product_coupon_id,omitempty"`
	StockId              *string               `json:"stock_id,omitempty"`
	Remark               *string               `json:"remark,omitempty"`
	CouponCodeMode       *CouponCodeMode       `json:"coupon_code_mode,omitempty"`
	CouponCodeCountInfo  *CouponCodeCountInfo  `json:"coupon_code_count_info,omitempty"`
	StockSendRule        *StockSendRule        `json:"stock_send_rule,omitempty"`
	SingleUsageRule      *SingleUsageRule      `json:"single_usage_rule,omitempty"`
	UsageRuleDisplayInfo *UsageRuleDisplayInfo `json:"usage_rule_display_info,omitempty"`
	CouponDisplayInfo    *CouponDisplayInfo    `json:"coupon_display_info,omitempty"`
	NotifyConfig         *NotifyConfig         `json:"notify_config,omitempty"`
	StoreScope           *StockStoreScope      `json:"store_scope,omitempty"`
	SentCountInfo        *StockSentCountInfo   `json:"sent_count_info,omitempty"`
	State                *StockState           `json:"state,omitempty"`
	DeactivateRequestNo  *string               `json:"deactivate_request_no,omitempty"`
	DeactivateTime       *time.Time            `json:"deactivate_time,omitempty"`
	DeactivateReason     *string               `json:"deactivate_reason,omitempty"`
	BrandId              *string               `json:"brand_id,omitempty"`
}

type CouponTagInfo struct {
	CouponTagList []UserProductCouponTag `json:"coupon_tag_list,omitempty"`
	MemberTagInfo *MemberTagInfo         `json:"member_tag_info,omitempty"`
}

type UserProductCouponAssociatedOrderInfo struct {
	TransactionId *string `json:"transaction_id,omitempty"`
	OutTradeNo    *string `json:"out_trade_no,omitempty"`
	Mchid         *string `json:"mchid,omitempty"`
	SubMchid      *string `json:"sub_mchid,omitempty"`
}

type UserProductCouponAssociatedPayScoreOrderInfo struct {
	OrderId    *string `json:"order_id,omitempty"`
	OutOrderNo *string `json:"out_order_no,omitempty"`
	Mchid      *string `json:"mchid,omitempty"`
	SubMchid   *string `json:"sub_mchid,omitempty"`
}

type ProductCouponScope string

func (e ProductCouponScope) Ptr() *ProductCouponScope { return &e }

const (
	PRODUCTCOUPONSCOPE_ALL    ProductCouponScope = "ALL"
	PRODUCTCOUPONSCOPE_SINGLE ProductCouponScope = "SINGLE"
)

type ProductCouponType string

func (e ProductCouponType) Ptr() *ProductCouponType { return &e }

const (
	PRODUCTCOUPONTYPE_NORMAL   ProductCouponType = "NORMAL"
	PRODUCTCOUPONTYPE_DISCOUNT ProductCouponType = "DISCOUNT"
	PRODUCTCOUPONTYPE_EXCHANGE ProductCouponType = "EXCHANGE"
)

type UsageMode string

func (e UsageMode) Ptr() *UsageMode { return &e }

const (
	USAGEMODE_SINGLE             UsageMode = "SINGLE"
	USAGEMODE_PROGRESSIVE_BUNDLE UsageMode = "PROGRESSIVE_BUNDLE"
)

type SingleUsageInfo struct {
	NormalCoupon   *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
}

type ProgressiveBundleUsageInfo struct {
	Count        *int64 `json:"count,omitempty"`
	IntervalDays *int64 `json:"interval_days,omitempty"`
}

type ProductCouponDisplayInfo struct {
	Name               *string        `json:"name,omitempty"`
	ImageUrl           *string        `json:"image_url,omitempty"`
	BackgroundUrl      *string        `json:"background_url,omitempty"`
	DetailImageUrlList []string       `json:"detail_image_url_list,omitempty"`
	OriginalPrice      *int64         `json:"original_price,omitempty"`
	ComboPackageList   []ComboPackage `json:"combo_package_list,omitempty"`
}

type ProductCouponState string

func (e ProductCouponState) Ptr() *ProductCouponState { return &e }

const (
	PRODUCTCOUPONSTATE_AUDITING    ProductCouponState = "AUDITING"
	PRODUCTCOUPONSTATE_EFFECTIVE   ProductCouponState = "EFFECTIVE"
	PRODUCTCOUPONSTATE_DEACTIVATED ProductCouponState = "DEACTIVATED"
)

type CouponCodeMode string

func (e CouponCodeMode) Ptr() *CouponCodeMode { return &e }

const (
	COUPONCODEMODE_WECHATPAY  CouponCodeMode = "WECHATPAY"
	COUPONCODEMODE_UPLOAD     CouponCodeMode = "UPLOAD"
	COUPONCODEMODE_API_ASSIGN CouponCodeMode = "API_ASSIGN"
)

type CouponCodeCountInfo struct {
	TotalCount     *int64 `json:"total_count,omitempty"`
	AvailableCount *int64 `json:"available_count,omitempty"`
}

type StockSendRule struct {
	MaxCount        *int64 `json:"max_count,omitempty"`
	MaxCountPerDay  *int64 `json:"max_count_per_day,omitempty"`
	MaxCountPerUser *int64 `json:"max_count_per_user,omitempty"`
}

type SingleUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type UsageRuleDisplayInfo struct {
	CouponUsageMethodList    []CouponUsageMethod       `json:"coupon_usage_method_list,omitempty"`
	MiniProgramAppid         *string                   `json:"mini_program_appid,omitempty"`
	MiniProgramPath          *string                   `json:"mini_program_path,omitempty"`
	AppPath                  *string                   `json:"app_path,omitempty"`
	UsageDescription         *string                   `json:"usage_description,omitempty"`
	CouponAvailableStoreInfo *CouponAvailableStoreInfo `json:"coupon_available_store_info,omitempty"`
}

type CouponDisplayInfo struct {
	CodeDisplayMode         *CouponCodeDisplayMode   `json:"code_display_mode,omitempty"`
	BackgroundColor         *string                  `json:"background_color,omitempty"`
	EntranceMiniProgram     *EntranceMiniProgram     `json:"entrance_mini_program,omitempty"`
	EntranceOfficialAccount *EntranceOfficialAccount `json:"entrance_official_account,omitempty"`
	EntranceFinder          *EntranceFinder          `json:"entrance_finder,omitempty"`
}

type NotifyConfig struct {
	NotifyAppid *string `json:"notify_appid,omitempty"`
}

type StockStoreScope string

func (e StockStoreScope) Ptr() *StockStoreScope { return &e }

const (
	STOCKSTORESCOPE_NONE     StockStoreScope = "NONE"
	STOCKSTORESCOPE_ALL      StockStoreScope = "ALL"
	STOCKSTORESCOPE_SPECIFIC StockStoreScope = "SPECIFIC"
)

type StockSentCountInfo struct {
	TotalCount *int64 `json:"total_count,omitempty"`
	TodayCount *int64 `json:"today_count,omitempty"`
}

type StockState string

func (e StockState) Ptr() *StockState { return &e }

const (
	STOCKSTATE_AUDITING    StockState = "AUDITING"
	STOCKSTATE_SENDING     StockState = "SENDING"
	STOCKSTATE_PAUSED      StockState = "PAUSED"
	STOCKSTATE_STOPPED     StockState = "STOPPED"
	STOCKSTATE_DEACTIVATED StockState = "DEACTIVATED"
)

type UserProductCouponTag string

func (e UserProductCouponTag) Ptr() *UserProductCouponTag { return &e }

const (
	USERPRODUCTCOUPONTAG_MEMBER UserProductCouponTag = "MEMBER"
)

type MemberTagInfo struct {
	MemberCardId *string `json:"member_card_id,omitempty"`
}

type NormalCouponUsageRule struct {
	Threshold      *int64 `json:"threshold,omitempty"`
	DiscountAmount *int64 `json:"discount_amount,omitempty"`
}

type DiscountCouponUsageRule struct {
	Threshold  *int64 `json:"threshold,omitempty"`
	PercentOff *int64 `json:"percent_off,omitempty"`
}

type ComboPackage struct {
	Name       *string              `json:"name,omitempty"`
	PickCount  *int64               `json:"pick_count,omitempty"`
	ChoiceList []ComboPackageChoice `json:"choice_list,omitempty"`
}

type CouponAvailablePeriod struct {
	AvailableBeginTime           *string          `json:"available_begin_time,omitempty"`
	AvailableEndTime             *string          `json:"available_end_time,omitempty"`
	AvailableDays                *int64           `json:"available_days,omitempty"`
	WaitDaysAfterReceive         *int64           `json:"wait_days_after_receive,omitempty"`
	WeeklyAvailablePeriod        *FixedWeekPeriod `json:"weekly_available_period,omitempty"`
	IrregularAvailablePeriodList []TimePeriod     `json:"irregular_available_period_list,omitempty"`
}

type ExchangeCouponUsageRule struct {
	Threshold     *int64 `json:"threshold,omitempty"`
	ExchangePrice *int64 `json:"exchange_price,omitempty"`
}

type CouponUsageMethod string

func (e CouponUsageMethod) Ptr() *CouponUsageMethod { return &e }

const (
	COUPONUSAGEMETHOD_OFFLINE      CouponUsageMethod = "OFFLINE"
	COUPONUSAGEMETHOD_MINI_PROGRAM CouponUsageMethod = "MINI_PROGRAM"
	COUPONUSAGEMETHOD_APP          CouponUsageMethod = "APP"
	COUPONUSAGEMETHOD_PAYMENT_CODE CouponUsageMethod = "PAYMENT_CODE"
)

type CouponAvailableStoreInfo struct {
	Description      *string `json:"description,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type CouponCodeDisplayMode string

func (e CouponCodeDisplayMode) Ptr() *CouponCodeDisplayMode { return &e }

const (
	COUPONCODEDISPLAYMODE_INVISIBLE CouponCodeDisplayMode = "INVISIBLE"
	COUPONCODEDISPLAYMODE_BARCODE   CouponCodeDisplayMode = "BARCODE"
	COUPONCODEDISPLAYMODE_QRCODE    CouponCodeDisplayMode = "QRCODE"
)

type EntranceMiniProgram struct {
	Appid           *string `json:"appid,omitempty"`
	Path            *string `json:"path,omitempty"`
	EntranceWording *string `json:"entrance_wording,omitempty"`
	GuidanceWording *string `json:"guidance_wording,omitempty"`
}

type EntranceOfficialAccount struct {
	Appid *string `json:"appid,omitempty"`
}

type EntranceFinder struct {
	FinderId                 *string `json:"finder_id,omitempty"`
	FinderVideoId            *string `json:"finder_video_id,omitempty"`
	FinderVideoCoverImageUrl *string `json:"finder_video_cover_image_url,omitempty"`
}

type ComboPackageChoice struct {
	Name             *string `json:"name,omitempty"`
	Price            *int64  `json:"price,omitempty"`
	Count            *int64  `json:"count,omitempty"`
	ImageUrl         *string `json:"image_url,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type FixedWeekPeriod struct {
	DayList       []WeekEnum       `json:"day_list,omitempty"`
	DayPeriodList []PeriodOfTheDay `json:"day_period_list,omitempty"`
}

type TimePeriod struct {
	BeginTime *string `json:"begin_time,omitempty"`
	EndTime   *string `json:"end_time,omitempty"`
}

type WeekEnum string

func (e WeekEnum) Ptr() *WeekEnum { return &e }

const (
	WEEKENUM_MONDAY    WeekEnum = "MONDAY"
	WEEKENUM_TUESDAY   WeekEnum = "TUESDAY"
	WEEKENUM_WEDNESDAY WeekEnum = "WEDNESDAY"
	WEEKENUM_THURSDAY  WeekEnum = "THURSDAY"
	WEEKENUM_FRIDAY    WeekEnum = "FRIDAY"
	WEEKENUM_SATURDAY  WeekEnum = "SATURDAY"
	WEEKENUM_SUNDAY    WeekEnum = "SUNDAY"
)

type PeriodOfTheDay struct {
	BeginTime *int64 `json:"begin_time,omitempty"`
	EndTime   *int64 `json:"end_time,omitempty"`
}


path: references/2-服务商/代码示例/Go/5-商品券查询/GetUserProductCoupon.go
content: package main

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &GetUserProductCouponRequest{
		ProductCouponId: wxpay_utility.String("1000000013"),
		StockId:         wxpay_utility.String("1000000013001"),
		CouponCode:      wxpay_utility.String("Code_123456"),
		Appid:           wxpay_utility.String("wx233544546545989"),
		Openid:          wxpay_utility.String("oh-394z-6CGkNoJrsDLTTUKiAnp4"),
		BrandId:         wxpay_utility.String("120344"),
	}

	response, err := GetUserProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func GetUserProductCoupon(config *wxpay_utility.MchConfig, request *GetUserProductCouponRequest) (response *UserProductCouponEntity, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "GET"
		path   = "/v3/marketing/partner/product-coupon/users/{openid}/coupons/{coupon_code}"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	reqUrl.Path = strings.Replace(reqUrl.Path, "{coupon_code}", url.PathEscape(*request.CouponCode), -1)
	reqUrl.Path = strings.Replace(reqUrl.Path, "{openid}", url.PathEscape(*request.Openid), -1)
	query := reqUrl.Query()
	if request.ProductCouponId != nil {
		query.Add("product_coupon_id", *request.ProductCouponId)
	}
	if request.StockId != nil {
		query.Add("stock_id", *request.StockId)
	}
	if request.Appid != nil {
		query.Add("appid", *request.Appid)
	}
	if request.BrandId != nil {
		query.Add("brand_id", *request.BrandId)
	}
	reqUrl.RawQuery = query.Encode()
	httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		// 2XX 成功，验证应答签名
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &UserProductCouponEntity{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}

		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type GetUserProductCouponRequest struct {
	ProductCouponId *string `json:"product_coupon_id,omitempty"`
	StockId         *string `json:"stock_id,omitempty"`
	CouponCode      *string `json:"coupon_code,omitempty"`
	Appid           *string `json:"appid,omitempty"`
	Openid          *string `json:"openid,omitempty"`
	BrandId         *string `json:"brand_id,omitempty"`
}

func (o *GetUserProductCouponRequest) MarshalJSON() ([]byte, error) {
	type Alias GetUserProductCouponRequest
	a := &struct {
		ProductCouponId *string `json:"product_coupon_id,omitempty"`
		StockId         *string `json:"stock_id,omitempty"`
		CouponCode      *string `json:"coupon_code,omitempty"`
		Appid           *string `json:"appid,omitempty"`
		Openid          *string `json:"openid,omitempty"`
		BrandId         *string `json:"brand_id,omitempty"`
		*Alias
	}{
		// 序列化时移除非 Body 字段
		ProductCouponId: nil,
		StockId:         nil,
		CouponCode:      nil,
		Appid:           nil,
		Openid:          nil,
		BrandId:         nil,
		Alias:           (*Alias)(o),
	}
	return json.Marshal(a)
}

type UserProductCouponEntity struct {
	CouponCode                   *string                       `json:"coupon_code,omitempty"`
	CouponState                  *UserProductCouponState       `json:"coupon_state,omitempty"`
	ValidBeginTime               *time.Time                    `json:"valid_begin_time,omitempty"`
	ValidEndTime                 *time.Time                    `json:"valid_end_time,omitempty"`
	ReceiveTime                  *string                       `json:"receive_time,omitempty"`
	SendRequestNo                *string                       `json:"send_request_no,omitempty"`
	SendChannel                  *UserProductCouponSendChannel `json:"send_channel,omitempty"`
	ConfirmRequestNo             *string                       `json:"confirm_request_no,omitempty"`
	ConfirmTime                  *time.Time                    `json:"confirm_time,omitempty"`
	DeactivateRequestNo          *string                       `json:"deactivate_request_no,omitempty"`
	DeactivateTime               *string                       `json:"deactivate_time,omitempty"`
	DeactivateReason             *string                       `json:"deactivate_reason,omitempty"`
	SingleUsageDetail            *CouponUsageDetail            `json:"single_usage_detail,omitempty"`
	ProgressiveBundleUsageDetail *CouponUsageDetail            `json:"progressive_bundle_usage_detail,omitempty"`
	UserProductCouponBundleInfo  *UserProductCouponBundleInfo  `json:"user_product_coupon_bundle_info,omitempty"`
	ProductCoupon                *ProductCouponEntity          `json:"product_coupon,omitempty"`
	Stock                        *StockEntity                  `json:"stock,omitempty"`
	Attach                       *string                       `json:"attach,omitempty"`
	ChannelCustomInfo            *string                       `json:"channel_custom_info,omitempty"`
	CouponTagInfo                *CouponTagInfo                `json:"coupon_tag_info,omitempty"`
	BrandId                      *string                       `json:"brand_id,omitempty"`
}

type UserProductCouponState string

func (e UserProductCouponState) Ptr() *UserProductCouponState { return &e }

const (
	USERPRODUCTCOUPONSTATE_CONFIRMING  UserProductCouponState = "CONFIRMING"
	USERPRODUCTCOUPONSTATE_PENDING     UserProductCouponState = "PENDING"
	USERPRODUCTCOUPONSTATE_EFFECTIVE   UserProductCouponState = "EFFECTIVE"
	USERPRODUCTCOUPONSTATE_USED        UserProductCouponState = "USED"
	USERPRODUCTCOUPONSTATE_EXPIRED     UserProductCouponState = "EXPIRED"
	USERPRODUCTCOUPONSTATE_DELETED     UserProductCouponState = "DELETED"
	USERPRODUCTCOUPONSTATE_DEACTIVATED UserProductCouponState = "DEACTIVATED"
)

type UserProductCouponSendChannel string

func (e UserProductCouponSendChannel) Ptr() *UserProductCouponSendChannel { return &e }

const (
	USERPRODUCTCOUPONSENDCHANNEL_BRAND_MANAGE      UserProductCouponSendChannel = "BRAND_MANAGE"
	USERPRODUCTCOUPONSENDCHANNEL_API               UserProductCouponSendChannel = "API"
	USERPRODUCTCOUPONSENDCHANNEL_RECEIVE_COMPONENT UserProductCouponSendChannel = "RECEIVE_COMPONENT"
)

type CouponUsageDetail struct {
	UseRequestNo                *string                                       `json:"use_request_no,omitempty"`
	UseTime                     *time.Time                                    `json:"use_time,omitempty"`
	ReturnRequestNo             *string                                       `json:"return_request_no,omitempty"`
	ReturnTime                  *time.Time                                    `json:"return_time,omitempty"`
	AssociatedOrderInfo         *UserProductCouponAssociatedOrderInfo         `json:"associated_order_info,omitempty"`
	AssociatedPayScoreOrderInfo *UserProductCouponAssociatedPayScoreOrderInfo `json:"associated_pay_score_order_info,omitempty"`
}

type UserProductCouponBundleInfo struct {
	UserCouponBundleId    *string `json:"user_coupon_bundle_id,omitempty"`
	UserCouponBundleIndex *int64  `json:"user_coupon_bundle_index,omitempty"`
	TotalCount            *int64  `json:"total_count,omitempty"`
	UsedCount             *int64  `json:"used_count,omitempty"`
}

type ProductCouponEntity struct {
	ProductCouponId            *string                     `json:"product_coupon_id,omitempty"`
	Scope                      *ProductCouponScope         `json:"scope,omitempty"`
	Type                       *ProductCouponType          `json:"type,omitempty"`
	UsageMode                  *UsageMode                  `json:"usage_mode,omitempty"`
	SingleUsageInfo            *SingleUsageInfo            `json:"single_usage_info,omitempty"`
	ProgressiveBundleUsageInfo *ProgressiveBundleUsageInfo `json:"progressive_bundle_usage_info,omitempty"`
	DisplayInfo                *ProductCouponDisplayInfo   `json:"display_info,omitempty"`
	OutProductNo               *string                     `json:"out_product_no,omitempty"`
	State                      *ProductCouponState         `json:"state,omitempty"`
	DeactivateRequestNo        *string                     `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *string                     `json:"deactivate_time,omitempty"`
	DeactivateReason           *string                     `json:"deactivate_reason,omitempty"`
	BrandId                    *string                     `json:"brand_id,omitempty"`
}

type StockEntity struct {
	ProductCouponId            *string               `json:"product_coupon_id,omitempty"`
	StockId                    *string               `json:"stock_id,omitempty"`
	Remark                     *string               `json:"remark,omitempty"`
	CouponCodeMode             *CouponCodeMode       `json:"coupon_code_mode,omitempty"`
	CouponCodeCountInfo        *CouponCodeCountInfo  `json:"coupon_code_count_info,omitempty"`
	StockSendRule              *StockSendRule        `json:"stock_send_rule,omitempty"`
	SingleUsageRule            *SingleUsageRule      `json:"single_usage_rule,omitempty"`
	ProgressiveBundleUsageRule *StockUsageRule       `json:"progressive_bundle_usage_rule,omitempty"`
	StockBundleInfo            *StockBundleInfo      `json:"stock_bundle_info,omitempty"`
	UsageRuleDisplayInfo       *UsageRuleDisplayInfo `json:"usage_rule_display_info,omitempty"`
	CouponDisplayInfo          *CouponDisplayInfo    `json:"coupon_display_info,omitempty"`
	NotifyConfig               *NotifyConfig         `json:"notify_config,omitempty"`
	StoreScope                 *StockStoreScope      `json:"store_scope,omitempty"`
	SentCountInfo              *StockSentCountInfo   `json:"sent_count_info,omitempty"`
	State                      *StockState           `json:"state,omitempty"`
	DeactivateRequestNo        *string               `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *time.Time            `json:"deactivate_time,omitempty"`
	DeactivateReason           *string               `json:"deactivate_reason,omitempty"`
	BrandId                    *string               `json:"brand_id,omitempty"`
}

type CouponTagInfo struct {
	CouponTagList []UserProductCouponTag `json:"coupon_tag_list,omitempty"`
	MemberTagInfo *MemberTagInfo         `json:"member_tag_info,omitempty"`
}

type UserProductCouponAssociatedOrderInfo struct {
	TransactionId *string `json:"transaction_id,omitempty"`
	OutTradeNo    *string `json:"out_trade_no,omitempty"`
	Mchid         *string `json:"mchid,omitempty"`
	SubMchid      *string `json:"sub_mchid,omitempty"`
}

type UserProductCouponAssociatedPayScoreOrderInfo struct {
	OrderId    *string `json:"order_id,omitempty"`
	OutOrderNo *string `json:"out_order_no,omitempty"`
	Mchid      *string `json:"mchid,omitempty"`
	SubMchid   *string `json:"sub_mchid,omitempty"`
}

type ProductCouponScope string

func (e ProductCouponScope) Ptr() *ProductCouponScope { return &e }

const (
	PRODUCTCOUPONSCOPE_ALL    ProductCouponScope = "ALL"
	PRODUCTCOUPONSCOPE_SINGLE ProductCouponScope = "SINGLE"
)

type ProductCouponType string

func (e ProductCouponType) Ptr() *ProductCouponType { return &e }

const (
	PRODUCTCOUPONTYPE_NORMAL   ProductCouponType = "NORMAL"
	PRODUCTCOUPONTYPE_DISCOUNT ProductCouponType = "DISCOUNT"
	PRODUCTCOUPONTYPE_EXCHANGE ProductCouponType = "EXCHANGE"
)

type UsageMode string

func (e UsageMode) Ptr() *UsageMode { return &e }

const (
	USAGEMODE_SINGLE             UsageMode = "SINGLE"
	USAGEMODE_PROGRESSIVE_BUNDLE UsageMode = "PROGRESSIVE_BUNDLE"
)

type SingleUsageInfo struct {
	NormalCoupon   *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
}

type ProgressiveBundleUsageInfo struct {
	Count        *int64 `json:"count,omitempty"`
	IntervalDays *int64 `json:"interval_days,omitempty"`
}

type ProductCouponDisplayInfo struct {
	Name               *string        `json:"name,omitempty"`
	ImageUrl           *string        `json:"image_url,omitempty"`
	BackgroundUrl      *string        `json:"background_url,omitempty"`
	DetailImageUrlList []string       `json:"detail_image_url_list,omitempty"`
	OriginalPrice      *int64         `json:"original_price,omitempty"`
	ComboPackageList   []ComboPackage `json:"combo_package_list,omitempty"`
}

type ProductCouponState string

func (e ProductCouponState) Ptr() *ProductCouponState { return &e }

const (
	PRODUCTCOUPONSTATE_AUDITING    ProductCouponState = "AUDITING"
	PRODUCTCOUPONSTATE_EFFECTIVE   ProductCouponState = "EFFECTIVE"
	PRODUCTCOUPONSTATE_DEACTIVATED ProductCouponState = "DEACTIVATED"
)

type CouponCodeMode string

func (e CouponCodeMode) Ptr() *CouponCodeMode { return &e }

const (
	COUPONCODEMODE_WECHATPAY  CouponCodeMode = "WECHATPAY"
	COUPONCODEMODE_UPLOAD     CouponCodeMode = "UPLOAD"
	COUPONCODEMODE_API_ASSIGN CouponCodeMode = "API_ASSIGN"
)

type CouponCodeCountInfo struct {
	TotalCount     *int64 `json:"total_count,omitempty"`
	AvailableCount *int64 `json:"available_count,omitempty"`
}

type StockSendRule struct {
	MaxCount        *int64 `json:"max_count,omitempty"`
	MaxCountPerDay  *int64 `json:"max_count_per_day,omitempty"`
	MaxCountPerUser *int64 `json:"max_count_per_user,omitempty"`
}

type SingleUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type StockUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type StockBundleInfo struct {
	StockBundleId    *string `json:"stock_bundle_id,omitempty"`
	StockBundleIndex *int64  `json:"stock_bundle_index,omitempty"`
}

type UsageRuleDisplayInfo struct {
	CouponUsageMethodList    []CouponUsageMethod       `json:"coupon_usage_method_list,omitempty"`
	MiniProgramAppid         *string                   `json:"mini_program_appid,omitempty"`
	MiniProgramPath          *string                   `json:"mini_program_path,omitempty"`
	AppPath                  *string                   `json:"app_path,omitempty"`
	UsageDescription         *string                   `json:"usage_description,omitempty"`
	CouponAvailableStoreInfo *CouponAvailableStoreInfo `json:"coupon_available_store_info,omitempty"`
}

type CouponDisplayInfo struct {
	CodeDisplayMode         *CouponCodeDisplayMode   `json:"code_display_mode,omitempty"`
	BackgroundColor         *string                  `json:"background_color,omitempty"`
	EntranceMiniProgram     *EntranceMiniProgram     `json:"entrance_mini_program,omitempty"`
	EntranceOfficialAccount *EntranceOfficialAccount `json:"entrance_official_account,omitempty"`
	EntranceFinder          *EntranceFinder          `json:"entrance_finder,omitempty"`
}

type NotifyConfig struct {
	NotifyAppid *string `json:"notify_appid,omitempty"`
}

type StockStoreScope string

func (e StockStoreScope) Ptr() *StockStoreScope { return &e }

const (
	STOCKSTORESCOPE_NONE     StockStoreScope = "NONE"
	STOCKSTORESCOPE_ALL      StockStoreScope = "ALL"
	STOCKSTORESCOPE_SPECIFIC StockStoreScope = "SPECIFIC"
)

type StockSentCountInfo struct {
	TotalCount *int64 `json:"total_count,omitempty"`
	TodayCount *int64 `json:"today_count,omitempty"`
}

type StockState string

func (e StockState) Ptr() *StockState { return &e }

const (
	STOCKSTATE_AUDITING    StockState = "AUDITING"
	STOCKSTATE_SENDING     StockState = "SENDING"
	STOCKSTATE_PAUSED      StockState = "PAUSED"
	STOCKSTATE_STOPPED     StockState = "STOPPED"
	STOCKSTATE_DEACTIVATED StockState = "DEACTIVATED"
)

type UserProductCouponTag string

func (e UserProductCouponTag) Ptr() *UserProductCouponTag { return &e }

const (
	USERPRODUCTCOUPONTAG_MEMBER UserProductCouponTag = "MEMBER"
)

type MemberTagInfo struct {
	MemberCardId *string `json:"member_card_id,omitempty"`
}

type NormalCouponUsageRule struct {
	Threshold      *int64 `json:"threshold,omitempty"`
	DiscountAmount *int64 `json:"discount_amount,omitempty"`
}

type DiscountCouponUsageRule struct {
	Threshold  *int64 `json:"threshold,omitempty"`
	PercentOff *int64 `json:"percent_off,omitempty"`
}

type ComboPackage struct {
	Name       *string              `json:"name,omitempty"`
	PickCount  *int64               `json:"pick_count,omitempty"`
	ChoiceList []ComboPackageChoice `json:"choice_list,omitempty"`
}

type CouponAvailablePeriod struct {
	AvailableBeginTime           *string          `json:"available_begin_time,omitempty"`
	AvailableEndTime             *string          `json:"available_end_time,omitempty"`
	AvailableDays                *int64           `json:"available_days,omitempty"`
	WaitDaysAfterReceive         *int64           `json:"wait_days_after_receive,omitempty"`
	WeeklyAvailablePeriod        *FixedWeekPeriod `json:"weekly_available_period,omitempty"`
	IrregularAvailablePeriodList []TimePeriod     `json:"irregular_available_period_list,omitempty"`
}

type ExchangeCouponUsageRule struct {
	Threshold     *int64 `json:"threshold,omitempty"`
	ExchangePrice *int64 `json:"exchange_price,omitempty"`
}

type CouponUsageMethod string

func (e CouponUsageMethod) Ptr() *CouponUsageMethod { return &e }

const (
	COUPONUSAGEMETHOD_OFFLINE      CouponUsageMethod = "OFFLINE"
	COUPONUSAGEMETHOD_MINI_PROGRAM CouponUsageMethod = "MINI_PROGRAM"
	COUPONUSAGEMETHOD_APP          CouponUsageMethod = "APP"
	COUPONUSAGEMETHOD_PAYMENT_CODE CouponUsageMethod = "PAYMENT_CODE"
)

type CouponAvailableStoreInfo struct {
	Description      *string `json:"description,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type CouponCodeDisplayMode string

func (e CouponCodeDisplayMode) Ptr() *CouponCodeDisplayMode { return &e }

const (
	COUPONCODEDISPLAYMODE_INVISIBLE CouponCodeDisplayMode = "INVISIBLE"
	COUPONCODEDISPLAYMODE_BARCODE   CouponCodeDisplayMode = "BARCODE"
	COUPONCODEDISPLAYMODE_QRCODE    CouponCodeDisplayMode = "QRCODE"
)

type EntranceMiniProgram struct {
	Appid           *string `json:"appid,omitempty"`
	Path            *string `json:"path,omitempty"`
	EntranceWording *string `json:"entrance_wording,omitempty"`
	GuidanceWording *string `json:"guidance_wording,omitempty"`
}

type EntranceOfficialAccount struct {
	Appid *string `json:"appid,omitempty"`
}

type EntranceFinder struct {
	FinderId                 *string `json:"finder_id,omitempty"`
	FinderVideoId            *string `json:"finder_video_id,omitempty"`
	FinderVideoCoverImageUrl *string `json:"finder_video_cover_image_url,omitempty"`
}

type ComboPackageChoice struct {
	Name             *string `json:"name,omitempty"`
	Price            *int64  `json:"price,omitempty"`
	Count            *int64  `json:"count,omitempty"`
	ImageUrl         *string `json:"image_url,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type FixedWeekPeriod struct {
	DayList       []WeekEnum       `json:"day_list,omitempty"`
	DayPeriodList []PeriodOfTheDay `json:"day_period_list,omitempty"`
}

type TimePeriod struct {
	BeginTime *string `json:"begin_time,omitempty"`
	EndTime   *string `json:"end_time,omitempty"`
}

type WeekEnum string

func (e WeekEnum) Ptr() *WeekEnum { return &e }

const (
	WEEKENUM_MONDAY    WeekEnum = "MONDAY"
	WEEKENUM_TUESDAY   WeekEnum = "TUESDAY"
	WEEKENUM_WEDNESDAY WeekEnum = "WEDNESDAY"
	WEEKENUM_THURSDAY  WeekEnum = "THURSDAY"
	WEEKENUM_FRIDAY    WeekEnum = "FRIDAY"
	WEEKENUM_SATURDAY  WeekEnum = "SATURDAY"
	WEEKENUM_SUNDAY    WeekEnum = "SUNDAY"
)

type PeriodOfTheDay struct {
	BeginTime *int64 `json:"begin_time,omitempty"`
	EndTime   *int64 `json:"end_time,omitempty"`
}


path: references/2-服务商/代码示例/Go/5-商品券查询/ListUserProductCoupons.go
content: package main

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &ListUserProductCouponsRequest{
		ProductCouponId: wxpay_utility.String("1000000013"),
		StockId:         wxpay_utility.String("1000000013001"),
		Appid:           wxpay_utility.String("wx233544546545989"),
		Openid:          wxpay_utility.String("oh-394z-6CGkNoJrsDLTTUKiAnp4"),
		PageSize:        wxpay_utility.Int64(10),
		BrandId:         wxpay_utility.String("120344"),
	}

	response, err := ListUserProductCoupons(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func ListUserProductCoupons(config *wxpay_utility.MchConfig, request *ListUserProductCouponsRequest) (response *ListUserProductCouponsResponse, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "GET"
		path   = "/v3/marketing/partner/product-coupon/users/{openid}/coupons"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	reqUrl.Path = strings.Replace(reqUrl.Path, "{openid}", url.PathEscape(*request.Openid), -1)
	query := reqUrl.Query()
	if request.ProductCouponId != nil {
		query.Add("product_coupon_id", *request.ProductCouponId)
	}
	if request.StockId != nil {
		query.Add("stock_id", *request.StockId)
	}
	if request.Appid != nil {
		query.Add("appid", *request.Appid)
	}
	if request.CouponState != nil {
		query.Add("coupon_state", fmt.Sprintf("%v", *request.CouponState))
	}
	if request.UserCouponBundleId != nil {
		query.Add("user_coupon_bundle_id", *request.UserCouponBundleId)
	}
	if request.PageSize != nil {
		query.Add("page_size", fmt.Sprintf("%v", *request.PageSize))
	}
	if request.PageToken != nil {
		query.Add("page_token", *request.PageToken)
	}
	if request.BrandId != nil {
		query.Add("brand_id", *request.BrandId)
	}
	reqUrl.RawQuery = query.Encode()
	httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		// 2XX 成功，验证应答签名
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &ListUserProductCouponsResponse{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}

		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type ListUserProductCouponsRequest struct {
	ProductCouponId    *string                 `json:"product_coupon_id,omitempty"`
	StockId            *string                 `json:"stock_id,omitempty"`
	Appid              *string                 `json:"appid,omitempty"`
	Openid             *string                 `json:"openid,omitempty"`
	UserCouponBundleId *string                 `json:"user_coupon_bundle_id,omitempty"`
	PageSize           *int64                  `json:"page_size,omitempty"`
	PageToken          *string                 `json:"page_token,omitempty"`
	BrandId            *string                 `json:"brand_id,omitempty"`
	CouponState        *UserProductCouponState `json:"coupon_state,omitempty"`
}

func (o *ListUserProductCouponsRequest) MarshalJSON() ([]byte, error) {
	type Alias ListUserProductCouponsRequest
	a := &struct {
		ProductCouponId    *string                 `json:"product_coupon_id,omitempty"`
		StockId            *string                 `json:"stock_id,omitempty"`
		Appid              *string                 `json:"appid,omitempty"`
		Openid             *string                 `json:"openid,omitempty"`
		UserCouponBundleId *string                 `json:"user_coupon_bundle_id,omitempty"`
		PageSize           *int64                  `json:"page_size,omitempty"`
		PageToken          *string                 `json:"page_token,omitempty"`
		BrandId            *string                 `json:"brand_id,omitempty"`
		CouponState        *UserProductCouponState `json:"coupon_state,omitempty"`
		*Alias
	}{
		// 序列化时移除非 Body 字段
		ProductCouponId:    nil,
		StockId:            nil,
		Appid:              nil,
		Openid:             nil,
		UserCouponBundleId: nil,
		PageSize:           nil,
		PageToken:          nil,
		BrandId:            nil,
		CouponState:        nil,
		Alias:              (*Alias)(o),
	}
	return json.Marshal(a)
}

type ListUserProductCouponsResponse struct {
	TotalCount     *int64                    `json:"total_count,omitempty"`
	UserCouponList []UserProductCouponEntity `json:"user_coupon_list,omitempty"`
	NextPageToken  *string                   `json:"next_page_token,omitempty"`
}

type UserProductCouponState string

func (e UserProductCouponState) Ptr() *UserProductCouponState {
	return &e
}

const (
	USERPRODUCTCOUPONSTATE_CONFIRMING  UserProductCouponState = "CONFIRMING"
	USERPRODUCTCOUPONSTATE_PENDING     UserProductCouponState = "PENDING"
	USERPRODUCTCOUPONSTATE_EFFECTIVE   UserProductCouponState = "EFFECTIVE"
	USERPRODUCTCOUPONSTATE_USED        UserProductCouponState = "USED"
	USERPRODUCTCOUPONSTATE_EXPIRED     UserProductCouponState = "EXPIRED"
	USERPRODUCTCOUPONSTATE_DELETED     UserProductCouponState = "DELETED"
	USERPRODUCTCOUPONSTATE_DEACTIVATED UserProductCouponState = "DEACTIVATED"
)

type UserProductCouponEntity struct {
	CouponCode                   *string                       `json:"coupon_code,omitempty"`
	CouponState                  *UserProductCouponState       `json:"coupon_state,omitempty"`
	ValidBeginTime               *time.Time                    `json:"valid_begin_time,omitempty"`
	ValidEndTime                 *time.Time                    `json:"valid_end_time,omitempty"`
	ReceiveTime                  *string                       `json:"receive_time,omitempty"`
	SendRequestNo                *string                       `json:"send_request_no,omitempty"`
	SendChannel                  *UserProductCouponSendChannel `json:"send_channel,omitempty"`
	ConfirmRequestNo             *string                       `json:"confirm_request_no,omitempty"`
	ConfirmTime                  *time.Time                    `json:"confirm_time,omitempty"`
	DeactivateRequestNo          *string                       `json:"deactivate_request_no,omitempty"`
	DeactivateTime               *string                       `json:"deactivate_time,omitempty"`
	DeactivateReason             *string                       `json:"deactivate_reason,omitempty"`
	SingleUsageDetail            *CouponUsageDetail            `json:"single_usage_detail,omitempty"`
	ProgressiveBundleUsageDetail *CouponUsageDetail            `json:"progressive_bundle_usage_detail,omitempty"`
	UserProductCouponBundleInfo  *UserProductCouponBundleInfo  `json:"user_product_coupon_bundle_info,omitempty"`
	ProductCoupon                *ProductCouponEntity          `json:"product_coupon,omitempty"`
	Stock                        *StockEntity                  `json:"stock,omitempty"`
	Attach                       *string                       `json:"attach,omitempty"`
	ChannelCustomInfo            *string                       `json:"channel_custom_info,omitempty"`
	CouponTagInfo                *CouponTagInfo                `json:"coupon_tag_info,omitempty"`
	BrandId                      *string                       `json:"brand_id,omitempty"`
}

type UserProductCouponSendChannel string

func (e UserProductCouponSendChannel) Ptr() *UserProductCouponSendChannel {
	return &e
}

const (
	USERPRODUCTCOUPONSENDCHANNEL_BRAND_MANAGE      UserProductCouponSendChannel = "BRAND_MANAGE"
	USERPRODUCTCOUPONSENDCHANNEL_API               UserProductCouponSendChannel = "API"
	USERPRODUCTCOUPONSENDCHANNEL_RECEIVE_COMPONENT UserProductCouponSendChannel = "RECEIVE_COMPONENT"
)

type CouponUsageDetail struct {
	UseRequestNo                *string                                       `json:"use_request_no,omitempty"`
	UseTime                     *time.Time                                    `json:"use_time,omitempty"`
	ReturnRequestNo             *string                                       `json:"return_request_no,omitempty"`
	ReturnTime                  *time.Time                                    `json:"return_time,omitempty"`
	AssociatedOrderInfo         *UserProductCouponAssociatedOrderInfo         `json:"associated_order_info,omitempty"`
	AssociatedPayScoreOrderInfo *UserProductCouponAssociatedPayScoreOrderInfo `json:"associated_pay_score_order_info,omitempty"`
}

type UserProductCouponBundleInfo struct {
	UserCouponBundleId    *string `json:"user_coupon_bundle_id,omitempty"`
	UserCouponBundleIndex *int64  `json:"user_coupon_bundle_index,omitempty"`
	TotalCount            *int64  `json:"total_count,omitempty"`
	UsedCount             *int64  `json:"used_count,omitempty"`
}

type ProductCouponEntity struct {
	ProductCouponId            *string                     `json:"product_coupon_id,omitempty"`
	Scope                      *ProductCouponScope         `json:"scope,omitempty"`
	Type                       *ProductCouponType          `json:"type,omitempty"`
	UsageMode                  *UsageMode                  `json:"usage_mode,omitempty"`
	SingleUsageInfo            *SingleUsageInfo            `json:"single_usage_info,omitempty"`
	ProgressiveBundleUsageInfo *ProgressiveBundleUsageInfo `json:"progressive_bundle_usage_info,omitempty"`
	DisplayInfo                *ProductCouponDisplayInfo   `json:"display_info,omitempty"`
	OutProductNo               *string                     `json:"out_product_no,omitempty"`
	State                      *ProductCouponState         `json:"state,omitempty"`
	DeactivateRequestNo        *string                     `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *string                     `json:"deactivate_time,omitempty"`
	DeactivateReason           *string                     `json:"deactivate_reason,omitempty"`
	BrandId                    *string                     `json:"brand_id,omitempty"`
}

type StockEntity struct {
	ProductCouponId            *string               `json:"product_coupon_id,omitempty"`
	StockId                    *string               `json:"stock_id,omitempty"`
	Remark                     *string               `json:"remark,omitempty"`
	CouponCodeMode             *CouponCodeMode       `json:"coupon_code_mode,omitempty"`
	CouponCodeCountInfo        *CouponCodeCountInfo  `json:"coupon_code_count_info,omitempty"`
	StockSendRule              *StockSendRule        `json:"stock_send_rule,omitempty"`
	SingleUsageRule            *SingleUsageRule      `json:"single_usage_rule,omitempty"`
	ProgressiveBundleUsageRule *StockUsageRule       `json:"progressive_bundle_usage_rule,omitempty"`
	StockBundleInfo            *StockBundleInfo      `json:"stock_bundle_info,omitempty"`
	UsageRuleDisplayInfo       *UsageRuleDisplayInfo `json:"usage_rule_display_info,omitempty"`
	CouponDisplayInfo          *CouponDisplayInfo    `json:"coupon_display_info,omitempty"`
	NotifyConfig               *NotifyConfig         `json:"notify_config,omitempty"`
	StoreScope                 *StockStoreScope      `json:"store_scope,omitempty"`
	SentCountInfo              *StockSentCountInfo   `json:"sent_count_info,omitempty"`
	State                      *StockState           `json:"state,omitempty"`
	DeactivateRequestNo        *string               `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *time.Time            `json:"deactivate_time,omitempty"`
	DeactivateReason           *string               `json:"deactivate_reason,omitempty"`
	BrandId                    *string               `json:"brand_id,omitempty"`
}

type CouponTagInfo struct {
	CouponTagList []UserProductCouponTag `json:"coupon_tag_list,omitempty"`
	MemberTagInfo *MemberTagInfo         `json:"member_tag_info,omitempty"`
}

type UserProductCouponAssociatedOrderInfo struct {
	TransactionId *string `json:"transaction_id,omitempty"`
	OutTradeNo    *string `json:"out_trade_no,omitempty"`
	Mchid         *string `json:"mchid,omitempty"`
	SubMchid      *string `json:"sub_mchid,omitempty"`
}

type UserProductCouponAssociatedPayScoreOrderInfo struct {
	OrderId    *string `json:"order_id,omitempty"`
	OutOrderNo *string `json:"out_order_no,omitempty"`
	Mchid      *string `json:"mchid,omitempty"`
	SubMchid   *string `json:"sub_mchid,omitempty"`
}

type ProductCouponScope string

func (e ProductCouponScope) Ptr() *ProductCouponScope {
	return &e
}

const (
	PRODUCTCOUPONSCOPE_ALL    ProductCouponScope = "ALL"
	PRODUCTCOUPONSCOPE_SINGLE ProductCouponScope = "SINGLE"
)

type ProductCouponType string

func (e ProductCouponType) Ptr() *ProductCouponType {
	return &e
}

const (
	PRODUCTCOUPONTYPE_NORMAL   ProductCouponType = "NORMAL"
	PRODUCTCOUPONTYPE_DISCOUNT ProductCouponType = "DISCOUNT"
	PRODUCTCOUPONTYPE_EXCHANGE ProductCouponType = "EXCHANGE"
)

type UsageMode string

func (e UsageMode) Ptr() *UsageMode {
	return &e
}

const (
	USAGEMODE_SINGLE             UsageMode = "SINGLE"
	USAGEMODE_PROGRESSIVE_BUNDLE UsageMode = "PROGRESSIVE_BUNDLE"
)

type SingleUsageInfo struct {
	NormalCoupon   *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
}

type ProgressiveBundleUsageInfo struct {
	Count        *int64 `json:"count,omitempty"`
	IntervalDays *int64 `json:"interval_days,omitempty"`
}

type ProductCouponDisplayInfo struct {
	Name               *string        `json:"name,omitempty"`
	ImageUrl           *string        `json:"image_url,omitempty"`
	BackgroundUrl      *string        `json:"background_url,omitempty"`
	DetailImageUrlList []string       `json:"detail_image_url_list,omitempty"`
	OriginalPrice      *int64         `json:"original_price,omitempty"`
	ComboPackageList   []ComboPackage `json:"combo_package_list,omitempty"`
}

type ProductCouponState string

func (e ProductCouponState) Ptr() *ProductCouponState {
	return &e
}

const (
	PRODUCTCOUPONSTATE_AUDITING    ProductCouponState = "AUDITING"
	PRODUCTCOUPONSTATE_EFFECTIVE   ProductCouponState = "EFFECTIVE"
	PRODUCTCOUPONSTATE_DEACTIVATED ProductCouponState = "DEACTIVATED"
)

type CouponCodeMode string

func (e CouponCodeMode) Ptr() *CouponCodeMode {
	return &e
}

const (
	COUPONCODEMODE_WECHATPAY  CouponCodeMode = "WECHATPAY"
	COUPONCODEMODE_UPLOAD     CouponCodeMode = "UPLOAD"
	COUPONCODEMODE_API_ASSIGN CouponCodeMode = "API_ASSIGN"
)

type CouponCodeCountInfo struct {
	TotalCount     *int64 `json:"total_count,omitempty"`
	AvailableCount *int64 `json:"available_count,omitempty"`
}

type StockSendRule struct {
	MaxCount        *int64 `json:"max_count,omitempty"`
	MaxCountPerDay  *int64 `json:"max_count_per_day,omitempty"`
	MaxCountPerUser *int64 `json:"max_count_per_user,omitempty"`
}

type SingleUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type StockUsageRule struct {
	CouponAvailablePeriod *CouponAvailablePeriod   `json:"coupon_available_period,omitempty"`
	NormalCoupon          *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon        *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
	ExchangeCoupon        *ExchangeCouponUsageRule `json:"exchange_coupon,omitempty"`
}

type StockBundleInfo struct {
	StockBundleId    *string `json:"stock_bundle_id,omitempty"`
	StockBundleIndex *int64  `json:"stock_bundle_index,omitempty"`
}

type UsageRuleDisplayInfo struct {
	CouponUsageMethodList    []CouponUsageMethod       `json:"coupon_usage_method_list,omitempty"`
	MiniProgramAppid         *string                   `json:"mini_program_appid,omitempty"`
	MiniProgramPath          *string                   `json:"mini_program_path,omitempty"`
	AppPath                  *string                   `json:"app_path,omitempty"`
	UsageDescription         *string                   `json:"usage_description,omitempty"`
	CouponAvailableStoreInfo *CouponAvailableStoreInfo `json:"coupon_available_store_info,omitempty"`
}

type CouponDisplayInfo struct {
	CodeDisplayMode         *CouponCodeDisplayMode   `json:"code_display_mode,omitempty"`
	BackgroundColor         *string                  `json:"background_color,omitempty"`
	EntranceMiniProgram     *EntranceMiniProgram     `json:"entrance_mini_program,omitempty"`
	EntranceOfficialAccount *EntranceOfficialAccount `json:"entrance_official_account,omitempty"`
	EntranceFinder          *EntranceFinder          `json:"entrance_finder,omitempty"`
}

type NotifyConfig struct {
	NotifyAppid *string `json:"notify_appid,omitempty"`
}

type StockStoreScope string

func (e StockStoreScope) Ptr() *StockStoreScope {
	return &e
}

const (
	STOCKSTORESCOPE_NONE     StockStoreScope = "NONE"
	STOCKSTORESCOPE_ALL      StockStoreScope = "ALL"
	STOCKSTORESCOPE_SPECIFIC StockStoreScope = "SPECIFIC"
)

type StockSentCountInfo struct {
	TotalCount *int64 `json:"total_count,omitempty"`
	TodayCount *int64 `json:"today_count,omitempty"`
}

type StockState string

func (e StockState) Ptr() *StockState {
	return &e
}

const (
	STOCKSTATE_AUDITING    StockState = "AUDITING"
	STOCKSTATE_SENDING     StockState = "SENDING"
	STOCKSTATE_PAUSED      StockState = "PAUSED"
	STOCKSTATE_STOPPED     StockState = "STOPPED"
	STOCKSTATE_DEACTIVATED StockState = "DEACTIVATED"
)

type UserProductCouponTag string

func (e UserProductCouponTag) Ptr() *UserProductCouponTag {
	return &e
}

const (
	USERPRODUCTCOUPONTAG_MEMBER UserProductCouponTag = "MEMBER"
)

type MemberTagInfo struct {
	MemberCardId *string `json:"member_card_id,omitempty"`
}

type NormalCouponUsageRule struct {
	Threshold      *int64 `json:"threshold,omitempty"`
	DiscountAmount *int64 `json:"discount_amount,omitempty"`
}

type DiscountCouponUsageRule struct {
	Threshold  *int64 `json:"threshold,omitempty"`
	PercentOff *int64 `json:"percent_off,omitempty"`
}

type ComboPackage struct {
	Name       *string              `json:"name,omitempty"`
	PickCount  *int64               `json:"pick_count,omitempty"`
	ChoiceList []ComboPackageChoice `json:"choice_list,omitempty"`
}

type CouponAvailablePeriod struct {
	AvailableBeginTime           *string          `json:"available_begin_time,omitempty"`
	AvailableEndTime             *string          `json:"available_end_time,omitempty"`
	AvailableDays                *int64           `json:"available_days,omitempty"`
	WaitDaysAfterReceive         *int64           `json:"wait_days_after_receive,omitempty"`
	WeeklyAvailablePeriod        *FixedWeekPeriod `json:"weekly_available_period,omitempty"`
	IrregularAvailablePeriodList []TimePeriod     `json:"irregular_available_period_list,omitempty"`
}

type ExchangeCouponUsageRule struct {
	Threshold     *int64 `json:"threshold,omitempty"`
	ExchangePrice *int64 `json:"exchange_price,omitempty"`
}

type CouponUsageMethod string

func (e CouponUsageMethod) Ptr() *CouponUsageMethod {
	return &e
}

const (
	COUPONUSAGEMETHOD_OFFLINE      CouponUsageMethod = "OFFLINE"
	COUPONUSAGEMETHOD_MINI_PROGRAM CouponUsageMethod = "MINI_PROGRAM"
	COUPONUSAGEMETHOD_APP          CouponUsageMethod = "APP"
	COUPONUSAGEMETHOD_PAYMENT_CODE CouponUsageMethod = "PAYMENT_CODE"
)

type CouponAvailableStoreInfo struct {
	Description      *string `json:"description,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type CouponCodeDisplayMode string

func (e CouponCodeDisplayMode) Ptr() *CouponCodeDisplayMode {
	return &e
}

const (
	COUPONCODEDISPLAYMODE_INVISIBLE CouponCodeDisplayMode = "INVISIBLE"
	COUPONCODEDISPLAYMODE_BARCODE   CouponCodeDisplayMode = "BARCODE"
	COUPONCODEDISPLAYMODE_QRCODE    CouponCodeDisplayMode = "QRCODE"
)

type EntranceMiniProgram struct {
	Appid           *string `json:"appid,omitempty"`
	Path            *string `json:"path,omitempty"`
	EntranceWording *string `json:"entrance_wording,omitempty"`
	GuidanceWording *string `json:"guidance_wording,omitempty"`
}

type EntranceOfficialAccount struct {
	Appid *string `json:"appid,omitempty"`
}

type EntranceFinder struct {
	FinderId                 *string `json:"finder_id,omitempty"`
	FinderVideoId            *string `json:"finder_video_id,omitempty"`
	FinderVideoCoverImageUrl *string `json:"finder_video_cover_image_url,omitempty"`
}

type ComboPackageChoice struct {
	Name             *string `json:"name,omitempty"`
	Price            *int64  `json:"price,omitempty"`
	Count            *int64  `json:"count,omitempty"`
	ImageUrl         *string `json:"image_url,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}

type FixedWeekPeriod struct {
	DayList       []WeekEnum       `json:"day_list,omitempty"`
	DayPeriodList []PeriodOfTheDay `json:"day_period_list,omitempty"`
}

type TimePeriod struct {
	BeginTime *string `json:"begin_time,omitempty"`
	EndTime   *string `json:"end_time,omitempty"`
}

type WeekEnum string

func (e WeekEnum) Ptr() *WeekEnum {
	return &e
}

const (
	WEEKENUM_MONDAY    WeekEnum = "MONDAY"
	WEEKENUM_TUESDAY   WeekEnum = "TUESDAY"
	WEEKENUM_WEDNESDAY WeekEnum = "WEDNESDAY"
	WEEKENUM_THURSDAY  WeekEnum = "THURSDAY"
	WEEKENUM_FRIDAY    WeekEnum = "FRIDAY"
	WEEKENUM_SATURDAY  WeekEnum = "SATURDAY"
	WEEKENUM_SUNDAY    WeekEnum = "SUNDAY"
)

type PeriodOfTheDay struct {
	BeginTime *int64 `json:"begin_time,omitempty"`
	EndTime   *int64 `json:"end_time,omitempty"`
}


path: references/2-服务商/代码示例/Go/1-商品券管理/QueryProductCouponGo.go
content: // 【查询商品券接口】示例代码
// SDK工具类（HTTP客户端 + 数据模型）见: ../9-SDK工具类/service_models_and_client.go
// ❗重要：本文件为官方示例代码，只允许替换参数和添加注释，禁止从零编写或拼接修改API路径、签名逻辑、请求结构
package main

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	// TODO: 请准备商户开发必要参数，参考：https://pay.weixin.qq.com/doc/v3/partner/4013080340
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &GetProductCouponRequest{
		ProductCouponId: wxpay_utility.String("1000000013"),
		BrandId:         wxpay_utility.String("120344"),
	}

	response, err := GetProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}

func GetProductCoupon(config *wxpay_utility.MchConfig, request *GetProductCouponRequest) (response *ProductCouponEntity, err error) {
	const (
		host   = "https://api.mch.weixin.qq.com"
		method = "GET"
		path   = "/v3/marketing/partner/product-coupon/product-coupons/{product_coupon_id}"
	)

	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
	if err != nil {
		return nil, err
	}
	reqUrl.Path = strings.Replace(reqUrl.Path, "{product_coupon_id}", url.PathEscape(*request.ProductCouponId), -1)
	query := reqUrl.Query()
	if request.BrandId != nil {
		query.Add("brand_id", *request.BrandId)
	}
	reqUrl.RawQuery = query.Encode()
	httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Accept", "application/json")
	httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
	if err != nil {
		return nil, err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		return nil, err
	}
	respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
	if err != nil {
		return nil, err
	}
	if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
		// 2XX 成功，验证应答签名
		err = wxpay_utility.ValidateResponse(
			config.WechatPayPublicKeyId(),
			config.WechatPayPublicKey(),
			&httpResponse.Header,
			respBody,
		)
		if err != nil {
			return nil, err
		}
		response := &ProductCouponEntity{}
		if err := json.Unmarshal(respBody, response); err != nil {
			return nil, err
		}

		return response, nil
	} else {
		return nil, wxpay_utility.NewApiException(
			httpResponse.StatusCode,
			httpResponse.Header,
			respBody,
		)
	}
}

type GetProductCouponRequest struct {
	ProductCouponId *string `json:"product_coupon_id,omitempty"`
	BrandId         *string `json:"brand_id,omitempty"`
}

func (o *GetProductCouponRequest) MarshalJSON() ([]byte, error) {
	type Alias GetProductCouponRequest
	a := &struct {
		ProductCouponId *string `json:"product_coupon_id,omitempty"`
		BrandId         *string `json:"brand_id,omitempty"`
		*Alias
	}{
		// 序列化时移除非 Body 字段
		ProductCouponId: nil,
		BrandId:         nil,
		Alias:           (*Alias)(o),
	}
	return json.Marshal(a)
}

type ProductCouponEntity struct {
	ProductCouponId            *string                     `json:"product_coupon_id,omitempty"`
	Scope                      *ProductCouponScope         `json:"scope,omitempty"`
	Type                       *ProductCouponType          `json:"type,omitempty"`
	UsageMode                  *UsageMode                  `json:"usage_mode,omitempty"`
	SingleUsageInfo            *SingleUsageInfo            `json:"single_usage_info,omitempty"`
	ProgressiveBundleUsageInfo *ProgressiveBundleUsageInfo `json:"progressive_bundle_usage_info,omitempty"`
	DisplayInfo                *ProductCouponDisplayInfo   `json:"display_info,omitempty"`
	OutProductNo               *string                     `json:"out_product_no,omitempty"`
	State                      *ProductCouponState         `json:"state,omitempty"`
	DeactivateRequestNo        *string                     `json:"deactivate_request_no,omitempty"`
	DeactivateTime             *string                     `json:"deactivate_time,omitempty"`
	DeactivateReason           *string                     `json:"deactivate_reason,omitempty"`
	BrandId                    *string                     `json:"brand_id,omitempty"`
}

type ProductCouponScope string

func (e ProductCouponScope) Ptr() *ProductCouponScope {
	return &e
}

const (
	PRODUCTCOUPONSCOPE_ALL    ProductCouponScope = "ALL"
	PRODUCTCOUPONSCOPE_SINGLE ProductCouponScope = "SINGLE"
)

type ProductCouponType string

func (e ProductCouponType) Ptr() *ProductCouponType {
	return &e
}

const (
	PRODUCTCOUPONTYPE_NORMAL   ProductCouponType = "NORMAL"
	PRODUCTCOUPONTYPE_DISCOUNT ProductCouponType = "DISCOUNT"
	PRODUCTCOUPONTYPE_EXCHANGE ProductCouponType = "EXCHANGE"
)

type UsageMode string

func (e UsageMode) Ptr() *UsageMode {
	return &e
}

const (
	USAGEMODE_SINGLE             UsageMode = "SINGLE"
	USAGEMODE_PROGRESSIVE_BUNDLE UsageMode = "PROGRESSIVE_BUNDLE"
)

type SingleUsageInfo struct {
	NormalCoupon   *NormalCouponUsageRule   `json:"normal_coupon,omitempty"`
	DiscountCoupon *DiscountCouponUsageRule `json:"discount_coupon,omitempty"`
}

type ProgressiveBundleUsageInfo struct {
	Count        *int64 `json:"count,omitempty"`
	IntervalDays *int64 `json:"interval_days,omitempty"`
}

type ProductCouponDisplayInfo struct {
	Name               *string        `json:"name,omitempty"`
	ImageUrl           *string        `json:"image_url,omitempty"`
	BackgroundUrl      *string        `json:"background_url,omitempty"`
	DetailImageUrlList []string       `json:"detail_image_url_list,omitempty"`
	OriginalPrice      *int64         `json:"original_price,omitempty"`
	ComboPackageList   []ComboPackage `json:"combo_package_list,omitempty"`
}

type ProductCouponState string

func (e ProductCouponState) Ptr() *ProductCouponState {
	return &e
}

const (
	PRODUCTCOUPONSTATE_AUDITING    ProductCouponState = "AUDITING"
	PRODUCTCOUPONSTATE_EFFECTIVE   ProductCouponState = "EFFECTIVE"
	PRODUCTCOUPONSTATE_DEACTIVATED ProductCouponState = "DEACTIVATED"
)

type NormalCouponUsageRule struct {
	Threshold      *int64 `json:"threshold,omitempty"`
	DiscountAmount *int64 `json:"discount_amount,omitempty"`
}

type DiscountCouponUsageRule struct {
	Threshold  *int64 `json:"threshold,omitempty"`
	PercentOff *int64 `json:"percent_off,omitempty"`
}

type ComboPackage struct {
	Name       *string              `json:"name,omitempty"`
	PickCount  *int64               `json:"pick_count,omitempty"`
	ChoiceList []ComboPackageChoice `json:"choice_list,omitempty"`
}

type ComboPackageChoice struct {
	Name             *string `json:"name,omitempty"`
	Price            *int64  `json:"price,omitempty"`
	Count            *int64  `json:"count,omitempty"`
	ImageUrl         *string `json:"image_url,omitempty"`
	MiniProgramAppid *string `json:"mini_program_appid,omitempty"`
	MiniProgramPath  *string `json:"mini_program_path,omitempty"`
}


path: references/2-服务商/代码示例/Go/1-商品券管理/SingleNormalSingleGo.go
content: package main

// SDK工具类（HTTP客户端 + 数据模型）见: ../9-SDK工具类/service_models_and_client.go
// ❗重要：本文件为官方示例代码，只允许替换参数和添加注释，禁止从零编写或拼接修改API路径、签名逻辑、请求结构

import (
	"demo/wxpay_utility" // 引用微信支付工具库
	"encoding/json"
	"fmt"
)

// 场景4：创建商品券 - 单券-单品-满减券
//
// 场景说明：
// - usage_mode: SINGLE（单券模式）
// - scope: SINGLE（单品券）
// - type: NORMAL（满减券）
//
// ❗重要：单品券的优惠规则配置在 stock.single_usage_rule.normal_coupon 中（不是single_usage_info）
//
func main() {
	// TODO: 请准备商户开发必要参数
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考商户平台
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考商户平台【API安全】
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考商户平台【API安全】
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &CreateProductCouponRequest{
		// ==================== 一级参数 ====================
		// 必填：创建请求单号，品牌侧需保持唯一性，6-40个字符
		OutRequestNo: wxpay_utility.String("SINGLE_SINGLE_NORMAL_20250101_004"),
		// 必填：品牌ID，由微信支付分配
		BrandId: wxpay_utility.String("120344"),
		// 必填：优惠范围，SINGLE=单品券(支持NORMAL/DISCOUNT/EXCHANGE)
		Scope: PRODUCTCOUPONSCOPE_SINGLE.Ptr(),
		// 必填：商品券类型，NORMAL=满减券
		Type: PRODUCTCOUPONTYPE_NORMAL.Ptr(),
		// 必填：使用模式，SINGLE=单券模式
		UsageMode: USAGEMODE_SINGLE.Ptr(),
		// 选填：商户侧商品券唯一标识
		OutProductNo: wxpay_utility.String("Product_SINGLE_004"),

		// ⚠️ scope=SINGLE时，不在single_usage_info中配置优惠规则
		// 优惠规则在 stock.single_usage_rule 中配置

		// ==================== 必填：商品券展示信息 ====================
		DisplayInfo: &ProductCouponDisplayInfo{
			// 必填：商品券名称，最多20个字符
			Name: wxpay_utility.String("指定商品立减15元"),
			// 必填：商品图片URL，需通过图片上传接口获取
			ImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
			// 选填：背景图URL
			BackgroundUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
			// 选填：详情图URL列表，最多6张
			DetailImageUrlList: []string{
				"https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx",
			},
			// 条件必填(单品券)：商品原价(单位：分)，scope=SINGLE时必填
			OriginalPrice: wxpay_utility.Int64(9900),
			// 条件必填(单品券)：套餐组合，scope=SINGLE时必填
			ComboPackageList: []ComboPackage{
				{
					Name:      wxpay_utility.String("超值套餐"),
					PickCount: wxpay_utility.Int64(1),
					ChoiceList: []ComboPackageChoice{
						{
							Name:     wxpay_utility.String("指定商品"),
							Price:    wxpay_utility.Int64(9900),
							Count:    wxpay_utility.Int64(1),
							ImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
						},
					},
				},
			},
		},

		// ==================== 批次信息（必填） ====================
		Stock: &StockForCreate{
			// 选填：备注
			Remark: wxpay_utility.String("8月单品满减批次"),
			// 必填：券Code分配模式，WECHATPAY=微信支付生成
			CouponCodeMode: COUPONCODEMODE_WECHATPAY.Ptr(),

			// ---------- 发放规则（必填） ----------
			StockSendRule: &StockSendRule{
				// 必填：总发放次数上限
				MaxCount: wxpay_utility.Int64(10000000),
				// 选填：每日发放上限
				MaxCountPerDay: wxpay_utility.Int64(100000),
				// 必填：每用户领取上限
				MaxCountPerUser: wxpay_utility.Int64(1),
			},

			// ---------- 单券使用规则（scope=SINGLE时在此配置优惠规则） ----------
			SingleUsageRule: &SingleUsageRule{
				// 券可核销时间（必填）
				CouponAvailablePeriod: &CouponAvailablePeriod{
					// 必填：开始时间，RFC3339格式
					AvailableBeginTime: wxpay_utility.String("2025-08-01T00:00:00+08:00"),
					// 必填：结束时间
					AvailableEndTime: wxpay_utility.String("2025-08-31T23:59:59+08:00"),
					// 选填：生效后N天有效，最多365天
					AvailableDays: wxpay_utility.Int64(30),
					// 选填：领取后N天生效，最多30天，0表示立即生效
					WaitDaysAfterReceive: wxpay_utility.Int64(0),
					// 选填：每周可用时间
					WeeklyAvailablePeriod: &FixedWeekPeriod{
						// 当配置weekly_available_period时必填：可用星期列表
						DayList: []WeekEnum{
							WEEKENUM_MONDAY,
							WEEKENUM_TUESDAY,
							WEEKENUM_WEDNESDAY,
							WEEKENUM_THURSDAY,
							WEEKENUM_FRIDAY,
						},
					},
				},
				// ⚠️【关键】当type=NORMAL且scope=SINGLE时，在此配置满减券优惠规则
				NormalCoupon: &NormalCouponUsageRule{
					// 必填：门槛金额，单位为分，0表示无门槛
					Threshold: wxpay_utility.Int64(0),
					// 必填：固定减免金额，单位为分，减15元填1500
					DiscountAmount: wxpay_utility.Int64(1500),
				},
			},

			// ---------- 使用规则展示信息（必填） ----------
			UsageRuleDisplayInfo: &UsageRuleDisplayInfo{
				// 必填：券使用方式列表
				CouponUsageMethodList: []CouponUsageMethod{
					COUPONUSAGEMETHOD_OFFLINE,      // 线下滴码核销
					COUPONUSAGEMETHOD_MINI_PROGRAM, // 线上小程序核销
					COUPONUSAGEMETHOD_PAYMENT_CODE, // 微信支付付款码核销
				},
				// 当coupon_usage_method_list包含MINI_PROGRAM时必填：核销小程序AppID
				MiniProgramAppid: wxpay_utility.String("wxYOUR_APPID"),
				// 当coupon_usage_method_list包含MINI_PROGRAM时必填：核销小程序路径
				MiniProgramPath: wxpay_utility.String("/pages/index/product"),
				// 必填：使用说明
				UsageDescription: wxpay_utility.String("指定商品可用，立减15元"),
				// 选填：可用门店信息
				CouponAvailableStoreInfo: &CouponAvailableStoreInfo{
					// 当配置coupon_available_store_info时必填：门店描述
					Description: wxpay_utility.String("所有门店可用"),
					// 选填：查看门店的小程序AppID
					MiniProgramAppid: wxpay_utility.String("wxYOUR_APPID"),
					// 选填：查看门店的小程序路径
					MiniProgramPath: wxpay_utility.String("/pages/index/store-list"),
				},
			},

			// ---------- 用户券展示信息（必填） ----------
			CouponDisplayInfo: &CouponDisplayInfo{
				// 必填：Code展示模式，QRCODE=二维码
				CodeDisplayMode: COUPONCODEDISPLAYMODE_QRCODE.Ptr(),
				// 选填：背景颜色
				BackgroundColor: wxpay_utility.String("Color010"),
				// 选填：小程序入口
				EntranceMiniProgram: &EntranceMiniProgram{
					Appid:           wxpay_utility.String("wxYOUR_APPID"),
					Path:            wxpay_utility.String("/pages/index/product"),
					EntranceWording: wxpay_utility.String("欢迎选购"),
					GuidanceWording: wxpay_utility.String("获取更多优惠"),
				},
				// 选填：公众号入口
				EntranceOfficialAccount: &EntranceOfficialAccount{
					Appid: wxpay_utility.String("wxYOUR_APPID"),
				},
				// 选填：视频号入口
				EntranceFinder: &EntranceFinder{
					FinderId:                 wxpay_utility.String("gh_12345678"),
					FinderVideoId:            wxpay_utility.String("UDFsdf24df34dD456Hdf34"),
					FinderVideoCoverImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
				},
			},

			// ---------- 事件通知配置（必填） ----------
			NotifyConfig: &NotifyConfig{
				// 必填：事件通知AppID
				NotifyAppid: wxpay_utility.String("wx4fd12345678"),
			},

			// 必填：可用门店范围，NONE=不限制
			StoreScope: STOCKSTORESCOPE_NONE.Ptr(),
		},
	}

	response, err := CreateProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}


path: references/2-服务商/代码示例/Go/1-商品券管理/SingleExchangeSingleGo.go
content: package main

// SDK工具类（HTTP客户端 + 数据模型）见: ../9-SDK工具类/service_models_and_client.go
// ❗重要：本文件为官方示例代码，只允许替换参数和添加注释，禁止从零编写或拼接修改API路径、签名逻辑、请求结构

import (
	"demo/wxpay_utility" // 引用微信支付工具库
	"encoding/json"
	"fmt"
)

// 场景5：创建商品券 - 单券-单品-兑换券
//
// 场景说明：
// - usage_mode: SINGLE（单券模式）
// - scope: SINGLE（单品券）
// - type: EXCHANGE（兑换券）
//
// ❗重要：
// 1. 兑换券仅支持单品券（scope=SINGLE）
// 2. 单品券的优惠规则配置在 stock.single_usage_rule.exchange_coupon 中（不是single_usage_info）
//
func main() {
	// TODO: 请准备商户开发必要参数
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考商户平台
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号，如何获取请参考商户平台【API安全】
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID，如何获取请参考商户平台【API安全】
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &CreateProductCouponRequest{
		// ==================== 一级参数 ====================
		// 必填：创建请求单号，品牌侧需保持唯一性，6-40个字符
		OutRequestNo: wxpay_utility.String("SINGLE_SINGLE_EXCHANGE_20250101_005"),
		// 必填：品牌ID，由微信支付分配
		BrandId: wxpay_utility.String("120344"),
		// 必填：优惠范围，SINGLE=单品券(兑换券仅支持单品券)
		Scope: PRODUCTCOUPONSCOPE_SINGLE.Ptr(),
		// 必填：商品券类型，EXCHANGE=兑换券(仅单品券支持)
		Type: PRODUCTCOUPONTYPE_EXCHANGE.Ptr(),
		// 必填：使用模式，SINGLE=单券模式
		UsageMode: USAGEMODE_SINGLE.Ptr(),
		// 选填：商户侧商品券唯一标识
		OutProductNo: wxpay_utility.String("Product_SINGLE_005"),

		// ⚠️ scope=SINGLE时，不在single_usage_info中配置优惠规则
		// 优惠规则在 stock.single_usage_rule 中配置

		// ==================== 必填：商品券展示信息 ====================
		DisplayInfo: &ProductCouponDisplayInfo{
			// 必填：商品券名称，最多20个字符
			Name: wxpay_utility.String("9.9元兑换原价99元商品"),
			// 必填：商品图片URL，需通过图片上传接口获取
			ImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
			// 选填：背景图URL
			BackgroundUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
			// 选填：详情图URL列表，最多6张
			DetailImageUrlList: []string{
				"https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx",
			},
			// 条件必填(单品券)：商品原价(单位：分)，scope=SINGLE时必填
			OriginalPrice: wxpay_utility.Int64(9900),
			// 条件必填(单品券)：套餐组合，scope=SINGLE时必填
			ComboPackageList: []ComboPackage{
				{
					Name:      wxpay_utility.String("超值套餐"),
					PickCount: wxpay_utility.Int64(1),
					ChoiceList: []ComboPackageChoice{
						{
							Name:     wxpay_utility.String("指定商品"),
							Price:    wxpay_utility.Int64(9900),
							Count:    wxpay_utility.Int64(1),
							ImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
						},
					},
				},
			},
		},

		// ==================== 批次信息（必填） ====================
		Stock: &StockForCreate{
			// 选填：备注
			Remark: wxpay_utility.String("8月单品兑换批次"),
			// 必填：券Code分配模式，WECHATPAY=微信支付生成
			CouponCodeMode: COUPONCODEMODE_WECHATPAY.Ptr(),

			// ---------- 发放规则（必填） ----------
			StockSendRule: &StockSendRule{
				// 必填：总发放次数上限
				MaxCount: wxpay_utility.Int64(10000000),
				// 选填：每日发放上限
				MaxCountPerDay: wxpay_utility.Int64(100000),
				// 必填：每用户领取上限
				MaxCountPerUser: wxpay_utility.Int64(1),
			},

			// ---------- 单券使用规则（scope=SINGLE时在此配置优惠规则） ----------
			SingleUsageRule: &SingleUsageRule{
				// 券可核销时间（必填）
				CouponAvailablePeriod: &CouponAvailablePeriod{
					// 必填：开始时间，RFC3339格式
					AvailableBeginTime: wxpay_utility.String("2025-08-01T00:00:00+08:00"),
					// 必填：结束时间
					AvailableEndTime: wxpay_utility.String("2025-08-31T23:59:59+08:00"),
					// 选填：生效后N天有效，最多365天
					AvailableDays: wxpay_utility.Int64(30),
					// 选填：领取后N天生效，最多30天，0表示立即生效
					WaitDaysAfterReceive: wxpay_utility.Int64(0),
					// 选填：每周可用时间
					WeeklyAvailablePeriod: &FixedWeekPeriod{
						// 当配置weekly_available_period时必填：可用星期列表
						DayList: []WeekEnum{
							WEEKENUM_MONDAY,
							WEEKENUM_TUESDAY,
							WEEKENUM_WEDNESDAY,
							WEEKENUM_THURSDAY,
							WEEKENUM_FRIDAY,
						},
					},
				},
				// ⚠️【关键】当type=EXCHANGE时，在此配置兑换券优惠规则
				ExchangeCoupon: &ExchangeCouponUsageRule{
					// 必填：门槛金额，单位为分，0表示无门槛
					Threshold: wxpay_utility.Int64(0),
					// 必填：兑换价格，单位为分，9.9元填990
					ExchangePrice: wxpay_utility.Int64(990),
				},
			},

			// ---------- 使用规则展示信息（必填） ----------
			UsageRuleDisplayInfo: &UsageRuleDisplayInfo{
				// 必填：券使用方式列表
				CouponUsageMethodList: []CouponUsageMethod{
					COUPONUSAGEMETHOD_OFFLINE,      // 线下滴码核销
					COUPONUSAGEMETHOD_MINI_PROGRAM, // 线上小程序核销
					COUPONUSAGEMETHOD_PAYMENT_CODE, // 微信支付付款码核销
				},
				// 当coupon_usage_method_list包含MINI_PROGRAM时必填：核销小程序AppID
				MiniProgramAppid: wxpay_utility.String("wxYOUR_APPID"),
				// 当coupon_usage_method_list包含MINI_PROGRAM时必填：核销小程序路径
				MiniProgramPath: wxpay_utility.String("/pages/index/product"),
				// 必填：使用说明
				UsageDescription: wxpay_utility.String("用9.9元兑换原价99元商品"),
				// 选填：可用门店信息
				CouponAvailableStoreInfo: &CouponAvailableStoreInfo{
					// 当配置coupon_available_store_info时必填：门店描述
					Description: wxpay_utility.String("所有门店可用"),
					// 选填：查看门店的小程序AppID
					MiniProgramAppid: wxpay_utility.String("wxYOUR_APPID"),
					// 选填：查看门店的小程序路径
					MiniProgramPath: wxpay_utility.String("/pages/index/store-list"),
				},
			},

			// ---------- 用户券展示信息（必填） ----------
			CouponDisplayInfo: &CouponDisplayInfo{
				// 必填：Code展示模式，QRCODE=二维码
				CodeDisplayMode: COUPONCODEDISPLAYMODE_QRCODE.Ptr(),
				// 选填：背景颜色
				BackgroundColor: wxpay_utility.String("Color010"),
				// 选填：小程序入口
				EntranceMiniProgram: &EntranceMiniProgram{
					Appid:           wxpay_utility.String("wxYOUR_APPID"),
					Path:            wxpay_utility.String("/pages/index/product"),
					EntranceWording: wxpay_utility.String("立即兑换"),
					GuidanceWording: wxpay_utility.String("获取更多优惠"),
				},
				// 选填：公众号入口
				EntranceOfficialAccount: &EntranceOfficialAccount{
					Appid: wxpay_utility.String("wxYOUR_APPID"),
				},
				// 选填：视频号入口
				EntranceFinder: &EntranceFinder{
					FinderId:                 wxpay_utility.String("gh_12345678"),
					FinderVideoId:            wxpay_utility.String("UDFsdf24df34dD456Hdf34"),
					FinderVideoCoverImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
				},
			},

			// ---------- 事件通知配置（必填） ----------
			NotifyConfig: &NotifyConfig{
				// 必填：事件通知AppID
				NotifyAppid: wxpay_utility.String("wx4fd12345678"),
			},

			// 必填：可用门店范围，NONE=不限制
			StoreScope: STOCKSTORESCOPE_NONE.Ptr(),
		},
	}

	response, err := CreateProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		// TODO: 请求失败，根据状态码执行不同的处理
		return
	}

	// TODO: 请求成功，继续业务逻辑
	fmt.Printf("请求成功: %+v\n", response)
}


path: references/2-服务商/代码示例/Go/1-商品券管理/AllDiscountManyGo.go
content: package main

// SDK工具类（HTTP客户端 + 数据模型）见: ../9-SDK工具类/service_models_and_client.go
// ❗重要：本文件为官方示例代码，只允许替换参数和添加注释，禁止从零编写或拼接修改API路径、签名逻辑、请求结构

import (
	"demo/wxpay_utility" // 引用微信支付工具库
	"encoding/json"
	"fmt"
)

// 创建商品券 - 场景1：多次优惠-全场-折扣券
//
// 场景说明：全场券（适用于全部商品）+ 折扣券（按百分比减免）+ 阶梯模式（可多次使用）
//
// 关键参数：
// - scope = ALL（全场券）
// - type = DISCOUNT（折扣券）
// - usage_mode = PROGRESSIVE_BUNDLE（阶梯模式）

func main() {
	// TODO: 请准备商户开发必要参数
	config, err := wxpay_utility.CreateMchConfig(
		"YOUR_MCHID",                 // 商户号
		"YOUR_CERT_SERIAL_NO",      // 商户API证书序列号
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径
		"YOUR_PUB_KEY_ID",   // 微信支付公钥ID
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	request := &CreateProductCouponRequest{
		OutRequestNo: wxpay_utility.String("MANY_ALL_DISCOUNT_20250101_001"), // 必填，创建请求单号，6-40个字符，品牌侧需保持唯一性
		BrandId:      wxpay_utility.String("120344"),                         // 必填，品牌ID，由微信支付分配
		OutProductNo: wxpay_utility.String("Product_1234567890"),             // 选填，商户侧商品券唯一标识
		Scope:        PRODUCTCOUPONSCOPE_ALL.Ptr(),                           // 必填，优惠范围：ALL-全场券(仅支持NORMAL/DISCOUNT), SINGLE-单品券
		Type:         PRODUCTCOUPONTYPE_DISCOUNT.Ptr(),                       // 必填，商品券类型：NORMAL-满减券, DISCOUNT-折扣券, EXCHANGE-兑换券(仅单品券)
		UsageMode:    USAGEMODE_PROGRESSIVE_BUNDLE.Ptr(),                     // 必填，使用模式：SINGLE-单券, PROGRESSIVE_BUNDLE-多次优惠
		// 条件必填，多次优惠模式配置信息(当usage_mode=PROGRESSIVE_BUNDLE时必填)
		ProgressiveBundleUsageInfo: &ProgressiveBundleUsageInfo{
			Count:        wxpay_utility.Int64(3), // 必填，可使用次数，最少3次，最多15次
			IntervalDays: wxpay_utility.Int64(0), // 选填，多次优惠使用间隔天数，0表示不限制，最高7天，默认0
		},
		// 必填，商品券展示信息
		DisplayInfo: &ProductCouponDisplayInfo{
			Name:          wxpay_utility.String("全场8折券(可用3次)"),                       // 必填，商品券名称，最多12个字符
			ImageUrl:      wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"), // 必填，商品券图片URL
			BackgroundUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"), // 选填，背景图URL
			DetailImageUrlList: []string{ // 选填，详情图URL列表，最多6张
				"https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx",
			},
		},
		// 条件必填，批次信息(当usage_mode=PROGRESSIVE_BUNDLE时必填)
		Stock: createDiscountStockPtr("8折批次", 10000, 20),
	}

	response, err := CreateProductCoupon(config, request)
	if err != nil {
		fmt.Printf("请求失败: %+v\n", err)
		return
	}
	fmt.Printf("请求成功: %+v\n", response)
}

// createDiscountStockPtr 创建折扣券批次(返回指针)
func createDiscountStockPtr(remark string, threshold, percentOff int64) *StockForProgressiveBundle {
	stock := createDiscountStock(remark, threshold, percentOff)
	return &stock
}

// createDiscountStock 创建折扣券批次
func createDiscountStock(remark string, threshold, percentOff int64) StockForProgressiveBundle {
	return StockForProgressiveBundle{
		Remark:         wxpay_utility.String(remark),
		CouponCodeMode: COUPONCODEMODE_WECHATPAY.Ptr(),
		StockSendRule: &StockSendRule{
			MaxCount:        wxpay_utility.Int64(10000000),
			MaxCountPerUser: wxpay_utility.Int64(1),
		},
		ProgressiveBundleUsageRule: &ProgressiveBundleUsageRule{
			CouponAvailablePeriod: &CouponAvailablePeriod{
				AvailableBeginTime: wxpay_utility.String("2025-08-01T00:00:00+08:00"),
				AvailableEndTime:   wxpay_utility.String("2025-08-31T23:59:59+08:00"),
				AvailableDays:      wxpay_utility.Int64(30),
				WeeklyAvailablePeriod: &FixedWeekPeriod{
					DayList: []WeekEnum{
						WEEKENUM_MONDAY,
						WEEKENUM_TUESDAY,
						WEEKENUM_WEDNESDAY,
						WEEKENUM_THURSDAY,
						WEEKENUM_FRIDAY,
					},
				},
			},
			DiscountCoupon: &DiscountCouponUsageRule{
				Threshold:  wxpay_utility.Int64(threshold),
				PercentOff: wxpay_utility.Int64(percentOff),
			},
		},
		UsageRuleDisplayInfo: &UsageRuleDisplayInfo{
			CouponUsageMethodList: []CouponUsageMethod{
				COUPONUSAGEMETHOD_OFFLINE,
				COUPONUSAGEMETHOD_MINI_PROGRAM,
				COUPONUSAGEMETHOD_PAYMENT_CODE,
			},
			MiniProgramAppid: wxpay_utility.String("wxYOUR_APPID"),
			MiniProgramPath:  wxpay_utility.String("/pages/index/product"),
			UsageDescription: wxpay_utility.String("工作日可用"),
			CouponAvailableStoreInfo: &CouponAvailableStoreInfo{
				Description:      wxpay_utility.String("所有门店可用，可使用小程序查看门店列表"),
				MiniProgramAppid: wxpay_utility.String("wxYOUR_APPID"),
				MiniProgramPath:  wxpay_utility.String("/pages/index/store-list"),
			},
		},
		CouponDisplayInfo: &CouponDisplayInfo{
			CodeDisplayMode: COUPONCODEDISPLAYMODE_QRCODE.Ptr(),
			BackgroundColor: wxpay_utility.String("Color010"),
			EntranceMiniProgram: &EntranceMiniProgram{
				Appid:           wxpay_utility.String("wxYOUR_APPID"),
				Path:            wxpay_utility.String("/pages/index/product"),
				EntranceWording: wxpay_utility.String("欢迎选购"),
				GuidanceWording: wxpay_utility.String("获取更多优惠"),
			},
			EntranceOfficialAccount: &EntranceOfficialAccount{
				Appid: wxpay_utility.String("wxYOUR_APPID"),
			},
			EntranceFinder: &EntranceFinder{
				FinderId:                 wxpay_utility.String("gh_12345678"),
				FinderVideoId:            wxpay_utility.String("UDFsdf24df34dD456Hdf34"),
				FinderVideoCoverImageUrl: wxpay_utility.String("https://wxpaylogo.qpic.cn/wxpaylogo/xxxxx/xxx"),
			},
		},
		StoreScope: STOCKSTORESCOPE_NONE.Ptr(),
		NotifyConfig: &NotifyConfig{
			NotifyAppid: wxpay_utility.String("wx4fd12345678"),
		},
	}
}


path: references/1-品牌/签名验签/品牌API密钥.md
content: # 什么是品牌API密钥？如何获取品牌API密钥

## 1、功能介绍

微信支付品牌API处理回调通知时，为防止报文被其他人恶意篡改，服务器会对数据进行加密。品牌商户收到报文后，要解密出明文，解密过程中用的 key 就是品牌API密钥。

> **注意**：品牌API密钥属于敏感信息，请妥善保管不要泄露，如果怀疑信息泄露，请重设密钥。

## 2、设置要求

1. 请与贵司技术人员沟通设置品牌API密钥（密钥为32个字符，支持数字和大小写字母组合）
2. 确保技术人员保管品牌API密钥，防止泄露
3. 将技术人员设置为"安全联系人"，同步接收相关的风险提醒

## 3、如何设置品牌API密钥

详细步骤请参考品牌经营平台的"配置品牌API密钥"操作指引。

## 4、如何更新品牌API密钥

### 4.1 更换的注意事项

1. 品牌管理员与所有相关服务的技术沟通，明确更换品牌API密钥的方案、时间点，建议选择没有或较少交易的时间段。修改品牌API密钥后，旧密钥会**立即失效**，若线上仍存在使用旧密钥的服务，会导致API调用失败。
2. 修改密钥之前，建议暂停相关业务服务，例如发出系统维护公告。
3. 按照操作完成更换后恢复业务服务使用。

### 4.2 更换的操作说明

1. 管理员登录【微信支付品牌经营平台 -> 账户管理 -> 安全中心】，找到品牌API密钥，点击"修改"
2. 输入需要修改的新密钥，确认技术人员已制定更换密钥计划，并将新密钥已妥善保管。（注意：密钥设置成功后不支持查看，仅支持修改重新设置），确认无误后点击"确认修改"
3. 安全验证：按提示输入操作密码 + 手机验证码，提交确认即可修改成功
4. 品牌API密钥修改成功，请联系贵司技术人员使用新密钥


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)