package com.mosquito.project.service; import com.mosquito.project.config.PosterConfig; import com.mosquito.project.domain.Activity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.net.URL; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.imageio.ImageIO; @Service public class PosterRenderService { private static final Logger log = LoggerFactory.getLogger(PosterRenderService.class); private final PosterConfig posterConfig; private final ShortLinkService shortLinkService; private final Map imageCache = new ConcurrentHashMap<>(); public PosterRenderService(PosterConfig posterConfig, ShortLinkService shortLinkService) { this.posterConfig = posterConfig; this.shortLinkService = shortLinkService; } public byte[] renderPoster(Long activityId, Long userId, String templateName) { PosterConfig.PosterTemplate template = posterConfig.getTemplate(templateName); if (template == null) { template = posterConfig.getTemplate(posterConfig.getDefaultTemplate()); } Activity activity = null; try { // 获取活动信息 // activity = activityService.getActivityById(activityId); } catch (Exception e) { log.debug("Could not load activity: {}", e.getMessage()); } BufferedImage image = new BufferedImage(template.getWidth(), template.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); try { // 绘制背景 if (template.getBackground() != null && !template.getBackground().isBlank()) { Image bgImage = loadImage(template.getBackground()); if (bgImage != null) { g.drawImage(bgImage, 0, 0, template.getWidth(), template.getHeight(), null); } else { g.setColor(Color.decode(template.getBackgroundColor())); g.fillRect(0, 0, template.getWidth(), template.getHeight()); } } else { g.setColor(Color.decode(template.getBackgroundColor())); g.fillRect(0, 0, template.getWidth(), template.getHeight()); } // 绘制元素 for (Map.Entry entry : template.getElements().entrySet()) { PosterConfig.PosterElement element = entry.getValue(); drawElement(g, element, activity, activityId, userId); } } finally { g.dispose(); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { ImageIO.write(image, "PNG", baos); return baos.toByteArray(); } catch (Exception e) { log.error("Failed to generate poster image", e); throw new RuntimeException("Failed to generate poster", e); } } public String renderPosterHtml(Long activityId, Long userId, String templateName) { PosterConfig.PosterTemplate template = posterConfig.getTemplate(templateName); if (template == null) { template = posterConfig.getTemplate(posterConfig.getDefaultTemplate()); } Activity activity = null; try { // activity = activityService.getActivityById(activityId); } catch (Exception e) { log.debug("Could not load activity: {}", e.getMessage()); } String shortUrl = "/r/" + shortLinkService.create( "https://example.com/landing?activityId=" + activityId + "&inviter=" + userId ).getCode(); StringBuilder html = new StringBuilder(); html.append(""); html.append(""); html.append(""); html.append(""); html.append("").append(activity != null ? escapeHtml(activity.getName()) : "分享").append(""); html.append(""); html.append("
"); for (Map.Entry entry : template.getElements().entrySet()) { PosterConfig.PosterElement element = entry.getValue(); String content = resolveContent(element, activity, activityId, userId, shortUrl); if ("text".equals(element.getType())) { html.append("
"); html.append(content); html.append("
"); } else if ("qrcode".equals(element.getType())) { String encodedUrl; try { encodedUrl = java.net.URLEncoder.encode(shortUrl, java.nio.charset.StandardCharsets.UTF_8); } catch (Exception e) { encodedUrl = shortUrl; } html.append("
"); html.append(""); html.append("
"); } else if ("image".equals(element.getType())) { html.append(""); } else if ("button".equals(element.getType())) { html.append(""); html.append(""); html.append(content); html.append(""); } } html.append("
"); return html.toString(); } private void drawElement(Graphics2D g, PosterConfig.PosterElement element, Activity activity, Long activityId, Long userId) { String content = resolveContent(element, activity, activityId, userId, "/r/abc123"); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); if ("text".equals(element.getType())) { g.setColor(Color.decode(element.getColor())); Font font = new Font(element.getFontFamily(), Font.BOLD, parseFontSize(element.getFontSize())); g.setFont(font); FontMetrics fm = g.getFontMetrics(); int textY = element.getY() + fm.getAscent(); g.drawString(content, element.getX(), textY); } else if ("qrcode".equals(element.getType())) { g.setColor(Color.WHITE); g.fillRect(element.getX(), element.getY(), element.getWidth(), element.getHeight()); } else if ("rect".equals(element.getType())) { g.setColor(Color.decode(element.getBackground() != null ? element.getBackground() : "#ffffff")); g.fillRect(element.getX(), element.getY(), element.getWidth(), element.getHeight()); } } private String resolveContent(PosterConfig.PosterElement element, Activity activity, Long activityId, Long userId, String shortUrl) { String raw = element.getContent(); if (raw == null) return ""; return raw.replace("{{activityName}}", activity != null ? activity.getName() : "活动") .replace("{{activityId}}", String.valueOf(activityId)) .replace("{{userId}}", String.valueOf(userId)) .replace("{{shortUrl}}", shortUrl) .replace("{{title}}", element.getContent()); } private Image loadImage(String urlStr) { return imageCache.computeIfAbsent(urlStr, k -> { try { URL url = new URL(posterConfig.getCdnBaseUrl() + "/" + urlStr); return ImageIO.read(url); } catch (Exception e) { log.debug("Failed to load image: {}", urlStr); return null; } }); } private int parseFontSize(String size) { try { return Integer.parseInt(size.replace("px", "")); } catch (Exception e) { return 16; } } private String escapeHtml(String text) { if (text == null) return ""; return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); } }