【悟凡】真正意义上的净土重生:只保留核心逻辑

This commit is contained in:
2026-04-04 11:19:01 +08:00
commit 6f127936c1
47 changed files with 20847 additions and 0 deletions

12
backend/.env.example Normal file
View File

@@ -0,0 +1,12 @@
# Django 配置
DEBUG=True
SECRET_KEY=your-secret-key-here-change-in-production
ALLOWED_HOSTS=localhost,127.0.0.1
# 数据库配置
DATABASE_URL=sqlite:///db.sqlite3
# 生产环境使用:
# DATABASE_URL=postgresql://user:pass@localhost:5432/meeting_room
# API 配置
API_BASE_URL=http://localhost:8000

21
backend/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.12-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制代码
COPY . .
# 暴露端口
EXPOSE 8000
CMD ["gunicorn", "meeting_room.wsgi:application", "--bind", "0.0.0.0:8000"]

0
backend/api/__init__.py Normal file
View File

3
backend/api/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/api/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"

View File

3
backend/api/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
backend/api/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
backend/api/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
龙虾议事厅 - 自然语言命令解析器
让 AI 理解人类的自然语言命令
"""
import re
from typing import Optional, Dict, Any
from dataclasses import dataclass
from enum import Enum
class CommandType(Enum):
"""命令类型"""
CREATE_MEETING = "create_meeting"
SEND_MESSAGE = "send_message"
LIST_MEETINGS = "list_meetings"
JOIN_MEETING = "join_meeting"
GET_MESSAGES = "get_messages"
UNKNOWN = "unknown"
@dataclass
class ParsedCommand:
"""解析后的命令"""
type: CommandType
params: Dict[str, Any]
confidence: float # 置信度 0-1
class CommandInterpreter:
"""
自然语言命令解析器
支持的命令:
- "创建一个会议,主题是 XXX"
- "发送消息XXX"
- "查看会议列表"
- "加入会议,邀请码 XXX"
- "查看消息"
"""
def __init__(self):
# 命令模式匹配(简化版)
self.patterns = {
CommandType.CREATE_MEETING: [
r'创建.*会议',
r'新建.*会议',
r'开.*会',
r'组织.*会议',
],
CommandType.SEND_MESSAGE: [
r'发送.*消息',
r'发消息',
r'说.*',
r'告诉.*',
],
CommandType.LIST_MEETINGS: [
r'查看.*列表',
r'显示.*会议',
r'有.*会议',
r'我的会议',
],
CommandType.JOIN_MEETING: [
r'加入.*',
r'进入.*',
r'参加会议',
],
CommandType.GET_MESSAGES: [
r'查看.*消息',
r'显示.*消息',
r'有.*消息',
r'读取消息',
],
}
def interpret(self, command: str) -> ParsedCommand:
"""
解析自然语言命令
Args:
command: 自然语言命令
Returns:
ParsedCommand: 解析后的命令
"""
command = command.strip() # 不要转小写,保留中文
# 尝试匹配各种命令模式
for cmd_type, patterns in self.patterns.items():
for pattern in patterns:
if re.search(pattern, command, re.IGNORECASE):
params = self._extract_params(cmd_type, command)
return ParsedCommand(
type=cmd_type,
params=params,
confidence=0.8
)
# 未匹配到任何模式
return ParsedCommand(
type=CommandType.UNKNOWN,
params={'raw': command},
confidence=0.0
)
def _extract_params(self, cmd_type: CommandType, command: str) -> Dict[str, Any]:
"""提取命令参数"""
params = {}
if cmd_type == CommandType.CREATE_MEETING:
# 提取会议主题
topic_match = re.search(r'主题是 (.+?)(?:|,|。|$)', command)
if topic_match:
params['topic'] = topic_match.group(1).strip()
else:
# 尝试其他模式
topic_match = re.search(r'创建.*?会议 (?:|,)?(.+?)(?:。|$)', command)
if topic_match:
params['topic'] = topic_match.group(1).strip()
else:
params['topic'] = '新会议'
elif cmd_type == CommandType.SEND_MESSAGE:
# 提取消息内容
content_match = re.search(r'(?::||说 | 告诉)(.+?)(?:。|$)', command)
if content_match:
params['content'] = content_match.group(1).strip()
else:
params['content'] = command
elif cmd_type == CommandType.JOIN_MEETING:
# 提取邀请码
code_match = re.search(r'(?:邀请码 | 码 |code)[:\s]?([A-Z0-9]+)', command, re.IGNORECASE)
if code_match:
params['invite_code'] = code_match.group(1).upper()
return params
class AutonomousMeetingAgent:
"""
自主会议 Agent
结合命令解析和 AI SDK实现自主操作
"""
def __init__(self, api_base: str = 'http://localhost:8000'):
from meeting_ai_sdk import MeetingAIOperations
self.api = MeetingAIOperations(api_base)
self.interpreter = CommandInterpreter()
self.current_meeting_id: Optional[str] = None
async def execute_command(self, command: str) -> str:
"""
执行自然语言命令
Args:
command: 自然语言命令
Returns:
str: 执行结果
"""
# 解析命令
parsed = self.interpreter.interpret(command)
if parsed.type == CommandType.UNKNOWN:
return f"❌ 无法理解命令:{command}"
# 执行对应操作
if parsed.type == CommandType.CREATE_MEETING:
return await self._create_meeting(parsed.params)
elif parsed.type == CommandType.SEND_MESSAGE:
return await self._send_message(parsed.params)
elif parsed.type == CommandType.LIST_MEETINGS:
return await self._list_meetings()
elif parsed.type == CommandType.JOIN_MEETING:
return await self._join_meeting(parsed.params)
elif parsed.type == CommandType.GET_MESSAGES:
return await self._get_messages()
return "❌ 未知命令"
async def _create_meeting(self, params: Dict) -> str:
"""创建会议"""
topic = params.get('topic', '新会议')
try:
await self.api.login('test', 'test123')
meeting = await self.api.create_meeting(topic)
self.current_meeting_id = meeting.id
return (
f"✅ 会议已创建!\n"
f" 主题:{meeting.topic}\n"
f" ID: {meeting.id}\n"
f" 邀请码:{meeting.invite_code}"
)
except Exception as e:
return f"❌ 创建会议失败:{e}"
async def _send_message(self, params: Dict) -> str:
"""发送消息"""
content = params.get('content', '')
if not self.current_meeting_id:
return "❌ 请先创建或加入会议"
try:
message = await self.api.send_message(self.current_meeting_id, content)
return f"✅ 消息已发送:{message.content}"
except Exception as e:
return f"❌ 发送消息失败:{e}"
async def _list_meetings(self) -> str:
"""获取会议列表"""
try:
await self.api.login('test', 'test123')
meetings = await self.api.list_meetings()
if not meetings:
return "📋 暂无会议"
result = "📋 会议列表:\n"
for m in meetings:
result += f"{m.topic} (邀请码:{m.invite_code})\n"
return result
except Exception as e:
return f"❌ 获取会议列表失败:{e}"
async def _join_meeting(self, params: Dict) -> str:
"""加入会议"""
invite_code = params.get('invite_code')
if not invite_code:
return "❌ 请提供邀请码"
try:
await self.api.login('test', 'test123')
meetings = await self.api.list_meetings()
for m in meetings:
if m.invite_code == invite_code:
self.current_meeting_id = m.id
return f"✅ 已加入会议:{m.topic}"
return "❌ 未找到该邀请码的会议"
except Exception as e:
return f"❌ 加入会议失败:{e}"
async def _get_messages(self) -> str:
"""获取消息"""
if not self.current_meeting_id:
return "❌ 请先创建或加入会议"
try:
messages = await self.api.get_messages(self.current_meeting_id)
if not messages:
return "💬 暂无消息"
result = "💬 消息列表:\n"
for msg in messages:
result += f" {msg.sender_name}: {msg.content}\n"
return result
except Exception as e:
return f"❌ 获取消息失败:{e}"
# ============ 演示 ============
async def demo():
"""演示自然语言命令"""
agent = AutonomousMeetingAgent('http://localhost:8000')
print("=" * 60)
print("🤖 自主会议 Agent - 自然语言命令演示")
print("=" * 60)
# 测试命令
commands = [
"创建一个会议,主题是 Q2 计划讨论",
"查看会议列表",
"发送消息:大家好!这是 AI 自动发送的消息",
"查看消息",
]
for cmd in commands:
print(f"\n🗣️ 人类命令:{cmd}")
result = await agent.execute_command(cmd)
print(f"🤖 AI 执行:{result}")
print("\n" + "=" * 60)
print("✅ 演示完成!")
print("=" * 60)
if __name__ == '__main__':
import asyncio
asyncio.run(demo())

View File

@@ -0,0 +1,48 @@
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: meeting_room
POSTGRES_USER: meeting_user
POSTGRES_PASSWORD: meeting_pass
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
web:
build: .
command: >
sh -c "python manage.py migrate &&
python manage.py runserver 0.0.0.0:8000"
volumes:
- .:/app
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://meeting_user:meeting_pass@db:5432/meeting_room
REDIS_URL: redis://redis:6379/0
SECRET_KEY: dev-secret-key-change-in-production
DEBUG: "True"
depends_on:
- db
- redis
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- web
volumes:
pgdata:

23
backend/manage.py Executable file
View File

@@ -0,0 +1,23 @@
#!/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", "meeting_room.settings")
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()

137
backend/meeting_agent.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
龙虾会议轮询客户端
用法python meeting_agent.py --config meeting_config.json
"""
import requests
import time
import json
import argparse
from datetime import datetime
class MeetingAgent:
"""会议 Agent 轮询客户端"""
def __init__(self, config):
self.meeting_id = config['meeting_id']
self.agent_id = config['agent_id']
self.agent_name = config.get('agent_name', '飞行侠')
self.agent_emoji = config.get('agent_emoji', '🦸')
self.api_key = config['api_key']
self.api_base = config.get('api_base', 'http://localhost:8000')
self.check_interval = config.get('check_interval', 5)
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
def check_inbox(self):
"""查阅信箱"""
try:
response = requests.get(
f'{self.api_base}/api/v1/meetings/{self.meeting_id}/inbox/',
params={'agent_id': self.agent_id},
headers=self.headers,
timeout=10
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"[{datetime.now()}] ❌ 查信箱失败:{e}")
return {'messages': []}
def respond(self, message_id, content):
"""回复消息"""
try:
response = requests.post(
f'{self.api_base}/api/v1/meetings/{self.meeting_id}/messages/',
json={
'agent_id': self.agent_id,
'in_reply_to': message_id,
'content': content,
},
headers=self.headers,
timeout=10
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"[{datetime.now()}] ❌ 回复失败:{e}")
return None
def generate_response(self, message):
"""生成回复内容"""
sender = message['sender_name']
content = message['content']
# 简单规则匹配
responses = {
'Q2 计划': f"关于 Q2 计划,我会全力以赴!",
'你好': f"你好,{sender}!👋",
'在吗': f"我在!有什么事吗?",
'说说': f"让我想想... 我觉得这个很重要。",
}
# 匹配关键词
for keyword, response in responses.items():
if keyword in content:
return response
# 默认回复
return f"收到 {sender} 的消息,我会认真考虑的。"
def run(self):
"""运行轮询"""
print(f"🦐 {self.agent_emoji} {self.agent_name} 开始会议轮询")
print(f" 会议 ID: {self.meeting_id}")
print(f" 轮询间隔:{self.check_interval}")
print(f" API: {self.api_base}")
print(f" 按 Ctrl+C 停止")
print()
message_count = 0
try:
while True:
# 查阅信箱
inbox = self.check_inbox()
# 处理消息
for message in inbox['messages']:
if not message.get('read') and message.get('requires_response'):
print(f"[{datetime.now()}] 📨 收到 {message['sender_name']}: {message['content']}")
# 生成回复
response_content = self.generate_response(message)
print(f"[{datetime.now()}] 📤 回复:{response_content}")
# 发送回复
result = self.respond(message['id'], response_content)
if result:
print(f"[{datetime.now()}] ✅ 回复成功")
message_count += 1
# 等待
time.sleep(self.check_interval)
except KeyboardInterrupt:
print(f"\n👋 停止轮询,共处理 {message_count} 条消息")
def main():
parser = argparse.ArgumentParser(description='龙虾会议轮询客户端')
parser.add_argument('--config', '-c', required=True, help='配置文件路径')
args = parser.parse_args()
with open(args.config) as f:
config = json.load(f)
agent = MeetingAgent(config)
agent.run()
if __name__ == '__main__':
main()

