更新这个货源
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+216
@@ -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 -> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user