feat(园区端): 园区不再自建账号 — 改为绑定已有平台账号为管理员

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登录园区端成功。
This commit is contained in:
Pine
2026-08-24 18:46:41 +08:00
parent 83e542c2c2
commit 6458d5eb98
2 changed files with 104 additions and 10 deletions
+37 -4
View File
@@ -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):
+67 -6
View File
@@ -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: