5f0acfa79e
- 删除 vision_yolo/vision_llm 及路由 /api/vision/frame|llm(保留遥测 event),去除 ultralytics 依赖 - 视频识别改为每园区(per-tenant)配置,经 park_settings(kv) 键 vision_api:<tid> 存取 - /park/api/vision/config GET/PUT(园区端读写本园区识别地址,可局域网);bind 下发含 vision_api - 新增 alembic 0004(tenant_id 可空) / 0005(park_settings 表),迁移脚本执行
566 lines
22 KiB
Python
566 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""园区租户(多租户)数据层 —— 唯一总库(SQLAlchemy async,config.DATABASE_URL)。
|
||
|
||
表:park_tenants / park_companies / park_kb_docs / park_screens(ORM 见 app/infrastructure/models.py)。
|
||
建表/种子由 alembic + scripts/db/seed.py 在非运行态完成;本层仅读写。运行时不建表不灌种子。
|
||
sim_engine 需同步 tick,故另提供同步只读访问器 get_tenant_data_sync。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
import hashlib
|
||
import json
|
||
import secrets
|
||
import time
|
||
import uuid
|
||
|
||
from sqlalchemy import delete, select, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||
|
||
from ..config import DATABASE_URL
|
||
from ..infrastructure.db import make_async_engine
|
||
from ..infrastructure.models import ParkCompany, ParkKbDoc, ParkScreen, ParkSetting, ParkTenant
|
||
from .sim_engine import DEFAULT_DATA, refresh_engine
|
||
|
||
_DEFAULT_TENANT_ID = "T001"
|
||
|
||
# 单一异步引擎(进程内共享,避免每操作重建);建表/种子不走这里
|
||
_ASYNC_ENGINE = None
|
||
|
||
|
||
def _get_session() -> AsyncSession:
|
||
global _ASYNC_ENGINE
|
||
if _ASYNC_ENGINE is None:
|
||
_ASYNC_ENGINE = make_async_engine(DATABASE_URL)
|
||
return async_sessionmaker(bind=_ASYNC_ENGINE, expire_on_commit=False)()
|
||
|
||
|
||
def _hash_password(password: str, salt: str | None = None) -> tuple[str, str]:
|
||
salt = salt or secrets.token_hex(16)
|
||
h = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 100_000).hex()
|
||
return salt, h
|
||
|
||
|
||
def _verify_password(password: str, salt: str, expected: str) -> bool:
|
||
return _hash_password(password, salt)[1] == expected
|
||
|
||
|
||
def _now() -> str:
|
||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
|
||
def _new_id(prefix: str) -> str:
|
||
return f"{prefix}{int(uuid.uuid4().int % 1000000000):09d}"
|
||
|
||
|
||
# ---------------- 同步只读(sim_engine 同步 tick 线程用) ----------------
|
||
|
||
def get_tenant_data_sync(tenant_id: str) -> dict:
|
||
"""同步读取该租户 data + companies(sim_engine 同步 tick,无事件循环时用)。"""
|
||
from sqlalchemy import create_engine, text
|
||
url = DATABASE_URL.replace("+aiosqlite", "")
|
||
engine = create_engine(url, future=True)
|
||
try:
|
||
with engine.connect() as conn:
|
||
row = conn.execute(text("SELECT data_json FROM park_tenants WHERE id=:id"), {"id": tenant_id}).fetchone()
|
||
data = json.loads(row[0] if row and row[0] else "{}")
|
||
comps = [_rd(r) for r in conn.execute(
|
||
text("SELECT * FROM park_companies WHERE tenant_id=:tid ORDER BY created_at DESC"), {"tid": tenant_id})]
|
||
if comps:
|
||
data = dict(data)
|
||
data["companies"] = comps
|
||
return data or dict(DEFAULT_DATA)
|
||
finally:
|
||
engine.dispose()
|
||
|
||
|
||
def _sync_conn():
|
||
from sqlalchemy import create_engine
|
||
return create_engine(DATABASE_URL.replace("+aiosqlite", ""), future=True)
|
||
|
||
|
||
def list_tenants_sync() -> list[dict]:
|
||
from sqlalchemy import text
|
||
engine = _sync_conn()
|
||
try:
|
||
with engine.connect() as conn:
|
||
rows = conn.execute(text("SELECT * FROM park_tenants ORDER BY created_at DESC")).fetchall()
|
||
return [_to_tenant(r) for r in rows]
|
||
finally:
|
||
engine.dispose()
|
||
|
||
|
||
def list_companies_sync(tenant_id: str) -> list[dict]:
|
||
from sqlalchemy import text
|
||
engine = _sync_conn()
|
||
try:
|
||
with engine.connect() as conn:
|
||
return [_rd(r) for r in conn.execute(
|
||
text("SELECT * FROM park_companies WHERE tenant_id=:tid ORDER BY created_at DESC"), {"tid": tenant_id})]
|
||
finally:
|
||
engine.dispose()
|
||
|
||
|
||
def get_agent_sync(tenant_id: str) -> dict:
|
||
from sqlalchemy import text
|
||
engine = _sync_conn()
|
||
try:
|
||
with engine.connect() as conn:
|
||
row = conn.execute(text("SELECT agent_json FROM park_tenants WHERE id=:id"), {"id": tenant_id}).fetchone()
|
||
return json.loads(row[0]) if row and row[0] else {}
|
||
finally:
|
||
engine.dispose()
|
||
|
||
|
||
# ---------------- 映射 ----------------
|
||
|
||
def _val(row, key):
|
||
if isinstance(row, dict):
|
||
return row.get(key)
|
||
if hasattr(row, key):
|
||
return getattr(row, key)
|
||
return row[key]
|
||
|
||
|
||
def _rd(row) -> dict:
|
||
"""sqlalchemy Row → dict(兼容 Row/RowMapping)。"""
|
||
if isinstance(row, dict):
|
||
return row
|
||
return dict(row._mapping) if hasattr(row, "_mapping") else {k: row[k] for k in row.keys()}
|
||
|
||
|
||
def _to_tenant(row) -> dict:
|
||
return {"id": _val(row, "id"), "name": _val(row, "name"),
|
||
"intro": json.loads(_val(row, "intro_json") or "[]"),
|
||
"auth": {"username": _val(row, "username"), "salt": _val(row, "salt") or "", "password_hash": _val(row, "password_hash") or ""},
|
||
"admin_username": _val(row, "admin_username") or "", "status": _val(row, "status") or "active",
|
||
"data": json.loads(_val(row, "data_json") or "{}"), "agent": json.loads(_val(row, "agent_json") or "{}")}
|
||
|
||
|
||
def _co(r) -> dict:
|
||
return {"id": r.id, "name": r.name, "zone": r.zone, "room": r.room, "industry": r.industry, "bio": r.bio,
|
||
"founder": r.founder, "status": r.status, "employees": r.employees, "created_at": r.created_at}
|
||
|
||
|
||
def _kb(r) -> dict:
|
||
return {"id": r.id, "grp": r.grp, "title": r.title, "content_md": r.content_md, "created_at": r.created_at}
|
||
|
||
|
||
def _sc(r) -> dict:
|
||
return {"id": r.id, "tenant_id": r.tenant_id, "device_id": r.device_id, "name": r.name, "role": r.role,
|
||
"location": r.location, "status": r.status, "code": r.code, "created_at": r.created_at}
|
||
|
||
|
||
def _summary(tid: str, name: str, intro: list, username: str, admin_username: str = "", status: str = "active") -> dict:
|
||
return {"id": tid, "name": name, "intro": intro, "username": username, "admin_username": admin_username, "status": status}
|
||
|
||
|
||
# ---------------- 租户 ----------------
|
||
|
||
async def list_tenants() -> list[dict]:
|
||
async with _get_session() as s:
|
||
rows = (await s.execute(select(ParkTenant).order_by(ParkTenant.created_at.desc()))).scalars().all()
|
||
return [_to_tenant(r) for r in rows]
|
||
|
||
|
||
async def get_tenant(tenant_id: str) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkTenant, tenant_id)
|
||
return _to_tenant(row) if row else None
|
||
|
||
|
||
async def get_tenant_data(tenant_id: str) -> dict:
|
||
data = (await get_tenant(tenant_id) or {}).get("data", {})
|
||
comps = await list_companies(tenant_id)
|
||
if comps:
|
||
data = dict(data)
|
||
data["companies"] = comps
|
||
return data or dict(DEFAULT_DATA)
|
||
|
||
|
||
async def create_tenant(name: str, intro: list[str], username: str = "", password: str = "", tenant_id: str | None = None) -> dict:
|
||
tid = tenant_id or _new_id("T")
|
||
data = dict(DEFAULT_DATA)
|
||
async with _get_session() as s:
|
||
s.add(ParkTenant(id=tid, name=name, intro_json=json.dumps(intro or ["", ""], ensure_ascii=False),
|
||
username=username, salt="", password_hash="", admin_username="", status="active",
|
||
data_json=json.dumps(data, ensure_ascii=False), agent_json="{}", created_at=_now()))
|
||
await s.commit()
|
||
await _append_companies(s, tid, DEFAULT_DATA["companies"])
|
||
await s.commit()
|
||
refresh_engine(tid, get_tenant_data_sync(tid))
|
||
return _summary(tid, name, intro, username)
|
||
|
||
|
||
async def _append_companies(s, tenant_id: str, companies: list) -> None:
|
||
for c in companies:
|
||
nm = c["name"] if isinstance(c, dict) else c
|
||
exists = (await s.execute(select(ParkCompany).where(ParkCompany.tenant_id == tenant_id, ParkCompany.name == nm).limit(1))).scalars().first()
|
||
if exists:
|
||
continue
|
||
s.add(ParkCompany(id=_new_id("PC"), tenant_id=tenant_id, name=nm,
|
||
zone=c.get("zone", "") if isinstance(c, dict) else "", room=c.get("room", "") if isinstance(c, dict) else "",
|
||
industry=c.get("industry", "") if isinstance(c, dict) else "", bio=c.get("bio", "") if isinstance(c, dict) else "",
|
||
founder=c.get("founder", "") if isinstance(c, dict) else "", status="active", employees=None, created_at=_now()))
|
||
|
||
|
||
async def bind_admin(tenant_id: str, username: str) -> bool:
|
||
async with _get_session() as s:
|
||
r = await s.execute(update(ParkTenant).where(ParkTenant.id == tenant_id).values(admin_username=username.strip()))
|
||
await s.commit()
|
||
return r.rowcount > 0
|
||
|
||
|
||
async def unbind_admin(tenant_id: str) -> bool:
|
||
return await bind_admin(tenant_id, "")
|
||
|
||
|
||
async def find_by_admin(username: str) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkTenant).where(ParkTenant.admin_username == username.strip()).limit(1))).scalars().first()
|
||
return _to_tenant(row) if row else None
|
||
|
||
|
||
async def update_tenant(tenant_id: str, patch: dict) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkTenant, tenant_id)
|
||
if row is None:
|
||
return None
|
||
if patch.get("name") is not None:
|
||
row.name = patch["name"]
|
||
if patch.get("intro") is not None:
|
||
row.intro_json = json.dumps(patch["intro"], ensure_ascii=False)
|
||
if patch.get("data") is not None:
|
||
row.data_json = json.dumps(patch["data"], ensure_ascii=False)
|
||
if patch.get("agent") is not None:
|
||
row.agent_json = json.dumps(patch["agent"], ensure_ascii=False)
|
||
if patch.get("username") is not None:
|
||
row.username = patch["username"]
|
||
if patch.get("password"):
|
||
salt, h = _hash_password(patch["password"])
|
||
row.salt, row.password_hash = salt, h
|
||
await s.commit()
|
||
await s.refresh(row)
|
||
if patch.get("data"):
|
||
refresh_engine(tenant_id, json.loads(row.data_json))
|
||
return _summary(tenant_id, row.name, json.loads(row.intro_json or "[]"), row.username, row.admin_username or "", row.status or "active")
|
||
|
||
|
||
async def delete_tenant(tenant_id: str) -> bool:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkTenant, tenant_id)
|
||
if row is None:
|
||
return False
|
||
await s.delete(row) # 级联删除 companies/kb/screens(FK CASCADE)
|
||
await s.commit()
|
||
from .sim_engine import _engines
|
||
_engines.pop(tenant_id, None)
|
||
return True
|
||
|
||
|
||
async def set_status(tenant_id: str, status: str) -> bool:
|
||
if status not in ("active", "disabled"):
|
||
return False
|
||
async with _get_session() as s:
|
||
r = await s.execute(update(ParkTenant).where(ParkTenant.id == tenant_id).values(status=status))
|
||
await s.commit()
|
||
return r.rowcount > 0
|
||
|
||
|
||
async def ensure_default_tenant() -> str:
|
||
t = await get_tenant(_DEFAULT_TENANT_ID)
|
||
if t is None:
|
||
await create_tenant("昆明市大学生创业园", ["云南省首家政府主办大学生创业孵化园区", "空间 + 孵化 + 融资 + 政策 + AI 赋能 + 综合服务"], "admin", tenant_id=_DEFAULT_TENANT_ID)
|
||
elif not await list_companies(_DEFAULT_TENANT_ID):
|
||
async with _get_session() as s:
|
||
await _append_companies(s, _DEFAULT_TENANT_ID, DEFAULT_DATA["companies"])
|
||
await s.commit()
|
||
return _DEFAULT_TENANT_ID
|
||
|
||
|
||
# ---------------- 入驻企业 ----------------
|
||
|
||
async def list_companies(tenant_id: str) -> list[dict]:
|
||
async with _get_session() as s:
|
||
rows = (await s.execute(select(ParkCompany).where(ParkCompany.tenant_id == tenant_id).order_by(ParkCompany.created_at.desc()))).scalars().all()
|
||
return [_co(r) for r in rows]
|
||
|
||
|
||
async def create_company(tenant_id: str, payload: dict) -> dict:
|
||
row = ParkCompany(id=_new_id("PC"), tenant_id=tenant_id, name=payload.get("name", ""),
|
||
zone=payload.get("zone", ""), room=payload.get("room", ""), industry=payload.get("industry", ""),
|
||
bio=payload.get("bio", ""), founder=payload.get("founder", ""),
|
||
status=payload.get("status", "applying"), employees=payload.get("employees"), created_at=_now())
|
||
async with _get_session() as s:
|
||
s.add(row)
|
||
await s.commit()
|
||
refresh_engine(tenant_id, get_tenant_data_sync(tenant_id))
|
||
return _co(row)
|
||
|
||
|
||
async def update_company(tenant_id: str, cid: str, payload: dict) -> dict | None:
|
||
fields = {k: v for k, v in payload.items() if v is not None and k in ("name", "zone", "room", "industry", "bio", "founder", "status", "employees")}
|
||
if not fields:
|
||
return None
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkCompany).where(ParkCompany.id == cid, ParkCompany.tenant_id == tenant_id))).scalars().first()
|
||
if row is None:
|
||
return None
|
||
for k, v in fields.items():
|
||
setattr(row, k, v)
|
||
await s.commit()
|
||
await s.refresh(row)
|
||
refresh_engine(tenant_id, get_tenant_data_sync(tenant_id))
|
||
return _co(row)
|
||
|
||
|
||
async def delete_company(tenant_id: str, cid: str) -> bool:
|
||
async with _get_session() as s:
|
||
r = await s.execute(delete(ParkCompany).where(ParkCompany.id == cid, ParkCompany.tenant_id == tenant_id))
|
||
await s.commit()
|
||
ok = r.rowcount > 0
|
||
refresh_engine(tenant_id, get_tenant_data_sync(tenant_id))
|
||
return ok
|
||
|
||
|
||
# ---------------- 智能体 ----------------
|
||
|
||
async def get_agent(tenant_id: str) -> dict:
|
||
return (await get_tenant(tenant_id) or {}).get("agent", {})
|
||
|
||
|
||
async def update_agent(tenant_id: str, payload: dict) -> dict:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkTenant, tenant_id)
|
||
agent = json.loads(row.agent_json or "{}")
|
||
for k, v in payload.items():
|
||
if v is not None:
|
||
agent[k] = v
|
||
row.agent_json = json.dumps(agent, ensure_ascii=False)
|
||
await s.commit()
|
||
return agent
|
||
|
||
|
||
# ---------------- 知识库 ----------------
|
||
|
||
async def kb_docs(tenant_id: str) -> list[dict]:
|
||
async with _get_session() as s:
|
||
rows = (await s.execute(select(ParkKbDoc).where(ParkKbDoc.tenant_id == tenant_id).order_by(ParkKbDoc.created_at.desc()))).scalars().all()
|
||
return [_kb(r) for r in rows]
|
||
|
||
|
||
async def create_kb_doc(tenant_id: str, payload: dict) -> dict:
|
||
row = ParkKbDoc(id=_new_id("KB"), tenant_id=tenant_id, grp=payload.get("group", "general"),
|
||
title=payload.get("title", ""), content_md=payload.get("content_md", ""), created_at=_now())
|
||
async with _get_session() as s:
|
||
s.add(row)
|
||
await s.commit()
|
||
return _kb(row)
|
||
|
||
|
||
async def update_kb_doc(tenant_id: str, did: str, payload: dict) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkKbDoc).where(ParkKbDoc.id == did, ParkKbDoc.tenant_id == tenant_id))).scalars().first()
|
||
if row is None:
|
||
return None
|
||
for k, v in payload.items():
|
||
if v is not None and k in ("grp", "title", "content_md"):
|
||
setattr(row, k, v)
|
||
await s.commit()
|
||
await s.refresh(row)
|
||
return _kb(row)
|
||
|
||
|
||
async def delete_kb_doc(tenant_id: str, did: str) -> bool:
|
||
async with _get_session() as s:
|
||
r = await s.execute(delete(ParkKbDoc).where(ParkKbDoc.id == did, ParkKbDoc.tenant_id == tenant_id))
|
||
await s.commit()
|
||
return r.rowcount > 0
|
||
|
||
|
||
async def get_kb_doc(tenant_id: str, did: str) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkKbDoc).where(ParkKbDoc.id == did, ParkKbDoc.tenant_id == tenant_id))).scalars().first()
|
||
return _kb(row) if row else None
|
||
|
||
|
||
# ---------------- 屏幕 ----------------
|
||
|
||
async def list_screens(tenant_id: str) -> list[dict]:
|
||
async with _get_session() as s:
|
||
rows = (await s.execute(select(ParkScreen).where(ParkScreen.tenant_id == tenant_id).order_by(ParkScreen.created_at.desc()))).scalars().all()
|
||
return [_sc(r) for r in rows]
|
||
|
||
|
||
async def create_screen(tenant_id: str, payload: dict) -> dict:
|
||
row = ParkScreen(id=_new_id("SCR"), tenant_id=tenant_id, device_id=payload.get("device_id", ""),
|
||
name=payload.get("name", ""), role=payload.get("role", "main"),
|
||
location=payload.get("location", ""), created_at=_now())
|
||
async with _get_session() as s:
|
||
s.add(row)
|
||
await s.commit()
|
||
return _sc(row)
|
||
|
||
|
||
async def delete_screen(tenant_id: str, sid: str) -> bool:
|
||
async with _get_session() as s:
|
||
r = await s.execute(delete(ParkScreen).where(ParkScreen.id == sid, ParkScreen.tenant_id == tenant_id))
|
||
await s.commit()
|
||
return r.rowcount > 0
|
||
|
||
|
||
# ---------------- 设备绑定(大屏 → 园区) ----------------
|
||
|
||
async def get_device_by_id(device_id: str) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
|
||
return _sc(row) if row else None
|
||
|
||
|
||
async def ensure_device(device_id: str) -> dict:
|
||
"""确保存在该设备记录(未绑定)。"""
|
||
existing = await get_device_by_id(device_id)
|
||
if existing:
|
||
return existing
|
||
async with _get_session() as s:
|
||
s.add(ParkScreen(id=_new_id("SCR"), device_id=device_id, status="unbound", tenant_id=None,
|
||
name="", role="main", location="", created_at=_now()))
|
||
await s.commit()
|
||
return await get_device_by_id(device_id)
|
||
|
||
|
||
async def set_device_code(device_id: str, code: str, expires: str) -> dict:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
|
||
if row is None:
|
||
return None
|
||
row.code = code
|
||
row.code_expires = expires
|
||
row.status = "unbound"
|
||
row.tenant_id = None
|
||
await s.commit()
|
||
await s.refresh(row)
|
||
return _sc(row)
|
||
|
||
|
||
async def find_device_by_code(code: str) -> dict | None:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkScreen).where(ParkScreen.code == code, ParkScreen.status == "unbound").limit(1))).scalars().first()
|
||
if row is None:
|
||
return None
|
||
# 连接码轮换/过期校验:过期码不可再绑定(保证当前展示的最新码才有效)
|
||
if row.code_expires:
|
||
try:
|
||
if datetime.fromisoformat(row.code_expires) < datetime.now():
|
||
return None
|
||
except ValueError:
|
||
return None
|
||
return _sc(row)
|
||
|
||
|
||
async def bind_device(tenant_id: str, device_id: str, name: str = "", role: str = "main", location: str = "") -> dict | None:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
|
||
if row is None:
|
||
return None
|
||
row.tenant_id = tenant_id
|
||
row.status = "bound"
|
||
row.name = name or row.name or "未命名屏"
|
||
row.role = role
|
||
row.location = location
|
||
row.bound_at = _now()
|
||
row.code = ""
|
||
row.code_expires = ""
|
||
await s.commit()
|
||
await s.refresh(row)
|
||
return _sc(row)
|
||
|
||
|
||
async def unbind_device(device_id: str) -> bool:
|
||
async with _get_session() as s:
|
||
row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
|
||
if row is None:
|
||
return False
|
||
row.tenant_id = None
|
||
row.status = "unbound"
|
||
row.code = ""
|
||
row.bound_at = ""
|
||
await s.commit()
|
||
return True
|
||
|
||
|
||
# ---------------- 大屏数据 ----------------
|
||
|
||
async def screen_view(tenant_id: str) -> dict:
|
||
t = await get_tenant(tenant_id) or {}
|
||
d = t.get("data", {})
|
||
park = d.get("park", {}) or {}
|
||
return {"tenant_id": tenant_id, "name": t.get("name", ""), "intro": t.get("intro", []),
|
||
"founded": park.get("founded"), "province_level": park.get("province_level") or park.get("provinceLevel"), "area": park.get("area"),
|
||
"region": park.get("region", ""), "address": park.get("address"), "phone": park.get("phone"), "email": park.get("email"),
|
||
"capacity": d.get("projects", {}).get("capacity"), "invested": d.get("projects", {}).get("invested"),
|
||
"jobs": d.get("jobs", {}).get("total"), "revenue_total": d.get("revenue", {}).get("total"), "revenue_tax": d.get("revenue", {}).get("tax"),
|
||
"zones": d.get("zones", []), "industryMix": d.get("industryMix", []), "feed": d.get("feed", []), "as_of": d.get("as_of", "")}
|
||
|
||
|
||
async def update_screen(tenant_id: str, payload: dict) -> dict:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkTenant, tenant_id)
|
||
d = dict(json.loads(row.data_json or "{}"))
|
||
park = dict(d.get("park", {}))
|
||
for k in ("founded", "area", "address", "phone", "email", "region"):
|
||
if payload.get(k) is not None:
|
||
park[k] = payload[k]
|
||
d["park"] = park
|
||
proj = dict(d.get("projects", {}))
|
||
if payload.get("capacity") is not None: proj["capacity"] = payload["capacity"]
|
||
if payload.get("invested") is not None: proj["invested"] = payload["invested"]
|
||
d["projects"] = proj
|
||
jobs = dict(d.get("jobs", {}))
|
||
if payload.get("jobs") is not None: jobs["total"] = payload["jobs"]
|
||
d["jobs"] = jobs
|
||
rev = dict(d.get("revenue", {}))
|
||
if payload.get("revenue_total") is not None: rev["total"] = payload["revenue_total"]
|
||
if payload.get("revenue_tax") is not None: rev["tax"] = payload["revenue_tax"]
|
||
d["revenue"] = rev
|
||
for k in ("feed", "zones", "industryMix"):
|
||
if payload.get(k) is not None: d[k] = payload[k]
|
||
row.name = payload.get("name") or row.name
|
||
row.intro_json = json.dumps(payload.get("intro") or json.loads(row.intro_json or "[]"), ensure_ascii=False)
|
||
row.data_json = json.dumps(d, ensure_ascii=False)
|
||
await s.commit()
|
||
await s.refresh(row)
|
||
refresh_engine(tenant_id, d)
|
||
return await screen_view(tenant_id)
|
||
|
||
|
||
# ---------------- 全局配置(park_settings kv) ----------------
|
||
|
||
async def get_setting(key: str, default: str = "") -> str:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkSetting, key)
|
||
return row.value if row else default
|
||
|
||
|
||
async def set_setting(key: str, value: str) -> None:
|
||
async with _get_session() as s:
|
||
row = await s.get(ParkSetting, key)
|
||
if row is None:
|
||
s.add(ParkSetting(key=key, value=value, updated_at=_now()))
|
||
else:
|
||
row.value = value
|
||
row.updated_at = _now()
|
||
await s.commit()
|
||
|
||
|
||
# ---------------- 视频识别(per-tenant,园区端控制,存 park_settings 命名键) ----------------
|
||
|
||
async def get_tenant_vision(tenant_id: str) -> str:
|
||
"""本园区的视频识别服务地址(空=禁用)。"""
|
||
return await get_setting(f"vision_api:{tenant_id}", "")
|
||
|
||
|
||
async def set_tenant_vision(tenant_id: str, api: str) -> None:
|
||
await set_setting(f"vision_api:{tenant_id}", (api or "").strip().rstrip("/"))
|