前后端第一版提交
This commit is contained in:
274
hertz_demo/README.md
Normal file
274
hertz_demo/README.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Hertz Demo 演示模块
|
||||
|
||||
## 📋 模块概述
|
||||
|
||||
Hertz Demo 模块是一个功能演示和测试模块,提供了完整的示例代码和交互式演示页面,帮助开发者快速了解和使用 Hertz Server Django 框架的各项功能特性。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **验证码演示**: 展示多种验证码类型的生成、刷新和验证功能
|
||||
- **邮件系统演示**: 提供邮件模板预览和发送测试功能
|
||||
- **WebSocket演示**: 实时通信功能演示和测试
|
||||
- **交互式界面**: 美观的Web界面,支持实时操作和反馈
|
||||
- **完整示例代码**: 提供可直接参考的实现代码
|
||||
|
||||
## 📁 模块结构
|
||||
|
||||
```
|
||||
hertz_demo/
|
||||
├── __init__.py # 模块初始化
|
||||
├── apps.py # Django应用配置
|
||||
├── models.py # 数据模型(预留)
|
||||
├── views.py # 视图函数和业务逻辑
|
||||
├── urls.py # URL路由配置
|
||||
├── tests.py # 单元测试
|
||||
├── consumers.py # WebSocket消费者
|
||||
├── routing.py # WebSocket路由
|
||||
└── templates/ # 模板文件
|
||||
├── captcha_demo.html # 验证码演示页面
|
||||
├── email_demo.html # 邮件系统演示页面
|
||||
└── websocket_demo.html # WebSocket演示页面
|
||||
```
|
||||
|
||||
## 🎯 核心功能详解
|
||||
|
||||
### 1. 验证码演示功能
|
||||
|
||||
验证码演示页面提供三种验证码类型:
|
||||
- **随机字符验证码**: 随机生成的字母数字组合
|
||||
- **数学运算验证码**: 简单的数学计算验证
|
||||
- **单词验证码**: 英文单词验证
|
||||
|
||||
**主要功能**:
|
||||
- 验证码实时生成和刷新
|
||||
- 前端Ajax验证
|
||||
- 后端表单验证
|
||||
- 验证码类型切换
|
||||
|
||||
### 2. 邮件系统演示功能
|
||||
|
||||
邮件演示页面提供多种邮件模板:
|
||||
- **欢迎邮件**: 用户注册欢迎邮件模板
|
||||
- **系统通知**: 系统消息通知模板
|
||||
- **邮箱验证**: 邮箱验证邮件模板
|
||||
- **自定义邮件**: 支持自定义主题和内容
|
||||
|
||||
**主要功能**:
|
||||
- 邮件模板实时预览
|
||||
- 邮件发送测试
|
||||
- 收件人邮箱验证
|
||||
- 发送状态反馈
|
||||
|
||||
### 3. WebSocket演示功能
|
||||
|
||||
WebSocket演示页面提供实时通信功能:
|
||||
- **连接状态管理**: 显示WebSocket连接状态
|
||||
- **消息发送接收**: 实时消息通信
|
||||
- **广播功能**: 消息广播演示
|
||||
- **错误处理**: 连接异常处理
|
||||
|
||||
## 🚀 API接口
|
||||
|
||||
### 演示页面路由
|
||||
|
||||
| 路由 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/demo/captcha/` | GET | 验证码演示页面 |
|
||||
| `/demo/email/` | GET | 邮件系统演示页面 |
|
||||
| `/demo/websocket/` | GET | WebSocket演示页面 |
|
||||
| `/websocket/test/` | GET | WebSocket测试页面 |
|
||||
|
||||
### Ajax接口
|
||||
|
||||
**验证码相关**:
|
||||
- `POST /demo/captcha/` (Ajax): 验证码刷新和验证
|
||||
- 请求体: `{"action": "refresh/verify", "captcha_id": "...", "user_input": "..."}`
|
||||
|
||||
**邮件发送**:
|
||||
- `POST /demo/email/` (Ajax): 发送演示邮件
|
||||
- 请求体: 邮件类型、收件人邮箱、自定义内容等
|
||||
|
||||
## ⚙️ 配置参数
|
||||
|
||||
### 邮件配置(settings.py)
|
||||
```python
|
||||
# 邮件服务器配置
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'smtp.gmail.com'
|
||||
EMAIL_PORT = 587
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = 'your-email@gmail.com'
|
||||
EMAIL_HOST_PASSWORD = 'your-app-password'
|
||||
DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'
|
||||
```
|
||||
|
||||
### WebSocket配置
|
||||
```python
|
||||
# ASGI配置
|
||||
ASGI_APPLICATION = 'hertz_server_django.asgi.application'
|
||||
|
||||
# Channel layers配置
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
'hosts': [('127.0.0.1', 6379)],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ 快速开始
|
||||
|
||||
### 1. 访问演示页面
|
||||
|
||||
启动开发服务器后,访问以下URL:
|
||||
|
||||
```bash
|
||||
# 验证码演示
|
||||
http://localhost:8000/demo/captcha/
|
||||
|
||||
# 邮件系统演示
|
||||
http://localhost:8000/demo/email/
|
||||
|
||||
# WebSocket演示
|
||||
http://localhost:8000/demo/websocket/
|
||||
```
|
||||
|
||||
### 2. 测试验证码功能
|
||||
|
||||
1. 打开验证码演示页面
|
||||
2. 选择验证码类型
|
||||
3. 点击验证码图片可刷新
|
||||
4. 输入验证码进行验证
|
||||
5. 观察验证结果反馈
|
||||
|
||||
### 3. 测试邮件功能
|
||||
|
||||
1. 打开邮件演示页面
|
||||
2. 选择邮件模板类型
|
||||
3. 输入收件人邮箱
|
||||
4. 点击发送测试邮件
|
||||
5. 查看发送状态
|
||||
|
||||
### 4. 测试WebSocket功能
|
||||
|
||||
1. 打开WebSocket演示页面
|
||||
2. 点击"连接"按钮建立连接
|
||||
3. 在输入框中发送消息
|
||||
4. 观察消息接收和广播
|
||||
5. 测试断开重连功能
|
||||
|
||||
## 🔧 高级用法
|
||||
|
||||
### 自定义邮件模板
|
||||
|
||||
在 `views.py` 中的 `generate_email_content` 函数中添加新的邮件模板:
|
||||
|
||||
```python
|
||||
def generate_email_content(email_type, recipient_name, custom_subject='', custom_message=''):
|
||||
email_templates = {
|
||||
'your_template': {
|
||||
'subject': '您的邮件主题',
|
||||
'html_template': '''
|
||||
<html>
|
||||
<!-- 您的HTML模板内容 -->
|
||||
</html>
|
||||
'''
|
||||
}
|
||||
}
|
||||
# ...
|
||||
```
|
||||
|
||||
### 扩展验证码类型
|
||||
|
||||
在验证码演示中扩展新的验证码类型:
|
||||
|
||||
```python
|
||||
# 在 captcha_demo 函数中添加新的验证码类型
|
||||
captcha_types = {
|
||||
'random_char': '随机字符验证码',
|
||||
'math': '数学运算验证码',
|
||||
'word': '单词验证码',
|
||||
'new_type': '您的新验证码类型' # 新增类型
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket消息处理
|
||||
|
||||
在 `consumers.py` 中扩展WebSocket消息处理逻辑:
|
||||
|
||||
```python
|
||||
class DemoConsumer(WebsocketConsumer):
|
||||
async def receive(self, text_data):
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get('type')
|
||||
|
||||
if message_type == 'custom_message':
|
||||
# 处理自定义消息类型
|
||||
await self.handle_custom_message(data)
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 运行单元测试
|
||||
|
||||
```bash
|
||||
python manage.py test hertz_demo
|
||||
```
|
||||
|
||||
### 测试覆盖范围
|
||||
|
||||
- 验证码功能测试
|
||||
- 邮件发送测试
|
||||
- WebSocket连接测试
|
||||
- 页面渲染测试
|
||||
|
||||
## 🔒 安全考虑
|
||||
|
||||
### 验证码安全
|
||||
- 验证码有效期限制
|
||||
- 验证次数限制
|
||||
- 防止暴力破解
|
||||
|
||||
### 邮件安全
|
||||
- 收件人邮箱验证
|
||||
- 发送频率限制
|
||||
- 防止邮件滥用
|
||||
|
||||
### WebSocket安全
|
||||
- 连接认证
|
||||
- 消息内容过滤
|
||||
- 防止DDoS攻击
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 邮件发送失败怎么办?
|
||||
A: 检查邮件服务器配置,确保SMTP设置正确,邮箱密码为应用专用密码。
|
||||
|
||||
### Q: WebSocket连接失败怎么办?
|
||||
A: 检查Redis服务是否运行,确保CHANNEL_LAYERS配置正确。
|
||||
|
||||
### Q: 验证码验证总是失败?
|
||||
A: 检查验证码存储后端(Redis)是否正常运行。
|
||||
|
||||
### Q: 如何添加新的演示功能?
|
||||
A: 在views.py中添加新的视图函数,在urls.py中配置路由,在templates中添加模板文件。
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 初始版本发布
|
||||
- 包含验证码、邮件、WebSocket演示功能
|
||||
- 提供完整的示例代码和文档
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [🏠 返回主项目](../README.md) - Hertz Server Django 主项目文档
|
||||
- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制
|
||||
- [🛠️ 工具类模块](../hertz_studio_django_utils/README.md) - 加密、邮件和验证工具
|
||||
- [📋 代码风格指南](../docs/CODING_STYLE.md) - 开发规范和最佳实践
|
||||
|
||||
---
|
||||
|
||||
💡 **提示**: 此模块主要用于功能演示和学习参考,生产环境请根据实际需求进行适当调整和优化。
|
||||
0
hertz_demo/__init__.py
Normal file
0
hertz_demo/__init__.py
Normal file
6
hertz_demo/apps.py
Normal file
6
hertz_demo/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DemoConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'hertz_demo'
|
||||
120
hertz_demo/consumers.py
Normal file
120
hertz_demo/consumers.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
|
||||
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
self.room_name = self.scope['url_route']['kwargs']['room_name']
|
||||
self.room_group_name = f'chat_{self.room_name}'
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Receive message from WebSocket
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message_type = text_data_json.get('type', 'chat_message')
|
||||
message = text_data_json.get('message', '')
|
||||
username = text_data_json.get('username', 'Anonymous')
|
||||
|
||||
# Send message to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': message_type,
|
||||
'message': message,
|
||||
'username': username,
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
async def chat_message(self, event):
|
||||
message = event['message']
|
||||
username = event['username']
|
||||
timestamp = event['timestamp']
|
||||
|
||||
# Send message to WebSocket
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'chat_message',
|
||||
'message': message,
|
||||
'username': username,
|
||||
'timestamp': timestamp
|
||||
}))
|
||||
|
||||
async def user_join(self, event):
|
||||
username = event['username']
|
||||
timestamp = event['timestamp']
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'user_notification',
|
||||
'message': f'{username} 加入了聊天室',
|
||||
'username': username,
|
||||
'timestamp': timestamp
|
||||
}))
|
||||
|
||||
async def user_leave(self, event):
|
||||
username = event['username']
|
||||
timestamp = event['timestamp']
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'user_notification',
|
||||
'message': f'{username} 离开了聊天室',
|
||||
'username': username,
|
||||
'timestamp': timestamp
|
||||
}))
|
||||
|
||||
|
||||
class EchoConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
pass
|
||||
|
||||
async def receive(self, text_data):
|
||||
try:
|
||||
# 解析接收到的JSON数据
|
||||
data = json.loads(text_data)
|
||||
message = data.get('message', '').strip()
|
||||
|
||||
if message:
|
||||
# 返回回声消息
|
||||
response = {
|
||||
'type': 'echo_message',
|
||||
'original_message': message,
|
||||
'echo_message': f'回声: {message}',
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
else:
|
||||
# 如果消息为空
|
||||
response = {
|
||||
'type': 'error',
|
||||
'message': '消息不能为空',
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
|
||||
await self.send(text_data=json.dumps(response, ensure_ascii=False))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# JSON解析错误
|
||||
error_response = {
|
||||
'type': 'error',
|
||||
'message': '无效的JSON格式',
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
await self.send(text_data=json.dumps(error_response, ensure_ascii=False))
|
||||
0
hertz_demo/migrations/__init__.py
Normal file
0
hertz_demo/migrations/__init__.py
Normal file
3
hertz_demo/models.py
Normal file
3
hertz_demo/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
7
hertz_demo/routing.py
Normal file
7
hertz_demo/routing.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
re_path(r'ws/echo/$', consumers.EchoConsumer.as_asgi()),
|
||||
]
|
||||
499
hertz_demo/templates/captcha_demo.html
Normal file
499
hertz_demo/templates/captcha_demo.html
Normal file
@@ -0,0 +1,499 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hertz验证码演示 - Hertz Server Django</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.captcha-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.captcha-image:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.info-box h3 {
|
||||
color: #667eea;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin-left: 1.5rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-box li {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.info-box code {
|
||||
background: #e9ecef;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.captcha-types {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #28a745;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.type-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
border: 2px solid #dee2e6;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
background: #dee2e6;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.demo-section h4 {
|
||||
color: #856404;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
color: #856404;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.success-message {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔐 Hertz验证码演示</h1>
|
||||
<p>{{ demo_description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- 验证码功能介绍 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>🎯 Hertz验证码功能特性</h3>
|
||||
<p>自定义验证码系统,支持Redis缓存</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="info-box">
|
||||
<ul>
|
||||
<li>🔤 随机字符验证码 - 生成随机字母数字组合</li>
|
||||
<li>🎨 自定义样式配置 - 支持颜色、字体、噪声等设置</li>
|
||||
<li>⚡ Ajax刷新功能 - 无需刷新页面</li>
|
||||
<li>💾 Redis缓存 - 高性能数据存储</li>
|
||||
<li>⏰ 超时自动失效 - 可配置过期时间</li>
|
||||
<li>🔧 灵活配置 - 通过settings.py进行配置</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="captcha-types">
|
||||
<h3 style="margin-bottom: 1rem; color: #667eea;">配置信息</h3>
|
||||
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 验证码长度: 可通过HERTZ_CAPTCHA_LENGTH配置</p>
|
||||
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 图片尺寸: 可通过HERTZ_CAPTCHA_WIDTH/HEIGHT配置</p>
|
||||
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 过期时间: 可通过HERTZ_CAPTCHA_TIMEOUT配置</p>
|
||||
<p style="color: #666; font-size: 0.9rem;">• Redis前缀: 可通过HERTZ_CAPTCHA_REDIS_KEY_PREFIX配置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码测试 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>🔒 Hertz验证码测试</h3>
|
||||
<p>输入验证码进行功能测试</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="success-message" style="{% if message.tags == 'error' %}background: #f8d7da; color: #721c24; border-color: #f5c6cb;{% endif %}">
|
||||
{% if message.tags == 'success' %}✅{% elif message.tags == 'error' %}❌{% endif %} {{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="demo-section">
|
||||
<h4>📋 Hertz验证码说明</h4>
|
||||
<p>• 随机字符验证码:生成随机字母和数字组合</p>
|
||||
<p>• 特点:自定义样式,支持噪声干扰,Redis缓存存储</p>
|
||||
<p>• 功能:支持Ajax刷新,自动过期失效</p>
|
||||
</div>
|
||||
|
||||
<form method="post" id="captcha-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="captcha_input">验证码:</label>
|
||||
<div class="captcha-container">
|
||||
<img id="captcha-image" src="{{ initial_captcha.image_data }}"
|
||||
alt="验证码" class="captcha-image" onclick="refreshCaptcha()"
|
||||
style="cursor: pointer; border: 2px solid #e1e5e9; border-radius: 8px;">
|
||||
<button type="button" class="refresh-btn" onclick="refreshCaptcha()">🔄 刷新</button>
|
||||
</div>
|
||||
<input type="text" id="captcha_input" name="captcha_input"
|
||||
placeholder="请输入验证码" required
|
||||
style="width: 100%; padding: 0.8rem; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 1rem; margin-top: 0.5rem;">
|
||||
<input type="hidden" id="captcha_id" name="captcha_id" value="{{ initial_captcha.captcha_id }}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">验证提交</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4 style="color: #007bff; margin-bottom: 0.5rem;">💡 使用提示</h4>
|
||||
<p style="color: #004085; margin-bottom: 0.3rem;">• 点击验证码图片可以刷新</p>
|
||||
<p style="color: #004085; margin-bottom: 0.3rem;">• 验证码不区分大小写</p>
|
||||
<p style="color: #004085;">• 验证码有效期为5分钟</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/" style="color: white; text-decoration: none; font-weight: 500; font-size: 1.1rem;">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentCaptchaId = '{{ initial_captcha.captcha_id }}';
|
||||
|
||||
function refreshCaptcha() {
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'refresh',
|
||||
captcha_id: currentCaptchaId
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('captcha-image').src = data.data.image_data;
|
||||
document.getElementById('captcha_id').value = data.data.captcha_id;
|
||||
document.getElementById('captcha_input').value = '';
|
||||
currentCaptchaId = data.data.captcha_id;
|
||||
} else {
|
||||
console.error('刷新验证码失败:', data.error);
|
||||
alert('刷新验证码失败,请重试');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('刷新验证码失败:', error);
|
||||
alert('刷新验证码失败,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交处理
|
||||
const captchaForm = document.getElementById('captcha-form');
|
||||
if (captchaForm) {
|
||||
captchaForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const captchaId = document.getElementById('captcha_id').value;
|
||||
const userInput = document.getElementById('captcha_input').value;
|
||||
|
||||
if (!userInput.trim()) {
|
||||
alert('请输入验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'verify',
|
||||
captcha_id: captchaId,
|
||||
user_input: userInput
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.valid) {
|
||||
alert('✅ ' + data.message);
|
||||
document.getElementById('captcha_input').value = '';
|
||||
refreshCaptcha();
|
||||
} else {
|
||||
alert('❌ ' + data.message);
|
||||
document.getElementById('captcha_input').value = '';
|
||||
refreshCaptcha();
|
||||
}
|
||||
} else {
|
||||
alert('❌ 验证失败: ' + (data.error || '未知错误'));
|
||||
refreshCaptcha();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('提交失败:', error);
|
||||
alert('❌ 提交失败,请重试');
|
||||
refreshCaptcha();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 回车键提交
|
||||
document.getElementById('captcha_input').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
captchaForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
520
hertz_demo/templates/email_demo.html
Normal file
520
hertz_demo/templates/email_demo.html
Normal file
@@ -0,0 +1,520 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邮件系统演示 - Hertz Server Django</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.demo-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.demo-card h3 {
|
||||
color: #667eea;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-card p {
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.email-types {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 2px solid #667eea;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.type-btn.active,
|
||||
.type-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.email-preview {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.email-preview h4 {
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.email-preview .preview-content {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 3px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.email-types {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="back-link">
|
||||
<a href="/">← 返回首页</a>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>📧 邮件系统演示</h1>
|
||||
<p>体验Django邮件发送功能,支持多种邮件类型和模板</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-card">
|
||||
<h3>📮 邮件发送功能</h3>
|
||||
<p>本演示展示了Django邮件系统的核心功能:</p>
|
||||
<ul style="color: #666; margin-left: 1.5rem; margin-bottom: 1rem;">
|
||||
<li>支持HTML和纯文本邮件</li>
|
||||
<li>多种邮件模板类型</li>
|
||||
<li>SMTP配置和发送状态</li>
|
||||
<li>邮件预览和验证</li>
|
||||
</ul>
|
||||
|
||||
<div class="email-types">
|
||||
<button class="type-btn active" data-type="welcome">欢迎邮件</button>
|
||||
<button class="type-btn" data-type="notification">通知邮件</button>
|
||||
<button class="type-btn" data-type="verification">验证邮件</button>
|
||||
<button class="type-btn" data-type="custom">自定义邮件</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3>✉️ 发送邮件测试</h3>
|
||||
|
||||
<div id="message-area"></div>
|
||||
|
||||
<form id="email-form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="email-type" name="email_type" value="welcome">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="recipient_email">收件人邮箱 *</label>
|
||||
<input type="email" id="recipient_email" name="recipient_email" required
|
||||
placeholder="请输入收件人邮箱地址">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="subject-group">
|
||||
<label for="subject">邮件主题</label>
|
||||
<input type="text" id="subject" name="subject"
|
||||
placeholder="邮件主题将根据类型自动生成">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="message-group" style="display: none;">
|
||||
<label for="message">邮件内容</label>
|
||||
<textarea id="message" name="message"
|
||||
placeholder="请输入自定义邮件内容"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="recipient_name">收件人姓名</label>
|
||||
<input type="text" id="recipient_name" name="recipient_name"
|
||||
placeholder="收件人姓名(可选)">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="send-btn">
|
||||
📤 发送邮件
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" id="preview-btn">
|
||||
👁️ 预览邮件
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在发送邮件,请稍候...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" id="preview-card" style="display: none;">
|
||||
<h3>📋 邮件预览</h3>
|
||||
<div class="email-preview">
|
||||
<h4>邮件内容预览:</h4>
|
||||
<div class="preview-content" id="preview-content">
|
||||
<!-- 预览内容将在这里显示 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 邮件类型切换
|
||||
document.querySelectorAll('.type-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// 移除所有active类
|
||||
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||||
// 添加active类到当前按钮
|
||||
this.classList.add('active');
|
||||
|
||||
const type = this.dataset.type;
|
||||
document.getElementById('email-type').value = type;
|
||||
|
||||
// 根据类型显示/隐藏字段
|
||||
const subjectGroup = document.getElementById('subject-group');
|
||||
const messageGroup = document.getElementById('message-group');
|
||||
const subjectInput = document.getElementById('subject');
|
||||
|
||||
if (type === 'custom') {
|
||||
messageGroup.style.display = 'block';
|
||||
subjectInput.placeholder = '请输入邮件主题';
|
||||
subjectInput.required = true;
|
||||
} else {
|
||||
messageGroup.style.display = 'none';
|
||||
subjectInput.placeholder = '邮件主题将根据类型自动生成';
|
||||
subjectInput.required = false;
|
||||
}
|
||||
|
||||
// 更新预览
|
||||
updatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
document.getElementById('email-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const loading = document.getElementById('loading');
|
||||
const messageArea = document.getElementById('message-area');
|
||||
|
||||
// 显示加载状态
|
||||
sendBtn.disabled = true;
|
||||
loading.classList.add('show');
|
||||
messageArea.innerHTML = '';
|
||||
|
||||
fetch('/demo/email/', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loading.classList.remove('show');
|
||||
sendBtn.disabled = false;
|
||||
|
||||
if (data.success) {
|
||||
messageArea.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
✅ ${data.message}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
messageArea.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ ${data.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.classList.remove('show');
|
||||
sendBtn.disabled = false;
|
||||
messageArea.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ 发送失败:网络错误
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// 预览功能
|
||||
document.getElementById('preview-btn').addEventListener('click', function() {
|
||||
updatePreview();
|
||||
const previewCard = document.getElementById('preview-card');
|
||||
previewCard.style.display = previewCard.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
function updatePreview() {
|
||||
const type = document.getElementById('email-type').value;
|
||||
const recipientName = document.getElementById('recipient_name').value || '用户';
|
||||
const subject = document.getElementById('subject').value;
|
||||
const message = document.getElementById('message').value;
|
||||
|
||||
let previewContent = '';
|
||||
|
||||
switch(type) {
|
||||
case 'welcome':
|
||||
previewContent = `
|
||||
<h4>🎉 欢迎加入我们!</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<p>欢迎您注册成为我们的用户!我们很高兴您能加入我们的大家庭。</p>
|
||||
<p>在这里,您可以享受到优质的服务和丰富的功能。如果您有任何问题,请随时联系我们。</p>
|
||||
<p>祝您使用愉快!</p>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
case 'notification':
|
||||
previewContent = `
|
||||
<h4>🔔 系统通知</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<p>您有一条新的系统通知:</p>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-left: 4px solid #007bff; margin: 1rem 0;">
|
||||
<p>您的账户设置已更新,如果这不是您的操作,请立即联系我们。</p>
|
||||
</div>
|
||||
<p>如有疑问,请联系客服。</p>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
case 'verification':
|
||||
previewContent = `
|
||||
<h4>🔐 邮箱验证</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<p>请点击下面的链接验证您的邮箱地址:</p>
|
||||
<div style="text-align: center; margin: 2rem 0;">
|
||||
<a href="#" style="background: #667eea; color: white; padding: 1rem 2rem; text-decoration: none; border-radius: 5px;">验证邮箱</a>
|
||||
</div>
|
||||
<p>如果您没有注册账户,请忽略此邮件。</p>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
case 'custom':
|
||||
previewContent = `
|
||||
<h4>${subject || '自定义邮件'}</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<div style="margin: 1rem 0;">
|
||||
${message ? message.replace(/\n/g, '<br>') : '请输入邮件内容'}
|
||||
</div>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
document.getElementById('preview-content').innerHTML = previewContent;
|
||||
}
|
||||
|
||||
// 初始化预览
|
||||
updatePreview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
556
hertz_demo/templates/websocket_demo.html
Normal file
556
hertz_demo/templates/websocket_demo.html
Normal file
@@ -0,0 +1,556 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket演示 - Hertz Server Django</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #f9f9f9;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.message-system {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.message-echo {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.message-notification {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #da190b;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-controls {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-controls input {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🌐 WebSocket演示</h1>
|
||||
<p>实时通信功能展示 - 支持聊天室和回声测试</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- 回声测试 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>🔄 回声测试</h3>
|
||||
<p>发送消息,服务器会回声返回</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="connection-status status-disconnected" id="echo-status">未连接</div>
|
||||
<div class="message-area" id="echo-messages"></div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="input-field" id="echo-input" placeholder="输入消息..." disabled>
|
||||
<button class="btn btn-primary" id="echo-send" disabled>发送</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-success" id="echo-connect">连接</button>
|
||||
<button class="btn btn-danger" id="echo-disconnect" disabled>断开</button>
|
||||
<button class="btn btn-primary" id="echo-clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天室 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>💬 聊天室</h3>
|
||||
<p>多用户实时聊天功能</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="connection-status status-disconnected" id="chat-status">未连接</div>
|
||||
<div class="chat-controls">
|
||||
<input type="text" class="input-field" id="username" placeholder="用户名" value="用户" style="width: 120px;">
|
||||
<input type="text" class="input-field" id="room-name" placeholder="房间名" value="general" style="width: 120px;">
|
||||
<button class="btn btn-success" id="chat-connect">加入聊天室</button>
|
||||
<button class="btn btn-danger" id="chat-disconnect" disabled>离开聊天室</button>
|
||||
</div>
|
||||
<div class="message-area" id="chat-messages"></div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="input-field" id="chat-input" placeholder="输入消息..." disabled>
|
||||
<button class="btn btn-primary" id="chat-send" disabled>发送</button>
|
||||
<button class="btn btn-primary" id="chat-clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// WebSocket连接管理
|
||||
let echoSocket = null;
|
||||
let chatSocket = null;
|
||||
|
||||
// DOM元素
|
||||
const echoStatus = document.getElementById('echo-status');
|
||||
const echoMessages = document.getElementById('echo-messages');
|
||||
const echoInput = document.getElementById('echo-input');
|
||||
const echoSendBtn = document.getElementById('echo-send');
|
||||
const echoConnectBtn = document.getElementById('echo-connect');
|
||||
const echoDisconnectBtn = document.getElementById('echo-disconnect');
|
||||
const echoClearBtn = document.getElementById('echo-clear');
|
||||
|
||||
const chatStatus = document.getElementById('chat-status');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatSendBtn = document.getElementById('chat-send');
|
||||
const chatConnectBtn = document.getElementById('chat-connect');
|
||||
const chatDisconnectBtn = document.getElementById('chat-disconnect');
|
||||
const chatClearBtn = document.getElementById('chat-clear');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const roomNameInput = document.getElementById('room-name');
|
||||
|
||||
// 工具函数
|
||||
function addMessage(container, message, type = 'system') {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message message-${type}`;
|
||||
messageDiv.innerHTML = `<strong>[${message.timestamp}]</strong> ${message.message}`;
|
||||
container.appendChild(messageDiv);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(statusElement, status, text) {
|
||||
statusElement.className = `connection-status status-${status}`;
|
||||
statusElement.textContent = text;
|
||||
}
|
||||
|
||||
// 回声测试WebSocket
|
||||
function connectEcho() {
|
||||
updateStatus(echoStatus, 'connecting', '连接中...');
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/echo/`;
|
||||
|
||||
echoSocket = new WebSocket(wsUrl);
|
||||
|
||||
echoSocket.onopen = function(e) {
|
||||
updateStatus(echoStatus, 'connected', '已连接');
|
||||
echoInput.disabled = false;
|
||||
echoSendBtn.disabled = false;
|
||||
echoConnectBtn.disabled = true;
|
||||
echoDisconnectBtn.disabled = false;
|
||||
};
|
||||
|
||||
echoSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('收到WebSocket消息:', data); // 调试信息
|
||||
let messageType = 'system';
|
||||
let displayMessage = data;
|
||||
|
||||
if (data.type === 'echo_message') {
|
||||
messageType = 'echo';
|
||||
// 解码Unicode字符并创建显示用的消息对象
|
||||
let echoText = data.echo_message;
|
||||
if (typeof echoText === 'string') {
|
||||
// 处理可能的Unicode编码
|
||||
try {
|
||||
echoText = decodeURIComponent(escape(echoText));
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始文本
|
||||
console.log('Unicode解码失败,使用原始文本');
|
||||
}
|
||||
}
|
||||
displayMessage = {
|
||||
message: echoText || data.echo_message || '回声消息',
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
};
|
||||
console.log('处理回声消息:', displayMessage); // 调试信息
|
||||
} else if (data.type === 'error') {
|
||||
messageType = 'error';
|
||||
displayMessage = {
|
||||
message: data.message || '发生错误',
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
};
|
||||
} else {
|
||||
// 处理其他类型的消息
|
||||
displayMessage = {
|
||||
message: data.message || JSON.stringify(data),
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
};
|
||||
}
|
||||
|
||||
addMessage(echoMessages, displayMessage, messageType);
|
||||
};
|
||||
|
||||
echoSocket.onclose = function(e) {
|
||||
updateStatus(echoStatus, 'disconnected', '已断开');
|
||||
echoInput.disabled = true;
|
||||
echoSendBtn.disabled = true;
|
||||
echoConnectBtn.disabled = false;
|
||||
echoDisconnectBtn.disabled = true;
|
||||
};
|
||||
|
||||
echoSocket.onerror = function(e) {
|
||||
updateStatus(echoStatus, 'disconnected', '连接错误');
|
||||
addMessage(echoMessages, {
|
||||
message: 'WebSocket连接发生错误',
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
}, 'error');
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectEcho() {
|
||||
if (echoSocket) {
|
||||
echoSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
function sendEchoMessage() {
|
||||
const message = echoInput.value.trim();
|
||||
if (message && echoSocket && echoSocket.readyState === WebSocket.OPEN) {
|
||||
echoSocket.send(JSON.stringify({
|
||||
'message': message
|
||||
}));
|
||||
echoInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天室WebSocket
|
||||
function connectChat() {
|
||||
const username = usernameInput.value.trim() || '匿名用户';
|
||||
const roomName = roomNameInput.value.trim() || 'general';
|
||||
|
||||
updateStatus(chatStatus, 'connecting', '连接中...');
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/chat/${roomName}/`;
|
||||
|
||||
chatSocket = new WebSocket(wsUrl);
|
||||
|
||||
chatSocket.onopen = function(e) {
|
||||
updateStatus(chatStatus, 'connected', `已连接到房间: ${roomName}`);
|
||||
chatInput.disabled = false;
|
||||
chatSendBtn.disabled = false;
|
||||
chatConnectBtn.disabled = true;
|
||||
chatDisconnectBtn.disabled = false;
|
||||
usernameInput.disabled = true;
|
||||
roomNameInput.disabled = true;
|
||||
|
||||
// 发送加入通知
|
||||
chatSocket.send(JSON.stringify({
|
||||
'type': 'user_join',
|
||||
'username': username
|
||||
}));
|
||||
};
|
||||
|
||||
chatSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
let messageType = 'system';
|
||||
|
||||
if (data.type === 'chat_message') {
|
||||
messageType = 'user';
|
||||
data.message = `${data.username}: ${data.message}`;
|
||||
} else if (data.type === 'user_notification') {
|
||||
messageType = 'notification';
|
||||
} else if (data.type === 'error') {
|
||||
messageType = 'error';
|
||||
}
|
||||
|
||||
addMessage(chatMessages, data, messageType);
|
||||
};
|
||||
|
||||
chatSocket.onclose = function(e) {
|
||||
updateStatus(chatStatus, 'disconnected', '已断开');
|
||||
chatInput.disabled = true;
|
||||
chatSendBtn.disabled = true;
|
||||
chatConnectBtn.disabled = false;
|
||||
chatDisconnectBtn.disabled = true;
|
||||
usernameInput.disabled = false;
|
||||
roomNameInput.disabled = false;
|
||||
};
|
||||
|
||||
chatSocket.onerror = function(e) {
|
||||
updateStatus(chatStatus, 'disconnected', '连接错误');
|
||||
addMessage(chatMessages, {
|
||||
message: 'WebSocket连接发生错误',
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
}, 'error');
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectChat() {
|
||||
if (chatSocket) {
|
||||
const username = usernameInput.value.trim() || '匿名用户';
|
||||
|
||||
// 发送离开通知
|
||||
chatSocket.send(JSON.stringify({
|
||||
'type': 'user_leave',
|
||||
'username': username
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
chatSocket.close();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
const message = chatInput.value.trim();
|
||||
const username = usernameInput.value.trim() || '匿名用户';
|
||||
|
||||
if (message && chatSocket && chatSocket.readyState === WebSocket.OPEN) {
|
||||
chatSocket.send(JSON.stringify({
|
||||
'type': 'chat_message',
|
||||
'message': message,
|
||||
'username': username
|
||||
}));
|
||||
chatInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
echoConnectBtn.addEventListener('click', connectEcho);
|
||||
echoDisconnectBtn.addEventListener('click', disconnectEcho);
|
||||
echoSendBtn.addEventListener('click', sendEchoMessage);
|
||||
echoClearBtn.addEventListener('click', () => {
|
||||
echoMessages.innerHTML = '';
|
||||
});
|
||||
|
||||
echoInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendEchoMessage();
|
||||
}
|
||||
});
|
||||
|
||||
chatConnectBtn.addEventListener('click', connectChat);
|
||||
chatDisconnectBtn.addEventListener('click', disconnectChat);
|
||||
chatSendBtn.addEventListener('click', sendChatMessage);
|
||||
chatClearBtn.addEventListener('click', () => {
|
||||
chatMessages.innerHTML = '';
|
||||
});
|
||||
|
||||
chatInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendChatMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时断开连接
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (echoSocket) {
|
||||
echoSocket.close();
|
||||
}
|
||||
if (chatSocket) {
|
||||
disconnectChat();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
hertz_demo/tests.py
Normal file
3
hertz_demo/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
hertz_demo/urls.py
Normal file
11
hertz_demo/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'hertz_demo'
|
||||
|
||||
urlpatterns = [
|
||||
path('demo/captcha/', views.captcha_demo, name='captcha_demo'),
|
||||
path('demo/websocket/', views.websocket_demo, name='websocket_demo'),
|
||||
path('websocket/test/', views.websocket_test, name='websocket_test'),
|
||||
path('demo/email/', views.email_demo, name='email_demo'),
|
||||
]
|
||||
331
hertz_demo/views.py
Normal file
331
hertz_demo/views.py
Normal file
@@ -0,0 +1,331 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.core.mail import send_mail, EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib import messages
|
||||
from django import forms
|
||||
from hertz_studio_django_captcha.captcha_generator import HertzCaptchaGenerator
|
||||
import json
|
||||
from django.conf import settings
|
||||
import random
|
||||
import string
|
||||
|
||||
class HertzCaptchaForm(forms.Form):
|
||||
"""Hertz验证码表单"""
|
||||
captcha_input = forms.CharField(
|
||||
max_length=10,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': '请输入验证码',
|
||||
'class': 'form-control',
|
||||
'autocomplete': 'off'
|
||||
}),
|
||||
label='验证码'
|
||||
)
|
||||
captcha_id = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def captcha_demo(request):
|
||||
"""
|
||||
验证码演示页面
|
||||
展示多种验证码功能的使用方法
|
||||
"""
|
||||
# 获取请求的验证码类型
|
||||
captcha_type = request.GET.get('type', 'random_char')
|
||||
|
||||
# 初始化验证码生成器
|
||||
captcha_generator = HertzCaptchaGenerator()
|
||||
|
||||
if request.method == 'POST':
|
||||
# 检查是否是Ajax请求
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
action = data.get('action')
|
||||
|
||||
if action == 'refresh':
|
||||
# 刷新验证码
|
||||
captcha_data = captcha_generator.generate_captcha()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': captcha_data
|
||||
})
|
||||
elif action == 'verify':
|
||||
# 验证验证码
|
||||
captcha_id = data.get('captcha_id', '')
|
||||
user_input = data.get('user_input', '')
|
||||
|
||||
is_valid = captcha_generator.verify_captcha(captcha_id, user_input)
|
||||
|
||||
if is_valid:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'valid': True,
|
||||
'message': f'验证成功!验证码类型: {captcha_type}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'valid': False,
|
||||
'message': '验证码错误,请重新输入'
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': '请求数据格式错误'
|
||||
})
|
||||
else:
|
||||
# 普通表单提交处理
|
||||
form = HertzCaptchaForm(request.POST)
|
||||
username = request.POST.get('username', '')
|
||||
captcha_id = request.POST.get('captcha_id', '')
|
||||
captcha_input = request.POST.get('captcha_input', '')
|
||||
|
||||
# 验证验证码
|
||||
is_valid = captcha_generator.verify_captcha(captcha_id, captcha_input)
|
||||
|
||||
if is_valid and username:
|
||||
# 生成新的验证码用于显示
|
||||
initial_captcha = captcha_generator.generate_captcha()
|
||||
return render(request, 'captcha_demo.html', {
|
||||
'form': HertzCaptchaForm(),
|
||||
'success_message': f'验证成功!用户名: {username},验证码类型: {captcha_type}',
|
||||
'captcha_unavailable': False,
|
||||
'current_type': captcha_type,
|
||||
'initial_captcha': initial_captcha,
|
||||
'captcha_types': {
|
||||
'random_char': '随机字符验证码',
|
||||
'math': '数学运算验证码',
|
||||
'word': '单词验证码'
|
||||
}
|
||||
})
|
||||
|
||||
# GET请求或表单验证失败时,生成初始验证码
|
||||
form = HertzCaptchaForm()
|
||||
initial_captcha = captcha_generator.generate_captcha()
|
||||
|
||||
return render(request, 'captcha_demo.html', {
|
||||
'form': form,
|
||||
'captcha_unavailable': False,
|
||||
'current_type': captcha_type,
|
||||
'initial_captcha': initial_captcha,
|
||||
'captcha_types': {
|
||||
'random_char': '随机字符验证码',
|
||||
'math': '数学运算验证码',
|
||||
'word': '单词验证码'
|
||||
}
|
||||
})
|
||||
|
||||
def websocket_demo(request):
|
||||
"""WebSocket演示页面"""
|
||||
return render(request, 'websocket_demo.html')
|
||||
|
||||
def websocket_test(request):
|
||||
"""
|
||||
WebSocket简单测试页面
|
||||
"""
|
||||
return render(request, 'websocket_test.html')
|
||||
|
||||
# 测试热重启功能 - 添加注释触发文件变化
|
||||
|
||||
def email_demo(request):
|
||||
"""邮件系统演示页面"""
|
||||
if request.method == 'GET':
|
||||
return render(request, 'email_demo.html')
|
||||
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
# 获取表单数据
|
||||
email_type = request.POST.get('email_type', 'welcome')
|
||||
recipient_email = request.POST.get('recipient_email')
|
||||
recipient_name = request.POST.get('recipient_name', '用户')
|
||||
custom_subject = request.POST.get('subject', '')
|
||||
custom_message = request.POST.get('message', '')
|
||||
|
||||
if not recipient_email:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': '请输入收件人邮箱地址'
|
||||
})
|
||||
|
||||
# 根据邮件类型生成内容
|
||||
email_content = generate_email_content(email_type, recipient_name, custom_subject, custom_message)
|
||||
|
||||
# 发送邮件
|
||||
success = send_demo_email(
|
||||
recipient_email=recipient_email,
|
||||
subject=email_content['subject'],
|
||||
html_content=email_content['html_content'],
|
||||
text_content=email_content['text_content']
|
||||
)
|
||||
|
||||
if success:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'邮件已成功发送到 {recipient_email}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': '邮件发送失败,请检查邮件配置'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'发送失败:{str(e)}'
|
||||
})
|
||||
|
||||
def generate_email_content(email_type, recipient_name, custom_subject='', custom_message=''):
|
||||
"""根据邮件类型生成邮件内容"""
|
||||
|
||||
email_templates = {
|
||||
'welcome': {
|
||||
'subject': '🎉 欢迎加入 Hertz Server Django!',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 28px;">🎉 欢迎加入我们!</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>欢迎您注册成为我们的用户!我们很高兴您能加入我们的大家庭。</p>
|
||||
<p>在这里,您可以享受到:</p>
|
||||
<ul style="color: #666;">
|
||||
<li>🔐 安全的验证码系统</li>
|
||||
<li>🌐 实时WebSocket通信</li>
|
||||
<li>📧 完善的邮件服务</li>
|
||||
<li>📚 详细的API文档</li>
|
||||
</ul>
|
||||
<p>如果您有任何问题,请随时联系我们。</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="http://127.0.0.1:8000/" style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">开始使用</a>
|
||||
</div>
|
||||
<p>祝您使用愉快!</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
},
|
||||
'notification': {
|
||||
'subject': '🔔 系统通知 - Hertz Server Django',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: #007bff; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">🔔 系统通知</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>您有一条新的系统通知:</p>
|
||||
<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #007bff; margin: 20px 0;">
|
||||
<p style="margin: 0; font-weight: 500;">您的账户设置已更新,如果这不是您的操作,请立即联系我们。</p>
|
||||
</div>
|
||||
<p>系统会持续为您提供安全保障,如有疑问请联系客服。</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="http://127.0.0.1:8000/" style="background: #007bff; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">查看详情</a>
|
||||
</div>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
},
|
||||
'verification': {
|
||||
'subject': '🔐 邮箱验证 - Hertz Server Django',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: #28a745; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">🔐 邮箱验证</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>感谢您注册 Hertz Server Django!请点击下面的按钮验证您的邮箱地址:</p>
|
||||
<div style="text-align: center; margin: 40px 0;">
|
||||
<a href="http://127.0.0.1:8000/verify?token=demo_token" style="background: #28a745; color: white; padding: 15px 40px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 16px; font-weight: 500;">验证邮箱地址</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">如果按钮无法点击,请复制以下链接到浏览器:<br>
|
||||
<code style="background: #f8f9fa; padding: 5px; border-radius: 3px;">http://127.0.0.1:8000/verify?token=demo_token</code></p>
|
||||
<p style="color: #666;">如果您没有注册账户,请忽略此邮件。此验证链接将在24小时后失效。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
},
|
||||
'custom': {
|
||||
'subject': custom_subject or '自定义邮件 - Hertz Server Django',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">{custom_subject or '自定义邮件'}</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<div style="margin: 20px 0; font-size: 16px;">
|
||||
{custom_message.replace(chr(10), '<br>') if custom_message else '这是一封自定义邮件。'}
|
||||
</div>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
template = email_templates.get(email_type, email_templates['welcome'])
|
||||
html_content = template['html_template']
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
return {
|
||||
'subject': template['subject'],
|
||||
'html_content': html_content,
|
||||
'text_content': text_content
|
||||
}
|
||||
|
||||
def send_demo_email(recipient_email, subject, html_content, text_content):
|
||||
"""发送演示邮件"""
|
||||
try:
|
||||
# 检查邮件配置
|
||||
if not settings.EMAIL_HOST_USER or not settings.EMAIL_HOST_PASSWORD:
|
||||
print("邮件配置不完整,使用控制台输出模式")
|
||||
print(f"收件人: {recipient_email}")
|
||||
print(f"主题: {subject}")
|
||||
print(f"内容: {text_content[:200]}...")
|
||||
return True
|
||||
|
||||
# 创建邮件
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[recipient_email]
|
||||
)
|
||||
|
||||
# 添加HTML内容
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
# 发送邮件
|
||||
email.send()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败: {str(e)}")
|
||||
return False
|
||||
Reference in New Issue
Block a user