Files
personal-progress-monitor/app/models.py
MobKBK 8340d1dbbd feat: 新增日程面板、复盘页面与 Agent 读写接口
- 日程:单次/每日/每周/每月重复,JSON 存储,月历按优先级着色圆点 + 悬停提示
- 复盘页:可自定义折线图卡片(SVG) + 每日心得总览(按日期/任务展开)
- Agent 接口:GET /api/agent/overview 读日程+进度,POST /api/agent/schedules 建日程(PPM_API_TOKEN Bearer 鉴权)
- 导出/导入/统计纳入日程与图表;47 项测试全绿
2026-09-04 14:46:41 +08:00

180 lines
5.4 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.
from __future__ import annotations
from datetime import date, datetime
from enum import Enum
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class MetricKind(str, Enum):
checkin = "checkin"
quantity = "quantity"
duration = "duration"
class MetricPeriod(str, Enum):
day = "day"
week = "week"
month = "month"
lifetime = "lifetime"
class DurationUnit(str, Enum):
second = "second"
minute = "minute"
hour = "hour"
class MetricInput(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
metricId: str | None = None
name: Annotated[str, Field(min_length=1, max_length=60)]
kind: MetricKind
period: MetricPeriod = MetricPeriod.day
target: Annotated[int | None, Field(ge=1)] = None
unit: Annotated[str, Field(max_length=20)] = ""
durationUnit: DurationUnit = DurationUnit.minute
@model_validator(mode="after")
def normalize_metric(self) -> "MetricInput":
if self.kind == MetricKind.checkin:
self.target = 1 if self.target is None else self.target
self.unit = self.unit or "次"
elif self.kind == MetricKind.duration:
# 时长统一以整数秒存储,durationUnit 仅是输入/显示口径。
self.unit = ""
return self
class TaskInput(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
name: Annotated[str, Field(min_length=1, max_length=120)]
description: Annotated[str, Field(max_length=2000)] = ""
startDate: date | None = None
endDate: date | None = None
priority: Annotated[int, Field(ge=1, le=5)] = 3
state: Literal["active", "paused"] = "active"
metrics: Annotated[list[MetricInput], Field(min_length=1, max_length=20)]
@model_validator(mode="after")
def validate_dates_and_metrics(self) -> "TaskInput":
if self.startDate and self.endDate and self.endDate < self.startDate:
raise ValueError("结束日期不能早于开始日期")
ids = [metric.metricId for metric in self.metrics if metric.metricId]
if len(ids) != len(set(ids)):
raise ValueError("metricId 不能重复")
return self
class TaskUpdate(TaskInput):
expectedVersion: Annotated[int, Field(ge=1)]
class ReorderInput(BaseModel):
model_config = ConfigDict(extra="forbid")
taskIds: list[str]
expectedVersion: Annotated[int, Field(ge=1)]
@field_validator("taskIds")
@classmethod
def unique_task_ids(cls, value: list[str]) -> list[str]:
if len(value) != len(set(value)):
raise ValueError("taskIds 不能重复")
return value
class MetricValue(BaseModel):
model_config = ConfigDict(extra="forbid")
metricId: str
value: Annotated[int, Field(ge=0)]
class DailyRecordInput(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
expectedVersion: Annotated[int | None, Field(ge=0)] = None
note: Annotated[str, Field(max_length=4000)] = ""
completedAt: datetime | None = None
values: list[MetricValue] = Field(default_factory=list)
@field_validator("values")
@classmethod
def unique_metric_values(cls, value: list[MetricValue]) -> list[MetricValue]:
ids = [item.metricId for item in value]
if len(ids) != len(set(ids)):
raise ValueError("同一指标只能提交一次")
return value
class LoginInput(BaseModel):
model_config = ConfigDict(extra="forbid")
password: Annotated[str, Field(min_length=1, max_length=500)]
class PurgeInput(BaseModel):
model_config = ConfigDict(extra="forbid")
confirmation: str
class ScheduleRecurrence(str, Enum):
none = "none"
daily = "daily"
weekly = "weekly"
monthly = "monthly"
class ScheduleInput(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
title: Annotated[str, Field(min_length=1, max_length=120)]
description: Annotated[str, Field(max_length=2000)] = ""
date: date
time: Annotated[str | None, Field(max_length=5)] = None
recurrence: ScheduleRecurrence = ScheduleRecurrence.none
priority: Annotated[int, Field(ge=1, le=5)] = 3
state: Literal["active", "archived"] = "active"
@field_validator("time")
@classmethod
def validate_time(cls, value: str | None) -> str | None:
if value is None or value == "":
return None
if len(value) != 5 or value[2] != ":" or not value[:2].isdigit() or not value[3:].isdigit():
raise ValueError("时间必须是 HH:MM 格式")
hour, minute = int(value[:2]), int(value[3:])
if hour > 23 or minute > 59:
raise ValueError("时间必须是合法的时分")
return value
class ScheduleUpdate(ScheduleInput):
expectedVersion: Annotated[int, Field(ge=1)]
class ScheduleCompleteInput(BaseModel):
model_config = ConfigDict(extra="forbid")
date: date
completed: bool = True
expectedVersion: Annotated[int, Field(ge=1)]
class ChartInput(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
title: Annotated[str, Field(min_length=1, max_length=80)]
taskId: str | None = None
metricId: str | None = None
days: Annotated[int, Field(ge=7, le=365)] = 30
class ChartUpdate(ChartInput):
expectedVersion: Annotated[int, Field(ge=1)]