Browse Source

首次更新

liu 7 months ago
parent
commit
5aa3aefed3

+ 19 - 49
.gitignore

@@ -1,60 +1,30 @@
-# ---> Python
-# Byte-compiled / optimized / DLL files
+# Python bytecode
 __pycache__/
 __pycache__/
-*.py[cod]
-*$py.class
+*.pyc
+*.pyo
+*.pyd
 
 
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
+# Distribution / build outputs
 build/
 build/
-develop-eggs/
 dist/
 dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
 *.egg-info/
 *.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
+.eggs/
 
 
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
+# IDE-specific files (e.g., VS Code)
+.vscode/
 
 
-# Translations
-*.mo
-*.pot
+# Environment variables
+.env
+venv/
+env/
 
 
-# Django stuff:
+# Logs and temporary files
+logs/
 *.log
 *.log
+tmp/
 
 
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
+# Instance specific data (if it's not meant to be committed)
+instance/
 
 
+# PyInstaller / cx_Freeze / other packaging tools
+*.spec  # This will ignore run.spec as seen in your image

+ 58 - 0
app/__init__.py

@@ -0,0 +1,58 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_restx import Api
+from app.utils.log_config import LogConfig
+
+# 初始化扩展,但不绑定到应用实例
+db = SQLAlchemy()
+log_config = LogConfig()
+
+def create_app(config_object=None):
+    app = Flask(__name__, instance_relative_config=True)
+    
+    # 默认配置
+    app.config['SECRET_KEY'] = 'a-very-secret-key'
+    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./tasks.db'
+    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+
+    if config_object:
+        app.config.from_object(config_object)
+
+    # 初始化扩展
+    db.init_app(app)
+    log_config.init_app(app)
+
+    # 创建 Flask-RESTX API 实例(加入全局 Bearer Token 安全定义以启用 Swagger Authorize 按钮)
+    authorizations = {
+        'BearerAuth': {
+            'type': 'apiKey',
+            'in': 'header',
+            'name': 'Authorization',
+            'description': '在此处输入: Bearer <token>'
+        }
+    }
+    api = Api(
+        app,
+        version='1.0',
+        title='Task API',
+        description='A simple Task API with Spring MVC style layers',
+        authorizations=authorizations,
+        security='BearerAuth'
+    )
+
+    # 导入并注册控制器(命名空间)
+    from app.controller import task_controller, auth_controller, lovelace_url_controller, user_room_controller
+    api.add_namespace(task_controller.api, path='/api/v1/tasks')
+    api.add_namespace(auth_controller.api, path='/api/v1/auth')
+    api.add_namespace(lovelace_url_controller.api, path='/api/v1/lovelace')
+    api.add_namespace(user_room_controller.api, path='/api/v1/user-room')
+
+    # 注册全局错误处理器
+    from app.utils.error_handler import register_error_handlers
+    register_error_handlers(app)
+
+    # 在应用上下文中创建数据库表
+    with app.app_context():
+        db.create_all()
+
+    return app

+ 276 - 0
app/controller/auth_controller.py

@@ -0,0 +1,276 @@
+from flask import request, g
+from flask_restx import Resource, Namespace, fields
+from app.service.user_service import UserService
+from app.utils.jwt_utils import JWTUtils, token_required
+from app.utils.logger import Logger, log_request_info, ErrorHandler
+from app import db
+
+# 创建认证API命名空间
+api = Namespace('auth', description='用户认证相关操作')
+
+# 定义API模型
+login_model = api.model('Login', {
+    'username': fields.String(required=True, description='用户名'),
+    'password': fields.String(required=True, description='密码')
+})
+
+register_model = api.model('Register', {
+    'username': fields.String(required=True, description='用户名'),
+    'email': fields.String(required=True, description='邮箱'),
+    'password': fields.String(required=True, description='密码')
+})
+
+change_password_model = api.model('ChangePassword', {
+    'old_password': fields.String(required=True, description='当前密码'),
+    'new_password': fields.String(required=True, description='新密码')
+})
+
+user_model = api.model('User', {
+    'id': fields.Integer(readOnly=True, description='用户ID'),
+    'username': fields.String(description='用户名'),
+    'email': fields.String(description='邮箱'),
+    'is_active': fields.Boolean(description='是否激活'),
+    'created_at': fields.String(description='创建时间'),
+    'updated_at': fields.String(description='更新时间')
+})
+
+token_response_model = api.model('TokenResponse', {
+    'access_token': fields.String(description='访问令牌'),
+    'token_type': fields.String(description='令牌类型'),
+    'expires_in': fields.Integer(description='过期时间(秒)'),
+    'user': fields.Nested(user_model, description='用户信息')
+})
+
+@api.route('/register')
+class RegisterResource(Resource):
+    @api.expect(register_model, validate=True)
+    @api.marshal_with(user_model, code=201)
+    @api.response(400, '注册失败')
+    @log_request_info
+    def post(self):
+        """用户注册"""
+        data = request.get_json()
+        service = UserService(db.session)
+        
+        # 记录注册尝试
+        Logger.info("用户注册尝试", {
+            'username': data.get('username'),
+            'email': data.get('email'),
+            'ip_address': request.remote_addr,
+            'user_agent': request.headers.get('User-Agent')
+        })
+        
+        try:
+            new_user = service.create_user(
+                username=data['username'],
+                email=data['email'],
+                password=data['password']
+            )
+            
+            # 记录注册成功
+            Logger.info("用户注册成功", {
+                'user_id': new_user.id,
+                'username': new_user.username,
+                'email': new_user.email,
+                'ip_address': request.remote_addr
+            })
+            
+            return new_user.to_dict(), 201
+        except ValueError as e:
+            # 记录注册失败
+            Logger.warning("用户注册失败", {
+                'username': data.get('username'),
+                'email': data.get('email'),
+                'error': str(e),
+                'ip_address': request.remote_addr
+            })
+            api.abort(400, str(e))
+
+@api.route('/login')
+class LoginResource(Resource):
+    @api.expect(login_model, validate=True)
+    @api.marshal_with(token_response_model)
+    @api.response(401, '登录失败')
+    @log_request_info
+    def post(self):
+        """用户登录"""
+        data = request.get_json()
+        service = UserService(db.session)
+        
+        # 记录登录尝试
+        Logger.info("用户登录尝试", {
+            'username': data.get('username'),
+            'ip_address': request.remote_addr,
+            'user_agent': request.headers.get('User-Agent')
+        })
+        
+        try:
+            user = service.authenticate_user(
+                username=data['username'],
+                password=data['password']
+            )
+            
+            # 生成永不过期的JWT token(expires_in=0)
+            token = JWTUtils.generate_token(user.id, user.username, expires_in=0)
+            
+            # 记录登录成功
+            Logger.info("用户登录成功", {
+                'user_id': user.id,
+                'username': user.username,
+                'ip_address': request.remote_addr
+            })
+            
+            return {
+                'access_token': token,
+                'token_type': 'Bearer',
+                'expires_in': 0,
+                'user': user.to_dict_public()
+            }
+        except ValueError as e:
+            # 记录登录失败
+            Logger.warning("用户登录失败", {
+                'username': data.get('username'),
+                'error': str(e),
+                'ip_address': request.remote_addr
+            })
+            api.abort(401, str(e))
+
+@api.route('/me')
+class UserProfileResource(Resource):
+    @token_required
+    @api.marshal_with(user_model)
+    @log_request_info
+    def get(self):
+        """获取当前用户信息"""
+        Logger.info("获取用户信息", {
+            'user_id': g.current_user.id,
+            'username': g.current_user.username
+        })
+        return g.current_user.to_dict()
+
+    @token_required
+    @api.expect(api.model('UpdateProfile', {
+        'username': fields.String(description='用户名'),
+        'email': fields.String(description='邮箱')
+    }))
+    @api.marshal_with(user_model)
+    @api.response(400, '更新失败')
+    @log_request_info
+    def put(self):
+        """更新当前用户信息"""
+        data = request.get_json()
+        service = UserService(db.session)
+        
+        Logger.info("用户信息更新尝试", {
+            'user_id': g.current_user.id,
+            'username': g.current_user.username,
+            'new_username': data.get('username'),
+            'new_email': data.get('email')
+        })
+        
+        try:
+            updated_user = service.update_user(
+                user_id=g.current_user.id,
+                username=data.get('username'),
+                email=data.get('email')
+            )
+            
+            Logger.info("用户信息更新成功", {
+                'user_id': updated_user.id,
+                'username': updated_user.username,
+                'email': updated_user.email
+            })
+            
+            return updated_user.to_dict()
+        except ValueError as e:
+            Logger.warning("用户信息更新失败", {
+                'user_id': g.current_user.id,
+                'error': str(e)
+            })
+            api.abort(400, str(e))
+
+@api.route('/change-password')
+class ChangePasswordResource(Resource):
+    @token_required
+    @api.expect(change_password_model, validate=True)
+    @api.response(200, '密码修改成功')
+    @api.response(400, '密码修改失败')
+    @log_request_info
+    def post(self):
+        """修改密码"""
+        data = request.get_json()
+        service = UserService(db.session)
+        
+        Logger.info("用户密码修改尝试", {
+            'user_id': g.current_user.id,
+            'username': g.current_user.username
+        })
+        
+        try:
+            service.change_password(
+                user_id=g.current_user.id,
+                old_password=data['old_password'],
+                new_password=data['new_password']
+            )
+            
+            Logger.info("用户密码修改成功", {
+                'user_id': g.current_user.id,
+                'username': g.current_user.username
+            })
+            
+            return {'message': '密码修改成功'}, 200
+        except ValueError as e:
+            Logger.warning("用户密码修改失败", {
+                'user_id': g.current_user.id,
+                'username': g.current_user.username,
+                'error': str(e)
+            })
+            api.abort(400, str(e))
+
+@api.route('/logout')
+class LogoutResource(Resource):
+    @token_required
+    @api.response(200, '登出成功')
+    @log_request_info
+    def post(self):
+        """用户登出(客户端需要删除token)"""
+        Logger.info("用户登出", {
+            'user_id': g.current_user.id,
+            'username': g.current_user.username
+        })
+        return {'message': '登出成功'}, 200
+
+@api.route('/verify-token')
+class VerifyTokenResource(Resource):
+    @token_required
+    @api.marshal_with(user_model)
+    @log_request_info
+    def get(self):
+        """验证token有效性"""
+        Logger.info("Token验证", {
+            'user_id': g.current_user.id,
+            'username': g.current_user.username
+        })
+        return g.current_user.to_dict()
+
+@api.route('/permanent-token')
+class PermanentTokenResource(Resource):
+    @token_required
+    @api.marshal_with(token_response_model)
+    @log_request_info
+    def post(self):
+        """使用当前有效token换取长期有效token(无过期)"""
+        user = g.current_user
+        
+        Logger.info("生成长期Token", {
+            'user_id': user.id,
+            'username': user.username
+        })
+        
+        token = JWTUtils.generate_token(user.id, user.username, expires_in=0)
+        return {
+            'access_token': token,
+            'token_type': 'Bearer',
+            'expires_in': 0,
+            'user': user.to_dict_public()
+        }

