PayrollManagementSystem/backend/app/schemas/dashboard.py
liuwenxin 1461a8d113 feat(后端): 添加授权体系、系统监控与试用期计提
按 2026-11-02 需求补齐计划交付后端能力:
- 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝;
  管理员判断改为按数据库角色重新判定,防止权限码持有者绕过
- 系统监控:单进程采集资源、请求与异常指标,分钟级聚合,
  阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义
- 试用期计提:按回款日判定并写入快照,年度结清与调整单联动;
  人员模型补试用期、离职日期与工资版本历史字段
- 计提模板:公式骨架受控,页面维护名称与参数绑定
- T+ 采集:自动化配置表与进度查询

内置角色禁停用保护对齐模块注释;核算规则版本提为常量并
补充批量核算失败堆栈日志。

影响范围:后端全部模块、数据库迁移 V22-V28
验证:uv run ruff check 通过;922 个单元测试全部通过;
集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
2026-11-03 11:20:55 +08:00

175 lines
6.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.

"""工作台的请求/响应模型(M4 ⑧)。
聚合端点一次返回三区块(前端一次请求渲染整页,避免多次往返):
经营绩效总览 / 待办事项 / 风险预警。金额一律 Decimal
(序列化为字符串),前端不做数值运算。
**最近动态独立成分页端点**:它可能有很长(操作日志全量),塞进聚合
响应既撑爆页面又只能截断前 20 条;拆开后翻页只重取这张列表,不必
重算总览/待办/预警。列表与模块筛选项同源返回(一次请求给全)。
待办与预警为**查询时实时派生**(不落库、不引调度器,见计划决策);
同步记录、读取开关和自动采集开关分别返回,不用当前开关覆盖历史采集结果。
"""
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 SyncBlock(BaseModel):
"""源采集历史与当前运行配置,仅公开汇总状态。"""
available: bool = False
enabled: bool = False
automation_enabled: bool = False
latest_status: str | None = None
last_success_at: datetime | None = None
historical_failure_total: int = 0
class DashboardOut(BaseModel):
"""工作台三区块一次性响应(最近动态走独立分页端点)。"""
month: str
overview: OverviewBlock
todos: TodoBlock
warnings: WarningBlock
sync: SyncBlock = Field(default_factory=SyncBlock)