PayrollManagementSystem/backend/app/schemas/personnel.py
liuwenxin a694570dd1 feat(base): 人员管理接口(岗位/工资标准/团队/辐射区域/拓展计划)
- 删除保护:被团队成员/客户归属引用禁删,引导改离职
- 子表全量替换采用 clear 后立即 flush 防御写法
- 工资标准字符串进出,超两位小数拒绝而非静默舍入

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 10:35:02 +08:00

148 lines
5.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.

"""营销人员的请求/响应模型(需求 2.1.1)。"""
from datetime import date
from decimal import Decimal, InvalidOperation
from pydantic import BaseModel, ConfigDict, Field, field_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
is_probation: int = 0
salary_standard: str | None = None
status: int = 1
remark: str | None = None
class PersonnelDetailOut(PersonnelOut):
"""人员详情:附区域与拓展计划,供编辑抽屉一次回显。"""
regions: list[PersonnelRegionOut] = Field(default_factory=list)
plans: list[PersonnelPlanOut] = Field(default_factory=list)
class PersonnelCreateIn(BaseModel):
"""新增人员。"""
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 格式")
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
class PersonnelUpdateIn(PersonnelCreateIn):
"""编辑人员,全量提交、全量替换(不含状态,状态走独立接口)。"""
class PersonnelStatusIn(BaseModel):
"""在职/离职切换。"""
status: int = Field(ge=0, le=1, description="1在职 0离职")
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="岗位筛选")