MySQL、FastAPI 和 React 构建一个完整的全栈待办事项应用

项目概述

我们将创建一个简单的待办事项(Todo)应用程序,包含以下组件:

  1. MySQL 数据库:存储待办事项
  2. FastAPI 后端:提供 RESTful API
  3. React 前端:用户界面

项目结构

mysql_fastapi_react/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── models.py
│   │   ├── database.py
│   │   └── routers/
│   │       └── todos.py
│   ├── requirements.txt
│   └── Dockerfile
├── frontend/
│   ├── src/
│   │   ├── App.jsx
│   │   ├── main.jsx
│   │   └── components/
│   │       └── TodoList.jsx
│   ├── package.json
│   └── index.html
├── docker-compose.yml
└── README.md

步骤 1:创建项目目录结构

首先,让我们创建项目的基本目录结构:

mkdir -p backend/app/routers
mkdir -p frontend/src/components

步骤 2:设置 MySQL 和 Docker

创建 docker-compose.yml 文件:

version: '3'

services:
  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_DATABASE: tododb
      MYSQL_USER: todouser
      MYSQL_PASSWORD: todopassword
      MYSQL_ROOT_PASSWORD: rootpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app-network

  backend:
    build: ./backend
    restart: always
    ports:
      - "8000:8000"
    depends_on:
      - db
    environment:
      - DATABASE_URL=mysql+pymysql://todouser:todopassword@db:3306/tododb
    networks:
      - app-network

  frontend:
    build: ./frontend
    restart: always
    ports:
      - "5173:5173"
    depends_on:
      - backend
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql_data:

步骤 3:设置 FastAPI 后端

创建 requirements.txt

fastapi
uvicorn
sqlalchemy
pymysql
cryptography
pydantic

创建 Dockerfile

FROM python:3.12-slim

WORKDIR /app

# 假设 requirements.txt 在 backend 目录下
COPY requirements.txt .
RUN pip install -r requirements.txt

# 将整个 app 目录复制到容器中
COPY . .

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

创建数据库连接

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

DATABASE_URL = os.getenv("DATABASE_URL", "mysql+pymysql://todouser:todopassword@localhost:3306/tododb")

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

# 依赖项,用于获取数据库会话
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

创建数据模型

from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.sql import func
from .database import Base

class Todo(Base):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(100), nullable=False)
    description = Column(String(255))
    completed = Column(Boolean, default=False)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(
        DateTime(timezone=True),
        server_default=func.now(),   # ✅ 新增
        onupdate=func.now(),
        nullable=False  # 确保不为空
    )

创建路由

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel
from datetime import datetime
from .. import models
from ..database import get_db

router = APIRouter(
    prefix="/todos",
    tags=["todos"],
)

# Pydantic 模型用于请求和响应
class TodoBase(BaseModel):
    title: str
    description: str = None
    completed: bool = False

class TodoCreate(TodoBase):
    pass

class TodoResponse(TodoBase):
    id: int
    created_at: datetime 
    updated_at: datetime 

    class Config:
        from_attributes = True 

@router.get("/", response_model=List[TodoResponse])
def get_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    todos = db.query(models.Todo).offset(skip).limit(limit).all()
    return todos

