feat: 多用户系统改造(数据模型 + 认证 API)
This commit is contained in:
80
MULTI_USER_PLAN.md
Normal file
80
MULTI_USER_PLAN.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 多用户改造方案
|
||||
|
||||
## 📋 改动清单
|
||||
|
||||
### 1. 数据模型改动
|
||||
|
||||
#### DiaryEntry
|
||||
```python
|
||||
# 添加字段
|
||||
user = ForeignKey(User, on_delete=CASCADE, verbose_name='用户')
|
||||
|
||||
# 修改唯一约束
|
||||
unique_together = ['user', 'date'] # 每个用户每天一条
|
||||
```
|
||||
|
||||
#### Experience
|
||||
```python
|
||||
user = ForeignKey(User, on_delete=CASCADE, verbose_name='用户')
|
||||
```
|
||||
|
||||
#### Task
|
||||
```python
|
||||
user = ForeignKey(User, on_delete=CASCADE, verbose_name='用户')
|
||||
assigned_to = ForeignKey(User, ..., null=True) # 改为关联用户
|
||||
```
|
||||
|
||||
#### Comment
|
||||
```python
|
||||
created_by = ForeignKey(User, on_delete=CASCADE) # 改为关联用户
|
||||
```
|
||||
|
||||
### 2. 新增认证 API
|
||||
|
||||
```
|
||||
POST /api/auth/register/ # 注册
|
||||
POST /api/auth/login/ # 登录
|
||||
POST /api/auth/logout/ # 登出
|
||||
GET /api/auth/me/ # 当前用户
|
||||
```
|
||||
|
||||
### 3. API 权限控制
|
||||
|
||||
所有 API 添加:
|
||||
```python
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Model.objects.filter(user=self.request.user)
|
||||
```
|
||||
|
||||
### 4. 前端新增
|
||||
|
||||
- 登录页面 `/login`
|
||||
- 注册页面 `/register`
|
||||
- 未登录重定向
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 预计工时
|
||||
|
||||
- 数据模型迁移:15 分钟
|
||||
- 认证 API:30 分钟
|
||||
- 权限控制:30 分钟
|
||||
- 前端登录界面:30 分钟
|
||||
- 测试验证:15 分钟
|
||||
|
||||
**总计:约 2 小时**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **数据迁移** - 现有数据需要关联到默认用户
|
||||
2. **向后兼容** - 保持现有 API 格式
|
||||
3. **密码安全** - 使用 Django 内置加密
|
||||
4. **用户隔离** - 确保用户只能访问自己的数据
|
||||
|
||||
---
|
||||
|
||||
_确认改造后开始实施_
|
||||
34
backend/authentication/serializers.py
Normal file
34
backend/authentication/serializers.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email', 'date_joined']
|
||||
read_only_fields = ['date_joined']
|
||||
|
||||
class RegisterSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True, min_length=6)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email', 'password']
|
||||
|
||||
def create(self, validated_data):
|
||||
user = User.objects.create_user(
|
||||
username=validated_data['username'],
|
||||
email=validated_data.get('email', ''),
|
||||
password=validated_data['password']
|
||||
)
|
||||
return user
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
user = authenticate(**data)
|
||||
if user and user.is_active:
|
||||
return user
|
||||
raise serializers.ValidationError("用户名或密码错误")
|
||||
9
backend/authentication/urls.py
Normal file
9
backend/authentication/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from .views import RegisterView, LoginView, LogoutView, CurrentUserView
|
||||
|
||||
urlpatterns = [
|
||||
path('register/', RegisterView.as_view(), name='register'),
|
||||
path('login/', LoginView.as_view(), name='login'),
|
||||
path('logout/', LogoutView.as_view(), name='logout'),
|
||||
path('me/', CurrentUserView.as_view(), name='current-user'),
|
||||
]
|
||||
54
backend/authentication/views.py
Normal file
54
backend/authentication/views.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from rest_framework import generics, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
from django.contrib.auth import login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from .serializers import UserSerializer, RegisterSerializer, LoginSerializer
|
||||
|
||||
class RegisterView(generics.CreateAPIView):
|
||||
"""用户注册"""
|
||||
serializer_class = RegisterSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.save()
|
||||
|
||||
return Response({
|
||||
'user': UserSerializer(user).data,
|
||||
'message': '注册成功'
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
|
||||
class LoginView(generics.GenericAPIView):
|
||||
"""用户登录"""
|
||||
serializer_class = LoginSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data
|
||||
|
||||
login(request, user)
|
||||
|
||||
return Response({
|
||||
'user': UserSerializer(user).data,
|
||||
'message': '登录成功'
|
||||
})
|
||||
|
||||
class LogoutView(generics.GenericAPIView):
|
||||
"""用户登出"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
logout(request)
|
||||
return Response({'message': '登出成功'})
|
||||
|
||||
class CurrentUserView(generics.RetrieveAPIView):
|
||||
"""当前用户信息"""
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.2.11 on 2026-04-15 02:59
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('diary', '0007_comment_creativity_comment_efficiency_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='创建者'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='diaryentry',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='diary_entries', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experience',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='experiences', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to=settings.AUTH_USER_MODEL, verbose_name='创建者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='diaryentry',
|
||||
name='date',
|
||||
field=models.DateField(verbose_name='日期'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='assigned_to',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tasks', to=settings.AUTH_USER_MODEL, verbose_name='负责人'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='diaryentry',
|
||||
unique_together={('user', 'date')},
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class DiaryEntry(models.Model):
|
||||
"""
|
||||
@@ -16,7 +17,8 @@ class DiaryEntry(models.Model):
|
||||
2. 确认不影响日历显示
|
||||
3. 运行 test_frontend.py diary 验证
|
||||
"""
|
||||
date = models.DateField('日期', unique=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户', related_name='diary_entries', null=True, blank=True)
|
||||
date = models.DateField('日期')
|
||||
title = models.CharField('标题', max_length=200, default='每日日记')
|
||||
content = models.TextField('日记内容', blank=True, default='')
|
||||
completed_tasks = models.TextField('完成的任务', blank=True, default='')
|
||||
@@ -35,6 +37,7 @@ class DiaryEntry(models.Model):
|
||||
ordering = ['-date']
|
||||
verbose_name = '日记'
|
||||
verbose_name_plural = '日记'
|
||||
unique_together = ['user', 'date'] # 每个用户每天一条日记
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date} - {self.title}"
|
||||
@@ -70,6 +73,7 @@ class Experience(models.Model):
|
||||
|
||||
修改前阅读 docs/EXPERIENCE.md
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户', related_name='experiences', null=True, blank=True)
|
||||
title = models.CharField('标题', max_length=200)
|
||||
category = models.CharField('类别', max_length=50, choices=[
|
||||
('deployment', '📦 部署'),
|
||||
@@ -134,7 +138,7 @@ class Comment(models.Model):
|
||||
creativity = models.IntegerField('创新性', null=True, blank=True, help_text='1-10 分')
|
||||
learning = models.IntegerField('学习价值', null=True, blank=True, help_text='1-10 分')
|
||||
|
||||
created_by = models.CharField('创建者', max_length=100, default='北极星')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='创建者', related_name='comments', null=True, blank=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
@@ -169,7 +173,8 @@ class Task(models.Model):
|
||||
priority = models.CharField('优先级', max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
||||
progress_percent = models.IntegerField('进展百分比', default=0)
|
||||
progress_notes = models.TextField('进展记录', blank=True, default='')
|
||||
assigned_to = models.CharField('负责人', max_length=100, blank=True, default='码神')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='创建者', related_name='tasks', null=True, blank=True)
|
||||
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='负责人', related_name='assigned_tasks')
|
||||
due_date = models.DateField('截止日期', null=True, blank=True)
|
||||
completed_at = models.DateTimeField('完成时间', null=True, blank=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
|
||||
@@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter
|
||||
from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet, CommentViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'entries', DiaryEntryViewSet)
|
||||
router.register(r'progress', DailyProgressViewSet)
|
||||
router.register(r'experiences', ExperienceViewSet)
|
||||
router.register(r'tasks', TaskViewSet)
|
||||
router.register(r'comments', CommentViewSet)
|
||||
router.register(r'entries', DiaryEntryViewSet, basename='diaryentry')
|
||||
router.register(r'progress', DailyProgressViewSet, basename='dailyprogress')
|
||||
router.register(r'experiences', ExperienceViewSet, basename='experience')
|
||||
router.register(r'tasks', TaskViewSet, basename='task')
|
||||
router.register(r'comments', CommentViewSet, basename='comment')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions
|
||||
from django.utils import timezone
|
||||
from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
|
||||
from .serializers import (
|
||||
@@ -9,21 +10,24 @@ from .serializers import (
|
||||
)
|
||||
|
||||
class DiaryEntryViewSet(viewsets.ModelViewSet):
|
||||
queryset = DiaryEntry.objects.all()
|
||||
serializer_class = DiaryEntrySerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return DiaryEntry.objects.filter(user=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def today(self, request):
|
||||
"""获取今天的日记"""
|
||||
today = timezone.now().date()
|
||||
entry, created = DiaryEntry.objects.get_or_create(date=today)
|
||||
entry, created = DiaryEntry.objects.get_or_create(user=request.user, date=today)
|
||||
serializer = self.get_serializer(entry)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def recent(self, request):
|
||||
"""获取最近 7 天的日记"""
|
||||
entries = DiaryEntry.objects.order_by('-date')[:7]
|
||||
entries = DiaryEntry.objects.filter(user=request.user).order_by('-date')[:7]
|
||||
serializer = self.get_serializer(entries, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -73,8 +77,11 @@ class DailyProgressViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class ExperienceViewSet(viewsets.ModelViewSet):
|
||||
queryset = Experience.objects.all()
|
||||
serializer_class = ExperienceSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Experience.objects.filter(user=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def by_category(self, request):
|
||||
@@ -96,8 +103,11 @@ class ExperienceViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = TaskSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Task.objects.filter(user=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def by_status(self, request):
|
||||
|
||||
@@ -21,8 +21,10 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'corsheaders',
|
||||
'diary',
|
||||
'authentication',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -6,5 +6,6 @@ from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/auth/', include('authentication.urls')),
|
||||
path('api/', include('diary.urls')),
|
||||
]
|
||||
|
||||
7
create_auth_app.py
Normal file
7
create_auth_app.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, sys
|
||||
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||
from django.core.management import execute_from_command_line
|
||||
sys.argv = ['manage.py', 'startapp', 'authentication']
|
||||
execute_from_command_line(sys.argv)
|
||||
7
makemigrations_multiuser.py
Normal file
7
makemigrations_multiuser.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, sys
|
||||
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||
from django.core.management import execute_from_command_line
|
||||
sys.argv = ['manage.py', 'makemigrations']
|
||||
execute_from_command_line(sys.argv)
|
||||
7
migrate_multiuser.py
Normal file
7
migrate_multiuser.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, sys
|
||||
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||
from django.core.management import execute_from_command_line
|
||||
sys.argv = ['manage.py', 'migrate']
|
||||
execute_from_command_line(sys.argv)
|
||||
Reference in New Issue
Block a user