package cn.iocoder.yudao.module.wxcp.service.push; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.wxcp.controller.admin.vo.WxCpPushResultVO; import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpPushScopeEnum; import cn.iocoder.yudao.module.wxcp.framework.config.WxCpProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.cp.bean.article.NewArticle; import me.chanjar.weixin.cp.bean.message.WxCpMessage; import me.chanjar.weixin.cp.bean.message.WxCpMessageSendResult; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.wxcp.enums.ErrorCodeConstants.ACTIVITY_PUSH_TARGET_EMPTY; /** * 企业微信消息推送 Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class WxCpMessagePushServiceImpl implements WxCpMessagePushService { @Resource private WxCpService wxCpService; @Resource private WxCpProperties wxCpProperties; @Override public WxCpPushResultVO pushCard(String title, String description, String url, String btnTxt, Integer scope, String target) { // textcard 类型的 url 必填,留空企业微信会返回 41010 if (StrUtil.isBlank(url)) { throw new IllegalArgumentException("推送卡片消息时 url 不能为空"); } WxCpMessage message = WxCpMessage.TEXTCARD() .agentId(agentId()) .title(title) .description(description) .url(url) .btnTxt(StrUtil.blankToDefault(btnTxt, "查看详情")) .build(); return doSend(message, scope, target, "card"); } @Override public WxCpPushResultVO pushText(String content, Integer scope, String target) { WxCpMessage message = WxCpMessage.TEXT() .agentId(agentId()) .content(content) .build(); return doSend(message, scope, target, "text"); } @Override public WxCpPushResultVO pushNews(String title, String description, String url, String picUrl, Integer scope, String target) { if (StrUtil.isBlank(url)) { throw new IllegalArgumentException("推送图文消息时 url 不能为空"); } NewArticle article = NewArticle.builder() .title(title) .description(StrUtil.blankToDefault(description, "")) .url(url) .picUrl(picUrl) .build(); WxCpMessage message = WxCpMessage.NEWS() .agentId(agentId()) .addArticle(article) .build(); return doSend(message, scope, target, "news"); } @Override public WxCpPushResultVO pushFile(java.io.File file, String fileName, String textContent, Integer scope, String target) { if (file == null || !file.exists()) { return new WxCpPushResultVO(false, null, "文件不存在,无法推送", null, null); } try { // 1. 上传文件到企业微信,换取 mediaId WxMediaUploadResult upload = wxCpService.getMediaService().upload("file", file); if (upload == null || StrUtil.isBlank(upload.getMediaId())) { return new WxCpPushResultVO(false, null, "文件上传企业微信失败", null, null); } // 2. 先发送说明文本,再发送文件;文本失败不阻塞文件发送 if (StrUtil.isNotBlank(textContent)) { pushText(textContent, scope, target); } // 3. 发送文件消息 WxCpMessage message = WxCpMessage.FILE() .agentId(agentId()) .mediaId(upload.getMediaId()) .build(); WxCpPushResultVO result = doSend(message, scope, target, "file"); log.info("[pushFile][文件推送完成 fileName={} success={}]", fileName, result.getSuccess()); return result; } catch (Exception e) { log.error("[pushFile][文件上传或推送异常 fileName={}]", fileName, e); return new WxCpPushResultVO(false, null, "文件推送异常:" + e.getMessage(), null, null); } } @Override public WxCpPushResultVO pushImage(java.io.File file, Integer scope, String target) { if (file == null || !file.exists()) { return new WxCpPushResultVO(false, null, "图片文件不存在,无法推送", null, null); } try { // 企业微信图片消息要求先用 image 类型上传换取 mediaId WxMediaUploadResult upload = wxCpService.getMediaService().upload("image", file); if (upload == null || StrUtil.isBlank(upload.getMediaId())) { return new WxCpPushResultVO(false, null, "图片上传企业微信失败", null, null); } WxCpMessage message = WxCpMessage.IMAGE() .agentId(agentId()) .mediaId(upload.getMediaId()) .build(); return doSend(message, scope, target, "image"); } catch (Exception e) { log.error("[pushImage][图片上传或推送异常 file={}]", file.getName(), e); return new WxCpPushResultVO(false, null, "图片推送异常:" + e.getMessage(), null, null); } } /** * 统一处理接收者解析、发送与错误包装 */ private WxCpPushResultVO doSend(WxCpMessage message, Integer scope, String target, String type) { Recipients recipients = resolveRecipients(scope, target); message.setToUser(recipients.toUser); message.setToParty(recipients.toParty); WxCpMessageSendResult result; try { result = wxCpService.getMessageService().send(message); } catch (Exception e) { // 网络/配置异常不向上抛,转成可读结果,避免前端拿到 500 不知道原因 log.error("[doSend][企业微信消息推送异常 type={} toUser={} toParty={}]", type, recipients.toUser, recipients.toParty, e); return new WxCpPushResultVO(false, null, "推送异常:" + e.getMessage(), recipients.toUser, recipients.toParty); } if (result.getErrCode() != null && result.getErrCode() == 0) { log.info("[doSend][企业微信消息推送成功 type={} toUser={} toParty={}]", type, recipients.toUser, recipients.toParty); return new WxCpPushResultVO(true, 0, "推送成功", recipients.toUser, recipients.toParty); } log.warn("[doSend][企业微信消息推送失败 type={} errCode={} errMsg={}]", type, result.getErrCode(), result.getErrMsg()); return new WxCpPushResultVO(false, result.getErrCode(), "企业微信返回:" + result.getErrMsg(), recipients.toUser, recipients.toParty); } /** * 解析推送范围与目标,转成企业微信要求的 | 分隔格式 */ private Recipients resolveRecipients(Integer scope, String target) { int actualScope = scope != null ? scope : WxCpPushScopeEnum.ALL.getScope(); if (Objects.equals(actualScope, WxCpPushScopeEnum.ALL.getScope())) { return new Recipients("@all", null); } if (StrUtil.isBlank(target)) { throw exception(ACTIVITY_PUSH_TARGET_EMPTY); } // 同时容忍中英文逗号与竖线,避免配置时手误 String normalized = target.replace(",", ",").replace(",", "|").replace(" ", ""); if (Objects.equals(actualScope, WxCpPushScopeEnum.PARTY.getScope())) { return new Recipients(null, normalized); } return new Recipients(normalized, null); } private Integer agentId() { return StrUtil.isBlank(wxCpProperties.getAgentId()) ? null : Integer.valueOf(wxCpProperties.getAgentId()); } private record Recipients(String toUser, String toParty) { } }