feat: 后端 CRUD 完成(episodes + yearly_targets + users 管理 API)
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
节目期次 API — 增改查(责编和管理员可操作)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, require_role
|
||||||
|
from app.db.session import get_session
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.episode import EpisodeCreate, EpisodeResponse, EpisodeUpdate
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/episodes", tags=["节目期次"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[EpisodeResponse])
|
||||||
|
def list_episodes(
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""获取节目期次列表(仅管理员/责编)。按播出日期倒序。"""
|
||||||
|
statement = select(Episode).order_by(Episode.air_date.desc()).offset(offset).limit(limit)
|
||||||
|
episodes = session.exec(statement).all()
|
||||||
|
return episodes
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{episode_id}", response_model=EpisodeResponse)
|
||||||
|
def get_episode(
|
||||||
|
episode_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""根据 ID 获取单期节目(仅管理员/责编)。"""
|
||||||
|
episode = session.get(Episode, episode_id)
|
||||||
|
if episode is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
|
||||||
|
return episode
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=EpisodeResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_episode(
|
||||||
|
body: EpisodeCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""新增一期节目(仅管理员/责编)。"""
|
||||||
|
# 检查期次号唯一
|
||||||
|
existing = session.exec(
|
||||||
|
select(Episode).where(Episode.episode_number == body.episode_number)
|
||||||
|
).first()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="期次号已存在")
|
||||||
|
|
||||||
|
episode = Episode.model_validate(body.model_dump())
|
||||||
|
session.add(episode)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(episode)
|
||||||
|
return episode
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{episode_id}", response_model=EpisodeResponse)
|
||||||
|
def update_episode(
|
||||||
|
episode_id: int,
|
||||||
|
body: EpisodeUpdate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""更新一期节目信息(仅管理员/责编)。"""
|
||||||
|
episode = session.get(Episode, episode_id)
|
||||||
|
if episode is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="期次不存在")
|
||||||
|
|
||||||
|
update_data = body.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(episode, field, value)
|
||||||
|
|
||||||
|
session.add(episode)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(episode)
|
||||||
|
return episode
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
用户管理 API — 管理员 CRUD(仅 zhipianren 可操作)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, require_role
|
||||||
|
from app.core.security import hash_password, verify_password
|
||||||
|
from app.db.session import get_session
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.user_admin import UserCreate, UserResponse, UserUpdate
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/users", tags=["用户管理"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[UserResponse])
|
||||||
|
def list_users(
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren)),
|
||||||
|
):
|
||||||
|
"""获取所有用户列表(仅管理员)。"""
|
||||||
|
statement = select(User).order_by(User.id)
|
||||||
|
users = session.exec(statement).all()
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_id}", response_model=UserResponse)
|
||||||
|
def get_user(
|
||||||
|
user_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren)),
|
||||||
|
):
|
||||||
|
"""根据 ID 获取单个用户(仅管理员)。"""
|
||||||
|
user = session.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_user(
|
||||||
|
body: UserCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren)),
|
||||||
|
):
|
||||||
|
"""创建新用户(仅管理员)。"""
|
||||||
|
# 检查用户名唯一
|
||||||
|
existing = session.exec(select(User).where(User.username == body.username)).first()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="用户名已存在")
|
||||||
|
|
||||||
|
user = User(
|
||||||
|
username=body.username,
|
||||||
|
display_name=body.display_name,
|
||||||
|
password_hash=hash_password(body.password),
|
||||||
|
role=body.role,
|
||||||
|
profile_text=body.profile_text,
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{user_id}", response_model=UserResponse)
|
||||||
|
def update_user(
|
||||||
|
user_id: int,
|
||||||
|
body: UserUpdate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren)),
|
||||||
|
):
|
||||||
|
"""更新用户信息(仅管理员)。可更新 display_name/role/profile_text/is_active。"""
|
||||||
|
user = session.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||||
|
|
||||||
|
update_data = body.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(user, field, value)
|
||||||
|
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def deactivate_user(
|
||||||
|
user_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren)),
|
||||||
|
):
|
||||||
|
"""停用用户(软删除,仅管理员)。"""
|
||||||
|
user = session.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||||
|
|
||||||
|
# 不能停用自己的账号
|
||||||
|
if user.id == current_user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能停用自己的账号")
|
||||||
|
|
||||||
|
user.is_active = False
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
return None
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
年度收视目标 API — 增改查
|
||||||
|
|
||||||
|
业务规则(.clinerules 5.2 只增不改):
|
||||||
|
- yearly_targets.year 有 UNIQUE 约束
|
||||||
|
- INSERT 时 year 冲突 → 直接报错,阻止静默覆盖
|
||||||
|
- 禁止 ON CONFLICT UPDATE
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, require_role
|
||||||
|
from app.db.session import get_session
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.yearly_target import YearlyTargetCreate, YearlyTargetResponse, YearlyTargetUpdate
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/yearly_targets", tags=["年度目标"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[YearlyTargetResponse])
|
||||||
|
def list_targets(
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""获取所有年度目标(仅管理员/责编)。"""
|
||||||
|
statement = select(YearlyTarget).order_by(YearlyTarget.year.desc())
|
||||||
|
targets = session.exec(statement).all()
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{target_id}", response_model=YearlyTargetResponse)
|
||||||
|
def get_target(
|
||||||
|
target_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""根据 ID 获取单个年度目标(仅管理员/责编)。"""
|
||||||
|
target = session.get(YearlyTarget, target_id)
|
||||||
|
if target is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=YearlyTargetResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_target(
|
||||||
|
body: YearlyTargetCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""新增年度目标(仅管理员/责编)。
|
||||||
|
|
||||||
|
注意:year 有 UNIQUE 约束,重复年份会直接报错。
|
||||||
|
这是预期行为——历史年份目标不可覆盖。
|
||||||
|
"""
|
||||||
|
# 先查是否已存在(让报错更友好,而不是数据库层直接崩溃)
|
||||||
|
existing = session.exec(select(YearlyTarget).where(YearlyTarget.year == body.year)).first()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=f"{body.year} 年目标已存在,历史年份不可覆盖。请新增其他年份。",
|
||||||
|
)
|
||||||
|
|
||||||
|
target = YearlyTarget.model_validate(body.model_dump())
|
||||||
|
session.add(target)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(target)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{target_id}", response_model=YearlyTargetResponse)
|
||||||
|
def update_target(
|
||||||
|
target_id: int,
|
||||||
|
body: YearlyTargetUpdate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian)),
|
||||||
|
):
|
||||||
|
"""更新年度目标(仅管理员/责编)。
|
||||||
|
|
||||||
|
注意:此接口仅用于修正错字等小修,year 字段不可修改。
|
||||||
|
"""
|
||||||
|
target = session.get(YearlyTarget, target_id)
|
||||||
|
if target is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="目标不存在")
|
||||||
|
|
||||||
|
update_data = body.model_dump(exclude_unset=True)
|
||||||
|
# 禁止修改 year 字段
|
||||||
|
update_data.pop("year", None)
|
||||||
|
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(target, field, value)
|
||||||
|
|
||||||
|
session.add(target)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(target)
|
||||||
|
return target
|
||||||
@@ -7,6 +7,9 @@ from starlette.middleware.cors import CORSMiddleware
|
|||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from app.api.auth import router as auth_router
|
from app.api.auth import router as auth_router
|
||||||
|
from app.api.episodes import router as episodes_router
|
||||||
|
from app.api.users import router as users_router
|
||||||
|
from app.api.yearly_targets import router as yearly_targets_router
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
app = FastAPI(title="军事科技工作台", version="0.1.0")
|
app = FastAPI(title="军事科技工作台", version="0.1.0")
|
||||||
@@ -34,3 +37,6 @@ app.add_middleware(
|
|||||||
|
|
||||||
# ---- 注册路由 ----
|
# ---- 注册路由 ----
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
app.include_router(episodes_router)
|
||||||
|
app.include_router(yearly_targets_router)
|
||||||
|
app.include_router(users_router)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
节目期次模型 — SQLModel
|
||||||
|
|
||||||
|
业务规则(.clinerules 5.3 软引用 + 快照模式):
|
||||||
|
- editor_id 可空(离职编导)
|
||||||
|
- editor_name_snapshot 必填(历史导入也走快照)
|
||||||
|
- 查询历史作品:先 editor_id,回退 editor_name_snapshot 文本匹配
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Column
|
||||||
|
from sqlalchemy import DateTime as SADateTime
|
||||||
|
from sqlalchemy import Text
|
||||||
|
from sqlalchemy import Boolean as SABoolean
|
||||||
|
from sqlalchemy import Integer as SAInteger
|
||||||
|
from sqlalchemy import Numeric as SANumeric
|
||||||
|
from sqlalchemy.sql import func as sa_func
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class Episode(SQLModel, table=True):
|
||||||
|
__tablename__ = "episodes"
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
episode_number: int = Field(unique=True, index=True)
|
||||||
|
program_name: str = Field(max_length=200)
|
||||||
|
air_date: date
|
||||||
|
|
||||||
|
# 编导(软引用 + 快照双字段)
|
||||||
|
# 在职编导:editor_id 和 editor_name_snapshot 都填
|
||||||
|
# 离职编导:editor_id 留空,editor_name_snapshot 填姓名
|
||||||
|
editor_id: int | None = Field(default=None, foreign_key="users.id")
|
||||||
|
editor_name_snapshot: str = Field(max_length=100)
|
||||||
|
|
||||||
|
# 收视数据
|
||||||
|
# 注意:.clinerules 5.1 颜色只对 audience_share 上色
|
||||||
|
# audience_rating 只展示数字,不参与颜色判定
|
||||||
|
audience_share: float | None = Field(default=None)
|
||||||
|
audience_rating: float | None = Field(default=None)
|
||||||
|
|
||||||
|
# 重播相关
|
||||||
|
is_rerun: bool = Field(default=False)
|
||||||
|
original_episode_id: int | None = Field(default=None, foreign_key="episodes.id")
|
||||||
|
is_time_sensitive: bool = Field(default=False)
|
||||||
|
|
||||||
|
# 外拍信息
|
||||||
|
is_outdoor_shoot: bool = Field(default=False)
|
||||||
|
outdoor_contact_person: str | None = Field(default=None, max_length=100)
|
||||||
|
outdoor_contact_info: str | None = Field(default=None, max_length=200)
|
||||||
|
|
||||||
|
# 线索
|
||||||
|
lead_source: str | None = Field(default=None, max_length=300)
|
||||||
|
|
||||||
|
# 责编备注
|
||||||
|
notes: str | None = Field(default=None)
|
||||||
|
|
||||||
|
created_at: datetime | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
|
||||||
|
)
|
||||||
|
updated_at: datetime | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
年度收视目标模型 — SQLModel
|
||||||
|
|
||||||
|
业务规则(.clinerules 5.2 年度目标只增不改):
|
||||||
|
- yearly_targets.year 有 UNIQUE 约束,数据库层已保证
|
||||||
|
- 代码端 INSERT 时只许 ON CONFLICT DO NOTHING,禁止 ON CONFLICT UPDATE
|
||||||
|
- 历史年份目标行绝不可被覆盖或删除
|
||||||
|
- 前端判颜色时按播出年份取对应年的目标,不是当前年份
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Column
|
||||||
|
from sqlalchemy import DateTime as SADateTime
|
||||||
|
from sqlalchemy import Integer as SAInteger
|
||||||
|
from sqlalchemy import Numeric as SANumeric
|
||||||
|
from sqlalchemy.sql import func as sa_func
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class YearlyTarget(SQLModel, table=True):
|
||||||
|
__tablename__ = "yearly_targets"
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
year: int = Field(unique=True, index=True)
|
||||||
|
base_target: float # 基础目标
|
||||||
|
stretch_target: float # 摸高目标
|
||||||
|
issued_date: date | None = Field(default=None)
|
||||||
|
notes: str | None = Field(default=None)
|
||||||
|
|
||||||
|
created_at: datetime | None = Field(
|
||||||
|
default=None,
|
||||||
|
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
|
||||||
|
)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
认证相关 Pydantic Schema — 请求 / 响应模型
|
认证相关 Pydantic Schema — 请求 / 响应模型
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
from app.models.user import UserRole
|
from app.models.user import UserRole
|
||||||
|
|
||||||
@@ -21,5 +21,4 @@ class UserResponse(BaseModel):
|
|||||||
profile_text: str | None = None
|
profile_text: str | None = None
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(from_attributes=True)
|
||||||
from_attributes = True
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
节目期次 Schema — 请求 / 响应模型
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from app.models.user import UserRole
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeBase(BaseModel):
|
||||||
|
episode_number: int
|
||||||
|
program_name: str
|
||||||
|
air_date: date
|
||||||
|
editor_id: int | None = None
|
||||||
|
editor_name_snapshot: str
|
||||||
|
audience_share: float | None = None
|
||||||
|
audience_rating: float | None = None
|
||||||
|
is_rerun: bool = False
|
||||||
|
original_episode_id: int | None = None
|
||||||
|
is_time_sensitive: bool = False
|
||||||
|
is_outdoor_shoot: bool = False
|
||||||
|
outdoor_contact_person: str | None = None
|
||||||
|
outdoor_contact_info: str | None = None
|
||||||
|
lead_source: str | None = None
|
||||||
|
notes: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeCreate(EpisodeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeUpdate(EpisodeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeResponse(EpisodeBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
用户管理 Schema — 管理员 CRUD(仅 zhipianren 可用)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from app.models.user import UserRole
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
password: str
|
||||||
|
role: UserRole
|
||||||
|
profile_text: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
display_name: str | None = None
|
||||||
|
role: UserRole | None = None
|
||||||
|
profile_text: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
role: UserRole
|
||||||
|
profile_text: str | None = None
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
年度收视目标 Schema — 请求 / 响应模型
|
||||||
|
|
||||||
|
业务规则:yearly_targets 只增不改,ON CONFLICT DO NOTHING
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class YearlyTargetBase(BaseModel):
|
||||||
|
year: int
|
||||||
|
base_target: float
|
||||||
|
stretch_target: float
|
||||||
|
issued_date: date | None = None
|
||||||
|
notes: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class YearlyTargetCreate(YearlyTargetBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class YearlyTargetUpdate(YearlyTargetBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class YearlyTargetResponse(YearlyTargetBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
Reference in New Issue
Block a user