Compare commits

..

12 Commits

Author SHA1 Message Date
Leo
a893f3cd61 1 2025-12-05 23:02:02 +08:00
Leo
c2be15e3f5 1 2025-12-05 23:01:41 +08:00
Leo
8445b500ae 1 2025-12-05 22:35:51 +08:00
Leo
ee67d1ae8f Merge branch 'master' of https://git.van333.cn/CC/Jarvis_java 2025-12-04 15:47:42 +08:00
Leo
a20e92d7bf 1 2025-12-04 15:47:39 +08:00
69d1d91f5e 1 2025-12-04 14:48:18 +08:00
Leo
570fcb0b93 1 2025-11-29 23:39:37 +08:00
Leo
7fda3da9ed 1 2025-11-29 22:47:41 +08:00
Leo
e7687c8909 1 2025-11-29 22:35:06 +08:00
Leo
8e12076225 1 2025-11-10 21:49:45 +08:00
Leo
5b48727fb2 1 2025-11-10 18:43:27 +08:00
Leo
bb6c907cda 1 2025-11-10 18:43:18 +08:00
13 changed files with 1571 additions and 24 deletions

View File

@@ -0,0 +1,185 @@
package cn.van.business.controller;
import cn.van.business.model.ApiResponse;
import cn.van.business.service.MarketingImageService;
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.List;
import java.util.Map;
/**
* 营销图片合成控制器
* 提供营销图片生成的HTTP接口
*
* @author System
*/
@Slf4j
@RestController
@RequestMapping("/jarvis/marketing-image")
public class MarketingImageController {
@Autowired
private MarketingImageService marketingImageService;
/**
* 生成单张营销图片
*
* POST /jarvis/marketing-image/generate
*
* 请求体:
* {
* "productImageUrl": "商品主图URL",
* "originalPrice": 499.0,
* "finalPrice": 199.0,
* "productName": "商品名称(可选)"
* }
*
* 返回:
* {
* "code": 200,
* "msg": "操作成功",
* "data": {
* "imageBase64": "data:image/jpg;base64,..."
* }
* }
*/
@PostMapping("/generate")
public JSONObject generateMarketingImage(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String productImageUrl = (String) request.get("productImageUrl");
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String productName = (String) request.get("productName");
if (productImageUrl == null || originalPriceObj == null || finalPriceObj == null) {
response.put("code", 400);
response.put("msg", "缺少必要参数: productImageUrl, originalPrice, finalPrice");
return response;
}
Double originalPrice = parseDouble(originalPriceObj);
Double finalPrice = parseDouble(finalPriceObj);
if (originalPrice == null || finalPrice == null) {
response.put("code", 400);
response.put("msg", "价格参数格式错误");
return response;
}
String base64Image = marketingImageService.generateMarketingImage(
productImageUrl, originalPrice, finalPrice, productName);
Map<String, Object> data = new HashMap<>();
data.put("imageBase64", base64Image);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", data);
} catch (Exception e) {
log.error("生成营销图片失败", e);
response.put("code", 500);
response.put("msg", "生成营销图片失败: " + e.getMessage());
}
return response;
}
/**
* 批量生成营销图片
*
* POST /jarvis/marketing-image/batch-generate
*
* 请求体:
* {
* "requests": [
* {
* "productImageUrl": "商品主图URL1",
* "originalPrice": 499.0,
* "finalPrice": 199.0,
* "productName": "商品名称1可选"
* },
* {
* "productImageUrl": "商品主图URL2",
* "originalPrice": 699.0,
* "finalPrice": 349.0,
* "productName": "商品名称2可选"
* }
* ]
* }
*
* 返回:
* {
* "code": 200,
* "msg": "操作成功",
* "data": {
* "results": [
* {
* "success": true,
* "imageBase64": "data:image/jpg;base64,...",
* "index": 0
* },
* {
* "success": false,
* "error": "错误信息",
* "index": 1
* }
* ],
* "total": 2,
* "successCount": 1,
* "failCount": 1
* }
* }
*/
@PostMapping("/batch-generate")
public JSONObject batchGenerateMarketingImages(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> requests = (List<Map<String, Object>>) request.get("requests");
if (requests == null || requests.isEmpty()) {
response.put("code", 400);
response.put("msg", "请求列表不能为空");
return response;
}
Map<String, Object> result = marketingImageService.batchGenerateMarketingImages(requests);
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;
}
}
}

View File

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

View File

