综合设计——多源异构数据采集与融合应用综合实践
| 这个项目属于哪个课程 | 2025数据采集与融合技术 |
|---|---|
| 组名、项目简介 | 组名:好运来 项目需求:智能运动辅助应用,针对用户上传的运动视频(以引体向上为核心),解决传统动作评估依赖主观经验、反馈延迟的问题,提供客观的动作分析与改进建议 项目目标:对用户上传的运动视频进行动作分析、评分,提供个性化改进意见,包含完整的用户成长记录和反馈系统,帮助用户科学提升运动水平 技术路线:基于Vue3+Python+openGauss的前后端分离架构,前端使用Vue3实现用户界面和可视化,后端用Python集成MediaPipe进行姿态分析算法处理,数据库采用openGauss存储用户数据和运动记录,实现引体向上动作分析系统 |
| 团队成员学号 | 102302148(谢文杰)、102302149(赖翊煊)、102302150(蔡骏)、102302151(薛雨晨)、102302108(赵雅萱)、102302111(海米沙)、102302139(尚子骐)、022304105(叶骋恺) |
| 这个项目的目标 | 通过上传的运动视频,运用人体姿态估计算法(双视角协同分析:正面看握距对称性、身体稳定性,侧面看动作完整性、躯干角度),自动识别身体关键点,分解动作周期、识别违规代偿,生成量化评分、可视化报告与个性化改进建议;同时搭建用户成长记录与反馈系统,存储用户数据与运动记录,最终打造低成本、高精度的自动化评估工具,助力个人训练、体育教育等场景的科学化训练,规避运动损伤、提升训练效果 |
| 其他参考文献 | [1] ZHOU P, CAO J J, ZHANG X Y, et al. Learning to Score Figure Skating Sport Videos [J]. IEEE Transactions on Circuits and Systems for Video Technology, 2019. 1802.02774 [2] Toshev, A., & Szegedy, C. (2014). DeepPose: Human Pose Estimation via Deep Neural Networks. DeepPose: Human Pose Estimation via Deep Neural Networks |
| 码云链接(代码已汇总,各小组成员代码不分开放) | 前后端代码:https://gitee.com/wsxxs233/SoftWare 爬虫代码:https://gitee.com/sike-0420/kuku/tree/master/bilibili spider |
一.项目背景
随着全民健身的深入与健身文化的普及,以引体向上为代表的自重训练,因其便捷性与高效性,成为衡量个人基础力量与身体素质的重要标志,广泛应用于学校体测、军事训练及大众健身。然而,传统的动作评估高度依赖教练员的肉眼观察与主观经验,存在标准不一、反馈延迟、难以量化等局限性。在缺少专业指导的环境中,训练者往往难以察觉自身动作模式的细微偏差,如借力、摆动、幅度不足等,这不仅影响训练效果,长期更可能导致运动损伤。如何将人工智能与计算机视觉技术,转化为每个人触手可及的“AI教练”,提供客观、即时、精准的动作反馈,已成为提升科学化训练水平的一个迫切需求。
二.项目概述:
本项目旨在开发一套基于计算机视觉的智能引体向上动作分析与评估系统。系统通过训练者上传的视频,运用先进的人体姿态估计算法,自动识别并追踪身体关键点。针对引体向上动作的复杂性,我们创新性地构建了双视角协同分析框架:正面视角专注于分析握距对称性、身体稳定性和左右平衡,确保动作的规范与基础架构;侧面视角则着重评估动作的完整性、躯干角度与发力模式,判断动作幅度与效率。通过多维度量化指标,系统能够自动分解动作周期、识别违规代偿,并生成直观的可视化报告与改进建议。最终,本项目致力于打造一个低成本、高精度的自动化评估工具,为个人训练者、体育教育及专业机构提供一种数据驱动的科学训练辅助解决方案。、
三.项目分工:
- 蔡骏:负责用户界面前端所需前端功能的构建。
- 赵雅萱:负责管理员系统构建。
- 薛雨晨:实现功能部署到服务器的使用,以及前后端接口的书写修订。
- 海米沙:墨刀进行原型设计,实时记录市场调研结果并汇报分析需求,项目logo及产品名称设计,进行软件测试。
- 谢文杰:负责正面评分标准制定,搭建知识库。
- 赖翊煊:负责侧面评分标准制定,API接口接入AI
- 叶骋恺:负责数据库方面创建与设计
- 尚子琪:负责进行爬虫爬取对应相关视频,进行软件测
四.个人贡献
本人主要负责后端代码的编写,包括功能接口编写,资源路由,作业管理与数据库调用。同时负责所有功能整合与调试,服务部署等工作。
4.1 后端服务
后端服务器使用了flask服务器进行功能开发,在部署阶段使用了gunicorn服务器进行业务处理。使用了flask的蓝图对项目各个模块进行划分,便于开发调试。认证管理使用jwt工具对安全页面进行保护。后端代码结构:
│ gunicorn.conf.py
│ run.py
│
├─app
│ │ admin.py
│ │ auth.py
│ │ celery_app.py
│ │ config.py
│ │ config.template.py
│ │ database.py
│ │ example.py
│ │ feedback.py
│ │ history.py
│ │ redis_manager.py
│ │ requirements.txt
│ │ train_plan.py
│ │ upload.py
│ │ init.py
│ │
│ ├─api
│ │ celery_tasks.py
│ │ mysql.py
│ │ opengauss.py
│ │ process_front.py
│ │ process_front.py.back
│ │ process_side.py
│ │ tools.py
│ │ init.py
│
│
├─docker
│ │ Dockerfile
│ │
│ ├─celery
│ │ Dockerfile
│ │
│ └─server
│ Dockerfile
│
├─example
│ ├─thumbnail
│ └─video
└─uploads
4.1.1 登录模块
这个模块注册了/auth路由,所有涉及认证的内容都走这个路由,主要功能如下:
- login:负责接收登录表单并进行身份认证,在认证成功后返回短期token。
- register:负责接收注册表单,对信息进行核查,无误后写入数据库。
- profile:负责个人信息的获取,通过token进行身份验证,通过后返回用户信息,前端自动登录与自动跳转是通过这个接口进行验证的。
- change_password:用于用户密码的修改,验证用户信息通过后进行修改操作。
- update_simple_profile:用于用户基本信息的修改。
- refresh:刷新token使用,使用户在登录活跃期不会因为token过期导致重新登录。
这个模块逻辑都比较简单,就不多说明了。
auth.py
import datetime
import hashlib
from flask import Flask, request, jsonify,Blueprint
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from .api.tools import error_response, success_response, hash_password
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
from .database import get_db
from . import config
system=get_db(config.DataBase_Name)
@auth_bp.route('/login', methods=[ 'POST'])
def login():
try:
data = request.get_json()
if not data:
return error_response('请求数据不能为空')
username = data.get('username', '').strip()
password = data.get('password', '').strip()
if not username or not password:
return error_response('用户名和密码不能为空')
# 验证用户
user = system.user_manager.get_user_by_username(username)
if not user:
return error_response('用户不存在',400)
hashed_password = hash_password(password)
if user['password'] != hashed_password:
return error_response('密码错误',401)
# 添加登录记录
system.login_manager.add_login_record(user['id'])
# 生成JWT令牌
access_token = create_access_token(identity=str(user['id']))
return success_response(
data={
'token': access_token,
'user': {
'id': user['id'],
'username': user['username'],
'role': user['role'],
'height': user['height'],
'weight': user['weight']
}
},
message='登录成功'
)
except Exception as e:
return error_response(f'登录失败: {str(e)}', 500)
@auth_bp.route('/register', methods=['POST'])
def register():
"""用户注册"""
try:
data = request.get_json()
if not data:
return error_response('请求数据不能为空')
username = data.get('username', '').strip()
password = data.get('password', '').strip()
height = data.get('height')
weight = data.get('weight')
# 验证必填字段
if not username or not password:
return error_response('用户名和密码不能为空')
if len(username) < 3:
return error_response('用户名至少3位')
if len(password) < 6:
return error_response('密码至少6位')
# 检查用户是否已存在
existing_user = system.user_manager.get_user_by_username(username)
if existing_user:
return error_response('用户名已存在')
# 创建用户
hashed_password = hash_password(password)
if system.user_manager.add_user(username, hashed_password, height, weight):
return success_response(message='注册成功')
else:
return error_response('注册失败')
except Exception as e:
return error_response(f'注册失败: {str(e)}', 500)
@auth_bp.route('/profile', methods=['GET'])
@jwt_required()
def get_profile():
"""获取用户信息"""
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user=system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
return success_response(
data={
'id': user['id'],
'username': user['username'],
'role': user['role'],
'height': user['height'],
'weight': user['weight']
}
)
except Exception as e:
return error_response(f'获取用户信息失败: {str(e)}', 500)
@auth_bp.route('/change_password', methods=['PUT'])
@jwt_required()
def change_password():
try:
user_id = get_jwt_identity()
data = request.get_json()
if not data:
return error_response('请求数据不能为空')
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
password=data.get('password')
hashed_password = hash_password(password)
# print(password)
username=None
height =None
weight =None
if system.user_manager.update_user(user['id'],username, hashed_password, height, weight):
return success_response(message='更新成功')
return error_response('更新失败')
except Exception as e:
return error_response(f'更新失败: {str(e)}', 500)
@auth_bp.route('/update_simple_profile', methods=['PUT'])
@jwt_required()
def update_simple_profile():
"""更新用户信息"""
try:
user_id = get_jwt_identity()
data = request.get_json()
if not data:
return error_response('请求数据不能为空')
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
password=None
username=data.get('username', user['username']).strip()
height = data.get('height')
weight = data.get('weight')
if system.user_manager.update_user(user['id'],username, password, height, weight):
return success_response(message='更新成功')
return error_response('更新失败')
except Exception as e:
return error_response(f'更新失败: {str(e)}', 500)
@auth_bp.route('/refresh',methods=['GET'])
@jwt_required()
def refresh():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
system.login_manager.add_login_record(user['id'])
access_token = create_access_token(identity=str(user['id']))
return success_response(
data={
'token': access_token,
},
message='刷新token'
)
except Exception as e:
return error_response(f'服务错误: {str(e)}', 500)
4.1.2 示例视频模块
这个模块负责精选视频等静态资源路由,父路由为/api。
- media/videos:负责获取所有精选视频信息,包括名称,链接,缩略图,时长等信息。
- video/<filename>: 负责获取某个指定视频。
- thumbnail/<filename>:负责获取视频对应的缩略图。
主要问题:相对路径难以控制,绝对路径又不合适。通过定位到项目目录再往下补目录解决的。
example.py
import datetime
import hashlib
import os
from pathlib import Path
from . import config
from flask import Flask, request, jsonify, Blueprint, send_file,send_from_directory
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from .api.tools import error_response, success_response, hash_password
example_bp = Blueprint('example', __name__, url_prefix='/api')
from .database import get_db
system=get_db(config.DataBase_Name)
@example_bp.route('/media/videos', methods=['GET'])
@jwt_required()
def get_media_videos():
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
videos=system.video_manager.get_all_video_records()
return success_response(data=videos)
@example_bp.route('/video/<filename>')
def video(filename):
try:
# print(f"请求视频文件: {filename}")
# 获取项目根目录(app 文件夹的父目录)
base_dir = Path(__file__).parent.parent # 这会指向 app 的父目录
video_path = base_dir / config.VIDEO_FOLDER / filename
# print(f"视频完整路径: {video_path}")
# print(f"路径是否存在: {video_path.exists()}")
if not video_path.exists():
return jsonify({"error": "视频文件不存在"}), 404
# 使用 send_file 自动处理范围请求
# print("使用 send_file 自动处理范围请求")
response = send_file(
str(video_path), # 转换为字符串
as_attachment=False,
conditional=True,
mimetype='video/mp4'
)
# 添加必要的响应头
response.headers['Accept-Ranges'] = 'bytes'
response.headers['Cache-Control'] = 'no-cache'
# print("文件发送成功")
return response
except Exception as e:
print(f"服务器错误详情: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"error": f"服务器错误: {str(e)}"}), 500
@example_bp.route('/thumbnail/<filename>')
def thumbnail(filename):
try:
# print(f"请求视频文件: {filename}")
# 获取项目根目录(app 文件夹的父目录)
base_dir = Path(__file__).parent.parent # 这会指向 app 的父目录
thumbnail_path = base_dir / config.THUMBNAIL_FOLDER / filename
print(f"查找文件路径: {thumbnail_path}")
print(f"文件是否存在: {thumbnail_path.exists()}")
if not thumbnail_path.exists():
return jsonify({"error": "缩略图不存在"}), 404
# 直接返回图片文件
return send_file(str(thumbnail_path))
except Exception as e:
print(f"<UNK>: {str(e)}")
# 返回错误图片
return send_file('static/error.jpg', mimetype='image/jpeg')
4.1.3 反馈信息模块
这个模块复用父路由/api,负责向服务器发送用户反馈信息并存入数据库。
- feedback:向数据库写入用户反馈的内容。
feedback.py
from flask import Flask, request, jsonify,Blueprint
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from . import config
from .api.tools import error_response, success_response, hash_password
feedback_bp = Blueprint('feedback', __name__, url_prefix='/api')
from .database import get_db
system=get_db(config.DataBase_Name)
@feedback_bp.route('/feedback', methods=['POST'])
@jwt_required()
def feedback():
try:
data=request.get_json()
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
content=data.get('content')
email=data.get('email','')
system.feedback_manager.add_feedback(user['id'],content, email)
return success_response('')
except Exception as e:
return error_response( str(e),400)
4.1.4 历史记录模块
该模块复用/api路由,负责所有有关历史记录信息的增删改查。
- history:获取用户的所有历史记录。
- history/detail/<id>:获取具体某条历史记录的内容。
history.py
import datetime
import hashlib
import json
import os
import threading
import time
import uuid
import requests
from flask import Flask, request, jsonify, Blueprint, Response
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from werkzeug.utils import secure_filename
from . import config
from .api.tools import error_response, success_response, hash_password
history_bp = Blueprint('history', __name__, url_prefix='/api')
from .database import get_db
system=get_db(config.DataBase_Name)
@history_bp.route('/history', methods=['GET'])
@jwt_required()
def history():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
historys=system.history_manager.get_history_by_user(user['id'])
# print(historys)
result=[]
for history in historys:
r={}
r['id'] = int(history['id'])
r['project'] = history['project']
r['time'] = history['time'].strftime("%Y-%m-%d %H:%M:%S")
r['score'] = int(history['score'])
r['date'] =history['time'].strftime("%Y-%m-%d")
result.append(r)
# print(result)
data={
'data':result
}
return success_response(data,200)
except Exception as ex:
return error_response(str(ex), 500)
@history_bp.route('/history/detail/<id>', methods=['GET'])
@jwt_required()
def history_detail(id):
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
history = system.history_manager.get_history_records_by_id(int(id))
data={
'project': history['project'],
'time': history['time'].strftime("%Y-%m-%d %H:%M:%S"),
'score': int(history['score']),
'evaluation': history['content'],
}
return success_response(data, 200)
except Exception as ex:
print(ex)
print(system.history_manager.get_history_records_by_id(int(id)))
return error_response(str(ex), 500)
4.1.5 训练计划模块
这个模块负责所有训练计划相关的内容,增删改查等。
- training-plan:写入用户的训练计划到数据库。
- training-plan/list:获取用户所有训练计划。
- training-plan/<id>(PUT):修改某条训练计划的内容。
- training-plan/<id>(DELETE):删除某条训练计划。
train_plan.py
import datetime
import hashlib
import json
import os
import threading
import time
import uuid
import requests
from flask import Flask, request, jsonify, Blueprint, Response
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from werkzeug.utils import secure_filename
from . import config
from .api.tools import error_response, success_response, hash_password
train_bp = Blueprint('train-plan', __name__, url_prefix='/api')
from .database import get_db
system=get_db(config.DataBase_Name)
@train_bp.route('/training-plan', methods=['POST'])
@jwt_required()
def train_plan():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
data=request.json
id=system.training_plan_manager.add_training_plan(user['id'],data['date'],data['project'],data['target'],data['note'])
result={
"id": id,
"date": data['date'],
"project": data['project'],
"target": data['target'],
"note": data['note'],
"completed": False,
"actualCount": 0
}
return success_response(message='创建成功',data=result)
except Exception as e:
return error_response(str(e), 500)
@train_bp.route('/training-plan/list',methods=['GET'])
@jwt_required()
def train_plan_list():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
result=system.training_plan_manager.get_user_plans(user['id'])
data={
'list':result,
'total':len(result)
}
return success_response(message='获取成功',data=data)
except Exception as e:
print(str(e))
return error_response(str(e), 500)
@train_bp.route('/training-plan/<id>',methods=['PUT'])
@jwt_required()
def train_plan_update(id):
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
data=request.json
target=data.get('target',None)
note=data.get('note',None)
actualCount=data.get('actualCount',None)
completed=data.get('completed',None)
bl=system.training_plan_manager.update_training_plan(id,user['id'],target,note,completed,actualCount)
if bl:
result=system.training_plan_manager.get_plan_by_id(id)
return success_response(message='更新成功',data=result)
else:
return error_response('更新失败', 201)
except Exception as e:
print(str(e))
return error_response(str(e), 500)
@train_bp.route('/training-plan/<id>',methods=['DELETE'])
@jwt_required()
def train_plan_delete(id):
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if system.training_plan_manager.delete_training_plan(id,user['id']):
return success_response(message='删除成功')
else:
return error_response('删除失败',201)
except Exception as e:
print(str(e))
return error_response(str(e), 500)
@train_bp.route('/training-plan/trained-dates',methods=['GET'])
@jwt_required()
def train_plan_trained_dates():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
year=request.args.get('year',None)
month=request.args.get('month',None)
print(year,month)
result=system.training_plan_manager.get_trained_date(user['id'],year,month)
result['date']=result['date']
return success_response(message='获取成功',data=result)
except Exception as e:
print(str(e))
return error_response(str(e), 500)
4.1.6 上传模块
核心业务在这个模块实现,路由复用/api,实现测评视频的上传,任务生成与大模型调用。
- clear:清除当前会话,用于开启新一轮对话。
- upload:负责视频接收,生成任务发送往redis,后续任务实现由celery负责。
- evaluate/result/<task_id>:负责任务状态与结果查询。
- chat:负责与大模型进行对话,返回流式信息。
- save:负责测评记录的保存。
这个模块起初没有使用redis管理短期数据,后面发现在多线程环境下出现任务丢失的情况,故使用redis专门管理数据。chat功能调用我们在云上托管的dify聊天机器,管理用户的会话id,上下文由平台自己管理省去了一部分工作,因为我们使用的deepseek api自身不支持多轮对话,需要自行管理上下文,所以使用dify作为我们定制的api。
upload.py
import datetime
import hashlib
import json
import os
import threading
import time
import uuid
from .api import process_side
from .api import process_front
import requests
from flask import Flask, request, jsonify, Blueprint, Response
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from werkzeug.utils import secure_filename
from .api.celery_tasks import process_video_task
from . import config
from .redis_manager import redis_manager
from .api.tools import error_response, success_response, hash_password
upload_bp = Blueprint('upload', __name__, url_prefix='/api')
from .database import get_db
system=get_db(config.DataBase_Name)
os.makedirs(config.UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in config.ALLOWED_EXTENSIONS
# conversation={}
# tasks = {}
#
# user_task={}
# def process_video(task_id,front_video_path, side_video_path):
# try:
# side=process_side.process(side_video_path)
# front,num=process_front.process(front_video_path)
# if side is None or front is None:
# raise Exception('视频处理失败,请检查视频清晰度或背景颜色')
# result = {
# 'message': front+side,
# }
# # 更新任务状态
# tasks[task_id]['status'] = 'completed'
# tasks[task_id]['result'] = result
# tasks[task_id]['project']='引体向上'+str(num)+'个'
#
#
# except Exception as e:
# tasks[task_id]['status'] = 'error'
# tasks[task_id]['message'] = str(e)
@upload_bp.route('/clear',methods=['GET'])
@jwt_required()
def clear():
print('clear')
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
# if user_task.get(user_id):
#
# tasks[user_task[user_id]]=None
# user_task[user_id] = None
# if conversation.get(user_id):
#
# conversation[user_id]=''
redis_manager.delete_task(redis_manager.get_user_task(user_id))
redis_manager.clear_conversation(user_id)
return success_response()
@upload_bp.route('/upload', methods=['POST'])
@jwt_required()
def upload():
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if 'front_video' not in request.files or 'side_video' not in request.files:
return jsonify({'success': False, 'message': '请上传正面和侧面两个视频'})
front_video = request.files['front_video']
side_video = request.files['side_video']
# 检查文件是否选择
if front_video.filename == '' or side_video.filename == '':
return jsonify({'success': False, 'message': '请选择视频文件'})
# 检查文件格式
if not (allowed_file(front_video.filename) and allowed_file(side_video.filename)):
return jsonify({'success': False, 'message': '不支持的文件格式'})
# 生成任务ID
task_id = str(uuid.uuid4())
# 保存文件
front_filename = secure_filename(front_video.filename)
side_filename = secure_filename(side_video.filename)
front_path = os.path.join(config.UPLOAD_FOLDER, f"{task_id}_front_{front_filename}")
side_path = os.path.join(config.UPLOAD_FOLDER, f"{task_id}_side_{side_filename}")
front_video.save(front_path)
side_video.save(side_path)
redis_manager.create_task(task_id,{
'status': 'processing',
'result': None
})
# 初始化任务状态
# tasks[task_id] = {
# 'status': 'processing',
# 'result': None
# }
# 在后台处理视频评估
# thread = threading.Thread(
# target=process_video,
# args=(task_id, front_path, side_path)
# )
# thread.daemon = True
# thread.start()
async_task = process_video_task.delay(task_id, front_path, side_path, user_id)
# 存储Celery任务ID
redis_manager.update_task(task_id, {
'celery_task_id':async_task.id,
'user_id': user_id,
})
redis_manager.set_user_task(user_id, task_id)
# tasks[task_id]['celery_task_id'] = async_task.id
# tasks[task_id]['user_id'] = user_id
# print('taskid',task_id)
# print('celery_id',tasks[task_id]['celery_task_id'])
print(redis_manager.get_task(task_id))
return jsonify({
'success': True,
'task_id': task_id,
'message': '视频上传成功,正在分析中...'
})
@upload_bp.route('/evaluate/result/<task_id>', methods=['GET'])
@jwt_required()
def get_evaluation_result(task_id):
"""
获取评估结果
"""
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
# if task_id not in tasks:
# return jsonify({'success': False, 'message': '任务不存在'})
# if task_id!=user_task[user_id]:
# return jsonify({'success': False, 'message': '权限不足'})
# task = tasks[task_id]
#
# if task['status'] == 'completed':
# print(task['result'])
# return jsonify({
# 'success': True,
# 'status': 'completed',
# 'result': task['result']
# })
# elif task['status'] == 'processing':
# return jsonify({
# 'success': True,
# 'status': 'processing',
# 'message': '视频分析中...'
# })
# else:
# return jsonify({
# 'success': False,
# 'status': 'error',
# 'message': '分析过程中出现错误'
# })
# 检查任务是否存在
# if task_id not in tasks:
# return jsonify({'success': False, 'message': '任务不存在'})
#
# # 检查任务权限
# if tasks[task_id]['user_id'] != user_id:
# return jsonify({'success': False, 'message': '权限不足'})
if redis_manager.get_task(task_id) is None:
return jsonify({'success': False, 'message': '任务不存在'})
# print(redis_manager.get_task(task_id),user_id)
if redis_manager.get_task(task_id)['user_id'] != int(user_id):
return jsonify({'success': False, 'message': '权限不足'})
# task = tasks[task_id]
task=redis_manager.get_task(task_id)
# print(task)
# 如果有Celery任务ID,查询任务状态
if 'celery_task_id' in task:
celery_task = process_video_task.AsyncResult(task['celery_task_id'])
if celery_task.ready():
if celery_task.successful():
result = celery_task.result
task['status'] = 'completed'
task['result'] = result.get('result')
task['project'] = result.get('project')
redis_manager.update_task(task_id, task)
else:
task['status'] = 'error'
task['error'] = str(celery_task.result)
if task['status'] == 'completed':
return jsonify({
'success': True,
'status': 'completed',
'result': task['result'],
'project': task.get('project', '')
})
elif task['status'] == 'processing':
return jsonify({
'success': True,
'status': 'processing',
'message': '视频分析中...'
})
elif task['status'] == 'error':
return jsonify({
'success': False,
'status': 'error',
'message': task.get('error', '分析过程中出现错误')
})
else:
return jsonify({
'success': False,
'status': 'unknown',
'message': '未知的任务状态'
})
@upload_bp.route('/chat', methods=['POST'])
@jwt_required()
def chat_stream():
"""处理聊天请求并返回流式响应"""
data = request.json
user_input = data.get('message')
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
conversation_id=redis_manager.get_conversation(user_id)
# conversation_id=conversation.get(user_id,'')
print(conversation_id)
def generate():
# 调用 Dify API
url = config.DIFY_API_URL
headers = {
"Authorization": f"Bearer {config.DIFY_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"inputs": {},
"query": user_input,
"response_mode": "streaming",
"user": user_id,
"conversation_id": conversation_id
}
# print(conversation.get(user_id,''))
# print(payload)
response = requests.post(url, json=payload, headers=headers, stream=True)
if response.status_code == 200:
for line in response.iter_lines(decode_unicode=True):
# print(line)
if line and line.startswith('data:'):
data = line[5:] # 移除 'data:' 前缀
try:
data_dict = json.loads(data)
# if conversation.get(user_id, '') == '':
# conversation[user_id] = data_dict.get('conversation_id', '')
if redis_manager.get_conversation(user_id) == '':
redis_manager.set_conversation(user_id, data_dict.get('conversation_id',''))
if data_dict['event'] == 'message_end':
# yield f"data: {json.dumps({'event': 'end'})}\n\n"
break
# 提取关键信息
event_data = {
'content': data_dict.get('answer', ''),
}
yield f"data: {json.dumps(event_data)}\n\n"
except json.JSONDecodeError:
continue
else:
error_data = {'error': f'请求失败: {response.status_code}'}
yield f"data: {json.dumps(error_data)}\n\n"
return Response(
generate(),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type'
}
)
@upload_bp.route('/save', methods=['POST'])
@jwt_required()
def save():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
msg=request.json['message'].replace('*','').replace('#','').replace('-','')
line=msg.split('\n')
message=''
score=None
for line in line:
line = line.strip()
if line.startswith('评分'):
try :
score=int(line[3:].strip())
except ValueError:
continue
elif line !='':
message=message+line+'\n'
# print(score)
# print(message)
project=redis_manager.get_task(redis_manager.get_user_task(user_id))['project']
rid=system.rating_manager.add_rating(score,message)
system.history_manager.add_history_record(user['id'],rid,project)
return success_response('保存记录成功')
except Exception as e:
return error_response('服务错误', 500)
4.1.7 管理员模块
这个模块负责处理管理员用户的一些相关操作:包括用户流量分析,用户反馈处理,精选视频上传。路由使用/api/admin
- media/upload:负责精选视频的上传,生成独特的文件名与访问链接生成。
- media/videos/<id>(DELETE):负责删除精选视频。
- media/videos:获取所有精选视频,加了管理员权限验证。
- dashboard/stats:管理员页的状态栏信息获取,包括单日登录量、注册量、待处理反馈与媒体数目。
- dashboard/chart-data:从数据库获取用户一周的登录量与注册量。
- feedback/pending:获取所有待处理的用户反馈数量。
- feedback_all:获取所有待处理的用户反馈内容。
- feedback/<int:id>/process:处理用户反馈。
- feedback/<int:id>/ignore:忽略用户反馈。
这个模块是后期新增的,所以按部就班的按照前端提供的接口信息编写的。
admin.py
import datetime
import hashlib
import json
import os
import threading
import time
import uuid
import numpy as np
import requests
from PIL import Image
from flask import Flask, request, jsonify, Blueprint, Response
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt
from jwt import ExpiredSignatureError, InvalidTokenError
from werkzeug.utils import secure_filename
from . import config
from .api.tools import error_response, success_response
from moviepy import VideoFileClip
admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin')
from .database import get_db
system=get_db(config.DataBase_Name)
def allowed_file(filename):
"""检查文件扩展名是否允许"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in config.ALLOWED_EXTENSIONS
def get_video_duration(file_path):
"""获取视频时长(需要安装moviepy或opencv)"""
try:
# 方法1: 使用moviepy(推荐)
with VideoFileClip(file_path) as video:
return video.duration
except Exception as e:
return 0
def extract_first_frame_moviepy(video_path, thumbnail_path=None, size=(320, 180)):
"""
使用moviepy提取视频第一帧
Args:
video_path: 视频文件路径
thumbnail_path: 缩略图保存路径
size: 缩略图尺寸 (宽, 高)
Returns:
str: 缩略图保存路径
"""
try:
# 打开视频文件
with VideoFileClip(video_path) as video:
# 获取第一帧
frame = video.get_frame(2) # 0表示第一秒的第一帧
# 转换为PIL Image
pil_image = Image.fromarray((frame).astype(np.uint8))
# print(pil_image.size)
# 调整大小
pil_image.thumbnail(size, Image.Resampling.LANCZOS)
if thumbnail_path:
pil_image.save(thumbnail_path, quality=100)
return thumbnail_path
else:
return pil_image
except Exception as e:
print(e)
return ''
@admin_bp.route('/media/upload', methods=['POST'])
@jwt_required()
def upload():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
if 'multipart/form-data' not in request.content_type:
return error_response('请求类型必须是 multipart/form-data',400)
name = request.form.get('name')
annotation = request.form.get('annotation', '')
file = request.files.get('file')
if not name:
return error_response('视频名称不能为空',400)
if not file:
return error_response('未提供视频文件',400)
if not allowed_file(file.filename):
return error_response(f'不支持的文件类型。允许的类型: {", ".join(config.ALLOWED_EXTENSIONS)}',400)
original_filename = secure_filename(file.filename)
file_extension = original_filename.rsplit('.', 1)[1].lower()
# 生成唯一文件名
unique_filename = f"{uuid.uuid4().hex}"
# 7. 保存文件
save_path = os.path.join(config.VIDEO_FOLDER, unique_filename+f'.{file_extension}')
thumbnail_path = os.path.join(config.THUMBNAIL_FOLDER, unique_filename+f'.jpeg')
file.save(save_path)
# print(save_path)
# 获取文件大小
file_size = os.path.getsize(save_path)
# 8. 获取视频信息(如时长)
duration = get_video_duration(save_path)
extract_first_frame_moviepy(save_path,thumbnail_path)
# 9. 生成文件URL(根据你的实际部署环境调整)
file_url = f"/api/video/{unique_filename}.{file_extension}"
thumbnail_url= f"/api/thumbnail/{unique_filename}.jpeg"
id=system.video_manager.add_video(name,annotation,file_size,file_url,duration, thumbnail_url)
if id==-1:
os.remove(save_path)
os.remove(thumbnail_path)
response_data = {
'code': 200,
'message': '上传成功',
'data': {
'id': id, # 生成简单ID,实际应该用数据库ID
'name': name,
'annotation': annotation,
'size': file_size,
'url': file_url,
'duration': duration
}
}
return success_response(data=response_data)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/media/videos/<id>', methods=['DELETE'])
@jwt_required()
def delete_video(id):
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
try:
d=system.video_manager.get_video_records_by_id(id)
url=d['url']
thumbnail_url=d['thumbnail']
file_name=config.VIDEO_FOLDER+'/'+url.split('/')[-1]
thumbnail_name=config.THUMBNAIL_FOLDER+'/'+thumbnail_url.split('/')[-1]
if os.path.exists(thumbnail_name):
os.remove(thumbnail_name)
if os.path.exists(file_name):
os.remove(file_name)
res=system.video_manager.delete_video_record(id)
if res:
return success_response(data=res,message='删除成功')
return error_response(message='删除失败')
except Exception as e:
return error_response(str(e), 500)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/media/videos', methods=['GET'])
@jwt_required()
def get_media_videos():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
videos = system.video_manager.get_all_video_records()
return success_response(data=videos,message='获取成功')
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/dashboard/stats', methods=['GET'])
@jwt_required()
def dashboard_stats():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
loginCount=system.login_manager.get_login_count_today()['logincount']
registerCount=system.user_manager.get_register_count_today()['registercount']
pendingFeedback=len(system.feedback_manager.get_pending_feedback())
mediaFiles=len(system.video_manager.get_all_video_records())
return success_response(data={
'loginCount': loginCount,
'registerCount': registerCount,
'pendingFeedback': pendingFeedback,
'mediaFiles': mediaFiles,
},
message='获取成功'
)
except Exception as e:
print(e)
print(system.login_manager.get_login_count_today())
return error_response(str(e), 500)
@admin_bp.route('/dashboard/chart-data', methods=['GET'])
@jwt_required()
def dashboard_chart_data():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
loginCounts=system.login_manager.get_login_count_week()
loginData=[]
dates=[]
for c in loginCounts:
loginData.append(c['logincount'])
dates.append(c['login_date'].strftime('%m-%d'))
registerCounts=system.user_manager.get_register_count_week()
registerData=[]
for c in registerCounts:
registerData.append(c['registercount'])
return success_response(data={
'loginData': loginData,
'registerData': registerData,
'dates': dates,
'totalLogin':sum(loginData),
'totalRegister':sum(registerData)
},message='获取成功')
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/feedback/pending', methods=['GET'])
@jwt_required()
def feedback_pending():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
feedbacks = system.feedback_manager.get_pending_feedback()
return success_response(data={
'list': feedbacks,
'totalCount': len(feedbacks),
},message='获取成功')
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/feedback_all', methods=['GET'])
@jwt_required()
def feedback_all():
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
feedbacks = system.feedback_manager.get_all_feedbacks()
print(feedbacks)
return success_response(
data={
'id': user['id'],
'feedbacks': feedbacks
}
)
except Exception as e:
return error_response(str(e), 400)
@admin_bp.route('/feedback/<int:id>/process', methods=['PUT'])
@jwt_required()
def feedback_process(id):
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
res=system.feedback_manager.upload_feedback(id,"已处理")
if res:
return success_response(data=True,message='处理成功')
else:
return error_response(message='错误')
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/feedback/<int:id>/ignore', methods=['PUT'])
@jwt_required()
def feedback_ignore(id):
try:
user_id = get_jwt_identity()
# 通过ID获取用户信息
# 通过ID获取用户信息
user = system.user_manager.get_user_by_id(user_id)
if user is None:
return error_response('用户不存在', 404)
if user['role'] != 'admin':
return error_response('权限不足', 405)
res=system.feedback_manager.delete_feedback_by_id(id)
if res:
return success_response(data=True,message='忽略成功')
else:
return error_response(message='错误')
except Exception as e:
return error_response(str(e), 500)
4.1.8 作业与数据管理
这里使用celery+redis的方式来实现在生产环境下任务与一些数据管理,包括用户会话id管理,用户任务管理,celery作业管理。
- app/redis_manager.py:这里负责编写用户会话id管理,用户任务管理的实现类与用户id_作业id映射表。
- app/celery_app.py:负责初始化celery作业管理器,配置连接信息,作业状态字段等。
- app/api/celery_task.py:负责任务具体内容实现,包括正侧面视频的处理,状态反馈等。
这里只使用了单个redis节点,没有使用连接池可能会出现神秘问题,不过暂时没有发现。
redis_manager.py
# app/redis_manager.py (新增部分)
import json
import redis
from datetime import timedelta
from . import config
class RedisManager:
def __init__(self):
self.redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
db=0,
decode_responses=True # 注意:存储JSON时需特殊处理
)
# ============== 任务字典管理 (核心新增部分) ==============
def create_task(self, task_id, initial_data=None):
"""
创建新任务记录
initial_data: 初始数据,如 {'status': 'processing', 'user_id': 123}
"""
key = f"task:{task_id}"
data = initial_data or {}
data.setdefault('created_at', self._current_time())
data.setdefault('status', 'pending')
# 使用哈希表存储,方便更新单个字段
mapping = {k: json.dumps(v, ensure_ascii=False) if isinstance(v, (dict, list)) else str(v)
for k, v in data.items()}
# 设置24小时过期
pipeline = self.redis_client.pipeline()
pipeline.hset(key, mapping=mapping)
pipeline.expire(key, timedelta(hours=24))
pipeline.execute()
return True
def update_task(self, task_id, updates):
"""
更新任务信息(部分字段)
updates: 要更新的字段字典,如 {'status': 'processing', 'progress': 50}
"""
key = f"task:{task_id}"
if not self.redis_client.exists(key):
return False
# 准备更新的字段
update_data = {}
for field, value in updates.items():
if isinstance(value, (dict, list)):
update_data[field] = json.dumps(value, ensure_ascii=False)
else:
update_data[field] = str(value)
if update_data:
self.redis_client.hset(key, mapping=update_data)
return True
def get_task(self, task_id):
"""
获取完整任务信息
"""
key = f"task:{task_id}"
data = self.redis_client.hgetall(key)
if not data:
return None
# 反序列化JSON字段
result = {}
for field, value in data.items():
# 尝试解析JSON
if value.startswith('{') or value.startswith('['):
try:
result[field] = json.loads(value)
except json.JSONDecodeError:
result[field] = value
elif value.lower() in ('true', 'false'):
result[field] = value.lower() == 'true'
elif value.isdigit():
result[field] = int(value)
else:
result[field] = value
return result
def delete_task(self, task_id):
"""删除任务记录"""
key = f"task:{task_id}"
return self.redis_client.delete(key)
def task_exists(self, task_id):
"""检查任务是否存在"""
key = f"task:{task_id}"
return self.redis_client.exists(key) > 0
def set_task_result(self, task_id, result, project=None):
"""
专门设置任务结果(完成时调用)
"""
updates = {
'status': 'completed',
'result': result,
'completed_at': self._current_time()
}
if project:
updates['project'] = project
return self.update_task(task_id, updates)
def set_task_error(self, task_id, error_message):
"""
设置任务错误状态
"""
return self.update_task(task_id, {
'status': 'error',
'error': error_message,
'failed_at': self._current_time()
})
# ============== 辅助方法 ==============
def _current_time(self):
"""获取当前时间字符串"""
from datetime import datetime
return datetime.now().isoformat()
# ============== 之前的用户任务和对话管理方法保持不变 ==============
def set_user_task(self, user_id, task_id, expire_hours=24):
"""关联用户和其最新的任务ID"""
key = f"user_task:{user_id}"
self.redis_client.setex(key, timedelta(hours=expire_hours), task_id)
return True
def get_user_task(self, user_id):
"""获取用户最新的任务ID"""
key = f"user_task:{user_id}"
return self.redis_client.get(key)
# ... 其他已有方法保持不变 ...
def set_conversation(self, user_id, conversation_id, expire_hours=24):
"""设置用户的对话ID"""
key = f"conversation:{user_id}"
self.redis_client.setex(key, timedelta(hours=expire_hours), conversation_id)
return True
def get_conversation(self, user_id):
"""获取用户的对话ID"""
key = f"conversation:{user_id}"
result=self.redis_client.get(key)
return result if result is not None else ''
def clear_conversation(self, user_id):
"""清除用户的对话ID"""
key = f"conversation:{user_id}"
return self.redis_client.delete(key)
# 创建全局实例
redis_manager = RedisManager()
celery_app.py
# app/celery_app.py
from celery import Celery
import os
from . import config
def make_celery(app_name=__name__):
# 从配置文件读取Redis配置,如果没有则使用默认值
redis_host = getattr(config, 'REDIS_HOST', 'localhost')
redis_port = getattr(config, 'REDIS_PORT', 6379)
redis_url = f"redis://{redis_host}:{redis_port}/0"
# 注意:include路径要正确指向任务模块
celery = Celery(
app_name,
broker=redis_url,
backend=redis_url,
include=['app.api.celery_tasks'] # 修改为绝对路径
)
# 配置
celery.conf.update(
task_serializer='json',
accept_content=['json'],
result_serializer='json',
timezone='Asia/Shanghai',
enable_utc=True,
task_track_started=True,
task_time_limit=300, # 5分钟超时
task_soft_time_limit=240, # 4分钟软超时
worker_prefetch_multiplier=1,
worker_max_tasks_per_child=100,
broker_connection_retry_on_startup=True,
# 添加更多配置
task_acks_late=True, # 任务完成后才确认
task_reject_on_worker_lost=True, # worker丢失时重新排队
)
return celery
# 创建全局Celery实例
celery_app = make_celery()
celery_task.py
# app/api/celery_tasks.py
import os
from app.celery_app import celery_app
from app.api import process_side, process_front
@celery_app.task(bind=True, name='process_video_task')
def process_video_task(self, task_id, front_video_path, side_video_path, user_id):
"""
Celery任务:处理视频分析
"""
try:
files_to_delete = []
if front_video_path and os.path.exists(front_video_path):
files_to_delete.append(front_video_path)
if side_video_path and os.path.exists(side_video_path):
files_to_delete.append(side_video_path)
# 更新任务进度
self.update_state(
state='PROCESSING',
meta={
'status': 'processing',
'progress': 10,
'message': '开始处理侧面视频...'
}
)
# 处理侧面视频
side = process_side.process(side_video_path)
# 更新进度
self.update_state(
state='PROCESSING',
meta={
'status': 'processing',
'progress': 50,
'message': '侧面视频处理完成,开始处理正面视频...'
}
)
# 处理正面视频
front, num = process_front.process(front_video_path)
if side is None or front is None:
raise Exception('视频处理失败,请检查视频清晰度或背景颜色')
# 构建结果
result = {
'message': front + side,
}
# 更新完成进度
self.update_state(
state='PROCESSING',
meta={
'status': 'processing',
'progress': 90,
'message': '视频处理完成,正在生成报告...'
}
)
# 这里可以添加保存结果到数据库的逻辑
# save_result_to_db(user_id, task_id, result, f'引体向上{num}个')
for file_path in files_to_delete:
try:
if os.path.exists(file_path):
os.remove(file_path)
except Exception as e:
pass
return {
'status': 'completed',
'result': result,
'project': f'引体向上{num}个',
'progress': 100
}
except Exception as e:
try:
for file_path in [front_video_path, side_video_path]:
if file_path and os.path.exists(file_path):
os.remove(file_path)
except Exception as cleanup_error:
pass
# 记录错误
self.update_state(
state='FAILURE',
meta={
'status': 'error',
'progress': 0,
'error': str(e)
}
)
raise e
4.1.9 数据库管理
这一部分主要不是我做的,我只稍加修改了驱动部分。我们早期使用了mysql与opengauss做为我们的数据库,最后我们使用了oceanbase数据库,由于oceanbase基本适配mysql的驱动与代码所以二者用起来暂时没有区别。所以此处只介绍连接驱动的配置。
- mysql使用pymysql作为基本连接驱动,我做了连接池的扩展,连接池工具使用dbutils。
class DatabaseConfig:
"""数据库配置类"""
def __init__(self, host, user, password, database,port=3306):
self.host = host
self.user = user
self.password = password
self.database = database
self.port = int(port)
class DatabaseConnection:
"""数据库连接管理"""
def __init__(self, config: DatabaseConfig):
self.config = config
self.connection = None
def get_connection(self):
pool = PersistentDB(
creator=pymysql,
maxusage=1000, # 单个连接最大使用次数
setsession=[], # 可选的会话命令列表
ping=0, # 检查连接是否可用(0=从不, 1=默认, 2=创建游标时, 4=执行查询时, 7=总是)
closeable=False,
threadlocal=None, # 线程局部变量
host=self.config.host,
port=self.config.port,
user=self.config.user,
password=self.config.password,
database=self.config.database,
charset='utf8mb4',
cursorclass=DictCursor
)
return pool.connection()
- opengauss使用psycopg2进行连接,连接池使用dbutils。与pymysql不同,PersistentDB不支持将cursor_factory作为游标参数传给psycopg2,所以定制一个creator来适配psycopg2。
class DatabaseConnection:
"""数据库连接管理"""
def __init__(self, config: DatabaseConfig):
self.config = config
self.connection = None
def get_connection(self):
def create_connection():
conn = psycopg2.connect(
host=self.config.host,
port=self.config.port,
user=self.config.user,
password=self.config.password,
database=self.config.database,
cursor_factory=RealDictCursor
)
return conn
pool = PersistentDB(
creator=create_connection,
maxusage=1000, # 单个连接最大使用次数
setsession=[], # 可选的会话命令列表
ping=0, # 检查连接是否可用(0=从不, 1=默认, 2=创建游标时, 4=执行查询时, 7=总是)
closeable=False,
threadlocal=None, # 线程局部变量
)
return pool.connection()
4.2 整合与调试
这一块我负责调试后端服务与前端通信,api调试,前端路由管理,安全认证等功能。完成代码详见前端部分。
4.2.1 路由管理与api调用
这里管理前端的路由,与前后端通信时信息的获取。
- /:这里设置了重定向到/login
- /login:这里装载了login组件页
- /main:这里装载了main组件页
- /profile:这里装载了profile组件页
- /admin:这里装载了admin组件页
- api.ts:负责常用auth相关的函数
api.ts
import type { LoginData, RegisterData, AuthResponse, User, refreshTokenResponse ,SimpleProfileForm,FeedbackData} from '@/types/auth';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
export const authAPI = {
async login(loginData: LoginData): Promise<AuthResponse> {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(loginData),
});
if (response.status !== 200 && response.status !== 401 && response.status !== 400) {
throw new Error('登录请求失败');
}
return await response.json();
},
async register(registerData: Omit<RegisterData, 'confirmPassword'>): Promise<AuthResponse> {
const response = await fetch(`${API_BASE_URL}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(registerData),
});
if (!response.ok) {
throw new Error('注册请求失败');
}
return await response.json();
},
async verifyToken(token: string): Promise<User> {
const response = await fetch(`${API_BASE_URL}/auth/profile`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Token验证失败');
}
const data = await response.json();
return data.data;
},
async refreshToken(Token: string): Promise<refreshTokenResponse> {
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${Token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('刷新Token失败');
}
const data = await response.json();
return data;
},
async update_simple_profile(token: string, userData: SimpleProfileForm): Promise<string> {
const response = await fetch(`${API_BASE_URL}/auth/update_simple_profile`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('更新用户资料失败');
}
return 'success';
},
async changePassword(token: string, newPassword: string): Promise<string> {
const response = await fetch(`${API_BASE_URL}/auth/change_password`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ password: newPassword }),
});
if (!response.ok) {
throw new Error('修改密码失败');
}
return 'success';
},
async feedback(token: string, content: FeedbackData): Promise<string> {
const response = await fetch(`${API_BASE_URL}/api/feedback`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(content),
});
if (!response.ok) {
throw new Error('提交反馈失败');
}
return 'success';
}
};
router/index.ts
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/LoginPage.vue'
import Main from '../views/MainPage.vue'
import Admin from '../views/AdminPage.vue'
import profile from '../views/ProfilePage.vue'
import { authAPI } from '@/utils/api'
const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/main',
name: 'Main',
component: Main,
meta: {
KeepAlive: true
}
},
{
path:'/profile',
name:'Profile',
component: profile,
},
{
path:'/admin',
name:'admin',
component:Admin,
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
const checkAuth = async (token: string) => {
try {
await authAPI.verifyToken(token);
}
catch {
return false
}
return true
}
const token = localStorage.getItem('token')||'';
const valid = await checkAuth(token);
// 添加路由守卫
router.beforeEach((to, from, next) => {
// 检查是否需要认证
if (to.meta.requiresAuth) {
if (token==='') {
next('/login')
} else {
if (valid) {
next()
} else {
next('/login')
}
}
} else {
next()
}
})
export default router
剩下一些函数基本一个组件就用一次,就没有编写成ts文件复用了。调试这一块没有什么特别的内容,前端页面写好了测一测功能,看看服务端的log再进行调整。路由管理这一块我主要负责自动登录与自动跳转的设计,大概逻辑就是通过token验证之后自动登录到主页,各个页面验证通不过就自动跳转到登录页,避免一些简单的安全问题。
4.2.2 安全认证
这一块负责一些权限认证,主要是服务端那边就做好了,在这里提一嘴逻辑。还是通过前端发送请求时带着token进行信息认证,通过后返回相关的信息给前端。否则返回错误信息。
4.3 服务部署
这里使用docker进行各个服务的部署,包括前后端,数据库,redis等。这里选择编写一个docker compose文件进行编排。同时我们的整个项目结构为:

docker-compose.yml
#docker-compose模板
#默认三个数据库都会启动,不需要可以删除相关的服务,同时在backend和celery-worker的启动依赖中删除相关的服务
services:
base:
build:
context: ./server
dockerfile: docker/Dockerfile
image: software-base:latest # 指定镜像名称和标签
front:
build:
context: ./web # 构建上下文为当前目录[citation:1]
dockerfile: docker/Dockerfile # 指定 Dockerfile 路径[citation:6]
image: vue-app:1.0 # 支持环境变量设置标签[citation:4]
container_name: software-web
ports:
- "80:80" # 主机端口:容器端口[citation:1]
networks:
- vue-network
restart: unless-stopped # 自动重启策略[citation:7]
environment:
- NODE_ENV=production
volumes:
# 挂载日志文件(可选)
- vue-logs:/var/log/nginx
depends_on:
- backend # 定义服务依赖关系[citation:6]
# 如果需要后端 API,可以在这里添加
backend:
build:
context: ./server
dockerfile: docker/server/Dockerfile
cache_from:
- software-base:latest
image: server:1.0
container_name: software-server
environment:
- FLASK_ENV=production
- MYSQL_HOST=${MYSQL_HOST}
- MYSQL_USER=root
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=app_db
- MYSQL_PORT=${MYSQL_PORT}
- GS_PORT=5432
- GS_PASSWORD=${GS_PASSWORD}
- GS_USERNAME=gaussdb
- GS_DATABASE=postgres
- GS_HOST=opengauss_db
- DATABASE=${DATABASE}
- DIFY_API_URL=${DIFY_API_URL}
- DIFY_API_KEY=${DIFY_API_KEY}
- REDIS_HOST=redis
- REDIS_PORT=6379
networks:
- vue-network
restart: unless-stopped # 自动重启策略[citation:7]
volumes:
- uploads_volume:/app/uploads
depends_on:
- oceanbase
- opengauss_db
- mysql
oceanbase:
image: oceanbase/oceanbase-ce:4.5.0.0-100000012025112711
container_name: software_oceanbase
environment:
- OB_SYS_PASSWORD=${OB_SYS_PASSWORD}
- MINI_MODE=true
ulimits:
nofile:
soft: 20000
hard: 20000
volumes:
- oceanbase_data:/root/obdata
networks:
vue-network:
ipv4_address: 172.18.0.100
entrypoint: ["/bin/bash", "-c"]
command: >
'
# 1. 等待OceanBase自动启动
/usr/sbin/sshd
/root/boot/start.sh &
echo "等待OceanBase启动..."
for i in {1..30}; do
if obclient -h127.1 -uroot -P2881 -p"$OB_SYS_PASSWORD" -e "SELECT 1" 2>/dev/null; then
echo "✅ OceanBase已就绪"
break
fi
echo "等待中..."
sleep 5
done
echo "________________________________"
# 2. 执行初始化SQL - EOF必须单独一行
obclient -h127.1 -uroot -P2881 -p"$OB_SYS_PASSWORD" << "EOF"
SET GLOBAL time_zone = "+08:00";
CREATE DATABASE IF NOT EXISTS app_db;
SELECT "init completed" as status;
EOF
wait
'
mysql:
image: mysql:8.0.17
container_name: software-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: lyxsb666
MYSQL_DATABASE: app_db
MYSQL_USER: root
TZ: Asia/Shanghai
networks:
- vue-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-ppassword"]
timeout: 10s
retries: 10
start_period: 30s
interval: 10s
volumes:
- mysql_data:/var/lib/mysql
opengauss_db:
image: opengauss/opengauss:7.0.0-RC2.B015-openEuler22.03
container_name: software-opengauss
restart: always
environment:
- GS_PASSWORD=${GS_PASSWORD}
- GS_PORT=5432
- TZ=Asia/Shanghai
- GAUSSDATA=/var/lib/opengauss/data # 数据目录
- GAUSSLOG=/var/lib/opengauss/log # 日志目录
volumes:
- opengauss_data:/var/lib/opengauss
privileged: true
# 为容器分配足够的内存
shm_size: '512MB'
networks:
- vue-network
redis:
image: redis:7-alpine
container_name: software_redis
# ports:
# - "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
networks:
- vue-network
celery-worker:
build:
context: ./server
dockerfile: docker/celery/Dockerfile
cache_from:
- software-base:latest
container_name: pullup_celery_worker
environment:
- FLASK_ENV=production
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- redis
- opengauss_db
restart: always
volumes:
- uploads_volume:/app/uploads # 与Web服务共享上传目录
- logs_volume:/app/logs
networks:
- vue-network
networks:
# create a network between sandbox, api and ssrf_proxy, and can not access outside.
vue-network:
driver: bridge # 使用桥接网络[citation:6]
ipam:
config:
- subnet: 172.18.0.0/16 # 使用桥接网络[citation:6]
volumes:
vue-logs:
mysql_data:
opengauss_data:
uploads_volume:
redis_data:
logs_volume:
oceanbase_data:
4.3.1 前端服务
前端服务首先使用node镜像进行编译构建,生成传统的静态页面。再使用nginx镜像进行生产部署。使用nginx服务器进行一些代理。
- Dockerfile:两阶段构建的指令文件
- nginx.config:nginx配置文件
Dockerfile
# 构建阶段
FROM node:24-alpine AS builder
WORKDIR /app
# 复制包文件并安装依赖
COPY web/package*.json ./
RUN npm install
# 复制源代码并构建
COPY web/ .
RUN npm run build
# 生产阶段
FROM nginx:alpine
# 复制构建好的静态文件到 Nginx
COPY --from=builder app/dist /usr/share/nginx/html
# 复制自定义 Nginx 配置
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 开启 gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
resolver 127.0.0.1 valid=30s;
resolver_timeout 5s;
# 处理 Vue Router 的 History 模式
location / {
try_files $uri $uri/ /index.html;
}
location /server/ {
proxy_pass "http://backend:5000/";
# # 设置代理头信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /server/api/upload {
# 重写路径:去掉/server前缀
rewrite ^/server(/.*)$ $1 break;
proxy_pass http://backend:5000;
client_max_body_size 100M;
# 相同的代理头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /server/api/admin/media/upload {
# 重写路径:去掉/server前缀
rewrite ^/server(/.*)$ $1 break;
proxy_pass http://backend:5000;
client_max_body_size 100M;
# 相同的代理头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /server/api/chat {
rewrite ^/server(/.*)$ $1 break;
# 继承父级配置
proxy_pass http://backend:5000;
# 只添加chat特有的配置,不要破坏代理设置
proxy_buffering off; # 流式传输需要关闭缓冲
proxy_read_timeout 300s; # 增加超时时间
# 保持原有的代理头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Dockerfile编写还是比较简单的,把相关操作写进去即可。nginx还是比较难调的,在调试阶段出现大文件拦截,流式传输卡顿,静态资源路由失效等情况,通过设置相关路由的代理配置解决。
- 反向代理:我们通过设置反向代理使浏览器发送的请求能顺利到达后端服务器,同时也能避免开发阶段浏览器出现的跨域拦截情况。
- 大文件拦截:这里通过增大/server/api/upload与/server/api/admin/media/upload的body大小限制,即可顺利解决ngnix拦截大文件的情况。
- 流式传输卡顿:ngnix服务器默认会将数据包自动缓存然后一次发送,所以通过减小缓冲区大小就能解决流式传输卡顿的问题。
- 静态路由失效:虽然服务端设置了静态资源的访问路由,但是请求会先经过nginx,所以如果nginx配置了静态路由,那么请求就不会发送到服务端,所以配置好静态文件路由使请求正确发送带服务端就行了。
4.3.2 基础镜像构建
我们的后端服务代码与celery作业代码是通用的,所以先进行基础镜像的构建,提高资源利用率。我们将基础镜像命名为software-base:latest方便后续使用。
Dockerfile
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 安装系统依赖
# 先备份并替换默认软件源
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
sed -i 's|security.debian.org/debian-security|mirrors.tuna.tsinghua.edu.cn/debian-security|g' /etc/apt/sources.list.d/debian.sources
# 然后更新并安装软件包
RUN apt-get update && apt-get install -y gcc libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY ./app/requirements.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制应用代码
RUN echo "----${date}" > /tmp/build.txt
COPY . .
4.3.3 后端与作业服务
- 服务端
这里使用刚刚构建的基础镜像,编写Dockerfile。
# Dockerfile
FROM software-base:latest
# 暴露端口
EXPOSE 5000
# 启动命令
CMD ["gunicorn", "-c", "gunicorn.conf.py", "run:app"]
- 作业管理
# Dockerfile
FROM software-base:latest
# 暴露端口
EXPOSE 5000
# 启动命令
CMD ["celery", "-A", "app.celery_app", "worker", "--loglevel=info", "--concurrency=4"]
backend的环境变量说明:
-
FLASK_ENV=production 这个用来指定是生产环境还是开发环境
-
MYSQL_HOST=${MYSQL_HOST} 这个用来指定mysql驱动连接的目标host(mysql|oceanbase)
-
MYSQL_USER=root 这个用来指定mysql连接的用户
-
MYSQL_PASSWORD=${MYSQL_PASSWORD} 这个是mysql连接的密码
-
MYSQL_DATABASE=app_db 这个是连接的数据库
-
MYSQL_PORT=${MYSQL_PORT} 这个是mysql连接的端口号(mysql:3306,oceanbase:2881)
-
GS_PORT=5432 这是opengauss的端口号
-
GS_PASSWORD=${GS_PASSWORD} opengauss的密码,需要强密码
-
GS_USERNAME=gaussdb opengauss官方镜像的默认用户
-
GS_DATABASE=postgres opengauss官方镜像的默认数据库
-
GS_HOST=opengauss_db 这个用来指定opengauss驱动连接的目标host
-
DATABASE=${DATABASE} 这个用来指定连接的数据库驱动类型(mysql|opengauss)
-
DIFY_API_URL=${DIFY_API_URL} 这个是dify的api链接
-
DIFY_API_KEY=${DIFY_API_KEY} 这是dify的连接密钥
-
REDIS_HOST=redis
-
REDIS_PORT=6379
4.3.4 redis容器配置
这部分配置内容在docker-compose文件里写了,没有特殊要求。
4.3.5 数据库配置
这里做了mysql,opengauss,oceanbase三种数据库的适配,oceanbase经过礼貌问价后发现财力不足,就也用docker部署了一个单机版。详细内容在docker-compose文件里都有。列举一些遇到的问题;
- opengauss出现密码强度问题。需要设置强密码,大小写,数字与特殊符号。
- opengauss官方镜像找不到gsql路径:进入容器后执行
export GAUSSHOME=/usr/local/opengauss
export PATH=$GAUSSHOME/bin:/scws/bin:$PATH
export LD_LIBRARY_PATH=$GAUSSHOME/lib:/scws/lib:$LD_LIBRARY_PATH
export DATAVEC_PQ_LIB_PATH=/usr/local/sra_recall/lib
- oceanbase在重启后连不上,由于首次启动后伪集群会记录当时的网络ip,重启后docker网络重新分配了一个ip就启动不了了,所以通过给容器设置静态ip就可以解决了。
- oceanbase无法执行自定义的初始化命令,只调用镜像默认的初始化命令,可能是镜像的原因导致的,不过可以通过修改entrypoint与command实现
4.3.6 华为云部署
4.3.6.1 系统运行环境配置:
我们通过配置docker容器来简化环境配置,提高部署效率。
- 数据库基础镜像:opengauss/opengauss:7.0.0-RC2.B015-openEuler22.03
- 后端基础镜像:python:3.11-slim 构建software-base
- flask服务端:software-base
- celery作业处理: software-base
- vue前端:构建使用node:24-alpine 生产使用nginx:alpine
- redis:redis:7-alpine
4.3.6.2安装操作说明
Docker配置:
- 首先我们需要安装docker,window环境下推荐使用docker desktop(linux安装docker-engine或docker desktop)。详见:https://docs.docker.com/engine/install/
- 其次需要拉取镜像,镜像见上。使用docker pull ****进行拉取,受限于网络环境,我们通过上传离线镜像的操作进行镜像拉取:使用docker save -o **.tar nginx:latest打包成压缩包,使用docker load -i **.tar进行解压镜像。
- 代码拉取:
我们的代码已经上传到github上zilv2333/SoftWare: 软工/数据采集课设
首先确保git的安装,其次建一个文件夹来存储代码。我们在文件夹执行git clone https://github.com/zilv2333/SoftWare 获取代码。 - 文件配置:
完善相关文件配置 - 在项目目录下首先使用docker compose build base构建后端基础镜像,然后使用docker compose up --build –d启动程序们就可以在本机的80端口看到我们的网页了。
4.3.6.3 展示
这是云上的链接,能正常访问。

五、心得
通过参与本次项目的开发,我在技术实践、团队协作和项目管理方面都获得了宝贵的经验。作为后端开发的主要负责人之一,我从最初的系统设计到最终的云端部署,全程参与了项目的各个关键环节,对软件工程的全流程有了更深刻的理解。
5.1技术层面的收获
5.1.1微服务架构的实践
项目中我们采用了前后端分离的微服务架构,前端使用Vue3,后端使用Flask框架。这种架构模式让我深刻体会到:
- 解耦优势:前后端独立开发部署,提高了开发效率,减少了团队间的依赖。
- 技术栈灵活性:可以根据不同模块的特点选择最适合的技术方案。
- 维护便利性:单个服务的更新不会影响整个系统的运行。
5.1.2异步任务处理
面对视频处理这类计算密集型任务,我引入了Celery + Redis的异步任务队列方案:
-
用户体验优化:用户无需长时间等待,后台处理后通过轮询获取结果。
-
系统稳定性:即使某个任务失败,也不会影响整个服务。
-
可扩展性:可以轻松增加worker节点来处理更多并发任务。
5.1.3数据库设计与优化
项目中同时使用MySQL/OpenGauss两种数据库,让我对数据库设计有了更全面的认识:
- 连接池管理:通过DBUtils实现高效的连接复用,显著提升了数据库访问性能。
- 跨数据库兼容:编写了统一的数据库访问接口,降低了系统对特定数据库的依赖。
5.1.4 Docker容器化部署
通过Docker Compose实现了一键部署,这个过程中我学到了:
-
环境一致性:确保了开发、测试、生产环境的高度一致。
-
资源隔离:每个服务运行在独立的容器中,互不干扰。
-
自动化部署:通过编排文件简化了复杂的多服务部署流程。
5.2 团队协作经验
5.2.1 接口设计与协调
作为后端负责人,我需要与前端同学密切配合:
-
API文档先行:在开发前就定义好接口规范,减少了后期的沟通成本。
-
及时沟通:通过定期会议和即时通讯工具,确保前后端进度同步。
-
版本管理:使用Git进行代码管理,建立分支策略,确保代码质量。
5.2.2 功能整合与调试
整合各个模块时,我体会到了:
-
模块化开发的重要性:清晰的模块边界让整合过程更加顺利。
-
自动化测试的必要性:编写单元测试和集成测试大大提高了代码质量。
-
问题定位能力:通过日志分析和调试工具快速定位和解决问题。
5.4 个人成长与反思
5.4.1 技术能力的提升
通过这个项目,我不仅巩固了Python Web开发的基础,还学习到了:
-
现代Web开发流程:从需求分析到部署上线的完整流程
-
系统设计能力:如何设计可扩展、易维护的系统架构
-
问题解决能力:面对复杂问题时如何分析、定位和解决
5.4.2 项目管理的体会
虽然主要负责技术实现,但我也对项目管理有了初步认识:
-
时间规划的重要性:合理的进度安排是项目成功的关键
-
需求变更的处理:如何平衡需求变更与项目进度
5.4.3 不足之处与改进方向
回顾整个项目,我也认识到一些需要改进的地方:
-
文档完善度:项目文档可以更加详细和规范
-
测试覆盖率:需要增加更多的自动化测试
-
性能监控:可以添加更完善的系统监控和告警机制

浙公网安备 33010602011771号