302
backend/meeting_ai_sdk.py Normal file
View File

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
龙虾议事厅 - AI 操作 SDK
让 AI 可以直接操作会议系统
"""
import requests
from typing import Optional, List, Dict
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Meeting:
"""会议数据类"""
id: str
topic: str
host: int
status: str
invite_code: str
created_at: datetime
participant_count: int
@dataclass
class Message:
"""消息数据类"""
id: int
meeting: str
sender_name: str
sender_emoji: str
content: str
created_at: datetime
class MeetingAIOperations:
"""
AI 专用的会议操作接口
使用示例:
```python
api = MeetingAIOperations('http://localhost:8000')
# 登录
await api.login('test', 'test123')
# 创建会议
meeting = await api.create_meeting('Q2 计划讨论')
# 发送消息
await api.send_message(meeting.id, '大家好!')
# 获取消息
messages = await api.get_messages(meeting.id)
```
"""
def __init__(self, api_base: str = 'http://localhost:8000'):
self.api_base = api_base
self.token: Optional[str] = None
self.session = requests.Session()
def _get_headers(self) -> Dict:
"""获取请求头"""
headers = {'Content-Type': 'application/json'}
if self.token:
headers['Authorization'] = f'Bearer {self.token}'
return headers
async def login(self, username: str, password: str) -> bool:
"""
登录到系统
Args:
username: 用户名
password: 密码
Returns:
bool: 登录是否成功
"""
response = self.session.post(
f'{self.api_base}/api/v1/auth/login/',
json={'username': username, 'password': password},
headers={'Content-Type': 'application/json'}
)
if response.status_code == 200:
data = response.json()
self.token = data['token']
return True
return False
async def create_meeting(self, topic: str) -> Meeting:
"""
创建会议
Args:
topic: 会议主题
Returns:
Meeting: 创建的会议对象
"""
response = self.session.post(
f'{self.api_base}/api/v1/meetings/',
json={'topic': topic},
headers=self._get_headers()
)
if response.status_code in [200, 201]:
data = response.json()
return Meeting(
id=data['id'],
topic=data['topic'],
host=data['host'],
status=data['status'],
invite_code=data['invite_code'],
created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),
participant_count=data['participant_count']
)
raise Exception(f"创建会议失败:{response.text}")
async def list_meetings(self) -> List[Meeting]:
"""
获取会议列表
Returns:
List[Meeting]: 会议列表
"""
response = self.session.get(
f'{self.api_base}/api/v1/meetings/',
headers=self._get_headers()
)
if response.status_code == 200:
data = response.json()
return [
Meeting(
id=m['id'],
topic=m['topic'],
host=m['host'],
status=m['status'],
invite_code=m['invite_code'],
created_at=datetime.fromisoformat(m['created_at'].replace('Z', '+00:00')),
participant_count=m['participant_count']
)
for m in data
]
raise Exception(f"获取会议列表失败:{response.text}")
async def send_message(self, meeting_id: str, content: str) -> Message:
"""
发送消息
Args:
meeting_id: 会议 ID
content: 消息内容
Returns:
Message: 发送的消息对象
"""
response = self.session.post(
f'{self.api_base}/api/v1/meetings/{meeting_id}/send_message/',
json={'content': content},
headers=self._get_headers()
)
if response.status_code in [200, 201]:
data = response.json()
return Message(
id=data['id'],
meeting=data['meeting'],
sender_name=data['sender_name'],
sender_emoji=data['sender_emoji'],
content=data['content'],
created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00'))
)
raise Exception(f"发送消息失败:{response.text}")
async def get_messages(self, meeting_id: str, last_id: int = 0) -> List[Message]:
"""
获取消息
Args:
meeting_id: 会议 ID
last_id: 最后一条消息 ID用于轮询
Returns:
List[Message]: 消息列表
"""
response = self.session.get(
f'{self.api_base}/api/v1/meetings/{meeting_id}/messages/',
params={'last_id': last_id},
headers=self._get_headers()
)
if response.status_code == 200:
data = response.json()
return [
Message(
id=m['id'],
meeting=m['meeting'],
sender_name=m['sender_name'],
sender_emoji=m['sender_emoji'],
content=m['content'],
created_at=datetime.fromisoformat(m['created_at'].replace('Z', '+00:00'))
)
for m in data['messages']
]
raise Exception(f"获取消息失败:{response.text}")
async def join_meeting(self, meeting_id: str, invite_code: str) -> bool:
"""
加入会议
Args:
meeting_id: 会议 ID
invite_code: 邀请码
Returns:
bool: 是否成功加入
"""
response = self.session.post(
f'{self.api_base}/api/v1/meetings/{meeting_id}/join/',
json={'invite_code': invite_code},
headers=self._get_headers()
)
return response.status_code in [200, 201]
async def get_meeting_info(self, meeting_id: str) -> Meeting:
"""
获取会议详情
Args:
meeting_id: 会议 ID
Returns:
Meeting: 会议对象
"""
response = self.session.get(
f'{self.api_base}/api/v1/meetings/{meeting_id}/',
headers=self._get_headers()
)
if response.status_code == 200:
data = response.json()
return Meeting(
id=data['id'],
topic=data['topic'],
host=data['host'],
status=data['status'],
invite_code=data['invite_code'],
created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),
participant_count=data.get('participant_count', 0)
)
raise Exception(f"获取会议详情失败:{response.text}")
# ============ 使用示例 ============
async def demo():
"""演示如何使用 AI 操作接口"""
api = MeetingAIOperations('http://localhost:8000')
# 1. 登录
print("🔐 登录...")
success = await api.login('test', 'test123')
if not success:
print("❌ 登录失败")
return
print("✅ 登录成功")
# 2. 创建会议
print("\n🏛️ 创建会议...")
meeting = await api.create_meeting('AI 自主创建的会议')
print(f"✅ 会议已创建:{meeting.id}")
print(f" 主题:{meeting.topic}")
print(f" 邀请码:{meeting.invite_code}")
# 3. 发送消息
print("\n💬 发送消息...")
message = await api.send_message(meeting.id, '这是 AI 自动发送的消息!')
print(f"✅ 消息已发送:{message.content}")
# 4. 获取消息
print("\n📨 获取消息...")
messages = await api.get_messages(meeting.id)
print(f"✅ 共 {len(messages)} 条消息")
for msg in messages:
print(f" {msg.sender_name}: {msg.content}")
# 5. 获取会议列表
print("\n📋 获取会议列表...")
meetings = await api.list_meetings()
print(f"✅ 共 {len(meetings)} 个会议")
for m in meetings:
print(f" {m.topic} ({m.invite_code})")
if __name__ == '__main__':
import asyncio
asyncio.run(demo())

View File

@@ -0,0 +1,9 @@
{
"meeting_id": "your-meeting-uuid-here",
"agent_id": "flying_hero",
"agent_name": "飞行侠",
"agent_emoji": "🦸",
"api_key": "your-api-key-here",
"api_base": "http://localhost:8000",
"check_interval": 5
}

View File

View File

@@ -0,0 +1,16 @@
"""
ASGI config for meeting_room project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meeting_room.settings")
application = get_asgi_application()

