后端设计治理:批次 0-4 全部完成(安全/部署/一致性/结构/架构)

按 ROADMAP §3.1 治理批次推进的后端设计审查整改:

- 批次 0(安全):/api/status/{task_id} 补 JWT 鉴权与任务归属校验;
  pythonocc_available 真实探测;bcrypt 超 72 字节显式拒绝;
  SECRET_KEY/RUSTFS_* 惰性校验,代码侧弱默认移除
- 批次 1(部署正确性):主处理链路改走 RustFS(分派入参 stp_file_id 化,
  worker 按 object_key 下载);AUTO_MIGRATE 开关 + 迁移目录 alembic/→migrations/
  修复包遮蔽(自动迁移此前从未真正生效);OCC 镜像改 conda 原生执行 +
  基础镜像 tag 锁定;compose 关键项改 ${VAR:?} 强制显式配置
- 批次 2(任务一致性):删除 Redis 进程内存回退,PG 为任务状态单一事实源;
  批量元数据入库(processing_tasks.batch_id,迁移 a3f8c2d91e47);
  型腔失败任务标 failed 不再静默 completed;事务边界收口
  (数据本体写 flush-only、失败先回滚再置 failed、进度更新保留即时 commit)
- 批次 3(API 与代码结构):592 行 advanced_router 拆为 design/cost/machining/
  export 四子路由,请求体全量 Pydantic 化;ROUTE_MODULES + route_registry
  (/api/health 呈现 degraded,DEBUG fail fast);纯计算端点统一 to_thread;
  StorageIntegrationService 按职责三拆;MAX_FILE_SIZE 接线生效、
  celery 复用 Settings.redis_url;管理员重置密码改 JSON body(端到端断裂修复);
  openapi.json 重导出(76 paths)+ 前端 gen:api
- 批次 4(架构演进):共享 ORM 按模块拆分(shared/models/base.py + identity.py、
  moldinsight/models/、inventory/models/,删除三条无使用方的跨模块
  relationship,跨模块桥接收敛为裸 FK 硬规则,无兼容 facade);
  OCC executor 重建补 cancel_futures=True(消除旧队列被慢恢复线程
  并行消化的数据竞争);OCC 吞吐方案设计先行
  (docs/topics/performance/OCC_THROUGHPUT.md);顺手清偿 D15
  (vite.config.ts 未用参数致 npm run build 失败)