@@ -0,0 +1,234 @@
package cn.van.business.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 小红书/抖音提示词模板配置Controller
*
* @author System
*/
@Slf4j
@RestController
@RequestMapping("/jarvis/social-media/prompt")
public class SocialMediaPromptController {
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
// Redis Key 前缀
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
// 模板键名列表
private static final String[] TEMPLATE_KEYS = {
"keywords",
"content:xhs",
"content:douyin",
"content:both"
};
// 模板说明
private static final Map<String, String> TEMPLATE_DESCRIPTIONS = new HashMap<String, String>() {{
put("keywords", "关键词提取提示词模板\n占位符%s - 商品名称");
put("content:xhs", "小红书文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:douyin", "抖音文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:both", "通用文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
}};
/**
* 获取所有提示词模板
*
* GET /jarvis/social-media/prompt/list
*/
@GetMapping("/list")
public JSONObject listTemplates() {
JSONObject response = new JSONObject();
try {
Map<String, Object> templates = new HashMap<>();
for (String key : TEMPLATE_KEYS) {
Map<String, Object> templateInfo = new HashMap<>();
templateInfo.put("key", key);
templateInfo.put("description", TEMPLATE_DESCRIPTIONS.get(key));
String template = getTemplateFromRedis(key);
templateInfo.put("template", template);
templateInfo.put("isDefault", template == null);
templates.put(key, templateInfo);
}
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", templates);
} catch (Exception e) {
log.error("获取提示词模板列表失败", e);
response.put("code", 500);
response.put("msg", "获取失败: " + e.getMessage());
}
return response;
}
/**
* 获取单个提示词模板
*
* GET /jarvis/social-media/prompt/{key}
*/
@GetMapping("/{key}")
public JSONObject getTemplate(@PathVariable String key) {
JSONObject response = new JSONObject();
try {
if (!isValidKey(key)) {
response.put("code", 400);
response.put("msg", "无效的模板键名");
return response;
}
String template = getTemplateFromRedis(key);
Map<String, Object> data = new HashMap<>();
data.put("key", key);
data.put("description", TEMPLATE_DESCRIPTIONS.get(key));
data.put("template", template);
data.put("isDefault", template == null);
response.put("code", 200);
response.put("msg", "操作成功");
response.put("data", data);
} catch (Exception e) {
log.error("获取提示词模板失败", e);
response.put("code", 500);
response.put("msg", "获取失败: " + e.getMessage());
}
return response;
}
/**
* 保存提示词模板
*
* POST /jarvis/social-media/prompt/save
*
* {
* "key": "keywords",
* "template": "提示词模板内容..."
* }
*/
@PostMapping("/save")
public JSONObject saveTemplate(@RequestBody Map<String, Object> request) {
JSONObject response = new JSONObject();
try {
String key = (String) request.get("key");
String template = (String) request.get("template");
if (!isValidKey(key)) {
response.put("code", 400);
response.put("msg", "无效的模板键名");
return response;
}
if (StrUtil.isBlank(template)) {
response.put("code", 400);
response.put("msg", "模板内容不能为空");
return response;
}
if (redisTemplate == null) {
response.put("code", 500);
response.put("msg", "Redis未配置无法保存模板");
return response;
}
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.opsForValue().set(redisKey, template);
log.info("保存提示词模板成功: {}", key);
response.put("code", 200);
response.put("msg", "保存成功");
} catch (Exception e) {
log.error("保存提示词模板失败", e);
response.put("code", 500);
response.put("msg", "保存失败: " + e.getMessage());
}
return response;
}
/**
* 删除提示词模板(恢复默认)
*
* DELETE /jarvis/social-media/prompt/{key}
*/
@DeleteMapping("/{key}")
public JSONObject deleteTemplate(@PathVariable String key) {
JSONObject response = new JSONObject();
try {
if (!isValidKey(key)) {
response.put("code", 400);
response.put("msg", "无效的模板键名");
return response;
}
if (redisTemplate == null) {
response.put("code", 500);
response.put("msg", "Redis未配置无法删除模板");
return response;
}
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.delete(redisKey);
log.info("删除提示词模板成功: {}", key);
response.put("code", 200);
response.put("msg", "删除成功,已恢复默认模板");
} catch (Exception e) {
log.error("删除提示词模板失败", e);
response.put("code", 500);
response.put("msg", "删除失败: " + e.getMessage());
}
return response;
}
/**
* 从 Redis 获取模板
*/
private String getTemplateFromRedis(String key) {
if (redisTemplate == null) {
return null;
}
try {
String redisKey = REDIS_KEY_PREFIX + key;
return redisTemplate.opsForValue().get(redisKey);
} catch (Exception e) {
log.warn("读取Redis模板失败: {}", key, e);
return null;
}
}
/**
* 验证模板键名是否有效
*/
private boolean isValidKey(String key) {
if (StrUtil.isBlank(key)) {
return false;
}
for (String validKey : TEMPLATE_KEYS) {
if (validKey.equals(key)) {
return true;
}
}
return false;
}
}

View File

@@ -61,6 +61,12 @@ public class SuperAdmin {
@Column(name = "is_active", nullable = false) @Column(name = "is_active", nullable = false)
private Integer isActive = 1; private Integer isActive = 1;
/**
* 接收人企业微信用户ID多个用逗号分隔
*/
@Column(name = "touser", length = 500)
private String touser;
/** /**
* 创建时间 * 创建时间
*/ */

View File

@@ -61,8 +61,9 @@ public class MessageConsumerService implements RocketMQListener<JSONObject> {
Integer msgType = data.getInteger("msgType"); Integer msgType = data.getInteger("msgType");
String fromWxid = data.getString("fromWxid"); String fromWxid = data.getString("fromWxid");
Boolean hiddenTime = data.getBoolean("hiddenTime"); Boolean hiddenTime = data.getBoolean("hiddenTime");
String touser = data.getString("touser"); // 获取接收人参数
wxtsUtil.sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime); wxtsUtil.sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime, touser);

View File

@@ -0,0 +1,433 @@
package cn.van.business.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.van.business.util.ds.DeepSeekClientUtil;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* 营销图片合成服务
* 用于生成小红书等平台的营销对比图
*
* @author System
*/
@Slf4j
@Service
public class MarketingImageService {
@Autowired
private DeepSeekClientUtil deepSeekClientUtil;
// 输出图片尺寸
private static final int OUTPUT_WIDTH = 1080;
private static final int OUTPUT_HEIGHT = 1080;
// 字体配置(支持回退)
private static final String[] FONT_NAMES = {"Microsoft YaHei", "SimHei", "Arial", Font.SANS_SERIF}; // 字体优先级
private static final int ORIGINAL_PRICE_FONT_SIZE = 36; // 官网价字体大小
private static final int FINAL_PRICE_FONT_SIZE = 72; // 到手价字体大小
private static final int PRODUCT_NAME_FONT_SIZE = 32; // 商品名称字体大小
/**
* 获取可用字体
*/
private Font getAvailableFont(int style, int size) {
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
String[] availableFonts = ge.getAvailableFontFamilyNames();
for (String fontName : FONT_NAMES) {
for (String available : availableFonts) {
if (available.equals(fontName)) {
return new Font(fontName, style, size);
}
}
}
// 如果都不可用,使用默认字体
return new Font(Font.SANS_SERIF, style, size);
}
// 颜色配置
private static final Color ORIGINAL_PRICE_COLOR = new Color(153, 153, 153); // 灰色 #999999
private static final Color FINAL_PRICE_COLOR = new Color(255, 0, 0); // 红色 #FF0000
private static final Color PRODUCT_NAME_COLOR = new Color(51, 51, 51); // 深灰色 #333333
private static final Color BACKGROUND_COLOR = Color.WHITE; // 背景色
/**
* 生成营销图片
*
* @param productImageUrl 商品主图URL
* @param originalPrice 官网价
* @param finalPrice 到手价
* @param productName 商品名称可选如果为空则使用AI提取
* @return Base64编码的图片
*/
public String generateMarketingImage(String productImageUrl, Double originalPrice, Double finalPrice, String productName) {
try {
log.info("开始生成营销图片: productImageUrl={}, originalPrice={}, finalPrice={}, productName={}",
productImageUrl, originalPrice, finalPrice, productName);
// 1. 加载商品主图
BufferedImage productImage = loadProductImage(productImageUrl);
if (productImage == null) {
throw new IOException("无法加载商品主图: " + productImageUrl);
}
// 2. 提取商品标题关键部分(如果未提供)
String keyProductName = productName;
if (StrUtil.isBlank(keyProductName)) {
// 如果未提供商品名称,则无法提取,留空
keyProductName = "";
} else {
// 如果提供了完整商品名称,提取关键部分
keyProductName = extractKeyProductName(keyProductName);
}
// 3. 创建画布
BufferedImage canvas = new BufferedImage(OUTPUT_WIDTH, OUTPUT_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = canvas.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// 4. 绘制背景
g2d.setColor(BACKGROUND_COLOR);
g2d.fillRect(0, 0, OUTPUT_WIDTH, OUTPUT_HEIGHT);
// 5. 缩放并绘制商品主图(居中,保持比例)
int productImageSize = 800; // 商品图尺寸
int productImageX = (OUTPUT_WIDTH - productImageSize) / 2;
int productImageY = 80; // 顶部留白
BufferedImage scaledProductImage = Thumbnails.of(productImage)
.size(productImageSize, productImageSize)
.asBufferedImage();
g2d.drawImage(scaledProductImage, productImageX, productImageY, null);
// 6. 绘制商品名称(如果有)
int textStartY = productImageY + productImageSize + 40;
if (StrUtil.isNotBlank(keyProductName)) {
drawProductName(g2d, keyProductName, textStartY);
textStartY += 60; // 增加间距
}
// 7. 绘制官网价(带删除线,在上方)
int originalPriceY = textStartY + 80;
drawOriginalPrice(g2d, originalPrice, originalPriceY);
// 8. 绘制向下箭头
int arrowY = originalPriceY + 60;
drawDownArrow(g2d, arrowY);
// 9. 绘制到手价(大红色,在下方)
int finalPriceY = arrowY + 80;
drawFinalPrice(g2d, finalPrice, finalPriceY);
// 10. 绘制爆炸贴图装饰(右下角)
drawExplosionDecoration(g2d);
g2d.dispose();
// 11. 转换为Base64
String base64Image = imageToBase64(canvas, "jpg");
log.info("营销图片生成成功");
return base64Image;
} catch (Exception e) {
log.error("生成营销图片失败", e);
throw new RuntimeException("生成营销图片失败: " + e.getMessage(), e);
}
}
/**
* 加载商品主图
*/
private BufferedImage loadProductImage(String imageUrl) throws IOException {
try {
byte[] imageData = HttpUtil.downloadBytes(imageUrl);
if (imageData == null || imageData.length == 0) {
throw new IOException("下载图片失败或图片数据为空");
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
return ImageIO.read(bais);
}
} catch (Exception e) {
log.error("加载商品主图失败: {}", imageUrl, e);
throw new IOException("加载商品主图失败: " + e.getMessage(), e);
}
}
/**
* 提取商品标题关键部分使用AI
*
* @param fullProductName 完整商品名称
* @return 提取的关键部分
*/
public String extractKeyProductName(String fullProductName) {
if (StrUtil.isBlank(fullProductName)) {
return "";
}
try {
// 使用DeepSeek提取商品标题关键部分
String prompt = String.format(
"请从以下商品标题中提取最关键的3-8个字作为核心卖点只返回提取的关键词不要其他内容\n%s",
fullProductName
);
String extracted = deepSeekClientUtil.getDeepSeekResponse(prompt);
if (StrUtil.isNotBlank(extracted)) {
// 清理可能的换行和多余空格
extracted = extracted.trim().replaceAll("\\s+", "");
// 限制长度
if (extracted.length() > 12) {
extracted = extracted.substring(0, 12);
}
log.info("提取商品标题关键部分成功: {} -> {}", fullProductName, extracted);
return extracted;
}
} catch (Exception e) {
log.warn("使用AI提取商品标题关键部分失败使用简单截取: {}", fullProductName, e);
}
// 降级方案:简单截取前部分
return simpleExtractKeyName(fullProductName);
}
/**
* 简单提取商品名称关键部分(降级方案)
*/
private String simpleExtractKeyName(String fullName) {
if (StrUtil.isBlank(fullName)) {
return "";
}
// 移除常见的规格信息如XL、175/96A等
String cleaned = fullName
.replaceAll("\\s*XL|L|M|S|XXL\\s*", "")
.replaceAll("\\s*\\d+/\\d+[A-Z]?\\s*", "")
.replaceAll("\\s*【.*?】\\s*", "")
.replaceAll("\\s*\\(.*?\\)\\s*", "");
// 提取前10-15个字符
if (cleaned.length() > 15) {
cleaned = cleaned.substring(0, 15);
}
return cleaned.trim();
}
/**
* 绘制商品名称
*/
private void drawProductName(Graphics2D g2d, String productName, int y) {
Font font = getAvailableFont(Font.BOLD, PRODUCT_NAME_FONT_SIZE);
g2d.setFont(font);
g2d.setColor(PRODUCT_NAME_COLOR);
// 计算文字宽度,如果太长则截断
FontMetrics fm = g2d.getFontMetrics();
String displayName = productName;
int maxWidth = OUTPUT_WIDTH - 80; // 左右各留40px
if (fm.stringWidth(displayName) > maxWidth) {
// 截断并添加省略号
while (fm.stringWidth(displayName + "...") > maxWidth && displayName.length() > 0) {
displayName = displayName.substring(0, displayName.length() - 1);
}
displayName += "...";
}
int textWidth = fm.stringWidth(displayName);
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
g2d.drawString(displayName, x, y);
}
/**
* 绘制官网价(带删除线)
*/
private void drawOriginalPrice(Graphics2D g2d, Double originalPrice, int y) {
Font font = getAvailableFont(Font.BOLD, ORIGINAL_PRICE_FONT_SIZE);
g2d.setFont(font);
g2d.setColor(ORIGINAL_PRICE_COLOR);
String priceText = "官网价:¥" + String.format("%.0f", originalPrice);
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(priceText);
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
// 绘制文字
g2d.drawString(priceText, x, y);
// 绘制删除线
int lineY = y - fm.getAscent() / 2;
g2d.setStroke(new BasicStroke(3.0f)); // 3px粗的删除线
g2d.drawLine(x, lineY, x + textWidth, lineY);
}
/**
* 绘制向下箭头
*/
private void drawDownArrow(Graphics2D g2d, int y) {
int centerX = OUTPUT_WIDTH / 2;
int arrowSize = 40;
g2d.setColor(new Color(200, 200, 200)); // 浅灰色箭头
g2d.setStroke(new BasicStroke(3.0f));
// 绘制竖线
g2d.drawLine(centerX, y, centerX, y + arrowSize);
// 绘制箭头(向下)
int[] xPoints = {centerX, centerX - 15, centerX + 15};
int[] yPoints = {y + arrowSize, y + arrowSize - 20, y + arrowSize - 20};
g2d.fillPolygon(xPoints, yPoints, 3);
}
/**
* 绘制到手价(大红色)
*/
private void drawFinalPrice(Graphics2D g2d, Double finalPrice, int y) {
Font font = getAvailableFont(Font.BOLD, FINAL_PRICE_FONT_SIZE);
g2d.setFont(font);
g2d.setColor(FINAL_PRICE_COLOR);
String priceText = "到手价:¥" + String.format("%.0f", finalPrice);
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(priceText);
int x = (OUTPUT_WIDTH - textWidth) / 2; // 居中
g2d.drawString(priceText, x, y);
}
/**
* 绘制爆炸贴图装饰(右下角)
*/
private void drawExplosionDecoration(Graphics2D g2d) {
// 绘制简单的爆炸形状(星形)
int centerX = OUTPUT_WIDTH - 120;
int centerY = OUTPUT_HEIGHT - 120;
int radius = 50;
g2d.setColor(new Color(255, 200, 0)); // 金黄色
g2d.setStroke(new BasicStroke(4.0f));
// 绘制星形爆炸效果
int points = 8;
int[] xPoints = new int[points * 2];
int[] yPoints = new int[points * 2];
for (int i = 0; i < points * 2; i++) {
double angle = Math.PI * i / points;
int r = (i % 2 == 0) ? radius : radius / 2;
xPoints[i] = (int) (centerX + r * Math.cos(angle));
yPoints[i] = (int) (centerY + r * Math.sin(angle));
}
g2d.fillPolygon(xPoints, yPoints, points * 2);
// 绘制内部圆形
g2d.setColor(new Color(255, 100, 0)); // 橙红色
g2d.fillOval(centerX - radius / 2, centerY - radius / 2, radius, radius);
}
/**
* 将BufferedImage转换为Base64字符串
*/
private String imageToBase64(BufferedImage image, String format) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, format, baos);
byte[] imageBytes = baos.toByteArray();
return "data:image/" + format + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
}
/**
* 批量生成营销图片
*
* @param requests 批量请求列表
* @return 结果列表每个元素包含base64图片
*/
public Map<String, Object> batchGenerateMarketingImages(java.util.List<Map<String, Object>> requests) {
Map<String, Object> result = new HashMap<>();
java.util.List<Map<String, Object>> results = new java.util.ArrayList<>();
int successCount = 0;
int failCount = 0;
for (int i = 0; i < requests.size(); i++) {
Map<String, Object> request = requests.get(i);
Map<String, Object> itemResult = new HashMap<>();
try {
String productImageUrl = (String) request.get("productImageUrl");
Double originalPrice = getDoubleValue(request.get("originalPrice"));
Double finalPrice = getDoubleValue(request.get("finalPrice"));
String productName = (String) request.get("productName");
if (productImageUrl == null || originalPrice == null || finalPrice == null) {
throw new IllegalArgumentException("缺少必要参数: productImageUrl, originalPrice, finalPrice");
}
String base64Image = generateMarketingImage(productImageUrl, originalPrice, finalPrice, productName);
itemResult.put("success", true);
itemResult.put("imageBase64", base64Image);
itemResult.put("index", i);
successCount++;
} catch (Exception e) {
log.error("批量生成第{}张图片失败", i, e);
itemResult.put("success", false);
itemResult.put("error", e.getMessage());
itemResult.put("index", i);
failCount++;
}
results.add(itemResult);
}
result.put("results", results);
result.put("total", requests.size());
result.put("successCount", successCount);
result.put("failCount", failCount);
return result;
}
/**
* 安全获取Double值
*/
private Double getDoubleValue(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;
}
}
}

