fix(strategy-map): 战略地图三处 P1 缺陷(D1 完备度路由 422 / D2 连线删除不落库 / D3 窄屏拖拽失效)
D1 GET /api/cma/maps/completeness/objectives 恒 422:真抢占者是先注册的
objectives.router GET /{map_id}/objectives(map_id 被当 int 解析)。把
map_completeness / map_summary 两个字面路径路由器前移到 objectives 之前,
不动 prefix、不删参数化路由。新增同前缀族全量巡检用例。
D2 DELETE /{id}/connections 返回 200 不落库:canvas_data 为裸 Column(JSON),
就地/换列表赋值不被 ORM 判脏。模型改 MutableDict.as_mutable(JSON)(不改 DDL),
add_connection / delete_connection_by_key 均显式 flag_modified 双保险。
D3 窄视口 .kp-panel(fixed/z-1000) 遮画布,onTreeDragOver 用 elementFromPoint
取节点恒 null ⇒ 拖拽建连线失效。改为遍历画布内 [data-node-key] 做 rect 包含
判定、多点取中心最近者,不再回退 elementFromPoint。
测试:新增 backend/tests/test_map_route_shadowing_20261020.py(路由解析断言到
endpoint.__module__)与 test_map_connection_persistence_20261020.py(新 session
原生 SQL 复读 + 负测)。零生产写、零上线。
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from typing import Optional
|
||||
from datetime import date, datetime, timedelta
|
||||
from app.database import get_db
|
||||
@@ -558,6 +559,8 @@ def add_connection(map_id: int, data: dict, db: Session = Depends(get_db), entit
|
||||
"label": data.get("label", ""),
|
||||
})
|
||||
m.canvas_data["connections"] = conns
|
||||
# D2(2026-10-20):裸 JSON 列不做就地变更跟踪;显式标脏,与模型层 MutableDict 双保险
|
||||
flag_modified(m, "canvas_data")
|
||||
db.commit()
|
||||
# updated_at: 供前端采纳为新基准(并发保护 base_updated_at),避免自伤 409
|
||||
return {"connections": conns, "updated_at": _stamp(m.updated_at)}
|
||||
@@ -580,6 +583,8 @@ def delete_connection_by_key(map_id: int, data: dict, db: Session = Depends(get_
|
||||
raise HTTPException(404, "连线不存在")
|
||||
|
||||
m.canvas_data["connections"] = new_conns
|
||||
# D2(2026-10-20):换新列表赋值同样不被裸 JSON 列跟踪 ⇒ 显式标脏(根因修复点)
|
||||
flag_modified(m, "canvas_data")
|
||||
db.commit()
|
||||
return {"connections": new_conns, "removed": {"from": from_id, "to": to_id},
|
||||
"updated_at": _stamp(m.updated_at)}
|
||||
|
||||
+5
-2
@@ -41,6 +41,11 @@ app.include_router(kpis.router)
|
||||
app.include_router(kpi_governance.router)
|
||||
app.include_router(templates.router)
|
||||
app.include_router(maps.router)
|
||||
# 顺序铁律(2026-10-20 D1 修复):同前缀族的路由必须把**字面路径段**路由器 include 在
|
||||
# 任何 `/{map_id}/<literal>` 参数化路由(objectives.router 的 GET /{map_id}/objectives)之前,
|
||||
# 否则 /api/cma/maps/completeness/objectives 会被判 map_id="completeness" → 恒 422。
|
||||
app.include_router(map_completeness.router) # 战略地图数据完备度(口径 CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0)
|
||||
app.include_router(map_summary.router) # 战略地图摘要条(收入分线/新式毛利率/管理指标 · 口径 CMA-A1 终稿)
|
||||
app.include_router(dashboard.router)
|
||||
app.include_router(data.router)
|
||||
app.include_router(alerts.router)
|
||||
@@ -56,8 +61,6 @@ app.include_router(action_plans.router)
|
||||
app.include_router(alignment.router)
|
||||
app.include_router(org.router)
|
||||
app.include_router(objectives.router)
|
||||
app.include_router(map_completeness.router) # 战略地图数据完备度(口径 CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0)
|
||||
app.include_router(map_summary.router) # 战略地图摘要条(收入分线/新式毛利率/管理指标 · 口径 CMA-A1 终稿)
|
||||
app.include_router(versions.router)
|
||||
app.include_router(budget.router)
|
||||
app.include_router(value_sources.router)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""管理会计OS 数据模型"""
|
||||
from sqlalchemy import Column, Integer, String, Text, Float, DateTime, ForeignKey, Boolean, JSON, func, text, UniqueConstraint, Index, Numeric, Date
|
||||
from sqlalchemy.ext.mutable import MutableDict
|
||||
from app.database import Base
|
||||
|
||||
from app.models.budget_plan import BudgetPlan
|
||||
@@ -59,7 +60,7 @@ class StrategicMap(Base):
|
||||
version = Column(String(20), default="v1.0", comment="版本号")
|
||||
status = Column(String(20), default="draft", comment="draft/published")
|
||||
dimensions = Column(JSON, nullable=True, comment="四维度和目标列表")
|
||||
canvas_data = Column(JSON, nullable=True, comment="画布连线数据")
|
||||
canvas_data = Column(MutableDict.as_mutable(JSON), nullable=True, comment="画布连线数据")
|
||||
created_by = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
Reference in New Issue
Block a user