# 消息推送回复

当你使用微信云托管配置[消息推送](/miniprogram/dev/wxcloudservice/wxcloudrun/src/guide/weixin/push)后，云托管服务就会接收用户从小程序/公众号发来的消息，并以你设定的格式（JSON 或 XML）传入你的服务路径中。

你可以在云托管服务中回复特定的 JSON 数据来回复。

## 一、被动回复

原理可以参考[此文档](/doc/service/guide/product/message/Receiving_standard_messages.md)，发送被动响应消息其实并不是一种接口，而是对微信服务器发过来消息的一次回复。

**需要注意，被动回复只支持公众号，小程序需要使用主动回复。**

具体消息格式如下：

### 1. 回复文本消息

``` json
{
  "ToUserName": "用户OPENID",
  "FromUserName": "公众号/小程序原始ID",
  "CreateTime": "发送时间", // 整型，例如：1648014186
  "MsgType": "text",
  "Content": "文本消息"
}
```

### 2. 回复图片消息

``` json
{
  "ToUserName": "用户OPENID",
  "FromUserName": "公众号/小程序原始ID",
  "CreateTime": "发送时间", // 整型，例如：1648014186
  "MsgType": "image",
  "Image": {
    "MediaId": "素材ID"
  }
}
```

素材的上传请移步文档最后

### 3. 回复语音消息

``` json
{
  "ToUserName": "用户OPENID",
  "FromUserName": "公众号/小程序原始ID",
  "CreateTime": "发送时间", // 整型，例如：1648014186
  "MsgType": "voice",
  "Voice": {
    "MediaId": "素材ID"
  }
}
```

素材的上传请移步文档最后

### 4. 回复视频消息

``` json
{
  "ToUserName": "用户OPENID",
  "FromUserName": "公众号/小程序原始ID",
  "CreateTime": "发送时间", // 整型，例如：1648014186
  "MsgType": "video",
  "Video": {
    "MediaId": "素材ID",
    "Title": "视频标题",
    "Description": "视频描述"
  }
}
```

素材的上传请移步文档最后

### 5. 回复音乐消息

``` json
{
  "ToUserName": "用户OPENID",
  "FromUserName": "公众号/小程序原始ID",
  "CreateTime": "发送时间", // 整型，例如：1648014186
  "MsgType": "music",
  "Music": {
    "Title": "音乐标题",
    "Description": "音乐描述",
    "MusicUrl": "音乐的链接地址",
    "HQMusicUrl": "高质量音乐链接，WIFI环境优先使用该链接播放音乐",
    "ThumbMediaId":"缩略图的媒体id"
  }
}
```

素材的上传请移步文档最后

### 6. 回复图文消息

``` json
{
  "ToUserName": "用户OPENID",
  "FromUserName": "公众号/小程序原始ID",
  "CreateTime": "发送时间", // 整型，例如：1648014186
  "MsgType": "news",
  "ArticleCount": 2, // 图文消息个数；当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时，开发者只能回复1条图文消息；其余场景最多可回复8条图文消息
  "Articles": [{
    "Title": "图文标题",
    "Description": "图文描述",
    "PicUrl": "图片链接", // 支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
    "Url":"点击图文消息跳转链接"
  },{
    "Title": "图文标题",
    "Description": "图文描述",
    "PicUrl": "图片链接", // 支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
    "Url":"点击图文消息跳转链接"
  }]
}
```

## 二、主动回复

如果你无法即时回复，需要一段时间后再回复，可以使用接口发起回复。

以下使用建议配合[开放接口服务](/miniprogram/dev/wxcloudservice/wxcloudrun/src/guide/weixin/open)一块使用。

### 接口地址

``` bash
http://api.weixin.qq.com/cgi-bin/message/custom/send
```

