分享我们团队最近开发的微信公众号运营助手,可以在手机上回复粉丝留言

由于公司旗下有好几个微信公众号,经常来回切换登录很麻烦,粉丝留言咨询的时候常常不能及时回复,导致订单流失。于是我们团队开发了一个公众号小助手,可以把多个公众号绑定进来,只要有粉丝留言,马上管理员就收到通知了,然后还可以在手机上进行回复。

实现的功能如下:

  1. 粉丝留言自动微信通知
  2. 在微信中回复粉丝留言,文字+图片
  3. 粉丝关注自动微信通知
  4. 粉丝关注自动推送多图文消息或者历史消息
  5. 更强大的自定义菜单管理
  6. 自定义客服消息模板
  7. 支持绑定多个管理员
  8. 支持关键词自动回复
  9. 支持二次开发

虽然这个小助手很小,但是里面用到的技术我觉得还是有一定分享价值。本文就向大家分享一下这个小助手中核心的技术方案,包括:公众号绑定、粉丝信息获取、给粉丝发送消息、微信图片上传与下载、公众号自定义菜单接口、公众号临时二维码的妙用等等。

免费在线体验:

核心技术1:多个公众号的绑定

想要调用微信公众号的API,首先要通过AppId和AppSecret获取AccessToken,而AccessToken过一段时间就会过期。为了提高AccessToken的利用率并且实现自动刷新,我们专门写了一个AccessTokenContext来管理多个公众号的AccessToken,这个类也是完成多个公众号绑定最重要的一步。

请看源码:

 1 public class AccessTokenContext
 2 {
 3     public static AccessTokenContext Instance { get; }
 4 
 5     static AccessTokenContext()
 6     {
 7         Instance = new AccessTokenContext();
 8     }
 9 
10     private readonly Dictionary<Guid, AccountAccessTokenDto> _keyValues;
11 
12     public AccessTokenContext()
13     {
14         _keyValues = new Dictionary<Guid, AccountAccessTokenDto>();
15     }
16 
17     public string GetDabenAccessToken()
18     {
19         return GetAccessToken(AppContext.DabenMpAccountId);
20     }
21 
22     public string GetAccessToken(Guid accountId)
23     {
24         if (_keyValues.ContainsKey(accountId))
25         {
26             var dto = _keyValues[accountId];
27             if (dto.IsExpired() == false)
28             {
29                 return dto.AccessToken;
30             }
31         }
32         var account = Ioc.Get<IAccountService>().Get(accountId);
33         var apidto = GetByApi(account);
34         _keyValues[account.Id] = apidto;
35         return apidto.AccessToken;
36     }
37 
38     public string GetAccessToken(MpAccount account)
39     {
40         if (_keyValues.ContainsKey(account.Id))
41         {
42             var dto = _keyValues[account.Id];
43             if (dto.IsExpired())
44             {
45                 dto = GetByApi(account);
46             }
47             return dto.AccessToken;
48         }
49         else
50         {
51             var dto = GetByApi(account);
52             _keyValues[account.Id] = dto;
53             return dto.AccessToken;
54         }
55     }
56 
57     private AccountAccessTokenDto GetByApi(MpAccount account)
58     {
59         var token = WeixinApi.GetAccessToken(account.AppId, account.AppSecret);
60         if (token == null || token.IsSuccess() == false)
61         {
62             throw new KnownException("Mp.GetAccessToken:" + account.Name);
63         }
64         return new AccountAccessTokenDto(account.Id, token);
65     }
66 }

一旦拿到了某个公众号的AccessToken,就可以调用绝大部分接口了。

核心技术2:拉取粉丝基本信息

 不同的微信公众号下面的粉丝拥有不同的OpenId,而OpenId是微信对于用于的唯一标识。

微信提供了几个事件发生的时候,程序可以获取用户的OpenId,而用OpenId就可以跟用户互动。我们仅用了2个事件获取OpenId:粉丝关注时和粉丝留言时。

下面的代码展示了如何通过OpenId和AccessToken获取粉丝基本信息。

