明确进度与取消契约

This commit is contained in:
张家训
2026-07-31 19:44:23 +08:00
parent c029b367f6
commit a9e9d1cc3e
9 changed files with 148 additions and 11 deletions
@@ -161,7 +161,7 @@ public class MyBatisExportWindow {
modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn);
OperationStatusPane progressRow = new OperationStatusPane(
exportOperation, fsBody() - 2);
exportOperation, fsBody() - 2, true);
// === 日志区域 ===
Label logTitle = new Label("执行日志");
@@ -24,19 +24,19 @@ public final class RunSqlExportUseCase {
new MyBatisExportService.ExportCallback() {
@Override
public void onProjectStart(int current, int total, String projectName) {
context.report(current + "/" + total + " " + projectName,
total == 0 ? 0 : (double) (current - 1) / total);
progressConsumer.accept(new SqlExportProgress(
current, total, projectName, 0));
SqlExportProgress progress = new SqlExportProgress(
current, total, projectName, 0);
context.report(progress.message(), progress.fraction());
progressConsumer.accept(progress);
}
@Override
public void onProjectDone(int current, int total,
String projectName, int recordCount) {
context.report(current + "/" + total + " " + projectName,
total == 0 ? 0 : (double) current / total);
progressConsumer.accept(new SqlExportProgress(
current, total, projectName, recordCount));
SqlExportProgress progress = new SqlExportProgress(
current, total, projectName, recordCount);
context.report(progress.message(), progress.fraction());
progressConsumer.accept(progress);
}
@Override
@@ -2,7 +2,24 @@ package com.example.workutils.biz.mybatisexport.application;
public record SqlExportProgress(int current, int total, String projectName, int recordCount) {
public SqlExportProgress {
projectName = projectName == null ? "" : projectName.trim();
if (current < 0 || total < 0 || recordCount < 0) {
throw new IllegalArgumentException("progress counts must not be negative");
}
if (current > total) {
throw new IllegalArgumentException("current must not exceed total");
}
if (projectName.isEmpty()) {
throw new IllegalArgumentException("projectName must not be blank");
}
}
public double fraction() {
return total == 0 ? 0 : (double) current / total;
}
public String message() {
return current + "/" + total + " " + projectName;
}
}
@@ -15,11 +15,11 @@ public final class RunScanUseCase {
}
public Result execute(ScanRequest request, OperationContext context) throws Exception {
context.report("正在扫描项目", -1);
report(context, new ScanProgress("正在扫描项目", -1));
ScannerService.ScanResult scanResult = scannerService.scanAllProjects(
ScannerService.ScanType.KEYWORD, request.effectivePattern());
context.report("正在生成 CSV", -1);
report(context, new ScanProgress("正在生成 CSV", -1));
String fileName = outputHelper.generateOutputFileName("scan_result", "csv");
scannerService.exportScanResults(scanResult, fileName);
return new Result(scanResult, fileName);
@@ -27,4 +27,8 @@ public final class RunScanUseCase {
public record Result(ScannerService.ScanResult scanResult, String fileName) {
}
private void report(OperationContext context, ScanProgress progress) {
context.report(progress.message(), progress.progress());
}
}
@@ -1,4 +1,14 @@
package com.example.workutils.biz.scanner.application;
public record ScanProgress(String message, double progress) {
public ScanProgress {
message = message == null ? "" : message.trim();
if (message.isEmpty()) {
throw new IllegalArgumentException("message must not be blank");
}
if (Double.isNaN(progress) || progress < -1 || progress > 1) {
throw new IllegalArgumentException("progress must be -1 or between 0 and 1");
}
}
}
@@ -0,0 +1,35 @@
package com.example.workutils.biz.mybatisexport.application;
import com.example.workutils.biz.mybatisexport.service.MyBatisExportService;
import com.example.workutils.system.ui.async.OperationContext;
import com.example.workutils.system.ui.async.OperationContextTestSupport;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RunSqlExportCancellationTest {
@Test
void exposesOperationCancellationToServiceCallback() throws Exception {
AtomicBoolean cancellationObserved = new AtomicBoolean();
MyBatisExportService service = new MyBatisExportService() {
@Override
public ExportResult exportWithCallback(
String prefix, String mode, String value, ExportCallback callback) {
cancellationObserved.set(callback.isCancelled());
return null;
}
};
OperationContext context = OperationContextTestSupport.withCancellation(() -> true);
RunSqlExportUseCase useCase = new RunSqlExportUseCase(service);
useCase.execute(
SqlExportRequest.create(SqlExportRequest.FilterMode.ALL, ""),
context,
progress -> { });
assertTrue(cancellationObserved.get());
}
}
@@ -0,0 +1,34 @@
package com.example.workutils.biz.mybatisexport.application;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class SqlExportProgressTest {
@Test
void derivesStableMessageAndFraction() {
SqlExportProgress preparing = new SqlExportProgress(0, 0, " 准备 ", 0);
SqlExportProgress done = new SqlExportProgress(2, 2, "project", 5);
assertEquals("0/0 准备", preparing.message());
assertEquals(0, preparing.fraction());
assertEquals("2/2 project", done.message());
assertEquals(1, done.fraction());
}
@Test
void rejectsInvalidCountsAndProjectName() {
assertThrows(IllegalArgumentException.class,
() -> new SqlExportProgress(-1, 1, "project", 0));
assertThrows(IllegalArgumentException.class,
() -> new SqlExportProgress(2, 1, "project", 0));
assertThrows(IllegalArgumentException.class,
() -> new SqlExportProgress(0, -1, "project", 0));
assertThrows(IllegalArgumentException.class,
() -> new SqlExportProgress(0, 0, " ", 0));
assertThrows(IllegalArgumentException.class,
() -> new SqlExportProgress(0, 0, "project", -1));
}
}
@@ -0,0 +1,24 @@
package com.example.workutils.biz.scanner.application;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ScanProgressTest {
@Test
void acceptsAndNormalizesSupportedProgressValues() {
assertEquals("扫描中", new ScanProgress(" 扫描中 ", -1).message());
assertEquals(0, new ScanProgress("开始", 0).progress());
assertEquals(1, new ScanProgress("完成", 1).progress());
}
@Test
void rejectsBlankMessageAndInvalidProgress() {
assertThrows(IllegalArgumentException.class, () -> new ScanProgress(" ", 0));
assertThrows(IllegalArgumentException.class, () -> new ScanProgress("扫描", Double.NaN));
assertThrows(IllegalArgumentException.class, () -> new ScanProgress("扫描", -1.1));
assertThrows(IllegalArgumentException.class, () -> new ScanProgress("扫描", 1.1));
}
}
@@ -0,0 +1,13 @@
package com.example.workutils.system.ui.async;
import java.util.function.BooleanSupplier;
public final class OperationContextTestSupport {
private OperationContextTestSupport() {
}
public static OperationContext withCancellation(BooleanSupplier cancellationRequested) {
return new OperationContext((message, progress) -> { }, cancellationRequested);
}
}