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:
@@ -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。
|
||||
"""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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} 不存在或未激活")
|
||||
|
||||
@@ -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} 不存在或未激活")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user