public class WeixinApi
{
    public static UserDto GetUserInfo(string openId, string accessToken = null)
    {
        return HttpHelper.GetApiDto<UserDto>(WeixinConfigs.Urls.GetUserInfo(openId, accessToken));
    }
}
internal class HttpHelper
{
    public static T GetApiDto<T>(string url) where T : ApiDtoBase
    {
        var html = DownloadString(url);
        try
        {
            var dto = Serializer.FromJson<T>(html);
            if (dto.IsSuccess() == false)
            {
                Logger.Error("GetApiDto." + typeof(T).FullName + ".NotSuccess", dto.GetFullError());
            }
            return dto;
        }
        catch (Exception ex)
        {
            Logger.Error("GetApiDto." + typeof(T).FullName + ".Exception", ex);
            Logger.Error("GetApiDto." + typeof(T).FullName + ".Exception", html);
        }
        return null;
    }
}

核心技术3:给粉丝发送消息

注意这里仅仅是发送的客服消息,也就是粉丝与公众号互动之后的48小时内可以随意给粉丝发送的消息,包括文字和图片。

public class WeixinApi
{
    public static ApiDtoBase TrySendMessage(MessageBase message, string accessToken = null)
    {
        try
        {
            return HttpHelper.PostApiDto<ApiDtoBase>(WeixinConfigs.Urls.SendMessage(message is TemplateMessageBase, accessToken), message.ToJson());
        }
        catch (Exception ex)
        {
            Logger.Error("WeixinApi TrySendMessage", ex);
        }
        return null;
    }
}
internal class HttpHelper
{
    public static T PostApiDto<T>(string url, string json) where T : ApiDtoBase
    {
        string html;
        using (var client = new WebClient())
        {
            var result = client.UploadData(url, "POST", Encoding.UTF8.GetBytes(json ?? string.Empty));
            html = Encoding.UTF8.GetString(result);
        }
        try
        {
            var dto = Serializer.FromJson<T>(html);
            if (dto.IsSuccess() == false)
            {
                Logger.Error("PostApiDto." + typeof(T).FullName + ".NotSuccess", dto.GetFullError());
            }
            return dto;
        }
        catch (Exception ex)
        {
            Logger.Error("PostApiDto." + typeof(T).FullName + ".Exception", ex);
            return null;
        }
    }
}

如果推送的是文本消息:

 1 public class TextMessage : MessageBase
 2 {
 3     public string Text { get; set; }
 4         
 5     public TextMessage(string openId, string text)
 6     {
 7         this.ToUserOpenId = openId;
 8         this.Text = text;
 9     }
10 
11     public override string ToJson()
12     {
13         return Serializer.ToJson(
14             new
15             {
16                 touser = this.ToUserOpenId,
17                 msgtype = "text",
18                 text = new
19                 {
20                     content = this.Text
21                 }
22             });
23     }
24 }

如果推送的是图片消息,则需要先上传图片到微信服务器拿到media_Id(本文后面会展示):

 1 public class ImageMessage : MessageBase
 2 {
 3     public string MediaId { get; set; }
 4         
 5     public ImageMessage(string openId, string mediaId)
 6     {
 7         this.ToUserOpenId = openId;
 8         this.MediaId = mediaId;
 9     }
10 
11     public override string ToJson()
12     {
13         return Serializer.ToJson(
14             new
15             {
16                 touser = this.ToUserOpenId,
17                 msgtype = "image",
18                 image = new
19                 {
20                     media_id = this.MediaId
21                 }
22             });
23     }
24 }

核心技术4:微信图片上传与下载

微信图片的上传与下载都是通过media_id进行的。上传一个图片文件之后,微信服务器返回media_id;如果要下载某张图片,也需要提供media_id。

关于图片上传这块,我们封装了一个非常方便的微信图片上传控件,等以后有时间再给大家详解这个控件,绝对超cool的,现在你可以先体验下。

图片上传之前,需要先将用户上传的图片保存到服务器,然后再将服务器的图片上传到微信服务器:

