refactor(structure): reorganize app layout

BREAKING CHANGE: root backend/frontend directories and old run/manage entrypoints were removed. Use apps/backend, apps/frontend, and python main.py commands instead.
This commit is contained in:
2026-05-03 16:43:11 +08:00
parent 7e8852877e
commit d4d6f87730
112 changed files with 347 additions and 1596 deletions
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
创建管理员用户的脚本
使用方法:
PYTHONPATH=apps python apps/backend/scripts/create_admin.py
或使用虚拟环境:
PYTHONPATH=apps ./venv/bin/python apps/backend/scripts/create_admin.py
"""
import sys
from pathlib import Path
APPS_DIR = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(APPS_DIR))
from backend.models import init_db, User
from backend.models.database import SessionLocal
from backend.services.auth_service import AuthService
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_admin_user(alias: str):
"""
将现有用户升级为管理员(或创建管理员占位符)
Args:
alias: 用户别名
"""
# 初始化数据库
init_db()
# 创建数据库会话
db = SessionLocal()
try:
# 检查别名是否已存在
existing_user = db.query(User).filter(User.alias == alias).first()
if existing_user:
print(f"[OK] 找到用户:{alias}")
print(f" 用户 ID: {existing_user.id}")
print(f" QQ 标识 (jwt_sub): {existing_user.jwt_sub}")
print(f" 当前角色: {existing_user.role}")
print(f" 审批状态: {existing_user.is_approved}")
# 如果已经是管理员
if existing_user.role == "admin":
print("\n该用户已经是管理员")
return
# 升级为管理员
response = input("\n是否将该用户升级为管理员?(y/n): ")
if response.lower() == 'y':
existing_user.role = "admin"
existing_user.is_approved = True # 确保已审批
db.commit()
print("\n" + "=" * 60)
print("[成功] 用户已升级为管理员!")
print("=" * 60)
print(f" 用户 ID: {existing_user.id}")
print(f" 别名: {existing_user.alias}")
print(f" QQ 标识: {existing_user.jwt_sub}")
print(f" 角色: admin")
print("=" * 60)
else:
print("操作已取消")
else:
print(f"\n[错误] 未找到别名为 '{alias}' 的用户")
print("\n请先使用该别名进行 QQ 扫码注册,然后再运行此脚本升级为管理员")
except Exception as e:
logger.error(f"[错误] 操作失败: {e}")
db.rollback()
raise
finally:
db.close()
def main():
"""主函数"""
print("=" * 60)
print("接龙自动打卡系统 - 设置管理员")
print("=" * 60)
print()
print("[说明]")
print(" 此脚本将已注册的用户升级为管理员")
print(" 请先使用别名进行 QQ 扫码注册,然后运行此脚本")
print()
# 获取用户别名
alias = input("请输入要设置为管理员的用户别名 [admin]: ").strip() or "admin"
print()
print("=" * 60)
print(f"准备将用户 '{alias}' 设置为管理员")
print("=" * 60)
print()
create_admin_user(alias)
if __name__ == "__main__":
main()
@@ -0,0 +1,78 @@
"""
数据库迁移脚本:添加账户锁定相关字段
添加字段:
- failed_login_attempts: 连续登录失败次数
- locked_until: 账户锁定到期时间
- last_failed_login: 最后一次登录失败时间
运行方式:
PYTHONPATH=apps python -m backend.scripts.migrate_add_account_lockout
python -m backend.scripts.migrate_add_account_lockout
"""
import sys
from pathlib import Path
APPS_DIR = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(APPS_DIR))
from sqlalchemy import text
from backend.models.database import engine
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def migrate():
"""执行迁移"""
logger.info("开始迁移:添加账户锁定相关字段...")
with engine.connect() as conn:
# 检查字段是否已存在
result = conn.execute(text("PRAGMA table_info(users)"))
columns = [row[1] for row in result]
# 添加 failed_login_attempts 字段
if 'failed_login_attempts' not in columns:
logger.info("添加 failed_login_attempts 字段...")
conn.execute(text(
"ALTER TABLE users ADD COLUMN failed_login_attempts INTEGER DEFAULT 0 NOT NULL"
))
conn.commit()
logger.info("✓ failed_login_attempts 字段添加成功")
else:
logger.info("✓ failed_login_attempts 字段已存在,跳过")
# 添加 locked_until 字段
if 'locked_until' not in columns:
logger.info("添加 locked_until 字段...")
conn.execute(text(
"ALTER TABLE users ADD COLUMN locked_until DATETIME"
))
conn.commit()
logger.info("✓ locked_until 字段添加成功")
else:
logger.info("✓ locked_until 字段已存在,跳过")
# 添加 last_failed_login 字段
if 'last_failed_login' not in columns:
logger.info("添加 last_failed_login 字段...")
conn.execute(text(
"ALTER TABLE users ADD COLUMN last_failed_login DATETIME"
))
conn.commit()
logger.info("✓ last_failed_login 字段添加成功")
else:
logger.info("✓ last_failed_login 字段已存在,跳过")
logger.info("✅ 迁移完成!账户锁定功能已启用")
if __name__ == "__main__":
try:
migrate()
except Exception as e:
logger.error(f"❌ 迁移失败: {e}")
sys.exit(1)
+127
View File
@@ -0,0 +1,127 @@
"""
测试新的异常处理系统
"""
import sys
from pathlib import Path
APPS_DIR = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(APPS_DIR))
from backend.exceptions import (
ValidationError,
AuthenticationError,
AuthorizationError,
ResourceNotFoundError,
BusinessLogicError,
)
from backend.schemas.response import ErrorResponse, ErrorDetail
def test_exceptions():
"""测试自定义异常"""
print("=" * 60)
print("测试自定义异常类")
print("=" * 60)
# 测试 ValidationError
try:
raise ValidationError("用户名长度必须在2-50之间")
except ValidationError as e:
print(f"✅ ValidationError: {e.message} (状态码: {e.status_code}, 代码: {e.error_code})")
# 测试 AuthenticationError
try:
raise AuthenticationError("Token已过期")
except AuthenticationError as e:
print(f"✅ AuthenticationError: {e.message} (状态码: {e.status_code}, 代码: {e.error_code})")
# 测试 AuthorizationError
try:
raise AuthorizationError("需要管理员权限")
except AuthorizationError as e:
print(f"✅ AuthorizationError: {e.message} (状态码: {e.status_code}, 代码: {e.error_code})")
# 测试 ResourceNotFoundError
try:
raise ResourceNotFoundError("用户不存在")
except ResourceNotFoundError as e:
print(f"✅ ResourceNotFoundError: {e.message} (状态码: {e.status_code}, 代码: {e.error_code})")
# 测试 BusinessLogicError
try:
raise BusinessLogicError("打卡任务已禁用")
except BusinessLogicError as e:
print(f"✅ BusinessLogicError: {e.message} (状态码: {e.status_code}, 代码: {e.error_code})")
def test_response_schemas():
"""测试响应 Schema"""
print("\n" + "=" * 60)
print("测试响应 Schema")
print("=" * 60)
# 测试 ErrorResponse
error_response = ErrorResponse(
error=ErrorDetail(
code="VALIDATION_ERROR",
message="邮箱格式不正确",
field="email"
)
)
response_dict = error_response.model_dump()
print(f"✅ ErrorResponse 序列化成功:")
print(f" {response_dict}")
assert response_dict["success"] == False
assert response_dict["error"]["code"] == "VALIDATION_ERROR"
assert response_dict["error"]["message"] == "邮箱格式不正确"
assert response_dict["error"]["field"] == "email"
print("✅ 所有断言通过")
def check_old_exception_patterns():
"""检查旧的异常处理模式"""
print("\n" + "=" * 60)
print("检查需要更新的旧异常代码")
print("=" * 60)
import os
import re
patterns = {
"HTTPException with detail": r'raise HTTPException.*detail=f?".*{',
"except Exception": r'except Exception as',
}
results = {}
for pattern_name, pattern in patterns.items():
results[pattern_name] = []
for root, dirs, files in os.walk(APPS_DIR / 'backend' / 'api'):
for file in files:
if file.endswith('.py'):
filepath = os.path.join(root, file)
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
matches = re.findall(pattern, content, re.MULTILINE)
if matches:
results[pattern_name].append((filepath, len(matches)))
for pattern_name, files in results.items():
print(f"\n{pattern_name}:")
if files:
print(f" ⚠️ 发现 {sum(count for _, count in files)} 处使用")
for filepath, count in files:
print(f" - {filepath}: {count}")
else:
print(f" ✅ 未发现使用")
if __name__ == "__main__":
test_exceptions()
test_response_schemas()
check_old_exception_patterns()
print("\n" + "=" * 60)
print("✅ 所有测试通过!新的异常处理系统工作正常")
print("=" * 60)