refactor(backend): 仓储层散参数收敛为查询 DTO
仓储层的 list_*/count_* 此前以 keyword/status/offset/limit 四五个散参数 传参,调用点重复铺开、易漏传且不可扩展;按项目「参数 >4 封装 DTO」的 约定统一收敛为各模块的 Query 对象(如 RoleQuery、BusinessQuery), 服务层与测试调用同步调整,行为不变。 - repositories 层查询方法签名改为 (session, query),分页与筛选条件 由 Query 承载,_apply_filters 同步收口 - schemas 层补齐各模块 Query 模型(approval/business_role/report 等) - services 层调用点同步改为传 Query - tests 随之调整;新增用例:待办角色流转(财务通过后领导出现待办)、 参数预览顺序稳定 验证:全量 pytest 900 passed(11:08);单元 526 passed;ruff check 绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
c48d24ab6d
commit
5ece9f6dab
45 files changed
+1045
-1028
No files matched your search
@@ -13,21 +13,24 @@ audit_service.py「上下文内不得吞掉异常后正常退出」的约定相
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.core.constants import ACTION_CREATE, MODULE_BUSINESS
|
||||
from app.core.constants import ACTION_CREATE, MODULE_BUSINESS, MODULE_RECEIPT
|
||||
from app.core.db import get_session_factory
|
||||
from app.main import app
|
||||
from app.models import OperationLog
|
||||
from app.services import business_service
|
||||
from app.services import business_service, receipt_service
|
||||
from tests.integration.factories import create_business
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
_BUSINESSES = "/api/v1/business/businesses"
|
||||
_CUSTOMERS = "/api/v1/base/customers"
|
||||
_RECEIPTS = "/api/v1/business/receipts"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -160,3 +163,94 @@ class TestAuditRetry:
|
||||
# 每层各写一条记录;修复后重试有上限,最终失败仅一条失败日志
|
||||
assert _log_count(normal["username"], success=False) == base_fail + 1
|
||||
assert _log_count(normal["username"], success=True) == base_ok
|
||||
|
||||
|
||||
def _receipt_log_count(username: str, *, success: bool) -> int:
|
||||
"""按操作人统计「新增回款」操作日志条数。"""
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
stmt = (
|
||||
select(func.count())
|
||||
.select_from(OperationLog)
|
||||
.where(
|
||||
OperationLog.username == username,
|
||||
OperationLog.module == MODULE_RECEIPT,
|
||||
OperationLog.action == ACTION_CREATE,
|
||||
OperationLog.summary.like("新增回款%"),
|
||||
OperationLog.success == int(success),
|
||||
)
|
||||
)
|
||||
return int(session.execute(stmt).scalar_one())
|
||||
|
||||
|
||||
def _receipt_payload(business_id: int) -> dict:
|
||||
return {
|
||||
"business_id": business_id,
|
||||
"receipt_date": str(date.today()),
|
||||
"amount": "1200.00",
|
||||
}
|
||||
|
||||
|
||||
class TestReceiptAuditRetry:
|
||||
"""回款新增的 1062 重试路径(回款与业务同构,但走独立实现)。
|
||||
|
||||
回款侧的重试循环本次从 create 函数体内移入 helper 调用,重试边界、
|
||||
审计条数、批次号重取都必须保持原样 —— 用例会盯住这三条。
|
||||
"""
|
||||
|
||||
def test_首次撞号重试成功_审计恰一条真成功(
|
||||
self, quiet_client: TestClient, normal: dict, monkeypatch
|
||||
) -> None:
|
||||
client = quiet_client
|
||||
business = create_business(client, normal["headers"])
|
||||
first = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert first.status_code == 200, first.text
|
||||
existing_no = first.json()["data"]["receipt_batch_no"]
|
||||
|
||||
real_generate = receipt_service._generate_no
|
||||
calls = {"n": 0}
|
||||
|
||||
def flaky_generate(session):
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
return existing_no # 首次返回库内已存在批次号 → flush 撞 1062
|
||||
return real_generate(session)
|
||||
|
||||
monkeypatch.setattr(receipt_service, "_generate_no", flaky_generate)
|
||||
base_ok = _receipt_log_count(normal["username"], success=True)
|
||||
base_fail = _receipt_log_count(normal["username"], success=False)
|
||||
|
||||
second = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert second.status_code == 200, second.text
|
||||
assert calls["n"] == 2, "重试应恰好再取一次号"
|
||||
assert _receipt_log_count(normal["username"], success=True) == base_ok + 1
|
||||
assert _receipt_log_count(normal["username"], success=False) == base_fail
|
||||
|
||||
def test_持续冲突_最终失败且审计恰一条失败(
|
||||
self, quiet_client: TestClient, normal: dict, monkeypatch
|
||||
) -> None:
|
||||
client = quiet_client
|
||||
business = create_business(client, normal["headers"])
|
||||
first = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert first.status_code == 200, first.text
|
||||
existing_no = first.json()["data"]["receipt_batch_no"]
|
||||
|
||||
def always_conflict(session):
|
||||
return existing_no
|
||||
|
||||
monkeypatch.setattr(receipt_service, "_generate_no", always_conflict)
|
||||
base_ok = _receipt_log_count(normal["username"], success=True)
|
||||
base_fail = _receipt_log_count(normal["username"], success=False)
|
||||
|
||||
second = client.post(
|
||||
_RECEIPTS, json=_receipt_payload(business["id"]), headers=normal["headers"]
|
||||
)
|
||||
assert second.status_code == 400, second.text
|
||||
assert _receipt_log_count(normal["username"], success=False) == base_fail + 1
|
||||
assert _receipt_log_count(normal["username"], success=True) == base_ok
|
||||
Reference in new issue
Block a user