Compare commits

...

20 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
Leo
bdd33581f1 1 2025-11-09 00:00:41 +08:00
Leo
ef358cc6b3 1 2025-11-08 15:25:43 +08:00
Leo
e76c6d4451 羽绒服 2025-11-08 02:18:35 +08:00
31ecfa6a2f 1 2025-11-03 19:49:12 +08:00
127a5b71c6 1 2025-11-03 19:44:22 +08:00
1872908dae 1 2025-11-03 16:03:34 +08:00
efdb727e48 1 2025-11-03 15:46:21 +08:00
4af64b58d6 1 2025-11-03 15:38:07 +08:00
15 changed files with 1863 additions and 72 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

@@ -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("随机选择:使用已使用过的京东评论");
}
}
}
@@ -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 }

View File

@@ -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 {

View File

@@ -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;
/**
* 创建时间
*/

View File

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

View File

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

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.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);
@@ -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 == '.';
}
}

View File

@@ -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";
/**
* 内部单号:
* 分销标记(标记用,勿改):

View File

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

View File

@@ -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();

View File

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