@router.post("/", response_model=TodoResponse, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
    db_todo = models.Todo(**todo.model_dump())  # 使用 model_dump() 替代 dict()
    db.add(db_todo)
    db.commit()
    db.refresh(db_todo)
    return db_todo

@router.get("/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
    db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
    if db_todo is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    return db_todo

@router.put("/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo: TodoBase, db: Session = Depends(get_db)):
    db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
    if db_todo is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    
    for key, value in todo.model_dump().items():  # 使用 model_dump() 替代 dict()
        setattr(db_todo, key, value)
    
    db.commit()
    db.refresh(db_todo)
    return db_todo

@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
    db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
    if db_todo is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    
    db.delete(db_todo)
    db.commit()
    return None

创建主应用

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import todos
from . import models
from .database import engine

# 创建数据库表
models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="Todo API")

# 配置 CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 在生产环境中应该限制为前端域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 包含路由
app.include_router(todos.router)

@app.get("/")
def read_root():
    return {"message": "Welcome to Todo API"}

创建 init.py 文件

# 初始化包

步骤 4:设置 React 前端

创建 package.json

{
  "name": "todo-frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@vitejs/plugin-react": "^4.0.3",
    "vite": "^4.4.5"
  }
}

创建 index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Todo App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

创建 main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

创建 App.jsx

import React from 'react';
import TodoList from './components/TodoList';

function App() {
  return (
    <div className="App" style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <h1>待办事项应用</h1>
      <TodoList />
    </div>
  );
}

export default App;

创建 TodoList 组件

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const API_URL = 'http://localhost:8000';

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    try {
      setLoading(true);
      const response = await axios.get(`${API_URL}/todos/`);
      setTodos(response.data);
      setError(null);
    } catch (err) {
      setError('获取待办事项失败');
      console.error('获取待办事项失败:', err);
    } finally {
      setLoading(false);
    }
  };

  const addTodo = async (e) => {
    e.preventDefault();
    if (!title.trim()) return;

    try {
      const response = await axios.post(`${API_URL}/todos/`, {
        title,
        description,
        completed: false
      });
      setTodos([...todos, response.data]);
      setTitle('');
      setDescription('');
    } catch (err) {
      setError('添加待办事项失败');
      console.error('添加待办事项失败:', err);
    }
  };

  const toggleTodo = async (todo) => {
    try {
      const updatedTodo = { ...todo, completed: !todo.completed };
      await axios.put(`${API_URL}/todos/${todo.id}`, updatedTodo);
      setTodos(todos.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t));
    } catch (err) {
      setError('更新待办事项失败');
      console.error('更新待办事项失败:', err);
    }
  };

  const deleteTodo = async (id) => {
    try {
      await axios.delete(`${API_URL}/todos/${id}`);
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (err) {
      setError('删除待办事项失败');
      console.error('删除待办事项失败:', err);
    }
  };

  if (loading) return <p>加载中...</p>;

  return (
    <div>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      
      <form onSubmit={addTodo} style={{ marginBottom: '20px' }}>
        <div style={{ marginBottom: '10px' }}>
          <input
            type="text"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="标题"
            style={{ padding: '8px', width: '300px' }}
            required
          />
        </div>
        <div style={{ marginBottom: '10px' }}>
          <textarea
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="描述"
            style={{ padding: '8px', width: '300px', height: '80px' }}
          />
        </div>
        <button 
          type="submit" 
          style={{ 
            padding: '8px 16px', 
            backgroundColor: '#4CAF50', 
            color: 'white', 
            border: 'none', 
            cursor: 'pointer' 
          }}
        >
          添加待办事项
        </button>
      </form>

      <h2>待办事项列表</h2>
      {todos.length === 0 ? (
        <p>没有待办事项</p>
      ) : (
        <ul style={{ listStyleType: 'none', padding: 0 }}>
          {todos.map((todo) => (
            <li 
              key={todo.id} 
              style={{ 
                padding: '10px', 
                border: '1px solid #ddd', 
                marginBottom: '10px',
                backgroundColor: todo.completed ? '#f8f8f8' : 'white',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center'
              }}
            >
              <div>
                <h3 style={{ 
                  textDecoration: todo.completed ? 'line-through' : 'none',
                  margin: '0 0 5px 0'
                }}>
                  {todo.title}
                </h3>
                <p style={{ margin: '0' }}>{todo.description}</p>
              </div>
              <div>
                <button 
                  onClick={() => toggleTodo(todo)}
                  style={{ 
                    marginRight: '5px', 
                    padding: '5px 10px', 
                    backgroundColor: todo.completed ? '#2196F3' : '#4CAF50', 
                    color: 'white', 
                    border: 'none', 
                    cursor: 'pointer' 
                  }}
                >
                  {todo.completed ? '标记为未完成' : '标记为完成'}
                </button>
                <button 
                  onClick={() => deleteTodo(todo.id)}
                  style={{ 
                    padding: '5px 10px', 
                    backgroundColor: '#f44336', 
                    color: 'white', 
                    border: 'none', 
                    cursor: 'pointer' 
                  }}
                >
                  删除
                </button>
              </div>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default TodoList;

创建 Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

步骤 5:创建 README.md

# MySQL + FastAPI + React 示例项目

这是一个简单的待办事项应用,展示了 MySQL、FastAPI 和 React 的交互过程。

## 技术栈

- **数据库**: MySQL 8.0 (Docker)
- **后端**: FastAPI + SQLAlchemy
- **前端**: React + Vite
- **包管理**: uv (Python), npm (JavaScript)
- **部署**: Docker Compose

## 项目结构

mysql_fastapi_react/
├── backend/            # FastAPI 后端
├── frontend/           # React 前端
└── docker-compose.yml  # Docker 配置


## 启动项目

1. 确保已安装 Docker 和 Docker Compose

2. 启动所有服务:

```bash
docker-compose up -d```

3. 访问应用:
   - 前端: http://localhost:5173
   - 后端 API: http://localhost:8000
   - API 文档: http://localhost:8000/docs

## 开发说明

### 后端开发

后端使用 FastAPI 框架,通过 SQLAlchemy ORM 连接 MySQL 数据库。

### 前端开发

前端使用 React 框架,通过 Axios 与后端 API 通信。

## 交互流程

1. React 前端通过 Axios 发送 HTTP 请求到 FastAPI 后端
2. FastAPI 处理请求,与 MySQL 数据库交互
3. FastAPI 返回 JSON 响应给前端
4. React 更新 UI 显示数据

如何运行项目

  1. 确保你已经安装了 Docker 和 Docker Compose

  2. 在项目根目录下运行以下命令启动所有服务:

docker-compose up -d
  1. 访问应用:
    • 前端: http://localhost:5173
    • 后端 API: http://localhost:8000
    • API 文档: http://localhost:8000/docs

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

交互过程说明

这个项目展示了三个组件之间的交互过程:

  1. 前端 (React) 与后端 (FastAPI) 的交互:

    • React 前端通过 Axios 发送 HTTP 请求到 FastAPI 后端
    • FastAPI 处理请求并返回 JSON 响应
    • React 更新 UI 显示数据
  2. 后端 (FastAPI) 与数据库 (MySQL) 的交互:

    • FastAPI 使用 SQLAlchemy ORM 连接 MySQL 数据库
    • 通过 SQLAlchemy 模型定义数据结构
    • 使用 SQLAlchemy 会话执行 CRUD 操作
  3. 数据流程:

    • 用户在 React 界面上执行操作 (如添加待办事项)
    • React 发送 HTTP POST 请求到 FastAPI
    • FastAPI 接收请求,验证数据
    • FastAPI 使用 SQLAlchemy 将数据存储到 MySQL
    • MySQL 保存数据并返回结果
    • FastAPI 将结果转换为 JSON 并返回给 React
    • React 更新 UI 显示新添加的待办事项

这个简单的项目展示了现代全栈应用的基本架构和交互方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值