init
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
FLASK_ENV=development
|
||||
|
||||
# 集群配置
|
||||
DATABRICKS_TOKEN=dapi188523cfb1cf34ad5a605fd75c5c0d31
|
||||
CLUSTER_ID=1203-054146-wkjf95i0
|
||||
DATABRICKS_HOST=https://adb-3035612432650494.2.databricks.azure.cn
|
||||
|
||||
# gen5 spark表的配置
|
||||
GEN5_TABLE_NAME=hive_metastore.taf_level_two_plus.accident_report_data
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET_KEY=dev_mb_project_2025_xyz123 # 开发环境可随意,生产环境需复杂密钥
|
||||
SECRET_KEY=dev_mb_2025_abc789
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=mysql+pymysql://root:Tsystems_2026@150.158.121.95:3306/incident?charset=utf8mb4
|
||||
|
||||
# oauth2配置
|
||||
CLIENT_ID=A17336FC-A8D7-4CEA-8777-384BFEABABC1
|
||||
CLIENT_PASSWORD=U7-C1BZQdL8V5c.z~qA_64SO2l9F3J0o
|
||||
#SSO 授权服务器地址
|
||||
SS0_HOST = https://ssoalpha.dvb.corpinter.net.cn/
|
||||
# 授权成功后,SSO 服务器重定向回你的应用的地址 (必须与 SSO 服务器上配置的一致)
|
||||
REDIRECT_URL = http://localhost:5221/report
|
||||
# 用户登录成功后,最终重定向到的前端页面地址
|
||||
DOMAIN = http://localhost:3001
|
||||
|
||||
# 前端请求头的key
|
||||
HEADERS_SECRET_KEY=mbincid-server12
|
||||
@@ -0,0 +1,44 @@
|
||||
FROM python:3.11
|
||||
|
||||
# 1. 替换为腾讯云APT源 + 仅装时区依赖
|
||||
RUN sed -i 's/deb.debian.org/mirrors.tencent.com/g' /etc/apt/sources.list.d/debian.sources && \
|
||||
apt update && apt install -y --no-install-recommends tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 2. 核心环境变量
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
FLASK_APP=run:app \
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 3. 设置时区
|
||||
RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone
|
||||
|
||||
# 4. 工作目录 + 非root用户
|
||||
WORKDIR /app
|
||||
RUN groupadd -r appuser && useradd -r -g appuser appuser && chown -R appuser:appuser /app
|
||||
|
||||
# 新增图片存放地址
|
||||
RUN mkdir -p /app/logs /app/uploads \
|
||||
&& chown -R appuser:appuser /app/logs /app/uploads \
|
||||
&& chmod -R 777 /app/logs /app/uploads
|
||||
|
||||
USER appuser
|
||||
|
||||
# 5. 腾讯云PyPI源 + 极简提速依赖安装
|
||||
COPY --chown=appuser:appuser requirements.txt /app/
|
||||
RUN python -m venv /app/venv && \
|
||||
# /app/venv/bin/pip install --no-cache-dir --upgrade pip && \
|
||||
/app/venv/bin/pip install --no-cache-dir --prefer-binary -r requirements.txt -i https://mirrors.cloud.tencent.com/pypi/simple/
|
||||
|
||||
# 6. 复制项目文件
|
||||
COPY --chown=appuser:appuser . /app/
|
||||
|
||||
# 7. 切换非root用户
|
||||
USER appuser
|
||||
|
||||
# 8. 暴露端口
|
||||
EXPOSE 5221
|
||||
|
||||
# 9. 启动Gunicorn
|
||||
CMD ["/app/venv/bin/gunicorn", "--bind", "0.0.0.0:5221", "--workers", "7", "--threads", "2", "run:app"]
|
||||
@@ -0,0 +1,87 @@
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
from flask import Flask, jsonify, request
|
||||
from app.config import config
|
||||
from app.services.remote_service import DatabricksQuery
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_cors import CORS
|
||||
from flask_jwt_extended import JWTManager
|
||||
import logging
|
||||
|
||||
db = SQLAlchemy()
|
||||
jwt = JWTManager()
|
||||
|
||||
|
||||
# 配置日志
|
||||
def setup_logging(app):
|
||||
# 创建日志目录
|
||||
if not os.path.exists('logs'):
|
||||
os.makedirs('logs')
|
||||
|
||||
# 配置文件处理器 - 按大小轮转
|
||||
file_handler = RotatingFileHandler(
|
||||
'logs/app.log',
|
||||
maxBytes=500*1024*1024, # 100MB
|
||||
backupCount=10
|
||||
)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# 配置控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# 设置日志格式
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s [%(levelname)s] %(funcName)s:%(lineno)d - %(message)s'
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# 添加到app.logger
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.addHandler(console_handler)
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 禁止传播到root logger(避免重复日志)
|
||||
app.logger.propagate = False
|
||||
|
||||
# 记录启动信息
|
||||
# app.logger.info('Flask应用启动成功!')
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config["JWT_SECRET_KEY"] = "your-secret-key" # 替换为你的密钥
|
||||
app.config["JWT_TOKEN_LOCATION"] = ["headers"] # 从请求头获取令牌
|
||||
app.config["JWT_HEADER_NAME"] = "Authorization" # 请求头字段名
|
||||
# 注意:这里可以将 JWT_HEADER_TYPE 设为空,避免强制要求 Bearer 前缀
|
||||
app.config["JWT_HEADER_TYPE"] = "" # 默认为 'Bearer',设为空则不强制前缀
|
||||
# 加载配置
|
||||
app.config.from_object(config)
|
||||
db.init_app(app)
|
||||
jwt.init_app(app)
|
||||
|
||||
# 配置日志
|
||||
setup_logging(app)
|
||||
|
||||
from app.blueprints.auth.routes import auth_bp
|
||||
from app.blueprints.login.routes import login_bp
|
||||
from app.blueprints.incident_gen5.routes import incident_gen5_bp
|
||||
from app.blueprints.oauth.routes import oauth_bp
|
||||
from app.blueprints.incident_gen6.routes import incident_gen6_bp
|
||||
from app.blueprints.accident.routes import accident_bp
|
||||
from app.blueprints.file_uploads.routes import file_uploads_bp
|
||||
|
||||
domain_prefix='accidentportal/api'
|
||||
|
||||
app.register_blueprint(auth_bp, url_prefix=f"/{domain_prefix}/api/auth")
|
||||
app.register_blueprint(login_bp, url_prefix=f"/{domain_prefix}/user")
|
||||
app.register_blueprint(incident_gen5_bp, url_prefix=f"/{domain_prefix}/gen5")
|
||||
app.register_blueprint(incident_gen6_bp, url_prefix=f"/{domain_prefix}/gen6")
|
||||
app.register_blueprint(oauth_bp, url_prefix="")
|
||||
app.register_blueprint(accident_bp, url_prefix=f"/{domain_prefix}/accident")
|
||||
app.register_blueprint(file_uploads_bp, url_prefix=f"/{domain_prefix}/file_uploads")
|
||||
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,12 @@
|
||||
from flask import Blueprint, json, jsonify, current_app, request
|
||||
from app.blueprints.incident_gen5.service import (
|
||||
aggregate_and_sort_incidents,
|
||||
aggregate_line_list,
|
||||
get_logging_list,
|
||||
process_incident_data,
|
||||
)
|
||||
from app.services.remote_service import DatabricksQuery
|
||||
from app.utils import convert_incident_time_format
|
||||
|
||||
|
||||
incident_bp = Blueprint("incident", __name__)
|
||||
@@ -0,0 +1,606 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
from flask import Blueprint, current_app, request, jsonify
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.models import User, Role, UserRole # 导入模型类
|
||||
from app.utils import encrypt_password # 导入密码加密工具
|
||||
from app import db
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity # 导入JWT相关工具
|
||||
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
@auth_bp.route("/create-user", methods=["POST"])
|
||||
# @jwt_required() # 需要有效Token才能创建用户
|
||||
def create_user():
|
||||
"""
|
||||
{
|
||||
"username": "new_user",
|
||||
"password": "securePass123",
|
||||
"role_ids": [1, 3] // 要分配的角色ID列表
|
||||
}
|
||||
"""
|
||||
data = request.get_json()
|
||||
# print(data)
|
||||
if not data or "username" not in data or "password" not in data:
|
||||
return jsonify({"error": "缺少用户名或密码"}), 400
|
||||
|
||||
# 1. 验证用户名唯一性
|
||||
existing_user = User.query.filter_by(username=data["username"]).first()
|
||||
if existing_user:
|
||||
return jsonify({"error": "用户名已存在"}), 409
|
||||
|
||||
# 2. 获取当前操作者
|
||||
# current_user_id = get_jwt_identity()
|
||||
current_user_id = 1
|
||||
print("current_user_id", current_user_id)
|
||||
current_user = User.query.get(int(current_user_id))
|
||||
current_user_id = 1
|
||||
# if not current_user:
|
||||
# return jsonify({"error": "无效的操作者"}), 401
|
||||
|
||||
# 3. 密码复杂度验证
|
||||
password = data["password"]
|
||||
if len(password) < 4 or not any(c.isdigit() for c in password):
|
||||
return jsonify({"error": "密码需至少4位且包含数字"}), 400
|
||||
|
||||
# 4. 验证角色(新增部分)
|
||||
role_ids = data.get("role_ids", [])
|
||||
valid_roles = []
|
||||
if role_ids:
|
||||
valid_roles = Role.query.filter(Role.id.in_(role_ids)).all()
|
||||
if len(valid_roles) != len(role_ids):
|
||||
return jsonify({"error": "包含无效的角色ID"}), 400
|
||||
|
||||
try:
|
||||
# 5. 创建新用户
|
||||
new_user = User(
|
||||
username=data["username"],
|
||||
password=encrypt_password(data["password"]),
|
||||
status=data.get("status", 1),
|
||||
created_by=current_user_id,
|
||||
email=data.get("email"),
|
||||
avatar=data.get("avatar"),
|
||||
region=data.get("region"),
|
||||
phone=data.get("phone"),
|
||||
brief=data.get("brief"),
|
||||
third_party_account=data.get("third_party_account"),
|
||||
position=data.get("position"),
|
||||
department=data.get("department"),
|
||||
label=data.get("label"),
|
||||
)
|
||||
|
||||
db.session.add(new_user)
|
||||
db.session.flush() # 获取新用户ID但不提交事务
|
||||
|
||||
# 6. 分配角色(新增核心功能)
|
||||
for role in valid_roles:
|
||||
user_role = UserRole(
|
||||
user_id=new_user.id, role_id=role.id, created_by=current_user_id
|
||||
)
|
||||
db.session.add(user_role)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 7. 构造响应数据(包含角色信息)
|
||||
response_data = {
|
||||
"id": new_user.id,
|
||||
"username": new_user.username,
|
||||
"created_at": new_user.created_at.isoformat(),
|
||||
"created_by": new_user.created_by,
|
||||
"roles": [
|
||||
{"id": r.id, "name": r.name} for r in valid_roles
|
||||
], # 新增角色信息
|
||||
}
|
||||
|
||||
# 8. 记录操作日志(新增角色信息)
|
||||
current_app.logger.info(
|
||||
f"用户创建成功: {new_user.username} (ID:{new_user.id}) "
|
||||
f"操作者: {current_user.username} (ID:{current_user_id}) "
|
||||
f"分配角色: {[r.name for r in valid_roles]}" # 记录分配的角色
|
||||
)
|
||||
|
||||
return jsonify(response_data), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"用户创建失败: {str(e)}")
|
||||
return jsonify({"error": "服务器内部错误"}), 500
|
||||
|
||||
|
||||
# 2.修改用户
|
||||
@auth_bp.route("/update-user", methods=["POST"])
|
||||
def update_user():
|
||||
"""
|
||||
修改用户信息(支持更新所有字段,包括新增字段)
|
||||
请求体示例:
|
||||
{
|
||||
"user_id":"12345",
|
||||
"username": "new_username", # 可选:修改用户名
|
||||
"password": "new_password", # 可选:修改密码(明文)
|
||||
"status": 1, # 可选:修改状态
|
||||
"updated_by": 1, # 更新人ID
|
||||
// 新增可选字段(需要更新的字段才传入)
|
||||
"email": "new@example.com",
|
||||
"phone": "+86 13987654321",
|
||||
"department": "产品部",
|
||||
"position": "产品经理",
|
||||
"label": "产品,管理"
|
||||
}
|
||||
"""
|
||||
data = request.get_json()
|
||||
user_id = data["user_id"]
|
||||
user = User.query.get(user_id)
|
||||
|
||||
if not user:
|
||||
return jsonify({"code": 404, "message": f"用户ID {user_id} 不存在"}), 404
|
||||
|
||||
try:
|
||||
# 基础字段更新(原有逻辑保留)
|
||||
if "username" in data and data["username"]:
|
||||
user.username = data["username"]
|
||||
if "password" in data and data["password"]:
|
||||
user.password = encrypt_password(data["password"])
|
||||
if "status" in data:
|
||||
user.status = data["status"]
|
||||
if "updated_by" in data:
|
||||
user.updated_by = data["updated_by"]
|
||||
|
||||
# 新增字段更新(按需更新,仅处理传入的字段)
|
||||
if "email" in data:
|
||||
user.email = data["email"]
|
||||
if "avatar" in data:
|
||||
user.avatar = data["avatar"]
|
||||
if "region" in data:
|
||||
user.region = data["region"]
|
||||
if "phone" in data:
|
||||
user.phone = data["phone"]
|
||||
if "brief" in data:
|
||||
user.brief = data["brief"]
|
||||
if "third_party_account" in data:
|
||||
user.third_party_account = data["third_party_account"]
|
||||
if "position" in data:
|
||||
user.position = data["position"]
|
||||
if "department" in data:
|
||||
user.department = data["department"]
|
||||
if "label" in data:
|
||||
user.label = data["label"]
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 返回更新结果(包含核心字段和新增关键字段)
|
||||
return jsonify(
|
||||
{
|
||||
"code": 200,
|
||||
"message": "用户信息更新成功",
|
||||
"data": {
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"department": user.department,
|
||||
"updated_at": user.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
return (
|
||||
jsonify({"code": 409, "message": f'用户名"{data["username"]}"已存在'}),
|
||||
409,
|
||||
)
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"code": 500, "message": f"更新失败:{str(e)}"}), 500
|
||||
|
||||
|
||||
# 3. 创建角色
|
||||
@auth_bp.route("/roles", methods=["POST"])
|
||||
def create_role():
|
||||
"""
|
||||
创建新角色
|
||||
请求体示例:
|
||||
{
|
||||
"name": "admin", # 角色名称(唯一)
|
||||
"description": "系统管理员",
|
||||
"status": 1,
|
||||
"created_by": 1 # 创建人ID
|
||||
}
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data.get("name"):
|
||||
return jsonify({"code": 400, "message": "角色名称不能为空"}), 400
|
||||
|
||||
try:
|
||||
new_role = Role(
|
||||
name=data["name"],
|
||||
description=data.get("description"),
|
||||
status=data.get("status", 1),
|
||||
created_by=data.get("created_by"),
|
||||
updated_by=data.get("created_by"),
|
||||
)
|
||||
|
||||
db.session.add(new_role)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"code": 200,
|
||||
"message": "角色创建成功",
|
||||
"data": {"role_id": new_role.id, "name": new_role.name},
|
||||
}
|
||||
)
|
||||
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
return jsonify({"code": 409, "message": f'角色"{data["name"]}"已存在'}), 409
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"code": 500, "message": f"创建失败:{str(e)}"}), 500
|
||||
|
||||
|
||||
# 4. 修改角色
|
||||
@auth_bp.route("/roles/<int:role_id>", methods=["PUT"])
|
||||
def update_role(role_id):
|
||||
"""
|
||||
修改角色信息
|
||||
请求体示例:
|
||||
{
|
||||
"name": "super_admin", # 可选:修改角色名称
|
||||
"description": "超级管理员", # 可选:修改描述
|
||||
"status": 1, # 可选:修改状态(1-启用,0-禁用)
|
||||
"updated_by": 1 # 更新人ID
|
||||
}
|
||||
"""
|
||||
data = request.get_json()
|
||||
role = Role.query.get(role_id)
|
||||
|
||||
if not role:
|
||||
return jsonify({"code": 404, "message": f"角色ID {role_id} 不存在"}), 404
|
||||
|
||||
try:
|
||||
if "name" in data and data["name"]:
|
||||
role.name = data["name"]
|
||||
if "description" in data:
|
||||
role.description = data["description"]
|
||||
if "status" in data:
|
||||
role.status = data["status"]
|
||||
if "updated_by" in data:
|
||||
role.updated_by = data["updated_by"]
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"code": 200,
|
||||
"message": "角色信息更新成功",
|
||||
"data": {"role_id": role.id, "name": role.name},
|
||||
}
|
||||
)
|
||||
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
return jsonify({"code": 409, "message": f'角色"{data["name"]}"已存在'}), 409
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"code": 500, "message": f"更新失败:{str(e)}"}), 500
|
||||
|
||||
|
||||
# 5. 给用户分配角色(补充功能:用户-角色关联)
|
||||
@auth_bp.route("/user-roles", methods=["POST"])
|
||||
def assign_role_to_user():
|
||||
"""
|
||||
给用户分配角色(多对多关联)
|
||||
请求体示例:
|
||||
{
|
||||
"user_id": 1,
|
||||
"role_id": 2,
|
||||
"created_by": 1 # 操作人ID
|
||||
}
|
||||
"""
|
||||
data = request.get_json()
|
||||
user_id = data.get("user_id")
|
||||
role_id = data.get("role_id")
|
||||
|
||||
# 校验用户和角色是否存在
|
||||
if not User.query.get(user_id):
|
||||
return jsonify({"code": 404, "message": f"用户ID {user_id} 不存在"}), 404
|
||||
if not Role.query.get(role_id):
|
||||
return jsonify({"code": 404, "message": f"角色ID {role_id} 不存在"}), 404
|
||||
|
||||
try:
|
||||
# 检查是否已关联
|
||||
existing = UserRole.query.filter_by(user_id=user_id, role_id=role_id).first()
|
||||
if existing:
|
||||
return (
|
||||
jsonify(
|
||||
{"code": 409, "message": f"用户ID {user_id} 已拥有角色ID {role_id}"}
|
||||
),
|
||||
409,
|
||||
)
|
||||
|
||||
# 创建关联
|
||||
user_role = UserRole(
|
||||
user_id=user_id, role_id=role_id, created_by=data.get("created_by")
|
||||
)
|
||||
db.session.add(user_role)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"code": 200, "message": "角色分配成功"})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"code": 500, "message": f"分配失败:{str(e)}"}), 500
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from operator import or_
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import traceback
|
||||
from flask import Blueprint, json, jsonify, current_app, request
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.models import AccidList, DictItem, DictType, FileUpload
|
||||
from app import db
|
||||
|
||||
file_uploads_bp = Blueprint("file_uploads_bp", __name__)
|
||||
|
||||
ALLOWED_EXTENSIONS={'png', 'jpg', 'jpeg', 'gif'}
|
||||
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
"""验证图片格式是否合法"""
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
def generate_file_alias(filename):
|
||||
"""生成唯一存储别名(避免重名):时间戳+6位随机字符串+扩展名"""
|
||||
ext = filename.rsplit('.', 1)[1].lower()
|
||||
random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
|
||||
timestamp = int(datetime.now().timestamp())
|
||||
return f"{timestamp}_{random_str}.{ext}"
|
||||
|
||||
|
||||
# 接口1:图片上传(基础接口,返回文件ID)
|
||||
@file_uploads_bp.route('/upload-image', methods=['POST'])
|
||||
def upload_image():
|
||||
"""
|
||||
图片上传接口
|
||||
- 请求方式:POST
|
||||
- Content-Type: multipart/form-data
|
||||
- 请求参数:
|
||||
file: 图片文件(必传)
|
||||
create_by: 上传人(可选,如admin)
|
||||
- 返回:文件ID、访问URL等信息
|
||||
"""
|
||||
try:
|
||||
# 1. 校验文件是否存在
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'code': 400, 'msg': '请选择要上传的图片', 'data': None}), 400
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'code': 400, 'msg': '文件名不能为空', 'data': None}), 400
|
||||
|
||||
# 2. 校验文件格式
|
||||
if not allowed_file(file.filename):
|
||||
return jsonify({
|
||||
'code': 400,
|
||||
'msg': f'仅支持{"/".join(ALLOWED_EXTENSIONS)}格式的图片',
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
# 3. 创建上传目录(不存在则自动创建)
|
||||
os.makedirs(current_app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
# 4. 生成唯一别名并保存文件
|
||||
file_alias = generate_file_alias(file.filename)
|
||||
file_save_path = os.path.join(current_app.config['UPLOAD_FOLDER'], file_alias)
|
||||
file.save(file_save_path)
|
||||
|
||||
# 5. 写入FileUpload表
|
||||
file_size = os.path.getsize(file_save_path) # 获取文件大小(字节)
|
||||
file_type = file.content_type # 获取MIME类型(如image/jpeg)
|
||||
create_by = request.form.get('create_by', None)
|
||||
|
||||
new_file = FileUpload(
|
||||
file_name=file.filename,
|
||||
file_alias=file_alias,
|
||||
file_path=f"/uploads/{file_alias}", # 相对路径,前端拼接域名访问
|
||||
file_size=file_size,
|
||||
file_type=file_type,
|
||||
create_by=create_by,
|
||||
business_type='case', # 固定关联事故工单业务
|
||||
business_id=None # 暂不关联具体工单,后续绑定
|
||||
)
|
||||
db.session.add(new_file)
|
||||
db.session.commit()
|
||||
|
||||
# 6. 返回成功响应
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '图片上传成功',
|
||||
'data': new_file.to_dict()
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback() # 异常回滚
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'msg': f'上传失败:{str(e)}',
|
||||
'data': None
|
||||
}), 500
|
||||
|
||||
|
||||
# 接口2:绑定图片到事故工单
|
||||
@file_uploads_bp.route('/bind-image', methods=['POST'])
|
||||
def bind_image_to_case():
|
||||
"""
|
||||
绑定图片到事故工单(更新image_ids和business_id)
|
||||
- 请求方式:POST
|
||||
- Content-Type: application/json
|
||||
- 请求参数:
|
||||
case_id: 事故工单ID(必传)
|
||||
file_ids: 图片ID列表(如[1,2,3],必传)
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
case_id = data.get('case_id')
|
||||
file_ids = data.get('file_ids', [])
|
||||
|
||||
# 1. 校验参数
|
||||
if not case_id or not isinstance(file_ids, list) or len(file_ids) == 0:
|
||||
return jsonify({'code': 400, 'msg': '工单ID和图片ID列表不能为空', 'data': None}), 400
|
||||
|
||||
# 2. 检查工单是否存在
|
||||
case = AccidList.query.get(case_id)
|
||||
if not case:
|
||||
return jsonify({'code': 404, 'msg': '事故工单不存在', 'data': None}), 404
|
||||
|
||||
# 3. 拼接图片ID串(去重)
|
||||
existing_ids = case.image_ids.split(',') if case.image_ids.strip() else []
|
||||
new_ids = list(set(existing_ids + [str(fid) for fid in file_ids])) # 去重
|
||||
case.image_ids = ','.join(new_ids)
|
||||
case.update_at = db.func.current_timestamp() # 更新工单修改时间
|
||||
|
||||
# 4. 更新FileUpload的business_id(关联具体工单)
|
||||
FileUpload.query.filter(
|
||||
FileUpload.id.in_(file_ids),
|
||||
FileUpload.business_type == 'case'
|
||||
).update({'business_id': case_id}, synchronize_session=False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 5. 返回结果
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '图片绑定成功',
|
||||
'data': {
|
||||
'case_id': case_id,
|
||||
'image_ids': case.image_ids,
|
||||
'image_count': len(new_ids)
|
||||
}
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'msg': f'绑定失败:{str(e)}',
|
||||
'data': None
|
||||
}), 500
|
||||
|
||||
|
||||
# 接口3:查询事故工单关联的图片
|
||||
@file_uploads_bp.route('/get-images/<int:case_id>', methods=['GET'])
|
||||
def get_case_images(case_id):
|
||||
"""
|
||||
查询工单关联的所有有效图片
|
||||
- 请求方式:GET
|
||||
- 路径参数:case_id - 事故工单ID
|
||||
"""
|
||||
try:
|
||||
# 1. 检查工单是否存在
|
||||
case = AccidList.query.get(case_id)
|
||||
if not case:
|
||||
return jsonify({'code': 404, 'msg': '事故工单不存在', 'data': None}), 404
|
||||
|
||||
# 2. 拆分图片ID并查询
|
||||
image_ids = case.image_ids.split(',') if case.image_ids.strip() else []
|
||||
if not image_ids:
|
||||
return jsonify({'code': 200, 'msg': '暂无关联图片', 'data': []}), 200
|
||||
|
||||
# 3. 查询有效图片(status=1)
|
||||
images = FileUpload.query.filter(
|
||||
FileUpload.id.in_(image_ids),
|
||||
FileUpload.status == 1
|
||||
).all()
|
||||
|
||||
# ========== 核心修改:拼接完整图片URL ==========
|
||||
image_base_url = current_app.config['IMAGE_BASE_URL'] # 获取配置的基础URL
|
||||
image_data = []
|
||||
for img in images:
|
||||
img_dict = img.to_dict() # 原有模型的to_dict方法
|
||||
# 把相对路径拼接成完整URL(覆盖原有file_path)
|
||||
img_dict['file_path'] = f"{image_base_url}{img_dict['file_path']}"
|
||||
image_data.append(img_dict)
|
||||
|
||||
# 4. 返回结果(改用拼接后的image_data)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '查询成功',
|
||||
'data': image_data # 现在data里的file_path是完整URL
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'msg': f'查询失败:{str(e)}',
|
||||
'data': None
|
||||
}), 500
|
||||
|
||||
|
||||
# 接口4:移除工单关联的图片(逻辑删除)
|
||||
@file_uploads_bp.route('/remove-image', methods=['POST'])
|
||||
def remove_case_image():
|
||||
"""
|
||||
移除工单关联的图片(逻辑删除)
|
||||
- 请求方式:POST
|
||||
- Content-Type: application/json
|
||||
- 请求参数:
|
||||
case_id: 工单ID(必传)
|
||||
file_id: 图片ID(必传)
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
case_id = data.get('case_id')
|
||||
file_id = data.get('file_id')
|
||||
|
||||
# 1. 校验参数
|
||||
if not case_id or not file_id:
|
||||
return jsonify({'code': 400, 'msg': '工单ID和图片ID不能为空', 'data': None}), 400
|
||||
|
||||
# 2. 检查工单和图片是否存在
|
||||
case = AccidList.query.get(case_id)
|
||||
file = FileUpload.query.get(file_id)
|
||||
if not case:
|
||||
return jsonify({'code': 404, 'msg': '事故工单不存在', 'data': None}), 404
|
||||
if not file:
|
||||
return jsonify({'code': 404, 'msg': '图片不存在', 'data': None}), 404
|
||||
|
||||
# 3. 移除工单的image_ids中的该ID
|
||||
existing_ids = case.image_ids.split(',') if case.image_ids.strip() else []
|
||||
if str(file_id) in existing_ids:
|
||||
existing_ids.remove(str(file_id))
|
||||
case.image_ids = ','.join(existing_ids)
|
||||
case.update_at = db.func.current_timestamp()
|
||||
|
||||
# 4. 逻辑删除图片(status=0)
|
||||
file.status = 0
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '图片移除成功',
|
||||
'data': {'case_id': case_id, 'file_id': file_id}
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'msg': f'移除失败:{str(e)}',
|
||||
'data': None
|
||||
}), 500
|
||||
@@ -0,0 +1,123 @@
|
||||
from flask import Blueprint, json, jsonify, current_app, request
|
||||
from app.blueprints.incident_gen5.service import (
|
||||
aggregate_and_sort_incidents,
|
||||
aggregate_line_list,
|
||||
get_logging_list,
|
||||
process_incident_data,
|
||||
)
|
||||
from app.services.remote_service import DatabricksQuery
|
||||
from app.link_wedata_utils import query_tencent_cloud_data
|
||||
from app.utils import decrypt_code,mask_vin
|
||||
|
||||
|
||||
incident_gen5_bp = Blueprint("incident", __name__)
|
||||
|
||||
|
||||
@incident_gen5_bp.route("/query", methods=["GET"])
|
||||
def query():
|
||||
try:
|
||||
|
||||
try:
|
||||
query_client = DatabricksQuery()
|
||||
results = query_client.query_table(
|
||||
table_name=current_app.config["GEN5_TABLE_NAME"], # 替换为实际表名 taf_level_two_plus.accident_report_data
|
||||
vin="LE4LG4GB6RLMLFJEH", # LE4LG4GB6RLMLFJEH
|
||||
limit=100,
|
||||
)
|
||||
# print(999, results)
|
||||
# for row in results:
|
||||
# print(111111, row)
|
||||
# print(type(results))
|
||||
finally:
|
||||
query_client.stop()
|
||||
|
||||
# for i in query_client:
|
||||
# print(i['incident_time'])
|
||||
return jsonify({"status": "success", "data": results}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
|
||||
|
||||
@incident_gen5_bp.route("/list", methods=["GET","POST"])
|
||||
def get_gen5_list():
|
||||
# 验证请求头的信息,通过才继续
|
||||
encrypted = request.headers.get('X-Encrypted-Timestamp')
|
||||
if not encrypted:
|
||||
return jsonify({"msg": "Parameters are Incorrect"}), 400
|
||||
|
||||
encrypt_str=decrypt_code(encrypted)
|
||||
if not encrypt_str:
|
||||
return jsonify({"msg": "Invalid request"}), 400
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({"code": 400, "data": None, "msg": "请填写有效参数"}), 400
|
||||
|
||||
required_fields = ["incident_time", "oneid"]
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return (
|
||||
jsonify({"code": 400, "data": None, "msg": f"缺少必需字段: {field}"}),
|
||||
400,
|
||||
)
|
||||
|
||||
incident_time = data["incident_time"]
|
||||
# 检查是否为非空列表
|
||||
if not isinstance(incident_time, list) or len(incident_time) != 2:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"code": 400,
|
||||
"data": [],
|
||||
"msg": "incident_time必须是包含两个时间的数组",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
oneid = data["oneid"]
|
||||
if not isinstance(oneid, str) or not oneid.strip():
|
||||
return jsonify({"code": 400, "data": None, "msg": "oneid必须是非空字符串"}), 400
|
||||
|
||||
mask_oneid=mask_vin(oneid)
|
||||
|
||||
query_client = DatabricksQuery()
|
||||
results = query_client.query_table(
|
||||
table_name=current_app.config["GEN5_TABLE_NAME"],
|
||||
vin=mask_oneid,
|
||||
start_time=incident_time[0],
|
||||
end_time=incident_time[1],
|
||||
# limit=10000,
|
||||
)
|
||||
# print(11,results)
|
||||
if len(results) == 0:
|
||||
return jsonify({"code": 200, "data": [], "msg": "查询结果为空"}), 200
|
||||
|
||||
try:
|
||||
events_list = process_incident_data(results)
|
||||
map_list = aggregate_and_sort_incidents(results)
|
||||
line_list = aggregate_line_list(map_list)
|
||||
logging_list = get_logging_list(map_list)
|
||||
|
||||
response = {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"events_list": events_list,
|
||||
"map_list":map_list,
|
||||
"line_list": line_list,
|
||||
"logging_list": logging_list,
|
||||
},
|
||||
"msg": "success",
|
||||
}
|
||||
return jsonify(response)
|
||||
except Exception as e:
|
||||
# 打印错误日志,方便后端排查
|
||||
print(f"数据处理异常:{str(e)}")
|
||||
# 返回友好的错误信息,前端正常解析
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"data": [],
|
||||
"msg": f"数据处理失败:{str(e)}"
|
||||
}), 200
|
||||
@@ -0,0 +1,447 @@
|
||||
import ast
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import pandas as pd # 确保导入pandas用于类型检查
|
||||
|
||||
def process_incident_data(test_val):
|
||||
"""
|
||||
处理事故数据,去重、排序并提取指定字段。
|
||||
|
||||
Args:
|
||||
test_val (dict): 包含 'status' 和 'data' 键的原始数据字典。
|
||||
|
||||
Returns:
|
||||
dict: 格式化后的数据,包含 'events' 键。
|
||||
"""
|
||||
unique_data_dict = {}
|
||||
for item in test_val:
|
||||
name = item.get("incident_name")
|
||||
if name and name not in unique_data_dict:
|
||||
unique_data_dict[name] = item
|
||||
|
||||
# 获取去重后的数据列表
|
||||
unique_data_list = list(unique_data_dict.values())
|
||||
|
||||
# 2. 根据 incident_time 升序排序
|
||||
# incident_time 是 GMT 格式字符串,可以直接排序
|
||||
sorted_data = sorted(unique_data_list, key=lambda x: x.get("incident_time", ""))
|
||||
|
||||
# 3. 提取指定字段并格式化时间
|
||||
id = 0
|
||||
processed_events_data = []
|
||||
for item in sorted_data:
|
||||
incident_time_str = item.get("incident_time")
|
||||
if not incident_time_str:
|
||||
# 如果 incident_time 为空或不存在,可以选择跳过或使用默认值
|
||||
# 这里我们跳过
|
||||
print(
|
||||
f"Warning: Missing 'incident_time' for item: {item.get('incident_name')}, skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
# 🔥 仅修改这里:解析 GMT 格式时间 "Wed, 10 Apr 2024 16:00:00 GMT"
|
||||
if isinstance(incident_time_str, datetime):
|
||||
incident_datetime = incident_time_str
|
||||
else:
|
||||
# 解析 GMT 格式字符串 "Wed, 10 Apr 2024 16:00:00 GMT"
|
||||
incident_datetime = datetime.strptime(incident_time_str, "%a, %d %b %Y %H:%M:%S %Z")
|
||||
except ValueError as e:
|
||||
# 如果时间格式不正确,可以选择跳过或使用默认值
|
||||
# 这里我们跳过
|
||||
print(
|
||||
f"Warning: Invalid 'incident_time' format for item: {item.get('incident_name')}, value: {incident_time_str}, error: {e}. Skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
# 格式化日期和时间(逻辑不变)
|
||||
date_str = incident_datetime.strftime("%Y-%m-%d")
|
||||
time_str = incident_datetime.strftime("%H:%M:%S")
|
||||
|
||||
# 创建新的字典,只包含需要的字段
|
||||
id += 1
|
||||
processed_item = {
|
||||
"id": id,
|
||||
"oneid": item.get("oneid", ""),
|
||||
"date": date_str,
|
||||
"time": time_str,
|
||||
"incident_name": item.get("incident_name", ""),
|
||||
"incident_description": item.get(
|
||||
"incident_description", ""
|
||||
),
|
||||
}
|
||||
processed_events_data.append(processed_item)
|
||||
|
||||
# 4. 构建最终返回的字典
|
||||
total_count = len(processed_events_data)
|
||||
result = {"data": processed_events_data, "total": total_count}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def process_incident_data1(test_val):
|
||||
"""
|
||||
处理事故数据,按照incident_time和incident_name去重,然后按incident_time排序。
|
||||
|
||||
Args:
|
||||
test_val (list): 原始数据列表
|
||||
|
||||
Returns:
|
||||
dict: 格式化后的数据
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
# 1. 按照 incident_name 和 incident_time 组合去重
|
||||
unique_data_dict = {}
|
||||
for item in test_val:
|
||||
name = item.get("incident_name")
|
||||
time = item.get("incident_time")
|
||||
if name and time:
|
||||
# 使用组合键去重
|
||||
key = f"{name}_{time}"
|
||||
if key not in unique_data_dict:
|
||||
unique_data_dict[key] = item
|
||||
|
||||
# 获取去重后的数据列表
|
||||
unique_data_list = list(unique_data_dict.values())
|
||||
|
||||
# 2. 根据 incident_time 升序排序
|
||||
def parse_time(time_str):
|
||||
try:
|
||||
# 处理 "Sat, 08 Mar 2025 09:00:00 GMT" 格式
|
||||
return datetime.strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z")
|
||||
except:
|
||||
# 处理 ISO 8601 格式作为备选
|
||||
try:
|
||||
return datetime.fromisoformat(time_str.replace("Z", "+00:00"))
|
||||
except:
|
||||
return datetime.min
|
||||
|
||||
sorted_data = sorted(unique_data_list, key=lambda x: parse_time(x.get("incident_time", "")))
|
||||
|
||||
# 3. 提取指定字段
|
||||
id = 0
|
||||
processed_events_data = []
|
||||
for item in sorted_data:
|
||||
incident_time_str = item.get("incident_time")
|
||||
if not incident_time_str:
|
||||
continue
|
||||
|
||||
try:
|
||||
incident_datetime = parse_time(incident_time_str)
|
||||
except:
|
||||
continue
|
||||
|
||||
# 格式化日期和时间
|
||||
date_str = incident_datetime.strftime("%Y-%m-%d")
|
||||
time_str = incident_datetime.strftime("%H:%M:%S")
|
||||
|
||||
# 创建新的字典
|
||||
id += 1
|
||||
processed_item = {
|
||||
"id": id,
|
||||
"oneid": item.get("oneid", ""),
|
||||
"date": date_str,
|
||||
"time": time_str,
|
||||
"incident_name": item.get("incident_name", ""),
|
||||
"incident_description": item.get("incident_description", ""),
|
||||
}
|
||||
processed_events_data.append(processed_item)
|
||||
|
||||
# 4. 构建最终返回的字典
|
||||
total_count = len(processed_events_data)
|
||||
result = {"data": processed_events_data, "total": total_count}
|
||||
|
||||
return result
|
||||
|
||||
def aggregate_and_sort_incidents(raw_data):
|
||||
"""
|
||||
按(idc_tickcount_ms + Incident__Name)聚合,整合同一组内的信号数据,消除视觉冗余
|
||||
1. 同一组只输出1条记录,包含公共信息+所有信号数据
|
||||
2. 按idc_tickcount_ms从小到大排序
|
||||
3. 保留所有关键数据(含mux_data解析)
|
||||
"""
|
||||
aggregate_dict = {}
|
||||
for item in raw_data:
|
||||
# 1. 处理聚合key:(idc_tickcount_ms整数, Incident__Name)
|
||||
try:
|
||||
tickcount_int = int(item.get("idc_tickcount_ms", "0"))
|
||||
except (ValueError, TypeError):
|
||||
tickcount_int = 0
|
||||
incident_name = item.get("incident_name", "unknown_incident")
|
||||
aggregate_key = (tickcount_int, incident_name)
|
||||
|
||||
# 2. 提取当前记录的关键数据(用于后续整合)
|
||||
# 处理数值:优先取int_value,无则取float_value,都无则为None
|
||||
# value = item.get("int_value") or item.get("float_value")
|
||||
int_value=item.get("int_value")
|
||||
float_value=item.get('float_value')
|
||||
if int_value is not None:
|
||||
value = int_value
|
||||
elif float_value is not None:
|
||||
value = float_value
|
||||
else:
|
||||
value = None
|
||||
|
||||
# 处理mux_data:若为JSON字符串,解析为字典(方便后续使用)
|
||||
mux_data = item.get("mux_data")
|
||||
if mux_data and mux_data != "null":
|
||||
try:
|
||||
mux_data = ast.literal_eval(mux_data)
|
||||
except (ValueError, SyntaxError):
|
||||
pass # 解析失败则保留原始字符串
|
||||
|
||||
# 3. 整合到聚合字典
|
||||
if aggregate_key not in aggregate_dict:
|
||||
# 首次遇到该key:初始化聚合结构(提取公共基础信息)
|
||||
aggregate_dict[aggregate_key] = {
|
||||
"base_info": { # 同一组的公共信息(只取第一条记录的)
|
||||
"oneid": item.get("oneid"),
|
||||
"ihd_version": item.get("ihd_version"),
|
||||
"incident_time": item.get("incident_time"),
|
||||
"speed": item.get("speed"),
|
||||
"gps_heading": item.get("gps_heading"),
|
||||
"incident_name": incident_name,
|
||||
"incident_description": item.get("incident_description"),
|
||||
"Trigger__condition": item.get("Trigger__condition"),
|
||||
"idc_tickcount_ms": item.get("idc_tickcount_ms"),
|
||||
"idc_tickcount_ms_int": tickcount_int,
|
||||
"gps_dr_position": item.get("gps_dr_position"),
|
||||
"incident_id": item.get("incident_id"),
|
||||
"longitude": item.get("longitude"),
|
||||
"latitude": item.get("latitude")
|
||||
},
|
||||
"signal_data": {} # 整合同一组的所有信号(key: 信号名,value: 信号数据)
|
||||
}
|
||||
|
||||
# print('item.get("signal_name")',item.get('signal_name'))
|
||||
# 4. 将当前记录的信号数据添加到signal_data中
|
||||
# signal_key = item.get("key", "unknown_key")
|
||||
signal_key=item.get('signal_name')
|
||||
aggregate_dict[aggregate_key]["signal_data"][signal_key] = {
|
||||
"value": value,
|
||||
"mux_data": mux_data,
|
||||
"signal_ihd_id": item.get("signal_ihd_id"),
|
||||
"Decoding__value": item.get("Decoding__value"),
|
||||
"Meaning": item.get("meaning")
|
||||
}
|
||||
|
||||
sorted_data=sort_incidents_by_heading(list(aggregate_dict.values())) # 跨周期的
|
||||
|
||||
sorted_result = sorted(
|
||||
sorted_data,
|
||||
key=lambda x: x.get("idc_tickcount_ms_tmp", x["base_info"]["idc_tickcount_ms_int"])
|
||||
)
|
||||
|
||||
return sorted_result
|
||||
|
||||
|
||||
def parse_coordinate(coord_str):
|
||||
"""
|
||||
解析坐标字符串为浮点数
|
||||
"""
|
||||
if coord_str is None:
|
||||
return None
|
||||
|
||||
if isinstance(coord_str, (int, float)):
|
||||
return float(coord_str)
|
||||
|
||||
if isinstance(coord_str, str):
|
||||
try:
|
||||
return float(coord_str)
|
||||
except ValueError:
|
||||
# 如果是 "[longitude, latitude]" 格式,提取第一个数字
|
||||
if coord_str.startswith('[') and coord_str.endswith(']'):
|
||||
try:
|
||||
coord_str_clean = coord_str.strip('[]')
|
||||
coords = coord_str_clean.split(',')
|
||||
if len(coords) >= 1:
|
||||
return float(coords[0].strip())
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def sort_incidents_by_heading(raw_data):
|
||||
"""
|
||||
按__head方法的逻辑进行排序:根据GPS航向角排序并处理时间戳回绕
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
# 将原始数据转换为DataFrame,并处理经纬度
|
||||
processed_data = []
|
||||
for item in raw_data:
|
||||
# 获取base_info
|
||||
base_info = item.get('base_info', {})
|
||||
|
||||
processed_item = item.copy() # 保持原始结构
|
||||
|
||||
# 解析经纬度和航向角,从base_info中获取
|
||||
longitude = parse_coordinate(base_info.get('longitude'))
|
||||
latitude = parse_coordinate(base_info.get('latitude'))
|
||||
gps_heading = parse_coordinate(base_info.get('gps_heading'))
|
||||
|
||||
# 添加到processed_item,用于排序
|
||||
processed_item['longitude'] = longitude if longitude is not None else 0.0
|
||||
processed_item['latitude'] = latitude if latitude is not None else 0.0
|
||||
processed_item['gps_heading'] = gps_heading if gps_heading is not None else 0.0
|
||||
processed_item['incident_time'] = base_info.get('incident_time', '')
|
||||
|
||||
# 确保时间戳是数值类型,从base_info中获取
|
||||
try:
|
||||
tickcount_ms = int(base_info.get('idc_tickcount_ms', 0))
|
||||
except (ValueError, TypeError):
|
||||
tickcount_ms = 0
|
||||
|
||||
processed_item['idc_tickcount_ms'] = tickcount_ms
|
||||
|
||||
processed_data.append(processed_item)
|
||||
|
||||
df = pd.DataFrame(processed_data)
|
||||
|
||||
# 确保所有相关列都是数值类型
|
||||
df['idc_tickcount_ms'] = pd.to_numeric(df['idc_tickcount_ms'], errors='coerce').fillna(0).astype(int)
|
||||
df['longitude'] = pd.to_numeric(df['longitude'], errors='coerce').fillna(0.0)
|
||||
df['latitude'] = pd.to_numeric(df['latitude'], errors='coerce').fillna(0.0)
|
||||
df['gps_heading'] = pd.to_numeric(df['gps_heading'], errors='coerce').fillna(0.0)
|
||||
|
||||
# 计算平均GPS航向角
|
||||
gps_heading = df["gps_heading"].mean()
|
||||
# print('gps_heading',gps_heading)
|
||||
|
||||
# 根据GPS航向角进行排序
|
||||
if (gps_heading >= 315):
|
||||
df.sort_values(by=['incident_time','latitude', 'longitude','idc_tickcount_ms'],
|
||||
ascending=[True, True, False, True], inplace=True)
|
||||
elif (gps_heading < 45):
|
||||
df.sort_values(by=['incident_time','latitude', 'longitude','idc_tickcount_ms'],
|
||||
ascending=[True, True, True, True], inplace=True)
|
||||
elif (gps_heading >= 45) and (gps_heading < 90):
|
||||
df.sort_values(by=['incident_time','longitude', 'latitude','idc_tickcount_ms'],
|
||||
ascending=[True, True, True, True], inplace=True)
|
||||
elif (gps_heading >= 90) and (gps_heading < 135):
|
||||
df.sort_values(by=['incident_time','longitude', 'latitude','idc_tickcount_ms'],
|
||||
ascending=[True, True, False, True], inplace=True)
|
||||
elif (gps_heading >= 135) and (gps_heading < 180):
|
||||
df.sort_values(by=['incident_time','latitude', 'longitude','idc_tickcount_ms'],
|
||||
ascending=[True, False, True, True], inplace=True)
|
||||
elif (gps_heading >= 180) and (gps_heading < 225):
|
||||
df.sort_values(by=['incident_time','latitude', 'longitude','idc_tickcount_ms'],
|
||||
ascending=[True, False, False, True], inplace=True)
|
||||
elif (gps_heading >= 225) and (gps_heading < 270):
|
||||
df.sort_values(by=['incident_time','longitude', 'latitude','idc_tickcount_ms'],
|
||||
ascending=[True, False, False, True], inplace=True)
|
||||
elif (gps_heading >= 270) and (gps_heading < 315):
|
||||
df.sort_values(by=['incident_time','longitude', 'latitude','idc_tickcount_ms'],
|
||||
ascending=[True, False, True, True], inplace=True)
|
||||
|
||||
# 处理时间戳回绕
|
||||
df['idc_tickcount_ms_tmp'] = df['idc_tickcount_ms'].copy()
|
||||
df.reset_index(inplace=True, drop=True)
|
||||
|
||||
# 确保数据类型一致
|
||||
df['idc_tickcount_ms'] = df['idc_tickcount_ms'].astype(int)
|
||||
|
||||
max_tickcount = df['idc_tickcount_ms'].max()
|
||||
loopcount = 65535 if max_tickcount < 65536 else 2097151
|
||||
period = 0
|
||||
|
||||
# 确保所有值都是整数
|
||||
df['idc_tickcount_ms'] = df['idc_tickcount_ms'].astype(int)
|
||||
|
||||
for i in range(1, len(df)):
|
||||
current_tick = int(df.loc[i, 'idc_tickcount_ms'])
|
||||
prev_tick = int(df.loc[i-1, 'idc_tickcount_ms'])
|
||||
if current_tick < prev_tick:
|
||||
period += 1
|
||||
df.loc[i, 'idc_tickcount_ms_tmp'] = current_tick + period * loopcount
|
||||
|
||||
# 按调整后的时间戳排序
|
||||
df.sort_values(by=["idc_tickcount_ms_tmp"], inplace=True)
|
||||
|
||||
# 转换回列表格式,移除临时添加的排序字段
|
||||
result = []
|
||||
for record in df.to_dict('records'):
|
||||
# 移除临时添加的排序字段
|
||||
cleaned_record = {k: v for k, v in record.items() if k not in ['longitude', 'latitude', 'gps_heading', 'incident_time']}
|
||||
result.append(cleaned_record)
|
||||
|
||||
# print(result)
|
||||
return result
|
||||
|
||||
|
||||
def aggregate_line_list(map_result):
|
||||
# 初始化聚合字典,key为idc_tickcount_ms_int,value为聚合后的数据
|
||||
aggregated_data = {}
|
||||
|
||||
# 遍历所有事件
|
||||
for item in map_result:
|
||||
base_info = item['base_info']
|
||||
# 提取需要的字段
|
||||
tickcount = item['idc_tickcount_ms_tmp']
|
||||
speed = base_info['speed']
|
||||
oneid = base_info['oneid']
|
||||
latitude = base_info['latitude']
|
||||
longitude = base_info['longitude']
|
||||
incident_name = base_info['incident_name']
|
||||
|
||||
# 聚合处理
|
||||
if tickcount not in aggregated_data:
|
||||
# 新的tickcount,初始化记录
|
||||
aggregated_data[tickcount] = {
|
||||
'idc_tickcount_ms_int': tickcount,
|
||||
'speed': speed,
|
||||
'oneid': oneid,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'incident_names': [incident_name] # 用列表收集多个事件名称
|
||||
}
|
||||
else:
|
||||
# 已存在的tickcount,追加事件名称
|
||||
aggregated_data[tickcount]['incident_names'].append(incident_name)
|
||||
|
||||
# 转换为列表形式(按tickcount排序)
|
||||
result = sorted(aggregated_data.values(), key=lambda x: x['idc_tickcount_ms_int'])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_logging_list(map_list):
|
||||
result=[]
|
||||
id=0
|
||||
for item in map_list:
|
||||
# print(item)
|
||||
id+=1
|
||||
row_data={}
|
||||
row_data['id']=id
|
||||
base_info = item['base_info']
|
||||
# print("base_info['incident_time']",base_info['incident_time'])
|
||||
# dt = datetime.strptime(base_info['incident_time'], "%Y-%m-%d %H:%M:%S%z")
|
||||
dt = base_info['incident_time']
|
||||
if not isinstance(dt, datetime):
|
||||
# 只有是字符串时,才解析
|
||||
dt = datetime.strptime(dt, "%Y-%m-%d %H:%M:%S%z")
|
||||
|
||||
row_data['time']=dt.strftime("%H:%M:%S")
|
||||
|
||||
# row_data['coordinates']=base_info['longitude'][:12]+'.'+'\n'+base_info['latitude'][:12]
|
||||
row_data['coordinates'] = str(base_info['longitude'])[:12] + '\n' + str(base_info['latitude'])[:12]
|
||||
row_data['idc_tickcount_ms']=base_info['idc_tickcount_ms_int']
|
||||
row_data['speed']=base_info['speed']
|
||||
row_data['incident']={
|
||||
"name":base_info['incident_name'],
|
||||
"description":base_info['incident_description'],
|
||||
}
|
||||
|
||||
row_data['signals']=[]
|
||||
for key,value in item["signal_data"].items():
|
||||
signals_name_value={}
|
||||
signals_name_value["name"]=key
|
||||
signals_name_value["value"]=value['value']
|
||||
signals_name_value["meaning"]=value['Meaning']
|
||||
row_data['signals'].append(signals_name_value)
|
||||
|
||||
result.append(row_data)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,12 @@
|
||||
from flask import Blueprint, json, jsonify, current_app, request
|
||||
from app.blueprints.incident_gen5.service import (
|
||||
aggregate_and_sort_incidents,
|
||||
aggregate_line_list,
|
||||
get_logging_list,
|
||||
process_incident_data,
|
||||
)
|
||||
from app.services.remote_service import DatabricksQuery
|
||||
from app.utils import convert_incident_time_format
|
||||
|
||||
|
||||
incident_bp = Blueprint("incident", __name__)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,333 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from queue import Queue
|
||||
import threading
|
||||
from flask import Blueprint, json, jsonify, current_app, request
|
||||
from app.blueprints.incident_gen6.service import aggregate_and_sort_incidents_gen6, aggregate_line_list_gen6, convert_to_histro_data_v2, get_logging_list_gen6, process_incident_data_gen6
|
||||
from app.link_wedata_utils import sync_query_tencent_cloud_data
|
||||
from app.utils import decrypt_code
|
||||
|
||||
incident_gen6_bp = Blueprint("incident_gen6_bp", __name__)
|
||||
|
||||
|
||||
@incident_gen6_bp.route("/list1", methods=["POST"])
|
||||
def acc_list1():
|
||||
encrypted = request.headers.get('X-Encrypted-Timestamp')
|
||||
if not encrypted:
|
||||
return jsonify({"msg": "Parameters are Incorrect"}), 400
|
||||
|
||||
encrypt_str=decrypt_code(encrypted)
|
||||
if not encrypt_str:
|
||||
return jsonify({"msg": "Invalid request"}), 400
|
||||
|
||||
data = request.get_json()
|
||||
incident_type = data.get("incident_type", "").strip() or None
|
||||
singal_name = data.get("singal_name", []) or None
|
||||
|
||||
# print("singal_name",singal_name)
|
||||
|
||||
import os
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
json_path = os.path.join(current_dir, "incident_sample.json")
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
events_data = json.load(f)
|
||||
|
||||
histo_path = os.path.join(current_dir, "histro.json")
|
||||
with open(histo_path, "r", encoding="utf-8") as f_histro:
|
||||
histo_data = json.load(f_histro)
|
||||
|
||||
histo_data_new = []
|
||||
for item in histo_data:
|
||||
new_item = {
|
||||
'signal': item['signal'],
|
||||
'time': item['time'],
|
||||
'value': item['value'],
|
||||
'valueExplanation': item['valueExplanation'],
|
||||
'pre_value':item['pre_value'],
|
||||
'pre_valueExplanation':item['pre_valueExplanation'],
|
||||
'is_pre_value_needed':item['is_pre_value_needed']
|
||||
}
|
||||
histo_data_new.append(new_item)
|
||||
|
||||
histo_list=convert_to_histro_data_v2(histo_data_new)
|
||||
events_data_new=[]
|
||||
for item in events_data:
|
||||
new_item = {
|
||||
'oneid': item['oneid'],
|
||||
'incident_name': item['incident_name'],
|
||||
'incident_time': item['incident_time'],
|
||||
'key': item['key'],
|
||||
'signal_value': item['signal_value'],
|
||||
'latitude': item['latitude'],
|
||||
'longitude': item['longitude'],
|
||||
'odometer': item['odometer'],
|
||||
'speed': item['speed']
|
||||
}
|
||||
events_data_new.append(new_item)
|
||||
|
||||
if incident_type == "Parking":
|
||||
# 过滤出 incident_name 等于 Test_I_Park_Trip 的数据
|
||||
events_data_new = [
|
||||
item for item in events_data_new
|
||||
if item.get("incident_name") in ["Test_I_Park_Trip", "Test_I_RMA"]
|
||||
]
|
||||
elif incident_type == "Driving":
|
||||
# 过滤出 incident_name 不等于 Test_I_Park_Trip 的数据
|
||||
events_data_new = [
|
||||
item for item in events_data_new
|
||||
if item.get("incident_name") not in ["Test_I_Park_Trip", "Test_I_RMA"]
|
||||
]
|
||||
|
||||
# event数据
|
||||
events_list=process_incident_data_gen6(events_data_new) # 初始的event list数据,但是没有15分钟处理
|
||||
map_list = aggregate_and_sort_incidents_gen6(events_data_new)
|
||||
# print(f"共 {len(map_list)} 条:")
|
||||
line_list = aggregate_line_list_gen6(map_list)
|
||||
logging_list=get_logging_list_gen6(map_list)
|
||||
response = {
|
||||
"code": 200,
|
||||
"data": {
|
||||
# "events_data":events_data,
|
||||
"events_list": events_list,
|
||||
"map_list": map_list,
|
||||
"line_list": line_list,
|
||||
"logging_list": logging_list,
|
||||
"histo_list":histo_list
|
||||
},
|
||||
"msg": "success",
|
||||
}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
|
||||
@incident_gen6_bp.route("/list", methods=["POST"])
|
||||
def acc_list():
|
||||
# 验证请求头的信息,通过才继续
|
||||
encrypted = request.headers.get('X-Encrypted-Timestamp')
|
||||
if not encrypted:
|
||||
return jsonify({"msg": "Parameters are Incorrect"}), 400
|
||||
|
||||
encrypt_str=decrypt_code(encrypted)
|
||||
if not encrypt_str:
|
||||
return jsonify({"msg": "Invalid request"}), 400
|
||||
|
||||
data = request.get_json()
|
||||
incident_type = data.get("incident_type", "").strip() or None
|
||||
signal_name = data.get("signal_name", [])
|
||||
|
||||
if not data:
|
||||
return jsonify({"code": 400, "data": None, "msg": "请填写有效参数"}), 400
|
||||
|
||||
|
||||
required_fields = ["incident_time", "oneid"]
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return (
|
||||
jsonify({"code": 400, "data": None, "msg": f"缺少必需字段: {field}"}),
|
||||
400,
|
||||
)
|
||||
|
||||
incident_time = data["incident_time"]
|
||||
# 检查是否为非空列表
|
||||
if not isinstance(incident_time, list) or len(incident_time) != 2:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"code": 400,
|
||||
"data": [],
|
||||
"msg": "incident_time必须是包含两个时间的数组",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
|
||||
oneid = data["oneid"]
|
||||
if not isinstance(oneid, str) or not oneid.strip():
|
||||
return jsonify({"code": 400, "data": None, "msg": "oneid必须是非空字符串"}), 400
|
||||
|
||||
|
||||
config = {
|
||||
'region': 'ap-shanghai', # 引擎所在地域
|
||||
'secret_id': current_app.config['WEDATA_SECRET_ID'],
|
||||
'secret_key': current_app.config['WEDATA_SECRET_KEY'],
|
||||
'engine': current_app.config['WEDATA_ENGINE'], # 引擎名称
|
||||
"database":current_app.config['WEDATA_DATABASE']
|
||||
}
|
||||
|
||||
# 生成sql
|
||||
try:
|
||||
incid_sql, incid_params = generate_incidents_sql(incident_time, oneid, signal_name, table_name='gen6_incidents')
|
||||
histo_sql, histo_params = generate_incidents_sql(incident_time, oneid, signal_name, table_name='gen6_histograms')
|
||||
except ValueError as e:
|
||||
return jsonify({"code": 400, "data": None, "msg": f"参数错误:{str(e)}"}), 400
|
||||
|
||||
print("incid_sql", incid_sql)
|
||||
print("histo_sql", histo_sql)
|
||||
|
||||
result_queue = Queue(maxsize=2) # 最多存放2个结果
|
||||
|
||||
# ========== 仅修改这里:给原函数加极简异常防护(不新增任何函数) ==========
|
||||
def safe_query(*args):
|
||||
"""极简包装:仅捕获异常,保证队列必有结果"""
|
||||
try:
|
||||
# 直接调用原函数,参数完全传透(args就是config, sql, params, queue, task_id)
|
||||
sync_query_tencent_cloud_data(*args)
|
||||
except Exception as e: # except Exception as e:
|
||||
# 异常时手动向队列写错误结果(args[3]是queue,args[4]是task_id)
|
||||
args[3].put({"task_id": args[4], "success": False, "data": None, "error": str(e)})
|
||||
|
||||
# try:
|
||||
# error_msg=(
|
||||
# f'数据库连接失败:{str(e)}'
|
||||
# if str(e)
|
||||
# else f'底层系统错误:{type(e).__name__}'
|
||||
# )
|
||||
# args[3].put({"task_id": args[4], "success": False, "data": None, "error":error_msg})
|
||||
# except Exception as query_err:
|
||||
# print(f'任务失败,无法写入队列:{query_err},原始错误是:{e}')
|
||||
|
||||
|
||||
# 5. 创建并启动两个查询线程
|
||||
thread1 = threading.Thread(
|
||||
target=safe_query,
|
||||
args=(config, incid_sql, incid_params, result_queue, "incidents") # task_id 标记为 incidents
|
||||
)
|
||||
thread2 = threading.Thread(
|
||||
target=safe_query,
|
||||
args=(config, histo_sql, histo_params, result_queue, "histo") # task_id 标记为 signals
|
||||
)
|
||||
|
||||
# 启动线程
|
||||
thread1.start()
|
||||
thread2.start()
|
||||
thread1.join()
|
||||
thread2.join()
|
||||
|
||||
# 从队列中提取结果
|
||||
results_dict = {}
|
||||
for _ in range(2):
|
||||
task_result = result_queue.get()
|
||||
results_dict[task_result["task_id"]] = task_result
|
||||
|
||||
# 8. 检查是否有查询失败
|
||||
has_error = False
|
||||
error_msg = ""
|
||||
for task_id, res in results_dict.items():
|
||||
if not res["success"]:
|
||||
has_error = True
|
||||
error_msg = f"查询异常:{res['error'][:80]}" # 错误信息保留前80
|
||||
|
||||
if has_error:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"msg": f"查询失败:{error_msg}",
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
|
||||
# 9. 获取两个表的查询结果(业务处理核心)
|
||||
events_data = results_dict["incidents"]["data"]
|
||||
histo_data = results_dict["histo"]["data"]
|
||||
|
||||
histo_list=convert_to_histro_data_v2(histo_data)
|
||||
|
||||
if incident_type == "Parking":
|
||||
# 过滤出 incident_name 等于 Test_I_Park_Trip 的数据
|
||||
events_data = [
|
||||
item for item in events_data
|
||||
if item.get("incident_name") in ["Test_I_Park_Trip", "Test_I_RMA"]
|
||||
]
|
||||
elif incident_type == "Driving":
|
||||
# 过滤出 incident_name 不等于 Test_I_Park_Trip 的数据
|
||||
events_data = [
|
||||
item for item in events_data
|
||||
if item.get("incident_name") not in ["Test_I_Park_Trip", "Test_I_RMA"]
|
||||
]
|
||||
|
||||
# event数据
|
||||
events_list=process_incident_data_gen6(events_data) # 初始的event list数据,但是没有15分钟处理
|
||||
map_list = aggregate_and_sort_incidents_gen6(events_data)
|
||||
line_list = aggregate_line_list_gen6(map_list)
|
||||
logging_list=get_logging_list_gen6(map_list)
|
||||
response = {
|
||||
"code": 200,
|
||||
"data": {
|
||||
# "events_data":events_data,
|
||||
"events_list": events_list,
|
||||
"map_list": [],
|
||||
"line_list": line_list,
|
||||
"logging_list": logging_list,
|
||||
"histo_list":histo_list
|
||||
},
|
||||
"msg": "success",
|
||||
}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
def generate_incidents_sql(incident_time: list, oneid: str, signal_list: list, table_name: str) -> tuple:
|
||||
"""
|
||||
简化版:仅处理用到的两个表,字段名映射极简,去掉冗余扩展
|
||||
- gen6_incidents:时间=incident_time,ID字段=oneid
|
||||
- gen6_histograms:时间=time,ID字段=vin
|
||||
"""
|
||||
# 处理UTC时间(带T/Z)转本地时间(东八区)
|
||||
def convert_utc_to_cst(utc_time_str):
|
||||
try:
|
||||
# 解析UTC时间(支持带T/Z的格式)
|
||||
utc_dt = datetime.fromisoformat(utc_time_str.replace('Z', '+00:00'))
|
||||
# 转东八区(UTC+8)
|
||||
cst_tz = timezone(timedelta(hours=8))
|
||||
cst_dt = utc_dt.astimezone(cst_tz)
|
||||
# 转为数据库兼容的格式(YYYY-MM-DD HH:mm:ss.fff)
|
||||
return cst_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] # 保留3位毫秒
|
||||
except Exception as e:
|
||||
# 解析失败则返回原时间(兼容测试参数)
|
||||
return utc_time_str
|
||||
|
||||
# 转换时间范围
|
||||
incident_time = [convert_utc_to_cst(t) for t in incident_time]
|
||||
|
||||
# 1. 极简字段映射(只保留用到的表,避免KeyError)
|
||||
FIELD_MAP = {
|
||||
"gen6_incidents": {"time": "incident_time",
|
||||
"id": "oneid",
|
||||
# "select_fields": "oneid, incident_name,incident_time,`key`,signal_value,latitude,longitude,odometer,speed" },
|
||||
"select_fields": "*" },
|
||||
"gen6_histograms": {"time": "time",
|
||||
"id": "vin",
|
||||
"select_fields": "signal, time, value, valueExplanation"},
|
||||
}
|
||||
|
||||
# 2. 基础校验(只校验用到的表,去掉冗余)
|
||||
if table_name not in FIELD_MAP:
|
||||
raise ValueError(f"仅支持查询表:{list(FIELD_MAP.keys())}")
|
||||
if len(incident_time) != 2 or not all(incident_time):
|
||||
raise ValueError("incident_time必须是包含两个非空时间的列表")
|
||||
if not isinstance(oneid, str) or not oneid.strip():
|
||||
raise ValueError("oneid必须是非空字符串")
|
||||
|
||||
# 3. 提取当前表的字段名(核心:只用两行完成字段替换)
|
||||
time_field = FIELD_MAP[table_name]["time"]
|
||||
id_field = FIELD_MAP[table_name]["id"]
|
||||
select_fields = FIELD_MAP[table_name]["select_fields"]
|
||||
|
||||
# 4. 拼接SQL条件(极简逻辑,去掉冗余注释)
|
||||
conditions = [
|
||||
f"{time_field} BETWEEN %s AND %s",
|
||||
f"{id_field} = %s"
|
||||
]
|
||||
params = [incident_time[0], incident_time[1], oneid]
|
||||
|
||||
# 仅gen6_histograms处理signal条件
|
||||
if table_name == "gen6_histograms":
|
||||
if signal_list:
|
||||
signal_placeholders = ",".join(["%s"] * len(signal_list))
|
||||
conditions.append(f"signal IN ({signal_placeholders})")
|
||||
params += signal_list
|
||||
else:
|
||||
conditions.append("signal = %s")
|
||||
params.append("DAS_DTR_UI_Stat_ST3")
|
||||
|
||||
# 5. 生成最终SQL(简化格式化)
|
||||
sql = f"SELECT {select_fields} FROM {table_name} WHERE {' AND '.join(conditions)}"
|
||||
return sql, params
|
||||
@@ -0,0 +1,427 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
|
||||
def process_incident_data_gen6(test_val):
|
||||
"""
|
||||
处理事故数据,按照incident_name去重(移除原incident_time组合去重),移除idc_tickcount_ms排序,其他逻辑不变。
|
||||
|
||||
Args:
|
||||
test_val (list): 原始数据列表
|
||||
|
||||
Returns:
|
||||
dict: 格式化后的数据
|
||||
"""
|
||||
|
||||
# 1. 仅按照 incident_name 去重(核心修改点1:移除incident_time组合)
|
||||
unique_data_dict = {}
|
||||
for item in test_val:
|
||||
name = item.get("incident_name")
|
||||
# 仅判断incident_name是否存在,不再依赖incident_time
|
||||
if name:
|
||||
# 仅用incident_name作为去重键
|
||||
key = name
|
||||
if key not in unique_data_dict:
|
||||
unique_data_dict[key] = item
|
||||
|
||||
# 获取去重后的数据列表(核心修改点2:移除排序步骤)
|
||||
unique_data_list = list(unique_data_dict.values())
|
||||
|
||||
# 3. 提取指定字段(原逻辑完全保留,仅将遍历对象从sorted_data改为unique_data_list)
|
||||
processed_events_data = []
|
||||
for index, item in enumerate(unique_data_list, 1):
|
||||
incident_time_str = item.get("incident_time")
|
||||
|
||||
if not incident_time_str:
|
||||
continue
|
||||
|
||||
try:
|
||||
# 解析新的时间格式 "2025-11-30 20:51:35.684000"
|
||||
# 先尝试直接解析完整格式
|
||||
try:
|
||||
# 移除毫秒部分,只保留到秒
|
||||
base_time_str = incident_time_str.split('.')[0]
|
||||
incident_datetime = datetime.strptime(base_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
# 如果失败,尝试其他可能的格式
|
||||
try:
|
||||
incident_datetime = datetime.fromisoformat(incident_time_str.replace('Z', '+00:00'))
|
||||
except:
|
||||
# 再次失败,使用当前时间
|
||||
incident_datetime = datetime.now()
|
||||
|
||||
# 格式化日期和时间
|
||||
date_str_result = incident_datetime.strftime("%Y-%m-%d")
|
||||
time_str_result = incident_datetime.strftime("%H:%M:%S")
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"Warning: Failed to parse incident_time '{incident_time_str}': {e}")
|
||||
# 使用默认值
|
||||
date_str_result = item.get("dt", "").split(' ')[0] if item.get("dt") else datetime.now().strftime("%Y-%m-%d")
|
||||
time_str_result = "00:00:00"
|
||||
|
||||
# 创建新的字典
|
||||
processed_item = {
|
||||
"id": index,
|
||||
"oneid": item.get("oneid", ""),
|
||||
"date": date_str_result,
|
||||
"time": time_str_result,
|
||||
"incident_name": item.get("incident_name", ""),
|
||||
"incident_description": "",
|
||||
}
|
||||
processed_events_data.append(processed_item)
|
||||
|
||||
# 4. 构建最终返回的字典
|
||||
total_count = len(processed_events_data)
|
||||
result = {"data": processed_events_data, "total": total_count}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def aggregate_and_sort_incidents_gen6(raw_data):
|
||||
"""
|
||||
简化版本:按(incident_time + incident_name)精确聚合
|
||||
适用于同一事件的incident_time完全一致的情况
|
||||
"""
|
||||
aggregate_dict = {}
|
||||
|
||||
for item in raw_data:
|
||||
incident_time = item.get("incident_time", "1970-01-01 00:00:00.000")
|
||||
incident_name = item.get("incident_name", "unknown_incident")
|
||||
aggregate_key = (incident_time, incident_name)
|
||||
|
||||
if aggregate_key not in aggregate_dict:
|
||||
aggregate_dict[aggregate_key] = {
|
||||
"base_info": {
|
||||
"oneid": item.get("oneid"),
|
||||
"incident_time": incident_time,
|
||||
"speed": item.get("speed"),
|
||||
"gps_heading": item.get("gps_heading"),
|
||||
"incident_name": incident_name,
|
||||
"latitude": item.get("latitude"),
|
||||
"longitude": item.get("longitude"),
|
||||
"odometer": item.get("odometer"),
|
||||
"sw_version": item.get("sw_version"),
|
||||
"carmodel": item.get("carmodel"),
|
||||
"session_id": item.get("session_id"),
|
||||
"dt": item.get("dt")
|
||||
},
|
||||
"signal_data": {}
|
||||
}
|
||||
|
||||
signal_key = item.get("key", "unknown_key")
|
||||
aggregate_dict[aggregate_key]["signal_data"][signal_key] = {
|
||||
'signal_value': item.get('signal_value', '0.0'),
|
||||
"odometer": item.get('odometer', '0.0')
|
||||
}
|
||||
|
||||
# 按incident_time排序
|
||||
sorted_result = sorted(
|
||||
aggregate_dict.values(),
|
||||
key=lambda x: x["base_info"]["incident_time"]
|
||||
)
|
||||
|
||||
return sorted_result
|
||||
|
||||
|
||||
def aggregate_line_list_gen6(map_list):
|
||||
"""
|
||||
按incident_time聚合事件数据,生成时间线列表
|
||||
改进点:
|
||||
1. 使用正确的时间字段名
|
||||
2. 支持精确的时间排序
|
||||
3. 处理同一时间点的多条记录
|
||||
4. 添加健壮的错误处理
|
||||
"""
|
||||
aggregated_data = {}
|
||||
|
||||
# 遍历所有事件
|
||||
for item in map_list:
|
||||
try:
|
||||
base_info = item['base_info']
|
||||
|
||||
# 提取需要的字段
|
||||
incident_time_str = base_info.get('incident_time', '1970-01-01 00:00:00.000')
|
||||
speed = base_info.get('speed', '0')
|
||||
oneid = base_info.get('oneid', '')
|
||||
latitude = base_info.get('latitude', '0')
|
||||
longitude = base_info.get('longitude', '0')
|
||||
incident_name = base_info.get('incident_name', 'Unknown Incident')
|
||||
|
||||
# 创建聚合键(使用完整时间字符串)
|
||||
aggregate_key = incident_time_str
|
||||
|
||||
# 聚合处理
|
||||
if aggregate_key not in aggregated_data:
|
||||
# 新的时间点,初始化记录
|
||||
aggregated_data[aggregate_key] = {
|
||||
'incident_time': incident_time_str, # 使用正确的字段名
|
||||
'speed': speed,
|
||||
'oneid': oneid,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'incident_names': [incident_name], # 使用复数形式,表示可能有多个
|
||||
'record_count': 1 # 记录该时间点的记录数
|
||||
}
|
||||
else:
|
||||
# 已存在的时间点
|
||||
existing = aggregated_data[aggregate_key]
|
||||
|
||||
# 可选:处理同一时间点不同记录的字段冲突
|
||||
# 这里选择保留第一条的速度,但如果需要可以取平均值或最新值
|
||||
# existing['speed'] = str((float(existing['speed']) + float(speed)) / 2)
|
||||
|
||||
# 添加事件名称(去重)
|
||||
if incident_name not in existing['incident_names']:
|
||||
existing['incident_names'].append(incident_name)
|
||||
|
||||
existing['record_count'] += 1
|
||||
|
||||
except (KeyError, TypeError) as e:
|
||||
print(f"Warning: Skipping item due to missing field: {e}")
|
||||
continue
|
||||
|
||||
# 转换为列表形式,并按时间正序排序
|
||||
def get_sort_time(record):
|
||||
try:
|
||||
time_str = record['incident_time']
|
||||
if '.' in time_str:
|
||||
# 处理带毫秒的时间
|
||||
return datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S.%f")
|
||||
else:
|
||||
# 处理不带毫秒的时间
|
||||
return datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"Warning: Failed to parse time '{record['incident_time']}' for sorting: {e}")
|
||||
return datetime(1970, 1, 1) # 返回默认时间
|
||||
|
||||
# 按incident_time正序排序(从小到大)
|
||||
result = sorted(aggregated_data.values(), key=get_sort_time)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_logging_list_gen6(map_list):
|
||||
"""
|
||||
处理map_list数据,生成logging列表,并按incident_time正序排序
|
||||
同时对每个记录的signals按name字段进行字符顺序排序
|
||||
"""
|
||||
# 1. 首先对map_list按incident_time进行排序
|
||||
def get_sort_key(item):
|
||||
try:
|
||||
incident_time_str = item['base_info']['incident_time']
|
||||
# 处理时间字符串,统一格式
|
||||
if '.' in incident_time_str:
|
||||
# 保留到毫秒级别进行排序
|
||||
base_time_str = incident_time_str.split('.')[0]
|
||||
milliseconds = incident_time_str.split('.')[1][:6] # 取最多6位毫秒
|
||||
full_time_str = f"{base_time_str}.{milliseconds}"
|
||||
return datetime.strptime(full_time_str, "%Y-%m-%d %H:%M:%S.%f")
|
||||
else:
|
||||
return datetime.strptime(incident_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
print(f"Warning: Failed to parse incident_time for sorting: {e}")
|
||||
# 解析失败时返回一个很早的时间,确保这些记录排在最后
|
||||
return datetime(1970, 1, 1)
|
||||
|
||||
# 按incident_time正序排序(从小到大)
|
||||
sorted_map_list = sorted(map_list, key=get_sort_key)
|
||||
|
||||
# 2. 处理排序后的数据
|
||||
result = []
|
||||
id = 0
|
||||
for item in sorted_map_list:
|
||||
id += 1
|
||||
row_data = {}
|
||||
row_data['id'] = id
|
||||
base_info = item['base_info']
|
||||
|
||||
# 处理时间格式 - 新格式为 "2025-11-30 20:23:05.394000"
|
||||
incident_time_str = base_info['incident_time']
|
||||
try:
|
||||
# 尝试解析新格式的时间
|
||||
if '.' in incident_time_str:
|
||||
# 移除毫秒部分,只保留到秒
|
||||
base_time_str = incident_time_str.split('.')[0]
|
||||
dt = datetime.strptime(base_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
dt = datetime.strptime(incident_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
row_data['time'] = dt.strftime("%H:%M:%S")
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"Warning: Failed to parse incident_time '{incident_time_str}': {e}")
|
||||
# 使用默认时间
|
||||
row_data['time'] = "00:00:00"
|
||||
|
||||
# 处理经纬度 - 可能为 "NULL" 字符串
|
||||
longitude = str(base_info.get('longitude', '')).strip()
|
||||
latitude = str(base_info.get('latitude', '')).strip()
|
||||
|
||||
# 处理 "NULL" 字符串
|
||||
if longitude.lower() == "null" or longitude == "":
|
||||
longitude = "N/A"
|
||||
if latitude.lower() == "null" or latitude == "":
|
||||
latitude = "N/A"
|
||||
|
||||
# 格式化坐标显示
|
||||
row_data['coordinates'] = f"{longitude[:12]}\n{latitude[:12]}"
|
||||
|
||||
# 处理idc_tickcount_ms
|
||||
row_data['idc_tickcount_ms'] = base_info.get('idc_tickcount_ms_int', 0)
|
||||
|
||||
# 处理速度 - 可能为 "NULL" 字符串
|
||||
speed = str(base_info.get('speed', '')).strip()
|
||||
if speed.lower() == "null" or speed == "":
|
||||
speed = "0"
|
||||
row_data['speed'] = speed
|
||||
|
||||
# 处理incident信息 - 新格式没有Incident__description
|
||||
row_data['incident'] = {
|
||||
"name": base_info.get('incident_name', 'Unknown Incident'),
|
||||
"description": "" # 使用incident_name作为描述
|
||||
}
|
||||
|
||||
# 处理信号数据
|
||||
row_data['signals'] = []
|
||||
for key, value in item["signal_data"].items():
|
||||
signals_name_value = {}
|
||||
signals_name_value["name"] = key
|
||||
|
||||
# 新格式使用signal_value字段
|
||||
signal_value = value.get('signal_value', '')
|
||||
if signal_value is None or signal_value == "":
|
||||
signal_value = "N/A"
|
||||
signals_name_value["value"] = signal_value
|
||||
|
||||
# 匹配到meanning的值
|
||||
meaning_result=match_meaning(key,signal_value)
|
||||
# 新格式没有Meaning字段,使用空字符串
|
||||
signals_name_value["meaning"] = meaning_result
|
||||
|
||||
row_data['signals'].append(signals_name_value)
|
||||
|
||||
# === 新增:对signals列表按name字段进行字符顺序排序 ===
|
||||
row_data['signals'] = sorted(row_data['signals'], key=lambda x: x['name'])
|
||||
|
||||
result.append(row_data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def convert_to_histro_data(raw_data):
|
||||
"""
|
||||
转换原始数据为前端折线图格式(time保留%Y-%m-%d %H:%M:%S.%f格式)
|
||||
:param raw_data: 原始字典列表
|
||||
:return: 前端所需格式的列表
|
||||
"""
|
||||
# 步骤1:按signal分组
|
||||
signal_groups = defaultdict(list)
|
||||
for item in raw_data:
|
||||
signal_groups[item["signal"]].append(item)
|
||||
|
||||
# 步骤2:处理每个分组,构造最终数据
|
||||
chart_data = []
|
||||
for signal_name, items in signal_groups.items():
|
||||
# 修复1:统一使用start_ts字段进行排序(因为这是时间轴数据)
|
||||
def sort_by_start_ts(item):
|
||||
return datetime.strptime(item["time"], "%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
sorted_items = sorted(items, key=sort_by_start_ts)
|
||||
|
||||
# 修复3:统一使用time作为X轴时间数据(与排序字段一致)
|
||||
time_list = [item["time"] for item in sorted_items] # 使用start_ts作为时间轴
|
||||
values_list = [float(item["value"]) for item in sorted_items] # value转数值
|
||||
|
||||
# 子步骤3:构造当前signal的折线图数据
|
||||
chart_item = {
|
||||
"signalName": signal_name,
|
||||
"time": time_list, # X轴时间数据
|
||||
"values": values_list # Y轴数值数据
|
||||
}
|
||||
chart_data.append(chart_item)
|
||||
|
||||
return chart_data
|
||||
|
||||
|
||||
def convert_to_histro_data_v2(raw_data):
|
||||
"""
|
||||
转换原始数据为前端图表格式 {x: 时间, y: 数值, value: 数值}
|
||||
:param raw_data: 原始字典列表,需包含 "signal"、"time"、"value" 字段
|
||||
:return: 字典(key=signal名称,value=对应{x,y,value}格式的列表);
|
||||
若需合并所有signal为单列表,可取消注释对应逻辑
|
||||
"""
|
||||
# 步骤1:按signal字段分组
|
||||
signal_groups = defaultdict(list)
|
||||
for item in raw_data:
|
||||
signal_groups[item["signal"]].append(item)
|
||||
|
||||
# 步骤2:处理每个分组,构造目标格式数据
|
||||
chart_data = {}
|
||||
for signal_name, items in signal_groups.items():
|
||||
# 按time字段排序(保证时间轴顺序)
|
||||
def sort_by_time(item):
|
||||
return datetime.strptime(item["time"], "%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
sorted_items = sorted(items, key=sort_by_time)
|
||||
|
||||
# 构造 {x: 时间, y: 数值, value: 数值} 格式的列表
|
||||
signal_item_list = []
|
||||
for item in sorted_items:
|
||||
val = float(item["value"]) # 确保数值类型为浮点数
|
||||
signal_item_list.append({
|
||||
"x": item["time"], # x轴:时间字符串(对应示例中的"销量4"类标签)
|
||||
"y": item['valueExplanation'], # y轴:原始value数值
|
||||
"value": val # value字段:与y轴数值一致(匹配示例格式)
|
||||
})
|
||||
|
||||
# ========== 新增逻辑开始 ==========
|
||||
# 获取排序后第一条原始数据
|
||||
first_raw_item = sorted_items[0]
|
||||
# 判断is_pre_value_needed是否非空(处理空字符串、None、"NULL"等情况)
|
||||
is_needed = first_raw_item.get("is_pre_value_needed", "")
|
||||
if is_needed and is_needed.strip() and is_needed != "NULL":
|
||||
try:
|
||||
# 提取pre相关值并转换类型
|
||||
pre_val = float(first_raw_item["pre_value"])
|
||||
pre_explanation = first_raw_item["pre_valueExplanation"]
|
||||
first_time = first_raw_item["time"] # 在第一条数据的时间基础上减10秒
|
||||
first_time_dt = datetime.strptime(first_time, "%Y-%m-%d %H:%M:%S.%f")
|
||||
pre_time_dt = first_time_dt - timedelta(seconds=10)
|
||||
pre_time_str = pre_time_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
# 构造前置数据项
|
||||
pre_item = {
|
||||
"x": '', # x轴:第一条数据的时间
|
||||
"y": pre_explanation, # y轴:pre_value对应的说明
|
||||
"value": pre_val, # value字段:pre_value的数值
|
||||
"show":1
|
||||
}
|
||||
|
||||
# 插入到列表最前面
|
||||
signal_item_list.insert(0, pre_item)
|
||||
except KeyError as e:
|
||||
print(f"警告:第一条数据缺少{e}字段,跳过前置数据插入")
|
||||
except ValueError as e:
|
||||
print(f"警告:pre_value转换浮点数失败({e}),跳过前置数据插入")
|
||||
# ========== 新增逻辑结束 ==========
|
||||
|
||||
# 按signal名称存储结果
|
||||
chart_data[signal_name] = signal_item_list
|
||||
|
||||
return chart_data
|
||||
|
||||
|
||||
def match_meaning(singla_name,value):
|
||||
import os
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
json_path = os.path.join(current_dir, "singal_value.json")
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
mean_data = json.load(f)
|
||||
|
||||
singla=singla_name.split('.')[-1]
|
||||
meaning_state=mean_data.get(singla,None)
|
||||
if meaning_state:
|
||||
return meaning_state[value] if meaning_state[value] else ''
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"l2p_function_state": {
|
||||
"0.0": "kOff",
|
||||
"1.0": "kStandby",
|
||||
"2.0": "kPassive",
|
||||
"3.0": "kActive",
|
||||
"4.0": "kOverrule",
|
||||
"5.0": "kError"
|
||||
},
|
||||
"mmt_l2pp_function_state": {
|
||||
"0.0": "Off",
|
||||
"1.0": "Deactivated",
|
||||
"2.0": "Preselected",
|
||||
"3.0": "Active",
|
||||
"4.0": "Overrule",
|
||||
"5.0": "Error",
|
||||
"6.0": "Unknown"
|
||||
},
|
||||
"adas_cua_funcroadclass_st3": {
|
||||
"0.0": "Unknown",
|
||||
"1.0": "Freeway",
|
||||
"2.0": "MainStreet/CitySpeedway",
|
||||
"3.0": "NationalRoad",
|
||||
"4.0": "ProvinceRoad/CountyRoad",
|
||||
"5.0": "MainRoad",
|
||||
"6.0": "SecondaryRoad/Commonroad/RuralRoad/InCountyRoad/Pathway",
|
||||
"7.0": "N/A"
|
||||
},
|
||||
"l2p_deactivation_reason": {
|
||||
"0.0": "kIdle",
|
||||
"1.0": "kDeactivatedByUser",
|
||||
"2.0": "kOutOfODD",
|
||||
"3.0": "kTollStation",
|
||||
"4.0": "kRouteEnd",
|
||||
"5.0": "kHeavyRain",
|
||||
"6.0": "kError",
|
||||
"7.0": "kOverSpeed",
|
||||
"8.0": "kOther"
|
||||
},
|
||||
"hoswd_handson_stat_master": {
|
||||
"0.0": "NOT_TOUCH",
|
||||
"1.0": "TOUCH",
|
||||
"2.0": "SLIGHTLY_TOUCH",
|
||||
"3.0": "GRASP",
|
||||
"4.0": "DOUBLE_GRABBED",
|
||||
"5.0": "SLIGHTLY_GRABBED",
|
||||
"6.0": "SLIGHTLY_DOUBLE_GRABBED",
|
||||
"7.0": "SNA"
|
||||
},
|
||||
"das_dtr_ui_stat_st3": {
|
||||
"0.0": "OFF",
|
||||
"1.0": "PRESEL",
|
||||
"2.0": "ACTV_SET_SPEED_CNTRL",
|
||||
"3.0": "ACTV_SPEED_LMT_CNTRL",
|
||||
"4.0": "ACTV_DSTNC_CNTRL_EGO_LNE",
|
||||
"5.0": "ACTV_DSTNC_CNTRL_NGHBR_LNE",
|
||||
"6.0": "ACTV_CRV_CTRL",
|
||||
"7.0": "ACTV_DRVAWAY_RDY",
|
||||
"8.0": "PASSIVE",
|
||||
"9.0": "ACTV_NOT_POSSBL",
|
||||
"13.0": "HIDDEN",
|
||||
"14.0": "ERROR",
|
||||
"15.0": "SNA"
|
||||
},
|
||||
"fcw_warning": {
|
||||
"0.0": "None",
|
||||
"1.0": "LEVEL_1",
|
||||
"2.0": "LEVEL_2",
|
||||
"3.0": "LEVEL_3",
|
||||
"4.0": "LEVEL_4",
|
||||
"5.0": "COUNT",
|
||||
"255.0": "FORCE32"
|
||||
},
|
||||
"aeb_aeb_event_type": {
|
||||
"0.0": "AEB_NONE",
|
||||
"1.0": "AEB_PARTIAL",
|
||||
"2.0": "AEB_FULL",
|
||||
"3.0": "AEB_HOLD",
|
||||
"4.0": "AEB_LIM",
|
||||
"5.0": "AEB_EH",
|
||||
"255.0": "AEB_FORCE32"
|
||||
},
|
||||
"pt4_ptcoor_drvposn_stat": {
|
||||
"0.0": "Unknown",
|
||||
"1.0": "D",
|
||||
"2.0": "N",
|
||||
"3.0": "R",
|
||||
"4.0": "P"
|
||||
},
|
||||
"das_cms_acustwarn_rq_st3": {
|
||||
"0.0": "NO_RQ",
|
||||
"1.0": "RQ",
|
||||
"3.0": "SNA"
|
||||
},
|
||||
"das_turnind_rq": {
|
||||
"0.0": "IDLE",
|
||||
"1.0": "LEFT",
|
||||
"2.0": "RIGHT"
|
||||
},
|
||||
"brkpdl_stat": {
|
||||
"0.0": "Pedal upstopped",
|
||||
"1.0": "Pedal pressed",
|
||||
"3.0": "not defined"
|
||||
},
|
||||
"aas_actvas_rq": {
|
||||
"0.0": "IDLE",
|
||||
"1.0": "NOT_ON_ROAD",
|
||||
"2.0": "DROW_LONG",
|
||||
"3.0": "MCRSLP_DSTRCT",
|
||||
"4.0": "SLP_UNRSP_DRIVER",
|
||||
"7.0": "SNA"
|
||||
},
|
||||
"aeb_state": {
|
||||
"0.0": "kStartUp",
|
||||
"1.0": "kAvailable",
|
||||
"2.0": "kActive",
|
||||
"3.0": "kActiveError",
|
||||
"4.0": "kAborting",
|
||||
"5.0": "kAbortingError",
|
||||
"6.0": "kInitSilent",
|
||||
"7.0": "kInit",
|
||||
"8.0": "kBlocked",
|
||||
"9.0": "kBlockedSilent",
|
||||
"10.0": "kError",
|
||||
"11.0": "kOff",
|
||||
"12.0": "kOffError",
|
||||
"13.0": "kNotAvailable",
|
||||
"14.0": "kCodingError"
|
||||
},
|
||||
"mpic_d_gaze_roi_idx": {
|
||||
"0.0": "NO_GAZE",
|
||||
"1.0": "ROI_01",
|
||||
"2.0": "ROI_02",
|
||||
"3.0": "ROI_03",
|
||||
"4.0": "ROI_04",
|
||||
"5.0": "ROI_05",
|
||||
"6.0": "ROI_06",
|
||||
"7.0": "ROI_07",
|
||||
"8.0": "ROI_08",
|
||||
"9.0": "ROI_09",
|
||||
"10.0": "ROI_10",
|
||||
"11.0": "ROI_11",
|
||||
"12.0": "ROI_12",
|
||||
"13.0": "ROI_13",
|
||||
"14.0": "ROI_14",
|
||||
"15.0": "ROI_15",
|
||||
"16.0": "ROI_16",
|
||||
"17.0": "ROI_17",
|
||||
"18.0": "ROI_18",
|
||||
"19.0": "ROI_19",
|
||||
"20.0": "ROI_20",
|
||||
"21.0": "ROI_21",
|
||||
"22.0": "ROI_22",
|
||||
"23.0": "ROI_23",
|
||||
"24.0": "ROI_24",
|
||||
"25.0": "ROI_25",
|
||||
"26.0": "ROI_26",
|
||||
"27.0": "ROI_27",
|
||||
"28.0": "ROI_28",
|
||||
"29.0": "ROI_29",
|
||||
"30.0": "ROI_30",
|
||||
"31.0": "SNA"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
from flask import Blueprint, current_app, redirect, request, jsonify
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.models import User, Role, UserRole # 导入模型类
|
||||
from app.utils import verify_password # 导入密码加密工具
|
||||
from app import db
|
||||
from flask_jwt_extended import (
|
||||
create_access_token,
|
||||
jwt_required,
|
||||
get_jwt_identity,
|
||||
) # 导入JWT相关工具
|
||||
|
||||
login_bp = Blueprint("login", __name__)
|
||||
|
||||
|
||||
@login_bp.route("/login1", methods=["POST"])
|
||||
def login():
|
||||
# 初始化响应结构(避免未定义变量问题)F
|
||||
response = {"code": 200, "message": "success"}
|
||||
|
||||
# 1. 获取登录凭证
|
||||
data = request.get_json()
|
||||
username = data.get("username")
|
||||
password = data.get("password")
|
||||
|
||||
# 2. 验证必填字段
|
||||
if not username or not password:
|
||||
response["code"] = 400
|
||||
response["message"] = "用户名和密码不能为空"
|
||||
return jsonify(response)
|
||||
|
||||
# 3. 查询用户(包含状态验证)
|
||||
user = User.query.filter_by(
|
||||
username=username, status=1
|
||||
).first() # 直接过滤启用状态的用户
|
||||
|
||||
# 4. 验证用户存在性和密码
|
||||
if not user or not verify_password(
|
||||
user.password, password
|
||||
): # 注意:verify_password(加密密码, 明文密码)
|
||||
response["code"] = 401
|
||||
response["message"] = "用户名或密码错误"
|
||||
return jsonify(response)
|
||||
|
||||
# 5. 生成JWT令牌
|
||||
access_token = create_access_token(identity=str(user.id))
|
||||
|
||||
# 6. 查询用户关联的角色(处理多角色情况)
|
||||
# 方式:查询用户所有角色名称,返回数组
|
||||
roles = (
|
||||
db.session.query(Role.name)
|
||||
.join(UserRole, Role.id == UserRole.role_id)
|
||||
.filter(UserRole.user_id == user.id) # 关联当前登录用户
|
||||
.all()
|
||||
)
|
||||
print("roles", roles)
|
||||
# 提取角色名称到数组(如果没有角色,返回空数组)
|
||||
role_list = [role.name for role in roles] if roles else []
|
||||
|
||||
# 7. 按指定格式包装结果
|
||||
result = {
|
||||
"accessToken": access_token,
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"nickname": user.username, # 假设nickname用username替代(如果表中有nickname字段可直接替换)
|
||||
"avatar": user.avatar
|
||||
or "https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png", # 默认为空时用默认头像
|
||||
"roles": role_list, # 角色数组(单角色时为['角色名'],多角色时为['角色1','角色2'])
|
||||
}
|
||||
|
||||
result = {"code": 200, "data": {"token": access_token}, "msg": "登录成功"}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@login_bp.route("/info", methods=["GET"])
|
||||
@jwt_required() # 要求登录状态(使用JWT验证)
|
||||
def get_user_info():
|
||||
try:
|
||||
# 获取当前登录用户的ID(假设使用JWT存储用户ID)
|
||||
current_user_id = get_jwt_identity()
|
||||
|
||||
# 查询数据库获取用户信息
|
||||
user = User.query.get(current_user_id)
|
||||
if not user:
|
||||
return jsonify({"code": 404, "msg": "用户不存在", "data": None}), 404
|
||||
|
||||
# 提取用户角色(假设Role模型有name字段存储角色名称)
|
||||
roles = [role.name for role in user.roles] if user.roles else []
|
||||
|
||||
# 构造返回数据
|
||||
res = {
|
||||
"code": 200,
|
||||
"msg": "获取成功",
|
||||
"data": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"nickname": user.username, # 模型中没有nickname字段,暂用username代替
|
||||
"avatar": user.avatar
|
||||
or "https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png", # 默认头像
|
||||
"roles": roles,
|
||||
},
|
||||
}
|
||||
return jsonify(res)
|
||||
|
||||
except Exception as e:
|
||||
# 异常处理
|
||||
return (
|
||||
jsonify({"code": 500, "msg": f"获取用户信息失败:{str(e)}", "data": None}),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
@login_bp.route("/get_record", methods=["GET"])
|
||||
def get_record_code():
|
||||
return jsonify({
|
||||
"code": 200, # 200表示成功
|
||||
"msg": "查询成功",
|
||||
"data": current_app.config['HEADERS_SECRET_KEY'] # 调用模型自带的转字典方法
|
||||
}), 200
|
||||
|
||||
|
||||
|
||||
@login_bp.route("/login")
|
||||
def authorization():
|
||||
"""
|
||||
授权入口。
|
||||
将用户浏览器重定向到 SSO 服务器的授权页面。
|
||||
"""
|
||||
# print("login123")
|
||||
CLIENT_ID = current_app.config["CLIENT_ID"]
|
||||
HOST = current_app.config["SS0_HOST"]
|
||||
REDIRECT_URL = current_app.config["REDIRECT_URL"]
|
||||
|
||||
# 1. 构造重定向到 SSO 授权页面的 URL
|
||||
auth_url = (
|
||||
f"{HOST}v1/auth"
|
||||
f"?response_type=code"
|
||||
f"&client_id={CLIENT_ID}"
|
||||
f"&scope=groups+openid+email+profile"
|
||||
f"&redirect_uri={REDIRECT_URL}"
|
||||
)
|
||||
# print(auth_url)
|
||||
# 2. 执行重定向
|
||||
return redirect(auth_url)
|
||||
@@ -0,0 +1,285 @@
|
||||
import base64
|
||||
from flask import (
|
||||
Blueprint,
|
||||
make_response,
|
||||
redirect,
|
||||
request,
|
||||
jsonify,
|
||||
session,
|
||||
current_app,
|
||||
)
|
||||
import jwt
|
||||
import requests
|
||||
|
||||
|
||||
oauth_bp = Blueprint("oauth", __name__)
|
||||
|
||||
|
||||
# 客户端 ID
|
||||
# CLIENT_ID = "A17336FC-A8D7-4CEA-8777-384BFEABABC1"
|
||||
# BASE64_CLIENT = "Basic QTE3MzM2RkMtQThENy00Q0VBLTg3NzctMzg0QkZFQUJBQkMxOlU3LUMxQlpRZEw4VjVjLnp+cUFfNjRTTzJsOUYzSjBv"
|
||||
# # SSO 授权服务器地址
|
||||
# # "https://ssoalpha.dvb.corpinter.net/"
|
||||
# HOST = "https://ssoalpha.dvb.corpinter.net.cn/"
|
||||
# # 授权成功后,SSO 服务器重定向回你的应用的地址 (必须与 SSO 服务器上配置的一致)
|
||||
# REDIRECT_URL = "http://localhost:5221/report"
|
||||
# # 用户登录成功后,最终重定向到的前端页面地址
|
||||
# DOMAIN = "http://localhost:3001"
|
||||
|
||||
@oauth_bp.route("/error")
|
||||
def error():
|
||||
"""错误页面,用于显示认证失败信息"""
|
||||
msg = request.args.get("msg", "Unknown error")
|
||||
return f"Error: {msg}"
|
||||
|
||||
|
||||
# @oauth_bp.route("/report")
|
||||
@oauth_bp.route("/incident/authorized")
|
||||
def authorized():
|
||||
"""
|
||||
SSO 回调接口。
|
||||
接收 SSO 服务器返回的授权码 (code),并使用它来兑换访问令牌 (access_token)。
|
||||
"""
|
||||
CLIENT_ID = current_app.config["CLIENT_ID"]
|
||||
CLIENT_PASSWORD = current_app.config["CLIENT_PASSWORD"]
|
||||
HOST = current_app.config["SS0_HOST"] # 建议检查是否是笔误(SSO_HOST)
|
||||
REDIRECT_URL = current_app.config["REDIRECT_URL"]
|
||||
DOMAIN = current_app.config["DOMAIN"]
|
||||
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state", "/dashboard") # 默认值设为/dashboard
|
||||
|
||||
if not code:
|
||||
# return redirect(f"/error?msg=No authorization code provided.")
|
||||
return redirect(f"/incident/error?msg=No authorization code provided.")
|
||||
|
||||
# 1. 准备请求体,用授权码兑换 Token
|
||||
body = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": REDIRECT_URL,
|
||||
}
|
||||
|
||||
auth_str = f"{CLIENT_ID}:{CLIENT_PASSWORD}"
|
||||
# 2. 准备请求头
|
||||
headers = {
|
||||
"Authorization": "Basic "+base64.b64encode(auth_str.encode("utf-8")).decode("utf-8"),
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
try:
|
||||
# 3. 发送 POST 请求到 SSO 服务器的 Token 端点
|
||||
response = requests.post(f"{HOST}v1/token", data=body, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
response_data = response.json()
|
||||
access_token = response_data.get("access_token")
|
||||
|
||||
# print('access_token', access_token)
|
||||
if not access_token:
|
||||
# return redirect(f"/error?msg=Failed to obtain access token. Response: {response_data}")
|
||||
return redirect(f"/incident/error?msg=Failed to obtain access token. Response: {response_data}")
|
||||
|
||||
# 4. 解析 JWT Token
|
||||
decoded_jwt = jwt.decode(access_token, options={"verify_signature": False})
|
||||
|
||||
# 5. 验证和清理 state 参数 (防止开放重定向攻击)
|
||||
safe_state = "/callback"
|
||||
print(f"Redirecting to: {DOMAIN}{safe_state}")
|
||||
|
||||
# 6. 设置 Cookie
|
||||
resp = make_response(redirect(f"{DOMAIN}{safe_state}"))
|
||||
|
||||
# 关键:设置 Cookie 域名和属性
|
||||
# domain = ".localhost" # 让所有 localhost 子域都能读取
|
||||
domain = None # 上线时改为None
|
||||
|
||||
# 设置 access_token
|
||||
resp.set_cookie(
|
||||
"authorization",
|
||||
access_token,
|
||||
max_age=3500,
|
||||
path="/",
|
||||
domain=domain,
|
||||
httponly=False, # 前端需要读取
|
||||
samesite="Lax", # 允许跨站
|
||||
secure=False,
|
||||
) # 开发环境不需要HTTPS
|
||||
|
||||
# 设置登录名
|
||||
login_name = decoded_jwt.get("sub", "")
|
||||
resp.set_cookie(
|
||||
"loginName",
|
||||
login_name,
|
||||
max_age=3500,
|
||||
path="/",
|
||||
domain=domain,
|
||||
httponly=False,
|
||||
samesite="Lax",
|
||||
secure=False,
|
||||
)
|
||||
|
||||
# 设置用户名 (直接存储,不需要Base64编码)
|
||||
user_name = decoded_jwt.get("name", "")
|
||||
print("user_name", user_name)
|
||||
resp.set_cookie(
|
||||
"userName",
|
||||
user_name, # 直接存储,前端处理编码
|
||||
max_age=3500,
|
||||
path="/",
|
||||
domain=domain,
|
||||
httponly=False,
|
||||
samesite="Lax",
|
||||
secure=False,
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error exchanging code for token: {e}")
|
||||
return redirect(f"/error?msg=Error communicating with SSO server.")
|
||||
except jwt.InvalidTokenError as e:
|
||||
print(f"Error decoding JWT: {e}")
|
||||
return redirect(f"/error?msg=Invalid access token received.")
|
||||
|
||||
|
||||
@oauth_bp.route("/logout", methods=["GET", "POST"])
|
||||
def logout():
|
||||
"""登出接口"""
|
||||
# 清除 session
|
||||
session.clear()
|
||||
# print('logout66677')
|
||||
# 返回标准的 JSON 响应
|
||||
return jsonify({"code": 200, "message": "退出登录成功", "data": {}}), 200
|
||||
|
||||
|
||||
@oauth_bp.route("/login")
|
||||
def authorization():
|
||||
"""
|
||||
授权入口。
|
||||
将用户浏览器重定向到 SSO 服务器的授权页面。
|
||||
"""
|
||||
# print("login123")
|
||||
CLIENT_ID = current_app.config["CLIENT_ID"]
|
||||
HOST = current_app.config["SS0_HOST"]
|
||||
REDIRECT_URL = current_app.config["REDIRECT_URL"]
|
||||
|
||||
# 1. 构造重定向到 SSO 授权页面的 URL
|
||||
auth_url = (
|
||||
f"{HOST}v1/auth"
|
||||
f"?response_type=code"
|
||||
f"&client_id={CLIENT_ID}"
|
||||
f"&scope=groups+openid+email+profile"
|
||||
f"&redirect_uri={REDIRECT_URL}"
|
||||
)
|
||||
# print(auth_url)
|
||||
# 2. 执行重定向
|
||||
return redirect(auth_url)
|
||||
|
||||
|
||||
@oauth_bp.route("/report")
|
||||
def authorized_test():
|
||||
"""
|
||||
SSO 回调接口。
|
||||
接收 SSO 服务器返回的授权码 (code),并使用它来兑换访问令牌 (access_token)。
|
||||
"""
|
||||
CLIENT_ID = current_app.config["CLIENT_ID"]
|
||||
CLIENT_PASSWORD = current_app.config["CLIENT_PASSWORD"]
|
||||
HOST = current_app.config["SS0_HOST"] # 建议检查是否是笔误(SSO_HOST)
|
||||
REDIRECT_URL = current_app.config["REDIRECT_URL"]
|
||||
DOMAIN = current_app.config["DOMAIN"]
|
||||
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state", "/dashboard") # 默认值设为/dashboard
|
||||
|
||||
if not code:
|
||||
# return redirect(f"/error?msg=No authorization code provided.")
|
||||
return redirect(f"/incident/error?msg=No authorization code provided.")
|
||||
|
||||
# 1. 准备请求体,用授权码兑换 Token
|
||||
body = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": REDIRECT_URL,
|
||||
}
|
||||
|
||||
auth_str = f"{CLIENT_ID}:{CLIENT_PASSWORD}"
|
||||
# 2. 准备请求头
|
||||
headers = {
|
||||
"Authorization": "Basic "+base64.b64encode(auth_str.encode("utf-8")).decode("utf-8"),
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
try:
|
||||
# 3. 发送 POST 请求到 SSO 服务器的 Token 端点
|
||||
response = requests.post(f"{HOST}v1/token", data=body, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
response_data = response.json()
|
||||
access_token = response_data.get("access_token")
|
||||
|
||||
# print('access_token', access_token)
|
||||
if not access_token:
|
||||
# return redirect(f"/error?msg=Failed to obtain access token. Response: {response_data}")
|
||||
return redirect(f"/incident/error?msg=Failed to obtain access token. Response: {response_data}")
|
||||
|
||||
# 4. 解析 JWT Token
|
||||
decoded_jwt = jwt.decode(access_token, options={"verify_signature": False})
|
||||
|
||||
# 5. 验证和清理 state 参数 (防止开放重定向攻击)
|
||||
safe_state = "/callback"
|
||||
print(f"Redirecting to: {DOMAIN}{safe_state}")
|
||||
|
||||
# 6. 设置 Cookie
|
||||
resp = make_response(redirect(f"{DOMAIN}{safe_state}"))
|
||||
|
||||
# 关键:设置 Cookie 域名和属性
|
||||
domain = ".localhost" # 让所有 localhost 子域都能读取
|
||||
|
||||
# 设置 access_token
|
||||
resp.set_cookie(
|
||||
"authorization",
|
||||
access_token,
|
||||
max_age=3500,
|
||||
path="/",
|
||||
domain=domain,
|
||||
httponly=False, # 前端需要读取
|
||||
samesite="Lax", # 允许跨站
|
||||
secure=False,
|
||||
) # 开发环境不需要HTTPS
|
||||
|
||||
# 设置登录名
|
||||
login_name = decoded_jwt.get("sub", "")
|
||||
resp.set_cookie(
|
||||
"loginName",
|
||||
login_name,
|
||||
max_age=3500,
|
||||
path="/",
|
||||
domain=domain,
|
||||
httponly=False,
|
||||
samesite="Lax",
|
||||
secure=False,
|
||||
)
|
||||
|
||||
# 设置用户名 (直接存储,不需要Base64编码)
|
||||
user_name = decoded_jwt.get("name", "")
|
||||
print("user_name", user_name)
|
||||
resp.set_cookie(
|
||||
"userName",
|
||||
user_name, # 直接存储,前端处理编码
|
||||
max_age=3500,
|
||||
path="/",
|
||||
domain=domain,
|
||||
httponly=False,
|
||||
samesite="Lax",
|
||||
secure=False,
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error exchanging code for token: {e}")
|
||||
return redirect(f"/error?msg=Error communicating with SSO server.")
|
||||
except jwt.InvalidTokenError as e:
|
||||
print(f"Error decoding JWT: {e}")
|
||||
return redirect(f"/error?msg=Invalid access token received.")
|
||||
@@ -0,0 +1,89 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
if os.getenv("FLASK_ENV") != "production":
|
||||
load_dotenv() # 自动读取项目根目录的 .env 文件
|
||||
|
||||
|
||||
class Config:
|
||||
"""基础配置(所有环境共享的公共参数)"""
|
||||
# 1. 敏感配置:强制从环境变量读取,无默认值(生产环境必须配置)
|
||||
DATABRICKS_TOKEN = os.getenv("DATABRICKS_TOKEN")
|
||||
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") # 移除弱默认值
|
||||
SECRET_KEY = os.getenv("SECRET_KEY") # 移除弱默认值
|
||||
|
||||
# 2. 公共非敏感配置
|
||||
DATABRICKS_HOST = (
|
||||
"https://adb-3035612432650494.2.databricks.azure.cn" # 固定地址可保留
|
||||
)
|
||||
REQUEST_TIMEOUT = 600 # 远程请求超时时间(秒)
|
||||
JWT_ACCESS_TOKEN_EXPIRES = 14400 # 更直观的过期时间(替代 14400 秒)
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False # 统一关闭查询跟踪,减少性能损耗
|
||||
|
||||
# 3. 安全基础配置(所有环境共享)
|
||||
SESSION_COOKIE_HTTPONLY = True # 禁止 JS 访问 Session Cookie(防 XSS)
|
||||
SESSION_COOKIE_SAMESITE = "Strict" # 限制跨站携带 Cookie(防 CSRF)
|
||||
|
||||
# 配置链接wedata的参数
|
||||
WEDATA_SECRET_ID='AKIDtfFdqqgYUflgdZaPFvOfkDd0EGEUkx0u'
|
||||
WEDATA_SECRET_KEY='sBKH5WrESUJwLdljV6Yrysc9TaTAh3Gk'
|
||||
WEDATA_ENGINE='gen6_prod_aiag'
|
||||
WEDATA_DATABASE='dws'
|
||||
|
||||
# 图片上传的配置
|
||||
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||
MAX_CONTENT_LENGTH = 5 * 1024 * 1024 # 5MB(开发环境)
|
||||
IMAGE_BASE_URL='http://127.0.0.1:5221'
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""开发环境配置(本地开发用,配置宽松)"""
|
||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:12345@localhost:3306/incid_db?charset=utf8mb4" # 开发库临时密码
|
||||
|
||||
GEN5_TABLE_NAME = os.getenv(
|
||||
"GEN5_TABLE_NAME",
|
||||
"hive_metastore.taf_level_two_plus.accident_report_raw_data"
|
||||
)
|
||||
CLUSTER_ID = os.getenv("CLUSTER_ID", "1203-054146-wkjf95i0")
|
||||
|
||||
CLIENT_ID = os.getenv("CLIENT_ID","A17336FC-A8D7-4CEA-8777-384BFEABABC1")
|
||||
CLIENT_PASSWORD = os.getenv("CLIENT_PASSWORD","U7-C1BZQdL8V5c.z~qA_64SO2l9F3J0o")
|
||||
SS0_HOST = os.getenv("SS0_HOST","https://ssoalpha.dvb.corpinter.net.cn/")
|
||||
REDIRECT_URL = os.getenv("REDIRECT_URL","http://localhost:5221/report")
|
||||
DOMAIN = os.getenv("DOMAIN","http://localhost:3001")
|
||||
# 2. 开发模式开关
|
||||
DEBUG = True
|
||||
TESTING = True
|
||||
|
||||
# 3. 开发环境 Cookie 配置(无 HTTPS,禁用 secure)
|
||||
SESSION_COOKIE_SECURE = False
|
||||
|
||||
HEADERS_SECRET_KEY=os.getenv("HEADERS_SECRET_KEY","mbincid-server12")
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""生产环境配置"""
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL")
|
||||
TABLE_NAME = os.getenv("TABLE_NAME")
|
||||
CLUSTER_ID = os.getenv("CLUSTER_ID")
|
||||
|
||||
CLIENT_ID = os.getenv("CLIENT_ID","A17336FC-A8D7-4CEA-8777-384BFEABABC1")
|
||||
CLIENT_PASSWORD = os.getenv("CLIENT_PASSWORD","U7-C1BZQdL8V5c.z~qA_64SO2l9F3J0o")
|
||||
SS0_HOST = os.getenv("SS0_HOST","https://ssoalpha.dvb.corpinter.net.cn/")
|
||||
REDIRECT_URL = os.getenv("REDIRECT_URL","http://localhost:5221/report")
|
||||
DOMAIN = os.getenv("DOMAIN","http://localhost:3001")
|
||||
DEBUG = False # 禁用调试(关键!避免生产环境暴露敏感信息)
|
||||
TESTING = False
|
||||
|
||||
UPLOAD_FOLDER=os.getenv('UPLOAD_FOLDER','/app/uploads')
|
||||
MAX_CONTENT_LENGTH=int(os.getenv('MAX_CONTENT_LENGTH', 5242880))
|
||||
IMAGE_BASE_URL=os.getenv('IMAGE_BASE_URL','http://127.0.0.1:5221')
|
||||
|
||||
GEN5_TABLE_NAME = os.getenv(
|
||||
"GEN5_TABLE_NAME",
|
||||
"hive_metastore.taf_level_two_plus.accident_report_raw_data"
|
||||
)
|
||||
|
||||
HEADERS_SECRET_KEY=os.getenv("HEADERS_SECRET_KEY","mbincid-server12")
|
||||
|
||||
env = os.getenv("FLASK_ENV", "development")
|
||||
config = {"development": DevelopmentConfig, "production": ProductionConfig}[env]
|
||||
@@ -0,0 +1,132 @@
|
||||
import traceback
|
||||
import tdlc_connector
|
||||
from tdlc_connector import constants
|
||||
|
||||
|
||||
def query_tencent_cloud_data(config,start_time,end_time,vin):
|
||||
try:
|
||||
# 配置参数(替换为您的实际参数)
|
||||
# config = {
|
||||
# 'region': 'ap-shanghai', # 引擎所在地域
|
||||
# 'secret_id': 'AKIDtfFdqqgYUflgdZaPFvOfkDd0EGEUkx0u',
|
||||
# 'secret_key': 'sBKH5WrESUJwLdljV6Yrysc9TaTAh3Gk',
|
||||
# 'engine': 'gen6_prod_aiag', # 引擎名称
|
||||
# "database":"dws"
|
||||
# }
|
||||
# 建立连接
|
||||
conn = tdlc_connector.connect(
|
||||
**config,
|
||||
engine_type=constants.EngineType.SPARK,
|
||||
result_style=constants.ResultStyles.LIST
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 执行查询
|
||||
sql = f"""
|
||||
SELECT * FROM gen6_incidents
|
||||
WHERE incident_time BETWEEN '{start_time}' AND '{end_time}'
|
||||
AND oneid = '{vin}'
|
||||
"""
|
||||
print(f"执行SQL: {sql}")
|
||||
cursor.execute(sql)
|
||||
|
||||
# 获取列名
|
||||
columns = [desc[0] for desc in cursor.description] # 使用cursor.description获取列名 [[5]]
|
||||
|
||||
# 获取结果
|
||||
results = cursor.fetchall()
|
||||
print(f"查询结果共 {len(results)} 条:")
|
||||
|
||||
# 将结果转换为字典列表格式
|
||||
dict_results = []
|
||||
for i, row in enumerate(results, 1):
|
||||
# 将每一行转换为字典,键为列名,值为对应的值
|
||||
row_dict = dict(zip(columns, row))
|
||||
dict_results.append(row_dict)
|
||||
print(f"第{i}行: {row_dict}")
|
||||
|
||||
return dict_results
|
||||
|
||||
except Exception as e:
|
||||
print(f"查询失败: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
# 确保关闭连接
|
||||
if 'cursor' in locals():
|
||||
cursor.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def sync_query_tencent_cloud_data(config, sql, params, result_queue, task_id):
|
||||
"""
|
||||
同步查询腾讯云TDLC数据(兼容单/多线程)
|
||||
:param config: TDLC连接配置
|
||||
:param sql: 含%s占位符的SQL语句
|
||||
:param params: SQL参数列表(用于替换%s)
|
||||
:param result_queue: 线程安全的结果队列(单线程传None)
|
||||
:param task_id: 任务ID(区分不同查询,单线程可传任意字符串)
|
||||
"""
|
||||
conn = None
|
||||
cursor = None
|
||||
try:
|
||||
# (原连接/执行SQL逻辑完全不变)
|
||||
conn = tdlc_connector.connect(
|
||||
**config,
|
||||
engine_type=constants.EngineType.SPARK,
|
||||
result_style=constants.ResultStyles.LIST
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
# print(f"[线程-{task_id}] 执行SQL: {sql} | 参数: {params}")
|
||||
cursor.execute(sql, params)
|
||||
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
||||
results = cursor.fetchall()
|
||||
dict_results = [dict(zip(columns, row)) for row in results]
|
||||
|
||||
# ===== 仅新增:判断队列是否为空,兼容单线程 =====
|
||||
if result_queue is not None:
|
||||
# 多线程:结果入队
|
||||
result_queue.put({
|
||||
"task_id": task_id,
|
||||
"success": True,
|
||||
"data": dict_results,
|
||||
"error": None
|
||||
})
|
||||
else:
|
||||
# 单线程:直接返回结果(新增返回逻辑)
|
||||
return {
|
||||
"success": True,
|
||||
"data": dict_results,
|
||||
"error": None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_info = f"{str(e)}\n{traceback.format_exc()}"
|
||||
print(f"[线程-{task_id}] 查询失败: {error_info}")
|
||||
# ===== 同样新增:队列空判断 =====
|
||||
if result_queue is not None:
|
||||
result_queue.put({
|
||||
"task_id": task_id,
|
||||
"success": False,
|
||||
"data": None,
|
||||
"error": error_info
|
||||
})
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"data": None,
|
||||
"error": error_info
|
||||
}
|
||||
finally:
|
||||
# (原关闭连接逻辑完全不变)
|
||||
if cursor:
|
||||
try:
|
||||
cursor.close()
|
||||
except Exception as e:
|
||||
print(f"[线程-{task_id}] 关闭游标失败: {str(e)}")
|
||||
if conn:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"[线程-{task_id}] 关闭连接失败: {str(e)}")
|
||||
@@ -0,0 +1,590 @@
|
||||
from datetime import datetime
|
||||
import enum
|
||||
|
||||
from sqlalchemy import (
|
||||
BIGINT,
|
||||
INTEGER,
|
||||
JSON,
|
||||
Column,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
String,
|
||||
Text,
|
||||
func,
|
||||
)
|
||||
from app import db
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
username = db.Column(db.String(50), unique=True, nullable=False)
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
status = db.Column(db.Integer, default=1, comment="用户状态:1-正常,0-禁用")
|
||||
created_at = db.Column(db.DateTime, default=datetime.now, comment="创建时间")
|
||||
updated_at = db.Column(
|
||||
db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间"
|
||||
)
|
||||
email = db.Column(db.String(255), nullable=True, comment="邮箱")
|
||||
avatar = db.Column(db.String(255), nullable=True, comment="头像(通常存储图片URL)")
|
||||
region = db.Column(db.String(255), nullable=True, comment="国家地区")
|
||||
phone = db.Column(db.String(255), nullable=True, comment="电话")
|
||||
brief = db.Column(db.String(255), nullable=True, comment="个人简介")
|
||||
third_party_account = db.Column(
|
||||
db.String(255), nullable=True, comment="第三方账号(如微信、QQ等)"
|
||||
)
|
||||
position = db.Column(db.String(255), nullable=True, comment="职务")
|
||||
department = db.Column(db.String(255), nullable=True, comment="部门")
|
||||
label = db.Column(
|
||||
db.String(255), nullable=True, comment="个人标签(多个标签可逗号分隔)"
|
||||
)
|
||||
|
||||
# 自引用外键
|
||||
created_by = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
|
||||
# 关系定义
|
||||
creator = db.relationship(
|
||||
"User", foreign_keys=[created_by], remote_side=[id], backref="created_users"
|
||||
)
|
||||
updater = db.relationship(
|
||||
"User", foreign_keys=[updated_by], remote_side=[id], backref="updated_users"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
roles = db.relationship(
|
||||
"Role",
|
||||
secondary="user_roles",
|
||||
back_populates="users",
|
||||
foreign_keys="[UserRole.user_id, UserRole.role_id]",
|
||||
) # 明确指定外键)
|
||||
created_roles = db.relationship(
|
||||
"Role", backref="creator", foreign_keys="Role.created_by"
|
||||
)
|
||||
updated_roles = db.relationship(
|
||||
"Role", backref="updater", foreign_keys="Role.updated_by"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.username}>"
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
__tablename__ = "roles"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(50), unique=True, nullable=False, comment="角色名称")
|
||||
description = db.Column(db.String(255), comment="角色描述")
|
||||
status = db.Column(db.Integer, default=1, comment="状态:1-启用,0-禁用")
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 外键字段
|
||||
created_by = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
|
||||
# 关系定义
|
||||
users = db.relationship(
|
||||
"User",
|
||||
secondary="user_roles",
|
||||
back_populates="roles",
|
||||
foreign_keys="[UserRole.role_id, UserRole.user_id]",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Role {self.name}>"
|
||||
|
||||
|
||||
class UserRole(db.Model):
|
||||
__tablename__ = "user_roles"
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), primary_key=True)
|
||||
role_id = db.Column(db.Integer, db.ForeignKey("roles.id"), primary_key=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
|
||||
# 关系定义
|
||||
user = db.relationship("User", foreign_keys=[user_id], backref="role_assignments")
|
||||
role = db.relationship("Role", foreign_keys=[role_id], backref="user_assignments")
|
||||
creator_rel = db.relationship(
|
||||
"User", foreign_keys=[created_by], backref="created_assignments"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserRole user={self.user_id} role={self.role_id}>"
|
||||
|
||||
|
||||
class AccidList(db.Model):
|
||||
"""事故工单列表模型类(对应 accid_list 表)"""
|
||||
|
||||
__tablename__ = "accid_list" # 对应 MySQL 表名
|
||||
__table_args__ = (
|
||||
# 修正:使用 db.Index(和 OperationHistory 模型一致),移除注释避免兼容问题
|
||||
db.Index("idx_ticket_id", "ticket_id"),
|
||||
db.Index("idx_creator", "creator"),
|
||||
db.Index("idx_create_time", "create_time"),
|
||||
db.Index("idx_carline", "carline"),
|
||||
db.Index("idx_affects_versions", "affects_versions"),
|
||||
db.Index("idx_components", "components"),
|
||||
db.Index("idx_labels", "labels"),
|
||||
db.Index("idx_fix_versions", "fix_versions"),
|
||||
db.Index("idx_accident_level", "accident_level"),
|
||||
db.Index("idx_assignee_name", "assignee_name"),
|
||||
# 表级属性(参考 Fst 模型的 collation 写法)
|
||||
{
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_row_format": "DYNAMIC",
|
||||
},
|
||||
)
|
||||
|
||||
# 核心修正:所有字段使用 db. 前缀(和 User 模型一致)
|
||||
id = db.Column(
|
||||
db.Integer, primary_key=True, autoincrement=True, comment="主键ID,自增"
|
||||
)
|
||||
ticket_id = db.Column(db.String(50), nullable=False, comment="工单ID")
|
||||
model = db.Column(db.String(100), nullable=True, default=None, comment="型号")
|
||||
vin = db.Column(db.String(50), nullable=False, comment="车辆识别码(VIN)")
|
||||
creator = db.Column(
|
||||
db.String(50), nullable=False, default="", comment="创建人(用户名,如ZHANMEN)"
|
||||
)
|
||||
create_time = db.Column(db.DateTime, nullable=False, comment="工单新建时间")
|
||||
occur_datetime = db.Column(db.DateTime, nullable=False, comment="问题发生时间")
|
||||
case_description = db.Column(db.Text, nullable=False, comment="Case描述")
|
||||
fo_solution = db.Column(db.Text, nullable=False, comment="Fo解答")
|
||||
category = db.Column(db.String(100), nullable=False, default="", comment="分类")
|
||||
keywords = db.Column(
|
||||
db.String(200), nullable=False, default="", comment="关键词(多个用逗号分隔)"
|
||||
)
|
||||
handle_status = db.Column(
|
||||
db.String(20),
|
||||
nullable=True,
|
||||
default="已完成",
|
||||
comment="处理状态:已完成/未完成",
|
||||
)
|
||||
# 修正:时间字段默认值用 db.func.current_timestamp()(和 BagFile 模型一致)
|
||||
create_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=True,
|
||||
default=func.current_timestamp(),
|
||||
comment="记录插入数据库时间",
|
||||
)
|
||||
update_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=True,
|
||||
default=func.current_timestamp(),
|
||||
onupdate=func.current_timestamp(),
|
||||
comment="记录最后更新时间",
|
||||
)
|
||||
bmbs_name = db.Column(
|
||||
db.String(50), nullable=True, default="", comment="BMBS联系人"
|
||||
)
|
||||
# 修正:Enum 默认值错误(原 default='' 改为 'P1'),使用 db.Enum
|
||||
accident_level = db.Column(
|
||||
db.Enum("P1", "P2", "P3"), nullable=True, default="P1", comment="事故评级"
|
||||
)
|
||||
rdca_reporter = db.Column(
|
||||
db.String(50), nullable=True, default="", comment="RDCA报告人"
|
||||
)
|
||||
carline = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
default=None,
|
||||
comment="车系(关联字典表dict_code=carline)",
|
||||
)
|
||||
software_version = db.Column(
|
||||
db.String(50), nullable=True, default="", comment="软件版本"
|
||||
)
|
||||
labels = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
default=None,
|
||||
comment="事故类型标签(关联字典表dict_code=labels)",
|
||||
)
|
||||
attachment_url = db.Column(
|
||||
db.String(255), nullable=True, default="", comment="附件地址(多个用逗号分隔)"
|
||||
)
|
||||
assignee_name = db.Column(
|
||||
db.String(50), nullable=True, default="", comment="经办人(关联字典表/用户表)"
|
||||
)
|
||||
affects_versions = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
default=None,
|
||||
comment="受影响的版本(关联字典表dict_code=affects_versions)",
|
||||
)
|
||||
components = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
default=None,
|
||||
comment="零部件/组件(关联字典表dict_code=components)",
|
||||
)
|
||||
fix_versions = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
default=None,
|
||||
comment="修复方案对应的版本(关联字典表dict_code=fix_versions)",
|
||||
)
|
||||
subtasks = db.Column(
|
||||
db.String(200), nullable=False, default="", comment="子任务(多个用逗号分隔)"
|
||||
)
|
||||
linked_work_items = db.Column(
|
||||
db.String(200),
|
||||
nullable=False,
|
||||
default="",
|
||||
comment="关联的工作项(格式:类型:ID,类型:ID)",
|
||||
)
|
||||
descriptions = db.Column(
|
||||
db.String(255),
|
||||
nullable=False,
|
||||
default="",
|
||||
comment="补充描述(区别于case_description)",
|
||||
)
|
||||
mviz_link = db.Column(
|
||||
db.String(255), nullable=False, default="", comment="mviz的链接"
|
||||
)
|
||||
system_function = db.Column(
|
||||
db.String(255), nullable=False, default="", comment="system_function字段"
|
||||
)
|
||||
image_ids = db.Column(
|
||||
db.String(255),
|
||||
nullable=False,
|
||||
default="",
|
||||
comment="关联图片ID串,逗号分隔(如:1,2,3)",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AccidList(ticket_id='{self.ticket_id}', vin='{self.vin}')>"
|
||||
|
||||
def to_dict(self):
|
||||
"""转为字典,适配接口返回(参考 DictType 的 to_dict 方法)"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"ticket_id": self.ticket_id,
|
||||
"model": self.model,
|
||||
"vin": self.vin,
|
||||
"creator": self.creator,
|
||||
"create_time": (
|
||||
self.create_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.create_time
|
||||
else None
|
||||
),
|
||||
"occur_datetime": (
|
||||
self.occur_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.occur_datetime
|
||||
else None
|
||||
),
|
||||
"case_description": self.case_description,
|
||||
"fo_solution": self.fo_solution,
|
||||
"category": self.category,
|
||||
"keywords": self.keywords,
|
||||
"handle_status": self.handle_status,
|
||||
"create_at": (
|
||||
self.create_at.strftime("%Y-%m-%d %H:%M:%S") if self.create_at else None
|
||||
),
|
||||
"update_at": (
|
||||
self.update_at.strftime("%Y-%m-%d %H:%M:%S") if self.update_at else None
|
||||
),
|
||||
"bmbs_name": self.bmbs_name,
|
||||
"accident_level": self.accident_level,
|
||||
"rdca_reporter": self.rdca_reporter,
|
||||
"carline": self.carline,
|
||||
"software_version": self.software_version,
|
||||
"labels": self.labels,
|
||||
"attachment_url": self.attachment_url,
|
||||
"assignee_name": self.assignee_name,
|
||||
"affects_versions": self.affects_versions,
|
||||
"components": self.components,
|
||||
"fix_versions": self.fix_versions,
|
||||
"subtasks": self.subtasks,
|
||||
"linked_work_items": self.linked_work_items,
|
||||
"descriptions": self.descriptions,
|
||||
"mviz_link": self.mviz_link,
|
||||
"system_function":self.system_function,
|
||||
"image_ids":self.image_ids
|
||||
}
|
||||
|
||||
|
||||
class DictType(db.Model):
|
||||
"""字典类型表模型类(对应 dict_type 表)"""
|
||||
|
||||
__tablename__ = "dict_type" # 匹配 MySQL 表名
|
||||
__table_args__ = (
|
||||
# 修正:使用 db.UniqueConstraint(参考 RuleVersions 模型)
|
||||
db.UniqueConstraint(
|
||||
"dict_code", name="uk_dict_code", comment="字典编码唯一(对应唯一字段)"
|
||||
),
|
||||
# 表级属性(对齐项目规范)
|
||||
{
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_row_format": "Dynamic",
|
||||
"comment": "字典类型表(管理需要下拉的字段)",
|
||||
},
|
||||
)
|
||||
|
||||
# 修正:所有字段使用 db. 前缀,类型改为 db.Integer(和 User 模型一致)
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="主键ID")
|
||||
dict_code = db.Column(
|
||||
db.String(50),
|
||||
nullable=False,
|
||||
comment="字典编码(对应accid_list的字段名,如carline、labels)",
|
||||
)
|
||||
dict_name = db.Column(
|
||||
db.String(50),
|
||||
nullable=False,
|
||||
comment="字典名称(字段中文名称,如车系、事故类型)",
|
||||
)
|
||||
remark = db.Column(
|
||||
db.String(200),
|
||||
nullable=True,
|
||||
default="",
|
||||
comment="备注(如“事故工单-车系下拉选项”)",
|
||||
)
|
||||
status = db.Column(
|
||||
db.Integer, nullable=False, default=1, comment="状态:1-启用,0-禁用"
|
||||
)
|
||||
create_time = db.Column(
|
||||
db.DateTime, nullable=True, default=func.current_timestamp(), comment="创建时间"
|
||||
)
|
||||
update_time = db.Column(
|
||||
db.DateTime,
|
||||
nullable=True,
|
||||
default=func.current_timestamp(),
|
||||
onupdate=func.current_timestamp(),
|
||||
comment="更新时间",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DictType(dict_code='{self.dict_code}', dict_name='{self.dict_name}', status={self.status})>"
|
||||
|
||||
def to_dict(self):
|
||||
"""将模型对象转为字典,便于接口返回数据"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"dict_code": self.dict_code,
|
||||
"dict_name": self.dict_name,
|
||||
"remark": self.remark,
|
||||
"status": self.status,
|
||||
"create_time": (
|
||||
self.create_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.create_time
|
||||
else None
|
||||
),
|
||||
"update_time": (
|
||||
self.update_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.update_time
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DictItem(db.Model):
|
||||
"""字典项表模型类(对应 dict_item 表)"""
|
||||
|
||||
__tablename__ = "dict_item" # 严格匹配 MySQL 表名
|
||||
__table_args__ = (
|
||||
# 修正:使用 db.Index(对齐 OperationHistory 模型)
|
||||
db.Index("idx_dict_id", "dict_id"),
|
||||
db.Index("idx_item_value", "item_value"),
|
||||
# 表级属性(对齐项目规范)
|
||||
{
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_row_format": "Dynamic",
|
||||
"comment": "字典项表(管理每个字段的具体下拉选项)",
|
||||
},
|
||||
)
|
||||
|
||||
# 修正:所有字段使用 db. 前缀,类型改为 db.Integer(和 User 模型一致)
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="主键ID")
|
||||
# 修正:外键使用 db.ForeignKey(参考 BagFile 模型)
|
||||
dict_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("dict_type.id"),
|
||||
nullable=False,
|
||||
comment="关联字典类型表的ID",
|
||||
)
|
||||
item_value = db.Column(
|
||||
db.String(100),
|
||||
nullable=False,
|
||||
comment="选项值(存储到accid_list的实际值,如“大众帕萨特”“P1”)",
|
||||
)
|
||||
item_label = db.Column(
|
||||
db.String(100),
|
||||
nullable=False,
|
||||
comment="选项标签(前端显示的文本,如“大众帕萨特(车系)”)",
|
||||
)
|
||||
sort = db.Column(
|
||||
db.Integer,
|
||||
nullable=True,
|
||||
default=0,
|
||||
comment="排序号(前端下拉框显示顺序,数字越小越靠前)",
|
||||
)
|
||||
status = db.Column(
|
||||
db.Integer,
|
||||
nullable=False,
|
||||
default=1,
|
||||
comment="状态:1-启用,0-禁用(禁用后前端不显示)",
|
||||
)
|
||||
remark = db.Column(
|
||||
db.String(200),
|
||||
nullable=True,
|
||||
default="",
|
||||
comment="备注(如“2025款帕萨特专属”)",
|
||||
)
|
||||
create_time = db.Column(
|
||||
db.DateTime, nullable=True, default=func.current_timestamp(), comment="创建时间"
|
||||
)
|
||||
update_time = db.Column(
|
||||
db.DateTime,
|
||||
nullable=True,
|
||||
default=func.current_timestamp(),
|
||||
onupdate=func.current_timestamp(),
|
||||
comment="更新时间",
|
||||
)
|
||||
|
||||
# 修正:关系使用 db.relationship + db.backref(参考 User 模型)
|
||||
dict_type = db.relationship(
|
||||
"DictType", backref=db.backref("dict_items", lazy="joined"), lazy="joined"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DictItem(dict_id={self.dict_id}, item_value='{self.item_value}', item_label='{self.item_label}', status={self.status})>"
|
||||
|
||||
def to_dict(self):
|
||||
"""将模型对象转为字典,适配接口返回数据"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"dict_id": self.dict_id,
|
||||
"item_value": self.item_value,
|
||||
"item_label": self.item_label,
|
||||
"sort": self.sort,
|
||||
"status": self.status,
|
||||
"remark": self.remark,
|
||||
"create_time": (
|
||||
self.create_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.create_time
|
||||
else None
|
||||
),
|
||||
"update_time": (
|
||||
self.update_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.update_time
|
||||
else None
|
||||
),
|
||||
# 关联返回字典类型信息(可选,提升接口易用性)
|
||||
"dict_code": self.dict_type.dict_code if self.dict_type else None,
|
||||
"dict_name": self.dict_type.dict_name if self.dict_type else None,
|
||||
}
|
||||
|
||||
|
||||
class FileUpload(db.Model):
|
||||
"""通用文件上传元信息表模型类(对应 file_uploads 表)"""
|
||||
|
||||
__tablename__ = "file_uploads" # 对应 MySQL 表名
|
||||
__table_args__ = (
|
||||
# 索引定义(与建表语句完全一致)
|
||||
db.Index("idx_business_type_id", "business_type", "business_id"),
|
||||
db.Index("idx_create_time", "create_time"),
|
||||
db.Index("idx_status", "status"),
|
||||
# 表级属性(与 AccidList 模型格式一致)
|
||||
{
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_row_format": "DYNAMIC",
|
||||
},
|
||||
)
|
||||
|
||||
# 核心修正:全用 db.Integer,通过 info 指定 MySQL 底层类型(无参数报错)
|
||||
id = db.Column(
|
||||
db.Integer, # 模型层用 int 兼容
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
info={'mysql_type': 'BIGINT UNSIGNED'}, # 数据库底层仍是 BIGINT UNSIGNED
|
||||
comment="主键(自增数值ID)",
|
||||
)
|
||||
file_name = db.Column(
|
||||
db.String(255), nullable=False, comment="原始文件名(如:事故图片1.jpg)"
|
||||
)
|
||||
file_alias = db.Column(
|
||||
db.String(255),
|
||||
nullable=False,
|
||||
comment="存储别名(避免重名,如:20260210_153950_89782.jpg)",
|
||||
)
|
||||
file_path = db.Column(
|
||||
db.String(512),
|
||||
nullable=False,
|
||||
comment="存储路径/访问URL(本地:/uploads/202602/xxx.jpg;OSS:https://xxx.oss-cn-beijing.aliyuncs.com/xxx.jpg)",
|
||||
)
|
||||
file_size = db.Column(
|
||||
db.Integer, # 模型层用 int 兼容
|
||||
nullable=False,
|
||||
info={'mysql_type': 'BIGINT UNSIGNED'}, # 数据库底层 BIGINT UNSIGNED
|
||||
comment="文件大小(字节)"
|
||||
)
|
||||
file_type = db.Column(
|
||||
db.String(50),
|
||||
nullable=False,
|
||||
comment="文件MIME类型(如:image/jpeg、image/png)",
|
||||
)
|
||||
business_type = db.Column(
|
||||
db.String(50),
|
||||
nullable=True,
|
||||
default=None,
|
||||
comment="关联业务类型(如:case=案例、user=用户头像)",
|
||||
)
|
||||
business_id = db.Column(
|
||||
db.Integer, # 模型层用 int 兼容
|
||||
nullable=True,
|
||||
default=None,
|
||||
info={'mysql_type': 'BIGINT UNSIGNED'}, # 数据库底层 BIGINT UNSIGNED
|
||||
comment="关联业务主键(案例ID/用户ID等,初始为NULL)",
|
||||
)
|
||||
create_time = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
default=func.current_timestamp(), # 对应 DEFAULT CURRENT_TIMESTAMP
|
||||
comment="上传时间",
|
||||
)
|
||||
create_by = db.Column(
|
||||
db.String(50), nullable=True, default=None, comment="上传人(用户ID/用户名)"
|
||||
)
|
||||
status = db.Column(
|
||||
db.Integer, # 模型层用 int 兼容
|
||||
nullable=False,
|
||||
default=1,
|
||||
info={'mysql_type': 'TINYINT(3) UNSIGNED'}, # 数据库底层 TINYINT(3) UNSIGNED
|
||||
comment="状态:1=有效,0=逻辑删除",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
"""模型字符串表示,便于调试"""
|
||||
return f"<FileUpload(id='{self.id}', file_name='{self.file_name}', business_type='{self.business_type}')>"
|
||||
|
||||
def to_dict(self):
|
||||
"""转为字典,适配接口返回(与 AccidList 模型风格一致)"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"file_name": self.file_name,
|
||||
"file_alias": self.file_alias,
|
||||
"file_path": self.file_path,
|
||||
"file_size": self.file_size,
|
||||
"file_type": self.file_type,
|
||||
"business_type": self.business_type,
|
||||
"business_id": self.business_id,
|
||||
"create_time": (
|
||||
self.create_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.create_time
|
||||
else None
|
||||
),
|
||||
"create_by": self.create_by,
|
||||
"status": self.status,
|
||||
"status_text": "有效" if self.status == 1 else "已删除",
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
from datetime import datetime, timezone
|
||||
from flask import current_app
|
||||
from databricks import sql
|
||||
|
||||
|
||||
class DatabricksQuery:
|
||||
def __init__(self):
|
||||
"""纯Python连接Databricks SQL,无JVM依赖"""
|
||||
try:
|
||||
# 读取配置(参数和原代码一致,无需修改)
|
||||
self.host = current_app.config["DATABRICKS_HOST"]
|
||||
self.token = current_app.config["DATABRICKS_TOKEN"]
|
||||
self.cluster_id = current_app.config["CLUSTER_ID"]
|
||||
|
||||
# 校验参数
|
||||
if not all([self.host, self.token, self.cluster_id]):
|
||||
raise ValueError(
|
||||
"必须配置DATABRICKS_HOST、DATABRICKS_TOKEN、CLUSTER_ID"
|
||||
)
|
||||
|
||||
print("Databricks SQL 连接器初始化成功")
|
||||
except Exception as e:
|
||||
print(f"初始化失败: {e}")
|
||||
raise
|
||||
|
||||
# 复用原有参数校验逻辑
|
||||
@staticmethod
|
||||
def _validate_table_name(table_name):
|
||||
if not table_name or not isinstance(table_name, str):
|
||||
raise ValueError(f"无效的表名: {table_name}")
|
||||
if ";" in table_name:
|
||||
raise ValueError(f"表名包含非法字符 ';': {table_name}")
|
||||
parts = table_name.split(".")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"表名格式应为 catalog.schema.table: {table_name}")
|
||||
|
||||
@staticmethod
|
||||
def _validate_vin(vin):
|
||||
if vin is None:
|
||||
return True
|
||||
if not isinstance(vin, str):
|
||||
raise ValueError(f"无效的 VIN: {vin} (必须是17位字母数字)")
|
||||
return True
|
||||
|
||||
def query_table(
|
||||
self, table_name, vin=None, start_time=None, end_time=None,limit=None
|
||||
):
|
||||
"""纯Python查询表数据,无JVM依赖"""
|
||||
try:
|
||||
# 验证参数
|
||||
self._validate_table_name(table_name)
|
||||
self._validate_vin(vin)
|
||||
|
||||
# 构建SQL查询语句(替代原PySpark DataFrame操作)
|
||||
select_fields = """
|
||||
Incident__Name AS incident_name,
|
||||
Incident__description AS incident_description,
|
||||
Meaning AS meaning,
|
||||
Signal__Name As signal_name,
|
||||
incident_time,
|
||||
oneid,
|
||||
speed,
|
||||
int_value,
|
||||
idc_tickcount_ms,
|
||||
latitude,
|
||||
longitude,
|
||||
mux_data,
|
||||
gps_heading,
|
||||
float_value
|
||||
"""
|
||||
|
||||
sql_query = f"SELECT {select_fields} FROM {table_name}"
|
||||
where_conditions = []
|
||||
|
||||
# 拼接过滤条件
|
||||
if vin:
|
||||
where_conditions.append(f"oneid = '{vin}'")
|
||||
if start_time:
|
||||
start_dt = self._parse_iso_time(start_time).isoformat()
|
||||
where_conditions.append(f"incident_time >= '{start_dt}'")
|
||||
if end_time:
|
||||
end_dt = self._parse_iso_time(end_time).isoformat()
|
||||
where_conditions.append(f"incident_time <= '{end_dt}'")
|
||||
|
||||
# 拼接WHERE子句
|
||||
if where_conditions:
|
||||
sql_query += " WHERE " + " AND ".join(where_conditions)
|
||||
# 拼接LIMIT
|
||||
if limit is not None:
|
||||
sql_query += f" LIMIT {limit}"
|
||||
|
||||
# sql_query="select * from hive_metastore.taf_level_two_plus.accident_report_data WHERE oneid = 'LE4LG4GB6SL256937' limit 1"
|
||||
print(f"查询sql:{sql_query}")
|
||||
|
||||
# 执行SQL(纯Python,无JVM)
|
||||
with sql.connect(
|
||||
server_hostname=self.host,
|
||||
http_path=f"sql/protocolv1/o/3035612432650494/{self.cluster_id}", # 注意路径格式
|
||||
access_token=self.token,
|
||||
) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql_query)
|
||||
# 获取列名和数据,转换为字典格式(和原代码返回格式一致)
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
results.append(dict(zip(columns, row)))
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
print(f"查询失败: {str(e)}")
|
||||
raise
|
||||
|
||||
# 复用原有时间解析逻辑
|
||||
def _parse_iso_time(self, time_str):
|
||||
try:
|
||||
if time_str.endswith("Z"):
|
||||
time_str = time_str[:-1] + "+00:00"
|
||||
dt = datetime.fromisoformat(time_str)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
except Exception as e:
|
||||
raise ValueError(f"无效的时间格式 '{time_str}': {str(e)}")
|
||||
|
||||
def stop(self):
|
||||
# 纯Python连接器无需关闭SparkSession,此处保留方法以兼容原有调用逻辑
|
||||
print("Databricks SQL 连接已释放")
|
||||
@@ -0,0 +1,115 @@
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
import math
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import current_app, json
|
||||
import numpy as np
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import threading # 用于线程安全
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
|
||||
|
||||
# --- 全局状态管理 ---
|
||||
# 使用一个简单的标志和锁来确保线程安全
|
||||
cluster_ensured_running = False
|
||||
cluster_check_lock = threading.Lock()
|
||||
|
||||
|
||||
def encrypt_password(raw_password: str) -> str:
|
||||
"""加密密码(使用pbkdf2:sha256算法)"""
|
||||
return generate_password_hash(raw_password, method="pbkdf2:sha256", salt_length=16)
|
||||
|
||||
|
||||
def verify_password(encrypted_password: str, raw_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return check_password_hash(encrypted_password, raw_password)
|
||||
|
||||
|
||||
def convert_incident_time_format(data_list):
|
||||
for item in data_list:
|
||||
# 原始数据是Timestamp类型,直接获取
|
||||
original_ts = item["incident_time"]
|
||||
print('original_ts',type(original_ts))
|
||||
# 1. 为Timestamp添加原时区(假设原时间是Asia/Shanghai时区,且原始Timestamp无时区)
|
||||
# 注意:如果Timestamp已有时区,直接跳过localize,用tz_convert即可
|
||||
if original_ts.tz is None: # 检查是否已有时区
|
||||
original_tz_ts = original_ts.tz_localize(ZoneInfo("Asia/Shanghai"))
|
||||
else:
|
||||
original_tz_ts = original_ts # 已有时区则直接使用
|
||||
|
||||
# 2. 转换为UTC时区
|
||||
utc_ts = original_tz_ts.tz_convert(ZoneInfo("UTC"))
|
||||
|
||||
# 3. 格式化输出目标格式(添加T、毫秒和时区标识)
|
||||
target_str = utc_ts.strftime("%Y-%m-%dT%H:%M:%S") + ".000+0000"
|
||||
|
||||
# 更新字段
|
||||
item["incident_time"] = target_str
|
||||
|
||||
|
||||
# 处理NaN的值
|
||||
# processed_data = {}
|
||||
for key, value in item.items():
|
||||
# 检查当前值是否为NaN(仅针对数值类型)
|
||||
if isinstance(value, (int, float)): # 确保是数值类型
|
||||
# 判断是否为NaN(同时兼容math.nan和np.nan)
|
||||
if math.isnan(value) or np.isnan(value):
|
||||
item[key] = None
|
||||
else:
|
||||
item[key] = value
|
||||
else:
|
||||
# 非数值类型(如字符串、None等)直接保留
|
||||
item[key] = value
|
||||
|
||||
return data_list
|
||||
|
||||
# gen5的车码查询前替换为真实的车码
|
||||
def mask_vin(real_vin):
|
||||
id_str = real_vin[-6:]
|
||||
if not id_str.isdigit():
|
||||
print('The last six digits of vin:%s are not numbers!' % real_vin)
|
||||
return real_vin
|
||||
masked_id = ''
|
||||
add = 0
|
||||
total = 0
|
||||
mapping = {
|
||||
73: 88,
|
||||
79: 89,
|
||||
81: 90,
|
||||
}
|
||||
for i in id_str:
|
||||
total += int(i)
|
||||
for i in id_str:
|
||||
add += total + 2
|
||||
c = int(i) + 65 + add % 13
|
||||
if c in [73, 79, 81]:
|
||||
c = mapping[c]
|
||||
masked_id += chr(c)
|
||||
|
||||
masked_vin = real_vin[:-6] + masked_id[::-1]
|
||||
|
||||
return masked_vin
|
||||
|
||||
|
||||
def decrypt_code(encrypted_data):
|
||||
try:
|
||||
raw_key=current_app.config['HEADERS_SECRET_KEY']
|
||||
key_bytes=raw_key.encode('utf-8')[:16]
|
||||
if len(key_bytes)<16:
|
||||
key_bytes=key_bytes.ljust(16,b"0")
|
||||
|
||||
combined=base64.b64decode(encrypted_data)
|
||||
iv_byte=combined[:16]
|
||||
ct_byte=combined[16:]
|
||||
|
||||
ciper=AES.new(key_bytes,AES.MODE_CBC,iv=iv_byte)
|
||||
des_padd=ciper.decrypt(ct_byte)
|
||||
|
||||
desrypt_res=unpad(des_padd,AES.block_size)
|
||||
|
||||
return desrypt_res.decode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
print('解析失败')
|
||||
return None
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
set -e # 出错立即退出
|
||||
|
||||
# 1. 验证关键配置
|
||||
echo "验证关键配置..."
|
||||
[ -z "$DATABASE_URL" ] && echo "ERROR: 缺少 DATABASE_URL!" && exit 1
|
||||
[ -z "$DATABRICKS_HOST" ] && echo "ERROR: 缺少 DATABRICKS_HOST!" && exit 1
|
||||
[ -z "$DATABRICKS_TOKEN" ] && echo "ERROR: 缺少 DATABRICKS_TOKEN!" && exit 1
|
||||
[ -z "$DATABRICKS_CLUSTER_ID" ] && echo "ERROR: 缺少 DATABRICKS_CLUSTER_ID!" && exit 1
|
||||
[ -z "$JWT_SECRET_KEY" ] && echo "ERROR: 缺少 JWT_SECRET_KEY!" && exit 1
|
||||
[ -z "$SECRET_KEY" ] && echo "ERROR: 缺少 SECRET_KEY!" && exit 1
|
||||
echo "✅ 关键配置验证通过"
|
||||
|
||||
# 2. 激活虚拟环境
|
||||
echo "激活虚拟环境..."
|
||||
. /app/venv/bin/activate
|
||||
echo "✅ 虚拟环境已激活"
|
||||
|
||||
# 3. 创建databricks-connect配置目录
|
||||
echo "创建配置目录..."
|
||||
mkdir -p /home/appuser/.databricks-connect
|
||||
echo "✅ 配置目录创建完成"
|
||||
|
||||
# 4. 生成配置文件(非交互式)
|
||||
echo "生成databricks-connect配置文件..."
|
||||
cat > /home/appuser/.databricks-connect/config << EOF
|
||||
[DEFAULT]
|
||||
host = $DATABRICKS_HOST
|
||||
token = $DATABRICKS_TOKEN
|
||||
cluster_id = $DATABRICKS_CLUSTER_ID
|
||||
port = 15001
|
||||
accept_eula = yes
|
||||
EOF
|
||||
echo "✅ 配置文件生成完成"
|
||||
|
||||
# 5. 设置必要的环境变量
|
||||
echo "设置环境变量..."
|
||||
export SPARK_HOME="$(dirname $(which python))/../lib/python3.12/site-packages/pyspark"
|
||||
export DATABRICKS_HOST="$DATABRICKS_HOST"
|
||||
export DATABRICKS_TOKEN="$DATABRICKS_TOKEN"
|
||||
export DATABRICKS_CLUSTER_ID="$DATABRICKS_CLUSTER_ID"
|
||||
export DATABRICKS_PORT=15001
|
||||
export PATH="$PATH:/app/venv/bin"
|
||||
echo "✅ 环境变量设置完成"
|
||||
echo "SPARK_HOME=$SPARK_HOME"
|
||||
|
||||
# 6. 验证配置
|
||||
echo "验证 databricks-connect 配置..."
|
||||
if databricks-connect test; then
|
||||
echo "✅ databricks-connect 配置验证成功"
|
||||
else
|
||||
echo "❌ databricks-connect 配置验证失败,但继续启动服务..."
|
||||
# 这里不退出,因为有时测试会失败但实际连接是正常的
|
||||
fi
|
||||
|
||||
# 7. 启动 Flask 服务
|
||||
echo "启动 Flask 服务..."
|
||||
exec gunicorn --bind 0.0.0.0:5221 --workers 14 --timeout 600 "$FLASK_APP"
|
||||
@@ -0,0 +1,20 @@
|
||||
"""初始化数据库:根据 app/models.py 中的模型自动创建全部表。
|
||||
|
||||
用法(在 backend 目录,已激活 .venv):
|
||||
python init_db.py
|
||||
|
||||
说明:
|
||||
- 会连接 app/config.py 中当前环境(默认 development)配置的数据库。
|
||||
- MySQL 需先手动建库:CREATE DATABASE incid_db ...(见 schema.sql 第一行)。
|
||||
- 若使用 SQLite(见 README/说明),无需提前建库,运行本脚本即可生成表。
|
||||
"""
|
||||
from app import create_app, db
|
||||
import app.models # noqa: F401 确保所有模型注册到 metadata
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
tables = sorted(db.metadata.tables.keys())
|
||||
print("数据库初始化完成,已创建/校验以下表:")
|
||||
for name in tables:
|
||||
print(" -", name)
|
||||
@@ -0,0 +1,32 @@
|
||||
# 集群配置
|
||||
DATABRICKS_TOKEN=dapi188523cfb1cf34ad5a605fd75c5c0d31
|
||||
CLUSTER_ID=1203-054146-wkjf95i0
|
||||
DATABRICKS_HOST=https://adb-3035612432650494.2.databricks.azure.cn
|
||||
|
||||
# gen5 spark表的配置
|
||||
GEN5_TABLE_NAME=hive_metastore.taf_level_two_plus.accident_report_data
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET_KEY=dev_mb_project_2025_xyz123 # 开发环境可随意,生产环境需复杂密钥
|
||||
SECRET_KEY=dev_mb_2025_abc789
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=mysql+pymysql://root:V2OuCHPL%40changeit@10.200.10.193:3306/incid_db?charset=utf8mb4
|
||||
|
||||
# oauth2配置
|
||||
CLIENT_ID=B7E86ADC-A8F7-4462-87FC-A04DFDF3C58F
|
||||
CLIENT_PASSWORD=AWd408ETS_V~j-yt.763P1D9ovC25fbe
|
||||
|
||||
SS0_HOST=https://ssoalpha.dvb.corpinter.net.cn/
|
||||
# 授权成功后,SSO 服务器重定向回你的应用的地址 (必须与 SSO 服务器上配置的一致)
|
||||
REDIRECT_URL=http://150.158.236.152:5221/incident/authorized
|
||||
# 用户登录成功后,最终重定向到的前端页面地址
|
||||
DOMAIN=http://150.158.236.152:5000
|
||||
|
||||
# 图片上传的配置
|
||||
UPLOAD_FOLDER=/app/uploads
|
||||
MAX_CONTENT_LENGTH=5242880
|
||||
IMAGE_BASE_URL=http://150.158.236.152:5221
|
||||
|
||||
# 前端请求头的key
|
||||
HEADERS_SECRET_KEY=mbincid-server12
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
# run.py(仅用于开发测试)
|
||||
import os
|
||||
|
||||
from flask import current_app, send_from_directory
|
||||
from app import create_app
|
||||
|
||||
|
||||
# 开发环境配置(启用 DEBUG、自动重载等)
|
||||
app = create_app()
|
||||
|
||||
@app.route('/uploads/<path:filename>')
|
||||
def serve_uploads(filename):
|
||||
"""静态资源路由:允许访问/uploads下的所有文件"""
|
||||
return send_from_directory(current_app.config['UPLOAD_FOLDER'], filename)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Flask 自带开发服务器:支持自动重载、调试日志
|
||||
app.run(
|
||||
host="0.0.0.0", # 允许外部访问
|
||||
port=5221, # 端口(与 Gunicorn 配置一致,便于测试)
|
||||
debug=True, # 开发模式:自动重载、显示错误详情
|
||||
use_reloader=True, # 代码修改后自动重启
|
||||
)
|
||||
|
||||
# app.run(host="0.0.0.0", port=5221, debug=False)
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
-- 自动生成:incid_db 建库建表脚本
|
||||
CREATE DATABASE IF NOT EXISTS incid_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE incid_db;
|
||||
|
||||
CREATE TABLE accid_list (
|
||||
id INTEGER NOT NULL COMMENT '主键ID,自增' AUTO_INCREMENT,
|
||||
ticket_id VARCHAR(50) NOT NULL COMMENT '工单ID',
|
||||
model VARCHAR(100) COMMENT '型号',
|
||||
vin VARCHAR(50) NOT NULL COMMENT '车辆识别码(VIN)',
|
||||
creator VARCHAR(50) NOT NULL COMMENT '创建人(用户名,如ZHANMEN)',
|
||||
create_time DATETIME NOT NULL COMMENT '工单新建时间',
|
||||
occur_datetime DATETIME NOT NULL COMMENT '问题发生时间',
|
||||
case_description TEXT NOT NULL COMMENT 'Case描述',
|
||||
fo_solution TEXT NOT NULL COMMENT 'Fo解答',
|
||||
category VARCHAR(100) NOT NULL COMMENT '分类',
|
||||
keywords VARCHAR(200) NOT NULL COMMENT '关键词(多个用逗号分隔)',
|
||||
handle_status VARCHAR(20) COMMENT '处理状态:已完成/未完成',
|
||||
create_at DATETIME COMMENT '记录插入数据库时间',
|
||||
update_at DATETIME COMMENT '记录最后更新时间',
|
||||
bmbs_name VARCHAR(50) COMMENT 'BMBS联系人',
|
||||
accident_level ENUM('P1','P2','P3') COMMENT '事故评级',
|
||||
rdca_reporter VARCHAR(50) COMMENT 'RDCA报告人',
|
||||
carline VARCHAR(100) COMMENT '车系(关联字典表dict_code=carline)',
|
||||
software_version VARCHAR(50) COMMENT '软件版本',
|
||||
labels VARCHAR(100) COMMENT '事故类型标签(关联字典表dict_code=labels)',
|
||||
attachment_url VARCHAR(255) COMMENT '附件地址(多个用逗号分隔)',
|
||||
assignee_name VARCHAR(50) COMMENT '经办人(关联字典表/用户表)',
|
||||
affects_versions VARCHAR(100) COMMENT '受影响的版本(关联字典表dict_code=affects_versions)',
|
||||
components VARCHAR(100) COMMENT '零部件/组件(关联字典表dict_code=components)',
|
||||
fix_versions VARCHAR(100) COMMENT '修复方案对应的版本(关联字典表dict_code=fix_versions)',
|
||||
subtasks VARCHAR(200) NOT NULL COMMENT '子任务(多个用逗号分隔)',
|
||||
linked_work_items VARCHAR(200) NOT NULL COMMENT '关联的工作项(格式:类型:ID,类型:ID)',
|
||||
descriptions VARCHAR(255) NOT NULL COMMENT '补充描述(区别于case_description)',
|
||||
mviz_link VARCHAR(255) NOT NULL COMMENT 'mviz的链接',
|
||||
system_function VARCHAR(255) NOT NULL COMMENT 'system_function字段',
|
||||
image_ids VARCHAR(255) NOT NULL COMMENT '关联图片ID串,逗号分隔(如:1,2,3)',
|
||||
PRIMARY KEY (id)
|
||||
)CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC ENGINE=InnoDB COLLATE utf8mb4_unicode_ci;
|
||||
CREATE INDEX idx_carline ON accid_list (carline);
|
||||
CREATE INDEX idx_labels ON accid_list (labels);
|
||||
CREATE INDEX idx_components ON accid_list (components);
|
||||
CREATE INDEX idx_ticket_id ON accid_list (ticket_id);
|
||||
CREATE INDEX idx_affects_versions ON accid_list (affects_versions);
|
||||
CREATE INDEX idx_accident_level ON accid_list (accident_level);
|
||||
CREATE INDEX idx_create_time ON accid_list (create_time);
|
||||
CREATE INDEX idx_fix_versions ON accid_list (fix_versions);
|
||||
CREATE INDEX idx_creator ON accid_list (creator);
|
||||
CREATE INDEX idx_assignee_name ON accid_list (assignee_name);
|
||||
|
||||
CREATE TABLE dict_type (
|
||||
id INTEGER NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
dict_code VARCHAR(50) NOT NULL COMMENT '字典编码(对应accid_list的字段名,如carline、labels)',
|
||||
dict_name VARCHAR(50) NOT NULL COMMENT '字典名称(字段中文名称,如车系、事故类型)',
|
||||
remark VARCHAR(200) COMMENT '备注(如“事故工单-车系下拉选项”)',
|
||||
status INTEGER NOT NULL COMMENT '状态:1-启用,0-禁用',
|
||||
create_time DATETIME COMMENT '创建时间',
|
||||
update_time DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT uk_dict_code UNIQUE (dict_code)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='字典类型表(管理需要下拉的字段)' ROW_FORMAT=Dynamic COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE file_uploads (
|
||||
id INTEGER NOT NULL COMMENT '主键(自增数值ID)' AUTO_INCREMENT,
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '原始文件名(如:事故图片1.jpg)',
|
||||
file_alias VARCHAR(255) NOT NULL COMMENT '存储别名(避免重名,如:20260210_153950_89782.jpg)',
|
||||
file_path VARCHAR(512) NOT NULL COMMENT '存储路径/访问URL(本地:/uploads/202602/xxx.jpg;OSS:https://xxx.oss-cn-beijing.aliyuncs.com/xxx.jpg)',
|
||||
file_size INTEGER NOT NULL COMMENT '文件大小(字节)',
|
||||
file_type VARCHAR(50) NOT NULL COMMENT '文件MIME类型(如:image/jpeg、image/png)',
|
||||
business_type VARCHAR(50) COMMENT '关联业务类型(如:case=案例、user=用户头像)',
|
||||
business_id INTEGER COMMENT '关联业务主键(案例ID/用户ID等,初始为NULL)',
|
||||
create_time DATETIME NOT NULL COMMENT '上传时间',
|
||||
create_by VARCHAR(50) COMMENT '上传人(用户ID/用户名)',
|
||||
status INTEGER NOT NULL COMMENT '状态:1=有效,0=逻辑删除',
|
||||
PRIMARY KEY (id)
|
||||
)CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC ENGINE=InnoDB COLLATE utf8mb4_unicode_ci;
|
||||
CREATE INDEX idx_business_type_id ON file_uploads (business_type, business_id);
|
||||
CREATE INDEX idx_create_time ON file_uploads (create_time);
|
||||
CREATE INDEX idx_status ON file_uploads (status);
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
status INTEGER COMMENT '用户状态:1-正常,0-禁用',
|
||||
created_at DATETIME COMMENT '创建时间',
|
||||
updated_at DATETIME COMMENT '更新时间',
|
||||
email VARCHAR(255) COMMENT '邮箱',
|
||||
avatar VARCHAR(255) COMMENT '头像(通常存储图片URL)',
|
||||
region VARCHAR(255) COMMENT '国家地区',
|
||||
phone VARCHAR(255) COMMENT '电话',
|
||||
brief VARCHAR(255) COMMENT '个人简介',
|
||||
third_party_account VARCHAR(255) COMMENT '第三方账号(如微信、QQ等)',
|
||||
position VARCHAR(255) COMMENT '职务',
|
||||
department VARCHAR(255) COMMENT '部门',
|
||||
label VARCHAR(255) COMMENT '个人标签(多个标签可逗号分隔)',
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (username),
|
||||
FOREIGN KEY(created_by) REFERENCES users (id),
|
||||
FOREIGN KEY(updated_by) REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE dict_item (
|
||||
id INTEGER NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
|
||||
dict_id INTEGER NOT NULL COMMENT '关联字典类型表的ID',
|
||||
item_value VARCHAR(100) NOT NULL COMMENT '选项值(存储到accid_list的实际值,如“大众帕萨特”“P1”)',
|
||||
item_label VARCHAR(100) NOT NULL COMMENT '选项标签(前端显示的文本,如“大众帕萨特(车系)”)',
|
||||
sort INTEGER COMMENT '排序号(前端下拉框显示顺序,数字越小越靠前)',
|
||||
status INTEGER NOT NULL COMMENT '状态:1-启用,0-禁用(禁用后前端不显示)',
|
||||
remark VARCHAR(200) COMMENT '备注(如“2025款帕萨特专属”)',
|
||||
create_time DATETIME COMMENT '创建时间',
|
||||
update_time DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY(dict_id) REFERENCES dict_type (id)
|
||||
)CHARSET=utf8mb4 ENGINE=InnoDB COMMENT='字典项表(管理每个字段的具体下拉选项)' ROW_FORMAT=Dynamic COLLATE utf8mb4_unicode_ci;
|
||||
CREATE INDEX idx_item_value ON dict_item (item_value);
|
||||
CREATE INDEX idx_dict_id ON dict_item (dict_id);
|
||||
|
||||
CREATE TABLE roles (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(50) NOT NULL COMMENT '角色名称',
|
||||
description VARCHAR(255) COMMENT '角色描述',
|
||||
status INTEGER COMMENT '状态:1-启用,0-禁用',
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (name),
|
||||
FOREIGN KEY(created_by) REFERENCES users (id),
|
||||
FOREIGN KEY(updated_by) REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
created_at DATETIME,
|
||||
created_by INTEGER,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users (id),
|
||||
FOREIGN KEY(role_id) REFERENCES roles (id),
|
||||
FOREIGN KEY(created_by) REFERENCES users (id)
|
||||
);
|
||||
Reference in New Issue
Block a user