View File

@@ -0,0 +1,139 @@
"""
Django settings for meeting_room project.
Generated by 'django-admin startproject' using Django 4.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-qnyz@tdj2#4zfmq0gp*y(*!l&f1ca-3ma_3q!sqn#hy(38b9)$"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# CORS 配置
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"corsheaders",
"meetings",
"users",
"api",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "meeting_room.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 = "meeting_room.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
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
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# REST Framework 配置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
}

View File

@@ -0,0 +1,16 @@
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from meetings.views import MeetingViewSet, ParticipantViewSet
from users.views import LoginView, RegisterView
router = DefaultRouter()
router.register(r'meetings', MeetingViewSet, basename='meeting')
router.register(r'meetings/(?P<meeting_pk>[^/.]+)/participants', ParticipantViewSet, basename='meeting-participant')
urlpatterns = [
path("admin/", admin.site.urls),
path("api/v1/", include(router.urls)),
path("api/v1/auth/login/", LoginView.as_view()),
path("api/v1/auth/register/", RegisterView.as_view()),
]

View File

@@ -0,0 +1,16 @@
"""
WSGI config for meeting_room project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meeting_room.settings")
application = get_wsgi_application()

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/meetings/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class MeetingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "meetings"

View File

@@ -0,0 +1,248 @@
# Generated by Django 4.2 on 2026-04-04 01:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Meeting",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("topic", models.CharField(max_length=200, verbose_name="会议主题")),
(
"status",
models.CharField(
choices=[
("pending", "待开始"),
("active", "进行中"),
("ended", "已结束"),
],
default="pending",
max_length=20,
),
),
(
"invite_code",
models.CharField(max_length=20, unique=True, verbose_name="邀请码"),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("started_at", models.DateTimeField(blank=True, null=True)),
("ended_at", models.DateTimeField(blank=True, null=True)),
(
"host",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="hosted_meetings",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "会议室",
"verbose_name_plural": "会议室",
"db_table": "meetings",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="Participant",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"agent_type",
models.CharField(
choices=[
("human", "人类"),
("openclaw", "OpenClaw Agent"),
("other", "其他 AI"),
],
max_length=20,
),
),
("agent_id", models.CharField(blank=True, max_length=100, null=True)),
(
"agent_name",
models.CharField(max_length=100, verbose_name="Agent 名称"),
),
(
"agent_emoji",
models.CharField(
default="🤖", max_length=10, verbose_name="Agent 表情"
),
),
("nickname", models.CharField(max_length=100, verbose_name="昵称")),
("is_host", models.BooleanField(default=False)),
("joined_at", models.DateTimeField(auto_now_add=True)),
("left_at", models.DateTimeField(blank=True, null=True)),
("api_key", models.CharField(blank=True, max_length=255, null=True)),
(
"meeting",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="participants",
to="meetings.meeting",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "参会者",
"verbose_name_plural": "参会者",
"db_table": "participants",
},
),
migrations.CreateModel(
name="Message",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"is_broadcast",
models.BooleanField(default=True, verbose_name="群发消息"),
),
(
"requires_response",
models.BooleanField(default=False, verbose_name="需要回复"),
),
(
"in_reply_to",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="replies",
to="meetings.message",
),
),
(
"meeting",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="messages",
to="meetings.meeting",
),
),
(
"read_by",
models.ManyToManyField(
blank=True,
related_name="read_messages",
to="meetings.participant",
),
),
(
"sender",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to="meetings.participant",
),
),
],
options={
"verbose_name": "消息",
"verbose_name_plural": "消息",
"db_table": "messages",
"ordering": ["created_at"],
},
),
migrations.CreateModel(
name="MeetingMinutes",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("generated_at", models.DateTimeField(auto_now_add=True)),
("exported_at", models.DateTimeField(blank=True, null=True)),
(
"meeting",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="minutes",
to="meetings.meeting",
),
),
],
options={
"verbose_name": "会议纪要",
"verbose_name_plural": "会议纪要",
"db_table": "meeting_minutes",
},
),
migrations.AddIndex(
model_name="participant",
index=models.Index(
fields=["meeting", "agent_id"], name="participant_meeting_74488d_idx"
),
),
migrations.AddIndex(
model_name="participant",
index=models.Index(
fields=["agent_type", "meeting"], name="participant_agent_t_c4a9dc_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["meeting", "created_at"], name="messages_meeting_b69008_idx"
),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["is_broadcast", "created_at"],
name="messages_is_broa_fae706_idx",
),
),
]

