diff --git a/.gitignore b/.gitignore index 9f3eedb..fe4b717 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,59 @@ -### Example user template template -### Example user template +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg -# IntelliJ project files -.idea -*.iml -out -gen +# 虚拟环境 +venv/ +env/ +ENV/ +.venv +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 数据库 +*.db +*.sqlite +*.sqlite3 + +# 环境变量 +.env +.env.local + +# 日志 +*.log + +# 上传文件 +uploads/ +static/uploads/ + +# 前端 +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ + +# 操作系统 +.DS_Store +Thumbs.db diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..7c60515 --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,190 @@ +# 项目结构说明 + +本文档详细说明了项目的目录结构和各文件的作用。 + +## 目录结构 + +``` +blogweb/ +├── backend/ # 后端代码目录 +│ ├── app/ # FastAPI 应用主目录 +│ │ ├── __init__.py +│ │ ├── main.py # FastAPI 应用入口 +│ │ │ +│ │ ├── api/ # API 路由模块 +│ │ │ ├── __init__.py +│ │ │ └── api_v1/ # API v1 版本 +│ │ │ ├── __init__.py +│ │ │ ├── api.py # API 路由聚合 +│ │ │ └── endpoints/ # API 端点 +│ │ │ ├── __init__.py +│ │ │ ├── auth.py # 认证相关端点 +│ │ │ ├── users.py # 用户相关端点 +│ │ │ ├── todos.py # 待办事项端点 +│ │ │ └── posts.py # 博客文章端点 +│ │ │ +│ │ ├── core/ # 核心配置模块 +│ │ │ ├── __init__.py +│ │ │ ├── config.py # 应用配置 +│ │ │ └── security.py # 安全相关(密码哈希、JWT) +│ │ │ +│ │ ├── db/ # 数据库相关 +│ │ │ ├── __init__.py +│ │ │ ├── base.py # 数据库基础配置 +│ │ │ ├── session.py # 数据库会话管理 +│ │ │ └── init_db.py # 数据库初始化脚本 ⭐ +│ │ │ +│ │ ├── models/ # 数据模型(SQLModel) +│ │ │ ├── __init__.py +│ │ │ ├── user.py # 用户模型 +│ │ │ ├── todo.py # 待办事项模型 +│ │ │ ├── post.py # 博客文章模型 +│ │ │ ├── transaction.py # 记账记录模型 +│ │ │ ├── media.py # 书影音收藏模型 +│ │ │ ├── tag.py # 标签模型 +│ │ │ ├── chat.py # 聊天消息模型 +│ │ │ └── upload.py # 文件上传模型 +│ │ │ +│ │ ├── schemas/ # Pydantic 模式(API 数据验证) +│ │ │ ├── __init__.py +│ │ │ ├── user.py # 用户相关模式 +│ │ │ ├── todo.py # 待办事项相关模式 +│ │ │ └── post.py # 博客文章相关模式 +│ │ │ +│ │ └── initial_data/ # 初始数据模块 +│ │ ├── __init__.py +│ │ └── README.md +│ │ +│ ├── database.py # (旧文件,可删除) +│ ├── models.py # (旧文件,可删除) +│ ├── init_db.py # (旧文件,可删除) +│ └── README.md # 后端说明文档 +│ +├── frontend/ # 前端代码目录 +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ │ ├── Home.vue # 首页 +│ │ │ ├── Login.vue # 登录页 +│ │ │ ├── Register.vue # 注册页 +│ │ │ ├── Todos.vue # 待办事项页 +│ │ │ └── Posts.vue # 博客文章页 +│ │ ├── stores/ # Pinia 状态管理 +│ │ │ └── auth.js # 认证状态 +│ │ ├── router/ # 路由配置 +│ │ │ └── index.js +│ │ ├── App.vue # 根组件 +│ │ └── main.js # 前端入口 +│ ├── index.html # HTML 模板 +│ ├── package.json # 前端依赖配置 +│ └── vite.config.js # Vite 配置 +│ +├── main.py # 项目启动入口 ⭐ +├── requirements.txt # Python 依赖 +├── .env.example # 环境变量示例 +├── .gitignore # Git 忽略规则 +├── README.md # 项目说明文档 +├── PROJECT_STRUCTURE.md # 本文件 +└── 数据库设计说明.md # 数据库设计文档 +``` + +## 关键文件说明 + +### ⭐ 重要文件 + +1. **`main.py`** - 项目启动入口 + - 用于开发环境快速启动 FastAPI 应用 + - 使用 uvicorn 运行 `app.main:app` + +2. **`backend/app/main.py`** - FastAPI 应用主文件 + - 创建 FastAPI 应用实例 + - 配置 CORS 中间件 + - 注册 API 路由 + +3. **`backend/app/db/init_db.py`** - 数据库初始化脚本 ⭐ + - **位置已调整**:从 `backend/init_db.py` 移动到 `backend/app/db/init_db.py` + - 运行方式: + ```bash + python -m app.db.init_db # 创建表 + python -m app.db.init_db --reset # 重置数据库 + ``` + +## 模块说明 + +### 后端模块 + +#### `app/core/` - 核心配置 +- `config.py`: 应用配置(数据库、安全、CORS 等) +- `security.py`: 密码哈希、JWT 令牌生成和验证 + +#### `app/db/` - 数据库 +- `base.py`: 数据库基础配置,导入所有模型 +- `session.py`: 数据库会话管理和依赖注入 +- `init_db.py`: 数据库初始化脚本 + +#### `app/models/` - 数据模型 +- 使用 SQLModel 定义数据库表结构 +- 每个模型一个文件,便于维护 + +#### `app/schemas/` - Pydantic 模式 +- 用于 API 请求和响应的数据验证 +- 定义创建、更新、响应等不同场景的模式 + +#### `app/api/` - API 路由 +- `api_v1/api.py`: 聚合所有 API 路由 +- `api_v1/endpoints/`: 各个功能模块的端点 + +### 前端模块 + +#### `src/views/` - 页面组件 +- Vue 3 单文件组件 +- 每个功能对应一个页面 + +#### `src/stores/` - 状态管理 +- 使用 Pinia 管理全局状态 +- `auth.js`: 用户认证状态 + +#### `src/router/` - 路由 +- Vue Router 配置 +- 定义前端路由规则 + +## 开发流程 + +1. **初始化数据库** + ```bash + python -m app.db.init_db + ``` + +2. **启动后端** + ```bash + python main.py + # 或 + uvicorn app.main:app --reload + ``` + +3. **启动前端** + ```bash + cd frontend + npm install + npm run dev + ``` + +## 注意事项 + +1. **数据库初始化脚本位置已调整** + - 旧位置:`backend/init_db.py` + - 新位置:`backend/app/db/init_db.py` + - 运行方式:`python -m app.db.init_db` + +2. **旧文件清理** + - `backend/database.py`、`backend/models.py`、`backend/init_db.py` 可以删除 + - 新代码已迁移到 `backend/app/` 目录下 + +3. **环境变量** + - 复制 `.env.example` 为 `.env` + - 修改其中的配置项(特别是 `SECRET_KEY`) + +4. **前后端分离** + - 后端运行在 `http://localhost:8000` + - 前端运行在 `http://localhost:5173` + - 前端通过代理访问后端 API + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c47d725 --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# 个人博客网站 + +基于 FastAPI + Vue 3 开发的全栈个人博客网站项目。 + +## 项目结构 + +``` +blogweb/ +├── backend/ # 后端代码 +│ ├── app/ +│ │ ├── api/ # API 路由 +│ │ │ └── api_v1/ +│ │ │ ├── endpoints/ # API 端点 +│ │ │ └── api.py +│ │ ├── core/ # 核心配置 +│ │ │ ├── config.py +│ │ │ └── security.py +│ │ ├── db/ # 数据库相关 +│ │ │ ├── base.py +│ │ │ ├── session.py +│ │ │ └── init_db.py +│ │ ├── models/ # 数据模型 +│ │ ├── schemas/ # Pydantic 模式 +│ │ └── main.py # FastAPI 应用入口 +│ ├── database.py # (旧文件,可删除) +│ ├── models.py # (旧文件,可删除) +│ └── init_db.py # (旧文件,可删除) +├── frontend/ # 前端代码 +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ ├── stores/ # Pinia 状态管理 +│ │ ├── router/ # 路由配置 +│ │ ├── App.vue +│ │ └── main.js +│ ├── package.json +│ └── vite.config.js +├── main.py # 项目启动入口 +├── requirements.txt # Python 依赖 +└── README.md +``` + +## 功能特性 + +### 已完成 +- ✅ 用户注册和登录(JWT 认证) +- ✅ 待办事项管理(CRUD) +- ✅ 博客文章发布(CRUD) + +### 开发中 +- 🚧 天气查询 +- 🚧 个人记账本 +- 🚧 书影音收藏 +- 🚧 实时聊天室(WebSocket) +- 🚧 文件上传 + +## 快速开始 + +### 1. 安装后端依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 初始化数据库 + +```bash +# 从项目根目录运行 +python -m app.db.init_db +``` + +或者: + +```bash +cd backend/app/db +python init_db.py +``` + +如果需要重置数据库(⚠️ 会删除所有数据): + +```bash +python -m app.db.init_db --reset +``` + +### 3. 配置环境变量 + +复制 `.env.example` 为 `.env` 并修改配置: + +```bash +cp .env.example .env +``` + +### 4. 启动后端服务 + +```bash +# 方式1:使用 uvicorn 直接运行 +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# 方式2:使用项目入口文件 +python main.py +``` + +后端服务将在 `http://localhost:8000` 启动。 + +API 文档:`http://localhost:8000/docs` + +### 5. 安装前端依赖 + +```bash +cd frontend +npm install +``` + +### 6. 启动前端服务 + +```bash +npm run dev +``` + +前端服务将在 `http://localhost:5173` 启动。 + +## 开发路线图 + +本项目按照以下阶段逐步开发: + +1. **阶段 1**:静态页面 + TodoList(无用户)✅ +2. **阶段 2**:博客系统(公开)✅ +3. **阶段 3**:用户系统 + 权限隔离 ✅ +4. **阶段 4**:天气查询 + 个人记账本 🚧 +5. **阶段 5**:书影音收藏站 🚧 +6. **阶段 6**:聊天室(WebSocket) + 文件上传 🚧 + +## 技术栈 + +### 后端 +- FastAPI - 现代、快速的 Web 框架 +- SQLModel - 基于 SQLAlchemy 和 Pydantic 的 ORM +- SQLite - 开发环境数据库 +- JWT - 用户认证 +- Pydantic - 数据验证 + +### 前端 +- Vue 3 - 渐进式 JavaScript 框架 +- Vue Router - 路由管理 +- Pinia - 状态管理 +- Axios - HTTP 客户端 +- Vite - 构建工具 + +## API 端点 + +### 认证 +- `POST /api/v1/auth/register` - 用户注册 +- `POST /api/v1/auth/login` - 用户登录 + +### 用户 +- `GET /api/v1/users/me` - 获取当前用户信息 +- `GET /api/v1/users/` - 获取用户列表 + +### 待办事项 +- `GET /api/v1/todos/` - 获取待办事项列表 +- `POST /api/v1/todos/` - 创建待办事项 +- `GET /api/v1/todos/{id}` - 获取单个待办事项 +- `PUT /api/v1/todos/{id}` - 更新待办事项 +- `DELETE /api/v1/todos/{id}` - 删除待办事项 + +### 博客文章 +- `GET /api/v1/posts/` - 获取文章列表 +- `POST /api/v1/posts/` - 创建文章 +- `GET /api/v1/posts/{id}` - 获取单个文章 +- `PUT /api/v1/posts/{id}` - 更新文章 +- `DELETE /api/v1/posts/{id}` - 删除文章 + +## 数据库设计 + +详见 `数据库设计说明.md` + +## 许可证 + +MIT + diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..6cb1a0b --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,183 @@ +# 项目设置指南 + +## 快速开始 + +### 1. 安装后端依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 初始化数据库 + +```bash +# 从项目根目录运行 +python -m app.db.init_db +``` + +如果需要重置数据库(⚠️ 会删除所有数据): + +```bash +python -m app.db.init_db --reset +``` + +### 3. 配置环境变量 + +复制 `.env.example` 为 `.env`: + +```bash +# Windows +copy .env.example .env + +# Linux/Mac +cp .env.example .env +``` + +编辑 `.env` 文件,修改以下配置: + +```env +# 必须修改(生产环境) +SECRET_KEY=your-secret-key-change-in-production + +# 可选:如果需要天气功能 +OPENWEATHER_API_KEY=your-api-key +``` + +### 4. 启动后端服务 + +```bash +# 方式1:使用项目入口文件 +python main.py + +# 方式2:使用 uvicorn 直接运行 +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +后端服务将在 `http://localhost:8000` 启动。 + +访问 API 文档:`http://localhost:8000/docs` + +### 5. 安装前端依赖 + +```bash +cd frontend +npm install +``` + +### 6. 启动前端服务 + +```bash +npm run dev +``` + +前端服务将在 `http://localhost:5173` 启动。 + +## 项目结构说明 + +详细的项目结构说明请查看 `PROJECT_STRUCTURE.md`。 + +## 重要变更 + +### 数据库初始化脚本位置调整 + +**旧位置**:`backend/init_db.py` +**新位置**:`backend/app/db/init_db.py` + +**新的运行方式**: +```bash +python -m app.db.init_db # 创建表 +python -m app.db.init_db --reset # 重置数据库 +``` + +### 旧文件清理 + +以下文件可以删除(代码已迁移到新位置): +- `backend/database.py` → 已迁移到 `backend/app/db/session.py` +- `backend/models.py` → 已迁移到 `backend/app/models/` +- `backend/init_db.py` → 已迁移到 `backend/app/db/init_db.py` + +## 开发路线图 + +### ✅ 已完成 + +1. **阶段 1**:静态页面 + TodoList(无用户) +2. **阶段 2**:博客系统(公开) +3. **阶段 3**:用户系统 + 权限隔离 + +### 🚧 开发中 + +4. **阶段 4**:天气查询 + 个人记账本 +5. **阶段 5**:书影音收藏站 +6. **阶段 6**:聊天室(WebSocket) + 文件上传 + +## 测试 API + +### 1. 用户注册 + +```bash +curl -X POST "http://localhost:8000/api/v1/auth/register" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "email": "test@example.com", + "password": "testpass123" + }' +``` + +### 2. 用户登录 + +```bash +curl -X POST "http://localhost:8000/api/v1/auth/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=testuser&password=testpass123" +``` + +返回的 `access_token` 用于后续 API 请求。 + +### 3. 获取当前用户信息 + +```bash +curl -X GET "http://localhost:8000/api/v1/users/me" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +### 4. 创建待办事项 + +```bash +curl -X POST "http://localhost:8000/api/v1/todos/" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "完成项目文档" + }' +``` + +## 常见问题 + +### Q: 运行 `python -m app.db.init_db` 报错 "No module named 'app'" + +**A**: 确保从项目根目录运行,并且 `backend` 目录在 Python 路径中。或者使用: + +```bash +cd backend +python -m app.db.init_db +``` + +### Q: 前端无法连接到后端 API + +**A**: 检查: +1. 后端服务是否在 `http://localhost:8000` 运行 +2. `frontend/vite.config.js` 中的代理配置是否正确 +3. 浏览器控制台是否有 CORS 错误 + +### Q: 数据库文件在哪里? + +**A**: 数据库文件 `blogweb.db` 在项目根目录下。 + +## 下一步 + +1. 完善前端 UI 设计 +2. 实现剩余功能模块(天气、记账、收藏、聊天、文件上传) +3. 添加单元测试 +4. 配置生产环境部署 + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..e925f0e --- /dev/null +++ b/backend/README.md @@ -0,0 +1,98 @@ +# 数据库模块说明 + +本目录包含数据库相关的所有代码。 + +## 文件说明 + +- `app/models/` - 数据库模型定义(9个表) +- `app/db/session.py` - 数据库连接和配置 +- `app/db/init_db.py` - 数据库初始化脚本 + +## 安装依赖 + +```bash +pip install -r ../requirements.txt +``` + +## 初始化数据库 + +### 首次创建数据库 + +运行以下命令创建数据库表: + +```bash +cd backend +python -m app.db.init_db +``` + +或者从项目根目录运行: + +```bash +python -m backend.app.db.init_db +``` + +执行后会在项目根目录生成 `blogweb.db` SQLite 数据库文件。 + +### 模型更新后更新数据库 + +⚠️ **重要提示**:`create_all()` 只会创建**不存在的表**,**不会修改已存在表的结构**。 + +如果 models 有更新(添加字段、修改字段类型等),有两种方式: + +#### 方式1:重置数据库(开发环境推荐) + +⚠️ **会删除所有数据**,适合开发环境: + +```bash +python -m app.db.init_db --reset +``` + +#### 方式2:使用数据库迁移工具(生产环境推荐) + +对于生产环境,建议使用 **Alembic** 进行数据库迁移: + +```bash +# 安装 Alembic +pip install alembic + +# 初始化迁移环境 +alembic init alembic + +# 生成迁移脚本 +alembic revision --autogenerate -m "描述变更" + +# 执行迁移 +alembic upgrade head +``` + +## 数据库表结构 + +根据 `数据库设计说明.md` 创建了以下9个表: + +1. **users** - 用户账户信息 +2. **todos** - 待办事项列表 +3. **posts** - 博客文章 +4. **transactions** - 个人记账记录 +5. **media** - 书影音收藏条目 +6. **tags** - 媒体标签 +7. **media_tags** - 媒体与标签的多对多关联表 +8. **chat_messages** - 聊天室消息记录 +9. **uploads** - 用户上传的文件元数据 + +## 使用示例 + +在 FastAPI 应用中使用数据库: + +```python +from app.db.session import get_db +from app.models import User, Todo +from sqlmodel import Session, select + +# 在路由中使用 +@app.get("/users") +def get_users(session: Session = Depends(get_db)): + statement = select(User) + users = session.exec(statement).all() + return users +``` + diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..92fb946 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1,30 @@ +""" +Backend 模块 +""" +from .app.db.session import engine, get_db +from .app.db.init_db import init_db, reset_db +from .app.models import ( + User, Todo, Post, Transaction, Media, Tag, MediaTag, + ChatMessage, Upload +) + +# 为了向后兼容,保留 get_session 别名 +get_session = get_db + +__all__ = [ + "engine", + "init_db", + "reset_db", + "get_db", + "get_session", + "User", + "Todo", + "Post", + "Transaction", + "Media", + "Tag", + "MediaTag", + "ChatMessage", + "Upload", +] + diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..21702a8 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,4 @@ +""" +FastAPI 应用包 +""" + diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..bcfebce --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,4 @@ +""" +API 路由模块 +""" + diff --git a/backend/app/api/api_v1/__init__.py b/backend/app/api/api_v1/__init__.py new file mode 100644 index 0000000..4ed9da1 --- /dev/null +++ b/backend/app/api/api_v1/__init__.py @@ -0,0 +1,4 @@ +""" +API v1 路由 +""" + diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py new file mode 100644 index 0000000..a47e369 --- /dev/null +++ b/backend/app/api/api_v1/api.py @@ -0,0 +1,14 @@ +""" +API v1 路由聚合 +""" +from fastapi import APIRouter +from app.api.api_v1.endpoints import auth, todos, posts, users + +api_router = APIRouter() + +# 注册各个功能模块的路由 +api_router.include_router(auth.router, prefix="/auth", tags=["认证"]) +api_router.include_router(users.router, prefix="/users", tags=["用户"]) +api_router.include_router(todos.router, prefix="/todos", tags=["待办事项"]) +api_router.include_router(posts.router, prefix="/posts", tags=["博客"]) + diff --git a/backend/app/api/api_v1/endpoints/__init__.py b/backend/app/api/api_v1/endpoints/__init__.py new file mode 100644 index 0000000..ab2f8d8 --- /dev/null +++ b/backend/app/api/api_v1/endpoints/__init__.py @@ -0,0 +1,4 @@ +""" +API 端点模块 +""" + diff --git a/backend/app/api/api_v1/endpoints/posts.py b/backend/app/api/api_v1/endpoints/posts.py new file mode 100644 index 0000000..9104c0f --- /dev/null +++ b/backend/app/api/api_v1/endpoints/posts.py @@ -0,0 +1,142 @@ +""" +博客文章相关 API 端点 +""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel import Session, select +from typing import List + +from app.db.session import get_db +from app.models.post import Post +from app.models.user import User +from app.schemas.post import Post as PostSchema, PostCreate, PostUpdate +from app.api.api_v1.endpoints.users import get_current_user + +router = APIRouter() + + +@router.post("/", response_model=PostSchema, status_code=status.HTTP_201_CREATED) +def create_post( + post_in: PostCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """创建博客文章""" + # 检查 slug 是否已存在(同一用户) + statement = select(Post).where( + Post.slug == post_in.slug, + Post.user_id == current_user.id + ) + existing_post = db.exec(statement).first() + if existing_post: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="该 slug 已存在" + ) + + db_post = Post( + title=post_in.title, + slug=post_in.slug, + content=post_in.content, + user_id=current_user.id + ) + db.add(db_post) + db.commit() + db.refresh(db_post) + return db_post + + +@router.get("/", response_model=List[PostSchema]) +def read_posts( + skip: int = 0, + limit: int = 100, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取当前用户的博客文章列表""" + statement = ( + select(Post) + .where(Post.user_id == current_user.id) + .offset(skip) + .limit(limit) + ) + posts = db.exec(statement).all() + return posts + + +@router.get("/{post_id}", response_model=PostSchema) +def read_post( + post_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取单个博客文章""" + statement = select(Post).where( + Post.id == post_id, + Post.user_id == current_user.id + ) + post = db.exec(statement).first() + if post is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文章不存在" + ) + return post + + +@router.put("/{post_id}", response_model=PostSchema) +def update_post( + post_id: int, + post_in: PostUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """更新博客文章""" + statement = select(Post).where( + Post.id == post_id, + Post.user_id == current_user.id + ) + post = db.exec(statement).first() + if post is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文章不存在" + ) + + if post_in.title is not None: + post.title = post_in.title + if post_in.slug is not None: + post.slug = post_in.slug + if post_in.content is not None: + post.content = post_in.content + + from datetime import datetime + post.updated_at = datetime.now() + + db.add(post) + db.commit() + db.refresh(post) + return post + + +@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_post( + post_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """删除博客文章""" + statement = select(Post).where( + Post.id == post_id, + Post.user_id == current_user.id + ) + post = db.exec(statement).first() + if post is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文章不存在" + ) + + db.delete(post) + db.commit() + return None + diff --git a/backend/app/api/api_v1/endpoints/todos.py b/backend/app/api/api_v1/endpoints/todos.py new file mode 100644 index 0000000..c09ee4b --- /dev/null +++ b/backend/app/api/api_v1/endpoints/todos.py @@ -0,0 +1,123 @@ +""" +待办事项相关 API 端点 +""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel import Session, select +from typing import List + +from app.db.session import get_db +from app.models.todo import Todo +from app.models.user import User +from app.schemas.todo import Todo as TodoSchema, TodoCreate, TodoUpdate +from app.api.api_v1.endpoints.users import get_current_user + +router = APIRouter() + + +@router.post("/", response_model=TodoSchema, status_code=status.HTTP_201_CREATED) +def create_todo( + todo_in: TodoCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """创建待办事项""" + db_todo = Todo( + title=todo_in.title, + user_id=current_user.id + ) + db.add(db_todo) + db.commit() + db.refresh(db_todo) + return db_todo + + +@router.get("/", response_model=List[TodoSchema]) +def read_todos( + skip: int = 0, + limit: int = 100, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取当前用户的待办事项列表""" + statement = ( + select(Todo) + .where(Todo.user_id == current_user.id) + .offset(skip) + .limit(limit) + ) + todos = db.exec(statement).all() + return todos + + +@router.get("/{todo_id}", response_model=TodoSchema) +def read_todo( + todo_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取单个待办事项""" + statement = select(Todo).where( + Todo.id == todo_id, + Todo.user_id == current_user.id + ) + todo = db.exec(statement).first() + if todo is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + return todo + + +@router.put("/{todo_id}", response_model=TodoSchema) +def update_todo( + todo_id: int, + todo_in: TodoUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """更新待办事项""" + statement = select(Todo).where( + Todo.id == todo_id, + Todo.user_id == current_user.id + ) + todo = db.exec(statement).first() + if todo is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + + if todo_in.title is not None: + todo.title = todo_in.title + if todo_in.done is not None: + todo.done = todo_in.done + + db.add(todo) + db.commit() + db.refresh(todo) + return todo + + +@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_todo( + todo_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """删除待办事项""" + statement = select(Todo).where( + Todo.id == todo_id, + Todo.user_id == current_user.id + ) + todo = db.exec(statement).first() + if todo is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="待办事项不存在" + ) + + db.delete(todo) + db.commit() + return None + diff --git a/backend/app/api/api_v1/endpoints/users.py b/backend/app/api/api_v1/endpoints/users.py new file mode 100644 index 0000000..448eb91 --- /dev/null +++ b/backend/app/api/api_v1/endpoints/users.py @@ -0,0 +1,63 @@ +""" +用户相关 API 端点 +""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel import Session, select +from typing import List + +from app.db.session import get_db +from app.models.user import User +from app.schemas.user import User as UserSchema +from app.core.security import decode_access_token +from fastapi.security import OAuth2PasswordBearer + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: Session = Depends(get_db) +) -> User: + """获取当前登录用户""" + payload = decode_access_token(token) + if payload is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的认证凭据", + headers={"WWW-Authenticate": "Bearer"}, + ) + + username: str = payload.get("sub") + if username is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的认证凭据", + headers={"WWW-Authenticate": "Bearer"}, + ) + + statement = select(User).where(User.username == username) + user = db.exec(statement).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在", + headers={"WWW-Authenticate": "Bearer"}, + ) + + return user + + +@router.get("/me", response_model=UserSchema) +async def read_users_me(current_user: User = Depends(get_current_user)): + """获取当前用户信息""" + return current_user + + +@router.get("/", response_model=List[UserSchema]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + """获取用户列表(示例端点)""" + statement = select(User).offset(skip).limit(limit) + users = db.exec(statement).all() + return users + diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..221e79c --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1,13 @@ +""" +核心配置模块 +""" +from .config import settings +from .security import get_password_hash, verify_password, create_access_token + +__all__ = [ + "settings", + "get_password_hash", + "verify_password", + "create_access_token", +] + diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..c20e77d --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,44 @@ +""" +安全相关功能:密码哈希、JWT 令牌等 +""" +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from .config import settings + +# 密码加密上下文 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def get_password_hash(password: str) -> str: + """生成密码哈希""" + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """验证密码""" + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """创建 JWT 访问令牌""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt + + +def decode_access_token(token: str) -> Optional[dict]: + """解码 JWT 令牌""" + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + return payload + except JWTError: + return None + diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..5733b0a --- /dev/null +++ b/backend/app/db/__init__.py @@ -0,0 +1,16 @@ +""" +数据库模块 +""" +from .base import Base +from .session import engine, SessionLocal, get_db +from .init_db import init_db, reset_db + +__all__ = [ + "Base", + "engine", + "SessionLocal", + "get_db", + "init_db", + "reset_db", +] + diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py new file mode 100644 index 0000000..6fe19a5 --- /dev/null +++ b/backend/app/db/init_db.py @@ -0,0 +1,71 @@ +""" +数据库初始化脚本 +运行此脚本创建数据库表结构 + +用法: + python -m app.db.init_db # 创建表(如果表已存在则跳过) + python -m app.db.init_db --reset # 删除所有表后重新创建(⚠️ 会丢失数据) +""" +import sys +import argparse +from sqlmodel import SQLModel + +from app.db.session import engine +from app.db.base import Base # 这会导入所有模型 + +# 导入所有模型以确保表被注册 +from app.models import ( + User, Todo, Post, Transaction, Media, Tag, MediaTag, + ChatMessage, Upload +) + + +def init_db() -> None: + """初始化数据库,创建所有表""" + SQLModel.metadata.create_all(engine) + print("✅ 数据库表创建完成") + + +def reset_db() -> None: + """重置数据库:删除所有表后重新创建(⚠️ 会丢失所有数据)""" + print("⚠️ 警告:将删除所有表和数据!") + SQLModel.metadata.drop_all(engine) + print("✅ 已删除所有表") + SQLModel.metadata.create_all(engine) + print("✅ 已重新创建所有表") + + +def main() -> None: + """初始化数据库""" + parser = argparse.ArgumentParser(description="初始化数据库") + parser.add_argument( + "--reset", + action="store_true", + help="删除所有表后重新创建(⚠️ 会丢失所有数据)" + ) + args = parser.parse_args() + + if args.reset: + print("🔄 重置模式:将删除所有表后重新创建...") + reset_db() + else: + print("📦 创建模式:创建不存在的表...") + init_db() + + print(f"\n✅ 数据库表操作完成!") + print(f"数据库文件位置: {engine.url}") + + # 显示创建的表 + print("\n数据库中的表:") + tables = [ + "users", "todos", "posts", "transactions", + "media", "tags", "media_tags", + "chat_messages", "uploads" + ] + for table in tables: + print(f" - {table}") + + +if __name__ == "__main__": + main() + diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..d7221d5 --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,26 @@ +""" +数据库会话管理 +""" +from sqlalchemy.engine import Engine +from sqlalchemy import create_engine +from sqlmodel import Session +from typing import Generator +from app.core.config import settings + +# 创建数据库引擎 +engine: Engine = create_engine( + settings.DATABASE_URL, + connect_args={"check_same_thread": False}, # SQLite 需要此参数 + echo=True # 开发环境显示SQL语句,生产环境设为False +) + + +def get_db() -> Generator[Session, None, None]: + """获取数据库会话(用于依赖注入)""" + with Session(engine) as session: + yield session + + +# 为了向后兼容,保留 SessionLocal 别名 +SessionLocal = Session + diff --git a/backend/app/initial_data/README.md b/backend/app/initial_data/README.md new file mode 100644 index 0000000..d83eb22 --- /dev/null +++ b/backend/app/initial_data/README.md @@ -0,0 +1,21 @@ +# 初始数据模块 + +此目录用于存放数据库初始化时的示例数据脚本。 + +## 使用说明 + +在数据库初始化后,可以运行此模块中的脚本来插入示例数据,方便开发和测试。 + +## 示例 + +```python +from app.db.session import SessionLocal +from app.models.user import User +from app.core.security import get_password_hash + +def init_data(): + db = SessionLocal() + # 创建示例用户等 + ... +``` + diff --git a/backend/app/initial_data/__init__.py b/backend/app/initial_data/__init__.py new file mode 100644 index 0000000..49e7982 --- /dev/null +++ b/backend/app/initial_data/__init__.py @@ -0,0 +1,5 @@ +""" +初始数据模块 +用于数据库初始化时插入示例数据 +""" + diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7d65256 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,45 @@ +""" +FastAPI 应用主入口 +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.core.config import settings +from app.api.api_v1.api import api_router + +# 创建 FastAPI 应用实例 +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + openapi_url=f"{settings.API_V1_STR}/openapi.json", +) + +# 配置 CORS +if settings.BACKEND_CORS_ORIGINS: + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + +# 注册 API 路由 +app.include_router(api_router, prefix=settings.API_V1_STR) + + +@app.get("/") +def root(): + """根路径""" + return { + "message": "欢迎使用个人博客网站 API", + "version": settings.VERSION, + "docs": "/docs", + "api": settings.API_V1_STR + } + + +@app.get("/health") +def health_check(): + """健康检查""" + return {"status": "ok"} + diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..cad8a64 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,23 @@ +""" +数据库模型模块 +""" +from .user import User +from .todo import Todo +from .post import Post +from .transaction import Transaction +from .media import Media, Tag, MediaTag +from .chat import ChatMessage +from .upload import Upload + +__all__ = [ + "User", + "Todo", + "Post", + "Transaction", + "Media", + "Tag", + "MediaTag", + "ChatMessage", + "Upload", +] + diff --git a/backend/app/models/media.py b/backend/app/models/media.py new file mode 100644 index 0000000..4400754 --- /dev/null +++ b/backend/app/models/media.py @@ -0,0 +1,40 @@ +""" +书影音收藏模型 +""" +from datetime import datetime +from typing import Optional, TYPE_CHECKING +from sqlmodel import SQLModel, Field, Relationship +from .user import User + +if TYPE_CHECKING: + from .tag import Tag +else: + Tag = "Tag" + + +class MediaTag(SQLModel, table=True): + """媒体-标签关联表""" + __tablename__ = "media_tags" + + media_id: int = Field(foreign_key="media.id", primary_key=True) + tag_id: int = Field(foreign_key="tags.id", primary_key=True) + + +class Media(SQLModel, table=True): + """书影音收藏表""" + __tablename__ = "media" + + id: Optional[int] = Field(default=None, primary_key=True) + title: str = Field(max_length=200) + media_type: str = Field(max_length=20) # book / movie / music + rating: Optional[float] = Field(default=None, ge=0.0, le=5.0) + comment: Optional[str] = Field(default=None) + external_id: Optional[str] = Field(default=None, max_length=100) # ISBN、IMDb ID等 + cover_url: Optional[str] = Field(default=None, max_length=300) + created_at: datetime = Field(default_factory=datetime.now) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + tags: list["Tag"] = Relationship(back_populates="media", link_model="MediaTag") + diff --git a/backend/app/models/post.py b/backend/app/models/post.py new file mode 100644 index 0000000..d3c81f6 --- /dev/null +++ b/backend/app/models/post.py @@ -0,0 +1,24 @@ +""" +博客文章模型 +""" +from datetime import datetime +from typing import Optional +from sqlmodel import SQLModel, Field, Relationship +from .user import User + + +class Post(SQLModel, table=True): + """博客文章表""" + __tablename__ = "posts" + + id: Optional[int] = Field(default=None, primary_key=True) + title: str = Field(max_length=200) + slug: str = Field(max_length=200, index=True) + content: str # 支持 Markdown + created_at: datetime = Field(default_factory=datetime.now) + updated_at: Optional[datetime] = Field(default=None) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + diff --git a/backend/app/models/tag.py b/backend/app/models/tag.py new file mode 100644 index 0000000..31903ac --- /dev/null +++ b/backend/app/models/tag.py @@ -0,0 +1,23 @@ +""" +媒体标签模型 +""" +from typing import Optional, TYPE_CHECKING +from sqlmodel import SQLModel, Field, Relationship + +if TYPE_CHECKING: + from .media import Media, MediaTag +else: + Media = "Media" + MediaTag = "MediaTag" + + +class Tag(SQLModel, table=True): + """媒体标签表""" + __tablename__ = "tags" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(max_length=50, unique=True, index=True) + + # 关系 + media: list["Media"] = Relationship(back_populates="tags", link_model="MediaTag") + diff --git a/backend/app/models/todo.py b/backend/app/models/todo.py new file mode 100644 index 0000000..ec8ceea --- /dev/null +++ b/backend/app/models/todo.py @@ -0,0 +1,22 @@ +""" +待办事项模型 +""" +from datetime import datetime +from typing import Optional +from sqlmodel import SQLModel, Field, Relationship +from .user import User + + +class Todo(SQLModel, table=True): + """待办事项表""" + __tablename__ = "todos" + + id: Optional[int] = Field(default=None, primary_key=True) + title: str = Field(max_length=200) + done: bool = Field(default=False) + created_at: datetime = Field(default_factory=datetime.now) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系(可选,用于ORM查询) + user: Optional[User] = Relationship() + diff --git a/backend/app/models/transaction.py b/backend/app/models/transaction.py new file mode 100644 index 0000000..2409b48 --- /dev/null +++ b/backend/app/models/transaction.py @@ -0,0 +1,25 @@ +""" +记账记录模型 +""" +from datetime import date +from typing import Optional +from decimal import Decimal +from sqlmodel import SQLModel, Field, Relationship, Column +from sqlalchemy import Numeric +from .user import User + + +class Transaction(SQLModel, table=True): + """记账记录表""" + __tablename__ = "transactions" + + id: Optional[int] = Field(default=None, primary_key=True) + amount: Decimal = Field(sa_column=Column(Numeric(10, 2))) # 正数为收入,负数为支出 + category: str = Field(max_length=50) + description: Optional[str] = Field(default=None, max_length=200) + date: date + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + diff --git a/backend/app/models/upload.py b/backend/app/models/upload.py new file mode 100644 index 0000000..3c80266 --- /dev/null +++ b/backend/app/models/upload.py @@ -0,0 +1,25 @@ +""" +文件上传模型 +""" +from datetime import datetime +from typing import Optional +from sqlmodel import SQLModel, Field, Relationship +from .user import User + + +class Upload(SQLModel, table=True): + """文件上传记录表""" + __tablename__ = "uploads" + + id: Optional[int] = Field(default=None, primary_key=True) + filename: str = Field(max_length=200) + stored_path: str = Field(max_length=300) + file_size: int # 字节数 + mime_type: str = Field(max_length=100) + uploaded_at: datetime = Field(default_factory=datetime.now) + expires_at: Optional[datetime] = Field(default=None) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..a096bb0 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,18 @@ +""" +用户模型 +""" +from datetime import datetime +from typing import Optional +from sqlmodel import SQLModel, Field + + +class User(SQLModel, table=True): + """用户表""" + __tablename__ = "users" + + id: Optional[int] = Field(default=None, primary_key=True) + username: str = Field(max_length=50, unique=True, index=True) + email: Optional[str] = Field(default=None, max_length=100, unique=True, index=True) + hashed_password: str + created_at: datetime = Field(default_factory=datetime.now) + diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..4840629 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1,5 @@ +""" +Pydantic 模式定义 +用于 API 请求和响应的数据验证 +""" + diff --git a/backend/app/schemas/post.py b/backend/app/schemas/post.py new file mode 100644 index 0000000..e2a322c --- /dev/null +++ b/backend/app/schemas/post.py @@ -0,0 +1,42 @@ +""" +博客文章相关的 Pydantic 模式 +""" +from datetime import datetime +from typing import Optional +from pydantic import BaseModel + + +class PostBase(BaseModel): + """博客文章基础模式""" + title: str + slug: str + content: str + + +class PostCreate(PostBase): + """创建博客文章请求模式""" + pass + + +class PostUpdate(BaseModel): + """更新博客文章请求模式""" + title: Optional[str] = None + slug: Optional[str] = None + content: Optional[str] = None + + +class PostInDB(PostBase): + """数据库中的博客文章模式""" + id: int + created_at: datetime + updated_at: Optional[datetime] = None + user_id: int + + class Config: + from_attributes = True + + +class Post(PostInDB): + """博客文章响应模式""" + pass + diff --git a/backend/app/schemas/todo.py b/backend/app/schemas/todo.py new file mode 100644 index 0000000..4ad83f5 --- /dev/null +++ b/backend/app/schemas/todo.py @@ -0,0 +1,39 @@ +""" +待办事项相关的 Pydantic 模式 +""" +from datetime import datetime +from typing import Optional +from pydantic import BaseModel + + +class TodoBase(BaseModel): + """待办事项基础模式""" + title: str + + +class TodoCreate(TodoBase): + """创建待办事项请求模式""" + pass + + +class TodoUpdate(BaseModel): + """更新待办事项请求模式""" + title: Optional[str] = None + done: Optional[bool] = None + + +class TodoInDB(TodoBase): + """数据库中的待办事项模式""" + id: int + done: bool + created_at: datetime + user_id: int + + class Config: + from_attributes = True + + +class Todo(TodoInDB): + """待办事项响应模式""" + pass + diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..122404a --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,50 @@ +""" +用户相关的 Pydantic 模式 +""" +from datetime import datetime +from typing import Optional +from pydantic import BaseModel, EmailStr + + +class UserBase(BaseModel): + """用户基础模式""" + username: str + email: Optional[EmailStr] = None + + +class UserCreate(UserBase): + """创建用户请求模式""" + password: str + + +class UserUpdate(BaseModel): + """更新用户请求模式""" + username: Optional[str] = None + email: Optional[EmailStr] = None + password: Optional[str] = None + + +class UserInDB(UserBase): + """数据库中的用户模式""" + id: int + created_at: datetime + + class Config: + from_attributes = True + + +class User(UserInDB): + """用户响应模式(不包含敏感信息)""" + pass + + +class Token(BaseModel): + """JWT 令牌响应模式""" + access_token: str + token_type: str = "bearer" + + +class TokenData(BaseModel): + """令牌数据模式""" + username: Optional[str] = None + diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..af4dfbc --- /dev/null +++ b/backend/models.py @@ -0,0 +1,144 @@ +""" +数据库模型定义 +使用 SQLModel 定义所有表结构 +""" +from datetime import datetime, date +from typing import Optional +from sqlmodel import SQLModel, Field, Relationship, Column +from sqlalchemy import Numeric +from decimal import Decimal + + +# ==================== 用户表 ==================== +class User(SQLModel, table=True): + """用户表""" + __tablename__ = "users" + + id: Optional[int] = Field(default=None, primary_key=True) + username: str = Field(max_length=50, unique=True, index=True) + email: Optional[str] = Field(default=None, max_length=100, unique=True, index=True) + hashed_password: str + created_at: datetime = Field(default_factory=datetime.now) + + +# ==================== 待办事项表 ==================== +class Todo(SQLModel, table=True): + """待办事项表""" + __tablename__ = "todos" + + id: Optional[int] = Field(default=None, primary_key=True) + title: str = Field(max_length=200) + done: bool = Field(default=False) + created_at: datetime = Field(default_factory=datetime.now) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系(可选,用于ORM查询) + user: Optional[User] = Relationship() + + +# ==================== 博客文章表 ==================== +class Post(SQLModel, table=True): + """博客文章表""" + __tablename__ = "posts" + + id: Optional[int] = Field(default=None, primary_key=True) + title: str = Field(max_length=200) + slug: str = Field(max_length=200, index=True) + content: str # 支持 Markdown + created_at: datetime = Field(default_factory=datetime.now) + updated_at: Optional[datetime] = Field(default=None) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + + +# ==================== 记账记录表 ==================== +class Transaction(SQLModel, table=True): + """记账记录表""" + __tablename__ = "transactions" + + id: Optional[int] = Field(default=None, primary_key=True) + amount: Decimal = Field(sa_column=Column(Numeric(10, 2))) # 正数为收入,负数为支出 + category: str = Field(max_length=50) + description: Optional[str] = Field(default=None, max_length=200) + date: date + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + + +# ==================== 媒体-标签关联表(多对多)==================== +class MediaTag(SQLModel, table=True): + """媒体-标签关联表""" + __tablename__ = "media_tags" + + media_id: int = Field(foreign_key="media.id", primary_key=True) + tag_id: int = Field(foreign_key="tags.id", primary_key=True) + + +# ==================== 书影音收藏表 ==================== +class Media(SQLModel, table=True): + """书影音收藏表""" + __tablename__ = "media" + + id: Optional[int] = Field(default=None, primary_key=True) + title: str = Field(max_length=200) + media_type: str = Field(max_length=20) # book / movie / music + rating: Optional[float] = Field(default=None, ge=0.0, le=5.0) + comment: Optional[str] = Field(default=None) + external_id: Optional[str] = Field(default=None, max_length=100) # ISBN、IMDb ID等 + cover_url: Optional[str] = Field(default=None, max_length=300) + created_at: datetime = Field(default_factory=datetime.now) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + tags: list["Tag"] = Relationship(back_populates="media", link_model=MediaTag) + + +# ==================== 媒体标签表 ==================== +class Tag(SQLModel, table=True): + """媒体标签表""" + __tablename__ = "tags" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(max_length=50, unique=True, index=True) + + # 关系 + media: list[Media] = Relationship(back_populates="tags", link_model=MediaTag) + + +# ==================== 聊天消息表 ==================== +class ChatMessage(SQLModel, table=True): + """聊天消息表""" + __tablename__ = "chat_messages" + + id: Optional[int] = Field(default=None, primary_key=True) + content: str + sent_at: datetime = Field(default_factory=datetime.now) + user_id: int = Field(foreign_key="users.id", index=True) + room: str = Field(max_length=50, default="main", index=True) + + # 关系 + user: Optional[User] = Relationship() + + +# ==================== 文件上传记录表 ==================== +class Upload(SQLModel, table=True): + """文件上传记录表""" + __tablename__ = "uploads" + + id: Optional[int] = Field(default=None, primary_key=True) + filename: str = Field(max_length=200) + stored_path: str = Field(max_length=300) + file_size: int # 字节数 + mime_type: str = Field(max_length=100) + uploaded_at: datetime = Field(default_factory=datetime.now) + expires_at: Optional[datetime] = Field(default=None) + user_id: int = Field(foreign_key="users.id", index=True) + + # 关系 + user: Optional[User] = Relationship() + diff --git a/frontend/src/views/Todos.vue b/frontend/src/views/Todos.vue new file mode 100644 index 0000000..496be15 --- /dev/null +++ b/frontend/src/views/Todos.vue @@ -0,0 +1,148 @@ + + + + + + diff --git a/main.py b/main.py index eb389a0..eaf3154 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,21 @@ -# 这是一个示例 Python 脚本。 +""" +项目启动入口 +用于开发环境快速启动应用 +""" +import sys +import os -# 按 Shift+F10 执行或将其替换为您的代码。 -# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。 +# 添加 backend 目录到 Python 路径 +backend_dir = os.path.join(os.path.dirname(__file__), "backend") +if backend_dir not in sys.path: + sys.path.insert(0, backend_dir) +import uvicorn -def print_hi(name): - # 在下面的代码行中使用断点来调试脚本。 - print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。 - - -# 按装订区域中的绿色按钮以运行脚本。 -if __name__ == '__main__': - print_hi('PyCharm') - -# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=8000, + reload=True, # 开发环境自动重载 + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..80b9688 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +# FastAPI 核心 +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 + +# 数据库 +sqlmodel>=0.0.14 +sqlalchemy>=2.0.0 + +# 数据验证 +pydantic>=2.0.0 +pydantic-settings>=2.0.0 +email-validator>=2.0.0 + +# 安全认证 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +python-multipart>=0.0.6 + +# HTTP 客户端(用于调用第三方 API) +httpx>=0.25.0 + +# 其他工具 +python-dotenv>=1.0.0 + diff --git a/start_backend.bat b/start_backend.bat new file mode 100644 index 0000000..97371b0 --- /dev/null +++ b/start_backend.bat @@ -0,0 +1,4 @@ +@echo off +echo 启动后端服务... +python main.py + diff --git a/start_backend.sh b/start_backend.sh new file mode 100644 index 0000000..05e9e6b --- /dev/null +++ b/start_backend.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "启动后端服务..." +python main.py + diff --git a/start_frontend.bat b/start_frontend.bat new file mode 100644 index 0000000..728a4db --- /dev/null +++ b/start_frontend.bat @@ -0,0 +1,5 @@ +@echo off +echo 启动前端服务... +cd frontend +npm run dev + diff --git a/start_frontend.sh b/start_frontend.sh new file mode 100644 index 0000000..9d8c654 --- /dev/null +++ b/start_frontend.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo "启动前端服务..." +cd frontend +npm run dev + diff --git a/数据库设计说明.md b/数据库设计说明.md new file mode 100644 index 0000000..ed40688 --- /dev/null +++ b/数据库设计说明.md @@ -0,0 +1,155 @@ +# 🗃️ 项目数据库设计说明 + +本文档描述了本 FastAPI 练手网站的数据库实体(表)结构与关系设计。 +所有用户相关数据均通过 `user_id` 外键关联,实现**多用户数据隔离**。 +数据库使用 **SQLite(开发阶段)**,ORM 框架推荐 **SQLModel**(兼容 Pydantic + SQLAlchemy)。 + +> 💡 **注**:天气查询功能调用第三方 API,**不持久化存储**,故无对应表。 + +--- + +## 📚 实体列表 + +| 表名 | 用途说明 | +|------------------|------------------------------| +| `users` | 用户账户信息 | +| `todos` | 待办事项列表 | +| `posts` | 博客文章 | +| `transactions` | 个人记账记录 | +| `media` | 书影音收藏条目 | +| `tags` | 媒体标签(用于分类/打标) | +| `media_tags` | `media` 与 `tags` 的多对多关联表 | +| `chat_messages` | 聊天室消息记录 | +| `uploads` | 用户上传的文件元数据 | + +--- + +## 🔍 详细表结构 + +### 1. `users` — 用户表 + +| 字段名 | 类型 | 约束/说明 | +|-------------------|----------------|-------------------------------| +| `id` | INTEGER | 主键 (PK) | +| `username` | VARCHAR(50) | 唯一,非空 | +<|`email`|VARCHAR(100)|唯一,可为空| +| `hashed_password` | TEXT | bcrypt 哈希后的密码 | +| `created_at` | DATETIME | 默认当前时间 | + +--- + +### 2. `todos` — 待办事项 + +| 字段名 | 类型 | 约束/说明 | +|--------------|-------------|--------------------------| +| `id` | INTEGER | PK | +| `title` | VARCHAR(200)| 非空 | +| `done` | BOOLEAN | 默认 `FALSE` | +| `created_at` | DATETIME | 默认当前时间 | +| `user_id` | INTEGER | 外键 → `users.id` | + +--- + +### 3. `posts` — 博客文章 + +| 字段名 | 类型 | 约束/说明 | +|---------------|-------------|----------------------------------------| +| `id` | INTEGER | PK | +| `title` | VARCHAR(200)| 非空 | +| `slug` | VARCHAR(200)| URL 友好标识,建议 `(user_id, slug)` 唯一 | +| `content` | TEXT | 支持 Markdown | +| `created_at` | DATETIME | 默认当前时间 | +| `updated_at` | DATETIME | 可为空 | +| `user_id` | INTEGER | 外键 → `users.id` | + +--- + +### 4. `transactions` — 记账记录 + +| 字段名 | 类型 | 约束/说明 | +|---------------|----------------|----------------------------------| +| `id` | INTEGER | PK | +| `amount` | NUMERIC(10,2) | 正数为收入,负数为支出 | +| `category` | VARCHAR(50) | 如“餐饮”、“交通”、“工资”等 | +| `description` | VARCHAR(200) | 可为空 | +| `date` | DATE | 交易发生日期 | +| `user_id` | INTEGER | 外键 → `users.id` | + +--- + +### 5. `media` — 书影音收藏 + +| 字段名 | 类型 | 约束/说明 | +|----------------|-------------|------------------------------------------------| +| `id` | INTEGER | PK | +| `title` | VARCHAR(200)| 非空 | +| `media_type` | VARCHAR(20) | 枚举值:`book` / `movie` / `music` | +| `rating` | FLOAT | 0.0 ~ 5.0 | +| `comment` | TEXT | 用户短评 | +| `external_id` | VARCHAR(100)| 如 ISBN、IMDb ID(用于对接外部 API) | +| `cover_url` | VARCHAR(300)| 封面图片 URL(可缓存) | +| `created_at` | DATETIME | 默认当前时间 | +| `user_id` | INTEGER | 外键 → `users.id` | + +--- + +### 6. `tags` — 媒体标签 + +| 字段名 | 类型 | 约束/说明 | +|----------|-------------|---------------| +| `id` | INTEGER | PK | +| `name` | VARCHAR(50) | 唯一,非空 | + +--- + +### 7. `media_tags` — 媒体-标签关联表(多对多) + +| 字段名 | 类型 | 约束/说明 | +|-------------|----------|-----------------------------| +| `media_id` | INTEGER | 外键 → `media.id` | +| `tag_id` | INTEGER | 外键 → `tags.id` | +| (联合主键)| — | `(media_id, tag_id)` 唯一 | + +--- + +### 8. `chat_messages` — 聊天消息 + +| 字段名 | 类型 | 约束/说明 | +|-------------|----------|-----------------------------------| +| `id` | INTEGER | PK | +| `content` | TEXT | 非空 | +| `sent_at` | DATETIME | 默认当前时间 | +| `user_id` | INTEGER | 外键 → `users.id`(发送者) | +| `room` | VARCHAR(50)| 聊天室名称(如 `"main"`) | + +--- + +### 9. `uploads` — 文件上传记录 + +| 字段名 | 类型 | 约束/说明 | +|----------------|-------------|--------------------------------------------| +| `id` | INTEGER | PK | +| `filename` | VARCHAR(200)| 原始文件名 | +| `stored_path` | VARCHAR(300)| 服务器存储路径(如 `/uploads/abc.jpg`) | +| `file_size` | INTEGER | 字节数 | +| `mime_type` | VARCHAR(100)| 如 `image/jpeg`, `application/pdf` | +| `uploaded_at` | DATETIME | 默认当前时间 | +| `expires_at` | DATETIME | 可为空(表示永不过期) | +| `user_id` | INTEGER | 外键 → `users.id` | + +> ⚠️ **安全提示**:前端不应直接访问 `stored_path`,应通过受控路由(如 `/download/{id}`)提供下载,并验证用户权限。 + +--- + +## 🔗 实体关系图(ERD) + +```mermaid +erDiagram + users ||--o{ todos : "1:N" + users ||--o{ posts : "1:N" + users ||--o{ transactions : "1:N" + users ||--o{ media : "1:N" + users ||--o{ chat_messages : "1:N" + users ||--o{ uploads : "1:N" + + media }o--o{ tags : "N:M via media_tags" \ No newline at end of file