PayrollManagementSystem/backend/app/api/v1/system/params.py
liuwenxin 8ad3650dd3 feat(system): 计提参数版本管理、变更留痕与计算预览接口
- 9 个端点:目录快照(asOf)/单参数历史/变更批次提交与撤回/批次分页与明细/两日期对比/Excel 导出/计算预览
- submit_change 全量前置校验(注册表/维度岗位/取值域/同批去重)→ 建批次 → 逐项留痕(旧值取提交时生效值)
- 撤回:FOR UPDATE 锁行 + 已生效禁撤,删除后解析自然回退;同日多次变更取最新留痕
- 预览按岗位模板分派公式(经理聚合口径明确拒绝),出参含所用参数与生效时点;导出走 openpyxl
- ECharts 决策记录:前端统计图使用 echarts(用户指定)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-12 20:24:17 +08:00

148 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""计提参数管理接口(需求 #28:参数配置化 + 版本管理 + 留痕)。
全部要求管理员权限(开放问题 #5 的当前默认,后续可放宽只读);
写操作(提交/撤回)经 `RequestMetaDep` 带入请求元信息写入操作日志,
变更批次本身另行留痕(变更人/时间/项/旧值/新值/生效日期)——双保险。
导出为文件流(非统一信封),前端以 blob 接收。
"""
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Path, Query
from fastapi.responses import StreamingResponse
from app.api.deps import AdminUser, DbSession, RequestMetaDep
from app.core.response import ApiResponse, PageResult, ok
from app.schemas.param import (
ParamBatchDetailOut,
ParamBatchOut,
ParamBatchQuery,
ParamCatalogItemOut,
ParamChangeBatchOut,
ParamChangeIn,
ParamDiffOut,
ParamHistoryOut,
ParamHistoryQuery,
ParamPreviewIn,
ParamPreviewOut,
)
from app.services import param_service
router = APIRouter(prefix="/params", tags=["系统管理 · 计提参数"])
@router.get(
"", response_model=ApiResponse[list[ParamCatalogItemOut]], summary="参数目录(含当前生效值)"
)
def get_catalog(
session: DbSession,
_: AdminUser,
as_of: Annotated[date | None, Query(description="业务日期,默认今日")] = None,
) -> ApiResponse[list[ParamCatalogItemOut]]:
"""全部启用参数的定义元数据与截至 asOf 的生效值(前端按分组展示)。"""
return ok(param_service.get_catalog(session, as_of=as_of))
@router.post("/changes", response_model=ApiResponse[ParamChangeBatchOut], summary="提交变更批次")
def submit_change(
session: DbSession,
current_user: AdminUser,
payload: ParamChangeIn,
meta: RequestMetaDep,
) -> ApiResponse[ParamChangeBatchOut]:
"""提交一批参数变更(1~50 项),返回版本号;立即生效或指定日期生效。"""
return ok(
param_service.submit_change(session, payload, operator=current_user, meta=meta),
message="变更已提交",
)
@router.get(
"/changes", response_model=ApiResponse[PageResult[ParamBatchOut]], summary="变更记录分页"
)
def list_changes(
session: DbSession,
_: AdminUser,
query: Annotated[ParamBatchQuery, Query()],
) -> ApiResponse[PageResult[ParamBatchOut]]:
"""按生效日期区间分页查询变更批次(创建时间倒序)。"""
return ok(param_service.list_batches(session, query))
@router.get(
"/changes/{batch_id}", response_model=ApiResponse[ParamBatchDetailOut], summary="批次明细"
)
def get_batch(
session: DbSession,
_: AdminUser,
batch_id: Annotated[int, Path(ge=1)],
) -> ApiResponse[ParamBatchDetailOut]:
"""批次头 + 逐项旧值 → 新值。"""
return ok(param_service.get_batch_detail(session, batch_id))
@router.delete("/changes/{batch_id}", response_model=ApiResponse[None], summary="撤回未生效批次")
def withdraw_batch(
session: DbSession,
current_user: AdminUser,
batch_id: Annotated[int, Path(ge=1)],
meta: RequestMetaDep,
) -> ApiResponse[None]:
"""撤回尚未生效的批次;已生效批次不可撤回。"""
param_service.withdraw_batch(session, batch_id, operator=current_user, meta=meta)
return ok(message="批次已撤回")
@router.get("/compare", response_model=ApiResponse[list[ParamDiffOut]], summary="两日期快照对比")
def compare(
session: DbSession,
_: AdminUser,
from_date: Annotated[date, Query(description="起始业务日期(含)")],
to_date: Annotated[date, Query(description="截止业务日期(含)")],
) -> ApiResponse[list[ParamDiffOut]]:
"""两个业务日期的全量参数快照逐项对比,changed 标记是否变化。"""
return ok(param_service.compare(session, from_date=from_date, to_date=to_date))
@router.get("/export", summary="导出变更记录 Excel")
def export_changes(
session: DbSession,
_: AdminUser,
start_date: Annotated[date | None, Query(description="生效日期起(含)")] = None,
end_date: Annotated[date | None, Query(description="生效日期止(含)")] = None,
) -> StreamingResponse:
"""变更记录导出为 Excel 文件流(非统一信封,前端以 blob 接收)。"""
content = param_service.export_changes(session, start_date=start_date, end_date=end_date)
filename = f"param_changes_{date.today():%Y%m%d}.xlsx"
return StreamingResponse(
iter([content]),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.post("/preview", response_model=ApiResponse[ParamPreviewOut], summary="计算预览")
def preview(
session: DbSession,
_: AdminUser,
payload: ParamPreviewIn,
) -> ApiResponse[ParamPreviewOut]:
"""输入业务场景(岗位/产品/发货/回款/金额),按回款到账日取参数实时试算。"""
return ok(param_service.preview(session, payload))
@router.get(
"/{param_code}/history",
response_model=ApiResponse[PageResult[ParamHistoryOut]],
summary="参数版本历史",
)
def get_history(
session: DbSession,
_: AdminUser,
param_code: Annotated[str, Path(max_length=64)],
query: Annotated[ParamHistoryQuery, Query()],
) -> ApiResponse[PageResult[ParamHistoryOut]]:
"""单参数的版本历史(含版本号、变更人与旧值 → 新值),最新在前。"""
return ok(param_service.get_history(session, param_code, query))