[migrate]:[FL-30][加装避雷器复算与报告表14]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -22,6 +22,9 @@ def _sample_report_data() -> dict[str, object]:
|
||||
"risk_job_name": "示例线路-风险评估",
|
||||
"mitigation_job_id": "mit-job-1",
|
||||
"mitigation_job_name": "示例线路-措施推荐",
|
||||
"scenario_job_id": "scenario-job-1",
|
||||
"scenario_job_name": "示例线路-加装避雷器复算",
|
||||
"scenario_base_job_type": "normal",
|
||||
},
|
||||
"risk_rows": [
|
||||
{
|
||||
@@ -108,6 +111,19 @@ def _sample_report_data() -> dict[str, object]:
|
||||
},
|
||||
}
|
||||
],
|
||||
"selected_scenario_rows": [
|
||||
{
|
||||
"tower_id": "tower-1",
|
||||
"tower_no": "001",
|
||||
"risk_level": "low",
|
||||
"result_json": {
|
||||
"counterstrike_withstand_ka": 112.45,
|
||||
"counterstrike_trip_rate": 0.0215,
|
||||
"shielding_withstand_ka": 136.2,
|
||||
"shielding_trip_rate": 0.0085,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +140,9 @@ def test_build_report_summary_payload_counts_risk_and_actions() -> None:
|
||||
assert summary["mitigation_action_counts"]["接地治理"] == 1
|
||||
assert summary["mitigation_action_counts"]["安装避雷器"] == 1
|
||||
assert summary["has_mitigation_data"] is True
|
||||
assert summary["scenario_row_count"] == 1
|
||||
assert summary["post_recalc_risk_counts"] == {"high": 0, "medium": 0, "low": 1}
|
||||
assert summary["has_scenario_data"] is True
|
||||
|
||||
|
||||
def test_build_report_document_renders_word_compatible_html() -> None:
|
||||
@@ -136,4 +155,6 @@ def test_build_report_document_renders_word_compatible_html() -> None:
|
||||
assert "雷害风险评估结果" in html
|
||||
assert "差异化防雷措施与预期效果" in html
|
||||
assert "安装避雷器" in html
|
||||
assert "表14 采取措施后的计算结果表" in html
|
||||
assert "反击耐雷水平(kA)" in html
|
||||
assert "001" in html
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from app.core.database import Base
|
||||
from app.models.fl_analysis import FlAnalysisJob, FlAnalysisRun, FlAnalysisTowerResult, FlAnalysisTowerSnapshot
|
||||
from app.models.line import Line
|
||||
from app.models.line_tower import LineTower
|
||||
from app.models.tower_profile import TowerProfile
|
||||
from app.schemas.fl_analysis import FlAnalysisJobCreateRequest
|
||||
from app.services import fl_analysis_service
|
||||
|
||||
|
||||
def _build_sessionmaker():
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
Base.metadata.create_all(
|
||||
bind=engine,
|
||||
tables=[
|
||||
Line.__table__,
|
||||
LineTower.__table__,
|
||||
TowerProfile.__table__,
|
||||
FlAnalysisJob.__table__,
|
||||
FlAnalysisRun.__table__,
|
||||
FlAnalysisTowerSnapshot.__table__,
|
||||
FlAnalysisTowerResult.__table__,
|
||||
],
|
||||
)
|
||||
return sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False)
|
||||
|
||||
|
||||
def _create_completed_job(
|
||||
session: Session,
|
||||
*,
|
||||
line: Line,
|
||||
tower: LineTower,
|
||||
profile: TowerProfile,
|
||||
job_type: str,
|
||||
job_name: str,
|
||||
risk_level: str,
|
||||
result_json: dict[str, object],
|
||||
execution_options_json: dict[str, object] | None = None,
|
||||
external_adapter: str = "placeholder",
|
||||
) -> tuple[FlAnalysisJob, FlAnalysisRun]:
|
||||
job = FlAnalysisJob(
|
||||
line_id=line.id,
|
||||
job_name=job_name,
|
||||
job_type=job_type,
|
||||
status="success",
|
||||
source_kind="line",
|
||||
total_tower_count=1,
|
||||
snapshotted_tower_count=1,
|
||||
result_tower_count=1,
|
||||
external_adapter=external_adapter,
|
||||
execution_options_json=execution_options_json or {},
|
||||
result_summary_json={},
|
||||
create_user="tester",
|
||||
update_user="tester",
|
||||
)
|
||||
session.add(job)
|
||||
session.flush()
|
||||
|
||||
run = FlAnalysisRun(
|
||||
job_id=job.id,
|
||||
status="success",
|
||||
runner_kind=external_adapter,
|
||||
create_user="tester",
|
||||
update_user="tester",
|
||||
)
|
||||
session.add(run)
|
||||
session.flush()
|
||||
|
||||
job.latest_run_id = run.id
|
||||
snapshot = FlAnalysisTowerSnapshot(
|
||||
job_id=job.id,
|
||||
run_id=run.id,
|
||||
tower_id=tower.id,
|
||||
seq_no=tower.seq_no,
|
||||
tower_no=tower.tower_no,
|
||||
tower_model=tower.tower_model,
|
||||
tower_type=tower.tower_type,
|
||||
longitude=tower.longitude,
|
||||
latitude=tower.latitude,
|
||||
altitude_m=tower.altitude_m,
|
||||
terrain=tower.terrain,
|
||||
base_tower_json=fl_analysis_service._build_base_tower_json(tower, line),
|
||||
profile_json=fl_analysis_service._build_profile_json(profile),
|
||||
)
|
||||
session.add(snapshot)
|
||||
session.flush()
|
||||
|
||||
session.add(
|
||||
FlAnalysisTowerResult(
|
||||
job_id=job.id,
|
||||
run_id=run.id,
|
||||
snapshot_id=snapshot.id,
|
||||
status="success",
|
||||
risk_level=risk_level,
|
||||
summary_text=str(result_json.get("summary_text") or ""),
|
||||
result_json=result_json,
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
return job, run
|
||||
|
||||
|
||||
def _seed_job_chain(session: Session) -> dict[str, object]:
|
||||
line = Line(
|
||||
code="L-100",
|
||||
name="迁移验证线路",
|
||||
voltage_kv=220,
|
||||
lightning_param_json={"雷电流幅值a": 31.0, "雷电流幅值b": 2.6},
|
||||
)
|
||||
session.add(line)
|
||||
session.flush()
|
||||
|
||||
tower = LineTower(
|
||||
line_id=line.id,
|
||||
seq_no=1,
|
||||
tower_no="T001",
|
||||
tower_model="220-TEST-ZX",
|
||||
tower_type="直线",
|
||||
longitude=120.0,
|
||||
latitude=30.0,
|
||||
altitude_m=128.0,
|
||||
terrain="山地",
|
||||
ground_resistance_ohm=16.0,
|
||||
lightning_density=4.2,
|
||||
span_large_m=320.0,
|
||||
span_small_m=280.0,
|
||||
slope_1=5.0,
|
||||
slope_2=3.0,
|
||||
circuit_geometry_json={
|
||||
"I": {
|
||||
"phase_spacing_m": {"upper": 9.0, "middle": 4.5, "lower": 8.5},
|
||||
"phase_height_m": {"upper": 29.0, "middle": 31.0, "lower": 25.0},
|
||||
},
|
||||
"lightning_wire": {
|
||||
"left_mid_distance_m": 8.0,
|
||||
"right_mid_distance_m": 8.0,
|
||||
"height_m": 38.0,
|
||||
},
|
||||
"insulator_length_mm": 4200.0,
|
||||
},
|
||||
lightning_result_json={},
|
||||
)
|
||||
session.add(tower)
|
||||
session.flush()
|
||||
|
||||
profile = TowerProfile(
|
||||
tower_id=tower.id,
|
||||
structure_kind="直线",
|
||||
stroke_mode="反击",
|
||||
arrester_a="否",
|
||||
arrester_b="否",
|
||||
arrester_c="否",
|
||||
insulator_length_m=4200.0,
|
||||
current_a=31.0,
|
||||
current_b=2.6,
|
||||
current_type="Heidler",
|
||||
current_head_time_us=2.6,
|
||||
current_tail_time_us=50.0,
|
||||
)
|
||||
session.add(profile)
|
||||
session.flush()
|
||||
|
||||
base_job, base_run = _create_completed_job(
|
||||
session,
|
||||
line=line,
|
||||
tower=tower,
|
||||
profile=profile,
|
||||
job_type="normal",
|
||||
job_name="基线普通计算",
|
||||
risk_level="high",
|
||||
result_json={
|
||||
"risk_level": "high",
|
||||
"summary_text": "基线普通计算完成",
|
||||
"score": 88,
|
||||
},
|
||||
execution_options_json={
|
||||
"current_waveform": "double_slope",
|
||||
"flashover_method": "intersection",
|
||||
"altitude_correction": "formula1",
|
||||
"induced_voltage_formula": "formula2",
|
||||
"head_time_min_us": 2.4,
|
||||
"head_time_max_us": 2.8,
|
||||
"head_time_step_us": 0.2,
|
||||
"tail_time_min_us": 45.0,
|
||||
"tail_time_max_us": 55.0,
|
||||
"tail_time_step_us": 5.0,
|
||||
},
|
||||
)
|
||||
risk_job, risk_run = _create_completed_job(
|
||||
session,
|
||||
line=line,
|
||||
tower=tower,
|
||||
profile=profile,
|
||||
job_type="risk",
|
||||
job_name="风险评估",
|
||||
risk_level="high",
|
||||
result_json={
|
||||
"risk_level": "high",
|
||||
"summary_text": "高风险",
|
||||
"score": 91,
|
||||
"cause_analysis": "接地电阻偏高",
|
||||
"mitigation_recommendation": "建议安装避雷器",
|
||||
"reason_details": [{"code": "ground_resistance", "label": "接地电阻", "triggered": True}],
|
||||
},
|
||||
)
|
||||
mitigation_job, mitigation_run = _create_completed_job(
|
||||
session,
|
||||
line=line,
|
||||
tower=tower,
|
||||
profile=profile,
|
||||
job_type="mitigation",
|
||||
job_name="措施推荐",
|
||||
risk_level="medium",
|
||||
result_json={
|
||||
"risk_level": "medium",
|
||||
"summary_text": "仍需补强",
|
||||
"current_risk_level": "high",
|
||||
"expected_risk_level": "medium",
|
||||
"cause_analysis": "接地电阻偏高",
|
||||
"recommendation_result": "需要安装避雷器",
|
||||
"mitigation_recommendation": "安装避雷器并优化接地",
|
||||
"mitigation_actions": [
|
||||
{"code": "arrester_install", "label": "安装避雷器", "summary": "关键相增设避雷器"},
|
||||
],
|
||||
},
|
||||
execution_options_json={
|
||||
"source_job_id": risk_job.id,
|
||||
"source_run_id": risk_run.id,
|
||||
"selected_tower_ids": [tower.id],
|
||||
},
|
||||
)
|
||||
|
||||
session.commit()
|
||||
return {
|
||||
"line": line,
|
||||
"tower": tower,
|
||||
"profile": profile,
|
||||
"base_job": base_job,
|
||||
"base_run": base_run,
|
||||
"risk_job": risk_job,
|
||||
"risk_run": risk_run,
|
||||
"mitigation_job": mitigation_job,
|
||||
"mitigation_run": mitigation_run,
|
||||
}
|
||||
|
||||
|
||||
def test_execute_scenario_job_reuses_base_waveform_and_forces_arresters(monkeypatch) -> None:
|
||||
testing_session = _build_sessionmaker()
|
||||
session = testing_session()
|
||||
try:
|
||||
seeded = _seed_job_chain(session)
|
||||
monkeypatch.setattr(fl_analysis_service, "_publish_change", lambda *args, **kwargs: None)
|
||||
|
||||
created = fl_analysis_service.create_job(
|
||||
session,
|
||||
FlAnalysisJobCreateRequest(
|
||||
line_id=seeded["line"].id,
|
||||
job_name="加装避雷器复算",
|
||||
job_type="scenario",
|
||||
execution_options_json={
|
||||
"source_job_id": seeded["mitigation_job"].id,
|
||||
"base_job_id": seeded["base_job"].id,
|
||||
"selected_tower_ids": [seeded["tower"].id],
|
||||
},
|
||||
),
|
||||
actor=SimpleNamespace(id="tester"),
|
||||
)
|
||||
|
||||
assert created.job.external_adapter == "placeholder"
|
||||
assert created.job.execution_options_json["base_job_type"] == "normal"
|
||||
assert created.job.execution_options_json["current_waveform"] == "double_slope"
|
||||
assert created.job.execution_options_json["mitigation_job_id"] == seeded["mitigation_job"].id
|
||||
|
||||
captured_payloads: list[dict[str, object]] = []
|
||||
|
||||
def fake_grade_normal_snapshot_payload(payload: dict[str, object], *, execution_options: dict[str, object]) -> dict[str, object]:
|
||||
captured_payloads.append(
|
||||
{
|
||||
"profile_json": dict(payload["profile_json"]),
|
||||
"execution_options": dict(execution_options),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"risk_level": "low",
|
||||
"summary_text": "复算完成",
|
||||
"score": 12,
|
||||
"workflow": {"scan_point_count": 1},
|
||||
"counterstrike_withstand_ka": 120.5,
|
||||
"counterstrike_trip_rate": 0.02,
|
||||
"shielding_withstand_ka": 140.0,
|
||||
"shielding_trip_rate": 0.005,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(fl_analysis_service, "SessionLocal", testing_session)
|
||||
monkeypatch.setattr(fl_analysis_service, "grade_normal_snapshot_payload", fake_grade_normal_snapshot_payload)
|
||||
monkeypatch.setattr(
|
||||
fl_analysis_service,
|
||||
"grade_tongtiao_snapshot_payload",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("scenario should reuse normal path")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
fl_analysis_service,
|
||||
"grade_snapshot_payload",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("scenario should not fall back to generic risk grading")),
|
||||
)
|
||||
|
||||
session.close()
|
||||
fl_analysis_service.execute_job(created.job.id)
|
||||
|
||||
verify_session = testing_session()
|
||||
try:
|
||||
saved_job = fl_analysis_service.get_job_by_id(verify_session, created.job.id)
|
||||
assert saved_job is not None
|
||||
assert saved_job.status == "success"
|
||||
assert saved_job.result_summary_json["base_job_type"] == "normal"
|
||||
assert saved_job.result_summary_json["selected_tower_count"] == 1
|
||||
assert saved_job.result_summary_json["workflow"]["current_waveform"] == "double_slope"
|
||||
|
||||
saved_row = verify_session.execute(
|
||||
select(FlAnalysisTowerResult).where(FlAnalysisTowerResult.job_id == created.job.id)
|
||||
).scalar_one()
|
||||
assert saved_row.risk_level == "low"
|
||||
assert saved_row.result_json["counterstrike_withstand_ka"] == 120.5
|
||||
finally:
|
||||
verify_session.close()
|
||||
|
||||
assert len(captured_payloads) == 1
|
||||
assert captured_payloads[0]["profile_json"]["arrester_a"] == "是"
|
||||
assert captured_payloads[0]["profile_json"]["arrester_b"] == "是"
|
||||
assert captured_payloads[0]["profile_json"]["arrester_c"] == "是"
|
||||
assert captured_payloads[0]["execution_options"]["current_waveform"] == "double_slope"
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_report_job_auto_links_latest_scenario_rows(monkeypatch) -> None:
|
||||
testing_session = _build_sessionmaker()
|
||||
session = testing_session()
|
||||
try:
|
||||
seeded = _seed_job_chain(session)
|
||||
monkeypatch.setattr(fl_analysis_service, "_publish_change", lambda *args, **kwargs: None)
|
||||
|
||||
scenario_job, scenario_run = _create_completed_job(
|
||||
session,
|
||||
line=seeded["line"],
|
||||
tower=seeded["tower"],
|
||||
profile=seeded["profile"],
|
||||
job_type="scenario",
|
||||
job_name="加装避雷器复算",
|
||||
risk_level="low",
|
||||
result_json={
|
||||
"risk_level": "low",
|
||||
"summary_text": "复算完成",
|
||||
"score": 14,
|
||||
"counterstrike_withstand_ka": 112.45,
|
||||
"counterstrike_trip_rate": 0.0215,
|
||||
"shielding_withstand_ka": 136.2,
|
||||
"shielding_trip_rate": 0.0085,
|
||||
},
|
||||
execution_options_json={
|
||||
"source_job_id": seeded["mitigation_job"].id,
|
||||
"source_run_id": seeded["mitigation_run"].id,
|
||||
"source_job_type": "mitigation",
|
||||
"mitigation_job_id": seeded["mitigation_job"].id,
|
||||
"mitigation_run_id": seeded["mitigation_run"].id,
|
||||
"risk_job_id": seeded["risk_job"].id,
|
||||
"risk_run_id": seeded["risk_run"].id,
|
||||
"base_job_id": seeded["base_job"].id,
|
||||
"base_run_id": seeded["base_run"].id,
|
||||
"base_job_type": "normal",
|
||||
"base_job_name": seeded["base_job"].job_name,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
|
||||
created = fl_analysis_service.create_job(
|
||||
session,
|
||||
FlAnalysisJobCreateRequest(
|
||||
line_id=seeded["line"].id,
|
||||
job_name="效果报告",
|
||||
job_type="report",
|
||||
execution_options_json={
|
||||
"source_job_id": seeded["mitigation_job"].id,
|
||||
"selected_tower_ids": [seeded["tower"].id],
|
||||
},
|
||||
),
|
||||
actor=SimpleNamespace(id="tester"),
|
||||
)
|
||||
|
||||
assert created.job.execution_options_json["scenario_job_id"] == scenario_job.id
|
||||
assert created.job.execution_options_json["scenario_run_id"] == scenario_run.id
|
||||
|
||||
monkeypatch.setattr(fl_analysis_service, "SessionLocal", testing_session)
|
||||
session.close()
|
||||
fl_analysis_service.execute_job(created.job.id)
|
||||
|
||||
verify_session = testing_session()
|
||||
try:
|
||||
report_job = fl_analysis_service.get_job_by_id(verify_session, created.job.id)
|
||||
assert report_job is not None
|
||||
assert report_job.status == "success"
|
||||
assert report_job.result_summary_json["scenario_row_count"] == 1
|
||||
assert report_job.result_summary_json["has_scenario_data"] is True
|
||||
assert report_job.result_summary_json["post_recalc_risk_counts"] == {"high": 0, "medium": 0, "low": 1}
|
||||
|
||||
filename, content = fl_analysis_service.download_report_document(verify_session, job_id=created.job.id)
|
||||
finally:
|
||||
verify_session.close()
|
||||
|
||||
html = content.decode("utf-8")
|
||||
assert filename.endswith(".doc")
|
||||
assert "表14 采取措施后的计算结果表" in html
|
||||
assert "加装避雷器复算" in html
|
||||
assert "112.45" in html
|
||||
finally:
|
||||
session.close()
|
||||
Reference in New Issue
Block a user