BBS 项目开发笔记(一):模型设计、注册与登录
项目启动
本项目模仿博客园的基本架构,构建一个简易的电子公告板(BBS)系统。
需求分析
主要功能
- 首页:展示文章列表与站内导航。
- 用户系统:支持注册、登录、密码修改、头像更换。
- 个人站点:用户可申请专属站点,并完成文章的增、删、改操作。
- 文章浏览:首页展示全部文章,个人站点展示本站文章。支持按分类、标签、日期归档筛选。
- 文章详情:展示文章完整内容,支持点赞、点踩、评论功能。
数据结构
根据功能需求,我们识别出以下核心实体及其关系:
- 用户:拥有站点创建、管理权限,以及文章浏览、交互权限。
- 个人站点:与用户是一对一关系,用户可选择是否创建。
- 文章:属于某个站点,可被所有用户查看、点赞、评论。
- 文章分类:属于站点,文章可属于一个分类。
- 文章标签:属于站点,文章与标签是多对多关系。
- 点赞/点踩记录:关联用户与文章,记录用户操作。
- 评论:关联用户与文章。支持自关联外键,实现子评论功能。
Django 项目初始化
配置项目设置,包括数据库、模板路径、语言和时区。
# settings.py
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True # 数据库存储 UTC 时间,展现时自动转换
功能模块与路由
项目按功能拆分为多个应用:
- datas_administer:集中存放模型表。
- user_app:用户注册、登录、密码修改、注销、头像管理。
- public_app:首页、个人站点展示、文章详情、点赞点踩、评论。
- backend_app:个人站点后台管理,文章和分类标签的增删改。
路由分发配置:
# urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('user/', include('user_app.urls')),
path('public/', include('public_app.urls')),
path('backend/', include('backend_app.urls')),
]
七张核心模型表
根据数据结构分析,定义以下模型:
# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
tel = models.CharField(max_length=32, verbose_name='电话号码')
avatar = models.FileField(upload_to='avatar/', verbose_name='个人头像')
site = models.ForeignKey(to='Site', on_delete=models.SET_NULL, null=True, blank=True)
class Site(models.Model):
name = models.CharField(max_length=32, verbose_name='站点域名')
title = models.CharField(max_length=32, verbose_name='站点标题')
theme = models.FileField(upload_to='css/', verbose_name='站点样式')
class Article(models.Model):
title = models.CharField(max_length=32, verbose_name='文章标题')
brief = models.CharField(max_length=100, verbose_name='文章简介')
content = models.TextField(verbose_name='文章内容')
publish_time = models.DateField(auto_now_add=True, verbose_name='发布日期')
like_num = models.IntegerField(default=0, verbose_name='点赞数')
unlike_num = models.IntegerField(default=0, verbose_name='点踩数')
comment_num = models.IntegerField(default=0, verbose_name='评论数')
site = models.ForeignKey(to='Site', on_delete=models.CASCADE)
category = models.ForeignKey(to='Category', on_delete=models.SET_NULL, null=True)
tags = models.ManyToManyField(to='Tag')
class Category(models.Model):
name = models.CharField(max_length=32, verbose_name='分类名称')
site = models.ForeignKey(to='Site', on_delete=models.CASCADE)
class Tag(models.Model):
name = models.CharField(max_length=32, verbose_name='标签名称')
site = models.ForeignKey(to='Site', on_delete=models.CASCADE)
class LikeOrNot(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
is_like = models.BooleanField(verbose_name='点赞或点踩')
class Comment(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
content = models.TextField(max_length=100, verbose_name='评论内容')
comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True, blank=True, verbose_name='父评论')
设计要点:
- 通过
AbstractUser快速扩展用户模型。 - 文章模型的点赞、点踩、评论数作为冗余字段,需同步更新。
- 评论表通过自关联实现嵌套回复。
注册功能
注册功能采用 AJAX 提交,提升用户体验。
视图层逻辑
# views.py
def register(request):
form_obj = RegisterForm()
if request.method == 'POST':
form_obj = RegisterForm(request.POST)
msg = {'code': 10000, 'msg': '', 'errors': {}}
if form_obj.is_valid():
cleaned_data = form_obj.cleaned_data
cleaned_data.pop('confirm_password')
if request.FILES.get('avatar'):
cleaned_data['avatar'] = request.FILES.get('avatar')
UserInfo.objects.create_user(**cleaned_data)
msg['url'] = reverse('login')
else:
msg['code'] = 10001
msg['msg'] = form_obj.errors
return JsonResponse(msg)
return render(request, 'register.html', locals())
模板层与前端交互
模板中包含表单、图片预览与 AJAX 提交逻辑:
<form id="regForm">
{% csrf_token %}
{% for field in form_obj %}
<div class="form-group">
<label for="{{ field.auto_id }}">{{ field.label }}</label>
{{ field }}
<span class="help-block"></span>
</div>
{% endfor %}
<div class="form-group">
<label>头像预览
<img id="avatarPreview" src="{% static 'avatar/default.png' %}" style="width:100px;">
</label>
<input type="file" id="avatarInput" name="avatar" style="display:none;">
</div>
<input type="button" id="submitBtn" value="提交注册" class="btn btn-primary">
</form>
<script>
$('#submitBtn').click(function() {
var fd = new FormData();
$.each($('#regForm').serializeArray(), function(i, field) {
fd.append(field.name, field.value);
});
fd.append('avatar', $('#avatarInput')[0].files[0]);
$.ajax({
url: '',
type: 'post',
data: fd,
contentType: false,
processData: false,
success: function(res) {
if (res.code === 10000) {
alert('注册成功');
window.location.href = res.url;
} else {
$.each(res.msg, function(field, errors) {
$('#id_' + field).next().text(errors[0]).parent().addClass('has-error');
});
}
}
});
});
// 头像预览
$('#avatarInput').change(function() {
var reader = new FileReader();
reader.onload = function(e) {
$('#avatarPreview').attr('src', e.target.result);
};
reader.readAsDataURL(this.files[0]);
});
// 错误清除
$('input').focus(function() {
$(this).next().text('').parent().removeClass('has-error');
});
</script>
表单组件
# myforms.py
from django import forms
from django.core.validators import RegexValidator
from . import models
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3, max_length=8, label='用户名',
widget=forms.TextInput(attrs={'class': 'form-control'}))
password = forms.CharField(min_length=3, max_length=8, label='密码',
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
confirm_password = forms.CharField(label='确认密码',
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
email = forms.EmailField(label='邮箱',
widget=forms.EmailInput(attrs={'class': 'form-control'}))
tel = forms.CharField(label='电话', required=False,
validators=[RegexValidator(r'^1[0-9]{4}$', '号码格式错误')],
widget=forms.TextInput(attrs={'class': 'form-control'}))
def clean_username(self):
name = self.cleaned_data.get('username')
if models.UserInfo.objects.filter(username=name).exists():
raise forms.ValidationError('用户名已存在')
return name
def clean(self):
pwd = self.cleaned_data.get('password')
confirm = self.cleaned_data.get('confirm_password')
if pwd and pwd != confirm:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data
登录与认证
登录功能使用 Django 内置认证,并增加验证码校验。
登录视图
def login(request):
error = ''
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code').upper()
if code == request.session.get('code', '').upper():
user = auth.authenticate(username=username, password=password)
if user:
auth.login(request, user)
return redirect('home')
else:
error = '用户名或密码错误'
else:
error = '验证码错误'
return render(request, 'login.html', {'error': error})
验证码生成
from PIL import Image, ImageFont, ImageDraw
from io import BytesIO
import random
def get_code(request):
img = Image.new('RGB', (250, 30), (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))
font = ImageFont.truetype('static/fonts/CHILLER.TTF', 25)
draw = ImageDraw.Draw(img)
code = ''
for i in range(5):
char = random.choice([str(random.randint(0,9)), chr(random.randint(65,90)), chr(random.randint(97,122))])
draw.text((i*45 + 40, random.randint(-2, 2)), char, font=font)
code += char
request.session['code'] = code
buf = BytesIO()
img.save(buf, 'png')
return HttpResponse(buf.getvalue())
前端验证码刷新
点击验证码图片可刷新:
$('#code_img').click(function() {
this.src += '?';
});
此外,还实现了密码修改(模态框)和注销功能,此处不再赘述。
Admin 后台管理
使用 Django 自带的 admin 模块快速管理数据。
在 admin.py 中注册模型:
from django.contrib import admin
from . import models
admin.site.register(models.UserInfo)
admin.site.register(models.Site)
admin.site.register(models.Article)
# 等
通过定义 __str__ 和 Meta 内部类自定义显示名称:
class UserInfo(AbstractUser):
# ...
def __str__(self):
if self.is_superuser:
return f'超级用户:{self.username}'
return f'用户:{self.username}'
class Meta:
verbose_name_plural = '用户信息'
Admin 后台提供直观的增删改查界面,便于测试和初期数据填充。