Files
Incidents-Data-Portal/backend/app/blueprints/accident/routes.py
T
2026-07-10 18:55:55 +08:00

606 lines
22 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.
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta, timezone
from operator import or_
import traceback
from flask import Blueprint, json, jsonify, current_app, request
from sqlalchemy.exc import SQLAlchemyError
from app.models import AccidList, DictItem, DictType
from app import db
accident_bp = Blueprint("accident_bp", __name__)
@accident_bp.route("/item-list", methods=["POST"])
def get_item_list():
"""
获取字典项列表接口(带分页)
核心逻辑:
- dict_id和item_value都为空 → 全查所有启用的字典项
- 有dict_id则按字典类型过滤,有item_value则按选项值过滤
参数/返回格式:(不变)
"""
try:
# 1. 接收并解析请求参数
req_data = request.get_json() or {}
# dict_id参数处理
dict_id = req_data.get("dict_id")
try:
dict_id = int(dict_id) if dict_id is not None else None
except (ValueError, TypeError):
dict_id = None
item_value = req_data.get("item_value", "").strip()
# 分页参数校验
try:
page = int(req_data.get("pageNum", 1))
page_size = int(req_data.get("pageSize", 10))
page = 1 if page < 1 else page
page_size = 10 if page_size < 1 else page_size
page_size = 50 if page_size > 50 else page_size
except ValueError:
page = 1
page_size = 10
# 2. 构建查询条件(核心:无过滤则全查启用的字典项)
# 基础查询:仅过滤启用状态(status=1)
base_query = DictItem.query.filter(DictItem.status == 1)
# 条件1:按dict_id过滤(有值才加)
if dict_id is not None:
dict_type = DictType.query.filter_by(id=dict_id, status=1).first()
if dict_type:
base_query = base_query.filter(DictItem.dict_id == dict_type.id)
else:
return jsonify({
"code": 200,
"msg": "success",
"data": {
"list": [],
"pagination": {"page": page, "page_size": page_size, "total": 0, "total_pages": 0}
}
})
# 条件2:按item_value过滤(有值才加)
if item_value:
base_query = base_query.filter(DictItem.item_value == item_value)
# 3. 分页查询(paginate简化版)
pagination_obj = base_query.order_by(DictItem.create_time.desc()).paginate(
page=page, per_page=page_size, error_out=False
)
item_list = pagination_obj.items
total = pagination_obj.total
total_pages = pagination_obj.pages
# 4. 格式化返回
result_list = [item.to_dict() for item in item_list]
pagination = {
"page": page,
"page_size": page_size,
"total": total,
"total_pages": total_pages
}
return jsonify({
"code": 200,
"msg": "success",
"data": {"list": result_list, "pagination": pagination}
})
except Exception as e:
print(f"获取字典项列表失败:{str(e)}")
return jsonify({
"code": 500,
"msg": "服务器内部错误",
"data": {
"list": [],
"pagination": {"page": 1, "page_size": 10, "total": 0, "total_pages": 0}
}
})
@accident_bp.route("/all_dict_code", methods=["GET"])
def get_all_dict_code():
"""
获取所有字典类型及对应字典项接口(无参数,返回全量数据)
请求方式:GET
返回格式:
{
"code": 200, // 200成功/500服务器错误
"msg": "success", // 提示信息
"data": [ // 所有字典类型+对应字典项列表
{
"id": 1, // 字典类型ID
"dict_code": "carline", // 字典编码
"status": 1, // 字典类型状态
"items": [ // 该字典编码对应的所有字典项
{"id": 101, "item_value": "大众帕萨特"},
{"id": 102, "item_value": "丰田凯美瑞"}
]
},
{
"id": 2,
"dict_code": "labels",
"status": 1,
"items": [
{"id": 201, "item_value": "碰撞",label:1,value:"123"},
# {"id": 202, "item_value": "异响"}
]
}
]
}
"""
try:
# 1. 查询所有字典类型(按id升序)
dict_type_list = DictType.query.order_by(DictType.id.asc()).all()
# 2. 格式化数据:关联查询每个字典类型对应的字典项
result_data = []
for dict_type in dict_type_list:
# 2.1 查询当前字典类型对应的所有字典项(过滤启用状态,按id升序)
dict_items = DictItem.query.filter(
DictItem.dict_id == dict_type.id, # 关联dict_type的id
DictItem.status == 1 # 仅返回启用的字典项(前端下拉常用)
).order_by(DictItem.id.asc()).all()
# 2.2 格式化字典项(仅保留id和item_value
item_list = [
{
"id": item.id,
"item_value": item.item_value,
"label":item.item_value,
"value":item.item_value,
}
for item in dict_items
]
# 2.3 组装当前字典类型的完整数据
result_data.append({
"id": dict_type.id,
"dict_code": dict_type.dict_code,
"status": dict_type.status,
"items": item_list # 新增:对应字典项的id和item_value
})
# 3. 返回标准化响应
return jsonify({
"code": 200,
"msg": "success",
"data": result_data
})
except Exception as e:
# 异常日志记录(生产环境替换为logging)
print(f"获取所有字典编码及对应项失败:{str(e)}")
# 异常时返回空数组,保证前端格式统一
return jsonify({
"code": 500,
"msg": "服务器内部错误",
"data": []
})
@accident_bp.route("/insert-dict", methods=["POST"])
def insert_dict_type():
"""
新增/恢复字典项接口(新增逻辑优化:存在则恢复status=1,不存在则新增)
请求参数(JSON格式):
{
"dict_id": 1, // 必传,字典类型ID(整数)
"item_value": "大众迈腾" // 必传,字典项值(非空字符串)
}
返回格式:
{
"code": 200, // 200成功/400参数错误/500服务器错误
"msg": "新增成功", // 提示信息(区分新增/更新)
"data": {} // 成功返回数据ID,失败返回空
}
"""
try:
# 1. 接收并解析JSON请求参数
req_data = request.get_json()
if not req_data:
return jsonify({
"code": 400,
"msg": "请求参数不能为空,且必须为JSON格式",
"data": {}
})
# 2. 提取参数并做基础校验
# 2.1 提取dict_id并校验(必传+整数)
dict_id = req_data.get("dict_id")
if dict_id is None:
return jsonify({
"code": 400,
"msg": "dict_id为必传参数",
"data": {}
})
try:
dict_id = int(dict_id)
except (ValueError, TypeError):
return jsonify({
"code": 400,
"msg": "dict_id必须为整数",
"data": {}
})
# 2.2 提取item_value并校验(必传+非空)
item_value = req_data.get("item_value", "").strip()
if not item_value:
return jsonify({
"code": 400,
"msg": "item_value为必传参数,且不能为空",
"data": {}
})
# 3. 业务校验:校验dict_id对应的字典类型是否存在
dict_type = DictType.query.filter_by(id=dict_id).first()
if not dict_type:
return jsonify({
"code": 400,
"msg": f"字典类型ID {dict_id} 不存在",
"data": {}
})
# ========== 核心逻辑修改:先查询所有status的记录 ==========
# 4. 校验dict_id + item_value是否存在(不限制status
exist_item = DictItem.query.filter_by(
dict_id=dict_id,
item_value=item_value
).first()
if exist_item:
# 4.1 记录存在:判断status是否为1
if exist_item.status != 1:
# 非1则改为1(恢复启用)
exist_item.status = 1
exist_item.update_time = datetime.now() # 手动更新时间(若模型未自动配置)
db.session.commit()
# 返回更新成功响应
return jsonify({
"code": 200,
"msg": f"字典项{item_value}已存在,已恢复启用状态(status=1)",
"data": {
"id": exist_item.id,
"dict_id": exist_item.dict_id,
"item_value": exist_item.item_value,
"status": exist_item.status
}
})
else:
# status=1:提示重复,不新增/更新
return jsonify({
"code": 400,
"msg": f"字典类型 {dict_type.dict_code} 下已存在{item_value}(启用状态),请勿重复添加",
"data": {}
})
else:
# 4.2 记录不存在:正常新增
new_dict_item = DictItem(
dict_id=dict_id,
item_value=item_value,
item_label=item_value, # 默认为item_value,如需自定义可改为req_data.get("item_label", item_value)
status=1, # 默认启用
sort=0, # 默认排序值,如需自定义可改为req_data.get("sort", 0)
remark="", # 默认空备注,如需自定义可改为req_data.get("remark", "")
create_time=datetime.now(), # 若模型已设置自动生成,可省略
update_time=datetime.now() # 若模型已设置自动生成,可省略
)
# 插入数据库并提交
db.session.add(new_dict_item)
db.session.commit()
# 返回新增成功响应
return jsonify({
"code": 200,
"msg": "新增字典项成功",
"data": {
"id": new_dict_item.id,
"dict_id": new_dict_item.dict_id,
"item_value": new_dict_item.item_value
}
})
except Exception as e:
# 异常时回滚数据库,避免脏数据
db.session.rollback()
print(f"新增/恢复字典项失败:{str(e)}", exc_info=True)
return jsonify({
"code": 500,
"msg": "服务器内部错误,操作失败",
"data": {}
})
@accident_bp.route("/update-item", methods=["POST"])
def update_item():
"""
根据ID和传入的status,翻转字典项的状态(1→0,0→1)
请求参数:JSON格式 {"id": 10, "status": 1} 或 {"id": 10, "status": 0}
返回格式:标准化JSON响应
"""
try:
# 1. 获取并解析请求的JSON参数
req_data = request.get_json()
if not req_data:
return jsonify({
"code": 400,
"msg": "请求参数不能为空,且必须为JSON格式",
"data": None
}), 400
# 2. 提取并校验id参数
item_id = req_data.get("id")
req_status = req_data.get("status")
# 3. 查询对应的字典项记录
dict_item = DictItem.query.filter_by(id=item_id).first()
if not dict_item:
return jsonify({
"code": 404,
"msg": f"未找到ID为{item_id}的字典项记录",
"data": None
}), 404
# ========== 核心逻辑修改:状态翻转 ==========
# 传入1则改为0,传入0则改为1
target_status = 1 - req_status
dict_item.status = target_status # 更新为目标状态
db.session.commit() # 提交数据库修改
# 5. 返回成功响应(包含更新后的记录信息)
return jsonify({
"code": 200,
"msg": f"字典项状态更新成功(传入{req_status},更新为{target_status}",
"data": dict_item.to_dict()
}), 200
# 捕获JSON解析异常
except ValueError as e:
return jsonify({
"code": 400,
"msg": f"JSON参数解析失败:{str(e)}",
"data": None
}), 400
# 捕获数据库操作异常
except SQLAlchemyError as e:
db.session.rollback()
return jsonify({
"code": 500,
"msg": f"数据库操作失败:{str(e)}",
"data": None
}), 500
# 捕获其他未知异常
except Exception as e:
return jsonify({
"code": 500,
"msg": f"接口执行异常:{str(e)}",
"data": None
}), 500
@accident_bp.route("/insert-accident", methods=["POST"])
def insert_accident():
"""
新增事故工单接口
请求参数:JSON格式(见前端传参示例)
返回格式:标准化JSON响应
"""
try:
# 1. 解析前端JSON参数(无参数/非JSON直接返回错误)
req_data = request.get_json()
# print(111,req_data)
if not req_data:
return jsonify({
"code": 400,
"msg": "请求参数不能为空,且必须为JSON格式",
"data": {}
})
# 2. 必传参数校验(模型中nullable=False的字段)
required_fields = [
"vin", "occur_datetime",
"case_description", "fo_solution"
]
missing_fields = [f for f in required_fields if f not in req_data or not req_data[f]]
if missing_fields:
return jsonify({
"code": 400,
"msg": f"缺少必传参数:{','.join(missing_fields)}",
"data": {}
})
# 3. 时间字段转换(前端传ISO格式字符串 → datetime对象)
# 处理问题发生时间(occur_datetime
try:
# 兼容前端传的带Z的ISO格式(如2026-01-29T06:32:10.017Z
occur_datetime = datetime.fromisoformat(req_data["occur_datetime"].replace("Z", "+00:00"))
except Exception as e:
return jsonify({
"code": 400,
"msg": f"时间格式错误(occur_datetime):请传入ISO格式(如2026-01-29T06:32:10.017Z),错误详情:{str(e)}",
"data": {}
})
# 处理工单新建时间(create_time):前端传则用前端值,否则用当前时间
if "create_time" in req_data and req_data["create_time"]:
try:
create_time = datetime.fromisoformat(req_data["create_time"].replace("Z", "+00:00"))
except Exception as e:
return jsonify({
"code": 400,
"msg": f"时间格式错误(create_time):请传入ISO格式(如2026-01-29T06:32:10.017Z),错误详情:{str(e)}",
"data": {}
})
else:
create_time = datetime.now() # 前端未传则用当前时间
# 4. 构建AccidList实例(严格映射模型字段)
new_accident = AccidList(
ticket_id='',
vin=req_data["vin"].strip(),
occur_datetime=occur_datetime,
case_description=req_data["case_description"].strip(),
fo_solution=req_data["fo_solution"].strip(),
create_time=create_time, # 工单新建时间(前端传则用,否则当前时间)
system_function=req_data['system_function'].strip(),
# 可选字段(前端传则用,否则用模型默认值)
creator=req_data.get("creator", "").strip(), # 创建人(有则更新,无则默认空)
model='', # 型号
carline=req_data.get("carline"), # 车系
labels=req_data.get("labels"), # 事故标签
accident_level=req_data.get("accident_level", "P1"), # 事故评级(默认P1
bmbs_name='', # BMBS联系人
rdca_reporter=req_data.get("rdca_reporter", "").strip(), # RDCA报告人
software_version=req_data.get("software_version", "").strip(), # 软件版本
attachment_url=req_data.get("attachment_url", "").strip(), # 附件地址
)
# 5. 插入数据库并提交
db.session.add(new_accident)
db.session.commit()
# 6. 返回成功响应(包含新增数据的完整信息)
return jsonify({
"code": 200,
"msg": "新增成功",
"data": new_accident.to_dict() # 调用模型的to_dict方法返回结构化数据
})
except Exception as e:
# 异常时回滚数据库,避免脏数据
db.session.rollback()
# 记录详细错误日志(便于排查)
print(f"新增事故工单失败:{str(e)}")
# 返回友好错误提示(不暴露敏感信息)
return jsonify({
"code": 500,
"msg": f"服务器内部错误,新增失败:{str(e)}",
"data": {}
})
@accident_bp.route("/get-accident-record", methods=["POST"])
def get_accident_record():
"""
查询事故工单记录接口(改用in实现多值过滤,无or_)
入参:JSON格式,支持case_description模糊查询、carline/labels/affects_versions多值精确过滤
出参:按create_time从近到远排序的事故记录列表
"""
try:
# 1. 解析前端参数
params = request.get_json(silent=True) or {}
case_desc = params.get("case_description", "").strip()
# 提取并清洗多值参数(转列表+去空)
carline_list = [item.strip() for item in (params.get("carline", [])) if item.strip()]
labels_list = [item.strip() for item in (params.get("labels", [])) if item.strip()]
affects_versions_list = [item.strip() for item in (params.get("affects_versions", [])) if item.strip()]
# 2. 初始化查询对象
query = AccidList.query
# 3. 构建查询条件
# 3.1 case_description:保留模糊查询
if case_desc:
query = query.filter(AccidList.case_description.like(f"%{case_desc}%"))
# 3.2 carline:改用in实现多值精确过滤(核心修改,无or_)
if carline_list:
query = query.filter(AccidList.carline.in_(carline_list))
# 3.3 labels:同carline逻辑
if labels_list:
query = query.filter(AccidList.labels.in_(labels_list))
# 3.4 affects_versions:同carline逻辑
if affects_versions_list:
query = query.filter(AccidList.affects_versions.in_(affects_versions_list))
# 4. 排序+查询
query = query.order_by(AccidList.create_time.desc())
accident_records = query.all()
result = [record.to_dict() for record in accident_records]
# 5. 返回响应
return jsonify({
"code": 200,
"msg": "查询成功",
"data": result
}), 200
except Exception as e:
print(f"查询事故记录异常:{str(e)}\n{traceback.format_exc()}")
return jsonify({
"code": 500,
"msg": f"查询失败:{str(e)}",
"data": None
}), 500
@accident_bp.route("/get-accident-by-id", methods=["POST"])
def get_accident_by_id():
"""根据ID查询事故工单详情接口"""
try:
params = request.get_json(silent=True) or {}
id_str=params.get('id')
if not id_str:
return jsonify({
"code": 400, # 400表示参数错误
"msg": "参数缺失:请传入id参数",
"data": None
}), 400
# 转换为整数,处理非数字的情况
try:
accident_id = int(id_str)
except ValueError:
return jsonify({
"code": 400,
"msg": "参数错误:id必须是整数",
"data": None
}), 400
# 2. 查询数据库
# filter_by按主键id查询,first()获取单条记录(None表示无匹配)
accident = AccidList.query.filter_by(id=accident_id).first()
# 3. 处理查询结果
if not accident:
return jsonify({
"code": 404, # 404表示资源不存在
"msg": f"未找到ID为{accident_id}的事故记录",
"data": None
}), 404
# 4. 格式化并返回数据(使用模型的to_dict()方法)
return jsonify({
"code": 200, # 200表示成功
"msg": "查询成功",
"data": accident.to_dict() # 调用模型自带的转字典方法
}), 200
# 捕获数据库相关异常
except SQLAlchemyError as e:
# 打印异常日志(方便排查问题)
print(f"数据库查询异常:{str(e)}")
return jsonify({
"code": 500, # 500表示服务器内部错误
"msg": "服务器内部错误:数据库查询失败",
"data": None
}), 500
# 捕获其他未知异常
except Exception as e:
print(f"接口异常:{str(e)}")
return jsonify({
"code": 500,
"msg": f"服务器内部错误:{str(e)}",
"data": None
}), 500