This commit is contained in:
ZhuJW
2026-07-10 18:55:55 +08:00
commit fce40c7d6c
317 changed files with 170079 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
**.venv
**.vscode
**/node_modules
__pycache__/
*.py[cod]
+29
View File
@@ -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
+44
View File
@@ -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"]
+87
View File
@@ -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
View File
@@ -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__)
+606
View File
@@ -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
+335
View File
@@ -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_intvalue为聚合后的数据
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]是queueargs[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_timeID字段=oneid
- gen6_histograms:时间=timeID字段=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"
}
}
+144
View File
@@ -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)
+285
View File
@@ -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.")
+89
View File
@@ -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]
+132
View File
@@ -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)}")
+590
View File
@@ -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.jpgOSShttps://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 "已删除",
}
View File
+128
View File
@@ -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 连接已释放")
+115
View File
@@ -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
+58
View File
@@ -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"
+20
View File
@@ -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)
View File
+32
View File
@@ -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.
+28
View File
@@ -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)
+144
View File
@@ -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.jpgOSShttps://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)
);
+60
View File
@@ -0,0 +1,60 @@
server {
listen 80;
server_name 150.158.236.152;
client_max_body_size 100M;
# root /usr/share/nginx/html;
include /etc/nginx/mime.types;
# 处理OPTIONS请求
# location / {
# if ($request_method = 'OPTIONS') {
# add_header 'Access-Control-Allow-Origin' '*' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
# add_header 'Access-Control-Max-Age' 1728000 always;
# add_header 'Content-Type' 'text/plain; charset=utf-8' always;
# add_header 'Content-Length' 0 always;
# return 204;
# }
# try_files $uri $uri/ /index.html;
# }
location / {
root /usr/share/nginx/html; # 末尾无斜杠
try_files $uri $uri/ /index.html;
}
location /accidentportal/ {
rewrite ^/accidentportal/(.*)$ /$1 last;
}
# 修复API代理
location /accidentportal/api/ {
# rewrite ^/api(.*)$ $1 break;
# proxy_pass http://150.158.236.152:5221/; # 注意结尾斜杠
proxy_buffer_size 128k;
proxy_buffers 16 128k;
proxy_busy_buffers_size 256k;
proxy_max_temp_file_size 1024m;
proxy_pass http://172.17.0.1:5221/accidentportal/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# 关键:Cookie处理
proxy_cookie_path / "/; SameSite=Lax";
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"init",
"build",
"ci",
"chore",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test"
]
]
}
}
+4
View File
@@ -0,0 +1,4 @@
VITE_APP_NAME=Backend
VITE_APP_BASE=/
VITE_APP_BASE_API=/api
VITE_APP_LOAD_ROUTE_WAY=BACKEND
+22
View File
@@ -0,0 +1,22 @@
VITE_APP_BASE_API=/accidentportal/api
VITE_APP_BASE_URL=http://localhost:5221
VITE_APP_LOAD_ROUTE_WAY=FRONTEND
VITE_APP_BASE_API_DEV=/api
VITE_APP_BASE_URL_DEV=http://localhost:5221
# The title of your application (string)
VITE_GLOB_APP_TITLE="RDCA_Incidents Data Portal"
# 标记为测试环境
VITE_ENV=test
# 登录跳转
VITE_LOGIN_URL = http://localhost:5221/accidentportal/user/login
# 请求头的密钥
VITE_APP_CHECK_KEY = 'mbincid-server12'
# 前端配置域名加的前缀名
VITE_APP_DOMAIN_PREFIX='accidentportal'
+14
View File
@@ -0,0 +1,14 @@
VITE_APP_BASE_API=/accidentportal/api
VITE_APP_BASE_URL=http://150.158.236.152:5221
# The title of your application (string)
VITE_GLOB_APP_TITLE="RDCA_Incidents Data Portal"
# 登录跳转
# VITE_LOGIN_URL = http://150.158.236.152:5221/login
VITE_LOGIN_URL = http://150.158.236.152:5221/accidentportal/api/user/login
# 请求头的密钥
VITE_APP_CHECK_KEY = 'mbincid-server12'
# 前端配置域名加的前缀名
VITE_APP_DOMAIN_PREFIX = 'accidentportal'
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
open_collective: antdv-pro
custom: ['https://docs.antdv-pro.com/other/sponsor.html']
+30
View File
@@ -0,0 +1,30 @@
name: deploy
on:
push:
branches:
- main
jobs:
DEPLOY:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 20.x
cache: pnpm
- name: Install
run: pnpm install --no-frozen-lockfile
- name: Build
run: pnpm run build
- name: Deploy
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
server: ${{ secrets.FTP_HOST }} # e.g. ftp.host.com or sftp.host.com (without ftp:// or ftps://)
username: ${{ secrets.FTP_USERNAME }} # FTP username
password: ${{ secrets.FTP_PASSWORD }} # FTP password
local-dir: dist/ # optional, local path, default is: dist
+20
View File
@@ -0,0 +1,20 @@
name: Issue Close Require
on:
schedule:
- cron: '0 0 * * *'
jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- name: need reproduce
uses: actions-cool/issues-helper@v1.7
with:
actions: close-issues
labels: 🤔 Need Reproduce
inactive-day: 7
body: |
Since the issue was labeled with `Need Reproduce`, but no response in 7 days. This issue will be closed. If you have any questions, you can comment and reply.
由于该 issue 被标记为需要复现信息,却 7 天未收到回应。现关闭 issue,若有任何问题,可评论回复。
+27
View File
@@ -0,0 +1,27 @@
name: Lint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
LINT:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 20.x
cache: pnpm
- name: Install
run: pnpm install --no-frozen-lockfile
- name: Lint
run: pnpm run lint
+25
View File
@@ -0,0 +1,25 @@
name: Release
permissions:
contents: write
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 20.x
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+3
View File
@@ -0,0 +1,3 @@
echo "husky commit-msg-lint start"
npx --no -- commitlint --edit ""
echo "husky commit-msg-lint end"
+3
View File
@@ -0,0 +1,3 @@
echo "husky lint-staged start"
npx lint-staged
echo "husky lint-staged end"
+4
View File
@@ -0,0 +1,4 @@
public-hoist-pattern[]=@vue/runtime-core
public-hoist-pattern[]=eslint-*
public-hoist-pattern[]=@typescript-eslint*
package-manager-strict=false
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
FROM nginx
RUN rm /etc/nginx/conf.d/default.conf
ADD default.conf /etc/nginx/conf.d/default.conf
COPY dist/ /usr/share/nginx/html/
RUN chmod 775 -R /usr/share/nginx/html
EXPOSE 80/tcp
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) [2023] [Antdv Pro]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+98
View File
@@ -0,0 +1,98 @@
<div align="center"> <a href="https://github.com/antdv-pro/antdv-pro"> <img alt="VbenAdmin Logo" width="200" height="200" src="./public/logo.svg"> </a> <br> <br>
<h1>Antdv Pro</h1>
</div>
![gitee](https://gitee.com/antdv-pro/antdv-pro/badge/star.svg)
![github](https://img.shields.io/github/stars/antdv-pro/antdv-pro?style=social)
**English** | [简体中文](./README.zh-CN.md)
## Introduction
AntdvPro is a complete set of enterprise-level mid-backend front-end/design solutions based on Vue3, Vite4, ant-design-vue4, Pinia, UnoCSS and Typescript. It refers to the design pattern of Ali react version antd-pro, using the latest and most popular The front-end technology stack has built-in dynamic routing, multi-theme, multi-layout and other functions, which can help you quickly build enterprise-level mid-background product prototypes.
## Features
* pnpm: Using the latest pnpm as a package management tool, it can greatly reduce the size of node_modules, speed up the installation speed of packages, and can also share dependencies to reduce disk usage.
* vite: vite as a front-end development tool, it can greatly speed up the start-up speed of the project, and also supports hot updates, which can greatly improve development efficiency.
* vue3: vue3.3.x as the front-end framework, the basic code is written in script-setup, with less code and low maintenance cost.
* nitro mock: Use nitro as the server's mock data, decoupled from the project, and more flexible and easy to use.
* ant-design-vue4: ant-design-vue4 as the UI framework, the author of admin-pro is also a core member of ant-design-vue, which can provide long-term maintenance support.
* pinia: pinia as a state management tool, it can greatly improve the readability and maintainability of the code, and also supports Typescript.
* UnoCSS: Atomic CSS framework, reduce the troubles caused by thinking about some common class names, and improve our development efficiency.
* Code specification: We have encapsulated a set of eslint-based code specification configuration files, which can be used out of the box to unify the problems brought by different teams.
* Theme: The design specifications of antd-pro of the react version are used, and a set of theme modes based on vue are developed. On this basis, some new functions are added to meet various different needs as much as possible.
* Request function: Based on axios, a set of request functions with complete types and some basic interceptor encapsulations are encapsulated. You only need to make corresponding implementation adjustments according to the requirements to meet the different needs of various projects.
* Mobile compatibility: We have tried our best to make the basic framework compatible with the mobile terminal mode, but because our main goal is the enterprise-level mid-background product, we have not made too much adaptation to the mobile terminal. If your project needs to adapt to the mobile terminal, you can refer to our code for corresponding adjustments.
## Preview
[antdv-pro](https://antdv-pro.com) - Test Account: admin/admin
[antdv-pro-js](https://github.com/antdv-pro/antdv-pro/tree/feat-js) - Javascript version
[antdv-pro-js-gitee](https://gitee.com/antdv-pro/antdv-pro/tree/feat-js/) - Gitee Javascript version
[antdv-pro-docs](https://docs.antdv-pro.com) - Documentation
## Community
QQ Group: apply wechat group
Wechat: aibayanyu2022
Wechat Group: Add author WeChat
Discord: [discord](https://discord.gg/tPb4G6gXmm)
## Useage
```bash
# Install degit
npm i -g degit
# Pull the code
degit antdv-pro/antdv-pro [your project name]
# Switch to the project directory
cd [your project name]
# Install
pnpm install
# Development
pnpm dev
```
## Contribute
We are very welcome to have you participate in our open source project.
**Pull Request:**
1. Fork code!
2. Create your own branch: `git checkout -b feat-xxxx`
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
4. Push your branch: `git push origin feat-xxxx`
5. submit`pull request`
Thank you to all the people who already contributed to antdv-pro!
<a href="https://github.com/antdv-pro/antdv-pro/graphs/contributors">
<img src="https://contrib.rocks/image?repo=antdv-pro/antdv-pro&max=100&columns=15" />
</a>
## Support
If you like our project, you can support us by clicking the "Star" button in the upper right corner. Your support is my motivation. Thank you ~
Thanks to the open source project license provided by [Jetbrains](https://www.jetbrains.com/?from=antdv-pro).
## Sponsor
If you like our project, you can sponsor us to help us maintain the project better.
[Alipay/Wechat](https://docs.antdv-pro.com/other/sponsor.html)
+97
View File
@@ -0,0 +1,97 @@
<div align="center"> <a href="https://github.com/antdv-pro/antdv-pro"> <img alt="VbenAdmin Logo" width="200" height="200" src="./public/logo.svg"> </a> <br> <br>
<h1>Antdv Pro</h1>
</div>
![gitee](https://gitee.com/antdv-pro/antdv-pro/badge/star.svg)
![github](https://img.shields.io/github/stars/antdv-pro/antdv-pro?style=social)
[English](./README.md) | **简体中文**
## 介绍
AntdvPro是一个基于Vue3、Vite4、ant-design-vue4、Pinia、UnoCSS和Typescript的一整套企业级中后台前端/设计解决方案,它参考了阿里react版本antd-pro的设计模式,使用了最新最流行的前端技术栈,内置了动态路由、多主题、多布局等功能,可以帮助你快速搭建企业级中后台产品原型。
## 特性
* pnpm:使用了最新的pnpm作为包管理工具,它可以大大减少node_modules的体积,加快包的安装速度,同时还可以共享依赖,减少磁盘占用。
* vite:vite作为前端开发工具,它可以大大加快项目的启动速度,同时还支持热更新,可以大大提高开发效率。
* vue3:vue3.3.x作为前端框架,基础代码全部使用script-setup的写法,代码量少维护成本低。
* nitro mock:采用nitro作为服务端的mock数据,从工程中解耦处理,更加灵活易用。
* ant-design-vue4ant-design-vue4作为UI框架,admin-pro的作者也是ant-design-vue的核心成员,可提供长期的维护支持。
* pinia:pinia作为状态管理工具,它可以大大提高代码的可读性和可维护性,同时还支持Typescript。
* UnoCSS:原子化的CSS框架,减少我们去想一些通用类名带来的烦恼,提升我们的开发效率。
* 代码规范:我们封装了一套基于eslint的代码规范配置文件,开箱即用,统一不同团队所带来的问题。
* 主题:延用了react版本的antd-pro的设计规范,开发了一套基于vue的主题模式,在此基础上增加了一些新的功能,尽可能的满足各种不同的需求。
* 请求函数:基于axios封装了一套具有完善类型的请求函数,以及一些基础的拦截器的封装,只需要按照需求做对应的实现调整就能满足各种项目带来的不一样的需求。
* 移动端兼容:基础框架部分我们尽可能的对移动端的模式进行了兼容处理,但是由于我们的主要目标是企业级中后台产品,所以我们并没有对移动端做过多的适配,如果你的项目需要移动端的适配,可以参考我们的代码进行相应的调整。
## 演示
[antdv-pro](https://antdv-pro.com) - 测试账号:admin/admin
[antdv-pro-js](https://github.com/antdv-pro/antdv-pro/tree/feat-js) - JS版本
[antdv-pro-js-gitee](https://gitee.com/antdv-pro/antdv-pro/tree/feat-js/) - Gitee JS 版本
[antdv-pro-docs](https://docs.antdv-pro.com) - 在线文档地址
## 社区
QQ群: 申请微信群
微信: 已经被广告闹麻了自己控制台 Console解密 atob("YWliYXlhbnl1MjAyMg==") 再有广告就对不起大家了,gitee删库不做gitee仓库了
Discord: [discord](https://discord.gg/tPb4G6gXmm)
## 使用
```bash
# 安装degit
npm i -g degit
# 拉取代码
degit antdv-pro/antdv-pro [your project name]
# 切换到项目目录
cd [your project name]
# 安装依赖
pnpm install
# 启动项目
pnpm dev
```
## 贡献
非常欢迎您参与到我们的开源项目中来~
**PR流程:**
1. Fork 代码!
2. 创建自己的分支: `git checkout -b feat-xxxx`
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支: `git push origin feat-xxxx`
5. 提交`pull request`
感谢所有为`antdv-pro`做出贡献的小伙伴儿们!
<a href="https://github.com/antdv-pro/antdv-pro/graphs/contributors">
<img src="https://contrib.rocks/image?repo=antdv-pro/antdv-pro&max=100&columns=15" />
</a>
## 支持
如果你觉得这个项目对你有帮助,你可以点右上角 "Star" 支持一下,你的支持就是我的动力,谢谢~
感谢[Jetbrains](https://www.jetbrains.com/?from=antdv-pro).提供的开源项目许可证支持
## 赞助
如果你觉得这个项目对你有帮助,你可以点击下方链接对我进行赞助,谢谢~
[赞助](https://docs.antdv-pro.com/other/sponsor.html)
+45
View File
@@ -0,0 +1,45 @@
server {
listen 80;
server_name 150.158.236.152; # 替换为你的服务器 IP/域名
client_max_body_size 100M; # 全局生效的文件上传大小限制
# 日志配置
access_log /var/log/nginx/front-access.log main;
error_log /var/log/nginx/front-error.log warn;
# gzip 压缩(保留原有配置)
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
# 前端静态文件根目录(适配 Docker 挂载)
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
# 静态资源缓存优化
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
access_log off;
}
# SPA 路由兜底
location / {
try_files $uri $uri/ /index.html;
}
# 错误页适配 SPA
error_page 404 500 502 503 504 /index.html;
# 后端 API 代理(替换为实际地址)
location /api {
rewrite ^/api(.*)$ $1 break;
proxy_pass http://150.158.236.152:5221; # 你的 Flask 后端地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
+21
View File
@@ -0,0 +1,21 @@
import antfu from '@antfu/eslint-config'
export default antfu({
formatters: {
css: true,
},
ignores: [
'types/auto-imports.d.ts',
'types/components.d.ts',
'public',
'tsconfig.*.json',
'tsconfig.json',
],
}, {
rules: {
'no-console': 0,
'style/quote-props': 0,
'unused-imports/no-unused-vars': 0,
'ts/no-unused-expressions': 0,
},
})
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<script>
(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
</head>
<body>
<div id="app"></div>
<script src="/loading.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+101
View File
@@ -0,0 +1,101 @@
{
"name": "antdv-pro",
"type": "module",
"version": "1.4.0",
"packageManager": "pnpm@10.17.1",
"author": "aibayanyu <aibayanyu@qq.com>",
"repository": "antdv-pro/antdv-pro",
"engines": {
"node": ">=20.15.0"
},
"scripts": {
"dev": "vite --host",
"build": "vite build",
"clear:vercel": "rm -rf ./vercel.json",
"build:vercel": "run-s clear:vercel",
"start": "node dist/servers/app.mjs",
"preview": "vite preview",
"lint": "eslint src --fix",
"typecheck": "vue-tsc --noEmit",
"bump:patch": "changelogen --bump --output CHANGELOG.md --release",
"bump:minor": "changelogen --bump --output CHANGELOG.md --release --minor",
"bump:major": "changelogen --bump --output CHANGELOG.md --release --major",
"prepare": "husky",
"dir-tree": "esno ./scripts/dir-tree",
"gen:uno": "esno ./scripts/gen-unocss",
"toJS": "esno scripts/to-js"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/icons-vue": "^7.0.1",
"@antv/g2plot": "^2.4.35",
"@antv/l7": "^2.23.0",
"@ctrl/tinycolor": "^4.2.0",
"@v-c/utils": "^0.0.26",
"@vueuse/core": "^10.11.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.12.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.18",
"echarts": "^6.0.0",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"pinia": "^2.3.1",
"sjcl": "^1.0.9",
"sm-crypto": "^0.4.0",
"vue": "^3.5.22",
"vue-i18n": "^9.14.5",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.3",
"@mistjs/vite-plugin-preload": "^0.0.1",
"@types/crypto-js": "^4.2.2",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.19.17",
"@types/treeify": "^1.0.3",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue/test-utils": "^2.4.6",
"antdv-component-resolver": "^1.0.8",
"antdv-style": "0.0.1-beta.4",
"changelogen": "^0.5.7",
"cross-env": "^7.0.3",
"directory-tree": "^3.5.2",
"esbuild": "^0.25.10",
"eslint": "^9.36.0",
"eslint-plugin-format": "^1.0.2",
"esno": "^0.17.0",
"execa": "^8.0.1",
"fs-extra": "^11.3.2",
"h3": "2.0.0-beta.4",
"husky": "^9.1.7",
"jiti": "^2.6.0",
"jsdom": "^22.1.0",
"less": "^4.4.1",
"lint-staged": "^14.0.1",
"lodash": "^4.17.21",
"mock-h3": "0.0.1-beta.12",
"npm-run-all": "^4.1.5",
"picocolors": "^1.1.1",
"treeify": "^1.1.0",
"ts-node": "^10.9.2",
"typescript": "~5.8.3",
"unocss": "^66.5.2",
"unocss-preset-chinese": "^0.3.3",
"unocss-preset-ease": "^0.0.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-config": "^0.1.5",
"unplugin-vue-components": "^29.1.0",
"vite": "npm:rolldown-vite@7.1.13",
"vitest": "^3.2.4",
"vue-tsc": "^3.0.8"
},
"lint-staged": {
"**/*.{vue,ts,js,jsx,tsx}": "eslint src --fix"
}
}
+5
View File
@@ -0,0 +1,5 @@
// This constant defines the name of the configuration file that will be used in the production environment
export const GLOB_CONFIG_FILE_NAME = '_app.config.js'
// This constant sets the output directory for the Vite package
export const OUTPUT_DIR = 'dist'
+52
View File
@@ -0,0 +1,52 @@
import type { PluginOption } from 'vite'
import VitePluginPreloadAll from '@mistjs/vite-plugin-preload'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AntdvResolver from 'antdv-component-resolver'
import { mockH3 } from 'mock-h3/vite'
import Unocss from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import GenerateConfig from 'unplugin-config/vite'
import Components from 'unplugin-vue-components/vite'
import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from './constants'
import { viteBuildInfo } from './vite-build-info'
export function createVitePlugins(env: Record<string, string>) {
const vitePluginList: (PluginOption | PluginOption[])[] = [
vue(),
vueJsx(),
// mockH3({}),
VitePluginPreloadAll(),
AutoImport({
imports: [
'vue',
'vue-router',
'vue-i18n',
'@vueuse/core',
'pinia',
],
dts: 'types/auto-imports.d.ts',
dirs: ['src/stores', 'src/composables'],
}),
Components({
resolvers: [AntdvResolver()],
dts: 'types/components.d.ts',
dirs: ['src/components'],
}),
// https://github.com/kirklin/unplugin-config
GenerateConfig({
appName: env.VITE_GLOB_APP_TITLE,
configFile: {
generate: true,
fileName: GLOB_CONFIG_FILE_NAME,
outputDir: OUTPUT_DIR,
},
envVariables: {
prefix: 'VITE_GLOB_',
},
}),
Unocss(),
viteBuildInfo(env.VITE_APP_NAME),
]
return vitePluginList
}
+207
View File
@@ -0,0 +1,207 @@
import type { Dayjs } from 'dayjs'
import type { Plugin, ResolvedConfig } from 'vite'
import { readdir, stat } from 'node:fs'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import pkg from 'picocolors'
const { green, blue, bold } = pkg
dayjs.extend(duration)
const fileListTotal: number[] = []
// eslint-disable-next-line ts/no-unsafe-function-type
function recursiveDirectory(folder: string, callback: Function): void {
readdir(folder, (err, files: string[]) => {
if (err)
throw err
let count = 0
const checkEnd = () => {
++count === files.length && callback()
}
files.forEach((item: string) => {
stat(`${folder}/${item}`, async (err, stats) => {
if (err)
throw err
if (stats.isFile()) {
fileListTotal.push(stats.size)
checkEnd()
}
else if (stats.isDirectory()) {
recursiveDirectory(`${folder}/${item}/`, checkEnd)
}
})
})
files.length === 0 && callback()
})
}
function sum(arr: number[]) {
return arr.reduce((t: number, c: number) => {
return t + c
}, 0)
}
function formatBytes(a: number, b?: number): string {
if (a === 0)
return '0 Bytes'
const c = 1024
const d = b || 2
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const f = Math.floor(Math.log(a) / Math.log(c))
return `${Number.parseFloat((a / c ** f).toFixed(d))} ${e[f]}`
}
export function viteBuildInfo(name: string): Plugin {
let config: ResolvedConfig
let startTime: Dayjs
let endTime: Dayjs
return {
name: 'vite:buildInfo',
configResolved(resolvedConfig) {
config = resolvedConfig
},
buildStart() {
console.log(
bold(
green(
`👏欢迎使用${blue(`[${name}]`)},现在正全力为您${config.command === 'build' ? '打包' : '编译'
}`,
),
),
)
if (config.command === 'build')
startTime = dayjs(new Date())
},
closeBundle() {
if (config.command === 'build') {
endTime = dayjs(new Date())
recursiveDirectory(config.build.outDir, () => {
console.log(
bold(
green(
`恭喜打包完成🎉(总用时${dayjs
.duration(endTime.diff(startTime))
.format('mm分ss秒')},打包后的大小为${formatBytes(
sum(fileListTotal),
)}`,
),
),
)
})
}
},
}
}
// import type { Dayjs } from 'dayjs'
// import type { Plugin, ResolvedConfig } from 'vite'
// import { readdir, stat, existsSync } from 'node:fs' // 👈 新增 existsSync
// import { resolve } from 'node:path' // 👈 新增 resolve
// import dayjs from 'dayjs'
// import duration from 'dayjs/plugin/duration'
// import pkg from 'picocolors'
// const { green, blue, bold } = pkg
// dayjs.extend(duration)
// const fileListTotal: number[] = []
// function recursiveDirectory(folder: string, callback: () => void): void {
// // 👇 先检查目录是否存在
// if (!existsSync(folder)) {
// console.warn(`[vite:buildInfo] 目录不存在: ${folder}`)
// callback()
// return
// }
// readdir(folder, (err, files: string[]) => {
// if (err) {
// console.error('[vite:buildInfo] 读取目录失败:', err)
// callback()
// return
// }
// let count = 0
// const checkEnd = () => {
// if (++count === files.length) callback()
// }
// if (files.length === 0) {
// callback()
// return
// }
// files.forEach((item: string) => {
// const fullPath = resolve(folder, item) // 👈 使用 resolve 拼接路径
// stat(fullPath, (err, stats) => {
// if (err) {
// checkEnd()
// return
// }
// if (stats.isFile()) {
// fileListTotal.push(stats.size)
// checkEnd()
// } else if (stats.isDirectory()) {
// recursiveDirectory(fullPath, checkEnd)
// }
// })
// })
// })
// }
// function sum(arr: number[]) {
// return arr.reduce((t, c) => t + c, 0)
// }
// function formatBytes(a: number, b = 2): string {
// if (a === 0) return '0 Bytes'
// const c = 1024
// const e = ['Bytes', 'KB', 'MB', 'GB', 'TB']
// const f = Math.floor(Math.log(a) / Math.log(c))
// return `${parseFloat((a / c ** f).toFixed(b))} ${e[f]}`
// }
// export function viteBuildInfo(name: string): Plugin {
// let config: ResolvedConfig
// let startTime: Dayjs
// let endTime: Dayjs
// return {
// name: 'vite:buildInfo',
// configResolved(resolvedConfig) {
// config = resolvedConfig
// },
// buildStart() {
// console.log(
// bold(
// green(
// `👏欢迎使用${blue(`[${name}]`)},现在正全力为您${config.command === 'build' ? '打包' : '编译'}`,
// ),
// ),
// )
// if (config.command === 'build') {
// startTime = dayjs()
// }
// },
// closeBundle() {
// if (config.command === 'build') {
// endTime = dayjs()
// // 👇 关键:将 outDir 转为绝对路径!
// const outDir = resolve(config.root || process.cwd(), config.build.outDir)
// recursiveDirectory(outDir, () => {
// console.log(
// bold(
// green(
// `恭喜打包完成🎉(总用时${dayjs
// .duration(endTime.diff(startTime))
// .format('mm分ss秒')},打包后的大小为${formatBytes(sum(fileListTotal))}`,
// ),
// ),
// )
// })
// }
// },
// }
// }
+12774
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild
View File
+211
View File
@@ -0,0 +1,211 @@
/**
* loading 占位
* 解决首次加载时白屏的问题
*/
(function () {
const div = document.createElement('div')
const body = document.querySelector('body')
body.appendChild(div)
div.setAttribute('id', 'loading-app')
if (div && div.innerHTML === '') {
div.innerHTML = `
<style>
html,
body,
#loading-app {
height: 100%;
margin: 0;
padding: 0;
}
#loading-app {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-repeat: no-repeat;
background-size: 100% auto;
}
.loading-title {
font-size: 1.1rem;
}
.loading-sub-title {
margin-top: 20px;
font-size: 1rem;
color: #888;
}
.page-loading-warp {
display: flex;
align-items: center;
justify-content: center;
padding: 26px;
}
.ant-spin {
position: absolute;
display: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
color: #1890ff;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
text-align: center;
list-style: none;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-webkit-font-feature-settings: "tnum";
font-feature-settings: "tnum";
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
width: 20px;
height: 20px;
font-size: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: ant-spin-move 1s infinite linear alternate;
animation: ant-spin-move 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: ant-rotate 1.2s infinite linear;
animation: ant-rotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
width: 32px;
height: 32px;
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes ant-spin-move {
to {
opacity: 1;
}
}
@keyframes ant-spin-move {
to {
opacity: 1;
}
}
@-webkit-keyframes ant-rotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes ant-rotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 362px;
">
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
<i class="ant-spin-dot-item"></i>
</span>
</div>
</div>
<div class="loading-title">
正在加载资源
</div>
<div class="loading-sub-title">
初次加载资源可能需要较多时间 请耐心等待
</div>
</div>
`
}
})()
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+23
View File
@@ -0,0 +1,23 @@
import dirTree from 'directory-tree'
import treeify from 'treeify'
// eslint-disable-next-line node/prefer-global/process
const filteredTree = dirTree(process.cwd(), {
exclude: [/node_modules/, /\.git/, /\.vscode/, /\.idea/],
})
const children = filteredTree.children ?? []
function genObj(children: any[]) {
const obj: Record<string, any> = {}
for (const child of children)
obj[child.name] = (child?.children && child.children.length > 0) ? genObj(child.children) : null
return obj
}
const obj = genObj(children)
const md = treeify.asTree(obj, true, null)
console.log(md)
+35
View File
@@ -0,0 +1,35 @@
import path from 'node:path'
import { theme } from 'ant-design-vue'
import fsExtra from 'fs-extra'
import lodash from 'lodash'
const { defaultAlgorithm, defaultSeed } = theme
const mapToken = defaultAlgorithm(defaultSeed)
function formatKey(key: string, prefixCls: string) {
return `${prefixCls}${lodash.kebabCase(key)}`
}
const prefixCls = '--pro-ant-'
const variables: {
colors: Record<string, any>
} = {
colors: {},
}
let colorTheme = ''
for (const key in mapToken) {
if (key.startsWith('color')) {
const cssVar = formatKey(key, prefixCls)
const colors = variables.colors
const themeKey = lodash.camelCase(key.slice(5))
colors[themeKey] = `var(${cssVar})`
colorTheme += `${themeKey}\n`
}
}
// eslint-disable-next-line node/prefer-global/process
fsExtra.outputFile(path.resolve(process.cwd(), './themes/antd-uno-theme.json'), JSON.stringify(variables, null, 2))
// eslint-disable-next-line node/prefer-global/process
fsExtra.outputFile(path.resolve(process.cwd(), './themes/color-theme-var.md'), colorTheme)
+17
View File
@@ -0,0 +1,17 @@
import { resolve } from 'node:path'
import * as process from 'node:process'
import { transformSync } from 'esbuild'
import fsExtra from 'fs-extra'
const themeConfig = resolve(process.cwd(), './src/config/default-setting.ts')
const code = fsExtra.readFileSync(themeConfig, 'utf-8')
function toJs(code: string) {
const res = transformSync(code, {
target: 'esnext',
loader: 'ts',
})
console.log(res.code)
}
toJs(code)
+10
View File
@@ -0,0 +1,10 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
// setResponseStatus(event, 401)
event.res.status = 401
return {
code: 401,
msg: '请先登录',
}
})
+9
View File
@@ -0,0 +1,9 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
event.res.status = 403
return {
code: 403,
msg: '请先登录',
}
})
+9
View File
@@ -0,0 +1,9 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
event.res.status = 500
return {
code: 500,
msg: '服务器错误',
}
})
+5
View File
@@ -0,0 +1,5 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(() => {
return { nitro: 'Hello Antdv Pro' }
})
@@ -0,0 +1,16 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
const id = event.context.params?.id as any
if (typeof id !== 'number') {
event.res.status = 403
return {
code: 403,
msg: '删除失败',
}
}
return {
code: 200,
msg: '删除成功',
}
})
@@ -0,0 +1,80 @@
import dayjs from 'dayjs'
import { defineEventHandler } from 'h3'
import { cloneDeep } from 'lodash-es'
export default defineEventHandler(async (_event) => {
const dataList = [
{
title: 'Aipay',
link: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
percent: 57,
content: '一生那么短,遗忘又那么漫长',
},
{
title: 'Ant Design Vue',
link: 'https://www.antdv.com/assets/logo.1ef800a8.svg',
percent: 60,
status: 'active',
content: '只有在梦想中,人才能真正自由',
},
{
title: 'Vue',
link: 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png',
percent: 70,
status: 'exception',
content: '生命就像一盒巧克力,结果往往出人意料',
},
{
title: 'Vite',
link: 'https://cn.vitejs.dev/logo.svg',
percent: 100,
status: 'active',
content: '有时,你必须进入别人的世界去发现自己的世界缺少什么',
},
{
title: 'React',
link: 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png',
percent: 50,
status: 'exception',
content: '希望是件美丽的东西,也许是最好的东西',
},
{
title: 'Antdv Pro',
link: '/logo.svg',
percent: 80,
status: 'active',
content: '人并非生来就伟大,而是越活越伟大',
},
{
title: 'Webpack',
link: 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png',
percent: 58,
content: '不管何时何地,做你想做的事永远都不嫌晚',
},
{
title: 'Angular',
link: 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png',
percent: 70,
status: 'active',
content: '你要一直不停地往前走,不然你不会知道生活还会给你什么',
},
]
const data: any[] = []
// 数据复制
for (let i = 0; i < 1000; i++) {
const arr = cloneDeep(dataList)
data.push(...arr)
}
// 配置任务时间
for (let i = 0; i < data.length; i++)
data[i].start = dayjs().subtract(i, 'hour').format('YYYY-MM-DD HH:mm')
return {
code: 200,
msg: '获取成功',
data,
}
})
@@ -0,0 +1,82 @@
import dayjs from 'dayjs'
import { defineEventHandler, readBody } from 'h3'
enum STATUS {
OFF = '0',
RUNNING = '1',
ONLINE = '2',
ERROR = '3',
}
export default defineEventHandler(async (_event) => {
const body = (await readBody(_event)) as { name?: string }
const dataList = [
{
id: 1,
name: '第一个任务',
callNo: 2000,
desc: '一生那么短,遗忘又那么漫长',
status: STATUS.ONLINE,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
{
id: 2,
name: 'Ant Design Vue',
callNo: 200,
desc: '有时,你必须进入别人的世界去发现自己的世界缺少什么',
status: STATUS.OFF,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
{
id: 3,
name: 'Vue',
callNo: 2010,
desc: '一生那么短,遗忘又那么漫长',
status: STATUS.ERROR,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
{
id: 4,
name: 'Vite',
callNo: 20300,
desc: '希望是件美丽的东西,也许是最好的东西',
status: STATUS.ERROR,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
{
id: 5,
name: 'React',
callNo: 2000,
desc: '人并非生来就伟大,而是越活越伟大',
status: STATUS.ONLINE,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
{
id: 6,
name: 'Antdv Pro',
callNo: 2000,
desc: '不管何时何地,做你想做的事永远都不嫌晚',
status: STATUS.OFF,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
{
id: 7,
name: 'Webpack',
callNo: 2000,
desc: '你要一直不停地往前走,不然你不会知道生活还会给你什么',
status: STATUS.ONLINE,
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
},
]
const data = dataList.filter((i) => {
if (body.name)
return body.name === i.name
else return true
})
return {
code: 200,
msg: '获取成功',
data,
}
})
@@ -0,0 +1,10 @@
import { defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
console.log(body)
return {
code: 200,
msg: '创建成功',
}
})
@@ -0,0 +1,63 @@
import { defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (_event) => {
const body = await readBody(_event)
const dataList = [
{
id: 1,
name: '第一个任务',
value: '2000',
remark: '一生那么短,遗忘又那么漫长',
},
{
id: 2,
name: 'Ant Design Vue',
value: '200',
remark: '有时,你必须进入别人的世界去发现自己的世界缺少什么',
},
{
id: 3,
name: 'Vue',
value: '2010',
remark: '一生那么短,遗忘又那么漫长',
},
{
id: 4,
name: 'Vite',
value: '20300',
remark: '希望是件美丽的东西,也许是最好的东西',
},
{
id: 5,
name: 'React',
value: '2000',
remark: '人并非生来就伟大,而是越活越伟大',
},
{
id: 6,
name: 'Antdv Pro',
value: '2000',
remark: '不管何时何地,做你想做的事永远都不嫌晚',
},
{
id: 7,
name: 'Webpack',
value: '2000',
remark: '你要一直不停地往前走,不然你不会知道生活还会给你什么',
},
]
const data = dataList.filter((i) => {
if (body.name)
return body.name === i.name
else return true
})
return {
code: 200,
msg: '获取成功',
data: {
records: data,
total: data.length,
},
}
})
@@ -0,0 +1,22 @@
import { defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const dataList = []
for (let i = 0; i < 10; i++) {
const item = {
id: i + 1,
title: `${body.title ?? '测试'}_${i}`,
username: `${body.username ?? 'test'}_${i}`,
password: `${body.username ?? 'test'}_pass_${i}`,
}
dataList.push(item)
}
return {
code: 200,
msg: '获取成功',
data: dataList,
}
})
@@ -0,0 +1,8 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(async (_event) => {
return {
code: 200,
msg: '编辑成功',
}
})
+33
View File
@@ -0,0 +1,33 @@
import { defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (event) => {
const body: any = await readBody(event)
const { type } = body
const success = {
code: 200,
data: {
token: '1234567890',
},
msg: '登录成功',
}
if (type !== 'mobile') {
// eslint-disable-next-line node/prefer-global/buffer
success.data.token = Buffer.from(body.username).toString('base64')
// 判断用户名密码是否正确
if (body.username === 'admin' && body.password === 'admin')
return success
if (body.username === 'user' && body.password === 'user')
return success
}
else {
return success
}
// setResponseStatus(event, 403)
event.res.status = 403
return {
code: 401,
msg: '用户名或密码错误',
}
})
+8
View File
@@ -0,0 +1,8 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(() => {
return {
code: 200,
msg: 'success',
}
})
+474
View File
@@ -0,0 +1,474 @@
import { defineEventHandler } from 'h3'
const menuData = [
{
id: 2,
parentId: null,
title: '分析页',
icon: 'DashboardOutlined',
component: '/dashboard/analysis',
path: '/dashboard/analysis',
name: 'DashboardAnalysis',
keepAlive: true,
locale: 'menu.dashboard.analysis',
},
{
id: 1,
parentId: null,
title: '仪表盘',
icon: 'DashboardOutlined',
component: 'RouteView',
redirect: '/dashboard/analysis',
path: '/dashboard',
name: 'Dashboard',
locale: 'menu.dashboard',
},
{
id: 3,
parentId: null,
title: '表单页',
icon: 'FormOutlined',
component: 'RouteView',
redirect: '/form/basic',
path: '/form',
name: 'Form',
locale: 'menu.form',
},
{
id: 5,
parentId: null,
title: '链接',
icon: 'LinkOutlined',
component: 'RouteView',
redirect: '/link/iframe',
path: '/link',
name: 'Link',
locale: 'menu.link',
},
{
id: 6,
parentId: 5,
title: 'AntDesign',
url: 'https://ant.design/',
component: 'Iframe',
path: '/link/iframe',
name: 'LinkIframe',
keepAlive: true,
locale: 'menu.link.iframe',
},
{
id: 7,
parentId: 5,
title: 'AntDesignVue',
url: 'https://antdv.com/',
component: 'Iframe',
path: '/link/antdv',
name: 'LinkAntdv',
keepAlive: true,
locale: 'menu.link.antdv',
},
{
id: 8,
parentId: 5,
path: 'https://www.baidu.com',
name: 'LinkExternal',
title: '跳转百度',
locale: 'menu.link.external',
},
{
id: 9,
parentId: null,
title: '菜单',
icon: 'BarsOutlined',
component: 'RouteView',
path: '/menu',
redirect: '/menu/menu1',
name: 'Menu',
locale: 'menu.menu',
},
{
id: 10,
parentId: 9,
title: '菜单1',
component: '/menu/menu1',
path: '/menu/menu1',
name: 'MenuMenu11',
keepAlive: true,
locale: 'menu.menu.menu1',
},
{
id: 11,
parentId: 9,
title: '菜单2',
component: '/menu/menu2',
path: '/menu/menu2',
keepAlive: true,
locale: 'menu.menu.menu2',
},
{
id: 12,
parentId: 9,
path: '/menu/menu3',
redirect: '/menu/menu3/menu1',
title: '菜单1-1',
component: 'RouteView',
locale: 'menu.menu.menu3',
},
{
id: 13,
parentId: 12,
path: '/menu/menu3/menu1',
component: '/menu/menu-1-1/menu1',
title: '菜单1-1-1',
keepAlive: true,
locale: 'menu.menu3.menu1',
},
{
id: 14,
parentId: 12,
path: '/menu/menu3/menu2',
component: '/menu/menu-1-1/menu2',
title: '菜单1-1-2',
keepAlive: true,
locale: 'menu.menu3.menu2',
},
{
id: 15,
path: '/access',
component: 'RouteView',
redirect: '/access/common',
title: '权限模块',
name: 'Access',
parentId: null,
icon: 'ClusterOutlined',
locale: 'menu.access',
},
{
id: 16,
parentId: 15,
path: '/access/common',
title: '通用权限',
name: 'AccessCommon',
component: '/access/common',
locale: 'menu.access.common',
},
{
id: 17,
parentId: 15,
path: '/access/user',
title: '普通用户',
name: 'AccessUser',
component: '/access/user',
locale: 'menu.access.user',
},
{
id: 19,
parentId: null,
title: '异常页',
icon: 'WarningOutlined',
component: 'RouteView',
redirect: '/exception/403',
path: '/exception',
name: 'Exception',
locale: 'menu.exception',
},
{
id: 20,
parentId: 19,
path: '/exception/403',
title: '403',
name: '403',
component: '/exception/403',
locale: 'menu.exception.not-permission',
},
{
id: 21,
parentId: 19,
path: '/exception/404',
title: '404',
name: '404',
component: '/exception/404',
locale: 'menu.exception.not-find',
},
{
id: 22,
parentId: 19,
path: '/exception/500',
title: '500',
name: '500',
component: '/exception/500',
locale: 'menu.exception.server-error',
},
{
id: 23,
parentId: null,
title: '结果页',
icon: 'CheckCircleOutlined',
component: 'RouteView',
redirect: '/result/success',
path: '/result',
name: 'Result',
locale: 'menu.result',
},
{
id: 24,
parentId: 23,
path: '/result/success',
title: '成功页',
name: 'ResultSuccess',
component: '/result/success',
locale: 'menu.result.success',
},
{
id: 25,
parentId: 23,
path: '/result/fail',
title: '失败页',
name: 'ResultFail',
component: '/result/fail',
locale: 'menu.result.fail',
},
{
id: 26,
parentId: null,
title: '列表页',
icon: 'TableOutlined',
component: 'RouteView',
redirect: '/list/card-list',
path: '/list',
name: 'List',
locale: 'menu.list',
},
{
id: 27,
parentId: 26,
path: '/list/card-list',
title: '卡片列表',
name: 'ListCard',
component: '/list/card-list',
locale: 'menu.list.card-list',
},
{
id: 28,
parentId: null,
title: '详情页',
icon: 'ProfileOutlined',
component: 'RouteView',
redirect: '/profile/basic',
path: '/profile',
name: 'Profile',
locale: 'menu.profile',
},
{
id: 29,
parentId: 28,
path: '/profile/basic',
title: '基础详情页',
name: 'ProfileBasic',
component: '/profile/basic/index',
locale: 'menu.profile.basic',
},
{
id: 30,
parentId: 26,
path: '/list/search-list',
title: '搜索列表',
name: 'SearchList',
component: '/list/search-list',
locale: 'menu.list.search-list',
},
{
id: 31,
parentId: 30,
path: '/list/search-list/articles',
title: '搜索列表(文章)',
name: 'SearchListArticles',
component: '/list/search-list/articles',
locale: 'menu.list.search-list.articles',
},
{
id: 32,
parentId: 30,
path: '/list/search-list/projects',
title: '搜索列表(项目)',
name: 'SearchListProjects',
component: '/list/search-list/projects',
locale: 'menu.list.search-list.projects',
},
{
id: 33,
parentId: 30,
path: '/list/search-list/applications',
title: '搜索列表(应用)',
name: 'SearchListApplications',
component: '/list/search-list/applications',
locale: 'menu.list.search-list.applications',
},
{
id: 34,
parentId: 26,
path: '/list/basic-list',
title: '标准列表',
name: 'BasicCard',
component: '/list/basic-list',
locale: 'menu.list.basic-list',
},
{
id: 35,
parentId: 28,
path: '/profile/advanced',
title: '高级详细页',
name: 'ProfileAdvanced',
component: '/profile/advanced/index',
locale: 'menu.profile.advanced',
},
{
id: 4,
parentId: 3,
title: '基础表单',
component: '/form/basic-form/index',
path: '/form/basic-form',
name: 'FormBasic',
keepAlive: false,
locale: 'menu.form.basic-form',
},
{
id: 36,
parentId: null,
title: '个人页',
icon: 'UserOutlined',
component: 'RouteView',
redirect: '/account/center',
path: '/account',
name: 'Account',
locale: 'menu.account',
},
{
id: 37,
parentId: 36,
path: '/account/center',
title: '个人中心',
name: 'AccountCenter',
component: '/account/center',
locale: 'menu.account.center',
},
{
id: 38,
parentId: 36,
path: '/account/settings',
title: '个人设置',
name: 'AccountSettings',
component: '/account/settings',
locale: 'menu.account.settings',
},
{
id: 39,
parentId: 3,
title: '分步表单',
component: '/form/step-form/index',
path: '/form/step-form',
name: 'FormStep',
keepAlive: false,
locale: 'menu.form.step-form',
},
{
id: 40,
parentId: 3,
title: '高级表单',
component: '/form/advanced-form/index',
path: '/form/advanced-form',
name: 'FormAdvanced',
keepAlive: false,
locale: 'menu.form.advanced-form',
},
{
id: 41,
parentId: 26,
path: '/list/table-list',
title: '查询表格',
name: 'ConsultTable',
component: '/list/table-list',
locale: 'menu.list.consult-table',
},
{
id: 42,
parentId: 1,
title: '监控页',
component: '/dashboard/monitor',
path: '/dashboard/monitor',
name: 'DashboardMonitor',
keepAlive: true,
locale: 'menu.dashboard.monitor',
},
{
id: 43,
parentId: 1,
title: '工作台',
component: '/dashboard/workplace',
path: '/dashboard/workplace',
name: 'DashboardWorkplace',
keepAlive: true,
locale: 'menu.dashboard.workplace',
},
{
id: 44,
parentId: 26,
path: '/list/crud-table',
title: '增删改查表格',
name: 'CrudTable',
component: '/list/crud-table',
locale: 'menu.list.crud-table',
},
{
id: 45,
parentId: 9,
path: '/menu/menu4',
redirect: '/menu/menu4/menu1',
title: '菜单2-1',
component: 'RouteView',
locale: 'menu.menu.menu4',
},
{
id: 46,
parentId: 45,
path: '/menu/menu4/menu1',
component: '/menu/menu-2-1/menu1',
title: '菜单2-1-1',
keepAlive: true,
locale: 'menu.menu4.menu1',
},
{
id: 47,
parentId: 45,
path: '/menu/menu4/menu2',
component: '/menu/menu-2-1/menu2',
title: '菜单2-1-2',
keepAlive: true,
locale: 'menu.menu4.menu2',
},
]
export const accessMenuData = [
{
id: 18,
parentId: 15,
path: '/access/admin',
title: '管理员',
name: 'AccessAdmin',
component: '/access/admin',
locale: 'menu.access.admin',
},
]
export default defineEventHandler((event) => {
const token = event.req.headers.get('Authorization')
// eslint-disable-next-line node/prefer-global/buffer
const username = Buffer.from(token as any, 'base64').toString('utf-8')
return {
code: 200,
msg: '获取成功',
data: [...menuData, ...(username === 'admin' ? accessMenuData : [])],
}
})
+8
View File
@@ -0,0 +1,8 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(() => {
return {
code: 200,
msg: 'delete',
}
})
+8
View File
@@ -0,0 +1,8 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(() => {
return {
code: 200,
msg: 'post',
}
})
+8
View File
@@ -0,0 +1,8 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler(() => {
return {
code: 200,
msg: 'put',
}
})
+24
View File
@@ -0,0 +1,24 @@
import { defineEventHandler } from 'h3'
export default defineEventHandler((event) => {
const token = event.req.headers.get('Authorization')
// eslint-disable-next-line node/prefer-global/buffer
const username = Buffer.from(token, 'base64').toString('utf-8')
if (!token) {
return {
code: 401,
msg: '登录失效',
}
}
return {
code: 200,
msg: '获取成功',
data: {
id: 1,
username,
nickname: username === 'admin' ? '超级管理员' : '普通用户',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
roles: username === 'admin' ? ['ADMIN'] : ['USER'],
},
}
})
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { useLayoutMenuProvide } from '~/components/page-container/context'
const appStore = useAppStore()
const { theme } = storeToRefs(appStore)
const { antd } = useI18nLocale()
const layoutMenu = useLayoutMenu()
useLayoutMenuProvide(layoutMenu, appStore)
</script>
<template>
<a-config-provider :theme="theme" :locale="antd">
<a-app class="h-full font-chinese antialiased">
<TokenProvider>
<RouterView />
</TokenProvider>
</a-app>
</a-config-provider>
</template>
+76
View File
@@ -0,0 +1,76 @@
export interface IncidentListParams {
dict_id?: string;
item_value?: string;
pageSize?: number;
pageNum?: number;
}
export interface accidentParams {
ticket_id?: string;
vin?: string;
carline?: string;
model?: string;
labels?: string;
accident_level?: string;
bmbs_name?: string;
rdca_reporter?: string;
software_version?: string;
affects_versions?: string;
components?: string;
fix_versions?: string;
case_description?: string;
fo_solution?: string;
assignee_name?: string;
occur_datetime?: string;
subtasks?: string;
linked_work_items?: string;
}
export async function getDictDataApi(params: IncidentListParams) {
return usePost('/accident/item-list', params)
}
export async function getAllDictCodeApi() {
return useGet('/accident/all_dict_code')
}
export async function insertDictTypetApi(params: IncidentListParams) {
return usePost('/accident/insert-dict', params)
}
export async function insertAccidentApi(params: accidentParams) {
return usePost('/accident/insert-accident', params)
}
export async function getAccidentRecordApi(params: any) {
return usePost('/accident/get-accident-record', params)
}
export async function getAccidentByIdApi(params: number) {
return usePost('/accident/get-accident-by-id', params)
}
export async function updataItemByIdApi(params: any) {
return usePost('/accident/update-item', params)
}
export async function bingImageApi(params: any) {
return usePost('/file_uploads/bind-image', params)
}
export function getImagesApi(case_id: number) {
return useGet(`/file_uploads/get-images/${case_id}`)
}
export async function getSecretKey() {
return useGet('/user/get_record')
}
+37
View File
@@ -0,0 +1,37 @@
export interface LoginParams {
username: string
password: string
type?: 'account'
}
export interface LoginMobileParams {
mobile: string
code: string
type: 'mobile'
}
export interface LoginResultModel {
token: string
}
export function loginApi(params: LoginParams | LoginMobileParams) {
return usePost<LoginResultModel, LoginParams | LoginMobileParams>('/user/login', params, {
// 设置为false的时候不会携带token
token: false,
// 开发模式下使用自定义的接口
customDev: true,
// 是否开启全局请求loading
loading: true,
})
}
// export function loginApi(params: any) {
// return usePost('/user/login', params, {
// token: false,
// loading: true,
// })
// }
export function logoutApi() {
return useGet('/logout')
}
+5
View File
@@ -0,0 +1,5 @@
import type { MenuData } from '~@/layouts/basic-layout/typing'
export function getRouteMenusApi() {
return useGet<MenuData>('/menu')
}
+11
View File
@@ -0,0 +1,11 @@
export interface UserInfo {
id: number | string
username: string
nickname: string
avatar: string
roles?: (string | number)[]
}
export function getUserInfoApi() {
return useGet<UserInfo>('/user/info')
}
+26
View File
@@ -0,0 +1,26 @@
export interface ListResultModel {
id: number
title: string
username: string
password: string
}
export type ListResultParams = Partial<Omit<ListResultModel, 'id' | 'password'>>
export async function getListApi(params?: ListResultParams) {
return usePost<ListResultModel[]>('/list', params)
}
export type CreateListParams = Partial<Omit<ListResultModel, 'id'>>
export async function createListApi(params: CreateListParams) {
return usePost('/list/create', params)
}
export async function editListApi(params: ListResultModel) {
return usePut('/list', params)
}
export async function delListApi(id: string | number) {
return useDelete(`/list/${id}`)
}
+12
View File
@@ -0,0 +1,12 @@
export interface ListResultModel {
id: number
title: string
username: string
password: string
}
export type ListResultParams = Partial<Omit<ListResultModel, 'id' | 'password'>>
export async function getListApi(params?: ListResultParams) {
return usePost<ListResultModel[]>('/list/basic-list', params)
}
+30
View File
@@ -0,0 +1,30 @@
interface CrudTableModel {
id?: number
/**
* 名称
*/
name: string
/**
* 值
*/
value: string
/**
* 描述
*/
remark?: string
}
type CrudTableParams = Partial<Omit<CrudTableModel, 'id'>>
export async function getListApi(params?: CrudTableParams) {
return usePost<CrudTableModel[]>('/list/crud-table', params)
}
export async function deleteApi(id: string | number) {
return useDelete(`/list/${id}`)
}
export type {
CrudTableModel,
CrudTableParams,
}
+46
View File
@@ -0,0 +1,46 @@
import type { STATUS } from '~@/utils/constant'
interface ConsultTableModel {
id: number
/**
* 服务名称
*/
name: string
/**
* 服务调用次数
*/
callNo: 805
/**
* 描述
*/
desc: string
/**
* 状态
*/
status: STATUS
/**
* 上次调用时间
*/
updatedAt: string
// 分页
current?: number
// size
pageSize?: number
}
type ConsultTableParams = Partial<Omit<ConsultTableModel, 'id'>>
export async function getListApi(params?: ConsultTableParams) {
return usePost<ConsultTableModel[]>('/list/consult-list', params)
}
export async function deleteApi(id: string | number) {
return useDelete(`/list/${id}`)
}
export type {
ConsultTableModel,
ConsultTableParams,
STATUS,
}
+3
View File
@@ -0,0 +1,3 @@
export function oauthLoginApi() {
return useGet('/login')
}
+23
View File
@@ -0,0 +1,23 @@
export interface IncidentListParams {
startTime?: string;
endTime?: string;
pageSize?: number;
pageNum?: number;
oneid?: string;
incident_time?: object;
}
export async function getIncidentListApi(params: IncidentListParams) {
return usePost('/incident/list', params, { timeout: 600000 })
}
export async function getGen6IncidentListApi(params: IncidentListParams,code:string) {
return usePost('/gen6/list', params, { timeout: 600000 ,headers:{'X-Encrypted-Timestamp':code}}) //,headers:{'X-Encrypted-Timestamp':code}
}
export async function getGen5IncidentListApi(params: IncidentListParams,code:string) {
return usePost('/gen5/list', params, { timeout: 600000, headers:{'X-Encrypted-Timestamp':code} })
}
+23
View File
@@ -0,0 +1,23 @@
export function test200() {
return useGet('/')
}
export function test401() {
return useGet('/401')
}
export function test500() {
return useGet('/500')
}
export function testPut() {
return usePut('/test')
}
export function testPost() {
return usePost('/test')
}
export function testDelete() {
return useDelete('/test')
}

Some files were not shown because too many files have changed in this diff Show More