C#开发微信门户及应用(30)--消息的群发处理和预览功能

在很多场合下,我们可能需要利用微信公众号的优势,定期给指定用户群发送一些推广消息或者新闻内容,以便给关注客户一种经常更新公众号内容的感觉,同时也方便我们经常和用户进行互动。微信公众号的高级群发接口就是为了处理这个场景的,本文介绍在C#代码中如何封装消息的群发和预览等功能。

1、消息群发的功能和限制

对于公众号中的服务号和订阅号,群发的消息有一定的限制,具体规则如下所示。
1、对于认证订阅号,群发接口每天可成功调用1次,此次群发可选择发送给全部用户或某个分组;2、对于认证服务号虽然开发者使用高级群发接口的每日调用限制为100次,但是用户每月只能接收4条,无论在公众平台网站上,还是使用接口群发,用户每月只能接收4条群发消息,多于4条的群发将对该用户发送失败;3、具备微信支付权限的公众号,在使用群发接口上传、群发图文消息类型时,可使用<a>标签加入外链;4、开发者可以使用预览接口校对消息样式和排版,通过预览接口可发送编辑好的消息给指定用户校验效果。
群发图文消息的过程如下:
1、首先,预先将图文消息中需要用到的图片,使用上传图文消息内图片接口,上传成功并获得图片URL2、上传图文消息素材,需要用到图片时,请使用上一步获取的图片URL3、使用对用户分组的群发,或对OpenID列表的群发,将图文消息群发出去4、在上述过程中,如果需要,还可以预览图文消息、查询群发状态,或删除已群发的消息等
群发图片、文本等其他消息类型的过程如下:
1、如果是群发文本消息,则直接根据下面的接口说明进行群发即可2、如果是群发图片、视频等消息,则需要预先通过素材管理接口准备好mediaID

2、消息的群发处理

虽然群发的消息类型有几种,如包括图文消息、文本消息、图片、视频、语音、卡劵等等,不过消息群发方式分为两类:根据群组发送消息和根据OpenID发送消息两种。
根据微信接口的定义,我们设计了对上面两种不同方式的发送接口,我们把不同类型的消息放到枚举MassMessageType 进行定义。

/// <summary>
/// 根据分组进行群发消息(图文消息、文本消息、语音消息、视频消息、图片、卡劵等)
/// </summary>
/// <param name="accessToken">访问凭证</param>
/// <param name="mediaIdOrContent">群发媒体文件时传入mediaId,群发文本消息时传入content,群发卡券时传入cardId</param>
/// <param name="groupId">群发到的分组的group_id</param>
/// <param name="isToAll">
/// 使用is_to_all为true且成功群发,会使得此次群发进入历史消息列表。
/// 设置is_to_all为false时是可以多次群发的,但每个用户只会收到最多4条,且这些群发不会进入历史消息列表</param>
/// <returns></returns>
MassMessageResult SendByGroup(string accessToken, MassMessageType messageType, string mediaIdOrContent, string groupId, bool isToAll = false);

/// <summary>
/// 根据OpenId进行群发消息(视频消息需要单独)
/// </summary>
/// <param name="accessToken">访问凭证</param>
/// <param name="messageType">消息类型</param>
/// <param name="mediaIdOrContent">用于群发的消息的media_id</param>
/// <param name="openIdList">openId字符串数组</param>        
/// <returns></returns>
MassMessageResult SendByOpenId(string accessToken, MassMessageType messageType, string mediaIdOrContent, List<string> openIdList);

其中枚举MassMessageType定义代码如下所示。

/// <summary>
/// 群发消息的类型
/// </summary>
public enum MassMessageType
{
    /// <summary>
    /// 图文消息
    /// </summary>
    mpnews,

    /// <summary>
    /// 文本消息
    /// </summary>
    text,

    /// <summary>
    /// 图片
    /// </summary>
    image,

    /// <summary>
    /// 语音
    /// </summary>
    voice,

    /// <summary>
    /// 音乐
    /// </summary>
    music,

    /// <summary>
    /// 视频
    /// </summary>
    video,

    /// <summary>
    /// 卡劵
    /// </summary>
    wxcard
}

然后我们根据上面的接口实现相关的处理函数,群发消息的类定义代码如下所示。

/// <summary>
/// 消息群发.
/// 在公众平台网站上,为订阅号提供了每天1条的群发权限,为服务号提供每月(自然月)4条的群发权限。
/// 而对于某些具备开发能力的公众号运营者,可以通过高级群发接口,实现更灵活的群发能力。
/// </summary>
public class MassSendApi : IMassSendApi

对于图文消息的群发规则,微信接口定义如下。
接口调用请求说明

http请求方式: POST
https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN

POST数据说明
POST数据示例如下:
图文消息(注意图文消息的media_id需要通过上述方法来得到):

