基础支付
JSAPI支付
APP支付
H5支付
Native支付
小程序支付
合单支付
付款码支付
经营能力
微信支付分
支付即服务
行业方案
智慧商圈
微信支付分停车服务
电子发票
营销工具
代金券
商家券
委托营销
支付有礼
小程序发券插件
H5发券
图片上传(营销专用)
现金红包
资金应用
商家转账到零钱
分账
风险合规
消费者投诉2.0
其他能力
清关报关
图片上传
视频上传
微信支付平台证书

消费者投诉2.0开发指引

1. 接口规则

为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付APIv3接口。该版本API的具体规则请参考“APIv3接口规则
备注:当前接口用于微信国内钱包

2. 开发准备

2.1. 搭建和配置开发环境

为了帮助开发者调用开放接口,我们提供了JAVA、PHP、GO三种语言版本的开发库,封装了签名生成、签名验证、敏感信息加/解密、媒体文件上传等基础功能(更多语言版本的开发库将在近期陆续提供

测试步骤

1、根据自身开发语言,选择对应的开发库并构建项目,具体配置请参考下面链接的详细说明:

    • wechatpay-java(推荐)wechatpay-apache-httpclient,适用于Java开发者。

    • wechatpay-php(推荐)、wechatpay-guzzle-middleware,适用于PHP开发者

    注:当前开发指引接口PHP示例代码采用wechatpay-guzzle-middleware版本

    • wechatpay-go,适用于Go开发者

更多资源可前往微信支付开发者社区搜索查看

2、创建加载商户私钥、加载平台证书、初始化httpClient的通用方法


@Before
public void setup() throws IOException {
    // 加载商户私钥(privateKey:私钥字符串)
    PrivateKey merchantPrivateKey = PemUtil
            .loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
 
    // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
    AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
            new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));
 
    // 初始化httpClient
    httpClient = WechatPayHttpClientBuilder.create()
            .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
            .withValidator(new WechatPay2Validator(verifier)).build();
}
 
@After
public void after() throws IOException {
    httpClient.close();
}

use GuzzleHttp\Exception\RequestException;
use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
use WechatPay\GuzzleMiddleware\Util\PemUtil;
use GuzzleHttp\HandlerStack;
 
// 商户相关配置,
$merchantId = '1000100'; // 商户号
$merchantSerialNumber = 'XXXXXXXXXX'; // 商户API证书序列号
$merchantPrivateKey = PemUtil::loadPrivateKey('./path/to/mch/private/key.pem'); // 商户私钥文件路径
 
// 微信支付平台配置
$wechatpayCertificate = PemUtil::loadCertificate('./path/to/wechatpay/cert.pem'); // 微信支付平台证书文件路径
 
// 构造一个WechatPayMiddleware
$wechatpayMiddleware = WechatPayMiddleware::builder()
    ->withMerchant($merchantId, $merchantSerialNumber, $merchantPrivateKey) // 传入商户相关配置
    ->withWechatPay([ $wechatpayCertificate ]) // 可传入多个微信支付平台证书,参数类型为array
    ->build();
 
// 将WechatPayMiddleware添加到Guzzle的HandlerStack中
$stack = GuzzleHttp\HandlerStack::create();
$stack->push($wechatpayMiddleware, 'wechatpay');
 
// 创建Guzzle HTTP Client时,将HandlerStack传入,接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
$client = new GuzzleHttp\Client(['handler' => $stack]);

/*
    Package core 微信支付api v3 go http-client 基础库,你可以使用它来创建一个client,并向微信支付发送http请求
    只需要你在初始化客户端的时候,传递credential以及validator
    credential用来生成http header中的authorization信息
    validator则用来校验回包是否被篡改
    如果http请求返回的err为nil,一般response.Body 都不为空,你可以尝试对其进行序列化
    请注意及时关闭response.Body
    注意:使用微信支付apiv3 go库需要引入相关的包,该示例代码必须引入的包名有以下信息

    "context"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "log"
    "github.com/wechatpay-apiv3/wechatpay-go/core"
    "github.com/wechatpay-apiv3/wechatpay-go/core/option"
    "github.com/wechatpay-apiv3/wechatpay-go/utils"

    */