+ 160 - 0
app/controller/log_integration_example.py

@@ -0,0 +1,160 @@
+"""
+日志集成示例
+展示如何在现有控制器中集成日志功能
+"""
+from flask import request, g
+from flask_restx import Resource, Namespace, fields
+from app.utils.logger import Logger, log_request_info, ErrorHandler
+from app.utils.error_handler import handle_authentication_error, handle_validation_error
+
+# 创建示例API命名空间
+api = Namespace('log_example', description='日志集成示例')
+
+# 定义API模型
+login_model = api.model('Login', {
+    'username': fields.String(required=True, description='用户名'),
+    'password': fields.String(required=True, description='密码')
+})
+
+@api.route('/login')
+class LoginResource(Resource):
+    @api.expect(login_model, validate=True)
+    @api.response(200, '登录成功')
+    @api.response(401, '登录失败')
+    @log_request_info  # 自动记录请求信息
+    def post(self):
+        """用户登录 - 集成日志示例"""
+        data = request.get_json()
+        
+        # 记录登录尝试
+        Logger.info("用户登录尝试", {
+            'username': data.get('username'),
+            'ip_address': request.remote_addr,
+            'user_agent': request.headers.get('User-Agent')
+        })
+        
+        try:
+            # 模拟用户验证
+            username = data.get('username')
+            password = data.get('password')
+            
+            # 验证输入
+            if not username or not password:
+                Logger.warning("登录失败:缺少必要参数", {
+                    'username': username,
+                    'has_password': bool(password)
+                })
+                return handle_validation_error(
+                    ValueError("用户名和密码不能为空"),
+                    "username/password"
+                )
+            
+            # 模拟用户验证逻辑
+            if username == 'admin' and password == 'password':
+                Logger.info("用户登录成功", {
+                    'username': username,
+                    'ip_address': request.remote_addr
+                })
+                
+                return {
+                    'message': '登录成功',
+                    'user': {'username': username}
+                }, 200
+            else:
+                Logger.warning("用户登录失败:凭据无效", {
+                    'username': username,
+                    'ip_address': request.remote_addr
+                })
+                
+                return handle_authentication_error(
+                    ValueError("用户名或密码错误"),
+                    "invalid_credentials"
+                )
+                
+        except Exception as e:
+            Logger.error("登录过程中发生异常", e, {
+                'username': data.get('username'),
+                'ip_address': request.remote_addr
+            })
+            
+            # 使用全局错误处理器
+            ErrorHandler.handle_api_error(
+                e,
+                request.endpoint,
+                {'username': data.get('username')}
+            )
+            
+            raise
+
+@api.route('/protected')
+class ProtectedResource(Resource):
+    @api.response(200, '访问成功')
+    @api.response(401, '未授权')
+    @log_request_info
+    def get(self):
+        """受保护的资源 - 展示访问日志"""
+        # 记录受保护资源的访问
+        Logger.access_log("访问受保护资源", {
+            'resource': 'protected',
+            'method': request.method,
+            'ip_address': request.remote_addr
+        })
+        
+        Logger.info("受保护资源被访问", {
+            'endpoint': request.endpoint,
+            'ip_address': request.remote_addr
+        })
+        
+        return {
+            'message': '这是受保护的资源',
+            'timestamp': Logger.get_logger().handlers[0].formatter.formatTime(
+                Logger.get_logger().handlers[0].formatter.converter()
+            )
+        }, 200
+
+@api.route('/error-test')
+class ErrorTestResource(Resource):
+    @api.response(500, '测试错误')
+    @log_request_info
+    def get(self):
+        """错误测试端点 - 展示错误处理"""
+        Logger.info("开始错误测试")
+        
+        try:
+            # 模拟不同类型的错误
+            error_type = request.args.get('type', 'general')
+            
+            if error_type == 'database':
+                Logger.error("模拟数据库错误", None, {'error_type': 'database'})
+                ErrorHandler.handle_database_error(
+                    ConnectionError("数据库连接失败"),
+                    "查询用户信息"
+                )
+            elif error_type == 'validation':
+                Logger.error("模拟验证错误", None, {'error_type': 'validation'})
+                return handle_validation_error(
+                    ValueError("数据验证失败"),
+                    "email"
+                )
+            elif error_type == 'auth':
+                Logger.error("模拟认证错误", None, {'error_type': 'auth'})
+                return handle_authentication_error(
+                    ValueError("Token无效"),
+                    "invalid_token"
+                )
+            else:
+                # 模拟一般异常
+                raise RuntimeError("这是一个测试异常")
+                
+        except Exception as e:
+            Logger.error("错误测试中发生异常", e, {
+                'error_type': request.args.get('type', 'general'),
+                'test_endpoint': True
+            })
+            
+            # 记录全局错误
+            Logger.global_error("错误测试异常", e, {
+                'test_type': request.args.get('type', 'general')
+            })
+            
+            raise

+ 95 - 0
app/controller/lovelace_url_controller.py

@@ -0,0 +1,95 @@
+from flask import request
+from flask_restx import Resource, Namespace, fields
+from app.service.lovelace_url_service import LovelaceURLService
+from app.utils.jwt_utils import token_required
+from app import db
+
+
+api = Namespace('lovelace', description='Home Assistant Lovelace URL 管理')
+
+
+lovelace_model = api.model('LovelaceURL', {
+    'id': fields.Integer(readOnly=True, description='记录ID'),
+    'room_name': fields.String(required=True, description='房间名称'),
+    'url': fields.String(required=True, description='Lovelace 卡片或视图 URL')
+})
+
+
+lovelace_input_model = api.model('LovelaceURLInput', {
+    'room_name': fields.String(required=True, description='房间名称'),
+    'url': fields.String(required=True, description='Lovelace 卡片或视图 URL')
+})
+
+
+@api.route('/')
+class LovelaceListResource(Resource):
+    @token_required
+    @api.marshal_list_with(lovelace_model)
+    def get(self):
+        """获取所有 Lovelace URL 记录"""
+        service = LovelaceURLService(db.session)
+        records = service.list_all()
+        return [r.to_dict() for r in records]
+
+    @token_required
+    @api.expect(lovelace_input_model, validate=True)
+    @api.marshal_with(lovelace_model, code=201)
+    def post(self):
+        """创建新的 Lovelace URL 记录"""
+        data = request.get_json()
+        service = LovelaceURLService(db.session)
+        try:
+            new_record = service.create(room_name=data['room_name'], url=data['url'])
+            return new_record.to_dict(), 201
+        except ValueError as e:
+            api.abort(400, str(e))
+
+
+@api.route('/<int:record_id>')
+class LovelaceResource(Resource):
+    @token_required
+    @api.marshal_with(lovelace_model)
+    @api.response(404, 'Record not found')
+    def get(self, record_id: int):
+        """根据 ID 获取记录"""
+        service = LovelaceURLService(db.session)
+        try:
+            record = service.get_by_id(record_id)
+            return record.to_dict()
+        except ValueError as e:
+            api.abort(404, str(e))
+
+    @token_required
+    @api.expect(api.model('LovelaceURLUpdate', {
+        'room_name': fields.String(description='房间名称'),
+        'url': fields.String(description='Lovelace 卡片或视图 URL')
+    }))
+    @api.marshal_with(lovelace_model)
+    @api.response(404, 'Record not found')
+    def put(self, record_id: int):
+        """更新记录"""
+        data = request.get_json()
+        service = LovelaceURLService(db.session)
+        try:
+            updated = service.update(
+                record_id=record_id,
+                room_name=data.get('room_name'),
+                url=data.get('url')
+            )
+            return updated.to_dict()
+        except ValueError as e:
+            api.abort(404, str(e))
+
+    @token_required
+    @api.response(204, 'Record deleted')
+    @api.response(404, 'Record not found')
+    def delete(self, record_id: int):
+        """删除记录"""
+        service = LovelaceURLService(db.session)
+        try:
+            service.delete(record_id)
+            return '', 204
+        except ValueError as e:
+            api.abort(404, str(e))
+
+

