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
a;{OL)9bNbU
literal 0
HcmV?d00001
diff --git a/src/accounts/accounts/admin.py b/src/accounts/accounts/admin.py
new file mode 100644
index 00000000..1a6c0345
--- /dev/null
+++ b/src/accounts/accounts/admin.py
@@ -0,0 +1,101 @@
+from django import forms
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.forms import UserChangeForm
+from django.contrib.auth.forms import UsernameField
+from django.utils.translation import gettext_lazy as _
+
+# 模块级注释——accounts应用的Admin后台表单配置文件,
+# 自定义BlogUser(自定义用户模型)的创建表单、修改表单,以及Admin后台管理配置,
+# 适配Django Admin的用户管理逻辑,支持自定义字段(如nickname、source)的后台操作
+from .models import BlogUser
+
+
+class BlogUserCreationForm(forms.ModelForm):
+ """
+ 自定义用户创建表单(用于Django Admin后台添加新用户)
+ 扩展默认表单,增加密码二次验证逻辑,适配BlogUser模型的字段要求
+ """
+ # 密码输入字段:label支持国际化,使用密码输入控件(隐藏输入内容)
+ password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
+ # 密码二次确认字段:用于验证两次输入密码一致
+ password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
+
+ class Meta:
+ model = BlogUser # 关联的模型:accounts应用的BlogUser(自定义用户模型)
+ fields = ('email',) # 后台创建用户时,必填的核心字段(仅邮箱,用户名可后续补充或自动生成)
+
+ def clean_password2(self):
+ """
+ 密码二次验证的清洁方法(Django表单验证机制)
+ 检查两次输入的密码是否一致,不一致则抛出验证错误
+ """
+ # 获取第一次和第二次输入的密码(已通过表单基础验证的清洁数据)
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ # 若两次密码均存在且不一致,抛出验证错误(错误信息支持国际化)
+ if password1 and password2 and password1 != password2:
+ raise forms.ValidationError(_("passwords do not match"))
+ # 验证通过,返回第二次输入的密码(用于后续保存)
+ return password2
+
+ def save(self, commit=True):
+ """
+ 重写保存方法,实现密码哈希存储和创建来源标记
+ Django默认会对密码进行哈希处理,此处明确调用set_password确保安全性
+ """
+ # 调用父类save方法,先不提交到数据库(commit=False)
+ user = super().save(commit=False)
+ # 对密码进行哈希处理后存储(避免明文存储,符合Django安全规范)
+ user.set_password(self.cleaned_data["password1"])
+ # 若需要提交到数据库(默认commit=True)
+ if commit:
+ user.source = 'adminsite' # 标记用户创建来源:Django Admin后台
+ user.save() # 最终保存用户数据到数据库
+ return user
+
+
+class BlogUserChangeForm(UserChangeForm):
+ """
+ 自定义用户修改表单(用于Django Admin后台编辑用户信息)
+ 继承Django内置UserChangeForm,适配BlogUser模型的所有字段
+ """
+ class Meta:
+ model = BlogUser # 关联的模型:accounts应用的BlogUser
+ fields = '__all__' # 后台可修改的字段:所有模型字段(支持自定义扩展字段)
+ field_classes = {'username': UsernameField} # 用户名字段的类:使用Django内置UsernameField(确保符合用户名验证规则)
+
+ def __init__(self, *args, **kwargs):
+ """
+ 初始化表单,调用父类构造方法保持默认逻辑
+ 若后续需要扩展表单初始化行为(如隐藏字段、设置默认值),可在此方法中添加
+ """
+ super().__init__(*args, **kwargs)
+
+
+class BlogUserAdmin(UserAdmin):
+ """
+ 自定义UserAdmin配置(用于Django Admin后台管理BlogUser模型)
+ 配置后台显示字段、排序规则、搜索字段等,优化用户管理体验
+ """
+ form = BlogUserChangeForm # 关联用户修改表单:使用自定义的BlogUserChangeForm
+ add_form = BlogUserCreationForm # 关联用户创建表单:使用自定义的BlogUserCreationForm
+
+ # 后台列表页显示的字段(按业务优先级排序)
+ list_display = (
+ 'id', # 用户ID(唯一标识)
+ 'nickname', # 用户昵称(自定义扩展字段)
+ 'username', # 用户名(Django用户模型核心字段)
+ 'email', # 邮箱(用于登录和通知,核心字段)
+ 'last_login', # 最后登录时间(安全审计字段)
+ 'date_joined', # 注册时间(业务统计字段)
+ 'source' # 创建来源(区分后台创建/前台注册等,自定义扩展字段)
+ )
+
+ # 后台列表页可点击跳转的字段(用于快速进入编辑页)
+ list_display_links = ('id', 'username')
+
+ # 后台列表页默认排序规则:按ID倒序(新创建的用户排在前面)
+ ordering = ('-id',)
+
+ # 后台搜索支持的字段(支持模糊查询,提升管理效率)
+ search_fields = ('username', 'nickname', 'email')
\ No newline at end of file
diff --git a/src/accounts/accounts/apps.py b/src/accounts/accounts/apps.py
new file mode 100644
index 00000000..92080f74
--- /dev/null
+++ b/src/accounts/accounts/apps.py
@@ -0,0 +1,12 @@
+from django.apps import AppConfig
+
+# 模块级注释——accounts应用的核心配置类文件,
+# 继承Django内置的AppConfig,用于定义应用的基本元信息,
+# 是Django识别和加载accounts应用的关键配置
+class AccountsConfig(AppConfig):
+ """
+ accounts应用的配置类,用于注册应用的核心信息
+ 遵循Django应用配置规范,定义应用的唯一标识名称
+ """
+ name = 'accounts' # 应用的唯一标识名称,与应用目录名一致,
+ # Django通过该名称识别并加载应用,关联应用内的模型、视图等组件
\ No newline at end of file
diff --git a/src/accounts/accounts/forms.py b/src/accounts/accounts/forms.py
new file mode 100644
index 00000000..62f5a88f
--- /dev/null
+++ b/src/accounts/accounts/forms.py
@@ -0,0 +1,166 @@
+from django import forms
+from django.contrib.auth import get_user_model, password_validation
+from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
+from django.core.exceptions import ValidationError
+from django.forms import widgets
+from django.utils.translation import gettext_lazy as _
+from . import utils # 导入自定义工具模块(用于验证码验证等功能)
+from .models import BlogUser # 导入自定义用户模型
+
+# 模块级注释——accounts应用的前台用户交互表单配置文件,
+# 包含登录、注册、忘记密码、验证码获取等核心业务表单,
+# 负责用户输入数据的验证、前端样式适配(如表单控件class、占位符),
+# 确保用户输入合法且符合业务规则(如邮箱唯一性、密码强度、验证码有效性)
+
+
+class LoginForm(AuthenticationForm):
+ """
+ 前台用户登录表单,继承Django内置AuthenticationForm
+ 重写表单控件样式和占位符,适配前端页面布局,提升用户体验
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ 初始化登录表单,重写用户名和密码字段的控件配置
+ """
+ super(LoginForm, self).__init__(*args, **kwargs)
+ # 用户名输入框:设置占位符和Bootstrap表单样式类,适配前端页面
+ self.fields['username'].widget = widgets.TextInput(
+ attrs={'placeholder': "username", "class": "form-control"})
+ # 密码输入框:设置占位符和Bootstrap表单样式类,使用密码隐藏控件
+ self.fields['password'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+
+
+class RegisterForm(UserCreationForm):
+ """
+ 前台用户注册表单,继承Django内置UserCreationForm
+ 扩展邮箱字段验证,重写表单控件样式,确保注册数据合法(用户名、邮箱唯一)
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ 初始化注册表单,重写用户名、邮箱、密码字段的控件配置
+ """
+ super(RegisterForm, self).__init__(*args, **kwargs)
+
+ # 用户名输入框:占位符+Bootstrap样式
+ self.fields['username'].widget = widgets.TextInput(
+ attrs={'placeholder': "username", "class": "form-control"})
+ # 邮箱输入框:使用EmailInput控件,占位符+Bootstrap样式
+ self.fields['email'].widget = widgets.EmailInput(
+ attrs={'placeholder': "email", "class": "form-control"})
+ # 密码输入框1:密码隐藏控件,占位符+Bootstrap样式
+ self.fields['password1'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+ # 密码确认框:密码隐藏控件,占位符+Bootstrap样式
+ self.fields['password2'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "repeat password", "class": "form-control"})
+
+ def clean_email(self):
+ """
+ 邮箱字段清洁验证:检查邮箱是否已被注册
+ 若已存在则抛出验证错误,确保邮箱唯一性
+ """
+ email = self.cleaned_data['email'] # 获取经过基础验证的邮箱数据
+ # 查询数据库,判断该邮箱是否已关联用户
+ if get_user_model().objects.filter(email=email).exists():
+ raise ValidationError(_("email already exists")) # 抛出国际化的验证错误
+ return email # 验证通过,返回邮箱数据
+
+ class Meta:
+ model = get_user_model() # 关联Django当前激活的用户模型(此处为BlogUser)
+ fields = ("username", "email") # 注册表单需填写的核心字段:用户名、邮箱(密码字段由父类提供)
+
+
+class ForgetPasswordForm(forms.Form):
+ """
+ 前台用户忘记密码重置表单
+ 包含新密码、密码确认、邮箱、验证码字段,实现密码重置的全流程验证
+ """
+ new_password1 = forms.CharField(
+ label=_("New password"), # 字段标签(支持国际化)
+ widget=forms.PasswordInput(
+ attrs={
+ "class": "form-control", # Bootstrap表单样式类
+ 'placeholder': _("New password") # 占位符(支持国际化)
+ }
+ ),
+ )
+
+ new_password2 = forms.CharField(
+ label="确认密码", # 密码确认字段标签
+ widget=forms.PasswordInput(
+ attrs={
+ "class": "form-control",
+ 'placeholder': _("Confirm password") # 占位符(支持国际化)
+ }
+ ),
+ )
+
+ email = forms.EmailField(
+ label='邮箱', # 邮箱字段标签
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _("Email") # 占位符(支持国际化)
+ }
+ ),
+ )
+
+ code = forms.CharField(
+ label=_('Code'), # 验证码字段标签(支持国际化)
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _("Code") # 占位符(支持国际化)
+ }
+ ),
+ )
+
+ def clean_new_password2(self):
+ """
+ 密码确认字段清洁验证:
+ 1. 检查两次输入的新密码是否一致
+ 2. 验证密码是否符合Django密码强度规则(如长度、复杂度)
+ """
+ password1 = self.data.get("new_password1") # 获取第一次输入的新密码
+ password2 = self.data.get("new_password2") # 获取第二次输入的确认密码
+ # 验证两次密码是否一致
+ if password1 and password2 and password1 != password2:
+ raise ValidationError(_("passwords do not match")) # 抛出密码不匹配错误
+ # 调用Django内置密码验证器,检查密码强度
+ password_validation.validate_password(password2)
+ return password2 # 验证通过,返回确认密码
+
+ def clean_email(self):
+ """
+ 邮箱字段清洁验证:检查输入的邮箱是否已注册
+ 若未注册则抛出错误,确保只有已注册用户能重置密码
+ """
+ user_email = self.cleaned_data.get("email") # 获取经过基础验证的邮箱
+ # 查询数据库,判断邮箱是否关联BlogUser
+ if not BlogUser.objects.filter(email=user_email).exists():
+ # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改(原代码注释保留,提示后续优化隐私保护)
+ raise ValidationError(_("email does not exist")) # 抛出邮箱未注册错误
+ return user_email # 验证通过,返回邮箱
+
+ def clean_code(self):
+ """
+ 验证码字段清洁验证:调用utils模块的verify方法验证验证码有效性
+ 若验证码无效(如过期、不匹配)则抛出错误
+ """
+ code = self.cleaned_data.get("code") # 获取用户输入的验证码
+ # 调用工具函数验证验证码(传入邮箱和验证码,返回错误信息或None)
+ error = utils.verify(email=self.cleaned_data.get("email"), code=code)
+ if error: # 若存在错误信息,抛出验证错误
+ raise ValidationError(error)
+ return code # 验证通过,返回验证码
+
+
+class ForgetPasswordCodeForm(forms.Form):
+ """
+ 前台用户获取忘记密码验证码的表单
+ 仅包含邮箱字段,用于接收用户邮箱并发送验证码
+ """
+ email = forms.EmailField(
+ label=_('Email'), # 字段标签(支持国际化)
+ )
\ No newline at end of file
diff --git a/src/accounts/accounts/migrations/0001_initial.py b/src/accounts/accounts/migrations/0001_initial.py
new file mode 100644
index 00000000..23ba59f7
--- /dev/null
+++ b/src/accounts/accounts/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.1.7 on 2023-03-02 07:14
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+#模块级注释——comments应用的初始数据库迁移文件,用于创建`Comment`模型,实现文章评论功能,支持评论层级、文章关联、用户关联等业务逻辑
+class Migration(migrations.Migration):
+
+ initial = True # xxx: 标记该迁移为应用的初始迁移
+
+ dependencies = [
+ ('blog', '0001_initial'), # xxx: 依赖`blog`应用的`0001_initial`迁移,确保`Article`模型已存在
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL), # xxx: 依赖Django可交换的用户模型,支持自定义用户模型场景
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Comment', # xxx: 定义`Comment`模型,用于存储文章评论数据
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 主键字段,自增大整数类型
+ ('body', models.TextField(max_length=300, verbose_name='正文')), # xxx: 评论正文字段,文本类型,最大长度300
+ ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # 评论创建时间字段,默认当前时间
+ ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # xxx: 评论修改时间字段,默认当前时间
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # xxx: 控制评论是否显示的布尔字段,默认显示
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), # xxx: 外键关联`blog`应用的`Article`模型,文章删除时评论级联删除
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # xxx: 外键关联用户模型,用户删除时评论级联删除
+ ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # xxx: 自外键关联,支持评论层级结构,允许空值,上级评论删除时当前评论级联删除
+ ],
+ options={
+ 'verbose_name': '评论', # xxx: 模型单数显示名称
+ 'verbose_name_plural': '评论', # xxx: 模型复数显示名称
+ 'ordering': ['-id'], # xxx: 数据查询时按ID倒序排列
+ 'get_latest_by': 'id', # xxx: 按ID字段获取最新记录
+ },
+ ),
+ ]
\ No newline at end of file
diff --git a/src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
new file mode 100644
index 00000000..0c8bccc9
--- /dev/null
+++ b/src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -0,0 +1,55 @@
+# Generated by Django 4.2.5 on 2023-09-06 13:13
+
+from django.db import migrations, models
+import django.utils.timezone
+
+# 模块级注释——accounts应用的模型更新迁移文件,用于调整`BlogUser`模型的选项、字段名称及属性,
+# 优化字段命名规范(如时间字段命名统一)、完善字段配置(如允许空值),确保模型设计更规范
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0001_initial'), # 依赖`accounts`应用的初始迁移`0001_initial`,确保`BlogUser`模型已创建
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='bloguser', # 目标模型:`accounts`应用的`BlogUser`(自定义用户模型)
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
+ # 调整模型选项:
+ # 1. get_latest_by: 按`id`字段获取最新记录
+ # 2. ordering: 查询时按`id`倒序排列(新用户在前)
+ # 3. verbose_name/verbose_name_plural: 模型单复数显示名称均为"user"
+ ),
+ migrations.RemoveField(
+ model_name='bloguser',
+ name='created_time', # 删除原有的"创建时间"字段(字段名称规范调整,后续用`creation_time`替代)
+ ),
+ migrations.RemoveField(
+ model_name='bloguser',
+ name='last_mod_time', # 删除原有的"修改时间"字段(字段名称规范调整,后续用`last_modify_time`替代)
+ ),
+ migrations.AddField(
+ model_name='bloguser',
+ name='creation_time', # 新增标准化的"创建时间"字段(替代原`created_time`)
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ # 字段配置:默认值为当前时间,后台显示名称为"creation time"
+ ),
+ migrations.AddField(
+ model_name='bloguser',
+ name='last_modify_time', # 新增标准化的"修改时间"字段(替代原`last_mod_time`)
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ # 字段配置:默认值为当前时间,后台显示名称为"last modify time"
+ ),
+ migrations.AlterField(
+ model_name='bloguser',
+ name='nickname', # 调整`nickname`(昵称)字段属性
+ field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
+ # 调整内容:允许空值(blank=True),最大长度100,后台显示名称为"nick name"
+ ),
+ migrations.AlterField(
+ model_name='bloguser',
+ name='source', # 调整`source`(创建来源)字段属性
+ field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
+ # 调整内容:允许空值(blank=True),最大长度100,后台显示名称为"create source"
+ ),
+ ]
\ No newline at end of file
diff --git a/src/accounts/accounts/migrations/__init__.py b/src/accounts/accounts/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/accounts/accounts/migrations/__pycache__/0001_initial.cpython-310.pyc b/src/accounts/accounts/migrations/__pycache__/0001_initial.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7f3ec25bac25fb7e26a5b8db07457769a1fcc05e
GIT binary patch
literal 1484
zcmY*Z&2Jk;6yKR$uh;fEahk+wODP{WOVkP{5JJ!-P5?nlMbgX4(rWD)Z+APMF|%u0
z`-DVM5t1bisI5c~MIaDDv=@XR74Uz|g#(F`UJ(a`5b*XRO**3)&HJ49e(%kQYPGDv
z_>IDYGvKWzmIhqIZ1E0dXEy
z^apHIa}d!SOpv1!OpUt*YKFznk&hjV8exg(#GvJ{LJ-UXF-dV^^f0l2XOj{slgb1G
zZ3A7UGvRDl3+G6c%%t4e$GEkeI|tko;O#_C&jURVwhLi7Tny`Efh>~xL2o9kV=5o*-u?g&y`T@+8c&J_78Xx?_;1C%TWECGrd)_pB;y9>TN@pcx$|
zd}S{$FF)@FJct92sp6XG_bHDR4#~6?df-4OmE&u3*}vN8RdR_SZE>!qPfA`mW(@%fO^i?T{T*|!;%Y035
z-{1S>tLg1u_P^XwMNh^-m(j7EB;L%aEWVpJ_P@Q`&{aur7rKYWRN+`;cbbs@Wg%0=
z-JkCqy#Jw^8F&&xaR6vz6xfHaKk>iOfKx&tNyO*}s-*GqJZmqwi>_-{_jZ|eV?Ul{s9m?eWD|g*Z$s%b(j=kBeW41&IAD>olBO67PL#8gUH2)U9(c}vdNxJ?0YU`!;#Ss*hUz8%G-MHOK?_GtyXnWnR81gu3
zAN?uq<8P!n7|8;-D#41Huvwacv0g^TzlMp?0#Z?xu5gVpuw;7@M(wu#OG{`oiTnBkjX_!gLv>D!DX$f)*+OmOgPd?C#y6Oo
zv{*aYNIHoJ9q<+wY%Ua=Lbv`yyKCqsqCG^nU>kPe2J9~EdYwY{u92HdBx?FyxP=+t
zmaXGus+hMMgke^uMJ_x0`}+@RoW?~QaOsYD7L_>@(u!ejKZrpR--{@R4$sM#Z__OE&VX6d|jR#QK$cGU!Q{
z>U=1bZK^UUIYYZc>H9L^idA{QrYr@PhB3>9r84gw@`8zDrN`@9t&KNql3lVFIurL0
zKwH9A)4e0q@g1sIp$$laG|Hq?7BSDItMr*?DU)`ZeUNRno>1=dg;e
zR{#xCK#2q!PG>?rY#0Chl{PG!{3VWMZkpr|aV^tQBUvA}P4b&GRAgo0>^i0__q%2t
yG-RvNX9#1hcBSEN9n_i)my(IQxW8VvlBBZkBOu);)tx>y)mag`kf~>b6GC=fW5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Hui&acOWl2VU
zUO-WPR%&udj7wHxUV6S$PJVh!VsdhRXn&Z
literal 0
HcmV?d00001
diff --git a/src/accounts/accounts/models.py b/src/accounts/accounts/models.py
new file mode 100644
index 00000000..d194f1b3
--- /dev/null
+++ b/src/accounts/accounts/models.py
@@ -0,0 +1,60 @@
+from django.contrib.auth.models import AbstractUser
+from django.db import models
+from django.urls import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from djangoblog.utils import get_current_site # 导入项目公共工具函数(获取当前站点域名)
+
+# 模块级注释——accounts应用的核心数据模型文件,
+# 定义自定义用户模型`BlogUser`,继承Django内置`AbstractUser`,
+# 扩展业务所需的自定义字段(如昵称、创建时间、创建来源),
+# 并重写核心方法以适配项目业务逻辑(如用户URL生成、字符串表示)
+# Create your models here.
+
+
+class BlogUser(AbstractUser):
+ """
+ 自定义用户模型,继承Django内置`AbstractUser`(保留用户名、密码、邮箱等核心字段)
+ 扩展项目所需的业务字段,适配博客系统的用户管理需求,支持国际化配置
+ """
+ # 昵称字段:支持国际化标签,最大长度100,允许空值(用户可选择不设置昵称)
+ nickname = models.CharField(_('nick name'), max_length=100, blank=True)
+ # 创建时间字段:支持国际化标签,默认值为当前时间,记录用户注册时间
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ # 最后修改时间字段:支持国际化标签,默认值为当前时间,记录用户信息最后更新时间
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+ # 创建来源字段:支持国际化标签,最大长度100,允许空值,用于标记用户注册渠道(如"adminsite"/"frontend"/"oauth")
+ source = models.CharField(_('create source'), max_length=100, blank=True)
+
+ def get_absolute_url(self):
+ """
+ 重写Django模型的`get_absolute_url`方法,获取用户的绝对路径URL
+ 关联博客系统的"作者详情页"路由,用于直接访问用户的个人主页
+ """
+ return reverse(
+ 'blog:author_detail', # 路由名称(对应blog应用的作者详情页路由)
+ kwargs={'author_name': self.username} # 路由参数:用户名(作为作者标识)
+ )
+
+ def __str__(self):
+ """
+ 重写模型的字符串表示方法,返回用户邮箱作为标识
+ 相比默认的用户名,邮箱更具唯一性,便于后台管理和日志输出时识别用户
+ """
+ return self.email
+
+ def get_full_url(self):
+ """
+ 扩展方法:获取用户个人主页的完整URL(包含站点域名)
+ 用于需要分享用户主页的场景(如邮件通知、第三方分享)
+ """
+ site = get_current_site().domain # 通过公共工具函数获取当前站点的域名(如"example.com")
+ # 拼接域名和用户绝对路径,生成完整URL(支持HTTPS协议)
+ url = "https://{site}{path}".format(site=site, path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ ordering = ['-id'] # 数据查询时默认按ID倒序排列(新注册用户优先展示)
+ verbose_name = _('user') # 模型单数显示名称(支持国际化)
+ verbose_name_plural = verbose_name # 模型复数显示名称(与单数一致,简化管理)
+ get_latest_by = 'id' # 按ID字段获取最新创建的用户记录
\ No newline at end of file
diff --git a/src/accounts/accounts/templatetags/__init__.py b/src/accounts/accounts/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/accounts/accounts/templatetags/__pycache__/__init__.cpython-310.pyc b/src/accounts/accounts/templatetags/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..56ee8decaa1577777cef804470e42dcbe8345548
GIT binary patch
literal 154
zcmd1j<>g`kf~>b6GC=fW5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Hmi&acOWl2VU
zUO-WPR%&udj7wHxUV6S$PJVh!VsdhRXxYDr>xaZG%CW?p7Ve7s&k
X typing.Optional[str]:
+ """
+ 验证验证码的有效性(核心功能:校验用户输入的验证码与缓存中存储的是否一致)
+
+ Args:
+ email: 待验证的邮箱地址(与验证码绑定的唯一标识,确保验证码针对性)
+ code: 用户输入的验证码(需与缓存中存储的验证码比对)
+
+ Return:
+ 验证失败返回错误提示字符串(支持国际化),验证成功返回None
+
+ Note:
+ 1. 原代码注释保留:当前错误处理逻辑不合理,建议改用raise抛出异常(而非返回错误字符串),
+ 便于调用方统一捕获和处理,减少错误处理冗余;
+ 2. 验证码校验逻辑:通过邮箱作为缓存key,获取存储的验证码,与输入值直接比对(大小写敏感);
+ 3. 若缓存中无该邮箱对应的验证码(如过期、未发送),则默认返回验证码错误提示。
+ """
+ # 从缓存中获取该邮箱对应的验证码(缓存key为邮箱地址,value为之前存储的验证码)
+ cache_code = get_code(email)
+ # 比对缓存中的验证码与用户输入的验证码,不一致则返回错误提示
+ if cache_code != code:
+ return gettext("Verification code error") # 国际化的验证码错误提示
+
+
+def set_code(email: str, code: str):
+ """
+ 将邮箱与验证码绑定并存储到缓存中(核心功能:为后续验证提供数据支持)
+ 存储时自动设置过期时间(与全局`_code_ttl`一致,5分钟),避免验证码永久有效
+
+ Args:
+ email: 验证码绑定的邮箱地址(作为缓存的唯一key,确保一对一关联)
+ code: 需存储的随机验证码(字符串类型,建议由`generate_code`等工具函数生成)
+ """
+ # 缓存存储:key=邮箱地址,value=验证码,timeout=有效期(秒数,由_timedelta转换)
+ cache.set(email, code, _code_ttl.seconds)
+
+
+def get_code(email: str) -> typing.Optional[str]:
+ """
+ 从缓存中获取指定邮箱对应的验证码(核心功能:为验证码校验提供数据源)
+
+ Args:
+ email: 目标邮箱地址(缓存key,用于精准获取绑定的验证码)
+
+ Return:
+ 缓存中存在该邮箱对应的验证码则返回字符串类型的验证码,否则返回None(如过期、未存储)
+ """
+ # 从缓存中获取值:key为邮箱地址,不存在或过期时返回None
+ return cache.get(email)
\ No newline at end of file
diff --git a/src/accounts/accounts/views.py b/src/accounts/accounts/views.py
new file mode 100644
index 00000000..c4e956af
--- /dev/null
+++ b/src/accounts/accounts/views.py
@@ -0,0 +1,306 @@
+import logging
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth import get_user_model
+from django.contrib.auth import logout
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.hashers import make_password
+from django.http import HttpResponseRedirect, HttpResponseForbidden
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme
+from django.views import View
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
+from django.views.generic import FormView, RedirectView
+
+# 导入项目公共工具函数:邮件发送、加密、站点信息、验证码生成、缓存删除
+from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
+from . import utils # 导入accounts应用自定义工具(验证码发送/存储)
+from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm # 导入用户交互表单
+from .models import BlogUser # 导入自定义用户模型
+
+logger = logging.getLogger(__name__) # 初始化日志对象,用于记录视图层操作日志
+
+# 模块级注释——accounts应用的核心视图文件,
+# 包含用户注册、登录、退出、邮箱验证、密码重置等核心业务的视图实现,
+# 基于Django类视图(FormView/RedirectView/View)和函数视图,
+# 整合表单验证、邮件发送、缓存操作、权限控制等逻辑,处理用户交互的全流程
+# Create your views here.
+
+
+class RegisterView(FormView):
+ """
+ 用户注册视图,继承Django FormView(专门处理表单提交的类视图)
+ 核心功能:接收注册表单数据、验证合法性、创建未激活用户、发送邮箱验证链接、重定向到结果页
+ """
+ form_class = RegisterForm # 关联注册表单类(处理用户名、邮箱、密码验证)
+ template_name = 'account/registration_form.html' # 注册页面模板路径
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ """
+ 重写分发方法,添加CSRF防护装饰器
+ 防止跨站请求伪造攻击,确保注册请求来自本网站合法表单
+ """
+ return super(RegisterView, self).dispatch(*args, **kwargs)
+
+ def form_valid(self, form):
+ """
+ 表单验证通过后的核心处理逻辑(FormView的核心方法)
+ 流程:创建未激活用户 → 生成邮箱验证链接 → 发送验证邮件 → 重定向到注册结果页
+ """
+ if form.is_valid():
+ # 1. 表单验证通过,先不提交到数据库(commit=False),后续补充字段
+ user = form.save(False)
+ user.is_active = False # 默认设置用户为未激活状态(需邮箱验证后激活)
+ user.source = 'Register' # 标记用户创建来源:前台注册(区别于后台创建/第三方登录)
+ user.save(True) # 最终保存用户数据到数据库
+
+ # 2. 生成邮箱验证链接(包含站点域名、用户ID、加密签名,确保链接安全性)
+ site = get_current_site().domain # 获取当前站点域名(如"example.com")
+ # 双重SHA256加密:结合SECRET_KEY和用户ID,防止链接被篡改
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+
+ # 开发环境适配:DEBUG模式下使用本地测试域名(127.0.0.1:8000)
+ if settings.DEBUG:
+ site = '127.0.0.1:8000'
+ path = reverse('account:result') # 验证结果页路由
+ # 拼接完整的验证链接(HTTP协议,开发环境可用;生产环境建议改为HTTPS)
+ url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
+ site=site, path=path, id=user.id, sign=sign)
+
+ # 3. 构建验证邮件内容(HTML格式,包含验证链接)
+ content = """
+ 请点击下面链接验证您的邮箱
+
+ {url}
+
+ 再次感谢您!
+
+ 如果上面链接无法打开,请将此链接复制至浏览器。
+ {url}
+ """.format(url=url)
+ # 发送验证邮件到用户注册邮箱
+ send_email(
+ emailto=[user.email], # 收件人(注册时填写的邮箱)
+ title='验证您的电子邮箱', # 邮件标题
+ content=content) # 邮件HTML内容
+
+ # 4. 重定向到注册结果页(告知用户验证邮件已发送)
+ url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
+ return HttpResponseRedirect(url)
+ else:
+ # 表单验证失败(如用户名已存在、密码不匹配),重新渲染注册页面并显示错误
+ return self.render_to_response({'form': form})
+
+
+class LogoutView(RedirectView):
+ """
+ 用户退出登录视图,继承Django RedirectView(专门处理重定向的类视图)
+ 核心功能:执行退出登录逻辑、清理缓存、重定向到登录页
+ """
+ url = '/login/' # 退出后重定向的目标URL(登录页)
+
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ """
+ 重写分发方法,添加never_cache装饰器
+ 禁止浏览器缓存退出页,避免用户后退到已登录状态
+ """
+ return super(LogoutView, self).dispatch(request, *args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ """
+ 处理GET请求(退出登录请求)
+ 流程:执行退出登录 → 清理侧边栏缓存 → 重定向到登录页
+ """
+ logout(request) # Django内置logout函数:清除用户会话,销毁登录状态
+ delete_sidebar_cache() # 清除侧边栏缓存(避免缓存中保留用户相关信息)
+ return super(LogoutView, self).get(request, *args, **kwargs)
+
+
+class LoginView(FormView):
+ """
+ 用户登录视图,继承Django FormView
+ 核心功能:接收登录表单数据、验证合法性、执行登录逻辑、处理"记住我"、重定向到目标页面
+ """
+ form_class = LoginForm # 关联自定义登录表单(适配前端样式)
+ template_name = 'account/login.html' # 登录页面模板路径
+ success_url = '/' # 登录成功后的默认重定向URL(网站根目录)
+ redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名(默认'redirect_to',用于跳转前的目标页面)
+ login_ttl = 2626560 # "记住我"的会话有效期:2626560秒 ≈ 1个月
+
+ @method_decorator(sensitive_post_parameters('password'))
+ @method_decorator(csrf_protect)
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ """
+ 重写分发方法,添加3个核心装饰器:
+ 1. sensitive_post_parameters('password'):保护密码字段,避免在错误报告中泄露
+ 2. csrf_protect:防跨站请求伪造攻击
+ 3. never_cache:禁止浏览器缓存登录页,确保每次请求都是最新状态
+ """
+ return super(LoginView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ """
+ 补充模板上下文数据(传递重定向地址给模板)
+ 用于登录成功后跳转到登录前的目标页面(如访问需要登录的页面时,先跳转登录,登录后返回原页面)
+ """
+ # 从GET请求中获取重定向地址(如?redirect_to=/article/1/)
+ redirect_to = self.request.GET.get(self.redirect_field_name)
+ if redirect_to is None:
+ redirect_to = '/' # 无重定向地址时,默认跳转到根目录
+ kwargs['redirect_to'] = redirect_to # 将重定向地址添加到上下文
+ return super(LoginView, self).get_context_data(** kwargs)
+
+ def form_valid(self, form):
+ """
+ 表单验证通过后的核心登录逻辑
+ 流程:验证表单 → 清理缓存 → 执行登录 → 处理"记住我" → 重定向
+ """
+ # 重新初始化Django内置AuthenticationForm(确保使用正确的认证逻辑)
+ form = AuthenticationForm(data=self.request.POST, request=self.request)
+
+ if form.is_valid():
+ delete_sidebar_cache() # 清理侧边栏缓存(更新用户相关的缓存数据)
+ logger.info(self.redirect_field_name) # 记录重定向字段名到日志
+
+ # 执行登录:将用户信息存入会话,创建登录状态
+ auth.login(self.request, form.get_user())
+ # 处理"记住我"选项:若勾选,则设置会话有效期为1个月
+ if self.request.POST.get("remember"):
+ self.request.session.set_expiry(self.login_ttl)
+ # 调用父类form_valid方法,执行重定向
+ return super(LoginView, self).form_valid(form)
+ else:
+ # 表单验证失败(如用户名/密码错误),重新渲染登录页并显示错误
+ return self.render_to_response({'form': form})
+
+ def get_success_url(self):
+ """
+ 自定义登录成功后的重定向URL
+ 核心:校验重定向地址的合法性,避免恶意重定向攻击
+ """
+ # 从POST请求中获取目标重定向地址(登录表单中隐藏字段传递)
+ redirect_to = self.request.POST.get(self.redirect_field_name)
+ # 校验重定向地址是否合法(仅允许本网站域名下的地址)
+ if not url_has_allowed_host_and_scheme(
+ url=redirect_to, allowed_hosts=[self.request.get_host()]):
+ redirect_to = self.success_url # 非法地址则使用默认重定向地址(根目录)
+ return redirect_to
+
+
+def account_result(request):
+ """
+ 账号操作结果展示函数视图
+ 处理两种场景:1. 注册成功后的提示 2. 邮箱验证后的激活与提示
+ 核心:根据URL参数区分场景,验证链接合法性,激活用户,渲染结果页面
+ """
+ # 从GET请求中获取操作类型(register/validation)和用户ID
+ type = request.GET.get('type')
+ id = request.GET.get('id')
+
+ # 根据用户ID查询用户,不存在则返回404页面
+ user = get_object_or_404(get_user_model(), id=id)
+ logger.info(type) # 记录操作类型到日志
+
+ # 若用户已激活,直接重定向到首页(避免重复验证)
+ if user.is_active:
+ return HttpResponseRedirect('/')
+
+ # 仅处理注册和验证两种合法操作类型
+ if type and type in ['register', 'validation']:
+ if type == 'register':
+ # 场景1:注册成功提示(告知用户验证邮件已发送)
+ content = '''
+ 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
+ '''
+ title = '注册成功'
+ else:
+ # 场景2:邮箱验证(校验链接签名合法性,激活用户)
+ # 重新计算签名(与注册时的加密规则一致)
+ c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ sign = request.GET.get('sign') # 从URL中获取传递的签名
+
+ # 签名不匹配则返回403禁止访问(防止恶意篡改链接)
+ if sign != c_sign:
+ return HttpResponseForbidden()
+
+ # 签名匹配,激活用户(设置is_active为True)
+ user.is_active = True
+ user.save()
+ # 验证成功提示
+ content = '''
+ 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
+ '''
+ title = '验证成功'
+ # 渲染结果页面,传递标题和内容
+ return render(request, 'account/result.html', {
+ 'title': title,
+ 'content': content
+ })
+ else:
+ # 非法操作类型,重定向到首页
+ return HttpResponseRedirect('/')
+
+
+class ForgetPasswordView(FormView):
+ """
+ 忘记密码重置视图,继承Django FormView
+ 核心功能:接收密码重置表单(新密码、验证码、邮箱)、验证合法性、更新用户密码、重定向到登录页
+ """
+ form_class = ForgetPasswordForm # 关联忘记密码表单(处理密码强度、验证码、邮箱验证)
+ template_name = 'account/forget_password.html' # 密码重置页面模板路径
+
+ def form_valid(self, form):
+ """
+ 表单验证通过后的密码重置逻辑
+ 流程:查询用户 → 哈希新密码 → 保存更新 → 重定向到登录页
+ """
+ if form.is_valid():
+ # 根据表单验证后的邮箱查询用户
+ blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
+ # 哈希处理新密码(make_password自动使用Django配置的哈希算法,避免明文存储)
+ blog_user.password = make_password(form.cleaned_data["new_password2"])
+ blog_user.save() # 保存更新后的密码
+ return HttpResponseRedirect('/login/') # 密码重置成功,重定向到登录页
+ else:
+ # 表单验证失败(如验证码错误、密码不匹配),重新渲染页面并显示错误
+ return self.render_to_response({'form': form})
+
+
+class ForgetPasswordEmailCode(View):
+ """
+ 忘记密码验证码发送视图,继承Django View
+ 核心功能:接收邮箱 → 验证邮箱合法性 → 生成验证码 → 发送验证邮件 → 存储验证码到缓存
+ """
+
+ def post(self, request: HttpRequest):
+ """
+ 处理POST请求(接收邮箱,发送验证码)
+ """
+ # 初始化忘记密码验证码表单,验证请求数据
+ form = ForgetPasswordCodeForm(request.POST)
+ if not form.is_valid():
+ return HttpResponse("错误的邮箱") # 表单验证失败(如邮箱格式错误),返回错误提示
+
+ # 表单验证通过,获取目标邮箱
+ to_email = form.cleaned_data["email"]
+
+ # 生成随机验证码(项目工具函数)
+ code = generate_code()
+ # 发送验证码邮件(调用accounts应用自定义工具函数)
+ utils.send_verify_email(to_email, code)
+ # 存储验证码到缓存(key=邮箱,value=验证码,有效期5分钟,由utils模块的_code_ttl控制)
+ utils.set_code(to_email, code)
+
+ return HttpResponse("ok") # 验证码发送成功,返回"ok"提示n
\ No newline at end of file