func SetUp() (opt []option.ClientOption, err error) {
    //商户号
    mchID := ""
    //商户证书序列号
    mchCertSerialNumber := ""
    //商户私钥文件路径
    privateKeyPath := ""
    //平台证书文件路径
    wechatCertificatePath := ""

    // 加载商户私钥
    privateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath)
    if err != nil {
        log.Printf("load private err:%s", err.Error())
        return nil, err
    }
    // 加载微信支付平台证书
    wechatPayCertificate, err := utils.LoadCertificateWithPath(wechatCertificatePath)
    if err != nil {
        log.Printf("load certificate err:%s",err)
        return nil, err
    }
    //设置header头中authorization信息
    opts := []option.ClientOption{
        option.WithMerchant(mchID, mchCertSerialNumber, privateKey), // 设置商户相关配置
        option.WithWechatPay([]*x509.Certificate{wechatPayCertificate}), // 设置微信支付平台证书,用于校验回包信息用
    }
    return opts, nil
}

3、基于接口的示例代码,替换请求参数后可发起测试

说明:

• 上面的开发库为微信支付官方开发库,其它没有审核或者控制下的第三方工具和库,微信支付不保证它们的安全性和可靠性

通过包管理工具引入SDK后,可根据下面每个接口的示例代码替换相关参数后进行快速测试

开发者如果想详细了解签名生成、签名验证、敏感信息加/解密、媒体文件上传等常用方法的具体代码实现,可阅读下面的详细说明:

    1.签名生成

    2.签名验证

    3.敏感信息加解密

    4.merchantPrivateKey(私钥)

    5.wechatpayCertificates(平台证书)

    6.APIV3Key(V3 key)

如想更详细的了解我们的接口规则,可查看我们的接口规则指引文档

3. 快速接入

3.1. 业务流程图

业务流程时序图:

重点步骤说明:

步骤3 用户发起投诉后,微信支付通过投诉通知回调API通知商户投诉单需处理。

步骤6 商户接收投诉后,可通过回复用户API接口向微信支付提交投诉处理回复。

步骤9 与用户协商一致后,商户通过反馈处理完成API反馈投诉单处理完成。

步骤14 用户如果不接受投诉处理结果,将再次发起投诉,微信支付会再次通知商户需处理投诉。

步骤15 商户在没有接收到微信投诉通知的情况下可主动调用查询投诉单详情API查询投诉单处理结果。

3.2. API接入(含示例代码)

文档展示了如何使用微信支付服务端 SDK 快速接入消费者投诉产品,完成与微信支付对接的部分。

注意

  • 文档中的代码示例是用来阐述 API 基本使用方法,代码中的示例参数需替换成商户自己账号及请求参数才能跑通
  • 以下接入步骤仅提供参考,请商户结合自身业务需求进行评估、修改。
3.2.1. 【服务端】查询投诉单列表

步骤说明:商户可通过调用此接口,查询指定时间段的所有用户投诉信息,以分页输出查询结果。

示例代码


public void GetComplaintsList() throws Exception{
    //请求URL
    HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2?limit=5&offset=10&begin_date=2019-01-01&end_date=2019-01-01&complainted_mchid=190000XXXX");
    httpGet.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpGet);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}     

