docs/sql/README.md
@@ -3,3 +3,5 @@ æ¬é¡¹ç®å å«ç SQL èæ¬æ±æ»å¦ä¸ï¼ - [mes_pro_task_assignee.sql](mes_pro_task_assignee.sql): å¢å çäº§ä»»å¡æ¥å·¥äººåå ³è表ï¼ç¨äºå¤äººåæ¥å·¥åé ã - [wxcp_notice_image_20261027.sql](wxcp_notice_image_20261027.sql): ä¼ä¸å¾®ä¿¡éç¥æ°å¢ãå¾çéç¥ã模æ¿ä¸å¤éä»¶æ¯æï¼ä» æ´æ° notice_type 注éï¼å¾ç/éä»¶èµ°éä»¶ä¸é´è¡¨ï¼æ æ°å¢å段ï¼ã - [wxcp_notice_templates_extend_20261027.sql](wxcp_notice_templates_extend_20261027.sql): ä¼ä¸å¾®ä¿¡éç¥æ©å±ãè§é¢/Markdown/å¤å¾æã模æ¿ï¼æ°å¢å¤å¾æå表 wxcp_notice_articleï¼å¹¶æ´æ° notice_type 注éã docs/sql/wxcp_module.sql
@@ -8,6 +8,8 @@ -- æ´»å¨å°é¢ record_type = 'wxcp_activity'ï¼æ´»å¨è¯¦æ å¾ç record_type = 'wxcp_activity_detail'ï¼ -- å¾çéç¥ record_type = 'wxcp_activity_image'ï¼éä»¶éç¥ record_type = 'wxcp_activity_file'ï¼ -- æ¥åéä»¶ record_type = 'wxcp_activity_signup' -- éç¥æ¨¡åçç´ æåçï¼record_type = 'wxcp_notice_image' / 'wxcp_notice_file' / -- 'wxcp_notice_video' / 'wxcp_notice_article_cover' -- ============================================================ -- ---------------------------- @@ -80,7 +82,7 @@ `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', `title` varchar(128) NOT NULL COMMENT 'éç¥æ é¢', `content` text COMMENT 'éç¥æ£æ', `notice_type` tinyint NOT NULL DEFAULT 1 COMMENT 'éç¥æ¨¡æ¿ï¼1çº¯ææ¬ 2徿å¡ç(å¡«æ¥/龿¥) 3æä»¶éç¥ 4å¾ææ¶æ¯', `notice_type` tinyint NOT NULL DEFAULT 1 COMMENT 'éç¥æ¨¡æ¿ï¼1çº¯ææ¬ 2徿å¡ç(å¡«æ¥/龿¥) 3æä»¶éç¥ 4å¾ææ¶æ¯ 5å¾çéç¥ 6è§é¢éç¥ 7Markdown 8å¤å¾ææ¶æ¯', `summary` varchar(512) DEFAULT NULL COMMENT 'æè¦ï¼å¾æå¡ç/å¾ææ¶æ¯ï¼', `link_url` varchar(1024) DEFAULT NULL COMMENT 'è·³è½¬é¾æ¥ï¼å¡«æ¥/详æ ï¼', `link_btn_text` varchar(32) DEFAULT NULL COMMENT '龿¥æé®ææ¡', @@ -102,6 +104,27 @@ KEY `idx_status` (`status`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'ä¼ä¸å¾®ä¿¡éç¥'; -- ---------------------------- -- éç¥ - å¤å¾ææ¶æ¯æ¡ç® -- ä» ãå¤å¾ææ¶æ¯ã模æ¿ä½¿ç¨ï¼å°é¢å¾åªå blob_idï¼å°åèµ°éä»¶ä¸é´è¡¨ -- ---------------------------- CREATE TABLE IF NOT EXISTS `wxcp_notice_article` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', `notice_id` bigint NOT NULL COMMENT 'éç¥ç¼å·', `title` varchar(128) NOT NULL COMMENT '徿æ é¢', `description` varchar(512) DEFAULT NULL COMMENT '徿æè¦', `link_url` varchar(1024) NOT NULL COMMENT 'ç¹å»è·³è½¬é¾æ¥', `cover_blob_id` bigint DEFAULT NULL COMMENT 'å°é¢å¾ Blob ID', `sort_no` int NOT NULL DEFAULT 0 COMMENT 'æåºå·', `creator` varchar(64) DEFAULT '' COMMENT 'å建è ', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` varchar(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), KEY `idx_notice_id` (`notice_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'ä¼ä¸å¾®ä¿¡éç¥å¤å¾ææ¶æ¯æ¡ç®'; -- ============================================================ -- åçº§èæ¬ï¼å°é¢å¾ä»ãä¸å¡è¡¨åå°åãæ¹ä¸ºãéä»¶ä¸é´è¡¨ã -- 已建è¿åºçç¯å¢æ§è¡ä¸æ¬¡å³å¯ï¼æ°åºæ§è¡ä¸é¢ç CREATE TABLE å·²æ¯ææ°ç»æï¼æ éæ§è¡ yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -89,7 +89,13 @@ WXCP_ACTIVITY_DETAIL("wxcp_activity_detail", "ä¼ä¸å¾®ä¿¡æ´»å¨è¯¦æ å¾"), WXCP_ACTIVITY_IMAGE("wxcp_activity_image", "ä¼ä¸å¾®ä¿¡æ´»å¨å¾çéç¥"), WXCP_ACTIVITY_FILE("wxcp_activity_file", "ä¼ä¸å¾®ä¿¡æ´»å¨éä»¶éç¥"), WXCP_ACTIVITY_SIGNUP("wxcp_activity_signup", "ä¼ä¸å¾®ä¿¡æ´»å¨æ¥å"); WXCP_ACTIVITY_SIGNUP("wxcp_activity_signup", "ä¼ä¸å¾®ä¿¡æ´»å¨æ¥å"), // ===== ä¼ä¸å¾®ä¿¡éç¥ ===== WXCP_NOTICE_IMAGE("wxcp_notice_image", "ä¼ä¸å¾®ä¿¡éç¥å¾ç"), WXCP_NOTICE_FILE("wxcp_notice_file", "ä¼ä¸å¾®ä¿¡éç¥éä»¶"), WXCP_NOTICE_VIDEO("wxcp_notice_video", "ä¼ä¸å¾®ä¿¡éç¥è§é¢"), WXCP_NOTICE_ARTICLE_COVER("wxcp_notice_article_cover", "ä¼ä¸å¾®ä¿¡éç¥å¾æå°é¢"); private final String type; private final String description; yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/notice/WxCpNoticeController.java
@@ -3,6 +3,8 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticeArticleRespVO; import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticePageReqVO; import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticeRespVO; import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticeSaveReqVO; @@ -21,6 +23,8 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -112,6 +116,23 @@ } WxCpNoticeRespVO vo = BeanUtils.toBean(notice, WxCpNoticeRespVO.class); fillNames(vo); // ç´ æè¯¦æ éè¦åæ¾ï¼å表页ä¸åæ¾ä»¥å 夿¥é件表 vo.setImageAttachmentList(noticeService.getNoticeImageAttachments(notice.getId())); vo.setFileAttachmentList(noticeService.getNoticeFileAttachments(notice.getId())); vo.setVideoAttachmentList(noticeService.getNoticeVideoAttachments(notice.getId())); vo.setImageBlobIds(vo.getImageAttachmentList().stream() .map(SystemStorageBlobRespVO::getId).toList()); vo.setFileBlobIds(vo.getFileAttachmentList().stream() .map(SystemStorageBlobRespVO::getId).toList()); vo.setVideoBlobIds(vo.getVideoAttachmentList().stream() .map(SystemStorageBlobRespVO::getId).toList()); vo.setArticleList(noticeService.getNoticeArticles(notice.getId()).stream().map(article -> { WxCpNoticeArticleRespVO item = BeanUtils.toBean(article, WxCpNoticeArticleRespVO.class); List<SystemStorageBlobRespVO> covers = noticeService.getNoticeArticleCoverAttachments(article.getId()); item.setCoverAttachment(covers.isEmpty() ? null : covers.get(0)); return item; }).toList()); return vo; } @@ -122,6 +143,8 @@ vo.setNoticeTypeName(type != null ? type.getName() : null); WxCpPushScopeEnum scope = WxCpPushScopeEnum.valueOf(vo.getPushScope()); vo.setPushScopeName(scope != null ? scope.getName() : null); // é¢è§é¾æ¥åªä¾èµé ç½®ï¼å表页ä¹è½ç´æ¥ç»åºï¼æ¹ä¾¿ç®¡çåå¤å¶åç»æå vo.setPreviewUrl(noticeService.buildPreviewUrl(vo.getId())); } } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/notice/WxCpNoticePublicController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,328 @@ package cn.iocoder.yudao.module.wxcp.controller.admin.notice; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.wxcp.dal.dataobject.notice.WxCpNoticeDO; 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.notice.WxCpNoticeService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.net.URI; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ä¼ä¸å¾®ä¿¡éç¥ - æåé¢è§é¡µï¼å ç»å½ï¼ * * 说æï¼æ¾å¨ controller.admin å ä¸ä»èå¸¦ä¸ /admin-api åç¼ã * å端 Vite å¼å代çåªè½¬å /admin-apiï¼è¥æ¾å° /app-apiï¼ * æåå¨ä¼ä¸å¾®ä¿¡éæå¼é¾æ¥ä¼æå°åç«¯éææå¡è¿å 404ã * * @author è¶ çº§ç®¡çå */ @Tag(name = "ç¨æ· App - ä¼ä¸å¾®ä¿¡éç¥é¢è§") @RestController @RequestMapping("/wxcp/notice") @Validated @Slf4j public class WxCpNoticePublicController { /** è¡å 代ç */ private static final Pattern MD_CODE = Pattern.compile("`([^`]+)`"); /** å ç² */ private static final Pattern MD_STRONG = Pattern.compile("\\*\\*(.+?)\\*\\*"); /** 龿¥ [ææ¡](å°å) */ private static final Pattern MD_LINK = Pattern.compile("\\[([^\\]]+)]\\(([^)]+)\\)"); @Resource private WxCpNoticeService noticeService; @GetMapping(value = "/preview-page", produces = MediaType.TEXT_HTML_VALUE) @PermitAll @Operation(summary = "éç¥é¢è§é¡µï¼HTMLï¼", description = "å¾ç/éä»¶éç¥æ¨éç»æååï¼æåæå¼æ¤é¡µæ¥çå¾çä¸éä»¶") @Parameter(name = "id", description = "éç¥ç¼å·", required = true) public String previewPage(@RequestParam("id") Long id) { WxCpNoticeDO notice = noticeService.getNotice(id); if (notice == null) { return renderMessagePage("éç¥ä¸åå¨", "éç¥å¯è½å·²è¢«å é¤ï¼è¯·èç³»éç¥å起人确认ã"); } if (!WxCpNoticeStatusEnum.PUBLISHED.getStatus().equals(notice.getStatus())) { return renderMessagePage("éç¥æªå¼æ¾", "该éç¥å°æªå叿已æ¤åï¼è¯·èç³»éç¥å起人确认ã"); } return renderPreviewPage(notice); } private String renderPreviewPage(WxCpNoticeDO notice) { List<SystemStorageBlobRespVO> images = noticeService.getNoticeImageAttachments(notice.getId()); List<SystemStorageBlobRespVO> files = noticeService.getNoticeFileAttachments(notice.getId()); List<SystemStorageBlobRespVO> videos = noticeService.getNoticeVideoAttachments(notice.getId()); StringBuilder sb = new StringBuilder(2048); sb.append("<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">") .append("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1\">") .append("<title>").append(esc(notice.getTitle())).append("</title>") .append("<style>") .append("*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}") .append("body{margin:0;padding:16px;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;") .append("background:#f5f6f8;color:#1f2329;font-size:15px;line-height:1.6}") .append(".card{background:#fff;border-radius:12px;padding:18px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.06)}") .append("h1{font-size:19px;margin:0 0 14px;font-weight:600;word-break:break-all}") .append(".desc{white-space:pre-wrap;word-break:break-word;color:#3f444d}") .append(".detail-img{display:block;margin:12px 0 0}") .append(".detail-img img{display:block;width:100%;height:auto;border-radius:8px;background:#f2f3f5}") .append(".file-title{font-weight:600;margin-bottom:8px}") .append(".file-list div{padding:8px 0;border-bottom:1px solid #f0f1f3;word-break:break-all}") .append(".file-list div:last-child{border-bottom:0}") .append(".file-list a{color:#3370ff;text-decoration:none}") .append(".file-size{color:#8f959e;font-size:12px;margin-left:6px}") .append(".video-wrap video{display:block;width:100%;border-radius:8px;background:#000}") .append(".markdown h1,.markdown h2,.markdown h3{margin:14px 0 8px;font-weight:600;line-height:1.4}") .append(".markdown h1{font-size:19px}.markdown h2{font-size:17px}.markdown h3{font-size:16px}") .append(".markdown p{margin:8px 0}") .append(".markdown ul,.markdown ol{margin:8px 0;padding-left:20px}") .append(".markdown li{margin:4px 0}") .append(".markdown blockquote{margin:10px 0;padding:8px 12px;border-left:3px solid #dee0e3;") .append("background:#f7f8fa;color:#646a73;border-radius:4px}") .append(".markdown code{padding:1px 5px;border-radius:4px;background:#f2f3f5;font-size:13px}") .append(".markdown a{color:#3370ff;text-decoration:none}") .append(".btn{display:block;margin-top:16px;padding:12px;border-radius:8px;background:#3370ff;color:#fff;") .append("font-size:15px;font-weight:500;text-align:center;text-decoration:none}") .append(".tip{margin-top:12px;font-size:12px;color:#8f959e;text-align:center}") .append("</style></head><body>"); sb.append("<div class=\"card\"><h1>").append(esc(notice.getTitle())).append("</h1>"); if (StrUtil.isNotBlank(notice.getContent())) { if (Objects.equals(notice.getNoticeType(), WxCpNoticeTypeEnum.MARKDOWN.getType())) { // Markdown éç¥å¨é¢è§é¡µæå¯ææ¬å±ç¤ºï¼æ¸²æåå·²å¯¹ç¨æ·å 容å HTML è½¬ä¹ sb.append("<div class=\"desc markdown\">").append(renderMarkdown(notice.getContent())).append("</div>"); } else { sb.append("<div class=\"desc\">").append(esc(notice.getContent())).append("</div>"); } } if (StrUtil.isNotBlank(notice.getLinkUrl())) { String link = StrUtil.blankToDefault(notice.getLinkBtnText(), "æ¥ç详æ "); sb.append("<a class=\"btn\" href=\"").append(esc(notice.getLinkUrl())) .append("\" target=\"_blank\" rel=\"noopener\">").append(esc(link)).append("</a>"); } sb.append("</div>"); // å¾çï¼æå徿¯ä¾å®æ´å±ç¤ºï¼ç¹ä¸ä¸ç¨æµè§å¨æå¼åå¾ï¼æ¹ä¾¿æ¾å¤§æ¥ç if (CollUtil.isNotEmpty(images)) { sb.append("<div class=\"card\">"); for (SystemStorageBlobRespVO image : images) { String raw = StrUtil.blankToDefault(image.getPreviewURL(), image.getUrl()); if (StrUtil.isBlank(raw)) { continue; } String url = toSameOriginUrl(raw); sb.append("<a class=\"detail-img\" href=\"").append(esc(url)) .append("\" target=\"_blank\" rel=\"noopener\">") .append("<img src=\"").append(esc(url)).append("\" alt=\"éç¥å¾ç\">") .append("</a>"); } sb.append("</div>"); } // è§é¢ï¼æåç¹å¼å³å¯ææ¾ if (CollUtil.isNotEmpty(videos)) { sb.append("<div class=\"card\">"); for (SystemStorageBlobRespVO video : videos) { String raw = StrUtil.blankToDefault(video.getPreviewURL(), video.getUrl()); if (StrUtil.isBlank(raw)) { continue; } String url = toSameOriginUrl(raw); sb.append("<div class=\"video-wrap\"><video controls preload=\"metadata\" src=\"") .append(esc(url)).append("\"></video></div>"); } sb.append("</div>"); } // éä»¶ï¼æåç¹å¼å³å¯é¢è§/ä¸è½½ if (CollUtil.isNotEmpty(files)) { sb.append("<div class=\"card\"><div class=\"file-title\">éä»¶</div><div class=\"file-list\">"); for (SystemStorageBlobRespVO file : files) { String raw = StrUtil.blankToDefault(file.getDownloadURL(), StrUtil.blankToDefault(file.getUrl(), file.getPreviewURL())); if (StrUtil.isBlank(raw)) { continue; } String url = toSameOriginUrl(raw); String name = StrUtil.blankToDefault(file.getOriginalFilename(), StrUtil.blankToDefault(file.getName(), "éä»¶")); sb.append("<div><a href=\"").append(esc(url)).append("\" target=\"_blank\" rel=\"noopener\">") .append(esc(name)).append("</a>"); if (file.getByteSize() != null) { sb.append("<span class=\"file-size\">").append(formatSize(file.getByteSize())).append("</span>"); } sb.append("</div>"); } sb.append("</div></div>"); } sb.append("<div class=\"tip\">æ¥èªä¼ä¸å¾®ä¿¡éç¥ï¼è¯·å¨ä¼ä¸å¾®ä¿¡å æ¥ç</div></body></html>"); return sb.toString(); } private String formatSize(Long bytes) { if (bytes == null || bytes <= 0) { return ""; } if (bytes < 1024) { return bytes + " B"; } if (bytes < 1024 * 1024) { return String.format("%.1f KB", bytes / 1024.0); } return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); } /** * æéä»¶å°å转æåæºç¸å¯¹å°å * * éä»¶éåçæ¯å¸¦ç¾åçç»å¯¹å°åï¼åååèª yudao.storage.domainï¼é常æ¯å ç½å°åï¼ã * æåææºéè¿å ¬ç½ååæå¼æ¬é¡µæ¶ç´è¿å ç½å°åä¼å è½½ä¸åºå¾çï¼ * è¿é奿åè®®ä¸åååªç /admin-api/... è·¯å¾ï¼äº¤ç»åæºä»£ç转åã */ private String toSameOriginUrl(String url) { String trimmed = url.trim(); if (!StrUtil.startWithAnyIgnoreCase(trimmed, "http://", "https://")) { return trimmed; } try { URI uri = URI.create(trimmed); String path = uri.getRawPath(); if (StrUtil.isBlank(path)) { return trimmed; } String query = uri.getRawQuery(); return StrUtil.isBlank(query) ? path : path + "?" + query; } catch (IllegalArgumentException e) { // å°åä¸åè§å°±åæ ·éåºï¼ä¸è½å 为ä¸å¼ å¾ç让æ´é¡µæä¸å¼ log.warn("éç¥éä»¶å°åè§£æå¤±è´¥ï¼æåæ ·è¾åºï¼{}", trimmed); return trimmed; } } private String renderMessagePage(String title, String desc) { return "<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">" + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" + "<title>" + esc(title) + "</title><style>" + "body{margin:0;padding:60px 24px;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;" + "background:#f5f6f8;color:#1f2329;text-align:center}" + "h1{font-size:18px;font-weight:600;margin:0 0 12px}" + "p{color:#646a73;font-size:14px;line-height:1.7;margin:0}" + "</style></head><body><h1>" + esc(title) + "</h1><p>" + esc(desc) + "</p></body></html>"; } /** * è½»é Markdown 渲æï¼åªæ¯æä¼ä¸å¾®ä¿¡éç¥å¸¸ç¨çæ é¢ãå ç²ãå表ãå¼ç¨ã代ç ã龿¥ãåé线 * * å®å ¨åæï¼ææç¨æ·å 容å ç¨ {@link #esc(String)} 转ä¹ï¼ä¹åææ¼æ¥æ ç¾ï¼ * å æ¤ä¸ä¼æ§è¡åå§ HTMLï¼é¾æ¥åªå 许 http/https/ç«å è·¯å¾ã */ private String renderMarkdown(String markdown) { String normalized = markdown.replace("\r\n", "\n").replace("\r", "\n"); String[] lines = normalized.split("\n", -1); StringBuilder html = new StringBuilder(1024); boolean inList = false; for (String rawLine : lines) { String line = rawLine.trim(); if (line.isEmpty()) { if (inList) { html.append("</ul>"); inList = false; } continue; } if (line.startsWith("- ") || line.startsWith("* ")) { if (!inList) { html.append("<ul>"); inList = true; } html.append("<li>").append(renderInline(line.substring(2))).append("</li>"); continue; } if (inList) { html.append("</ul>"); inList = false; } if (line.startsWith("### ")) { html.append("<h3>").append(renderInline(line.substring(4))).append("</h3>"); } else if (line.startsWith("## ")) { html.append("<h2>").append(renderInline(line.substring(3))).append("</h2>"); } else if (line.startsWith("# ")) { html.append("<h1>").append(renderInline(line.substring(2))).append("</h1>"); } else if (line.startsWith("> ")) { html.append("<blockquote>").append(renderInline(line.substring(2))).append("</blockquote>"); } else if ("---".equals(line) || "***".equals(line)) { html.append("<hr>"); } else { html.append("<p>").append(renderInline(line)).append("</p>"); } } if (inList) { html.append("</ul>"); } return html.toString(); } /** * 渲æè¡å Markdownï¼è¾å ¥å·²è½¬ä¹ï¼è¾åºåªå å«åæ§æ ç¾ */ private String renderInline(String text) { String result = esc(text); result = MD_CODE.matcher(result).replaceAll("<code>$1</code>"); result = MD_STRONG.matcher(result).replaceAll("<strong>$1</strong>"); Matcher matcher = MD_LINK.matcher(result); StringBuilder sb = new StringBuilder(); while (matcher.find()) { String label = matcher.group(1); String url = matcher.group(2).trim(); String replacement = isSafeUrl(url) ? "<a href=\"" + url + "\" target=\"_blank\" rel=\"noopener\">" + label + "</a>" : label; matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); } matcher.appendTail(sb); return sb.toString(); } /** * åªå 许 http/https ä¸ç«å è·¯å¾ï¼é¿å javascript: çå±é©åè®® */ private boolean isSafeUrl(String url) { String lower = url.trim().toLowerCase(); return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("/"); } /** * HTML 转ä¹ï¼é¿å éç¥æ é¢/å 容éçç¹æ®åç¬¦ç ´åé¡µé¢æé æ XSS */ private String esc(String raw) { if (raw == null) { return ""; } return raw.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/notice/vo/WxCpNoticeArticleRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,32 @@ package cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管çåå° - ä¼ä¸å¾®ä¿¡éç¥å¤å¾ææ¡ç® Response VO") @Data public class WxCpNoticeArticleRespVO { @Schema(description = "ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "徿æ é¢", example = "å ³äº2026å¹´æ¥å£è¿å¨ä¼çéç¥") private String title; @Schema(description = "徿æè¦") private String description; @Schema(description = "ç¹å»è·³è½¬é¾æ¥") private String linkUrl; @Schema(description = "å°é¢å¾ Blob ID") private Long coverBlobId; @Schema(description = "å°é¢å¾éä»¶ï¼å«é¢è§å°åï¼éå¡«ï¼") private SystemStorageBlobRespVO coverAttachment; @Schema(description = "æåºå·") private Integer sortNo; } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/notice/vo/WxCpNoticeArticleSaveReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,35 @@ package cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.Data; @Schema(description = "管çåå° - ä¼ä¸å¾®ä¿¡éç¥å¤å¾ææ¡ç® å建/ä¿®æ¹ Request VO") @Data public class WxCpNoticeArticleSaveReqVO { @Schema(description = "ç¼å·ï¼ä¿®æ¹æ¶ä¼ ï¼æ°å¢ä¸ä¼ ï¼", example = "1024") private Long id; @Schema(description = "徿æ é¢", requiredMode = Schema.RequiredMode.REQUIRED, example = "å ³äº2026å¹´æ¥å£è¿å¨ä¼çéç¥") @NotEmpty(message = "徿æ é¢ä¸è½ä¸ºç©º") @Size(max = 128, message = "徿æ é¢é¿åº¦ä¸è½è¶ è¿ 128 个å符") private String title; @Schema(description = "徿æè¦", example = "请åççº§äºæ¬å¨äºå宿æ¥å") @Size(max = 512, message = "徿æè¦é¿åº¦ä¸è½è¶ è¿ 512 个å符") private String description; @Schema(description = "ç¹å»è·³è½¬é¾æ¥", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://example.com/news/1") @NotEmpty(message = "å¾æè·³è½¬é¾æ¥ä¸è½ä¸ºç©º") @Size(max = 1024, message = "è·³è½¬é¾æ¥é¿åº¦ä¸è½è¶ è¿ 1024 个å符") private String linkUrl; @Schema(description = "å°é¢å¾ Blob IDï¼éå¡«ï¼", example = "1024") private Long coverBlobId; @Schema(description = "æåºå·ï¼è¶å°è¶é å", example = "1") private Integer sortNo; } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/notice/vo/WxCpNoticeRespVO.java
@@ -1,9 +1,11 @@ package cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管çåå° - ä¼ä¸å¾®ä¿¡éç¥ Response VO") @Data @@ -18,7 +20,8 @@ @Schema(description = "éç¥æ£æ") private String content; @Schema(description = "éç¥æ¨¡æ¿ç±»å", example = "1") @Schema(description = "éç¥æ¨¡æ¿ç±»åï¼1çº¯ææ¬ 2徿å¡ç 3æä»¶éç¥ 4å¾ææ¶æ¯ 5å¾çéç¥ 6è§é¢éç¥ 7Markdown 8å¤å¾ææ¶æ¯", example = "1") private Integer noticeType; @Schema(description = "éç¥æ¨¡æ¿ç±»ååç§°", example = "çº¯ææ¬éç¥") @@ -36,12 +39,36 @@ @Schema(description = "å¾ææ¶æ¯å°é¢å¾å°å") private String coverUrl; @Schema(description = "æä»¶éç¥çéä»¶ Blob ID") @Schema(description = "æä»¶éç¥çéä»¶ Blob IDï¼æ§çåéä»¶åæ®µï¼") private Long fileBlobId; @Schema(description = "æä»¶éç¥çéä»¶æä»¶å") @Schema(description = "æä»¶éç¥çéä»¶æä»¶åï¼æ§çåéä»¶åæ®µï¼") private String fileName; @Schema(description = "å¾çéç¥çå¾ç Blob ID å表") private List<Long> imageBlobIds; @Schema(description = "éä»¶éç¥çéä»¶ Blob ID å表") private List<Long> fileBlobIds; @Schema(description = "å¾çéç¥çå¾çå表ï¼å«é¢è§å°åï¼") private List<SystemStorageBlobRespVO> imageAttachmentList; @Schema(description = "éä»¶éç¥çéä»¶å表ï¼å«é¢è§/ä¸è½½å°åï¼") private List<SystemStorageBlobRespVO> fileAttachmentList; @Schema(description = "è§é¢éç¥çè§é¢ Blob ID å表") private List<Long> videoBlobIds; @Schema(description = "è§é¢éç¥çè§é¢å表ï¼å«é¢è§å°åï¼") private List<SystemStorageBlobRespVO> videoAttachmentList; @Schema(description = "å¤å¾ææ¶æ¯ç徿å表") private List<WxCpNoticeArticleRespVO> articleList; @Schema(description = "æåé¢è§é¡µå°åï¼å¾ç/éä»¶/è§é¢éç¥å¯ç´æ¥åç»æåæ¥çï¼") private String previewUrl; @Schema(description = "ç¶æ", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/notice/vo/WxCpNoticeSaveReqVO.java
@@ -4,9 +4,12 @@ import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpPushScopeEnum; import cn.iocoder.yudao.module.wxcp.enums.notice.WxCpNoticeTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.Data; import java.util.List; @Schema(description = "管çåå° - ä¼ä¸å¾®ä¿¡éç¥ å建/ä¿®æ¹ Request VO") @Data @@ -24,7 +27,8 @@ @Size(max = 10000, message = "éç¥æ£æé¿åº¦ä¸è½è¶ è¿ 10000 个å符") private String content; @Schema(description = "éç¥æ¨¡æ¿ç±»åï¼1-çº¯ææ¬ 2-徿å¡ç(å¡«æ¥/龿¥) 3-æä»¶éç¥ 4-å¾ææ¶æ¯", example = "1") @Schema(description = "éç¥æ¨¡æ¿ç±»åï¼1çº¯ææ¬ 2徿å¡ç(å¡«æ¥/龿¥) 3æä»¶éç¥ 4å¾ææ¶æ¯ 5å¾çéç¥ 6è§é¢éç¥ 7Markdown 8å¤å¾ææ¶æ¯", example = "1") @InEnum(WxCpNoticeTypeEnum.class) private Integer noticeType; @@ -44,13 +48,30 @@ @Size(max = 1024, message = "å°é¢å¾å°åé¿åº¦ä¸è½è¶ è¿ 1024 个å符") private String coverUrl; @Schema(description = "æä»¶éç¥çéä»¶ Blob IDï¼å è°ç¨ /system/storage-blob/upload ä¸ä¼ ï¼", example = "1024") @Schema(description = "æä»¶éç¥çéä»¶ Blob IDï¼æ§çåéä»¶åæ®µï¼å»ºè®®æ¹ç¨ fileBlobIdsï¼", example = "1024") private Long fileBlobId; @Schema(description = "æä»¶éç¥çéä»¶æä»¶å", example = "å ³äºå¼å±XXå·¥ä½çéç¥.pdf") @Schema(description = "æä»¶éç¥çéä»¶æä»¶åï¼æ§çåéä»¶åæ®µï¼", example = "å ³äºå¼å±XXå·¥ä½çéç¥.pdf") @Size(max = 255, message = "æä»¶åé¿åº¦ä¸è½è¶ è¿ 255 个å符") private String fileName; @Schema(description = "å¾çéç¥çå¾ç Blob ID åè¡¨ï¼æå¤ 9 å¼ ï¼ï¼null = 䏿¹å¨ï¼[] = æ¸ ç©º", example = "[1024,1025]") @Size(max = 9, message = "å¾çæå¤ä¸ä¼ 9 å¼ ") private List<Long> imageBlobIds; @Schema(description = "éä»¶éç¥çéä»¶ Blob ID åè¡¨ï¼æå¤ 5 个ï¼ï¼null = 䏿¹å¨ï¼[] = æ¸ ç©º", example = "[1024,1025]") @Size(max = 5, message = "éä»¶æå¤ä¸ä¼ 5 个") private List<Long> fileBlobIds; @Schema(description = "è§é¢éç¥çè§é¢ Blob ID åè¡¨ï¼æå¤ 1 个ï¼ï¼null = 䏿¹å¨ï¼[] = æ¸ ç©º", example = "[1024]") @Size(max = 1, message = "è§é¢æå¤ä¸ä¼ 1 个") private List<Long> videoBlobIds; @Schema(description = "å¤å¾ææ¶æ¯ç徿å表ï¼null = 䏿¹å¨ï¼[] = æ¸ ç©º", example = "[]") @Valid @Size(max = 8, message = "å¤å¾ææ¶æ¯æå¤ 8 ç¯") private List<WxCpNoticeArticleSaveReqVO> articleList; @Schema(description = "æ¨éèå´ï¼éå¡«ï¼ä¸ä¼ é»è®¤å ¨åï¼ï¼æ¨éæ¶ä»å¯å¨æ¨éå¼¹çªééæ°éæ©", example = "0") @InEnum(WxCpPushScopeEnum.class) private Integer pushScope; yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/controller/admin/vo/WxCpPushReqVO.java
@@ -1,11 +1,15 @@ package cn.iocoder.yudao.module.wxcp.controller.admin.vo; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticeArticleSaveReqVO; import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpPushScopeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; import java.util.List; @Schema(description = "管çåå° - ä¼ä¸å¾®ä¿¡æ¨é Request VOï¼æ´»å¨ä¸éç¥å ±ç¨ï¼") @Data @@ -23,7 +27,8 @@ @Size(max = 1024, message = "æ¨éç®æ é¿åº¦ä¸è½è¶ è¿ 1024 个å符") private String pushTarget; @Schema(description = "æ¨éæ¶éæ©ç模æ¿ç±»åï¼1çº¯ææ¬ 2徿å¡ç 3æä»¶éç¥ 4å¾ææ¶æ¯ï¼ä¸ä¼ 沿ç¨éç¥é ç½®", example = "2") @Schema(description = "æ¨éæ¶éæ©ç模æ¿ç±»åï¼1çº¯ææ¬ 2徿å¡ç 3æä»¶éç¥ 4å¾ææ¶æ¯ 5å¾çéç¥ 6è§é¢éç¥ 7Markdown 8å¤å¾ææ¶æ¯ï¼ä¸ä¼ 沿ç¨éç¥é ç½®", example = "2") @InEnum(cn.iocoder.yudao.module.wxcp.enums.notice.WxCpNoticeTypeEnum.class) private Integer noticeType; @@ -43,11 +48,28 @@ @Size(max = 1024, message = "å°é¢å¾å°åé¿åº¦ä¸è½è¶ è¿ 1024 个å符") private String coverUrl; @Schema(description = "æä»¶éç¥çéä»¶ Blob ID") @Schema(description = "æä»¶éç¥çéä»¶ Blob IDï¼æ§çåéä»¶åæ®µï¼å»ºè®®æ¹ç¨ fileBlobIdsï¼") private Long fileBlobId; @Schema(description = "æä»¶éç¥çéä»¶æä»¶å") @Schema(description = "æä»¶éç¥çéä»¶æä»¶åï¼æ§çåéä»¶åæ®µï¼") @Size(max = 255, message = "æä»¶åé¿åº¦ä¸è½è¶ è¿ 255 个å符") private String fileName; @Schema(description = "å¾çéç¥çå¾ç Blob ID å表ï¼ä¸ä¼ 沿ç¨éç¥ä¸å·²ä¿åçå¾ç", example = "[1024,1025]") @Size(max = 9, message = "å¾çæå¤ä¸ä¼ 9 å¼ ") private List<Long> imageBlobIds; @Schema(description = "éä»¶éç¥çéä»¶ Blob ID å表ï¼ä¸ä¼ 沿ç¨éç¥ä¸å·²ä¿åçéä»¶", example = "[1024,1025]") @Size(max = 5, message = "éä»¶æå¤ä¸ä¼ 5 个") private List<Long> fileBlobIds; @Schema(description = "è§é¢éç¥çè§é¢ Blob ID å表ï¼ä¸ä¼ 沿ç¨éç¥ä¸å·²ä¿åçè§é¢", example = "[1024]") @Size(max = 1, message = "è§é¢æå¤ä¸ä¼ 1 个") private List<Long> videoBlobIds; @Schema(description = "å¤å¾ææ¶æ¯ç徿å表ï¼ä¸ä¼ 沿ç¨éç¥ä¸å·²ä¿åç徿", example = "[]") @Valid @Size(max = 8, message = "å¤å¾ææ¶æ¯æå¤ 8 ç¯") private List<WxCpNoticeArticleSaveReqVO> articleList; } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/dal/dataobject/notice/WxCpNoticeArticleDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,54 @@ package cn.iocoder.yudao.module.wxcp.dal.dataobject.notice; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * ä¼ä¸å¾®ä¿¡éç¥ - å¤å¾ææ¶æ¯æ¡ç® DO * * ä» ãå¤å¾ææ¶æ¯ã模æ¿ä½¿ç¨ï¼ä¸æ¡éç¥å¯ä»¥å¸¦å¤ç¯å¾æï¼ä¼ä¸å¾®ä¿¡ news æ¶æ¯æå¤ 8 ç¯ï¼ã * å°é¢å¾ä¸åå°åï¼åªå blobIdï¼æ¨éæ¶ååå¯è®¿é®å°åã * * @author è¶ çº§ç®¡çå */ @TableName("wxcp_notice_article") @KeySequence("wxcp_notice_article_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class WxCpNoticeArticleDO extends BaseDO { @TableId private Long id; /** * éç¥ç¼å· */ private Long noticeId; /** * 徿æ é¢ */ private String title; /** * 徿æè¦ */ private String description; /** * ç¹å»è·³è½¬é¾æ¥ */ private String linkUrl; /** * å°é¢å¾ Blob IDï¼éå¡«ï¼ */ private Long coverBlobId; /** * æåºå· */ private Integer sortNo; } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/dal/mysql/notice/WxCpNoticeArticleMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,30 @@ package cn.iocoder.yudao.module.wxcp.dal.mysql.notice; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.wxcp.dal.dataobject.notice.WxCpNoticeArticleDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * ä¼ä¸å¾®ä¿¡éç¥ - å¤å¾ææ¡ç® Mapper * * @author è¶ çº§ç®¡çå */ @Mapper public interface WxCpNoticeArticleMapper extends BaseMapperX<WxCpNoticeArticleDO> { default List<WxCpNoticeArticleDO> selectListByNoticeId(Long noticeId) { return selectList(new LambdaQueryWrapperX<WxCpNoticeArticleDO>() .eq(WxCpNoticeArticleDO::getNoticeId, noticeId) .orderByAsc(WxCpNoticeArticleDO::getSortNo) .orderByAsc(WxCpNoticeArticleDO::getId)); } default int deleteByNoticeId(Long noticeId) { return delete(new LambdaQueryWrapperX<WxCpNoticeArticleDO>() .eq(WxCpNoticeArticleDO::getNoticeId, noticeId)); } } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/enums/ErrorCodeConstants.java
@@ -57,6 +57,21 @@ ErrorCode NOTICE_FILE_REQUIRED = new ErrorCode(1_050_003_006, "æä»¶éç¥å¿ é¡»ä¸ä¼ æä»¶ï¼è¯·ä¸ä¼ å忍é"); ErrorCode NOTICE_FILE_NOT_EXISTS = new ErrorCode(1_050_003_007, "éç¥æä»¶ä¸åå¨æå·²è¢«å é¤ï¼è¯·éæ°ä¸ä¼ "); ErrorCode NOTICE_FILE_TOO_LARGE = new ErrorCode(1_050_003_008, "éç¥æä»¶è¶ è¿ä¼ä¸å¾®ä¿¡ 20MB éå¶ï¼è¯·åç¼©ææ¹ç¨å¾æå¡çæ¨éä¸è½½é¾æ¥"); ErrorCode NOTICE_IMAGE_REQUIRED = new ErrorCode(1_050_003_009, "å¾çéç¥å¿ é¡»è³å°ä¸ä¼ 1 å¼ å¾çï¼è¯·ä¸ä¼ å忍é"); ErrorCode NOTICE_IMAGE_ONLY = new ErrorCode(1_050_003_010, "å¾çéç¥åªè½ä¸ä¼ å¾çæä»¶ï¼è¯·éæ°éæ©"); ErrorCode NOTICE_IMAGE_TOO_LARGE = new ErrorCode(1_050_003_011, "éç¥å¾çè¶ è¿ä¼ä¸å¾®ä¿¡ 10MB éå¶ï¼è¯·å缩åéæ°ä¸ä¼ "); ErrorCode NOTICE_IMAGE_COUNT_EXCEEDED = new ErrorCode(1_050_003_012, "å¾çéç¥æå¤ä¸ä¼ 9 å¼ å¾ç"); ErrorCode NOTICE_FILE_COUNT_EXCEEDED = new ErrorCode(1_050_003_013, "éä»¶éç¥æå¤ä¸ä¼ 5 个éä»¶"); ErrorCode NOTICE_VIDEO_REQUIRED = new ErrorCode(1_050_003_014, "è§é¢éç¥å¿ é¡»ä¸ä¼ 1 个è§é¢ï¼è¯·ä¸ä¼ å忍é"); ErrorCode NOTICE_VIDEO_ONLY = new ErrorCode(1_050_003_015, "è§é¢éç¥åªè½ä¸ä¼ è§é¢æä»¶ï¼å¦ mp4ï¼ï¼è¯·éæ°éæ©"); ErrorCode NOTICE_VIDEO_TOO_LARGE = new ErrorCode(1_050_003_016, "éç¥è§é¢è¶ è¿ä¼ä¸å¾®ä¿¡ 10MB éå¶ï¼è¯·å缩åéæ°ä¸ä¼ "); ErrorCode NOTICE_MARKDOWN_CONTENT_REQUIRED = new ErrorCode(1_050_003_017, "Markdown éç¥çæ£æä¸è½ä¸ºç©ºï¼è¯·å¡«åéç¥å 容"); ErrorCode NOTICE_MARKDOWN_TOO_LONG = new ErrorCode(1_050_003_018, "Markdown éç¥æ£æè¶ è¿ä¼ä¸å¾®ä¿¡ 2048 åèéå¶ï¼å½å {} åèï¼ï¼è¯·ç²¾ç®å忍é"); ErrorCode NOTICE_ARTICLE_REQUIRED = new ErrorCode(1_050_003_019, "å¤å¾ææ¶æ¯è³å°éè¦ 1 ç¯å¾æï¼è¯·æ·»å å忍é"); ErrorCode NOTICE_ARTICLE_TITLE_REQUIRED = new ErrorCode(1_050_003_020, "第 {} ç¯å¾æçæ é¢ä¸è½ä¸ºç©ºï¼è¯·å¡«åæ é¢"); ErrorCode NOTICE_ARTICLE_URL_REQUIRED = new ErrorCode(1_050_003_021, "第 {} ç¯å¾æçè·³è½¬é¾æ¥ä¸è½ä¸ºç©ºï¼è¯·å¡«å龿¥å°å"); ErrorCode NOTICE_ARTICLE_COUNT_EXCEEDED = new ErrorCode(1_050_003_022, "å¤å¾ææ¶æ¯æå¤æ·»å 8 ç¯å¾æï¼è¯·å é¤å¤ä½æ¡ç®"); // ========== ä¼ä¸å¾®ä¿¡éè®¯å½ 1-050-004-000 ========== ErrorCode WXCP_API_ERROR = new ErrorCode(1_050_004_000, "è°ç¨ä¼ä¸å¾®ä¿¡æ¥å£å¤±è´¥ï¼errcode={}ï¼errmsg={}"); yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/enums/notice/WxCpNoticeTypeEnum.java
@@ -18,6 +18,10 @@ CARD(2, "徿å¡çï¼å¡«æ¥/龿¥ï¼"), FILE(3, "æä»¶éç¥"), NEWS(4, "å¾ææ¶æ¯"), IMAGE(5, "å¾çéç¥"), VIDEO(6, "è§é¢éç¥"), MARKDOWN(7, "Markdown éç¥"), MULTI_NEWS(8, "å¤å¾ææ¶æ¯"), ; public static final Integer[] ARRAYS = Arrays.stream(values()) yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/notice/WxCpNoticeService.java
@@ -1,12 +1,16 @@ package cn.iocoder.yudao.module.wxcp.service.notice; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; 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.WxCpNoticeArticleDO; import cn.iocoder.yudao.module.wxcp.dal.dataobject.notice.WxCpNoticeDO; import jakarta.validation.Valid; import java.util.List; /** * ä¼ä¸å¾®ä¿¡éç¥ Service æ¥å£ @@ -54,6 +58,46 @@ PageResult<WxCpNoticeDO> getNoticePage(@Valid WxCpNoticePageReqVO pageReqVO); /** * è·å¾å¾çéç¥çå¾çå表 * * @param noticeId éç¥ç¼å· * @return å¾çå表ï¼å«é¢è§å°åï¼ */ List<SystemStorageBlobRespVO> getNoticeImageAttachments(Long noticeId); /** * è·å¾éä»¶éç¥çéä»¶å表 * * @param noticeId éç¥ç¼å· * @return éä»¶å表ï¼å«é¢è§/ä¸è½½å°åï¼ */ List<SystemStorageBlobRespVO> getNoticeFileAttachments(Long noticeId); /** * è·å¾è§é¢éç¥çè§é¢å表 * * @param noticeId éç¥ç¼å· * @return è§é¢å表ï¼å«é¢è§å°åï¼ */ List<SystemStorageBlobRespVO> getNoticeVideoAttachments(Long noticeId); /** * è·å¾å¤å¾ææ¶æ¯ç徿å表 * * @param noticeId éç¥ç¼å· * @return 徿å表 */ List<WxCpNoticeArticleDO> getNoticeArticles(Long noticeId); /** * è·å¾å¤å¾ææ¡ç®çå°é¢éä»¶ * * @param articleId 徿æ¡ç®ç¼å· * @return å°é¢éä»¶åè¡¨ï¼æå¤ 1 å¼ ï¼ */ List<SystemStorageBlobRespVO> getNoticeArticleCoverAttachments(Long articleId); /** * åå¸éç¥ï¼è稿 -> å·²åå¸ï¼ * * @param id éç¥ç¼å· @@ -75,4 +119,12 @@ */ WxCpPushResultVO pushNotice(@Valid WxCpPushReqVO pushReqVO); /** * æ¼è£ éç¥é¢è§é¡µå°åï¼æåå¨ä¼ä¸å¾®ä¿¡å æå¼å坿¥çå¾çä¸éä»¶ * * @param id éç¥ç¼å· * @return é¢è§é¡µå°åï¼æªé ç½®å¤ç½å°ååç¼æ¶è¿å null */ String buildPreviewUrl(Long id); } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/notice/WxCpNoticeServiceImpl.java
@@ -1,28 +1,44 @@ package cn.iocoder.yudao.module.wxcp.service.notice; import cn.hutool.core.collection.CollUtil; 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.system.controller.admin.storage.vo.SystemStorageAttachmentListReqVO; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentSaveReqVO; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.service.storage.SystemStorageAttachmentService; import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService; import cn.iocoder.yudao.module.wxcp.controller.admin.notice.vo.WxCpNoticeArticleSaveReqVO; 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.WxCpNoticeArticleDO; import cn.iocoder.yudao.module.wxcp.dal.dataobject.notice.WxCpNoticeDO; import cn.iocoder.yudao.module.wxcp.dal.mysql.notice.WxCpNoticeArticleMapper; 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.framework.config.WxCpProperties; 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 com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.cp.bean.article.NewArticle; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.io.File; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.wxcp.enums.ErrorCodeConstants.*; @@ -43,15 +59,78 @@ */ private static final int MAX_PUSH_CONTENT_LENGTH = 600; /** * å¾çéç¥çå¾çå ³èè®°å½ç±»åï¼å¯¹åº system_storage_attachment.record_type */ public static final String NOTICE_IMAGE_RECORD_TYPE = "wxcp_notice_image"; /** * éä»¶éç¥çéä»¶å ³èè®°å½ç±»åï¼å¯¹åº system_storage_attachment.record_type */ public static final String NOTICE_FILE_RECORD_TYPE = "wxcp_notice_file"; /** * è§é¢éç¥çè§é¢å ³èè®°å½ç±»åï¼å¯¹åº system_storage_attachment.record_type */ public static final String NOTICE_VIDEO_RECORD_TYPE = "wxcp_notice_video"; /** * å¤å¾æå°é¢å¾çå ³èè®°å½ç±»åï¼record_id ä¸ºå¾ææ¡ç®ç¼å· */ public static final String NOTICE_ARTICLE_COVER_RECORD_TYPE = "wxcp_notice_article_cover"; /** * éä»¶ç¨éï¼ä¸å ¶å®æ¨¡åä¿æä¸è´ */ private static final String ATTACHMENT_APPLICATION = "file"; /** * ä¼ä¸å¾®ä¿¡æä»¶æ¶æ¯ä¸é 20MB */ private static final long MAX_FILE_SIZE = 20L * 1024L * 1024L; /** * ä¼ä¸å¾®ä¿¡å¾çæ¶æ¯ä¸é 10MB */ private static final long MAX_IMAGE_SIZE = 10L * 1024L * 1024L; /** * ä¼ä¸å¾®ä¿¡è§é¢æ¶æ¯ä¸é 10MB */ private static final long MAX_VIDEO_SIZE = 10L * 1024L * 1024L; /** * ä¼ä¸å¾®ä¿¡ Markdown æ¶æ¯ä¸é 2048 åè */ private static final int MAX_MARKDOWN_BYTES = 2048; /** * 䏿¬¡æ¨éçå¾çæ°éä¸é */ private static final int MAX_IMAGE_COUNT = 9; /** * 䏿¬¡æ¨éçéä»¶æ°éä¸é */ private static final int MAX_FILE_COUNT = 5; /** * å¤å¾ææ¶æ¯æå¤ç¯æ° */ private static final int MAX_ARTICLE_COUNT = 8; @Resource private WxCpNoticeMapper wxcpNoticeMapper; @Resource private WxCpNoticeArticleMapper wxcpNoticeArticleMapper; @Resource private WxCpMessagePushService messagePushService; @Resource private SystemStorageBlobService storageBlobService; /** ä¼ä¸å¾®ä¿¡æä»¶æ¶æ¯ä¸é 20MB */ private static final long MAX_FILE_SIZE = 20L * 1024L * 1024L; @Resource private SystemStorageAttachmentService storageAttachmentService; @Resource private WxCpProperties wxCpProperties; @Override public Long createNotice(WxCpNoticeSaveReqVO createReqVO) { @@ -66,6 +145,14 @@ notice.setPushScope(WxCpPushScopeEnum.ALL.getScope()); } wxcpNoticeMapper.insert(notice); // ç´ æç»ä¸èµ°éä»¶ä¸é´è¡¨ï¼æ°å»ºæ¶ null ä¸ç©ºéåé½è¡¨ç¤ºæ²¡æç´ æ bindAttachments(NOTICE_IMAGE_RECORD_TYPE, notice.getId(), createReqVO.getImageBlobIds()); List<Long> fileBlobIds = resolveFileBlobIds(createReqVO); bindAttachments(NOTICE_FILE_RECORD_TYPE, notice.getId(), fileBlobIds); bindAttachments(NOTICE_VIDEO_RECORD_TYPE, notice.getId(), createReqVO.getVideoBlobIds()); syncFileSnapshot(notice.getId(), fileBlobIds, createReqVO.getFileName()); saveArticles(notice.getId(), createReqVO.getArticleList()); return notice.getId(); } @@ -81,12 +168,37 @@ updateObj.setPushStatus(null); updateObj.setPushTime(null); wxcpNoticeMapper.updateById(updateObj); // ç´ æå表 null = 䏿¹å¨ï¼[] = æ¸ ç©ºï¼è¿æ ·åªæ¹æ 颿¶ä¸ä¼è¯¯å å·²ä¸ä¼ çç´ æ if (updateReqVO.getImageBlobIds() != null) { bindAttachments(NOTICE_IMAGE_RECORD_TYPE, notice.getId(), updateReqVO.getImageBlobIds()); } if (updateReqVO.getVideoBlobIds() != null) { bindAttachments(NOTICE_VIDEO_RECORD_TYPE, notice.getId(), updateReqVO.getVideoBlobIds()); } List<Long> fileBlobIds = resolveFileBlobIds(updateReqVO); if (fileBlobIds != null) { bindAttachments(NOTICE_FILE_RECORD_TYPE, notice.getId(), fileBlobIds); syncFileSnapshot(notice.getId(), fileBlobIds, updateReqVO.getFileName()); } if (updateReqVO.getArticleList() != null) { saveArticles(notice.getId(), updateReqVO.getArticleList()); } } @Override public void deleteNotice(Long id) { validateNoticeExists(id); wxcpNoticeMapper.deleteById(id); storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(NOTICE_IMAGE_RECORD_TYPE, id); storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(NOTICE_FILE_RECORD_TYPE, id); storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(NOTICE_VIDEO_RECORD_TYPE, id); // 徿æ¡ç®åå ¶å°é¢éä»¶ä¸èµ·æ¸ ç for (WxCpNoticeArticleDO article : wxcpNoticeArticleMapper.selectListByNoticeId(id)) { storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId( NOTICE_ARTICLE_COVER_RECORD_TYPE, article.getId()); } wxcpNoticeArticleMapper.deleteByNoticeId(id); } @Override @@ -97,6 +209,31 @@ @Override public PageResult<WxCpNoticeDO> getNoticePage(WxCpNoticePageReqVO pageReqVO) { return wxcpNoticeMapper.selectPage(pageReqVO); } @Override public List<SystemStorageBlobRespVO> getNoticeImageAttachments(Long noticeId) { return listAttachments(NOTICE_IMAGE_RECORD_TYPE, noticeId); } @Override public List<SystemStorageBlobRespVO> getNoticeFileAttachments(Long noticeId) { return listAttachments(NOTICE_FILE_RECORD_TYPE, noticeId); } @Override public List<SystemStorageBlobRespVO> getNoticeVideoAttachments(Long noticeId) { return listAttachments(NOTICE_VIDEO_RECORD_TYPE, noticeId); } @Override public List<WxCpNoticeArticleDO> getNoticeArticles(Long noticeId) { return wxcpNoticeArticleMapper.selectListByNoticeId(noticeId); } @Override public List<SystemStorageBlobRespVO> getNoticeArticleCoverAttachments(Long articleId) { return listAttachments(NOTICE_ARTICLE_COVER_RECORD_TYPE, articleId); } @Override @@ -141,8 +278,20 @@ 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()); // æ¬æ¬¡æ¨éçç´ æï¼è¯·æ±éä¼ äºå°±è¦çï¼æ²¡ä¼ åæ²¿ç¨éç¥ä¸å·²ä¿åç List<Long> imageBlobIds = pushReqVO.getImageBlobIds() != null ? pushReqVO.getImageBlobIds() : toBlobIds(getNoticeImageAttachments(notice.getId())); List<Long> fileBlobIds = pushReqVO.getFileBlobIds() != null ? pushReqVO.getFileBlobIds() : toBlobIds(getNoticeFileAttachments(notice.getId())); List<Long> videoBlobIds = pushReqVO.getVideoBlobIds() != null ? pushReqVO.getVideoBlobIds() : toBlobIds(getNoticeVideoAttachments(notice.getId())); List<WxCpNoticeArticleSaveReqVO> articles = pushReqVO.getArticleList() != null ? pushReqVO.getArticleList() : toArticleReqList(getNoticeArticles(notice.getId())); if (CollUtil.isEmpty(fileBlobIds) && notice.getFileBlobId() != null && notice.getFileBlobId() > 0) { // å ¼å®¹æ§æ°æ®ï¼åå²ä¸æä»¶éç¥åªåäºä¸ä¸ª fileBlobId fileBlobIds = List.of(notice.getFileBlobId()); } // ææ¬æ¬¡éæ©çæ¨¡æ¿ç±»åæ ¡éªå¿ 填项 if (Objects.equals(noticeType, WxCpNoticeTypeEnum.TEXT.getType()) @@ -154,8 +303,29 @@ && StrUtil.isBlank(linkUrl)) { throw exception(NOTICE_LINK_URL_REQUIRED); } if (Objects.equals(noticeType, WxCpNoticeTypeEnum.FILE.getType()) && fileBlobId == null) { if (Objects.equals(noticeType, WxCpNoticeTypeEnum.FILE.getType()) && CollUtil.isEmpty(fileBlobIds)) { throw exception(NOTICE_FILE_REQUIRED); } if (Objects.equals(noticeType, WxCpNoticeTypeEnum.IMAGE.getType()) && CollUtil.isEmpty(imageBlobIds)) { throw exception(NOTICE_IMAGE_REQUIRED); } if (Objects.equals(noticeType, WxCpNoticeTypeEnum.VIDEO.getType()) && CollUtil.isEmpty(videoBlobIds)) { throw exception(NOTICE_VIDEO_REQUIRED); } if (Objects.equals(noticeType, WxCpNoticeTypeEnum.MARKDOWN.getType())) { validateMarkdownContent(buildMarkdownContent(notice)); } if (Objects.equals(noticeType, WxCpNoticeTypeEnum.MULTI_NEWS.getType())) { validateArticles(articles); } if (imageBlobIds.size() > MAX_IMAGE_COUNT) { throw exception(NOTICE_IMAGE_COUNT_EXCEEDED); } if (fileBlobIds.size() > MAX_FILE_COUNT) { throw exception(NOTICE_FILE_COUNT_EXCEEDED); } WxCpPushResultVO result; @@ -164,11 +334,19 @@ 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); result = pushFileNotice(fileBlobIds, 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 if (Objects.equals(noticeType, WxCpNoticeTypeEnum.IMAGE.getType())) { result = pushImageNotice(imageBlobIds, notice, scope, target); } else if (Objects.equals(noticeType, WxCpNoticeTypeEnum.VIDEO.getType())) { result = pushVideoNotice(videoBlobIds.get(0), notice, scope, target); } else if (Objects.equals(noticeType, WxCpNoticeTypeEnum.MARKDOWN.getType())) { result = messagePushService.pushMarkdown(buildMarkdownContent(notice), scope, target); } else if (Objects.equals(noticeType, WxCpNoticeTypeEnum.MULTI_NEWS.getType())) { result = pushMultiNewsNotice(articles, scope, target); } else { result = messagePushService.pushText(buildPushContent(notice), scope, target); } @@ -186,47 +364,150 @@ } /** * æä»¶éç¥ï¼ä»ç³»ç»éä»¶ä¸å¿åæä»¶ï¼äº¤ç»æ¨éæå¡ä¸ä¼ ä¼ä¸å¾®ä¿¡å¹¶åé * å¾çéç¥ï¼å å䏿¡æå说æï¼ææ£ææ¶ï¼ï¼åéå¼ åéå¾ç */ private WxCpPushResultVO pushFileNotice(Long fileBlobId, String fileName, WxCpNoticeDO notice, Integer scope, String target) { if (fileBlobId == null) { throw exception(NOTICE_FILE_REQUIRED); private WxCpPushResultVO pushImageNotice(List<Long> imageBlobIds, WxCpNoticeDO notice, Integer scope, String target) { List<WxCpPushResultVO> results = new ArrayList<>(); if (StrUtil.isNotBlank(notice.getContent())) { // æå失败ä¸å½±ååç»å¾çåéï¼æç»ç± aggregatePushResults æ±æ» results.add(messagePushService.pushText(buildPushContent(notice), scope, target)); } SystemStorageBlobDO blob = storageBlobService.getStorageBlob(fileBlobId); for (Long blobId : imageBlobIds) { SystemStorageBlobDO blob = storageBlobService.getStorageBlob(blobId); if (blob == null) { throw exception(NOTICE_FILE_NOT_EXISTS); } if (StrUtil.isNotBlank(blob.getContentType()) && !StrUtil.startWithIgnoreCase(blob.getContentType(), "image/")) { throw exception(NOTICE_IMAGE_ONLY); } if (blob.getByteSize() != null && blob.getByteSize() > MAX_IMAGE_SIZE) { throw exception(NOTICE_IMAGE_TOO_LARGE); } File file = storageBlobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey()); if (file == null || !file.exists()) { throw exception(NOTICE_FILE_NOT_EXISTS); } results.add(messagePushService.pushImage(file, scope, target)); } // å¾çæ¶æ¯æ¬èº«ä¸å¯ç¹å»ï¼åè¡¥ä¸å¼ å¡çæåé¢è§é¡µï¼æåå¯å¨é¡µå éå¼ æ¾å¤§æ¥ç appendPreviewCard(results, notice, "æ¥çå ¨é¨å¾ç", scope, target); return aggregatePushResults(results, "å¾ç"); } /** * éä»¶éç¥ï¼å å䏿¡æå说æï¼ææ£ææ¶ï¼ï¼åé个åéæä»¶ */ private WxCpPushResultVO pushFileNotice(List<Long> fileBlobIds, WxCpNoticeDO notice, Integer scope, String target) { List<WxCpPushResultVO> results = new ArrayList<>(); if (StrUtil.isNotBlank(notice.getContent())) { results.add(messagePushService.pushText(buildPushContent(notice), scope, target)); } for (Long blobId : fileBlobIds) { SystemStorageBlobDO blob = storageBlobService.getStorageBlob(blobId); 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(blob.getOriginalFilename(), blob.getUidFilename()); results.add(messagePushService.pushFile(file, actualFileName, null, scope, target)); } // æä»¶æ¶æ¯åªè½ä¸è½½å个éä»¶ï¼åè¡¥ä¸å¼ å¡çæåé¢è§é¡µï¼æåå¯å¨é¡µå çå°å ¨é¨éä»¶ appendPreviewCard(results, notice, "æ¥çå ¨é¨éä»¶", scope, target); return aggregatePushResults(results, "éä»¶"); } /** * è§é¢éç¥ï¼åéè§é¢æ¶æ¯ï¼å¹¶è¡¥ä¸å¼ å¡çæåé¢è§é¡µ */ private WxCpPushResultVO pushVideoNotice(Long videoBlobId, WxCpNoticeDO notice, Integer scope, String target) { SystemStorageBlobDO blob = storageBlobService.getStorageBlob(videoBlobId); if (blob == null) { throw exception(NOTICE_FILE_NOT_EXISTS); } if (blob.getByteSize() != null && blob.getByteSize() > MAX_FILE_SIZE) { throw exception(NOTICE_FILE_TOO_LARGE); if (StrUtil.isNotBlank(blob.getContentType()) && !StrUtil.startWithIgnoreCase(blob.getContentType(), "video/")) { throw exception(NOTICE_VIDEO_ONLY); } if (blob.getByteSize() != null && blob.getByteSize() > MAX_VIDEO_SIZE) { throw exception(NOTICE_VIDEO_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); List<WxCpPushResultVO> results = new ArrayList<>(); String description = StrUtil.blankToDefault(notice.getSummary(), notice.getContent()); if (description.length() > 300) { description = description.substring(0, 300) + "â¦â¦"; } results.add(messagePushService.pushVideo(file, notice.getTitle(), description, scope, target)); appendPreviewCard(results, notice, "æ¥çéç¥è¯¦æ ", scope, target); return aggregatePushResults(results, "è§é¢"); } /** * ææ¨¡æ¿ç±»åæ ¡éªå¿ 填项 * å¤å¾ææ¶æ¯ï¼ä¸æ¬¡æ¶æ¯åéå¤ç¯å¾æ */ 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); private WxCpPushResultVO pushMultiNewsNotice(List<WxCpNoticeArticleSaveReqVO> articles, Integer scope, String target) { List<NewArticle> newsList = new ArrayList<>(articles.size()); for (WxCpNoticeArticleSaveReqVO article : articles) { newsList.add(NewArticle.builder() .title(StrUtil.trim(article.getTitle())) .description(StrUtil.blankToDefault(article.getDescription(), "")) .url(StrUtil.trim(article.getLinkUrl())) .picUrl(resolveArticleCoverUrl(article)) .build()); } if ((Objects.equals(type, WxCpNoticeTypeEnum.CARD.getType()) || Objects.equals(type, WxCpNoticeTypeEnum.NEWS.getType())) && StrUtil.isBlank(reqVO.getLinkUrl())) { throw exception(NOTICE_LINK_URL_REQUIRED); return messagePushService.pushMultiNews(newsList, scope, target); } /** * å¤å¾æå°é¢å°åï¼å徿æ¡ç®ç»å®çå°é¢éä»¶ï¼éå¡«ï¼ */ private String resolveArticleCoverUrl(WxCpNoticeArticleSaveReqVO article) { Long articleId = article.getId(); if (articleId == null) { // æ¨éæ¶ä¸´æ¶æ·»å ãæªè½åºçå¾ææ²¡æéä»¶å ³ç³»ï¼ç´æ¥å¿½ç¥å°é¢ return null; } if (Objects.equals(type, WxCpNoticeTypeEnum.FILE.getType()) && reqVO.getFileBlobId() == null) { throw exception(NOTICE_FILE_REQUIRED); List<SystemStorageBlobRespVO> covers = listAttachments(NOTICE_ARTICLE_COVER_RECORD_TYPE, articleId); if (CollUtil.isEmpty(covers)) { return null; } return StrUtil.blankToDefault(covers.get(0).getPreviewURL(), covers.get(0).getUrl()); } /** * 夿¡æ¶æ¯åå¹¶ç»æï¼ä»»ä¸æ¡å¤±è´¥å³è§ä¸ºæ´ä½å¤±è´¥ï¼å¹¶æå¤±è´¥åå æ±æ»ç»å端 */ private WxCpPushResultVO aggregatePushResults(List<WxCpPushResultVO> results, String label) { if (CollUtil.isEmpty(results)) { return new WxCpPushResultVO(false, null, label + "æ¨éå 容为空", null, null); } List<WxCpPushResultVO> failures = results.stream() .filter(r -> !Boolean.TRUE.equals(r.getSuccess())) .toList(); if (failures.isEmpty()) { return new WxCpPushResultVO(true, 0, "æ¨éæå", null, null); } String message = failures.stream() .map(WxCpPushResultVO::getMessage) .filter(StrUtil::isNotBlank) .distinct() .collect(Collectors.joining("ï¼")); return new WxCpPushResultVO(false, failures.get(0).getErrCode(), StrUtil.blankToDefault(message, label + "æ¨é失败"), null, null); } /** @@ -242,6 +523,229 @@ return content; } /** * Markdown æ£æï¼æéç¥æ é¢ä½ä¸ºä¸çº§æ 颿¼å¨æ£æåï¼ä¾¿äºæå第ä¸ç¼çå°ä¸»é¢ */ private String buildMarkdownContent(WxCpNoticeDO notice) { String body = StrUtil.blankToDefault(notice.getContent(), ""); if (StrUtil.isBlank(body)) { return "## " + notice.getTitle(); } if (body.startsWith("#")) { return body; } return "## " + notice.getTitle() + "\n" + body; } /** * æ ¡éª Markdown å 容æ¯å¦è¶ åºä¼ä¸å¾®ä¿¡ 2048 åèéå¶ */ private void validateMarkdownContent(String content) { if (StrUtil.isBlank(content)) { throw exception(NOTICE_MARKDOWN_CONTENT_REQUIRED); } int bytes = content.getBytes(StandardCharsets.UTF_8).length; if (bytes > MAX_MARKDOWN_BYTES) { throw exception(NOTICE_MARKDOWN_TOO_LONG, bytes); } } /** * æ ¡éªå¤å¾ææ¶æ¯çæ¡ç® */ private void validateArticles(List<WxCpNoticeArticleSaveReqVO> articles) { if (CollUtil.isEmpty(articles)) { throw exception(NOTICE_ARTICLE_REQUIRED); } if (articles.size() > MAX_ARTICLE_COUNT) { throw exception(NOTICE_ARTICLE_COUNT_EXCEEDED); } int index = 1; for (WxCpNoticeArticleSaveReqVO article : articles) { if (article == null || StrUtil.isBlank(article.getTitle())) { throw exception(NOTICE_ARTICLE_TITLE_REQUIRED, index); } if (StrUtil.isBlank(article.getLinkUrl())) { throw exception(NOTICE_ARTICLE_URL_REQUIRED, index); } index++; } } // ==================== éä»¶ä¸å¾æ ==================== /** * ç»å®å¾ç/éä»¶/è§é¢å°éç¥ * * èµ° system 模åçéä»¶ä¸é´è¡¨ï¼ä¸å¡è¡¨ä¸å æä»¶å°ååãä¸èªå»º upload æ¥å£ã * ä¼ ç©ºéåè¡¨ç¤ºæ¸ ç©ºå ³èï¼åªå ä¸é´è¡¨è®°å½ï¼ä¸å æä»¶æ¬èº«ï¼ã */ private void bindAttachments(String recordType, Long recordId, List<Long> blobIds) { SystemStorageAttachmentSaveReqVO bindReqVO = new SystemStorageAttachmentSaveReqVO(); bindReqVO.setApplication(ATTACHMENT_APPLICATION); bindReqVO.setRecordType(recordType); bindReqVO.setRecordId(recordId); bindReqVO.setBlobItems(toBlobItems(blobIds)); storageAttachmentService.bindAttachments(bindReqVO); } private List<SystemStorageAttachmentSaveReqVO.BlobItem> toBlobItems(List<Long> blobIds) { if (CollUtil.isEmpty(blobIds)) { return Collections.emptyList(); } return blobIds.stream() .filter(Objects::nonNull) .distinct() .map(blobId -> { SystemStorageAttachmentSaveReqVO.BlobItem item = new SystemStorageAttachmentSaveReqVO.BlobItem(); item.setBlobId(blobId); return item; }) .toList(); } private List<SystemStorageBlobRespVO> listAttachments(String recordType, Long recordId) { SystemStorageAttachmentListReqVO listReqVO = new SystemStorageAttachmentListReqVO(); listReqVO.setApplication(ATTACHMENT_APPLICATION); listReqVO.setRecordType(recordType); listReqVO.setRecordId(recordId); return storageAttachmentService.listAttachments(listReqVO); } private List<Long> toBlobIds(List<SystemStorageBlobRespVO> attachments) { if (CollUtil.isEmpty(attachments)) { return Collections.emptyList(); } // åºå±æ¯ selectByIdsï¼ä¸æ¿è¯ºè¿å顺åºï¼è¿éæ blobId ååºï¼å³ä¸ä¼ å åï¼ä¿è¯æ¨é顺åºç¨³å® return attachments.stream() .map(SystemStorageBlobRespVO::getId) .filter(Objects::nonNull) .sorted() .toList(); } /** * ä¿åå¤å¾ææ¡ç®ï¼å æ¸ ç©ºåæå ¥ï¼ä¿è¯æäº¤é¡ºåºå°±æ¯å±ç¤ºé¡ºåº */ private void saveArticles(Long noticeId, List<WxCpNoticeArticleSaveReqVO> articles) { if (articles == null) { return; } // æ¸ ææ§æ¡ç®åå ¶å°é¢éä»¶å ³ç³» for (WxCpNoticeArticleDO article : wxcpNoticeArticleMapper.selectListByNoticeId(noticeId)) { storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId( NOTICE_ARTICLE_COVER_RECORD_TYPE, article.getId()); } wxcpNoticeArticleMapper.deleteByNoticeId(noticeId); int index = 1; for (WxCpNoticeArticleSaveReqVO item : articles) { if (item == null) { continue; } WxCpNoticeArticleDO article = WxCpNoticeArticleDO.builder() .noticeId(noticeId) .title(StrUtil.trim(item.getTitle())) .description(StrUtil.trim(item.getDescription())) .linkUrl(StrUtil.trim(item.getLinkUrl())) .coverBlobId(item.getCoverBlobId()) .sortNo(item.getSortNo() != null ? item.getSortNo() : index) .build(); wxcpNoticeArticleMapper.insert(article); if (item.getCoverBlobId() != null) { bindAttachments(NOTICE_ARTICLE_COVER_RECORD_TYPE, article.getId(), List.of(item.getCoverBlobId())); } index++; } } private List<WxCpNoticeArticleSaveReqVO> toArticleReqList(List<WxCpNoticeArticleDO> articles) { if (CollUtil.isEmpty(articles)) { return Collections.emptyList(); } return articles.stream().map(article -> { WxCpNoticeArticleSaveReqVO item = new WxCpNoticeArticleSaveReqVO(); item.setId(article.getId()); item.setTitle(article.getTitle()); item.setDescription(article.getDescription()); item.setLinkUrl(article.getLinkUrl()); item.setCoverBlobId(article.getCoverBlobId()); item.setSortNo(article.getSortNo()); return item; }).toList(); } /** * å ¼å®¹æ§çåéä»¶åæ®µ fileBlobIdï¼æªä¼ fileBlobIds æ¶å½ä½åªæä¸ä¸ªéä»¶ */ private List<Long> resolveFileBlobIds(WxCpNoticeSaveReqVO reqVO) { if (reqVO.getFileBlobIds() != null) { return reqVO.getFileBlobIds(); } if (reqVO.getFileBlobId() != null) { return List.of(reqVO.getFileBlobId()); } return null; } /** * æé¦ä¸ªéä»¶ååå°æ§ç fileBlobId/fileName åæ®µï¼ä¾¿äºèå端åè¡¨ç´æ¥å±ç¤º */ private void syncFileSnapshot(Long noticeId, List<Long> fileBlobIds, String fileName) { if (fileBlobIds == null) { return; } if (CollUtil.isEmpty(fileBlobIds)) { // éä»¶æ¸ ç©ºæ¶æ¾å¼ç½®ç©ºï¼updateById ä¼å¿½ç¥ null åæ®µï¼å¿ é¡»ç¨ UpdateWrapper wxcpNoticeMapper.update(null, new LambdaUpdateWrapper<WxCpNoticeDO>() .eq(WxCpNoticeDO::getId, noticeId) .set(WxCpNoticeDO::getFileBlobId, null) .set(WxCpNoticeDO::getFileName, null)); return; } Long firstBlobId = fileBlobIds.get(0); String actualFileName = fileName; if (StrUtil.isBlank(actualFileName)) { SystemStorageBlobDO blob = storageBlobService.getStorageBlob(firstBlobId); actualFileName = blob != null ? blob.getOriginalFilename() : null; } wxcpNoticeMapper.updateById(WxCpNoticeDO.builder() .id(noticeId).fileBlobId(firstBlobId).fileName(actualFileName).build()); } /** * å¾ç/éä»¶/è§é¢æ¨éå追å é¢è§é¡µå¡ç * * æªé ç½®å¤ç½å°ååç¼æ¶ä¸è¿½å ï¼é¿å æ¨éä¸ä¸ªæä¸å¼ç龿¥ã */ private void appendPreviewCard(List<WxCpPushResultVO> results, WxCpNoticeDO notice, String btnText, Integer scope, String target) { String previewUrl = buildPreviewUrl(notice.getId()); if (StrUtil.isBlank(previewUrl)) { log.warn("[appendPreviewCard][æªé ç½® wx.cp.signup-url-prefixï¼è·³è¿é¢è§å¡ç noticeId={}]", notice.getId()); return; } String description = StrUtil.isNotBlank(notice.getContent()) ? StrUtil.blankToDefault(notice.getSummary(), notice.getContent()) : "ç¹å»ä¸æ¹æé®æ¥çéç¥å 容"; if (description.length() > 300) { description = description.substring(0, 300) + "â¦â¦"; } results.add(messagePushService.pushCard(notice.getTitle(), description, previewUrl, btnText, scope, target)); } @Override public String buildPreviewUrl(Long id) { String prefix = wxCpProperties.getSignupUrlPrefix(); if (StrUtil.isBlank(prefix)) { return null; } // 廿ç»å°¾ææ ï¼é¿å æ¼åº //admin-api String base = StrUtil.removeSuffix(prefix.trim(), "/"); return base + "/admin-api/wxcp/notice/preview-page?id=" + id; } private WxCpNoticeDO validateNoticeExists(Long id) { if (id == null) { throw exception(NOTICE_NOT_EXISTS); @@ -253,4 +757,4 @@ return notice; } } } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/push/WxCpMessagePushService.java
@@ -72,4 +72,38 @@ */ WxCpPushResultVO pushImage(java.io.File file, Integer scope, String target); /** * æ¨éè§é¢æ¶æ¯ï¼å æè§é¢ä¸ä¼ å°ä¼ä¸å¾®ä¿¡æ¢å mediaIdï¼ååéè§é¢ * * @param file è§é¢æä»¶ * @param title è§é¢æ é¢ * @param description è§é¢æè¿°ï¼å¯ä¸ºç©º * @param scope æ¨éèå´ * @param target æ¨éç®æ * @return æ¨éç»æ */ WxCpPushResultVO pushVideo(java.io.File file, String title, String description, Integer scope, String target); /** * æ¨é Markdown æ¶æ¯ï¼éå带æ é¢ãå ç²ãå表ãå¼ç¨çæççæ£å¼éç¥ * * @param content Markdown å 容ï¼ä¼ä¸å¾®ä¿¡éå¶ 2048 åèï¼ * @param scope æ¨éèå´ * @param target æ¨éç®æ * @return æ¨éç»æ */ WxCpPushResultVO pushMarkdown(String content, Integer scope, String target); /** * æ¨éå¤å¾ææ¶æ¯ï¼ä¸æ¬¡æ¶æ¯å¸¦å¤ç¯æç« ï¼ * * @param articles 徿å表ï¼ä¼ä¸å¾®ä¿¡éå¶ 1~8 ç¯ * @param scope æ¨éèå´ * @param target æ¨éç®æ * @return æ¨éç»æ */ WxCpPushResultVO pushMultiNews(java.util.List<me.chanjar.weixin.cp.bean.article.NewArticle> articles, Integer scope, String target); } yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/push/WxCpMessagePushServiceImpl.java
@@ -131,6 +131,52 @@ } } @Override public WxCpPushResultVO pushVideo(java.io.File file, String title, String description, Integer scope, String target) { if (file == null || !file.exists()) { return new WxCpPushResultVO(false, null, "è§é¢æä»¶ä¸åå¨ï¼æ æ³æ¨é", null, null); } try { // ä¼ä¸å¾®ä¿¡è§é¢æ¶æ¯è¦æ±å ç¨ video ç±»åä¸ä¼ æ¢å mediaId WxMediaUploadResult upload = wxCpService.getMediaService().upload("video", file); if (upload == null || StrUtil.isBlank(upload.getMediaId())) { return new WxCpPushResultVO(false, null, "è§é¢ä¸ä¼ ä¼ä¸å¾®ä¿¡å¤±è´¥", null, null); } WxCpMessage message = WxCpMessage.VIDEO() .agentId(agentId()) .mediaId(upload.getMediaId()) .title(StrUtil.blankToDefault(title, "éç¥è§é¢")) .description(StrUtil.blankToDefault(description, "")) .build(); return doSend(message, scope, target, "video"); } catch (Exception e) { log.error("[pushVideo][è§é¢ä¸ä¼ ææ¨éå¼å¸¸ file={}]", file.getName(), e); return new WxCpPushResultVO(false, null, "è§é¢æ¨éå¼å¸¸ï¼" + e.getMessage(), null, null); } } @Override public WxCpPushResultVO pushMarkdown(String content, Integer scope, String target) { WxCpMessage message = WxCpMessage.MARKDOWN() .agentId(agentId()) .content(content) .build(); return doSend(message, scope, target, "markdown"); } @Override public WxCpPushResultVO pushMultiNews(java.util.List<NewArticle> articles, Integer scope, String target) { if (articles == null || articles.isEmpty()) { return new WxCpPushResultVO(false, null, "徿å å®¹ä¸ºç©ºï¼æ æ³æ¨é", null, null); } WxCpMessage message = WxCpMessage.NEWS() .agentId(agentId()) .articles(articles) .build(); return doSend(message, scope, target, "multi_news"); } /** * ç»ä¸å¤çæ¥æ¶è è§£æãåéä¸é误å è£ */