Initial commit: React + Django full-stack project setup
- Backend: Django 4.2 + DRF + JWT + GraphQL - Frontend: React 18 + MobX + styled-components - Deployment: Docker + Docker Compose + Nginx - Database: PostgreSQL support - Documentation: README, INIT, PROJECT_DOCS, TESTING
This commit is contained in:
22
.env.example
Normal file
22
.env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
# Django Settings
|
||||
DJANGO_SECRET_KEY=your-secret-key-here-change-in-production
|
||||
DJANGO_DEBUG=False
|
||||
|
||||
# Database
|
||||
DB_NAME=your_database_name
|
||||
DB_USER=your_database_user
|
||||
DB_PASSWORD=your_database_password
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS=localhost,yourdomain.com
|
||||
|
||||
# CORS
|
||||
CORS_ALLOWED_ORIGINS=http://localhost,https://yourdomain.com
|
||||
|
||||
# Email (optional)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=your-email@example.com
|
||||
EMAIL_HOST_PASSWORD=your-email-password
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Django
|
||||
*.log
|
||||
.env
|
||||
/local_settings.py
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
# Build
|
||||
build/
|
||||
dist/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
120
INIT.md
Normal file
120
INIT.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 项目初始化指南
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置环境变量
|
||||
|
||||
```bash
|
||||
# 复制环境变量模板
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑 .env 文件,配置以下内容:
|
||||
# - DJANGO_SECRET_KEY (生成一个安全的密钥)
|
||||
# - DB_NAME, DB_USER, DB_PASSWORD, DB_HOST (数据库配置)
|
||||
# - ALLOWED_HOSTS (允许的域名)
|
||||
```
|
||||
|
||||
### 2. 后端初始化
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 配置自定义用户模型 (首次运行需要)
|
||||
# 在运行迁移前,确保 apps.users.models.User 已创建
|
||||
|
||||
# 运行迁移
|
||||
python manage.py migrate
|
||||
|
||||
# 创建超级用户
|
||||
python manage.py createsuperuser
|
||||
|
||||
# 运行开发服务器
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
后端将在 http://localhost:8000 启动
|
||||
|
||||
### 3. 前端初始化
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env,设置 REACT_APP_API_URL
|
||||
|
||||
# 启动开发服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
前端将在 http://localhost:3000 启动
|
||||
|
||||
## Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建并启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
|
||||
# 停止并删除卷
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## 数据库配置
|
||||
|
||||
### 使用外部 PostgreSQL
|
||||
|
||||
在 `.env` 中配置:
|
||||
|
||||
```env
|
||||
DB_HOST=your-db-host
|
||||
DB_PORT=5432
|
||||
DB_NAME=your_database_name
|
||||
DB_USER=your_database_user
|
||||
DB_PASSWORD=your_database_password
|
||||
```
|
||||
|
||||
### 使用 Docker PostgreSQL
|
||||
|
||||
默认配置会启动一个 PostgreSQL 容器,无需额外配置。
|
||||
|
||||
## 验证安装
|
||||
|
||||
1. 访问 http://localhost:8000/admin - Django 管理后台
|
||||
2. 访问 http://localhost:8000/api/ - REST API
|
||||
3. 访问 http://localhost:8000/graphql/?graphiql - GraphQL playground
|
||||
4. 访问 http://localhost:3000 - React 前端
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 迁移错误
|
||||
如果遇到迁移错误,删除 migrations 文件并重新生成:
|
||||
|
||||
```bash
|
||||
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### 静态文件收集错误
|
||||
```bash
|
||||
python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
### 数据库连接错误
|
||||
检查 `.env` 文件中的数据库配置是否正确。
|
||||
262
PROJECT_DOCS.md
Normal file
262
PROJECT_DOCS.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 项目架构文档
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端 (Django)
|
||||
- **框架**: Django 4.2
|
||||
- **API 框架**: Django REST Framework
|
||||
- **认证**: JWT (djangorestframework-simplejwt)
|
||||
- **GraphQL**: graphene-django
|
||||
- **数据库**: PostgreSQL
|
||||
- **静态文件**: Whitenoise
|
||||
- **生产服务器**: Gunicorn
|
||||
|
||||
### 前端 (React)
|
||||
- **框架**: React 18
|
||||
- **构建工具**: Create React App
|
||||
- **状态管理**: MobX + mobx-react-lite
|
||||
- **样式**: styled-components
|
||||
- **路由**: React Router 6
|
||||
- **HTTP 客户端**: axios
|
||||
|
||||
### 部署
|
||||
- **容器化**: Docker + Docker Compose
|
||||
- **反向代理**: Nginx
|
||||
- **进程管理**: Gunicorn
|
||||
|
||||
## 项目结构
|
||||
|
||||
### 后端结构
|
||||
```
|
||||
backend/
|
||||
├── config/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py # 基础配置
|
||||
│ │ ├── dev.py # 开发环境
|
||||
│ │ └── prod.py # 生产环境
|
||||
│ ├── urls.py # 主路由
|
||||
│ ├── wsgi.py # WSGI 配置
|
||||
│ └── asgi.py # ASGI 配置
|
||||
├── apps/
|
||||
│ ├── users/ # 用户管理
|
||||
│ │ ├── models.py # User 模型
|
||||
│ │ ├── serializers.py
|
||||
│ │ ├── views.py # ViewSets
|
||||
│ │ └── urls.py
|
||||
│ ├── core/ # 核心业务
|
||||
│ └── api/ # API 配置
|
||||
│ ├── serializers.py
|
||||
│ ├── views.py
|
||||
│ ├── urls.py
|
||||
│ ├── schema.py # GraphQL schema
|
||||
│ └── graphql_urls.py
|
||||
├── static/ # 静态文件
|
||||
├── media/ # 媒体文件
|
||||
├── requirements.txt
|
||||
├── manage.py
|
||||
└── start.sh # 快速启动脚本
|
||||
```
|
||||
|
||||
### 前端结构
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/ # React 组件
|
||||
│ ├── stores/ # MobX stores
|
||||
│ │ ├── AuthStore.js
|
||||
│ │ └── UserStore.js
|
||||
│ ├── services/ # API 服务
|
||||
│ │ └── api.js # axios 配置
|
||||
│ ├── styles/ # 全局样式
|
||||
│ │ └── global.js
|
||||
│ ├── App.js # 主应用
|
||||
│ └── index.js # 入口文件
|
||||
├── public/ # 公共资源
|
||||
├── package.json
|
||||
├── .env.example
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── Dockerfile
|
||||
└── start.sh # 快速启动脚本
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 用户认证
|
||||
|
||||
**JWT 认证流程:**
|
||||
1. 用户发送 email + password 到 `/api/auth/login/`
|
||||
2. 后端验证并返回 access_token 和 refresh_token
|
||||
3. 前端保存 tokens 到 localStorage
|
||||
4. 后续请求在 Header 中携带 Bearer token
|
||||
5. Token 过期时自动使用 refresh_token 刷新
|
||||
|
||||
**自定义用户模型:**
|
||||
```python
|
||||
# apps/users/models.py
|
||||
class User(AbstractUser):
|
||||
email = models.EmailField(unique=True)
|
||||
avatar = models.ImageField(upload_to='avatars/')
|
||||
USERNAME_FIELD = 'email'
|
||||
```
|
||||
|
||||
### 2. REST API
|
||||
|
||||
**端点:**
|
||||
- `POST /api/auth/login/` - 登录获取 token
|
||||
- `POST /api/auth/token/refresh/` - 刷新 token
|
||||
- `GET /api/users/` - 获取用户列表
|
||||
- `GET /api/users/me/` - 获取当前用户
|
||||
- `GET /api/users/:id/` - 获取特定用户
|
||||
|
||||
### 3. GraphQL
|
||||
|
||||
**Schema:**
|
||||
```graphql
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
allUsers: [User!]!
|
||||
me: User
|
||||
}
|
||||
```
|
||||
|
||||
访问 `/graphql/?graphiql` 查看调试界面。
|
||||
|
||||
### 4. 状态管理 (MobX)
|
||||
|
||||
**AuthStore:**
|
||||
- 管理认证状态
|
||||
- 处理登录/登出
|
||||
- 保存和读取 tokens
|
||||
|
||||
**UserStore:**
|
||||
- 获取当前用户信息
|
||||
- 管理用户数据状态
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 1. 添加新的 API 端点
|
||||
|
||||
```python
|
||||
# apps/yourapp/views.py
|
||||
class YourViewSet(viewsets.ModelViewSet):
|
||||
queryset = YourModel.objects.all()
|
||||
serializer_class = YourSerializer
|
||||
|
||||
# apps/yourapp/urls.py
|
||||
router = DefaultRouter()
|
||||
router.register(r'endpoint', YourViewSet)
|
||||
```
|
||||
|
||||
### 2. 添加新的 MobX Store
|
||||
|
||||
```javascript
|
||||
// src/stores/YourStore.js
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
class YourStore {
|
||||
data = null;
|
||||
loading = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
this.loading = true;
|
||||
// API 调用
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default YourStore;
|
||||
```
|
||||
|
||||
### 3. 添加新的 React 组件
|
||||
|
||||
```javascript
|
||||
// src/components/YourComponent.js
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
function YourComponent() {
|
||||
return <Container>Your content</Container>;
|
||||
}
|
||||
|
||||
export default YourComponent;
|
||||
```
|
||||
|
||||
## 部署配置
|
||||
|
||||
### Docker Compose 服务
|
||||
|
||||
**backend:**
|
||||
- 基于 Python 3.11
|
||||
- 运行 Gunicorn
|
||||
- 暴露 8000 端口
|
||||
- 挂载 static 和 media 卷
|
||||
|
||||
**frontend:**
|
||||
- 基于 Node 18 构建
|
||||
- 使用 nginx 作为服务器
|
||||
- 暴露 80 端口
|
||||
- 代理 API 请求到 backend
|
||||
|
||||
**db:**
|
||||
- PostgreSQL 15
|
||||
- 持久化数据卷
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
- `/` → React 应用
|
||||
- `/api/*` → Django 后端
|
||||
- `/graphql/*` → Django GraphQL
|
||||
- `/media/*` → Django media
|
||||
- `/static/*` → Django static
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **生产环境必须:**
|
||||
- 设置 `DJANGO_SECRET_KEY` 为强随机值
|
||||
- `DEBUG=False`
|
||||
- 配置 `ALLOWED_HOSTS`
|
||||
- 启用 HTTPS
|
||||
- 设置 `CORS_ALLOWED_ORIGINS`
|
||||
|
||||
2. **数据库:**
|
||||
- 使用强密码
|
||||
- 不暴露到公网
|
||||
- 定期备份
|
||||
|
||||
3. **JWT:**
|
||||
- Token 过期时间合理配置
|
||||
- 刷新 token 轮换
|
||||
- 生产环境使用 HTTPS
|
||||
|
||||
## 下一步开发建议
|
||||
|
||||
1. **后端:**
|
||||
- 添加更多业务 apps
|
||||
- 实现权限控制
|
||||
- 添加 API 文档 (Swagger)
|
||||
- 实现文件上传功能
|
||||
|
||||
2. **前端:**
|
||||
- 创建登录/注册页面
|
||||
- 添加路由保护
|
||||
- 实现加载状态处理
|
||||
- 添加错误处理
|
||||
|
||||
3. **部署:**
|
||||
- 配置 HTTPS (Let's Encrypt)
|
||||
- 设置 CI/CD
|
||||
- 配置日志收集
|
||||
- 实现自动备份
|
||||
187
README.md
Normal file
187
README.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# React + Django Full-Stack Project
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Backend
|
||||
- Django 4.2
|
||||
- Django REST Framework
|
||||
- JWT Authentication (djangorestframework-simplejwt)
|
||||
- GraphQL (graphene-django)
|
||||
- PostgreSQL
|
||||
|
||||
### Frontend
|
||||
- React 18 (Create React App)
|
||||
- MobX (state management)
|
||||
- styled-components (CSS-in-JS)
|
||||
- React Router
|
||||
|
||||
### Deployment
|
||||
- Docker & Docker Compose
|
||||
- Nginx (reverse proxy)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── backend/ # Django project
|
||||
│ ├── config/ # Settings and configuration
|
||||
│ ├── apps/ # Django apps
|
||||
│ ├── static/ # Static files
|
||||
│ └── media/ # Media files
|
||||
├── frontend/ # React project
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ ├── stores/ # MobX stores
|
||||
│ │ ├── services/ # API calls
|
||||
│ │ └── styles/
|
||||
│ └── public/
|
||||
├── docker-compose.yml
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Environment Variables
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Update the following variables:
|
||||
- `DJANGO_SECRET_KEY` - Generate a secure secret key
|
||||
- Database credentials (DB_NAME, DB_USER, DB_PASSWORD)
|
||||
- `ALLOWED_HOSTS` - Add your domain(s)
|
||||
|
||||
### 2. PostgreSQL Configuration
|
||||
|
||||
If using external PostgreSQL (already deployed):
|
||||
|
||||
Update `.env` with your database credentials:
|
||||
```env
|
||||
DB_HOST=your-db-host
|
||||
DB_PORT=5432
|
||||
DB_NAME=your_database_name
|
||||
DB_USER=your_database_user
|
||||
DB_PASSWORD=your_database_password
|
||||
```
|
||||
|
||||
If using Docker PostgreSQL:
|
||||
|
||||
The default values in `docker-compose.yml` will work.
|
||||
|
||||
### 3. Backend Setup
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run migrations
|
||||
python manage.py migrate
|
||||
|
||||
# Create superuser
|
||||
python manage.py createsuperuser
|
||||
|
||||
# Run development server
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
### 4. Frontend Setup
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm start
|
||||
```
|
||||
|
||||
### 5. Docker Deployment
|
||||
|
||||
```bash
|
||||
# Build and start all services
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### REST API
|
||||
- `/api/users/` - User management
|
||||
- `/api/users/me/` - Current user
|
||||
- `/api/auth/token/` - Login (POST)
|
||||
- `/api/auth/token/refresh/` - Refresh token (POST)
|
||||
|
||||
### GraphQL
|
||||
- `/graphql/` - GraphQL endpoint
|
||||
- `/graphql/?graphiql` - GraphQL playground
|
||||
|
||||
## Development
|
||||
|
||||
### Running Backend
|
||||
```bash
|
||||
cd backend
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
### Running Frontend
|
||||
```bash
|
||||
cd frontend
|
||||
npm start
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
Backend:
|
||||
```bash
|
||||
cd backend
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
Frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm test
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Using Docker Compose
|
||||
1. Update `.env` with production values
|
||||
2. Build images: `docker-compose build`
|
||||
3. Start services: `docker-compose up -d`
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
The frontend Dockerfile includes nginx configuration that:
|
||||
- Serves React app
|
||||
- Proxies `/api` requests to Django backend
|
||||
- Proxies `/graphql` requests to Django backend
|
||||
- Handles static and media files
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never commit `.env` files**
|
||||
- Change `DJANGO_SECRET_KEY` in production
|
||||
- Use strong passwords for database
|
||||
- Enable HTTPS in production
|
||||
- Configure `ALLOWED_HOSTS` properly
|
||||
- Set `DEBUG=False` in production
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
163
TESTING.md
Normal file
163
TESTING.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# API 测试指南
|
||||
|
||||
## 测试后端 API
|
||||
|
||||
### 1. 获取 Token (登录)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/auth/login/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "your@email.com",
|
||||
"password": "your_password"
|
||||
}'
|
||||
```
|
||||
|
||||
响应示例:
|
||||
```json
|
||||
{
|
||||
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"email": "your@email.com",
|
||||
"username": "your_username",
|
||||
"first_name": "First",
|
||||
"last_name": "Last"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 刷新 Token
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"refresh": "your_refresh_token"
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. 获取用户列表 (需要认证)
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/api/users/ \
|
||||
-H "Authorization: Bearer your_access_token"
|
||||
```
|
||||
|
||||
### 4. 获取当前用户
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8000/api/users/me/ \
|
||||
-H "Authorization: Bearer your_access_token"
|
||||
```
|
||||
|
||||
## GraphQL 测试
|
||||
|
||||
访问 http://localhost:8000/graphql/?graphiql
|
||||
|
||||
### 查询所有用户
|
||||
|
||||
```graphql
|
||||
query {
|
||||
allUsers {
|
||||
id
|
||||
email
|
||||
username
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查询当前用户
|
||||
|
||||
```graphql
|
||||
query {
|
||||
me {
|
||||
id
|
||||
email
|
||||
username
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Postman 集合
|
||||
|
||||
你可以导入以下 Postman 集合来测试 API:
|
||||
|
||||
### 环境变量
|
||||
- `base_url`: http://localhost:8000
|
||||
- `access_token`: (登录后自动填充)
|
||||
|
||||
### 请求示例
|
||||
|
||||
**1. 登录**
|
||||
- Method: POST
|
||||
- URL: `{{base_url}}/api/auth/login/`
|
||||
- Body:
|
||||
```json
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "testpass123"
|
||||
}
|
||||
```
|
||||
- Tests (自动提取 token):
|
||||
```javascript
|
||||
var jsonData = pm.response.json();
|
||||
pm.environment.set("access_token", jsonData.access);
|
||||
```
|
||||
|
||||
**2. 获取用户**
|
||||
- Method: GET
|
||||
- URL: `{{base_url}}/api/users/`
|
||||
- Headers:
|
||||
```
|
||||
Authorization: Bearer {{access_token}}
|
||||
```
|
||||
|
||||
**3. 获取当前用户**
|
||||
- Method: GET
|
||||
- URL: `{{base_url}}/api/users/me/`
|
||||
- Headers:
|
||||
```
|
||||
Authorization: Bearer {{access_token}}
|
||||
```
|
||||
|
||||
## 自动化测试
|
||||
|
||||
### 后端测试
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
### 前端测试
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm test
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 401 Unauthorized
|
||||
- Token 过期,使用 refresh_token 刷新
|
||||
- Token 格式错误,确保 `Bearer` 前缀存在
|
||||
- Token 被吊销
|
||||
|
||||
### 403 Forbidden
|
||||
- 权限不足,检查用户是否有相应权限
|
||||
- CSRF token 问题(开发环境可能遇到)
|
||||
|
||||
### 404 Not Found
|
||||
- 端点不存在,检查 URL 路径
|
||||
- 检查 Django 是否正确启动
|
||||
|
||||
### 500 Server Error
|
||||
- 检查 Django 日志
|
||||
- 检查数据库连接
|
||||
- 检查代码语法错误
|
||||
45
backend/.gitignore
vendored
Normal file
45
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Django
|
||||
*.log
|
||||
/local_settings.py
|
||||
/staticfiles/
|
||||
/media/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
31
backend/Dockerfile
Normal file
31
backend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
-y --no-install-recommends && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project
|
||||
COPY . .
|
||||
|
||||
# Collect static files
|
||||
RUN python manage.py collectstatic --noinput --settings=config.settings.prod
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run gunicorn
|
||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
||||
1
backend/apps/__init__.py
Normal file
1
backend/apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Apps package
|
||||
1
backend/apps/api/__init__.py
Normal file
1
backend/apps/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API app
|
||||
6
backend/apps/api/apps.py
Normal file
6
backend/apps/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.api'
|
||||
7
backend/apps/api/graphql_urls.py
Normal file
7
backend/apps/api/graphql_urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from graphene_django.views import GraphQLView
|
||||
from apps.api.schema import schema
|
||||
|
||||
urlpatterns = [
|
||||
path('', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
]
|
||||
31
backend/apps/api/schema.py
Normal file
31
backend/apps/api/schema.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class UserType(DjangoObjectType):
|
||||
"""GraphQL type for User."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
"""Root GraphQL Query."""
|
||||
|
||||
all_users = graphene.List(UserType)
|
||||
me = graphene.Field(UserType)
|
||||
|
||||
def resolve_all_users(root, info):
|
||||
"""Resolve all users query."""
|
||||
return User.objects.all()
|
||||
|
||||
def resolve_me(root, info):
|
||||
"""Resolve current user query."""
|
||||
if info.context.user.is_authenticated:
|
||||
return info.context.user
|
||||
return None
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
29
backend/apps/api/serializers.py
Normal file
29
backend/apps/api/serializers.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
"""Custom JWT token serializer that includes user data."""
|
||||
|
||||
@classmethod
|
||||
def get_token(cls, user):
|
||||
token = super().get_token(user)
|
||||
# Add custom claims
|
||||
token['email'] = user.email
|
||||
token['username'] = user.username
|
||||
return token
|
||||
|
||||
def validate(self, attrs):
|
||||
data = super().validate(attrs)
|
||||
# Add user data to response
|
||||
data['user'] = {
|
||||
'id': self.user.id,
|
||||
'email': self.user.email,
|
||||
'username': self.user.username,
|
||||
'first_name': self.user.first_name,
|
||||
'last_name': self.user.last_name,
|
||||
}
|
||||
return data
|
||||
12
backend/apps/api/urls.py
Normal file
12
backend/apps/api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenRefreshView,
|
||||
)
|
||||
from apps.users.urls import urlpatterns as users_urls
|
||||
from apps.api.views import CustomTokenObtainPairView
|
||||
|
||||
urlpatterns = [
|
||||
path('auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('', include(users_urls)),
|
||||
]
|
||||
11
backend/apps/api/views.py
Normal file
11
backend/apps/api/views.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from .serializers import CustomTokenObtainPairSerializer
|
||||
|
||||
|
||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
"""Custom token view that returns user data with tokens."""
|
||||
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
1
backend/apps/core/__init__.py
Normal file
1
backend/apps/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Core app
|
||||
6
backend/apps/core/apps.py
Normal file
6
backend/apps/core/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.core'
|
||||
1
backend/apps/users/__init__.py
Normal file
1
backend/apps/users/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Users app
|
||||
6
backend/apps/users/apps.py
Normal file
6
backend/apps/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
||||
22
backend/apps/users/models.py
Normal file
22
backend/apps/users/models.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending AbstractUser."""
|
||||
|
||||
email = models.EmailField(unique=True)
|
||||
first_name = models.CharField(max_length=150)
|
||||
last_name = models.CharField(max_length=150)
|
||||
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username', 'first_name']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
20
backend/apps/users/serializers.py
Normal file
20
backend/apps/users/serializers.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from rest_framework import serializers
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for User model."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar')
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class UserDetailSerializer(serializers.ModelSerializer):
|
||||
"""Detailed serializer for User model."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
read_only_fields = ('id', 'date_joined', 'last_login')
|
||||
10
backend/apps/users/urls.py
Normal file
10
backend/apps/users/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'users', UserViewSet, basename='user')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
23
backend/apps/users/views.py
Normal file
23
backend/apps/users/views.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from rest_framework import viewsets, permissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from .models import User
|
||||
from .serializers import UserSerializer, UserDetailSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for User model."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ['retrieve', 'update', 'partial_update']:
|
||||
return UserDetailSerializer
|
||||
return UserSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def me(self, request):
|
||||
"""Get current user."""
|
||||
serializer = self.get_serializer(request.user)
|
||||
return Response(serializer.data)
|
||||
1
backend/config/__init__.py
Normal file
1
backend/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Config package
|
||||
9
backend/config/asgi.py
Normal file
9
backend/config/asgi.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
ASGI config for the project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.prod')
|
||||
application = get_asgi_application()
|
||||
165
backend/config/settings/base.py
Normal file
165
backend/config/settings/base.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Base Django settings for the project.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-change-this-in-production')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Third party
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'corsheaders',
|
||||
'django_filters',
|
||||
'graphene_django',
|
||||
|
||||
# Local apps
|
||||
'apps.users',
|
||||
'apps.core',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.environ.get('DB_NAME', 'postgres'),
|
||||
'USER': os.environ.get('DB_USER', 'postgres'),
|
||||
'PASSWORD': os.environ.get('DB_PASSWORD', 'postgres'),
|
||||
'HOST': os.environ.get('DB_HOST', 'localhost'),
|
||||
'PORT': os.environ.get('DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
# Password validation
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
TIME_ZONE = 'UTC'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
# Media files
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# REST Framework
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
'rest_framework.filters.OrderingFilter',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
}
|
||||
|
||||
# JWT Settings
|
||||
from datetime import timedelta
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
'ROTATE_REFRESH_TOKENS': True,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'UPDATE_LAST_LOGIN': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
}
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
]
|
||||
|
||||
# GraphQL
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'apps.api.schema.schema',
|
||||
'MIDDLEWARE': [
|
||||
'graphql_jwt.middleware.JSONWebTokenMiddleware',
|
||||
],
|
||||
}
|
||||
19
backend/config/settings/dev.py
Normal file
19
backend/config/settings/dev.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Development settings for the project.
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
# Email backend (console for development)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Additional apps for development
|
||||
INSTALLED_APPS += [
|
||||
'django_extensions',
|
||||
]
|
||||
31
backend/config/settings/prod.py
Normal file
31
backend/config/settings/prod.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Production settings for the project.
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
|
||||
|
||||
# Security settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# CORS - use environment variable
|
||||
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',')
|
||||
CSORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Email backend (use your email service in production)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = os.environ.get('EMAIL_HOST')
|
||||
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
||||
18
backend/config/urls.py
Normal file
18
backend/config/urls.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
URL configuration for the project.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('apps.api.urls')),
|
||||
path('graphql/', include('apps.api.graphql_urls')),
|
||||
]
|
||||
|
||||
# Serve media files in development
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
9
backend/config/wsgi.py
Normal file
9
backend/config/wsgi.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
WSGI config for the project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.prod')
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Normal file
22
backend/manage.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Django>=4.2,<5.0
|
||||
djangorestframework>=3.14
|
||||
djangorestframework-simplejwt>=5.2
|
||||
django-cors-headers>=4.0
|
||||
psycopg2-binary>=2.9
|
||||
python-dotenv>=1.0
|
||||
Pillow>=10.0
|
||||
graphene-django>=3.1
|
||||
django-filter>=23.0
|
||||
gunicorn>=21.0
|
||||
whitenoise>=6.5
|
||||
29
backend/start.sh
Executable file
29
backend/start.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Starting Django Backend..."
|
||||
|
||||
# 激活虚拟环境
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
echo "✅ Virtual environment activated"
|
||||
else
|
||||
echo "❌ Virtual environment not found. Creating one..."
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "📦 Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行迁移
|
||||
echo "🔄 Running migrations..."
|
||||
python manage.py migrate
|
||||
|
||||
# 收集静态文件
|
||||
echo "📁 Collecting static files..."
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# 启动服务器
|
||||
echo "🎉 Starting development server on http://localhost:8000"
|
||||
python manage.py runserver
|
||||
54
docker-compose.yml
Normal file
54
docker-compose.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: django_backend
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- backend_static:/app/staticfiles
|
||||
- backend_media:/app/media
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- app_network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: react_frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app_network
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: postgres_db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- app_network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
backend_static:
|
||||
backend_media:
|
||||
|
||||
networks:
|
||||
app_network:
|
||||
driver: bridge
|
||||
2
frontend/.env.example
Normal file
2
frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
REACT_APP_API_URL=http://localhost:8000
|
||||
REACT_APP_ENV=development
|
||||
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
31
frontend/Dockerfile
Normal file
31
frontend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM node:18-alpine as build
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy project
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built files
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
|
||||
# Copy nginx config
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
37
frontend/nginx.conf
Normal file
37
frontend/nginx.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /graphql {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /media {
|
||||
proxy_pass http://backend:8000;
|
||||
}
|
||||
|
||||
location /static {
|
||||
proxy_pass http://backend:8000;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
}
|
||||
48
frontend/package.json
Normal file
48
frontend/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "react-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react-lite": "^4.0.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"styled-components": "^6.1.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"eslint": "^8.55.0",
|
||||
"prettier": "^3.1.0"
|
||||
},
|
||||
"proxy": "http://localhost:8000"
|
||||
}
|
||||
15
frontend/public/index.html
Normal file
15
frontend/public/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="React + Django App" />
|
||||
<title>React + Django App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
35
frontend/src/App.js
Normal file
35
frontend/src/App.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const Header = styled.header`
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
`;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Container>
|
||||
<Header>
|
||||
<Title>React + Django App</Title>
|
||||
</Header>
|
||||
<Routes>
|
||||
<Route path="/" element={<div>Welcome to the app!</div>} />
|
||||
</Routes>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
27
frontend/src/index.js
Normal file
27
frontend/src/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'mobx-react-lite';
|
||||
import App from './App';
|
||||
import './styles/global';
|
||||
|
||||
// Import stores
|
||||
import UserStore from './stores/UserStore';
|
||||
import AuthStore from './stores/AuthStore';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
const stores = {
|
||||
userStore: new UserStore(),
|
||||
authStore: new AuthStore(),
|
||||
};
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider {...stores}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
54
frontend/src/services/api.js
Normal file
54
frontend/src/services/api.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8000',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add token to requests
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Handle token refresh
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refresh');
|
||||
const response = await axios.post('/api/token/refresh/', {
|
||||
refresh: refreshToken,
|
||||
});
|
||||
|
||||
const newToken = response.data.access;
|
||||
localStorage.setItem('token', newToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
|
||||
return api(originalRequest);
|
||||
} catch (refreshError) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
5
frontend/src/setupTests.js
Normal file
5
frontend/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
44
frontend/src/stores/AuthStore.js
Normal file
44
frontend/src/stores/AuthStore.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import axios from 'axios';
|
||||
|
||||
class AuthStore {
|
||||
token = localStorage.getItem('token') || null;
|
||||
isAuthenticated = !!localStorage.getItem('token');
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
async login(email, password) {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login/', {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
this.token = response.data.access;
|
||||
this.isAuthenticated = true;
|
||||
localStorage.setItem('token', this.token);
|
||||
localStorage.setItem('refresh', response.data.refresh);
|
||||
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.detail || 'Login failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
this.isAuthenticated = false;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthStore;
|
||||
32
frontend/src/stores/UserStore.js
Normal file
32
frontend/src/stores/UserStore.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import axios from 'axios';
|
||||
|
||||
class UserStore {
|
||||
user = null;
|
||||
loading = false;
|
||||
error = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
async fetchCurrentUser() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/users/me/');
|
||||
this.user = response.data;
|
||||
} catch (error) {
|
||||
this.error = error.response?.data || 'Failed to fetch user';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
clearUser() {
|
||||
this.user = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserStore;
|
||||
24
frontend/src/styles/global.js
Normal file
24
frontend/src/styles/global.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyle;
|
||||
13
frontend/start.sh
Executable file
13
frontend/start.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Starting React Frontend..."
|
||||
|
||||
# 检查 node_modules
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# 启动开发服务器
|
||||
echo "🎉 Starting development server on http://localhost:3000"
|
||||
npm start
|
||||
Reference in New Issue
Block a user