Files
renjianbo f2e65a8fbb feat: virtual company module, team projects, PWA dishes app, and startup scripts overhaul
- 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>
2026-07-10 22:37:56 +08:00

289 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
家庭阅读激励工具 - 数据模型定义
==================================
使用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),
)