提交
This commit is contained in:
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
.idea
|
||||||
@@ -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);
|
||||||
@@ -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 '备注';
|
||||||
@@ -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
@@ -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"]
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import os
|
||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_jwt_extended import JWTManager
|
||||||
|
from flask_pymongo import PyMongo
|
||||||
|
from .config import Config,config_map
|
||||||
|
from flask_cors import CORS
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
mongo = PyMongo()
|
||||||
|
jwt = JWTManager()
|
||||||
|
|
||||||
|
# def create_app():
|
||||||
|
# app = Flask(__name__)
|
||||||
|
# app.config.from_object(Config)
|
||||||
|
|
||||||
|
# db.init_app(app)
|
||||||
|
# mongo.init_app(app)
|
||||||
|
# # jwt.init_app(app)
|
||||||
|
|
||||||
|
# # 注册蓝图
|
||||||
|
# from .blueprints.auth import auth_bp
|
||||||
|
# from .blueprints.bag_data.routes import data_bp
|
||||||
|
|
||||||
|
|
||||||
|
# app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||||
|
# app.register_blueprint(data_bp, url_prefix='/api/data')
|
||||||
|
|
||||||
|
# return app
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
# app.config.from_object(Config)
|
||||||
|
config_name = os.environ.get('FLASK_ENV', 'default')
|
||||||
|
app.config.from_object(config_map[config_name])
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
mongo.init_app(app)
|
||||||
|
jwt.init_app(app)
|
||||||
|
setup_logging(app) # 配置日志
|
||||||
|
# 注册蓝图
|
||||||
|
from .blueprints.auth.routes import auth_bp
|
||||||
|
from .blueprints.bag_data.routes import data_bp
|
||||||
|
from app.blueprints.data_display.routes import check_data
|
||||||
|
from app.blueprints.data_factory.routes import data_factory_bp
|
||||||
|
from app.blueprints.label_data.rotutes import label_bp
|
||||||
|
from app.blueprints.remote_data.rotutes import remote_bp
|
||||||
|
from app.blueprints.vlm.rotutes import vlm_bp
|
||||||
|
|
||||||
|
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||||
|
app.register_blueprint(data_bp, url_prefix='/api/data')
|
||||||
|
app.register_blueprint(check_data, url_prefix='/api/inspect')
|
||||||
|
app.register_blueprint(data_factory_bp, url_prefix='/api/factory')
|
||||||
|
app.register_blueprint(label_bp, url_prefix='/api/label')
|
||||||
|
app.register_blueprint(remote_bp, url_prefix='/api/remote')
|
||||||
|
app.register_blueprint(vlm_bp, url_prefix='/api/vlm')
|
||||||
|
|
||||||
|
CORS(app, supports_credentials=True)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
def setup_logging(app):
|
||||||
|
# 确保日志目录存在
|
||||||
|
os.makedirs(app.config['LOG_DIR'], exist_ok=True)
|
||||||
|
|
||||||
|
# 创建文件处理器
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
os.path.join(app.config['LOG_DIR'], 'app.log'),
|
||||||
|
maxBytes=app.config['LOG_FILE_MAX_BYTES'],
|
||||||
|
backupCount=app.config['LOG_FILE_BACKUP_COUNT']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置日志级别
|
||||||
|
log_level = getattr(logging, app.config['LOG_LEVEL'])
|
||||||
|
file_handler.setLevel(log_level)
|
||||||
|
app.logger.setLevel(log_level)
|
||||||
|
|
||||||
|
# 设置日志格式
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
# 添加处理器
|
||||||
|
if not app.logger.handlers:
|
||||||
|
app.logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# 记录启动信息
|
||||||
|
app.logger.info(f'Application started with config')
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# from flask import Blueprint
|
||||||
|
|
||||||
|
# auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
# from . import routes
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
from flask import Blueprint, current_app, request, jsonify
|
||||||
|
from flask_jwt_extended import create_access_token, get_jwt, get_jwt_identity, jwt_required
|
||||||
|
from sqlalchemy import and_
|
||||||
|
from app import jwt,db
|
||||||
|
from app.models import OperationHistory, Role, User, UserRole
|
||||||
|
from app.utils.password_hasher import hash_password,check_password
|
||||||
|
from app.utils.response_dict import response
|
||||||
|
|
||||||
|
|
||||||
|
auth_bp= Blueprint('data', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
# 1. 获取登录凭证
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
print('username',username)
|
||||||
|
print('password',password)
|
||||||
|
|
||||||
|
# 2. 验证必填字段
|
||||||
|
# if not username or not password:
|
||||||
|
# response['code']=400
|
||||||
|
# response['message']="用户名和密码不能为空"
|
||||||
|
# return jsonify(response)
|
||||||
|
#
|
||||||
|
# # 3. 查询用户
|
||||||
|
# user = User.query.filter_by(username=username).first()
|
||||||
|
#
|
||||||
|
# # 4. 验证用户存在性和密码
|
||||||
|
# if not user or not check_password(password,user.password):
|
||||||
|
# response['code']=401
|
||||||
|
# response['message']="用户名或密码错误"
|
||||||
|
# return jsonify({"msg": "用户名或密码错误"}), 401
|
||||||
|
#
|
||||||
|
# if user.status != 1:
|
||||||
|
# return jsonify({"msg": "用户已禁用"}), 403
|
||||||
|
|
||||||
|
# 5. 生成JWT令牌[2,6](@ref)
|
||||||
|
access_token = create_access_token(identity=str(1))
|
||||||
|
|
||||||
|
|
||||||
|
result = db.session.query(
|
||||||
|
User.id,
|
||||||
|
User.username.label('realName'), # 使用 label 实现别名
|
||||||
|
User.password,
|
||||||
|
User.status,
|
||||||
|
Role.name.label('roles'),
|
||||||
|
Role.description
|
||||||
|
).join(
|
||||||
|
UserRole, User.id == UserRole.user_id # 第一次 JOIN:用户 ↔ 用户角色
|
||||||
|
).join(
|
||||||
|
Role, UserRole.role_id == Role.id # 第二次 JOIN:用户角色 ↔ 角色
|
||||||
|
).filter(
|
||||||
|
User.status == 1, # 状态过滤
|
||||||
|
User.id == 1 # 用户ID过滤
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return jsonify({"msg": "用户未分配角色或已禁用"}), 403
|
||||||
|
|
||||||
|
|
||||||
|
# dict_list = [row._asdict() for row in result]
|
||||||
|
# dict_list[0]['accessToken']=access_token
|
||||||
|
# dict_list[0]['roles']=[dict_list[0]['roles']]
|
||||||
|
# data={
|
||||||
|
# "accessToken": access_token,
|
||||||
|
# "user_id": user.id,
|
||||||
|
# "message": "登录成功"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# response['data']= dict_list[0]
|
||||||
|
# response['error']=''
|
||||||
|
# 6. 返回响应
|
||||||
|
res={"accessToken":access_token}
|
||||||
|
data={}
|
||||||
|
data['data']=res
|
||||||
|
return jsonify(res)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/userinfo', methods=['GET'])
|
||||||
|
@jwt_required() # 要求有效Token[1,5](@ref)
|
||||||
|
def get_user_info():
|
||||||
|
userid = get_jwt_identity()
|
||||||
|
"""
|
||||||
|
获取用户信息
|
||||||
|
"""
|
||||||
|
result = db.session.query(
|
||||||
|
User.id,
|
||||||
|
User.username.label('realName'), # 使用 label 实现别名
|
||||||
|
User.password,
|
||||||
|
User.status,
|
||||||
|
Role.name.label('roles'),
|
||||||
|
Role.description
|
||||||
|
).join(
|
||||||
|
UserRole, User.id == UserRole.user_id # 第一次 JOIN:用户 ↔ 用户角色
|
||||||
|
).join(
|
||||||
|
Role, UserRole.role_id == Role.id # 第二次 JOIN:用户角色 ↔ 角色
|
||||||
|
).filter(
|
||||||
|
User.status == 1, # 状态过滤
|
||||||
|
User.id == userid # 用户ID过滤
|
||||||
|
).all()
|
||||||
|
dict_list = [row._asdict() for row in result]
|
||||||
|
if not dict_list:
|
||||||
|
return jsonify({"msg": "用户未分配角色或已禁用"}), 403
|
||||||
|
role_list = [row.get('roles') for row in dict_list if row.get('roles')]
|
||||||
|
if not role_list:
|
||||||
|
return jsonify({"msg": "用户未分配角色或已禁用"}), 403
|
||||||
|
dict_list[0]['roles'] = role_list
|
||||||
|
|
||||||
|
data={"roles": dict_list[0]['roles'],"realName": dict_list[0]['realName'],"homePath": '/datamanage/datalabel'}
|
||||||
|
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/codes', methods=['GET'])
|
||||||
|
@jwt_required() # 要求有效Token[1,5](@ref)
|
||||||
|
def get_code():
|
||||||
|
response['code']=200
|
||||||
|
response['data']=["AC_100100","AC_100110"]
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/refresh', methods=['POST'])
|
||||||
|
@jwt_required() # 要求有效Token[1,5](@ref)
|
||||||
|
def refresh():
|
||||||
|
# data: string;
|
||||||
|
# status: number;
|
||||||
|
token = create_access_token(identity='service_account')
|
||||||
|
data={}
|
||||||
|
data['data']=token
|
||||||
|
data['status']=1
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== 受保护路由示例 =====
|
||||||
|
@auth_bp.route('/protected', methods=['GET'])
|
||||||
|
@jwt_required() # 要求有效Token[1,5](@ref)
|
||||||
|
def protected():
|
||||||
|
# 从Token获取用户身份
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
user = User.query.get(current_user_id)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"message": f"欢迎回来, {user.username}!",
|
||||||
|
"user_id": user.id
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@jwt.unauthorized_loader
|
||||||
|
def missing_token_callback(error):
|
||||||
|
"""缺少Token时的自定义响应"""
|
||||||
|
return jsonify({
|
||||||
|
'status': 401,
|
||||||
|
'msg': '缺少访问令牌'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
|
||||||
|
blacklist = set()
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/createuser', methods=['POST'])
|
||||||
|
@jwt_required() # 需要有效Token才能创建用户
|
||||||
|
def create_user():
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"username": "new_user",
|
||||||
|
"password": "securePass123",
|
||||||
|
"role_ids": [1, 3] // 要分配的角色ID列表
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
data = request.get_json()
|
||||||
|
print(data)
|
||||||
|
if not data or 'username' not in data or 'password' not in data:
|
||||||
|
return jsonify({"error": "缺少用户名或密码"}), 400
|
||||||
|
|
||||||
|
# 1. 验证用户名唯一性
|
||||||
|
existing_user = User.query.filter_by(username=data['username']).first()
|
||||||
|
if existing_user:
|
||||||
|
return jsonify({"error": "用户名已存在"}), 409
|
||||||
|
|
||||||
|
# 2. 获取当前操作者
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
print('current_user_id',current_user_id)
|
||||||
|
current_user = User.query.get(int(current_user_id))
|
||||||
|
if not current_user:
|
||||||
|
return jsonify({"error": "无效的操作者"}), 401
|
||||||
|
|
||||||
|
# 3. 密码复杂度验证
|
||||||
|
password = data['password']
|
||||||
|
if len(password) < 8 or not any(c.isdigit() for c in password):
|
||||||
|
return jsonify({"error": "密码需至少8位且包含数字"}), 400
|
||||||
|
|
||||||
|
# 4. 验证角色(新增部分)
|
||||||
|
role_ids = data.get('role_ids', [])
|
||||||
|
valid_roles = []
|
||||||
|
if role_ids:
|
||||||
|
valid_roles = Role.query.filter(Role.id.in_(role_ids)).all()
|
||||||
|
if len(valid_roles) != len(role_ids):
|
||||||
|
return jsonify({"error": "包含无效的角色ID"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 5. 创建新用户
|
||||||
|
new_user = User(
|
||||||
|
username=data['username'],
|
||||||
|
password=hash_password(data['password']),
|
||||||
|
status=data.get('status', 1),
|
||||||
|
created_by=current_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.flush() # 获取新用户ID但不提交事务
|
||||||
|
|
||||||
|
# 6. 分配角色(新增核心功能)
|
||||||
|
for role in valid_roles:
|
||||||
|
user_role = UserRole(
|
||||||
|
user_id=new_user.id,
|
||||||
|
role_id=role.id,
|
||||||
|
created_by=current_user_id
|
||||||
|
)
|
||||||
|
db.session.add(user_role)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 7. 构造响应数据(包含角色信息)
|
||||||
|
response_data = {
|
||||||
|
"id": new_user.id,
|
||||||
|
"username": new_user.username,
|
||||||
|
"created_at": new_user.created_at.isoformat(),
|
||||||
|
"created_by": new_user.created_by,
|
||||||
|
"roles": [{"id": r.id, "name": r.name} for r in valid_roles] # 新增角色信息
|
||||||
|
}
|
||||||
|
|
||||||
|
# 8. 记录操作日志(新增角色信息)
|
||||||
|
current_app.logger.info(
|
||||||
|
f"用户创建成功: {new_user.username} (ID:{new_user.id}) "
|
||||||
|
f"操作者: {current_user.username} (ID:{current_user_id}) "
|
||||||
|
f"分配角色: {[r.name for r in valid_roles]}" # 记录分配的角色
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(response_data), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"用户创建失败: {str(e)}")
|
||||||
|
return jsonify({"error": "服务器内部错误"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/updateuser', methods=['POST'])
|
||||||
|
@jwt_required() # 需要有效Token才能访问
|
||||||
|
def update_user():
|
||||||
|
'''
|
||||||
|
请求体格式:
|
||||||
|
{
|
||||||
|
"user_id": 123, // 必需:目标用户ID
|
||||||
|
"username": "updated_user", // 可选:新用户名
|
||||||
|
"password": "newSecurePass456", // 可选:新密码
|
||||||
|
"status": 1 // 可选:用户状态(1-启用,0-禁用)
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'user_id' not in data:
|
||||||
|
return jsonify({"error": "请求数据不能为空且必须包含user_id"}), 400
|
||||||
|
|
||||||
|
# 1. 验证操作者身份
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
current_user = User.query.get(int(current_user_id))
|
||||||
|
if not current_user:
|
||||||
|
return jsonify({"error": "无效的操作者"}), 401
|
||||||
|
|
||||||
|
# 2. 验证目标用户是否存在
|
||||||
|
target_user_id = data['user_id']
|
||||||
|
target_user = User.query.get(int(target_user_id))
|
||||||
|
if not target_user:
|
||||||
|
return jsonify({"error": "目标用户不存在"}), 404
|
||||||
|
|
||||||
|
# 3. 权限校验:仅允许管理员修改(核心修改点)
|
||||||
|
# 检查当前用户是否为管理员(假设管理员角色ID为1)
|
||||||
|
is_admin = UserRole.query.filter_by( # 注意:这里修正为UserRole表(用户-角色关联表)
|
||||||
|
user_id=current_user_id,
|
||||||
|
role_id=1
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not is_admin: # 只有管理员能通过校验
|
||||||
|
return jsonify({"error": "仅管理员有权限修改用户信息"}), 403
|
||||||
|
|
||||||
|
role_ids = data.get('role_ids', None)
|
||||||
|
valid_roles = []
|
||||||
|
if role_ids is not None:
|
||||||
|
if not isinstance(role_ids, list):
|
||||||
|
return jsonify({"error": "role_ids必须为数组"}), 400
|
||||||
|
if role_ids:
|
||||||
|
valid_roles = Role.query.filter(Role.id.in_(role_ids)).all()
|
||||||
|
if len(valid_roles) != len(role_ids):
|
||||||
|
return jsonify({"error": "包含无效的角色ID"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 4. 处理用户名修改(如提供)
|
||||||
|
if 'username' in data and data['username']:
|
||||||
|
# 验证用户名唯一性(排除自身)
|
||||||
|
existing_user = User.query.filter(
|
||||||
|
User.username == data['username'],
|
||||||
|
User.id != target_user_id
|
||||||
|
).first()
|
||||||
|
if existing_user:
|
||||||
|
return jsonify({"error": "用户名已存在"}), 409
|
||||||
|
target_user.username = data['username']
|
||||||
|
|
||||||
|
# 5. 处理密码修改(如提供)
|
||||||
|
if 'password' in data and data['password']:
|
||||||
|
password = data['password']
|
||||||
|
# 密码复杂度验证(与创建用户保持一致)
|
||||||
|
if len(password) < 8 or not any(c.isdigit() for c in password):
|
||||||
|
return jsonify({"error": "密码需至少8位且包含数字"}), 400
|
||||||
|
target_user.password = hash_password(password) # 复用哈希函数
|
||||||
|
|
||||||
|
# 6. 处理状态修改(如提供)
|
||||||
|
if 'status' in data:
|
||||||
|
target_user.status = data['status'] # 管理员无需额外校验
|
||||||
|
|
||||||
|
# 6.1 处理角色修改(如提供)
|
||||||
|
if role_ids is not None:
|
||||||
|
UserRole.query.filter_by(user_id=target_user_id).delete()
|
||||||
|
for role in valid_roles:
|
||||||
|
user_role = UserRole(
|
||||||
|
user_id=target_user_id,
|
||||||
|
role_id=role.id,
|
||||||
|
created_by=current_user_id,
|
||||||
|
)
|
||||||
|
db.session.add(user_role)
|
||||||
|
|
||||||
|
# 7. 提交修改
|
||||||
|
target_user.updated_at = datetime.now() # 假设模型有updated_at字段
|
||||||
|
target_user.updated_by = current_user_id # 假设模型有updated_by字段
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 8. 构造响应数据
|
||||||
|
response_data = {
|
||||||
|
"id": target_user.id,
|
||||||
|
"username": target_user.username,
|
||||||
|
"status": target_user.status,
|
||||||
|
"updated_at": target_user.updated_at.isoformat(),
|
||||||
|
"updated_by": current_user.username
|
||||||
|
}
|
||||||
|
if role_ids is not None:
|
||||||
|
response_data["roles"] = [{"id": r.id, "name": r.name} for r in valid_roles]
|
||||||
|
|
||||||
|
# 9. 记录操作日志
|
||||||
|
current_app.logger.info(
|
||||||
|
f"用户信息更新成功: {target_user.username} (ID:{target_user.id}) "
|
||||||
|
f"操作者: {current_user.username} (ID:{current_user_id}) "
|
||||||
|
f"修改内容: {[k for k in data.keys() if k != 'user_id']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(response_data), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"用户信息更新失败: {str(e)}")
|
||||||
|
return jsonify({"error": "服务器内部错误"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/logout', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def log_out():
|
||||||
|
jti = get_jwt()["jti"] # 获取 Token 的唯一标识
|
||||||
|
blacklist.add(jti) # 将 Token 添加到黑名单
|
||||||
|
return jsonify({"msg": "Successfully logged out"}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/getuserlist', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_users():
|
||||||
|
result = db.session.query(
|
||||||
|
User.id,
|
||||||
|
User.username,
|
||||||
|
User.status,
|
||||||
|
User.created_at,
|
||||||
|
User.created_by,
|
||||||
|
User.updated_at,
|
||||||
|
UserRole.role_id,
|
||||||
|
Role.description,
|
||||||
|
Role.name
|
||||||
|
).join(
|
||||||
|
UserRole, User.id == UserRole.user_id
|
||||||
|
).join(
|
||||||
|
Role, UserRole.role_id == Role.id
|
||||||
|
).all()
|
||||||
|
dict_list = [row._asdict() for row in result]
|
||||||
|
return jsonify(dict_list), 200
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/getroles', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_roles():
|
||||||
|
roles = Role.query.filter(Role.status == 1).all()
|
||||||
|
data = [{"id": r.id, "name": r.name, "description": r.description} for r in roles]
|
||||||
|
return jsonify(data), 200
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/deleteuser', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_user():
|
||||||
|
|
||||||
|
user_id=request.args.get('userid')
|
||||||
|
# 1. 查询用户(确保状态之前为 1)
|
||||||
|
user = User.query.filter(
|
||||||
|
User.id == user_id,
|
||||||
|
User.status == 1 # 只选择状态为 1 的用户
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify({"error": "用户不存在或状态不是 1"}), 400 # 用户不存在或状态不是 1
|
||||||
|
|
||||||
|
# 2. 修改状态
|
||||||
|
user.status = 0
|
||||||
|
|
||||||
|
# 3. 提交事务
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"msg": "Successfully"}),200
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return jsonify({"error": f"出现错误{e}"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/test', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def test():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/gethisttory', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def query_operation_history():
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"username": "admin", // 可选,精确匹配用户名
|
||||||
|
"api_path": "/api/user/create", // 可选,精确匹配接口路径
|
||||||
|
"start_time": "2025-07-01T00:00:00", // 可选,开始时间(ISO 8601格式)
|
||||||
|
"end_time": "2025-07-10T23:59:59", // 可选,结束时间(ISO 8601格式)
|
||||||
|
"request_keyword": "email", // 可选,在请求参数中模糊搜索关键词
|
||||||
|
"page": 2, // 可选,页码(默认1)
|
||||||
|
"per_page": 15 // 可选,每页条数(默认20)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""查询操作历史记录(支持多条件过滤)"""
|
||||||
|
# 接收JSON格式的查询参数
|
||||||
|
query_params = request.get_json()
|
||||||
|
|
||||||
|
# 构建基础查询
|
||||||
|
query = OperationHistory.query
|
||||||
|
|
||||||
|
# 1. 用户名过滤(精确匹配)
|
||||||
|
if query_params['username']:
|
||||||
|
query = query.filter(OperationHistory.username == query_params['username'])
|
||||||
|
|
||||||
|
# 2. 接口路径过滤(精确匹配)
|
||||||
|
if query_params['api_path']:
|
||||||
|
query = query.filter(OperationHistory.api_path == query_params['api_path'])
|
||||||
|
|
||||||
|
# 3. 操作时间范围过滤
|
||||||
|
if query_params['start_time'] and query_params['end_time']:
|
||||||
|
# 解析时间参数(支持ISO 8601格式)
|
||||||
|
start_time = datetime.fromisoformat(query_params['start_time']) if 'start_time' in query_params else None
|
||||||
|
end_time = datetime.fromisoformat(query_params['end_time']) if 'end_time' in query_params else None
|
||||||
|
# 构建时间范围查询条件
|
||||||
|
time_conditions = []
|
||||||
|
if start_time:
|
||||||
|
time_conditions.append(OperationHistory.operation_time >= start_time)
|
||||||
|
if end_time:
|
||||||
|
time_conditions.append(OperationHistory.operation_time <= end_time)
|
||||||
|
|
||||||
|
if time_conditions:
|
||||||
|
query = query.filter(and_(*time_conditions))
|
||||||
|
|
||||||
|
# 4. 请求参数关键词过滤(模糊匹配)
|
||||||
|
if query_params['request_keyword']:
|
||||||
|
keyword = f"%{query_params['request_keyword']}%"
|
||||||
|
query = query.filter(
|
||||||
|
OperationHistory.request_params.like(keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 分页参数处理
|
||||||
|
page = query_params.get('page', 1)
|
||||||
|
per_page = query_params.get('per_page', 20)
|
||||||
|
|
||||||
|
# 执行分页查询(使用索引优化)
|
||||||
|
pagination = query.order_by(OperationHistory.operation_time.desc()) \
|
||||||
|
.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
|
||||||
|
# 序列化结果
|
||||||
|
results = []
|
||||||
|
for record in pagination.items:
|
||||||
|
results.append({
|
||||||
|
'id': record.id,
|
||||||
|
'username': record.username,
|
||||||
|
'api_path': record.api_path,
|
||||||
|
'http_method': record.http_method,
|
||||||
|
'operation_time': record.operation_time.isoformat(),
|
||||||
|
'response_code': record.response_code,
|
||||||
|
'ip_address': record.ip_address,
|
||||||
|
'operation_result': record.operation_result,
|
||||||
|
'error_message': record.error_message,
|
||||||
|
'trace_id': record.trace_id,
|
||||||
|
# 请求参数反序列化(JSON字符串→Python对象)
|
||||||
|
'request_params': json.loads(record.request_params) if record.request_params else None
|
||||||
|
})
|
||||||
|
|
||||||
|
# 返回分页结果
|
||||||
|
return jsonify({
|
||||||
|
'total': pagination.total,
|
||||||
|
'pages': pagination.pages,
|
||||||
|
'current_page': pagination.page,
|
||||||
|
'per_page': pagination.per_page,
|
||||||
|
'results': results
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# from routes import Blueprint
|
||||||
|
|
||||||
|
# posts_bp = Blueprint('posts', __name__)
|
||||||
|
|
||||||
|
# from . import routes
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# from datetime import datetime,timedelta
|
||||||
|
# from app import db
|
||||||
|
# from app.models import BagRecord
|
||||||
|
# from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
# from sqlalchemy import func
|
||||||
|
|
||||||
|
# def add():
|
||||||
|
# new_record = BagRecord(
|
||||||
|
# bag_name="sensor_data_2025.bag",
|
||||||
|
# collection_time=datetime(2025, 5, 1, 12, 1, 1),
|
||||||
|
# bag_state=0,
|
||||||
|
# frames=120,
|
||||||
|
# car_state=1,
|
||||||
|
# address="beijing",
|
||||||
|
# collect_month=5
|
||||||
|
# )
|
||||||
|
# db.session.add(new_record)
|
||||||
|
# db.session.commit() # 提交事务 [1,7](@ref)
|
||||||
|
|
||||||
|
|
||||||
|
# def create_record(data):
|
||||||
|
# """创建新记录"""
|
||||||
|
# try:
|
||||||
|
# # 转换日期类型字段
|
||||||
|
# if 'collection_time' in data:
|
||||||
|
# data['collection_time'] = datetime.fromisoformat(data['collection_time'])
|
||||||
|
# if 'parse_time' in data:
|
||||||
|
# data['parse_time'] = datetime.fromisoformat(data['parse_time'])
|
||||||
|
|
||||||
|
# record = BagRecord(**data)
|
||||||
|
# db.session.add(record)
|
||||||
|
# db.session.commit()
|
||||||
|
# return record
|
||||||
|
# except SQLAlchemyError as e:
|
||||||
|
# db.session.rollback()
|
||||||
|
# raise e
|
||||||
|
|
||||||
|
# def get_record_by_id(record_id):
|
||||||
|
# """根据ID获取记录"""
|
||||||
|
# return BagRecord.query.get(record_id)
|
||||||
|
|
||||||
|
|
||||||
|
# def get_all_records(**filters):
|
||||||
|
# """获取所有记录(支持过滤)"""
|
||||||
|
# query = BagRecord.query
|
||||||
|
# if filters:
|
||||||
|
# query = query.filter_by(**filters)
|
||||||
|
# return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
# def get_daily_counts(start_date, end_date):
|
||||||
|
# # 执行 SQL 查询,按日期分组并统计数量
|
||||||
|
# query = (
|
||||||
|
# db.session.query(
|
||||||
|
# db.func.date(BagRecord.collection_time).label('date'),
|
||||||
|
# db.func.count(BagRecord.id).label('count')
|
||||||
|
# )
|
||||||
|
# .filter(BagRecord.collection_time >= start_date)
|
||||||
|
# .filter(BagRecord.collection_time <= end_date + timedelta(days=1)) # 包含结束日期
|
||||||
|
# .group_by(db.func.date(BagRecord.collection_time))
|
||||||
|
# .order_by(db.func.date(BagRecord.collection_time))
|
||||||
|
# )
|
||||||
|
|
||||||
|
# print(query.all())
|
||||||
|
# # 将结果转换为字典列表
|
||||||
|
# results = []
|
||||||
|
# for date, count in query.all():
|
||||||
|
# results.append({
|
||||||
|
# 'date': str(date), # 将日期对象转换为字符串
|
||||||
|
# 'count': count
|
||||||
|
# })
|
||||||
|
|
||||||
|
# return results
|
||||||
@@ -0,0 +1,752 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from flask import Blueprint, current_app, request, jsonify
|
||||||
|
from flask_jwt_extended import (
|
||||||
|
create_access_token,
|
||||||
|
get_jwt,
|
||||||
|
get_jwt_identity,
|
||||||
|
jwt_required,
|
||||||
|
)
|
||||||
|
from sqlalchemy import and_, asc, desc, exists, or_,func
|
||||||
|
from app.models import (
|
||||||
|
BagFile,
|
||||||
|
BagStatus,
|
||||||
|
Fst,
|
||||||
|
QaStatus,
|
||||||
|
Role,
|
||||||
|
SyncStatus,
|
||||||
|
TaggingEvents,
|
||||||
|
User,
|
||||||
|
UserRole,
|
||||||
|
BagMergeRecord,
|
||||||
|
)
|
||||||
|
from app.utils.fst_tree import build_tree
|
||||||
|
from app import db
|
||||||
|
from app.utils.log_record import log_operation
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
import requests
|
||||||
|
from sqlalchemy.orm import aliased, joinedload
|
||||||
|
|
||||||
|
check_data = Blueprint("display", __name__)
|
||||||
|
|
||||||
|
def serialize_tagging_event(event: TaggingEvents) -> dict:
|
||||||
|
def safe_name(tag):
|
||||||
|
return getattr(tag, "name", None) if tag else None
|
||||||
|
|
||||||
|
level1 = safe_name(event.level1_tag)
|
||||||
|
level2 = safe_name(event.level2_tag)
|
||||||
|
level3 = safe_name(event.level3_tag)
|
||||||
|
level4 = safe_name(event.level4_tag)
|
||||||
|
tag_path = " / ".join([x for x in [level1, level2, level3, level4] if x])
|
||||||
|
return {
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"bag_id": event.bag_id,
|
||||||
|
"tag_path": tag_path,
|
||||||
|
"level1_tag_id": event.level1_tag_id,
|
||||||
|
"level1_tag_name": level1,
|
||||||
|
"level2_tag_id": event.level2_tag_id,
|
||||||
|
"level2_tag_name": level2,
|
||||||
|
"level3_tag_id": event.level3_tag_id,
|
||||||
|
"level3_tag_name": level3,
|
||||||
|
"level4_tag_id": event.level4_tag_id,
|
||||||
|
"level4_tag_name": level4,
|
||||||
|
"qa_status": event.qa_status.value if event.qa_status else None,
|
||||||
|
"case_type": event.case_type,
|
||||||
|
"front_starttime": event.front_starttime.isoformat()
|
||||||
|
if event.front_starttime
|
||||||
|
else None,
|
||||||
|
"front_endtime": event.front_endtime.isoformat() if event.front_endtime else None,
|
||||||
|
"front_start_sec": event.front_start_sec,
|
||||||
|
"front_end_sec": event.front_end_sec,
|
||||||
|
"high_speed": event.high_speed,
|
||||||
|
"urban": event.urban,
|
||||||
|
"parking": event.parking,
|
||||||
|
"note": event.note,
|
||||||
|
"source": event.source.value if event.source else None,
|
||||||
|
"reviewer_id": event.reviewer_id,
|
||||||
|
"ts_event": event.ts_event.isoformat() if event.ts_event else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@check_data.route("/bagtotal", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def bag_total():
|
||||||
|
"""查询BagFile表记录总数"""
|
||||||
|
# 执行SELECT count(*) FROM bag_file
|
||||||
|
total_count = BagFile.query.count()
|
||||||
|
|
||||||
|
return jsonify({"total": total_count}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@check_data.route("/noprocess-bagtotal", methods=["GET"])
|
||||||
|
@jwt_required()
|
||||||
|
def no_process_bag_total():
|
||||||
|
count = BagFile.query.filter(BagFile.operation_status == 0).count()
|
||||||
|
return jsonify({"total": count}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@check_data.route("/insert-qa-status", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def insert_qa_status():
|
||||||
|
try:
|
||||||
|
# 获取请求数据
|
||||||
|
data = request.get_json()
|
||||||
|
current_user = get_jwt_identity()
|
||||||
|
|
||||||
|
# 验证必要参数(bag_id、qa_status为必填,评论为可选)
|
||||||
|
required_fields = ["bag_id", "qa_status"]
|
||||||
|
if not data or not all(field in data for field in required_fields):
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f"缺少必要参数,需要: {required_fields}",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证qa_status是否为有效值
|
||||||
|
try:
|
||||||
|
qa_status = QaStatus(data["qa_status"])
|
||||||
|
except ValueError:
|
||||||
|
valid_statuses = [status.value for status in QaStatus]
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f"无效的qa_status值,有效值为: {valid_statuses}",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 查询对应的BagFile记录
|
||||||
|
bag_file = BagFile.query.get(data["bag_id"])
|
||||||
|
if not bag_file:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f'未找到ID为{data["bag_id"]}的BagFile记录',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 仅汇总 TaggingEvents,不覆盖每条事件的 qa_status
|
||||||
|
tag_events = TaggingEvents.query.filter_by(
|
||||||
|
bag_id=bag_file.id, is_deleted=False
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def aggregate_qa_status(events):
|
||||||
|
has_not_reviewed = False
|
||||||
|
has_invalid = False
|
||||||
|
has_modify = False
|
||||||
|
has_passed = False
|
||||||
|
for ev in events:
|
||||||
|
status = ev.qa_status or QaStatus.QA_NOT_REVIEWED
|
||||||
|
if status == QaStatus.QA_NOT_REVIEWED:
|
||||||
|
has_not_reviewed = True
|
||||||
|
elif status == QaStatus.QA_INVALID:
|
||||||
|
has_invalid = True
|
||||||
|
elif status == QaStatus.QA_MODIFY:
|
||||||
|
has_modify = True
|
||||||
|
elif status == QaStatus.QA_PASSED:
|
||||||
|
has_passed = True
|
||||||
|
if has_not_reviewed:
|
||||||
|
return QaStatus.QA_NOT_REVIEWED
|
||||||
|
if has_invalid:
|
||||||
|
return QaStatus.QA_INVALID
|
||||||
|
if has_modify:
|
||||||
|
return QaStatus.QA_MODIFY
|
||||||
|
if has_passed:
|
||||||
|
return QaStatus.QA_PASSED
|
||||||
|
return QaStatus.QA_NOT_REVIEWED
|
||||||
|
|
||||||
|
# 更新字段(包含评论字段)
|
||||||
|
if tag_events:
|
||||||
|
bag_file.qa_status = aggregate_qa_status(tag_events)
|
||||||
|
bag_file.qa_confirm_time = datetime.now()
|
||||||
|
bag_file.qa_id = current_user
|
||||||
|
if "bag_status" in data:
|
||||||
|
bag_file.bag_status = data.get("bag_status")
|
||||||
|
|
||||||
|
# 处理评论字段(使用get方法避免参数不存在时报错,默认空字符串)
|
||||||
|
bag_file.comment1 = data.get("comment1") if data.get("comment1") else None
|
||||||
|
bag_file.comment2 = data.get("comment2") if data.get("comment2") else None
|
||||||
|
bag_file.comment3 = data.get("comment3") if data.get("comment3") else None
|
||||||
|
|
||||||
|
# 提交到数据库
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
final_qa_status = bag_file.qa_status.value if bag_file.qa_status else None
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": f'BagFile ID {data["bag_id"]} 的qa_status已更新为 {final_qa_status}',
|
||||||
|
"data": {
|
||||||
|
"bag_id": data["bag_id"],
|
||||||
|
"qa_status": final_qa_status,
|
||||||
|
"bag_status": data.get("bag_status"),
|
||||||
|
"comments": {
|
||||||
|
"comment1": bag_file.comment1,
|
||||||
|
"comment2": bag_file.comment2,
|
||||||
|
"comment3": bag_file.comment3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 发生错误时回滚
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({"success": False, "message": f"更新失败: {str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@check_data.route("/get-finish-list", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def get_processed():
|
||||||
|
try:
|
||||||
|
# 1. 解析请求参数(新增sync_status参数)
|
||||||
|
params = request.get_json() or {}
|
||||||
|
print(f"【请求参数】原始参数: {params}")
|
||||||
|
|
||||||
|
page = params.get("page", 1)
|
||||||
|
per_page = params.get("per_page", 20)
|
||||||
|
file_name = params.get("file_name", "").strip()
|
||||||
|
start_datetime = params.get("start_datetime", "").strip()
|
||||||
|
end_datetime = params.get("end_datetime", "").strip()
|
||||||
|
level1_tag = params.get("level1_tag", "")
|
||||||
|
status_str = params.get("status", "").strip()
|
||||||
|
user_id = params.get("user_id", "")
|
||||||
|
# sync_status_str = params.get('sync_status', '').strip() # 新增:同步状态参数
|
||||||
|
|
||||||
|
# 2. merge 表别名
|
||||||
|
M = aliased(BagMergeRecord)
|
||||||
|
|
||||||
|
# 3. 构建基础查询:
|
||||||
|
# 选出 BagFile + 逻辑文件名 logical_name = coalesce(joined_name, file_name)
|
||||||
|
logical_name = func.coalesce(M.joined_name, BagFile.file_name)
|
||||||
|
|
||||||
|
# 2. 构建基础查询(关联User表用于查询username)
|
||||||
|
query = (
|
||||||
|
db.session.query(
|
||||||
|
BagFile,
|
||||||
|
logical_name.label("logical_name"),
|
||||||
|
)
|
||||||
|
.outerjoin(User, BagFile.user_id == User.id)
|
||||||
|
.outerjoin(M, M.src_name == BagFile.file_name)
|
||||||
|
.options(
|
||||||
|
joinedload(BagFile.level1_tag),
|
||||||
|
joinedload(BagFile.level2_tag),
|
||||||
|
joinedload(BagFile.level3_tag),
|
||||||
|
joinedload(BagFile.level4_tag),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 动态添加过滤条件
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
# 固定返回bag_status>=1的数据
|
||||||
|
conditions.append(BagFile.bag_status >= 1)
|
||||||
|
conditions.append(BagFile.sync_status == "SYNCED")
|
||||||
|
|
||||||
|
# 只保留 merge 主 bag 或未 merge 的 bag
|
||||||
|
conditions.append(
|
||||||
|
or_(
|
||||||
|
M.is_initiator == True, # 参与 merge 的主 bag
|
||||||
|
M.id.is_(None), # 完全没 merge 过的 bag(outer join 不命中,M.* 为 NULL)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 根据user_id过滤
|
||||||
|
if user_id:
|
||||||
|
try:
|
||||||
|
user_id_int = int(user_id)
|
||||||
|
conditions.append(BagFile.user_id == user_id_int)
|
||||||
|
except ValueError:
|
||||||
|
print(f"【过滤条件】user_id格式错误(非整数): {user_id}")
|
||||||
|
|
||||||
|
# 新增:根据sync_status过滤
|
||||||
|
# if sync_status_str:
|
||||||
|
# try:
|
||||||
|
# sync_status_enum = SyncStatus[sync_status_str] # 假设SyncStatus是定义的枚举类
|
||||||
|
# conditions.append(BagFile.sync_status == sync_status_enum)
|
||||||
|
# except KeyError:
|
||||||
|
# print(f"【过滤条件】无效的sync_status值: {sync_status_str}")
|
||||||
|
|
||||||
|
# 文件名模糊查询
|
||||||
|
if file_name:
|
||||||
|
conditions.append(BagFile.file_name.like(f"%{file_name}%"))
|
||||||
|
print(f"【过滤条件】添加文件名模糊查询: {file_name}")
|
||||||
|
|
||||||
|
# 时间范围查询
|
||||||
|
if start_datetime:
|
||||||
|
try:
|
||||||
|
start_dt = datetime.fromisoformat(start_datetime)
|
||||||
|
conditions.append(BagFile.capture_datetime >= start_dt)
|
||||||
|
except ValueError:
|
||||||
|
print(f"【过滤条件】开始时间格式错误: {start_datetime}")
|
||||||
|
|
||||||
|
if end_datetime:
|
||||||
|
try:
|
||||||
|
end_dt = datetime.fromisoformat(end_datetime)
|
||||||
|
conditions.append(BagFile.capture_datetime <= end_dt)
|
||||||
|
except ValueError:
|
||||||
|
print(f"【过滤条件】结束时间格式错误: {end_datetime}")
|
||||||
|
|
||||||
|
# 一级标签查询
|
||||||
|
if level1_tag:
|
||||||
|
try:
|
||||||
|
level1_id = int(level1_tag)
|
||||||
|
conditions.append(BagFile.level1_tag_id == level1_id)
|
||||||
|
except ValueError:
|
||||||
|
print(f"【过滤条件】level1_tag转换失败(非整数): {level1_tag}")
|
||||||
|
|
||||||
|
# 状态查询
|
||||||
|
if status_str:
|
||||||
|
try:
|
||||||
|
status_enum = BagStatus[status_str]
|
||||||
|
conditions.append(BagFile.status == status_enum)
|
||||||
|
except KeyError:
|
||||||
|
print(f"【过滤条件】无效状态值: {status_str}")
|
||||||
|
|
||||||
|
# 应用所有条件
|
||||||
|
query = query.filter(and_(*conditions))
|
||||||
|
|
||||||
|
# 4. 排序:按update_time降序(核心调整)
|
||||||
|
query = query.order_by(BagFile.qa_confirm_time.desc())
|
||||||
|
|
||||||
|
# 5. 分页查询
|
||||||
|
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
total_items = pagination.total
|
||||||
|
total_pages = pagination.pages
|
||||||
|
|
||||||
|
# 6. 序列化(新增sync_status字段)
|
||||||
|
bag_files = []
|
||||||
|
bag_ids = [bag.id for bag, _ in pagination.items]
|
||||||
|
tag_events_by_bag = {}
|
||||||
|
if bag_ids:
|
||||||
|
tag_events = (
|
||||||
|
TaggingEvents.query.options(
|
||||||
|
joinedload(TaggingEvents.level1_tag),
|
||||||
|
joinedload(TaggingEvents.level2_tag),
|
||||||
|
joinedload(TaggingEvents.level3_tag),
|
||||||
|
joinedload(TaggingEvents.level4_tag),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
TaggingEvents.bag_id.in_(bag_ids),
|
||||||
|
TaggingEvents.is_deleted == False,
|
||||||
|
)
|
||||||
|
.order_by(TaggingEvents.bag_id.asc(), TaggingEvents.ts_event.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for ev in tag_events:
|
||||||
|
tag_events_by_bag.setdefault(ev.bag_id, []).append(
|
||||||
|
serialize_tagging_event(ev)
|
||||||
|
)
|
||||||
|
|
||||||
|
for bag ,logical_name_value in pagination.items:
|
||||||
|
qa_username = (
|
||||||
|
bag.qa_user.username if (bag.qa_user and bag.qa_user.username) else ""
|
||||||
|
)
|
||||||
|
bag_files.append(
|
||||||
|
{
|
||||||
|
"id": bag.id,
|
||||||
|
"file_name": logical_name_value,
|
||||||
|
"capture_datetime": (
|
||||||
|
bag.capture_datetime.isoformat()
|
||||||
|
if bag.capture_datetime
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"status": bag.status.value if bag.status else None,
|
||||||
|
"create_time": (
|
||||||
|
bag.create_time.isoformat() if bag.create_time else None
|
||||||
|
),
|
||||||
|
"update_time": (
|
||||||
|
bag.qa_confirm_time.isoformat() if bag.qa_confirm_time else None
|
||||||
|
), # 排序字段显示
|
||||||
|
"level1_tag": bag.level1_tag.name if bag.level1_tag else None,
|
||||||
|
"level2_tag": bag.level2_tag.name if bag.level2_tag else None,
|
||||||
|
"level3_tag": bag.level3_tag.name if bag.level3_tag else None,
|
||||||
|
"level4_tag": bag.level4_tag.name if bag.level4_tag else None,
|
||||||
|
"bag_status": bag.bag_status,
|
||||||
|
"user_id": bag.user_id,
|
||||||
|
"username": (
|
||||||
|
bag.user.username if (bag.user and bag.user.username) else ""
|
||||||
|
),
|
||||||
|
"qa_status": bag.qa_status.value,
|
||||||
|
"sync_status": (
|
||||||
|
bag.sync_status.value if bag.sync_status else None
|
||||||
|
), # 新增:同步状态
|
||||||
|
"qa_user_id": bag.qa_id, # 可选:返回QA操作人ID
|
||||||
|
"qa_username": qa_username,
|
||||||
|
"comment1": bag.comment1,
|
||||||
|
"front_start_sec": bag.front_start_sec,
|
||||||
|
"front_end_sec": bag.front_end_sec,
|
||||||
|
"high_speed": bag.high_speed,
|
||||||
|
"urban": bag.urban,
|
||||||
|
"parking": bag.parking,
|
||||||
|
"tag_events": tag_events_by_bag.get(bag.id, []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 7. 返回响应
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": bag_files,
|
||||||
|
"total": total_items,
|
||||||
|
"page": page,
|
||||||
|
"pages": total_pages,
|
||||||
|
"message": "查询成功(包含sync_status过滤及按update_time排序)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"【接口异常】: {str(e)}")
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"code": 500,
|
||||||
|
"data": [],
|
||||||
|
"total": 0,
|
||||||
|
"page": page if "page" in locals() else 1,
|
||||||
|
"pages": 0,
|
||||||
|
"message": f"查询出错:{str(e)}",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@check_data.route("/insert-db-ids", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def insert_db_ids():
|
||||||
|
try:
|
||||||
|
# 获取请求体中的ID列表
|
||||||
|
id_list = request.get_json()
|
||||||
|
|
||||||
|
# 1. 验证参数格式
|
||||||
|
if not isinstance(id_list, list):
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "参数格式错误,需提供数组格式,如[1,2,3]",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查列表中是否全为整数
|
||||||
|
if not all(isinstance(id_val, int) for id_val in id_list):
|
||||||
|
return jsonify({"success": False, "message": "数组元素必须为整数ID"}), 400
|
||||||
|
|
||||||
|
# 检查列表是否为空
|
||||||
|
if len(id_list) == 0:
|
||||||
|
return jsonify({"success": False, "message": "ID列表不能为空"}), 400
|
||||||
|
|
||||||
|
# 2. 执行批量更新
|
||||||
|
current_time = datetime.now()
|
||||||
|
# 使用SQLAlchemy的批量更新
|
||||||
|
updated_rows = BagFile.query.filter(BagFile.id.in_(id_list)).update(
|
||||||
|
{BagFile.sync_status: "SYNCED", BagFile.qa_confirm_time: current_time},
|
||||||
|
synchronize_session=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 提交事务
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 3. 返回结果
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": f"成功更新{updated_rows}条记录",
|
||||||
|
"updated_count": updated_rows,
|
||||||
|
"updated_ids": id_list,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 发生错误时回滚事务
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({"success": False, "message": f"更新失败:{str(e)}"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@check_data.route("/insert-rootdb", methods=["POST"])
|
||||||
|
@jwt_required()
|
||||||
|
def insert_rootdb():
|
||||||
|
API_URL = "http://10.0.240.4:5232/api/fst/bags/update"
|
||||||
|
TIMEOUT = 10
|
||||||
|
result = {"success_count": 0, "fail_count": 0, "fail_details": []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
param_list = request.get_json()
|
||||||
|
# print(f"【insert_rootdb】请求参数: {param_list}")
|
||||||
|
if not param_list or not isinstance(param_list, list):
|
||||||
|
return jsonify({"success": False, "message": "请求参数必须为非空数组"}), 400
|
||||||
|
|
||||||
|
bag_ids = [
|
||||||
|
item.get("rowid")
|
||||||
|
for item in param_list
|
||||||
|
if isinstance(item, dict) and "rowid" in item
|
||||||
|
]
|
||||||
|
bag_files = BagFile.query.filter(BagFile.id.in_(bag_ids)).all() if bag_ids else []
|
||||||
|
bag_by_id = {bag.id: bag for bag in bag_files}
|
||||||
|
merge_name_by_src = {}
|
||||||
|
if bag_files:
|
||||||
|
src_names = [bag.file_name for bag in bag_files if bag.file_name]
|
||||||
|
if src_names:
|
||||||
|
merge_records = (
|
||||||
|
BagMergeRecord.query.filter(BagMergeRecord.src_name.in_(src_names))
|
||||||
|
.filter(BagMergeRecord.is_initiator == True)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for record in merge_records:
|
||||||
|
merge_name_by_src[record.src_name] = record.joined_name
|
||||||
|
|
||||||
|
tag_events = (
|
||||||
|
TaggingEvents.query.options(
|
||||||
|
joinedload(TaggingEvents.level1_tag),
|
||||||
|
joinedload(TaggingEvents.level2_tag),
|
||||||
|
joinedload(TaggingEvents.level3_tag),
|
||||||
|
joinedload(TaggingEvents.level4_tag),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
TaggingEvents.bag_id.in_(bag_ids),
|
||||||
|
TaggingEvents.is_deleted == False,
|
||||||
|
)
|
||||||
|
.order_by(TaggingEvents.bag_id.asc(), TaggingEvents.ts_event.asc())
|
||||||
|
.all()
|
||||||
|
if bag_ids
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
tag_events_by_bag = {}
|
||||||
|
for ev in tag_events:
|
||||||
|
tag_events_by_bag.setdefault(ev.bag_id, []).append(ev)
|
||||||
|
|
||||||
|
def _tags_from_flags(high_speed, urban, parking):
|
||||||
|
tags = []
|
||||||
|
if high_speed == 1:
|
||||||
|
tags.append("highway")
|
||||||
|
if urban == 1:
|
||||||
|
tags.append("city")
|
||||||
|
if parking == 0:
|
||||||
|
tags.append("driving")
|
||||||
|
return tags
|
||||||
|
|
||||||
|
def _nodes_and_level(level1, level2, level3, level4):
|
||||||
|
nodes = [level1 or "", level2 or "", level3 or "", level4 or ""]
|
||||||
|
first_empty_index = next(
|
||||||
|
(idx for idx, node in enumerate(nodes) if node == ""), -1
|
||||||
|
)
|
||||||
|
if first_empty_index == -1:
|
||||||
|
fst_node_level = len(nodes) - 1
|
||||||
|
else:
|
||||||
|
fst_node_level = first_empty_index - 1
|
||||||
|
return nodes, fst_node_level
|
||||||
|
|
||||||
|
def _build_payload(
|
||||||
|
bag_name,
|
||||||
|
level1,
|
||||||
|
level2,
|
||||||
|
level3,
|
||||||
|
level4,
|
||||||
|
start_sec,
|
||||||
|
end_sec,
|
||||||
|
comments,
|
||||||
|
high_speed,
|
||||||
|
urban,
|
||||||
|
parking,
|
||||||
|
):
|
||||||
|
nodes, fst_node_level = _nodes_and_level(level1, level2, level3, level4)
|
||||||
|
return {
|
||||||
|
"bag_name": bag_name,
|
||||||
|
"nodes": nodes,
|
||||||
|
"start_time": start_sec if start_sec is not None else 0,
|
||||||
|
"end_time": end_sec if end_sec is not None else 0,
|
||||||
|
"comments": comments or "",
|
||||||
|
"tags": _tags_from_flags(high_speed, urban, parking),
|
||||||
|
"fst_node_level": fst_node_level,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _safe_name(tag):
|
||||||
|
return tag.name if tag else ""
|
||||||
|
|
||||||
|
def _payload_from_event(event, bag_name):
|
||||||
|
return _build_payload(
|
||||||
|
bag_name,
|
||||||
|
_safe_name(event.level1_tag),
|
||||||
|
_safe_name(event.level2_tag),
|
||||||
|
_safe_name(event.level3_tag),
|
||||||
|
_safe_name(event.level4_tag),
|
||||||
|
event.front_start_sec,
|
||||||
|
event.front_end_sec,
|
||||||
|
event.note,
|
||||||
|
event.high_speed,
|
||||||
|
event.urban,
|
||||||
|
event.parking,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _payload_from_bag(bag, bag_name):
|
||||||
|
return _build_payload(
|
||||||
|
bag_name,
|
||||||
|
_safe_name(bag.level1_tag),
|
||||||
|
_safe_name(bag.level2_tag),
|
||||||
|
_safe_name(bag.level3_tag),
|
||||||
|
_safe_name(bag.level4_tag),
|
||||||
|
bag.front_start_sec,
|
||||||
|
bag.front_end_sec,
|
||||||
|
bag.comment1,
|
||||||
|
bag.high_speed,
|
||||||
|
bag.urban,
|
||||||
|
bag.parking,
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, item in enumerate(param_list):
|
||||||
|
bag_failures = []
|
||||||
|
try:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValueError("单条数据必须为对象")
|
||||||
|
if "rowid" not in item:
|
||||||
|
raise ValueError("参数缺少必要的'rowid'字段")
|
||||||
|
|
||||||
|
current_rowid = item["rowid"]
|
||||||
|
bag_file = bag_by_id.get(current_rowid)
|
||||||
|
if not bag_file:
|
||||||
|
raise ValueError("未找到对应ID的本地记录")
|
||||||
|
|
||||||
|
raw_params = item.get("params")
|
||||||
|
current_param = (
|
||||||
|
item.get("param") if isinstance(item.get("param"), dict) else None
|
||||||
|
)
|
||||||
|
bag_name = item.get("bag_name")
|
||||||
|
if not bag_name and current_param:
|
||||||
|
bag_name = current_param.get("bag_name")
|
||||||
|
if not bag_name and isinstance(raw_params, list) and raw_params:
|
||||||
|
first_param = raw_params[0]
|
||||||
|
if isinstance(first_param, dict):
|
||||||
|
bag_name = first_param.get("bag_name")
|
||||||
|
if not bag_name:
|
||||||
|
bag_name = (
|
||||||
|
merge_name_by_src.get(bag_file.file_name)
|
||||||
|
or bag_file.file_name
|
||||||
|
)
|
||||||
|
if not bag_name:
|
||||||
|
raise ValueError("bag_name为空")
|
||||||
|
|
||||||
|
payloads = []
|
||||||
|
if raw_params is not None:
|
||||||
|
if not isinstance(raw_params, list) or not raw_params:
|
||||||
|
raise ValueError("params必须为非空数组")
|
||||||
|
for param in raw_params:
|
||||||
|
if not isinstance(param, dict):
|
||||||
|
raise ValueError("params元素必须为对象")
|
||||||
|
if not param.get("bag_name"):
|
||||||
|
param["bag_name"] = bag_name
|
||||||
|
payloads.append((param, None))
|
||||||
|
elif current_param:
|
||||||
|
if not current_param.get("bag_name"):
|
||||||
|
current_param["bag_name"] = bag_name
|
||||||
|
payloads.append((current_param, None))
|
||||||
|
else:
|
||||||
|
events = tag_events_by_bag.get(current_rowid, [])
|
||||||
|
if events:
|
||||||
|
for ev in events:
|
||||||
|
payloads.append((_payload_from_event(ev, bag_name), ev))
|
||||||
|
else:
|
||||||
|
payloads.append((_payload_from_bag(bag_file, bag_name), None))
|
||||||
|
except Exception as e:
|
||||||
|
bag_failures.append(
|
||||||
|
{
|
||||||
|
"index": idx,
|
||||||
|
"rowid": item.get("rowid") if isinstance(item, dict) else None,
|
||||||
|
"message": f"处理失败:{str(e)}",
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result["fail_count"] += 1
|
||||||
|
result["fail_details"].extend(bag_failures)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ev_idx, (payload, ev) in enumerate(payloads):
|
||||||
|
# print(
|
||||||
|
# f"【insert_rootdb】发送到RootDB: rowid={current_rowid}, "
|
||||||
|
# f"event_index={ev_idx}, event_id={ev.event_id if ev else None}, "
|
||||||
|
# f"payload={payload}"
|
||||||
|
# )
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url=API_URL,
|
||||||
|
json=[payload],
|
||||||
|
timeout=TIMEOUT,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if not isinstance(response_data, list) or len(response_data) == 0:
|
||||||
|
raise ValueError("远程API返回格式错误:预期非空列表")
|
||||||
|
if not isinstance(response_data[0], dict) or "success" not in response_data[0]:
|
||||||
|
raise ValueError("远程API返回缺少'success'字段")
|
||||||
|
|
||||||
|
if not response_data[0].get("success"):
|
||||||
|
bag_failures.append(
|
||||||
|
{
|
||||||
|
"index": idx,
|
||||||
|
"rowid": current_rowid,
|
||||||
|
"event_index": ev_idx,
|
||||||
|
"event_id": ev.event_id if ev else None,
|
||||||
|
"message": "远程API插入失败",
|
||||||
|
"remote_response": response_data[0],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
bag_failures.append(
|
||||||
|
{
|
||||||
|
"index": idx,
|
||||||
|
"rowid": current_rowid,
|
||||||
|
"event_index": ev_idx,
|
||||||
|
"event_id": ev.event_id if ev else None,
|
||||||
|
"message": f"网络请求失败:{str(e)}",
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
bag_failures.append(
|
||||||
|
{
|
||||||
|
"index": idx,
|
||||||
|
"rowid": current_rowid,
|
||||||
|
"event_index": ev_idx,
|
||||||
|
"event_id": ev.event_id if ev else None,
|
||||||
|
"message": f"处理失败:{str(e)}",
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if bag_failures:
|
||||||
|
result["fail_count"] += 1
|
||||||
|
result["fail_details"].extend(bag_failures)
|
||||||
|
continue
|
||||||
|
|
||||||
|
bag_file.sync_status = SyncStatus.SYNCED
|
||||||
|
bag_file.qa_confirm_time = datetime.now()
|
||||||
|
result["success_count"] += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return (
|
||||||
|
jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": f"处理完成,成功{result['success_count']}条,失败{result['fail_count']}条",
|
||||||
|
"result": result,
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({"success": False, "message": f"服务器处理错误:{str(e)}"}), 500
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.models import EventSource, TaggingEvents, VerdictStatus
|
||||||
|
|
||||||
|
def create_tagging_event(db, **kwargs):
|
||||||
|
"""
|
||||||
|
创建并提交TaggingEvents记录
|
||||||
|
参数:
|
||||||
|
db: SQLAlchemy数据库实例
|
||||||
|
kwargs: 字段键值对,支持所有非主键字段
|
||||||
|
|
||||||
|
返回: (success: bool, message: str, event: TaggingEvents)
|
||||||
|
"""
|
||||||
|
# 1. 创建事件对象并设置属性
|
||||||
|
event = TaggingEvents()
|
||||||
|
|
||||||
|
# 2. 动态设置字段值
|
||||||
|
valid_fields = {
|
||||||
|
'bag_id', 'source', 'rule_version_id', 'reviewer_id', 'verdict',
|
||||||
|
'level1_tag_id', 'level2_tag_id', 'level3_tag_id', 'level4_tag_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in kwargs.items():
|
||||||
|
if field in valid_fields:
|
||||||
|
setattr(event, field, value)
|
||||||
|
# 自动处理特殊字段类型[1,4](@ref)
|
||||||
|
elif field == 'ts_event' and isinstance(value, datetime):
|
||||||
|
event.ts_event = value
|
||||||
|
|
||||||
|
# 3. 枚举字段验证[1](@ref)
|
||||||
|
if 'source' in kwargs and not isinstance(kwargs['source'], EventSource):
|
||||||
|
return False, "source必须是EventSource枚举类型", None
|
||||||
|
|
||||||
|
if 'verdict' in kwargs and not isinstance(kwargs['verdict'], VerdictStatus):
|
||||||
|
return False, "verdict必须是VerdictStatus枚举类型", None
|
||||||
|
|
||||||
|
# 4. 提交到数据库[2,4](@ref)
|
||||||
|
try:
|
||||||
|
db.session.add(event)
|
||||||
|
db.session.commit()
|
||||||
|
return True, "记录创建成功", event
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
# app.logger.error(f"数据库写入失败: {str(e)}")
|
||||||
|
return False, f"数据库错误: {str(e)}", None
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
# app.logger.error(f"未知错误: {str(e)}")
|
||||||
|
return False, f"系统错误: {str(e)}", None
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from flask import Blueprint, current_app, request, jsonify
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
import requests
|
||||||
|
from sqlalchemy import func
|
||||||
|
from app import db
|
||||||
|
from app.models import Fst
|
||||||
|
from app.utils.fst_tree import build_tree
|
||||||
|
|
||||||
|
|
||||||
|
label_bp = Blueprint('label', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def format_fst_item(fst_item):
|
||||||
|
"""将Fst对象转换为指定格式的字典"""
|
||||||
|
# 从reserved_json中获取type,如果没有则默认为空字符串
|
||||||
|
item_type = ""
|
||||||
|
if fst_item.reserved_json and "type" in fst_item.reserved_json:
|
||||||
|
item_type = fst_item.reserved_json["type"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"date": fst_item.update_time.strftime('%Y-%m-%d'), # 格式化日期
|
||||||
|
"id": fst_item.id,
|
||||||
|
"name": fst_item.name,
|
||||||
|
"parentId": None if fst_item.parent_id==591 else fst_item.parent_id, # 注意这里是驼峰命名
|
||||||
|
"name_cn": fst_item.name_cn,
|
||||||
|
"annotation":fst_item.annotation,
|
||||||
|
"level":fst_item.level
|
||||||
|
}
|
||||||
|
|
||||||
|
@label_bp.route('/fst-tree', methods=['GET'])
|
||||||
|
def get_fst_tree():
|
||||||
|
"""获取所有FST数据并格式化为树形结构列表"""
|
||||||
|
try:
|
||||||
|
# 查询所有FST记录
|
||||||
|
all_fst = Fst.query.all()
|
||||||
|
|
||||||
|
# 转换为指定格式
|
||||||
|
formatted_data = [format_fst_item(item) for item in all_fst]
|
||||||
|
|
||||||
|
# 返回JSON响应
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"data": formatted_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@label_bp.route('/create-levelone', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def create_levelone():
|
||||||
|
# 获取请求数据
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 验证必要参数
|
||||||
|
required_fields = ['name', 'level', 'annotation']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||||
|
|
||||||
|
# 验证层级是否为1
|
||||||
|
if data['level'] != 1:
|
||||||
|
return jsonify({'error': 'Level must be 1 for first-level tags'}), 400
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 查询当前最大的id值
|
||||||
|
max_id_result = db.session.query(func.max(Fst.id)).first()
|
||||||
|
max_id = max_id_result[0] if max_id_result[0] is not None else 0
|
||||||
|
|
||||||
|
# 新id为最大id + 1
|
||||||
|
new_id = max_id + 1
|
||||||
|
|
||||||
|
# 创建新的一级标签
|
||||||
|
new_tag = Fst(
|
||||||
|
id=new_id,
|
||||||
|
name=data['name'],
|
||||||
|
level=data['level'],
|
||||||
|
annotation=data['annotation'],
|
||||||
|
parent_id=591, # 固定设置parent_id为591
|
||||||
|
update_time=datetime.now() # 设置更新时间
|
||||||
|
# 其他字段如name_cn、reserved_json、bag_sum如果有默认值可以不用指定
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到数据库会话并提交
|
||||||
|
db.session.add(new_tag)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# print(f"新标签的ID: {new_tag.id}")
|
||||||
|
# 返回创建成功的标签信息
|
||||||
|
return jsonify({
|
||||||
|
'message': 'First-level tag created successfully',
|
||||||
|
'tag': {
|
||||||
|
'id': new_tag.id,
|
||||||
|
'name': new_tag.name,
|
||||||
|
'level': new_tag.level,
|
||||||
|
'annotation': new_tag.annotation,
|
||||||
|
'parent_id': new_tag.parent_id,
|
||||||
|
'update_time': new_tag.update_time.isoformat()
|
||||||
|
}
|
||||||
|
}), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 发生错误时回滚
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@label_bp.route('/update-fst-annotation', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def update_fst_annotation():
|
||||||
|
# 获取请求数据
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 验证必要参数
|
||||||
|
required_fields = ['name', 'level', 'annotation']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 根据name和level查询对应的记录
|
||||||
|
fst_record = Fst.query.filter_by(
|
||||||
|
name=data['name'],
|
||||||
|
level=data['level']
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# 检查记录是否存在
|
||||||
|
if not fst_record:
|
||||||
|
return jsonify({
|
||||||
|
'error': f'No record found with name: {data["name"]} and level: {data["level"]}'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# 更新annotation字段和更新时间
|
||||||
|
fst_record.annotation = data['annotation']
|
||||||
|
fst_record.update_time = datetime.now() # 更新时间戳
|
||||||
|
|
||||||
|
# 提交修改
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 返回更新成功的信息
|
||||||
|
return jsonify({
|
||||||
|
'message': 'Annotation updated successfully',
|
||||||
|
'updated_record': {
|
||||||
|
'id': fst_record.id,
|
||||||
|
'name': fst_record.name,
|
||||||
|
'level': fst_record.level,
|
||||||
|
'annotation': fst_record.annotation,
|
||||||
|
'update_time': fst_record.update_time.isoformat()
|
||||||
|
}
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 发生错误时回滚
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@label_bp.route('/add-fst', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def add_fst():
|
||||||
|
# 获取请求数据
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 验证必要参数
|
||||||
|
required_fields = ['name', 'level', 'parentName', 'annotation']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 根据parentName查询父标签的记录,获取parent_id
|
||||||
|
parent_record = Fst.query.filter_by(name=data['parentName']).first()
|
||||||
|
|
||||||
|
# 检查父标签是否存在
|
||||||
|
if not parent_record:
|
||||||
|
return jsonify({
|
||||||
|
'error': f'Parent tag not found with name: {data["parentName"]}'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# 查询当前最大的id值
|
||||||
|
max_id_result = db.session.query(func.max(Fst.id)).first()
|
||||||
|
max_id = max_id_result[0] if max_id_result[0] is not None else 0
|
||||||
|
|
||||||
|
# 新id为最大id + 1
|
||||||
|
new_id = max_id + 1
|
||||||
|
|
||||||
|
# 创建新的fst记录
|
||||||
|
new_fst = Fst(
|
||||||
|
id=new_id,
|
||||||
|
name=data['name'],
|
||||||
|
level=data['level'],
|
||||||
|
parent_id=parent_record.id, # 使用查询到的父标签ID
|
||||||
|
annotation=data['annotation'],
|
||||||
|
update_time=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到数据库并提交
|
||||||
|
db.session.add(new_fst)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 返回成功信息
|
||||||
|
return jsonify({
|
||||||
|
'message': 'Fst record created successfully',
|
||||||
|
'record': {
|
||||||
|
'id': new_fst.id,
|
||||||
|
'name': new_fst.name,
|
||||||
|
'level': new_fst.level,
|
||||||
|
'parent_id': new_fst.parent_id,
|
||||||
|
'parentName': data['parentName'],
|
||||||
|
'annotation': new_fst.annotation,
|
||||||
|
'update_time': new_fst.update_time.isoformat()
|
||||||
|
}
|
||||||
|
}), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@label_bp.route("/sync-fst", methods=["POST"])
|
||||||
|
def sync_fst():
|
||||||
|
# 1. 解析请求参数
|
||||||
|
params = request.get_json() or {}
|
||||||
|
print(f"【请求参数】原始参数: {params}")
|
||||||
|
|
||||||
|
name = params.get("name", "")
|
||||||
|
parent_name = params.get("parent_name", "")
|
||||||
|
|
||||||
|
# 参数校验
|
||||||
|
if not name or not parent_name:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "缺少必要参数(name 或 parent_name)",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 转发请求配置
|
||||||
|
API_URL = "http://10.0.240.4:5232/api/fst/update"
|
||||||
|
TIMEOUT = 10 # 超时时间(秒)
|
||||||
|
data = {"name": name, "parent_name": parent_name}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 3. 发送转发请求
|
||||||
|
res = requests.post(url=API_URL, json=data, timeout=TIMEOUT)
|
||||||
|
res.raise_for_status() # 自动抛出 4xx/5xx 状态码的异常
|
||||||
|
|
||||||
|
# 4. 处理对方 API 的响应(假设返回 JSON 格式)
|
||||||
|
try:
|
||||||
|
remote_response = res.json()
|
||||||
|
except ValueError:
|
||||||
|
# 对方返回非 JSON 格式响应
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": "同步失败:目标接口返回无效格式",
|
||||||
|
"details": res.text,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 返回成功响应
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "同步成功",
|
||||||
|
"data": remote_response, # 携带对方 API 的返回数据
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
# 捕获所有 requests 相关异常(超时、连接失败、4xx/5xx 等)
|
||||||
|
error_msg = f"同步失败:{str(e)}"
|
||||||
|
print(f"【转发请求错误】{error_msg}")
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": error_msg,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
)
|
||||||
@@ -0,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)}"})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# from flask import Blueprint
|
||||||
|
|
||||||
|
# auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
# from . import routes
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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,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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"])
|
||||||
@@ -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,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
|
||||||
@@ -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}"
|
||||||
@@ -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
|
||||||
Binary file not shown.
+1198
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user