This commit is contained in:
lhx
2026-02-12 14:49:47 +08:00
5 changed files with 458 additions and 192 deletions

View File

@@ -9,7 +9,12 @@ from ..schemas.account import (
AccountApiResponse, AccountListResponse,AccountGetRequestYH AccountApiResponse, AccountListResponse,AccountGetRequestYH
) )
from ..services.account import AccountService from ..services.account import AccountService
import json
from typing import List, Optional
from urllib.error import HTTPError, URLError
from urllib import request as urllib_request
from urllib.parse import urlencode
from socket import timeout
router = APIRouter(prefix="/accounts", tags=["账号管理"]) router = APIRouter(prefix="/accounts", tags=["账号管理"])
@router.post("/create", response_model=AccountApiResponse, status_code=status.HTTP_201_CREATED) @router.post("/create", response_model=AccountApiResponse, status_code=status.HTTP_201_CREATED)
@@ -112,6 +117,7 @@ def update_account(request: AccountUpdateRequest, db: Session = Depends(get_db))
@router.post("/delete", response_model=AccountApiResponse) @router.post("/delete", response_model=AccountApiResponse)
def delete_account(request: AccountDeleteRequest, db: Session = Depends(get_db)): def delete_account(request: AccountDeleteRequest, db: Session = Depends(get_db)):
"""删除账号""" """删除账号"""
if not AccountService.delete_account(db, request.account_id): if not AccountService.delete_account(db, request.account_id):
return AccountApiResponse( return AccountApiResponse(
@@ -124,3 +130,101 @@ def delete_account(request: AccountDeleteRequest, db: Session = Depends(get_db))
message="账号删除成功", message="账号删除成功",
data=None data=None
) )
# 获取今日上传的数据接口
@router.post("/get_uplaod_data", response_model=AccountListResponse)
def get_uplaod_data(request: AccountGetRequest, db: Session = Depends(get_db)):
"""根据多种条件查询账号,并合并外部接口的 is_ok 字段(仅返回 today_data 中存在的账号)"""
# 1. 从数据库查询账号列表(原有逻辑不变)
accounts = AccountService.search_accounts(
db,
account_id=request.account_id,
username=request.username,
project_name=request.project_name,
status=request.status,
today_updated=request.today_updated,
yh_id=request.yh_id,
cl_name=request.cl_name
)
# 2. 调用外部接口构建 user_is_ok_map替换为 urllib 实现,修复命名冲突和超时异常)
user_is_ok_map = {}
api_url = "https://engineering.yuxindazhineng.com/index/index/get_over_data"
payload = {"user_id": "68c0dbfdb7cbcd616e7c5ab5"}
try:
# 步骤1将表单 payload 转为 URL 编码的字节流urllib POST 要求数据为字节流)
payload_bytes = urlencode(payload).encode("utf-8")
# 步骤2构造 Request 对象(使用重命名后的 urllib_request避免冲突
req = urllib_request.Request(
url=api_url,
data=payload_bytes,
method="POST" # 显式指定 POST 方法
)
# 步骤3发送请求并读取响应设置 10 秒超时,避免接口挂起)
with urllib_request.urlopen(req, timeout=10) as resp:
# 读取响应字节流并解码为 UTF-8 字符串
response_str = resp.read().decode("utf-8")
# 步骤4将 JSON 字符串解析为字典
api_response = json.loads(response_str)
# 步骤5验证接口返回格式并构建 user_is_ok_map 映射
if api_response.get("code") == 0:
today_data = api_response.get("data", []) # 给默认值,避免 KeyError
for item in today_data:
# 安全获取字段并校验类型
if 'user_name' in item and 'is_ok' in item:
user_is_ok_map[item['user_name']] = item['is_ok']
except HTTPError as e:
# 捕获 HTTP 状态码错误4xx/5xx
print(f"外部接口 HTTP 错误:{e.code} - {e.reason}")
except TimeoutError: # 修复:正确的超时异常名称(首字母大写,原 timeout 会未定义报错)
# 捕获请求超时异常
print(f"外部接口调用超时(超过 10 秒)")
except URLError as e:
# 捕获 URL 解析错误、网络连接错误等
print(f"外部接口网络错误:{e.reason}")
except json.JSONDecodeError:
# 捕获非合法 JSON 格式响应
print(f"外部接口返回数据格式错误,非合法 JSON 字符串")
except Exception as e:
# 捕获其他未知异常
print(f"外部接口处理未知异常:{str(e)}")
# 3. 关键修改:仅保留 today_data 中存在的账号(核心过滤逻辑)
accounts_with_is_ok = []
for account in accounts:
# 步骤1将 AccountResponse 对象转为字典Pydantic 模型自带 dict() 方法)
account_dict = account.dict()
# 步骤2获取当前账号 username判断是否在 user_is_ok_map 中(不存在则跳过)
current_username = account_dict.get("username", "")
if current_username not in user_is_ok_map:
continue # 核心:过滤掉 today_data 中没有的账号
# 步骤3给字典添加 is_ok 字段(仅处理存在的账号,无需默认值 0
account_dict['is_ok'] = user_is_ok_map[current_username]
# 步骤4将处理后的字典加入新列表
accounts_with_is_ok.append(account_dict)
# 4. 处理空结果返回(原有逻辑不变)
if not accounts_with_is_ok:
return AccountListResponse(
code=ResponseCode.ACCOUNT_NOT_FOUND,
message=ResponseMessage.ACCOUNT_NOT_FOUND,
total=0,
data=[]
)
# 5. 正常返回:数据传入新列表 accounts_with_is_ok仅包含 today_data 中的账号)
# print(accounts_with_is_ok)
return AccountListResponse(
code=ResponseCode.SUCCESS,
message="查询成功",
total=len(accounts_with_is_ok),
data=accounts_with_is_ok
)

