feat(park): 平台园区/屏幕绑定 + OPC认证入园转园审核 + 园区端carrier修复
- rbac_admin 新增 /admin/park/screens|tenants|devices/{id}/bind|unbind 及 OPC认证/入园/转园审核端点
- tenants.list_all_screens 带 online(MQTT心跳),bind/unbind 完备
- park/routers: tenant_bind 绑定即 set_role(carrier);_my_park 兜底 username→admin_username 解析园区(修"未关联任何园区")
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -317,3 +317,142 @@ async def review_park_transfer(
|
||||
await write_audit(db, action="opc.park_transfer_review", resource="park_transfer",
|
||||
resource_id=tid, detail=f"status={req.status}", user=actor, request=request)
|
||||
return {"ok": True, "transfer": updated}
|
||||
|
||||
|
||||
# ==================== 平台园区管理(operator;复用 park 数据层,不依赖 app.state.db) ====================
|
||||
|
||||
class AdminBindDeviceBody(BaseModel):
|
||||
tenant_id: str
|
||||
name: str = ""
|
||||
role: str = "main"
|
||||
location: str = ""
|
||||
|
||||
|
||||
class AdminCreateTenantBody(BaseModel):
|
||||
name: str
|
||||
intro: list[str] = ["", ""]
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
|
||||
|
||||
class AdminTenantStatusBody(BaseModel):
|
||||
status: str # active | disabled
|
||||
|
||||
|
||||
def _park_tenant_public(t: dict) -> dict:
|
||||
"""脱敏园区:剔除 auth(username/salt/password_hash),仅留管理用字段。"""
|
||||
return {k: t.get(k) for k in ("id", "name", "intro", "admin_username", "status")}
|
||||
|
||||
|
||||
@router.get("/park/screens", summary="全部屏幕(含未绑定+归属园区名,平台)")
|
||||
async def admin_park_screens(
|
||||
db: Database = Depends(get_db),
|
||||
_role: dict = Depends(require_roles("operator")),
|
||||
_perm: dict = Depends(require_permission("menu:admin_user_mgmt")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
return {"items": await tnt.list_all_screens()}
|
||||
|
||||
|
||||
@router.get("/park/tenants", summary="园区列表(脱敏,平台)")
|
||||
async def admin_park_tenants(
|
||||
db: Database = Depends(get_db),
|
||||
_role: dict = Depends(require_roles("operator")),
|
||||
_perm: dict = Depends(require_permission("menu:admin_user_mgmt")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
rows = await tnt.list_tenants()
|
||||
return {"items": [_park_tenant_public(t) for t in rows]}
|
||||
|
||||
|
||||
@router.post("/park/tenants", summary="平台创建园区")
|
||||
async def admin_park_create_tenant(
|
||||
body: AdminCreateTenantBody,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:user.manage")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
t = await tnt.create_tenant(body.name, body.intro, body.username, body.password)
|
||||
await write_audit(db, action="park.create", resource="park_tenant",
|
||||
resource_id=(t or {}).get("id", ""), detail=body.name, user=actor, request=request)
|
||||
return {"ok": True, "tenant": _park_tenant_public(t or {})}
|
||||
|
||||
|
||||
@router.delete("/park/tenants/{tid}", summary="平台删除园区(级联删屏)")
|
||||
async def admin_park_delete_tenant(
|
||||
tid: str,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:user.manage")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
ok = await tnt.delete_tenant(tid)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="园区不存在")
|
||||
await write_audit(db, action="park.delete", resource="park_tenant",
|
||||
resource_id=tid, detail=tid, user=actor, request=request)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/park/tenants/{tid}/status", summary="平台停用/启用园区")
|
||||
async def admin_park_tenant_status(
|
||||
tid: str,
|
||||
body: AdminTenantStatusBody,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:user.manage")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
if body.status not in ("active", "disabled"):
|
||||
raise HTTPException(status_code=400, detail="无效状态")
|
||||
ok = await tnt.set_status(tid, body.status)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="园区不存在")
|
||||
await write_audit(db, action="park.status", resource="park_tenant",
|
||||
resource_id=tid, detail=body.status, user=actor, request=request)
|
||||
return {"ok": True, "status": body.status}
|
||||
|
||||
|
||||
@router.post("/park/devices/{device_id}/bind", summary="平台手动绑定屏幕到园区")
|
||||
async def admin_park_bind_device(
|
||||
device_id: str,
|
||||
body: AdminBindDeviceBody,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:user.manage")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
tenant = await tnt.get_tenant(body.tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=404, detail="目标园区不存在")
|
||||
dev = await tnt.get_device_by_id(device_id)
|
||||
if dev is None:
|
||||
raise HTTPException(status_code=404, detail="屏幕不存在")
|
||||
if dev.get("status") == "bound" and dev.get("tenant_id") and dev.get("tenant_id") != body.tenant_id:
|
||||
raise HTTPException(status_code=400, detail="该屏幕已绑定到其它园区,请先解绑")
|
||||
updated = await tnt.bind_device(body.tenant_id, device_id, body.name, body.role, body.location)
|
||||
await write_audit(db, action="park.bind_screen", resource="park_screen",
|
||||
resource_id=device_id, detail=f"tid={body.tenant_id}", user=actor, request=request)
|
||||
return {"ok": True, "screen": updated}
|
||||
|
||||
|
||||
@router.post("/park/devices/{device_id}/unbind", summary="平台解绑屏幕(从园区解绑,回未绑定)")
|
||||
async def admin_park_unbind_device(
|
||||
device_id: str,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:user.manage")),
|
||||
):
|
||||
from app.park import tenants as tnt
|
||||
dev = await tnt.get_device_by_id(device_id)
|
||||
if dev is None:
|
||||
raise HTTPException(status_code=404, detail="屏幕不存在")
|
||||
if dev.get("status") != "bound":
|
||||
raise HTTPException(status_code=400, detail="该屏幕当前未绑定,无需解绑")
|
||||
ok = await tnt.unbind_device(device_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=500, detail="解绑失败")
|
||||
await write_audit(db, action="park.unbind_screen", resource="park_screen",
|
||||
resource_id=device_id, detail=f"from tid={dev.get('tenant_id')}", user=actor, request=request)
|
||||
return {"ok": True}
|
||||
|
||||
Reference in New Issue
Block a user