+ 116 - 0
app/controller/task_controller.py

@@ -0,0 +1,116 @@
+from flask import request
+from flask_restx import Resource, Namespace, fields
+from app.service.task_service import TaskService
+from app.utils.jwt_utils import token_required
+from app.utils.logger import Logger, log_request_info
+from app import db  # 从 app/__init__.py 导入 db
+
+# 1. 创建 API 命名空间,用于组织相关的路由
+api = Namespace('tasks', description='Task related operations')
+
+# 2. 定义 DTO (Data Transfer Objects) / API Models
+# 用于请求体解析和响应体序列化
+task_model = api.model('Task', {
+    'id': fields.Integer(readOnly=True, description='The task unique identifier'),
+    'title': fields.String(required=True, description='The task title'),
+    'description': fields.String(description='The task description'),
+    'done': fields.Boolean(description='The task status', default=False)
+})
+
+task_input_model = api.model('TaskInput', {
+    'title': fields.String(required=True, description='The task title'),
+    'description': fields.String(description='The task description'),
+})
+
+# 3. 创建资源类,每个类对应一个资源,并绑定 HTTP 方法
+@api.route('/')
+class TaskListResource(Resource):
+    @token_required
+    @api.marshal_list_with(task_model)  # 定义响应的 Swagger 文档
+    @log_request_info
+    def get(self):
+        """获取所有任务列表"""
+        Logger.info("获取任务列表")
+        service = TaskService(db.session)
+        tasks = service.get_all_tasks()
+        
+        Logger.info("任务列表获取成功", {
+            'task_count': len(tasks)
+        })
+        
+        return [task.to_dict() for task in tasks]
+
+    @token_required
+    @api.expect(task_input_model, validate=True)  # 定义请求体的 Swagger 文档并验证
+    @api.marshal_with(task_model, code=201)       # 定义成功响应的 Swagger 文档
+    @log_request_info
+    def post(self):
+        """创建一个新任务"""
+        data = request.get_json()
+        service = TaskService(db.session)
+        
+        Logger.info("创建新任务", {
+            'title': data.get('title'),
+            'description': data.get('description')
+        })
+        
+        try:
+            new_task = service.create_new_task(title=data['title'], description=data.get('description'))
+            
+            Logger.info("任务创建成功", {
+                'task_id': new_task.id,
+                'title': new_task.title
+            })
+            
+            return new_task.to_dict(), 201
+        except ValueError as e:
+            Logger.warning("任务创建失败", {
+                'title': data.get('title'),
+                'error': str(e)
+            })
+            api.abort(400, str(e))
+
+@api.route('/<int:task_id>')
+class TaskResource(Resource):
+    @token_required
+    @api.marshal_with(task_model)
+    @api.response(404, 'Task not found')
+    def get(self, task_id):
+        """根据 ID 获取单个任务"""
+        service = TaskService(db.session)
+        try:
+            task = service.get_task_by_id(task_id)
+            return task.to_dict()
+        except ValueError as e:
+            api.abort(404, str(e))
+
+    @token_required
+    @api.expect(task_input_model)
+    @api.marshal_with(task_model)
+    @api.response(404, 'Task not found')
+    def put(self, task_id):
+        """根据 ID 更新一个已存在的任务"""
+        data = request.get_json()
+        service = TaskService(db.session)
+        try:
+            updated_task = service.update_existing_task(
+                task_id=task_id,
+                title=data.get('title'),
+                description=data.get('description'),
+                done=data.get('done')
+            )
+            return updated_task.to_dict()
+        except ValueError as e:
+            api.abort(404, str(e))
+
+    @token_required
+    @api.response(204, 'Task deleted')
+    @api.response(404, 'Task not found')
+    def delete(self, task_id):
+        """根据 ID 删除一个任务"""
+        service = TaskService(db.session)
+        try:
+            service.delete_task(task_id)
+            return '', 204
+        except ValueError as e:
+            api.abort(404, str(e))

+ 139 - 0
app/controller/user_room_controller.py

@@ -0,0 +1,139 @@
+from flask import request
+from flask_restx import Resource, Namespace, fields
+from app.service.user_room_service import UserRoomService
+from app.utils.jwt_utils import token_required
+from app.utils.logger import Logger, log_request_info
+from app import db
+
+
+api = Namespace('user_room', description='用户账号与房间ID映射管理')
+
+
+user_room_model = api.model('UserRoom', {
+    'id': fields.Integer(readOnly=True, description='记录ID'),
+    'user_account': fields.String(required=True, description='用户账号'),
+    'room_id': fields.String(required=True, description='房间ID')
+})
+
+
+user_room_input_model = api.model('UserRoomInput', {
+    'user_account': fields.String(required=True, description='用户账号'),
+    'room_id': fields.String(required=True, description='房间ID')
+})
+
+
+@api.route('/')
+class UserRoomListResource(Resource):
+    @token_required
+    @api.marshal_list_with(user_room_model)
+    @log_request_info
+    def get(self):
+        """获取所有映射"""
+        Logger.info("获取用户房间映射列表")
+        service = UserRoomService(db.session)
+        records = service.list_all()
+        
+        Logger.info("用户房间映射列表获取成功", {
+            'record_count': len(records)
+        })
+        
+        return [r.to_dict() for r in records]
+
+    @token_required
+    @api.expect(user_room_input_model, validate=True)
+    @api.marshal_with(user_room_model, code=201)
+    @log_request_info
+    def post(self):
+        """创建新映射"""
+        data = request.get_json()
+        service = UserRoomService(db.session)
+        
+        Logger.info("创建用户房间映射", {
+            'user_account': data.get('user_account'),
+            'room_id': data.get('room_id')
+        })
+        
+        try:
+            new_record = service.create(user_account=data['user_account'], room_id=data['room_id'])
+            
+            Logger.info("用户房间映射创建成功", {
+                'record_id': new_record.id,
+                'user_account': new_record.user_account,
+                'room_id': new_record.room_id
+            })
+            
+            return new_record.to_dict(), 201
+        except ValueError as e:
+            Logger.warning("用户房间映射创建失败", {
+                'user_account': data.get('user_account'),
+                'room_id': data.get('room_id'),
+                'error': str(e)
+            })
+            api.abort(400, str(e))
+
+
+@api.route('/<int:record_id>')
+class UserRoomResource(Resource):
+    @token_required
+    @api.marshal_with(user_room_model)
+    @api.response(404, 'Record not found')
+    def get(self, record_id: int):
+        """根据ID获取映射"""
+        service = UserRoomService(db.session)
+        try:
+            record = service.get_by_id(record_id)
+            return record.to_dict()
+        except ValueError as e:
+            api.abort(404, str(e))
+
+    @token_required
+    @api.expect(api.model('UserRoomUpdate', {
+        'user_account': fields.String(description='用户账号'),
+        'room_id': fields.String(description='房间ID')
+    }))
+    @api.marshal_with(user_room_model)
+    @api.response(404, 'Record not found')
+    def put(self, record_id: int):
+        """更新映射"""
+        data = request.get_json()
+        service = UserRoomService(db.session)
+        try:
+            updated = service.update(
+                record_id=record_id,
+                user_account=data.get('user_account'),
+                room_id=data.get('room_id')
+            )
+            return updated.to_dict()
+        except ValueError as e:
+            # 可能是未找到或唯一冲突等
+            api.abort(400 if 'exists' in str(e) else 404, str(e))
+
+    @token_required
+    @api.response(204, 'Record deleted')
+    @api.response(404, 'Record not found')
+    def delete(self, record_id: int):
+        """删除映射"""
+        service = UserRoomService(db.session)
+        try:
+            service.delete(record_id)
+            return '', 204
+        except ValueError as e:
+            api.abort(404, str(e))
+
+@api.route('/by-user/<string:user_account>')
+class UserRoomByUserResource(Resource):
+    @token_required
+    @api.marshal_list_with(api.model('UserRoomDetails', {
+        'id': fields.Integer(description='记录ID'),
+        'user_account': fields.String(description='用户账号'),
+        'room_name': fields.String(description='房间名称'),
+        'room_url': fields.String(description='房间URL'),
+        'room_id': fields.String(description='房间ID')
+    }))
+    def get(self, user_account: str):
+        """根据用户账号返回房间映射详情列表(用户账号、房间名、房间URL)"""
+        service = UserRoomService(db.session)
+        results = service.list_details_by_user_account(user_account)
+        return results
+
+

