- Add company module (3-tier org, CEO planning, parallel departments) - Add company orchestrator, knowledge extractor, presets, scheduler - Add company API endpoints, models, and frontend views - Add 今天吃啥 PWA app (69 dishes, real images, offline support) - Add team_projects output directory structure - Add unified manage.ps1 for service lifecycle - Add Windows startup guide v1.0 - Add TTS troubleshooting doc - Update frontend (AgentChat UX overhaul, new views) - Update backend (voice engine fix, multi-tenant, RBAC) - Remove deprecated startup scripts and old docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
289 lines
9.0 KiB
Python
289 lines
9.0 KiB
Python
"""
|
||
家庭阅读激励工具 - 数据模型定义
|
||
==================================
|
||
使用Python dataclass定义7个实体模型,与数据库表一一对应。
|
||
支持 dict ↔ dataclass 双向转换,便于DAL层与前端交互。
|
||
|
||
表映射:
|
||
Child → children
|
||
Book → books
|
||
ReadingRecord → reading_records
|
||
PointRecord → points
|
||
Reward → rewards
|
||
RedemptionRecord→ redemption_records
|
||
AppSetting → app_settings
|
||
"""
|
||
from dataclasses import dataclass, field, asdict
|
||
from typing import Optional
|
||
from datetime import datetime
|
||
|
||
|
||
# ============================================================
|
||
# 1. Child — 孩子账户
|
||
# ============================================================
|
||
@dataclass
|
||
class Child:
|
||
"""孩子账户实体"""
|
||
name: str
|
||
id: Optional[int] = None
|
||
avatar_path: Optional[str] = None
|
||
age: Optional[int] = None
|
||
total_points: int = 0
|
||
is_active: bool = True
|
||
created_at: Optional[str] = None
|
||
updated_at: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "Child":
|
||
"""从数据库行字典构造"""
|
||
return cls(
|
||
id=row.get("id"),
|
||
name=row["name"],
|
||
avatar_path=row.get("avatar_path"),
|
||
age=row.get("age"),
|
||
total_points=row.get("total_points", 0),
|
||
is_active=bool(row.get("is_active", 1)),
|
||
created_at=row.get("created_at"),
|
||
updated_at=row.get("updated_at"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
"""转为字典(排除None值,适配前端)"""
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 2. Book — 书籍信息
|
||
# ============================================================
|
||
@dataclass
|
||
class Book:
|
||
"""书籍实体"""
|
||
title: str
|
||
id: Optional[int] = None
|
||
author: Optional[str] = None
|
||
cover_image_path: Optional[str] = None
|
||
thumbnail_path: Optional[str] = None
|
||
page_count: Optional[int] = None
|
||
is_active: bool = True
|
||
created_at: Optional[str] = None
|
||
updated_at: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "Book":
|
||
return cls(
|
||
id=row.get("id"),
|
||
title=row["title"],
|
||
author=row.get("author"),
|
||
cover_image_path=row.get("cover_image_path"),
|
||
thumbnail_path=row.get("thumbnail_path"),
|
||
page_count=row.get("page_count"),
|
||
is_active=bool(row.get("is_active", 1)),
|
||
created_at=row.get("created_at"),
|
||
updated_at=row.get("updated_at"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 3. ReadingRecord — 阅读打卡记录
|
||
# ============================================================
|
||
@dataclass
|
||
class ReadingRecord:
|
||
"""阅读打卡记录实体"""
|
||
child_id: int
|
||
book_id: int
|
||
read_date: str # YYYY-MM-DD
|
||
id: Optional[int] = None
|
||
duration_minutes: Optional[int] = None
|
||
pages_read: Optional[int] = None
|
||
notes: Optional[str] = None
|
||
created_at: Optional[str] = None
|
||
|
||
# 关联对象(非数据库字段,JOIN填充)
|
||
child_name: Optional[str] = None
|
||
book_title: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "ReadingRecord":
|
||
return cls(
|
||
id=row.get("id"),
|
||
child_id=row["child_id"],
|
||
book_id=row["book_id"],
|
||
read_date=row["read_date"],
|
||
duration_minutes=row.get("duration_minutes"),
|
||
pages_read=row.get("pages_read"),
|
||
notes=row.get("notes"),
|
||
created_at=row.get("created_at"),
|
||
child_name=row.get("child_name"),
|
||
book_title=row.get("book_title"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 4. PointRecord — 积分流水
|
||
# ============================================================
|
||
@dataclass
|
||
class PointRecord:
|
||
"""积分流水实体(只追加,不可修改)"""
|
||
child_id: int
|
||
points_change: int # 正=获得,负=消费
|
||
reason: str # reading|redemption|bonus|adjustment
|
||
balance_after: int
|
||
id: Optional[int] = None
|
||
reference_type: Optional[str] = None # reading_record|redemption_record
|
||
reference_id: Optional[int] = None
|
||
created_at: Optional[str] = None
|
||
|
||
# 关联
|
||
child_name: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "PointRecord":
|
||
return cls(
|
||
id=row.get("id"),
|
||
child_id=row["child_id"],
|
||
points_change=row["points_change"],
|
||
reason=row["reason"],
|
||
balance_after=row["balance_after"],
|
||
reference_type=row.get("reference_type"),
|
||
reference_id=row.get("reference_id"),
|
||
created_at=row.get("created_at"),
|
||
child_name=row.get("child_name"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 5. Reward — 奖品/兑换项
|
||
# ============================================================
|
||
@dataclass
|
||
class Reward:
|
||
"""奖品实体"""
|
||
name: str
|
||
points_required: int
|
||
id: Optional[int] = None
|
||
description: Optional[str] = None
|
||
image_path: Optional[str] = None
|
||
stock: int = -1 # -1=无限库存
|
||
is_active: bool = True
|
||
created_at: Optional[str] = None
|
||
updated_at: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "Reward":
|
||
return cls(
|
||
id=row.get("id"),
|
||
name=row["name"],
|
||
description=row.get("description"),
|
||
image_path=row.get("image_path"),
|
||
points_required=row["points_required"],
|
||
stock=row.get("stock", -1),
|
||
is_active=bool(row.get("is_active", 1)),
|
||
created_at=row.get("created_at"),
|
||
updated_at=row.get("updated_at"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 6. RedemptionRecord — 兑换记录
|
||
# ============================================================
|
||
@dataclass
|
||
class RedemptionRecord:
|
||
"""兑换记录实体"""
|
||
child_id: int
|
||
reward_id: int
|
||
points_spent: int
|
||
id: Optional[int] = None
|
||
status: str = "fulfilled" # pending|fulfilled|cancelled
|
||
redeemed_at: Optional[str] = None
|
||
created_at: Optional[str] = None
|
||
|
||
# 关联
|
||
child_name: Optional[str] = None
|
||
reward_name: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "RedemptionRecord":
|
||
return cls(
|
||
id=row.get("id"),
|
||
child_id=row["child_id"],
|
||
reward_id=row["reward_id"],
|
||
points_spent=row["points_spent"],
|
||
status=row.get("status", "fulfilled"),
|
||
redeemed_at=row.get("redeemed_at"),
|
||
created_at=row.get("created_at"),
|
||
child_name=row.get("child_name"),
|
||
reward_name=row.get("reward_name"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 7. AppSetting — 应用配置
|
||
# ============================================================
|
||
@dataclass
|
||
class AppSetting:
|
||
"""应用配置键值对实体"""
|
||
key: str
|
||
value: str
|
||
updated_at: Optional[str] = None
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "AppSetting":
|
||
return cls(
|
||
key=row["key"],
|
||
value=row["value"],
|
||
updated_at=row.get("updated_at"),
|
||
)
|
||
|
||
def to_dict(self) -> dict:
|
||
d = asdict(self)
|
||
return {k: v for k, v in d.items() if v is not None}
|
||
|
||
|
||
# ============================================================
|
||
# 辅助:统计/聚合DTO
|
||
# ============================================================
|
||
@dataclass
|
||
class ChildReadingStats:
|
||
"""孩子阅读统计(聚合查询结果)"""
|
||
child_id: int
|
||
child_name: str
|
||
total_records: int = 0
|
||
total_minutes: int = 0
|
||
total_pages: int = 0
|
||
total_books: int = 0
|
||
current_points: int = 0
|
||
total_redemptions: int = 0
|
||
|
||
@classmethod
|
||
def from_row(cls, row: dict) -> "ChildReadingStats":
|
||
return cls(
|
||
child_id=row["child_id"],
|
||
child_name=row.get("child_name", ""),
|
||
total_records=row.get("total_records", 0),
|
||
total_minutes=row.get("total_minutes", 0),
|
||
total_pages=row.get("total_pages", 0),
|
||
total_books=row.get("total_books", 0),
|
||
current_points=row.get("current_points", 0),
|
||
total_redemptions=row.get("total_redemptions", 0),
|
||
)
|