package cn.van.business.service; import cn.hutool.core.util.StrUtil; import cn.van.business.util.ds.SocialMediaLlmClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 小红书/抖音内容生成服务 * 提供关键词提取、文案生成等功能 * * @author System */ @Slf4j @Service public class SocialMediaService { @Autowired private SocialMediaLlmClient socialMediaLlmClient; @Autowired private MarketingImageService marketingImageService; @Autowired(required = false) private StringRedisTemplate redisTemplate; // Redis Key 前缀 private static final String REDIS_KEY_PREFIX = "social_media:prompt:"; // 默认提示词模板 private static final String DEFAULT_KEYWORDS_PROMPT = "请从以下商品标题中提取3-5个最核心的关键词,这些关键词要能突出商品的核心卖点和特色。\n" + "要求:\n" + "1. 每个关键词2-4个字\n" + "2. 关键词要能吸引小红书/抖音用户\n" + "3. 用逗号分隔,只返回关键词,不要其他内容\n" + "商品标题:%s"; private static final String DEFAULT_CONTENT_PROMPT_XHS = "请为小红书平台生成一篇商品推广文案,要求:\n" + "1. 风格:真实、种草、有温度\n" + "2. 开头:用emoji或感叹句吸引注意\n" + "3. 内容:突出商品亮点、使用场景、性价比\n" + "4. 结尾:引导行动(如:快冲、闭眼入等)\n" + "5. 长度:150-300字\n" + "6. 适当使用emoji和换行\n" + "\n商品信息:\n" + "商品名称:%s\n" + "%s" + // 价格信息 "%s" + // 关键词 "\n请直接生成文案内容,不要添加其他说明:"; private static final String DEFAULT_CONTENT_PROMPT_DOUYIN = "请为抖音平台生成一篇商品推广文案,要求:\n" + "1. 风格:直接、有冲击力、吸引眼球\n" + "2. 开头:用疑问句或对比句抓住注意力\n" + "3. 内容:强调价格优势、限时优惠、稀缺性\n" + "4. 结尾:制造紧迫感,引导立即行动\n" + "5. 长度:100-200字\n" + "6. 使用短句,节奏感强\n" + "\n商品信息:\n" + "商品名称:%s\n" + "%s" + // 价格信息 "%s" + // 关键词 "\n请直接生成文案内容,不要添加其他说明:"; private static final String DEFAULT_CONTENT_PROMPT_BOTH = "请生成一篇适合小红书和抖音平台的商品推广文案,要求:\n" + "1. 风格:真实、有吸引力\n" + "2. 突出商品亮点和价格优势\n" + "3. 长度:150-250字\n" + "\n商品信息:\n" + "商品名称:%s\n" + "%s" + // 价格信息 "%s" + // 关键词 "\n请直接生成文案内容,不要添加其他说明:"; /** * 提取商品标题关键词 * * @param productName 商品名称 * @return 关键词列表 */ public Map extractKeywords(String productName) { Map result = new HashMap<>(); if (StrUtil.isBlank(productName)) { result.put("success", false); result.put("error", "商品名称不能为空"); return result; } try { // 从 Redis 读取提示词模板,如果没有则使用默认模板 String promptTemplate = getPromptTemplate("keywords", DEFAULT_KEYWORDS_PROMPT); String prompt = String.format(promptTemplate, productName); String response = socialMediaLlmClient.getResponse(prompt); if (StrUtil.isNotBlank(response)) { // 解析关键词 String[] keywords = response.trim() .replaceAll("[,,]", ",") .split(","); List keywordList = new ArrayList<>(); for (String keyword : keywords) { String cleaned = keyword.trim(); if (StrUtil.isNotBlank(cleaned) && cleaned.length() <= 6) { keywordList.add(cleaned); } } // 限制数量 if (keywordList.size() > 5) { keywordList = keywordList.subList(0, 5); } result.put("success", true); result.put("keywords", keywordList); result.put("keywordsText", String.join("、", keywordList)); log.info("提取关键词成功: {} -> {}", productName, keywordList); } else { throw new Exception("AI返回结果为空"); } } catch (Exception e) { log.error("提取关键词失败", e); result.put("success", false); result.put("error", "提取关键词失败: " + e.getMessage()); // 降级方案:简单提取 result.put("keywords", simpleExtractKeywords(productName)); result.put("keywordsText", String.join("、", simpleExtractKeywords(productName))); } return result; } /** * 生成小红书/抖音文案 * * @param productName 商品名称 * @param originalPrice 原价 * @param finalPrice 到手价 * @param keywords 关键词(可选) * @param style 文案风格:xhs(小红书)、douyin(抖音)、both(通用) * @return 生成的文案 */ public Map generateContent(String productName, Double originalPrice, Double finalPrice, String keywords, String style) { Map result = new HashMap<>(); if (StrUtil.isBlank(productName)) { result.put("success", false); result.put("error", "商品名称不能为空"); return result; } try { // 构建价格信息 StringBuilder priceInfo = new StringBuilder(); if (originalPrice != null && originalPrice > 0) { priceInfo.append("原价:¥").append(String.format("%.0f", originalPrice)).append("\n"); } if (finalPrice != null && finalPrice > 0) { priceInfo.append("到手价:¥").append(String.format("%.0f", finalPrice)).append("\n"); } // 构建关键词信息 String keywordsInfo = ""; if (StrUtil.isNotBlank(keywords)) { keywordsInfo = "关键词:" + keywords + "\n"; } // 从 Redis 读取提示词模板,如果没有则使用默认模板 String promptTemplate; if ("xhs".equals(style)) { promptTemplate = getPromptTemplate("content:xhs", DEFAULT_CONTENT_PROMPT_XHS); } else if ("douyin".equals(style)) { promptTemplate = getPromptTemplate("content:douyin", DEFAULT_CONTENT_PROMPT_DOUYIN); } else { promptTemplate = getPromptTemplate("content:both", DEFAULT_CONTENT_PROMPT_BOTH); } String prompt = String.format(promptTemplate, productName, priceInfo.toString(), keywordsInfo); String content = socialMediaLlmClient.getResponse(prompt.toString()); if (StrUtil.isNotBlank(content)) { result.put("success", true); result.put("content", content.trim()); log.info("生成文案成功: {}", productName); } else { throw new Exception("AI返回结果为空"); } } catch (Exception e) { log.error("生成文案失败", e); result.put("success", false); result.put("error", "生成文案失败: " + e.getMessage()); // 降级方案:生成简单文案 result.put("content", generateSimpleContent(productName, originalPrice, finalPrice)); } return result; } /** * 一键生成完整内容(关键词 + 文案 + 图片) * * @param productImageUrl 商品主图URL * @param productName 商品名称 * @param originalPrice 原价 * @param finalPrice 到手价 * @param style 文案风格 * @return 完整内容 */ public Map generateCompleteContent(String productImageUrl, String productName, Double originalPrice, Double finalPrice, String style) { Map result = new HashMap<>(); try { // 1. 提取关键词 Map keywordResult = extractKeywords(productName); List keywords = (List) keywordResult.get("keywords"); String keywordsText = (String) keywordResult.get("keywordsText"); // 2. 生成文案 Map contentResult = generateContent( productName, originalPrice, finalPrice, keywordsText, style ); String content = (String) contentResult.get("content"); // 3. 生成营销图片 String imageBase64 = null; if (StrUtil.isNotBlank(productImageUrl)) { try { // 使用提取的关键词作为商品名称显示 String displayName = keywords != null && !keywords.isEmpty() ? keywords.get(0) : productName; imageBase64 = marketingImageService.generateMarketingImage( productImageUrl, originalPrice, finalPrice, displayName ); } catch (Exception e) { log.warn("生成营销图片失败", e); } } result.put("success", true); result.put("keywords", keywords); result.put("keywordsText", keywordsText); result.put("content", content); result.put("imageBase64", imageBase64); } catch (Exception e) { log.error("生成完整内容失败", e); result.put("success", false); result.put("error", "生成完整内容失败: " + e.getMessage()); } return result; } /** * 简单提取关键词(降级方案) */ private List simpleExtractKeywords(String productName) { List keywords = new ArrayList<>(); // 移除常见规格信息 String cleaned = productName .replaceAll("\\s*XL|L|M|S|XXL\\s*", "") .replaceAll("\\s*\\d+/\\d+[A-Z]?\\s*", "") .replaceAll("\\s*【.*?】\\s*", "") .replaceAll("\\s*\\(.*?\\)\\s*", ""); // 提取前几个词 String[] words = cleaned.split("\\s+"); for (String word : words) { if (word.length() >= 2 && word.length() <= 6 && keywords.size() < 5) { keywords.add(word); } } return keywords; } /** * 从 Redis 获取提示词模板,如果没有则返回默认模板 * * @param templateKey 模板键名(如:keywords, content:xhs, content:douyin, content:both) * @param defaultTemplate 默认模板 * @return 提示词模板 */ private String getPromptTemplate(String templateKey, String defaultTemplate) { if (redisTemplate == null) { log.debug("Redis未配置,使用默认模板: {}", templateKey); return defaultTemplate; } try { String redisKey = REDIS_KEY_PREFIX + templateKey; String template = redisTemplate.opsForValue().get(redisKey); if (StrUtil.isNotBlank(template)) { log.debug("从Redis读取模板: {}", templateKey); return template; } else { log.debug("Redis中未找到模板,使用默认模板: {}", templateKey); return defaultTemplate; } } catch (Exception e) { log.warn("读取Redis模板失败,使用默认模板: {}", templateKey, e); return defaultTemplate; } } /** * 生成简单文案(降级方案) */ private String generateSimpleContent(String productName, Double originalPrice, Double finalPrice) { StringBuilder content = new StringBuilder(); content.append("🔥 ").append(productName).append("\n\n"); if (originalPrice != null && finalPrice != null && originalPrice > finalPrice) { content.append("💰 原价:¥").append(String.format("%.0f", originalPrice)).append("\n"); content.append("💸 到手价:¥").append(String.format("%.0f", finalPrice)).append("\n"); double discount = ((originalPrice - finalPrice) / originalPrice) * 100; content.append("✨ 立省:¥").append(String.format("%.0f", originalPrice - finalPrice)) .append("(").append(String.format("%.0f", discount)).append("%)\n\n"); } content.append("💡 超值好物,不容错过!\n"); content.append("🎁 限时优惠,先到先得!"); return content.toString(); } }