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

4
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,4 @@
# 默认忽略的文件
/shelf/
/workspace.xml
.idea

42
20251118_update.sql Normal file
View 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
View 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 '备注';

View 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
View 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
View 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')

View File

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

View File

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

View File

View File

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

View File

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

View File

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

76
app/config.py Normal file
View 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
View 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
View File

63
app/utils/bagname.py Normal file
View 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

File diff suppressed because it is too large Load Diff

120
app/utils/fst_tree.py Normal file
View 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

View 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
View 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"])

View 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
View File

View 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
View 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
View 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

Binary file not shown.

1198
rulebase.sql Normal file

File diff suppressed because it is too large Load Diff

9
run.py Normal file
View File

@@ -0,0 +1,9 @@
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True,port=5000)
# app.run(host='0.0.0.0',port=5222,debug=False)
# app.run(host='0.0.0.0',port=5000,debug=False)