feat(accounts): 实现用户资料与认证页面

This commit is contained in:
bruce
2026-09-09 13:01:36 +08:00
parent f4a92699f0
commit 163d20f710
11 changed files with 177 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""用户资料与认证页面应用。"""
+11
View File
@@ -0,0 +1,11 @@
from django.contrib import admin
from .models import UserProfile
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
"""管理员维护用户基础资料。"""
list_display = ("user", "display_name", "timezone", "updated_at")
search_fields = ("user__username", "display_name")
+8
View File
@@ -0,0 +1,8 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""用户资料应用配置。"""
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"
+17
View File
@@ -0,0 +1,17 @@
"""用户资料表单。"""
from django import forms
from .models import UserProfile
TIMEZONE_CHOICES = [("Asia/Shanghai", "中国标准时间(上海)")]
class UserProfileForm(forms.ModelForm):
"""只暴露第一阶段允许用户自行修改的资料字段。"""
timezone = forms.ChoiceField(label="时区", choices=TIMEZONE_CHOICES)
class Meta:
model = UserProfile
fields = ("display_name", "timezone")
+32
View File
@@ -0,0 +1,32 @@
# Generated by Django 6.0.8 on 2026-09-09 02:53
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('display_name', models.CharField(blank=True, max_length=80, verbose_name='显示名')),
('timezone', models.CharField(default='Asia/Shanghai', max_length=64, verbose_name='时区')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '用户资料',
'verbose_name_plural': '用户资料',
},
),
]
View File
+23
View File
@@ -0,0 +1,23 @@
"""Django 内置用户的非认证资料模型。"""
from django.conf import settings
from django.db import models
from common.models import TimeStampedModel
class UserProfile(TimeStampedModel):
"""保存可独立演进的展示资料,不复制认证字段。"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile", verbose_name="用户"
)
display_name = models.CharField("显示名", max_length=80, blank=True)
timezone = models.CharField("时区", max_length=64, default="Asia/Shanghai")
class Meta:
verbose_name = "用户资料"
verbose_name_plural = "用户资料"
def __str__(self):
return self.display_name or self.user.get_username()
+26
View File
@@ -0,0 +1,26 @@
"""用户资料服务。"""
from django.contrib.auth import get_user_model
from django.db import transaction
from .models import UserProfile
@transaction.atomic
def get_or_create_profile(user) -> UserProfile:
"""显式建立用户资料,避免依赖难以追踪的模型信号。"""
profile, _ = UserProfile.objects.get_or_create(user=user)
return profile
@transaction.atomic
def deactivate_user(actor, target_user_id: int):
"""仅允许管理员停用账户;保留关联审计记录。"""
if not actor.is_staff:
raise PermissionError("只有管理员可以停用用户。")
user = get_user_model().objects.select_for_update().get(pk=target_user_id)
user.is_active = False
user.save(update_fields=("is_active",))
return user
+21
View File
@@ -0,0 +1,21 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
class ProfileTests(TestCase):
"""验证资料显式创建、认证保护和更新边界。"""
def setUp(self):
self.user = get_user_model().objects.create_user(username="alice", password="safe-pass-123")
def test_profile_requires_login(self):
response = self.client.get(reverse("accounts:profile"))
self.assertEqual(response.status_code, 302)
def test_profile_is_created_and_updated(self):
self.client.force_login(self.user)
response = self.client.post(reverse("accounts:profile"), {"display_name": "爱丽丝", "timezone": "Asia/Shanghai"})
self.assertRedirects(response, reverse("accounts:profile"))
self.user.refresh_from_db()
self.assertEqual(self.user.profile.display_name, "爱丽丝")
+14
View File
@@ -0,0 +1,14 @@
"""账户页面路由。"""
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views
app_name = "accounts"
urlpatterns = [
path("login/", auth_views.LoginView.as_view(), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
path("profile/", views.profile, name="profile"),
]
+24
View File
@@ -0,0 +1,24 @@
"""认证与当前用户资料页面。"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from .forms import UserProfileForm
from .services import get_or_create_profile
@login_required
def profile(request):
"""读取或更新当前用户自己的资料,不接受目标用户参数。"""
user_profile = get_or_create_profile(request.user)
if request.method == "POST":
form = UserProfileForm(request.POST, instance=user_profile)
if form.is_valid():
form.save()
messages.success(request, "个人资料已保存。")
return redirect("accounts:profile")
else:
form = UserProfileForm(instance=user_profile)
return render(request, "accounts/profile.html", {"form": form})