From e6aecd2752890ef67efab022074e33f6ebfabfeb Mon Sep 17 00:00:00 2001 From: maoshen Date: Wed, 15 Apr 2026 02:59:36 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E7=94=A8=E6=88=B7=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=94=B9=E9=80=A0=EF=BC=88=E6=95=B0=E6=8D=AE=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=20+=20=E8=AE=A4=E8=AF=81=20API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MULTI_USER_PLAN.md | 80 +++++++++++++++++++ backend/authentication/serializers.py | 34 ++++++++ backend/authentication/urls.py | 9 +++ backend/authentication/views.py | 54 +++++++++++++ ...omment_created_by_comment_user_and_more.py | 54 +++++++++++++ backend/diary/models.py | 11 ++- backend/diary/urls.py | 10 +-- backend/diary/views.py | 20 +++-- backend/diary_system/settings.py | 2 + backend/diary_system/urls.py | 1 + create_auth_app.py | 7 ++ makemigrations_multiuser.py | 7 ++ migrate_multiuser.py | 7 ++ 13 files changed, 283 insertions(+), 13 deletions(-) create mode 100644 MULTI_USER_PLAN.md create mode 100644 backend/authentication/serializers.py create mode 100644 backend/authentication/urls.py create mode 100644 backend/authentication/views.py create mode 100644 backend/diary/migrations/0008_remove_comment_created_by_comment_user_and_more.py create mode 100644 create_auth_app.py create mode 100644 makemigrations_multiuser.py create mode 100644 migrate_multiuser.py diff --git a/MULTI_USER_PLAN.md b/MULTI_USER_PLAN.md new file mode 100644 index 0000000..bb72d38 --- /dev/null +++ b/MULTI_USER_PLAN.md @@ -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. **用户隔离** - 确保用户只能访问自己的数据 + +--- + +_确认改造后开始实施_ diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py new file mode 100644 index 0000000..c701ec2 --- /dev/null +++ b/backend/authentication/serializers.py @@ -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("用户名或密码错误") diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py new file mode 100644 index 0000000..a9527fe --- /dev/null +++ b/backend/authentication/urls.py @@ -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'), +] diff --git a/backend/authentication/views.py b/backend/authentication/views.py new file mode 100644 index 0000000..6b29587 --- /dev/null +++ b/backend/authentication/views.py @@ -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 diff --git a/backend/diary/migrations/0008_remove_comment_created_by_comment_user_and_more.py b/backend/diary/migrations/0008_remove_comment_created_by_comment_user_and_more.py new file mode 100644 index 0000000..e79fd13 --- /dev/null +++ b/backend/diary/migrations/0008_remove_comment_created_by_comment_user_and_more.py @@ -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')}, + ), + ] diff --git a/backend/diary/models.py b/backend/diary/models.py index c79a09b..0a5ecdb 100755 --- a/backend/diary/models.py +++ b/backend/diary/models.py @@ -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) diff --git a/backend/diary/urls.py b/backend/diary/urls.py index f460b15..2537a23 100755 --- a/backend/diary/urls.py +++ b/backend/diary/urls.py @@ -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)), diff --git a/backend/diary/views.py b/backend/diary/views.py index d8f581f..f7e7066 100755 --- a/backend/diary/views.py +++ b/backend/diary/views.py @@ -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): diff --git a/backend/diary_system/settings.py b/backend/diary_system/settings.py index b3df2ea..7db2301 100755 --- a/backend/diary_system/settings.py +++ b/backend/diary_system/settings.py @@ -21,8 +21,10 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'rest_framework.authtoken', 'corsheaders', 'diary', + 'authentication', ] MIDDLEWARE = [ diff --git a/backend/diary_system/urls.py b/backend/diary_system/urls.py index 1c04e18..96a9ed0 100755 --- a/backend/diary_system/urls.py +++ b/backend/diary_system/urls.py @@ -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')), ] diff --git a/create_auth_app.py b/create_auth_app.py new file mode 100644 index 0000000..ce39b33 --- /dev/null +++ b/create_auth_app.py @@ -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) diff --git a/makemigrations_multiuser.py b/makemigrations_multiuser.py new file mode 100644 index 0000000..62cc064 --- /dev/null +++ b/makemigrations_multiuser.py @@ -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) diff --git a/migrate_multiuser.py b/migrate_multiuser.py new file mode 100644 index 0000000..6a5f1b1 --- /dev/null +++ b/migrate_multiuser.py @@ -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)