View File

126
backend/meetings/models.py Normal file
View File

@@ -0,0 +1,126 @@
from django.db import models
from django.contrib.auth import get_user_model
import uuid
User = get_user_model()
class Meeting(models.Model):
"""会议室模型"""
STATUS_CHOICES = [
('pending', '待开始'),
('active', '进行中'),
('ended', '已结束'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
topic = models.CharField(max_length=200, verbose_name='会议主题')
host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='hosted_meetings')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
invite_code = models.CharField(max_length=20, unique=True, verbose_name='邀请码')
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
ended_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'meetings'
verbose_name = '会议室'
verbose_name_plural = '会议室'
ordering = ['-created_at']
def __str__(self):
return f"{self.topic} ({self.host.username})"
def save(self, *args, **kwargs):
if not self.invite_code:
self.invite_code = uuid.uuid4().hex[:8].upper()
super().save(*args, **kwargs)
class Participant(models.Model):
"""参会者模型"""
AGENT_TYPE_CHOICES = [
('human', '人类'),
('openclaw', 'OpenClaw Agent'),
('other', '其他 AI'),
]
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='participants')
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
# Agent 信息
agent_type = models.CharField(max_length=20, choices=AGENT_TYPE_CHOICES)
agent_id = models.CharField(max_length=100, null=True, blank=True)
agent_name = models.CharField(max_length=100, verbose_name='Agent 名称')
agent_emoji = models.CharField(max_length=10, default='🤖', verbose_name='Agent 表情')
# 显示信息
nickname = models.CharField(max_length=100, verbose_name='昵称')
is_host = models.BooleanField(default=False)
joined_at = models.DateTimeField(auto_now_add=True)
left_at = models.DateTimeField(null=True, blank=True)
# API 认证Agent 用)
api_key = models.CharField(max_length=255, null=True, blank=True)
class Meta:
db_table = 'participants'
verbose_name = '参会者'
verbose_name_plural = '参会者'
indexes = [
models.Index(fields=['meeting', 'agent_id']),
models.Index(fields=['agent_type', 'meeting']),
]
def __str__(self):
return f"{self.agent_emoji} {self.nickname}"
def save(self, *args, **kwargs):
if not self.api_key and self.agent_type != 'human':
self.api_key = uuid.uuid4().hex
super().save(*args, **kwargs)
class Message(models.Model):
"""消息模型"""
meeting = models.ForeignKey(Meeting, on_delete=models.CASCADE, related_name='messages')
sender = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name='sent_messages')
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# 信箱机制
is_broadcast = models.BooleanField(default=True, verbose_name='群发消息')
requires_response = models.BooleanField(default=False, verbose_name='需要回复')
in_reply_to = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='replies')
# 读取状态
read_by = models.ManyToManyField(Participant, related_name='read_messages', blank=True)
class Meta:
db_table = 'messages'
verbose_name = '消息'
verbose_name_plural = '消息'
ordering = ['created_at']
indexes = [
models.Index(fields=['meeting', 'created_at']),
models.Index(fields=['is_broadcast', 'created_at']),
]
def __str__(self):
return f"{self.sender.nickname}: {self.content[:50]}"
class MeetingMinutes(models.Model):
"""会议纪要模型"""
meeting = models.OneToOneField(Meeting, on_delete=models.CASCADE, related_name='minutes')
content = models.TextField()
generated_at = models.DateTimeField(auto_now_add=True)
exported_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'meeting_minutes'
verbose_name = '会议纪要'
verbose_name_plural = '会议纪要'
def __str__(self):
return f"会议纪要 - {self.meeting.topic}"

