管理後台需支援三類群發:文字、圖片、圖文消息。圖文消息包含標題、內容、封面圖、跳轉鏈接,封面圖必須先上傳至微信獲取 media_id,再組裝圖文素材上傳,最後按 OpenID 列表推送。
MapPath 轉物理路徑。POST /cgi-bin/media/upload?type=image。POST /cgi-bin/media/uploadnews,JSON 含多篇文章。/cgi-bin/user/get。POST /cgi-bin/message/mass/send,msgtype=mpnews。public static string HttpUploadFile(string url, string filePath)
{
string boundary = DateTime.Now.Ticks.ToString("x");
var req = (HttpWebRequest)WebRequest.Create(url);
req.Method = "POST";
req.ContentType = $"multipart/form-data;charset=utf-8;boundary={boundary}";
req.CookieContainer = new CookieContainer();
req.AllowAutoRedirect = true;
byte[] boundaryBytes = Encoding.UTF8.GetBytes($"\r\n--{boundary}\r\n");
byte[] endBoundaryBytes = Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n");
string fileName = Path.GetFileName(filePath);
string header = $"Content-Disposition:form-data;name=\"file\";filename=\"{fileName}\"\r\nContent-Type:application/octet-stream\r\n\r\n";
byte[] headerBytes = Encoding.UTF8.GetBytes(header);
byte[] fileBytes = File.ReadAllBytes(filePath);
using (var postStream = req.GetRequestStream())
{
postStream.Write(boundaryBytes, 0, boundaryBytes.Length);
postStream.Write(headerBytes, 0, headerBytes.Length);
postStream.Write(fileBytes, 0, fileBytes.Length);
postStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
}
using (var resp = (HttpWebResponse)req.GetResponse())
using (var stream = resp.GetResponseStream())
using (var reader = new StreamReader(stream, Encoding.UTF8))
return reader.ReadToEnd(); // {"media_id":"...","created_at":...}
}
public static class WXApi
{
// 上傳圖片/縮圖
public static string UploadMedia(string accessToken, string type, string filePath)
{
string url = $"https://file.api.weixin.qq.com/cgi-bin/media/upload?access_token={accessToken}&type={type}";
return HttpRequestUtil.HttpUploadFile(url, filePath);
}
// 上傳圖文素材
public static string UploadNews(string accessToken, string articlesJson)
{
string url = $"https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token={accessToken}";
return HttpRequestUtil.PostUrl(url, articlesJson);
}
// 獲取所有關注者 OpenID(自動分頁)
public static List GetOpenIDs(string accessToken)
{
var result = new List();
string nextOpenId = null;
do
{
string url = $"https://api.weixin.qq.com/cgi-bin/user/get?access_token={accessToken}&next_openid={nextOpenId ?? ""}";
string json = HttpRequestUtil.RequestUrl(url);
int count = int.Parse(Tools.GetJsonValue(json, "count"));
if (count == 0) break;
string openIdBlock = Tools.GetJsonValue(json, "openid"); // 簡化:實際需解析 data.openid 陣列
// 這裡需完整解析 JSON 陣列,建議改用 Json.NET
// 範例略過,實際專案請用 JObject.Parse(json)["data"]["openid"].ToObject>()
nextOpenId = count == 10000 ? /* 最後一個 openid */ null : null;
result.AddRange(/* 解析出的 openid 列表 */);
} while (nextOpenId != null);
return result;
}
// 發送群發
public static string Send(string accessToken, string postData)
{
string url = $"https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token={accessToken}";
return HttpRequestUtil.PostUrl(url, postData);
}
}
public static string GetArticlesJsonStr(PageBase page, string accessToken, DataTable dt)
{
var sb = new StringBuilder("{\"articles\":[");
for (int i = 0; i < dt.Rows.Count; i++)
{
var dr = dt.Rows[i];
string physPath = page.MapPath(dr["ImgUrl"].ToString());
if (!File.Exists(physPath)) return "{\"code\":0,\"msg\":\"圖片不存在\"}";
string mediaJson = WXApi.UploadMedia(accessToken, "image", physPath);
string mediaId = Tools.GetJsonValue(mediaJson, "media_id");
sb.Append("{");
sb.Append($"\"thumb_media_id\":\"{mediaId}\",");
sb.Append($"\"author\":\"{dr["Author"]}\",");
sb.Append($"\"title\":\"{dr["Title"]}\",");
sb.Append($"\"content_source_url\":\"{dr["TextUrl"]}\",");
sb.Append($"\"content\":\"{dr["Content"]}\",");
sb.Append($"\"digest\":\"{dr["Content"]}\",");
sb.Append($"\"show_cover_pic\":\"1\"");
sb.Append(i == dt.Rows.Count - 1 ? "}" : "},");
}
sb.Append("]}");
return sb.ToString();
}
public static class WXMsgUtil
{
public static string CreateNewsJson(string mediaId, List openIds)
{
var sb = new StringBuilder();
sb.Append("{\"touser\":[");
sb.Append(string.Join(",", openIds.Select(id => $"\"{id}\"")));
sb.Append("],\"msgtype\":\"mpnews\",\"mpnews\":{\"media_id\":\"");
sb.Append(mediaId);
sb.Append("\"}}");
return sb.ToString();
}
}
// 頁面按鈕事件
public string Send()
{
string type = Request["type"]; // 1=文字 2=圖片 3=圖文
string data = Request["data"];
string token = AdminUtil.GetAccessToken(this);
var openIds = WXApi.GetOpenIDs(token);
string resultMsg = null;
if (type == "3") // 圖文
{
var dt = ImgItemDal.GetImgItemTable(loginUser.OrgID, data);
string articlesJson = ImgItemDal.GetArticlesJsonStr(this, token, dt);
string newsJson = WXApi.UploadNews(token, articlesJson);
string newsId = Tools.GetJsonValue(newsJson, "media_id");
resultMsg = WXApi.Send(token, WXMsgUtil.CreateNewsJson(newsId, openIds));
}
// ... 文字、圖片類似邏輯略
// 統一結果處理
string errcode = Tools.GetJsonValue(resultMsg, "errcode");
return errcode == "0"
? "{\"code\":1,\"msg\":\"\"}"
: $"{{\"code\":0,\"msg\":\"errcode:{errcode}, errmsg:{Tools.GetJsonValue(resultMsg, "errmsg")}\"}}";
}
media_id 3 天有效,群發需在有效期內完成。next_openid 迴圈拉取全量。本文梳理了微信高級群發的完整鏈路:本地素材 → 微信媒體庫 → 圖文素材 → 精準推送,並給出可直接落地的程式碼骨架。實際專案中請務必引入 Json.NET 替換手工字串拼接,並加入重試、日誌、配額監控,構建生產級穩定服務。