新增小程序后端代码,包括数据库、路由、服务层等。
小程序前后端打通,包括登录、注册、绑定设备、查询绑定信息,修改小朋友名称等功能。
This commit is contained in:
BIN
mini-program/app/dao/.DS_Store
vendored
Normal file
BIN
mini-program/app/dao/.DS_Store
vendored
Normal file
Binary file not shown.
13
mini-program/app/dao/__init__.py
Normal file
13
mini-program/app/dao/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class BaseDAO:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def execute(self, query, params: dict = None):
|
||||
return self.db.execute(text(query), params or {})
|
||||
|
||||
def commit(self):
|
||||
self.db.commit()
|
||||
226
mini-program/app/dao/binding.py
Normal file
226
mini-program/app/dao/binding.py
Normal file
@@ -0,0 +1,226 @@
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.dao import BaseDAO
|
||||
|
||||
logger = logging.getLogger("app.dao.binding")
|
||||
|
||||
|
||||
class BindingDAO(BaseDAO):
|
||||
def start_bind(self, user_id: int, device_id: str, child_id: int) -> str:
|
||||
bind_token = str(uuid.uuid4())
|
||||
expires_at = datetime.utcnow() + timedelta(minutes=10)
|
||||
|
||||
self.db.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO device_bind_sessions (bind_token, device_id, initiator_user_id, target_child_id, expires_at, status)
|
||||
VALUES (:bind_token, :device_id, :initiator_user_id, :target_child_id, :expires_at, 1)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"bind_token": bind_token,
|
||||
"device_id": device_id,
|
||||
"initiator_user_id": user_id,
|
||||
"target_child_id": child_id,
|
||||
"expires_at": expires_at,
|
||||
},
|
||||
)
|
||||
self.commit()
|
||||
return bind_token
|
||||
|
||||
def get_session(self, bind_token: str, user_id: int) -> Optional[Mapping]:
|
||||
return (
|
||||
self.db.execute(
|
||||
text(
|
||||
"SELECT * FROM device_bind_sessions WHERE bind_token = :bind_token AND initiator_user_id = :user_id"
|
||||
),
|
||||
{"bind_token": bind_token, "user_id": user_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
def confirm_bind(self, session_id: int, device_id: str, child_id: int, user_id: int) -> None:
|
||||
self.db.execute(
|
||||
text("UPDATE device_bind_sessions SET status = 2, confirmed_at = CURRENT_TIMESTAMP WHERE id = :id"),
|
||||
{"id": session_id},
|
||||
)
|
||||
|
||||
existing = self.db.execute(
|
||||
text("SELECT id FROM device_bindings WHERE device_id = :device_id OR child_id = :child_id"),
|
||||
{"device_id": device_id, "child_id": child_id},
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
self.db.execute(
|
||||
text(
|
||||
"UPDATE device_bindings SET child_id = :child_id, status = 1, unbound_at = NULL WHERE device_id = :device_id"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id},
|
||||
)
|
||||
else:
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bindings (device_id, child_id, status, bound_at) VALUES (:device_id, :child_id, 1, CURRENT_TIMESTAMP)"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id},
|
||||
)
|
||||
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bind_history (device_id, child_id, bound_by_user_id, bind_source, bound_at) VALUES (:device_id, :child_id, :user_id, 1, CURRENT_TIMESTAMP)"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id, "user_id": user_id},
|
||||
)
|
||||
self.commit()
|
||||
|
||||
def direct_bind(self, device_id: str, child_id: int, user_id: int) -> None:
|
||||
# Check if binding exists for this device_id
|
||||
existing_by_device = (
|
||||
self.db.execute(
|
||||
text("SELECT id FROM device_bindings WHERE device_id = :device_id"),
|
||||
{"device_id": device_id},
|
||||
).fetchone()
|
||||
)
|
||||
|
||||
# Check if this child is already bound to a different device
|
||||
existing_by_child = (
|
||||
self.db.execute(
|
||||
text("SELECT id FROM device_bindings WHERE child_id = :child_id AND status = 1"),
|
||||
{"child_id": child_id},
|
||||
).fetchone()
|
||||
)
|
||||
|
||||
if existing_by_device:
|
||||
# Update existing binding for this device
|
||||
self.db.execute(
|
||||
text(
|
||||
"UPDATE device_bindings SET child_id = :child_id, status = 1, unbound_at = NULL WHERE device_id = :device_id"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id},
|
||||
)
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bind_history (device_id, child_id, bound_by_user_id, bind_source, bound_at) VALUES (:device_id, :child_id, :user_id, 2, CURRENT_TIMESTAMP)"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id, "user_id": user_id},
|
||||
)
|
||||
self.commit()
|
||||
return
|
||||
elif existing_by_child:
|
||||
# Child already bound to another device - update that binding
|
||||
self.db.execute(
|
||||
text(
|
||||
"UPDATE device_bindings SET device_id = :device_id, status = 1, unbound_at = NULL WHERE child_id = :child_id AND status = 1"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id},
|
||||
)
|
||||
# Skip INSERT since we updated
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bind_history (device_id, child_id, bound_by_user_id, bind_source, bound_at) VALUES (:device_id, :child_id, :user_id, 2, CURRENT_TIMESTAMP)"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id, "user_id": user_id},
|
||||
)
|
||||
self.commit()
|
||||
return
|
||||
else:
|
||||
# Insert new binding
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bindings (device_id, child_id, status, bound_at) VALUES (:device_id, :child_id, 1, CURRENT_TIMESTAMP)"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id},
|
||||
)
|
||||
|
||||
# Add history
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bind_history (device_id, child_id, bound_by_user_id, bind_source, bound_at) VALUES (:device_id, :child_id, :user_id, 2, CURRENT_TIMESTAMP)"
|
||||
),
|
||||
{"device_id": device_id, "child_id": child_id, "user_id": user_id},
|
||||
)
|
||||
self.commit()
|
||||
|
||||
def get_current_by_user(self, user_id: int) -> Optional[Mapping]:
|
||||
return (
|
||||
self.db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT b.* FROM device_bindings b
|
||||
JOIN children c ON b.child_id = c.child_id
|
||||
WHERE c.parent_user_id = :user_id AND b.status = 1
|
||||
ORDER BY b.bound_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_by_device(self, device_id: str, user_id: int) -> Optional[Mapping]:
|
||||
return (
|
||||
self.db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT b.* FROM device_bindings b
|
||||
JOIN children c ON b.child_id = c.child_id
|
||||
WHERE b.device_id = :device_id AND c.parent_user_id = :user_id AND b.status = 1
|
||||
"""
|
||||
),
|
||||
{"device_id": device_id, "user_id": user_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
def unbind(self, device_id: str, user_id: int) -> bool:
|
||||
row = self.get_by_device(device_id, user_id)
|
||||
if not row:
|
||||
return False
|
||||
|
||||
self.db.execute(
|
||||
text("UPDATE device_bindings SET status = 0, unbound_at = CURRENT_TIMESTAMP WHERE id = :id"),
|
||||
{"id": row["id"]},
|
||||
)
|
||||
|
||||
self.db.execute(
|
||||
text(
|
||||
"INSERT INTO device_bind_history (device_id, child_id, bound_by_user_id, unbound_by_user_id, bind_source, bound_at, unbound_at, unbind_reason) SELECT device_id, child_id, bound_by_user_id, :user_id, bind_source, bound_at, CURRENT_TIMESTAMP, 'user_unbind' FROM device_bind_history WHERE device_id = :device_id AND unbound_at IS NULL"
|
||||
),
|
||||
{"device_id": device_id, "user_id": user_id},
|
||||
)
|
||||
self.commit()
|
||||
return True
|
||||
|
||||
def list_history(self, device_id: str, limit: int = 20, cursor: datetime = None) -> list[Mapping]:
|
||||
params = {"device_id": device_id, "limit": limit + 1}
|
||||
where = "device_id = :device_id"
|
||||
if cursor:
|
||||
where += " AND bound_at < :cursor"
|
||||
params["cursor"] = cursor
|
||||
|
||||
rows = (
|
||||
self.db.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT * FROM device_bind_history
|
||||
WHERE {where}
|
||||
ORDER BY bound_at DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
return rows
|
||||
95
mini-program/app/dao/child.py
Normal file
95
mini-program/app/dao/child.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from collections.abc import Mapping
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.dao import BaseDAO
|
||||
|
||||
|
||||
class ChildDAO(BaseDAO):
|
||||
def create(
|
||||
self,
|
||||
user_id: int,
|
||||
child_name: str,
|
||||
child_gender: int = 2,
|
||||
child_birthday: Optional[date] = None,
|
||||
) -> int:
|
||||
result = self.db.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO children (parent_user_id, child_name, child_gender, child_birthday, status)
|
||||
VALUES (:user_id, :child_name, :child_gender, :child_birthday, 1)
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id, "child_name": child_name, "child_gender": child_gender, "child_birthday": child_birthday},
|
||||
)
|
||||
child_id = int(result.lastrowid)
|
||||
self.commit()
|
||||
return child_id
|
||||
|
||||
def get_by_id(self, child_id: int) -> Optional[Mapping]:
|
||||
return (
|
||||
self.db.execute(
|
||||
text("SELECT * FROM children WHERE child_id = :child_id"),
|
||||
{"child_id": child_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
def list_by_parent(self, user_id: int, limit: int = 20, cursor: int = None) -> list[Mapping]:
|
||||
params = {"user_id": user_id, "limit": limit + 1}
|
||||
where = "parent_user_id = :user_id AND status = 1"
|
||||
if cursor:
|
||||
where += " AND child_id < :cursor"
|
||||
params["cursor"] = cursor
|
||||
|
||||
rows = (
|
||||
self.db.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT * FROM children
|
||||
WHERE {where}
|
||||
ORDER BY child_id DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
),
|
||||
params,
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
return rows
|
||||
|
||||
def update(
|
||||
self,
|
||||
child_id: int,
|
||||
child_name: Optional[str] = None,
|
||||
child_gender: Optional[int] = None,
|
||||
child_birthday: Optional[date] = None,
|
||||
) -> None:
|
||||
self.db.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE children
|
||||
SET child_name = COALESCE(:child_name, child_name),
|
||||
child_gender = COALESCE(:child_gender, child_gender),
|
||||
child_birthday = COALESCE(:child_birthday, child_birthday)
|
||||
WHERE child_id = :child_id
|
||||
"""
|
||||
),
|
||||
{"child_id": child_id, "child_name": child_name, "child_gender": child_gender, "child_birthday": child_birthday},
|
||||
)
|
||||
self.commit()
|
||||
|
||||
def has_access(self, child_id: int, user_id: int) -> bool:
|
||||
return (
|
||||
self.db.execute(
|
||||
text(
|
||||
"SELECT 1 FROM children WHERE child_id = :child_id AND parent_user_id = :user_id AND status = 1"
|
||||
),
|
||||
{"child_id": child_id, "user_id": user_id},
|
||||
).scalar_one_or_none()
|
||||
is not None
|
||||
)
|
||||
100
mini-program/app/dao/parent.py
Normal file
100
mini-program/app/dao/parent.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.dao import BaseDAO
|
||||
|
||||
|
||||
class ParentDAO(BaseDAO):
|
||||
def create(
|
||||
self,
|
||||
openid: str,
|
||||
unionid: Optional[str] = None,
|
||||
nickname: Optional[str] = None,
|
||||
avatar_url: Optional[str] = None,
|
||||
) -> int:
|
||||
result = self.db.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO parents (openid, unionid, nickname, avatar_url, status)
|
||||
VALUES (:openid, :unionid, :nickname, :avatar_url, 1)
|
||||
"""
|
||||
),
|
||||
{"openid": openid, "unionid": unionid, "nickname": nickname, "avatar_url": avatar_url},
|
||||
)
|
||||
self.commit()
|
||||
return int(self.db.execute(text("SELECT last_insert_rowid()")).scalar_one())
|
||||
|
||||
def get_by_id(self, user_id: int) -> Optional[Mapping]:
|
||||
return (
|
||||
self.db.execute(
|
||||
text("SELECT * FROM parents WHERE user_id = :user_id"),
|
||||
{"user_id": user_id},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_by_openid(self, openid: str) -> Optional[Mapping]:
|
||||
return (
|
||||
self.db.execute(
|
||||
text("SELECT * FROM parents WHERE openid = :openid"),
|
||||
{"openid": openid},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
|
||||
def update(
|
||||
self,
|
||||
user_id: int,
|
||||
nickname: Optional[str] = None,
|
||||
avatar_url: Optional[str] = None,
|
||||
phone: Optional[str] = None,
|
||||
) -> None:
|
||||
self.db.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE parents
|
||||
SET nickname = COALESCE(:nickname, nickname),
|
||||
avatar_url = COALESCE(:avatar_url, avatar_url),
|
||||
phone = COALESCE(:phone, phone)
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id, "nickname": nickname, "avatar_url": avatar_url, "phone": phone},
|
||||
)
|
||||
self.commit()
|
||||
|
||||
def upsert(
|
||||
self,
|
||||
openid: str,
|
||||
unionid: Optional[str] = None,
|
||||
nickname: Optional[str] = None,
|
||||
avatar_url: Optional[str] = None,
|
||||
) -> int:
|
||||
from app.settings import settings
|
||||
|
||||
existing = self.get_by_openid(openid)
|
||||
if existing:
|
||||
self.update(existing["user_id"], nickname, avatar_url)
|
||||
return existing["user_id"]
|
||||
|
||||
if settings.db_type == "sqlite":
|
||||
return self.create(openid, unionid, nickname, avatar_url)
|
||||
|
||||
self.db.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO parents (openid, unionid, nickname, avatar_url, status)
|
||||
VALUES (:openid, :unionid, :nickname, :avatar_url, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
user_id = LAST_INSERT_ID(user_id),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""
|
||||
),
|
||||
{"openid": openid, "unionid": unionid, "nickname": nickname, "avatar_url": avatar_url},
|
||||
)
|
||||
self.commit()
|
||||
return int(self.db.execute(text("SELECT LAST_INSERT_ID()")).scalar_one())
|
||||
Reference in New Issue
Block a user