liu
10 小时以前 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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
package cn.iocoder.yudao.module.wxcp.service.notice;
 
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticePageReqVO;
import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticeSaveReqVO;
import cn.iocoder.yudao.module.wxcp.controller.admin.vo.WxCpPushReqVO;
import cn.iocoder.yudao.module.wxcp.controller.admin.vo.WxCpPushResultVO;
import cn.iocoder.yudao.module.wxcp.dal.dataobject.notice.WxCpNoticeDO;
import cn.iocoder.yudao.module.wxcp.dal.mysql.notice.WxCpNoticeMapper;
import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpPushScopeEnum;
import cn.iocoder.yudao.module.wxcp.enums.notice.WxCpNoticeStatusEnum;
import cn.iocoder.yudao.module.wxcp.enums.notice.WxCpNoticeTypeEnum;
import cn.iocoder.yudao.module.wxcp.service.push.WxCpMessagePushService;
import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO;
import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
 
import java.io.File;
import java.time.LocalDateTime;
import java.util.Objects;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.wxcp.enums.ErrorCodeConstants.*;
 
/**
 * 企业微信通知 Service 实现类
 *
 * @author 超级管理员
 */
@Service
@Validated
@Slf4j
public class WxCpNoticeServiceImpl implements WxCpNoticeService {
 
    /**
     * 企业微信文本消息上限约 2048 字节,中文按 3 字节算。
     * 这里按字符数留出足够余量,超出部分截断并在末尾提示,避免整条推送失败。
     */
    private static final int MAX_PUSH_CONTENT_LENGTH = 600;
 
    @Resource
    private WxCpNoticeMapper wxcpNoticeMapper;
    @Resource
    private WxCpMessagePushService messagePushService;
    @Resource
    private SystemStorageBlobService storageBlobService;
 
    /** 企业微信文件消息上限 20MB */
    private static final long MAX_FILE_SIZE = 20L * 1024L * 1024L;
 
    @Override
    public Long createNotice(WxCpNoticeSaveReqVO createReqVO) {
        WxCpNoticeDO notice = BeanUtils.toBean(createReqVO, WxCpNoticeDO.class);
        if (notice.getNoticeType() == null) {
            notice.setNoticeType(WxCpNoticeTypeEnum.TEXT.getType());
        }
        notice.setStatus(WxCpNoticeStatusEnum.DRAFT.getStatus());
        notice.setPushStatus(0);
        // 推送范围选填,不传默认全员;推送时还能在弹窗里改
        if (notice.getPushScope() == null) {
            notice.setPushScope(WxCpPushScopeEnum.ALL.getScope());
        }
        wxcpNoticeMapper.insert(notice);
        return notice.getId();
    }
 
    @Override
    public void updateNotice(WxCpNoticeSaveReqVO updateReqVO) {
        WxCpNoticeDO notice = validateNoticeExists(updateReqVO.getId());
        if (Objects.equals(notice.getStatus(), WxCpNoticeStatusEnum.REVOKED.getStatus())) {
            throw exception(NOTICE_CANNOT_UPDATE);
        }
        WxCpNoticeDO updateObj = BeanUtils.toBean(updateReqVO, WxCpNoticeDO.class);
        // 状态与推送信息由系统维护,不允许前端覆盖
        updateObj.setStatus(null);
        updateObj.setPushStatus(null);
        updateObj.setPushTime(null);
        wxcpNoticeMapper.updateById(updateObj);
    }
 
    @Override
    public void deleteNotice(Long id) {
        validateNoticeExists(id);
        wxcpNoticeMapper.deleteById(id);
    }
 
    @Override
    public WxCpNoticeDO getNotice(Long id) {
        return wxcpNoticeMapper.selectById(id);
    }
 
    @Override
    public PageResult<WxCpNoticeDO> getNoticePage(WxCpNoticePageReqVO pageReqVO) {
        return wxcpNoticeMapper.selectPage(pageReqVO);
    }
 
