# 消息推送回复

当你使用微信云托管配置消息推送后,云托管服务就会接收用户从小程序/公众号发来的消息,并以你设定的格式(json或xml)传入你的服务路径中。

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

# 一、被动回复

原理可以参考此文档,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。

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

具体消息格式如下:

# 1. 回复文本消息

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

# 2. 回复图片消息

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

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

# 3. 回复语音消息

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

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

# 4. 回复视频消息

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

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

# 5. 回复音乐消息

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

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

# 6. 回复图文消息

{
  "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":"点击图文消息跳转链接"
  }]
}

# 二、主动回复

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

以下使用建议配合开放接口服务一块使用。

# 接口地址
http://api.weixin.qq.com/cgi-bin/message/custom/send

需要将 /cgi-bin/message/custom/send 加入配置白名单

# 入参数据
  1. 发送文本
{
  "touser":"用户OPENID",
  "msgtype":"text",
  "text": {
    "content":"文本消息"
  }
}
  1. 发送图片
{
  "touser":"用户OPENID",
  "msgtype":"image",
  "image": {
    "media_id":"素材ID"
  }
}
  1. 发送链接,公众号等同图文,只限1条
{
  "touser":"用户OPENID",
  "msgtype":"link",
  "link": {
    "title": "图文标题",
    "description": "图文描述",
    "thumb_url": "图片链接", // 支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
    "url": "跳转链接"
  }
}
  1. 发送音乐
{
  "touser":"用户OPENID",
  "msgtype":"music",
  "music": {
    "title": "音乐标题",
    "description": "音乐描述",
    "music_url": "音乐链接地址",
    "HQ_music_url": "高清音乐链接地址,用于wifi",
    "thumb_media_id": "缩略图素材ID"
  }
}
  1. 发送视频
{
  "touser":"用户OPENID",
  "msgtype":"video",
  "video":
  {
    "media_id": "媒体素材ID",
    "title": "视频标题",
    "description": "视频描述"
  }
}
  1. 发送语音
{
  "touser":"用户OPENID",
  "msgtype":"voice",
  "voice":
  {
    "media_id": "媒体素材ID"
  }
}
  1. 小程序卡片
{
  "touser":"用户OPENID",
  "msgtype":"miniprogrampage",
  "miniprogrampage":
  {
    "title": "消息标题",
    "pagepath":"小程序的页面路径", // 跟 app.json 对齐,支持参数,比如pages/index/index?foo=bar
    "thumb_media_id": "缩略图素材ID"
  }
}
# 回参数据
{
    "errcode": 0,
    "errmsg": "ok"
}

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

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

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

# 三、素材上传

上面发送消息需要的各类素材,请使用下面的接口上传

以下使用建议配合开放接口服务一块使用。

# 接口地址
http://api.weixin.qq.com/cgi-bin/media/upload?type=TYPE

需要将 /cgi-bin/media/upload 加入配置白名单

# 入参数据

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 媒体文件,有filename、filelength、content-type等信息
# 出参数据

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

{
  "type":"TYPE",
  "media_id":"MEDIA_ID",
  "created_at":123456789
}
参数 描述
type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb,主要用于视频与音乐格式的缩略图)
media_id 媒体文件上传后,获取标识
created_at 媒体文件上传时间戳

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

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

# 四、演示DEMO

# 1. 主动回复例子

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

小程序演示效果如下:

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

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 文件

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 文件

{
  "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"
  }
}

首先,在云托管控制台 - 云调用中打开开放接口服务,并配置 /cgi-bin/message/custom/send 到微信令牌权限配置。

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

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

# 2. 被动回复例子

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

演示效果如下:

index.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 文件

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 文件

{
  "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,部署发布。

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