public class WeixinApi
{
    public static UploadFileDto UploadFile(string localFilePath, ResourceType type, string accessToken = null)
    {
        return HttpHelper.PostFile<UploadFileDto>(WeixinConfigs.Urls.UploadFile(type, accessToken), localFilePath);
    }
}
 1 internal class HttpHelper
 2 {
 3     public static T PostFile<T>(string url, string filePath) where T : ApiDtoBase
 4     {
 5         string html;
 6         using (var client = new WebClient())
 7         {
 8             var result = client.UploadFile(url, "POST", filePath);
 9             html = Encoding.UTF8.GetString(result);
10         }
11         try
12         {
13             var dto = Serializer.FromJson<T>(html);
14             if (dto.IsSuccess() == false)
15             {
16                 Logger.Error("PostFile." + typeof(T).FullName + ".NotSuccess", dto.GetFullError());
17             }
18             return dto;
19         }
20         catch (Exception ex)
21         {
22             Logger.Error("PostFile." + typeof(T).FullName + ".Exception", ex);
23             return null;
24         }
25     }
26 }

下载图片就非常简单了,只需要通过media_id获取下载图片的URL即可:

public static string GetMediaDownloadUrl(string mediaId, string accessToken = null)
{
    return "http://file.api.weixin.qq.com/cgi-bin/media/get"
            + $"?access_token={accessToken ?? WeixinKeyManager.Instance.GetAccessToken()}&media_id={mediaId}";
}

核心技术5:自定义菜单的实现

由于微信定义的接口可以接受JSON格式的自定义菜单项,所以我们就用JS在浏览器中编辑菜单,然后最终提交的时候,将整个菜单序列化成JSON,一起提交到微信服务器。

先看下我们的自定义菜单编辑器吧,纯JS打造的,有机会也给大家分享下:

调用这个JS的菜单编辑器非常简单,只需要传入一个容器和accesstoken即可:

(function() {
    $(document).ready(function () {
        var manager = new WeixinMenuAppManager($("#hfAccessToken").val(), $(".content"));
        manager.init();

        $("#hfAccessToken").remove();
    });
})();

而传到我们服务器之后,调用微信接口的代码就非常简单了:

 1 public class WeixinApi
 2 {
 3     public static string GetMenuJson(string accessToken = null)
 4     {
 5         return HttpHelper.DownloadString(WeixinConfigs.Urls.GetMenu(accessToken));
 6     }
 7         
 8     public static ApiDtoBase SaveMenu(string json, string accessToken = null)
 9     {
10         return HttpHelper.PostApiDto<ApiDtoBase>(WeixinConfigs.Urls.SaveMenu(accessToken), json);
11     }
12 
13     public static ApiDtoBase DeleteMenu(string accessToken = null)
14     {
15         return HttpHelper.PostApiDto<ApiDtoBase>(WeixinConfigs.Urls.DeleteMenu(accessToken), null);
16     }
17 }

核心技术6:带参数的临时二维码

首先说一下这个临时二维码是由微信服务器生成的。用户扫码之后,首先是关注公众号,同时我们服务器还接收到了这个二维码额外的一个参数(系统唯一标识),我们利用这个参数就可以很方便的完成多管理员扫码自动绑定的功能了。

业务流程:公众号所有者点击【添加管理员】,我们系统就弹出一个有效期5分钟的临时二维码,另一个管理员扫码关注公众号之后,自动将他绑定到该公众号,再给用户推送一条客服消息,告诉他绑定成功。

这个体验是相当的帅啊!

生成临时二维码的代码:

public static string GetTempQrCodeUrl(int autoId, int expireMinutes, string accessToken = null)
{
    var data = Serializer.ToJson(new
    {
        expire_seconds = expireMinutes*60,
        action_name = "QR_SCENE",
        action_info = new
        {
            scene = new
            {
                scene_id = autoId
            }
        }
    });
    var result = HttpHelper.PostApiDto<GetQrCodeDto>(WeixinConfigs.Urls.GenerateQrCode(accessToken), data);
    if (result.IsSuccess() == false)
    {
        throw new KnownException(result.GetFullError());
    }
    return WeixinConfigs.Urls.ShowQrCode(result.Ticket);
}

用户扫码关注后,服务器完成自动绑定的代码就不贴了,太多了,并且夹杂着我们系统其他的业务逻辑,不容易理解。

结语

这个公众号小助手虽然很小,但几乎完全包含了我们团队在微信公众号开发方面积累的经验,以及我们自己封装的很多控件、类库。如果大家感兴趣的话,后面我再专门把这些积累开源。

原文地址:https://www.cnblogs.com/leotsai/p/weixin-mp-small-tool.html