This commit is contained in:
van
2026-04-06 16:16:44 +08:00
parent 49c855ff78
commit 9af1a369f7
3 changed files with 179 additions and 11 deletions

View File

@@ -297,9 +297,7 @@ public class SocialMediaController extends BaseController
public AjaxResult generateXianyuWenan(@RequestBody Map<String, Object> request)
{
try {
String title = (String) request.get("title");
String remark = (String) request.get("remark");
Map<String, Object> result = socialMediaService.generateXianyuWenan(title, remark);
Map<String, Object> result = socialMediaService.generateXianyuWenan(request);
if (Boolean.TRUE.equals(result.get("success"))) {
return AjaxResult.success(result);
}

View File

@@ -65,13 +65,17 @@ public interface ISocialMediaService
com.ruoyi.common.core.domain.AjaxResult deletePromptTemplate(String key);
/**
* 根据标题(+可选型号备注)生成闲鱼文案(代下单、教你下单),不依赖JD接口
* 生成闲鱼文案(代下单、教你下单),并可选生成种草文案。
*
* @param title 商品标题(必填)
* @param remark 型号/备注(可选
* @return 包含代下单、教你下单两种文案的 Map
* request 常用字段:
* - title/remark兼容旧版手动入口
* - generateSeedNote(boolean) 是否额外生成种草文案
* - goods_title/goods_model/goods_type种草文案必传
* - goods_brand/channel_source/official_price/sell_price/warranty/delivery_install可选
*
* @return 包含代下单、教你下单,以及可选 seedNote 的 Map
*/
Map<String, Object> generateXianyuWenan(String title, String remark);
Map<String, Object> generateXianyuWenan(Map<String, Object> request);
/** 列出多套大模型接入配置及当前激活的 id与 Jarvis 共用 Redis */
com.ruoyi.common.core.domain.AjaxResult listLlmProfiles();

View File

@@ -50,7 +50,8 @@ public class SocialMediaServiceImpl implements ISocialMediaService
"content:both",
"xianyu:wenan_base",
"xianyu:jiaonixiadan_extra",
"xianyu:title_clean_regex"
"xianyu:title_clean_regex",
"xianyu:seed_note_prompt"
};
// 模板说明
@@ -62,6 +63,7 @@ public class SocialMediaServiceImpl implements ISocialMediaService
put("xianyu:wenan_base", "闲鱼文案·正文基础说明\n用于「一键代下」与「教你下单」两版文案中紧接在标题/型号行之后(纯文本,无占位符)");
put("xianyu:jiaonixiadan_extra", "闲鱼文案·教你下单版尾部附加说明\n接在「更新日期yyyy-MM-dd」之后纯文本");
put("xianyu:title_clean_regex", "闲鱼文案·标题/型号清洗正则\n从标题与型号备注中移除营销敏感片段须为 Java 正则,匹配到的内容会被删除");
put("xianyu:seed_note_prompt", "闲鱼文案·种草文案提示词模板\n占位符{{goods_title}} {{goods_model}} {{goods_type}} {{goods_brand}} {{channel_source}} {{official_price}} {{sell_price}} {{warranty}} {{delivery_install}}");
}};
/** 多套大模型 JSON 存储,与 Jarvis_java SocialMediaLlmClient 一致 */
@@ -88,6 +90,50 @@ public class SocialMediaServiceImpl implements ISocialMediaService
+ "优先回复你合适的下单渠道和详细步骤,让你安全省钱地完成下单。";
/** 标题/型号清洗:去掉营销敏感词 */
private static final String DEFAULT_XIANYU_TITLE_CLEAN_REGEX = "以旧|政府|换新|领取|国家|补贴|15%|20%|国补|立减|【|】";
/** 闲鱼种草文案提示词模板Redis 未配置时使用) */
private static final String DEFAULT_XIANYU_SEED_NOTE_PROMPT =
"# 角色设定\n"
+ "你是拥有10年家电行业经验的资深渠道商同时是闲鱼平台家电类目TOP级种草文案达人精通闲鱼家电用户的消费心理、搜索流量规则、高转化文案写作逻辑擅长用专业且亲切的口吻为全新未拆封的正品家电写出强信任、高种草、高成交的闲鱼转卖文案拒绝冰冷的官方参数堆砌、浮夸的营销硬广、二手闲置话术。\n\n"
+ "# 核心任务\n"
+ "严格基于我提供的家电商品信息,生成一段完全适配闲鱼平台、符合全新正品家电属性、有强吸引力、高转化的种草文案,全程遵循以下所有规则。\n\n"
+ "# 家电商品输入信息(接口传入)\n"
+ "【必传核心信息】\n"
+ "1. 商品完整标题:{{goods_title}}\n"
+ "2. 商品精准完整型号:{{goods_model}}\n"
+ "3. 家电品类类型:{{goods_type}}\n\n"
+ "【可选补充信息(无则按默认兜底规则生成)】\n"
+ "4. 商品品牌:{{goods_brand}}\n"
+ "5. 渠道来源:{{channel_source}}\n"
+ "6. 官方指导价:{{official_price}}\n"
+ "7. 闲鱼售卖价格:{{sell_price}}\n"
+ "8. 官方质保政策:{{warranty}}\n"
+ "9. 配送安装政策:{{delivery_install}}\n\n"
+ "# 字段优先级&兜底规则(必须严格遵守)\n"
+ "1. 型号优先级:必须以「{{goods_model}}」为唯一精准型号,商品标题里的型号简写仅做补充,禁止写错、简写型号\n"
+ "2. 品类优先级:必须以「{{goods_type}}」为唯一品类标准,所有场景、卖点、话术必须围绕该家电品类展开\n"
+ "3. 品牌兜底:若{{goods_brand}}为空,自动从{{goods_title}}中提取品牌名,提取不到则不强制体现\n"
+ "4. 渠道兜底:若{{channel_source}}为空,默认使用「品牌授权经销商直供」,禁止使用二手、个人闲置相关渠道描述\n"
+ "5. 价格兜底:若{{official_price}}为空,默认使用「比官方旗舰店优惠力度大」相关话术;若{{sell_price}}为空,不写固定售价,引导用户私信询价\n"
+ "6. 售后兜底:若{{warranty}}为空,默认使用「品牌官方全国联保,和官方旗舰店享受同等售后权益」\n"
+ "7. 配送兜底:若{{delivery_install}}为空,默认使用「全国包邮送货入户,乡镇可达」\n\n"
+ "# 核心字段强制融合规则必须100%严格执行,解决生硬堆砌问题)\n"
+ "1. 标题植入规则:生成的闲鱼标题,必须同时包含「{{goods_type}}+{{goods_model}}+品牌+全新正品」4个核心元素同时适配闲鱼家电用户搜索习惯字数控制在25-35字\n"
+ "2. 正文植入规则:\n"
+ " - 开篇前30字必须同时出现「{{goods_type}}+{{goods_model}}」,强化型号和品类,同时给出正品信任背书\n"
+ " - 正文卖点模块,必须围绕「{{goods_type}}」的品类核心需求展开,同时绑定{{goods_model}}型号,禁止脱离品类写通用卖点\n"
+ " - 正文至少3次自然提及{{goods_model}}完整型号适配闲鱼搜索权重规则禁止只出现1次\n"
+ " - 正文所有场景化描述,必须贴合{{goods_type}}的使用场景,比如空调对应卧室/客厅制冷、冰箱对应食材囤货/嵌入装修、洗衣机对应家庭大容量/除菌等\n"
+ "3. 结尾植入规则:结尾行动引导和话题标签,必须包含「{{goods_type}}」相关流量词标签数量4-6个必须有1个标签带{{goods_model}}型号\n"
+ "4. 卖点提取规则:核心卖点自动从{{goods_title}}中提取,优先提取能效、容量、变频、智能、超薄、除菌等家电核心决策卖点,转化为场景化话术,禁止参数堆砌\n\n"
+ "# 创作必须严格遵守的通用规则\n"
+ "1. 全程使用第一人称「我/我们」写作,贴合家电渠道商的专业且亲切的身份,像给朋友推荐靠谱好货,绝对不能出现官方旗舰店硬广话术、二手个人闲置转手话术,全程围绕「全新未拆封正品家电」核心属性创作\n"
+ "2. 文案100%基于接口传入的信息创作,不得凭空捏造与输入信息不符的功能、参数、质保、服务政策,核心卖点必须转化为用户可感知的场景化好处\n"
+ "3. 必须完整包含以下核心模块,顺序不可调换,缺一不可:吸睛搜索标题、开篇信任背书、正品保障、卖点拆解、价格优势、配送安装、售后承诺、结尾行动和标签\n"
+ "4. 文案排版短句为主段落清晰每段不超过3行手机阅读友好\n"
+ "5. 字数要求正文控制在400-600字\n"
+ "6. 合规要求严格符合闲鱼平台规则和3C家电宣传规范不使用极限词不夸大宣传\n\n"
+ "# 输出格式\n"
+ "直接输出最终完整文案,不要额外解释。";
/**
* Redis 无记录时与 Jarvis_java SocialMediaService 使用的默认模板一致,供接口回显到前端参考。
@@ -139,6 +185,7 @@ public class SocialMediaServiceImpl implements ISocialMediaService
put("xianyu:wenan_base", DEFAULT_XIANYU_WENAN_BASE);
put("xianyu:jiaonixiadan_extra", DEFAULT_XIANYU_JIAONIXIADAN_EXTRA);
put("xianyu:title_clean_regex", DEFAULT_XIANYU_TITLE_CLEAN_REGEX);
put("xianyu:seed_note_prompt", DEFAULT_XIANYU_SEED_NOTE_PROMPT);
}};
/**
@@ -933,11 +980,18 @@ public class SocialMediaServiceImpl implements ISocialMediaService
}
/**
* 根据标题(+可选型号备注生成闲鱼文案代下单、教你下单不依赖JD接口
* 生成闲鱼文案:默认输出「代下单 + 教你下单」,可选额外生成「种草文案」。
*/
@Override
public Map<String, Object> generateXianyuWenan(String title, String remark) {
public Map<String, Object> generateXianyuWenan(Map<String, Object> request) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> req = request == null ? new HashMap<String, Object>() : request;
String title = asString(req.get("title"));
String remark = asString(req.get("remark"));
if (StringUtils.isEmpty(title)) {
// 兼容只传新字段的调用方式
title = asString(req.get("goods_title"));
}
if (StringUtils.isEmpty(title) || StringUtils.isEmpty(title.trim())) {
result.put("success", false);
result.put("error", "商品标题不能为空");
@@ -973,9 +1027,121 @@ public class SocialMediaServiceImpl implements ISocialMediaService
result.put("success", true);
result.put("daixiadan", daixiadanBuilder.toString());
result.put("jiaonixiadan", jiaonixiadanBuilder.toString());
boolean generateSeed = isTrue(req.get("generateSeedNote"));
if (generateSeed) {
String seedError = validateSeedRequiredFields(req);
if (seedError != null) {
result.put("success", false);
result.put("error", seedError);
return result;
}
try {
String seedPrompt = buildSeedPrompt(req);
String seedNote = callJarvisLlm(seedPrompt, asString(req.get("profileId")));
if (StringUtils.isEmpty(seedNote)) {
throw new RuntimeException("种草文案生成返回为空");
}
result.put("seedNote", seedNote.trim());
} catch (Exception e) {
log.error("生成闲鱼种草文案失败", e);
result.put("success", false);
result.put("error", "种草文案生成失败: " + e.getMessage());
return result;
}
}
return result;
}
private String buildSeedPrompt(Map<String, Object> request) {
String template = getPromptTemplateWithDefault("xianyu:seed_note_prompt", DEFAULT_XIANYU_SEED_NOTE_PROMPT);
String goodsTitle = asString(request.get("goods_title"));
String goodsModel = asString(request.get("goods_model"));
String goodsType = asString(request.get("goods_type"));
String goodsBrand = asString(request.get("goods_brand"));
String channelSource = asString(request.get("channel_source"));
String officialPrice = asString(request.get("official_price"));
String sellPrice = asString(request.get("sell_price"));
String warranty = asString(request.get("warranty"));
String deliveryInstall = asString(request.get("delivery_install"));
String channelFinal = StringUtils.isNotEmpty(channelSource) ? channelSource : "品牌授权经销商直供";
String warrantyFinal = StringUtils.isNotEmpty(warranty) ? warranty : "品牌官方全国联保,和官方旗舰店享受同等售后权益";
String deliveryFinal = StringUtils.isNotEmpty(deliveryInstall) ? deliveryInstall : "全国包邮送货入户,乡镇可达";
String officialFinal = StringUtils.isNotEmpty(officialPrice) ? officialPrice : "比官方旗舰店优惠力度大";
String sellFinal = StringUtils.isNotEmpty(sellPrice) ? sellPrice : "私信询价(到手更划算)";
return template
.replace("{{goods_title}}", goodsTitle)
.replace("{{goods_model}}", goodsModel)
.replace("{{goods_type}}", goodsType)
.replace("{{goods_brand}}", StringUtils.isNotEmpty(goodsBrand) ? goodsBrand : "")
.replace("{{channel_source}}", channelFinal)
.replace("{{official_price}}", officialFinal)
.replace("{{sell_price}}", sellFinal)
.replace("{{warranty}}", warrantyFinal)
.replace("{{delivery_install}}", deliveryFinal);
}
private String validateSeedRequiredFields(Map<String, Object> request) {
if (request == null) {
return "请求体不能为空";
}
if (StringUtils.isEmpty(asString(request.get("goods_title")))) {
return "生成种草文案时goods_title 不能为空";
}
if (StringUtils.isEmpty(asString(request.get("goods_model")))) {
return "生成种草文案时goods_model 不能为空";
}
if (StringUtils.isEmpty(asString(request.get("goods_type")))) {
return "生成种草文案时goods_type 不能为空";
}
return null;
}
private String callJarvisLlm(String message, String profileId) {
String url = jarvisBaseUrl + "/jarvis/social-media/llm/test";
JSONObject body = new JSONObject();
body.put("message", message);
if (StringUtils.isNotEmpty(profileId)) {
body.put("profileId", profileId);
}
String resp = HttpUtils.sendJsonPost(url, body.toJSONString());
if (StringUtils.isEmpty(resp)) {
throw new RuntimeException("Jarvis 返回为空,请检查服务可达性");
}
Object parsed = JSON.parse(resp);
if (!(parsed instanceof JSONObject)) {
throw new RuntimeException("Jarvis 响应格式错误");
}
JSONObject jo = (JSONObject) parsed;
if (!Integer.valueOf(200).equals(jo.getInteger("code"))) {
throw new RuntimeException(jo.getString("msg"));
}
Object dataObj = jo.get("data");
if (!(dataObj instanceof JSONObject)) {
throw new RuntimeException("Jarvis data 结构异常");
}
JSONObject data = (JSONObject) dataObj;
if (!Boolean.TRUE.equals(data.getBoolean("success"))) {
throw new RuntimeException(data.getString("error"));
}
return data.getString("reply");
}
private static boolean isTrue(Object value) {
return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(String.valueOf(value));
}
private static String asString(Object value) {
if (value == null) {
return "";
}
return StringUtils.trim(value.toString());
}
/**
* 清洗标题/型号中的敏感词(正则来自可配置模板 xianyu:title_clean_regex
*/