{
   "filter":{
      "is_to_all":false,
      "group_id":2
   },
   "mpnews":{
      "media_id":"123dsdajkasd231jhksad"
   },
    "msgtype":"mpnews"
}

其他类似文本消息、图片、视频、语音、卡劵等发送方式类似,都是提供一个不同的JSON字符串,然后提交到对应的连接地址就可以了,因此我们可以把它们进行统一的封装处理。

我们可以在一个条件语句里面对内容进行组装,例如对于图文消息的处理代码如下所示。

switch (messageType)
{
    case MassMessageType.mpnews://图文消息
        postData = new
        {
            filter = new
            {
                is_to_all = isToAll, //是否让此次群发进入历史消息列表
                group_id = groupId //群发到的分组的group_id
            },
            mpnews = new
            {
                media_id = mediaIdOrContent  //用于群发的消息的media_id
            },
            msgtype = "mpnews"
        }.ToJson();
        break;

对于文本消息的组装如下所示。

case MassMessageType.text://文本消息
    postData = new
    {
        filter = new
        {
            is_to_all = isToAll, //是否让此次群发进入历史消息列表
            group_id = groupId //群发到的分组的group_id
        },
        text = new
        {
            content = mediaIdOrContent  //用于群发的消息的内容
        },
        msgtype = "text"
    }.ToJson();
    break;

最后我们通过代码进行提交JSON数据,并获取返回结果即可,如下代码所示。

string url = string.Format("https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token={0}", accessToken);

MassMessageResult result = JsonHelper<MassMessageResult>.ConvertJson(url, postData);
return result;

这样,整合各个消息类型的处理,我们就可以得到一个完整的消息群发操作了。


群发给openid的操作也是类似上面的处理方式,也是通过一个switch的条件语句,进行不同内容的构建,然后统一发送即可。
请注意:在返回成功时,意味着群发任务提交成功,并不意味着此时群发已经结束,所以,仍有可能在后续的发送过程中出现异常情况导致用户未收到消息,如消息有时会进行审核、服务器不稳定等。此外,群发任务一般需要较长的时间才能全部发送完毕,请耐心等待
由于群发任务提交后,群发任务可能在一定时间后才完成,因此,群发接口调用时,仅会给出群发任务是否提交成功的提示,若群发任务提交成功,则在群发任务结束时,会向开发者在公众平台填写的开发者URL(callback URL)推送事件。
推送的XML结构如下(发送成功时):

<xml>
<ToUserName><![CDATA[gh_3e8adccde292]]></ToUserName>
<FromUserName><![CDATA[oR5Gjjl_eiZoUpGozMo7dbBJ362A]]></FromUserName>
<CreateTime>1394524295</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[MASSSENDJOBFINISH]]></Event>
<MsgID>1988</MsgID>
<Status><![CDATA[sendsuccess]]></Status>
<TotalCount>100</TotalCount>
<FilterCount>80</FilterCount>
<SentCount>75</SentCount>
<ErrorCount>5</ErrorCount>
</xml>

对应的字段说明如下所示。

ToUserName 公众号的微信号
FromUserName 公众号群发助手的微信号,为mphelper
CreateTime 创建时间的时间戳
MsgType 消息类型,此处为event
Event 事件信息,此处为MASSSENDJOBFINISH
MsgID 群发的消息ID
Status 群发的结构,为“send success”或“send fail”或“err(num)”。但send success时,也有可能因用户拒收公众号的消息、系统错误等原因造成少量用户接收失败。err(num)是审核失败的具体原因
TotalCount group_id下粉丝数;或者openid_list中的粉丝数
FilterCount 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,FilterCount = SentCount + ErrorCount
SentCount 发送成功的粉丝数
ErrorCount 发送失败的粉丝数

因此我们需要通过处理消息群发的发送完成操作,定义一个实体类来承载这个消息。

public class RequestMassSendJobFinish : BaseEvent
{
    public RequestMassSendJobFinish()
    {
        this.MsgType = RequestMsgType.Event.ToString().ToLower();
        this.Event = RequestEvent.MASSSENDJOBFINISH.ToString();
    }

    /// <summary>
    /// 群发的消息ID
    /// </summary>
    public int MsgID { get; set; }

    /// <summary>
    /// 返回状态。
    /// </summary>
    public string Status { get; set; }

    /// <summary>
    /// group_id下粉丝数;或者openid_list中的粉丝数
    /// </summary>
    public int TotalCount { get; set; }

    /// <summary>
    /// 过滤(过滤是指,有些用户在微信设置不接收该公众号的消息)后,准备发送的粉丝数,原则上,FilterCount = SentCount + ErrorCount
    /// </summary>
    public int FilterCount { get; set; }

    /// <summary>
    /// 发送成功的粉丝数
    /// </summary>
    public int SendCount { get; set; }

    /// <summary>
    /// 发送失败的粉丝数
    /// </summary>
    public int ErrorCount { get; set; }
}

