feat(calc): 重算授权留痕与批量核算
- calc_receipt 双模式:recalc_reason 非 None=重算(audit action=重算/ reason 留痕、作废旧版本行、calc_version 自动递增、CAS 仅首算执行—— 重算状态本就是已核算,误跑 CAS 会误报并发撞车) - recalc_receipt 薄委托(reason 必填、未核算过 400);重算授权 require_roles(admin, finance),营销后勤不可重算 - batch_calc:逐笔独立事务不开外层 audit(嵌套禁令),逐笔成败清单 (同业务第二笔回款走 #25 重复分支 ok=True/repeated=True) - 测试 +5:重算版本对/原因缺失 400/未核算重算 400/越权 403/批量清单 - 修 next_version 空表 max NULL → int(None) TypeError Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
df1a3839a1
commit
22a2872100
4 files changed
+288
-24
No files matched your search
@@ -8,9 +8,10 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.deps import DbSession, RequestMetaDep, require_roles
|
||||
from app.core.constants import ROLE_ADMIN, ROLE_MARKETING_OPS
|
||||
from app.core.constants import ROLE_ADMIN, ROLE_FINANCE, ROLE_MARKETING_OPS
|
||||
from app.core.response import ApiResponse, PageResult, ok
|
||||
from app.models import User
|
||||
from app.schemas.calc import CalcReceiptOut, LedgerOut, LedgerQuery
|
||||
@@ -19,6 +20,8 @@ from app.services import calc_service
|
||||
router = APIRouter(prefix="/calc", tags=["绩效工资 · 绩效核算"])
|
||||
|
||||
_CalcWriter = Annotated[User, Depends(require_roles(ROLE_ADMIN, ROLE_MARKETING_OPS))]
|
||||
# 重算是授权动作:管理员/财务(营销后勤不可重算)
|
||||
_RecalcUser = Annotated[User, Depends(require_roles(ROLE_ADMIN, ROLE_FINANCE))]
|
||||
|
||||
|
||||
@router.get("/ledger", response_model=ApiResponse[PageResult[LedgerOut]], summary="绩效台账")
|
||||
@@ -55,3 +58,50 @@ def calc_receipt(
|
||||
calc_service.calc_receipt(session, receipt_id, operator=current_user, meta=meta),
|
||||
message="核算完成",
|
||||
)
|
||||
|
||||
|
||||
class RecalcIn(BaseModel):
|
||||
"""重算入参:原因必填(重算授权留痕)。"""
|
||||
|
||||
reason: str = Field(min_length=1, max_length=255, description="重算原因")
|
||||
|
||||
|
||||
class BatchIn(BaseModel):
|
||||
"""批量核算入参。"""
|
||||
|
||||
receipt_ids: list[int] = Field(min_length=1, description="回款 ID 列表")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/receipts/{receipt_id}/recalc",
|
||||
response_model=ApiResponse[CalcReceiptOut],
|
||||
summary="重算回款",
|
||||
)
|
||||
def recalc_receipt(
|
||||
receipt_id: Annotated[int, Path(ge=1)],
|
||||
payload: RecalcIn,
|
||||
session: DbSession,
|
||||
current_user: _RecalcUser,
|
||||
meta: RequestMetaDep,
|
||||
) -> ApiResponse[CalcReceiptOut]:
|
||||
"""重算(管理员/财务):作废旧版本、按当前参数落新版本并留痕。"""
|
||||
return ok(
|
||||
calc_service.recalc_receipt(
|
||||
session, receipt_id, payload.reason, operator=current_user, meta=meta
|
||||
),
|
||||
message="重算完成",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/batch", response_model=ApiResponse[list[dict]], summary="批量核算")
|
||||
def batch_calc(
|
||||
payload: BatchIn,
|
||||
session: DbSession,
|
||||
current_user: _CalcWriter,
|
||||
meta: RequestMetaDep,
|
||||
) -> ApiResponse[list[dict]]:
|
||||
"""批量核算:逐笔独立事务,返回逐笔成败清单(失败不中断其它笔)。"""
|
||||
return ok(
|
||||
calc_service.batch_calc(session, payload.receipt_ids, operator=current_user, meta=meta),
|
||||
message="批量核算完成",
|
||||
)
|
||||
Reference in new issue
Block a user