"""工作台的请求/响应模型(M4 ⑧)。 聚合端点一次返回三区块(前端一次请求渲染整页,避免多次往返): 经营绩效总览 / 待办事项 / 风险预警。金额一律 Decimal (序列化为字符串),前端不做数值运算。 **最近动态独立成分页端点**:它可能有很长(操作日志全量),塞进聚合 响应既撑爆页面又只能截断前 20 条;拆开后翻页只重取这张列表,不必 重算总览/待办/预警。列表与模块筛选项同源返回(一次请求给全)。 待办与预警为**查询时实时派生**(不落库、不引调度器,见计划决策); ⑥ T+ 同步未建前,同步相关条目用 `sync_available=false` 显式降级, 不返回伪造的 0 条。 """ from datetime import date, datetime from decimal import Decimal from pydantic import BaseModel, ConfigDict, Field from app.core.response import PageQuery class TrendPoint(BaseModel): """近 6 个月趋势点(绩效按台账归属月,回款按到账月)。""" month: str performance_total: Decimal | None = None receipt_total: Decimal | None = None class SettlementBrief(BaseModel): """当月月结状态(工作台总览角标)。""" exists: bool = False status: int | None = Field(default=None, description="1草稿 2待审批 3已审定 4已驳回") total_performance: Decimal | None = None total_payable: Decimal | None = None locked_at: datetime | None = None class OverviewBlock(BaseModel): """经营绩效总览(FR-033):口径与台账/月结一致。""" month: str performance_total: Decimal | None = Field(default=None, description="当月单笔绩效合计") payable_total: Decimal | None = Field(default=None, description="当月应付绩效合计") person_count: int = Field(default=0, description="当月有台账的人员数(拆行去重)") team_count: int = Field(default=0, description="在职营销经理数(团队数)") settlement: SettlementBrief trend: list[TrendPoint] = Field(default_factory=list, description="近 6 个月") note: str = Field(default="", description="口径说明(页面展示)") class ApprovalTodoItem(BaseModel): """待审批条目。""" order_id: int order_no: str order_type: int submitter_name: str | None = None create_time: datetime class ReceiptTodoItem(BaseModel): """回款未指定发货批次条目。""" receipt_id: int receipt_batch_no: str business_id: int business_no: str | None = None amount: Decimal receipt_date: date class TodoBlock(BaseModel): """待办事项(FR-034,澄清 #11 起步范围)。""" pending_approval_total: int = 0 pending_approvals: list[ApprovalTodoItem] = Field(default_factory=list) unassigned_receipt_total: int = 0 unassigned_receipts: list[ReceiptTodoItem] = Field(default_factory=list) sync_available: bool = Field(default=False, description="⑥ 同步模块是否已启用") sync_failure_total: int = 0 class WarningItem(BaseModel): """风险预警条目(实时派生,不改变业务数据)。""" code: str = Field(description="规则编码:receipt_overdue / approval_timeout") level: str = Field(description="级别:warning / danger") object_type: str object_id: int title: str detail: str suggest: str link: str | None = Field(default=None, description="前端跳转路径") class WarningBlock(BaseModel): """风险预警(FR-035,首批两条可算规则;同步规则待 ⑥)。""" enabled: bool = True receipt_overdue_days: int = 90 approval_timeout_days: int = 3 items: list[WarningItem] = Field(default_factory=list) total: int = Field(default=0, description="全部规则命中总数(items 仅为展示上限内的子集)") receipt_overdue_total: int = 0 approval_timeout_total: int = 0 sync_available: bool = False class ActivityItem(BaseModel): """最近动态条目(来源 operation_logs,只读聚合)。""" id: int module: str action: str summary: str username: str | None = None success: int create_time: datetime class ActivityQuery(PageQuery): """最近动态查询条件:分页 + 模块筛选。 只支持模块筛选(工作台只需要这一个维度);其余筛选项见日志查询页。 """ module: str | None = Field(default=None, max_length=64, description="模块,精确匹配") class ActivityPageOut(BaseModel): """最近动态分页结果:当前页 + 总数 + 模块筛选项。 分页字段名沿用全站规范(list/total/pageNum/pageSize),与 `PageResult` 同形;额外带 `modules`(全量模块名), 供前端模块下拉使用 —— 不随当前页变化,避免翻页后选项增减。 """ model_config = ConfigDict(populate_by_name=True) records: list[ActivityItem] = Field( default_factory=list, alias="list", serialization_alias="list", description="当前页数据" ) total: int = Field(default=0, description="总记录数") page_num: int = Field( default=1, alias="pageNum", serialization_alias="pageNum", description="当前页码" ) page_size: int = Field( default=10, alias="pageSize", serialization_alias="pageSize", description="每页条数" ) modules: list[str] = Field(default_factory=list, description="全部模块名(筛选项)") class DashboardOut(BaseModel): """工作台三区块一次性响应(最近动态走独立分页端点)。""" month: str overview: OverviewBlock todos: TodoBlock warnings: WarningBlock