修改schema

This commit is contained in:
strepsiades
2026-03-24 10:27:14 +08:00
parent 65bf2346a3
commit 8ee0131d47
12 changed files with 350 additions and 15 deletions
+33
View File
@@ -59,6 +59,39 @@ class BaseResponseWithID(BaseModel):
# 默认返回(已有 id
return data
def _get_plan_node_date(node: Any) -> Any:
if isinstance(node, dict):
return node.get("date")
return getattr(node, "date", None)
def trim_plan_nodes_by_boundary_dates(
plan_nodes: Optional[List[Any]],
start_date: Optional[Any] = None,
end_date: Optional[Any] = None,
) -> List[Any]:
if not plan_nodes:
return []
cleaned_nodes = list(plan_nodes)
while (
start_date is not None
and cleaned_nodes
and str(_get_plan_node_date(cleaned_nodes[0])) == str(start_date)
):
cleaned_nodes.pop(0)
while (
end_date is not None
and cleaned_nodes
and str(_get_plan_node_date(cleaned_nodes[-1])) == str(end_date)
):
cleaned_nodes.pop()
return cleaned_nodes
class RequestApprovalMixin(BaseModel):
"""
请求审批相关的基础模型
+12 -1
View File
@@ -24,7 +24,18 @@
from pydantic import BaseModel, Field
from typing import TypeVar, Generic, Optional, List, Dict, Any
from ..project.enums import ProjectType
from datetime import datetime
# from backend.modules.admin.approval.models import ApprovalStep
class ApprovalStep(BaseModel):
"""审批流程中的一个步骤"""
step_id: str = Field(description="步骤的唯一ID")
operator_id: Optional[str] = Field("",description="操作人ID")
operator_name: str = Field(description="操作人姓名")
action: str = Field(description="执行的动作,如 'submit', 'approve', 'reject'")
comment: Optional[str] = Field(None, description="审批意见")
created_at: datetime = Field(default_factory=datetime.utcnow, description="步骤的创建时间")
# 定义泛型
T = TypeVar('T', bound='BasePushData')
@@ -52,7 +63,7 @@ class SignatureMixin(BaseModel):
class PushRequestMixin(BaseModel):
"""推送ID字段混入"""
push_id: str = Field(..., description="推送ID,客户端生成的唯一标识(UUIDv7,支持时间排序)")
steps : List[ApprovalStep] =Field(default_factory=list,description="当前数据的审批流程步骤") # 加入字段step用来记录审批流程
# ==================== 响应相关 ====================
+10 -1
View File
@@ -2,7 +2,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.base import BaseResponseWithID, trim_plan_nodes_by_boundary_dates
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
@@ -40,6 +40,15 @@ class ConstructionResponse(BaseResponseWithID):
# 2025-10-20新增字段,延迟时间
delay_days: Optional[int] = Field(None, description="延迟时间(天)", examples=[0])
@model_validator(mode='after')
def clean_response_plan_nodes(self):
self.plan_node = trim_plan_nodes_by_boundary_dates(
self.plan_node,
start_date=self.plan_start_date,
end_date=self.plan_complete_date,
)
return self
class ConstructionCreate(BaseModel):
"""
施工任务创建模型
@@ -2,7 +2,7 @@ from pydantic import Field, BaseModel,field_validator,model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.base import BaseResponseWithID, trim_plan_nodes_by_boundary_dates
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
@@ -39,6 +39,15 @@ class ContractSettlementResponse(BaseResponseWithID):
None, description="统计节点截止日期", examples=["2024-05-31"]
)
@model_validator(mode='after')
def clean_response_plan_nodes(self):
self.plan_node = trim_plan_nodes_by_boundary_dates(
self.plan_node,
start_date=self.plan_start_date,
end_date=self.plan_complete_date,
)
return self
class ContractSettlementCreate(BaseModel):
"""
合同结算创建模型
@@ -90,7 +99,7 @@ class ContractSettlementPlanUpdate(BaseModel):
cleaned_nodes and
cleaned_nodes[0].date == self.plan_start_date):
cleaned_nodes.pop(0)
# 删除结尾的边界节点
if (self.plan_complete_date and
cleaned_nodes and
+10 -1
View File
@@ -2,7 +2,7 @@ from pydantic import Field, BaseModel,field_validator,model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.base import BaseResponseWithID, trim_plan_nodes_by_boundary_dates
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
@@ -52,6 +52,15 @@ class MaterialResponse(BaseResponseWithID):
None, description="统计节点截止日期", examples=["2024-05-31"]
)
@model_validator(mode='after')
def clean_response_plan_nodes(self):
self.plan_node = trim_plan_nodes_by_boundary_dates(
self.plan_node,
start_date=self.plan_start_date,
end_date=self.plan_complete_date,
)
return self
class MaterialCreate(BaseModel):
"""
创建物资
+10 -1
View File
@@ -2,7 +2,7 @@ from pydantic import Field, BaseModel,field_validator,model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.base import BaseResponseWithID, trim_plan_nodes_by_boundary_dates
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
@@ -61,6 +61,15 @@ class PowerResponse(BaseResponseWithID):
None, description="实际首批到场人数", examples=[4]
)
@model_validator(mode='after')
def clean_response_plan_nodes(self):
self.plan_node = trim_plan_nodes_by_boundary_dates(
self.plan_node,
start_date=self.plan_start_date,
end_date=self.plan_exit_date,
)
return self
class PowerCreate(BaseModel):
"""
创建力能
+9
View File
@@ -74,3 +74,12 @@ class ProjectInfoResponse(BaseModel):
# 里程碑节点
preparation_list: list[PreparationDetail] = Field(default_factory=list, description="里程碑节点列表")
class ProjectListResponse(BaseModel):
"""
项目列表模型:分页
"""
page: int
page_size: int
total: int
list: list[ProjectInfoResponse]
+150
View File
@@ -0,0 +1,150 @@
"""
一个接口按 domain 返回项目聚合数据。
"""
from typing import Literal
from pydantic import BaseModel, Field
from ..construction.construction import ConstructionResponse
from ..construction.construction_detail import ConstructionDetailResponse
from ..contract.contract import ContractResponse
from ..contract_settlement.contract_settlement import (
ContractSettlementResponse,
)
from ..contract_settlement.contract_settlement_detail import (
ContractSettlementDetailResponse,
)
from ..material.material import MaterialResponse
from ..material.material_detail import MaterialDetailResponse
from ..power.power import PowerResponse
from ..power.power_detail import PowerDetail
from ..project.project_base import ProjectResponseBase
from ..project_extensions.contract_change import ContractChangeResponse
from ..project_extensions.generator_unit import GeneratorUnitProject
from ..project_extensions.lar import (
LarChangeDetailSchema,
LarDetailSchema,
)
from ..project_extensions.preparation import PreparationDetail
from ..project_extensions.production import ProductionDetail
from ..project_extensions.project_detail import ProjectDetailProject
from ..project_multi_type import (
GasTurbineExtension,
HydropowerExtension,
OffShoreWindExtension,
OnShoreWindExtension,
PhotovoltaicExtension,
PumpedStorageExtension,
ThermalPowerExtension,
)
ProjectAggregateDomain = Literal[
"project",
"project_detail",
"generator_units",
"lar",
"lar_change",
"contract_changes",
"photovoltaic_extension",
"onshore_wind_extension",
"offshore_wind_extension",
"hydropower_extension",
"pumped_storage_extension",
"gas_turbine_extension",
"thermal_power_extension",
"production_list",
"preparation_list",
"contracts",
"materials",
"material_details",
"constructions",
"construction_details",
"contract_settlements",
"contract_settlement_details",
"power",
"power_details",
]
ALL_PROJECT_AGGREGATE_DOMAINS: list[ProjectAggregateDomain] = [
"project",
"project_detail",
"generator_units",
"lar",
"lar_change",
"contract_changes",
"photovoltaic_extension",
"onshore_wind_extension",
"offshore_wind_extension",
"hydropower_extension",
"pumped_storage_extension",
"gas_turbine_extension",
"thermal_power_extension",
"production_list",
"preparation_list",
"contracts",
"materials",
"material_details",
"constructions",
"construction_details",
"contract_settlements",
"contract_settlement_details",
"power",
"power_details",
]
class ProjectAggregateData(BaseModel):
"""按 domain 组织的项目聚合数据。"""
project: ProjectResponseBase | None = Field(None, description="项目基本信息")
project_detail: list[ProjectDetailProject] = Field(default_factory=list, description="项目容量信息")
generator_units: list[GeneratorUnitProject] = Field(default_factory=list, description="发电单元信息")
lar: LarDetailSchema | None = Field(None, description="移民征地信息")
lar_change: list[LarChangeDetailSchema] = Field(default_factory=list, description="移民变更信息")
contract_changes: list[ContractChangeResponse] = Field(default_factory=list, description="合同变更信息")
photovoltaic_extension: PhotovoltaicExtension | None = Field(None, description="光伏项目扩展信息")
onshore_wind_extension: OnShoreWindExtension | None = Field(None, description="陆风项目扩展信息")
offshore_wind_extension: OffShoreWindExtension | None = Field(None, description="海风项目扩展信息")
hydropower_extension: HydropowerExtension | None = Field(None, description="水电项目扩展信息")
pumped_storage_extension: PumpedStorageExtension | None = Field(None, description="抽蓄项目扩展信息")
gas_turbine_extension: GasTurbineExtension | None = Field(None, description="燃气轮机项目扩展信息")
thermal_power_extension: ThermalPowerExtension | None = Field(None, description="火电项目扩展信息")
production_list: list[ProductionDetail] = Field(default_factory=list, description="投产节点列表")
preparation_list: list[PreparationDetail] = Field(default_factory=list, description="里程碑节点列表")
contracts: list[ContractResponse] = Field(default_factory=list, description="合同列表")
materials: list[MaterialResponse] = Field(default_factory=list, description="物资主表列表")
material_details: list[MaterialDetailResponse] = Field(default_factory=list, description="物资明细列表")
constructions: list[ConstructionResponse] = Field(default_factory=list, description="施工主表列表")
construction_details: list[ConstructionDetailResponse] = Field(default_factory=list, description="施工明细列表")
contract_settlements: list[ContractSettlementResponse] = Field(default_factory=list, description="合同结算主表列表")
contract_settlement_details: list[ContractSettlementDetailResponse] = Field(default_factory=list, description="合同结算明细列表")
power: list[PowerResponse] = Field(default_factory=list, description="力能主表列表")
power_details: list[PowerDetail] = Field(default_factory=list, description="力能明细列表")
class ProjectAggregateMeta(BaseModel):
"""聚合请求回显和截断信息。"""
project_id: str = Field(..., description="项目ID")
requested_domains: list[ProjectAggregateDomain] = Field(
default_factory=lambda: list(ALL_PROJECT_AGGREGATE_DOMAINS),
description="本次请求的 domain 列表",
)
domain_limit: int = Field(10000, description="每个 domain 的最大返回条数")
truncated_domains: list[ProjectAggregateDomain] = Field(default_factory=list, description="被截断的 domain 列表")
class ProjectAggregateResponse(BaseModel):
"""项目聚合同步响应。"""
meta: ProjectAggregateMeta = Field(..., description="请求元信息")
data: ProjectAggregateData = Field(
default_factory=lambda: ProjectAggregateData.model_construct(),
description="按 domain 分组的数据",
)
+1
View File
@@ -78,6 +78,7 @@ class ProjectResponseBase(BaseResponseWithID):
# 拓展信息
dbtc_score_list: Optional[list] = Field(default_factory=list, description='达标投产评分列表') # 这里项目用的是list而不是List
approval_status: Optional[str] = Field(None, description="审核状态", examples=["pending"])
+2 -2
View File
@@ -21,8 +21,8 @@ class PostGeneratorUnits(BaseModel):
@field_validator('actual_production_date', mode="before")
def validate_actual_date_not_future(cls, v: str) -> str:
if v == "":
return ""
# if v == "":
# return ""
try:
actual = datetime_date(v)
except ValueError:
+95 -1
View File
@@ -1,8 +1,39 @@
from pydantic import BaseModel, Field
from fastapi import HTTPException
from pydantic import BaseModel, Field, model_validator, field_validator
from typing import List, Optional
from enum import Enum
from ..common.base import BaseResponseWithID
class SupplierType(str, Enum):
"""类型枚举"""
PROJECT = "project" # 项目类型
OTHER = "other" # 其他类型
@classmethod
def supplier_list(cls):
return [cls.PROJECT.value, cls.OTHER.value]
class SupplierNature(str, Enum):
"""
性质枚举
"""
CONSTRUCTION = "construction" # 施工单位
DESIGN = "design" # 设计单位
SUPERVISORY = "supervisory" # 监理单位
SUPPLIER = "supplier" # 供应商
SURVEY = "survey" # 2026.3.11 推送系统测试时发现与项目侧 org_role 不一致,已补充勘察单位
GOVERMENT = "goverment" # 政府
@classmethod
def supplier_type_list(cls):
return [member.value for member in cls]
class SupplierStatus(str, Enum):
"""状态枚举"""
ACTIVE = "active" # 正常
INACTIVE = "inactive" # 禁用
class SupplierDetailResponse(BaseResponseWithID):
"""
供应商详情模型
@@ -41,3 +72,66 @@ class SupplierListResponse(BaseModel):
page_size: int
total: int
list: List[SupplierDetailResponse] = []
class SupplierCreateRequest(BaseModel):
"""
供应商推送创建请求模型
"""
name: str = Field(..., description="供应商名称", examples=["腾讯集团"])
short_name: str = Field(..., description="供应商简称", examples=["腾讯"])
code: str = Field(..., description="供应商编码", examples=["TX001"])
company_type: SupplierType = Field(..., description="供应商类型", examples=["project"])
parent_id: Optional[str] = Field(None, description="上级供应商ID", examples=[None])
status: SupplierStatus = Field(..., description="供应商状态", examples=["active"])
path: Optional[List[str]] = Field(default_factory=list, description="供应商路径 [..., 上级id, 本身id]")
path_name: Optional[List[str]] = Field(default_factory=list, description="供应商路径名称 [..., 上级名称, 本身名称]")
credit_code: str = Field(..., description="供应商信用代码", examples=["914403007109310121"])
region_path: Optional[list] = Field(default_factory=list, description="供应商国家城市地址编码列表, 需对接集团侧地图组件, 没有对接可以不传递", examples=[[
"100000",
"420000",
"420800"
]])
company_nature: List[SupplierNature] = Field(..., description="供应商性质数组 可多选",
examples=[['construction',
'supplier',
'design',
'survey',
'supervisory',
'goverment']])
@model_validator(mode='after')
def validate_company_type_and_nature(self):
data = self.model_dump()
company_nature = data.get("company_nature")
if not company_nature:
raise HTTPException(detail=f"供应商:company_nature为空", status_code=400)
for cn in company_nature:
if cn not in SupplierNature.supplier_type_list():
raise HTTPException(detail=f"供应商:company_nature范围:{SupplierNature.supplier_type_list()}",
status_code=400)
company_type = data.get("company_type")
if not company_type:
raise HTTPException(detail=f"供应商:company_type为空", status_code=400)
if company_type not in SupplierType.supplier_list():
raise HTTPException(
detail=f"供应商:company_type范围({SupplierType.supplier_list()})", status_code=400)
for field in ["name", "short_name", "code", "credit_code", "status"]:
field_value = data.get(field)
if not field_value:
raise HTTPException(detail=f"字段{field}为空", status_code=400)
return self
@field_validator(
"credit_code",
mode="after"
)
def clean_credit_code(cls, field_value):
if field_value is None:
return None
import re
return re.sub(r'\s+', '', str(field_value))
+7 -6
View File
@@ -17,16 +17,17 @@ class UserListDetailItem(BaseResponseWithID):
status: Optional[str] = Field(None, description="状态")
email: Optional[EmailStr] = Field(None, description="邮箱地址")
user_type: str = Field(..., description="用户类型")
last_login_at: Optional[str] = Field(None, description="最后登录时间")
updated_at: Optional[str] = Field(None, description="更新时间")
# last_login_at: Optional[str] = Field(None, description="最后登录时间")
# updated_at: Optional[str] = Field(None, description="更新时间")
company_id: Optional[str] = Field(None, description="公司id")
company_name: Optional[str] = Field(None, description="公司名称")
created_at: Optional[str] = Field(None, description="创建时间")
approval_status: Optional[str] = Field(None, description="审批状态")
approval_time: Optional[str] = Field(None, description="审批通过时间")
last_login_ip: Optional[str] = Field(None, description="最后登录ip")
# created_at: Optional[str] = Field(None, description="创建时间")
# approval_status: Optional[str] = Field(None, description="审批状态") # 不再区分审批状态和审批时间
# approval_time: Optional[str] = Field(None, description="审批通过时间")
# last_login_ip: Optional[str] = Field(None, description="最后登录ip")
project_ids: Optional[List[str]] = Field([], description="权限内项目id")
contract_ids: Optional[List[str]] = Field([], description="权限内合同id")
phone_hash: Optional[str] = Field(None, description="手机号hash")
apply_files: Optional[List[Dict]] = Field([],
examples=[
{"id": "file:001", "name": "文件1",