View File

@@ -17,6 +17,10 @@ class Account(Base):
max_variation = Column(Integer, default=1, comment="变化量的绝对值,单位是毫米") max_variation = Column(Integer, default=1, comment="变化量的绝对值,单位是毫米")
yh_id = Column(String(1000), comment="宇恒一号用户id") yh_id = Column(String(1000), comment="宇恒一号用户id")
cl_name = Column(String(100), nullable=True, comment="测量人员") cl_name = Column(String(100), nullable=True, comment="测量人员")
device_name = Column(String(1000), comment="设备名称")
device_port = Column(String(1000), comment="设备端口")
device_ip = Column(String(1000), comment="设备局域网内ip地址")
is_ok = Column(Integer, default=0, comment="是否可以上传")
# 模型转字典 # 模型转字典

View File

@@ -12,6 +12,10 @@ class AccountBase(BaseModel):
max_variation: Optional[int] = None max_variation: Optional[int] = None
yh_id: Optional[str] = None yh_id: Optional[str] = None
cl_name: Optional[str] = None cl_name: Optional[str] = None
device_name: Optional[str] = None
device_port: Optional[str] = None
device_ip: Optional[str] = None
is_ok: Optional[int] = None
class AccountCreate(AccountBase): class AccountCreate(AccountBase):
pass pass
@@ -24,6 +28,10 @@ class AccountUpdate(BaseModel):
project_name: Optional[str] = None project_name: Optional[str] = None
update_time: Optional[str] = None update_time: Optional[str] = None
cl_name: Optional[str] = None cl_name: Optional[str] = None
device_name: Optional[str] = None
device_port: Optional[str] = None
device_ip: Optional[str] = None
is_ok: Optional[int] = None
class AccountResponse(AccountBase): class AccountResponse(AccountBase):
account_id: int account_id: int
@@ -49,7 +57,11 @@ class AccountResponse(AccountBase):
update_time=account.update_time, update_time=account.update_time,
max_variation=account.max_variation, max_variation=account.max_variation,
yh_id=account.yh_id, yh_id=account.yh_id,
cl_name=account.cl_name cl_name=account.cl_name,
device_name=account.device_name,
device_port=account.device_port,
device_ip=account.device_ip,
is_ok = account.is_ok
) )
class AccountListRequest(BaseModel): class AccountListRequest(BaseModel):
@@ -91,3 +103,4 @@ class AccountListResponse(BaseModel):
message: str message: str
total: int total: int
data: List[AccountResponse] = [] data: List[AccountResponse] = []

View File

@@ -11,7 +11,12 @@ class ConstructionMonitorUtils:
def __init__(self): def __init__(self):
# 原始工况周期映射表(保持不变) # 原始工况周期映射表(保持不变)
self.base_periods = { self.base_periods = {
"路基或预压土填筑,连续填筑":1,
"路基或预压土填筑,两次填筑间隔时间较长":7,
"预压土或路基填筑完成第1~3个月":7,
"预压土或路基填筑完成第4~6个月":14,
"仰拱底板施工完成后第1个月": 7, "仰拱底板施工完成后第1个月": 7,
"预压土或路基填筑完成6个月以后":30,
"仰拱底板施工完成后第2至3个月": 14, "仰拱底板施工完成后第2至3个月": 14,
"仰拱底板施工完成后3个月以后": 30, "仰拱底板施工完成后3个月以后": 30,
"仰拱(底板)施工完成后第1个月": 7, # 原:仰拱(底板)施工完成后,第1个月 "仰拱(底板)施工完成后第1个月": 7, # 原:仰拱(底板)施工完成后,第1个月
@@ -27,7 +32,7 @@ class ConstructionMonitorUtils:
"预制梁桥,预制梁架设后": 7, # 原:预制梁桥,预制梁架设后 "预制梁桥,预制梁架设后": 7, # 原:预制梁桥,预制梁架设后
"桥位施工桥梁,制梁前": 30, # 原:桥位施工桥梁,制梁前 "桥位施工桥梁,制梁前": 30, # 原:桥位施工桥梁,制梁前
"桥位施工桥梁,上部结构施工中": 1, # 原:桥位施工桥梁,上部结构施工中 "桥位施工桥梁,上部结构施工中": 1, # 原:桥位施工桥梁,上部结构施工中
"架桥机(运梁车)通过": 7, # 无格式差异,保留原样 # "架桥机(运梁车)通过": 7, # 无格式差异,保留原样
"桥梁主体工程完工后,第1至3个月": 7, # 原:桥梁主体工程完工后,第1至3个月 "桥梁主体工程完工后,第1至3个月": 7, # 原:桥梁主体工程完工后,第1至3个月
"桥梁主体工程完工后第4至6个月": 14, # 原:桥梁主体工程完工后,第4至6个月 "桥梁主体工程完工后第4至6个月": 14, # 原:桥梁主体工程完工后,第4至6个月
"桥梁主体工程完工后,6个月以后": 30, # 原:桥梁主体工程完工后,6个月以后 '' "桥梁主体工程完工后,6个月以后": 30, # 原:桥梁主体工程完工后,6个月以后 ''
@@ -50,7 +55,40 @@ class ConstructionMonitorUtils:
"架桥机(运梁车) 首次通过后": 7, # 原:架桥机(运梁车)首次通过后(仅加空格) "架桥机(运梁车) 首次通过后": 7, # 原:架桥机(运梁车)首次通过后(仅加空格)
"轨道板(道床)铺设后第1个月": 14, # 原:轨道板(道床)铺设后,第1个月 "轨道板(道床)铺设后第1个月": 14, # 原:轨道板(道床)铺设后,第1个月
"轨道板(道床)铺设后第2至3个月": 30, # 原:轨道板(道床)铺设后,第2至3个月 "轨道板(道床)铺设后第2至3个月": 30, # 原:轨道板(道床)铺设后,第2至3个月
"轨道板(道床)铺设后3个月以后": 90 # 未出现在待处理集,保留原始格式 "轨道板(道床)铺设后3个月以后": 90,
"架桥机(运梁车)首次通过前": 1,
"架桥机(运梁车)首次通过后前3天": 1,
"架桥机(运梁车)首次通过后": 7,
"轨道板铺设前": 14,
"轨道板(道床)铺设后第1至3个月": 14,
"轨道板(道床)铺设后第4至6个月": 30,
"轨道板(道床)铺设后6个月以后": 90,
"站场填方路基段填筑完成至静态验收": 14,
"桥墩(台)地面处拆模后": 30,
"敦身混凝土施工": 30,
# "预制梁桥,架梁前": 30,
# "预制梁桥,预制梁架设前": 1,
"预制梁桥预制梁架设后": 7,
"现浇梁,浇筑前": 30,
"现浇梁上部结构施工中": 1,
"架桥机(运梁车)通过": 2,
"桥梁主体工程完工后第1至3个月": 7,
# "桥梁主体工程完工后第4至6个月": 14,
"侨梁主体工程完工后6个月以后": 30,
"轨道铺设,前": 30,
"轨道铺设,后": 14,
# "轨道铺设完成后第1个月": 14,
# "轨道铺设完成后2至3个月": 30,
# "轨道铺设完成后4至12个月": 90,
# "轨道铺设完成后12个月以后": 180,
# "仰拱(底板)施工完成后第1个月": 7,
# "仰拱(底板)施工完成后第2至3个月": 14,
# "仰拱(底板)施工完成后3个月以后": 30,
# "轨道板铺设前": 14,
# "无砟轨道铺设后第1至3个月": 30,
# "无砟轨道铺设后4至12个月": 90,
# "无砟轨道铺设后12个月以后": 180,
"特殊地段隧道施工完成后至静态验收": 14
} }
# 构建中英文括号+逗号兼容映射表 # 构建中英文括号+逗号兼容映射表
self.compatible_periods = self._build_compatible_brackets_map() self.compatible_periods = self._build_compatible_brackets_map()

