from __future__ import annotations from fastapi import APIRouter, Depends, File, Query, Request, UploadFile from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from iti.db import get_db from iti.exceptions import BizError from iti.responses import ok, raw_response from .base import ExchangeTemplateSourceKind, ExchangeVariable from .registry import get_exchange_registry from .schemas import ( ExchangePlanResolveRequest, ExchangePlanTemplateFileRequest, ExchangeTaskCreateRequest, ExchangeTaskResponse, ExchangeTemplateCreateRequest, ExchangeTemplateResponse, ExchangeTemplateVersionCreateRequest, ExchangeTemplateVersionResponse, ExchangeTemplateUpdateRequest, ) from .service import ExchangeService from .sources import get_exchange_source router = APIRouter(prefix="/exchange", tags=["exchange"]) def _template_payload(item): return { "id": item.id, "code": item.code, "name": item.name, "biz_domain": item.biz_domain, "biz_obj": item.biz_obj, "operation": item.operation, "status": item.status, "description": item.description, "current_version": item.current_version, "layout": item.layout, "meta": item.meta, "created_at": item.created_at, "updated_at": item.updated_at, } def _version_payload(item): return { "id": item.id, "template_id": item.template_id, "version": item.version, "biz_domain": item.biz_domain, "biz_obj": item.biz_obj, "operation": item.operation, "published_at": item.published_at, "file_key": item.file_key, "checksum": item.checksum, "layout": item.layout, "variables": item.variables, "meta": item.meta, "created_at": item.created_at, "updated_at": item.updated_at, } def _task_payload(item): return { "id": item.id, "template_id": item.template_id, "template_version_id": item.template_version_id, "biz_domain": item.biz_domain, "biz_obj": item.biz_obj, "operation": item.operation, "status": item.status, "requested_by": item.requested_by, "storage_key": item.storage_key, "success_count": item.success_count, "failed_count": item.failed_count, "error_count": item.error_count, "message": item.message, "input_payload": item.input_payload, "result_payload": item.result_payload, "started_at": item.started_at, "finished_at": item.finished_at, "meta": item.meta, "created_at": item.created_at, "updated_at": item.updated_at, } def _plan_payload(plan): return plan.as_payload() def _variable_from_schema(item) -> ExchangeVariable: return ExchangeVariable(**item.model_dump()) def _layout_payload(layout): return layout.model_dump() if layout is not None else None def _resolve_source(payload, request: Request, db: Session): source_kind = payload.source_kind source_name = getattr(payload, "source_name", None) if source_name: return get_exchange_source( request.app, source_name=source_name, db=db, service_name=getattr(payload, "source_service", "exchange"), ) if source_kind == ExchangeTemplateSourceKind.LOCAL: return get_exchange_source(request.app, source_kind=source_kind, db=db) if source_kind == ExchangeTemplateSourceKind.REMOTE: return get_exchange_source( request.app, source_kind=source_kind, service_name=payload.source_service or "exchange", ) if source_kind is None: return None if source_kind == ExchangeTemplateSourceKind.MAPPING: return get_exchange_source(request.app, source_kind=ExchangeTemplateSourceKind.MAPPING) return get_exchange_source(request.app, source_kind=source_kind, db=db) @router.get("/catalog") def exchange_catalog(request: Request): return ok(get_exchange_registry(request.app).catalog()) @router.get("/scopes/{biz_domain}/{biz_obj}/{operation}") def get_scope_spec( biz_domain: str, biz_obj: str, operation: str, request: Request, ): spec = get_exchange_registry(request.app).get_spec( biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, ) if spec is None: raise BizError("业务导入导出规格不存在", code=404) return ok(spec.as_payload()) @router.get("/templates/code") def generate_template_code( biz_domain: str, biz_obj: str, operation: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) return ok( { "code": service.generate_template_code( biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, ) } ) @router.get("/templates") def list_templates( request: Request, biz_domain: str | None = Query(default=None), biz_obj: str | None = Query(default=None), operation: str | None = Query(default=None), db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) return ok( [ ExchangeTemplateResponse.model_validate(_template_payload(item)).model_dump(mode="json") for item in service.list_templates( biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, ) ] ) @router.get("/templates/{template_id}") def get_template(template_id: str, request: Request, db: Session = Depends(get_db)): service = ExchangeService(request.app, db) template = service.get_template_or_404(template_id) return ok(ExchangeTemplateResponse.model_validate(_template_payload(template)).model_dump(mode="json")) @router.post("/templates") def create_template( payload: ExchangeTemplateCreateRequest, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) template = service.create_template( code=payload.code, name=payload.name, biz_domain=payload.biz_domain, biz_obj=payload.biz_obj, operation=payload.operation, description=payload.description, layout=_layout_payload(payload.layout), meta=payload.meta, ) return ok(ExchangeTemplateResponse.model_validate(_template_payload(template)).model_dump(mode="json")) @router.patch("/templates/{template_id}") def update_template( template_id: str, payload: ExchangeTemplateUpdateRequest, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) template = service.update_template( template_id, name=payload.name, description=payload.description, status=payload.status, current_version=payload.current_version, layout=_layout_payload(payload.layout), meta=payload.meta, ) return ok(ExchangeTemplateResponse.model_validate(_template_payload(template)).model_dump(mode="json")) @router.delete("/templates/{template_id}") def delete_template( template_id: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) service.delete_template(template_id) return ok() @router.get("/templates/{template_id}/versions") def list_versions( template_id: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) return ok( [ ExchangeTemplateVersionResponse.model_validate(_version_payload(item)).model_dump(mode="json") for item in service.list_versions(template_id) ] ) @router.get("/templates/{template_id}/versions/{version_id}") def get_version( template_id: str, version_id: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) version = service.get_version_or_404(version_id) if version.template_id != template_id: raise BizError("模板版本不存在", code=404) return ok(ExchangeTemplateVersionResponse.model_validate(_version_payload(version)).model_dump(mode="json")) @router.get("/templates/{template_id}/versions/by-version/{version}") def get_version_by_value( template_id: str, version: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) snapshot = service.get_snapshot(template_id=template_id, version=version) if snapshot is None: raise BizError("模板版本不存在", code=404) return ok(snapshot.as_payload()) @router.post("/templates/{template_id}/versions") def publish_version( template_id: str, payload: ExchangeTemplateVersionCreateRequest, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) version = service.publish_version( template_id=template_id, version=payload.version, variables=[ _variable_from_schema(item) for item in payload.variables ] if payload.variables is not None else None, layout=_layout_payload(payload.layout), meta=payload.meta, make_current=payload.make_current, ) return ok(ExchangeTemplateVersionResponse.model_validate(_version_payload(version)).model_dump(mode="json")) @router.delete("/templates/{template_id}/versions/{version_id}") def delete_version( template_id: str, version_id: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) version = service.get_version_or_404(version_id) if version.template_id != template_id: raise BizError("模板版本不存在", code=404) service.delete_version(version_id) return ok() @router.get("/template-versions/{version_id}") def get_template_version_by_id( version_id: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) snapshot = service.get_snapshot_by_version_id(version_id) if snapshot is None: raise BizError("模板版本不存在", code=404) return ok(snapshot.as_payload()) @router.get("/template-versions/{version_id}/download") @raw_response def download_template_version( version_id: str, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) content = service.build_template_file(version_id) return StreamingResponse( iter([content]), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": 'attachment; filename="template.xlsx"'}, ) @router.post("/templates/{template_id}/versions/upload") def upload_template_version( template_id: str, version: str, request: Request, file: UploadFile = File(...), db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) content = file.file.read() parsed = _excel_template_codec().load(content) snapshot = service.publish_version( template_id=template_id, version=version, variables=[_variable_from_payload(item) for item in parsed.get("variables", [])], layout={ "title": parsed.get("title"), "sheet_name": parsed.get("sheet_name"), }, meta={ "source_file": file.filename, }, file_content=content, file_name=file.filename, ) return ok(ExchangeTemplateVersionResponse.model_validate(_version_payload(snapshot)).model_dump(mode="json")) @router.post("/plans/resolve") def resolve_plan( payload: ExchangePlanResolveRequest, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) source = _resolve_source(payload, request, db) plan = service.resolve_plan( biz_domain=payload.biz_domain, biz_obj=payload.biz_obj, operation=payload.operation, template_id=payload.template_id, version_id=payload.version_id, version=payload.version, code=payload.code, name=payload.name, description=payload.description, layout=_layout_payload(payload.layout), variables=[ _variable_from_schema(item) for item in payload.variables ] if payload.variables is not None else None, source=source, ) return ok(_plan_payload(plan)) @router.post("/plans/template-file") @raw_response def build_plan_template_file( payload: ExchangePlanTemplateFileRequest, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) source = _resolve_source(payload, request, db) plan = service.resolve_plan( biz_domain=payload.biz_domain, biz_obj=payload.biz_obj, operation=payload.operation, template_id=payload.template_id, version_id=payload.version_id, version=payload.version, code=payload.code, name=payload.name, description=payload.description, layout=_layout_payload(payload.layout), variables=[ _variable_from_schema(item) for item in payload.variables ] if payload.variables is not None else None, source=source, ) content = source.load_template_file(plan) if source is not None else None if content is None: content = service.build_plan_template_file(plan) return StreamingResponse( iter([content]), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": 'attachment; filename="template.xlsx"'}, ) @router.get("/tasks") def list_tasks(request: Request, db: Session = Depends(get_db)): service = ExchangeService(request.app, db) return ok( [ ExchangeTaskResponse.model_validate(_task_payload(item)).model_dump(mode="json") for item in service.list_tasks() ] ) @router.get("/tasks/{task_id}") def get_task(task_id: str, request: Request, db: Session = Depends(get_db)): service = ExchangeService(request.app, db) task = service.get_task_or_404(task_id) return ok(ExchangeTaskResponse.model_validate(_task_payload(task)).model_dump(mode="json")) @router.post("/tasks") def create_task( payload: ExchangeTaskCreateRequest, request: Request, db: Session = Depends(get_db), ): service = ExchangeService(request.app, db) source = _resolve_source(payload, request, db) plan = service.resolve_plan( biz_domain=payload.biz_domain, biz_obj=payload.biz_obj, operation=payload.operation, template_id=payload.template_id, version_id=payload.version_id, version=payload.version, code=payload.code, name=payload.name, description=payload.description, layout=_layout_payload(payload.layout), variables=[ _variable_from_schema(item) for item in payload.variables ] if payload.variables is not None else None, source=source, ) task = service.create_task( biz_domain=plan.scope.biz_domain, biz_obj=plan.scope.biz_obj, operation=plan.scope.operation, template_id=plan.template_id, version_id=plan.version_id, version=plan.version, storage_key=payload.storage_key, input_payload=payload.input_payload, ) return ok(ExchangeTaskResponse.model_validate(_task_payload(task)).model_dump(mode="json")) @router.post("/tasks/{task_id}/run") def run_task(task_id: str, request: Request, db: Session = Depends(get_db)): service = ExchangeService(request.app, db) task = service.run_task(task_id) return ok(ExchangeTaskResponse.model_validate(_task_payload(task)).model_dump(mode="json")) @router.get("/tasks/{task_id}/rows") def list_task_rows(task_id: str, request: Request, db: Session = Depends(get_db)): service = ExchangeService(request.app, db) return ok( [ { "id": item.id, "task_id": item.task_id, "row_index": item.row_index, "status": item.status, "data": item.data, "message": item.message, "result": item.result, } for item in service.list_task_rows(task_id) ] ) def _variable_from_payload(item: dict) -> ExchangeVariable: return ExchangeVariable( key=item.get("key"), label=item.get("label") or item.get("key"), header=item.get("header"), description=item.get("description"), required=bool(item.get("required", False)), example=item.get("example"), ) def _excel_template_codec(): from .excel import ExcelTemplateCodec return ExcelTemplateCodec()