feat(base): 人员管理接口(岗位/工资标准/团队/辐射区域/拓展计划)
- 删除保护:被团队成员/客户归属引用禁删,引导改离职 - 子表全量替换采用 clear 后立即 flush 防御写法 - 工资标准字符串进出,超两位小数拒绝而非静默舍入 Co-Authored-By: Claude Code <noreply@anthropic.com>
这个提交包含在:
1 父节点
f22d14e439
当前提交
a694570dd1
5 个文件已更改
+885
没有与搜索条件相匹配的文件
@@ -0,0 +1,147 @@
|
||||
"""营销人员的请求/响应模型(需求 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="岗位筛选")
|
||||
在新工单中引用
屏蔽一个用户