提交
This commit is contained in:
4
.idea/.gitignore
generated
vendored
Normal file
4
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
.idea
|
||||
42
20251118_update.sql
Normal file
42
20251118_update.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- 1. 取消 file_path 的唯一约束,保留列
|
||||
ALTER TABLE bag_file DROP INDEX `file_path`;
|
||||
|
||||
-- 2. 为 file_name 建索引(用于 join / like 查询)
|
||||
ALTER TABLE bag_file
|
||||
ADD INDEX `idx_bag_file_name` (`file_name`);
|
||||
|
||||
-- 3. 为 bag_update_time 建索引(用于 “每个 logical_name 取最新一条” 的子查询)
|
||||
ALTER TABLE bag_file
|
||||
ADD INDEX `idx_bag_update_time` (`bag_update_time`);
|
||||
|
||||
ALTER TABLE bag_file
|
||||
ADD INDEX `idx_create_time` (`create_time`);
|
||||
|
||||
-- 4. 为 bag_status 建索引
|
||||
ALTER TABLE bag_file
|
||||
ADD INDEX idx_bag_status (bag_status);
|
||||
|
||||
|
||||
-- ============================
|
||||
-- Table structure for bag_merge_record
|
||||
-- ============================
|
||||
create table bag_merge_record
|
||||
(
|
||||
id int(11) unsigned auto_increment comment '主键'
|
||||
primary key,
|
||||
joined_name varchar(255) not null comment '合并后 bag 名称(joined_name)',
|
||||
src_name varchar(255) not null comment '参与合并的原始 bag 名称',
|
||||
create_time datetime default CURRENT_TIMESTAMP not null comment '记录创建时间',
|
||||
created_by int null comment '操作用户 id',
|
||||
is_initiator tinyint(1) default 0 not null comment '1: 此 src 是本次合并的主 bag',
|
||||
constraint fk_bag_merge_created_by
|
||||
foreign key (created_by) references users (id)
|
||||
on update cascade on delete set null
|
||||
)
|
||||
row_format = DYNAMIC;
|
||||
create index idx_created_by
|
||||
on bag_merge_record (created_by);
|
||||
create index idx_joined_name
|
||||
on bag_merge_record (joined_name);
|
||||
create index idx_src_name
|
||||
on bag_merge_record (src_name);
|
||||
14
20251216_multi_tag.sql
Normal file
14
20251216_multi_tag.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Multi-tag support: per TaggingEvent note + time range + scene flags
|
||||
-- Run this against the same MySQL schema used by `flask_rulebase_serve/app/models.py`.
|
||||
|
||||
ALTER TABLE tagging_events
|
||||
ADD COLUMN front_starttime DATETIME NULL COMMENT '前摄像头开始时间',
|
||||
ADD COLUMN front_endtime DATETIME NULL COMMENT '前摄像头结束时间',
|
||||
ADD COLUMN front_start_sec INT NULL COMMENT '前摄的开始秒',
|
||||
ADD COLUMN front_end_sec INT NULL COMMENT '前摄的结束秒',
|
||||
ADD COLUMN high_speed INT NULL DEFAULT 0 COMMENT '高速,1是,0否',
|
||||
ADD COLUMN urban INT NULL DEFAULT 0 COMMENT '城区,1是,0否',
|
||||
ADD COLUMN parking INT NULL DEFAULT 0 COMMENT '停车,1是,0否',
|
||||
ADD COLUMN qa_status ENUM('QA_NOT_REVIEWED','QA_PASSED','QA_MODIFY','QA_INVALID') NULL DEFAULT 'QA_NOT_REVIEWED' COMMENT '质检状态',
|
||||
ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'soft delete flag',
|
||||
ADD COLUMN note TEXT NULL COMMENT '备注';
|
||||
72
20251217_migrate_retest_tag_events.sql
Normal file
72
20251217_migrate_retest_tag_events.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
--todo 检查一下 数据情况
|
||||
--SELECT COUNT(*) AS will_update_rows
|
||||
--FROM tagging_events t
|
||||
--JOIN bag_file b ON b.id = t.bag_id
|
||||
--LEFT JOIN bag_merge_record m ON m.src_name = b.file_name
|
||||
--WHERE
|
||||
-- t.is_deleted = 0
|
||||
-- AND b.bag_status >= 1
|
||||
-- AND (m.is_initiator = 1 OR m.id IS NULL)
|
||||
-- AND (
|
||||
-- t.front_starttime IS NULL
|
||||
-- OR t.front_endtime IS NULL
|
||||
-- OR t.front_start_sec IS NULL
|
||||
-- OR t.front_end_sec IS NULL
|
||||
-- OR t.high_speed IS NULL
|
||||
-- OR t.urban IS NULL
|
||||
-- OR t.parking IS NULL
|
||||
-- OR t.qa_status IS NULL
|
||||
-- OR t.note IS NULL
|
||||
-- OR t.is_deleted IS NULL
|
||||
-- );
|
||||
--
|
||||
--SELECT COUNT(*) AS will_update_rows
|
||||
--FROM tagging_events t
|
||||
--JOIN bag_file b ON b.id = t.bag_id
|
||||
--LEFT JOIN bag_merge_record m ON m.src_name = b.file_name
|
||||
--WHERE
|
||||
-- t.is_deleted = 0
|
||||
-- AND b.bag_status >= 1
|
||||
-- AND b.sync_status = 'SYNC_NOT_READY'
|
||||
-- AND (m.is_initiator = 1 OR m.id IS NULL)
|
||||
-- AND (
|
||||
-- t.front_starttime IS NULL
|
||||
-- OR t.front_endtime IS NULL
|
||||
-- OR t.front_start_sec IS NULL
|
||||
-- OR t.front_end_sec IS NULL
|
||||
-- OR t.high_speed IS NULL
|
||||
-- OR t.urban IS NULL
|
||||
-- OR t.parking IS NULL
|
||||
-- OR t.qa_status IS NULL
|
||||
-- OR t.note IS NULL
|
||||
-- OR t.is_deleted IS NULL
|
||||
-- );
|
||||
|
||||
-- Backfill new columns for existing TaggingEvents that match getretestbaglist.
|
||||
-- This updates only existing rows; it does not create new TaggingEvents.
|
||||
|
||||
UPDATE tagging_events t
|
||||
JOIN bag_file b
|
||||
ON b.id = t.bag_id
|
||||
LEFT JOIN bag_merge_record m
|
||||
ON m.src_name = b.file_name
|
||||
SET
|
||||
t.front_starttime = COALESCE(t.front_starttime, b.front_starttime),
|
||||
t.front_endtime = COALESCE(t.front_endtime, b.front_endtime),
|
||||
t.front_start_sec = COALESCE(t.front_start_sec, b.front_start_sec),
|
||||
t.front_end_sec = COALESCE(t.front_end_sec, b.front_end_sec),
|
||||
t.high_speed = COALESCE(t.high_speed, b.high_speed, 0),
|
||||
t.urban = COALESCE(t.urban, b.urban, 0),
|
||||
t.parking = COALESCE(t.parking, b.parking, 0),
|
||||
t.qa_status = COALESCE(t.qa_status, b.qa_status, 'QA_NOT_REVIEWED'),
|
||||
t.note = COALESCE(t.note, b.comment1),
|
||||
t.is_deleted = COALESCE(t.is_deleted, 0)
|
||||
WHERE
|
||||
t.is_deleted = 0
|
||||
AND b.bag_status >= 1
|
||||
AND b.sync_status = 'SYNC_NOT_READY'
|
||||
AND (
|
||||
m.is_initiator = 1
|
||||
OR m.id IS NULL
|
||||
);
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# 多阶段构建:减小最终镜像体积
|
||||
FROM python:3.11
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装运行时依赖
|
||||
RUN pip install -r requirements.txt -i https://mirrors.cloud.tencent.com/pypi/simple/
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 配置应用目录结构
|
||||
RUN mkdir -p /app/__pycache__ /app/logs
|
||||
|
||||
# 配置时区
|
||||
RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone
|
||||
|
||||
# 端口配置(默认生产 5222,可用于本地测试 5228)
|
||||
ARG APP_PORT=5222
|
||||
ENV APP_PORT=$APP_PORT
|
||||
|
||||
# 暴露Gunicorn监听的端口(对外提供服务)
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
# 环境变量配置
|
||||
# ENV FLASK_APP="app:app" \
|
||||
# FLASK_ENV="production" \
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PATH="/venv/bin:$PATH"
|
||||
|
||||
# Gunicorn启动命令(核心配置)
|
||||
CMD ["sh", "-c", "gunicorn \
|
||||
--bind 0.0.0.0:${APP_PORT} \
|
||||
--workers 14 \
|
||||
--timeout 60 \
|
||||
--keep-alive 5 \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
run:app"]
|
||||
94
app/__init__.py
Normal file
94
app/__init__.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_jwt_extended import JWTManager
|
||||
from flask_pymongo import PyMongo
|
||||
from .config import Config,config_map
|
||||
from flask_cors import CORS
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
mongo = PyMongo()
|
||||
jwt = JWTManager()
|
||||
|
||||
# def create_app():
|
||||
# app = Flask(__name__)
|
||||
# app.config.from_object(Config)
|
||||
|
||||
# db.init_app(app)
|
||||
# mongo.init_app(app)
|
||||
# # jwt.init_app(app)
|
||||
|
||||
# # 注册蓝图
|
||||
# from .blueprints.auth import auth_bp
|
||||
# from .blueprints.bag_data.routes import data_bp
|
||||
|
||||
|
||||
# app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||
# app.register_blueprint(data_bp, url_prefix='/api/data')
|
||||
|
||||
# return app
|
||||
|
||||
# 加载配置
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
# app.config.from_object(Config)
|
||||
config_name = os.environ.get('FLASK_ENV', 'default')
|
||||
app.config.from_object(config_map[config_name])
|
||||
|
||||
db.init_app(app)
|
||||
mongo.init_app(app)
|
||||
jwt.init_app(app)
|
||||
setup_logging(app) # 配置日志
|
||||
# 注册蓝图
|
||||
from .blueprints.auth.routes import auth_bp
|
||||
from .blueprints.bag_data.routes import data_bp
|
||||
from app.blueprints.data_display.routes import check_data
|
||||
from app.blueprints.data_factory.routes import data_factory_bp
|
||||
from app.blueprints.label_data.rotutes import label_bp
|
||||
from app.blueprints.remote_data.rotutes import remote_bp
|
||||
from app.blueprints.vlm.rotutes import vlm_bp
|
||||
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||
app.register_blueprint(data_bp, url_prefix='/api/data')
|
||||
app.register_blueprint(check_data, url_prefix='/api/inspect')
|
||||
app.register_blueprint(data_factory_bp, url_prefix='/api/factory')
|
||||
app.register_blueprint(label_bp, url_prefix='/api/label')
|
||||
app.register_blueprint(remote_bp, url_prefix='/api/remote')
|
||||
app.register_blueprint(vlm_bp, url_prefix='/api/vlm')
|
||||
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
return app
|
||||
|
||||
# 配置日志
|
||||
def setup_logging(app):
|
||||
# 确保日志目录存在
|
||||
os.makedirs(app.config['LOG_DIR'], exist_ok=True)
|
||||
|
||||
# 创建文件处理器
|
||||
file_handler = RotatingFileHandler(
|
||||
os.path.join(app.config['LOG_DIR'], 'app.log'),
|
||||
maxBytes=app.config['LOG_FILE_MAX_BYTES'],
|
||||
backupCount=app.config['LOG_FILE_BACKUP_COUNT']
|
||||
)
|
||||
|
||||
# 设置日志级别
|
||||
log_level = getattr(logging, app.config['LOG_LEVEL'])
|
||||
file_handler.setLevel(log_level)
|
||||
app.logger.setLevel(log_level)
|
||||
|
||||
# 设置日志格式
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# 添加处理器
|
||||
if not app.logger.handlers:
|
||||
app.logger.addHandler(file_handler)
|
||||
|
||||
# 记录启动信息
|
||||
app.logger.info(f'Application started with config')
|
||||
0
app/blueprints/__init__.py
Normal file
0
app/blueprints/__init__.py
Normal file
5
app/blueprints/auth/__init__.py
Normal file
5
app/blueprints/auth/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# from flask import Blueprint
|
||||
|
||||
# auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
# from . import routes
|
||||
522
app/blueprints/auth/routes.py
Normal file
522
app/blueprints/auth/routes.py
Normal 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
|
||||
})
|
||||
0
app/blueprints/auth/services.py
Normal file
0
app/blueprints/auth/services.py
Normal file
5
app/blueprints/bag_data/__init__.py
Normal file
5
app/blueprints/bag_data/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# from routes import Blueprint
|
||||
|
||||
# posts_bp = Blueprint('posts', __name__)
|
||||
|
||||
# from . import routes
|
||||
55
app/blueprints/bag_data/routes.py
Normal file
55
app/blueprints/bag_data/routes.py
Normal 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
|
||||
|
||||
|
||||
73
app/blueprints/bag_data/services.py
Normal file
73
app/blueprints/bag_data/services.py
Normal 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
|
||||
0
app/blueprints/data_display/__init__.py
Normal file
0
app/blueprints/data_display/__init__.py
Normal file
752
app/blueprints/data_display/routes.py
Normal file
752
app/blueprints/data_display/routes.py
Normal 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 过的 bag(outer 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
|
||||
0
app/blueprints/data_factory/__inin__.py
Normal file
0
app/blueprints/data_factory/__inin__.py
Normal file
2600
app/blueprints/data_factory/routes.py
Normal file
2600
app/blueprints/data_factory/routes.py
Normal file
File diff suppressed because it is too large
Load Diff
50
app/blueprints/data_factory/services.py
Normal file
50
app/blueprints/data_factory/services.py
Normal 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
|
||||
0
app/blueprints/label_data/__inin__.py
Normal file
0
app/blueprints/label_data/__inin__.py
Normal file
294
app/blueprints/label_data/rotutes.py
Normal file
294
app/blueprints/label_data/rotutes.py
Normal 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,
|
||||
)
|
||||
0
app/blueprints/remote_data/__init__.py
Normal file
0
app/blueprints/remote_data/__init__.py
Normal file
539
app/blueprints/remote_data/rotutes.py
Normal file
539
app/blueprints/remote_data/rotutes.py
Normal 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)}"})
|
||||
|
||||
|
||||
|
||||
5
app/blueprints/vlm/__init__.py
Normal file
5
app/blueprints/vlm/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# from flask import Blueprint
|
||||
|
||||
# auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
# from . import routes
|
||||
885
app/blueprints/vlm/rotutes.py
Normal file
885
app/blueprints/vlm/rotutes.py
Normal 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
|
||||
76
app/config.py
Normal file
76
app/config.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key-here')
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'mysql+pymysql://mb:mb12345@150.158.121.95:3306/rulebase?charset=utf8mb4')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False # 关闭警告
|
||||
|
||||
|
||||
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'bag_tag')
|
||||
JWT_ACCESS_TOKEN_EXPIRES = 7200 # 1小时
|
||||
|
||||
MONGO_URI = os.environ.get('MONGO_URI', 'mongodb://localhost:27017/vlm')
|
||||
|
||||
|
||||
# 基础配置
|
||||
class BaseConfig:
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'default-secret-key')
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL = 'INFO'
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
|
||||
LOG_FILE_MAX_BYTES = 1024 * 1024 * 10 # 10MB
|
||||
LOG_FILE_BACKUP_COUNT = 10
|
||||
|
||||
# MySQL 配置
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||
'DATABASE_URL',
|
||||
'mysql+pymysql://mb:mb12345@150.158.121.95:3306/rulebase?charset=utf8mb4'
|
||||
# 'mysql+pymysql://root:mb12345@10.0.220.217:3306/rulebase1?charset=utf8mb4'
|
||||
# 'mysql+pymysql://root:mb12345@10.204.22.142:3306/rulebase?charset=utf8mb4'
|
||||
)
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# MongoDB 配置
|
||||
MONGO_URI = os.environ.get('MONGO_URI', 'mongodb://150.158.121.95:27017/vlm')
|
||||
|
||||
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'bag_tag')
|
||||
JWT_ACCESS_TOKEN_EXPIRES = 14400 # 4小时
|
||||
|
||||
|
||||
# 开发环境配置
|
||||
class DevelopmentConfig(BaseConfig):
|
||||
DEBUG = True
|
||||
LOG_LEVEL = 'INFO'
|
||||
|
||||
# 生产环境配置
|
||||
class ProductionConfig(BaseConfig):
|
||||
# 生产环境从环境变量获取所有敏感信息
|
||||
# 生产环境专用配置
|
||||
# SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:mb12345@10.0.220.217:3306/rulebase?charset=utf8mb4'
|
||||
# MONGO_URI = 'mongodb://localhost:27017/vlm'
|
||||
|
||||
# # 可添加生产环境特有配置
|
||||
# SESSION_COOKIE_SECURE = True # 启用安全Cookie
|
||||
# PERMANENT_SESSION_LIFETIME = 3600 # 会话有效期1小时
|
||||
pass
|
||||
|
||||
# 测试环境配置
|
||||
class TestingConfig(BaseConfig):
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
||||
MONGODB_SETTINGS = {
|
||||
'db': 'test_database',
|
||||
'host': 'localhost',
|
||||
'port': 27017,
|
||||
}
|
||||
|
||||
# 配置映射
|
||||
config_map = {
|
||||
'development': DevelopmentConfig,
|
||||
# 'production': ProductionConfig,
|
||||
'testing': TestingConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
514
app/models.py
Normal file
514
app/models.py
Normal file
@@ -0,0 +1,514 @@
|
||||
from datetime import datetime
|
||||
import enum
|
||||
|
||||
from sqlalchemy import JSON, DateTime, Enum, ForeignKey, Index, 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="更新时间"
|
||||
)
|
||||
|
||||
# 自引用外键
|
||||
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]",
|
||||
)
|
||||
permissions = db.relationship(
|
||||
"Permission", secondary="role_permissions", back_populates="roles"
|
||||
)
|
||||
|
||||
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 Permission(db.Model):
|
||||
__tablename__ = "permissions"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
code = db.Column(
|
||||
db.String(100), unique=True, nullable=False, comment="权限码(资源:操作)"
|
||||
)
|
||||
description = db.Column(db.String(255), comment="权限描述")
|
||||
category = db.Column(db.String(50), comment="权限分类(如用户管理、系统设置)")
|
||||
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"))
|
||||
|
||||
# 关系定义
|
||||
roles = db.relationship(
|
||||
"Role", secondary="role_permissions", back_populates="permissions"
|
||||
)
|
||||
creator = db.relationship(
|
||||
"User", foreign_keys=[created_by], backref="created_permissions"
|
||||
)
|
||||
updater = db.relationship(
|
||||
"User", foreign_keys=[updated_by], backref="updated_permissions"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Permission {self.code}>"
|
||||
|
||||
|
||||
class RolePermission(db.Model):
|
||||
__tablename__ = "role_permissions"
|
||||
|
||||
role_id = db.Column(db.Integer, db.ForeignKey("roles.id"), primary_key=True)
|
||||
permission_id = db.Column(
|
||||
db.Integer, db.ForeignKey("permissions.id"), primary_key=True
|
||||
)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
|
||||
# 关系定义
|
||||
role = db.relationship(
|
||||
"Role", foreign_keys=[role_id], backref="permission_assignments"
|
||||
)
|
||||
permission = db.relationship(
|
||||
"Permission", foreign_keys=[permission_id], backref="role_assignments"
|
||||
)
|
||||
creator = db.relationship(
|
||||
"User", foreign_keys=[created_by], backref="created_role_permissions"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RolePermission role={self.role_id} perm={self.permission_id}>"
|
||||
|
||||
|
||||
class Fst(db.Model):
|
||||
__tablename__ = "fst"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255, collation="utf8mb4_unicode_ci"), nullable=False)
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey("fst.id"))
|
||||
update_time = db.Column(
|
||||
db.DateTime, default=func.current_timestamp(), nullable=False
|
||||
)
|
||||
reserved_json = db.Column(JSON)
|
||||
bag_sum = db.Column(db.Integer)
|
||||
level = db.Column(
|
||||
db.SmallInteger, comment="标签层级:0最高,1=一级, 2=二级, 3=三级, 4=四级"
|
||||
)
|
||||
name_cn = db.Column(db.String(255, collation="utf8mb4_unicode_ci"), comment="中文")
|
||||
annotation = db.Column(
|
||||
db.String(500, collation="utf8mb4_unicode_ci"), comment="标签注释"
|
||||
)
|
||||
|
||||
# 自引用关系(树形结构)
|
||||
children = db.relationship(
|
||||
"Fst", backref=db.backref("parent", remote_side=[id]), lazy="dynamic"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Fst {self.name}>"
|
||||
|
||||
|
||||
|
||||
class OperationHistory(db.Model):
|
||||
__tablename__ = "operation_history"
|
||||
|
||||
id = db.Column(
|
||||
db.BigInteger, primary_key=True, autoincrement=True, comment="主键ID"
|
||||
)
|
||||
user_id = db.Column(db.BigInteger, nullable=False, comment="用户ID")
|
||||
username = db.Column(db.String(50), nullable=False, comment="用户名")
|
||||
api_path = db.Column(db.String(255), nullable=False, comment="接口路径")
|
||||
http_method = db.Column(db.String(10), nullable=False, comment="HTTP方法")
|
||||
operation_time = db.Column(db.DateTime, default=datetime.now, comment="操作时间")
|
||||
request_params = db.Column(db.Text, comment="请求参数")
|
||||
response_code = db.Column(db.Integer, comment="响应状态码")
|
||||
ip_address = db.Column(db.String(45), comment="客户端IP地址")
|
||||
user_agent = db.Column(db.String(255), comment="用户代理信息")
|
||||
operation_result = db.Column(db.Integer, default=1, comment="1=成功,0=失败")
|
||||
error_message = db.Column(db.String(255), comment="错误信息")
|
||||
trace_id = db.Column(db.String(50), comment="请求追踪ID")
|
||||
|
||||
# 索引(在SQLAlchemy中通过__table_args__定义)
|
||||
__table_args__ = (
|
||||
db.Index("idx_user_id", "user_id"),
|
||||
db.Index("idx_operation_time", "operation_time"),
|
||||
db.Index("idx_api_path", "api_path"),
|
||||
db.Index("idx_user_operation", "user_id", "operation_time"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OperationHistory {self.api_path} by {self.username}>"
|
||||
|
||||
|
||||
# ===== 枚举类型定义 =====
|
||||
class BagStatus(enum.Enum):
|
||||
UNPROCESSED = "UNPROCESSED"
|
||||
PROCESSED_NOT_A_CANDIDATE = "PROCESSED_NOT_A_CANDIDATE"
|
||||
PROCESSED_NOT_REVIEWED = "PROCESSED_NOT_REVIEWED"
|
||||
REVIEW_IN_PROGRESS = "REVIEW_IN_PROGRESS"
|
||||
RULE_BASED_ACCEPTED = "RULE_BASED_ACCEPTED"
|
||||
MANUAL_OVERRIDE_ACCEPTED = "MANUAL_OVERRIDE_ACCEPTED"
|
||||
REVIEWED_BUT_INVALID = "REVIEWED_BUT_INVALID"
|
||||
|
||||
|
||||
class EventSource(enum.Enum):
|
||||
RULE = "RULE"
|
||||
MANUAL = "MANUAL"
|
||||
|
||||
|
||||
class VerdictStatus(enum.Enum):
|
||||
ACCEPTED = "ACCEPTED"
|
||||
OVERRIDDEN = "OVERRIDDEN"
|
||||
|
||||
|
||||
class OperationResult(enum.Enum):
|
||||
SUCCESS = 1
|
||||
FAILURE = 0
|
||||
|
||||
|
||||
class QaStatus(enum.Enum):
|
||||
QA_NOT_REVIEWED = "QA_NOT_REVIEWED"
|
||||
QA_PASSED = "QA_PASSED"
|
||||
QA_MODIFY = "QA_MODIFY"
|
||||
QA_INVALID = "QA_INVALID"
|
||||
|
||||
|
||||
class SyncStatus(enum.Enum):
|
||||
SYNC_NOT_READY = "SYNC_NOT_READY"
|
||||
SYNCED = "SYNCED"
|
||||
|
||||
|
||||
# ===== 核心数据模型 =====
|
||||
|
||||
|
||||
class RuleVersions(db.Model):
|
||||
"""规则版本表"""
|
||||
|
||||
__tablename__ = "rule_versions"
|
||||
|
||||
rule_version_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
ruleset_name = db.Column(db.String(128), nullable=False)
|
||||
version_str = db.Column(db.String(32), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint("ruleset_name", "version_str", name="uk_ruleset_version"),
|
||||
)
|
||||
|
||||
|
||||
class Reviewers(db.Model):
|
||||
"""审核人员表"""
|
||||
|
||||
__tablename__ = "reviewers"
|
||||
|
||||
reviewer_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
login_name = db.Column(db.String(64), unique=True, nullable=False)
|
||||
display_name = db.Column(db.String(128))
|
||||
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
|
||||
|
||||
|
||||
# ===== 业务数据模型 =====
|
||||
class BagFile(db.Model):
|
||||
"""BAG文件主表"""
|
||||
|
||||
__tablename__ = "bag_file"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
file_name = db.Column(db.String(255), nullable=False, comment="bag名称")
|
||||
event = db.Column(db.String(255), comment="event")
|
||||
file_path = db.Column(db.String(500))
|
||||
capture_datetime = db.Column(
|
||||
db.DateTime, nullable=False, comment="采集时间(含时分秒)"
|
||||
)
|
||||
vehicle_status = db.Column(JSON, comment="车辆状态JSON")
|
||||
user_id = db.Column(db.Integer, ForeignKey("users.id"), comment="操作用户id")
|
||||
create_time = db.Column(
|
||||
db.DateTime, default=db.func.current_timestamp(), comment="标签的创建时间"
|
||||
)
|
||||
update_time = db.Column(
|
||||
db.DateTime,
|
||||
default=db.func.current_timestamp(),
|
||||
onupdate=db.func.current_timestamp(),
|
||||
comment="标签的更新时间",
|
||||
)
|
||||
car_state = db.Column(db.SmallInteger, comment="车辆状态: 0=静止, 1=运动")
|
||||
frame_url = db.Column(db.String(500), comment="图片url")
|
||||
video_url = db.Column(db.String(500), comment="视频url")
|
||||
level1_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="一级标签id")
|
||||
level2_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="二级标签id")
|
||||
level3_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="三级标签id")
|
||||
level4_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="四级标签id")
|
||||
batch_name = db.Column(db.String(255), comment="批次名称")
|
||||
status = db.Column(
|
||||
Enum(BagStatus),
|
||||
nullable=False,
|
||||
default=BagStatus.PROCESSED_NOT_REVIEWED,
|
||||
comment="处理的状态",
|
||||
)
|
||||
last_time = db.Column(db.DateTime, comment="最后处理的时间")
|
||||
last_rule_version_id = db.Column(
|
||||
db.Integer,
|
||||
ForeignKey("rule_versions.rule_version_id"),
|
||||
comment="rule_versions的外键",
|
||||
)
|
||||
front_starttime = db.Column(db.DateTime, comment="前摄像头的开始时间")
|
||||
front_endtime = db.Column(db.DateTime, comment="前摄像头的结束时间")
|
||||
case_type = db.Column(db.Integer, comment="1简单场景;2复杂场景")
|
||||
comment1 = db.Column(db.String(255), comment="注释1")
|
||||
comment2 = db.Column(db.String(255), comment="注释2")
|
||||
comment3 = db.Column(db.String(255), comment="注释3")
|
||||
bag_status = db.Column(db.Integer, comment="标签状态,0开始,1更新")
|
||||
|
||||
# 新增字段
|
||||
qa_status = db.Column(
|
||||
Enum(QaStatus),
|
||||
nullable=False,
|
||||
default=QaStatus.QA_NOT_REVIEWED,
|
||||
comment="质检的状态",
|
||||
)
|
||||
sync_status = db.Column(
|
||||
Enum(SyncStatus),
|
||||
nullable=False,
|
||||
default=SyncStatus.SYNC_NOT_READY,
|
||||
comment="同步的状态",
|
||||
)
|
||||
fst_version = db.Column(db.String(255), comment="fst版本")
|
||||
front_start_sec = db.Column(db.Integer, comment="前摄的开始秒")
|
||||
front_end_sec = db.Column(db.Integer, comment="前摄的结束秒")
|
||||
|
||||
bag_update_time = db.Column(
|
||||
db.DateTime, default=db.func.current_timestamp(), comment="bag的更新时间"
|
||||
)
|
||||
|
||||
qa_confirm_time = db.Column(
|
||||
db.DateTime, default=db.func.current_timestamp(), comment="qa的确认时间"
|
||||
)
|
||||
|
||||
db_time = db.Column(
|
||||
db.DateTime,
|
||||
default=db.func.current_timestamp(),
|
||||
comment="更新到远程数据库的时间",
|
||||
)
|
||||
|
||||
qa_id = db.Column(db.Integer, ForeignKey("users.id"),comment="操作用户id")
|
||||
|
||||
high_speed = db.Column(db.Integer, comment="高速,1是,0否")
|
||||
urban = db.Column(db.Integer, comment="城区,1是,0否")
|
||||
parking = db.Column(db.Integer, comment="停车,1是,0否")
|
||||
# 关系定义
|
||||
user = db.relationship("User", foreign_keys=[user_id],backref="bag_files")
|
||||
rule_version = db.relationship("RuleVersions", backref="bag_files")
|
||||
level1_tag = db.relationship("Fst", foreign_keys=[level1_tag_id])
|
||||
level2_tag = db.relationship("Fst", foreign_keys=[level2_tag_id])
|
||||
level3_tag = db.relationship("Fst", foreign_keys=[level3_tag_id])
|
||||
level4_tag = db.relationship("Fst", foreign_keys=[level4_tag_id])
|
||||
qa_user = db.relationship('User', foreign_keys=[qa_id], backref='qa_bag_files')
|
||||
|
||||
|
||||
__table_args__ = (
|
||||
db.Index("idx_capture_time", "capture_datetime"),
|
||||
db.Index("status", "status"),
|
||||
db.Index("idx_bag_file_name", "file_name"),
|
||||
)
|
||||
|
||||
|
||||
class TaggingEvents(db.Model):
|
||||
"""标签事件记录表"""
|
||||
|
||||
__tablename__ = "tagging_events"
|
||||
|
||||
event_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
bag_id = db.Column(
|
||||
db.Integer, ForeignKey("bag_file.id"), nullable=False, comment="bag的id"
|
||||
)
|
||||
source = db.Column(Enum(EventSource), nullable=False, comment="来源")
|
||||
rule_version_id = db.Column(
|
||||
db.Integer, ForeignKey("rule_versions.rule_version_id"), comment="rule版本id"
|
||||
)
|
||||
# reviewer_id = db.Column(db.Integer, ForeignKey('reviewers.reviewer_id'),
|
||||
# comment='审核人id')
|
||||
reviewer_id = db.Column(db.Integer, ForeignKey("users.id"), comment="审核人id")
|
||||
verdict = db.Column(Enum(VerdictStatus), comment="结论")
|
||||
ts_event = db.Column(
|
||||
db.DateTime, default=db.func.current_timestamp(), comment="当前时间"
|
||||
)
|
||||
level1_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="一级标签id")
|
||||
level2_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="二级标签id")
|
||||
level3_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="三级标签id")
|
||||
level4_tag_id = db.Column(db.Integer, ForeignKey("fst.id"), comment="四级标签id")
|
||||
case_type = db.Column(db.Integer, comment="1简单场景;2复杂场景")
|
||||
front_starttime = db.Column(db.DateTime, comment="前摄像头开始时间")
|
||||
front_endtime = db.Column(db.DateTime, comment="前摄像头结束时间")
|
||||
front_start_sec = db.Column(db.Integer, comment="前摄的开始秒")
|
||||
front_end_sec = db.Column(db.Integer, comment="前摄的结束秒")
|
||||
high_speed = db.Column(db.Integer, comment="高速,1是,0否")
|
||||
urban = db.Column(db.Integer, comment="城区,1是,0否")
|
||||
parking = db.Column(db.Integer, comment="停车,1是,0否")
|
||||
qa_status = db.Column(
|
||||
Enum(QaStatus),
|
||||
nullable=False,
|
||||
default=QaStatus.QA_NOT_REVIEWED,
|
||||
comment="质检状态",
|
||||
)
|
||||
is_deleted = db.Column(
|
||||
db.Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="soft delete flag",
|
||||
)
|
||||
note = db.Column(db.Text, comment="备注")
|
||||
|
||||
# 关系定义
|
||||
bag = db.relationship("BagFile", backref="tagging_events")
|
||||
rule_version = db.relationship("RuleVersions")
|
||||
reviewer = db.relationship("User")
|
||||
level1_tag = db.relationship("Fst", foreign_keys=[level1_tag_id])
|
||||
level2_tag = db.relationship("Fst", foreign_keys=[level2_tag_id])
|
||||
level3_tag = db.relationship("Fst", foreign_keys=[level3_tag_id])
|
||||
level4_tag = db.relationship("Fst", foreign_keys=[level4_tag_id])
|
||||
|
||||
__table_args__ = (db.Index("idx_evt_bag_ts", "bag_id", "ts_event"),)
|
||||
|
||||
|
||||
class VlmFilter(db.Model):
|
||||
__tablename__ = 'vlm_filter'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='主键')
|
||||
bag_name = db.Column(db.String(255), nullable=True, comment='bag的名称')
|
||||
level1_tag_id = db.Column(db.Integer, db.ForeignKey('fst.id', ondelete='CASCADE', onupdate='CASCADE'),
|
||||
nullable=True, index=True, comment='一级标签id')
|
||||
level2_tag_id = db.Column(db.Integer, db.ForeignKey('fst.id', ondelete='CASCADE', onupdate='CASCADE'),
|
||||
nullable=True, index=True, comment='二级标签id')
|
||||
level3_tag_id = db.Column(db.Integer, db.ForeignKey('fst.id', ondelete='CASCADE', onupdate='CASCADE'),
|
||||
nullable=True, index=True, comment='三级标签id')
|
||||
level4_tag_id = db.Column(db.Integer, db.ForeignKey('fst.id', ondelete='CASCADE', onupdate='CASCADE'),
|
||||
nullable=True, index=True, comment='四级标签id')
|
||||
video_url = db.Column(db.String(255), nullable=True, comment='视频路径')
|
||||
comment = db.Column(db.String(255), nullable=True, comment='备注')
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE'),
|
||||
nullable=True, index=True, comment='写入的用户')
|
||||
collection_time = db.Column(db.DateTime, nullable=True, comment='标签采集时间')
|
||||
create_time = db.Column(db.DateTime, nullable=True, comment='写入的时间')
|
||||
init_label = db.Column(db.Integer, nullable=True, comment='入库的状态,0是标注,1是质检')
|
||||
status = db.Column(db.Integer, nullable=True, comment='入库的状态,0是标注,1是质检')
|
||||
|
||||
# 可以根据需要添加关系引用
|
||||
level1_tag = db.relationship('Fst', foreign_keys=[level1_tag_id])
|
||||
level2_tag = db.relationship('Fst', foreign_keys=[level2_tag_id])
|
||||
level3_tag = db.relationship('Fst', foreign_keys=[level3_tag_id])
|
||||
level4_tag = db.relationship('Fst', foreign_keys=[level4_tag_id])
|
||||
user = db.relationship('User', foreign_keys=[user_id])
|
||||
|
||||
class BagMergeRecord(db.Model):
|
||||
__tablename__ = "bag_merge_record"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='主键')
|
||||
joined_name = db.Column(
|
||||
db.String(255),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="合并后 bag 名称(joined_name)"
|
||||
)
|
||||
src_name = db.Column(
|
||||
db.String(255),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="参与合并的原始 bag 名称"
|
||||
)
|
||||
is_initiator = db.Column(
|
||||
db.Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="是否为本次合并操作的主 bag(发起合并的那个 src)",
|
||||
)
|
||||
create_time = db.Column(
|
||||
db.DateTime,
|
||||
default=db.func.current_timestamp(),
|
||||
comment="记录创建时间",
|
||||
)
|
||||
created_by = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("users.id"),
|
||||
index=True,
|
||||
comment="操作用户 id",
|
||||
)
|
||||
|
||||
# 可选:反向关系,如果有需要
|
||||
creator = db.relationship("User", backref="bag_merge_records")
|
||||
|
||||
__table_args__ = (
|
||||
# 如果希望避免“同一个 joined_name + src_name 被插入多次”,可以加一个联合唯一约束:
|
||||
# db.UniqueConstraint("joined_name", "src_name", name="uk_joined_src"),
|
||||
)
|
||||
0
app/schemas.py
Normal file
0
app/schemas.py
Normal file
63
app/utils/bagname.py
Normal file
63
app/utils/bagname.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple
|
||||
|
||||
# 你 COS 的公共前缀可以做成默认参数,便于不同环境覆盖
|
||||
DEFAULT_COS_PREFIX = "https://data-miningc01-1318950322.cos.ap-shanghai-adc.myqcloud.com/"
|
||||
|
||||
def extract_datetime_from_filename(
|
||||
bag_filename: str,
|
||||
cos_prefix: str = DEFAULT_COS_PREFIX,
|
||||
) -> Tuple[str, Dict[str, str]]:
|
||||
"""
|
||||
从 .bag 文件名中提取采集时间,并生成对应的多视角视频 URL。
|
||||
|
||||
期望文件名包含片段:_{YYYYMMDD}-{HHMMSS}_ 或以 .bag 结尾,例如:
|
||||
PL162802_event_all_time_event_20250416-193015_0.bag
|
||||
PL123456_event_all_time_event_20250828-221018.bag
|
||||
xx_20250524-180926_.bag
|
||||
xx_20250524-180926_3.bag
|
||||
|
||||
返回:
|
||||
(datetime_full_str, video_urls)
|
||||
- datetime_full_str: 'YYYY-MM-DD HH:MM:SS'
|
||||
- video_urls: COS 上该 bag 的多视角视频 URL 字典
|
||||
|
||||
异常:
|
||||
ValueError: 当文件名不匹配预期格式时抛出
|
||||
"""
|
||||
# 提取日期时间
|
||||
match = re.search(r"_(\d{8})-(\d{6})(?:_|\.|$)", bag_filename)
|
||||
|
||||
if not match:
|
||||
raise ValueError(f"文件名格式错误,无法提取时间信息:{bag_filename}")
|
||||
|
||||
date_part, time_part = match.groups()
|
||||
year, month, day = date_part[:4], date_part[4:6], date_part[6:8]
|
||||
hour, minute, second = time_part[:2], time_part[2:4], time_part[4:6]
|
||||
|
||||
datetime_full = f"{year}-{month}-{day} {hour}:{minute}:{second}"
|
||||
|
||||
|
||||
print(f'datetime_full:{datetime_full}')
|
||||
# 生成视频路径(与现有逻辑一致)
|
||||
# 去掉 .bag 后缀,再补上 " - {View}.mp4"
|
||||
base = bag_filename[:-4] if bag_filename.endswith(".bag") else bag_filename
|
||||
view_map = {
|
||||
"wide": "Wide",
|
||||
"left": "Left",
|
||||
"right": "Right",
|
||||
"rear_left": "Rear Left",
|
||||
"rear_right": "Rear Right",
|
||||
"rear": "Rear",
|
||||
}
|
||||
video_urls = {}
|
||||
for key, label in view_map.items():
|
||||
video_final_path = f"{base}- {label}.mp4"
|
||||
# 目录层级:momenta/videos/YYYY/MM/DD/{文件名 - {View}.mp4}
|
||||
video_key = f"momenta/videos/{year}/{month}/{day}/{video_final_path}"
|
||||
video_urls[key] = f"{cos_prefix}{video_key}"
|
||||
|
||||
print(f'v{video_urls}')
|
||||
|
||||
return datetime_full, video_urls
|
||||
3301
app/utils/driving_tree.py
Normal file
3301
app/utils/driving_tree.py
Normal file
File diff suppressed because it is too large
Load Diff
120
app/utils/fst_tree.py
Normal file
120
app/utils/fst_tree.py
Normal file
@@ -0,0 +1,120 @@
|
||||
def build_tree(records):
|
||||
"""
|
||||
构建带层级属性的树形结构
|
||||
返回: 根节点字典 {id, name, children: [...], level}
|
||||
"""
|
||||
nodes = {}
|
||||
root_candidates = set() # 存储可能的根节点ID [2,7](@ref)
|
||||
|
||||
# 创建所有节点(暂不设置level)
|
||||
for id_str, name, parent_id in records:
|
||||
try:
|
||||
node_id = int(id_str)
|
||||
parent_id_val = int(parent_id) if parent_id and str(parent_id).isdigit() else None
|
||||
|
||||
nodes[node_id] = {
|
||||
'id': node_id,
|
||||
'label': name,
|
||||
'children': [] # level属性稍后通过递归设置
|
||||
}
|
||||
|
||||
# 标记根节点候选(父节点不存在或无效)
|
||||
if parent_id_val is None or parent_id_val not in nodes:
|
||||
root_candidates.add(node_id)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# 构建父子关系
|
||||
for id_str, _, parent_id in records:
|
||||
try:
|
||||
node_id = int(id_str)
|
||||
parent_id_val = int(parent_id) if parent_id and str(parent_id).isdigit() else None
|
||||
|
||||
# 连接父子节点
|
||||
if parent_id_val in nodes and node_id in nodes:
|
||||
nodes[parent_id_val]['children'].append(nodes[node_id])
|
||||
# 从根候选移除(有父节点说明不是根)
|
||||
if node_id in root_candidates:
|
||||
root_candidates.remove(node_id)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# ============ 新增核心逻辑:递归设置层级属性 ============
|
||||
def set_node_level(node, current_level):
|
||||
"""递归设置节点层级属性(DFS深度优先遍历)[3,7](@ref)"""
|
||||
node['level'] = current_level # 设置当前节点层级
|
||||
for child in node['children']:
|
||||
set_node_level(child, current_level + 1) # 子节点层级+1
|
||||
|
||||
# 为所有根节点设置层级
|
||||
for root_id in root_candidates:
|
||||
set_node_level(nodes[root_id], 0) # 根节点层级从0开始
|
||||
|
||||
# 返回第一个根节点(或None)
|
||||
return nodes[next(iter(root_candidates))] if root_candidates else None
|
||||
|
||||
|
||||
def build_sub_tree(records, target_parent_id=None):
|
||||
"""
|
||||
构建带层级属性的树形结构
|
||||
|
||||
参数:
|
||||
records: 记录列表,格式为[(id_str, name, parent_id)]
|
||||
target_parent_id: 指定的一级标签ID(作为子树根节点)
|
||||
|
||||
返回:
|
||||
带level属性的子树或完整树结构
|
||||
"""
|
||||
nodes = {}
|
||||
root_candidates = set() # 存储可能的根节点ID
|
||||
|
||||
# 创建所有节点(暂不设置level)
|
||||
for id_str, name, parent_id in records:
|
||||
try:
|
||||
node_id = int(id_str)
|
||||
parent_id_val = int(parent_id) if parent_id and str(parent_id).isdigit() else None
|
||||
|
||||
nodes[node_id] = {
|
||||
'id': node_id,
|
||||
'label': name,
|
||||
'children': [] # level属性稍后通过递归设置
|
||||
}
|
||||
|
||||
if parent_id_val is None or parent_id_val not in nodes:
|
||||
root_candidates.add(node_id)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# 构建父子关系
|
||||
for id_str, _, parent_id in records:
|
||||
try:
|
||||
node_id = int(id_str)
|
||||
parent_id_val = int(parent_id) if parent_id and str(parent_id).isdigit() else None
|
||||
|
||||
if parent_id_val in nodes and node_id in nodes:
|
||||
nodes[parent_id_val]['children'].append(nodes[node_id])
|
||||
if node_id in root_candidates:
|
||||
root_candidates.remove(node_id)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# ============ 新增核心逻辑:递归设置层级属性 ============
|
||||
def set_node_level(node, current_level):
|
||||
"""递归设置节点层级属性(DFS深度优先遍历)"""
|
||||
node['level'] = current_level # 设置当前节点层级
|
||||
for child in node['children']:
|
||||
set_node_level(child, current_level + 1) # 子节点层级+1[8](@ref)
|
||||
|
||||
# 场景1:处理子树
|
||||
if target_parent_id is not None:
|
||||
target_id = int(target_parent_id)
|
||||
subtree_root = nodes.get(target_id)
|
||||
if subtree_root:
|
||||
set_node_level(subtree_root, 1) # 子树根节点从0开始
|
||||
return subtree_root
|
||||
|
||||
# 场景2/3:处理完整树或森林
|
||||
for root_id in root_candidates:
|
||||
set_node_level(nodes[root_id], 0) # 每个根节点从0开始[7](@ref)
|
||||
|
||||
return nodes[next(iter(root_candidates))] if root_candidates else nodes
|
||||
29
app/utils/get_vlm_token.py
Normal file
29
app/utils/get_vlm_token.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import requests
|
||||
|
||||
|
||||
class Demo:
|
||||
def __init__(self):
|
||||
self.api_key = "bX7pK9mR2nL4qF6s" # 用户的 API Key
|
||||
self.api_secret = "eJ8wT3hY5cA2vN9uZ1xD7fG4" # 用户的 API Secret
|
||||
self.host = "http://10.0.220.110" # 接口主地址
|
||||
self.headers = {"Content-Type": "application/json", "Authorization": ""}
|
||||
|
||||
# 获取认证token
|
||||
def getAuthToken(self):
|
||||
# print("开始登录")
|
||||
url = f"{self.host}/api/openapi/authenticate"
|
||||
data = {"apiKey": self.api_key, "apiSecret": self.api_secret}
|
||||
try:
|
||||
response = requests.post(url, json=data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result["success"]:
|
||||
self.headers["Authorization"] = f"Bearer {result['data']['token']}"
|
||||
print("登录vlm成功")
|
||||
return result["data"]["token"]
|
||||
else:
|
||||
raise Exception(f"登录失败: {result['message']}")
|
||||
except requests.RequestException as e:
|
||||
print(f"登录请求失败: {e}")
|
||||
raise
|
||||
104
app/utils/log_record.py
Normal file
104
app/utils/log_record.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from functools import wraps
|
||||
from flask import request, g
|
||||
import json
|
||||
|
||||
from flask_jwt_extended import current_user
|
||||
from app.models import OperationHistory, User
|
||||
|
||||
from app import db,jwt
|
||||
|
||||
def log_operation(func):
|
||||
@wraps(func)
|
||||
def decorated(*args, **kwargs):
|
||||
log_entry = OperationHistory(
|
||||
api_path=request.path,
|
||||
http_method=request.method,
|
||||
ip_address=request.remote_addr,
|
||||
request_params=None
|
||||
# 用户信息暂留空
|
||||
)
|
||||
# 记录GET参数
|
||||
if request.method == "GET":
|
||||
log_entry.request_params = request.args.to_dict()
|
||||
|
||||
# 记录POST参数(支持JSON/表单/原始数据)
|
||||
elif request.method == "POST":
|
||||
if request.is_json:
|
||||
res=request.get_json(silent=True) or None
|
||||
json_str = json.dumps(res,ensure_ascii=False)
|
||||
log_entry.request_params = json_str
|
||||
elif request.form:
|
||||
json_str = json.dumps(request.form.to_dict(),ensure_ascii=False)
|
||||
log_entry.request_params =json_str
|
||||
elif request.data:
|
||||
json_str = json.dumps({"raw_data": request.data.decode('utf-8')[:500]},ensure_ascii=False)
|
||||
# 原始数据截取前500字符
|
||||
log_entry.request_params =json_str
|
||||
|
||||
|
||||
try:
|
||||
response = func(*args, **kwargs) # 执行原函数(触发 JWT 验证)
|
||||
log_entry.user_id = current_user.id # 此时 current_user 已就绪
|
||||
log_entry.username = current_user.username
|
||||
log_entry.response_code = response.status_code
|
||||
return response
|
||||
except Exception as e:
|
||||
log_entry.error_message = str(e)
|
||||
log_entry.operation_result = 0
|
||||
raise
|
||||
finally:
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
return decorated
|
||||
|
||||
|
||||
# def log_operation(func):
|
||||
# @wraps(func)
|
||||
# def decorated(*args, **kwargs):
|
||||
# # 初始化日志对象(示例结构)
|
||||
# log_entry = {
|
||||
# "path": request.path,
|
||||
# "method": request.method,
|
||||
# "ip": request.remote_addr,
|
||||
# "params": None # 待填充
|
||||
# }
|
||||
|
||||
# try:
|
||||
# # ---- 核心:记录请求参数 ----
|
||||
# if request.method == "GET":
|
||||
# params = request.args.to_dict()
|
||||
# log_entry["params"] = filter_sensitive_data(params)
|
||||
|
||||
# elif request.method == "POST":
|
||||
# if request.is_json:
|
||||
# json_data = request.get_json(silent=True) or {}
|
||||
# log_entry["params"] = filter_sensitive_data(json_data)
|
||||
# elif request.form:
|
||||
# form_data = request.form.to_dict()
|
||||
# log_entry["params"] = filter_sensitive_data(form_data)
|
||||
# elif request.files:
|
||||
# log_entry["files"] = [f.filename for f in request.files.values()]
|
||||
|
||||
# # ---- 执行原函数 ----
|
||||
# start_time = time()
|
||||
# response = func(*args, **kwargs)
|
||||
# duration = time() - start_time
|
||||
|
||||
# # ---- 记录响应 ----
|
||||
# log_entry["status"] = response.status_code
|
||||
# log_entry["duration"] = f"{duration:.3f}s"
|
||||
# return response
|
||||
|
||||
# except Exception as e:
|
||||
# log_entry["error"] = str(e)
|
||||
# raise
|
||||
# finally:
|
||||
# # 实际存储到数据库/文件(此处打印示例)
|
||||
# current_app.logger.info(json.dumps(log_entry, ensure_ascii=False))
|
||||
|
||||
# return decorated
|
||||
|
||||
# JWT 用户回调
|
||||
@jwt.user_lookup_loader
|
||||
def load_user(jwt_header, jwt_data):
|
||||
return User.query.get(jwt_data["sub"])
|
||||
38
app/utils/password_hasher.py
Normal file
38
app/utils/password_hasher.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import hashlib
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""
|
||||
使用 MD5 对密码进行加密(不推荐用于生产环境)
|
||||
|
||||
参数:
|
||||
password (str): 明文密码
|
||||
|
||||
返回:
|
||||
str: MD5 加密后的密码(32 位十六进制字符串)
|
||||
"""
|
||||
# 创建 MD5 哈希对象
|
||||
md5 = hashlib.md5()
|
||||
|
||||
# 更新哈希对象内容(需将字符串编码为 bytes)
|
||||
md5.update(password.encode('utf-8'))
|
||||
|
||||
# 获取十六进制表示的哈希值
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
def check_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
验证密码是否匹配(MD5 版本)
|
||||
|
||||
参数:
|
||||
plain_password (str): 明文密码
|
||||
hashed_password (str): MD5 加密后的密码
|
||||
|
||||
返回:
|
||||
bool: 密码是否匹配
|
||||
"""
|
||||
# 计算明文密码的 MD5 值
|
||||
plain_password_md5 = hash_password(plain_password)
|
||||
|
||||
# 比较两个 MD5 值是否相同
|
||||
return plain_password_md5 == hashed_password
|
||||
0
app/utils/remote_db.py
Normal file
0
app/utils/remote_db.py
Normal file
18
app/utils/response_dict.py
Normal file
18
app/utils/response_dict.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
response={
|
||||
'code':0,
|
||||
'data':{},
|
||||
'error':'',
|
||||
'message':'',
|
||||
|
||||
}
|
||||
|
||||
|
||||
def api_response(code=200, message="success", data=None):
|
||||
return jsonify({
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data
|
||||
}), code
|
||||
47
build-docker-image.sh
Normal file
47
build-docker-image.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
|
||||
ENV_NAME="${1:-test}"
|
||||
IMAGE_REPO="${2:-}"
|
||||
CUSTOM_TAG="${3:-}"
|
||||
DOCKERFILE="${SCRIPT_DIR}/Dockerfile"
|
||||
|
||||
if [[ "${IMAGE_REPO}" == *":"* ]]; then
|
||||
echo "IMAGE_NAME should not include a tag. Pass the tag as the third argument." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${ENV_NAME}" in
|
||||
prod)
|
||||
APP_PORT=5222
|
||||
DEFAULT_IMAGE_REPO="fst-app-release"
|
||||
;;
|
||||
test)
|
||||
APP_PORT=5228
|
||||
DEFAULT_IMAGE_REPO="fst-app-test"
|
||||
;;
|
||||
*)
|
||||
echo "ENV_NAME must be 'prod' or 'test' (got: ${ENV_NAME})." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "${IMAGE_REPO}" ]]; then
|
||||
IMAGE_REPO="${DEFAULT_IMAGE_REPO}"
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker not found. Please install Docker first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_TIME=$(date +%Y%m%d%H%M)
|
||||
TAG_SUFFIX="${CUSTOM_TAG:-${BUILD_TIME}}"
|
||||
IMAGE_NAME="${IMAGE_REPO}:${TAG_SUFFIX}"
|
||||
|
||||
echo "Building image '${IMAGE_NAME}' with APP_PORT=${APP_PORT}"
|
||||
docker build "${SCRIPT_DIR}" -f "${DOCKERFILE}" -t "${IMAGE_NAME}" --build-arg APP_PORT="${APP_PORT}"
|
||||
|
||||
echo "Build complete."
|
||||
echo "Run example: docker run -d -p ${APP_PORT}:${APP_PORT} --name ${IMAGE_NAME} ${IMAGE_NAME}"
|
||||
31
dev-debug.sh
Normal file
31
dev-debug.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
|
||||
PORT="${1:-5228}"
|
||||
|
||||
VENV_BIN="${SCRIPT_DIR}/venv/bin"
|
||||
PYTHON="python3"
|
||||
FLASK="flask"
|
||||
|
||||
if [[ -x "${VENV_BIN}/python" ]]; then
|
||||
PYTHON="${VENV_BIN}/python"
|
||||
fi
|
||||
|
||||
if [[ -x "${VENV_BIN}/flask" ]]; then
|
||||
FLASK="${VENV_BIN}/flask"
|
||||
fi
|
||||
|
||||
if [[ ! -x "${PYTHON}" ]]; then
|
||||
echo "Python not found. Create or activate a venv under ${SCRIPT_DIR}/venv first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PYTHONUNBUFFERED=1
|
||||
export FLASK_ENV=development
|
||||
|
||||
if [[ -x "${FLASK}" ]]; then
|
||||
exec "${FLASK}" --app run:app run --host 0.0.0.0 --port "${PORT}" --debug
|
||||
fi
|
||||
|
||||
exec "${PYTHON}" -m flask --app run:app run --host 0.0.0.0 --port "${PORT}" --debug
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
1198
rulebase.sql
Normal file
1198
rulebase.sql
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user