在我们需要记录或者更新处理这种群发消息的状态的时候,我们可以在整个微信的消息链里面对这样的请求事件进行处理,如下代码是处理这种群发消息的通知的。

case RequestEvent.MASSSENDJOBFINISH:
    {
        //由于群发任务彻底完成需要较长时间,将会在群发任务即将完成的时候,就推送群发结果,此时的推送人数数据将会与实际情形存在一定误差
        RequestMassSendJobFinish info = XmlConvertor.XmlToObject(postStr, typeof(RequestMassSendJobFinish)) as RequestMassSendJobFinish;
        if(info != null)
        {
            //在此记录群发完成的处理
        }
        LogTextHelper.Info(eventName + ((info == null) ? "info is null" : info.ToJson()));
    }
    break;

3、待群发消息的预览

在很多时候,我们群发消息之前,我们希望通过自己的微信号来看看具体的群发消息效果,如果没有问题我们在统一群发,相当于一个真实的审核过程,这样对于我们发送高质量的消息是一个很好的习惯。

对于普通的消息预览,我们定义的接口如下所示。

/// <summary>
/// 预览接口【订阅号与服务号认证后均可用】。
/// 开发者可通过该接口发送消息给指定用户,在手机端查看消息的样式和排版。
/// 为了满足第三方平台开发者的需求,在保留对openID预览能力的同时,增加了对指定微信号发送预览的能力,但该能力每日调用次数有限制(100次),请勿滥用。
/// </summary>
/// <param name="accessToken">访问凭证</param>
/// <param name="messageType">消息类型</param>
/// <param name="media_id">用于群发的消息的media_id</param>
/// <param name="touserOpenId">接收消息用户对应该公众号的openid</param>
/// <param name="towxname">可以针对微信号进行预览(而非openID),towxname和touser同时赋值时,以towxname优先</param>
/// <returns></returns>
MassMessageResult PreviewMessage(string accessToken, MassMessageType messageType, string media_id, string touserOpenId, string towxname = null);

具体的实现也就是针对不同的消息类型,构建一个不同的处理机制,把它们差异性的JSON构造出来,然后统一调用就可以了,具体代码如下所示。

public MassMessageResult PreviewMessage(string accessToken, MassMessageType messageType, string media_id, string touserOpenId, string towxname = null)
{
    string postData = "";
    switch (messageType)
    {
        case MassMessageType.mpnews://图文消息
            postData = new
            {
                touser = touserOpenId,
                towxname = towxname,
                mpnews = new
                {
                    media_id = media_id
                },
                msgtype = "mpnews"
            }.ToJson();
            break;

        case MassMessageType.text://文本消息
            postData = new
            {
                touser = touserOpenId,
                towxname = towxname,
                text = new
                {
                    content = media_id
                },
                msgtype = "text"
            }.ToJson();
            break;

        case MassMessageType.voice://语音
            postData = new
            {
                touser = touserOpenId,
                towxname = towxname,
                voice = new
                {
                    media_id = media_id
                },
                msgtype = "voice"
            }.ToJson();
            break;

        case MassMessageType.image://图片
            postData = new
            {
                touser = touserOpenId,
                towxname = towxname,
                image = new
                {
                    media_id = media_id
                },
                msgtype = "image"
            }.ToJson();
            break;

        case MassMessageType.video://视频
            postData = new
            {
                touser = touserOpenId,
                towxname = towxname,
                mpvideo = new
                {
                    media_id = media_id
                },
                msgtype = "mpvideo"
            }.ToJson();
            break;

        case MassMessageType.wxcard: //卡劵
            throw new WeixinException("发送卡券息请使用PreviewCardMessage方法。");
            break;
    }

    var url = string.Format("https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token={0}", accessToken);
    return JsonHelper<MassMessageResult>.ConvertJson(url, postData);
}

消息的预览在我们正式群发消息前的审核是比较有用的,我们可以通过接口进行一个消息的预览,可以在微信公众号上看到的效果与正式群发后的消息是一样的。

例如我们通过下面的代码进行一个简单的预览消息操作。

/// <summary>
/// 群发消息的预览
/// </summary>
private void btnPreviewMass_Click(object sender, EventArgs e)
{
    //上传图片
    btnUpload_Click(null, null);

    //上传图文消息
    btnUploadNews_Click(null, null);

    //消息群发前的预览操作
    List<string> list = new List<string>() { openId };
    IMassSendApi api = new MassSendApi();
    var mediaId = this.news_mediaId;
    MassMessageResult result = api.PreviewMessage(token, MassMessageType.mpnews, mediaId, openId);
    if (result != null)
    {
        Console.WriteLine(result.msg_id);
    }
}

最后可以看到例子代码的预览效果如下所示。


如果对这个《C#开发微信门户及应用》系列感兴趣,可以关注我的其他文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容