文章出處

由于公司旗下有好幾個微信公眾號,經常來回切換登錄很麻煩,粉絲留言咨詢的時候常常不能及時回復,導致訂單流失。于是我們團隊開發了一個公眾號小助手,可以把多個公眾號綁定進來,只要有粉絲留言,馬上管理員就收到通知了,然后還可以在手機上進行回復。

 

實現的功能如下:

  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);
}

 

用戶掃碼關注后,服務器完成自動綁定的代碼就不貼了,太多了,并且夾雜著我們系統其他的業務邏輯,不容易理解。

 

結語

這個公眾號小助手雖然很小,但幾乎完全包含了我們團隊在微信公眾號開發方面積累的經驗,以及我們自己封裝的很多控件、類庫。如果大家感興趣的話,后面我再專門把這些積累開源。

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()