This commit is contained in:
ZhuJW
2026-04-22 13:35:40 +08:00
commit 26a7fdf6c0
40 changed files with 11602 additions and 0 deletions

View File

View File

@@ -0,0 +1,5 @@
# from flask import Blueprint
# auth_bp = Blueprint('auth', __name__)
# from . import routes

View File

@@ -0,0 +1,522 @@
from datetime import datetime
import json
from flask import Blueprint, current_app, request, jsonify
from flask_jwt_extended import create_access_token, get_jwt, get_jwt_identity, jwt_required
from sqlalchemy import and_
from app import jwt,db
from app.models import OperationHistory, Role, User, UserRole
from app.utils.password_hasher import hash_password,check_password
from app.utils.response_dict import response
auth_bp= Blueprint('data', __name__)
@auth_bp.route('/login', methods=['POST'])
def login():
# 1. 获取登录凭证
data = request.get_json()
username = data.get('username')
password = data.get('password')
print('username',username)
print('password',password)
# 2. 验证必填字段
# if not username or not password:
# response['code']=400
# response['message']="用户名和密码不能为空"
# return jsonify(response)
#
# # 3. 查询用户
# user = User.query.filter_by(username=username).first()
#
# # 4. 验证用户存在性和密码
# if not user or not check_password(password,user.password):
# response['code']=401
# response['message']="用户名或密码错误"
# return jsonify({"msg": "用户名或密码错误"}), 401
#
# if user.status != 1:
# return jsonify({"msg": "用户已禁用"}), 403
# 5. 生成JWT令牌[2,6](@ref)
access_token = create_access_token(identity=str(1))
result = db.session.query(
User.id,
User.username.label('realName'), # 使用 label 实现别名
User.password,
User.status,
Role.name.label('roles'),
Role.description
).join(
UserRole, User.id == UserRole.user_id # 第一次 JOIN用户 ↔ 用户角色
).join(
Role, UserRole.role_id == Role.id # 第二次 JOIN用户角色 ↔ 角色
).filter(
User.status == 1, # 状态过滤
User.id == 1 # 用户ID过滤
).all()
if not result:
return jsonify({"msg": "用户未分配角色或已禁用"}), 403
# dict_list = [row._asdict() for row in result]
# dict_list[0]['accessToken']=access_token
# dict_list[0]['roles']=[dict_list[0]['roles']]
# data={
# "accessToken": access_token,
# "user_id": user.id,
# "message": "登录成功"
# }
# response['data']= dict_list[0]
# response['error']=''
# 6. 返回响应
res={"accessToken":access_token}
data={}
data['data']=res
return jsonify(res)
@auth_bp.route('/userinfo', methods=['GET'])
@jwt_required() # 要求有效Token[1,5](@ref)
def get_user_info():
userid = get_jwt_identity()
"""
获取用户信息
"""
result = db.session.query(
User.id,
User.username.label('realName'), # 使用 label 实现别名
User.password,
User.status,
Role.name.label('roles'),
Role.description
).join(
UserRole, User.id == UserRole.user_id # 第一次 JOIN用户 ↔ 用户角色
).join(
Role, UserRole.role_id == Role.id # 第二次 JOIN用户角色 ↔ 角色
).filter(
User.status == 1, # 状态过滤
User.id == userid # 用户ID过滤
).all()
dict_list = [row._asdict() for row in result]
if not dict_list:
return jsonify({"msg": "用户未分配角色或已禁用"}), 403
role_list = [row.get('roles') for row in dict_list if row.get('roles')]
if not role_list:
return jsonify({"msg": "用户未分配角色或已禁用"}), 403
dict_list[0]['roles'] = role_list
data={"roles": dict_list[0]['roles'],"realName": dict_list[0]['realName'],"homePath": '/datamanage/datalabel'}
return jsonify(data)
@auth_bp.route('/codes', methods=['GET'])
@jwt_required() # 要求有效Token[1,5](@ref)
def get_code():
response['code']=200
response['data']=["AC_100100","AC_100110"]
return jsonify(response)
@auth_bp.route('/refresh', methods=['POST'])
@jwt_required() # 要求有效Token[1,5](@ref)
def refresh():
# data: string;
# status: number;
token = create_access_token(identity='service_account')
data={}
data['data']=token
data['status']=1
return jsonify(data)
# ===== 受保护路由示例 =====
@auth_bp.route('/protected', methods=['GET'])
@jwt_required() # 要求有效Token[1,5](@ref)
def protected():
# 从Token获取用户身份
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
return jsonify({
"message": f"欢迎回来, {user.username}!",
"user_id": user.id
}), 200
@jwt.unauthorized_loader
def missing_token_callback(error):
"""缺少Token时的自定义响应"""
return jsonify({
'status': 401,
'msg': '缺少访问令牌'
}), 401
blacklist = set()
@auth_bp.route('/createuser', 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()
print('current_user_id',current_user_id)
current_user = User.query.get(int(current_user_id))
if not current_user:
return jsonify({"error": "无效的操作者"}), 401
# 3. 密码复杂度验证
password = data['password']
if len(password) < 8 or not any(c.isdigit() for c in password):
return jsonify({"error": "密码需至少8位且包含数字"}), 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=hash_password(data['password']),
status=data.get('status', 1),
created_by=current_user_id
)
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
@auth_bp.route('/updateuser', methods=['POST'])
@jwt_required() # 需要有效Token才能访问
def update_user():
'''
请求体格式:
{
"user_id": 123, // 必需目标用户ID
"username": "updated_user", // 可选:新用户名
"password": "newSecurePass456", // 可选:新密码
"status": 1 // 可选用户状态1-启用0-禁用)
}
'''
data = request.get_json()
if not data or 'user_id' not in data:
return jsonify({"error": "请求数据不能为空且必须包含user_id"}), 400
# 1. 验证操作者身份
current_user_id = get_jwt_identity()
current_user = User.query.get(int(current_user_id))
if not current_user:
return jsonify({"error": "无效的操作者"}), 401
# 2. 验证目标用户是否存在
target_user_id = data['user_id']
target_user = User.query.get(int(target_user_id))
if not target_user:
return jsonify({"error": "目标用户不存在"}), 404
# 3. 权限校验:仅允许管理员修改(核心修改点)
# 检查当前用户是否为管理员假设管理员角色ID为1
is_admin = UserRole.query.filter_by( # 注意这里修正为UserRole表用户-角色关联表)
user_id=current_user_id,
role_id=1
).first()
if not is_admin: # 只有管理员能通过校验
return jsonify({"error": "仅管理员有权限修改用户信息"}), 403
role_ids = data.get('role_ids', None)
valid_roles = []
if role_ids is not None:
if not isinstance(role_ids, list):
return jsonify({"error": "role_ids必须为数组"}), 400
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:
# 4. 处理用户名修改(如提供)
if 'username' in data and data['username']:
# 验证用户名唯一性(排除自身)
existing_user = User.query.filter(
User.username == data['username'],
User.id != target_user_id
).first()
if existing_user:
return jsonify({"error": "用户名已存在"}), 409
target_user.username = data['username']
# 5. 处理密码修改(如提供)
if 'password' in data and data['password']:
password = data['password']
# 密码复杂度验证(与创建用户保持一致)
if len(password) < 8 or not any(c.isdigit() for c in password):
return jsonify({"error": "密码需至少8位且包含数字"}), 400
target_user.password = hash_password(password) # 复用哈希函数
# 6. 处理状态修改(如提供)
if 'status' in data:
target_user.status = data['status'] # 管理员无需额外校验
# 6.1 处理角色修改(如提供)
if role_ids is not None:
UserRole.query.filter_by(user_id=target_user_id).delete()
for role in valid_roles:
user_role = UserRole(
user_id=target_user_id,
role_id=role.id,
created_by=current_user_id,
)
db.session.add(user_role)
# 7. 提交修改
target_user.updated_at = datetime.now() # 假设模型有updated_at字段
target_user.updated_by = current_user_id # 假设模型有updated_by字段
db.session.commit()
# 8. 构造响应数据
response_data = {
"id": target_user.id,
"username": target_user.username,
"status": target_user.status,
"updated_at": target_user.updated_at.isoformat(),
"updated_by": current_user.username
}
if role_ids is not None:
response_data["roles"] = [{"id": r.id, "name": r.name} for r in valid_roles]
# 9. 记录操作日志
current_app.logger.info(
f"用户信息更新成功: {target_user.username} (ID:{target_user.id}) "
f"操作者: {current_user.username} (ID:{current_user_id}) "
f"修改内容: {[k for k in data.keys() if k != 'user_id']}"
)
return jsonify(response_data), 200
except Exception as e:
db.session.rollback()
current_app.logger.error(f"用户信息更新失败: {str(e)}")
return jsonify({"error": "服务器内部错误"}), 500
@auth_bp.route('/logout', methods=['POST'])
@jwt_required()
def log_out():
jti = get_jwt()["jti"] # 获取 Token 的唯一标识
blacklist.add(jti) # 将 Token 添加到黑名单
return jsonify({"msg": "Successfully logged out"}), 200
@auth_bp.route('/getuserlist', methods=['GET'])
@jwt_required()
def get_users():
result = db.session.query(
User.id,
User.username,
User.status,
User.created_at,
User.created_by,
User.updated_at,
UserRole.role_id,
Role.description,
Role.name
).join(
UserRole, User.id == UserRole.user_id
).join(
Role, UserRole.role_id == Role.id
).all()
dict_list = [row._asdict() for row in result]
return jsonify(dict_list), 200
@auth_bp.route('/getroles', methods=['GET'])
@jwt_required()
def get_roles():
roles = Role.query.filter(Role.status == 1).all()
data = [{"id": r.id, "name": r.name, "description": r.description} for r in roles]
return jsonify(data), 200
@auth_bp.route('/deleteuser', methods=['GET'])
@jwt_required()
def delete_user():
user_id=request.args.get('userid')
# 1. 查询用户(确保状态之前为 1
user = User.query.filter(
User.id == user_id,
User.status == 1 # 只选择状态为 1 的用户
).first()
if not user:
return jsonify({"error": "用户不存在或状态不是 1"}), 400 # 用户不存在或状态不是 1
# 2. 修改状态
user.status = 0
# 3. 提交事务
try:
db.session.commit()
return jsonify({"msg": "Successfully"}),200
except Exception as e:
db.session.rollback()
print(f"Error: {e}")
return jsonify({"error": f"出现错误{e}"}), 400
@auth_bp.route('/test', methods=['GET'])
@jwt_required()
def test():
pass
@auth_bp.route('/gethisttory', methods=['POST'])
@jwt_required()
def query_operation_history():
"""
{
"username": "admin", // 可选,精确匹配用户名
"api_path": "/api/user/create", // 可选,精确匹配接口路径
"start_time": "2025-07-01T00:00:00", // 可选开始时间ISO 8601格式
"end_time": "2025-07-10T23:59:59", // 可选结束时间ISO 8601格式
"request_keyword": "email", // 可选,在请求参数中模糊搜索关键词
"page": 2, // 可选页码默认1
"per_page": 15 // 可选每页条数默认20
}
"""
"""查询操作历史记录(支持多条件过滤)"""
# 接收JSON格式的查询参数
query_params = request.get_json()
# 构建基础查询
query = OperationHistory.query
# 1. 用户名过滤(精确匹配)
if query_params['username']:
query = query.filter(OperationHistory.username == query_params['username'])
# 2. 接口路径过滤(精确匹配)
if query_params['api_path']:
query = query.filter(OperationHistory.api_path == query_params['api_path'])
# 3. 操作时间范围过滤
if query_params['start_time'] and query_params['end_time']:
# 解析时间参数支持ISO 8601格式
start_time = datetime.fromisoformat(query_params['start_time']) if 'start_time' in query_params else None
end_time = datetime.fromisoformat(query_params['end_time']) if 'end_time' in query_params else None
# 构建时间范围查询条件
time_conditions = []
if start_time:
time_conditions.append(OperationHistory.operation_time >= start_time)
if end_time:
time_conditions.append(OperationHistory.operation_time <= end_time)
if time_conditions:
query = query.filter(and_(*time_conditions))
# 4. 请求参数关键词过滤(模糊匹配)
if query_params['request_keyword']:
keyword = f"%{query_params['request_keyword']}%"
query = query.filter(
OperationHistory.request_params.like(keyword)
)
# 5. 分页参数处理
page = query_params.get('page', 1)
per_page = query_params.get('per_page', 20)
# 执行分页查询(使用索引优化)
pagination = query.order_by(OperationHistory.operation_time.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
# 序列化结果
results = []
for record in pagination.items:
results.append({
'id': record.id,
'username': record.username,
'api_path': record.api_path,
'http_method': record.http_method,
'operation_time': record.operation_time.isoformat(),
'response_code': record.response_code,
'ip_address': record.ip_address,
'operation_result': record.operation_result,
'error_message': record.error_message,
'trace_id': record.trace_id,
# 请求参数反序列化JSON字符串→Python对象
'request_params': json.loads(record.request_params) if record.request_params else None
})
# 返回分页结果
return jsonify({
'total': pagination.total,
'pages': pagination.pages,
'current_page': pagination.page,
'per_page': pagination.per_page,
'results': results
})

View File

View File

@@ -0,0 +1,5 @@
# from routes import Blueprint
# posts_bp = Blueprint('posts', __name__)
# from . import routes

View File

@@ -0,0 +1,55 @@
from collections import defaultdict
from datetime import datetime
from flask import Blueprint, current_app, request, jsonify
from flask_jwt_extended import create_access_token, get_jwt, get_jwt_identity, jwt_required
from sqlalchemy import and_, asc, desc, exists, func, or_
from app.models import BagFile, Fst, Role, User, UserRole
from app.utils.fst_tree import build_tree,build_sub_tree
from app import db
from app.utils.log_record import log_operation
from sqlalchemy.orm import aliased
# 1.创建蓝图实例,指定名称和导入名称
data_bp = Blueprint('bag', __name__)
@data_bp.route('/fstlist',methods=['GET'])
@jwt_required()
def get_fstlist():
res=Fst.query.with_entities(Fst.id,Fst.name,Fst.parent_id).all()
response=build_tree(res)
return jsonify(response), 200
@data_bp.route('/fstlevel1', methods=['GET'])
def get_level1_fst():
# 查询 level=1 的记录,仅返回 id, name, level 字段
results = Fst.query.with_entities(
Fst.id,
Fst.name,
Fst.level
).filter_by(level=1).all()
# 转换为字典列表(方便 JSON 序列化)
data = [{
"id": r.id,
"name": r.name,
"level": r.level
} for r in results]
return jsonify(data),200
# 跨表查询所有的bag
@data_bp.route('/getnode', methods=['GET'])
@jwt_required()
def query_sub_node():
'''
根据一级标签查询对应的所有子标签
'''
tagid=request.args.get('tagid')
res=Fst.query.with_entities(Fst.id,Fst.name,Fst.parent_id).all()
response=build_sub_tree(res,tagid)
return jsonify(response), 200

View File

@@ -0,0 +1,73 @@
# from datetime import datetime,timedelta
# from app import db
# from app.models import BagRecord
# from sqlalchemy.exc import SQLAlchemyError
# from sqlalchemy import func
# def add():
# new_record = BagRecord(
# bag_name="sensor_data_2025.bag",
# collection_time=datetime(2025, 5, 1, 12, 1, 1),
# bag_state=0,
# frames=120,
# car_state=1,
# address="beijing",
# collect_month=5
# )
# db.session.add(new_record)
# db.session.commit() # 提交事务 [1,7](@ref)
# def create_record(data):
# """创建新记录"""
# try:
# # 转换日期类型字段
# if 'collection_time' in data:
# data['collection_time'] = datetime.fromisoformat(data['collection_time'])
# if 'parse_time' in data:
# data['parse_time'] = datetime.fromisoformat(data['parse_time'])
# record = BagRecord(**data)
# db.session.add(record)
# db.session.commit()
# return record
# except SQLAlchemyError as e:
# db.session.rollback()
# raise e
# def get_record_by_id(record_id):
# """根据ID获取记录"""
# return BagRecord.query.get(record_id)
# def get_all_records(**filters):
# """获取所有记录(支持过滤)"""
# query = BagRecord.query
# if filters:
# query = query.filter_by(**filters)
# return query.all()
# def get_daily_counts(start_date, end_date):
# # 执行 SQL 查询,按日期分组并统计数量
# query = (
# db.session.query(
# db.func.date(BagRecord.collection_time).label('date'),
# db.func.count(BagRecord.id).label('count')
# )
# .filter(BagRecord.collection_time >= start_date)
# .filter(BagRecord.collection_time <= end_date + timedelta(days=1)) # 包含结束日期
# .group_by(db.func.date(BagRecord.collection_time))
# .order_by(db.func.date(BagRecord.collection_time))
# )
# print(query.all())
# # 将结果转换为字典列表
# results = []
# for date, count in query.all():
# results.append({
# 'date': str(date), # 将日期对象转换为字符串
# 'count': count
# })
# return results

View File

View File

@@ -0,0 +1,752 @@
from datetime import datetime
from flask import Blueprint, current_app, request, jsonify
from flask_jwt_extended import (
create_access_token,
get_jwt,
get_jwt_identity,
jwt_required,
)
from sqlalchemy import and_, asc, desc, exists, or_,func
from app.models import (
BagFile,
BagStatus,
Fst,
QaStatus,
Role,
SyncStatus,
TaggingEvents,
User,
UserRole,
BagMergeRecord,
)
from app.utils.fst_tree import build_tree
from app import db
from app.utils.log_record import log_operation
from requests.exceptions import RequestException
import requests
from sqlalchemy.orm import aliased, joinedload
check_data = Blueprint("display", __name__)
def serialize_tagging_event(event: TaggingEvents) -> dict:
def safe_name(tag):
return getattr(tag, "name", None) if tag else None
level1 = safe_name(event.level1_tag)
level2 = safe_name(event.level2_tag)
level3 = safe_name(event.level3_tag)
level4 = safe_name(event.level4_tag)
tag_path = " / ".join([x for x in [level1, level2, level3, level4] if x])
return {
"event_id": event.event_id,
"bag_id": event.bag_id,
"tag_path": tag_path,
"level1_tag_id": event.level1_tag_id,
"level1_tag_name": level1,
"level2_tag_id": event.level2_tag_id,
"level2_tag_name": level2,
"level3_tag_id": event.level3_tag_id,
"level3_tag_name": level3,
"level4_tag_id": event.level4_tag_id,
"level4_tag_name": level4,
"qa_status": event.qa_status.value if event.qa_status else None,
"case_type": event.case_type,
"front_starttime": event.front_starttime.isoformat()
if event.front_starttime
else None,
"front_endtime": event.front_endtime.isoformat() if event.front_endtime else None,
"front_start_sec": event.front_start_sec,
"front_end_sec": event.front_end_sec,
"high_speed": event.high_speed,
"urban": event.urban,
"parking": event.parking,
"note": event.note,
"source": event.source.value if event.source else None,
"reviewer_id": event.reviewer_id,
"ts_event": event.ts_event.isoformat() if event.ts_event else None,
}
@check_data.route("/bagtotal", methods=["GET"])
@jwt_required()
def bag_total():
"""查询BagFile表记录总数"""
# 执行SELECT count(*) FROM bag_file
total_count = BagFile.query.count()
return jsonify({"total": total_count}), 200
@check_data.route("/noprocess-bagtotal", methods=["GET"])
@jwt_required()
def no_process_bag_total():
count = BagFile.query.filter(BagFile.operation_status == 0).count()
return jsonify({"total": count}), 200
@check_data.route("/insert-qa-status", methods=["POST"])
@jwt_required()
def insert_qa_status():
try:
# 获取请求数据
data = request.get_json()
current_user = get_jwt_identity()
# 验证必要参数bag_id、qa_status为必填评论为可选
required_fields = ["bag_id", "qa_status"]
if not data or not all(field in data for field in required_fields):
return (
jsonify(
{
"success": False,
"message": f"缺少必要参数,需要: {required_fields}",
}
),
400,
)
# 验证qa_status是否为有效值
try:
qa_status = QaStatus(data["qa_status"])
except ValueError:
valid_statuses = [status.value for status in QaStatus]
return (
jsonify(
{
"success": False,
"message": f"无效的qa_status值有效值为: {valid_statuses}",
}
),
400,
)
# 查询对应的BagFile记录
bag_file = BagFile.query.get(data["bag_id"])
if not bag_file:
return (
jsonify(
{
"success": False,
"message": f'未找到ID为{data["bag_id"]}的BagFile记录',
}
),
404,
)
# 仅汇总 TaggingEvents不覆盖每条事件的 qa_status
tag_events = TaggingEvents.query.filter_by(
bag_id=bag_file.id, is_deleted=False
).all()
def aggregate_qa_status(events):
has_not_reviewed = False
has_invalid = False
has_modify = False
has_passed = False
for ev in events:
status = ev.qa_status or QaStatus.QA_NOT_REVIEWED
if status == QaStatus.QA_NOT_REVIEWED:
has_not_reviewed = True
elif status == QaStatus.QA_INVALID:
has_invalid = True
elif status == QaStatus.QA_MODIFY:
has_modify = True
elif status == QaStatus.QA_PASSED:
has_passed = True
if has_not_reviewed:
return QaStatus.QA_NOT_REVIEWED
if has_invalid:
return QaStatus.QA_INVALID
if has_modify:
return QaStatus.QA_MODIFY
if has_passed:
return QaStatus.QA_PASSED
return QaStatus.QA_NOT_REVIEWED
# 更新字段(包含评论字段)
if tag_events:
bag_file.qa_status = aggregate_qa_status(tag_events)
bag_file.qa_confirm_time = datetime.now()
bag_file.qa_id = current_user
if "bag_status" in data:
bag_file.bag_status = data.get("bag_status")
# 处理评论字段使用get方法避免参数不存在时报错默认空字符串
bag_file.comment1 = data.get("comment1") if data.get("comment1") else None
bag_file.comment2 = data.get("comment2") if data.get("comment2") else None
bag_file.comment3 = data.get("comment3") if data.get("comment3") else None
# 提交到数据库
db.session.commit()
final_qa_status = bag_file.qa_status.value if bag_file.qa_status else None
return jsonify(
{
"success": True,
"message": f'BagFile ID {data["bag_id"]} 的qa_status已更新为 {final_qa_status}',
"data": {
"bag_id": data["bag_id"],
"qa_status": final_qa_status,
"bag_status": data.get("bag_status"),
"comments": {
"comment1": bag_file.comment1,
"comment2": bag_file.comment2,
"comment3": bag_file.comment3,
},
},
}
)
except Exception as e:
# 发生错误时回滚
db.session.rollback()
return jsonify({"success": False, "message": f"更新失败: {str(e)}"}), 500
@check_data.route("/get-finish-list", methods=["POST"])
@jwt_required()
def get_processed():
try:
# 1. 解析请求参数新增sync_status参数
params = request.get_json() or {}
print(f"【请求参数】原始参数: {params}")
page = params.get("page", 1)
per_page = params.get("per_page", 20)
file_name = params.get("file_name", "").strip()
start_datetime = params.get("start_datetime", "").strip()
end_datetime = params.get("end_datetime", "").strip()
level1_tag = params.get("level1_tag", "")
status_str = params.get("status", "").strip()
user_id = params.get("user_id", "")
# sync_status_str = params.get('sync_status', '').strip() # 新增:同步状态参数
# 2. merge 表别名
M = aliased(BagMergeRecord)
# 3. 构建基础查询:
# 选出 BagFile + 逻辑文件名 logical_name = coalesce(joined_name, file_name)
logical_name = func.coalesce(M.joined_name, BagFile.file_name)
# 2. 构建基础查询关联User表用于查询username
query = (
db.session.query(
BagFile,
logical_name.label("logical_name"),
)
.outerjoin(User, BagFile.user_id == User.id)
.outerjoin(M, M.src_name == BagFile.file_name)
.options(
joinedload(BagFile.level1_tag),
joinedload(BagFile.level2_tag),
joinedload(BagFile.level3_tag),
joinedload(BagFile.level4_tag),
)
)
# 3. 动态添加过滤条件
conditions = []
# 固定返回bag_status>=1的数据
conditions.append(BagFile.bag_status >= 1)
conditions.append(BagFile.sync_status == "SYNCED")
# 只保留 merge 主 bag 或未 merge 的 bag
conditions.append(
or_(
M.is_initiator == True, # 参与 merge 的主 bag
M.id.is_(None), # 完全没 merge 过的 bagouter join 不命中M.* 为 NULL
)
)
# 根据user_id过滤
if user_id:
try:
user_id_int = int(user_id)
conditions.append(BagFile.user_id == user_id_int)
except ValueError:
print(f"【过滤条件】user_id格式错误非整数: {user_id}")
# 新增根据sync_status过滤
# if sync_status_str:
# try:
# sync_status_enum = SyncStatus[sync_status_str] # 假设SyncStatus是定义的枚举类
# conditions.append(BagFile.sync_status == sync_status_enum)
# except KeyError:
# print(f"【过滤条件】无效的sync_status值: {sync_status_str}")
# 文件名模糊查询
if file_name:
conditions.append(BagFile.file_name.like(f"%{file_name}%"))
print(f"【过滤条件】添加文件名模糊查询: {file_name}")
# 时间范围查询
if start_datetime:
try:
start_dt = datetime.fromisoformat(start_datetime)
conditions.append(BagFile.capture_datetime >= start_dt)
except ValueError:
print(f"【过滤条件】开始时间格式错误: {start_datetime}")
if end_datetime:
try:
end_dt = datetime.fromisoformat(end_datetime)
conditions.append(BagFile.capture_datetime <= end_dt)
except ValueError:
print(f"【过滤条件】结束时间格式错误: {end_datetime}")
# 一级标签查询
if level1_tag:
try:
level1_id = int(level1_tag)
conditions.append(BagFile.level1_tag_id == level1_id)
except ValueError:
print(f"【过滤条件】level1_tag转换失败非整数: {level1_tag}")
# 状态查询
if status_str:
try:
status_enum = BagStatus[status_str]
conditions.append(BagFile.status == status_enum)
except KeyError:
print(f"【过滤条件】无效状态值: {status_str}")
# 应用所有条件
query = query.filter(and_(*conditions))
# 4. 排序按update_time降序核心调整
query = query.order_by(BagFile.qa_confirm_time.desc())
# 5. 分页查询
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
total_items = pagination.total
total_pages = pagination.pages
# 6. 序列化新增sync_status字段
bag_files = []
bag_ids = [bag.id for bag, _ in pagination.items]
tag_events_by_bag = {}
if bag_ids:
tag_events = (
TaggingEvents.query.options(
joinedload(TaggingEvents.level1_tag),
joinedload(TaggingEvents.level2_tag),
joinedload(TaggingEvents.level3_tag),
joinedload(TaggingEvents.level4_tag),
)
.filter(
TaggingEvents.bag_id.in_(bag_ids),
TaggingEvents.is_deleted == False,
)
.order_by(TaggingEvents.bag_id.asc(), TaggingEvents.ts_event.asc())
.all()
)
for ev in tag_events:
tag_events_by_bag.setdefault(ev.bag_id, []).append(
serialize_tagging_event(ev)
)
for bag ,logical_name_value in pagination.items:
qa_username = (
bag.qa_user.username if (bag.qa_user and bag.qa_user.username) else ""
)
bag_files.append(
{
"id": bag.id,
"file_name": logical_name_value,
"capture_datetime": (
bag.capture_datetime.isoformat()
if bag.capture_datetime
else None
),
"status": bag.status.value if bag.status else None,
"create_time": (
bag.create_time.isoformat() if bag.create_time else None
),
"update_time": (
bag.qa_confirm_time.isoformat() if bag.qa_confirm_time else None
), # 排序字段显示
"level1_tag": bag.level1_tag.name if bag.level1_tag else None,
"level2_tag": bag.level2_tag.name if bag.level2_tag else None,
"level3_tag": bag.level3_tag.name if bag.level3_tag else None,
"level4_tag": bag.level4_tag.name if bag.level4_tag else None,
"bag_status": bag.bag_status,
"user_id": bag.user_id,
"username": (
bag.user.username if (bag.user and bag.user.username) else ""
),
"qa_status": bag.qa_status.value,
"sync_status": (
bag.sync_status.value if bag.sync_status else None
), # 新增:同步状态
"qa_user_id": bag.qa_id, # 可选返回QA操作人ID
"qa_username": qa_username,
"comment1": bag.comment1,
"front_start_sec": bag.front_start_sec,
"front_end_sec": bag.front_end_sec,
"high_speed": bag.high_speed,
"urban": bag.urban,
"parking": bag.parking,
"tag_events": tag_events_by_bag.get(bag.id, []),
}
)
# 7. 返回响应
return jsonify(
{
"code": 200,
"data": bag_files,
"total": total_items,
"page": page,
"pages": total_pages,
"message": "查询成功包含sync_status过滤及按update_time排序",
}
)
except Exception as e:
print(f"【接口异常】: {str(e)}")
return (
jsonify(
{
"code": 500,
"data": [],
"total": 0,
"page": page if "page" in locals() else 1,
"pages": 0,
"message": f"查询出错:{str(e)}",
}
),
500,
)
@check_data.route("/insert-db-ids", methods=["POST"])
@jwt_required()
def insert_db_ids():
try:
# 获取请求体中的ID列表
id_list = request.get_json()
# 1. 验证参数格式
if not isinstance(id_list, list):
return (
jsonify(
{
"success": False,
"message": "参数格式错误,需提供数组格式,如[1,2,3]",
}
),
400,
)
# 检查列表中是否全为整数
if not all(isinstance(id_val, int) for id_val in id_list):
return jsonify({"success": False, "message": "数组元素必须为整数ID"}), 400
# 检查列表是否为空
if len(id_list) == 0:
return jsonify({"success": False, "message": "ID列表不能为空"}), 400
# 2. 执行批量更新
current_time = datetime.now()
# 使用SQLAlchemy的批量更新
updated_rows = BagFile.query.filter(BagFile.id.in_(id_list)).update(
{BagFile.sync_status: "SYNCED", BagFile.qa_confirm_time: current_time},
synchronize_session=False,
)
# 提交事务
db.session.commit()
# 3. 返回结果
return (
jsonify(
{
"success": True,
"message": f"成功更新{updated_rows}条记录",
"updated_count": updated_rows,
"updated_ids": id_list,
}
),
200,
)
except Exception as e:
# 发生错误时回滚事务
db.session.rollback()
return jsonify({"success": False, "message": f"更新失败:{str(e)}"}), 500
@check_data.route("/insert-rootdb", methods=["POST"])
@jwt_required()
def insert_rootdb():
API_URL = "http://10.0.240.4:5232/api/fst/bags/update"
TIMEOUT = 10
result = {"success_count": 0, "fail_count": 0, "fail_details": []}
try:
param_list = request.get_json()
# print(f"【insert_rootdb】请求参数: {param_list}")
if not param_list or not isinstance(param_list, list):
return jsonify({"success": False, "message": "请求参数必须为非空数组"}), 400
bag_ids = [
item.get("rowid")
for item in param_list
if isinstance(item, dict) and "rowid" in item
]
bag_files = BagFile.query.filter(BagFile.id.in_(bag_ids)).all() if bag_ids else []
bag_by_id = {bag.id: bag for bag in bag_files}
merge_name_by_src = {}
if bag_files:
src_names = [bag.file_name for bag in bag_files if bag.file_name]
if src_names:
merge_records = (
BagMergeRecord.query.filter(BagMergeRecord.src_name.in_(src_names))
.filter(BagMergeRecord.is_initiator == True)
.all()
)
for record in merge_records:
merge_name_by_src[record.src_name] = record.joined_name
tag_events = (
TaggingEvents.query.options(
joinedload(TaggingEvents.level1_tag),
joinedload(TaggingEvents.level2_tag),
joinedload(TaggingEvents.level3_tag),
joinedload(TaggingEvents.level4_tag),
)
.filter(
TaggingEvents.bag_id.in_(bag_ids),
TaggingEvents.is_deleted == False,
)
.order_by(TaggingEvents.bag_id.asc(), TaggingEvents.ts_event.asc())
.all()
if bag_ids
else []
)
tag_events_by_bag = {}
for ev in tag_events:
tag_events_by_bag.setdefault(ev.bag_id, []).append(ev)
def _tags_from_flags(high_speed, urban, parking):
tags = []
if high_speed == 1:
tags.append("highway")
if urban == 1:
tags.append("city")
if parking == 0:
tags.append("driving")
return tags
def _nodes_and_level(level1, level2, level3, level4):
nodes = [level1 or "", level2 or "", level3 or "", level4 or ""]
first_empty_index = next(
(idx for idx, node in enumerate(nodes) if node == ""), -1
)
if first_empty_index == -1:
fst_node_level = len(nodes) - 1
else:
fst_node_level = first_empty_index - 1
return nodes, fst_node_level
def _build_payload(
bag_name,
level1,
level2,
level3,
level4,
start_sec,
end_sec,
comments,
high_speed,
urban,
parking,
):
nodes, fst_node_level = _nodes_and_level(level1, level2, level3, level4)
return {
"bag_name": bag_name,
"nodes": nodes,
"start_time": start_sec if start_sec is not None else 0,
"end_time": end_sec if end_sec is not None else 0,
"comments": comments or "",
"tags": _tags_from_flags(high_speed, urban, parking),
"fst_node_level": fst_node_level,
}
def _safe_name(tag):
return tag.name if tag else ""
def _payload_from_event(event, bag_name):
return _build_payload(
bag_name,
_safe_name(event.level1_tag),
_safe_name(event.level2_tag),
_safe_name(event.level3_tag),
_safe_name(event.level4_tag),
event.front_start_sec,
event.front_end_sec,
event.note,
event.high_speed,
event.urban,
event.parking,
)
def _payload_from_bag(bag, bag_name):
return _build_payload(
bag_name,
_safe_name(bag.level1_tag),
_safe_name(bag.level2_tag),
_safe_name(bag.level3_tag),
_safe_name(bag.level4_tag),
bag.front_start_sec,
bag.front_end_sec,
bag.comment1,
bag.high_speed,
bag.urban,
bag.parking,
)
for idx, item in enumerate(param_list):
bag_failures = []
try:
if not isinstance(item, dict):
raise ValueError("单条数据必须为对象")
if "rowid" not in item:
raise ValueError("参数缺少必要的'rowid'字段")
current_rowid = item["rowid"]
bag_file = bag_by_id.get(current_rowid)
if not bag_file:
raise ValueError("未找到对应ID的本地记录")
raw_params = item.get("params")
current_param = (
item.get("param") if isinstance(item.get("param"), dict) else None
)
bag_name = item.get("bag_name")
if not bag_name and current_param:
bag_name = current_param.get("bag_name")
if not bag_name and isinstance(raw_params, list) and raw_params:
first_param = raw_params[0]
if isinstance(first_param, dict):
bag_name = first_param.get("bag_name")
if not bag_name:
bag_name = (
merge_name_by_src.get(bag_file.file_name)
or bag_file.file_name
)
if not bag_name:
raise ValueError("bag_name为空")
payloads = []
if raw_params is not None:
if not isinstance(raw_params, list) or not raw_params:
raise ValueError("params必须为非空数组")
for param in raw_params:
if not isinstance(param, dict):
raise ValueError("params元素必须为对象")
if not param.get("bag_name"):
param["bag_name"] = bag_name
payloads.append((param, None))
elif current_param:
if not current_param.get("bag_name"):
current_param["bag_name"] = bag_name
payloads.append((current_param, None))
else:
events = tag_events_by_bag.get(current_rowid, [])
if events:
for ev in events:
payloads.append((_payload_from_event(ev, bag_name), ev))
else:
payloads.append((_payload_from_bag(bag_file, bag_name), None))
except Exception as e:
bag_failures.append(
{
"index": idx,
"rowid": item.get("rowid") if isinstance(item, dict) else None,
"message": f"处理失败:{str(e)}",
"error_type": type(e).__name__,
}
)
result["fail_count"] += 1
result["fail_details"].extend(bag_failures)
continue
for ev_idx, (payload, ev) in enumerate(payloads):
# print(
# f"【insert_rootdb】发送到RootDB: rowid={current_rowid}, "
# f"event_index={ev_idx}, event_id={ev.event_id if ev else None}, "
# f"payload={payload}"
# )
try:
response = requests.post(
url=API_URL,
json=[payload],
timeout=TIMEOUT,
)
response.raise_for_status()
response_data = response.json()
if not isinstance(response_data, list) or len(response_data) == 0:
raise ValueError("远程API返回格式错误预期非空列表")
if not isinstance(response_data[0], dict) or "success" not in response_data[0]:
raise ValueError("远程API返回缺少'success'字段")
if not response_data[0].get("success"):
bag_failures.append(
{
"index": idx,
"rowid": current_rowid,
"event_index": ev_idx,
"event_id": ev.event_id if ev else None,
"message": "远程API插入失败",
"remote_response": response_data[0],
}
)
except RequestException as e:
bag_failures.append(
{
"index": idx,
"rowid": current_rowid,
"event_index": ev_idx,
"event_id": ev.event_id if ev else None,
"message": f"网络请求失败:{str(e)}",
"error_type": type(e).__name__,
}
)
except Exception as e:
bag_failures.append(
{
"index": idx,
"rowid": current_rowid,
"event_index": ev_idx,
"event_id": ev.event_id if ev else None,
"message": f"处理失败:{str(e)}",
"error_type": type(e).__name__,
}
)
if bag_failures:
result["fail_count"] += 1
result["fail_details"].extend(bag_failures)
continue
bag_file.sync_status = SyncStatus.SYNCED
bag_file.qa_confirm_time = datetime.now()
result["success_count"] += 1
db.session.commit()
return (
jsonify({
"success": True,
"message": f"处理完成,成功{result['success_count']}条,失败{result['fail_count']}",
"result": result,
}),
200,
)
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "message": f"服务器处理错误:{str(e)}"}), 500

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
from sqlalchemy.exc import SQLAlchemyError
from datetime import datetime
from app.models import EventSource, TaggingEvents, VerdictStatus
def create_tagging_event(db, **kwargs):
"""
创建并提交TaggingEvents记录
参数:
db: SQLAlchemy数据库实例
kwargs: 字段键值对,支持所有非主键字段
返回: (success: bool, message: str, event: TaggingEvents)
"""
# 1. 创建事件对象并设置属性
event = TaggingEvents()
# 2. 动态设置字段值
valid_fields = {
'bag_id', 'source', 'rule_version_id', 'reviewer_id', 'verdict',
'level1_tag_id', 'level2_tag_id', 'level3_tag_id', 'level4_tag_id'
}
for field, value in kwargs.items():
if field in valid_fields:
setattr(event, field, value)
# 自动处理特殊字段类型[1,4](@ref)
elif field == 'ts_event' and isinstance(value, datetime):
event.ts_event = value
# 3. 枚举字段验证[1](@ref)
if 'source' in kwargs and not isinstance(kwargs['source'], EventSource):
return False, "source必须是EventSource枚举类型", None
if 'verdict' in kwargs and not isinstance(kwargs['verdict'], VerdictStatus):
return False, "verdict必须是VerdictStatus枚举类型", None
# 4. 提交到数据库[2,4](@ref)
try:
db.session.add(event)
db.session.commit()
return True, "记录创建成功", event
except SQLAlchemyError as e:
db.session.rollback()
# app.logger.error(f"数据库写入失败: {str(e)}")
return False, f"数据库错误: {str(e)}", None
except Exception as e:
db.session.rollback()
# app.logger.error(f"未知错误: {str(e)}")
return False, f"系统错误: {str(e)}", None

