Files
crawler-plugin/backend-java/src/test/java/com/nanri/aiimage/config/IndexMigrationV101Test.java
T
huangzd1997 fa5a59e5cd
Build Backend JAR / build (push) Has been cancelled
task-116: 后台管理新增/导入表单弹窗化 + 分组管理独立菜单与UI优化 + 轻量进度端点
- 后台管理页(admin)所有面板的新增/导入表单改为弹窗操作,原有字段 ID 全部保留、提交逻辑不变;导入删除入口不再触发二次确认拦截
- 分组管理升级为独立菜单(V102 + schema initializer),移除 5 个面板内的管理分组按钮;分组列表改为蓝白主题、增加权限分组横幅
- Python 侧 group-manage 权限守卫(_ensure_backend_menu_access 补充 group-manage)
- 引入 V101(biz_task_file_job 复合索引)+ 新增 TaskProgressLight/TaskFileJob 轻量端点与进度聚合作
- 前端 progress-light / page-separated-loads / dispatch-guard 共享模块及单元测试
2026-09-01 12:54:13 +08:00

133 lines
5.6 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.nanri.aiimage.config;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 任务 116:索引迁移 V{N+1}(审计清单落地)。
* 迁移文件存在且版本续接;按 115 审计清单追加 (module_type, job_type, task_id) 索引;
* 含验证 SQL、锁表风险标注、回滚步骤;不改历史迁移。
*/
class IndexMigrationV101Test {
private static final Path DB_DIR = Paths.get("src", "main", "resources", "db");
private static final Path MIGRATION = DB_DIR.resolve("V101__task_file_job_module_type_task_index.sql");
private static final Path AUDIT = Paths.get("src", "main", "resources", "..", "..", "..",
"docs", "explain-index-audit.md").normalize();
private static String read(Path path) throws IOException {
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
}
private static List<Path> allMigrations() throws IOException {
try (Stream<Path> stream = Files.list(DB_DIR)) {
return stream.filter(p -> p.getFileName().toString().matches("V\\d+__.*\\.sql"))
.sorted()
.toList();
}
}
private static int versionOf(Path path) {
String name = path.getFileName().toString();
return Integer.parseInt(name.substring(1, name.indexOf('_')));
}
@Test
void test_migration_file_exists() {
assertTrue(Files.isRegularFile(MIGRATION), "迁移文件必须存在: " + MIGRATION);
}
@Test
void test_migration_version_sequential() throws Exception {
List<Path> all = allMigrations();
assertTrue(!all.isEmpty(), "存在迁移文件");
int maxVersion = all.stream().mapToInt(IndexMigrationV101Test::versionOf).max().orElse(0);
assertTrue(maxVersion >= 100, "现有迁移最大版本 >= V100: " + maxVersion);
assertTrue(versionOf(MIGRATION) == 101, "新迁移版本号 V101 续接 V100 之后");
}
@Test
void test_migration_index_created() throws Exception {
String sql = read(MIGRATION);
assertTrue(sql.contains("biz_task_file_job"), "迁移作用于 biz_task_file_job");
assertTrue(sql.contains("idx_file_job_module_type_task"), "索引名与审计清单一致");
assertTrue(sql.contains("module_type") && sql.contains("job_type") && sql.contains("task_id"),
"索引列为 (module_type, job_type, task_id)");
assertTrue(sql.contains("ADD INDEX"), "使用 ADD INDEX");
}
@Test
void test_migration_conditional_repeatable() throws Exception {
String sql = read(MIGRATION);
assertTrue(sql.contains("information_schema.STATISTICS"), "通过 information_schema 判存在");
assertTrue(sql.contains("INDEX_NAME"), "按索引名判断");
assertTrue(sql.contains("PREPARE") && sql.contains("EXECUTE"), "条件执行(幂等,可重复运行)");
}
@Test
void test_migration_validated() throws Exception {
String sql = read(MIGRATION);
assertTrue(sql.contains("SELECT 1"), "验证 SQL(幂等分支)");
String audit = read(AUDIT);
assertTrue(audit.contains("idx_file_job_module_type_task"), "审计清单已列出该索引");
assertTrue(audit.contains("收益") && audit.contains("风险"), "审计清单含收益/风险");
}
@Test
void test_explain_improved() throws Exception {
String audit = read(AUDIT);
assertTrue(audit.contains("findAssembleJobsByTaskIds"), "审计覆盖 progress/light Job 查询");
assertTrue(audit.contains("执行后重跑") || audit.contains("前后计划")
|| audit.contains("对比"), "审计含迁移前后 EXPLAIN 对比步骤");
}
@Test
void test_migration_no_alter_legacy() throws Exception {
List<Path> all = allMigrations();
List<Path> others = all.stream()
.filter(p -> versionOf(p) != 101)
.toList();
for (Path legacy : others) {
String content = read(legacy);
assertFalse(content.contains("idx_file_job_module_type_task"),
"历史迁移不得包含新索引: " + legacy.getFileName());
}
}
@Test
void test_migration_rollback_doc() throws Exception {
String sql = read(MIGRATION);
assertTrue(sql.contains("DROP INDEX") || sql.contains("drop index"),
"迁移文件含回滚语句(DROP INDEX idx_file_job_module_type_task");
String audit = read(AUDIT);
assertTrue(audit.contains("回滚") || audit.contains("DROP"),
"审计文档含回滚步骤说明");
}
@Test
void test_migration_lock_window() throws Exception {
String sql = read(MIGRATION);
assertTrue(sql.contains("LOCK") || sql.contains("低峰") || sql.contains("lock")
|| sql.contains("窗口"), "迁移标注锁表风险/窗口");
}
@Test
void test_migration_backward_safe() throws Exception {
String sql = read(MIGRATION);
assertTrue(sql.contains("ALTER TABLE"), "普通 BTREE 索引(不重建表、不删列)");
assertFalse(sql.contains("DROP COLUMN"), "不删列");
assertFalse(sql.contains("RENAME"), "不重命名");
assertTrue(sql.contains("ADD INDEX"), "仅追加索引,向后兼容");
}
}