commit fb0a53676fcaf0321a3dca91f4b6ec49402948d7
Author: dengwenhao <1403595435@qq.com>
Date: Thu Jul 16 15:42:31 2026 +0800
Initial TodoList MySQL delivery
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..13275e7
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+MYSQL_ROOT_PASSWORD=replace_with_a_strong_root_password
+MYSQL_DATABASE=todolist
+MYSQL_USER=todolist_app
+MYSQL_PASSWORD=replace_with_a_strong_app_password
+MYSQL_HOST=127.0.0.1
+MYSQL_PORT=3306
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cbb619b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.env
+.venv/
+__pycache__/
+*.py[cod]
+todos.json*
+db_test.py
+db_insert_test.py
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..380ab00
--- /dev/null
+++ b/README.md
@@ -0,0 +1,129 @@
+# TodoList 全栈项目(FastAPI + MySQL)
+
+这是一个用于学习后端开发的 TodoList 项目。前端由 Nginx 提供,`/api` 请求反向代理到 FastAPI,FastAPI 使用参数化 SQL 读写 Docker 中的 MySQL 8.4。
+
+## 架构
+
+```text
+浏览器
+ -> Windows 局域网端口 8080(当前环境使用端口转发)
+ -> WSL Nginx :80
+ -> FastAPI/Uvicorn 127.0.0.1:8000
+ -> MySQL 127.0.0.1:3306
+```
+
+## 目录
+
+```text
+main.py FastAPI 后端
+frontend/index.html 前端页面
+compose.yaml MySQL Docker Compose 配置
+schema.sql 数据库建表脚本
+requirements.txt Python 依赖及版本
+.env.example 环境变量示例(不含真实密码)
+deploy/nginx-todolist.conf Nginx 站点配置
+deploy/todolist.service systemd 服务配置
+验收记录.md 已完成测试和已知限制
+```
+
+## 环境要求
+
+- Windows + WSL2 Ubuntu(当前部署方式)
+- Python 3.12 或兼容版本
+- Docker Engine 与 Docker Compose 插件
+- Nginx
+- systemd(用于后台运行 FastAPI)
+
+## 1. 准备 Python 环境
+
+```bash
+cd /home/dwh/code/fastapi_todolist
+python3 -m venv .venv
+source .venv/bin/activate
+python -m pip install -r requirements.txt
+```
+
+如果部署用户或目录不同,需要同步修改 `deploy/todolist.service` 中的 `User`、`WorkingDirectory` 和 `ExecStart`。
+
+## 2. 准备环境变量
+
+```bash
+cp .env.example .env
+chmod 600 .env
+vim .env
+```
+
+请替换两个密码占位符。不要提交或对外发送真实 `.env`。
+
+## 3. 启动 MySQL 并建表
+
+```bash
+docker compose up -d
+docker compose ps
+```
+
+首次创建数据卷后执行建表脚本:
+
+```bash
+docker compose exec -T mysql sh -lc 'exec mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' < schema.sql
+```
+
+MySQL 端口仅绑定到 `127.0.0.1:3306`,不会直接暴露给局域网。
+
+## 4. 安装前端与 Nginx 配置
+
+```bash
+sudo mkdir -p /var/www/todolist
+sudo cp frontend/index.html /var/www/todolist/index.html
+sudo cp deploy/nginx-todolist.conf /etc/nginx/sites-available/todolist
+sudo ln -s /etc/nginx/sites-available/todolist /etc/nginx/sites-enabled/todolist
+sudo nginx -t
+sudo systemctl restart nginx
+```
+
+若 `/etc/nginx/sites-enabled/default` 与交付配置同时声明 `default_server`,请先停用默认站点或去掉其中一方的 `default_server`。
+
+## 5. 安装 FastAPI systemd 服务
+
+```bash
+sudo cp deploy/todolist.service /etc/systemd/system/todolist.service
+sudo systemctl daemon-reload
+sudo systemctl enable --now todolist.service
+sudo systemctl status todolist.service --no-pager -l
+```
+
+## 6. 验证
+
+直连 FastAPI:
+
+```bash
+curl http://127.0.0.1:8000/health
+curl http://127.0.0.1:8000/todos
+```
+
+通过 Nginx:
+
+```bash
+curl http://127.0.0.1/
+curl http://127.0.0.1/api/todos
+```
+
+当前测试环境的 Windows 局域网入口为 `http://192.168.10.55:8080`。该地址属于环境配置,不应视为固定生产地址。
+
+## API
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| GET | `/health` | 健康检查 |
+| GET | `/todos` | 获取全部任务 |
+| POST | `/todos` | 新增任务 |
+| PATCH | `/todos/{todo_id}` | 修改任务文字或完成状态 |
+| DELETE | `/todos/{todo_id}` | 删除任务 |
+
+Nginx 对外使用 `/api` 前缀,例如浏览器访问 `/api/todos`,代理到 FastAPI 的 `/todos`。
+
+## 安全说明
+
+- 所有带用户数据的 SQL 均使用参数化查询,不使用 f-string 拼接。
+- `.env` 和 MySQL 数据卷不在交付包内。
+- 交付包中的 `.env.example` 只有占位符。
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..1dbf79a
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ mysql:
+ image: mysql:8.4
+ container_name: todolist_mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ MYSQL_DATABASE: ${MYSQL_DATABASE}
+ MYSQL_USER: ${MYSQL_USER}
+ MYSQL_PASSWORD: ${MYSQL_PASSWORD}
+ ports:
+ - "127.0.0.1:3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+
+volumes:
+ mysql_data:
diff --git a/deploy/nginx-todolist.conf b/deploy/nginx-todolist.conf
new file mode 100644
index 0000000..00cfe9a
--- /dev/null
+++ b/deploy/nginx-todolist.conf
@@ -0,0 +1,19 @@
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ server_name _;
+
+ root /var/www/todolist;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /api/ {
+ proxy_pass http://127.0.0.1:8000/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
diff --git a/deploy/todolist.service b/deploy/todolist.service
new file mode 100644
index 0000000..e254406
--- /dev/null
+++ b/deploy/todolist.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=TodoList FastAPI service
+After=network.target
+
+[Service]
+Type=simple
+User=dwh
+WorkingDirectory=/home/dwh/code/fastapi_todolist
+ExecStart=/home/dwh/code/fastapi_todolist/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
+Restart=always
+RestartSec=3
+Environment=PYTHONUNBUFFERED=1
+
+[Install]
+WantedBy=multi-user.target
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..f3a9d49
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,337 @@
+
+
+
+
+
+ 待办清单
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..dbcf629
--- /dev/null
+++ b/main.py
@@ -0,0 +1,127 @@
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+import os
+import mysql.connector
+from dotenv import load_dotenv
+
+load_dotenv()
+
+app = FastAPI()
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+def get_db_connection():
+ connection = mysql.connector.connect(
+ host = os.getenv("MYSQL_HOST"),
+ port = int(os.getenv("MYSQL_PORT")),
+ user = os.getenv("MYSQL_USER"),
+ password = os.getenv("MYSQL_PASSWORD"),
+ database = os.getenv("MYSQL_DATABASE"),
+ )
+ return connection
+
+
+class todocreate(BaseModel):
+ task: str
+
+class todoupdate(BaseModel):
+ completion: bool | None = None
+ task: str | None = None
+
+class todoresponse(BaseModel):
+ id: int
+ task: str
+ completion: bool
+@app.get("/health")
+def health():
+ return {"status":"ok"}
+
+@app.get("/todos", response_model=list[todoresponse])
+def todos_get():
+ connection = get_db_connection()
+ cursor = connection.cursor(dictionary = True)
+ try:
+ cursor.execute("SELECT id, task, completion FROM todos")
+ results = cursor.fetchall()
+ return results
+ finally:
+ cursor.close()
+ connection.close()
+
+@app.post("/todos", response_model=todoresponse)
+def todos_create(todo_data:todocreate):
+ connection = get_db_connection()
+ cursor = connection.cursor(dictionary = True)
+ try:
+ sql = "INSERT INTO todos (task) VALUES (%s)"
+ params = (todo_data.task,)
+ cursor.execute(sql,params)
+ connection.commit()
+ new_id = cursor.lastrowid
+ new_todos = {
+ "id": new_id,
+ "task": todo_data.task,
+ "completion": False
+ }
+ return new_todos
+ except mysql.connector.Error:
+ connection.rollback()
+ raise
+ finally:
+ cursor.close()
+ connection.close()
+@app.patch("/todos/{todo_id}", response_model=todoresponse)
+def todos_patch(todo_id:int, update_data:todoupdate):
+ connection = get_db_connection()
+ cursor = connection.cursor(dictionary = True)
+ try:
+ sql = """
+ UPDATE todos
+ SET task = COALESCE(%s,task),
+ completion = COALESCE(%s,completion)
+ WHERE id = %s
+ """
+ params = (update_data.task,update_data.completion,todo_id,)
+ cursor.execute(sql,params)
+ connection.commit()
+ select_params = (todo_id,)
+ cursor.execute("SELECT id, task, completion FROM todos WHERE id = %s",select_params)
+ result = cursor.fetchone()
+
+ if result is None:
+ raise HTTPException(status_code = 404, detail = "Todo not found")
+ return result
+ except mysql.connector.Error:
+ connection.rollback()
+ raise
+ finally:
+ cursor.close()
+ connection.close()
+
+@app.delete("/todos/{todo_id}", response_model=todoresponse)
+def todos_delete(todo_id:int):
+ connection = get_db_connection()
+ cursor = connection.cursor(dictionary = True)
+ try:
+ params = (todo_id,)
+ cursor.execute("SELECT id,task,completion FROM todos WHERE id = %s",params)
+ result = cursor.fetchone()
+ if result is None:
+ raise HTTPException(status_code = 404, detail = "Todo not found")
+ sql = "DELETE FROM todos WHERE id = %s"
+ cursor.execute(sql,params)
+ connection.commit()
+ return result
+ except mysql.connector.Error:
+ connection.rollback()
+ raise
+ finally:
+ cursor.close()
+ connection.close()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..0063e14
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,15 @@
+annotated-doc==0.0.4
+annotated-types==0.7.0
+anyio==4.14.1
+click==8.4.2
+fastapi==0.139.0
+h11==0.16.0
+idna==3.18
+mysql-connector-python==9.7.0
+pydantic==2.13.4
+pydantic_core==2.46.4
+python-dotenv==1.2.2
+starlette==1.3.1
+typing-inspection==0.4.2
+typing_extensions==4.16.0
+uvicorn==0.51.0
diff --git a/schema.sql b/schema.sql
new file mode 100644
index 0000000..58c64a0
--- /dev/null
+++ b/schema.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS todos (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ task VARCHAR(200) NOT NULL,
+ completion BOOLEAN NOT NULL DEFAULT FALSE,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/验收记录.md b/验收记录.md
new file mode 100644
index 0000000..6cee75f
--- /dev/null
+++ b/验收记录.md
@@ -0,0 +1,45 @@
+# TodoList 交付验收记录
+
+验收日期:2026-07-16
+
+## 已完成
+
+- FastAPI 已从 `todos.json` 迁移到 MySQL 8.4。
+- MySQL 通过 Docker Compose 运行,数据保存在命名卷 `mysql_data`。
+- Python 使用 `mysql-connector-python` 执行原始 SQL。
+- 数据库连接配置从 `.env` 读取,代码中未写死密码。
+- INSERT、UPDATE、DELETE 使用参数化 SQL,并包含提交、回滚和资源关闭逻辑。
+- GET、POST、PATCH、DELETE 已通过本机接口测试。
+- 不存在的 Todo ID 已验证返回 404。
+- 响应模型已将 `completion` 统一输出为 JSON 布尔值。
+- Nginx `/api` 反向代理已通过测试。
+- Windows 局域网地址已成功打开前端。
+- 页面新增、修改、完成状态、删除及刷新持久化链路已完成联调。
+
+## 当前运行链路
+
+```text
+Windows 192.168.10.55:8080
+ -> WSL Nginx :80
+ -> FastAPI 127.0.0.1:8000
+ -> MySQL 127.0.0.1:3306
+```
+
+## 未包含内容
+
+- 真实 `.env` 与数据库密码
+- Python `.venv`
+- MySQL Docker 数据卷
+- 旧 `todos.json` 数据及备份
+- 数据库连接测试脚本
+
+## 已知限制与后续建议
+
+- 当前没有用户登录和权限控制。
+- 当前没有自动化测试。
+- 当前没有 Alembic 等数据库版本迁移工具。
+- 当前每次请求建立一次数据库连接,后续可学习连接池。
+- `task` 的数据库长度上限为 200;建议后续在 Pydantic 请求模型中增加长度和空字符串校验。
+- CORS 当前为宽松配置;正式部署时应限制允许的来源。
+- systemd 文件包含当前用户与项目绝对路径,换机器部署时必须调整。
+- Windows 端口转发和防火墙规则属于当前机器的基础设施配置,未作为通用部署脚本放入本交付包。