    @Override
    public void publishNotice(Long id) {
        WxCpNoticeDO notice = validateNoticeExists(id);
        if (Objects.equals(notice.getStatus(), WxCpNoticeStatusEnum.PUBLISHED.getStatus())) {
            return; // 已发布,幂等返回
        }
        if (Objects.equals(notice.getStatus(), WxCpNoticeStatusEnum.REVOKED.getStatus())) {
            throw exception(NOTICE_STATUS_REVOKED);
        }
        wxcpNoticeMapper.updateById(WxCpNoticeDO.builder()
                .id(id).status(WxCpNoticeStatusEnum.PUBLISHED.getStatus()).build());
    }
 
    @Override
    public void revokeNotice(Long id) {
        validateNoticeExists(id);
        wxcpNoticeMapper.updateById(WxCpNoticeDO.builder()
                .id(id).status(WxCpNoticeStatusEnum.REVOKED.getStatus()).build());
    }
 
    @Override
    public WxCpPushResultVO pushNotice(WxCpPushReqVO pushReqVO) {
        WxCpNoticeDO notice = validateNoticeExists(pushReqVO.getId());
        if (!Objects.equals(notice.getStatus(), WxCpNoticeStatusEnum.PUBLISHED.getStatus())) {
            throw exception(NOTICE_STATUS_NOT_PUBLISHED);
        }
 
        Integer scope = pushReqVO.getPushScope() != null ? pushReqVO.getPushScope() : notice.getPushScope();
        String target = StrUtil.isNotBlank(pushReqVO.getPushTarget())
                ? pushReqVO.getPushTarget() : notice.getPushTarget();
        if (scope == null) {
            scope = WxCpPushScopeEnum.ALL.getScope();
        }
 
        // 推送时允许重新选择模板类型;未选择则沿用通知上保存的模板
        Integer noticeType = pushReqVO.getNoticeType() != null
                ? pushReqVO.getNoticeType()
                : (notice.getNoticeType() != null ? notice.getNoticeType() : WxCpNoticeTypeEnum.TEXT.getType());
        String summary = StrUtil.blankToDefault(pushReqVO.getSummary(), notice.getSummary());
        String linkUrl = StrUtil.blankToDefault(pushReqVO.getLinkUrl(), notice.getLinkUrl());
        String linkBtnText = StrUtil.blankToDefault(pushReqVO.getLinkBtnText(), notice.getLinkBtnText());
        String coverUrl = StrUtil.blankToDefault(pushReqVO.getCoverUrl(), notice.getCoverUrl());
        Long fileBlobId = pushReqVO.getFileBlobId() != null ? pushReqVO.getFileBlobId() : notice.getFileBlobId();
        String fileName = StrUtil.blankToDefault(pushReqVO.getFileName(), notice.getFileName());
 
        // 按本次选择的模板类型校验必填项
        if (Objects.equals(noticeType, WxCpNoticeTypeEnum.TEXT.getType())
                && StrUtil.isBlank(notice.getContent())) {
            throw exception(NOTICE_CONTENT_REQUIRED);
        }
        if ((Objects.equals(noticeType, WxCpNoticeTypeEnum.CARD.getType())
                || Objects.equals(noticeType, WxCpNoticeTypeEnum.NEWS.getType()))
                && StrUtil.isBlank(linkUrl)) {
            throw exception(NOTICE_LINK_URL_REQUIRED);
        }
        if (Objects.equals(noticeType, WxCpNoticeTypeEnum.FILE.getType()) && fileBlobId == null) {
            throw exception(NOTICE_FILE_REQUIRED);
        }
 
        WxCpPushResultVO result;
        if (Objects.equals(noticeType, WxCpNoticeTypeEnum.CARD.getType())) {
            result = messagePushService.pushCard(notice.getTitle(),
                    StrUtil.blankToDefault(summary, notice.getContent()),
                    linkUrl, StrUtil.blankToDefault(linkBtnText, "去填报"), scope, target);
        } else if (Objects.equals(noticeType, WxCpNoticeTypeEnum.FILE.getType())) {
            result = pushFileNotice(fileBlobId, fileName, notice, scope, target);
        } else if (Objects.equals(noticeType, WxCpNoticeTypeEnum.NEWS.getType())) {
            result = messagePushService.pushNews(notice.getTitle(),
                    StrUtil.blankToDefault(summary, notice.getContent()),
                    linkUrl, coverUrl, scope, target);
        } else {
            result = messagePushService.pushText(buildPushContent(notice), scope, target);
        }
 
        if (Boolean.TRUE.equals(result.getSuccess())) {
            wxcpNoticeMapper.updateById(WxCpNoticeDO.builder()
                    .id(notice.getId())
                    .pushScope(scope)
                    .pushTarget(target)
                    .pushStatus(1)
                    .pushTime(LocalDateTime.now())
                    .build());
        }
        return result;
    }
 