测试基线:125 passed, 2 skipped(pytest + sqlite+aiosqlite;归属边界、
路由契约、配置治理、鉴权回归等随批新增)
文档同步:STATUS / TECH_DEBT / ROADMAP / ARCHITECTURE / API_CONTRACT /
OPERATIONS / AGENTS

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-17 16:15:49 +08:00
parent 4537faf2c4
commit 0e6b3b1811
82 changed files with 6513 additions and 3709 deletions
+34 -28
View File
@@ -20,14 +20,15 @@ from moldinsight.core.geometry_analyzer import GeometryAnalyzer
from moldinsight.core.mesh_generator import MeshGenerator
from moldinsight.core.multi_scheme_planner import MultiSchemeMoldPlanner
from moldinsight.core.cad_exporter import CADExporter
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from moldinsight.services.task_storage_service import TaskStorageService
from moldinsight.services.analysis_storage_service import AnalysisStorageService
from moldinsight.storage.rustfs_storage import rustfs_manager
from shared.services.redis_task_manager import redis_task_manager
from moldinsight.services.material_service import MaterialService
from moldinsight.services.calculation_service import CalculationService
from moldinsight.services.llm_service import llm_service
from shared.models.schemas import ProcessingStatus
from shared.models.database import STPFile
from moldinsight.models import STPFile
from shared.database.database import db_manager
from shared.utils.html_generator import HTMLGenerator
from shared.utils.logger import get_logger
@@ -43,7 +44,9 @@ class ProcessingService:
self.geometry_analyzer = GeometryAnalyzer()
self.mesh_generator = MeshGenerator(quality="medium")
self.html_generator = HTMLGenerator()
self.storage_service = StorageIntegrationService()
# 批次 3 按职责拆分:任务/文件生命周期 与 分析结果数据(原 StorageIntegrationService)
self.task_storage = TaskStorageService()
self.analysis_storage = AnalysisStorageService()
self.multi_scheme_planner = MultiSchemeMoldPlanner()
self.cad_exporter = CADExporter()
# TopoDS_Shape 为 C++ 原生内存对象,LRU 上限防止长期运行内存只涨不降
@@ -59,12 +62,15 @@ class ProcessingService:
asyncio.wait_for 只能取消协程,正在执行 OCC 布尔运算的线程无法中断;
单 worker executor 中一个挂死线程会让后续任务永久排队直至重启。
代价是泄漏 1 个线程,收益是恢复服务可用性。
cancel_futures=True 丢弃旧 executor 中尚未开跑的排队任务(否则旧线程
恢复后仍会继续消化旧队列,与新 executor 并发操作 OCC 必然崩溃)。
已在运行中的 C++ 线程在 Python 层不可杀,仍会滞留——这是已知残留
泄漏(每次超时 1 线程),根治需进程级 OCC 隔离,见 docs/OCC_THROUGHPUT.md。
"""
old = self._occ_executor
self._occ_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="occ")
old.shutdown(wait=False)
logger.warning("OCC executor 已因处理超时重建(放弃等待旧线程,可能泄漏 1 个线程)")
old.shutdown(wait=False, cancel_futures=True)
logger.warning("OCC executor 已因处理超时重建(排队任务已丢弃,运行中线程可能滞留 1 个)")
async def run_occ(self, fn, *args):
"""在 OCC 单线程 executor 中执行同步几何操作。
@@ -169,8 +175,8 @@ class ProcessingService:
# 避免 failed 更新把半成品 flush 数据一起带上
await db_session.rollback()
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "failed")
await self.storage_service.update_task_status(
await self.task_storage.update_stp_file_status(db_session, stp_file_id, "failed")
await self.task_storage.update_task_status(
db_session, task_id, "failed", error_message=str(e)
)
@@ -203,7 +209,7 @@ class ProcessingService:
stage_timings: Dict[str, float] = {}
# 1. 解析STP文件
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 20, "解析STP文件"
)
@@ -218,7 +224,7 @@ class ProcessingService:
stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3)
# 2. 生成网格数据并持久化
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 30, "生成网格数据"
)
@@ -229,7 +235,7 @@ class ProcessingService:
stage_timings["generate_mesh"] = round(time.perf_counter() - stage_started, 3)
# 3. 生成模具型腔
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 40, "生成模具型腔"
)
@@ -257,7 +263,7 @@ class ProcessingService:
)
# 4. 生成详细JSON数据 — 委托 CalculationService
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 60, "生成型腔详细数据"
)
@@ -284,12 +290,12 @@ class ProcessingService:
cavity_key_info = best_key_info
# 6. 保存几何数据到数据库
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 70, "保存几何数据"
)
stage_started = time.perf_counter()
await self.storage_service.save_geometry_data(
await self.analysis_storage.save_geometry_data(
db_session,
stp_file_id,
geometry_data,
@@ -301,7 +307,7 @@ class ProcessingService:
await db_session.commit()
# 7. 生成HTML可视化
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 85, "生成可视化报告"
)
@@ -337,7 +343,7 @@ class ProcessingService:
best_key_info = best_scheme.get("key_info", {}) if best_scheme else best_key_info
# 8. 保存模具型腔数据(包含方案级预览链接)
await self.storage_service.save_mold_cavity_data(
await self.analysis_storage.save_mold_cavity_data(
db_session, stp_file_id, detailed_cavity_json
)
@@ -349,7 +355,7 @@ class ProcessingService:
lod_data=lod_data,
)
await self.storage_service.save_html_file(
await self.analysis_storage.save_html_file(
db_session,
stp_file_id,
Path(html_file_path).name,
@@ -370,7 +376,7 @@ class ProcessingService:
)
if analysis_result:
await self.storage_service.save_features_and_recommendations(
await self.analysis_storage.save_features_and_recommendations(
db_session,
stp_file_id,
analysis_result.get("detected_features", []),
@@ -381,7 +387,7 @@ class ProcessingService:
stage_timings["analyze_design"] = round(time.perf_counter() - stage_started, 3)
# 9.6 更新STP文件的分析摘要字段
await self.storage_service.update_stp_file_analysis_summary(
await self.task_storage.update_stp_file_analysis_summary(
db_session,
stp_file_id,
volume=geometry_data.get("volume", 0),
@@ -419,7 +425,7 @@ class ProcessingService:
stage_timings["generate_llm_report"] = round(time.perf_counter() - stage_started, 3)
# 10. 完成处理——先 flush 任务参数,完成状态提交时一并原子落库(D9)
await self.storage_service.update_task_parameters(
await self.task_storage.update_task_parameters(
db_session,
task_id,
{
@@ -431,8 +437,8 @@ class ProcessingService:
**process_params,
},
)
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
await self.storage_service.update_task_status(
await self.task_storage.update_stp_file_status(db_session, stp_file_id, "completed")
await self.task_storage.update_task_status(
db_session, task_id, "completed", 100, "模具型腔生成完成"
)
@@ -464,8 +470,8 @@ class ProcessingService:
# D9:先丢弃未提交的数据本体再置失败(同外层说明)
await db_session.rollback()
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "failed")
await self.storage_service.update_task_status(
await self.task_storage.update_stp_file_status(db_session, stp_file_id, "failed")
await self.task_storage.update_task_status(
db_session, task_id, "failed", error_message=str(e)
)
@@ -524,7 +530,7 @@ class ProcessingService:
"bounding_box": bbox,
}
await self.storage_service.save_mesh_data(
await self.analysis_storage.save_mesh_data(
db_session,
stp_file_id=stp_file_id,
mesh_json=mesh_json,
@@ -732,7 +738,7 @@ class ProcessingService:
logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)")
return {"status": "disabled", "reason": "FreeCAD验证已禁用"}
await self.storage_service.update_task_status(
await self.task_storage.update_task_status(
db_session, task_id, "processing", 90, "FreeCAD几何验证"
)
@@ -787,7 +793,7 @@ class ProcessingService:
async def _save_analysis_metrics(self, session: AsyncSession, stp_file_id: int, analysis_result: dict):
"""保存分析指标到数据库"""
from shared.models.database import AnalysisMetrics
from moldinsight.models import AnalysisMetrics
quality_metrics = analysis_result.get("quality_metrics", {})
analysis_summary = analysis_result.get("analysis_summary", "")
@@ -807,7 +813,7 @@ class ProcessingService:
async def _save_verification_metrics(self, session: AsyncSession, stp_file_id: int, verification_result: dict):
"""保存验证指标到数据库"""
from shared.models.database import AnalysisMetrics
from moldinsight.models import AnalysisMetrics
from sqlalchemy import select
result = await session.execute(