|
|
@@ -166,12 +166,16 @@ public class AiSmartCleanService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- // 判定并发:闸门 + 虚拟线程,结果按批次聚合
|
|
|
+ // 判定并发:闸门 + 虚拟线程,结果按批次聚合。
|
|
|
+ // ★ 判定任务跑在独立虚拟线程上,ScopedValue 绑定不会跨线程传播,
|
|
|
+ // 必须用 runWith 显式带上案件上下文,否则 judge 里的 slave 查询
|
|
|
+ // 会因路由不到案件库抛「请先打开案件后再操作数据」,整批判定全灭。
|
|
|
Semaphore gate = new Semaphore(Math.max(1, maxConcurrent), true);
|
|
|
+ Long judgeUserId = CaseContextHolder.getUserId();
|
|
|
List<AiJudgeResult.ExistingHit> existingHits = java.util.Collections.synchronizedList(new ArrayList<>());
|
|
|
Map<Long, AiJudgeResult.AiPlan> plans = new ConcurrentHashMap<>();
|
|
|
List<CompletableFuture<Void>> futures = pending.stream()
|
|
|
- .map(p -> CompletableFuture.runAsync(() -> {
|
|
|
+ .map(p -> CompletableFuture.runAsync(() -> CaseContextHolder.runWith(caseId, judgeUserId, () -> {
|
|
|
// 预算护栏(LiteLLM budget 同款思路):近似检查,超限就把剩余表转人工,不再烧钱。
|
|
|
// 放在 acquire 之前 —— 不为一张注定不判的表占闸门;精确性不重要,兜底才重要
|
|
|
if (batchTokenBudget > 0 && st.tokens.get() >= batchTokenBudget) {
|
|
|
@@ -202,7 +206,7 @@ public class AiSmartCleanService {
|
|
|
// 三种结论都算「判完」,异常也算:否则前端进度会永远差几条卡在 99%
|
|
|
bump(batchId, "aiJudged");
|
|
|
}
|
|
|
- }, executor)).toList();
|
|
|
+ }), executor)).toList();
|
|
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
|
|
|
// 判定全部归零之后才能进清洗阶段,否则会漏掉 AI 命中的表
|
|
|
@@ -259,6 +263,9 @@ public class AiSmartCleanService {
|
|
|
}
|
|
|
case AiJudgeResult.Skip r -> {
|
|
|
aiCleanService.finish(p.job(), AiCleanStatusEnum.SKIP, r.reason(), System.currentTimeMillis());
|
|
|
+ // 跳过的表也要进待人工明细:「源文件不存在」「结构探测不可用」用户必须能看到,
|
|
|
+ // 否则批次计数(needReview)与清单条数对不上,前端看起来像丢了几条
|
|
|
+ addReview(batchId, p, r.reason());
|
|
|
st.needReview.incrementAndGet();
|
|
|
bump(batchId, "needReview");
|
|
|
}
|
|
|
@@ -450,8 +457,8 @@ public class AiSmartCleanService {
|
|
|
|
|
|
private void addReview(Long batchId, PendingAi p, String reason) {
|
|
|
reviewItems.computeIfAbsent(batchId, k -> java.util.Collections.synchronizedList(new ArrayList<>()))
|
|
|
- .add(new NeedReviewItem(p.sheet().getId(), p.root().getFileName(),
|
|
|
- p.sheet().getSheetName(), reason));
|
|
|
+ .add(new NeedReviewItem(p.job().getId(), p.job().getFileId(), p.sheet().getId(),
|
|
|
+ p.root().getFileName(), p.sheet().getSheetName(), reason));
|
|
|
// 明细也落库:进程重启后前端仍能看到「哪些表需要人工」。
|
|
|
// 序列化前先在容器锁内做快照拷贝,避免边并发 add 边遍历(CME/撕裂 JSON)
|
|
|
List<NeedReviewItem> list = reviewItems.get(batchId);
|
|
|
@@ -501,8 +508,12 @@ public class AiSmartCleanService {
|
|
|
private record PendingAi(FileInfo root, FileInfo sheet, AiCleanJob job) {
|
|
|
}
|
|
|
|
|
|
- /** 交人工提示项,前端据此跳手动清洗 */
|
|
|
- public record NeedReviewItem(Long sheetId, String fileName, String sheetName, String reason) {
|
|
|
+ /**
|
|
|
+ * 交人工提示项,前端据此跳手动清洗。jobId/fileId 让前端能定位到 job 与文件根
|
|
|
+ * (进度页跳转、清单去重、详情查询都靠它们)。
|
|
|
+ */
|
|
|
+ public record NeedReviewItem(Long jobId, Long fileId, Long sheetId, String fileName, String sheetName,
|
|
|
+ String reason) {
|
|
|
}
|
|
|
|
|
|
/** 受理响应 */
|