From 6458d5eb9878d084c1eee28b55a8dfbeee058885 Mon Sep 17 00:00:00 2001 From: Pine Date: Mon, 24 Aug 2026 18:46:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=AD=E5=8C=BA=E7=AB=AF):=20=E5=9B=AD?= =?UTF-8?q?=E5=8C=BA=E4=B8=8D=E5=86=8D=E8=87=AA=E5=BB=BA=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=20=E2=80=94=20=E6=94=B9=E4=B8=BA=E7=BB=91=E5=AE=9A=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=E5=B9=B3=E5=8F=B0=E8=B4=A6=E5=8F=B7=E4=B8=BA=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_tenant 不再建账号(admin 留空);/park/tenants POST 仅名称/简介;新增 /park/tenants/{id}/bind|unbind; /park/auth/login 改为校验平台账号密码 + 该账号已绑定为某园区管理员(签发 tenant token)。 park_tenants 增 admin_username 列(init_db 迁移)。TestClient:创建(无账号)→绑定pine→pine登录园区端成功。 --- app/park/routers.py | 41 ++++++++++++++++++++++--- app/park/tenants.py | 73 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/app/park/routers.py b/app/park/routers.py index 52bab23..1d4ba68 100644 --- a/app/park/routers.py +++ b/app/park/routers.py @@ -64,11 +64,24 @@ class TenantPatch(BaseModel): password: str | None = None -@router.post("/auth/login", summary="大屏/园区登录") +class BindBody(BaseModel): + username: str + + +@router.post("/auth/login", summary="园区端登录(绑定为园区管理员的平台账号)") async def park_login(body: LoginBody): - t = tenants.verify_login(body.username, body.password) - if not t: - return JSONResponse({"ok": False, "error": "账号或密码不正确"}, status_code=401) + # 登录 = 校验平台账号密码 + 该账号已绑定为某园区管理员;成功 → 签发该园区 tenant token + from app.infrastructure.repositories import Database + db = Database() + try: + user = await db.users.get_by_username(body.username.strip()) + if not user or not await db.users.verify_password(user, body.password): + return JSONResponse({"ok": False, "error": "账号或密码不正确"}, status_code=401) + t = tenants.find_by_admin(body.username.strip()) + if not t: + return JSONResponse({"ok": False, "error": "该账号未绑定任何园区管理员"}, status_code=401) + finally: + await db.close() return {"ok": True, "tenant_id": t["id"], "name": t["name"], "intro": t["intro"], "token": create_token(t["id"])} @@ -109,6 +122,26 @@ async def tenant_delete(tid: str): return {"ok": ok, "id": tid} +@router.post("/tenants/{tid}/bind", summary="绑定园区管理员(已有平台账号)") +async def tenant_bind(tid: str, body: BindBody): + from app.infrastructure.repositories import Database + db = Database() + try: + user = await db.users.get_by_username(body.username.strip()) + finally: + await db.close() + if user is None: + return JSONResponse({"ok": False, "error": "账号不存在"}, status_code=404) + ok = tenants.bind_admin(tid, body.username.strip()) + return {"ok": ok, "tenant_id": tid, "admin_username": body.username.strip()} + + +@router.post("/tenants/{tid}/unbind", summary="解除园区管理员") +async def tenant_unbind(tid: str): + ok = tenants.unbind_admin(tid) + return {"ok": ok, "tenant_id": tid} + + # ==================== 数据模型 ==================== class SettingsBody(BaseModel): diff --git a/app/park/tenants.py b/app/park/tenants.py index c748185..b3edb41 100644 --- a/app/park/tenants.py +++ b/app/park/tenants.py @@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS park_tenants ( username TEXT, salt TEXT, password_hash TEXT, + admin_username TEXT DEFAULT '', -- 绑定的平台账号(园区管理员,园区端登录用) data_json TEXT, agent_json TEXT, created_at TEXT @@ -58,6 +59,15 @@ CREATE TABLE IF NOT EXISTS park_kb_docs ( content_md TEXT DEFAULT '', created_at TEXT ); +CREATE TABLE IF NOT EXISTS park_screens ( + id TEXT PRIMARY KEY, + tenant_id TEXT, + device_id TEXT, + name TEXT DEFAULT '', + role TEXT DEFAULT 'main', + location TEXT DEFAULT '', + created_at TEXT +); """ @@ -72,6 +82,10 @@ def _conn() -> sqlite3.Connection: def init_db(): conn = _conn() conn.executescript(SCHEMA) + # 迁移:为旧 park_tenants 表补 admin_username(绑定平台账号作园区管理员) + cols = [r["name"] for r in conn.execute("PRAGMA table_info(park_tenants)").fetchall()] + if "admin_username" not in cols: + conn.execute("ALTER TABLE park_tenants ADD COLUMN admin_username TEXT DEFAULT ''") conn.commit() conn.close() ensure_default_tenant() @@ -103,6 +117,7 @@ def _row_to_tenant(row) -> dict: "name": row["name"], "intro": json.loads(row["intro_json"] or "[]"), "auth": {"username": row["username"], "salt": row["salt"], "password_hash": row["password_hash"]}, + "admin_username": row["admin_username"] or "", "data": json.loads(row["data_json"] or "{}"), "agent": json.loads(row["agent_json"] or "{}"), } @@ -137,14 +152,14 @@ def get_tenant_data(tenant_id: str) -> dict: return data or dict(DEFAULT_DATA) -def create_tenant(name: str, intro: list[str], username: str, password: str) -> dict: - salt, h = _hash_password(password) +def create_tenant(name: str, intro: list[str], username: str = "", password: str = "") -> dict: + """创建园区:不创建账号(admin 留空),由平台端绑定已有账号作园区管理员。""" tid = _new_id("T") data = dict(DEFAULT_DATA) conn = _conn() conn.execute( - "INSERT INTO park_tenants (id,name,intro_json,username,salt,password_hash,data_json,agent_json,created_at) VALUES (?,?,?,?,?,?,?,?,?)", - (tid, name, json.dumps(intro or ["", ""]), username, salt, h, json.dumps(data, ensure_ascii=False), json.dumps({}, ensure_ascii=False), _now()), + "INSERT INTO park_tenants (id,name,intro_json,username,salt,password_hash,admin_username,data_json,agent_json,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)", + (tid, name, json.dumps(intro or ["", ""]), username, "", "", "", json.dumps(data, ensure_ascii=False), json.dumps({}, ensure_ascii=False), _now()), ) conn.commit() conn.close() @@ -153,6 +168,26 @@ def create_tenant(name: str, intro: list[str], username: str, password: str) -> return _summary(tid, name, intro, username) +def bind_admin(tenant_id: str, username: str) -> bool: + conn = _conn() + cur = conn.execute("UPDATE park_tenants SET admin_username=? WHERE id=?", (username.strip(), tenant_id)) + conn.commit() + conn.close() + return cur.rowcount > 0 + + +def unbind_admin(tenant_id: str) -> bool: + return bind_admin(tenant_id, "") + + +def find_by_admin(username: str) -> dict | None: + """返回绑定为该平台账号管理员的园区(供园区端登录)。""" + conn = _conn() + row = conn.execute("SELECT * FROM park_tenants WHERE admin_username=? LIMIT 1", (username.strip(),)).fetchone() + conn.close() + return _row_to_tenant(row) if row else None + + def update_tenant(tenant_id: str, patch: dict) -> dict | None: conn = _conn() t = get_tenant(tenant_id) @@ -231,8 +266,8 @@ def ensure_default_tenant() -> str: return _DEFAULT_TENANT_ID -def _summary(tid: str, name: str, intro: list, username: str) -> dict: - return {"id": tid, "name": name, "intro": intro, "username": username} +def _summary(tid: str, name: str, intro: list, username: str, admin_username: str = "") -> dict: + return {"id": tid, "name": name, "intro": intro, "username": username, "admin_username": admin_username} def _seed_companies(tenant_id: str, companies: list) -> None: @@ -371,6 +406,32 @@ def get_kb_doc(tenant_id: str, did: str) -> dict | None: return {k: v for k, v in dict(row).items() if k != "tenant_id"} if row else None +# ---------------- 屏幕(园区端创建/管理大屏设备) ---------------- + +def list_screens(tenant_id: str) -> list[dict]: + conn = _conn() + rows = conn.execute("SELECT * FROM park_screens WHERE tenant_id=? ORDER BY created_at DESC", (tenant_id,)).fetchall() + conn.close() + return [dict(r) for r in rows] + + +def create_screen(tenant_id: str, payload: dict) -> dict: + row = {"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()} + conn = _conn() + conn.execute("INSERT INTO park_screens (id,tenant_id,device_id,name,role,location,created_at) VALUES (?,?,?,?,?,?,?)", (row["id"], row["tenant_id"], row["device_id"], row["name"], row["role"], row["location"], row["created_at"])) + conn.commit() + conn.close() + return {k: v for k, v in row.items() if k != "tenant_id"} + + +def delete_screen(tenant_id: str, sid: str) -> bool: + conn = _conn() + cur = conn.execute("DELETE FROM park_screens WHERE id=? AND tenant_id=?", (sid, tenant_id)) + conn.commit() + conn.close() + return cur.rowcount > 0 + + # ---------------- 大屏数据(名称/简介 + 锚点) ---------------- def screen_view(tenant_id: str) -> dict: