更新这个货源

This commit is contained in:
super
2026-05-22 09:41:31 +08:00
parent 73d25fcbcb
commit 70e902e7ea
22 changed files with 1136 additions and 52 deletions
@@ -8,11 +8,21 @@ public class BusinessException extends RuntimeException {
this(null, message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = null;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public Integer getCode() {
return code;
}
@@ -0,0 +1,216 @@
package com.nanri.aiimage.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* Coze 回流数据按 ID 分组传播工具。
*
* <p>业务背景:
* 解析行按 Excel 行顺序排列,ID 形如 "1"、"1_1"、"1_2"、"2"、"2_1"。
* 同一 baseId(第一个下划线 '_' 之前的部分)的连续行视为同一组——即所谓
* "分组只看下一行数据":按行顺序、相邻同 baseId 行才是同组,遇到不同 baseId 立即开新组。
*
* <p>传播规则:
* <ol>
* <li>单行组(孤立 baseId)跳过,不做任何处理。</li>
* <li>组内任一行的目标字段值"包含" {@code hitValues} 中某个候选值,
* 则把该组所有行的目标字段统一覆盖为该候选值(标准值)。</li>
* <li>多个候选值同时命中时,按 {@code hitValues} 列表顺序优先匹配
* (列表越靠前优先级越高,比如专利场景把"已侵权"放在"侵权"前面,
* 避免短串先命中导致丢失"已"字)。</li>
* <li>无法解析到对应 result 行(resolver 返回 null)的 parsed 行不参与
* 组内匹配,但仍占据分组位置——不会打断同 baseId 的连续性,
* 也不会被强行写入。</li>
* </ol>
*
* <p>使用方式:
* <pre>
* // 专利结论列:组内任一行结论命中 "已侵权" 或 "侵权",组内都改为该标准值
* CozeGroupResultPropagator.propagateByGroup(
* receivedRows,
* AppearancePatentParsedRowVo::getDisplayId,
* row -&gt; findResultRow(row, resultMap),
* AppearancePatentResultRowDto::getConclusion,
* AppearancePatentResultRowDto::setConclusion,
* List.of("已侵权", "侵权"),
* "[appearance-patent] taskId=" + task.getId()
* );
* </pre>
*/
public final class CozeGroupResultPropagator {
private static final Logger log = LoggerFactory.getLogger(CozeGroupResultPropagator.class);
/**
* 否定前缀关键字。若当前值同时包含 standard 和这些关键字之一,则不视为命中。
* 用于规避"没有侵权""不侵权""未发现明显侵权风险"等被 contains 误判为命中"侵权"的情况。
*/
private static final List<String> NEGATIVE_KEYWORDS = Arrays.asList("没有", "", "");
private CozeGroupResultPropagator() {
}
/**
* 按 baseId 连续分组、命中即组内传播。无日志上下文标签的便捷重载。
*/
public static <P, R> int propagateByGroup(List<P> parsedRows,
Function<P, String> displayIdGetter,
Function<P, R> resultRowResolver,
Function<R, String> fieldGetter,
BiConsumer<R, String> fieldSetter,
List<String> hitValues) {
return propagateByGroup(parsedRows, displayIdGetter, resultRowResolver,
fieldGetter, fieldSetter, hitValues, null);
}
/**
* 按 baseId 连续分组、命中即组内传播。
*
* @param parsedRows 解析行(按 Excel 行顺序)
* @param displayIdGetter parsed 行 → ID 字符串(用于计算 baseId)
* @param resultRowResolver parsed 行 → 对应的 result DTO(找不到返回 null
* @param fieldGetter result DTO → 当前目标字段值
* @param fieldSetter result DTO ← 新目标字段值
* @param hitValues 需要传播的标准值列表,靠前优先匹配(包含语义)
* @param logTag 日志前缀(如 "[appearance-patent] taskId=9975"),null 则只在汇总层打日志
* @param <P> parsed 行类型
* @param <R> result DTO 类型
* @return 实际被改写的 result 行数(用于上层日志埋点)
*/
public static <P, R> int propagateByGroup(List<P> parsedRows,
Function<P, String> displayIdGetter,
Function<P, R> resultRowResolver,
Function<R, String> fieldGetter,
BiConsumer<R, String> fieldSetter,
List<String> hitValues,
String logTag) {
if (parsedRows == null || parsedRows.isEmpty()
|| displayIdGetter == null || resultRowResolver == null
|| fieldGetter == null || fieldSetter == null
|| hitValues == null || hitValues.isEmpty()) {
return 0;
}
String tagPrefix = (logTag == null || logTag.isEmpty()) ? "" : logTag + " ";
int totalUpdated = 0;
int n = parsedRows.size();
int i = 0;
while (i < n) {
String currentBaseId = baseId(displayIdGetter.apply(parsedRows.get(i)));
int groupStart = i;
int j = i + 1;
// 连续同 baseId 视为一组
while (j < n && currentBaseId.equals(baseId(displayIdGetter.apply(parsedRows.get(j))))) {
j++;
}
int groupEnd = j; // 半开区间 [groupStart, groupEnd)
i = j;
// 单行组跳过
if (groupEnd - groupStart < 2) {
continue;
}
// 收集组内所有能解析到 result DTO 的行,同时记录显示 ID 方便日志
List<R> groupResults = new ArrayList<>(groupEnd - groupStart);
List<String> groupDisplayIds = new ArrayList<>(groupEnd - groupStart);
for (int k = groupStart; k < groupEnd; k++) {
P parsedRow = parsedRows.get(k);
R resultRow = resultRowResolver.apply(parsedRow);
if (resultRow != null) {
groupResults.add(resultRow);
groupDisplayIds.add(displayIdGetter.apply(parsedRow));
}
}
if (groupResults.isEmpty()) {
continue;
}
// 按 hitValues 顺序检测组内是否有命中(包含语义),命中即定下"标准值"。
// 命中需同时满足:当前值包含 standard 且不含任何否定关键字("没有"/"不"/"未"),
// 避免"没有侵权""不侵权""未发现明显侵权风险"这类被 contains 误判为命中。
String matchedStandardValue = null;
outer:
for (String standard : hitValues) {
if (standard == null || standard.isEmpty()) {
continue;
}
for (int k = 0; k < groupResults.size(); k++) {
R row = groupResults.get(k);
String currentValue = fieldGetter.apply(row);
if (currentValue == null || !currentValue.contains(standard)) {
continue;
}
if (containsNegative(currentValue)) {
log.debug("{}group-propagate skip negative baseId={} displayId={} currentValue={} candidate={}",
tagPrefix, currentBaseId, groupDisplayIds.get(k), currentValue, standard);
continue;
}
matchedStandardValue = standard;
break outer;
}
}
if (matchedStandardValue == null) {
continue;
}
// 把组内所有 result DTO 的目标字段统一覆盖为标准值,并逐行打日志
int groupUpdated = 0;
for (int idx = 0; idx < groupResults.size(); idx++) {
R row = groupResults.get(idx);
String currentValue = fieldGetter.apply(row);
if (matchedStandardValue.equals(currentValue)) {
continue;
}
fieldSetter.accept(row, matchedStandardValue);
groupUpdated++;
totalUpdated++;
log.info("{}group-propagate row baseId={} displayId={} oldValue={} newValue={}",
tagPrefix, currentBaseId, groupDisplayIds.get(idx),
currentValue, matchedStandardValue);
}
if (groupUpdated > 0) {
log.info("{}group-propagate group hit baseId={} groupSize={} standardValue={} updatedRows={}",
tagPrefix, currentBaseId, groupResults.size(),
matchedStandardValue, groupUpdated);
}
}
return totalUpdated;
}
/**
* 判断当前值是否含任意否定关键字。命中关键字即视为"未发生"语义,
* 上层应跳过该值,避免被 {@link String#contains(CharSequence)} 误判命中标准值。
*/
private static boolean containsNegative(String value) {
if (value == null || value.isEmpty()) {
return false;
}
for (String neg : NEGATIVE_KEYWORDS) {
if (value.contains(neg)) {
return true;
}
}
return false;
}
/**
* 取 ID 中第一个 '_' 之前的部分。例如 "1" → "1""1_2" → "1"null → ""。
* 与两个 TaskService 内的 {@code baseId} 实现保持一致。
*/
private static String baseId(String id) {
if (id == null) {
return "";
}
String s = id.trim();
int idx = s.indexOf('_');
return idx > 0 ? s.substring(0, idx) : s;
}
}