+ 28 - 0
app/dao/lovelace_url_dao.py

@@ -0,0 +1,28 @@
+from sqlalchemy.orm import Session
+from app.model.lovelace_url_model import LovelaceURL
+
+
+class LovelaceURLDAO:
+    def __init__(self, db_session: Session):
+        self.db_session = db_session
+
+    def get_all(self):
+        return self.db_session.query(LovelaceURL).all()
+
+    def get_by_id(self, record_id: int):
+        return self.db_session.query(LovelaceURL).get(record_id)
+
+    def create(self, record: LovelaceURL):
+        self.db_session.add(record)
+        self.db_session.commit()
+        return record
+
+    def update(self, record: LovelaceURL):
+        self.db_session.commit()
+        return record
+
+    def delete(self, record: LovelaceURL):
+        self.db_session.delete(record)
+        self.db_session.commit()
+
+

+ 25 - 0
app/dao/task_dao.py

@@ -0,0 +1,25 @@
+from sqlalchemy.orm import Session
+from app.model.task_model import Task
+
+class TaskDAO:
+    def __init__(self, db_session: Session):
+        self.db_session = db_session
+
+    def get_all(self):
+        return self.db_session.query(Task).all()
+
+    def get_by_id(self, task_id: int):
+        return self.db_session.query(Task).get(task_id)
+
+    def create(self, task: Task):
+        self.db_session.add(task)
+        self.db_session.commit()
+        return task
+
+    def update(self, task: Task):
+        self.db_session.commit()
+        return task
+
+    def delete(self, task: Task):
+        self.db_session.delete(task)
+        self.db_session.commit()

+ 46 - 0
app/dao/user_dao.py

@@ -0,0 +1,46 @@
+from sqlalchemy.orm import Session
+from app.model.user_model import User
+
+class UserDAO:
+    def __init__(self, db_session: Session):
+        self.db_session = db_session
+
+    def get_all(self):
+        """获取所有用户"""
+        return self.db_session.query(User).all()
+
+    def get_by_id(self, user_id: int):
+        """根据ID获取用户"""
+        return self.db_session.query(User).get(user_id)
+
+    def get_by_username(self, username: str):
+        """根据用户名获取用户"""
+        return self.db_session.query(User).filter_by(username=username).first()
+
+    def get_by_email(self, email: str):
+        """根据邮箱获取用户"""
+        return self.db_session.query(User).filter_by(email=email).first()
+
+    def create(self, user: User):
+        """创建新用户"""
+        self.db_session.add(user)
+        self.db_session.commit()
+        return user
+
+    def update(self, user: User):
+        """更新用户信息"""
+        self.db_session.commit()
+        return user
+
+    def delete(self, user: User):
+        """删除用户"""
+        self.db_session.delete(user)
+        self.db_session.commit()
+
+    def exists_by_username(self, username: str):
+        """检查用户名是否已存在"""
+        return self.db_session.query(User).filter_by(username=username).first() is not None
+
+    def exists_by_email(self, email: str):
+        """检查邮箱是否已存在"""
+        return self.db_session.query(User).filter_by(email=email).first() is not None

+ 37 - 0
app/dao/user_room_dao.py

@@ -0,0 +1,37 @@
+from sqlalchemy.orm import Session
+from app.model.user_room_model import UserRoom
+
+
+class UserRoomDAO:
+    def __init__(self, db_session: Session):
+        self.db_session = db_session
+
+    def get_all(self):
+        return self.db_session.query(UserRoom).all()
+
+    def get_by_id(self, record_id: int):
+        return self.db_session.query(UserRoom).get(record_id)
+
+    def get_by_user_account(self, user_account: str):
+        return self.db_session.query(UserRoom).filter_by(user_account=user_account).all()
+
+    def get_by_room_id(self, room_id: str):
+        return self.db_session.query(UserRoom).filter_by(room_id=room_id).all()
+
+    def get_unique(self, user_account: str, room_id: str):
+        return self.db_session.query(UserRoom).filter_by(user_account=user_account, room_id=room_id).first()
+
+    def create(self, record: UserRoom):
+        self.db_session.add(record)
+        self.db_session.commit()
+        return record
+
+    def update(self, record: UserRoom):
+        self.db_session.commit()
+        return record
+
+    def delete(self, record: UserRoom):
+        self.db_session.delete(record)
+        self.db_session.commit()
+
+

+ 19 - 0
app/model/lovelace_url_model.py

