diff --git a/yunke-server/app/db/models.py b/yunke-server/app/db/models.py index 9bce5b7..d9cbe28 100644 --- a/yunke-server/app/db/models.py +++ b/yunke-server/app/db/models.py @@ -1129,7 +1129,8 @@ class EmployeeCallDaily(Base): class EmployeeScoreConfig(Base): """评分配置版本(v1 §10.4/v2 §3.3)——config_json 存维度/指标/权重/ 方向/阈值/样本门槛;激活前须过权重总和与阈值校验(批次B scoring 实现)。 - MVP 无可视化编辑器,受控 CLI 种子维护。""" + MVP 无可视化编辑器,受控 CLI 种子维护。 + JSON 列(config_json 等)修改须整列重赋值,原地修改不会提交。""" __tablename__ = "employee_score_config" @@ -1142,7 +1143,7 @@ class EmployeeScoreConfig(Base): ) effective_from: Mapped[int | None] = mapped_column(Integer, nullable=True) effective_to: Mapped[int | None] = mapped_column(Integer, nullable=True) - config_json: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=False) + config_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False) created_by: Mapped[str | None] = mapped_column(String(64), nullable=True) approved_by: Mapped[str | None] = mapped_column(String(64), nullable=True) created_at: Mapped[dt.datetime] = mapped_column( diff --git a/yunke-server/tests/test_management_models.py b/yunke-server/tests/test_management_models.py index 2582ea9..03c51b4 100644 --- a/yunke-server/tests/test_management_models.py +++ b/yunke-server/tests/test_management_models.py @@ -3,8 +3,6 @@ 只验证:建表可用、主键/唯一约束生效、默认值正确。业务逻辑在 first_reply_facts / employee_metrics 的测试里。 """ -import datetime as dt # noqa: F401 - import pytest import sqlalchemy as sa from sqlalchemy.exc import IntegrityError @@ -62,6 +60,19 @@ async def test_metric_daily_defaults_zero_not_none(make_session): assert row.data_complete is False or row.data_complete == 0 # SQLite 0/1 +async def test_metric_daily_pk_duplicate_rejected(make_session): + """同 (day, channel, user_id, metric_version) 复合主键防重。 + + Task 6 delete+insert 幂等重算的安全网:版本内重复落表直接 IntegrityError。 + """ + s = make_session + s.add(EmployeeMetricDaily(day=20260901, channel="wechat", user_id="u1")) + await s.commit() + s.add(EmployeeMetricDaily(day=20260901, channel="wechat", user_id="u1")) + with pytest.raises(IntegrityError): + await s.commit() + + async def test_call_daily_roundtrip(make_session): s = make_session s.add(EmployeeCallDaily(day=20260901, user_id="u1", call_count=2, @@ -72,6 +83,15 @@ async def test_call_daily_roundtrip(make_session): assert row.call_seconds == 164 +async def test_call_daily_pk_duplicate_rejected(make_session): + s = make_session + s.add(EmployeeCallDaily(day=20260901, user_id="u1")) + await s.commit() + s.add(EmployeeCallDaily(day=20260901, user_id="u1")) + with pytest.raises(IntegrityError): + await s.commit() + + async def test_score_config_version_unique(make_session): s = make_session s.add(EmployeeScoreConfig(version="v1.0", name="一期试算", @@ -83,6 +103,19 @@ async def test_score_config_version_unique(make_session): await s.commit() +async def test_score_config_config_json_not_null(make_session): + """config_json NOT NULL——注解与 DDL 一致(修复:去掉 | None)。 + + 注意 JSON 列语义:Python None 经 JSON 序列化变成字符串 'null' 而非 + SQL NULL,不会违反约束;测 NOT NULL 须用 sa.null() 显式发 SQL NULL。 + """ + s = make_session + s.add(EmployeeScoreConfig(version="v1.0", name="空配置", + config_json=sa.null())) + with pytest.raises(IntegrityError): + await s.commit() + + async def test_snapshot_batch_user_unique(make_session): s = make_session common = {"snapshot_batch_id": "b1", "period_type": "week",