liu
11 小时以前 5d34bdd3c426bb2ca5f6627424ae67984f745603
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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) {
    }
 
}