1
This commit is contained in:
@@ -0,0 +1,176 @@
|
|||||||
|
package cn.van.business.controller;
|
||||||
|
|
||||||
|
import cn.van.business.service.SocialMediaService;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小红书/抖音内容生成控制器
|
||||||
|
*
|
||||||
|
* @author System
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/jarvis/social-media")
|
||||||
|
public class SocialMediaController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SocialMediaService socialMediaService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取关键词
|
||||||
|
*
|
||||||
|
* POST /jarvis/social-media/extract-keywords
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "productName": "商品名称"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@PostMapping("/extract-keywords")
|
||||||
|
public JSONObject extractKeywords(@RequestBody Map<String, Object> request) {
|
||||||
|
JSONObject response = new JSONObject();
|
||||||
|
try {
|
||||||
|
String productName = (String) request.get("productName");
|
||||||
|
|
||||||
|
if (productName == null || productName.trim().isEmpty()) {
|
||||||
|
response.put("code", 400);
|
||||||
|
response.put("msg", "商品名称不能为空");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = socialMediaService.extractKeywords(productName);
|
||||||
|
|
||||||
|
response.put("code", 200);
|
||||||
|
response.put("msg", "操作成功");
|
||||||
|
response.put("data", result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("提取关键词失败", e);
|
||||||
|
response.put("code", 500);
|
||||||
|
response.put("msg", "提取关键词失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文案
|
||||||
|
*
|
||||||
|
* POST /jarvis/social-media/generate-content
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "productName": "商品名称",
|
||||||
|
* "originalPrice": 499.0,
|
||||||
|
* "finalPrice": 199.0,
|
||||||
|
* "keywords": "关键词1、关键词2",
|
||||||
|
* "style": "xhs" // xhs/douyin/both
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@PostMapping("/generate-content")
|
||||||
|
public JSONObject generateContent(@RequestBody Map<String, Object> request) {
|
||||||
|
JSONObject response = new JSONObject();
|
||||||
|
try {
|
||||||
|
String productName = (String) request.get("productName");
|
||||||
|
Object originalPriceObj = request.get("originalPrice");
|
||||||
|
Object finalPriceObj = request.get("finalPrice");
|
||||||
|
String keywords = (String) request.get("keywords");
|
||||||
|
String style = (String) request.getOrDefault("style", "both");
|
||||||
|
|
||||||
|
if (productName == null || productName.trim().isEmpty()) {
|
||||||
|
response.put("code", 400);
|
||||||
|
response.put("msg", "商品名称不能为空");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Double originalPrice = parseDouble(originalPriceObj);
|
||||||
|
Double finalPrice = parseDouble(finalPriceObj);
|
||||||
|
|
||||||
|
Map<String, Object> result = socialMediaService.generateContent(
|
||||||
|
productName, originalPrice, finalPrice, keywords, style
|
||||||
|
);
|
||||||
|
|
||||||
|
response.put("code", 200);
|
||||||
|
response.put("msg", "操作成功");
|
||||||
|
response.put("data", result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成文案失败", e);
|
||||||
|
response.put("code", 500);
|
||||||
|
response.put("msg", "生成文案失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一键生成完整内容(关键词 + 文案 + 图片)
|
||||||
|
*
|
||||||
|
* POST /jarvis/social-media/generate-complete
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "productImageUrl": "商品主图URL",
|
||||||
|
* "productName": "商品名称",
|
||||||
|
* "originalPrice": 499.0,
|
||||||
|
* "finalPrice": 199.0,
|
||||||
|
* "style": "xhs"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@PostMapping("/generate-complete")
|
||||||
|
public JSONObject generateComplete(@RequestBody Map<String, Object> request) {
|
||||||
|
JSONObject response = new JSONObject();
|
||||||
|
try {
|
||||||
|
String productImageUrl = (String) request.get("productImageUrl");
|
||||||
|
String productName = (String) request.get("productName");
|
||||||
|
Object originalPriceObj = request.get("originalPrice");
|
||||||
|
Object finalPriceObj = request.get("finalPrice");
|
||||||
|
String style = (String) request.getOrDefault("style", "both");
|
||||||
|
|
||||||
|
if (productName == null || productName.trim().isEmpty()) {
|
||||||
|
response.put("code", 400);
|
||||||
|
response.put("msg", "商品名称不能为空");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Double originalPrice = parseDouble(originalPriceObj);
|
||||||
|
Double finalPrice = parseDouble(finalPriceObj);
|
||||||
|
|
||||||
|
Map<String, Object> result = socialMediaService.generateCompleteContent(
|
||||||
|
productImageUrl, productName, originalPrice, finalPrice, style
|
||||||
|
);
|
||||||
|
|
||||||
|
response.put("code", 200);
|
||||||
|
response.put("msg", "操作成功");
|
||||||
|
response.put("data", result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成完整内容失败", e);
|
||||||
|
response.put("code", 500);
|
||||||
|
response.put("msg", "生成完整内容失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析Double值
|
||||||
|
*/
|
||||||
|
private Double parseDouble(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof Double) {
|
||||||
|
return (Double) value;
|
||||||
|
}
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).doubleValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(value.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
282
src/main/java/cn/van/business/service/SocialMediaService.java
Normal file
282
src/main/java/cn/van/business/service/SocialMediaService.java
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package cn.van.business.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.van.business.util.ds.DeepSeekClientUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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 DeepSeekClientUtil deepSeekClientUtil;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MarketingImageService marketingImageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取商品标题关键词
|
||||||
|
*
|
||||||
|
* @param productName 商品名称
|
||||||
|
* @return 关键词列表
|
||||||
|
*/
|
||||||
|
public Map<String, Object> extractKeywords(String productName) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
if (StrUtil.isBlank(productName)) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("error", "商品名称不能为空");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String prompt = String.format(
|
||||||
|
"请从以下商品标题中提取3-5个最核心的关键词,这些关键词要能突出商品的核心卖点和特色。\n" +
|
||||||
|
"要求:\n" +
|
||||||
|
"1. 每个关键词2-4个字\n" +
|
||||||
|
"2. 关键词要能吸引小红书/抖音用户\n" +
|
||||||
|
"3. 用逗号分隔,只返回关键词,不要其他内容\n" +
|
||||||
|
"商品标题:%s",
|
||||||
|
productName
|
||||||
|
);
|
||||||
|
|
||||||
|
String response = deepSeekClientUtil.getDeepSeekResponse(prompt);
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(response)) {
|
||||||
|
// 解析关键词
|
||||||
|
String[] keywords = response.trim()
|
||||||
|
.replaceAll("[,,]", ",")
|
||||||
|
.split(",");
|
||||||
|
|
||||||
|
List<String> 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<String, Object> generateContent(String productName, Double originalPrice,
|
||||||
|
Double finalPrice, String keywords, String style) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
if (StrUtil.isBlank(productName)) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("error", "商品名称不能为空");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建提示词
|
||||||
|
StringBuilder prompt = new StringBuilder();
|
||||||
|
|
||||||
|
if ("xhs".equals(style)) {
|
||||||
|
prompt.append("请为小红书平台生成一篇商品推广文案,要求:\n");
|
||||||
|
prompt.append("1. 风格:真实、种草、有温度\n");
|
||||||
|
prompt.append("2. 开头:用emoji或感叹句吸引注意\n");
|
||||||
|
prompt.append("3. 内容:突出商品亮点、使用场景、性价比\n");
|
||||||
|
prompt.append("4. 结尾:引导行动(如:快冲、闭眼入等)\n");
|
||||||
|
prompt.append("5. 长度:150-300字\n");
|
||||||
|
prompt.append("6. 适当使用emoji和换行\n");
|
||||||
|
} else if ("douyin".equals(style)) {
|
||||||
|
prompt.append("请为抖音平台生成一篇商品推广文案,要求:\n");
|
||||||
|
prompt.append("1. 风格:直接、有冲击力、吸引眼球\n");
|
||||||
|
prompt.append("2. 开头:用疑问句或对比句抓住注意力\n");
|
||||||
|
prompt.append("3. 内容:强调价格优势、限时优惠、稀缺性\n");
|
||||||
|
prompt.append("4. 结尾:制造紧迫感,引导立即行动\n");
|
||||||
|
prompt.append("5. 长度:100-200字\n");
|
||||||
|
prompt.append("6. 使用短句,节奏感强\n");
|
||||||
|
} else {
|
||||||
|
prompt.append("请生成一篇适合小红书和抖音平台的商品推广文案,要求:\n");
|
||||||
|
prompt.append("1. 风格:真实、有吸引力\n");
|
||||||
|
prompt.append("2. 突出商品亮点和价格优势\n");
|
||||||
|
prompt.append("3. 长度:150-250字\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.append("\n商品信息:\n");
|
||||||
|
prompt.append("商品名称:").append(productName).append("\n");
|
||||||
|
if (originalPrice != null && originalPrice > 0) {
|
||||||
|
prompt.append("原价:¥").append(String.format("%.0f", originalPrice)).append("\n");
|
||||||
|
}
|
||||||
|
if (finalPrice != null && finalPrice > 0) {
|
||||||
|
prompt.append("到手价:¥").append(String.format("%.0f", finalPrice)).append("\n");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(keywords)) {
|
||||||
|
prompt.append("关键词:").append(keywords).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.append("\n请直接生成文案内容,不要添加其他说明:");
|
||||||
|
|
||||||
|
String content = deepSeekClientUtil.getDeepSeekResponse(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<String, Object> generateCompleteContent(String productImageUrl, String productName,
|
||||||
|
Double originalPrice, Double finalPrice, String style) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 提取关键词
|
||||||
|
Map<String, Object> keywordResult = extractKeywords(productName);
|
||||||
|
List<String> keywords = (List<String>) keywordResult.get("keywords");
|
||||||
|
String keywordsText = (String) keywordResult.get("keywordsText");
|
||||||
|
|
||||||
|
// 2. 生成文案
|
||||||
|
Map<String, Object> 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<String> simpleExtractKeywords(String productName) {
|
||||||
|
List<String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成简单文案(降级方案)
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user