    /**
     * 文件通知:从系统附件中心取文件,交给推送服务上传企业微信并发送
     */
    private WxCpPushResultVO pushFileNotice(Long fileBlobId, String fileName, WxCpNoticeDO notice,
                                            Integer scope, String target) {
        if (fileBlobId == null) {
            throw exception(NOTICE_FILE_REQUIRED);
        }
        SystemStorageBlobDO blob = storageBlobService.getStorageBlob(fileBlobId);
        if (blob == null) {
            throw exception(NOTICE_FILE_NOT_EXISTS);
        }
        if (blob.getByteSize() != null && blob.getByteSize() > MAX_FILE_SIZE) {
            throw exception(NOTICE_FILE_TOO_LARGE);
        }
        File file = storageBlobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey());
        if (file == null || !file.exists()) {
            throw exception(NOTICE_FILE_NOT_EXISTS);
        }
        String actualFileName = StrUtil.blankToDefault(fileName, blob.getOriginalFilename());
        return messagePushService.pushFile(file, actualFileName, buildPushContent(notice), scope, target);
    }
 
    /**
     * 按模板类型校验必填项
     */
    private void validateNoticeByType(WxCpNoticeSaveReqVO reqVO) {
        Integer type = reqVO.getNoticeType() != null
                ? reqVO.getNoticeType() : WxCpNoticeTypeEnum.TEXT.getType();
        if (Objects.equals(type, WxCpNoticeTypeEnum.TEXT.getType())
                && StrUtil.isBlank(reqVO.getContent())) {
            throw exception(NOTICE_CONTENT_REQUIRED);
        }
        if ((Objects.equals(type, WxCpNoticeTypeEnum.CARD.getType())
                || Objects.equals(type, WxCpNoticeTypeEnum.NEWS.getType()))
                && StrUtil.isBlank(reqVO.getLinkUrl())) {
            throw exception(NOTICE_LINK_URL_REQUIRED);
        }
        if (Objects.equals(type, WxCpNoticeTypeEnum.FILE.getType())
                && reqVO.getFileBlobId() == null) {
            throw exception(NOTICE_FILE_REQUIRED);
        }
    }
 
    /**
     * 拼装推送正文:标题加粗样式用不了,改用【】包裹;过长则截断并提示
     */
    private String buildPushContent(WxCpNoticeDO notice) {
        String body = StrUtil.blankToDefault(notice.getContent(), "");
        String content = "【" + notice.getTitle() + "】\n" + body;
        if (content.length() > MAX_PUSH_CONTENT_LENGTH) {
            content = content.substring(0, MAX_PUSH_CONTENT_LENGTH)
                    + "\n……(内容过长已截断,请到系统内查看完整通知)";
        }
        return content;
    }
 
    private WxCpNoticeDO validateNoticeExists(Long id) {
        if (id == null) {
            throw exception(NOTICE_NOT_EXISTS);
        }
        WxCpNoticeDO notice = wxcpNoticeMapper.selectById(id);
        if (notice == null) {
            throw exception(NOTICE_NOT_EXISTS);
        }
        return notice;
    }
 
}