View File

@@ -0,0 +1,76 @@
from rest_framework import serializers
from .models import Meeting, Participant, Message, MeetingMinutes
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
"""用户序列化器"""
class Meta:
model = User
fields = ['id', 'username', 'email', 'created_at']
class MeetingSerializer(serializers.ModelSerializer):
"""会议室序列化器"""
host_name = serializers.CharField(source='host.username', read_only=True)
participant_count = serializers.SerializerMethodField()
class Meta:
model = Meeting
fields = [
'id', 'topic', 'host', 'host_name', 'status', 'invite_code',
'created_at', 'started_at', 'ended_at', 'participant_count'
]
read_only_fields = ['host', 'invite_code', 'status']
def get_participant_count(self, obj):
return obj.participants.filter(left_at__isnull=True).count()
def create(self, validated_data):
# 使用第一个用户作为 host临时方案
host = User.objects.first()
validated_data['host'] = host
return super().create(validated_data)
class ParticipantSerializer(serializers.ModelSerializer):
"""参会者序列化器"""
class Meta:
model = Participant
fields = [
'id', 'meeting', 'user', 'agent_type', 'agent_id',
'agent_name', 'agent_emoji', 'nickname', 'is_host',
'joined_at', 'api_key'
]
read_only_fields = ['api_key', 'joined_at']
class MessageSerializer(serializers.ModelSerializer):
"""消息序列化器"""
sender_name = serializers.CharField(source='sender.nickname', read_only=True)
sender_emoji = serializers.CharField(source='sender.agent_emoji', read_only=True)
class Meta:
model = Message
fields = [
'id', 'meeting', 'sender', 'sender_name', 'sender_emoji',
'content', 'created_at', 'is_broadcast', 'requires_response',
'in_reply_to', 'read_by'
]
read_only_fields = ['sender', 'created_at']
class InboxSerializer(serializers.Serializer):
"""信箱序列化器"""
unread_count = serializers.IntegerField()
messages = MessageSerializer(many=True)
class MeetingMinutesSerializer(serializers.ModelSerializer):
"""会议纪要序列化器"""
class Meta:
model = MeetingMinutes
fields = ['meeting', 'content', 'generated_at', 'exported_at']
read_only_fields = ['meeting', 'generated_at']

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

