From 9af1a369f7fc1c05d9cc7110084ec9e7c30d55b3 Mon Sep 17 00:00:00 2001 From: van Date: Mon, 6 Apr 2026 16:16:44 +0800 Subject: [PATCH] 1 --- .../jarvis/SocialMediaController.java | 4 +- .../jarvis/service/ISocialMediaService.java | 14 +- .../service/impl/SocialMediaServiceImpl.java | 172 +++++++++++++++++- 3 files changed, 179 insertions(+), 11 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java index bc0fed0..ca2dfca 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/SocialMediaController.java @@ -297,9 +297,7 @@ public class SocialMediaController extends BaseController public AjaxResult generateXianyuWenan(@RequestBody Map request) { try { - String title = (String) request.get("title"); - String remark = (String) request.get("remark"); - Map result = socialMediaService.generateXianyuWenan(title, remark); + Map result = socialMediaService.generateXianyuWenan(request); if (Boolean.TRUE.equals(result.get("success"))) { return AjaxResult.success(result); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java index d1e1533..b5927fb 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ISocialMediaService.java @@ -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 generateXianyuWenan(String title, String remark); + Map generateXianyuWenan(Map request); /** 列出多套大模型接入配置及当前激活的 id(与 Jarvis 共用 Redis) */ com.ruoyi.common.core.domain.AjaxResult listLlmProfiles(); diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java index b7c485a..e884277 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/SocialMediaServiceImpl.java @@ -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 generateXianyuWenan(String title, String remark) { + public Map generateXianyuWenan(Map request) { Map result = new HashMap<>(); + Map req = request == null ? new HashMap() : 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 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 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) */