View File

@@ -7,145 +7,246 @@ class OperatingModePredictor:
特性: 特性:
1. 输入为二维列表每个内嵌列表对应一个point_id且为倒序排列最新记录在索引0 1. 输入为二维列表每个内嵌列表对应一个point_id且为倒序排列最新记录在索引0
2. 输出为一维列表,仅保留每个内嵌列表的最新记录,新增工况推导结果字段 2. 输出为一维列表,仅保留每个内嵌列表的最新记录,新增工况推导结果字段
3. 无需切换工况时next_workinfo返回当前工况名称需要切换时返回目标工况名称 3. 推导规则有等效映射返回新工况无等效保留旧工况切换下一工况以base_periods为准
4. 时间计算仅按日期(天)维度,忽略时分秒 4. 时间计算仅按日期(天)维度,忽略时分秒
5. 冬休场景:仅以冬休前上一有效工况为判断基准,切换规则与非冬休完全一致
6. 适配中英文括号、逗号、空格,内部标准化匹配,外部返回规范名称
""" """
def __init__(self): def __init__(self):
"""初始化类,加载内置的工况配置、分组规则和切换触发规则""" """初始化类,加载核心配置"""
# 基础工况配置(键:工况名称,值:基础监测周期参考 # 1. 基础工况配置(最终返回的规范名称,含所有新旧工况
self.base_periods = self._load_base_periods() self.base_periods = self._load_base_periods()
# 工况分组(将同义不同格式的工况归类,共用切换规则 # 2. 旧→新等效映射(优先返回新工况
self.old_to_new_map = self._load_old_to_new_map()
# 3. 工况分组(同义工况归为同一分组,复用切换规则)
self.condition_group = self._load_condition_group() self.condition_group = self._load_condition_group()
# 工况切换触发规则(分组ID触发天数+目标工况候选 # 4. 切换触发规则(沿用原逻辑,触发天数+目标工况)
self.group_transition_rules = self._load_group_transition_rules() self.transition_rules = self._load_transition_rules()
# 5. 冬休标识
self.winter_break_labels = {"冬休"}
# 辅助映射标准化名称→base_periods中的规范名称用于最终返回
self.std_to_canonical = {
self._standardize_name(name): name for name in self.base_periods.keys()
}
# 辅助映射标准化名称→分组ID
self.std_to_group = {
self._standardize_name(name): group_id
for name, group_id in self.condition_group.items()
}
def _standardize_name(self, name):
"""
标准化工况名称(内部匹配用):去空格、统一中英文符号
:param name: 原始工况名称
:return: 标准化后的名称
"""
if not name:
return ""
# 去所有空格
std_name = name.replace(" ", "").strip()
# 统一中英文符号
replace_map = {
"": "(", "": ")", "": ",", "": ",", "~": "",
"": ";", "": ":", "": "'", "": "\""
}
for old, new in replace_map.items():
std_name = std_name.replace(old, new)
return std_name
def _load_base_periods(self): def _load_base_periods(self):
"""加载基础工况配置(私有方法,内部使用""" """加载基础工况配置(最终返回的规范名称,无多余数字"""
return { return {
"仰拱底板施工完成后第1个月": 7, # 路基工况(新工况优先)
"仰拱底板施工完成后第2至3个月": 14, "路基或预压土填筑,连续填筑": 1,
"仰拱底板施工完成后3个月以后": 30, "路基或预压土填筑,两次填筑间隔时间较长": 7,
"预压土或路基填筑完成第1~3个月": 7,
"预压土或路基填筑完成第4~6个月": 14,
"预压土或路基填筑完成6个月以后": 30,
"架桥机(运梁车)首次通过前": 1,
"架桥机(运梁车)首次通过后前3天": 1,
"架桥机(运梁车)首次通过后": 7,
"轨道板(道床)铺设后第1至3个月": 14,
"轨道板(道床)铺设后第4至6个月": 30,
"轨道板(道床)铺设后6个月以后": 90,
# 路基旧工况(保留,无等效则返回)
"填筑或堆载,一般情况": 1,
"填筑或堆载,两次填筑间隔时间较长情况": 7,
"堆载预压或路基填筑完成6个月以后": 30,
"轨道板(道床)铺设后第1个月": 14,
"轨道板(道床)铺设后第2至3个月": 30,
"轨道板(道床)铺设后3个月以后": 90,
# 桥梁工况(新工况优先)
"桥墩(台)地面处拆模后": 30,
"敦身混凝土施工": 30,
"预制梁桥,预制梁架设前": 1,
"预制梁桥,预制梁架设后": 7,
"现浇梁,浇筑前": 30,
"现浇梁上部结构施工中": 1,
"架桥机(运梁车)通过": 2,
"桥梁主体工程完工后第1至3个月": 7,
"桥梁主体工程完工后第4至6个月": 14,
"桥梁主体工程完工后6个月以后": 30,
"轨道铺设,前": 30,
"轨道铺设,后": 14,
"轨道铺设完成后第1个月": 14,
"轨道铺设完成后2至3个月": 30,
"轨道铺设完成后4至12个月": 90,
"轨道铺设完成后12个月以后": 180,
# 桥梁旧工况(保留,无等效则返回)
"墩台施工到一定高度": 30,
"墩台混凝土施工": 30,
"预制梁桥,架梁前": 30,
"桥位施工桥梁,制梁前": 30,
"桥位施工桥梁,上部结构施工中": 1,
"桥梁主体工程完工后,第1至3个月": 7,
"轨道铺设期间,前": 30,
"轨道铺设期间,后": 14,
# 隧道工况
"仰拱(底板)施工完成后第1个月": 7, "仰拱(底板)施工完成后第1个月": 7,
"仰拱(底板)施工完成后第2至3个月": 14, "仰拱(底板)施工完成后第2至3个月": 14,
"仰拱(底板)施工完成后3个月以后": 30, "仰拱(底板)施工完成后3个月以后": 30,
"无砟轨道铺设后第1至3个月": 30, "无砟轨道铺设后第1至3个月": 30,
"无砟轨道铺设后4至12个月": 90, "无砟轨道铺设后4至12个月": 90,
"无砟轨道铺设后12个月以后": 180, "无砟轨道铺设后12个月以后": 180,
"墩台施工到一定高度": 30,
"墩台混凝土施工": 30, # 特殊工况
"预制梁桥,架梁前": 30, "冬休": 0
"预制梁桥,预制梁架设前": 1, }
"预制梁桥,预制梁架设后": 7,
"桥位施工桥梁,制梁前": 30, def _load_old_to_new_map(self):
"桥位施工桥梁,上部结构施工中": 1, """加载旧→新等效映射(优先返回新工况)"""
"架桥机(运梁车)通过": 7, return {
"桥梁主体工程完工后,第1至3个月": 7, # 路基等效
"桥梁主体工程完工后第4至6个月": 14, "填筑或堆载,一般情况": "路基或预压土填筑,连续填筑",
"桥梁主体工程完工后,6个月以后": 30, "填筑或堆载,两次填筑间隔时间较长情况": "路基或预压土填筑,两次填筑间隔时间较长",
"轨道铺设期间,前": 30, "预压土或路基填筑完成。第1~3个月": "预压土或路基填筑完成第1~3个月",
"轨道铺设期间,后": 14, "堆载预压或路基填筑完成6个月以后": "预压土或路基填筑完成6个月以后",
"轨道铺设完成第1个月": 14, "轨道板(道床)铺设后第1个月": "轨道板(道床)铺设后第1至3个月",
"轨道铺设完成2至3个月": 30, "轨道板(道床)铺设后,2至3个月": "轨道板(道床)铺设后第4至6个月",
"轨道铺设完成后4至12个月": 90, "轨道板(道床)铺设后3个月以后": "轨道板(道床)铺设后6个月以后",
"轨道铺设完成后12个月以后": 180,
"铺路或堆载,一般情况": 1, # 桥梁等效
"填筑或堆载,一般情况": 1, "墩台施工到一定高度": "桥墩(台)地面处拆模后",
"铺路或堆载,沉降量突变情况": 1, "墩台混凝土施工": "敦身混凝土施工",
"填筑或堆载,两次填筑间隔时间较长情况": 3, "桥位施工桥梁,制梁前": "现浇梁,浇筑前",
"铺路或堆载,两次铺路间隔时间较长情况": 3, "桥位施工桥梁,上部结构施工中": "现浇梁上部结构施工中",
"堆载预压或路基填筑完成,第1至3个月": 7, "桥梁主体工程完工后,第1至3个月": "桥梁主体工程完工后第1至3个月",
"堆载预压或路基填筑完成第4至6个月": 14, "轨道铺设期间,前": "轨道铺设,前",
"堆载预压或路基填筑完成6个月以后": 30, "轨道铺设期间,后": "轨道铺设,后"
"架桥机(运梁车) 首次通过前": 1,
"架桥机(运梁车) 首次通过后前3天": 1,
"架桥机(运梁车) 首次通过后": 7,
"轨道板(道床)铺设后第1个月": 14,
"轨道板(道床)铺设后第2至3个月": 30,
"轨道板(道床)铺设后3个月以后": 90
} }
def _load_condition_group(self): def _load_condition_group(self):
"""加载工况分组规则(私有方法,内部使用""" """加载工况分组(同义工况归为同一分组"""
return { group_map = {
"仰拱底板施工完成后第1个月": "YG_DIBAN_1", # 路基分组
"仰拱(底板)施工完成后第1个月": "YG_DIBAN_1", "路基或预压土填筑,连续填筑": "DZ_CONTINUE",
"仰拱底板施工完成后第2至3个月": "YG_DIBAN_2_3", "路基或预压土填筑,两次填筑间隔时间较长": "DZ_INTERVAL",
"仰拱(底板)施工完成,第2至3个月": "YG_DIBAN_2_3", "预压土或路基填筑完成,第1~3个月": "DZ_FINISH_1_3",
"仰拱底板施工完成后3个月以后": "YG_DIBAN_AFTER_3", "预压土或路基填筑完成第4~6个月": "DZ_FINISH_4_6",
"仰拱(底板)施工完成后3个月以后": "YG_DIBAN_AFTER_3", "预压土或路基填筑完成6个月以后": "DZ_FINISH_AFTER_6",
"架桥机(运梁车)首次通过前": "JQJ_FIRST_BEFORE", "架桥机(运梁车)首次通过前": "JQJ_FIRST_BEFORE",
"架桥机(运梁车)首次通过后前3天": "JQJ_FIRST_AFTER_3D", "架桥机(运梁车)首次通过后前3天": "JQJ_FIRST_AFTER_3D",
"架桥机(运梁车)首次通过后": "JQJ_FIRST_AFTER", "架桥机(运梁车)首次通过后": "JQJ_FIRST_AFTER",
"堆载预压或路基填筑完成第1至3个月": "DZYY_1_3", "轨道板(道床)铺设后第1至3个月": "GDB_FINISH_1_3",
"堆载预压或路基填筑完成第4至6个月": "DZYY_4_6", "轨道板(道床)铺设后第4至6个月": "GDB_FINISH_4_6",
"堆载预压或路基填筑完成6个月以后": "DZYY_AFTER_6", "轨道板(道床)铺设后6个月以后": "GDB_FINISH_AFTER_6",
"轨道板(道床)铺设后第1个月": "GDB_1",
"轨道板(道床)铺设后第2至3个月": "GDB_2_3", # 路基旧工况分组(复用新工况分组)
"轨道板(道床)铺设后3个月以后": "GDB_AFTER_3", "填筑或堆载,一般情况": "DZ_CONTINUE",
"填筑或堆载,两次填筑间隔时间较长情况": "DZ_INTERVAL",
"堆载预压或路基填筑完成6个月以后": "DZ_FINISH_AFTER_6",
"轨道板(道床)铺设后第1个月": "GDB_FINISH_1_3",
"轨道板(道床)铺设后第2至3个月": "GDB_FINISH_4_6",
"轨道板(道床)铺设后3个月以后": "GDB_FINISH_AFTER_6",
# 桥梁分组
"桥墩(台)地面处拆模后": "STATIC",
"敦身混凝土施工": "STATIC",
"预制梁桥,架梁前": "STATIC",
"预制梁桥,预制梁架设前": "YZLQ_BEFORE_JS", "预制梁桥,预制梁架设前": "YZLQ_BEFORE_JS",
"预制梁桥,预制梁架设后": "YZLQ_AFTER_JS", "预制梁桥,预制梁架设后": "YZLQ_AFTER_JS",
"桥梁主体工程完工后,第1至3个月": "QL_ZHUTI_1_3", "现浇梁,浇筑前": "STATIC",
"现浇梁上部结构施工中": "STATIC",
"架桥机(运梁车)通过": "STATIC",
"桥梁主体工程完工后第1至3个月": "QL_ZHUTI_1_3",
"桥梁主体工程完工后第4至6个月": "QL_ZHUTI_4_6", "桥梁主体工程完工后第4至6个月": "QL_ZHUTI_4_6",
"桥梁主体工程完工后,6个月以后": "QL_ZHUTI_AFTER_6", "桥梁主体工程完工后6个月以后": "QL_ZHUTI_AFTER_6",
"轨道铺设完成后第1个月": "GD_1", "轨道铺设,前": "STATIC",
"轨道铺设完成后2至3个月": "GD_2_3", "轨道铺设,后": "STATIC",
"轨道铺设完成后,4至12个月": "GD_4_12", "轨道铺设完成后,第1个月": "GD_FINISH_1",
"轨道铺设完成后,12个月以后": "GD_AFTER_12", "轨道铺设完成后,2至3个月": "GD_FINISH_2_3",
"轨道铺设完成后4至12个月": "GD_FINISH_4_12",
"轨道铺设完成后12个月以后": "GD_FINISH_AFTER_12",
# 桥梁旧工况分组(复用新工况分组)
"墩台施工到一定高度": "STATIC",
"墩台混凝土施工": "STATIC",
"桥位施工桥梁,制梁前": "STATIC",
"桥位施工桥梁,上部结构施工中": "STATIC",
"桥梁主体工程完工后,第1至3个月": "QL_ZHUTI_1_3",
"轨道铺设期间,前": "STATIC",
"轨道铺设期间,后": "STATIC",
# 隧道分组
"仰拱(底板)施工完成后第1个月": "YG_DIBAN_1",
"仰拱(底板)施工完成后第2至3个月": "YG_DIBAN_2_3",
"仰拱(底板)施工完成后3个月以后": "YG_DIBAN_AFTER_3",
"无砟轨道铺设后第1至3个月": "WZGD_1_3", "无砟轨道铺设后第1至3个月": "WZGD_1_3",
"无砟轨道铺设后4至12个月": "WZGD_4_12", "无砟轨道铺设后4至12个月": "WZGD_4_12",
"无砟轨道铺设后12个月以后": "WZGD_AFTER_12", "无砟轨道铺设后12个月以后": "WZGD_AFTER_12",
"墩台施工到一定高度": "STATIC",
"墩台混凝土施工": "STATIC",
"预制梁桥,架梁前": "STATIC",
"桥位施工桥梁,制梁前": "STATIC",
"桥位施工桥梁,上部结构施工中": "STATIC",
"架桥机(运梁车)通过": "STATIC",
"轨道铺设期间,前": "STATIC",
"轨道铺设期间,后": "STATIC",
"铺路或堆载,一般情况": "STATIC",
"填筑或堆载,一般情况": "STATIC",
"铺路或堆载,沉降量突变情况": "STATIC",
"填筑或堆载,两次填筑间隔时间较长情况": "STATIC",
"铺路或堆载,两次铺路间隔时间较长情况": "STATIC"
}
def _load_group_transition_rules(self): # 特殊工况
"""加载工况切换触发规则(私有方法,内部使用)""" "冬休": "STATIC"
}
return group_map
def _load_transition_rules(self):
"""加载切换触发规则沿用原逻辑以base_periods为准"""
return { return {
"YG_DIBAN_1": {"trigger_days": 30, "next_candidates": ["仰拱底板施工完成后第2至3个月", "仰拱(底板)施工完成后第2至3个月"]}, # 路基切换规则
"YG_DIBAN_2_3": {"trigger_days": 60, "next_candidates": ["仰拱底板施工完成后3个月以后", "仰拱(底板)施工完成后3个月以后"]}, "DZ_FINISH_1_3": {"trigger_days": 90, "next": ["预压土或路基填筑完成第4~6个月"]},
"YG_DIBAN_AFTER_3": {"trigger_days": None, "next_candidates": None}, "DZ_FINISH_4_6": {"trigger_days": 90, "next": ["预压土或路基填筑完成6个月以后"]},
"JQJ_FIRST_BEFORE": {"trigger_days": 1, "next_candidates": ["架桥机(运梁车) 首次通过后前3天"]}, "DZ_FINISH_AFTER_6": {"trigger_days": None, "next": None},
"JQJ_FIRST_AFTER_3D": {"trigger_days": 3, "next_candidates": ["架桥机(运梁车) 首次通过后"]}, "JQJ_FIRST_BEFORE": {"trigger_days": 1, "next": ["架桥机(运梁车)首次通过后前3天"]},
"JQJ_FIRST_AFTER": {"trigger_days": None, "next_candidates": None}, "JQJ_FIRST_AFTER_3D": {"trigger_days": 3, "next": ["架桥机(运梁车)首次通过后"]},
"DZYY_1_3": {"trigger_days": 90, "next_candidates": ["堆载预压或路基填筑完成第4至6个月"]}, "JQJ_FIRST_AFTER": {"trigger_days": None, "next": None},
"DZYY_4_6": {"trigger_days": 90, "next_candidates": ["堆载预压或路基填筑完成,6个月以后"]}, "GDB_FINISH_1_3": {"trigger_days": 30, "next": ["轨道板(道床)铺设后第4至6个月"]},
"DZYY_AFTER_6": {"trigger_days": None, "next_candidates": None}, "GDB_FINISH_4_6": {"trigger_days": 30, "next": ["轨道板(道床)铺设后6个月以后"]},
"GDB_1": {"trigger_days": 30, "next_candidates": ["轨道板(道床)铺设后第2至3个月"]}, "GDB_FINISH_AFTER_6": {"trigger_days": None, "next": None},
"GDB_2_3": {"trigger_days": 30, "next_candidates": ["轨道板(道床)铺设后3个月以后"]},
"GDB_AFTER_3": {"trigger_days": None, "next_candidates": None}, # 桥梁切换规则
"YZLQ_BEFORE_JS": {"trigger_days": 1, "next_candidates": ["架桥机(运梁车)通过"]}, "YZLQ_BEFORE_JS": {"trigger_days": 1, "next": ["架桥机(运梁车)通过"]},
"YZLQ_AFTER_JS": {"trigger_days": 7, "next_candidates": ["桥梁主体工程完工后,第1至3个月"]}, "YZLQ_AFTER_JS": {"trigger_days": 7, "next": ["桥梁主体工程完工后第1至3个月"]},
"QL_ZHUTI_1_3": {"trigger_days": 90, "next_candidates": ["桥梁主体工程完工后第4至6个月"]}, "QL_ZHUTI_1_3": {"trigger_days": 90, "next": ["桥梁主体工程完工后第4至6个月"]},
"QL_ZHUTI_4_6": {"trigger_days": 90, "next_candidates": ["桥梁主体工程完工后,6个月以后"]}, "QL_ZHUTI_4_6": {"trigger_days": 90, "next": ["桥梁主体工程完工后6个月以后"]},
"QL_ZHUTI_AFTER_6": {"trigger_days": None, "next_candidates": None}, "QL_ZHUTI_AFTER_6": {"trigger_days": None, "next": None},
"GD_1": {"trigger_days": 30, "next_candidates": ["轨道铺设完成后2至3个月"]}, "GD_FINISH_1": {"trigger_days": 30, "next": ["轨道铺设完成后2至3个月"]},
"GD_2_3": {"trigger_days": 60, "next_candidates": ["轨道铺设完成后4至12个月"]}, "GD_FINISH_2_3": {"trigger_days": 60, "next": ["轨道铺设完成后4至12个月"]},
"GD_4_12": {"trigger_days": 240, "next_candidates": ["轨道铺设完成后12个月以后"]}, "GD_FINISH_4_12": {"trigger_days": 240, "next": ["轨道铺设完成后12个月以后"]},
"GD_AFTER_12": {"trigger_days": None, "next_candidates": None}, "GD_FINISH_AFTER_12": {"trigger_days": None, "next": None},
"WZGD_1_3": {"trigger_days": 90, "next_candidates": ["无砟轨道铺设后4至12个月"]},
"WZGD_4_12": {"trigger_days": 240, "next_candidates": ["无砟轨道铺设后12个月以后"]}, # 隧道切换规则
"WZGD_AFTER_12": {"trigger_days": None, "next_candidates": None}, "YG_DIBAN_1": {"trigger_days": 30, "next": ["仰拱(底板)施工完成后第2至3个月"]},
"STATIC": {"trigger_days": None, "next_candidates": None} "YG_DIBAN_2_3": {"trigger_days": 60, "next": ["仰拱(底板)施工完成后3个月以后"]},
"YG_DIBAN_AFTER_3": {"trigger_days": None, "next": None},
"WZGD_1_3": {"trigger_days": 90, "next": ["无砟轨道铺设后4至12个月"]},
"WZGD_4_12": {"trigger_days": 240, "next": ["无砟轨道铺设后12个月以后"]},
"WZGD_AFTER_12": {"trigger_days": None, "next": None},
# 静态分组(无切换)
"DZ_CONTINUE": {"trigger_days": None, "next": None},
"DZ_INTERVAL": {"trigger_days": None, "next": None},
"STATIC": {"trigger_days": None, "next": None}
} }
def _parse_to_date(self, time_str): def _parse_to_date(self, time_str):
""" """解析时间字符串为date对象仅保留年月日"""
私有辅助方法:将时间字符串解析为日期对象,仅保留年月日,忽略时分秒
:param time_str: 时间字符串,格式为 "YYYY-MM-DD HH:MM:SS"
:return: date对象 / None解析失败时
"""
if not time_str: if not time_str:
return None return None
try: try:
@@ -154,66 +255,49 @@ class OperatingModePredictor:
except ValueError: except ValueError:
return None return None
def _get_time_statistics_from_reversed(self, data, workinfo): def _get_time_statistics(self, data, target_workinfo):
""" """从倒序数据中提取目标工况的时间统计信息"""
私有辅助方法:从倒序数据中提取时间统计信息 # 筛选目标工况的有效记录
:param data: 单个point_id的倒序数据列表 target_records = [
:param workinfo: 当前工况名称 d for d in data
:return: 元组 (首次测量日期date对象, 首次到末次持续天数, 今日与首次测量天数差) if self._standardize_name(d.get("workinfoname")) == self._standardize_name(target_workinfo)
""" and d.get("workinfoname") not in self.winter_break_labels
# 筛选当前工况的有效记录 ]
reversed_records = [d for d in data if d.get("workinfoname") == workinfo] if not target_records:
if not reversed_records:
return None, 0, 0 return None, 0, 0
# 解析所有有效记录的日期 # 解析日期
reversed_dates = [] target_dates = []
for item in reversed_records: for item in target_records:
d = self._parse_to_date(item.get("MTIME_W")) d = self._parse_to_date(item.get("MTIME_W"))
if d: if d:
reversed_dates.append(d) target_dates.append(d)
if not reversed_dates: if not target_dates:
return None, 0, 0 return None, 0, 0
# 提取倒序数据最新、最旧日期 # 计算时间差(倒序数据最新在0最早在-1
last_date = reversed_dates[0] last_date = target_dates[0]
first_date = reversed_dates[-1] first_date = target_dates[-1]
# 计算累计天数和今日与首次测量的天数差
cumulative_days = (last_date - first_date).days cumulative_days = (last_date - first_date).days
today = date.today() today = date.today()
days_to_today = (today - first_date).days if first_date else 0 days_to_today = (today - first_date).days if first_date else 0
return first_date, cumulative_days, days_to_today return first_date, cumulative_days, days_to_today
def _match_next_condition(self, current_name, candidates): def _get_pre_winter_break_workinfo(self, inner_data_list):
""" """提取冬休前的上一个有效工况"""
私有辅助方法:按符号风格匹配下一个工况名称(全角/半角括号对应) if not inner_data_list:
:param current_name: 当前工况名称 return None
:param candidates: 目标工况候选列表 # 倒序遍历,跳过冬休,找第一个有效工况
:return: 匹配的工况名称 / 当前工况名称(无候选时) for record in inner_data_list:
""" current_work = record.get("workinfoname")
if not candidates: if current_work and current_work not in self.winter_break_labels:
return current_name # 优先返回等效新工况
# 优先匹配全角括号工况 return self.old_to_new_map.get(current_work, current_work)
if "" in current_name: return None
for cand in candidates:
if "" in cand:
return cand
# 再匹配半角括号工况
if "(" in current_name:
for cand in candidates:
if "(" in cand:
return cand
# 无对应符号时,返回第一个候选工况
return candidates[0]
def _validate_point_id(self, inner_list): def _validate_point_id(self, inner_list):
""" """校验内嵌列表内point_id是否一致"""
私有辅助方法校验内嵌列表内所有元素的point_id是否一致
:param inner_list: 单个point_id的倒序数据列表
:return: point_id / None校验失败或列表为空时
"""
if not inner_list: if not inner_list:
return None return None
base_point_id = inner_list[0].get("point_id") base_point_id = inner_list[0].get("point_id")
@@ -224,23 +308,21 @@ class OperatingModePredictor:
def predict(self, data_2d_list): def predict(self, data_2d_list):
""" """
公有核心方法:执行工况预测,处理二维输入数据,返回一维结果列表 公有核心方法:执行工况预测
:param data_2d_list: 二维倒序数据列表,格式 [[{},{},{}], [{},{},{}]] :param data_2d_list: 二维倒序数据列表,格式 [[{},{},{}], [{},{},{}]]
:return: 一维结果列表,格式 [{}, {}, {}],每个元素为对应内嵌列表的最新记录+推导字段 :return: 一维结果列表,格式 [{}, {}, {}]
""" """
final_result_1d = [] final_result = []
# 遍历二维列表逐个处理每个point_id的内嵌数据
for inner_data_list in data_2d_list: for inner_data_list in data_2d_list:
# 跳过空列表
if not isinstance(inner_data_list, list) or len(inner_data_list) == 0: if not isinstance(inner_data_list, list) or len(inner_data_list) == 0:
continue continue
# 1. 提取当前内嵌列表的最新记录倒序数据索引0为最新 # 取最新记录并复制(避免修改原数据
latest_record = inner_data_list[0].copy() latest_record = inner_data_list[0].copy()
# 2. 校验point_id一致性
point_id = self._validate_point_id(inner_data_list) point_id = self._validate_point_id(inner_data_list)
# 校验point_id
if not point_id: if not point_id:
latest_record.update({ latest_record.update({
"status": "fail", "status": "fail",
@@ -248,55 +330,80 @@ class OperatingModePredictor:
"first_measure_date": None, "first_measure_date": None,
"days_from_first_to_today": None, "days_from_first_to_today": None,
"next_workinfo": None, "next_workinfo": None,
"judge_based_workinfo": None,
"error_msg": "point_id不一致或缺失" "error_msg": "point_id不一致或缺失"
}) })
final_result_1d.append(latest_record) final_result.append(latest_record)
continue continue
# 3. 提取并校验当前工况 # 获取当前工况并标准化
current_workinfo = latest_record.get("workinfoname") current_workinfo = latest_record.get("workinfoname")
if not current_workinfo or current_workinfo not in self.base_periods: if not current_workinfo or self._standardize_name(current_workinfo) not in self.std_to_canonical:
latest_record.update({ latest_record.update({
"status": "fail", "status": "fail",
"current_workinfo": None, "current_workinfo": None,
"first_measure_date": None, "first_measure_date": None,
"days_from_first_to_today": None, "days_from_first_to_today": None,
"next_workinfo": None, "next_workinfo": None,
"judge_based_workinfo": None,
"error_msg": "工况无效或缺失" "error_msg": "工况无效或缺失"
}) })
final_result_1d.append(latest_record) final_result.append(latest_record)
continue continue
# 4. 提取时间统计信息 # 冬休逻辑:取冬休前工况作为判断基准
first_dt, cumulative_days, days_to_today = self._get_time_statistics_from_reversed( if current_workinfo in self.winter_break_labels:
inner_data_list, current_workinfo judge_based_workinfo = self._get_pre_winter_break_workinfo(inner_data_list)
if not judge_based_workinfo:
latest_record.update({
"status": "fail",
"current_workinfo": current_workinfo,
"first_measure_date": None,
"days_from_first_to_today": None,
"next_workinfo": None,
"judge_based_workinfo": None,
"error_msg": "冬休前未找到有效工况"
})
final_result.append(latest_record)
continue
else:
# 非冬休:优先使用等效新工况作为判断基准
judge_based_workinfo = self.old_to_new_map.get(current_workinfo, current_workinfo)
# 计算时间统计信息
first_dt, cumulative_days, days_to_today = self._get_time_statistics(
inner_data_list, judge_based_workinfo
) )
first_measure_date = first_dt.strftime("%Y-%m-%d") if first_dt else None first_measure_date = first_dt.strftime("%Y-%m-%d") if first_dt else None
# 5. 判断工况切换条件,推导下一工况 # 推导下一工况以base_periods中的名称为准
group_id = self.condition_group.get(current_workinfo, "STATIC") std_judge_work = self._standardize_name(judge_based_workinfo)
rule = self.group_transition_rules.get(group_id, {}) group_id = self.std_to_group.get(std_judge_work, "STATIC")
rule = self.transition_rules.get(group_id, {})
trigger_days = rule.get("trigger_days") trigger_days = rule.get("trigger_days")
next_candidates = rule.get("next_candidates", []) next_candidates = rule.get("next", [])
if trigger_days is not None and cumulative_days >= trigger_days: if trigger_days is not None and cumulative_days >= trigger_days and next_candidates:
# 满足切换条件:返回目标工况 # 切换工况取base_periods中的规范名称
next_workname = self._match_next_condition(current_workinfo, next_candidates) next_workinfo = next_candidates[0]
else: else:
# 不满足切换条件:返回当前工况 # 不切换:有等效则返回新工况,无则返回旧工况
next_workname = current_workinfo next_workinfo = self.old_to_new_map.get(judge_based_workinfo, judge_based_workinfo)
# 6. 组装结果字段,更新最新记录 # 组装最终结果
latest_record.update({ latest_record.update({
"status": "success", "status": "success",
"current_workinfo": current_workinfo, "current_workinfo": current_workinfo, # 保留原始输入工况
"judge_based_workinfo": judge_based_workinfo, # 实际判断用的工况(新工况优先)
"first_measure_date": first_measure_date, "first_measure_date": first_measure_date,
"days_from_first_to_today": days_to_today, "days_from_first_to_today": days_to_today,
"next_workinfo": next_workname, "next_workinfo": next_workinfo, # 推导结果:有等效返回新工况,无则返回旧工况
"point_id": point_id "point_id": point_id,
"error_msg": ""
}) })
# 7. 加入最终结果列表 final_result.append(latest_record)
final_result_1d.append(latest_record)
return final_result
return final_result_1d