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 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; } }