需要将 `/cgi-bin/message/custom/send` 加入[配置白名单](https://cloud.weixin.qq.com/cloudrun/openapi)

### 入参数据

1. 发送文本

``` json
{
  "touser":"用户OPENID",
  "msgtype":"text",
  "text": {
    "content":"文本消息"
  }
}
```

2. 发送图片

``` json
{
  "touser":"用户OPENID",
  "msgtype":"image",
  "image": {
    "media_id":"素材ID"
  }
}
```

3. 发送链接，公众号等同图文，只限 1 条

``` json
{
  "touser":"用户OPENID",
  "msgtype":"link",
  "link": {
    "title": "图文标题",
    "description": "图文描述",
    "thumb_url": "图片链接", // 支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
    "url": "跳转链接"
  }
}
```

4. 发送音乐

``` json
{
  "touser":"用户OPENID",
  "msgtype":"music",
  "music": {
    "title": "音乐标题",
    "description": "音乐描述",
    "music_url": "音乐链接地址",
    "HQ_music_url": "高清音乐链接地址，用于wifi",
    "thumb_media_id": "缩略图素材ID"
  }
}
```

5. 发送视频

``` json
{
  "touser":"用户OPENID",
  "msgtype":"video",
  "video":
  {
    "media_id": "媒体素材ID",
    "title": "视频标题",
    "description": "视频描述"
  }
}
```

6. 发送语音

``` json
{
  "touser":"用户OPENID",
  "msgtype":"voice",
  "voice":
  {
    "media_id": "媒体素材ID"
  }
}
```

7. 小程序卡片

``` json
{
  "touser":"用户OPENID",
  "msgtype":"miniprogrampage",
  "miniprogrampage":
  {
    "title": "消息标题",
    "pagepath":"小程序的页面路径", // 跟app.json对齐，支持参数，比如pages/index/index?foo=bar
    "thumb_media_id": "缩略图素材ID"
  }
}
```

### 回参数据

``` json
{
    "errcode": 0,
    "errmsg": "ok"
}
```

注意，上述的消息类型不是所有都能发送的，不同类型的账号有一定的限制。

比如，公众号类型不可发送「小程序卡片」，小程序类型不可发「视频」、「语音」等，具体都会动态变化，请先验证一下是否可以发送成功。

如果报`invalid appid`、`invalid type` 信息，就意味着你的 **AppID** 类型不能发这种消息，这是正常的，不要将其视为错误。

![](https://res8.wxqcloud.qq.com.cn/wxdoc/3fc53570-0405-4394-95b6-293451438ba1.png)

## 三、素材上传

上面发送消息需要的各类素材，可根据业务实际使用情况按需使用对应的接口上传素材，下面以小程序新增图片素材接口为例。公众号可按[公众号素材管理](https://developers.weixin.qq.com/doc/subscription/api/material/temporary/api_uploadmedia_image)相关接口使用即可。

以下使用建议配合[开放接口服务](/miniprogram/dev/wxcloudservice/wxcloudrun/src/guide/weixin/open)一块使用。

### 接口地址

``` bash
http://api.weixin.qq.com/cgi-bin/media/upload?type=TYPE
```

需要将 `/cgi-bin/media/upload` 加入[配置白名单](https://cloud.weixin.qq.com/cloudrun/openapi)

### 入参数据

URL 参数中的 `type` 需要填写自己上传的素材类型值，范围如下：

|type 值|类型|描述|
|---|---|---|
|image|图片|10M，支持 PNG\JPEG\JPG\GIF 格式|
|voice|语音|2M，播放长度不超过 60s，支持 AMR\MP3 格式|
|video|视频|10MB，支持 MP4 格式|
|thumb|缩略图|64KB，支持 JPG 格式|

body 中传`form-data`形式，参数如下：

|参数值|类型|描述|
|---|---|---|
|media|[File](https://developer.mozilla.org/zh-CN/docs/Web/API/File)|媒体文件，有 filename、filelength、content-type 等信息|

### 出参数据

正确情况下的返回 JSON 数据包结果如下：

``` json
{
  "type":"TYPE",
  "media_id":"MEDIA_ID",
  "created_at":123456789
}
```

|参数|描述|
|---|---|
|type|媒体文件类型，分别有图片（image）、语音（voice）、视频（video）和缩略图（thumb，主要用于视频与音乐格式的缩略图）|
|media_id|媒体文件上传后，获取标识，3 天内有效|
|created_at|媒体文件上传时间戳|

错误情况下的返回 JSON 数据包示例如下：

``` json
{
  "errcode":40004,
  "errmsg":"invalid media type"
}
```

## 四、演示 DEMO

### 1. 主动回复例子

以下例子使用`node.js` 来构建公众号主动回复 DEMO，其中 mediaid 自行上传替换，文件夹下共三个文件。

小程序演示效果如下：

![](https://res8.wxqcloud.qq.com.cn/wxdoc/710fa1ef-1eb2-4504-9318-00915ce450df.png)

`index.js` 文件，需要注意替换里面所有 media_id，以及小程序路径等配置项。

``` js
const express = require('express')
const request = require('request')

const app = express()

app.use(express.json())

app.all('/', async (req, res) => {
  console.log('消息推送', req.body)
  // 从header中取appid，如果from-appid不存在，则不是资源复用场景，可以直接传空字符串，使用环境所属账号发起云调用
  const appid = req.headers['x-wx-from-appid'] || ''
  const { ToUserName, FromUserName, MsgType, Content, CreateTime } = req.body
  console.log('推送接收的账号', ToUserName, '创建时间', CreateTime)
  if (MsgType === 'text') {
    if (Content === '回复文字') { // 小程序、公众号可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'text',
        text: {
          content: '这是回复的消息'
        }
      })
    } else if (Content === '回复图片') { // 小程序、公众号可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'image',
        image: {
          media_id: 'P-hoCzCgrhBsrvBZIZT3jx1M08WeCCHf-th05M4nac9TQO8XmJc5uc0VloZF7XKI'
        }
      })
    } else if (Content === '回复语音') { // 仅公众号可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'voice',
        voice: {
          media_id: '06JVovlqL4v3DJSQTwas1QPIS-nlBlnEFF-rdu03k0dA9a_z6hqel3SCvoYrPZzp'
        }
      })
    } else if (Content === '回复视频') {  // 仅公众号可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'video',
        video: {
          media_id: 'XrfwjfAMf820PzHu9s5GYsvb3etWmR6sC6tTH2H1b3VPRDedW-4igtt6jqYSBxJ2',
          title: '微信云托管官方教程',
          description: '微信官方团队打造，贴近业务场景的实战教学'
        }
      })
    } else if (Content === '回复音乐') {  // 仅公众号可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'music',
        music: {
          title: 'Relax｜今日推荐音乐',
          description: '每日推荐一个好听的音乐，感谢收听～',
          music_url: 'https://c.y.qq.com/base/fcgi-bin/u?__=0zVuus4U',
          HQ_music_url: 'https://c.y.qq.com/base/fcgi-bin/u?__=0zVuus4U',
          thumb_media_id: 'XrfwjfAMf820PzHu9s5GYgOJbfbnoUucToD7A5HFbBM6_nU6TzR4EGkCFTTHLo0t'
        }
      })
    } else if (Content === '回复图文') {  // 小程序、公众号可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'link',
        link: {
          title: 'Relax｜今日推荐音乐',
          description: '每日推荐一个好听的音乐，感谢收听～',
          thumb_url: 'https://y.qq.com/music/photo_new/T002R300x300M000004NEn9X0y2W3u_1.jpg?max_age=2592000', // 支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
          url: 'https://c.y.qq.com/base/fcgi-bin/u?__=0zVuus4U'
        }
      })
    } else if (Content === '回复小程序') { // 仅小程序可用
      await sendmess(appid, {
        touser: FromUserName,
        msgtype: 'miniprogrampage',
        miniprogrampage: {
          title: '小程序卡片标题',
          pagepath: 'pages/index/index', // 跟app.json对齐，支持参数，比如pages/index/index?foo=bar
          thumb_media_id: 'XrfwjfAMf820PzHu9s5GYgOJbfbnoUucToD7A5HFbBM6_nU6TzR4EGkCFTTHLo0t'
        }
      })
    }
    res.send('success')
  } else {
    res.send('success')
  }
})

app.listen(80, function () {
  console.log('服务启动成功！')
})

function sendmess (appid, mess) {
  return new Promise((resolve, reject) => {
    request({
      method: 'POST',
      url: `http://api.weixin.qq.com/cgi-bin/message/custom/send?from_appid=${appid}`,
      body: JSON.stringify(mess)
    }, function (error, response) {
      if (error) {
        console.log('接口返回错误', error)
        reject(error.toString())
      } else {
        console.log('接口返回内容', response.body)
        resolve(response.body)
      }
    })
  })
}
```

`Dockerfile` 文件

``` dockerfile
FROM node:12-slim

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm config set registry https://mirrors.tencent.com/npm/

RUN npm install

COPY . ./

CMD ["node", "index.js"]
```

`package.json` 文件

```json
{
  "name": "cloudbase-push",
  "version": "1.0.0",
  "description": "call push server",
  "main": "index.js",
  "scripts": {},
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4",
    "request": "^2.88.2"
  }
}
```

首先，在[云托管控制台-云调用](https://cloud.weixin.qq.com/cloudrun/openapi)中打开开放接口服务，并配置 `/cgi-bin/message/custom/send` 到微信令牌权限配置。

将上述 3 个文件组成文件夹，版本创建上传，端口填写 `80`，部署发布。

在消息推送配置填写对应的服务，路径填写 `/`。

### 2. 被动回复例子

以下例子使用`node.js` 来构建公众号被动回复 DEMO，其中 mediaid 自行上传替换，文件夹下共三个文件，**只适用于公众号**。

演示效果如下：

![](https://res8.wxqcloud.qq.com.cn/wxdoc/b13f408a-33d1-419f-8cf7-88d0c98529a9.png)

`index.js` 文件

``` js
const express = require('express')
const bodyParser = require('body-parser')

