初始版本
This commit is contained in:
+57
-7
@@ -1,9 +1,59 @@
|
|||||||
### Example user template template
|
# Python
|
||||||
### Example user template
|
__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
|
venv/
|
||||||
*.iml
|
env/
|
||||||
out
|
ENV/
|
||||||
gen
|
.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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
+183
@@ -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. 配置生产环境部署
|
||||||
|
|
||||||
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
FastAPI 应用包
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
API 路由模块
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
API v1 路由
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -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=["博客"])
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
API 端点模块
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
# 创建示例用户等
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""
|
||||||
|
初始数据模块
|
||||||
|
用于数据库初始化时插入示例数据
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -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"}
|
||||||
|
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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")
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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")
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""
|
||||||
|
Pydantic 模式定义
|
||||||
|
用于 API 请求和响应的数据验证
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div class="todos">
|
||||||
|
<h1>待办事项</h1>
|
||||||
|
<div v-if="!isAuthenticated" class="warning">
|
||||||
|
请先 <router-link to="/login">登录</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<form @submit.prevent="addTodo">
|
||||||
|
<input v-model="newTodo" placeholder="输入待办事项..." />
|
||||||
|
<button type="submit">添加</button>
|
||||||
|
</form>
|
||||||
|
<ul>
|
||||||
|
<li v-for="todo in todos" :key="todo.id">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="todo.done"
|
||||||
|
@change="toggleTodo(todo)"
|
||||||
|
/>
|
||||||
|
<span :class="{ done: todo.done }">{{ todo.title }}</span>
|
||||||
|
<button @click="deleteTodo(todo.id)">删除</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||||
|
|
||||||
|
const todos = ref([])
|
||||||
|
const newTodo = ref('')
|
||||||
|
|
||||||
|
const fetchTodos = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/v1/todos/')
|
||||||
|
todos.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取待办事项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTodo = async () => {
|
||||||
|
if (!newTodo.value.trim()) return
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/v1/todos/', {
|
||||||
|
title: newTodo.value
|
||||||
|
})
|
||||||
|
todos.value.push(response.data)
|
||||||
|
newTodo.value = ''
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加待办事项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTodo = async (todo) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/v1/todos/${todo.id}`, {
|
||||||
|
done: !todo.done
|
||||||
|
})
|
||||||
|
const index = todos.value.findIndex(t => t.id === todo.id)
|
||||||
|
todos.value[index] = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新待办事项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTodo = async (id) => {
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/v1/todos/${id}`)
|
||||||
|
todos.value = todos.value.filter(t => t.id !== id)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除待办事项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
fetchTodos()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.todos {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos input[type="text"] {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: #42b983;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos li span.done {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todos li button {
|
||||||
|
margin-left: auto;
|
||||||
|
background: #dc3545;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
# 这是一个示例 Python 脚本。
|
"""
|
||||||
|
项目启动入口
|
||||||
|
用于开发环境快速启动应用
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
# 按 Shift+F10 执行或将其替换为您的代码。
|
# 添加 backend 目录到 Python 路径
|
||||||
# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。
|
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):
|
if __name__ == "__main__":
|
||||||
# 在下面的代码行中使用断点来调试脚本。
|
uvicorn.run(
|
||||||
print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。
|
"app.main:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8000,
|
||||||
# 按装订区域中的绿色按钮以运行脚本。
|
reload=True, # 开发环境自动重载
|
||||||
if __name__ == '__main__':
|
)
|
||||||
print_hi('PyCharm')
|
|
||||||
|
|
||||||
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
echo 启动后端服务...
|
||||||
|
python main.py
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "启动后端服务..."
|
||||||
|
python main.py
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
echo 启动前端服务...
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "启动前端服务..."
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
+155
@@ -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"
|
||||||
Reference in New Issue
Block a user