View File

@@ -0,0 +1,347 @@
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.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 DeepSeekClientUtil deepSeekClientUtil;
@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<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 {
// 从 Redis 读取提示词模板,如果没有则使用默认模板
String promptTemplate = getPromptTemplate("keywords", DEFAULT_KEYWORDS_PROMPT);
String prompt = String.format(promptTemplate, 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 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 = 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;
}
/**
* 从 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();
}
}

View File

@@ -32,7 +32,7 @@ import java.text.SimpleDateFormat;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.regex.Pattern;
import static cn.van.business.util.JDUtil.*; import static cn.van.business.util.JDUtil.*;
@@ -43,8 +43,10 @@ import static cn.van.business.util.JDUtil.*;
@Slf4j @Slf4j
public class JDProductService { public class JDProductService {
private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59"; // 自己的98e21c89ae5610240ec3f5f575f86a59
private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb"; private static final String LPF_APP_KEY_WZ = "34407d6cae6d43eca740370b8e12b01e";
// 自己的3dcb6b23a1104639ac433fd07adb6dfb
private static final String LPF_SECRET_KEY_WZ = "ad4966e1df3348a185fe6b33aa679a69";
private static final String SERVER_URL = "https://api.jd.com/routerjson"; private static final String SERVER_URL = "https://api.jd.com/routerjson";
private static final String ACCESS_TOKEN = ""; private static final String ACCESS_TOKEN = "";
private static final Logger logger = LoggerFactory.getLogger(JDProductService.class); private static final Logger logger = LoggerFactory.getLogger(JDProductService.class);
@@ -84,17 +86,29 @@ public class JDProductService {
for (String url : urls) { for (String url : urls) {
try { try {
String format = dateFormat.format(new Date()); String format = dateFormat.format(new Date());
GoodsQueryResult productInfo = queryProductInfoByUJDUrl(url); String originalUrlInText = url;
String normalizedUrl = normalizeJdUrl(originalUrlInText);
if (normalizedUrl == null) {
log.warn("检测到的链接无法识别为合法京东链接,跳过处理: {}", originalUrlInText);
JSONObject errorObj = new JSONObject();
errorObj.put("url", originalUrlInText);
errorObj.put("error", "链接格式不支持或识别失败");
resultArray.add(errorObj);
continue;
}
GoodsQueryResult productInfo = queryProductInfoByUJDUrl(normalizedUrl);
if (productInfo == null || productInfo.getCode() != 200 || productInfo.getData() == null || productInfo.getData().length == 0) { if (productInfo == null || productInfo.getCode() != 200 || productInfo.getData() == null || productInfo.getData().length == 0) {
JSONObject errorObj = new JSONObject(); JSONObject errorObj = new JSONObject();
errorObj.put("url", url); errorObj.put("url", originalUrlInText);
errorObj.put("error", "链接查询失败"); errorObj.put("error", "链接查询失败");
resultArray.add(errorObj); resultArray.add(errorObj);
continue; continue;
} }
JSONObject productObj = new JSONObject(); JSONObject productObj = new JSONObject();
productObj.put("originalUrl", url); productObj.put("originalUrl", originalUrlInText);
productObj.put("normalizedUrl", normalizedUrl);
// 商品基本信息 // 商品基本信息
productObj.put("materialUrl", productInfo.getData()[0].getMaterialUrl()); productObj.put("materialUrl", productInfo.getData()[0].getMaterialUrl());
@@ -124,22 +138,24 @@ public class JDProductService {
// 生成转链后的短链 // 生成转链后的短链
try { try {
String shortUrl = transfer(url, null); String shortUrl = transfer(normalizedUrl, null);
String effectiveUrl = normalizedUrl;
if (shortUrl != null && !shortUrl.isEmpty()) { if (shortUrl != null && !shortUrl.isEmpty()) {
productObj.put("shortUrl", shortUrl); productObj.put("shortUrl", shortUrl);
productObj.put("transferSuccess", true); productObj.put("transferSuccess", true);
// 将短链替换原始链接,用于后续文案生成 effectiveUrl = shortUrl;
url = shortUrl;
} else { } else {
productObj.put("shortUrl", url); // 如果转链失败,使用链接 productObj.put("shortUrl", normalizedUrl); // 如果转链失败,使用归一化后的链接
productObj.put("transferSuccess", false); productObj.put("transferSuccess", false);
log.warn("转链失败,使用原链接: {}", url); log.warn("转链失败,使用原链接: {}", normalizedUrl);
} }
productObj.put("effectiveUrl", effectiveUrl);
} catch (Exception e) { } catch (Exception e) {
log.error("生成转链时发生异常: {}", url, e); log.error("生成转链时发生异常: {}", normalizedUrl, e);
productObj.put("shortUrl", url); // 转链异常时使用原链接 productObj.put("shortUrl", normalizedUrl); // 转链异常时使用原链接
productObj.put("transferSuccess", false); productObj.put("transferSuccess", false);
productObj.put("transferError", e.getMessage()); productObj.put("transferError", e.getMessage());
productObj.put("effectiveUrl", normalizedUrl);
} }
// 文案信息 // 文案信息
@@ -194,7 +210,16 @@ public class JDProductService {
JSONObject commonWenan = new JSONObject(); JSONObject commonWenan = new JSONObject();
commonWenan.put("type", "通用文案"); commonWenan.put("type", "通用文案");
// 将原始消息中的链接替换为转链后的短链 // 将原始消息中的链接替换为转链后的短链
String messageWithShortUrl = message.replace(productObj.getString("originalUrl"), url); String targetUrl = productObj.getString("effectiveUrl");
String normalizedForReplace = productObj.getString("normalizedUrl");
String messageWithShortUrl = message;
if (targetUrl != null) {
String replaced = message.replace(originalUrlInText, targetUrl);
if (replaced.equals(message) && normalizedForReplace != null) {
replaced = replaced.replace(normalizedForReplace, targetUrl);
}
messageWithShortUrl = replaced;
}
commonWenan.put("content", format + FANAN_COMMON + messageWithShortUrl); commonWenan.put("content", format + FANAN_COMMON + messageWithShortUrl);
wenanArray.add(commonWenan); wenanArray.add(commonWenan);
@@ -547,4 +572,78 @@ public class JDProductService {
log.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount); log.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
return results; return results;
} }
private static final Pattern UJD_LINK_PATTERN = Pattern.compile("^https?://u\\.jd\\.com/[A-Za-z0-9]+[A-Za-z0-9_-]*$", Pattern.CASE_INSENSITIVE);
private static final Pattern JINGFEN_LINK_PATTERN = Pattern.compile("^https?://jingfen\\.jd\\.com/detail/[A-Za-z0-9]+\\.html$", Pattern.CASE_INSENSITIVE);
private static final Pattern TRAILING_SYMBOLS_PATTERN = Pattern.compile("[))】>》。,;;!!??“”\"'、…—\\s]+$");
private static String normalizeJdUrl(String rawUrl) {
if (rawUrl == null || rawUrl.trim().isEmpty()) {
return null;
}
String trimmed = rawUrl.trim();
// 截断常见中文/英文括号后的内容
int cutoffIndex = findCutoffIndex(trimmed);
if (cutoffIndex > -1) {
trimmed = trimmed.substring(0, cutoffIndex);
}
// 去掉末尾的标点符号
trimmed = TRAILING_SYMBOLS_PATTERN.matcher(trimmed).replaceAll("");
if (trimmed.isEmpty()) {
return null;
}
if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
trimmed = "https://" + trimmed;
}
if (UJD_LINK_PATTERN.matcher(trimmed).matches() || JINGFEN_LINK_PATTERN.matcher(trimmed).matches()) {
return trimmed;
}
// 针对 u.jd.com 链接,尝试进一步截断到第一个不合法字符
if (trimmed.contains("u.jd.com/")) {
int schemeEnd = trimmed.indexOf("u.jd.com/") + "u.jd.com/".length();
StringBuilder sb = new StringBuilder(trimmed.substring(0, schemeEnd));
for (int i = schemeEnd; i < trimmed.length(); i++) {
char c = trimmed.charAt(i);
if (isAllowedShortLinkChar(c)) {
sb.append(c);
} else {
break;
}
}
String candidate = sb.toString();
if (UJD_LINK_PATTERN.matcher(candidate).matches()) {
return candidate;
}
}
return null;
}
private static int findCutoffIndex(String text) {
char[] stopChars = new char[]{'', '(', '[', '【', '<', '《', '「', '『'};
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (Character.isWhitespace(c)) {
return i;
}
for (char stopChar : stopChars) {
if (c == stopChar) {
return i;
}
}
}
return -1;
}
private static boolean isAllowedShortLinkChar(char c) {
return (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| c == '-' || c == '_' || c == '.';
}
} }

View File

@@ -78,8 +78,10 @@ public class JDUtil {
*/ */
// van论坛 // van论坛
private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59"; // 自己的98e21c89ae5610240ec3f5f575f86a59
private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb"; private static final String LPF_APP_KEY_WZ = "34407d6cae6d43eca740370b8e12b01e";
// 自己的3dcb6b23a1104639ac433fd07adb6dfb
private static final String LPF_SECRET_KEY_WZ = "ad4966e1df3348a185fe6b33aa679a69";
private static final String SERVER_URL = "https://api.jd.com/routerjson"; private static final String SERVER_URL = "https://api.jd.com/routerjson";
//accessToken //accessToken

View File

@@ -68,9 +68,13 @@ public class OrderUtil {
if (!isAutoFlush || !lastValidCode.equals(newValidCode)) { if (!isAutoFlush || !lastValidCode.equals(newValidCode)) {
String content = getFormattedOrderInfo(orderRow); String content = getFormattedOrderInfo(orderRow);
String wxId = getWxidFromJdid(orderRow.getUnionId().toString()); String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
// 根据unionId获取接收人列表
String unionIdStr = orderRow.getUnionId().toString();
String touser = WXUtil.getTouserByUnionId(unionIdStr);
logger.info("京粉订单推送 - unionId={}, wxId={}, touser={}", unionIdStr, wxId, touser);
if (Util.isNotEmpty(wxId)) { if (Util.isNotEmpty(wxId)) {
wxUtil.sendTextMessage(wxId, content, 1, wxId, true); wxUtil.sendTextMessage(wxId, content, 1, wxId, true, touser);
// 不是已完成,不是违规的才发送 // 不是已完成,不是违规的才发送
if (newValidCode != 17 && newValidCode != 25 && newValidCode != 26 && newValidCode != 27 && newValidCode != 28) { if (newValidCode != 17 && newValidCode != 25 && newValidCode != 26 && newValidCode != 27 && newValidCode != 28) {
// 发送今日统计信息 // 发送今日统计信息
@@ -98,13 +102,15 @@ public class OrderUtil {
} }
if (shouldNotify) { if (shouldNotify) {
String wxId = getWxidFromJdid(orderRow.getUnionId().toString()); String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
// 根据unionId获取接收人列表
String touser = WXUtil.getTouserByUnionId(orderRow.getUnionId().toString());
if (Util.isNotEmpty(wxId)) { if (Util.isNotEmpty(wxId)) {
String content = getFormattedOrderInfoForJB(orderRow); String content = getFormattedOrderInfoForJB(orderRow);
String alertMsg = "[爱心] 价保/赔付 " + newProPriceAmount + " [爱心] \n" + content; String alertMsg = "[爱心] 价保/赔付 " + newProPriceAmount + " [爱心] \n" + content;
try { try {
// 先发送通知 // 先发送通知
wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true); wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true, touser);
// 通知成功后更新Redis格式为 "金额:true" // 通知成功后更新Redis格式为 "金额:true"
if (!isAutoFlushJB) { if (!isAutoFlushJB) {
@@ -176,6 +182,8 @@ public class OrderUtil {
if (!orderRowList.isEmpty()) { if (!orderRowList.isEmpty()) {
int i = 1; int i = 1;
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString()); String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
// 根据unionId获取接收人列表
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
StringBuilder content = new StringBuilder(); StringBuilder content = new StringBuilder();
content.append("批量订单:\n\r ").append("").append(orderRowList.size()).append("\r"); content.append("批量订单:\n\r ").append("").append(orderRowList.size()).append("\r");
List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList(); List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList();
@@ -187,7 +195,7 @@ public class OrderUtil {
} }
if (Util.isNotEmpty(wxId)) { if (Util.isNotEmpty(wxId)) {
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false); wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
} }
} }
@@ -247,6 +255,8 @@ public class OrderUtil {
if (!orderRowList.isEmpty()) { if (!orderRowList.isEmpty()) {
int i = 1; int i = 1;
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString()); String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
// 根据unionId获取接收人列表
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
StringBuilder content = new StringBuilder(); StringBuilder content = new StringBuilder();
content.append("批量订单:\n\r ").append("").append(orderRowList.size()).append("\r"); content.append("批量订单:\n\r ").append("").append(orderRowList.size()).append("\r");
List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList(); List<OrderRow> filterList = orderRowList.stream().filter(orderRow -> orderRow.getValidCode() != 2 && orderRow.getValidCode() != 3).toList();
@@ -258,7 +268,7 @@ public class OrderUtil {
} }
if (Util.isNotEmpty(wxId)) { if (Util.isNotEmpty(wxId)) {
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false); wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
} }
} }

View File

@@ -133,6 +133,33 @@ public class WXUtil {
return result; return result;
} }
/**
* 根据unionId获取SuperAdmin的接收人列表
* @param unionId 联盟ID
* @return 接收人列表企业微信用户ID多个用逗号分隔如果未配置则返回null
*/
public static String getTouserByUnionId(String unionId) {
if (unionId == null || unionId.trim().isEmpty()) {
logger.debug("getTouserByUnionId: unionId为空");
return null;
}
logger.debug("getTouserByUnionId: 查找unionId={}, super_admins数量={}", unionId, super_admins.size());
for (SuperAdmin admin : super_admins.values()) {
if (unionId.equals(admin.getUnionId())) {
String touser = admin.getTouser();
logger.debug("getTouserByUnionId: 找到匹配的SuperAdmin, unionId={}, name={}, touser={}",
admin.getUnionId(), admin.getName(), touser);
if (touser != null && !touser.trim().isEmpty()) {
return touser.trim();
} else {
logger.debug("getTouserByUnionId: SuperAdmin的touser字段为空或未配置");
}
}
}
logger.debug("getTouserByUnionId: 未找到匹配的SuperAdmin, unionId={}", unionId);
return null;
}
public static List<String> splitStringByLength(String input, int length) { public static List<String> splitStringByLength(String input, int length) {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
@@ -158,7 +185,8 @@ public class WXUtil {
jdidToWxidMap.put(superAdmin.getUnionId(), superAdmin.getWxid()); jdidToWxidMap.put(superAdmin.getUnionId(), superAdmin.getWxid());
jdidToRemarkMap.put(superAdmin.getUnionId(), superAdmin.getName()); jdidToRemarkMap.put(superAdmin.getUnionId(), superAdmin.getName());
} }
logger.info("超级管理员:{} {}", superAdmin.getName(), superAdmin.getWxid()); logger.info("超级管理员:{} {}, unionId={}, touser={}",
superAdmin.getName(), superAdmin.getWxid(), superAdmin.getUnionId(), superAdmin.getTouser());
} }
/* 内部管理群 */ /* 内部管理群 */
@@ -218,6 +246,10 @@ public class WXUtil {
} }
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime) { public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime) {
sendTextMessage(wxid, content, msgType, fromwxid, hiddenTime, null);
}
public void sendTextMessage(String wxid, String content, Integer msgType, String fromwxid, Boolean hiddenTime, String touser) {
// 全部打印 // 全部打印
//logger.info("发送文本消息 msgType: {} wxid: {} fromwxid: {} content: {}", msgType, wxid, fromwxid, content); //logger.info("发送文本消息 msgType: {} wxid: {} fromwxid: {} content: {}", msgType, wxid, fromwxid, content);
// 先在content顶部插入时间戳 // 先在content顶部插入时间戳
@@ -264,6 +296,10 @@ public class WXUtil {
data.put("msgType", msgType); data.put("msgType", msgType);
data.put("fromWxid", fromwxid); data.put("fromWxid", fromwxid);
data.put("hiddenTime", hiddenTime); data.put("hiddenTime", hiddenTime);
// 如果提供了接收人列表,添加到数据中
if (touser != null && !touser.trim().isEmpty()) {
data.put("touser", touser.trim());
}
wxReqDate.setData(data); wxReqDate.setData(data);
// wxReqDate 转成 JSONObject // wxReqDate 转成 JSONObject
JSONObject message = JSON.parseObject(JSON.toJSONString(wxReqDate)); JSONObject message = JSON.parseObject(JSON.toJSONString(wxReqDate));

View File

@@ -59,11 +59,29 @@ public class WxtsUtil {
* @param hiddenTime 是否隐藏时间戳 * @param hiddenTime 是否隐藏时间戳
*/ */
public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime) { public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime) {
sendWxTextMessage(wxid, content, msgType, fromWxid, hiddenTime, null);
}
/**
* 发送微信文本消息到wxts接口带接收人参数
* @param wxid 接收者微信ID
* @param content 消息内容
* @param msgType 消息类型
* @param fromWxid 发送者微信ID
* @param hiddenTime 是否隐藏时间戳
* @param touser 接收人列表企业微信用户ID多个用逗号分隔
*/
public void sendWxTextMessage(String wxid, String content, Integer msgType, String fromWxid, Boolean hiddenTime, String touser) {
try { try {
String url = SERVER_URL + "/wx/send/jd"; String url = SERVER_URL + "/wx/send/jd";
HashMap<String, Object> paramMap = new HashMap<>(); HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("text", content); paramMap.put("text", content);
// 如果提供了接收人列表,添加到参数中
if (touser != null && !touser.trim().isEmpty()) {
paramMap.put("touser", touser.trim());
logger.info("企业微信推送设置接收人 - 接收人: {}", touser);
}
HttpResponse execute = HttpRequest.post(url) HttpResponse execute = HttpRequest.post(url)
.header("vanToken", TOKEN) .header("vanToken", TOKEN)
@@ -72,7 +90,7 @@ public class WxtsUtil {
.execute(); .execute();
if (execute.getStatus() == 200) { if (execute.getStatus() == 200) {
logger.info("微信文本消息发送成功wxid={}, content={}", wxid, content); logger.info("微信文本消息发送成功wxid={}, content={}, touser={}", wxid, content, touser);
} else { } else {
logger.error("微信文本消息发送失败status={}, response={}", execute.getStatus(), execute.body()); logger.error("微信文本消息发送失败status={}, response={}", execute.getStatus(), execute.body());
} }