初始版本

This commit is contained in:
z66
2025-12-26 13:42:22 +08:00
parent ddb90d6c20
commit b495bc1dca
43 changed files with 2179 additions and 20 deletions
+57 -7
View File
@@ -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
+190
View File
@@ -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
+179
View File
@@ -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
View File
@@ -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. 配置生产环境部署
+98
View File
@@ -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
```
+30
View File
@@ -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",
]
+4
View File
@@ -0,0 +1,4 @@
"""
FastAPI 应用包
"""
+4
View File
@@ -0,0 +1,4 @@
"""
API 路由模块
"""
+4
View File
@@ -0,0 +1,4 @@
"""
API v1 路由
"""
+14
View File
@@ -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 端点模块
"""
+142
View File
@@ -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
+123
View File
@@ -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
+63
View File
@@ -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
+13
View File
@@ -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",
]
+44
View File
@@ -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
+16
View File
@@ -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",
]
+71
View File
@@ -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()
+26
View File
@@ -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
+21
View File
@@ -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()
# 创建示例用户等
...
```
+5
View File
@@ -0,0 +1,5 @@
"""
初始数据模块
用于数据库初始化时插入示例数据
"""
+45
View File
@@ -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"}
+23
View File
@@ -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",
]
+40
View File
@@ -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")
+24
View File
@@ -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()
+23
View File
@@ -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")
+22
View File
@@ -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()
+25
View File
@@ -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()
+25
View File
@@ -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()
+18
View File
@@ -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)
+5
View File
@@ -0,0 +1,5 @@
"""
Pydantic 模式定义
用于 API 请求和响应的数据验证
"""
+42
View File
@@ -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
+39
View File
@@ -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
+50
View File
@@ -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
+144
View File
@@ -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()
+148
View File
@@ -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>
+18 -13
View File
@@ -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, # 开发环境自动重载
)
+24
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
@echo off
echo 启动后端服务...
python main.py
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
echo "启动后端服务..."
python main.py
+5
View File
@@ -0,0 +1,5 @@
@echo off
echo 启动前端服务...
cd frontend
npm run dev
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
echo "启动前端服务..."
cd frontend
npm run dev
+155
View File
@@ -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"