feat(customer): 客户评级并入客户管理并落库

- customers 表新增 rating_score/rating_time(V13 迁移 + V10 SQL + init-dev 同步)
- 新端点 PUT /customers/{id}/rating:评分是档案属性,只存总分与评级时间(五项明细不落库)
- 列表排序 order_by=rating_score,未评分客户升序/降序都垫底,id desc 兜底稳定分页
- 前端 RatingCalculatorDialog 并入客户管理(评分列 + 评级计算按钮),删除独立页与路由
- 测试:评级保存/越界/排序垫底 3 例,test_customers.py 18 passed;schema 同步 216 passed
This commit is contained in:
liuwenxin committed 2026-10-23 10:59:33 +08:00
1 parent b9781f7d3f
commit 32b7940067
17 files changed
+917 -512

No files matched your search

@@ -376,3 +376,73 @@ class TestAudit:
latest = logs[0]
assert customer["company_name"] in latest.summary, "摘要应含客户名"
assert "→" in latest.summary, "摘要应体现新旧归属走向"
class TestRating:
"""评级保存与列表排序 —— 评分是档案属性,只存总分与评级时间。
守的是:评分只走专属端点(编辑端点不含评分,旁路写评分会绕开口径);
分值红线 0-100 在入口拦截;按评分排序时未评分客户必须垫底,不能因
MySQL 把 NULL 视为最小值而在升序里压过打了低分的客户。
"""
def test_保存评级_回显总分与时间_且编辑端点不含评分(
self, client: TestClient, normal: dict
) -> None:
headers = normal["headers"]
customer = _create_customer(client, headers)
assert customer["rating_score"] is None, "新客户应无评分"
resp = client.put(
f"{_CUSTOMERS}/{customer['id']}/rating", json={"score": 88}, headers=headers
)
assert resp.status_code == 200, resp.text
body = resp.json()["data"]
assert body["rating_score"] == 88
assert body["rating_time"] is not None, "保存评分必须刷新评级时间"
# 覆盖式保存:新分数替换旧分数
resp = client.put(
f"{_CUSTOMERS}/{customer['id']}/rating", json={"score": 60}, headers=headers
)
assert resp.status_code == 200, resp.text
assert resp.json()["data"]["rating_score"] == 60
def test_评分越界_400(self, client: TestClient, normal: dict) -> None:
headers = normal["headers"]
customer = _create_customer(client, headers)
resp = client.put(
f"{_CUSTOMERS}/{customer['id']}/rating", json={"score": 101}, headers=headers
)
assert resp.status_code == 400, resp.text
def test_按评分排序_未评分垫底_升序降序都不压过低分(
self, client: TestClient, normal: dict
) -> None:
headers = normal["headers"]
high = _create_customer(client, headers)
low = _create_customer(client, headers)
unrated = _create_customer(client, headers)
for target, score in ((high, 80), (low, 60)):
resp = client.put(
f"{_CUSTOMERS}/{target['id']}/rating", json={"score": score}, headers=headers
)
assert resp.status_code == 200, resp.text
def _order_names(order: str) -> list[str]:
resp = client.get(
_CUSTOMERS,
params={"pageNum": 1, "pageSize": 50, "order_by": "rating_score", "order": order},
headers=headers,
)
assert resp.status_code == 200, resp.text
names = [row["company_name"] for row in resp.json()["data"]["list"]]
return names
# 降序:高分在前,未评分最后;升序:低分在前,未评分仍垫底
desc_names = _order_names("desc")
assert desc_names.index(high["company_name"]) < desc_names.index(low["company_name"])
assert desc_names[-1] == unrated["company_name"]
asc_names = _order_names("asc")
assert asc_names.index(low["company_name"]) < asc_names.index(high["company_name"])
assert asc_names[-1] == unrated["company_name"], "未评分必须垫底"