try {
    $resp = $client->request(
        'GET',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2?limit=5&offset=10&begin_date=2019-01-01&end_date=2019-01-01&complainted_mchid=190000XXXX', //请求URL
        [
            'headers' => [ 'Accept' => 'application/json']
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func GetComplaintsList() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
 //设置请求地址
 URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2?limit=5&offset=10&begin_date=2019-01-01&end_date=2019-01-01&complainted_mchid=1900012181"
  // 发起请求
  response, err := client.Get(ctx, URL)
  if err != nil{
    log.Printf("client get err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

begin_date:开始日期,投诉发生的开始日期,格式为yyyy-MM-dd。注意,查询日期跨度不超过30天

end_date:结束日期,投诉发生的结束日期,格式为yyyy-MM-dd。注意,查询日期跨度不超过30天

注意

更多参数、响应详情及错误码请参见查询投诉单列表接口文档

3.2.2. 【服务端】查询投诉单详情

步骤说明:商户可通过调用此接口,查询指定投诉单的用户投诉详情,包含投诉内容、投诉关联订单、投诉人联系方式等信息,方便商户处理投诉。

示例代码


public void GetComplaintsInfo() throws Exception{
    //请求URL
    HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000");
    httpGet.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpGet);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'GET',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000', //请求URL
        [
            'headers' => [ 'Accept' => 'application/json']
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func GetComplaintsInfo() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
 //设置请求地址
 URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000"
  // 发起请求
  response, err := client.Get(ctx, URL)
  if err != nil{
    log.Printf("client get err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

complaint_id:投诉单对应的投诉单号,在投诉通知回调中会返回这个参数

注意

更多参数、响应详情及错误码请参见查询投诉单详情接口文档

3.2.3. 【服务端】查询投诉协商历史

步骤说明:商户可通过调用此接口,查询指定投诉的用户商户协商历史,以分页输出查询结果,方便商户根据处理历史来制定后续处理方案。

示例代码


public void GetComplaintsHis() throws Exception{
    //请求URL
    HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/negotiation-historys?limit=50&offset=10");
    httpGet.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpGet);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'GET',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/negotiation-historys?limit=50&offset=10', //请求URL
        [
            'headers' => [ 'Accept' => 'application/json']
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func GetComplaintsHis() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
 //设置请求地址
 URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/negotiation-historys?limit=50&offset=10"
  // 发起请求
  response, err := client.Get(ctx, URL)
  if err != nil{
    log.Printf("client get err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

complaint_id:投诉单对应的投诉单号,在投诉通知回调中会返回这个参数

注意

更多参数、响应详情及错误码请参见查询投诉协商历史接口文档

3.2.4. 【服务端】投诉通知回调

步骤说明:商户创建投诉通知回调URL 后,当有新的投诉事件发生、投诉状态发生变化时,商户会收到通知回调。商户需要接收处理,并按文档规范返回应答。

注意:

  • 投诉通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情
  • 加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》
  • 投诉通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx
  • 商户成功接收到回调通知后应返回成功的http应答码为200或204
  • 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

注意

更多参数、响应详情及错误码请参见投诉通知回调接口文档

3.2.5. 【服务端】创建投诉通知回调地址

步骤说明:商户通过调用此接口创建投诉通知回调URL,当用户产生新投诉且投诉状态已变更时,微信支付会通过回调URL通知商户。

示例代码


public void CreateComplaintsNotify() throws Exception{
    //请求URL
    HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications");
    // 请求body参数
    String reqdata = "{"
            + "\"url\":\"https://www.xxx.com/notify\""
            + "}";
    StringEntity entity = new StringEntity(reqdata,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpPost);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'POST',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications', //请求URL
        [
            // JSON请求体
            'json' => [
                "url" => "https://www.xxx.com/notify",
            ],
            'headers' => [ 'Accept' => 'application/json' ]
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func CreateComplaintsNotify() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
    //设置请求地址
  URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications"
  //设置请求信息,此处也可以使用结构体来进行请求
  mapInfo := map[string]interface{}{
    "url": "https://www.xxx.com/notify",
  }

  // 发起请求
  response, err := client.Post(ctx, URL, mapInfo)
  if err != nil{
    log.Printf("client post err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

url:通知地址,通知地址,仅支持https。

注意

更多参数、响应详情及错误码请参见创建投诉通知回调地址接口文档

3.2.6. 【服务端】查询投诉通知回调地址

步骤说明:商户通过调用此接口查询投诉通知的回调URL。

示例代码


public void GetComplaintsNotify() throws Exception{
    //请求URL
    HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications");
    httpGet.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpGet);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'GET',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications', //请求URL
        [
            'headers' => [ 'Accept' => 'application/json']
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func GetComplaintsNotify() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
 //设置请求地址
 URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications"
  // 发起请求
  response, err := client.Get(ctx, URL)
  if err != nil{
    log.Printf("client get err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

注意

更多参数、响应详情及错误码请参见查询投诉通知回调地址接口文档

3.2.7. 【服务端】更新投诉通知回调地址

步骤说明:商户通过调用此接口创建投诉通知回调URL,当用户产生新投诉且投诉状态已变更时,微信支付会通过回 调URL通知商户。

示例代码


public void UpdateComplaintsNotify() throws Exception{
    //请求URL
    HttpPut httpPut = new HttpPut("https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications");
    // 请求body参数
    String reqdata = "{"
            + "\"url\":\"https://www.123.com/notify\""
            + "}";
    StringEntity entity = new StringEntity(reqdata,"utf-8");
    entity.setContentType("application/json");
    httpPut.setEntity(entity);
    httpPut.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpPut);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'PUT',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications', //请求URL
        [
            // JSON请求体
            'json' => [
                "url" => "https://www.123.com/notify",
            ],
            'headers' => [ 'Accept' => 'application/json' ]
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func UpdateComplaintsNotify() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
    //设置请求地址
  URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications"
  //设置请求信息,此处也可以使用结构体来进行请求
  mapInfo := map[string]interface{}{
    "url": "https://www.xxx.com/notify",
  }

  // 发起请求
  response, err := client.Put(ctx, URL, mapInfo)
  if err != nil{
    log.Printf("client put err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

url:通知地址,通知地址,仅支持https。

注意

更多参数、响应详情及错误码请参见更新投诉通知回调地址接口文档

3.2.8. 【服务端】删除投诉通知回调地址

步骤说明:商户通过调用此接口查询投诉通知的回调URL。

示例代码


public void DelComplaintsNotify() throws Exception{
    //请求URL
    HttpDelete httpDel = new HttpDelete("https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications");
    httpDel.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpDel);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'DELETE',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications', //请求URL
        [
            'headers' => [ 'Accept' => 'application/json']
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func DelComplaintsNotify() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
 //设置请求地址
  URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaint-notifications"
  //设置请求信息,根据业务接口来填入
  mapInfo := map[string]interface{}{}
  // 发起请求
  response, err := client.Delete(ctx, URL, mapInfo)
  if err != nil{
    log.Printf("client delete err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

注意

更多参数、响应详情及错误码请参见删除投诉通知回调地址接口文档

3.2.9. 【服务端】提交回复

步骤说明:商户可调用此接口回复用户。其中上传图片凭证需首先调用商户上传反馈图片接口,得到图片id,再将id填入请求。

示例代码


public void ReplyInfo() throws Exception{
    //请求URL
    HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/response");
    // 请求body参数
    String reqdata = "{"
            + "\"complainted_mchid\":190001XXX,"
            + "\"response_content\":\"已与用户沟通解决\","
            + "\"response_images\": ["
            + "\"0\":\"8692450418652C00E4E623AF421E00B5.png\""
            + "]"
            + "}";
    StringEntity entity = new StringEntity(reqdata,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpPost);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'POST',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/response', //请求URL
        [
            // JSON请求体
            'json' => [
                "complainted_mchid" => 190001XXX, 
                "response_content" => "已与用户沟通解决", 
                "response_images" => [
                    "0" => "8692450418652C00E4E623AF421E00B5.png", 
                ]
            ],
            'headers' => [ 'Accept' => 'application/json' ]
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func ReplyInfo() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
    //设置请求地址
  URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/20020182020*****0000/response"
  //设置请求信息,此处也可以使用结构体来进行请求
  mapInfo := map[string]interface{}{
    "complainted_mchid": 1900012181,
    "response_content": "已与用户沟通解决",
    "response_images": [...]interface{}{
      "file23578_21798531.jpg",
    },
  }

  // 发起请求
  response, err := client.Post(ctx, URL, mapInfo)
  if err != nil{
    log.Printf("client post err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

complaint_id:投诉单对应的投诉单号

complainted_mchid:被诉商户号,投诉单对应的被诉商户号

response_content:回复内容,具体的投诉处理方案,限制200个字符以内。

注意

更多参数、响应详情及错误码请参见提交回复接口文档

3.2.10. 【服务端】反馈处理完成

步骤说明:商户可通过调用此接口,反馈投诉单已处理完成。

示例代码


public void CompleteComplaints() throws Exception{
    //请求URL
    HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/complete");
    // 请求body参数
    String reqdata = "{"
            + "\"complainted_mchid\":190001XXX"
            + "}";
    StringEntity entity = new StringEntity(reqdata,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = httpClient.execute(httpPost);

    try {
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
        } else if (statusCode == 204) { //处理成功,无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
            throw new IOException("request failed");
        }
    } finally {
        response.close();
    }
}

try {
    $resp = $client->request(
        'POST',
        'https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/200201820200101080076610000/complete', //请求URL
        [
            // JSON请求体
            'json' => [
                "complainted_mchid" => 190001XXX,
            ],
            'headers' => [ 'Accept' => 'application/json' ]
        ]
    );
    $statusCode = $resp->getStatusCode();
    if ($statusCode == 200) { //处理成功
        echo "success,return body = " . $resp->getBody()->getContents()."\n";
    } else if ($statusCode == 204) { //处理成功,无返回Body
        echo "success";
    }
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
    }
    return;
}
      

func CompleteComplaints() {
       // 初始化客户端
    ctx := context.TODO()
    opts, err := SetUp()
    if err != nil {
        return
    }
    client, err := core.NewClient(ctx, opts...,)
    if err != nil{
        log.Printf("init client err:%s",err)
        return
    }
    //设置请求地址
  URL := "https://api.mch.weixin.qq.com/v3/merchant-service/complaints-v2/02018202001011e/complete"
  //设置请求信息,此处也可以使用结构体来进行请求
  mapInfo := map[string]interface{}{
    "complainted_mchid": "1900012181",
  }

  // 发起请求
  response, err := client.Post(ctx, URL, mapInfo)
  if err != nil{
    log.Printf("client post err:%s",err)
    return
  }
  // 校验回包内容是否有逻辑错误
  err = core.CheckResponse(response)
  if err != nil{
    log.Printf("check response err:%s",err)
    return
  }
  // 读取回包信息
  body, err := ioutil.ReadAll(response.Body)
  if err != nil{
    log.Printf("read response body err:%s",err)
    return
  }
  fmt.Println(string(body))
}
    

重要入参说明

complaint_id:投诉单对应的投诉单号,在投诉通知回调中会返回这个参数

complainted_mchid:被诉商户号,投诉单对应的被诉商户号

注意

更多参数、响应详情及错误码请参见查询投诉单详情接口文档

3.2.11. 【服务端】商户上传反馈图片

步骤说明:商户上传反馈图片的接口。 将媒体图片进行二进制转换,得到的媒体图片二进制内容,在请求body中上传此二进制内容。 媒体图片只支持jpg、png、bmp格式,文件大小不能超过2M。

示例代码


String filePath = "/your/home/hellokitty.png";
URI uri = new URI("https://api.mch.weixin.qq.com/v3/merchant-service/images/upload");
File file = new File(filePath);

try (FileInputStream ins1 = new FileInputStream(file)) { 
  String sha256 = DigestUtils.sha256Hex(ins1);
  try (InputStream ins2 = new FileInputStream(file)) {
    HttpPost request = new WechatPayUploadHttpPost.Builder(uri)
        .withImage(file.getName(), sha256, ins2)
        .build();
    CloseableHttpResponse response1 = httpClient.execute(request);
  }
}


try {
    $resp = $client->request('POST', 'https://api.mch.weixin.qq.com/v3/merchant-service/images/upload', [
        'body'    => $media->getStream(),
        'headers' => [
            'Accept'       => 'application/json',
            'content-type' => $media->getContentType(),
        ]
    ]);
    // POST 语法糖
    $resp = $client->post('merchant-service/images/upload', [
        'body'    => $media->getStream(),
        'headers' => [
            'Accept'       => 'application/json',
            'content-type' => $media->getContentType(),
        ]
    ]);
    echo $resp->getStatusCode().' '.$resp->getReasonPhrase()."\n";
    echo $resp->getBody()."\n";
} catch (Exception $e) {
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo $e->getResponse()->getStatusCode().' '.$e->getResponse()->getReasonPhrase()."\n";
        echo $e->getResponse()->getBody();
    }
    return;
}
      

uploadURL := "https://api.mch.weixin.qq.com/v3/merchant-service/images/upload"
filePath := ""
fileName := ""
type Meta struct {
    FileName string `json:"filename" binding:"required"` // 商户上传的媒体图片的名称,商户自定义,必须以JPG、BMP、PNG为后缀。
    Sha256   string `json:"sha256" binding:"required"`   // 图片文件的文件摘要,即对图片文件的二进制内容进行sha256计算得到的值。
}
// 读取文件
pictureByes, err := ioutil.ReadFile(filePath)
if err != nil {
    log.Printf("read file err:%s", err.Error())
    return
}
// 计算文件序列化后的sha256
h := sha256.New()
if _, err = h.Write(pictureByes); err != nil {
    return
}

meta := &Meta{}
pictureSha256 := h.Sum(nil)
meta.FileName = fileName
meta.Sha256 = fmt.Sprintf("%x", string(pictureSha256))
metaByte, err := json.Marshal(meta)
if err != nil {
    log.Printf("json marshal meta info err:%s", err.Error())
    return
}
reqBody := &bytes.Buffer{}
writer := multipart.NewWriter(reqBody)
// 设置reqbody中的meta部分
if err = core.CreateFormField(writer, "meta", "application/json", metaByte); err != nil {
    log.Printf("read form field err:%s", err.Error())
    return
}
// 设置reqbody中的file部分
if err = core.CreateFormFile(writer, fileName, "image/jpg", pictureByes); err != nil {
    log.Printf("read form file err:%s", err.Error())
    return
}
if err = writer.Close(); err != nil {
    log.Printf("close io.writer err:%s", err.Error())
    return
}
response, err = client.Upload(ctx, uploadURL, string(metaByte), reqBody.String(), writer.FormDataContentType())
if err != nil {
    log.Printf("weChatPayHttpClient Upload:%s uploadBody:%+v Response Err:%s",
        uploadURL, reqBody.String(), err.Error())
    return
}
if response.Body != nil {
    defer response.Body.Close()
}
// 获取回包中的信息
body, err = ioutil.ReadAll(response.Body)
if err != nil {
    log.Printf("read response body err:%s", err.Error())
    return
}
log.Printf("upload success response body:%s", string(body))
    

重要入参说明

file:图片文件,将媒体图片进行二进制转换,得到的媒体图片二进制内容,在请求body中上传此二进制内容。媒体图片只支持jpg、png、bmp格式,文件大小不能超过2M。

注意

更多参数、响应详情及错误码请参见商户上传反馈图片接口文档

4. 常见问题

Q:商户反馈API,查询投诉单列表 API频率是多少?

A:商户反馈API每分钟60次,查询投诉单列表 API频率是每分钟200次

Q:商户反馈API,商户传给微信侧的反馈和留言是同步实时处理的吗?

A:是实时的,会直接展示给用户

Q:投诉通知回调API中的通知ID,在重复通知的情况下,ID是一致的吗?

A:微信存在重复通知的情况,关于重复通知的客户投诉,通知id是一致的

Q:设置了投诉通知url,存在用户投诉,但收不到投诉通知的情况,如何解决?

A:请按照以下几点进行排查:

1. 请检查回调url是否能正常公网访问,不能是中文域名,不能是http,必须是https

2. 请注意回调url不能携带参数

3. 请检查是否开启了防火墙,如果开启了防火墙,请添加微信支付回调IP(微信支付回调通知出口IP列表

4. 请检查设置通知url的商户号与投诉订单的商户号是否为同一个

5. APIv3密钥没有设置,设置路径:【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】



技术咨询

文档反馈