2026-11-01 20:42:53 -05:00
|
|
|
|
"""T+ 基础导入和预演的管理员入口;HTTP 不执行长查询或核算。"""
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Annotated
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Path, Query
|
|
|
|
|
|
from starlette.responses import StreamingResponse
|
|
|
|
|
|
|
|
|
|
|
|
from app.api.deps import DbSession, PayrollWriter, WorkbenchReader
|
|
|
|
|
|
from app.core.response import ApiResponse, PageQuery, PageResult, ok
|
|
|
|
|
|
from app.schemas.tplus_workbench import FactIn, SourceChoiceQuery, WorkCreateIn, WorkQuery
|
|
|
|
|
|
from app.services.tplus import workbench
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
RunNo = Annotated[str, Path(pattern=r"^[A-Za-z0-9_-]+$", max_length=32)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/facts", response_model=ApiResponse[PageResult[dict]])
|
|
|
|
|
|
def facts(session: DbSession, _: WorkbenchReader, query: Annotated[WorkQuery, Query()]):
|
|
|
|
|
|
"""有界查询每项补充资料的当前版本。"""
|
|
|
|
|
|
return ok(workbench.list_facts(session, query))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/facts", response_model=ApiResponse[dict])
|
|
|
|
|
|
def save_fact(session: DbSession, admin: PayrollWriter, payload: FactIn):
|
|
|
|
|
|
"""追加有依据的人工事实;旧版本不覆盖。"""
|
|
|
|
|
|
return ok(workbench.save_fact(session, payload, admin.id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/facts/{fact_id}/history", response_model=ApiResponse[PageResult[dict]])
|
|
|
|
|
|
def fact_history(
|
|
|
|
|
|
session: DbSession, _: WorkbenchReader, fact_id: int, query: Annotated[PageQuery, Query()]
|
|
|
|
|
|
):
|
|
|
|
|
|
"""按事实身份分页查看修改历史,不允许跨账套读取。"""
|
2026-11-01 20:42:55 -05:00
|
|
|
|
return ok(workbench.fact_history(session, fact_id, query))
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/work-runs", response_model=ApiResponse[dict])
|
|
|
|
|
|
def create_work(session: DbSession, admin: PayrollWriter, payload: WorkCreateIn):
|
|
|
|
|
|
"""创建导入预览、执行导入或核算预演任务。"""
|
|
|
|
|
|
return ok(workbench.queue_work(session, payload, admin.id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/work-runs", response_model=ApiResponse[PageResult[dict]])
|
|
|
|
|
|
def work_runs(session: DbSession, _: WorkbenchReader, query: Annotated[WorkQuery, Query()]):
|
|
|
|
|
|
"""分页查询任务,所有金额结果仅为预演。"""
|
|
|
|
|
|
return ok(workbench.list_work(session, query))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/work-runs/{run_no}", response_model=ApiResponse[dict])
|
|
|
|
|
|
def work_detail(session: DbSession, _: WorkbenchReader, run_no: RunNo):
|
|
|
|
|
|
"""查询规则版本、输入指纹及结果。"""
|
|
|
|
|
|
return ok(workbench.work_out(workbench.get_run(session, run_no)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/work-runs/{run_no}/lines", response_model=ApiResponse[PageResult[dict]])
|
|
|
|
|
|
def work_lines(
|
|
|
|
|
|
session: DbSession, _: WorkbenchReader, run_no: RunNo, query: Annotated[WorkQuery, Query()]
|
|
|
|
|
|
):
|
|
|
|
|
|
"""分页查看新增、待补及分配明细,不公开原始快照 JSON。"""
|
|
|
|
|
|
return ok(workbench.list_lines(session, run_no, query))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/source-records", response_model=ApiResponse[PageResult[dict]])
|
|
|
|
|
|
def source_choices(
|
|
|
|
|
|
session: DbSession,
|
|
|
|
|
|
_: WorkbenchReader,
|
|
|
|
|
|
query: Annotated[SourceChoiceQuery, Query()],
|
|
|
|
|
|
):
|
|
|
|
|
|
"""仅提供挑选资料所需的源编号和名称,不返回电话、地址和原始载荷。"""
|
2026-11-01 20:42:55 -05:00
|
|
|
|
return ok(workbench.source_choices(session, query))
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/work-runs/{run_no}/report")
|
|
|
|
|
|
def work_report(session: DbSession, _: WorkbenchReader, run_no: RunNo):
|
|
|
|
|
|
"""导出全部预演明细,防止源单号被电子表格当作公式执行。"""
|
|
|
|
|
|
return StreamingResponse(
|
2026-11-01 20:42:55 -05:00
|
|
|
|
workbench.report_csv(session, run_no),
|
2026-11-01 20:42:53 -05:00
|
|
|
|
media_type="text/csv; charset=utf-8",
|
|
|
|
|
|
headers={"Content-Disposition": f'attachment; filename="{run_no}-preview.csv"'},
|
|
|
|
|
|
)
|