task-116: 后台管理新增/导入表单弹窗化 + 分组管理独立菜单与UI优化 + 轻量进度端点
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
- 后台管理页(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 共享模块及单元测试
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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 static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 任务 115:高频查询 EXPLAIN 索引审计(文档产出验证)。
|
||||
* 审计文档必须覆盖 history/progress/dashboard 三类高频查询的执行计划结论、
|
||||
* 候选索引清单(表/列/类型/收益/风险)、全表扫描标记、可重复执行步骤,
|
||||
* 且本任务明确不做 DDL(只审计,不加索引)。
|
||||
*/
|
||||
class ExplainIndexAuditDocTest {
|
||||
|
||||
private static final Path AUDIT = Paths.get("src", "main", "resources", "..", "..", "..",
|
||||
"docs", "explain-index-audit.md").normalize();
|
||||
private static final Path FLYWAY_SPEC = Paths.get("src", "main", "resources", "..", "..", "..",
|
||||
"docs", "specs", "12-flyway-and-inspection.md").normalize();
|
||||
|
||||
private static String read(Path path) throws IOException {
|
||||
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static List<String> linesOf(Path path) throws IOException {
|
||||
return List.of(read(path).split("\r?\n"));
|
||||
}
|
||||
|
||||
private static String lineContaining(String content, String needle) {
|
||||
for (String line : content.split("\r?\n")) {
|
||||
if (line.contains(needle)) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_explain_history_plan() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("history"), "审计文档覆盖 history 查询");
|
||||
assertTrue(doc.contains("EXPLAIN"), "审计文档包含 EXPLAIN 执行计划记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_explain_progress_plan() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("progress"), "审计文档覆盖 progress 查询");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_explain_dashboard_plan() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("dashboard"), "审计文档覆盖 dashboard 查询");
|
||||
assertTrue(doc.contains("GROUP BY"), "dashboard 聚合查询计划已记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_full_scan_detected() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("type=ALL") || doc.contains("全表扫描") || doc.contains("ALL")
|
||||
|| doc.contains("possible_keys") || doc.contains("扫描"),
|
||||
"审计文档记录执行计划扫描类型(full scan 标记)");
|
||||
String line = lineContaining(doc, "idx_file_job_task");
|
||||
assertTrue(line != null && line.contains("task_id"),
|
||||
"审计文档指出 task_id 前缀索引对 (module_type, job_type, task_id IN) 的局限");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_index_candidates_listed() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("idx_file_job_module_type_task")
|
||||
|| doc.contains("module_type") && doc.contains("job_type") && doc.contains("task_id"),
|
||||
"候选索引清单含 (module_type, job_type, task_id) 组合");
|
||||
assertTrue(doc.contains("收益") && doc.contains("风险"),
|
||||
"候选索引清单含收益与风险说明");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_plan_documented() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("biz_task_file_job"), "审计文档引用目标表");
|
||||
assertTrue(doc.contains("biz_file_task") || doc.contains("biz_file_result"),
|
||||
"审计文档引用业务任务/结果表");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_plan_repeatable() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("EXPLAIN SELECT") || doc.contains("EXPLAIN")
|
||||
&& doc.contains("手工执行") || doc.contains("执行"),
|
||||
"审计文档包含可重复执行的 EXPLAIN SQL 步骤");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_no_ddl_this_task() throws Exception {
|
||||
String audit = read(AUDIT);
|
||||
String spec = read(FLYWAY_SPEC);
|
||||
assertTrue(audit.contains("不做 DDL") || audit.contains("本任务无 DDL")
|
||||
|| audit.contains("不新增索引") || audit.contains("只审计"),
|
||||
"审计文档明确本任务不做 DDL");
|
||||
assertFalse(audit.contains("ALTER TABLE"),
|
||||
"审计文档不包含 ALTER TABLE(DDL 属于后续任务 116)");
|
||||
assertTrue(spec.contains("只读") || spec.contains("巡检"),
|
||||
"12 spec 巡检报表模式与审计文档对应");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_audit_reflects_batch_loading_queries() throws Exception {
|
||||
String doc = read(AUDIT);
|
||||
assertTrue(doc.contains("findAssembleJobsByTaskIds"), "审计覆盖轻量进度 Job 批量查询");
|
||||
assertTrue(doc.contains("findAssembleJobsByResultIds"), "审计覆盖结果列表 Job 批量查询");
|
||||
assertTrue(doc.contains("selectMaps") || doc.contains("GROUP BY"), "审计覆盖 dashboard 聚合查询");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
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"), "仅追加索引,向后兼容");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user