feat:[FL-211][高程管理扁平化为文件记录]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-20 18:03:27 +08:00
parent fac00c0536
commit 899d5316cf
11 changed files with 732 additions and 1882 deletions
+52 -2
View File
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session, sessionmaker
from app.core.database import Base
from app.models.atp_model import AtpModel, AtpModelVersion, AtpSimulationRun
from app.models.elevation import ElevationDataImportJob, ElevationDataset
from app.models.elevation import ElevationDataImportJob, ElevationDataset, ElevationFileRecord
from app.models.user import User
from app.models.wine import WineRun
from app.schemas.atp_model import AtpSimulationRunRequest
@@ -233,6 +233,53 @@ def test_queue_dataset_terrain_build_reuses_existing_running_task(monkeypatch) -
session.close()
def test_file_record_terrain_layer_and_tile_read_from_record_storage(monkeypatch) -> None:
testing_session = _build_sessionmaker(ElevationFileRecord.__table__)
session: Session = testing_session()
try:
record = ElevationFileRecord(
id="abcdef1234567890abcdef1234567890",
file_name="terrain.tif",
file_path="/elevation/records/ab/cd/terrain.tif",
file_format="tif",
file_size=128,
mount_code="default",
status="active",
terrain_status="ready",
terrain_root_path="/elevation/terrain/records/ab/cd/abcdef1234567890abcdef1234567890",
terrain_url_template="/api/v1/elevation/records/abcdef1234567890abcdef1234567890/terrain/{z}/{x}/{y}.terrain?v=1.0.0",
terrain_min_zoom=0,
terrain_max_zoom=0,
)
session.add(record)
session.commit()
driver = _MemoryStorageDriver()
layer_payload = b'{"tilejson":"2.1.0","format":"heightmap-1.0","version":"1.0.0","scheme":"tms","projection":"EPSG:4326","tiles":["{z}/{x}/{y}.terrain?v=1.0.0"],"minzoom":0,"maxzoom":0}'
driver.write_file(
"/elevation/terrain/records/ab/cd/abcdef1234567890abcdef1234567890/layer.json",
content=layer_payload,
content_type="application/json",
)
driver.write_file(
"/elevation/terrain/records/ab/cd/abcdef1234567890abcdef1234567890/0/0/0.terrain",
content=b"tile-bytes",
content_type="application/octet-stream",
)
monkeypatch.setattr(elevation_service, "_require_mount", lambda *_args, **_kwargs: SimpleNamespace(code="default"))
monkeypatch.setattr(elevation_service, "_build_driver_or_400", lambda *_args, **_kwargs: driver)
layer = elevation_service.get_file_record_terrain_layer(session, record_id=record.id)
tile = elevation_service.get_file_record_terrain_tile(session, record_id=record.id, z=0, x=0, y=0)
assert layer.maxzoom == 0
assert layer.tiles == ["{z}/{x}/{y}.terrain?v=1.0.0"]
assert tile == b"tile-bytes"
finally:
session.close()
def test_import_dataset_data_files_queue_job_and_worker_keeps_preferred_raster(monkeypatch) -> None:
testing_session = _build_sessionmaker(ElevationDataset.__table__, ElevationDataImportJob.__table__)
session: Session = testing_session()
@@ -296,7 +343,10 @@ def test_import_dataset_data_files_queue_job_and_worker_keeps_preferred_raster(m
assert first.job.uploaded_file_count == 1
assert first.job.analysis_task_queued is False
assert import_calls == [(first.job.id, actor.id)]
assert any(path.endswith(".img") and "/.imports/" in path for path in driver.files)
saved_pending_job = session.get(ElevationDataImportJob, first.job.id)
assert saved_pending_job is not None
assert saved_pending_job.staged_files_json[0]["filename"] == "terrain.img"
assert "content_base64" in saved_pending_job.staged_files_json[0]
second = elevation_service.import_dataset_data_files(
session,
+129 -1
View File
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.core.database import Base
from app.models.elevation import ElevationApplyJob, ElevationDataset
from app.models.elevation import ElevationApplyJob, ElevationDataset, ElevationFileRecord
from app.models.line import Line
from app.models.line_tower import LineTower
from app.models.tower_profile import TowerProfile
@@ -28,6 +28,7 @@ def _build_session_factory() -> sessionmaker[Session]:
LineTower.__table__,
TowerProfile.__table__,
ElevationDataset.__table__,
ElevationFileRecord.__table__,
ElevationApplyJob.__table__,
],
)
@@ -81,6 +82,53 @@ def test_create_apply_job_dispatches_actor_user_id(monkeypatch) -> None:
session.close()
def test_create_apply_job_accepts_file_record_id(monkeypatch) -> None:
testing_session = _build_session_factory()
session = testing_session()
try:
line = Line(code="L-APPLY-003", name="文件记录回填线路", voltage_kv=220, lightning_param_json={})
record = ElevationFileRecord(
file_name="record.csv",
file_path="/elevation/records/ab/cd/record.csv",
file_format="csv",
file_size=32,
mount_code="default",
status="active",
)
session.add_all([line, record])
session.flush()
session.add(LineTower(line_id=line.id, seq_no=1, tower_no="T1", longitude=120.0, latitude=30.0))
session.commit()
dispatched: dict[str, str | None] = {}
def _fake_dispatch(*, job_id: str, actor_user_id: str | None) -> SimpleNamespace:
dispatched["job_id"] = job_id
dispatched["actor_user_id"] = actor_user_id
return SimpleNamespace(id="celery-task-file-record")
monkeypatch.setattr(elevation_service, "_dispatch_elevation_apply_task", _fake_dispatch)
monkeypatch.setattr(elevation_service, "_publish_elevation_change", lambda *args, **kwargs: None)
response = elevation_service.create_apply_job(
session,
ElevationApplyJobCreateRequest(line_id=line.id, file_record_id=record.id, mode="overwrite_all"),
actor=SimpleNamespace(id="tester"),
)
saved_job = session.get(ElevationApplyJob, response.job.id)
assert response.queued is True
assert dispatched == {"job_id": response.job.id, "actor_user_id": "tester"}
assert saved_job is not None
assert saved_job.file_record_id == record.id
assert saved_job.dataset_id is None
assert saved_job.task_id == "celery-task-file-record"
assert response.job.file_record_id == record.id
assert response.job.file_record_name == "record.csv"
finally:
session.close()
def test_execute_apply_job_uses_saved_actor_for_preparation_source(monkeypatch) -> None:
testing_session = _build_session_factory()
session = testing_session()
@@ -160,3 +208,83 @@ def test_execute_apply_job_uses_saved_actor_for_preparation_source(monkeypatch)
verification_session.close()
finally:
session.close()
def test_execute_apply_job_uses_file_record_source(monkeypatch) -> None:
testing_session = _build_session_factory()
session = testing_session()
try:
line = Line(code="L-APPLY-004", name="文件记录高程回填线路", voltage_kv=110, lightning_param_json={})
record = ElevationFileRecord(
file_name="record.csv",
file_path="/elevation/records/ab/cd/record.csv",
file_format="csv",
file_size=32,
mount_code="default",
status="active",
)
session.add_all([line, record])
session.flush()
meter_to_lat = 1 / 111_320.0
session.add_all(
[
LineTower(line_id=line.id, seq_no=1, tower_no="P1", longitude=120.0, latitude=30.0 + 300 * meter_to_lat),
LineTower(line_id=line.id, seq_no=2, tower_no="P2", longitude=120.0, latitude=30.0 + 600 * meter_to_lat),
LineTower(line_id=line.id, seq_no=3, tower_no="P3", longitude=120.0, latitude=30.0 + 900 * meter_to_lat),
]
)
session.flush()
job = ElevationApplyJob(
line_id=line.id,
file_record_id=record.id,
mode="overwrite_all",
status="pending",
total_tower_count=3,
create_user="tester",
update_user="tester",
)
session.add(job)
session.commit()
points = [
elevation_service.ElevationSamplePoint(
lon=120.0,
lat=30.0 + distance_m * meter_to_lat,
altitude_m=100.0 + distance_m * 0.12,
)
for distance_m in range(0, 1251, 50)
]
monkeypatch.setattr(elevation_service, "SessionLocal", testing_session)
monkeypatch.setattr(elevation_service, "_load_dataset_points", lambda *_args, **_kwargs: (points, []))
monkeypatch.setattr(elevation_service, "_publish_elevation_change", lambda *args, **kwargs: None)
monkeypatch.setattr(elevation_service, "_publish_line_change", lambda *args, **kwargs: None)
elevation_service.execute_apply_job(job.id)
verification_session = testing_session()
try:
saved_job = verification_session.get(ElevationApplyJob, job.id)
saved_line = verification_session.get(Line, line.id)
towers = verification_session.execute(
select(LineTower).where(LineTower.line_id == line.id).order_by(LineTower.seq_no.asc())
).scalars().all()
assert saved_job is not None
assert saved_job.status == "success"
assert saved_line is not None
assert saved_line.update_user == "tester"
assert all(tower.altitude_m is not None for tower in towers)
source = saved_line.lightning_param_json["preparation_sources"]["ground_slope"]
assert source["prepared_by_user_id"] == "tester"
assert source["file_record_id"] == record.id
assert source["file_record_name"] == "record.csv"
assert source["dataset_id"] is None
assert source["job_id"] == job.id
finally:
verification_session.close()
finally:
session.close()
+1 -1
View File
@@ -158,7 +158,7 @@ def test_apply_points_to_line_towers_computes_ground_slopes() -> None:
stats = elevation_service._apply_points_to_line_towers(
session,
line_id=line.id,
dataset=SimpleNamespace(id="ds-1", code="DEM-001"),
elevation_source=SimpleNamespace(id="ds-1", code="DEM-001"),
mode="overwrite_all",
points=points,
)