ASK-20260924-15/17: Bot 通道缺省账套→400 + 差异/趋势入口期间口径回落

- deps.get_entity_id: Bot 通道(带 X-BOT-KEY)无显式账套 → 400(旧为兜底 entity 1=酣客);
  无 key 的存量公开/用户端点行为不变(仍 401/兜底 1)
- 新增 deps.require_bot_entity_id():写端点缺省账套一处收口 → 400(406/422 同族)
  接入 bot_bridge(×2) / bot_bridge_v2 / bot_suggestions / cash_vouchers_json,并同步文档口径
- deviation_engine.run_deviation_check: period=None 不再硬编码当月,逐 KPI 回落
  「该 KPI 自己的最近可用期间」(裁定2b,单一实现收口到 alert_entity_scope);(当月无
  kpi_values ⇒ 差异/趋势入口永久 0 新增);R2/R3 窗口判定改为逐 KPI;显式 period 语义不变
- alert_generator.resolve_latest_period 委托同一实现(消除期间口径两份)
- 测试:新增 tests/test_ask15_17_engine_and_entity_required_20260925.py(12 例);
  4 个既有测试文件把「旧兜底账套 1」显式化(断言未改)
This commit is contained in:
wecom-fullstack
2026-09-25 22:42:25 +08:00
parent 38787a006f
commit 52b693ddbb
13 changed files with 353 additions and 43 deletions
+11 -3
View File
@@ -9,7 +9,7 @@ from sqlalchemy import func, desc
from datetime import datetime
from typing import Optional
from app.database import get_db
from app.deps import get_entity_id
from app.deps import get_entity_id, require_bot_entity_id
from app.models import (
User, UserEntity, StrategicMap, KPIDefinition, KPITemplate, KPIValue,
DataSourceConfig, KPIAlert, OperationLog, NotificationChannel,
@@ -666,7 +666,8 @@ def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dic
kpi_id = data.get("kpi_id")
actual_value = data.get("actual_value")
period = data.get("period")
entity_id = int(data.get("entity_id") or 1)
# ASK-20260924-17(任总 2026-09-24 放行):Bot 通道缺省账套 = 报错,不再静默落 entity 1
entity_id = require_bot_entity_id(data, source="bot_bridge.kpi-value-with-check")
run_check = bool(data.get("run_check", True))
if not kpi_id or actual_value is None:
raise HTTPException(400, "kpi_id 和 actual_value 必填")
@@ -753,7 +754,7 @@ def bot_kpi_create_with_links(data: dict, db: Session = Depends(get_db), bot: di
考核基准类(目标值)= 人拍板;target_value 一律显式置 0(满足治理必填,
后续由人工设定或走 POST /api/cma/bot/ai/suggestions 建议通道待审批)。
"""
entity_id = int(data.get("entity_id") or 1)
entity_id = require_bot_entity_id(data, source="bot_bridge.kpis.create-with-links")
from app.api.kpis import _validate_kpi_data, apply_calc_type_inference
kpi_data = {k: v for k, v in data.items() if k not in ("entity_id", "link_map_id", "link_causality")}
@@ -1202,6 +1203,13 @@ def bot_subjects_upsert(
true → 更新 name/parent/level/category —— **但只更新本账套私有行**;
同码若只以**全局标准科目**(`entity_id IS NULL`)存在,则**永不被账套 UPSERT 改写**(计入 `skipped_global`),
同时也不再插入本账套同码副本(避免标准科目重复)。
- **幂等语义(2026-09-24 R6 收口,判据勿误读)**:`update_existing=true` ⇒ **批内既存行全量重写**
(逐行 UPDATE,不做值级 diff)⇒ 返回的 `updated=N` 是「**批内既存行数**」,**不是**「值发生变化的行数」,
且**含本轮 `inserted` 的新行**:同一 payload 连跑两次,首跑 `inserted=k, updated=m`,
复跑 `inserted=0, updated=k+m`(实测例:首跑 `inserted=5/updated=2` → 复跑 `updated=7`,
见 `operation_logs` detail 原文 id=347/348)。此形态**属既定行为、非异常写入**:库内不产生重复行
(唯一键 `(entity_id, subject_code)` + 只 UPDATE 不 DELETE/INSERT)。幂等判据取 `inserted=0`
且 `before == after`(叠加 `COUNT(*)`/`MAX(id)` 不变),**不得用 `updated` 判幂等**。
- dry_run=true → 只校验与回显,零写入
返回: {"ok":true,"inserted":n,"updated":m,"skipped":k,"skipped_global":g,"entity_id":e,"errors":[]};未授权 401。
"""
+2 -1
View File
@@ -18,6 +18,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
from app.database import get_db
from app.deps import require_bot_entity_id
from app.models import (
MpmResult, BotBridgeConfig,
KPIDefinition, KPIValue, KPIAlert,
@@ -486,7 +487,7 @@ def push_kpi_result(
2. 写入KPIValue (source_type=bot, data_status=verified) — 与auto-verify引擎兼容
3. 调用 run_alert_check 触发阈值预警 → 返回 new_alerts
"""
entity_id = data.get("entity_id", 1)
entity_id = require_bot_entity_id(data, source="bot_bridge_v2.kpi-values")
kpi_code = data.get("kpi_code")
period = data.get("period")
value = data.get("value")
+6 -5
View File
@@ -24,6 +24,7 @@ from sqlalchemy.orm import Session
from app.database import get_db
from app.models import AISuggestion, KPIDefinition, Entity
from app.api.bot_bridge import verify_bot_key
from app.deps import require_bot_entity_id
from app.api.ai_suggestions import _sug_dict
from app.risk_levels import risk_level
@@ -41,10 +42,13 @@ _TARGET_TYPE_MAP = {
def _resolve_bot_entity(request: Request, data: dict, db: Session) -> int:
"""账套解析(Bot无token通道): body.entity_id → X-Entity-Id header → 默认1,校验active。
"""账套解析(Bot无token通道): body.entity_id → X-Entity-Id header → **缺省即报错**。
与 deps.get_entity_id 的 Bot 白名单语义一致(query/header/body 任一来源),
本端点不接收登录token,故不在此处理 Bearer。
ASK-20260924-17(任总 2026-09-24 放行):**取消「默认 1(酣客)」兜底**——
缺省账套改用 `deps.require_bot_entity_id` 收口(缺 → 400,非数字 → 422)。
"""
raw = None
if data.get("entity_id") not in (None, ""):
@@ -53,10 +57,7 @@ def _resolve_bot_entity(request: Request, data: dict, db: Session) -> int:
xh = request.headers.get("X-Entity-Id")
if xh and str(xh).isdigit():
raw = int(xh)
try:
eid = int(raw) if raw is not None else 1
except (TypeError, ValueError):
raise HTTPException(422, f"entity_id 无效: {raw!r}")
eid = require_bot_entity_id({"entity_id": raw}, source="bot_suggestions")
ent = db.query(Entity).filter(Entity.id == eid).first()
if not ent or ent.status != "active":
raise HTTPException(403, f"企业 entity_id={eid} 不存在或未激活")
+13 -5
View File
@@ -29,7 +29,9 @@
—— period 是 voucher_date 派生属性,**不进幂等键**(2026-09-11 加固:日期口径
在 summary 派生/主表 VoucherDate 之间切换时,含 period 会判重失效导致重复入库)。
命中既有行即跳过;仅当命中行 period 与新行不一致时计入 period_conflict 留痕(不覆盖)。
7. entity_id:默认 1(陕西酣客,source=zxt-sxzxb20210228),Bot 通道可传 X-Entity-Id 或 body entity_id。
7. entity_id:**必传**(ASK-20260924-17,任总 2026-09-24 放行)——body `entity_id` 或
header `X-Entity-Id`(1=陕西酣客、2=陕西博海);**缺省 → 400**,
不再兜底默认账套 1(原口径:默认 1 陕西酣客,source=zxt-sxzxb20210228)。
8. 入库后不触发现金流/ledger 联动(凭证完整性未证实前不作为权威来源,见财务口径警示);
也不影响 Excel multipart 导入(原端点不动)。
"""
@@ -45,7 +47,7 @@ from sqlalchemy import text as sa_text
from sqlalchemy.orm import Session
from app.database import get_db
from app.deps import resolve_entity_for_request
from app.deps import resolve_entity_for_request, require_bot_entity_id
from app.models import Entity, ImportLog, VoucherDetail
from app.api.bot_bridge import verify_bot_key
@@ -225,12 +227,18 @@ def api_import_vouchers_json(
nr["CwAccountID"] = v
norm_rows.append(nr)
# ── entity 解析 + active 校验 ──
entity_id = resolve_entity_for_request(request, req_entity if req_entity not in (None, "") else 1)
# ── entity 解析 + active 校验(ASK-20260924-17:Bot 通道必须显式传账套)──
entity_id = resolve_entity_for_request(
request,
require_bot_entity_id(
{"entity_id": req_entity if req_entity not in (None, "") else request.headers.get("X-Entity-Id")},
source="cash_vouchers_json.import/vouchers-json",
),
)
try:
entity_id = int(entity_id)
except Exception:
entity_id = 1
raise HTTPException(422, f"entity_id 无效: {entity_id!r}")
if not _entity_active(db, entity_id):
raise HTTPException(403, f"企业ID={entity_id} 不存在或未激活")