diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java index 3efb017..077475f 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java @@ -1056,66 +1056,61 @@ public class TencentDocController extends BaseController { return AjaxResult.error("无法识别表头,表头数据为空"); } - // 查找所有相关列 - log.info("开始识别表头列,共 {} 列", headerRowData.size()); + // 列名须与表格完全一致(仅忽略首尾空白、不间断空格等):备注、是否安排、物流单号、下单电话、标记、京东下单订单号;另需「单号」或「第三方单号」 + log.info("开始识别表头列(完全匹配列名),共 {} 列", headerRowData.size()); for (int i = 0; i < headerRowData.size(); i++) { String cellValue = headerRowData.getString(i); - if (cellValue != null) { - String cellValueTrim = cellValue.trim(); - log.debug("列 {} 内容: [{}]", i, cellValueTrim); + if (cellValue == null) { + continue; + } + String norm = TencentDocDataParser.normalizeTencentDocHeader(cellValue); + log.debug("列 {} 原始: [{}] 规范化: [{}]", i, cellValue.trim(), norm); - if (jdPlaceOrderNoColumn == null && TencentDocDataParser.isJdPlaceOrderNoHeader(cellValueTrim)) { - jdPlaceOrderNoColumn = i; - log.info("✓ 识别到 '京东下单订单号' 列:第 {} 列(索引{})", i + 1, i); - } - - // 识别"单号"列 - if (orderNoColumn == null && cellValueTrim.contains("单号") - && !TencentDocDataParser.isJdPlaceOrderNoHeader(cellValueTrim) - && !cellValueTrim.contains("物流")) { - orderNoColumn = i; - log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i); - } - - // 识别"物流单号"或"物流链接"列 - if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) { - logisticsLinkColumn = i; - log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i); - } - - // 识别"备注"列(可选) - if (remarkColumn == null && cellValueTrim.contains("备注")) { - remarkColumn = i; - log.info("✓ 识别到 '备注' 列:第 {} 列(索引{})", i + 1, i); - } - - // 识别"是否安排"列(可选) - if (arrangedColumn == null && (cellValueTrim.contains("是否安排") || cellValueTrim.contains("安排"))) { - arrangedColumn = i; - log.info("✓ 识别到 '是否安排' 列:第 {} 列(索引{})", i + 1, i); - } - - // 识别"标记"列(可选) - if (markColumn == null && cellValueTrim.contains("标记")) { - markColumn = i; - log.info("✓ 识别到 '标记' 列:第 {} 列(索引{})", i + 1, i); - } - - // 识别"下单电话"列(可选) - if (phoneColumn == null && (cellValueTrim.contains("下单电话"))) { - phoneColumn = i; - log.info("✓ 识别到 '下单电话' 列:第 {} 列(索引{}),列名: [{}]", i + 1, i, cellValueTrim); - } + if (jdPlaceOrderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "京东下单订单号")) { + jdPlaceOrderNoColumn = i; + log.info("✓ 列名完全匹配「京东下单订单号」:第 {} 列(索引{})", i + 1, i); + } + if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) { + orderNoColumn = i; + log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i); + } + if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) { + orderNoColumn = i; + log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i); + } + if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) { + logisticsLinkColumn = i; + log.info("✓ 列名完全匹配「物流单号」:第 {} 列(索引{})", i + 1, i); + } + if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) { + logisticsLinkColumn = i; + log.warn("✓ 列名匹配「物流链接」(建议改为「物流单号」):第 {} 列(索引{})", i + 1, i); + } + if (remarkColumn == null && TencentDocDataParser.headerEquals(cellValue, "备注")) { + remarkColumn = i; + log.info("✓ 列名完全匹配「备注」:第 {} 列(索引{})", i + 1, i); + } + if (arrangedColumn == null && TencentDocDataParser.headerEquals(cellValue, "是否安排")) { + arrangedColumn = i; + log.info("✓ 列名完全匹配「是否安排」:第 {} 列(索引{})", i + 1, i); + } + if (markColumn == null && TencentDocDataParser.headerEquals(cellValue, "标记")) { + markColumn = i; + log.info("✓ 列名完全匹配「标记」:第 {} 列(索引{})", i + 1, i); + } + if (phoneColumn == null && TencentDocDataParser.headerEquals(cellValue, "下单电话")) { + phoneColumn = i; + log.info("✓ 列名完全匹配「下单电话」:第 {} 列(索引{})", i + 1, i); } } log.info("表头列识别完成"); // 检查必需的列是否都已识别 if (orderNoColumn == null) { - return AjaxResult.error("无法找到'单号'列,请检查表头是否包含'单号'字段"); + return AjaxResult.error("无法找到列名完全为「单号」或「第三方单号」的列(请与表格列名一致,勿加空格或后缀)"); } if (logisticsLinkColumn == null) { - return AjaxResult.error("无法找到'物流单号'或'物流链接'列,请检查表头"); + return AjaxResult.error("无法找到列名完全为「物流单号」的列(兼容「物流链接」);请与表格列名一致"); } // 提示可选列的识别情况 @@ -2237,24 +2232,29 @@ public class TencentDocController extends BaseController { for (int i = 0; i < headerRowData.size(); i++) { String cellValue = headerRowData.getString(i); - if (cellValue != null) { - String cellValueTrim = cellValue.trim(); - - if (orderNoColumn == null && cellValueTrim.contains("单号") && !cellValueTrim.contains("物流") - && !TencentDocDataParser.isJdPlaceOrderNoHeader(cellValueTrim)) { - orderNoColumn = i; - log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i); - } - - if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) { - logisticsLinkColumn = i; - log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i); - } + if (cellValue == null) { + continue; + } + if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) { + orderNoColumn = i; + log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i); + } + if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) { + orderNoColumn = i; + log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i); + } + if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) { + logisticsLinkColumn = i; + log.info("✓ 列名完全匹配「物流单号」:第 {} 列(索引{})", i + 1, i); + } + if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) { + logisticsLinkColumn = i; + log.info("✓ 列名匹配「物流链接」:第 {} 列(索引{})", i + 1, i); } } if (orderNoColumn == null || logisticsLinkColumn == null) { - return AjaxResult.error("无法识别表头列,请确保表头包含'单号'和'物流单号'列"); + return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」或「第三方单号」,以及「物流单号」(或「物流链接」)"); } // 统计结果 @@ -2536,24 +2536,29 @@ public class TencentDocController extends BaseController { for (int i = 0; i < headerRowData.size(); i++) { String cellValue = headerRowData.getString(i); - if (cellValue != null) { - String cellValueTrim = cellValue.trim(); - - if (orderNoColumn == null && cellValueTrim.contains("单号") && !cellValueTrim.contains("物流") - && !TencentDocDataParser.isJdPlaceOrderNoHeader(cellValueTrim)) { - orderNoColumn = i; - log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i); - } - - if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) { - logisticsLinkColumn = i; - log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i); - } + if (cellValue == null) { + continue; + } + if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) { + orderNoColumn = i; + log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i); + } + if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) { + orderNoColumn = i; + log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i); + } + if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) { + logisticsLinkColumn = i; + log.info("✓ 列名完全匹配「物流单号」:第 {} 列(索引{})", i + 1, i); + } + if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) { + logisticsLinkColumn = i; + log.info("✓ 列名匹配「物流链接」:第 {} 列(索引{})", i + 1, i); } } if (orderNoColumn == null || logisticsLinkColumn == null) { - return AjaxResult.error("无法识别表头列,请确保表头包含'单号'和'物流单号'列"); + return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」或「第三方单号」,以及「物流单号」(或「物流链接」)"); } // 统计结果 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 031523a..0081e75 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 @@ -15,6 +15,8 @@ import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; /** * 小红书/抖音内容生成Service业务层处理 @@ -41,7 +43,10 @@ public class SocialMediaServiceImpl implements ISocialMediaService "keywords", "content:xhs", "content:douyin", - "content:both" + "content:both", + "xianyu:wenan_base", + "xianyu:jiaonixiadan_extra", + "xianyu:title_clean_regex" }; // 模板说明 @@ -50,6 +55,9 @@ public class SocialMediaServiceImpl implements ISocialMediaService put("content:xhs", "小红书文案生成提示词模板\n占位符:%s - 商品名称,%s - 价格信息,%s - 关键词信息"); put("content:douyin", "抖音文案生成提示词模板\n占位符:%s - 商品名称,%s - 价格信息,%s - 关键词信息"); put("content:both", "通用文案生成提示词模板\n占位符:%s - 商品名称,%s - 价格信息,%s - 关键词信息"); + put("xianyu:wenan_base", "闲鱼文案·正文基础说明\n用于「一键代下」与「教你下单」两版文案中,紧接在标题/型号行之后(纯文本,无占位符)"); + put("xianyu:jiaonixiadan_extra", "闲鱼文案·教你下单版尾部附加说明\n接在「更新日期:yyyy-MM-dd」之后(纯文本)"); + put("xianyu:title_clean_regex", "闲鱼文案·标题/型号清洗正则\n从标题与型号备注中移除营销敏感片段;须为 Java 正则,匹配到的内容会被删除"); }}; /** 与 Jarvis_java SocialMediaLlmClient 使用相同 Redis 键 */ @@ -61,19 +69,19 @@ public class SocialMediaServiceImpl implements ISocialMediaService private static final String LLM_MODE_OLLAMA = "ollama"; private static final String LLM_MODE_OPENAI = "openai"; - /** 闲鱼文案-通用基础说明 */ - private static final String WENAN_BASE = + /** 闲鱼文案-通用基础说明(Redis 未配置时使用) */ + private static final String DEFAULT_XIANYU_WENAN_BASE = "全新未拆封正品,包邮包安装,支持查验后再签收。\n" + "运损可免费换新。\n" + "售后全部支持全国联保。"; /** 闲鱼文案-教你下单附加说明(不含更新日期) */ - private static final String WENAN_JIAONIXIADAN_EXTRA = + private static final String DEFAULT_XIANYU_JIAONIXIADAN_EXTRA = "\n无偿提供下单方案,包价格成立:\n" + "只要告诉我【需要下单的型号 + 收货地址】,\n" + "我会根据你所在地区和需求,\n" + "优先回复你合适的下单渠道和详细步骤,让你安全省钱地完成下单。"; /** 标题/型号清洗:去掉营销敏感词 */ - private static final String TITLE_CLEAN_REGEX = "以旧|政府|换新|领取|国家|补贴|15%|20%|国补|立减|【|】"; + private static final String DEFAULT_XIANYU_TITLE_CLEAN_REGEX = "以旧|政府|换新|领取|国家|补贴|15%|20%|国补|立减|【|】"; /** * 提取商品标题关键词 @@ -352,6 +360,13 @@ public class SocialMediaServiceImpl implements ISocialMediaService if (StringUtils.isEmpty(templateValue)) { return AjaxResult.error("模板内容不能为空"); } + if ("xianyu:title_clean_regex".equals(key)) { + try { + Pattern.compile(templateValue); + } catch (PatternSyntaxException e) { + return AjaxResult.error("正则表达式无效: " + e.getMessage()); + } + } redisTemplate.opsForValue().set(redisKey, templateValue); log.info("保存提示词模板成功: {}", key); @@ -407,6 +422,12 @@ public class SocialMediaServiceImpl implements ISocialMediaService } } + /** Redis 无配置时返回 defaultTemplate */ + private String getPromptTemplateWithDefault(String templateKey, String defaultTemplate) { + String fromRedis = getTemplateFromRedis(templateKey); + return StringUtils.isNotEmpty(fromRedis) ? fromRedis : defaultTemplate; + } + /** * 验证模板键名是否有效 */ @@ -571,6 +592,9 @@ public class SocialMediaServiceImpl implements ISocialMediaService String cleanTitle = cleanTitleOrRemark(title.trim()); String cleanRemark = StringUtils.isNotEmpty(remark) ? cleanTitleOrRemark(remark.trim()) : ""; + String wenanBase = getPromptTemplateWithDefault("xianyu:wenan_base", DEFAULT_XIANYU_WENAN_BASE); + String jiaonixiadanExtra = getPromptTemplateWithDefault("xianyu:jiaonixiadan_extra", DEFAULT_XIANYU_JIAONIXIADAN_EXTRA); + // 标题行 StringBuilder daixiadanBuilder = new StringBuilder(); daixiadanBuilder.append("(一键代下) ").append(cleanTitle).append("\n"); @@ -578,7 +602,7 @@ public class SocialMediaServiceImpl implements ISocialMediaService if (StringUtils.isNotEmpty(cleanRemark)) { daixiadanBuilder.append("型号:").append(cleanRemark).append("\n"); } - daixiadanBuilder.append(WENAN_BASE); + daixiadanBuilder.append(wenanBase); // 教你下单版 StringBuilder jiaonixiadanBuilder = new StringBuilder(); @@ -586,11 +610,11 @@ public class SocialMediaServiceImpl implements ISocialMediaService if (StringUtils.isNotEmpty(cleanRemark)) { jiaonixiadanBuilder.append("型号:").append(cleanRemark).append("\n"); } - jiaonixiadanBuilder.append(WENAN_BASE).append("\n\n"); + jiaonixiadanBuilder.append(wenanBase).append("\n\n"); java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd"); String dateStr = sdf.format(new java.util.Date()); - jiaonixiadanBuilder.append("更新日期:").append(dateStr).append(WENAN_JIAONIXIADAN_EXTRA); + jiaonixiadanBuilder.append("更新日期:").append(dateStr).append(jiaonixiadanExtra); result.put("success", true); result.put("daixiadan", daixiadanBuilder.toString()); @@ -599,13 +623,19 @@ public class SocialMediaServiceImpl implements ISocialMediaService } /** - * 清洗标题/型号中的敏感词 + * 清洗标题/型号中的敏感词(正则来自可配置模板 xianyu:title_clean_regex) */ - private static String cleanTitleOrRemark(String text) { + private String cleanTitleOrRemark(String text) { if (text == null) { return ""; } - return text.replaceAll(TITLE_CLEAN_REGEX, ""); + String regex = getPromptTemplateWithDefault("xianyu:title_clean_regex", DEFAULT_XIANYU_TITLE_CLEAN_REGEX); + try { + return text.replaceAll(regex, ""); + } catch (Exception e) { + log.warn("标题清洗正则执行失败,使用内置默认: {}", e.getMessage()); + return text.replaceAll(DEFAULT_XIANYU_TITLE_CLEAN_REGEX, ""); + } } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java index 3081653..d098886 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java @@ -218,24 +218,39 @@ public class TencentDocServiceImpl implements ITencentDocService { for (int i = 0; i < headerCells.size(); i++) { String cellText = headerCells.getString(i); - if (cellText != null) { - if (TencentDocDataParser.isJdPlaceOrderNoHeader(cellText)) { - if (jdPlaceOrderNoColumn == null) { - jdPlaceOrderNoColumn = i; - } - } else if (cellText.contains("日期")) dateColumn = i; - else if (cellText.contains("公司")) companyColumn = i; - else if (cellText.contains("单号") && !cellText.contains("物流") - && !TencentDocDataParser.isJdPlaceOrderNoHeader(cellText)) orderNoColumn = i; - else if (cellText.contains("型号")) modelColumn = i; - else if (cellText.contains("数量")) quantityColumn = i; - else if (cellText.contains("姓名")) nameColumn = i; - else if (cellText.contains("电话")) phoneColumn = i; - else if (cellText.contains("地址")) addressColumn = i; - else if (cellText.contains("价格")) priceColumn = i; - else if (cellText.contains("备注")) remarkColumn = i; - else if (cellText.contains("是否安排") || cellText.contains("安排")) arrangedColumn = i; - else if (cellText.contains("物流")) logisticsColumn = i; + if (cellText == null) { + continue; + } + if (jdPlaceOrderNoColumn == null && TencentDocDataParser.isJdPlaceOrderNoHeader(cellText)) { + jdPlaceOrderNoColumn = i; + } else if (cellText.contains("日期")) { + dateColumn = i; + } else if (cellText.contains("公司")) { + companyColumn = i; + } else if (TencentDocDataParser.headerEquals(cellText, "单号")) { + orderNoColumn = i; + } else if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellText, "第三方单号")) { + orderNoColumn = i; + } else if (cellText.contains("型号")) { + modelColumn = i; + } else if (cellText.contains("数量")) { + quantityColumn = i; + } else if (cellText.contains("姓名")) { + nameColumn = i; + } else if (cellText.contains("电话")) { + phoneColumn = i; + } else if (cellText.contains("地址")) { + addressColumn = i; + } else if (cellText.contains("价格")) { + priceColumn = i; + } else if (TencentDocDataParser.headerEquals(cellText, "备注")) { + remarkColumn = i; + } else if (TencentDocDataParser.headerEquals(cellText, "是否安排")) { + arrangedColumn = i; + } else if (TencentDocDataParser.headerEquals(cellText, "物流单号")) { + logisticsColumn = i; + } else if (logisticsColumn == null && TencentDocDataParser.headerEquals(cellText, "物流链接")) { + logisticsColumn = i; } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java index dcf53f5..0198cd1 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java @@ -215,20 +215,36 @@ public class TencentDocDataParser { } /** - * 表头是否为「京东下单订单号」列(与第三方「单号」等列区分) + * 表头规范化:去 BOM、首尾空白、不间断空格与全角空格后,去掉中间空白再比较(与腾讯表格展示列名一致) + */ + public static String normalizeTencentDocHeader(String raw) { + if (raw == null) { + return ""; + } + String t = raw.trim(); + if (t.startsWith("\uFEFF")) { + t = t.substring(1).trim(); + } + t = t.replace('\u00A0', ' ').replace('\u3000', ' '); + t = t.replaceAll("\\s+", ""); + return t; + } + + /** + * 表头是否与期望列名完全一致(规范化后) + */ + public static boolean headerEquals(String raw, String expectedName) { + if (expectedName == null) { + return false; + } + return expectedName.equals(normalizeTencentDocHeader(raw)); + } + + /** + * 表头是否为「京东下单订单号」列(列名须完全一致) */ public static boolean isJdPlaceOrderNoHeader(String cellValueTrim) { - if (cellValueTrim == null) { - return false; - } - String t = cellValueTrim.trim(); - if (t.isEmpty()) { - return false; - } - if (t.contains("京东下单订单号")) { - return true; - } - return t.contains("京东") && t.contains("下单") && t.contains("订单号"); + return headerEquals(cellValueTrim, "京东下单订单号"); } }