@@ -0,0 +1,19 @@
+from app import db
+
+
+class LovelaceURL(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    room_name = db.Column(db.String(120), nullable=False)
+    url = db.Column(db.String(512), nullable=False)
+
+    def to_dict(self):
+        return {
+            'id': self.id,
+            'room_name': self.room_name,
+            'url': self.url
+        }
+
+    def __repr__(self):
+        return f'<LovelaceURL {self.id}: {self.room_name}>'
+
+

+ 20 - 0
app/model/task_model.py

@@ -0,0 +1,20 @@
+from flask_sqlalchemy import SQLAlchemy
+from app import db  # 从 app/__init__.py 导入 db 实例
+
+class Task(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    title = db.Column(db.String(120), nullable=False)
+    description = db.Column(db.String(255), nullable=True)
+    done = db.Column(db.Boolean, default=False)
+
+    def to_dict(self):
+        """将模型实例转换为字典,方便序列化为 JSON"""
+        return {
+            'id': self.id,
+            'title': self.title,
+            'description': self.description,
+            'done': self.done
+        }
+
+    def __repr__(self):
+        return f'<Task {self.id}: {self.title}>'

+ 43 - 0
app/model/user_model.py

@@ -0,0 +1,43 @@
+from flask_sqlalchemy import SQLAlchemy
+from werkzeug.security import generate_password_hash, check_password_hash
+from app import db  # 从 app/__init__.py 导入 db 实例
+
+class User(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    username = db.Column(db.String(80), unique=True, nullable=False)
+    email = db.Column(db.String(120), unique=True, nullable=False)
+    password_hash = db.Column(db.String(128), nullable=False)
+    is_active = db.Column(db.Boolean, default=True)
+    created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
+    updated_at = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp())
+
+    def set_password(self, password):
+        """设置密码哈希"""
+        self.password_hash = generate_password_hash(password)
+
+    def check_password(self, password):
+        """验证密码"""
+        return check_password_hash(self.password_hash, password)
+
+    def to_dict(self):
+        """将模型实例转换为字典,方便序列化为 JSON"""
+        return {
+            'id': self.id,
+            'username': self.username,
+            'email': self.email,
+            'is_active': self.is_active,
+            'created_at': self.created_at.isoformat() if self.created_at else None,
+            'updated_at': self.updated_at.isoformat() if self.updated_at else None
+        }
+
+    def to_dict_public(self):
+        """返回公开的用户信息(不包含敏感信息)"""
+        return {
+            'id': self.id,
+            'username': self.username,
+            'email': self.email,
+            'is_active': self.is_active
+        }
+
+    def __repr__(self):
+        return f'<User {self.id}: {self.username}>'

+ 23 - 0
app/model/user_room_model.py

@@ -0,0 +1,23 @@
+from app import db
+
+
+class UserRoom(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    user_account = db.Column(db.String(120), nullable=False, index=True)
+    room_id = db.Column(db.String(120), nullable=False, index=True)
+
+    __table_args__ = (
+        db.UniqueConstraint('user_account', 'room_id', name='uq_user_account_room_id'),
+    )
+
+    def to_dict(self):
+        return {
+            'id': self.id,
+            'user_account': self.user_account,
+            'room_id': self.room_id,
+        }
+
+    def __repr__(self):
+        return f'<UserRoom {self.id}: {self.user_account}-{self.room_id}>'
+
+

+ 48 - 0
app/service/lovelace_url_service.py

@@ -0,0 +1,48 @@
+from sqlalchemy.orm import Session
+from app.dao.lovelace_url_dao import LovelaceURLDAO
+from app.model.lovelace_url_model import LovelaceURL
+
+
+class LovelaceURLService:
+    def __init__(self, db_session: Session):
+        self.dao = LovelaceURLDAO(db_session)
+
+    def list_all(self):
+        return self.dao.get_all()
+
+    def get_by_id(self, record_id: int):
+        record = self.dao.get_by_id(record_id)
+        if not record:
+            raise ValueError(f"LovelaceURL with id {record_id} not found.")
+        return record
+
+    def create(self, room_name: str, url: str):
+        if not room_name or not room_name.strip():
+            raise ValueError("room_name cannot be empty.")
+        if not url or not url.strip():
+            raise ValueError("url cannot be empty.")
+
+        record = LovelaceURL(room_name=room_name.strip(), url=url.strip())
+        return self.dao.create(record)
+
+    def update(self, record_id: int, room_name: str = None, url: str = None):
+        record = self.get_by_id(record_id)
+
+        if room_name is not None:
+            if not room_name.strip():
+                raise ValueError("room_name cannot be empty when provided.")
+            record.room_name = room_name.strip()
+
+        if url is not None:
+            if not url.strip():
+                raise ValueError("url cannot be empty when provided.")
+            record.url = url.strip()
+
+        return self.dao.update(record)
+
+    def delete(self, record_id: int):
+        record = self.get_by_id(record_id)
+        self.dao.delete(record)
+        return True
+
+

+ 40 - 0
app/service/task_service.py

@@ -0,0 +1,40 @@
+from sqlalchemy.orm import Session
+from app.dao.task_dao import TaskDAO
+from app.model.task_model import Task
+
+class TaskService:
+    def __init__(self, db_session: Session):
+        self.task_dao = TaskDAO(db_session)
+
+    def get_all_tasks(self):
+        return self.task_dao.get_all()
+
+    def get_task_by_id(self, task_id: int):
+        task = self.task_dao.get_by_id(task_id)
+        if not task:
+            raise ValueError(f"Task with id {task_id} not found.")
+        return task
+
+    def create_new_task(self, title: str, description: str = None):
+        if not title:
+            raise ValueError("Title cannot be empty.")
+        
+        new_task = Task(title=title, description=description)
+        return self.task_dao.create(new_task)
+
+    def update_existing_task(self, task_id: int, title: str = None, description: str = None, done: bool = None):
+        task = self.get_task_by_id(task_id) # 复用 get_task_by_id 方法,包含了查找不到的逻辑
+
+        if title is not None:
+            task.title = title
+        if description is not None:
+            task.description = description
+        if done is not None:
+            task.done = done
+            
+        return self.task_dao.update(task)
+
+    def delete_task(self, task_id: int):
+        task = self.get_task_by_id(task_id)
+        self.task_dao.delete(task)
+        return True

+ 110 - 0
app/service/user_room_service.py

@@ -0,0 +1,110 @@
+from sqlalchemy.orm import Session
+from app.dao.user_room_dao import UserRoomDAO
+from app.model.user_room_model import UserRoom
+from app.dao.lovelace_url_dao import LovelaceURLDAO
+
+
+class UserRoomService:
+    def __init__(self, db_session: Session):
+        self.user_room_dao = UserRoomDAO(db_session)
+        self.lovelace_url_dao = LovelaceURLDAO(db_session)
+
+    def list_all(self):
+        return self.user_room_dao.get_all()
+
+    def get_by_id(self, record_id: int):
+        record = self.user_room_dao.get_by_id(record_id)
+        if not record:
+            raise ValueError(f"UserRoom with id {record_id} not found.")
+        return record
+
+    def list_by_user_account(self, user_account: str):
+        return self.user_room_dao.get_by_user_account(user_account)
+
+    def list_by_room_id(self, room_id: str):
+        return self.user_room_dao.get_by_room_id(room_id)
+
+    def create(self, user_account: str, room_id: str):
+        if not user_account or not room_id:
+            raise ValueError("user_account and room_id are required.")
+
+        existed = self.user_room_dao.get_unique(user_account, room_id)
+        if existed:
+            raise ValueError("Mapping already exists.")
+
+        record = UserRoom(user_account=user_account, room_id=room_id)
+        return self.user_room_dao.create(record)
+
+    def update(self, record_id: int, user_account: str = None, room_id: str = None):
+        record = self.get_by_id(record_id)
+
+        if user_account is not None:
+            record.user_account = user_account
+        if room_id is not None:
+            record.room_id = room_id
+
+        # 如果用户传了双方都更新,需检查唯一键是否冲突
+        if user_account is not None or room_id is not None:
+            existed = self.user_room_dao.get_unique(record.user_account, record.room_id)
+            if existed and existed.id != record.id:
+                raise ValueError("Mapping already exists with given user_account and room_id.")
+
+        return self.user_room_dao.update(record)
+
+    def delete(self, record_id: int):
+        record = self.get_by_id(record_id)
+        self.user_room_dao.delete(record)
+        return True
+
+    def list_details_by_user_account(self, user_account: str):
+        """返回包含用户账号、房间名、房间URL的映射详情列表。
+        当前通过 UserRoom.room_id 与 LovelaceURL.id 进行关联(假设 room_id 存储的是 LovelaceURL.id)。
+        如果实际是其他关联方式,请告知我调整。
+        """
+        mappings = self.user_room_dao.get_by_user_account(user_account)
+        if not mappings:
+            return []
+
+        # 预取所有涉及的 room_id 对应的 LovelaceURL
+        room_ids = [m.room_id for m in mappings]
+        # 将 room_id 当作 LovelaceURL 的主键 id 来查找(如果是字符串,需要尝试转换)
+        id_ints = []
+        for rid in room_ids:
+            try:
+                id_ints.append(int(rid))
+            except (TypeError, ValueError):
+                continue
+
+        # 构建 id -> LovelaceURL 映射
+        lovelace_map = {}
+        if id_ints:
+            # 简单拉全量再过滤(DAO 暂无批量 by_ids 方法)
+            all_records = self.lovelace_url_dao.get_all()
+            for rec in all_records:
+                if rec.id in id_ints:
+                    lovelace_map[rec.id] = rec
+
+        results = []
+        for m in mappings:
+            room_name = None
+            room_url = None
+            try:
+                key = int(m.room_id)
+                match = lovelace_map.get(key)
+                if match:
+                    room_name = match.room_name
+                    room_url = match.url
+            except (TypeError, ValueError):
+                pass
+
+            results.append({
+                'id': m.id,
+                'user_account': m.user_account,
+                'room_name': room_name,
+                'room_url': room_url,
+                'room_id' : m.room_id,
+            })
+
+        return results
+
+

+ 198 - 0
app/service/user_service.py

@@ -0,0 +1,198 @@
+from sqlalchemy.orm import Session
+from app.dao.user_dao import UserDAO
+from app.model.user_model import User
+from app.utils.logger import Logger, ErrorHandler
+
+class UserService:
+    def __init__(self, db_session: Session):
+        self.user_dao = UserDAO(db_session)
+
+    def get_all_users(self):
+        """获取所有用户"""
+        return self.user_dao.get_all()
+
+    def get_user_by_id(self, user_id: int):
+        """根据ID获取用户"""
+        user = self.user_dao.get_by_id(user_id)
+        if not user:
+            raise ValueError(f"User with id {user_id} not found.")
+        return user
+
+    def get_user_by_username(self, username: str):
+        """根据用户名获取用户"""
+        user = self.user_dao.get_by_username(username)
+        if not user:
+            raise ValueError(f"User with username {username} not found.")
+        return user
+
+    def create_user(self, username: str, email: str, password: str):
+        """创建新用户"""
+        Logger.info("开始创建用户", {
+            'username': username,
+            'email': email
+        })
+        
+        try:
+            # 验证输入
+            if not username or not email or not password:
+                Logger.warning("用户创建失败:缺少必要参数", {
+                    'username': username,
+                    'email': email,
+                    'has_password': bool(password)
+                })
+                raise ValueError("Username, email and password are required.")
+            
+            if len(password) < 6:
+                Logger.warning("用户创建失败:密码长度不足", {
+                    'username': username,
+                    'password_length': len(password)
+                })
+                raise ValueError("Password must be at least 6 characters long.")
+            
+            # 检查用户名是否已存在
+            if self.user_dao.exists_by_username(username):
+                Logger.warning("用户创建失败:用户名已存在", {
+                    'username': username
+                })
+                raise ValueError("Username already exists.")
+            
+            # 检查邮箱是否已存在
+            if self.user_dao.exists_by_email(email):
+                Logger.warning("用户创建失败:邮箱已存在", {
+                    'email': email
+                })
+                raise ValueError("Email already exists.")
+            
+            # 创建新用户
+            new_user = User(username=username, email=email)
+            new_user.set_password(password)
+            
+            created_user = self.user_dao.create(new_user)
+            
+            Logger.info("用户创建成功", {
+                'user_id': created_user.id,
+                'username': created_user.username,
+                'email': created_user.email
+            })
+            
+            return created_user
+            
+        except Exception as e:
+            Logger.error("用户创建过程中发生异常", e, {
+                'username': username,
+                'email': email
+            })
+            raise
+
+    def authenticate_user(self, username: str, password: str):
+        """验证用户登录"""
+        Logger.info("开始用户认证", {
+            'username': username
+        })
+        
+        try:
+            if not username or not password:
+                Logger.warning("用户认证失败:缺少用户名或密码", {
+                    'username': username,
+                    'has_password': bool(password)
+                })
+                raise ValueError("Username and password are required.")
+            
+            user = self.user_dao.get_by_username(username)
+            if not user:
+                Logger.warning("用户认证失败:用户不存在", {
+                    'username': username
+                })
+                raise ValueError("Invalid username or password.")
+            
+            if not user.is_active:
+                Logger.warning("用户认证失败:账户已禁用", {
+                    'user_id': user.id,
+                    'username': username
+                })
+                raise ValueError("User account is disabled.")
+            
+            if not user.check_password(password):
+                Logger.warning("用户认证失败:密码错误", {
+                    'user_id': user.id,
+                    'username': username
+                })
+                raise ValueError("Invalid username or password.")
+            
+            Logger.info("用户认证成功", {
+                'user_id': user.id,
+                'username': user.username
+            })
+            
+            return user
+            
+        except Exception as e:
+            Logger.error("用户认证过程中发生异常", e, {
+                'username': username
+            })
+            raise
+
+    def update_user(self, user_id: int, username: str = None, email: str = None, is_active: bool = None):
+        """更新用户信息"""
+        user = self.get_user_by_id(user_id)
+        
+        if username is not None and username != user.username:
+            if self.user_dao.exists_by_username(username):
+                raise ValueError("Username already exists.")
+            user.username = username
+        
+        if email is not None and email != user.email:
+            if self.user_dao.exists_by_email(email):
+                raise ValueError("Email already exists.")
+            user.email = email
+        
+        if is_active is not None:
+            user.is_active = is_active
+        
+        return self.user_dao.update(user)
+
+    def change_password(self, user_id: int, old_password: str, new_password: str):
+        """修改用户密码"""
+        Logger.info("开始修改用户密码", {
+            'user_id': user_id
+        })
+        
+        try:
+            user = self.get_user_by_id(user_id)
+            
+            if not user.check_password(old_password):
+                Logger.warning("密码修改失败:当前密码错误", {
+                    'user_id': user_id,
+                    'username': user.username
+                })
+                raise ValueError("Current password is incorrect.")
+            
+            if len(new_password) < 6:
+                Logger.warning("密码修改失败:新密码长度不足", {
+                    'user_id': user_id,
+                    'username': user.username,
+                    'new_password_length': len(new_password)
+                })
+                raise ValueError("New password must be at least 6 characters long.")
+            
+            user.set_password(new_password)
+            updated_user = self.user_dao.update(user)
+            
+            Logger.info("用户密码修改成功", {
+                'user_id': user_id,
+                'username': user.username
+            })
+            
+            return updated_user
+            
+        except Exception as e:
+            Logger.error("密码修改过程中发生异常", e, {
+                'user_id': user_id
+            })
+            raise
+
+    def delete_user(self, user_id: int):
+        """删除用户"""
+        user = self.get_user_by_id(user_id)
+        self.user_dao.delete(user)
+        return True

+ 179 - 0
app/utils/error_handler.py

@@ -0,0 +1,179 @@
+"""
+全局错误处理器
+处理应用中的各种异常和错误
+"""
+from flask import jsonify, request, current_app
+from app.utils.logger import Logger, ErrorHandler
+import traceback
+
+
+def register_error_handlers(app):
+    """注册全局错误处理器"""
+    
+    @app.errorhandler(400)
+    def bad_request(error):
+        """处理400错误"""
+        ErrorHandler.handle_api_error(
+            error,
+            request.endpoint if request else None,
+            request.get_json() if request and request.is_json else None
+        )
+        
+        return jsonify({
+            'error': 'Bad Request',
+            'message': '请求格式错误',
+            'status_code': 400
+        }), 400
+    
+    @app.errorhandler(401)
+    def unauthorized(error):
+        """处理401错误"""
+        ErrorHandler.handle_api_error(
+            error,
+            request.endpoint if request else None,
+            {'auth_error': True}
+        )
+        
+        return jsonify({
+            'error': 'Unauthorized',
+            'message': '未授权访问',
+            'status_code': 401
+        }), 401
+    
+    @app.errorhandler(403)
+    def forbidden(error):
+        """处理403错误"""
+        ErrorHandler.handle_api_error(
+            error,
+            request.endpoint if request else None,
+            {'permission_error': True}
+        )
+        
+        return jsonify({
+            'error': 'Forbidden',
+            'message': '禁止访问',
+            'status_code': 403
+        }), 403
+    
+    @app.errorhandler(404)
+    def not_found(error):
+        """处理404错误"""
+        ErrorHandler.handle_api_error(
+            error,
+            request.endpoint if request else None,
+            {'not_found': True}
+        )
+        
+        return jsonify({
+            'error': 'Not Found',
+            'message': '资源未找到',
+            'status_code': 404
+        }), 404
+    
+    @app.errorhandler(405)
+    def method_not_allowed(error):
+        """处理405错误"""
+        ErrorHandler.handle_api_error(
+            error,
+            request.endpoint if request else None,
+            {'method': request.method if request else None}
+        )
+        
+        return jsonify({
+            'error': 'Method Not Allowed',
+            'message': '请求方法不允许',
+            'status_code': 405
+        }), 405
+    
+    @app.errorhandler(422)
+    def unprocessable_entity(error):
+        """处理422错误"""
+        ErrorHandler.handle_api_error(
+            error,
+            request.endpoint if request else None,
+            request.get_json() if request and request.is_json else None
+        )
+        
+        return jsonify({
+            'error': 'Unprocessable Entity',
+            'message': '请求数据无法处理',
+            'status_code': 422
+        }), 422
+    
+    @app.errorhandler(500)
+    def internal_server_error(error):
+        """处理500错误"""
+        ErrorHandler.handle_exception(
+            error,
+            f"内部服务器错误 - {request.endpoint if request else '未知端点'}"
+        )
+        
+        return jsonify({
+            'error': 'Internal Server Error',
+            'message': '服务器内部错误',
+            'status_code': 500
+        }), 500
+    
+    @app.errorhandler(Exception)
+    def handle_unexpected_error(error):
+        """处理未预期的异常"""
+        ErrorHandler.handle_exception(
+            error,
+            f"未预期异常 - {request.endpoint if request else '未知端点'}"
+        )
+        
+        # 在开发模式下返回详细错误信息
+        if current_app.debug:
+            return jsonify({
+                'error': 'Unexpected Error',
+                'message': str(error),
+                'traceback': traceback.format_exc(),
+                'status_code': 500
+            }), 500
+        
+        return jsonify({
+            'error': 'Unexpected Error',
+            'message': '发生未预期的错误',
+            'status_code': 500
+        }), 500
+
+
+def handle_database_error(error, operation=None):
+    """处理数据库错误"""
+    ErrorHandler.handle_database_error(error, operation)
+    
+    return jsonify({
+        'error': 'Database Error',
+        'message': '数据库操作失败',
+        'status_code': 500
+    }), 500
+
+
+def handle_validation_error(error, field=None):
+    """处理验证错误"""
+    ErrorHandler.handle_api_error(
+        error,
+        request.endpoint if request else None,
+        {'validation_error': True, 'field': field}
+    )
+    
+    return jsonify({
+        'error': 'Validation Error',
+        'message': f'数据验证失败: {field or "未知字段"}',
+        'status_code': 400
+    }), 400
+
+
+def handle_authentication_error(error, reason=None):
+    """处理认证错误"""
+    ErrorHandler.handle_api_error(
+        error,
+        request.endpoint if request else None,
+        {'auth_error': True, 'reason': reason}
+    )
+    
+    return jsonify({
+        'error': 'Authentication Error',
+        'message': f'认证失败: {reason or "未知原因"}',
+        'status_code': 401
+    }), 401

+ 147 - 0
app/utils/jwt_utils.py

@@ -0,0 +1,147 @@
+import jwt
+import datetime
+from functools import wraps
+from flask import request, current_app
+from app.model.user_model import User
+from app import db
+
+class JWTUtils:
+    @staticmethod
+    def generate_token(user_id: int, username: str, expires_in: int = 3600):
+        """
+        生成JWT token
+        
+        Args:
+            user_id: 用户ID
+            username: 用户名
+            expires_in: token过期时间(秒),默认1小时
+        
+        Returns:
+            str: JWT token
+        """
+        payload = {
+            'user_id': user_id,
+            'username': username,
+            'iat': datetime.datetime.utcnow()
+        }
+        # 仅当 expires_in > 0 时设置过期时间;<=0 表示永不过期
+        if isinstance(expires_in, int) and expires_in > 0:
+            payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires_in)
+        
+        token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
+        return token
+
+    @staticmethod
+    def verify_token(token: str):
+        """
+        验证JWT token
+        
+        Args:
+            token: JWT token字符串
+        
+        Returns:
+            dict: token payload,如果验证失败则返回None
+        """
+        try:
+            payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
+            return payload
+        except jwt.ExpiredSignatureError:
+            return None
+        except jwt.InvalidTokenError:
+            return None
+
+    @staticmethod
+    def get_current_user():
+        """
+        从请求头中获取当前用户
+        
+        Returns:
+            User: 当前用户对象,如果未认证则返回None
+        """
+        auth_header = request.headers.get('Authorization')
+        if not auth_header:
+            return None
+        
+        try:
+            # 期望格式: "Bearer <token>"
+            token = auth_header.split(' ')[1]
+        except IndexError:
+            return None
+        
+        payload = JWTUtils.verify_token(token)
+        if not payload:
+            return None
+        
+        user_id = payload.get('user_id')
+        if not user_id:
+            return None
+        
+        return db.session.query(User).get(user_id)
+
+def token_required(f):
+    """
+    JWT认证装饰器
+    
+    使用方法:
+    @token_required
+    def protected_route():
+        # 当前用户可以通过 g.current_user 访问
+        pass
+    """
+    @wraps(f)
+    def decorated(*args, **kwargs):
+        from flask import g
+        
+        auth_header = request.headers.get('Authorization')
+        if not auth_header:
+            return {'message': '缺少认证token'}, 401
+        
+        try:
+            # 期望格式: "Bearer <token>"
+            token = auth_header.split(' ')[1]
+        except IndexError:
+            return {'message': '认证token格式错误'}, 401
+        
+        payload = JWTUtils.verify_token(token)
+        if not payload:
+            return {'message': '认证token无效或已过期'}, 401
+        
+        user_id = payload.get('user_id')
+        if not user_id:
+            return {'message': '认证token无效'}, 401
+        
+        # 获取用户信息
+        user = db.session.query(User).get(user_id)
+        if not user or not user.is_active:
+            return {'message': '用户不存在或已被禁用'}, 401
+        
+        # 将用户信息存储到g对象中,供视图函数使用
+        g.current_user = user
+        return f(*args, **kwargs)
+    
+    return decorated
+
+def admin_required(f):
+    """
+    管理员权限装饰器(可以扩展用户角色功能)
+    
+    使用方法:
+    @admin_required
+    def admin_route():
+        pass
+    """
+    @wraps(f)
+    def decorated(*args, **kwargs):
+        from flask import g
+        
+        if not hasattr(g, 'current_user') or not g.current_user:
+            return jsonify({'message': '需要管理员权限'}), 403
+        
+        # 这里可以添加角色检查逻辑
+        # 目前简单检查用户名是否为admin
+        if g.current_user.username != 'admin':
+            return jsonify({'message': '需要管理员权限'}), 403
+        
+        return f(*args, **kwargs)
+    
+    return decorated

+ 116 - 0
app/utils/log_config.py

@@ -0,0 +1,116 @@
+"""
+日志配置模块
+支持日志轮换、不同级别的日志记录
+"""
+import os
+import logging
+from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
+from datetime import datetime
+
+
+class LogConfig:
+    """日志配置类"""
+    
+    def __init__(self, app=None):
+        self.app = app
+        if app is not None:
+            self.init_app(app)
+    
+    def init_app(self, app):
+        """初始化日志配置"""
+        # 确保logs目录存在
+        log_dir = os.path.join(app.instance_path, '..', 'logs')
+        if not os.path.exists(log_dir):
+            os.makedirs(log_dir)
+        
+        # 设置日志级别
+        log_level = app.config.get('LOG_LEVEL', 'INFO')
+        
+        # 清除默认的handlers
+        app.logger.handlers.clear()
+        
+        # 设置应用日志级别
+        app.logger.setLevel(getattr(logging, log_level.upper()))
+        
+        # 创建格式化器
+        formatter = logging.Formatter(
+            '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
+        )
+        
+        # 1. 控制台处理器
+        console_handler = logging.StreamHandler()
+        console_handler.setLevel(logging.INFO)
+        console_handler.setFormatter(formatter)
+        app.logger.addHandler(console_handler)
+        
+        # 2. 应用日志文件处理器(按大小轮换)
+        app_log_file = os.path.join(log_dir, 'app.log')
+        app_file_handler = RotatingFileHandler(
+            app_log_file,
+            maxBytes=10*1024*1024,  # 10MB
+            backupCount=5,
+            encoding='utf-8'
+        )
+        app_file_handler.setLevel(logging.INFO)
+        app_file_handler.setFormatter(formatter)
+        app.logger.addHandler(app_file_handler)
+        
+        # 3. 错误日志文件处理器(按时间轮换)
+        error_log_file = os.path.join(log_dir, 'error.log')
+        error_file_handler = TimedRotatingFileHandler(
+            error_log_file,
+            when='midnight',
+            interval=1,
+            backupCount=30,  # 保留30天
+            encoding='utf-8'
+        )
+        error_file_handler.setLevel(logging.ERROR)
+        error_file_handler.setFormatter(formatter)
+        app.logger.addHandler(error_file_handler)
+        
+        # 4. 全局错误日志处理器(按时间轮换)
+        global_error_log_file = os.path.join(log_dir, 'global_error.log')
+        global_error_handler = TimedRotatingFileHandler(
+            global_error_log_file,
+            when='midnight',
+            interval=1,
+            backupCount=30,  # 保留30天
+            encoding='utf-8'
+        )
+        global_error_handler.setLevel(logging.ERROR)
+        global_error_handler.setFormatter(formatter)
+        
+        # 创建全局错误日志器
+        global_error_logger = logging.getLogger('global_error')
+        global_error_logger.setLevel(logging.ERROR)
+        global_error_logger.addHandler(global_error_handler)
+        global_error_logger.propagate = False  # 不传播到父日志器
+        
+        # 5. 访问日志处理器
+        access_log_file = os.path.join(log_dir, 'access.log')
+        access_handler = TimedRotatingFileHandler(
+            access_log_file,
+            when='midnight',
+            interval=1,
+            backupCount=7,  # 保留7天
+            encoding='utf-8'
+        )
+        access_handler.setLevel(logging.INFO)
+        access_handler.setFormatter(formatter)
+        
+        # 创建访问日志器
+        access_logger = logging.getLogger('access')
+        access_logger.setLevel(logging.INFO)
+        access_logger.addHandler(access_handler)
+        access_logger.propagate = False
+        
+        # 设置werkzeug日志级别(减少Flask开发服务器的日志噪音)
+        werkzeug_logger = logging.getLogger('werkzeug')
+        werkzeug_logger.setLevel(logging.WARNING)
+        
+        # 将日志器保存到app配置中,方便其他地方使用
+        app.config['LOGGERS'] = {
+            'app': app.logger,
+            'global_error': global_error_logger,
+            'access': access_logger
+        }

+ 112 - 0
app/utils/log_example.py

@@ -0,0 +1,112 @@
+"""
+日志使用示例
+演示如何在应用中使用日志模块
+"""
+from app.utils.logger import Logger, log_request_info, log_function_call, ErrorHandler
+
+
+class LogExample:
+    """日志使用示例类"""
+    
+    @log_request_info
+    def api_example(self):
+        """API示例 - 自动记录请求信息"""
+        Logger.info("处理API请求")
+        
+        try:
+            # 模拟一些业务逻辑
+            result = self.business_logic()
+            Logger.info("API处理成功", {'result_count': len(result)})
+            return result
+        except Exception as e:
+            Logger.error("API处理失败", e, {'endpoint': 'api_example'})
+            raise
+    
+    @log_function_call("业务逻辑处理")
+    def business_logic(self):
+        """业务逻辑示例"""
+        Logger.info("开始执行业务逻辑")
+        
+        # 模拟一些操作
+        data = {'user_id': 123, 'action': 'create_task'}
+        Logger.info("业务逻辑执行中", data)
+        
+        # 模拟可能的错误
+        if False:  # 这里可以改为True来测试错误处理
+            raise ValueError("模拟的业务逻辑错误")
+        
+        Logger.info("业务逻辑执行完成")
+        return [1, 2, 3, 4, 5]
+    
+    def error_handling_example(self):
+        """错误处理示例"""
+        try:
+            # 模拟数据库操作
+            self.database_operation()
+        except Exception as e:
+            ErrorHandler.handle_database_error(e, "查询用户信息")
+        
+        try:
+            # 模拟API调用
+            self.api_call()
+        except Exception as e:
+            ErrorHandler.handle_api_error(e, "/api/v1/users", {'user_id': 123})
+        
+        try:
+            # 模拟一般异常
+            raise RuntimeError("这是一个测试异常")
+        except Exception as e:
+            ErrorHandler.handle_exception(e, "错误处理示例")
+    
+    def database_operation(self):
+        """模拟数据库操作"""
+        raise ConnectionError("数据库连接失败")
+    
+    def api_call(self):
+        """模拟API调用"""
+        raise TimeoutError("API调用超时")
+    
+    def access_log_example(self):
+        """访问日志示例"""
+        Logger.access_log("用户登录", {
+            'user_id': 123,
+            'login_method': 'password',
+            'ip_address': '192.168.1.100'
+        })
+        
+        Logger.access_log("用户操作", {
+            'user_id': 123,
+            'action': 'create_task',
+            'resource_id': 456
+        })
+
+
+# 使用示例
+def demo_logging():
+    """演示日志功能"""
+    example = LogExample()
+    
+    print("=== 日志功能演示 ===")
+    
+    # 基本日志记录
+    Logger.info("应用启动", {'version': '1.0.0'})
+    Logger.warning("这是一个警告信息", {'warning_type': 'deprecated_api'})
+    Logger.error("这是一个错误信息", None, {'error_code': 'E001'})
+    
+    # 全局错误日志
+    Logger.global_error("全局错误测试", None, {'severity': 'high'})
+    
+    # 访问日志
+    Logger.access_log("页面访问", {'page': '/dashboard'})
+    
+    # 函数调用日志
+    example.business_logic()
+    
+    # 错误处理示例
+    example.error_handling_example()
+    
+    print("=== 日志演示完成 ===")
+
+
+if __name__ == "__main__":
+    demo_logging()

+ 195 - 0
app/utils/logger.py

@@ -0,0 +1,195 @@
+"""
+日志工具类
+提供统一的日志记录接口
+"""
+import logging
+import traceback
+from functools import wraps
+from flask import request, current_app, g
+import json
+
+
+class Logger:
+    """日志工具类"""
+    
+    @staticmethod
+    def get_logger(name='app'):
+        """获取日志器"""
+        if current_app and 'LOGGERS' in current_app.config:
+            return current_app.config['LOGGERS'].get(name, current_app.logger)
+        return logging.getLogger(name)
+    
+    @staticmethod
+    def info(message, extra_data=None, logger_name='app'):
+        """记录info级别日志"""
+        logger = Logger.get_logger(logger_name)
+        
+        if extra_data:
+            message = f"{message} | 额外数据: {json.dumps(extra_data, ensure_ascii=False)}"
+        
+        logger.info(message)
+    
+    @staticmethod
+    def error(message, exception=None, extra_data=None, logger_name='app'):
+        """记录error级别日志"""
+        logger = Logger.get_logger(logger_name)
+        
+        full_message = message
+        
+        if exception:
+            full_message += f" | 异常: {str(exception)}"
+            full_message += f" | 堆栈跟踪: {traceback.format_exc()}"
+        
+        if extra_data:
+            full_message += f" | 额外数据: {json.dumps(extra_data, ensure_ascii=False)}"
+        
+        logger.error(full_message)
+    
+    @staticmethod
+    def warning(message, extra_data=None, logger_name='app'):
+        """记录warning级别日志"""
+        logger = Logger.get_logger(logger_name)
+        
+        if extra_data:
+            message = f"{message} | 额外数据: {json.dumps(extra_data, ensure_ascii=False)}"
+        
+        logger.warning(message)
+    
+    @staticmethod
+    def debug(message, extra_data=None, logger_name='app'):
+        """记录debug级别日志"""
+        logger = Logger.get_logger(logger_name)
+        
+        if extra_data:
+            message = f"{message} | 额外数据: {json.dumps(extra_data, ensure_ascii=False)}"
+        
+        logger.debug(message)
+    
+    @staticmethod
+    def global_error(message, exception=None, extra_data=None):
+        """记录全局错误日志"""
+        Logger.error(message, exception, extra_data, 'global_error')
+    
+    @staticmethod
+    def access_log(message, extra_data=None):
+        """记录访问日志"""
+        logger = Logger.get_logger('access')
+        
+        # 添加请求信息
+        request_info = {
+            'method': request.method if request else None,
+            'url': request.url if request else None,
+            'remote_addr': request.remote_addr if request else None,
+            'user_agent': request.headers.get('User-Agent') if request else None,
+        }
+        
+        if extra_data:
+            request_info.update(extra_data)
+        
+        full_message = f"{message} | 请求信息: {json.dumps(request_info, ensure_ascii=False)}"
+        logger.info(full_message)
+
+
+def log_request_info(f):
+    """装饰器:记录请求信息"""
+    @wraps(f)
+    def decorated_function(*args, **kwargs):
+        if request:
+            Logger.access_log(
+                f"API请求: {request.method} {request.path}",
+                {
+                    'args': dict(request.args),
+                    'json': request.get_json() if request.is_json else None,
+                    'form': dict(request.form) if request.form else None
+                }
+            )
+        
+        try:
+            result = f(*args, **kwargs)
+            if request:
+                Logger.access_log(f"API响应成功: {request.method} {request.path}")
+            return result
+        except Exception as e:
+            if request:
+                Logger.access_log(
+                    f"API响应错误: {request.method} {request.path}",
+                    {'error': str(e)}
+                )
+            raise
+    
+    return decorated_function
+
+
+def log_function_call(func_name=None):
+    """装饰器:记录函数调用"""
+    def decorator(f):
+        @wraps(f)
+        def decorated_function(*args, **kwargs):
+            name = func_name or f.__name__
+            Logger.info(f"函数调用开始: {name}")
+            
+            try:
+                result = f(*args, **kwargs)
+                Logger.info(f"函数调用成功: {name}")
+                return result
+            except Exception as e:
+                Logger.error(f"函数调用失败: {name}", e)
+                raise
+        
+        return decorated_function
+    return decorator
+
+
+class ErrorHandler:
+    """全局错误处理器"""
+    
+    @staticmethod
+    def handle_exception(exception, context=None):
+        """处理异常并记录日志"""
+        error_data = {
+            'exception_type': type(exception).__name__,
+            'context': context or '未知上下文'
+        }
+        
+        # 记录到全局错误日志
+        Logger.global_error(
+            f"全局异常捕获: {str(exception)}",
+            exception,
+            error_data
+        )
+        
+        # 记录到应用日志
+        Logger.error(
+            f"应用异常: {str(exception)}",
+            exception,
+            error_data
+        )
+    
+    @staticmethod
+    def handle_database_error(exception, operation=None):
+        """处理数据库错误"""
+        error_data = {
+            'operation': operation or '未知数据库操作',
+            'error_type': 'database_error'
+        }
+        
+        Logger.global_error(
+            f"数据库操作失败: {operation or '未知操作'}",
+            exception,
+            error_data
+        )
+    
+    @staticmethod
+    def handle_api_error(exception, endpoint=None, request_data=None):
+        """处理API错误"""
+        error_data = {
+            'endpoint': endpoint or '未知端点',
+            'request_data': request_data,
+            'error_type': 'api_error'
+        }
+        
+        Logger.global_error(
+            f"API错误: {endpoint or '未知端点'}",
+            exception,
+            error_data
+        )

BIN
release/run20250909_2.zip


BIN
release/run20250909_3.zip


BIN
release/run_20250909.zip


+ 7 - 0
requirements.txt

@@ -0,0 +1,7 @@
+Flask
+Flask-SQLAlchemy
+Flask-RESTX
+PyJWT
+Werkzeug
+requests
+waitress

+ 51 - 0
run.py

@@ -0,0 +1,51 @@
+from app import create_app, db
+from app.model.user_model import User
+from app.utils.logger import Logger
+from waitress import serve
+
+app = create_app()
+
+def create_admin_user():
+    """创建管理员用户"""
+    
+    with app.app_context():
+        Logger.info("开始检查管理员用户")
+        
+        # 检查是否已存在admin用户
+        existing_admin = User.query.filter_by(username='admin').first()
+        if existing_admin:
+            Logger.info("管理员用户已存在", {
+                'user_id': existing_admin.id,
+                'username': existing_admin.username
+            })
+            return
+        
+        # 创建管理员用户
+        Logger.info("创建管理员用户")
+        admin_user = User(
+            username='admin',
+            email='admin@ygtx.com'
+        )
+        admin_user.set_password('HNYZ0821')
+        
+        db.session.add(admin_user)
+        db.session.commit()
+        
+        Logger.info("管理员用户创建成功", {
+            'user_id': admin_user.id,
+            'username': admin_user.username,
+            'email': admin_user.email
+        })
+
+if __name__ == '__main__':
+    Logger.info("应用启动", {
+        'host': '0.0.0.0',
+        'port': 5000
+    })
+    
+    # debug=True 会在代码修改后自动重启服务器,并提供调试器
+    create_admin_user()
+    
+    Logger.info("开始启动Web服务器")
+    # app.run(debug=True, port=5000)
+    serve(app, host='0.0.0.0', port=5001)

+ 50 - 0
run.spec

@@ -0,0 +1,50 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+block_cipher = None
+
+
+a = Analysis(
+    ['run.py'],
+    pathex=[],
+    binaries=[],
+    datas=[],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    win_no_prefer_redirects=False,
+    win_private_assemblies=False,
+    cipher=block_cipher,
+    noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    [],
+    exclude_binaries=True,
+    name='run',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    console=True,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+)
+coll = COLLECT(
+    exe,
+    a.binaries,
+    a.zipfiles,
+    a.datas,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    name='run',
+)