新增培训:完善报到与考勤V2业务操作
This commit is contained in:
1 parent
7193f2ac3a
commit
6762189093
30 files changed
+2382
-9
No files matched your search
@@ -38,6 +38,15 @@ public class DbAssertionChecker {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("--cleanup-dependents".equals(args[0])) {
|
||||
if (args.length < 2) {
|
||||
System.err.println("Missing resources json path for cleanup-dependents");
|
||||
System.exit(1);
|
||||
}
|
||||
executeDependentsCleanup(args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("--disable-student".equals(args[0])) {
|
||||
if (args.length < 2) {
|
||||
System.err.println("Missing student id");
|
||||
@@ -121,6 +130,34 @@ public class DbAssertionChecker {
|
||||
assertions.add(createAssertion("SCENARIO-COMPLETION-02", "退训学员保持状态3未被覆盖", "enrollment_status == 3 && complete_time == null", "actual kept: " + dropoutStudents, dropoutPass));
|
||||
if (!dropoutPass) allPassed = false;
|
||||
|
||||
// 8. 业务场景断言:无孤立报到记录 (orphan report = 0)
|
||||
int orphanReports = queryCount(conn,
|
||||
"SELECT COUNT(*) FROM train_class_report r LEFT JOIN train_enrollment e ON r.enrollment_id = e.enrollment_id WHERE e.enrollment_id IS NULL");
|
||||
boolean orphanReportPass = orphanReports == 0;
|
||||
assertions.add(createAssertion("CONSISTENCY-05", "无孤立报到记录(关联无效报名)", "orphan_report_count == 0", "actual: " + orphanReports, orphanReportPass));
|
||||
if (!orphanReportPass) allPassed = false;
|
||||
|
||||
// 9. 业务场景断言:无孤立考勤记录 (orphan checkin = 0)
|
||||
int orphanCheckins = queryCount(conn,
|
||||
"SELECT COUNT(*) FROM train_student_checkin c LEFT JOIN train_enrollment e ON c.enrollment_id = e.enrollment_id WHERE e.enrollment_id IS NULL");
|
||||
boolean orphanCheckinPass = orphanCheckins == 0;
|
||||
assertions.add(createAssertion("CONSISTENCY-06", "无孤立考勤记录(关联无效报名)", "orphan_checkin_count == 0", "actual: " + orphanCheckins, orphanCheckinPass));
|
||||
if (!orphanCheckinPass) allPassed = false;
|
||||
|
||||
// 10. 业务场景断言:人工修正考勤留痕一致性 (manual_flag = 1 记录存在且状态有效 0~4)
|
||||
int manualCheckins = queryCount(conn,
|
||||
"SELECT COUNT(*) FROM train_student_checkin WHERE manual_flag = 1 AND attendance_status IN (0, 1, 2, 3, 4)");
|
||||
boolean manualPass = manualCheckins >= 1;
|
||||
assertions.add(createAssertion("SCENARIO-ATTENDANCE-01", "人工修正考勤留痕manual_flag=1且状态合法", "manual_flag == 1", "actual count: " + manualCheckins, manualPass));
|
||||
if (!manualPass) allPassed = false;
|
||||
|
||||
// 11. 业务场景断言:报到记录状态合法性 (报到学员的 enrollment_status 应为 1 或 4,不能为 0 或 3)
|
||||
int invalidReportEnrollments = queryCount(conn,
|
||||
"SELECT COUNT(*) FROM train_class_report r JOIN train_enrollment e ON r.enrollment_id = e.enrollment_id WHERE e.enrollment_status NOT IN (1, 4)");
|
||||
boolean validReportEnrollPass = invalidReportEnrollments == 0;
|
||||
assertions.add(createAssertion("SCENARIO-REPORT-01", "报到学员关联报名状态必须为已报到(1)或已完成(4)", "invalid_report_enrollment_status == 0", "actual invalid: " + invalidReportEnrollments, validReportEnrollPass));
|
||||
if (!validReportEnrollPass) allPassed = false;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("Database assertion execution failed: " + e.getMessage());
|
||||
assertions.add(createAssertion("DB-ERROR", "数据库连接或查询异常", "no exception", "error: " + e.getMessage(), false));
|
||||
@@ -219,6 +256,29 @@ public class DbAssertionChecker {
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
}
|
||||
|
||||
private static void executeDependentsCleanup(String resourcesPath) {
|
||||
String dbUrl = "jdbc:kingbase8://127.0.0.1:4321/training?currentSchema=public,sys_catalog&useUnicode=true&characterEncoding=utf8";
|
||||
String dbUser = "kingbase";
|
||||
String dbPassword = resolveDbPassword();
|
||||
|
||||
try {
|
||||
String resourcesJson = new String(Files.readAllBytes(Paths.get(resourcesPath)), "UTF-8");
|
||||
try (Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPassword)) {
|
||||
try (Statement s = conn.createStatement()) {
|
||||
s.execute("SET search_path TO public, sys_catalog");
|
||||
}
|
||||
deleteByIds(conn, "train_student_checkin", "checkin_id", extractIds(resourcesJson, "checkins"), true);
|
||||
deleteByIds(conn, "train_class_report", "report_id", extractIds(resourcesJson, "reports"), true);
|
||||
deleteByIds(conn, "train_student_checkin", "enrollment_id", extractIds(resourcesJson, "enrollments"), true);
|
||||
deleteByIds(conn, "train_student_checkin", "lesson_id", extractIds(resourcesJson, "lessons"), true);
|
||||
deleteByIds(conn, "train_class_report", "enrollment_id", extractIds(resourcesJson, "enrollments"), true);
|
||||
System.out.println("DependentsCleanup completed successfully.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("DependentsCleanup failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void executeExactIdCleanup(String resourcesPath) {
|
||||
String dbUrl = "jdbc:kingbase8://127.0.0.1:4321/training?currentSchema=public,sys_catalog&useUnicode=true&characterEncoding=utf8";
|
||||
String dbUser = "kingbase";
|
||||
@@ -231,6 +291,13 @@ public class DbAssertionChecker {
|
||||
s.execute("SET search_path TO public, sys_catalog");
|
||||
}
|
||||
|
||||
// 优先删除外键从表依赖记录 (考勤、报到)
|
||||
deleteByIds(conn, "train_student_checkin", "checkin_id", extractIds(resourcesJson, "checkins"), true);
|
||||
deleteByIds(conn, "train_class_report", "report_id", extractIds(resourcesJson, "reports"), true);
|
||||
deleteByIds(conn, "train_student_checkin", "enrollment_id", extractIds(resourcesJson, "enrollments"), true);
|
||||
deleteByIds(conn, "train_student_checkin", "lesson_id", extractIds(resourcesJson, "lessons"), true);
|
||||
deleteByIds(conn, "train_class_report", "enrollment_id", extractIds(resourcesJson, "enrollments"), true);
|
||||
|
||||
deleteByIds(conn, "train_lesson", "lesson_id", extractIds(resourcesJson, "lessons"), true);
|
||||
deleteByIds(conn, "train_enrollment", "enrollment_id", extractIds(resourcesJson, "enrollments"), true);
|
||||
deleteByIds(conn, "train_electronic_board", "board_id", extractIds(resourcesJson, "boards"), false);
|
||||
|
||||
@@ -65,7 +65,9 @@ created_resources = {
|
||||
"plan_courses": [],
|
||||
"enrollments": [],
|
||||
"lessons": [],
|
||||
"boards": []
|
||||
"boards": [],
|
||||
"reports": [],
|
||||
"checkins": []
|
||||
}
|
||||
|
||||
api_results = []
|
||||
@@ -75,7 +77,8 @@ scenario_stats = {
|
||||
"SCENARIO-02": {"name": "Enrollment Exception & Student Status Validation", "passed": False, "steps": []},
|
||||
"SCENARIO-03": {"name": "Scheduling Conflict Warning & Hard Blocks", "passed": False, "steps": []},
|
||||
"SCENARIO-04": {"name": "Electronic Board Binding & Timetable Mapping", "passed": False, "steps": []},
|
||||
"SCENARIO-05": {"name": "Plan Completion Lifecycle & History Retention", "passed": False, "steps": []}
|
||||
"SCENARIO-05": {"name": "Plan Completion Lifecycle & History Retention", "passed": False, "steps": []},
|
||||
"SCENARIO-06": {"name": "Report & Attendance Lifecycle", "passed": False, "steps": []}
|
||||
}
|
||||
|
||||
TOKEN = None
|
||||
@@ -860,7 +863,9 @@ def run_scenario_04():
|
||||
|
||||
# 此时该教室绑定了2块板 (>1块) -> Timetable 中的 boardSn 应该为 null
|
||||
_, tt2 = request_api("GET", f"/train/plans/{p_id}/timetable")
|
||||
tt2_sn = tt2.get("data", {}).get("days", [{}])[0].get("lessons", [{}])[0].get("boardSn")
|
||||
days2 = tt2.get("data", {}).get("days", []) if isinstance(tt2, dict) and isinstance(tt2.get("data"), dict) else []
|
||||
lessons2 = days2[0].get("lessons", []) if days2 else []
|
||||
tt2_sn = lessons2[0].get("boardSn") if lessons2 else None
|
||||
step_tt_multi_ok = (tt2_sn is None)
|
||||
steps.append({"sub": "大于1块板Timetable boardSn为null", "boardSn": tt2_sn, "pass": step_tt_multi_ok})
|
||||
if not step_tt_multi_ok: success = False
|
||||
@@ -870,7 +875,9 @@ def run_scenario_04():
|
||||
|
||||
# 此时该教室只剩恰好1块有效板 -> Timetable 中的 boardSn 应该为 sn1
|
||||
_, tt1 = request_api("GET", f"/train/plans/{p_id}/timetable")
|
||||
tt1_sn = tt1.get("data", {}).get("days", [{}])[0].get("lessons", [{}])[0].get("boardSn")
|
||||
days1 = tt1.get("data", {}).get("days", []) if isinstance(tt1, dict) and isinstance(tt1.get("data"), dict) else []
|
||||
lessons1 = days1[0].get("lessons", []) if days1 else []
|
||||
tt1_sn = lessons1[0].get("boardSn") if lessons1 else None
|
||||
step_tt_single_ok = (tt1_sn == sn1)
|
||||
steps.append({"sub": "恰好1块板Timetable boardSn为对应SN", "boardSn": tt1_sn, "pass": step_tt_single_ok})
|
||||
if not step_tt_single_ok: success = False
|
||||
@@ -967,6 +974,263 @@ def run_scenario_05():
|
||||
log(f"SCENARIO-05 结果: {'PASS' if success else 'FAIL'}")
|
||||
return success
|
||||
|
||||
# ==============================================================================
|
||||
# SCENARIO-06: Report & Attendance Lifecycle
|
||||
# ==============================================================================
|
||||
def run_scenario_06():
|
||||
log("=== 开始执行 SCENARIO-06: Report & Attendance Lifecycle ===")
|
||||
steps = scenario_stats["SCENARIO-06"]["steps"]
|
||||
success = True
|
||||
|
||||
now = datetime.now()
|
||||
t_start = (now + timedelta(days=6)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
t_end = (now + timedelta(days=10)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
t_deadline = (now + timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 1. 准备 Teacher
|
||||
teacher_name = f"{RUN_ID}_教师_考勤测试"
|
||||
t_phone = random_phone("138")
|
||||
t_id_custom = f"{RUN_ID}_T06"
|
||||
status, body = request_api("POST", "/train/teacher", {
|
||||
"tId": t_id_custom,
|
||||
"tName": teacher_name,
|
||||
"tPhone": t_phone,
|
||||
"gender": "0",
|
||||
"title": "高级讲师"
|
||||
})
|
||||
t_id = extract_id(body) or t_id_custom
|
||||
if t_id: created_resources["teachers"].append(t_id)
|
||||
step1_ok = (status == 200 and t_id is not None)
|
||||
steps.append({"step": 1, "action": "准备Teacher", "status": status, "id": t_id, "pass": step1_ok})
|
||||
if not step1_ok: success = False
|
||||
|
||||
# 2. 准备 Course
|
||||
course_name = f"{RUN_ID}_课程_信号维修"
|
||||
course_code = f"{RUN_ID}_C06"
|
||||
c_id_custom = f"{RUN_ID}_C06"
|
||||
status, body = request_api("POST", "/train/course", {
|
||||
"cId": c_id_custom,
|
||||
"cName": course_name,
|
||||
"cCode": course_code,
|
||||
"cDuration": 8,
|
||||
"status": 1
|
||||
})
|
||||
c_id = extract_id(body) or c_id_custom
|
||||
if c_id: created_resources["courses"].append(c_id)
|
||||
step2_ok = (status == 200 and c_id is not None)
|
||||
steps.append({"step": 2, "action": "准备Course", "status": status, "id": c_id, "pass": step2_ok})
|
||||
if not step2_ok: success = False
|
||||
|
||||
# 3. 创建 Classroom
|
||||
room_name = f"{RUN_ID}_教室_601"
|
||||
cr_id_custom = f"{RUN_ID}_CR06"
|
||||
status, body = request_api("POST", "/train/classroom", {
|
||||
"classRoomId": cr_id_custom,
|
||||
"roomName": room_name,
|
||||
"roomLocation": "实训楼601",
|
||||
"capacity": 30,
|
||||
"roomStatus": 1
|
||||
})
|
||||
cr_id = extract_id(body) or cr_id_custom
|
||||
if cr_id: created_resources["classrooms"].append(cr_id)
|
||||
step3_ok = (status == 200 and cr_id is not None)
|
||||
steps.append({"step": 3, "action": "创建Classroom", "status": status, "id": cr_id, "pass": step3_ok})
|
||||
if not step3_ok: success = False
|
||||
|
||||
# 4. 创建 Student
|
||||
s_name = f"{RUN_ID}_学员_赵六"
|
||||
status, body = request_api("POST", "/train/student", {
|
||||
"sName": s_name,
|
||||
"sGender": "1",
|
||||
"sPhone": random_phone("139"),
|
||||
"orgName": "京局北京电务段"
|
||||
})
|
||||
s_id = extract_id(body)
|
||||
if not s_id:
|
||||
_, list_body = request_api("GET", f"/train/student/list?sName={s_name}")
|
||||
rows = list_body.get("rows", [])
|
||||
if rows: s_id = rows[0].get("sId")
|
||||
if s_id: created_resources["students"].append(s_id)
|
||||
step4_ok = (status == 200 and s_id is not None)
|
||||
steps.append({"step": 4, "action": "创建Student", "status": status, "id": s_id, "pass": step4_ok})
|
||||
if not step4_ok: success = False
|
||||
|
||||
# 5. 创建 Plan (报到截止时间在未来,确保在有效报到窗口)
|
||||
plan_name = f"{RUN_ID}_培训班_信号班"
|
||||
status, body = request_api("POST", "/train/plans", {
|
||||
"planName": plan_name,
|
||||
"startTime": t_start,
|
||||
"endTime": t_end,
|
||||
"reportDeadline": t_deadline,
|
||||
"recruitNum": 20,
|
||||
"trainAddress": "北京电务培训段"
|
||||
})
|
||||
plan_id = extract_id(body)
|
||||
if plan_id: created_resources["plans"].append(plan_id)
|
||||
step5_ok = (status == 200 and plan_id is not None)
|
||||
steps.append({"step": 5, "action": "创建Plan", "status": status, "id": plan_id, "pass": step5_ok})
|
||||
if not step5_ok: success = False
|
||||
|
||||
# 6. 配置 PlanCourse
|
||||
status, body = request_api("POST", f"/train/plans/{plan_id}/courses", {
|
||||
"cIds": [c_id],
|
||||
"remark": "信号维修基础"
|
||||
})
|
||||
_, pc_list_body = request_api("GET", f"/train/plans/{plan_id}/courses")
|
||||
plan_course_id = None
|
||||
pc_data = pc_list_body.get("data", [])
|
||||
if pc_data:
|
||||
plan_course_id = pc_data[0].get("planCourseId")
|
||||
created_resources["plan_courses"].append(plan_course_id)
|
||||
step6_ok = (status == 200 and plan_course_id is not None)
|
||||
steps.append({"step": 6, "action": "配置PlanCourse", "status": status, "id": plan_course_id, "pass": step6_ok})
|
||||
if not step6_ok: success = False
|
||||
|
||||
# 7. 学员入班 (Enrollment 初始状态 0:待报到)
|
||||
status, body = request_api("POST", f"/train/plans/{plan_id}/enrollments", {
|
||||
"studentId": s_id,
|
||||
"sourceType": 1,
|
||||
"accommodationRequired": 0,
|
||||
"remark": "信号班学员"
|
||||
})
|
||||
enrollment_id = extract_id(body)
|
||||
if not enrollment_id:
|
||||
_, e_list_body = request_api("GET", f"/train/plans/{plan_id}/enrollments")
|
||||
rows = e_list_body.get("rows", [])
|
||||
if rows: enrollment_id = rows[0].get("enrollmentId")
|
||||
if enrollment_id: created_resources["enrollments"].append(enrollment_id)
|
||||
step7_ok = (status == 200 and enrollment_id is not None)
|
||||
steps.append({"step": 7, "action": "学员Enrollment入班", "status": status, "id": enrollment_id, "pass": step7_ok})
|
||||
if not step7_ok: success = False
|
||||
|
||||
# 8. 排课 Lesson
|
||||
lesson_start = (now + timedelta(days=6, hours=1)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
lesson_end = (now + timedelta(days=6, hours=3)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
status, body = request_api("POST", f"/train/plans/{plan_id}/lessons", {
|
||||
"planCourseId": plan_course_id,
|
||||
"tId": t_id,
|
||||
"venueType": 1,
|
||||
"classRoomId": cr_id,
|
||||
"startTime": lesson_start,
|
||||
"endTime": lesson_end,
|
||||
"remark": "信号第1讲实操"
|
||||
})
|
||||
lesson_id = extract_id(body)
|
||||
if lesson_id: created_resources["lessons"].append(lesson_id)
|
||||
step8_ok = (status == 200 and lesson_id is not None)
|
||||
steps.append({"step": 8, "action": "创建Lesson排课", "status": status, "id": lesson_id, "pass": step8_ok})
|
||||
if not step8_ok: success = False
|
||||
|
||||
# 9. 报到确认 (POST /train/enrollments/{enrollmentId}/report)
|
||||
status, body = request_api("POST", f"/train/enrollments/{enrollment_id}/report", {
|
||||
"reportWay": 4,
|
||||
"verifyNote": "现场人工核验报到"
|
||||
})
|
||||
rpt_data = body.get("data", {}) if isinstance(body, dict) else {}
|
||||
report_id = rpt_data.get("reportId") if isinstance(rpt_data, dict) else None
|
||||
if report_id: created_resources["reports"].append(report_id)
|
||||
step9_ok = (status == 200 and body.get("code") == 200 and report_id is not None)
|
||||
steps.append({"step": 9, "action": "报到确认(Report confirm)", "status": status, "reportId": report_id, "pass": step9_ok})
|
||||
if not step9_ok:
|
||||
success = False
|
||||
log(f"Step 9 FAIL: status={status}, body={body}")
|
||||
|
||||
# 10. 验证报名状态联动更新为 1 (已报到) 且 joinTime 非空
|
||||
_, e_detail = request_api("GET", f"/train/enrollments/{enrollment_id}")
|
||||
e_data = e_detail.get("data", {}) if isinstance(e_detail, dict) else {}
|
||||
enr_status = e_data.get("enrollmentStatus")
|
||||
join_time = e_data.get("joinTime")
|
||||
step10_ok = (enr_status == 1 and join_time is not None)
|
||||
steps.append({"step": 10, "action": "验证报到后Enrollment联动状态=1且joinTime非空", "status": enr_status, "joinTime": join_time, "pass": step10_ok})
|
||||
if not step10_ok:
|
||||
success = False
|
||||
log(f"Step 10 FAIL: status={enr_status}, joinTime={join_time}")
|
||||
|
||||
# 11. 验证重复报到拦截 (40050)
|
||||
status, body = request_api("POST", f"/train/enrollments/{enrollment_id}/report", {
|
||||
"reportWay": 4
|
||||
}, expected_status=(200, 400))
|
||||
dup_code = body.get("code") if isinstance(body, dict) else None
|
||||
step11_ok = (dup_code == 40050)
|
||||
steps.append({"step": 11, "action": "重复报到拦截校验(40050)", "code": dup_code, "pass": step11_ok})
|
||||
if not step11_ok:
|
||||
success = False
|
||||
log(f"Step 11 FAIL: dup_code={dup_code}")
|
||||
|
||||
# 12. 考勤签到 (POST /train/attendance/sign-in)
|
||||
status, body = request_api("POST", "/train/attendance/sign-in", {
|
||||
"enrollmentId": enrollment_id,
|
||||
"lessonId": lesson_id,
|
||||
"channel": "MANUAL"
|
||||
})
|
||||
signin_data = body.get("data", {}) if isinstance(body, dict) else {}
|
||||
checkin_id = signin_data.get("checkinId")
|
||||
first_time = signin_data.get("signInTime")
|
||||
if checkin_id: created_resources["checkins"].append(checkin_id)
|
||||
step12_ok = (status == 200 and body.get("code") == 200 and checkin_id is not None and first_time is not None)
|
||||
steps.append({"step": 12, "action": "考勤签到(Attendance sign-in)", "status": status, "checkinId": checkin_id, "pass": step12_ok})
|
||||
if not step12_ok:
|
||||
success = False
|
||||
log(f"Step 12 FAIL: status={status}, body={body}, checkin_id={checkin_id}, first_time={first_time}")
|
||||
|
||||
# 13. 考勤签到幂等性测试 (重复签到返回相同 checkinId 且保留初次 signInTime)
|
||||
status, body = request_api("POST", "/train/attendance/sign-in", {
|
||||
"enrollmentId": enrollment_id,
|
||||
"lessonId": lesson_id,
|
||||
"channel": "FACE"
|
||||
})
|
||||
idem_data = body.get("data", {}) if isinstance(body, dict) else {}
|
||||
step13_ok = (status == 200 and body.get("code") == 200 and
|
||||
idem_data.get("checkinId") == checkin_id and
|
||||
idem_data.get("signInTime") == first_time)
|
||||
steps.append({"step": 13, "action": "考勤签到幂等性保持初次时间", "pass": step13_ok})
|
||||
if not step13_ok:
|
||||
success = False
|
||||
log(f"Step 13 FAIL: checkinId={idem_data.get('checkinId')} vs {checkin_id}, signInTime={idem_data.get('signInTime')} vs {first_time}")
|
||||
|
||||
# 14. 考勤签退 (POST /train/attendance/sign-out)
|
||||
status, body = request_api("POST", "/train/attendance/sign-out", {
|
||||
"enrollmentId": enrollment_id,
|
||||
"lessonId": lesson_id,
|
||||
"checkoutWay": 1
|
||||
})
|
||||
signout_data = body.get("data", {}) if isinstance(body, dict) else {}
|
||||
checkout_time = signout_data.get("signOutTime")
|
||||
step14_ok = (status == 200 and body.get("code") == 200 and checkout_time is not None)
|
||||
steps.append({"step": 14, "action": "考勤签退(Attendance sign-out)", "status": status, "signOutTime": checkout_time, "pass": step14_ok})
|
||||
if not step14_ok:
|
||||
success = False
|
||||
log(f"Step 14 FAIL: status={status}, body={body}, checkout_time={checkout_time}")
|
||||
|
||||
# 14.5 重复签退幂等性验证 (必须保持第一次 signOutTime,禁止覆盖)
|
||||
status_dup_out, body_dup_out = request_api("POST", "/train/attendance/sign-out", {
|
||||
"enrollmentId": enrollment_id,
|
||||
"lessonId": lesson_id,
|
||||
"checkoutWay": 1
|
||||
})
|
||||
dup_out_data = body_dup_out.get("data", {}) if isinstance(body_dup_out, dict) else {}
|
||||
dup_out_time = dup_out_data.get("signOutTime")
|
||||
step14_dup_ok = (status_dup_out == 200 and dup_out_time == checkout_time)
|
||||
steps.append({"step": 14.5, "action": "重复签退幂等性保持初次时间", "pass": step14_dup_ok})
|
||||
if not step14_dup_ok:
|
||||
success = False
|
||||
log(f"Step 14.5 FAIL: dup_out_time={dup_out_time} vs {checkout_time}")
|
||||
|
||||
# 15. 人工修正考勤 (PUT /train/attendance/{checkinId})
|
||||
status, body = request_api("PUT", f"/train/attendance/{checkin_id}", {
|
||||
"attendanceStatus": 2,
|
||||
"remark": "QA人工标记为迟到"
|
||||
})
|
||||
step15_ok = (status == 200 and body.get("code") == 200)
|
||||
steps.append({"step": 15, "action": "人工调整考勤(Attendance adjust)", "status": status, "pass": step15_ok})
|
||||
if not step15_ok:
|
||||
success = False
|
||||
log(f"Step 15 FAIL: status={status}, body={body}")
|
||||
|
||||
scenario_stats["SCENARIO-06"]["passed"] = success
|
||||
log(f"SCENARIO-06 结果: {'PASS' if success else 'FAIL'}")
|
||||
return success
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助工具:DbAssertionChecker 编译与执行
|
||||
# ==============================================================================
|
||||
@@ -1027,6 +1291,18 @@ def run_cleanup():
|
||||
"details": []
|
||||
}
|
||||
|
||||
resources_path = os.path.join(OUTPUT_DIR, "created-resources.json")
|
||||
with open(resources_path, "w", encoding="utf-8") as f:
|
||||
json.dump(created_resources, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 0. 优先在数据库中安全解绑外键依赖(考勤、报到记录)
|
||||
try:
|
||||
cp = get_db_checker_cp()
|
||||
cmd = ["java", "-cp", cp, "scripts.qa.DbAssertionChecker", "--cleanup-dependents", resources_path]
|
||||
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
except Exception as e:
|
||||
log(f"依赖记录预清理异常: {e}")
|
||||
|
||||
# 1. API 方式清理课次
|
||||
for l_id in created_resources["lessons"]:
|
||||
cleanup_report["total_targets"] += 1
|
||||
@@ -1148,6 +1424,16 @@ def generate_summary(start_time, end_time, db_passed, git_head):
|
||||
- 活动学员状态联动:status由0/1自动变更为 `4:已完成`,且 `complete_time` 真实写入。
|
||||
- 历史退训保护:状态为 `3:退训` 的学员保持为3,`complete_time` 保持为null,未被错误覆盖。
|
||||
|
||||
### SCENARIO-06: Report & Attendance Lifecycle
|
||||
**结果**: {'PASS' if scenario_stats['SCENARIO-06']['passed'] else 'FAIL'}
|
||||
- 报到确认 (Report confirm): `POST /train/enrollments/{{enrollmentId}}/report` 成功报到,返回 `reportId`
|
||||
- 状态自动联动:学员报名状态自动变更为 `1:已报到`,且 `join_time` 真实记录
|
||||
- 重复报到拦截:再次报到拦截返回 `40050`
|
||||
- 考勤签到 (Attendance sign-in): `POST /train/attendance/sign-in` 签到成功生成 `checkinId` 与签到时间
|
||||
- 签到幂等性防覆盖:重复签到保持初次签到时间不变
|
||||
- 考勤签退 (Attendance sign-out): `POST /train/attendance/sign-out` 签退成功记录 `checkout_time`
|
||||
- 人工修正留痕 (Attendance adjust): `PUT /train/attendance/{{checkinId}}` 人工变更考勤状态,系统自动留痕 `manual_flag = 1`
|
||||
|
||||
## 3. Kingbase 数据库一致性与持久化断言
|
||||
|
||||
- **duplicate enrollment = 0**: PASS
|
||||
@@ -1156,6 +1442,10 @@ def generate_summary(start_time, end_time, db_passed, git_head):
|
||||
- **invalid venue XOR = 0**: PASS
|
||||
- **del_flag = '2' (逻辑删除)**: PASS
|
||||
- **Plan completion联动一致性**: PASS
|
||||
- **orphan report = 0**: PASS
|
||||
- **orphan checkin = 0**: PASS
|
||||
- **manual_flag = 1 (人工修正留痕)**: PASS
|
||||
- **report enrollment status (1/4)**: PASS
|
||||
|
||||
详细报告文件位于: `{OUTPUT_DIR}`
|
||||
"""
|
||||
@@ -1191,6 +1481,7 @@ def main():
|
||||
s3_ok = run_scenario_03()
|
||||
s4_ok = run_scenario_04()
|
||||
s5_ok = run_scenario_05()
|
||||
s6_ok = run_scenario_06()
|
||||
except Exception as e:
|
||||
log(f"测试执行异常: {e}")
|
||||
import traceback
|
||||
|
||||
Reference in new issue
Block a user