初始版本
This commit is contained in:
+57
-7
@@ -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
|
||||
|
||||
@@ -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 执行或将其替换为您的代码。
|
||||
# 按 双击 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, # 开发环境自动重载
|
||||
)
|
||||
|
||||
@@ -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