按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
"""营销人员的请求/响应模型(需求 2.1.1)。"""
|
||
|
||
from datetime import date
|
||
from decimal import Decimal, InvalidOperation
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||
|
||
from app.core.response import PageQuery
|
||
|
||
# salary_standard 列为 Numeric(12,2):整数部分最多 10 位。
|
||
# 工资系统金额红线——入参出参一律 str,服务层转 Decimal,全程不经 float
|
||
SALARY_MAX = Decimal("9999999999.99")
|
||
|
||
|
||
class PersonnelRegionOut(BaseModel):
|
||
"""辐射区域条目响应。"""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
id: int
|
||
region_name: str
|
||
remark: str | None = None
|
||
|
||
|
||
class PersonnelPlanOut(BaseModel):
|
||
"""拓展计划条目响应。"""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
id: int
|
||
plan_period: str
|
||
plan_content: str
|
||
target_desc: str | None = None
|
||
|
||
|
||
class PersonnelOut(BaseModel):
|
||
"""人员响应(列表与写操作回包共用)。"""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
id: int
|
||
personnel_name: str
|
||
business_role_id: int
|
||
role_name: str | None = None
|
||
manager_id: int | None = None
|
||
manager_name: str | None = None
|
||
phone: str | None = None
|
||
hire_date: date | None = None
|
||
probation_start_date: date | None = None
|
||
regularization_date: date | None = None
|
||
resignation_date: date | None = None
|
||
is_probation: int = 0
|
||
salary_standard: str | None = None
|
||
status: int = 1
|
||
remark: str | None = None
|
||
|
||
|
||
class PersonnelDetailOut(PersonnelOut):
|
||
salary_history_json: list[dict] | None = None
|
||
"""人员详情:附区域与拓展计划,供编辑抽屉一次回显。"""
|
||
|
||
regions: list[PersonnelRegionOut] = Field(default_factory=list)
|
||
plans: list[PersonnelPlanOut] = Field(default_factory=list)
|
||
|
||
|
||
class PersonnelCreateIn(BaseModel):
|
||
salary_effective_date: date | None = None
|
||
"""新增人员。"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
personnel_name: str = Field(min_length=1, max_length=64, description="姓名")
|
||
business_role_id: int = Field(ge=1, description="岗位角色ID")
|
||
manager_id: int | None = Field(default=None, ge=1, description="直属营销经理ID,空=无")
|
||
phone: str | None = Field(default=None, max_length=20, description="联系电话")
|
||
hire_date: date | None = Field(default=None, description="入职日期,ISO 格式")
|
||
probation_start_date: date | None = None
|
||
regularization_date: date | None = None
|
||
resignation_date: date | None = None
|
||
is_probation: int = Field(default=0, ge=0, le=1, description="是否试用期:1是 0否")
|
||
salary_standard: str | None = Field(
|
||
default=None, description="月工资标准(元),字符串形式的数字"
|
||
)
|
||
remark: str | None = Field(default=None, max_length=255)
|
||
|
||
@field_validator("salary_standard")
|
||
@classmethod
|
||
def _check_salary_standard(cls, value: str | None) -> str | None:
|
||
"""按 Numeric(12,2) 的容量校验字符串金额。
|
||
|
||
金额以字符串进出避免 float 精度损失(项目金额红线);
|
||
小数位超过两位直接拒绝而非静默舍入——舍入即算错钱。
|
||
"""
|
||
if value is None:
|
||
return None
|
||
try:
|
||
amount = Decimal(value)
|
||
except InvalidOperation:
|
||
raise ValueError("工资标准须为有效数字") from None
|
||
if not amount.is_finite():
|
||
raise ValueError("工资标准须为有效数字")
|
||
if amount < 0:
|
||
raise ValueError("工资标准不能为负数")
|
||
if amount > SALARY_MAX:
|
||
raise ValueError("工资标准超出可录入上限")
|
||
if -amount.as_tuple().exponent > 2:
|
||
raise ValueError("工资标准最多两位小数")
|
||
return value
|
||
|
||
|
||
@model_validator(mode="after")
|
||
def _validate_probation_dates(self):
|
||
if self.probation_start_date and self.regularization_date and self.regularization_date <= self.probation_start_date:
|
||
raise ValueError("转正生效日必须晚于试用期开始日")
|
||
return self
|
||
|
||
|
||
class PersonnelUpdateIn(PersonnelCreateIn):
|
||
"""编辑人员,全量提交、全量替换(不含状态,状态走独立接口)。"""
|
||
|
||
|
||
class PersonnelStatusIn(BaseModel):
|
||
"""在职/离职切换。"""
|
||
|
||
status: int = Field(ge=0, le=1, description="1在职 0离职")
|
||
effective_date: date | None = None
|
||
|
||
|
||
class RegionItemIn(BaseModel):
|
||
"""辐射区域条目。"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
region_name: str = Field(min_length=1, max_length=128, description="区域名称")
|
||
remark: str | None = Field(default=None, max_length=255)
|
||
|
||
|
||
class RegionReplaceIn(BaseModel):
|
||
"""辐射区域全量替换。空列表即清空该人员的全部区域。"""
|
||
|
||
items: list[RegionItemIn] = Field(default_factory=list, description="全量条目")
|
||
|
||
|
||
class PlanItemIn(BaseModel):
|
||
"""拓展计划条目。"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
plan_period: str = Field(min_length=1, max_length=32, description="计划期间,如 2026Q1")
|
||
plan_content: str = Field(min_length=1, max_length=500, description="计划内容")
|
||
target_desc: str | None = Field(default=None, max_length=255, description="指标说明")
|
||
|
||
|
||
class PlanReplaceIn(BaseModel):
|
||
"""拓展计划全量替换。空列表即清空该人员的全部计划。"""
|
||
|
||
items: list[PlanItemIn] = Field(default_factory=list, description="全量条目")
|
||
|
||
|
||
class PersonnelQuery(PageQuery):
|
||
"""人员列表查询条件。"""
|
||
|
||
keyword: str | None = Field(default=None, max_length=64, description="姓名,模糊匹配")
|
||
status: int | None = Field(default=None, ge=0, le=1, description="状态筛选:1在职 0离职")
|
||
business_role_id: int | None = Field(default=None, ge=1, description="岗位筛选")
|