Compare commits
20 Commits
424cf37260
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a893f3cd61 | ||
|
|
c2be15e3f5 | ||
|
|
8445b500ae | ||
|
|
ee67d1ae8f | ||
|
|
a20e92d7bf | ||
| 69d1d91f5e | |||
|
|
570fcb0b93 | ||
|
|
7fda3da9ed | ||
|
|
e7687c8909 | ||
|
|
8e12076225 | ||
|
|
5b48727fb2 | ||
|
|
bb6c907cda | ||
|
|
bdd33581f1 | ||
|
|
ef358cc6b3 | ||
|
|
e76c6d4451 | ||
| 31ecfa6a2f | |||
| 127a5b71c6 | |||
| 1872908dae | |||
| efdb727e48 | |||
| 4af64b58d6 |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@@ -168,23 +169,43 @@ public class JDInnerController {
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ 尝试使用已使用过的京东评论
|
||||
if (commentToUse == null && !usedComments.isEmpty()) {
|
||||
Collections.shuffle(usedComments);
|
||||
commentToUse = usedComments.get(0);
|
||||
logger.info("使用已使用过的京东评论");
|
||||
}
|
||||
|
||||
// 4️⃣ 尝试使用已使用过的淘宝评论
|
||||
// 3️⃣ 尝试使用已使用过的评论(随机从京东和淘宝中选择)
|
||||
if (commentToUse == null) {
|
||||
// 准备候选评论列表
|
||||
List<Comment> candidateComments = new ArrayList<>();
|
||||
List<String> candidateSources = new ArrayList<>(); // 记录来源,用于标识是京东还是淘宝
|
||||
|
||||
// 添加已使用过的京东评论
|
||||
if (!usedComments.isEmpty()) {
|
||||
Collections.shuffle(usedComments);
|
||||
candidateComments.add(usedComments.get(0));
|
||||
candidateSources.add("JD");
|
||||
logger.info("已添加已使用过的京东评论到候选列表");
|
||||
}
|
||||
|
||||
// 添加已使用过的淘宝评论
|
||||
String taobaoProductIdMap = tbMap.getOrDefault(productId, null);
|
||||
if (taobaoProductIdMap != null && !taobaoProductIdMap.isEmpty()) {
|
||||
logger.info("尝试获取已使用过的淘宝评论");
|
||||
Comment taobaoComment = generateTaobaoComment(productType, true);
|
||||
if (taobaoComment != null) {
|
||||
commentToUse = taobaoComment;
|
||||
candidateComments.add(taobaoComment);
|
||||
candidateSources.add("TB");
|
||||
logger.info("已添加已使用过的淘宝评论到候选列表");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果候选列表不为空,随机选择
|
||||
if (!candidateComments.isEmpty()) {
|
||||
Random random = new Random();
|
||||
int selectedIndex = random.nextInt(candidateComments.size());
|
||||
commentToUse = candidateComments.get(selectedIndex);
|
||||
String selectedSource = candidateSources.get(selectedIndex);
|
||||
|
||||
if ("TB".equals(selectedSource)) {
|
||||
isTb = true;
|
||||
logger.info("使用已使用过的淘宝评论");
|
||||
logger.info("随机选择:使用已使用过的淘宝评论");
|
||||
} else {
|
||||
logger.info("随机选择:使用已使用过的京东评论");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,9 +301,9 @@ public class JDInnerController {
|
||||
// 查询未使用的评论(isUse != 1,即0或null)
|
||||
taobaoComments = taobaoCommentRepository.findByProductIdAndIsUseNotAndPictureUrlsIsNotNull(taobaoProductId, 1);
|
||||
}
|
||||
|
||||
|
||||
logger.info("taobaoComments.size() {} (includeUsed={})", taobaoComments.size(), includeUsed);
|
||||
|
||||
|
||||
if (!taobaoComments.isEmpty()) {
|
||||
Collections.shuffle(taobaoComments);
|
||||
TaobaoComment selected = taobaoComments.get(0);
|
||||
@@ -298,7 +319,7 @@ public class JDInnerController {
|
||||
comment.setProductId(product_id);
|
||||
comment.setUserName(selected.getUserName());
|
||||
comment.setCreatedAt(selected.getCreatedAt());
|
||||
|
||||
|
||||
// 只在获取未使用的评论时才标记为已使用
|
||||
if (!includeUsed) {
|
||||
selected.setIsUse(1);
|
||||
@@ -355,19 +376,19 @@ public class JDInnerController {
|
||||
|
||||
try {
|
||||
String giftKey = jdProductService.createGiftCoupon(idOrUrl, amount, quantity, owner, skuName);
|
||||
|
||||
|
||||
// 如果giftKey为null,返回错误而不是成功响应
|
||||
if (giftKey == null || giftKey.trim().isEmpty()) {
|
||||
String errorDetail = String.format("礼金创建失败,giftCouponKey为null。参数: idOrUrl=%s, amount=%.2f, quantity=%d, owner=%s, skuName=%s。可能原因:商品不支持创建礼金、商品类型错误、京东API调用失败。请查看JD项目日志获取详细信息。",
|
||||
String errorDetail = String.format("礼金创建失败,giftCouponKey为null。参数: idOrUrl=%s, amount=%.2f, quantity=%d, owner=%s, skuName=%s。可能原因:商品不支持创建礼金、商品类型错误、京东API调用失败。请查看JD项目日志获取详细信息。",
|
||||
idOrUrl, amount, quantity, owner, skuName);
|
||||
logger.error("礼金创建失败 - giftKey为null, {}", errorDetail);
|
||||
return error(errorDetail);
|
||||
}
|
||||
|
||||
|
||||
// 创建成功,保存到Redis
|
||||
jdProductService.saveGiftCouponToRedis(idOrUrl, giftKey, skuName, owner);
|
||||
logger.info("礼金创建成功 - giftKey={}, idOrUrl={}, owner={}, amount={}, quantity={}", giftKey, idOrUrl, owner, amount, quantity);
|
||||
|
||||
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("giftCouponKey", giftKey);
|
||||
return resp;
|
||||
@@ -399,6 +420,68 @@ public class JDInnerController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建礼金券并生成包含礼金的推广链接
|
||||
* 入参:{ skey, skuId/materialUrl, amount, quantity, batchSize, owner, skuName }
|
||||
* 返回:{ results: [ {index, success, giftCouponKey, shortURL, error} ], total, successCount, failCount }
|
||||
*/
|
||||
@PostMapping("/batchCreateGiftCoupons")
|
||||
public Object batchCreateGiftCoupons(@RequestBody Map<String, Object> body) {
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
}
|
||||
|
||||
String skuId = body.get("skuId") != null ? String.valueOf(body.get("skuId")) : null;
|
||||
String materialUrl = body.get("materialUrl") != null ? String.valueOf(body.get("materialUrl")) : null;
|
||||
String owner = body.get("owner") != null ? String.valueOf(body.get("owner")) : "g";
|
||||
String skuName = body.get("skuName") != null ? String.valueOf(body.get("skuName")) : "";
|
||||
double amount = parseDouble(body.get("amount"), 1.8);
|
||||
int quantity = parseInt(body.get("quantity"), 1);
|
||||
int batchSize = parseInt(body.get("batchSize"), 15);
|
||||
|
||||
String idOrUrl = skuId != null && !skuId.trim().isEmpty() ? skuId : materialUrl;
|
||||
if (idOrUrl == null || idOrUrl.trim().isEmpty()) {
|
||||
return error("skuId or materialUrl is required");
|
||||
}
|
||||
if (amount <= 0 || quantity <= 0) {
|
||||
return error("amount and quantity must be positive");
|
||||
}
|
||||
if (batchSize <= 0 || batchSize > 100) {
|
||||
return error("batchSize must be between 1 and 100");
|
||||
}
|
||||
|
||||
logger.info("批量创建礼金券请求 - idOrUrl={}, amount={}, quantity={}, batchSize={}, owner={}, skuName={}",
|
||||
idOrUrl, amount, quantity, batchSize, owner, skuName);
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> results = jdProductService.batchCreateGiftCouponsWithLinks(
|
||||
idOrUrl, amount, quantity, batchSize, owner, skuName);
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (Map<String, Object> result : results) {
|
||||
if (Boolean.TRUE.equals(result.get("success"))) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject resp = new JSONObject();
|
||||
resp.put("results", results);
|
||||
resp.put("total", batchSize);
|
||||
resp.put("successCount", successCount);
|
||||
resp.put("failCount", failCount);
|
||||
|
||||
logger.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
logger.error("batchCreateGiftCoupons error", e);
|
||||
return error("batchCreateGiftCoupons failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动清理Redis中超过93天的旧数据
|
||||
* 请求参数:{ skey }
|
||||
@@ -414,7 +497,7 @@ public class JDInnerController {
|
||||
tips.put("tip", "请在Postman的Body标签中选择raw/JSON格式,并输入: {\"skey\": \"your_skey_here\"}");
|
||||
return tips;
|
||||
}
|
||||
|
||||
|
||||
String skey = body.get("skey") != null ? String.valueOf(body.get("skey")) : null;
|
||||
if (checkSkey(skey)) {
|
||||
return error("invalid skey");
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.time.LocalDateTime;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "image_conversions", indexes = {
|
||||
@Index(name = "idx_original_url", columnList = "originalUrl"),
|
||||
@Index(name = "idx_converted_url", columnList = "convertedUrl")
|
||||
@Index(name = "idx_original_url", columnList = "original_url"),
|
||||
@Index(name = "idx_converted_url", columnList = "converted_url")
|
||||
})
|
||||
@Data
|
||||
public class ImageConversion {
|
||||
|
||||
@@ -61,6 +61,12 @@ public class SuperAdmin {
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Integer isActive = 1;
|
||||
|
||||
/**
|
||||
* 接收人(企业微信用户ID,多个用逗号分隔)
|
||||
*/
|
||||
@Column(name = "touser", length = 500)
|
||||
private String touser;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
|
||||
@@ -61,8 +61,9 @@ public class MessageConsumerService implements RocketMQListener<JSONObject> {
|
||||
Integer msgType = data.getInteger("msgType");
|
||||
String fromWxid = data.getString("fromWxid");
|
||||
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);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.van.business.service;
|
||||
|
||||
import cn.van.business.model.pl.ImageConversion;
|
||||
import cn.van.business.repository.ImageConversionRepository;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -16,9 +15,7 @@ import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -97,22 +94,37 @@ public class ImageConvertService {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
log.info("开始转换图片URL列表,共{}张图片", imageUrls.size());
|
||||
List<String> convertedUrls = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
|
||||
for (String imageUrl : imageUrls) {
|
||||
if (StrUtil.isBlank(imageUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debug("处理图片URL: {}", imageUrl);
|
||||
try {
|
||||
String convertedUrl = convertImageUrl(imageUrl);
|
||||
if (!convertedUrl.equals(imageUrl)) {
|
||||
successCount++;
|
||||
log.debug("图片转换成功: {} -> {}", imageUrl, convertedUrl);
|
||||
} else {
|
||||
skipCount++;
|
||||
log.debug("图片无需转换(非webp格式): {}", imageUrl);
|
||||
}
|
||||
convertedUrls.add(convertedUrl);
|
||||
} catch (Exception e) {
|
||||
// 转换失败时使用原URL,不中断流程
|
||||
skipCount++;
|
||||
log.warn("图片转换失败,使用原URL: {}. 错误: {}", imageUrl, e.getMessage());
|
||||
convertedUrls.add(imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("图片URL转换完成,共{}张,成功转换{}张,跳过/失败{}张",
|
||||
imageUrls.size(), successCount, skipCount);
|
||||
return convertedUrls;
|
||||
}
|
||||
|
||||
@@ -128,31 +140,59 @@ public class ImageConvertService {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
// 规范化URL:处理协议相对URL(//开头)
|
||||
String normalizedUrl = normalizeUrl(originalUrl);
|
||||
|
||||
// 检查是否为webp格式
|
||||
if (!isWebpFormat(originalUrl)) {
|
||||
return originalUrl;
|
||||
if (!isWebpFormat(normalizedUrl)) {
|
||||
return originalUrl; // 返回原URL,保持一致性
|
||||
}
|
||||
|
||||
// 检查系统是否支持webp转换
|
||||
if (!WebPImageIO.isWebPSupported()) {
|
||||
log.debug("系统不支持webp格式,跳过转换: {}", originalUrl);
|
||||
log.warn("系统不支持webp格式,跳过转换: {}", normalizedUrl);
|
||||
throw new IOException("系统不支持webp格式转换");
|
||||
}
|
||||
|
||||
// 检查是否已转换
|
||||
Optional<ImageConversion> existing = imageConversionRepository.findByOriginalUrl(originalUrl);
|
||||
// 使用规范化后的URL进行缓存查询和转换
|
||||
// 检查是否已转换(使用规范化URL作为key)
|
||||
Optional<ImageConversion> existing = imageConversionRepository.findByOriginalUrl(normalizedUrl);
|
||||
if (existing.isPresent()) {
|
||||
ImageConversion conversion = existing.get();
|
||||
log.debug("使用缓存的转换结果: {} -> {}", originalUrl, conversion.getConvertedUrl());
|
||||
log.debug("使用缓存的转换结果: {} -> {}", normalizedUrl, conversion.getConvertedUrl());
|
||||
return conversion.getConvertedUrl();
|
||||
}
|
||||
|
||||
// 执行转换
|
||||
String convertedUrl = performConversion(originalUrl);
|
||||
log.info("图片转换成功: {} -> {}", originalUrl, convertedUrl);
|
||||
// 执行转换(使用规范化URL)
|
||||
log.info("开始转换webp图片: {}", normalizedUrl);
|
||||
String convertedUrl = performConversion(normalizedUrl);
|
||||
log.info("图片转换成功: {} -> {}", normalizedUrl, convertedUrl);
|
||||
return convertedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化URL:处理协议相对URL等特殊情况
|
||||
*
|
||||
* @param url 原始URL
|
||||
* @return 规范化后的URL
|
||||
*/
|
||||
private String normalizeUrl(String url) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 清理特殊字符(如零宽字符)
|
||||
String cleanUrl = url.trim().replaceAll("[\\u200B-\\u200D\\uFEFF]", "");
|
||||
|
||||
// 处理协议相对URL(//开头)
|
||||
if (cleanUrl.startsWith("//")) {
|
||||
cleanUrl = "https:" + cleanUrl;
|
||||
log.debug("转换协议相对URL: {} -> {}", url, cleanUrl);
|
||||
}
|
||||
|
||||
return cleanUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否为webp格式
|
||||
*
|
||||
@@ -164,13 +204,22 @@ public class ImageConvertService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查URL中是否包含.webp
|
||||
String lowerUrl = url.toLowerCase();
|
||||
// 清理URL中的特殊字符(如零宽字符)
|
||||
String cleanUrl = url.trim().replaceAll("[\\u200B-\\u200D\\uFEFF]", "");
|
||||
|
||||
// 检查URL中是否包含.webp扩展名(不区分大小写)
|
||||
String lowerUrl = cleanUrl.toLowerCase();
|
||||
// 检查URL参数或路径中是否包含webp
|
||||
return lowerUrl.contains(".webp") ||
|
||||
lowerUrl.contains("format=webp") ||
|
||||
lowerUrl.contains("?webp") ||
|
||||
lowerUrl.contains("&webp");
|
||||
boolean isWebp = lowerUrl.contains(".webp") ||
|
||||
lowerUrl.contains("format=webp") ||
|
||||
lowerUrl.contains("?webp") ||
|
||||
lowerUrl.contains("&webp");
|
||||
|
||||
if (isWebp) {
|
||||
log.debug("检测到webp格式图片: {}", cleanUrl);
|
||||
}
|
||||
|
||||
return isWebp;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
433
src/main/java/cn/van/business/service/MarketingImageService.java
Normal file
433
src/main/java/cn/van/business/service/MarketingImageService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
347
src/main/java/cn/van/business/service/SocialMediaService.java
Normal file
347
src/main/java/cn/van/business/service/SocialMediaService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static cn.van.business.util.JDUtil.*;
|
||||
|
||||
@@ -43,8 +43,10 @@ import static cn.van.business.util.JDUtil.*;
|
||||
@Slf4j
|
||||
public class JDProductService {
|
||||
|
||||
private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59";
|
||||
private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb";
|
||||
// 自己的98e21c89ae5610240ec3f5f575f86a59
|
||||
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 ACCESS_TOKEN = "";
|
||||
private static final Logger logger = LoggerFactory.getLogger(JDProductService.class);
|
||||
@@ -84,17 +86,29 @@ public class JDProductService {
|
||||
for (String url : urls) {
|
||||
try {
|
||||
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) {
|
||||
JSONObject errorObj = new JSONObject();
|
||||
errorObj.put("url", url);
|
||||
errorObj.put("url", originalUrlInText);
|
||||
errorObj.put("error", "链接查询失败");
|
||||
resultArray.add(errorObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject productObj = new JSONObject();
|
||||
productObj.put("originalUrl", url);
|
||||
productObj.put("originalUrl", originalUrlInText);
|
||||
productObj.put("normalizedUrl", normalizedUrl);
|
||||
|
||||
// 商品基本信息
|
||||
productObj.put("materialUrl", productInfo.getData()[0].getMaterialUrl());
|
||||
@@ -124,22 +138,24 @@ public class JDProductService {
|
||||
|
||||
// 生成转链后的短链
|
||||
try {
|
||||
String shortUrl = transfer(url, null);
|
||||
String shortUrl = transfer(normalizedUrl, null);
|
||||
String effectiveUrl = normalizedUrl;
|
||||
if (shortUrl != null && !shortUrl.isEmpty()) {
|
||||
productObj.put("shortUrl", shortUrl);
|
||||
productObj.put("transferSuccess", true);
|
||||
// 将短链替换原始链接,用于后续文案生成
|
||||
url = shortUrl;
|
||||
effectiveUrl = shortUrl;
|
||||
} else {
|
||||
productObj.put("shortUrl", url); // 如果转链失败,使用原链接
|
||||
productObj.put("shortUrl", normalizedUrl); // 如果转链失败,使用归一化后的链接
|
||||
productObj.put("transferSuccess", false);
|
||||
log.warn("转链失败,使用原链接: {}", url);
|
||||
log.warn("转链失败,使用原链接: {}", normalizedUrl);
|
||||
}
|
||||
productObj.put("effectiveUrl", effectiveUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("生成转链时发生异常: {}", url, e);
|
||||
productObj.put("shortUrl", url); // 转链异常时使用原链接
|
||||
log.error("生成转链时发生异常: {}", normalizedUrl, e);
|
||||
productObj.put("shortUrl", normalizedUrl); // 转链异常时使用原链接
|
||||
productObj.put("transferSuccess", false);
|
||||
productObj.put("transferError", e.getMessage());
|
||||
productObj.put("effectiveUrl", normalizedUrl);
|
||||
}
|
||||
|
||||
// 文案信息
|
||||
@@ -182,13 +198,28 @@ public class JDProductService {
|
||||
wenan4.put("content", "【教你下单】 " + title + cleanSkuName + "\n" + WENAN_FANAN_BX.replaceAll("信息更新日期:", "信息更新日期:" + format));
|
||||
wenanArray.add(wenan4);
|
||||
|
||||
|
||||
JSONObject wenan5 = new JSONObject();
|
||||
wenan5.put("type", "羽绒服专属-标价到手"); // type明确产品类型+下单方式,与其他方案区分
|
||||
wenan5.put("content", "(羽绒服专属) " + title + cleanSkuName + "\n" + WENAN_YURONGFU.replaceAll("更新", format + "更新"));
|
||||
wenanArray.add(wenan5);
|
||||
|
||||
productObj.put("wenan", wenanArray);
|
||||
|
||||
// 添加通用文案 - 使用转链后的短链替换原始链接
|
||||
JSONObject commonWenan = new JSONObject();
|
||||
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);
|
||||
wenanArray.add(commonWenan);
|
||||
|
||||
@@ -294,12 +325,12 @@ public class JDProductService {
|
||||
String errorCode = response != null ? response.getCode() : "null";
|
||||
String errorMsg = response != null ? response.getMsg() : "null";
|
||||
Integer resultCode = response != null && response.getGetResult() != null ? response.getGetResult().getCode() : null;
|
||||
|
||||
|
||||
// 尝试解析response.getMsg()中的详细错误信息(JSON格式)
|
||||
String detailErrorMsg = errorMsg;
|
||||
Integer detailCode = resultCode;
|
||||
boolean parsedDetail = false;
|
||||
|
||||
|
||||
try {
|
||||
if (errorMsg != null && errorMsg.startsWith("{") && errorMsg.contains("message")) {
|
||||
// 解析外层JSON
|
||||
@@ -318,7 +349,7 @@ public class JDProductService {
|
||||
detailCode = parsedDetailCode;
|
||||
}
|
||||
parsedDetail = true;
|
||||
log.error("礼金创建失败 - 京东API错误: code={}, message={}, SKU={}, owner={}, amount={}, quantity={}",
|
||||
log.error("礼金创建失败 - 京东API错误: code={}, message={}, SKU={}, owner={}, amount={}, quantity={}",
|
||||
detailCode, detailErrorMsg, skuId, owner, amount, quantity);
|
||||
}
|
||||
}
|
||||
@@ -329,22 +360,22 @@ public class JDProductService {
|
||||
// JSON解析失败,使用原始错误信息
|
||||
log.warn("解析错误信息失败,使用原始信息: {}", errorMsg);
|
||||
}
|
||||
|
||||
|
||||
// 记录日志并抛出包含详细错误信息的异常
|
||||
if (!parsedDetail) {
|
||||
log.error("礼金创建失败 - response.code={}, response.msg={}, result.code={}, SKU={}, owner={}, amount={}, quantity={}",
|
||||
log.error("礼金创建失败 - response.code={}, response.msg={}, result.code={}, SKU={}, owner={}, amount={}, quantity={}",
|
||||
errorCode, errorMsg, resultCode, skuId, owner, amount, quantity);
|
||||
}
|
||||
|
||||
|
||||
// 构造错误消息
|
||||
String finalErrorMsg;
|
||||
if (parsedDetail) {
|
||||
finalErrorMsg = String.format("礼金创建失败:%s (错误码:%d)", detailErrorMsg, detailCode);
|
||||
} else {
|
||||
finalErrorMsg = String.format("礼金创建失败:京东API返回错误 (response.code=%s, result.code=%s, msg=%s)",
|
||||
finalErrorMsg = String.format("礼金创建失败:京东API返回错误 (response.code=%s, result.code=%s, msg=%s)",
|
||||
errorCode, resultCode != null ? resultCode : "null", detailErrorMsg);
|
||||
}
|
||||
|
||||
|
||||
throw new Exception(finalErrorMsg);
|
||||
}
|
||||
|
||||
@@ -451,4 +482,168 @@ public class JDProductService {
|
||||
return errorMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建礼金券并生成包含礼金的推广链接
|
||||
*
|
||||
* @param skuId 商品SKU ID或materialUrl
|
||||
* @param amount 礼金金额(单位:元)
|
||||
* @param quantity 每个礼金券的数量
|
||||
* @param batchSize 批量创建的个数(默认20)
|
||||
* @param owner 商品类型(g=自营,pop=POP)
|
||||
* @param skuName 商品名称
|
||||
* @return 批量创建结果列表,包含giftCouponKey和shortURL
|
||||
*/
|
||||
public List<Map<String, Object>> batchCreateGiftCouponsWithLinks(String skuId, double amount, int quantity, int batchSize, String owner, String skuName) {
|
||||
log.info("开始批量创建礼金券 - SKU={}, 金额={}元, 数量={}, 批次大小={}, Owner={}", skuId, amount, quantity, batchSize, owner);
|
||||
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (int i = 0; i < batchSize; i++) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("index", i + 1);
|
||||
result.put("success", false);
|
||||
|
||||
try {
|
||||
// 创建礼金券
|
||||
String giftCouponKey = createGiftCoupon(skuId, amount, quantity, owner, skuName);
|
||||
|
||||
if (giftCouponKey == null || giftCouponKey.trim().isEmpty()) {
|
||||
log.error("批量创建礼金券失败 [{}/{}] - giftCouponKey为空", i + 1, batchSize);
|
||||
result.put("error", "礼金创建失败,giftCouponKey为空");
|
||||
result.put("giftCouponKey", null);
|
||||
result.put("shortURL", null);
|
||||
failCount++;
|
||||
} else {
|
||||
log.info("批量创建礼金券成功 [{}/{}] - giftCouponKey={}", i + 1, batchSize, giftCouponKey);
|
||||
|
||||
// 保存到Redis
|
||||
try {
|
||||
saveGiftCouponToRedis(skuId, giftCouponKey, skuName, owner);
|
||||
} catch (Exception e) {
|
||||
log.warn("保存礼金到Redis失败,但礼金创建成功 - giftCouponKey={}, error={}", giftCouponKey, e.getMessage());
|
||||
}
|
||||
|
||||
// 生成包含礼金的推广链接
|
||||
String shortURL = null;
|
||||
try {
|
||||
// 使用materialUrl或skuId作为原始链接
|
||||
String originalUrl = skuId;
|
||||
// transfer方法支持SKU ID或materialUrl直接传入
|
||||
shortURL = transfer(originalUrl, giftCouponKey);
|
||||
|
||||
if (shortURL == null || shortURL.trim().isEmpty()) {
|
||||
log.warn("生成推广链接失败 - giftCouponKey={}, 礼金创建成功但转链失败", giftCouponKey);
|
||||
} else {
|
||||
log.info("生成推广链接成功 [{}/{}] - giftCouponKey={}, shortURL={}", i + 1, batchSize, giftCouponKey, shortURL);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("生成推广链接异常 - giftCouponKey={}, error={}", giftCouponKey, e.getMessage(), e);
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("giftCouponKey", giftCouponKey);
|
||||
result.put("shortURL", shortURL);
|
||||
successCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("批量创建礼金券异常 [{}/{}] - error={}", i + 1, batchSize, e.getMessage(), e);
|
||||
result.put("error", "创建异常: " + e.getMessage());
|
||||
result.put("giftCouponKey", null);
|
||||
result.put("shortURL", null);
|
||||
failCount++;
|
||||
}
|
||||
|
||||
results.add(result);
|
||||
|
||||
// 避免请求过快,每创建一张礼金后稍作延迟
|
||||
if (i < batchSize - 1) {
|
||||
try {
|
||||
Thread.sleep(100); // 延迟100ms
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("批量创建礼金券延迟被中断");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量创建礼金券完成 - 总数={}, 成功={}, 失败={}", batchSize, successCount, failCount);
|
||||
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 == '.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,10 @@ public class JDUtil {
|
||||
*/
|
||||
|
||||
// van论坛
|
||||
private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59";
|
||||
private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb";
|
||||
// 自己的98e21c89ae5610240ec3f5f575f86a59
|
||||
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";
|
||||
//accessToken
|
||||
@@ -107,8 +109,24 @@ public class JDUtil {
|
||||
5:全国联保,全国统一安装标准。支持官方 400,服务号查询,假一赔十。
|
||||
""";
|
||||
public static final String WENAN_FANAN_BX = "本人提供免费指导下单服务,一台也是团购价,细心指导\n" + "\n" + "【质量】官旗下单,包正的\n" + "【物流】您自己账户可跟踪,24小时发货\n" + "【售后】您自己账户直接联系,无忧售后\n" + "【安装】专业人员安装,全程无需您操心\n" + "【价格】标价就是到手价,骑共享单车去酒吧,该省省该花花\n" + "【服务】手把手教您下单,有问题随时咨询\n" + "【体验】所有服务都是官旗提供,价格有内部渠道优惠,同品质更优惠!\n" + "\n" + "信息更新日期:\n" + "\n" + "捡漏价格不定时有变动,优惠不等人,发「省份+型号」免费咨询当日最低价!";
|
||||
public static final String WENAN_YURONGFU = "坦博尔正品羽绒服!大额优惠券直接送!立省几百\n" +
|
||||
"品牌官方授权渠道直发,比旗舰自营店到手价更低,用你自己的账号下单更放心~\n" +
|
||||
"款色码全任你选(仅限店内展示款式),下单前必询库存!拒绝盲拍哦~\n" +
|
||||
"先确认货号、颜色、尺码,再拍不踩雷,建议先去实体店试穿合身,避免后续麻烦呀\n" +
|
||||
"到手价 = 页面标价 + 6 元代拍费(划重点:不是所有款都二百多,以页面标价为准)\n" +
|
||||
"叠加专属优惠券后,比自己直接买省不少,福利不等人!\n" +
|
||||
"正品保障拉满!假一罚十,支持任何渠道验货无忧\n" +
|
||||
"后续有质量问题,直接用自己的账号走官方售后,售后有保障\n" +
|
||||
"目前仅店内展示款式可拍,暂时没有额外款式补充哈\n" +
|
||||
"粉丝优先回复处理,官方渠道直发,代拍不退代拍费,望理解~\n" +
|
||||
"\n" +
|
||||
"更新\n" + // 日期替换标记,代码自动替换为“yyyyMMdd更新”
|
||||
"\n" ;
|
||||
|
||||
|
||||
public static final String FANAN_COMMON = "\n 文案复制到微x,点击领券,把商品加到J东,去APP结算才能显示折扣补贴\n";
|
||||
|
||||
|
||||
/**
|
||||
* 内部单号:
|
||||
* 分销标记(标记用,勿改):
|
||||
|
||||
@@ -68,9 +68,13 @@ public class OrderUtil {
|
||||
if (!isAutoFlush || !lastValidCode.equals(newValidCode)) {
|
||||
String content = getFormattedOrderInfo(orderRow);
|
||||
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)) {
|
||||
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) {
|
||||
// 发送今日统计信息
|
||||
@@ -98,13 +102,15 @@ public class OrderUtil {
|
||||
}
|
||||
if (shouldNotify) {
|
||||
String wxId = getWxidFromJdid(orderRow.getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String touser = WXUtil.getTouserByUnionId(orderRow.getUnionId().toString());
|
||||
if (Util.isNotEmpty(wxId)) {
|
||||
String content = getFormattedOrderInfoForJB(orderRow);
|
||||
String alertMsg = "[爱心] 价保/赔付 : " + newProPriceAmount + " [爱心] \n" + content;
|
||||
|
||||
try {
|
||||
// 先发送通知
|
||||
wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true);
|
||||
wxUtil.sendTextMessage(wxId, alertMsg, 1, wxId, true, touser);
|
||||
|
||||
// 通知成功后更新Redis,格式为 "金额:true"
|
||||
if (!isAutoFlushJB) {
|
||||
@@ -176,6 +182,8 @@ public class OrderUtil {
|
||||
if (!orderRowList.isEmpty()) {
|
||||
int i = 1;
|
||||
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("批量订单:\n\r ").append(" 共 ").append(orderRowList.size()).append("单 \r");
|
||||
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)) {
|
||||
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()) {
|
||||
int i = 1;
|
||||
String wxId = getWxidFromJdid(orderRowList.get(0).getUnionId().toString());
|
||||
// 根据unionId获取接收人列表
|
||||
String touser = WXUtil.getTouserByUnionId(orderRowList.get(0).getUnionId().toString());
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("批量订单:\n\r ").append(" 共 ").append(orderRowList.size()).append("单 \r");
|
||||
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)) {
|
||||
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false);
|
||||
wxUtil.sendTextMessage(wxId, content.toString(), 1, wxId, false, touser);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -133,6 +133,33 @@ public class WXUtil {
|
||||
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) {
|
||||
List<String> result = new ArrayList<>();
|
||||
@@ -158,7 +185,8 @@ public class WXUtil {
|
||||
jdidToWxidMap.put(superAdmin.getUnionId(), superAdmin.getWxid());
|
||||
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) {
|
||||
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);
|
||||
// 先在content顶部插入时间戳
|
||||
@@ -264,6 +296,10 @@ public class WXUtil {
|
||||
data.put("msgType", msgType);
|
||||
data.put("fromWxid", fromwxid);
|
||||
data.put("hiddenTime", hiddenTime);
|
||||
// 如果提供了接收人列表,添加到数据中
|
||||
if (touser != null && !touser.trim().isEmpty()) {
|
||||
data.put("touser", touser.trim());
|
||||
}
|
||||
wxReqDate.setData(data);
|
||||
// wxReqDate 转成 JSONObject
|
||||
JSONObject message = JSON.parseObject(JSON.toJSONString(wxReqDate));
|
||||
@@ -355,7 +391,7 @@ public class WXUtil {
|
||||
return wxReqDate;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 * * * * ?")
|
||||
//@Scheduled(cron = "0 * * * * ?")
|
||||
public void checkWxStatus() {
|
||||
WxReqDate wxReqDate = createWxReqData(WXReqType.GET_WX_STATUS);
|
||||
JSONObject data = new JSONObject();
|
||||
|
||||
@@ -59,11 +59,29 @@ public class WxtsUtil {
|
||||
* @param 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 {
|
||||
String url = SERVER_URL + "/wx/send/jd";
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("text", content);
|
||||
|
||||
|
||||
// 如果提供了接收人列表,添加到参数中
|
||||
if (touser != null && !touser.trim().isEmpty()) {
|
||||
paramMap.put("touser", touser.trim());
|
||||
logger.info("企业微信推送设置接收人 - 接收人: {}", touser);
|
||||
}
|
||||
|
||||
HttpResponse execute = HttpRequest.post(url)
|
||||
.header("vanToken", TOKEN)
|
||||
@@ -72,7 +90,7 @@ public class WxtsUtil {
|
||||
.execute();
|
||||
|
||||
if (execute.getStatus() == 200) {
|
||||
logger.info("微信文本消息发送成功:wxid={}, content={}", wxid, content);
|
||||
logger.info("微信文本消息发送成功:wxid={}, content={}, touser={}", wxid, content, touser);
|
||||
} else {
|
||||
logger.error("微信文本消息发送失败:status={}, response={}", execute.getStatus(), execute.body());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user