完善登录绑定定位与管理页交互
This commit is contained in:
@@ -34,6 +34,20 @@ class BindingDAO(BaseDAO):
|
||||
)
|
||||
).mappings().first()
|
||||
|
||||
async def get_active_binding_by_device(self, device_id: str) -> Optional[Mapping]:
|
||||
return (
|
||||
await self.execute(
|
||||
"""
|
||||
SELECT id, device_id, owner_user_id, child_id, status
|
||||
FROM device_bindings
|
||||
WHERE device_id = :device_id
|
||||
AND status = 1
|
||||
LIMIT 1
|
||||
""",
|
||||
{"device_id": device_id},
|
||||
)
|
||||
).mappings().first()
|
||||
|
||||
async def _clear_child_from_binding(self, binding_row: Mapping[str, object]) -> None:
|
||||
row_id = int(binding_row["id"])
|
||||
status = int(binding_row["status"])
|
||||
@@ -372,6 +386,8 @@ class BindingDAO(BaseDAO):
|
||||
row = await self.get_by_device(device_id=device_id, user_id=user_id)
|
||||
if not row:
|
||||
return False
|
||||
if row["child_id"] is not None:
|
||||
return False
|
||||
|
||||
await self._upsert_parent_child_relation(user_id=user_id, child_id=child_id)
|
||||
await self._bind_device(device_id=device_id, user_id=user_id, child_id=child_id)
|
||||
|
||||
@@ -90,19 +90,17 @@ class ChildDAO(BaseDAO):
|
||||
await self.commit()
|
||||
|
||||
async def has_access(self, child_id: int, user_id: int) -> bool:
|
||||
return (
|
||||
await self.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM children AS c
|
||||
JOIN parent_child_relations AS pcr
|
||||
ON pcr.child_id = c.child_id
|
||||
WHERE c.child_id = :child_id
|
||||
AND pcr.user_id = :user_id
|
||||
AND c.status = 1
|
||||
AND pcr.status = 1
|
||||
""",
|
||||
{"child_id": child_id, "user_id": user_id},
|
||||
).scalar_one_or_none()
|
||||
is not None
|
||||
result = await self.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM children AS c
|
||||
JOIN parent_child_relations AS pcr
|
||||
ON pcr.child_id = c.child_id
|
||||
WHERE c.child_id = :child_id
|
||||
AND pcr.user_id = :user_id
|
||||
AND c.status = 1
|
||||
AND pcr.status = 1
|
||||
""",
|
||||
{"child_id": child_id, "user_id": user_id},
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@@ -18,6 +18,21 @@ class ParentDeviceAccess:
|
||||
|
||||
|
||||
class LocationDAO(BaseDAO):
|
||||
async def get_active_binding_by_device(self, *, device_id: str) -> Mapping[str, Any] | None:
|
||||
result = await self.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT child_id
|
||||
FROM device_bindings
|
||||
WHERE device_id = :device_id
|
||||
AND status = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"device_id": device_id},
|
||||
)
|
||||
return result.mappings().first()
|
||||
|
||||
async def assert_parent_device_access(self, *, device_id: str, user_id: int) -> ParentDeviceAccess:
|
||||
from fastapi import HTTPException, status
|
||||
result = await self.execute(
|
||||
@@ -321,8 +336,7 @@ class LocationDAO(BaseDAO):
|
||||
await self.execute(text(update_current_sql), params)
|
||||
await self.commit()
|
||||
|
||||
async def get_device_current_location(self, *, device_id: str) -> Mapping[str, Any]:
|
||||
# sql 查询
|
||||
async def get_current_location_by_device_id(self, *, device_id: str) -> Mapping[str, Any]:
|
||||
result = await self.execute(
|
||||
text(
|
||||
f"""
|
||||
@@ -348,4 +362,4 @@ class LocationDAO(BaseDAO):
|
||||
),
|
||||
{"device_id": device_id},
|
||||
)
|
||||
return result.mappings().first()
|
||||
return result.mappings().first()
|
||||
|
||||
@@ -32,6 +32,14 @@ class LoginResponse(BaseModel):
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
user_id: int
|
||||
nickname: str | None = None
|
||||
|
||||
|
||||
def _normalize_nickname(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip()
|
||||
return normalized or None
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
@@ -56,20 +64,26 @@ async def login(
|
||||
)
|
||||
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
||||
|
||||
normalized_nickname = _normalize_nickname(payload.nickname)
|
||||
parent_service = ParentService()
|
||||
parent = await parent_service.create(
|
||||
openid=wechat_session.openid,
|
||||
unionid=wechat_session.unionid,
|
||||
nickname=payload.nickname,
|
||||
nickname=normalized_nickname,
|
||||
avatar_url=payload.avatar_url,
|
||||
)
|
||||
user_id = int(parent["user_id"])
|
||||
|
||||
access_token, expires_in = create_access_token(user_id=user_id)
|
||||
logger.info("wechat login succeeded", extra={"event": "wechat_login_succeeded", "user_id": user_id})
|
||||
return LoginResponse(access_token=access_token, expires_in=expires_in, user_id=user_id)
|
||||
return LoginResponse(
|
||||
access_token=access_token,
|
||||
expires_in=expires_in,
|
||||
user_id=user_id,
|
||||
nickname=parent.get("nickname"),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
return {"message": "logged out"}
|
||||
return {"message": "logged out"}
|
||||
|
||||
@@ -24,6 +24,13 @@ class BindingService(DatabaseServiceBase):
|
||||
def __init__(self):
|
||||
super().__init__(service_name="binding_service")
|
||||
|
||||
async def _ensure_device_unbound(self, db_session, device_id: str) -> None:
|
||||
dao = BindingDAO(db_session)
|
||||
active_binding = await dao.get_active_binding_by_device(device_id)
|
||||
if active_binding is None:
|
||||
return
|
||||
raise BindingError("device is already bound, unbind it before binding again", status_code=409)
|
||||
|
||||
async def _ensure_bindable_device(self, db_session, device_id: str, serial_number: str) -> None:
|
||||
dao = BindingDAO(db_session)
|
||||
row = await dao.get_device_auth(device_id)
|
||||
@@ -50,6 +57,7 @@ class BindingService(DatabaseServiceBase):
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
await self._ensure_bindable_device(db_session, device_id, serial_number)
|
||||
await self._ensure_device_unbound(db_session, device_id)
|
||||
dao = BindingDAO(db_session)
|
||||
bind_token, expires_at = await dao.start_bind(user_id, device_id, child_id)
|
||||
await db_session.commit()
|
||||
@@ -188,6 +196,7 @@ class BindingService(DatabaseServiceBase):
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
await self._ensure_bindable_device(db_session, device_id, serial_number)
|
||||
await self._ensure_device_unbound(db_session, device_id)
|
||||
dao = BindingDAO(db_session)
|
||||
await dao.direct_bind(device_id, child_id, user_id)
|
||||
await db_session.commit()
|
||||
@@ -201,6 +210,9 @@ class BindingService(DatabaseServiceBase):
|
||||
dao = BindingDAO(db_session)
|
||||
ok = await dao.set_binding_child(device_id=device_id, child_id=child_id, user_id=user_id)
|
||||
if not ok:
|
||||
active_binding = await dao.get_active_binding_by_device(device_id)
|
||||
if active_binding and active_binding["child_id"] is not None:
|
||||
raise BindingError("device is already bound, unbind it before binding again", status_code=409)
|
||||
raise ValueError("binding not found")
|
||||
await db_session.commit()
|
||||
return {"device_id": device_id, "child_id": child_id}
|
||||
|
||||
@@ -89,7 +89,7 @@ class LocationService(DatabaseServiceBase):
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = LocationDAO(db_session)
|
||||
current_row = await dao.get_device_current_location(device_id=device_id)
|
||||
current_row = await dao.get_current_location_by_device_id(device_id=device_id)
|
||||
if current_row:
|
||||
await dao.update(device_id=device_id, latitude=location.lat, longitude=location.lng)
|
||||
# 更新成功加到历史记录表
|
||||
@@ -98,5 +98,63 @@ class LocationService(DatabaseServiceBase):
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def report_mqtt_device_location(
|
||||
self,
|
||||
*,
|
||||
device_id: str,
|
||||
latitude: float | None,
|
||||
longitude: float | None,
|
||||
coord_type: str | None = None,
|
||||
accuracy_m: int | None = None,
|
||||
altitude_m: float | None = None,
|
||||
speed_mps: float | None = None,
|
||||
heading_deg: int | None = None,
|
||||
source: int | None = None,
|
||||
battery_pct: int | None = None,
|
||||
device_time: datetime | None = None,
|
||||
) -> Mapping[str, Any] | None:
|
||||
if latitude is None or longitude is None:
|
||||
return None
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _MQTTLocationPayload:
|
||||
coord_type: str
|
||||
lat: float
|
||||
lng: float
|
||||
accuracy_m: int | None
|
||||
altitude_m: float | None
|
||||
speed_mps: float | None
|
||||
heading_deg: int | None
|
||||
source: int
|
||||
battery_pct: int | None
|
||||
device_time: datetime
|
||||
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = LocationDAO(db_session)
|
||||
binding_row = await dao.get_active_binding_by_device(device_id=device_id)
|
||||
if not binding_row or binding_row["child_id"] is None:
|
||||
return None
|
||||
|
||||
payload = _MQTTLocationPayload(
|
||||
coord_type=(coord_type or "gcj02").strip() or "gcj02",
|
||||
lat=float(latitude),
|
||||
lng=float(longitude),
|
||||
accuracy_m=accuracy_m,
|
||||
altitude_m=altitude_m,
|
||||
speed_mps=speed_mps,
|
||||
heading_deg=heading_deg,
|
||||
source=source if source is not None else 0,
|
||||
battery_pct=battery_pct,
|
||||
device_time=device_time or datetime.now(),
|
||||
)
|
||||
return await dao.report_device_location(
|
||||
device_id=device_id,
|
||||
child_id=int(binding_row["child_id"]),
|
||||
payload=payload,
|
||||
)
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
# 创建全局 LocationService 实例
|
||||
location_service = LocationService()
|
||||
|
||||
Reference in New Issue
Block a user