当前位置:首页 > 技术 > 正文内容

BBS 项目开发笔记(一):模型设计、注册与登录

访客 技术 2026年6月21日 1

项目启动

本项目模仿博客园的基本架构,构建一个简易的电子公告板(BBS)系统。

需求分析

主要功能

  • 首页:展示文章列表与站内导航。
  • 用户系统:支持注册、登录、密码修改、头像更换。
  • 个人站点:用户可申请专属站点,并完成文章的增、删、改操作。
  • 文章浏览:首页展示全部文章,个人站点展示本站文章。支持按分类、标签、日期归档筛选。
  • 文章详情:展示文章完整内容,支持点赞、点踩、评论功能。

数据结构

根据功能需求,我们识别出以下核心实体及其关系:

  1. 用户:拥有站点创建、管理权限,以及文章浏览、交互权限。
  2. 个人站点:与用户是一对一关系,用户可选择是否创建。
  3. 文章:属于某个站点,可被所有用户查看、点赞、评论。
  4. 文章分类:属于站点,文章可属于一个分类。
  5. 文章标签:属于站点,文章与标签是多对多关系。
  6. 点赞/点踩记录:关联用户与文章,记录用户操作。
  7. 评论:关联用户与文章。支持自关联外键,实现子评论功能。

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 后台提供直观的增删改查界面,便于测试和初期数据填充。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。