commit fe73da9cf5e16b9dab8229edb6199549ad5a67df Author: lhx Date: Fri Sep 26 15:58:32 2025 +0800 初始化 diff --git a/.env b/.env new file mode 100644 index 0000000..74dc564 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# 数据库配置 +DATABASE_URL=mysql+pymysql://railway:Railway01.@8.134.75.237:3309/railway +DB_HOST=8.134.75.237 +DB_PORT=3309 +DB_USER=railway +DB_PASSWORD=Railway01. +DB_NAME=railway + +# 应用配置 +APP_HOST=0.0.0.0 +APP_PORT=8000 +APP_DEBUG=True \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..313697d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +*.md +!README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c530481 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# 铁路项目管理系统 + +基于FastAPI、MySQL、SQLAlchemy的铁路项目管理系统。 + +## 功能特性 + +- **账号管理**: 账号的增删改查,包含id、账号、密码、状态、是否更新、标段等字段 +- **数据库管理**: 表数据导入导出、SQL执行、建表删表等数据库操作 +- **定时任务**: 支持cron、间隔、指定时间的定时任务调度 + +## 技术栈 + +- FastAPI: Web框架 +- SQLAlchemy: ORM框架 +- MySQL: 数据库 +- APScheduler: 定时任务调度 +- Pandas: 数据处理 +- Pydantic: 数据验证 + +## 项目结构 + +``` +app/ +├── api/ # API路由 +├── core/ # 核心配置 +├── models/ # 数据库模型 +├── schemas/ # Pydantic模型 +├── services/ # 业务逻辑层 +└── utils/ # 工具类 +``` + +## 安装运行 + +1. 安装依赖: +```bash +pip install -r requirements.txt +``` + +2. 配置环境变量: +复制 `.env.example` 到 `.env` 并修改配置 + +3. 运行应用: +```bash +python main.py +``` + +## API文档 + +启动后访问 http://localhost:8000/docs 查看API文档 + +## 主要API端点 + +### 账号管理 +- `POST /api/accounts/` - 创建账号 +- `GET /api/accounts/` - 获取账号列表 +- `GET /api/accounts/{id}` - 获取单个账号 +- `PUT /api/accounts/{id}` - 更新账号 +- `DELETE /api/accounts/{id}` - 删除账号 + +### 数据库管理 +- `POST /api/database/execute` - 执行SQL +- `POST /api/database/table-data` - 获取表数据 +- `POST /api/database/create-table` - 创建表 +- `DELETE /api/database/drop-table/{name}` - 删除表 +- `POST /api/database/import-data` - 导入数据 +- `GET /api/database/tables` - 获取表列表 + +### 定时任务 +- `POST /api/tasks/cron` - 添加cron任务 +- `POST /api/tasks/interval` - 添加间隔任务 +- `POST /api/tasks/date` - 添加定时任务 +- `GET /api/tasks/` - 获取任务列表 +- `DELETE /api/tasks/{id}` - 删除任务 +- `PUT /api/tasks/{id}/pause` - 暂停任务 +- `PUT /api/tasks/{id}/resume` - 恢复任务 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..8350c9b Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..4e4a62b Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..f1eef64 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,5 @@ +from .account import router as account_router +from .database import router as database_router +from .task import router as task_router + +__all__ = ["account_router", "database_router", "task_router"] \ No newline at end of file diff --git a/app/api/__pycache__/__init__.cpython-312.pyc b/app/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d022cdc Binary files /dev/null and b/app/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/__pycache__/account.cpython-312.pyc b/app/api/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000..f6cea05 Binary files /dev/null and b/app/api/__pycache__/account.cpython-312.pyc differ diff --git a/app/api/__pycache__/database.cpython-312.pyc b/app/api/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..6ff5068 Binary files /dev/null and b/app/api/__pycache__/database.cpython-312.pyc differ diff --git a/app/api/__pycache__/task.cpython-312.pyc b/app/api/__pycache__/task.cpython-312.pyc new file mode 100644 index 0000000..cb50459 Binary files /dev/null and b/app/api/__pycache__/task.cpython-312.pyc differ diff --git a/app/api/account.py b/app/api/account.py new file mode 100644 index 0000000..b6cd636 --- /dev/null +++ b/app/api/account.py @@ -0,0 +1,60 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ..core.database import get_db +from ..schemas.account import ( + AccountCreate, AccountUpdate, AccountResponse, + AccountListRequest, AccountGetRequest, AccountUpdateRequest, AccountDeleteRequest +) +from ..services.account import AccountService + +router = APIRouter(prefix="/accounts", tags=["账号管理"]) + +@router.post("/create", response_model=AccountResponse, status_code=status.HTTP_201_CREATED) +def create_account(account: AccountCreate, db: Session = Depends(get_db)): + """创建账号""" + # 检查账号是否已存在 + existing_account = AccountService.get_account_by_account(db, account.account) + if existing_account: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="账号已存在" + ) + + return AccountService.create_account(db, account) + +@router.post("/list", response_model=List[AccountResponse]) +def get_accounts(request: AccountListRequest, db: Session = Depends(get_db)): + """获取账号列表""" + return AccountService.get_accounts(db, skip=request.skip, limit=request.limit) + +@router.post("/get", response_model=AccountResponse) +def get_account(request: AccountGetRequest, db: Session = Depends(get_db)): + """根据ID获取账号""" + account = AccountService.get_account(db, request.account_id) + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="账号不存在" + ) + return account + +@router.post("/update", response_model=AccountResponse) +def update_account(request: AccountUpdateRequest, db: Session = Depends(get_db)): + """更新账号""" + account = AccountService.update_account(db, request.account_id, request.account_data) + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="账号不存在" + ) + return account + +@router.post("/delete", status_code=status.HTTP_204_NO_CONTENT) +def delete_account(request: AccountDeleteRequest, db: Session = Depends(get_db)): + """删除账号""" + if not AccountService.delete_account(db, request.account_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="账号不存在" + ) \ No newline at end of file diff --git a/app/api/database.py b/app/api/database.py new file mode 100644 index 0000000..ef59cdf --- /dev/null +++ b/app/api/database.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from ..core.database import get_db +from ..schemas.database import ( + SQLExecuteRequest, SQLExecuteResponse, TableDataRequest, + TableDataResponse, CreateTableRequest, ImportDataRequest +) +from ..services.database import DatabaseService + +router = APIRouter(prefix="/database", tags=["数据库管理"]) + +@router.post("/execute", response_model=SQLExecuteResponse) +def execute_sql(request: SQLExecuteRequest, db: Session = Depends(get_db)): + """执行SQL语句""" + result = DatabaseService.execute_sql(db, request.sql) + return SQLExecuteResponse(**result) + +@router.post("/table-data", response_model=TableDataResponse) +def get_table_data(request: TableDataRequest, db: Session = Depends(get_db)): + """获取表数据""" + result = DatabaseService.get_table_data( + db, + request.table_name, + request.limit or 100, + request.offset or 0 + ) + return TableDataResponse(**result) + +@router.post("/create-table", response_model=SQLExecuteResponse) +def create_table(request: CreateTableRequest, db: Session = Depends(get_db)): + """创建表""" + result = DatabaseService.create_table( + db, + request.table_name, + request.columns, + request.primary_key + ) + return SQLExecuteResponse(**result) + +@router.delete("/drop-table/{table_name}", response_model=SQLExecuteResponse) +def drop_table(table_name: str, db: Session = Depends(get_db)): + """删除表""" + result = DatabaseService.drop_table(db, table_name) + return SQLExecuteResponse(**result) + +@router.post("/import-data", response_model=SQLExecuteResponse) +def import_data(request: ImportDataRequest, db: Session = Depends(get_db)): + """导入数据""" + result = DatabaseService.import_data(db, request.table_name, request.data) + return SQLExecuteResponse(**result) + +@router.get("/tables", response_model=List[str]) +def get_table_list(): + """获取所有表名""" + return DatabaseService.get_table_list() \ No newline at end of file diff --git a/app/api/task.py b/app/api/task.py new file mode 100644 index 0000000..7b6bce5 --- /dev/null +++ b/app/api/task.py @@ -0,0 +1,187 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +from ..schemas.task import ( + JobResponse, AddCronJobRequest, AddIntervalJobRequest, + AddDateJobRequest, TaskResponse +) +from ..utils.scheduler import task_scheduler, example_task, database_cleanup_task + +router = APIRouter(prefix="/tasks", tags=["定时任务管理"]) + +# 可用的任务函数映射 +AVAILABLE_FUNCTIONS = { + "example_task": example_task, + "database_cleanup_task": database_cleanup_task, +} + +@router.post("/cron", response_model=TaskResponse) +def add_cron_job(request: AddCronJobRequest): + """添加cron定时任务""" + try: + if request.func_name not in AVAILABLE_FUNCTIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"函数 {request.func_name} 不可用" + ) + + func = AVAILABLE_FUNCTIONS[request.func_name] + + # 构建cron参数 + cron_kwargs = {} + if request.year is not None: + cron_kwargs['year'] = request.year + if request.month is not None: + cron_kwargs['month'] = request.month + if request.day is not None: + cron_kwargs['day'] = request.day + if request.week is not None: + cron_kwargs['week'] = request.week + if request.day_of_week is not None: + cron_kwargs['day_of_week'] = request.day_of_week + if request.hour is not None: + cron_kwargs['hour'] = request.hour + if request.minute is not None: + cron_kwargs['minute'] = request.minute + if request.second is not None: + cron_kwargs['second'] = request.second + + job = task_scheduler.add_cron_job(func, request.job_id, **cron_kwargs) + + return TaskResponse( + success=True, + message=f"Cron任务 {request.job_id} 添加成功", + data={"job_id": job.id, "next_run": str(job.next_run_time)} + ) + + except Exception as e: + return TaskResponse( + success=False, + message=f"添加Cron任务失败: {str(e)}" + ) + +@router.post("/interval", response_model=TaskResponse) +def add_interval_job(request: AddIntervalJobRequest): + """添加间隔执行任务""" + try: + if request.func_name not in AVAILABLE_FUNCTIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"函数 {request.func_name} 不可用" + ) + + func = AVAILABLE_FUNCTIONS[request.func_name] + + # 构建interval参数 + interval_kwargs = {} + if request.seconds is not None: + interval_kwargs['seconds'] = request.seconds + if request.minutes is not None: + interval_kwargs['minutes'] = request.minutes + if request.hours is not None: + interval_kwargs['hours'] = request.hours + if request.days is not None: + interval_kwargs['days'] = request.days + + job = task_scheduler.add_interval_job(func, request.job_id, **interval_kwargs) + + return TaskResponse( + success=True, + message=f"间隔任务 {request.job_id} 添加成功", + data={"job_id": job.id, "next_run": str(job.next_run_time)} + ) + + except Exception as e: + return TaskResponse( + success=False, + message=f"添加间隔任务失败: {str(e)}" + ) + +@router.post("/date", response_model=TaskResponse) +def add_date_job(request: AddDateJobRequest): + """添加指定时间执行任务""" + try: + if request.func_name not in AVAILABLE_FUNCTIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"函数 {request.func_name} 不可用" + ) + + func = AVAILABLE_FUNCTIONS[request.func_name] + job = task_scheduler.add_date_job(func, request.job_id, run_date=request.run_date) + + return TaskResponse( + success=True, + message=f"定时任务 {request.job_id} 添加成功", + data={"job_id": job.id, "run_date": str(job.next_run_time)} + ) + + except Exception as e: + return TaskResponse( + success=False, + message=f"添加定时任务失败: {str(e)}" + ) + +@router.get("/", response_model=List[JobResponse]) +def get_jobs(): + """获取所有任务""" + jobs = task_scheduler.get_jobs() + result = [] + for job in jobs: + result.append(JobResponse( + id=job.id, + name=job.name, + func=str(job.func), + trigger=str(job.trigger), + next_run_time=job.next_run_time + )) + return result + +@router.delete("/{job_id}", response_model=TaskResponse) +def remove_job(job_id: str): + """删除任务""" + success = task_scheduler.remove_job(job_id) + if success: + return TaskResponse( + success=True, + message=f"任务 {job_id} 删除成功" + ) + else: + return TaskResponse( + success=False, + message=f"删除任务 {job_id} 失败" + ) + +@router.put("/{job_id}/pause", response_model=TaskResponse) +def pause_job(job_id: str): + """暂停任务""" + success = task_scheduler.pause_job(job_id) + if success: + return TaskResponse( + success=True, + message=f"任务 {job_id} 已暂停" + ) + else: + return TaskResponse( + success=False, + message=f"暂停任务 {job_id} 失败" + ) + +@router.put("/{job_id}/resume", response_model=TaskResponse) +def resume_job(job_id: str): + """恢复任务""" + success = task_scheduler.resume_job(job_id) + if success: + return TaskResponse( + success=True, + message=f"任务 {job_id} 已恢复" + ) + else: + return TaskResponse( + success=False, + message=f"恢复任务 {job_id} 失败" + ) + +@router.get("/functions", response_model=List[str]) +def get_available_functions(): + """获取可用的任务函数列表""" + return list(AVAILABLE_FUNCTIONS.keys()) \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/__pycache__/__init__.cpython-312.pyc b/app/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..4c03a56 Binary files /dev/null and b/app/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/core/__pycache__/config.cpython-312.pyc b/app/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..840ac20 Binary files /dev/null and b/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/app/core/__pycache__/database.cpython-312.pyc b/app/core/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..7156437 Binary files /dev/null and b/app/core/__pycache__/database.cpython-312.pyc differ diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..e29762c --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,18 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Settings: + DATABASE_URL = os.getenv("DATABASE_URL", "mysql+pymysql://root:password@localhost:3306/tielu_db") + DB_HOST = os.getenv("DB_HOST", "localhost") + DB_PORT = int(os.getenv("DB_PORT", 3306)) + DB_USER = os.getenv("DB_USER", "root") + DB_PASSWORD = os.getenv("DB_PASSWORD", "password") + DB_NAME = os.getenv("DB_NAME", "tielu_db") + + APP_HOST = os.getenv("APP_HOST", "0.0.0.0") + APP_PORT = int(os.getenv("APP_PORT", 8000)) + APP_DEBUG = os.getenv("APP_DEBUG", "True").lower() == "true" + +settings = Settings() \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..e4e372f --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine, MetaData +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from .config import settings + +engine = create_engine(settings.DATABASE_URL, echo=settings.APP_DEBUG) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def init_db(): + """初始化数据库表""" + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0219392 --- /dev/null +++ b/app/main.py @@ -0,0 +1,95 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager +import logging + +from .core.config import settings +from .core.database import init_db +from .api import account_router, database_router, task_router +from .utils.scheduler import task_scheduler + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理""" + # 启动时执行 + logger.info("应用启动中...") + + # 初始化数据库 + try: + init_db() + logger.info("数据库初始化完成") + except Exception as e: + logger.error(f"数据库初始化失败: {e}") + + # 启动定时任务调度器 + try: + task_scheduler.start() + logger.info("定时任务调度器启动完成") + except Exception as e: + logger.error(f"定时任务调度器启动失败: {e}") + + yield + + # 关闭时执行 + logger.info("应用关闭中...") + try: + task_scheduler.shutdown() + logger.info("定时任务调度器已关闭") + except Exception as e: + logger.error(f"定时任务调度器关闭失败: {e}") + +# 创建FastAPI应用 +app = FastAPI( + title="铁路项目管理系统", + description="基于FastAPI、MySQL、SQLAlchemy的铁路项目管理系统", + version="1.0.0", + lifespan=lifespan +) + +# 配置CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 生产环境建议配置具体域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +app.include_router(account_router, prefix="/api") +app.include_router(database_router, prefix="/api") +app.include_router(task_router, prefix="/api") + +# 根路径 +@app.get("/") +async def root(): + """根路径""" + return { + "message": "铁路项目管理系统 API", + "version": "1.0.0", + "docs": "/docs", + "redoc": "/redoc" + } + +# 健康检查 +@app.get("/health") +async def health_check(): + """健康检查""" + return { + "status": "healthy", + "database": "connected", + "scheduler": "running" if task_scheduler.scheduler.running else "stopped" + } + +# 全局异常处理 +@app.exception_handler(Exception) +async def global_exception_handler(request, exc): + logger.error(f"全局异常: {str(exc)}") + return HTTPException( + status_code=500, + detail="服务器内部错误" + ) \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..17ba02a --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,3 @@ +from .account import Account + +__all__ = ["Account"] \ No newline at end of file diff --git a/app/models/__pycache__/__init__.cpython-312.pyc b/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..acf85bc Binary files /dev/null and b/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/models/__pycache__/account.cpython-312.pyc b/app/models/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000..18a9620 Binary files /dev/null and b/app/models/__pycache__/account.cpython-312.pyc differ diff --git a/app/models/account.py b/app/models/account.py new file mode 100644 index 0000000..f82082d --- /dev/null +++ b/app/models/account.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from sqlalchemy.sql import func +from ..core.database import Base + +class Account(Base): + __tablename__ = "accounts" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + account = Column(String(50), unique=True, index=True, nullable=False, comment="账号") + password = Column(String(100), nullable=False, comment="密码") + status = Column(Integer, default=1, comment="状态: 1-正常, 0-禁用") + today_updated = Column(Integer, default=0, comment="是否更新") + section = Column(String(100), comment="标段") + created_at = Column(DateTime, server_default=func.now(), comment="创建时间") + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间") \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..b936502 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1,14 @@ +from .account import ( + AccountBase, AccountCreate, AccountUpdate, AccountResponse, + AccountListRequest, AccountGetRequest, AccountUpdateRequest, AccountDeleteRequest +) +from .database import SQLExecuteRequest, SQLExecuteResponse, TableDataRequest, TableDataResponse, CreateTableRequest, ImportDataRequest +from .task import JobResponse, AddCronJobRequest, AddIntervalJobRequest, AddDateJobRequest, TaskResponse + +__all__ = [ + "AccountBase", "AccountCreate", "AccountUpdate", "AccountResponse", + "AccountListRequest", "AccountGetRequest", "AccountUpdateRequest", "AccountDeleteRequest", + "SQLExecuteRequest", "SQLExecuteResponse", "TableDataRequest", + "TableDataResponse", "CreateTableRequest", "ImportDataRequest", + "JobResponse", "AddCronJobRequest", "AddIntervalJobRequest", "AddDateJobRequest", "TaskResponse" +] \ No newline at end of file diff --git a/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..789885e Binary files /dev/null and b/app/schemas/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/account.cpython-312.pyc b/app/schemas/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000..fbe67c1 Binary files /dev/null and b/app/schemas/__pycache__/account.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/database.cpython-312.pyc b/app/schemas/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..a5b89b2 Binary files /dev/null and b/app/schemas/__pycache__/database.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/task.cpython-312.pyc b/app/schemas/__pycache__/task.cpython-312.pyc new file mode 100644 index 0000000..f6c0111 Binary files /dev/null and b/app/schemas/__pycache__/task.cpython-312.pyc differ diff --git a/app/schemas/account.py b/app/schemas/account.py new file mode 100644 index 0000000..333f565 --- /dev/null +++ b/app/schemas/account.py @@ -0,0 +1,42 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class AccountBase(BaseModel): + account: str + password: str + status: Optional[int] = 1 + today_updated: Optional[int] = 0 + section: Optional[str] = None + +class AccountCreate(AccountBase): + pass + +class AccountUpdate(BaseModel): + account: Optional[str] = None + password: Optional[str] = None + status: Optional[int] = None + today_updated: Optional[int] = None + section: Optional[str] = None + +class AccountResponse(AccountBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class AccountListRequest(BaseModel): + skip: Optional[int] = 0 + limit: Optional[int] = 100 + +class AccountGetRequest(BaseModel): + account_id: int + +class AccountUpdateRequest(BaseModel): + account_id: int + account_data: AccountUpdate + +class AccountDeleteRequest(BaseModel): + account_id: int \ No newline at end of file diff --git a/app/schemas/database.py b/app/schemas/database.py new file mode 100644 index 0000000..b96a231 --- /dev/null +++ b/app/schemas/database.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import Any, Dict, List, Optional + +class SQLExecuteRequest(BaseModel): + sql: str + +class SQLExecuteResponse(BaseModel): + success: bool + message: str + data: Optional[Any] = None + rows_affected: Optional[int] = None + +class TableDataRequest(BaseModel): + table_name: str + limit: Optional[int] = 100 + offset: Optional[int] = 0 + +class TableDataResponse(BaseModel): + success: bool + message: str + data: Optional[List[Dict[str, Any]]] = None + total_count: Optional[int] = None + +class CreateTableRequest(BaseModel): + table_name: str + columns: Dict[str, str] # 列名: 数据类型 + primary_key: Optional[str] = None + +class ImportDataRequest(BaseModel): + table_name: str + data: List[Dict[str, Any]] \ No newline at end of file diff --git a/app/schemas/task.py b/app/schemas/task.py new file mode 100644 index 0000000..c03d098 --- /dev/null +++ b/app/schemas/task.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from typing import Optional, List, Dict, Any +from datetime import datetime + +class JobResponse(BaseModel): + id: str + name: Optional[str] = None + func: str + trigger: str + next_run_time: Optional[datetime] = None + +class AddCronJobRequest(BaseModel): + job_id: str + func_name: str + cron_expression: Optional[str] = None + year: Optional[int] = None + month: Optional[int] = None + day: Optional[int] = None + week: Optional[int] = None + day_of_week: Optional[int] = None + hour: Optional[int] = None + minute: Optional[int] = None + second: Optional[int] = None + +class AddIntervalJobRequest(BaseModel): + job_id: str + func_name: str + seconds: Optional[int] = None + minutes: Optional[int] = None + hours: Optional[int] = None + days: Optional[int] = None + +class AddDateJobRequest(BaseModel): + job_id: str + func_name: str + run_date: datetime + +class TaskResponse(BaseModel): + success: bool + message: str + data: Optional[Any] = None \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..a06d677 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,4 @@ +from .account import AccountService +from .database import DatabaseService + +__all__ = ["AccountService", "DatabaseService"] \ No newline at end of file diff --git a/app/services/__pycache__/__init__.cpython-312.pyc b/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..67860a2 Binary files /dev/null and b/app/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/services/__pycache__/account.cpython-312.pyc b/app/services/__pycache__/account.cpython-312.pyc new file mode 100644 index 0000000..3dde1d0 Binary files /dev/null and b/app/services/__pycache__/account.cpython-312.pyc differ diff --git a/app/services/__pycache__/database.cpython-312.pyc b/app/services/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..2a70ce7 Binary files /dev/null and b/app/services/__pycache__/database.cpython-312.pyc differ diff --git a/app/services/account.py b/app/services/account.py new file mode 100644 index 0000000..800d016 --- /dev/null +++ b/app/services/account.py @@ -0,0 +1,51 @@ +from sqlalchemy.orm import Session +from ..models.account import Account +from ..schemas.account import AccountCreate, AccountUpdate +from typing import List, Optional + +class AccountService: + @staticmethod + def create_account(db: Session, account_data: AccountCreate) -> Account: + """创建账号""" + db_account = Account(**account_data.dict()) + db.add(db_account) + db.commit() + db.refresh(db_account) + return db_account + + @staticmethod + def get_account(db: Session, account_id: int) -> Optional[Account]: + """根据ID获取账号""" + return db.query(Account).filter(Account.id == account_id).first() + + @staticmethod + def get_account_by_account(db: Session, account: str) -> Optional[Account]: + """根据账号名获取账号""" + return db.query(Account).filter(Account.account == account).first() + + @staticmethod + def get_accounts(db: Session, skip: int = 0, limit: int = 100) -> List[Account]: + """获取账号列表""" + return db.query(Account).offset(skip).limit(limit).all() + + @staticmethod + def update_account(db: Session, account_id: int, account_data: AccountUpdate) -> Optional[Account]: + """更新账号""" + db_account = db.query(Account).filter(Account.id == account_id).first() + if db_account: + update_data = account_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_account, field, value) + db.commit() + db.refresh(db_account) + return db_account + + @staticmethod + def delete_account(db: Session, account_id: int) -> bool: + """删除账号""" + db_account = db.query(Account).filter(Account.id == account_id).first() + if db_account: + db.delete(db_account) + db.commit() + return True + return False \ No newline at end of file diff --git a/app/services/database.py b/app/services/database.py new file mode 100644 index 0000000..0e2f238 --- /dev/null +++ b/app/services/database.py @@ -0,0 +1,195 @@ +from sqlalchemy.orm import Session +from sqlalchemy import text, MetaData, Table, Column, create_engine, inspect +from sqlalchemy.exc import SQLAlchemyError +from typing import List, Dict, Any, Optional +from ..core.database import engine +import pandas as pd + +class DatabaseService: + @staticmethod + def execute_sql(db: Session, sql: str) -> Dict[str, Any]: + """执行SQL语句""" + try: + result = db.execute(text(sql)) + + # 判断是否为SELECT查询 + if sql.strip().upper().startswith('SELECT'): + data = [] + columns = result.keys() + for row in result: + data.append(dict(zip(columns, row))) + + return { + "success": True, + "message": "查询成功", + "data": data + } + else: + # 非SELECT语句,提交事务 + db.commit() + return { + "success": True, + "message": "执行成功", + "rows_affected": result.rowcount + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"SQL执行失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def get_table_data(db: Session, table_name: str, limit: int = 100, offset: int = 0) -> Dict[str, Any]: + """获取表数据""" + try: + # 先检查表是否存在 + inspector = inspect(engine) + if table_name not in inspector.get_table_names(): + return { + "success": False, + "message": f"表 {table_name} 不存在" + } + + # 获取总数 + count_sql = f"SELECT COUNT(*) as total FROM {table_name}" + count_result = db.execute(text(count_sql)).fetchone() + total_count = count_result.total if count_result else 0 + + # 获取数据 + sql = f"SELECT * FROM {table_name} LIMIT {limit} OFFSET {offset}" + result = db.execute(text(sql)) + + data = [] + columns = result.keys() + for row in result: + data.append(dict(zip(columns, row))) + + return { + "success": True, + "message": "获取数据成功", + "data": data, + "total_count": total_count + } + + except SQLAlchemyError as e: + return { + "success": False, + "message": f"获取表数据失败: {str(e)}" + } + except Exception as e: + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def create_table(db: Session, table_name: str, columns: Dict[str, str], primary_key: Optional[str] = None) -> Dict[str, Any]: + """创建表""" + try: + # 构建CREATE TABLE语句 + column_definitions = [] + for col_name, col_type in columns.items(): + column_definitions.append(f"{col_name} {col_type}") + + if primary_key: + column_definitions.append(f"PRIMARY KEY ({primary_key})") + + sql = f"CREATE TABLE {table_name} ({', '.join(column_definitions)})" + + db.execute(text(sql)) + db.commit() + + return { + "success": True, + "message": f"表 {table_name} 创建成功" + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"创建表失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def drop_table(db: Session, table_name: str) -> Dict[str, Any]: + """删除表""" + try: + sql = f"DROP TABLE IF EXISTS {table_name}" + db.execute(text(sql)) + db.commit() + + return { + "success": True, + "message": f"表 {table_name} 删除成功" + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"删除表失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def import_data(db: Session, table_name: str, data: List[Dict[str, Any]]) -> Dict[str, Any]: + """导入数据到表""" + try: + if not data: + return { + "success": False, + "message": "导入数据不能为空" + } + + # 使用pandas DataFrame来处理数据导入 + df = pd.DataFrame(data) + + # 使用pandas的to_sql方法 + df.to_sql(table_name, engine, if_exists='append', index=False) + + return { + "success": True, + "message": f"成功导入 {len(data)} 条数据到表 {table_name}" + } + + except SQLAlchemyError as e: + db.rollback() + return { + "success": False, + "message": f"导入数据失败: {str(e)}" + } + except Exception as e: + db.rollback() + return { + "success": False, + "message": f"未知错误: {str(e)}" + } + + @staticmethod + def get_table_list() -> List[str]: + """获取所有表名""" + try: + inspector = inspect(engine) + return inspector.get_table_names() + except Exception as e: + return [] \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..3f0fb19 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,3 @@ +from .scheduler import task_scheduler, example_task, database_cleanup_task + +__all__ = ["task_scheduler", "example_task", "database_cleanup_task"] \ No newline at end of file diff --git a/app/utils/__pycache__/__init__.cpython-312.pyc b/app/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..39ce5e2 Binary files /dev/null and b/app/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/utils/__pycache__/scheduler.cpython-312.pyc b/app/utils/__pycache__/scheduler.cpython-312.pyc new file mode 100644 index 0000000..9705b7c Binary files /dev/null and b/app/utils/__pycache__/scheduler.cpython-312.pyc differ diff --git a/app/utils/scheduler.py b/app/utils/scheduler.py new file mode 100644 index 0000000..de39819 --- /dev/null +++ b/app/utils/scheduler.py @@ -0,0 +1,124 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore +from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR +import logging +from ..core.config import settings + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class TaskScheduler: + def __init__(self): + # 配置作业存储 + jobstores = { + 'default': SQLAlchemyJobStore(url=settings.DATABASE_URL) + } + + # 配置执行器 + executors = { + 'default': ThreadPoolExecutor(20) + } + + # 作业默认配置 + job_defaults = { + 'coalesce': False, + 'max_instances': 3 + } + + # 创建调度器 + self.scheduler = BackgroundScheduler( + jobstores=jobstores, + executors=executors, + job_defaults=job_defaults, + timezone='Asia/Shanghai' + ) + + # 添加事件监听器 + self.scheduler.add_listener(self._job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) + + def _job_listener(self, event): + """作业执行监听器""" + if event.exception: + logger.error(f"Job {event.job_id} crashed: {event.exception}") + else: + logger.info(f"Job {event.job_id} executed successfully") + + def start(self): + """启动调度器""" + if not self.scheduler.running: + self.scheduler.start() + logger.info("定时任务调度器已启动") + + def shutdown(self): + """关闭调度器""" + if self.scheduler.running: + self.scheduler.shutdown() + logger.info("定时任务调度器已关闭") + + def add_cron_job(self, func, job_id: str, **kwargs): + """添加cron定时任务""" + return self.scheduler.add_job(func, 'cron', id=job_id, **kwargs) + + def add_interval_job(self, func, job_id: str, **kwargs): + """添加间隔执行任务""" + return self.scheduler.add_job(func, 'interval', id=job_id, **kwargs) + + def add_date_job(self, func, job_id: str, **kwargs): + """添加指定时间执行任务""" + return self.scheduler.add_job(func, 'date', id=job_id, **kwargs) + + def remove_job(self, job_id: str): + """移除任务""" + try: + self.scheduler.remove_job(job_id) + logger.info(f"任务 {job_id} 已移除") + return True + except Exception as e: + logger.error(f"移除任务失败: {e}") + return False + + def get_job(self, job_id: str): + """获取任务""" + return self.scheduler.get_job(job_id) + + def get_jobs(self): + """获取所有任务""" + return self.scheduler.get_jobs() + + def pause_job(self, job_id: str): + """暂停任务""" + try: + self.scheduler.pause_job(job_id) + logger.info(f"任务 {job_id} 已暂停") + return True + except Exception as e: + logger.error(f"暂停任务失败: {e}") + return False + + def resume_job(self, job_id: str): + """恢复任务""" + try: + self.scheduler.resume_job(job_id) + logger.info(f"任务 {job_id} 已恢复") + return True + except Exception as e: + logger.error(f"恢复任务失败: {e}") + return False + +# 全局调度器实例 +task_scheduler = TaskScheduler() + +# 示例定时任务函数 +def example_task(): + """示例定时任务""" + logger.info("执行示例定时任务") + # 这里可以添加具体的业务逻辑 + return "任务执行完成" + +def database_cleanup_task(): + """数据库清理任务示例""" + logger.info("执行数据库清理任务") + # 这里可以添加数据库清理逻辑 + return "数据库清理完成" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..2a3568e --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +import uvicorn +from app.main import app +from app.core.config import settings + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=settings.APP_HOST, + port=settings.APP_PORT, + reload=settings.APP_DEBUG + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0d72d9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +sqlalchemy==2.0.23 +pymysql==1.1.0 +cryptography==41.0.7 +pydantic==2.5.0 +python-dotenv==1.0.0 +apscheduler==3.10.4 +pandas==2.1.3 +python-multipart==0.0.6 \ No newline at end of file