236
backend/meetings/views.py Normal file
View File

@@ -0,0 +1,236 @@
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.utils import timezone
from .models import Meeting, Participant, Message, MeetingMinutes
from .serializers import (
MeetingSerializer, ParticipantSerializer,
MessageSerializer, InboxSerializer
)
from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404
import uuid
User = get_user_model()
class MeetingViewSet(viewsets.ModelViewSet):
"""会议室视图集"""
queryset = Meeting.objects.all()
serializer_class = MeetingSerializer
permission_classes = [] # 临时开放所有权限
def get_queryset(self):
# 简单返回所有会议
return Meeting.objects.all().order_by('-created_at')
def create(self, request, *args, **kwargs):
"""创建会议"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 临时:使用第一个用户作为 host
from django.contrib.auth import get_user_model
User = get_user_model()
host = User.objects.first()
meeting = serializer.save(host=host)
# 创建主持人参会记录
Participant.objects.create(
meeting=meeting,
user=host,
agent_type='human',
nickname=host.username,
is_host=True
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['post'])
def start(self, request, pk=None):
"""开始会议"""
meeting = self.get_object()
if meeting.host != request.user:
return Response(
{'error': '只有主持人可以开始会议'},
status=status.HTTP_403_FORBIDDEN
)
meeting.status = 'active'
meeting.started_at = timezone.now()
meeting.save()
return Response({'status': '会议已开始'})
@action(detail=True, methods=['post'])
def end(self, request, pk=None):
"""结束会议"""
meeting = self.get_object()
if meeting.host != request.user:
return Response(
{'error': '只有主持人可以结束会议'},
status=status.HTTP_403_FORBIDDEN
)
meeting.status = 'ended'
meeting.ended_at = timezone.now()
meeting.save()
return Response({'status': '会议已结束'})
@action(detail=True, methods=['post'])
def join(self, request, pk=None):
"""加入会议"""
meeting = self.get_object()
if meeting.status == 'ended':
return Response(
{'error': '会议已结束'},
status=status.HTTP_400_BAD_REQUEST
)
invite_code = request.data.get('invite_code')
if invite_code != meeting.invite_code:
return Response(
{'error': '邀请码错误'},
status=status.HTTP_400_BAD_REQUEST
)
# 检查是否已加入
existing = Participant.objects.filter(
meeting=meeting,
user=request.user,
left_at__isnull=True
).first()
if existing:
return Response(ParticipantSerializer(existing).data)
# 创建参会记录
participant = Participant.objects.create(
meeting=meeting,
user=request.user,
agent_type='human',
nickname=request.user.username
)
return Response(ParticipantSerializer(participant).data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['get'])
def participants(self, request, pk=None):
"""获取参会者列表"""
meeting = self.get_object()
participants = meeting.participants.filter(left_at__isnull=True)
serializer = ParticipantSerializer(participants, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def messages(self, request, pk=None):
"""获取消息(人类轮询)"""
meeting = self.get_object()
last_id = request.query_params.get('last_id', 0)
messages = meeting.messages.filter(id__gt=last_id).select_related('sender')
serializer = MessageSerializer(messages, many=True)
return Response({'messages': serializer.data})
@action(detail=True, methods=['post'])
def send_message(self, request, pk=None):
"""发送消息"""
meeting = self.get_object()
# 获取或创建参会者(临时:使用第一个参会者或创建)
participant = Participant.objects.filter(
meeting=meeting,
left_at__isnull=True
).first()
if not participant:
# 创建默认参会者
host = meeting.host
participant = Participant.objects.create(
meeting=meeting,
user=host,
agent_type='human',
nickname=host.username
)
content = request.data.get('content')
if not content:
return Response(
{'error': '消息内容不能为空'},
status=status.HTTP_400_BAD_REQUEST
)
message = Message.objects.create(
meeting=meeting,
sender=participant,
content=content,
is_broadcast=request.data.get('is_broadcast', True),
requires_response=request.data.get('requires_response', False)
)
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['get'])
def inbox(self, request, pk=None):
"""Agent 查阅信箱"""
meeting = self.get_object()
agent_id = request.query_params.get('agent_id')
if not agent_id:
return Response(
{'error': '缺少 agent_id 参数'},
status=status.HTTP_400_BAD_REQUEST
)
# 找到这个 Agent 的参会记录
participant = get_object_or_404(
Participant,
meeting=meeting,
agent_id=agent_id,
left_at__isnull=True
)
# 获取发给这个 Agent 的消息(未读)
messages = Message.objects.filter(
meeting=meeting
).exclude(
read_by=participant
)
# 如果是群发消息,所有人都能看到
# 如果是指定消息,需要检查 recipients
# 简化版:所有未读消息都返回
serializer = MessageSerializer(messages, many=True)
return Response({
'unread_count': messages.count(),
'messages': serializer.data
})
class ParticipantViewSet(viewsets.ModelViewSet):
"""参会者视图集"""
queryset = Participant.objects.all()
serializer_class = ParticipantSerializer
permission_classes = [permissions.IsAuthenticated]
@action(detail=True, methods=['post'])
def leave(self, request, pk=None):
"""离开会议"""
participant = self.get_object()
if participant.user != request.user:
return Response(
{'error': '无权操作'},
status=status.HTTP_403_FORBIDDEN
)
participant.left_at = timezone.now()
participant.save()
return Response({'status': '已离开会议'})

6
backend/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
Django>=4.2,<5.0
djangorestframework>=3.14
django-cors-headers>=4.3
psycopg2-binary>=2.9
python-dotenv>=1.0
djangorestframework-simplejwt>=5.3

167
backend/test_api.py Normal file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
龙虾议事厅 - API 测试脚本
自动测试所有核心功能
"""
import requests
import json
import sys
API_BASE = 'http://localhost:8000/api/v1'
def print_result(name, success, data=None):
"""打印测试结果"""
if success:
print(f"{name}")
if data:
print(f" {json.dumps(data, indent=2, ensure_ascii=False)}")
else:
print(f"{name}")
return success
def test_register():
"""测试注册"""
print("\n=== 测试用户注册 ===")
try:
response = requests.post(f'{API_BASE}/auth/register/', json={
'username': 'testuser',
'email': 'test@example.com',
'password': 'password123'
})
if response.status_code in [200, 201]:
return print_result("用户注册", True, response.json())
else:
return print_result("用户注册", False)
except Exception as e:
return print_result("用户注册", False)
def test_login():
"""测试登录"""
print("\n=== 测试用户登录 ===")
try:
response = requests.post(f'{API_BASE}/auth/login/', json={
'username': 'test',
'password': 'test123'
})
if response.status_code == 200:
data = response.json()
print_result("用户登录", True, data)
return data.get('token')
else:
print_result("用户登录", False)
return None
except Exception as e:
print_result("用户登录", False)
return None
def test_create_meeting(token):
"""测试创建会议"""
print("\n=== 测试创建会议 ===")
try:
headers = {'Authorization': f'Bearer {token}'} if token else {}
response = requests.post(f'{API_BASE}/meetings/', json={
'topic': 'API 测试会议'
}, headers=headers)
if response.status_code in [200, 201]:
data = response.json()
print_result("创建会议", True, data)
return data.get('id')
else:
print_result("创建会议", False)
print(f" 错误:{response.text[:200]}")
return None
except Exception as e:
print_result("创建会议", False)
return None
def test_list_meetings(token):
"""测试获取会议列表"""
print("\n=== 测试获取会议列表 ===")
try:
headers = {'Authorization': f'Bearer {token}'} if token else {}
response = requests.get(f'{API_BASE}/meetings/', headers=headers)
if response.status_code == 200:
data = response.json()
print_result("获取会议列表", True, data)
return data
else:
print_result("获取会议列表", False)
return None
except Exception as e:
print_result("获取会议列表", False)
return None
def test_send_message(token, meeting_id):
"""测试发送消息"""
print("\n=== 测试发送消息 ===")
try:
headers = {'Authorization': f'Bearer {token}'} if token else {}
response = requests.post(
f'{API_BASE}/meetings/{meeting_id}/send_message/',
json={'content': 'Hello, 这是测试消息!'},
headers=headers
)
if response.status_code in [200, 201]:
data = response.json()
print_result("发送消息", True, data)
return True
else:
print_result("发送消息", False)
return None
except Exception as e:
print_result("发送消息", False)
return None
def test_get_messages(meeting_id):
"""测试获取消息"""
print("\n=== 测试获取消息 ===")
try:
response = requests.get(f'{API_BASE}/meetings/{meeting_id}/messages/?last_id=0')
if response.status_code == 200:
data = response.json()
print_result("获取消息", True, data)
return True
else:
print_result("获取消息", False)
return None
except Exception as e:
print_result("获取消息", False)
return None
def main():
"""主测试流程"""
print("=" * 60)
print("🏛️ 龙虾议事厅 - API 自动化测试")
print("=" * 60)
# 1. 测试登录
token = test_login()
if not token:
print("\n⚠️ 登录失败,尝试注册新用户...")
test_register()
token = test_login()
if not token:
print("\n❌ 无法登录,测试终止")
return False
# 2. 测试创建会议
meeting_id = test_create_meeting(token)
# 3. 测试获取会议列表
test_list_meetings(token)
# 4. 测试发送消息
if meeting_id:
test_send_message(token, meeting_id)
test_get_messages(meeting_id)
print("\n" + "=" * 60)
print("✅ 测试完成!")
print("=" * 60)
return True
if __name__ == '__main__':
success = main()
sys.exit(0 if success else 1)

