dev #1

Merged
huangyicao merged 12 commits from dev into main 2026-04-27 09:39:05 +08:00
4 changed files with 110 additions and 50 deletions
Showing only changes of commit 9d7c1ce7b4 - Show all commits

View File

@@ -5,9 +5,21 @@ from typing import Optional
from sqlalchemy import text from sqlalchemy import text
from app.dao import BaseDAO from app.dao import BaseDAO
from app.db_compat import inserted_primary_key
class ChildDAO(BaseDAO): class ChildDAO(BaseDAO):
def _create_relation(self, user_id: int, child_id: int) -> None:
self.db.execute(
text(
"""
INSERT INTO parent_child_relations (user_id, child_id, relation_type, is_primary, status)
VALUES (:user_id, :child_id, 9, 0, 1)
"""
),
{"user_id": user_id, "child_id": child_id},
)
def create( def create(
self, self,
user_id: int, user_id: int,
@@ -18,13 +30,14 @@ class ChildDAO(BaseDAO):
result = self.db.execute( result = self.db.execute(
text( text(
""" """
INSERT INTO children (parent_user_id, child_name, child_gender, child_birthday, status) INSERT INTO children (child_name, child_gender, child_birthday, status)
VALUES (:user_id, :child_name, :child_gender, :child_birthday, 1) VALUES (:child_name, :child_gender, :child_birthday, 1)
""" """
), ),
{"user_id": user_id, "child_name": child_name, "child_gender": child_gender, "child_birthday": child_birthday}, {"child_name": child_name, "child_gender": child_gender, "child_birthday": child_birthday},
) )
child_id = int(result.lastrowid) child_id = inserted_primary_key(result)
self._create_relation(user_id, child_id)
self.commit() self.commit()
return child_id return child_id
@@ -40,18 +53,21 @@ class ChildDAO(BaseDAO):
def list_by_parent(self, user_id: int, limit: int = 20, cursor: int = None) -> list[Mapping]: def list_by_parent(self, user_id: int, limit: int = 20, cursor: int = None) -> list[Mapping]:
params = {"user_id": user_id, "limit": limit + 1} params = {"user_id": user_id, "limit": limit + 1}
where = "parent_user_id = :user_id AND status = 1" where = "pcr.user_id = :user_id AND pcr.status = 1 AND c.status = 1"
if cursor: if cursor is not None:
where += " AND child_id < :cursor" where += " AND c.child_id < :cursor"
params["cursor"] = cursor params["cursor"] = cursor
rows = ( rows = (
self.db.execute( self.db.execute(
text( text(
f""" f"""
SELECT * FROM children SELECT c.*
FROM children AS c
JOIN parent_child_relations AS pcr
ON pcr.child_id = c.child_id
WHERE {where} WHERE {where}
ORDER BY child_id DESC ORDER BY c.child_id DESC
LIMIT :limit LIMIT :limit
""" """
), ),
@@ -87,7 +103,16 @@ class ChildDAO(BaseDAO):
return ( return (
self.db.execute( self.db.execute(
text( text(
"SELECT 1 FROM children WHERE child_id = :child_id AND parent_user_id = :user_id AND status = 1" """
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}, {"child_id": child_id, "user_id": user_id},
).scalar_one_or_none() ).scalar_one_or_none()

View File

@@ -0,0 +1,41 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy.orm import Session
def get_db_dialect_name(db: Session) -> str:
bind = db.get_bind()
if bind is None:
raise RuntimeError("Database session is not bound to an engine")
return bind.dialect.name
def current_timestamp_sql(db: Session) -> str:
if get_db_dialect_name(db) == "mysql":
return "CURRENT_TIMESTAMP(3)"
return "CURRENT_TIMESTAMP"
def select_for_update_clause(db: Session) -> str:
if get_db_dialect_name(db) == "mysql":
return " FOR UPDATE"
return ""
def inserted_primary_key(result: Any) -> int:
lastrowid = getattr(result, "lastrowid", None)
if lastrowid is not None:
return int(lastrowid)
try:
inserted_primary_key = result.inserted_primary_key
except Exception:
inserted_primary_key = None
if isinstance(inserted_primary_key, Sequence) and inserted_primary_key:
primary_key = inserted_primary_key[0]
if primary_key is not None:
return int(primary_key)
raise RuntimeError("Could not determine inserted primary key")

View File

@@ -1,35 +1,29 @@
import os import os
import logging import logging
import sys
from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
try: if __package__ in (None, ""):
# For module mode: `uvicorn app.main:app` # Make `python app/main.py` behave like module execution from the project root.
from app.db import check_db_connection, close_db_engine, init_db_tables project_root = Path(__file__).resolve().parent.parent
from app.logging_setup import configure_logging project_root_str = str(project_root)
from app.middleware.auth import install_auth_middleware if project_root_str not in sys.path:
from app.middleware.request_log import install_request_logging_middleware sys.path.insert(0, project_root_str)
from app.routers.auth import router as auth_router
from app.routers.wechat_auth import router as wechat_auth_router from app.db import check_db_connection, close_db_engine, init_db_tables
from app.routers.health import router as health_router from app.logging_setup import configure_logging
from app.routers.messages import router as messages_router from app.middleware.auth import install_auth_middleware
from app.routers.parents import router as parents_router from app.middleware.request_log import install_request_logging_middleware
from app.routers.children import router as children_router from app.routers.auth import router as auth_router
from app.routers.bindings import router as bindings_router from app.routers.wechat_auth import router as wechat_auth_router
from app.settings import settings from app.routers.health import router as health_router
except ModuleNotFoundError: from app.routers.messages import router as messages_router
# For script mode: `python app/main.py` or VS Code "Run Python File" from app.routers.parents import router as parents_router
from db import check_db_connection, close_db_engine, init_db_tables from app.routers.children import router as children_router
from logging_setup import configure_logging from app.routers.bindings import router as bindings_router
from middleware.auth import install_auth_middleware from app.settings import settings
from middleware.request_log import install_request_logging_middleware
from routers.auth import router as auth_router
from routers.health import router as health_router
from routers.messages import router as messages_router
from routers.parents import router as parents_router
from routers.children import router as children_router
from routers.bindings import router as bindings_router
from settings import settings
configure_logging() configure_logging()
@@ -59,12 +53,9 @@ def create_app() -> FastAPI:
app.include_router(wechat_auth_router) app.include_router(wechat_auth_router)
app.include_router(health_router) app.include_router(health_router)
app.include_router(messages_router) app.include_router(messages_router)
try: app.include_router(parents_router)
app.include_router(parents_router) app.include_router(children_router)
app.include_router(children_router) app.include_router(bindings_router)
app.include_router(bindings_router)
except NameError:
pass
return app return app

View File

@@ -8,11 +8,13 @@ from sqlalchemy.orm import Session
try: try:
# For module mode: `uvicorn app.main:app` # For module mode: `uvicorn app.main:app`
from app.db import get_db from app.db import get_db
from app.db_compat import current_timestamp_sql
from app.schemas.auth import LoginRequest, LoginResponse from app.schemas.auth import LoginRequest, LoginResponse
from app.security import create_access_token from app.security import create_access_token
except ModuleNotFoundError: except ModuleNotFoundError:
# For script mode: `python app/main.py` or VS Code "Run Python File" # For script mode: `python app/main.py` or VS Code "Run Python File"
from db import get_db from db import get_db
from db_compat import current_timestamp_sql
from schemas.auth import LoginRequest, LoginResponse from schemas.auth import LoginRequest, LoginResponse
from security import create_access_token from security import create_access_token
@@ -34,6 +36,7 @@ def login(
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> LoginResponse: ) -> LoginResponse:
username_masked = _mask_username(payload.username) username_masked = _mask_username(payload.username)
now_sql = current_timestamp_sql(db)
with db.begin(): with db.begin():
row = ( row = (
db.execute( db.execute(
@@ -107,10 +110,10 @@ def login(
db.execute( db.execute(
text( text(
""" f"""
UPDATE chat_user_auth UPDATE chat_user_auth
SET last_login_at = CURRENT_TIMESTAMP(3), SET last_login_at = {now_sql},
updated_at = CURRENT_TIMESTAMP(3) updated_at = {now_sql}
WHERE id = :user_auth_id WHERE id = :user_auth_id
""" """
), ),
@@ -118,10 +121,10 @@ def login(
) )
db.execute( db.execute(
text( text(
""" f"""
UPDATE chat_user UPDATE chat_user
SET last_login_at = CURRENT_TIMESTAMP(3), SET last_login_at = {now_sql},
updated_at = CURRENT_TIMESTAMP(3) updated_at = {now_sql}
WHERE id = :user_id WHERE id = :user_id
""" """
), ),