基础消息能力
环境:.NetCore、Senparc.Weixin.MP
被动回复用户消息
需求:通过后台配置,不同业务平台的公众号被关注时回复不同的内容;配置关键字对应的回复内容;
1 /// <summary> 2 /// 公众号消息回复 3 /// </summary> 4 /// <returns></returns> 5 public async Task<string> WeChatSendMsgAsync(string wechatBody) 6 { 7 WechatEventDto wechatEvent = XmlToJsonConverter.Convert<WechatEventDto>(wechatBody); 8 if (wechatEvent is null) 9 { 10 Logger.LogWarning("xml解析失败"); 11 return ""; 12 } 13 Logger.LogWarning($"xml解析内容{JsonConvert.SerializeObject(wechatEvent)}"); 14 15 var (success, msg, data) = await _weChatService.GetUnionInfoAsync(wechatEvent.FromUserName); 16 Logger.LogWarning($"公众号消息回复获取UnionInfo结果:{JsonConvert.SerializeObject(data)}"); 17 18 string xmlStr = ""; 19 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); 20 var createTime = Convert.ToInt64(ts.TotalSeconds).ToString(); 21 string fromUserName = wechatEvent.FromUserName; 22 string toUserName = wechatEvent.ToUserName; 23 //事件推送 24 if (wechatEvent.MsgType == "event") 25 { 26 if (wechatEvent.Event == EventType.subscribe.ToString()) 27 { 28 if (string.IsNullOrEmpty(wechatEvent.EventKey)) //普通关注 29 { 30 var commonConfig = await _autoResponseConfigRepository.FindConfigByPlatformIdAsync(null); 31 if (commonConfig != null) 32 { 33 xmlStr = ReturnMsgXmlStr(commonConfig, fromUserName, toUserName); 34 } 35 } 36 else //扫描关注 37 { 38 var appId = wechatEvent.EventKey.Substring(0, wechatEvent.EventKey.LastIndexOf("&")).Replace(WeChatConstConfig.EventKeyPrefix, ""); 39 var appInfo = await _systemAPPRepository.FirstOrDefaultAsync(x => x.AppId == appId); 40 if (appInfo is null) 41 { 42 Logger.LogWarning("未找到平台信息"); 43 return ""; 44 } 45 var autoResponseConfig = await _autoResponseConfigRepository.FindConfigByPlatformIdAsync(appInfo.Id); 46 if (autoResponseConfig != null) 47 { 48 xmlStr = ReturnMsgXmlStr(autoResponseConfig, fromUserName, toUserName); 49 } 50 } 51 } 52 53 //模板消息事件推送 54 if (wechatEvent.Event == EventType.TEMPLATESENDJOBFINISH.ToString()) 55 { 56 //保存推送记录 57 TemplateMessagePushRecord pushRecord = new TemplateMessagePushRecord() 58 { 59 OpenId = data.openid, 60 UnionId = data.unionid, 61 MsgId = wechatEvent.MsgID, 62 WeChatReturnStr = wechatEvent.Status 63 }; 64 await _pushRecordRepository.InsertAsync(pushRecord); 65 } 66 } 67 //文本消息推送 68 if (wechatEvent.MsgType == EnumMessageType.text.ToString()) 69 { 70 AppAutoResponseConfig config = new AppAutoResponseConfig() 71 { 72 MessageType = EnumMessageType.text 73 }; 74 var keywordConfig = await _keywordConfigRepository.FindAppKeywordConfigAsync(wechatEvent.Content); 75 config.Content = keywordConfig?.ReplyContent; 76 xmlStr = ReturnMsgXmlStr(config, fromUserName, toUserName); 77 } 78 79 return xmlStr; 80 } 81 82 /// <summary> 83 /// 返回格式化后的回复消息 84 /// </summary> 85 /// <returns></returns> 86 private string ReturnMsgXmlStr(AppAutoResponseConfig autoResponseConfig, string toUserName, string fromUserName) 87 { 88 string xmlStr = ""; 89 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); 90 var createTime = Convert.ToInt64(ts.TotalSeconds).ToString(); 91 92 switch (autoResponseConfig.MessageType) 93 { 94 case EnumMessageType.text: 95 TextMessageResponseDto textDto = new TextMessageResponseDto 96 { 97 FromUserName = fromUserName, 98 ToUserName = toUserName, 99 CreateTime = createTime, 100 Content = autoResponseConfig.Content 101 }; 102 xmlStr = MessageResponse.TextMsgResponse(textDto); 103 break; 104 case EnumMessageType.news: 105 PicTextMessageResponseDto newsDto = new PicTextMessageResponseDto 106 { 107 FromUserName = fromUserName, 108 ToUserName = toUserName, 109 CreateTime = createTime, 110 Title = autoResponseConfig.Title, 111 Description = autoResponseConfig.Description, 112 PicUrl = autoResponseConfig.PicUrl, 113 Url = autoResponseConfig.Url 114 }; 115 xmlStr = MessageResponse.PicTextMsgResponse(newsDto); 116 break; 117 } 118 Logger.LogWarning($"xmlStr:{xmlStr}"); 119 return xmlStr; 120 }
1 public class XmlToJsonConverter 2 { 3 public static T Convert<T>(string xmlString) where T : new() 4 { 5 try 6 { 7 XDocument doc = XDocument.Parse(xmlString); 8 return ProcessElement<T>(doc.Root); 9 } 10 catch (Exception) 11 { 12 return default(T); 13 14 } 15 16 } 17 private static T ProcessElement<T>(XElement element) where T : new() 18 { 19 T result = new T(); 20 21 foreach (XElement childElement in element.Elements()) 22 { 23 if (childElement.HasElements) 24 { 25 var property = typeof(T).GetProperty(childElement.Name.LocalName); 26 if (property != null) 27 { 28 property.SetValue(result, ProcessElement(property.PropertyType, childElement)); 29 } 30 } 31 else 32 { 33 var property = typeof(T).GetProperty(childElement.Name.LocalName); 34 if (property != null) 35 { 36 property.SetValue(result, childElement.Value); 37 } 38 } 39 } 40 return result; 41 } 42 private static object ProcessElement(Type type, XElement element) 43 { 44 var method = typeof(XmlToJsonConverter).GetMethod("ProcessElement", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); 45 var genericMethod = method.MakeGenericMethod(type); 46 return genericMethod.Invoke(null, new object[] { element }); 47 } 48 }
1 public class MessageResponse 2 { 3 /// <summary> 4 /// 回复文本消息 5 /// </summary> 6 /// <param name="data"></param> 7 /// <returns></returns> 8 public static string TextMsgResponse(TextMessageResponseDto data) 9 { 10 StringBuilder str = new StringBuilder(); 11 str.Append("<xml>"); 12 str.Append($" <ToUserName><![CDATA[{data.ToUserName}]]></ToUserName>"); 13 str.Append($" <FromUserName><![CDATA[{data.FromUserName}]]></FromUserName>"); 14 str.Append($" <CreateTime>{data.CreateTime}</CreateTime>"); 15 str.Append(" <MsgType><![CDATA[text]]></MsgType>"); 16 str.Append($" <Content>{data.Content}</Content>"); 17 str.Append("</xml>"); 18 return str.ToString(); 19 } 20 21 /// <summary> 22 /// 回复图文消息 23 /// </summary> 24 /// <param name="data"></param> 25 /// <returns></returns> 26 public static string PicTextMsgResponse(PicTextMessageResponseDto data) 27 { 28 StringBuilder str = new StringBuilder(); 29 str.Append("<xml>"); 30 str.Append(" <ToUserName><![CDATA[" + data.ToUserName + "]]></ToUserName>"); 31 str.Append(" <FromUserName><![CDATA[" + data.FromUserName + "]]></FromUserName>"); 32 str.Append(" <CreateTime>" + data.CreateTime + "</CreateTime>"); 33 str.Append(" <MsgType><![CDATA[news]]></MsgType>"); 34 str.Append(" <ArticleCount>1</ArticleCount>"); 35 str.Append(" <Articles>"); 36 str.Append(" <item>"); 37 str.Append(" <Title><![CDATA[" + data.Title + "]]></Title>"); 38 str.Append(" <Description><![CDATA[" + data.Description + "]]></Description>"); 39 str.Append(" <PicUrl><![CDATA[" + data.PicUrl + "]]></PicUrl>"); 40 str.Append(" <Url><![CDATA[" + data.Url + "]]></Url>"); 41 str.Append(" </item>"); 42 str.Append(" </Articles>"); 43 str.Append("</xml>"); 44 return str.ToString(); 45 } 46 }
模板消息
需求:根据Excel模板导入学员信息发送消息提醒
1 [HttpPost] 2 [Route("import-msg")] 3 public async Task<(bool success, string msg)> SendTempMsgAsync(IFormFile file) 4 { 5 if (file is null) 6 { 7 return (false, "未获取到文件信息"); 8 } 9 10 var fileExt = Path.GetExtension(file.FileName) ?? ""; 11 if (fileExt != ".xlsx" && fileExt != ".xls") 12 { 13 return (false, "文件类型错误,请使用xlsx或xls后缀的文件"); 14 } 15 16 // 从Excel中获取数据 17 var excelData = ExcelExportHelper.ExcelToDataTable(file.OpenReadStream(), file.FileName, 3); 18 if (excelData == null) 19 { 20 return (false, "数据不可为空"); 21 } 22 23 var list = new List<ImportStudentsInput>(); 24 foreach (DataRow row in excelData.Rows) 25 { 26 var model = new ImportStudentsInput() 27 { 28 WorkTypeName = row["岗位名称"].ToString(), 29 OpenId = row["OpenId"].ToString(), 30 UnionId = row["UnionId"].ToString() 31 }; 32 list.Add(model); 33 } 34 var result = await _wcService.SendTempMsgAsync(list); 35 return (result, "操作完成"); 36 } 37 38 /// <summary> 39 /// 根据导入学员发送模板消息 40 /// </summary> 41 /// <param name="input"></param> 42 /// <returns></returns> 43 public async Task<bool> SendTempMsgAsync(List<ImportStudentsInput> input) 44 { 45 if (input != null) 46 { 47 //统一获取token 48 var (success, msg, accessToken) = await _weChatService.BuildWeChatTokenAsync(); 49 50 List<TemplateMessagePushRecord> recordList = new List<TemplateMessagePushRecord>(); 51 var templateId = _configuration.GetSection("TemplateMsgPush:LearningProgressTempId").Value; 52 var first = "您有一条消息待确认!"; 53 foreach (var item in input) 54 { 55 if (string.IsNullOrEmpty(item.OpenId)) 56 { 57 continue; 58 } 59 TemplateMessagePushRecord record = new TemplateMessagePushRecord(); //推送记录 60 record.OpenId = item.OpenId; 61 record.UnionId = item.UnionId; 62 record.MsgId = templateId; 63 record.PushStatus = 0; 64 try 65 { 66 //推送内容 67 var pushTemplate = new 68 { 69 first = new TemplateDataItem(first), 70 keyword1 = new TemplateDataItem(item.WorkTypeName), 71 keyword2 = new TemplateDataItem("2024年3月31日"), 72 remark = new TemplateDataItem("请在规定时间内完成学习,如已完成请忽略") 73 }; 74 var sendResult = TemplateApi.SendTemplateMessage(accessToken, item.OpenId, templateId, "", pushTemplate); 75 record.WeChatReturnStr = sendResult.ToString(); 76 record.PushStatus = sendResult.ErrorCodeValue; 77 } 78 catch (Exception ex) 79 { 80 record.WeChatReturnStr = ex.ToString(); 81 return false; 82 } 83 recordList.Add(record); 84 } 85 await _pushRecordRepository.InsertManyAsync(recordList); 86 } 87 return true; 88 }
1 public class ExcelExportHelper 2 { 3 /// <summary> 4 /// 将 Excel 文件流转换成 DataTable 5 /// </summary> 6 /// <param name="fileStream">Excel 文件流</param> 7 /// <param name="fileName">Excel 文件名称</param> 8 /// <param name="cellCount">读取的列数(从 0 开始)</param> 9 /// <returns>转换后的 DataTable</returns> 10 public static DataTable ExcelToDataTable(Stream fileStream, string fileName, int cellCount) 11 { 12 DataTable table = new DataTable(); 13 14 IWorkbook workbook = null; 15 if (fileStream != null) 16 { 17 if (fileName.ToLower().EndsWith(".xlsx")) 18 { 19 workbook = new XSSFWorkbook(fileStream); 20 } 21 else if (fileName.ToLower().EndsWith(".xls")) 22 { 23 workbook = new HSSFWorkbook(fileStream); 24 } 25 26 if (workbook != null) 27 { 28 ISheet sheet = workbook.GetSheetAt(0); 29 30 //添加表头,首行为说明文字,从第二行开始 31 IRow headerRow = sheet.GetRow(1); 32 for (int i = headerRow.FirstCellNum; i < cellCount; i++) 33 { 34 DataColumn column = new DataColumn(headerRow.GetCell(i).StringCellValue.Trim()); 35 table.Columns.Add(column); 36 } 37 38 // 添加数据 39 int rowCount = sheet.LastRowNum; 40 for (int i = (sheet.FirstRowNum + 2); i <= rowCount; i++) 41 { 42 IRow row = sheet.GetRow(i); 43 DataRow dataRow = table.NewRow(); 44 45 for (int j = row.FirstCellNum; j < cellCount; j++) 46 { 47 if (row.GetCell(j) != null) 48 { 49 dataRow[j] = row.GetCell(j).ToString().Trim(); 50 } 51 } 52 53 table.Rows.Add(dataRow); 54 } 55 56 // 移除空行 57 for (var i = table.Rows.Count - 1; i >= 0; i--) 58 { 59 var isEmptyRow = true; 60 for (var j = 0; j < table.Columns.Count; j++) 61 { 62 if (!string.IsNullOrEmpty(table.Rows[i][j].ToString())) 63 { 64 isEmptyRow = false; 65 break; 66 } 67 } 68 if (isEmptyRow) 69 { 70 table.Rows.RemoveAt(i); 71 } 72 } 73 } 74 } 75 76 return table; 77 } 78 79 /// <summary> 80 /// 导出数据到Excel 81 /// </summary> 82 /// <typeparam name="T"></typeparam> 83 /// <param name="dataList"></param> 84 /// <param name="columnMapping"></param> 85 /// <returns></returns> 86 public static byte[] ExportToExcel<T>(IEnumerable<T> dataList, Dictionary<string, string> columnMapping) 87 { 88 // 创建Excel文档对象 89 var workbook = new XSSFWorkbook(); 90 var sheet = workbook.CreateSheet("Sheet1"); 91 92 // 创建表头行并设置列名称 93 var headerRow = sheet.CreateRow(0); 94 int columnIndex = 0; 95 foreach (var column in columnMapping) 96 { 97 headerRow.CreateCell(columnIndex).SetCellValue(column.Value); 98 columnIndex++; 99 } 100 101 // 将数据填充到Excel文档中 102 int rowIndex = 1; 103 foreach (var data in dataList) 104 { 105 var dataRow = sheet.CreateRow(rowIndex); 106 columnIndex = 0; 107 foreach (var column in columnMapping) 108 { 109 var property = typeof(T).GetProperty(column.Key); 110 var value = property.GetValue(data); 111 dataRow.CreateCell(columnIndex).SetCellValue(value?.ToString()); 112 columnIndex++; 113 } 114 rowIndex++; 115 } 116 117 // 导出Excel文档 118 using (var stream = new MemoryStream()) 119 { 120 workbook.Write(stream); 121 return stream.ToArray(); 122 } 123 } 124 }
可能遇到的问题
微信公众号 throw exception when excuting local service: Common.IService.IWxMsgTemplateService.SendTemplate(openid,templateId,url,data)Senparc.Weixin.Exceptions.UnRegisterAppIdException: 尚无已经注册的AppId,请先使用AccessTokenContainer.Register完成注册(全局执行一次即可)!模块:WeChat_OfficialAccount
解决方案
微信公众号IP 白名单上加上部署的服务器IP 地址;在启动类中注册