View File

3
backend/users/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/users/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"

View File

3
backend/users/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
backend/users/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

80
backend/users/views.py Normal file
View File

@@ -0,0 +1,80 @@
from rest_framework import serializers, status, views
from rest_framework.response import Response
from django.contrib.auth import authenticate, get_user_model
User = get_user_model()
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
class LoginView(views.APIView):
def post(self, request):
serializer = LoginSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
username = serializer.validated_data['username']
password = serializer.validated_data['password']
user = authenticate(username=username, password=password)
if not user:
return Response(
{'detail': '用户名或密码错误'},
status=status.HTTP_401_UNAUTHORIZED
)
# 简单 Token生产环境应该用 JWT
import uuid
token = uuid.uuid4().hex
# 实际项目中应该存储 token 到数据库/缓存
# 这里简化处理
return Response({
'token': token,
'user': {
'id': user.id,
'username': user.username,
'email': user.email
}
})
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField()
email = serializers.EmailField()
password = serializers.CharField()
class RegisterView(views.APIView):
def post(self, request):
serializer = RegisterSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
user = User.objects.create_user(
username=serializer.validated_data['username'],
email=serializer.validated_data['email'],
password=serializer.validated_data['password']
)
import uuid
token = uuid.uuid4().hex
return Response({
'token': token,
'user': {
'id': user.id,
'username': user.username,
'email': user.email
}
}, status=status.HTTP_201_CREATED)
except Exception as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)