View File

View File

@@ -0,0 +1,294 @@
from datetime import datetime
from flask import Blueprint, current_app, request, jsonify
from flask_jwt_extended import jwt_required
import requests
from sqlalchemy import func
from app import db
from app.models import Fst
from app.utils.fst_tree import build_tree
label_bp = Blueprint('label', __name__)
def format_fst_item(fst_item):
"""将Fst对象转换为指定格式的字典"""
# 从reserved_json中获取type如果没有则默认为空字符串
item_type = ""
if fst_item.reserved_json and "type" in fst_item.reserved_json:
item_type = fst_item.reserved_json["type"]
return {
"date": fst_item.update_time.strftime('%Y-%m-%d'), # 格式化日期
"id": fst_item.id,
"name": fst_item.name,
"parentId": None if fst_item.parent_id==591 else fst_item.parent_id, # 注意这里是驼峰命名
"name_cn": fst_item.name_cn,
"annotation":fst_item.annotation,
"level":fst_item.level
}
@label_bp.route('/fst-tree', methods=['GET'])
def get_fst_tree():
"""获取所有FST数据并格式化为树形结构列表"""
try:
# 查询所有FST记录
all_fst = Fst.query.all()
# 转换为指定格式
formatted_data = [format_fst_item(item) for item in all_fst]
# 返回JSON响应
return jsonify({
"success": True,
"data": formatted_data
})
except Exception as e:
return jsonify({
"success": False,
"error": str(e)
}), 500
@label_bp.route('/create-levelone', methods=['POST'])
@jwt_required()
def create_levelone():
# 获取请求数据
data = request.get_json()
# 验证必要参数
required_fields = ['name', 'level', 'annotation']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400
# 验证层级是否为1
if data['level'] != 1:
return jsonify({'error': 'Level must be 1 for first-level tags'}), 400
try:
# 查询当前最大的id值
max_id_result = db.session.query(func.max(Fst.id)).first()
max_id = max_id_result[0] if max_id_result[0] is not None else 0
# 新id为最大id + 1
new_id = max_id + 1
# 创建新的一级标签
new_tag = Fst(
id=new_id,
name=data['name'],
level=data['level'],
annotation=data['annotation'],
parent_id=591, # 固定设置parent_id为591
update_time=datetime.now() # 设置更新时间
# 其他字段如name_cn、reserved_json、bag_sum如果有默认值可以不用指定
)
# 添加到数据库会话并提交
db.session.add(new_tag)
db.session.commit()
# print(f"新标签的ID: {new_tag.id}")
# 返回创建成功的标签信息
return jsonify({
'message': 'First-level tag created successfully',
'tag': {
'id': new_tag.id,
'name': new_tag.name,
'level': new_tag.level,
'annotation': new_tag.annotation,
'parent_id': new_tag.parent_id,
'update_time': new_tag.update_time.isoformat()
}
}), 201
except Exception as e:
# 发生错误时回滚
db.session.rollback()
return jsonify({'error': str(e)}), 500
@label_bp.route('/update-fst-annotation', methods=['POST'])
@jwt_required()
def update_fst_annotation():
# 获取请求数据
data = request.get_json()
# 验证必要参数
required_fields = ['name', 'level', 'annotation']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400
try:
# 根据name和level查询对应的记录
fst_record = Fst.query.filter_by(
name=data['name'],
level=data['level']
).first()
# 检查记录是否存在
if not fst_record:
return jsonify({
'error': f'No record found with name: {data["name"]} and level: {data["level"]}'
}), 404
# 更新annotation字段和更新时间
fst_record.annotation = data['annotation']
fst_record.update_time = datetime.now() # 更新时间戳
# 提交修改
db.session.commit()
# 返回更新成功的信息
return jsonify({
'message': 'Annotation updated successfully',
'updated_record': {
'id': fst_record.id,
'name': fst_record.name,
'level': fst_record.level,
'annotation': fst_record.annotation,
'update_time': fst_record.update_time.isoformat()
}
}), 200
except Exception as e:
# 发生错误时回滚
db.session.rollback()
return jsonify({'error': str(e)}), 500
@label_bp.route('/add-fst', methods=['POST'])
@jwt_required()
def add_fst():
# 获取请求数据
data = request.get_json()
# 验证必要参数
required_fields = ['name', 'level', 'parentName', 'annotation']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400
try:
# 根据parentName查询父标签的记录获取parent_id
parent_record = Fst.query.filter_by(name=data['parentName']).first()
# 检查父标签是否存在
if not parent_record:
return jsonify({
'error': f'Parent tag not found with name: {data["parentName"]}'
}), 404
# 查询当前最大的id值
max_id_result = db.session.query(func.max(Fst.id)).first()
max_id = max_id_result[0] if max_id_result[0] is not None else 0
# 新id为最大id + 1
new_id = max_id + 1
# 创建新的fst记录
new_fst = Fst(
id=new_id,
name=data['name'],
level=data['level'],
parent_id=parent_record.id, # 使用查询到的父标签ID
annotation=data['annotation'],
update_time=datetime.now()
)
# 添加到数据库并提交
db.session.add(new_fst)
db.session.commit()
# 返回成功信息
return jsonify({
'message': 'Fst record created successfully',
'record': {
'id': new_fst.id,
'name': new_fst.name,
'level': new_fst.level,
'parent_id': new_fst.parent_id,
'parentName': data['parentName'],
'annotation': new_fst.annotation,
'update_time': new_fst.update_time.isoformat()
}
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@label_bp.route("/sync-fst", methods=["POST"])
def sync_fst():
# 1. 解析请求参数
params = request.get_json() or {}
print(f"【请求参数】原始参数: {params}")
name = params.get("name", "")
parent_name = params.get("parent_name", "")
# 参数校验
if not name or not parent_name:
return (
jsonify(
{
"success": False,
"message": "缺少必要参数name 或 parent_name",
}
),
400,
)
# 2. 转发请求配置
API_URL = "http://10.0.240.4:5232/api/fst/update"
TIMEOUT = 10 # 超时时间(秒)
data = {"name": name, "parent_name": parent_name}
try:
# 3. 发送转发请求
res = requests.post(url=API_URL, json=data, timeout=TIMEOUT)
res.raise_for_status() # 自动抛出 4xx/5xx 状态码的异常
# 4. 处理对方 API 的响应(假设返回 JSON 格式)
try:
remote_response = res.json()
except ValueError:
# 对方返回非 JSON 格式响应
return (
jsonify(
{
"success": False,
"message": "同步失败:目标接口返回无效格式",
"details": res.text,
}
),
500,
)
# 5. 返回成功响应
return jsonify(
{
"success": True,
"message": "同步成功",
"data": remote_response, # 携带对方 API 的返回数据
}
)
except requests.exceptions.RequestException as e:
# 捕获所有 requests 相关异常超时、连接失败、4xx/5xx 等)
error_msg = f"同步失败:{str(e)}"
print(f"【转发请求错误】{error_msg}")
return (
jsonify(
{
"success": False,
"message": error_msg,
}
),
500,
)

View File

View File

@@ -0,0 +1,539 @@
from datetime import datetime
import re
from flask import Blueprint, current_app, json, request, jsonify
from flask_jwt_extended import jwt_required
import requests
from sqlalchemy import and_, func
from app import db
from app.models import BagFile, Fst
from app.utils.fst_tree import build_tree
remote_bp = Blueprint("remote", __name__)
def extract_datetime_from_filename(bag_filename):
"""
从文件名中提取日期和时间。
返回包含年月日时分秒的字符串、datetime对象以及视频URL。
"""
match = re.search(r"_(\d{8})-(\d{6})_", bag_filename)
if not match:
raise ValueError("文件名格式错误,无法提取时间信息")
date_part, time_part = match.groups()
# 提取年月日时分秒各部分
year = date_part[:4]
month = date_part[4:6]
day = date_part[6:8]
hour = time_part[:2]
minute = time_part[2:4]
second = time_part[4:6]
# 年月日时分秒整合到一个变量(字符串格式)
datetime_full = f"{year}-{month}-{day} {hour}:{minute}:{second}"
# 原有视频路径相关逻辑保持不变
bag_filename = bag_filename.replace(".bag", "")
video_final_path = f"{bag_filename}- Wide.mp4"
video_cos = f"momenta/videos/{year}/{month}/{day}/{video_final_path}"
video_url = (
"https://data-miningc01-1318950322.cos.ap-shanghai-adc.myqcloud.com/"
+ video_cos
)
# 返回整合后的完整日期时间字符串、datetime对象和视频URL
return datetime_full, video_url
def get_level1_labels(root_data, target_level1_id, use_label=False):
"""
收集特定一级标签及其所有子标签的id或label为扁平数组
:param root_data: 原始数据字典或JSON字符串
:param target_level1_id: 目标一级标签的id
:param use_label: 若为True则返回label否则返回id默认False
:return: 扁平标签数组(如未找到目标则返回空数组)
"""
# 确保root_data是字典
if isinstance(root_data, str):
try:
root_data = json.loads(root_data)
except json.JSONDecodeError:
raise ValueError("root_data无法解析为JSON")
if not isinstance(root_data, dict):
raise TypeError("root_data必须是字典或JSON字符串")
result = []
key = "label" if use_label else "id" # 选择使用label还是id
def recursive_collect(node):
"""递归收集节点的标签id或label"""
if not isinstance(node, dict) or key not in node:
return
# 只添加当前节点的标签(不保留其他字段)
result.append(node[key])
# 递归处理子节点
for child in node.get("children", []):
recursive_collect(child)
# 查找目标一级标签
target_node = None
for node in root_data.get("children", []):
if (
isinstance(node, dict)
and node.get("level") == 1
and node.get("id") == target_level1_id
):
target_node = node
break
# 收集目标节点及其所有子标签
if target_node:
recursive_collect(target_node)
return result
@remote_bp.route("/fstmenu", methods=["GET"])
def fstmenu():
# pass
"""
返回值:
{
path: string
name: string
title: string
component: string
children?: BackendRoute[]
}
"""
level1_tags = Fst.query.filter(Fst.level == 1).all()
# result = [
# {"id": tag.id, "name": tag.name, "level": tag.level} for tag in level1_tags
# ]
result = [
{
"path": tag.name,
"name": tag.name,
"title": tag.name,
"component": "",
}
for tag in level1_tags
]
return jsonify(result)
@remote_bp.route("/fstmenu1", methods=["GET"])
def fstmenu1():
# pass
"""
返回值:
{
path: string
name: string
title: string
component: string
children?: BackendRoute[]
}
"""
API_URL = "http://10.0.240.4:5232/api/fst/print_tree"
TIMEOUT = 10 # 超时时间(秒)
try:
response = requests.get(url=API_URL, timeout=TIMEOUT)
# 3. 验证HTTP响应状态
response.raise_for_status()
level1_results = []
# 遍历最外层的 children 列表level=0 的子节点即 level=1
for node in response.json()[0]["children"]:
# 二次验证 level 是否为 1避免结构异常
if node.get("level") == 1:
level1_results.append(
{
"path": node["id"],
"name": node["label"],
"title": node["label"],
"component": "",
}
)
for node_park in response.json()[1]["children"]:
# 二次验证 level 是否为 1避免结构异常
if node_park.get("level") == 1:
level1_results.append(
{
"path": node_park["id"],
"name": node_park["label"],
"title": node_park["label"],
"component": "",
}
)
return jsonify(level1_results)
except Exception as e:
# 其他未捕获异常
return jsonify({"success": False, "message": f"服务器处理错误:{str(e)}"})
# 根据fst标签返回bag信息----一级标签的所有
@remote_bp.route("/remote-baglist1", methods=["POST"])
def query_bag_file_by_fst():
try:
# 1. 解析核心请求参数(仅保留:分页参数 + Fst 1-4级标签
params = request.get_json() or {}
print(f"【请求参数】原始参数: {params}")
# 分页参数默认第1页每页20条
page = params.get("page", 1)
per_page = params.get("per_page", 20)
# Fst 标签参数1-4级标签字符串格式ID
level1_tag = params.get("level1_tag", "").strip()
level2_tag = params.get("level2_tag", "").strip()
level3_tag = params.get("level3_tag", "").strip()
level4_tag = params.get("level4_tag", "").strip()
# 2. 构建基础查询
query = BagFile.query
# 3. 核心过滤:仅保留 Fst 标签筛选条件
conditions = []
# 一级标签过滤转换为整数ID匹配
if level1_tag:
try:
conditions.append(BagFile.level1_tag_id == int(level1_tag))
except ValueError:
print(f"【过滤警告】一级标签ID格式错误: {level1_tag}")
# 二级标签过滤
if level2_tag:
try:
conditions.append(BagFile.level2_tag_id == int(level2_tag))
except ValueError:
print(f"【过滤警告】二级标签ID格式错误: {level2_tag}")
# 三级标签过滤
if level3_tag:
try:
conditions.append(BagFile.level3_tag_id == int(level3_tag))
except ValueError:
print(f"【过滤警告】三级标签ID格式错误: {level3_tag}")
# 四级标签过滤
if level4_tag:
try:
conditions.append(BagFile.level4_tag_id == int(level4_tag))
except ValueError:
print(f"【过滤警告】四级标签ID格式错误: {level4_tag}")
# 应用标签过滤条件
if conditions:
query = query.filter(and_(*conditions))
# 4. 核心排序:按 bag_update_time 降序(最新更新优先)
query = query.order_by(BagFile.bag_update_time.desc())
# 5. 分页查询(页码超出时返回空列表,不报错)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
total_items = pagination.total # 总数据条数
total_pages = pagination.pages # 总页数
# 6. 数据序列化(仅保留核心字段,包含 Fst 标签信息)
bag_files = []
for bag in pagination.items:
# 拼接 Fst 标签“中文+英文”名称(提升可读性)
level1_name = f"{bag.level1_tag.name}" if bag.level1_tag else None
level2_name = f"{bag.level2_tag.name}" if bag.level2_tag else None
level3_name = f"{bag.level3_tag.name}" if bag.level3_tag else None
level4_name = f"{bag.level4_tag.name}" if bag.level4_tag else None
bag_files.append(
{
"id": bag.id,
"file_name": bag.file_name, # BAG文件名
"capture_datetime": (
bag.capture_datetime.isoformat()
if bag.capture_datetime
else None
), # 采集时间
"bag_update_time": (
bag.bag_update_time.isoformat() if bag.bag_update_time else None
), # 排序依据字段
# Fst 标签核心信息
"level1_tag_id": bag.level1_tag_id,
"level1_tag_name": level1_name,
"level2_tag_id": bag.level2_tag_id,
"level2_tag_name": level2_name,
"level3_tag_id": bag.level3_tag_id,
"level3_tag_name": level3_name,
"level4_tag_id": bag.level4_tag_id,
"level4_tag_name": level4_name,
# 基础状态字段(按需保留,非核心可删除)
"status": bag.status.name if bag.status else None,
"bag_status": bag.bag_status,
"comment": bag.comment1,
"front_starttime": bag.front_start_sec,
"front_endtime": bag.front_end_sec,
"case_type": "简单场景" if bag.case_type == 1 else "复杂场景",
"highway": "高速" if bag.high_speed == 1 else None,
"city": "城区" if bag.urban == 1 else None,
"driving": "parking" if bag.urban else "driving",
"video_url": bag.video_url,
}
)
# 7. 返回精简响应
return jsonify(
{
"code": 200,
"data": bag_files,
"total": total_items,
"page": page,
"pages": total_pages,
"message": "根据 Fst 标签查询成功",
}
)
except Exception as e:
# 异常捕获与响应
error_msg = f"查询出错:{str(e)}"
print(f"【接口异常】{error_msg}")
return (
jsonify(
{
"code": 500,
"data": [],
"total": 0,
"page": page if "page" in locals() else 1,
"pages": 0,
"message": error_msg,
}
),
500,
)
@remote_bp.route("/remote-baglist", methods=["POST"])
def query_bag_file_by_fst1():
# 1. 解析核心请求参数(仅保留:分页参数 + Fst 1-4级标签
params = request.get_json() or {}
print(f"【请求参数】原始参数: {params}")
# 分页参数默认第1页每页20条
page = params.get("page", 1)
per_page = params.get("per_page", 20)
# Fst 标签参数1-4级标签字符串格式ID
fst_tag_list = params.get("fst_tag", "")
print(f"【请求参数】Fst 标签列表: {fst_tag_list}")
# 获取过滤的参数
filter_fst = params.get("filter_fst", [])
# 把请求到的数据进行处理
API_LEVEL1_URL = "http://10.0.240.4:5232/api/fst/print_tree"
API_NODES_URL = "http://10.0.240.4:5232/api/fst/bags/nodes"
API_OTHER_INFO_URL = "http://10.0.240.4:5232/api/bags/fst/nodes"
TIMEOUT = 10 # 超时时间(秒)
try:
# 如果filter_fst为空说明为一级标签
if len(filter_fst) == 0:
response = requests.get(url=API_LEVEL1_URL, timeout=TIMEOUT)
# print(123,response.json())
# 3. 验证HTTP响应状态
response.raise_for_status()
# 遍历最外层的 children 列表level=0 的子节点即 level=1
driving_tree = response.json()[0]
park_tree = response.json()[1]
# 获取所有的一级标签的子标签,组成一个新的列表
driving_fst_list = get_level1_labels(driving_tree, fst_tag_list)
park_fst_list = get_level1_labels(park_tree, fst_tag_list)
if len(driving_fst_list)>0:
fst_list=driving_fst_list
if len(park_fst_list)>0:
fst_list=park_fst_list
# print(555,fst_list)
filter_fst = fst_list
# 查询所有的标签
query_params = {"page": page, "per_page": per_page} # 页码 # 每页条数
# bag_files = []
if filter_fst:
res_bags = requests.post(
url=API_NODES_URL,
params=query_params, # URL查询参数自动拼接为 ?page=1&per_page=20
json=filter_fst, # JSON请求体自动设置Content-Type头
timeout=TIMEOUT, # 超时时间(秒)
)
# 对查询到结果,循环进行查询
all_bag_list = res_bags.json()["items"]
total_nums = res_bags.json()["total"]
# 获取所有的bag_name组成一个列表
bag_name_list = []
for item in all_bag_list:
bag_name = item.get("bag_name")
if bag_name:
bag_name_list.append(bag_name)
# 查询所有的bagname的列表数据,返回的数据是字典中对应的列表
bag_other_info_list = requests.post(url=API_OTHER_INFO_URL, json=bag_name_list)
# 组装数据
res_list=[]
for row in all_bag_list:
bag_name = row.get("bag_name")
if bag_name:
record={}
record["file_name"]=bag_name
record["level1_tag_name"] = fst_tag_list
record["sub_tag_name"] = row.get('sts')
# 过滤值
bag_other_detail=bag_other_info_list.json()[bag_name]
if len(bag_other_detail)==0:
record["comment"] = ""
record["highway"] = ""
record["city"] = ""
record["driving"] = ""
record["front_starttime"] = ""
record["front_endtime"] = ""
if len(bag_other_detail)==1:
record["comment"] = bag_other_detail[0]["comments"]
record["highway"] = ""
record["city"] = ""
record["driving"] = ""
record["front_starttime"] = bag_other_detail[0]["start"]
record["front_endtime"] = bag_other_detail[0]["end"]
if len(bag_other_detail)>1:
# 循环bag_other_detail的值当row.get('sts')等于某项的name时。
for info in bag_other_detail:
if info['name']==row.get('sts'):
record["comment"] = info['comments']
record["highway"] = ""
record["city"] = ""
record["driving"] = ""
record["front_starttime"] = info['start']
record["front_endtime"] = info['end']
else:
record["comment"] = ""
record["highway"] = ""
record["city"] = ""
record["driving"] = ""
record["front_starttime"] = ""
record["front_endtime"] = ""
datetime_full, video_url = extract_datetime_from_filename(bag_name)
record["video_url"] = video_url
record["capture_datetime"] = datetime_full
res_list.append(record)
# 7. 返回精简响应
return jsonify(
{
"code": 200,
"data": res_list,
"total": total_nums,
"page": page,
"pages": page,
"message": "根据 Fst 标签查询成功",
}
)
else:
return (
jsonify(
{
"code": 500,
"data": [],
"total": 0,
"page": page if "page" in locals() else 1,
"pages": 0,
"message": "标签数据出错",
}
),
500,
)
except Exception as e:
# 异常捕获与响应
error_msg = f"查询出错:{str(e)}"
print(f"【接口异常】{error_msg}")
return (
jsonify(
{
"code": 500,
"data": [],
"total": 0,
"page": page if "page" in locals() else 1,
"pages": 0,
"message": error_msg,
}
),
500,
)
@remote_bp.route("/sub-fst", methods=["GET"])
def get_sub_fst1():
target_label = request.args.get("fst_id")
API_URL = "http://10.0.240.4:5232/api/fst/print_tree"
TIMEOUT = 10 # 超时时间(秒)
try:
response = requests.get(url=API_URL, timeout=TIMEOUT)
# 3. 验证HTTP响应状态
response.raise_for_status()
# 遍历最外层的 children 列表level=0 的子节点即 level=1
result=[]
for node in response.json()[0].get("children", []):
if node.get("level") == 1 and node.get("label") == target_label:
data = node.get("children", [])
if data:
result=data
for node_park in response.json()[1].get("children", []):
if node_park.get("level") == 1 and node_park.get("label") == target_label:
data_park = node_park.get("children", [])
if data_park:
result=data_park
return jsonify({"code": 200, "data": result}) # 返回该节点的children
return (
jsonify({"code": 404, "message": "Fst not found"}),
404,
) # 未找到匹配节点时返回None
except Exception as e:
return jsonify({"success": False, "message": f"服务器处理错误:{str(e)}"})
@remote_bp.route("/all-fst", methods=["GET"])
def get_all_fst1():
type = request.args.get("type")
API_URL = "http://10.0.240.4:5232/api/fst/print_tree"
TIMEOUT = 10 # 超时时间(秒)
try:
response = requests.get(url=API_URL, timeout=TIMEOUT)
# 3. 验证HTTP响应状态
response.raise_for_status()
if type == "driving":
return jsonify({"code": 200, "data": response.json()[0]})
if type == "parking":
return jsonify({"code": 200, "data": response.json()[1]})
except Exception as e:
return jsonify({"success": False, "message": f"服务器处理错误:{str(e)}"})

View File

@@ -0,0 +1,5 @@
# from flask import Blueprint
# auth_bp = Blueprint('auth', __name__)
# from . import routes

View File

@@ -0,0 +1,885 @@
from datetime import datetime
from functools import lru_cache
import os
import re
import time
import uuid
from flask import Blueprint, current_app, json, request, jsonify
from flask_jwt_extended import get_jwt_identity, jwt_required
import requests
from sqlalchemy import and_, func
from app import db
from app.models import BagFile, BagStatus, Fst, User, VlmFilter
from app import create_app
from app.utils.get_vlm_token import Demo
from app.utils.driving_tree import vlm_data
vlm_bp = Blueprint("vlm", __name__)
def extract_datetime_from_filename(bag_filename):
"""
从文件名中提取日期和时间。
返回包含年月日时分秒的字符串、datetime对象以及视频URL。
"""
match = re.search(r"_(\d{8})-(\d{6})_", bag_filename)
if not match:
raise ValueError("文件名格式错误,无法提取时间信息")
date_part, time_part = match.groups()
# 提取年月日时分秒各部分
year = date_part[:4]
month = date_part[4:6]
day = date_part[6:8]
hour = time_part[:2]
minute = time_part[2:4]
second = time_part[4:6]
# 年月日时分秒整合到一个变量(字符串格式)
datetime_full = f"{year}-{month}-{day} {hour}:{minute}:{second}"
# 原有视频路径相关逻辑保持不变
bag_filename = bag_filename.replace(".bag", "")
video_final_path = f"{bag_filename}- Wide.mp4"
video_cos = f"momenta/videos/{year}/{month}/{day}/{video_final_path}"
video_url = (
"https://data-miningc01-1318950322.cos.ap-shanghai-adc.myqcloud.com/"
+ video_cos
)
# 返回整合后的完整日期时间字符串、datetime对象和视频URL
return datetime_full, video_url
@lru_cache(maxsize=1)
def get_cached_token():
TOKEN_EXPIRE_SECONDS = 7200
"""获取缓存的Token如果过期则重新获取固定有效期"""
# 获取Token时记录当前时间作为获取时间
token_info = Demo().getAuthToken()
token_info["fetch_time"] = time.time() # 手动添加获取时间
# 检查是否过期(当前时间 - 获取时间 > 有效期)
if time.time() - token_info["fetch_time"] >= TOKEN_EXPIRE_SECONDS - 60:
# 提前60秒刷新
get_cached_token.cache_clear()
new_token = Demo().getAuthToken()
new_token["fetch_time"] = time.time() # 记录新的获取时间
return new_token
return token_info
@vlm_bp.route("/insert-csv", methods=["POST"])
@jwt_required()
def insert_db_by_csv():
try:
# 1. 获取上传的CSV文件
if "file" not in request.files:
return jsonify({"code": 400, "message": "未上传CSV文件"}), 400
file = request.files["file"]
if file.filename == "":
return jsonify({"code": 400, "message": "未选择文件"}), 400
# 验证文件类型
if not file.filename.endswith(".csv"):
return jsonify({"code": 400, "message": "仅支持CSV格式文件"}), 400
# 2. 直接从form中获取表单字段无需解析JSON
# 验证必要的表单参数
required_fields = ["level1Tag", "status"]
form_params = {}
for field in required_fields:
value = request.form.get(field)
if not value:
return (
jsonify({"code": 400, "message": f"缺少必要的表单参数: {field}"}),
400,
)
form_params[field] = value
# 额外获取可选的表单字段
form_params["radioGroup"] = request.form.get("radioGroup", "")
form_params["fileKey"] = request.form.get("fileKey", "")
# 转换level1Tag为整数
try:
level1_tag = int(form_params["level1Tag"])
except ValueError:
return jsonify({"code": 400, "message": "level1Tag必须是整数"}), 400
# 验证status是否有效
status_str = form_params["status"]
try:
status = BagStatus[status_str] # 假设使用枚举类
except KeyError:
return (
jsonify({"code": 400, "message": f"无效的status值: {status_str}"}),
400,
)
# 替换bag_status
if form_params["radioGroup"] == "annotation":
bag_status = 0
if form_params["radioGroup"] == "qa":
bag_status = 1
# 插入fst_version字段
# 获取当前时间
current_time = datetime.now()
# 格式化年月日时,用下划线连接
formatted_time = f"{current_time.year}-{current_time.month:02d}-{current_time.day:02d}-{current_time.hour:02d}"
fst_version = "vlm_" + get_jwt_identity() + "_" + formatted_time
# 3. 解析CSV文件内容
import csv
from io import StringIO
# from datetime import datetime
# 读取CSV内容
csv_content = file.stream.read().decode("utf-8")
csv_file = StringIO(csv_content)
csv_reader = csv.DictReader(csv_file)
# 验证CSV表头
if "bag_name" not in csv_reader.fieldnames:
return (
jsonify({"code": 400, "message": "CSV文件缺少必要的'bag_name'"}),
400,
)
# 4. 组合CSV数据与表单数据插入数据库
inserted_count = 0
skipped_count = 0
error_records = []
for row_num, row in enumerate(
csv_reader, start=2
): # 行号从2开始表头为第1行
bag_name = row.get("bag_name", "").strip()
if not bag_name:
skipped_count += 1
error_records.append(
{"row": row_num, "reason": "bag_name为空", "content": row}
)
continue
# 从文件名提取时间和视频URL复用你的函数
datetime_full, video_url = extract_datetime_from_filename(bag_name)
# 组合数据CSV的bag_name + 表单的level1Tag、status等
new_bag = BagFile(
file_name=bag_name,
level1_tag_id=level1_tag, # 表单字段
status=status, # 表单字段
capture_datetime=datetime_full,
create_time=datetime.now(),
update_time=datetime.now(),
bag_status=bag_status,
sync_status="SYNC_NOT_READY",
user_id=get_jwt_identity(), # 当前用户ID
video_url=video_url,
fst_version=fst_version,
)
db.session.add(new_bag)
inserted_count += 1
# 提交事务
db.session.commit()
# 5. 返回处理结果
return jsonify(
{
"code": 200,
"message": f"处理完成,成功插入 {inserted_count} 条记录,跳过 {skipped_count} 条记录",
"data": {
"inserted_count": inserted_count,
"skipped_count": skipped_count,
"errors": error_records if skipped_count > 0 else None,
},
}
)
except Exception as e:
db.session.rollback()
print(f"【上传CSV接口异常】: {str(e)}")
return jsonify({"code": 500, "message": f"处理失败:{str(e)}"}), 500
@vlm_bp.route("/get-models", methods=["GET"])
def get_models():
result = {
"code": 200,
"data": [
{
"id": 3,
"name": "图片模型Base",
"url": "http://10.0.220.110:20080",
"type": 0,
"remark": "没有finetune的初始模型\nhttp://10.0.220.110:20080",
"createUserId": 3,
"status": 1,
"createdAt": "2024-11-21T07:36:34.337Z",
"updatedAt": "2025-04-10T08:00:03.727Z",
},
{
"id": 4,
"name": "图片模型FT4",
"url": "http://10.0.220.226:20081",
"type": 0,
"remark": "第4轮微调模型\nhttp://10.0.220.226:20081",
"createUserId": 3,
"status": 1,
"createdAt": "2025-04-25T07:22:06.986Z",
"updatedAt": "2025-05-09T07:00:22.667Z",
},
],
"success": True,
"message": "success",
}
# token = get_cached_token()
# API_URL = "http://10.0.220.110/api/app/model/models-server?modelType=0"
# headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# response = requests.get(url=API_URL, headers=headers)
# result = response.json()
return jsonify({"code": 200, "data": result})
@vlm_bp.route("/get-datasets", methods=["GET"])
def get_datasets():
# result = {
# "code": 200,
# "data": [
# {
# "id": 99,
# "name": "Momenta",
# "path": None,
# "type": 0,
# "status": 3,
# "remark": "这是数据集",
# "index_name": "0_33f0ba5e-d00d-4033-b2d4-3a491a347f25",
# "create_user_id": None,
# "is_delete": 0,
# "createdAt": "2025-04-25T09:10:33.072Z",
# "updatedAt": "2025-05-19T15:03:33.366Z",
# "file_count": None,
# },
# {
# "id": 107,
# "name": "MB",
# "path": None,
# "type": 0,
# "status": 3,
# "remark": "这是数据集",
# "index_name": "0_abcb005e-6782-4b91-877e-4b1cebcf52e7",
# "create_user_id": None,
# "is_delete": 0,
# "createdAt": "2025-05-15T09:49:23.056Z",
# "updatedAt": "2025-05-19T11:35:21.063Z",
# "file_count": None,
# },
# {
# "id": 124,
# "name": "MB_DC_2508",
# "path": None,
# "type": 0,
# "status": 3,
# "remark": "这是数据集",
# "index_name": "0_35caf344-a572-40d1-bc31-e426b785eaa2",
# "create_user_id": None,
# "is_delete": 0,
# "createdAt": "2025-08-29T01:30:34.199Z",
# "updatedAt": "2025-09-12T07:45:58.658Z",
# "file_count": None,
# },
# {
# "id": 125,
# "name": "MB_DC_2504_06",
# "path": None,
# "type": 0,
# "status": 3,
# "remark": "这是数据集",
# "index_name": "0_46805e78-738e-439d-9b65-2b5a9cdd5ee5",
# "create_user_id": None,
# "is_delete": 0,
# "createdAt": "2025-09-04T09:54:12.411Z",
# "updatedAt": "2025-09-11T19:37:03.399Z",
# "file_count": None,
# },
# {
# "id": 126,
# "name": "MB_DC_2507",
# "path": None,
# "type": 0,
# "status": 3,
# "remark": "这是数据集",
# "index_name": "0_14b37c68-cf81-4c5b-b232-43323c7501e7",
# "create_user_id": None,
# "is_delete": 0,
# "createdAt": "2025-09-11T01:47:57.010Z",
# "updatedAt": "2025-09-12T03:45:47.002Z",
# "file_count": None,
# },
# ],
# "success": True,
# "message": "success",
# }
token = get_cached_token()
API_URL = "http://10.0.220.110/api/app/model/datasets-list?modelId=4"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
response = requests.get(url=API_URL, headers=headers)
result = response.json()
return jsonify({"code": 200, "data": result})
@vlm_bp.route("/get-search", methods=["POST"])
def get_search_list():
# result = vlm_data
API_URL = "http://10.0.220.110/api/app/search/4"
data = request.json
result = requests.post(url=API_URL, json=data)
result=result.json()
return jsonify({"code": 200, "data": result})
@vlm_bp.route("/get-alltags", methods=["GET"])
def get_alltags():
try:
# 查询所有标签
all_tags = Fst.query.all()
# 转换为所需的结构
result = []
for tag in all_tags:
tag_data = {
"id": tag.id,
"name": tag.name,
"level": tag.level,
"parent_id": tag.parent_id,
}
result.append(tag_data)
# 返回JSON响应
return jsonify(result), 200
except Exception as e:
# 错误处理
return jsonify({"error": str(e)}), 500
@vlm_bp.route("/insert-vlm-filter", methods=["POST"])
@jwt_required()
def add_vlm_filter():
"""
新增VlmFilter记录
请求参数:
{
"level1": {
"id": 407,
"name": "TOLL_STATION",
"level": 1
},
"level2": {
"id": 616,
"name": "TOLL_STATION_LEAVE",
"level": 2
},
"level3": {
"id": 579,
"name": "TOLL_STATION_LEAVE_PASSING_NO_LANE_MARKING_SQUARE_AFTER_ROD",
"level": 3
},
"level4": null,
"bagname": "PL162802_event_hmi_console_event_20250827-182132_0.bag",
"comment": "12345",
"status": 0,
"video_url":"",
}
"""
try:
# 1. 获取当前用户ID从JWT中提取
current_user_id = get_jwt_identity()
# 2. 解析请求参数
params = request.json
# 3. 验证必要参数
required_fields = ["level1", "level2", "level3", "bagname", "status"]
if not all(key in params for key in required_fields):
return (
jsonify(
{
"code": 400,
"message": f"缺少必要参数: {', '.join(required_fields)}",
}
),
400,
)
# 4. 提取参数值
level1 = params["level1"]
level2 = params["level2"]
level3 = params["level3"]
level4 = params.get("level4")
bagname = params["bagname"]
comment = params.get("comment", "")
status = params["status"]
video_url = params["video_url"]
# 5. 提取各级标签ID
level1_tag_id = level1.get("id") if level1 else None
level2_tag_id = level2.get("id") if level2 else None
level3_tag_id = level3.get("id") if level3 else None
level4_tag_id = level4.get("id") if level4 else None
# 6. 验证标签层级关系和存在性
# 验证一级标签
if level1_tag_id is not None:
level1_tag = Fst.query.get(level1_tag_id)
if not level1_tag:
return (
jsonify(
{
"code": 400,
"message": f"一级标签不存在ID: {level1_tag_id}",
}
),
400,
)
# 验证二级标签
if level2_tag_id is not None:
level2_tag = Fst.query.get(level2_tag_id)
if not level2_tag:
return (
jsonify(
{
"code": 400,
"message": f"二级标签不存在ID: {level2_tag_id}",
}
),
400,
)
if not level1_tag_id:
return (
jsonify(
{"code": 400, "message": "二级标签存在时,一级标签不能为空"}
),
400,
)
if level2_tag.parent_id != level1_tag_id:
return (
jsonify(
{
"code": 400,
"message": f"二级标签的父级ID{level2_tag.parent_id}与一级标签ID{level1_tag_id})不匹配",
}
),
400,
)
# 验证三级标签
if level3_tag_id is not None:
level3_tag = Fst.query.get(level3_tag_id)
if not level3_tag:
return (
jsonify(
{
"code": 400,
"message": f"三级标签不存在ID: {level3_tag_id}",
}
),
400,
)
if not level2_tag_id:
return (
jsonify(
{"code": 400, "message": "三级标签存在时,二级标签不能为空"}
),
400,
)
if level3_tag.parent_id != level2_tag_id:
return (
jsonify(
{
"code": 400,
"message": f"三级标签的父级ID{level3_tag.parent_id}与二级标签ID{level2_tag_id})不匹配",
}
),
400,
)
# 验证四级标签
if level4_tag_id is not None:
level4_tag = Fst.query.get(level4_tag_id)
if not level4_tag:
return (
jsonify(
{
"code": 400,
"message": f"四级标签不存在ID: {level4_tag_id}",
}
),
400,
)
if not level3_tag_id:
return (
jsonify(
{"code": 400, "message": "四级标签存在时,三级标签不能为空"}
),
400,
)
if level4_tag.parent_id != level3_tag_id:
return (
jsonify(
{
"code": 400,
"message": f"四级标签的父级ID{level4_tag.parent_id}与三级标签ID{level3_tag_id})不匹配",
}
),
400,
)
# 7. 创建新的VlmFilter记录
new_vlm = VlmFilter(
bag_name=bagname,
level1_tag_id=level1_tag_id,
level2_tag_id=level2_tag_id,
level3_tag_id=level3_tag_id,
level4_tag_id=level4_tag_id,
comment=comment,
user_id=current_user_id,
# collection_time=datetime.now(),
create_time=datetime.now(),
status=status,
video_url=video_url,
)
# 8. 添加到数据库并提交
db.session.add(new_vlm)
db.session.commit()
# 9. 返回成功响应
return (
jsonify(
{
"code": 200,
"message": "数据插入成功",
"data": {
"id": new_vlm.id,
"bag_name": new_vlm.bag_name,
"level1_tag_id": new_vlm.level1_tag_id,
"level2_tag_id": new_vlm.level2_tag_id,
"level3_tag_id": new_vlm.level3_tag_id,
"level4_tag_id": new_vlm.level4_tag_id,
"user_id": new_vlm.user_id,
"create_time": new_vlm.create_time.strftime(
"%Y-%m-%d %H:%M:%S"
),
"status": new_vlm.status,
},
}
),
200,
)
except Exception as e:
db.session.rollback()
return jsonify({"code": 500, "message": f"服务器错误: {str(e)}"}), 500
@vlm_bp.route("/get-vlm-filter-list", methods=["POST"])
@jwt_required()
def get_vlm_filter_list():
try:
# 1. 解析请求参数
params = request.get_json() or {}
# 提取分页参数,设置默认值
page = params.get("page", 1)
per_page = params.get("per_page", 20)
# 提取过滤参数
bag_name = params.get("bag_name", "").strip()
level1_tag = params.get("level1_tag", "")
start_datetime = params.get("start_datetime", "").strip()
end_datetime = params.get("end_datetime", "").strip()
user_id = params.get("user_id")
# 2. 构建基础查询
query = VlmFilter.query
# 3. 动态添加过滤条件
conditions = []
# 固定条件只展示status=0的数据核心新增
conditions.append(VlmFilter.status == 0)
# 袋名模糊查询
if bag_name:
conditions.append(VlmFilter.bag_name.like(f"%{bag_name}%"))
# 一级标签查询
if level1_tag:
try:
level1_id = int(level1_tag)
conditions.append(VlmFilter.level1_tag_id == level1_id)
except ValueError:
print(f"【过滤条件】level1_tag转换失败非整数: {level1_tag}")
# 用户ID过滤
if user_id:
try:
user_id_int = int(user_id)
conditions.append(VlmFilter.user_id == user_id_int)
except ValueError:
print(f"【过滤条件】user_id转换失败非整数: {user_id}")
# 时间范围查询按create_time
if start_datetime:
try:
start_dt = datetime.fromisoformat(start_datetime)
conditions.append(VlmFilter.create_time >= start_dt)
except ValueError:
print(f"【过滤条件】开始时间格式错误: {start_datetime}")
if end_datetime:
try:
end_dt = datetime.fromisoformat(end_datetime)
conditions.append(VlmFilter.create_time <= end_dt)
except ValueError:
print(f"【过滤条件】结束时间格式错误: {end_datetime}")
# 应用所有条件包含固定的status=0
if conditions:
query = query.filter(and_(*conditions))
# 4. 排序按create_time降序
query = query.order_by(VlmFilter.create_time.desc())
# 5. 分页查询
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
total_items = pagination.total
total_pages = pagination.pages
# 6. 序列化结果
result_list = []
for item in pagination.items:
# 获取标签名称
level1_name = (
Fst.query.get(item.level1_tag_id).name if item.level1_tag_id else None
)
level2_name = (
Fst.query.get(item.level2_tag_id).name if item.level2_tag_id else None
)
level3_name = (
Fst.query.get(item.level3_tag_id).name if item.level3_tag_id else None
)
level4_name = (
Fst.query.get(item.level4_tag_id).name if item.level4_tag_id else None
)
# 获取用户名
username = User.query.get(item.user_id).username if item.user_id else None
result_list.append(
{
"id": item.id,
"bag_name": item.bag_name,
"level1_tag_id": item.level1_tag_id,
"level1_tag_name": level1_name,
"level2_tag_id": item.level2_tag_id,
"level2_tag_name": level2_name,
"level3_tag_id": item.level3_tag_id,
"level3_tag_name": level3_name,
"level4_tag_id": item.level4_tag_id,
"level4_tag_name": level4_name,
"video_url": item.video_url,
"comment": item.comment,
"user_id": item.user_id,
"username": username,
"collection_time": (
item.collection_time.isoformat()
if item.collection_time
else None
),
"create_time": (
item.create_time.isoformat() if item.create_time else None
),
"init_label": item.init_label,
"status": item.status, # 此处返回的status始终为0
}
)
# 7. 返回响应
return jsonify(
{
"code": 200,
"data": result_list,
"total": total_items,
"page": page,
"pages": total_pages,
"message": "查询成功仅返回status=0的数据",
}
)
except Exception as e:
print(f"【接口异常】: {str(e)}")
return (
jsonify(
{
"code": 500,
"data": [],
"total": 0,
"page": page if "page" in locals() else 1,
"pages": 0,
"message": f"查询出错:{str(e)}",
}
),
500,
)
@vlm_bp.route("/send-loacldb", methods=["POST"])
@jwt_required()
def send_filter_vlm_localdb():
try:
# 1. 解析请求参数
params = request.get_json() or {}
# 验证必要参数
if "data" not in params or not isinstance(params["data"], list):
return (
jsonify({"code": 400, "message": "缺少必要参数data或data不是数组"}),
400,
)
if "add_status" not in params:
return jsonify({"code": 400, "message": "缺少必要参数add_status"}), 400
# 提取参数
data_list = params["data"]
add_status = params["add_status"]
label_status = params["label_status"]
# 验证add_status是否为整数
try:
add_status = int(add_status)
except ValueError:
return jsonify({"code": 400, "message": "add_status必须是整数"}), 400
# 初始化统计变量
inserted_count = 0
updated_count = 0
error_records = []
# 获取当前用户ID
current_user_id = get_jwt_identity()
current_time = datetime.now()
# 获取当前时间
current_time = datetime.now()
# 格式化年月日时,用下划线连接
formatted_time = f"{current_time.year}-{current_time.month:02d}-{current_time.day:02d}-{current_time.hour:02d}"
fst_version = "send_" + get_jwt_identity() + "_" + formatted_time
# 2. 循环处理每条数据
for idx, item in enumerate(data_list):
try:
# print("item",item)
# 验证必要字段
if "bag_name" not in item or not item["bag_name"]:
error_records.append(
{
"index": idx,
"reason": "缺少bag_name或bag_name为空",
"data": item,
}
)
continue
bag_name=item["bag_name"]
datetime_full, video_url = extract_datetime_from_filename(bag_name)
# 创建新的BagFile记录
new_bag = BagFile(
file_name=bag_name,
capture_datetime=datetime_full,
level1_tag_id=item.get("level1_tag_id"),
level2_tag_id=item.get("level2_tag_id"),
level3_tag_id=item.get("level3_tag_id"),
level4_tag_id=item.get("level4_tag_id"),
video_url=item.get("video_url"),
comment1=item.get("comment"),
user_id=item.get("user_id") or current_user_id,
create_time=(
datetime.fromisoformat(item["create_time"])
if item.get("create_time")
else current_time
),
update_time=current_time,
fst_version=fst_version,
bag_status=add_status, # 0是标注1是质检
sync_status="SYNC_NOT_READY",
status=BagStatus[label_status],
)
print("new_bag", new_bag)
# 添加到会话
db.session.add(new_bag)
inserted_count += 1
# 3. 更新vlm_filter表
# 假设根据bag_name和id进行匹配
vlm_filter = VlmFilter.query.filter_by(
id=item.get("id"), bag_name=item["bag_name"]
).first()
if vlm_filter:
vlm_filter.status = 1 # 1是入库
vlm_filter.init_label = add_status # 0是标注1是质检
updated_count += 1
else:
error_records.append(
{
"index": idx,
"reason": f"未找到对应的vlm_filter记录 (id: {item.get('id')}, bag_name: {item['bag_name']})",
"data": item,
}
)
except Exception as e:
error_records.append(
{"index": idx, "reason": f"处理错误: {str(e)}", "data": item}
)
# 4. 提交事务
db.session.commit()
# 5. 返回结果
return jsonify(
{
"code": 200,
"message": f"批量处理完成,成功插入 {inserted_count} 条记录,{len(error_records)} 条记录处理失败",
"data": {
"inserted_count": inserted_count,
"updated_count": updated_count,
"error_count": len(error_records),
"errors": error_records if error_records else None,
},
}
)
except Exception as e:
db.session.rollback()
print(f"【批量插入接口异常】: {str(e)}")
return jsonify({"code": 500, "message": f"处理失败:{str(e)}"}), 500