新增小程序后端代码,包括数据库、路由、服务层等。
小程序前后端打通,包括登录、注册、绑定设备、查询绑定信息,修改小朋友名称等功能。
This commit is contained in:
BIN
mini-program/app/routers/.DS_Store
vendored
Normal file
BIN
mini-program/app/routers/.DS_Store
vendored
Normal file
Binary file not shown.
164
mini-program/app/routers/bindings.py
Normal file
164
mini-program/app/routers/bindings.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from app.security import get_current_user_id
|
||||
from app.service.binding import BindingService
|
||||
from app.service import get_db_session
|
||||
except ModuleNotFoundError:
|
||||
from security import get_current_user_id
|
||||
from service.binding import BindingService
|
||||
from service import get_db_session
|
||||
|
||||
|
||||
router = APIRouter(prefix="/bindings", tags=["bindings"])
|
||||
logger = logging.getLogger("app.bindings")
|
||||
|
||||
|
||||
class BindStartRequest(BaseModel):
|
||||
device_id: str
|
||||
child_id: int
|
||||
|
||||
|
||||
class BindStartResponse(BaseModel):
|
||||
bind_token: str
|
||||
expires_at: str
|
||||
|
||||
|
||||
class BindConfirmRequest(BaseModel):
|
||||
bind_token: str
|
||||
challenge_code: str
|
||||
|
||||
|
||||
class BindConfirmResponse(BaseModel):
|
||||
device_id: str
|
||||
child_id: int
|
||||
|
||||
|
||||
class BindingGetResponse(BaseModel):
|
||||
device_id: str
|
||||
child_id: int
|
||||
status: int
|
||||
bound_at: str
|
||||
|
||||
|
||||
class BindHistoryItem(BaseModel):
|
||||
device_id: str
|
||||
child_id: int
|
||||
bound_at: str
|
||||
unbound_at: str | None
|
||||
|
||||
|
||||
class BindHistoryResponse(BaseModel):
|
||||
items: list[BindHistoryItem]
|
||||
total: int
|
||||
next_cursor: str | None
|
||||
|
||||
|
||||
@router.post("/start", response_model=BindStartResponse)
|
||||
def start_bind(
|
||||
payload: BindStartRequest,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> BindStartResponse:
|
||||
service = BindingService(db)
|
||||
bind_token, expires_at = service.start_bind(current_user_id, payload.device_id, payload.child_id)
|
||||
return BindStartResponse(bind_token=bind_token, expires_at=expires_at.isoformat())
|
||||
|
||||
|
||||
@router.post("/confirm", response_model=BindConfirmResponse)
|
||||
def confirm_bind(
|
||||
payload: BindConfirmRequest,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> BindConfirmResponse:
|
||||
service = BindingService(db)
|
||||
try:
|
||||
result = service.confirm_bind(payload.bind_token, current_user_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return BindConfirmResponse(**result)
|
||||
|
||||
|
||||
class DirectBindRequest(BaseModel):
|
||||
device_id: str
|
||||
child_id: int
|
||||
|
||||
|
||||
class DirectBindResponse(BaseModel):
|
||||
device_id: str
|
||||
child_id: int
|
||||
|
||||
|
||||
@router.post("/direct", response_model=DirectBindResponse)
|
||||
def direct_bind(
|
||||
payload: DirectBindRequest,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> DirectBindResponse:
|
||||
service = BindingService(db)
|
||||
result = service.direct_bind(payload.device_id, payload.child_id, current_user_id)
|
||||
return DirectBindResponse(**result)
|
||||
|
||||
|
||||
@router.get("/current", response_model=BindingGetResponse)
|
||||
def get_current_binding(
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
):
|
||||
service = BindingService(db)
|
||||
binding = service.get_current_binding(current_user_id)
|
||||
if not binding:
|
||||
raise HTTPException(status_code=404, detail="no binding found")
|
||||
return binding
|
||||
|
||||
|
||||
@router.get("/{device_id}", response_model=BindingGetResponse)
|
||||
def get_binding(
|
||||
device_id: str,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> BindingGetResponse:
|
||||
service = BindingService(db)
|
||||
binding = service.get_binding(device_id, current_user_id)
|
||||
if not binding:
|
||||
raise HTTPException(status_code=404, detail="binding not found")
|
||||
return BindingGetResponse(**binding)
|
||||
|
||||
|
||||
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def unbind_device(
|
||||
device_id: str,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> None:
|
||||
service = BindingService(db)
|
||||
if not service.unbind(device_id, current_user_id):
|
||||
raise HTTPException(status_code=404, detail="binding not found")
|
||||
|
||||
|
||||
@router.get("/history/{device_id}", response_model=BindHistoryResponse)
|
||||
def get_bind_history(
|
||||
device_id: str,
|
||||
request: Request,
|
||||
cursor: str | None = None,
|
||||
limit: int = 20,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> BindHistoryResponse:
|
||||
from datetime import datetime
|
||||
|
||||
cursor_dt = datetime.fromisoformat(cursor) if cursor else None
|
||||
service = BindingService(db)
|
||||
rows, has_more = service.list_history(device_id, limit, cursor_dt)
|
||||
next_cursor = rows[-1]["bound_at"].isoformat() if has_more and rows else None
|
||||
items = [BindHistoryItem(**row) for row in rows]
|
||||
return BindHistoryResponse(items=items, total=len(items), next_cursor=next_cursor)
|
||||
102
mini-program/app/routers/children.py
Normal file
102
mini-program/app/routers/children.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from app.security import get_current_user_id
|
||||
from app.service.child import ChildService
|
||||
from app.service import get_db_session
|
||||
except ModuleNotFoundError:
|
||||
from security import get_current_user_id
|
||||
from service.child import ChildService
|
||||
from service import get_db_session
|
||||
|
||||
|
||||
router = APIRouter(prefix="/children", tags=["children"])
|
||||
logger = logging.getLogger("app.children")
|
||||
|
||||
|
||||
class ChildCreateRequest(BaseModel):
|
||||
child_name: str
|
||||
child_gender: int = 2
|
||||
child_birthday: str | None = None
|
||||
|
||||
|
||||
class ChildResponse(BaseModel):
|
||||
child_id: int
|
||||
child_name: str
|
||||
child_gender: int
|
||||
child_birthday: str | None = None
|
||||
status: int
|
||||
|
||||
|
||||
class ChildUpdateRequest(BaseModel):
|
||||
child_name: str | None = None
|
||||
child_gender: int | None = None
|
||||
child_birthday: str | None = None
|
||||
|
||||
|
||||
class ChildListResponse(BaseModel):
|
||||
items: list[ChildResponse]
|
||||
total: int
|
||||
next_cursor: int | None = None
|
||||
|
||||
|
||||
@router.post("", response_model=ChildResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_child(
|
||||
payload: ChildCreateRequest,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> ChildResponse:
|
||||
service = ChildService(db)
|
||||
child = service.create(current_user_id, payload.child_name, payload.child_gender, payload.child_birthday)
|
||||
return ChildResponse(**child)
|
||||
|
||||
|
||||
@router.get("", response_model=ChildListResponse)
|
||||
def list_children(
|
||||
request: Request,
|
||||
cursor: int | None = None,
|
||||
limit: int = 20,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> ChildListResponse:
|
||||
service = ChildService(db)
|
||||
rows, has_more = service.list_children(current_user_id, limit, cursor)
|
||||
next_cursor = rows[-1]["child_id"] if has_more and rows else None
|
||||
items = [ChildResponse(**row) for row in rows]
|
||||
return ChildListResponse(items=items, total=len(items), next_cursor=next_cursor)
|
||||
|
||||
|
||||
@router.get("/{child_id}", response_model=ChildResponse)
|
||||
def get_child(
|
||||
child_id: int,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> ChildResponse:
|
||||
service = ChildService(db)
|
||||
child = service.get(child_id)
|
||||
if not child:
|
||||
raise HTTPException(status_code=404, detail="child not found")
|
||||
return ChildResponse(**child)
|
||||
|
||||
|
||||
@router.patch("/{child_id}", response_model=ChildResponse)
|
||||
def update_child(
|
||||
child_id: int,
|
||||
payload: ChildUpdateRequest,
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
db=Depends(get_db_session),
|
||||
) -> ChildResponse:
|
||||
service = ChildService(db)
|
||||
try:
|
||||
child = service.update(child_id, current_user_id, payload.child_name, payload.child_gender, payload.child_birthday)
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail="no permission to access this child")
|
||||
if not child:
|
||||
raise HTTPException(status_code=404, detail="child not found")
|
||||
return ChildResponse(**child)
|
||||
63
mini-program/app/routers/parents.py
Normal file
63
mini-program/app/routers/parents.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from app.service.parent import ParentService
|
||||
from app.service import get_db_session
|
||||
except ModuleNotFoundError:
|
||||
from service.parent import ParentService
|
||||
from service import get_db_session
|
||||
|
||||
|
||||
router = APIRouter(prefix="/parents", tags=["parents"])
|
||||
logger = logging.getLogger("app.parents")
|
||||
|
||||
|
||||
class ParentCreateRequest(BaseModel):
|
||||
openid: str
|
||||
unionid: str | None = None
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
|
||||
|
||||
class ParentResponse(BaseModel):
|
||||
user_id: int
|
||||
openid: str
|
||||
unionid: str | None = None
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
phone: str | None = None
|
||||
status: int
|
||||
|
||||
|
||||
class ParentUpdateRequest(BaseModel):
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
phone: str | None = None
|
||||
|
||||
|
||||
@router.post("", response_model=ParentResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_parent(payload: ParentCreateRequest, request: Request, db=Depends(get_db_session)) -> ParentResponse:
|
||||
service = ParentService(db)
|
||||
parent = service.create(payload.openid, payload.unionid, payload.nickname, payload.avatar_url)
|
||||
return ParentResponse(**parent)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=ParentResponse)
|
||||
def get_parent(user_id: int, request: Request, db=Depends(get_db_session)) -> ParentResponse:
|
||||
service = ParentService(db)
|
||||
parent = service.get(user_id)
|
||||
if not parent:
|
||||
raise HTTPException(status_code=404, detail="parent not found")
|
||||
return ParentResponse(**parent)
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=ParentResponse)
|
||||
def update_parent(user_id: int, payload: ParentUpdateRequest, request: Request, db=Depends(get_db_session)) -> ParentResponse:
|
||||
service = ParentService(db)
|
||||
parent = service.update(user_id, payload.nickname, payload.avatar_url, payload.phone)
|
||||
if not parent:
|
||||
raise HTTPException(status_code=404, detail="parent not found")
|
||||
return ParentResponse(**parent)
|
||||
87
mini-program/app/routers/wechat_auth.py
Normal file
87
mini-program/app/routers/wechat_auth.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
try:
|
||||
from app.db import get_db
|
||||
from app.security import create_access_token
|
||||
except ModuleNotFoundError:
|
||||
from db import get_db
|
||||
from security import create_access_token
|
||||
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
logger = logging.getLogger("app.auth")
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
code: Optional[str] = None
|
||||
nickname: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
expires_in: int
|
||||
user_id: int
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def login(
|
||||
payload: LoginRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> LoginResponse:
|
||||
logger.info(f"/auth/login called with code={payload.code[:20] if payload.code else None}...")
|
||||
identifier = payload.code or payload.username
|
||||
if not identifier:
|
||||
raise HTTPException(status_code=400, detail="username or code is required")
|
||||
|
||||
with db.begin():
|
||||
row = (
|
||||
db.execute(
|
||||
text("SELECT user_id FROM parents WHERE openid = :openid"),
|
||||
{"openid": identifier},
|
||||
).mappings().first()
|
||||
)
|
||||
|
||||
if row:
|
||||
user_id = int(row["user_id"])
|
||||
logger.info(f"Existing user found: user_id={user_id}")
|
||||
if payload.nickname or payload.avatar_url:
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE parents
|
||||
SET nickname = COALESCE(:nickname, nickname),
|
||||
avatar_url = COALESCE(:avatar_url, avatar_url)
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id, "nickname": payload.nickname, "avatar_url": payload.avatar_url},
|
||||
)
|
||||
else:
|
||||
logger.info("Creating new user...")
|
||||
db.execute(
|
||||
text(
|
||||
"INSERT INTO parents (openid, nickname, avatar_url, status) VALUES (:openid, :nickname, :avatar_url, 1)"
|
||||
),
|
||||
{"openid": identifier, "nickname": payload.nickname, "avatar_url": payload.avatar_url},
|
||||
)
|
||||
user_id = int(db.execute(text("SELECT last_insert_rowid()")).scalar_one())
|
||||
logger.info(f"New user created: user_id={user_id}")
|
||||
|
||||
access_token, expires_in = create_access_token(user_id=user_id)
|
||||
logger.info(f"Login succeeded: user_id={user_id}, expires_in={expires_in}")
|
||||
return LoginResponse(access_token=access_token, expires_in=expires_in, user_id=user_id)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(request: Request):
|
||||
return {"message": "logged out"}
|
||||
Reference in New Issue
Block a user