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:
liuwenxinandClaude Code committed 2026-10-22 19:43:20 +08:00
1 parent c48d24ab6d
commit 5ece9f6dab
45 files changed
+1045 -1028

No files matched your search

+36 -33
View File
@@ -44,7 +44,9 @@ def _role_id(code: str) -> int:
)
def _person(client: TestClient, headers: dict, *, role: str = "salesperson", manager_id=None) -> dict:
def _person(
client: TestClient, headers: dict, *, role: str = "salesperson", manager_id=None
) -> dict:
body = {
"personnel_name": f"t_报表_{uuid.uuid4().hex[:8]}",
"business_role_id": _role_id(role),
@@ -194,9 +196,7 @@ def test_部门汇总_无成员经理出零值行(admin, client: TestClient):
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
team = next(
(t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"]), None
)
team = next((t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"]), None)
assert team is not None
assert Decimal(str(team["team_base"])) == Decimal("0")
assert team["member_count"] == 0
@@ -224,7 +224,14 @@ def test_回款统计_空桶恒在且分摊拆行金额只计一次(admin, clien
assert len(data["buckets"]) == 8
assert [b["code"] for b in data["buckets"]] == [
"le0", "1-30", "31-60", "61-90", "91-180", "181-360", "361+", "none",
"le0",
"1-30",
"31-60",
"61-90",
"91-180",
"181-360",
"361+",
"none",
]
# 60 天前发货 → 落在 61-90 桶(发货日首日计第 1 天)
bucket = next(b for b in data["buckets"] if b["code"] == "61-90")
@@ -293,11 +300,15 @@ def test_导出_excel含签字栏且入操作日志(admin, client: TestClient):
factory = get_session_factory()
with factory() as session:
log = session.execute(
select(OperationLog)
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
.order_by(OperationLog.id.desc())
).scalars().first()
log = (
session.execute(
select(OperationLog)
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
.order_by(OperationLog.id.desc())
)
.scalars()
.first()
)
assert log is not None
@@ -389,19 +400,21 @@ def _latest_export_log():
factory = get_session_factory()
with factory() as session:
return session.execute(
select(OperationLog)
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
.order_by(OperationLog.id.desc())
).scalars().first()
return (
session.execute(
select(OperationLog)
.where(OperationLog.module == "报表统计", OperationLog.action == "导出")
.order_by(OperationLog.id.desc())
)
.scalars()
.first()
)
def test_部门汇总_无台账经理挂上级不报500(admin, client: TestClient):
"""经理 A 无台账且上级是经理 B:不能因 member_map 直接索引而 KeyError。"""
manager_b = _person(client, admin["headers"], role="sales_manager")
manager_a = _person(
client, admin["headers"], role="sales_manager", manager_id=manager_b["id"]
)
manager_a = _person(client, admin["headers"], role="sales_manager", manager_id=manager_b["id"])
resp = client.get(
f"{_REPORTS}/department-summary",
@@ -467,9 +480,7 @@ def test_部门汇总_经理个人户计入时基数等于成员合计(admin, cl
headers=admin["headers"],
)
assert resp.status_code == 200, resp.text
team = next(
t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"]
)
team = next(t for t in resp.json()["data"]["teams"] if t["manager_id"] == manager["id"])
assert Decimal(str(team["team_base"])) == Decimal("100.00")
assert sum(
(Decimal(str(m["performance_sum"])) for m in team["members"]), Decimal("0")
@@ -516,9 +527,7 @@ def test_部门汇总_按经理筛选不误判其他团队下属(admin, client:
assert [t["manager_id"] for t in data["teams"]] == [manager_1["id"]]
assert data["unassigned"] == []
assert all(
m["personnel_id"] != salesperson_2["id"]
for team in data["teams"]
for m in team["members"]
m["personnel_id"] != salesperson_2["id"] for team in data["teams"] for m in team["members"]
)
assert Decimal(str(data["reconciliation"]["other_team_base"])) > 0
@@ -557,9 +566,7 @@ def test_部门汇总_对账恒等式(admin, client: TestClient):
)
def _two_manager_scene(
client: TestClient, headers: dict
) -> tuple[dict, dict, dict, dict, dict]:
def _two_manager_scene(client: TestClient, headers: dict) -> tuple[dict, dict, dict, dict, dict]:
"""两位经理各有下属与业务 + 一位零成员经理(无聚合行)。"""
manager_1 = _person(client, headers, role="sales_manager")
manager_2 = _person(client, headers, role="sales_manager")
@@ -777,9 +784,7 @@ def test_导出取数异常_失败审计留痕(admin, client: TestClient, monkey
assert "导出个人绩效工资明细表" in (log.summary or "")
@pytest.mark.parametrize(
"path", ["/personal-detail", "/department-summary", "/payment-stats"]
)
@pytest.mark.parametrize("path", ["/personal-detail", "/department-summary", "/payment-stats"])
def test_非法月份_查询返回400(admin, client: TestClient, path: str):
"""非法月份必须是参数错误 400,不能落 500。"""
resp = client.get(
@@ -790,9 +795,7 @@ def test_非法月份_查询返回400(admin, client: TestClient, path: str):
assert resp.status_code == 400, resp.text
@pytest.mark.parametrize(
"code", ["personal-detail", "department-summary", "payment-stats"]
)
@pytest.mark.parametrize("code", ["personal-detail", "department-summary", "payment-stats"])
def test_非法月份_导出返回400(admin, client: TestClient, code: str):
"""导出手工构造查询模型同样要转 400。"""
resp = client.get(