const PORT = process.env.PORT || 80

const app = express()

app.use(bodyParser.raw())
app.use(bodyParser.json({}))
app.use(bodyParser.urlencoded({ extended: true }))

app.all('/', async (req, res) => {
  console.log('消息推送', req.body)
  const { ToUserName, FromUserName, MsgType, Content, CreateTime } = req.body
  if (MsgType === 'text') {
    if (Content === '回复文字') {
      res.send({
        ToUserName: FromUserName,
        FromUserName: ToUserName,
        CreateTime: CreateTime,
        MsgType: 'text',
        Content: '这是回复的消息'
      })
    } else if (Content === '回复图片') {
      res.send({
        ToUserName: FromUserName,
        FromUserName: ToUserName,
        CreateTime: CreateTime,
        MsgType: 'image',
        Image: {
          MediaId: 'P-hoCzCgrhBsrvBZIZT3jx1M08WeCCHf-th05M4nac9TQO8XmJc5uc0VloZF7XKI'
        }
      })
    } else if (Content === '回复语音') {
      res.send({
        ToUserName: FromUserName,
        FromUserName: ToUserName,
        CreateTime: CreateTime,
        MsgType: 'voice',
        Voice: {
          MediaId: '06JVovlqL4v3DJSQTwas1QPIS-nlBlnEFF-rdu03k0dA9a_z6hqel3SCvoYrPZzp'
        }
      })
    } else if (Content === '回复视频') {
      res.send({
        ToUserName: FromUserName,
        FromUserName: ToUserName,
        CreateTime: CreateTime,
        MsgType: 'video',
        Video: {
          MediaId: 'XrfwjfAMf820PzHu9s5GYsvb3etWmR6sC6tTH2H1b3VPRDedW-4igtt6jqYSBxJ2',
          Title: '微信云托管官方教程',
          Description: '微信官方团队打造，贴近业务场景的实战教学'
        }
      })
    } else if (Content === '回复音乐') {
      res.send({
        ToUserName: FromUserName,
        FromUserName: ToUserName,
        CreateTime: CreateTime,
        MsgType: 'music',
        Music: {
          Title: 'Relax｜今日推荐音乐',
          Description: '每日推荐一个好听的音乐，感谢收听～',
          MusicUrl: 'https://c.y.qq.com/base/fcgi-bin/u?__=0zVuus4U',
          HQMusicUrl: 'https://c.y.qq.com/base/fcgi-bin/u?__=0zVuus4U',
          ThumbMediaId: 'XrfwjfAMf820PzHu9s5GYgOJbfbnoUucToD7A5HFbBM6_nU6TzR4EGkCFTTHLo0t'
        }
      })
    } else if (Content === '回复图文') {
      res.send({
        ToUserName: FromUserName,
        FromUserName: ToUserName,
        CreateTime: CreateTime,
        MsgType: 'news',
        ArticleCount: 1,
        Articles: [{
          Title: 'Relax｜今日推荐音乐',
          Description: '每日推荐一个好听的音乐，感谢收听～',
          PicUrl: 'https://y.qq.com/music/photo_new/T002R300x300M000004NEn9X0y2W3u_1.jpg?max_age=2592000',
          Url: 'https://c.y.qq.com/base/fcgi-bin/u?__=0zVuus4U'
        }]
      })
    } else {
      res.send('success')
    }
  } else {
    res.send('success')
  }
})

app.listen(PORT, function () {
  console.log(`运行成功，端口：${PORT}`)
})
```

`Dockerfile` 文件

``` dockerfile
FROM node:12-slim

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm config set registry https://mirrors.tencent.com/npm/

RUN npm install

COPY . ./

CMD ["node", "index.js"]
```

`package.json` 文件

```json
{
  "name": "cloudbase-push",
  "version": "1.0.0",
  "description": "call push server",
  "main": "index.js",
  "scripts": {},
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4"
  }
}
```

将 3 个文件组成文件夹，版本创建上传，端口填写 `80`，部署发布。

在消息推送配置填写对应的服务，路径填写 `/`
