侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Pycharm+django2.2+python3.6+MySQL实现简单的考试报名系统

Python  /  管理员 发布于 7年前   232

1 准备工作

1.1 环境搭建

1.1.1 安装python3.6

python安装官网

1.1.2 安装django2.2

pip install django(==2.2.0) //不加版本默认安装最新版

1.1.3 安装pycharm(社区版,官网下载安装即可)

在安装完成后要配置好需要的第三方库:(pip下载,推荐在pycharm下也配置虚拟环境)

Django2.2

连接mysql需要的库:PyMySQL, mysql, mysqlclinet

验证码用到的库:django-simple-captcha(只需在虚拟环境下配置)

(由于下载库较多,忘记用到的库,下附截图)

1.1.4 安装数据库,

我使用的是MySQL,推荐安装界面管理文件(我使用的是MySQLWorkbench)数据库配置,settings.py文件

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME':'cet',    #数据库名字 'USER':'root',   #登陆数据库的用户名 'PASSWORD':'123',   #登陆数据库的密码 'HOST':'localhost',   #数据库的ip地址 'PORT':'3306',   #ip地址的端口号,默认(3306) }}

__init__.py里面导入pymysql

import pymysql pymysql.install_as_MySQLdb()

1.2 创建django项目及app

1.2.1 创建指令

django-admin startproject project_name #创建项目python manage.py startapp app_name  #创建app(可能会报错)#上面创建app失败用下面这个指令django-admin startapp app_name  1.2.2 注册appINSTALLED_APPS = [ 'django.contrib.admin',   'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'login',   #登录注册 'captcha',  #验证码 'home',   #报名主页]

1.4 更改时区和语言

settings.py文件中,将默认改为亚洲/上海和中文

LANGUAGE_CODE = 'zh-hans'TIME_ZONE = 'Asia/Shanghai'USE_I18N = TrueUSE_L10N = TrueUSE_TZ = False

1.5 数据库迁移

更改models.py后,要将更改同步到数据库之中,这时就要用到数据库迁移指令(若迁移失败:no changes问题)。

python manage.py makemigrationspython manage.py migrate

1.6 创建超级管理员

命令行冲使用指令创建管理员账号用于使用django自带的框架。

python manage.py createsuperuser

1.7 运行准备

添加端口号:

2 注册登录模块

(我将其放在了app名为login里面)

2.1 数据库模型设计

特殊参数说明:verbose_name――用于修改django框架各表成员的名字(相当于副名,只用于显示),其他可以从文章

开头推荐的博客了解。

#login/models.pyfrom django.db import models# Create your models here.class User(models.Model): '''用户表''' gender = ( ('male', '男'), ('female', '女'), ) name = models.CharField(verbose_name="用户名", max_length=128, unique=True) # unique表示唯一 password = models.CharField(verbose_name="密码", max_length=256) email = models.EmailField(verbose_name="邮箱", unique=True) sex = models.CharField(verbose_name="性别", max_length=32, choices=gender, default='男') c_time = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name #用于将django自带管理员端汉化 class Meta: ordering = ['c_time'] verbose_name = '用户' verbose_name_plural = '用户'

2.2 在admin中注册模型

#login/admin.pyfrom django.contrib import admin# Register your models here.from . import modelsadmin.site.register(models.User)

2.3 创建表单

在我们创建数据库用户表后,采用表单验证前端的数据并进行存储到数据库中。

#login/forms.pyfrom django import formsfrom captcha.fields import CaptchaFieldclass user_entry(forms.Form): user_name = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) user_true_name = forms.CharField(label="真实姓名", max_length=128,widget=forms.TextInput(attrs={'class': 'form-control'})) user_id = forms.CharField(label="身份证号", max_length=18, widget=forms.TextInput(attrs={'class': 'form-control'})) email = forms.EmailField(label="邮箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'})) exam_point = forms.CharField(label="考点", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'}))class user_datas(forms.Form): email = forms.EmailField(label="邮箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'})) user_name = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) user_true_name = forms.CharField(label="真实姓名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) user_id = forms.CharField(label="身份证号", max_length=18, widget=forms.TextInput(attrs={'class': 'form-control'})) # user_img = user_school = forms.CharField(label="所在学校", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) # user_f_score = forms.CharField(label="四级成绩", max_length=10, widget=forms.TextInput(attrs={'class': 'form-control'})) # user_s_score = forms.CharField(label="六级成绩", max_length=10, widget=forms.TextInput(attrs={'class': 'form-control'}))

2.4 路由及视图设计

2.4.1 路由设计

由于登陆注册使用的路径不多,就将其全放在项目目录下的urls.py文件,当然也可以采用项目和app相结合。

#项目名/urls.pyfrom django.contrib import adminfrom django.urls import path,includefrom login import viewsurlpatterns = [ path('', views.index),  #修改端口默认主页 path('admin/', admin.site.urls), #管理员端 #登录注册 path('index/', views.index), path('check/', views.check), path('login/', views.login), path('register/', views.register), path('logout/', views.logout), #使用验证码 path('captcha', include('captcha.urls')), #报名app路由 path('homepage/',include('home.urls')),]

2.4.2 试图设计

#login/views.pyfrom django.shortcuts import render, redirectfrom . import modelsfrom .forms import UserForm,RegisterFormfrom home.models import user_data# Create your views here.def check(request): pass return render(request, 'login/hello.html')def index(request): pass return render(request, 'login/index.html')#加入sessondef login(request): #不允许重复登录 if request.session.get('is_login', None): return redirect('/index') if request.method == "POST": login_form = UserForm(request.POST) message = "请检查填写的内容!" if login_form.is_valid():  username = login_form.cleaned_data['username']  password = login_form.cleaned_data['password']  try:  user = models.User.objects.get(name=username)  if user.password == password:   #往session字典内写入用户状态和数据   request.session['is_login'] = True   request.session['user_id'] = user.id   request.session['user_name'] = user.name   return redirect('/index/')  else:   message = "密码不正确!"  except:  message = "用户不存在!" return render(request, 'login/login.html', locals()) login_form = UserForm() return render(request, 'login/login.html', locals())def logout(request): if not request.session.get('is_login', None): # 如果本来就未登录,也就没有登出一说 return redirect("/index/") request.session.flush() # 或者使用下面的方法 # del request.session['is_login'] # del request.session['user_id'] # del request.session['user_name'] return redirect("/index/")def register(request): if request.session.get('is_login', None): # 登录状态不允许注册。 return redirect("/index/") if request.method == "POST": register_form = RegisterForm(request.POST) message = "请检查填写的内容!" if register_form.is_valid(): # 获取数据  username = register_form.cleaned_data['username']  password1 = register_form.cleaned_data['password1']  password2 = register_form.cleaned_data['password2']  email = register_form.cleaned_data['email']  sex = register_form.cleaned_data['sex']  if password1 != password2: # 判断两次密码是否相同  message = "两次输入的密码不同!"  return render(request, 'login/register.html', locals())  else:  same_name_user = models.User.objects.filter(name=username)  if same_name_user: # 用户名唯一   message = '用户已经存在,请重新选择用户名!'   return render(request, 'login/register.html', locals())  same_email_user = models.User.objects.filter(email=email)  if same_email_user: # 邮箱地址唯一   message = '该邮箱地址已被注册,请使用别的邮箱!'   return render(request, 'login/register.html', locals())  message = "注册成功!"  # 当一切都OK的情况下,创建新用户  # 创建用户信息//有问题:放在创建用户表后面会出现DJANGO pymysql.err.IntegrityError:  # (1062, "Duplicate entry '' for key 'user_name'")  new_user_data = user_data.objects.create()  new_user_data.user_name = username  new_user_data.user_true_name = '无'  new_user_data.user_id = '无'  new_user_data.user_school = '无'  # new_user_data.user_f_score = 425  # new_user_data.user_s_score = 0  new_user_data.save()  #创建用户表  new_user = models.User.objects.create()  new_user.name = username  new_user.password = password1  new_user.email = email  new_user.sex = sex  new_user.save()  return redirect('/login/') # 自动跳转到登录页面 register_form = RegisterForm() return render(request, 'login/register.html', locals())

3 个人信息及报名管理

(我将其放在了app名为home里面)

3.1 数据库模型设计

#home/models.pyfrom django.db import models# Create your models here.class exam_entry_table(models.Model): #考点信息表 exam_id = models.CharField(verbose_name="考试编号",max_length=10) exam_type = models.CharField(verbose_name="考试类别",max_length=128) exam_point = models.CharField(verbose_name="考点",max_length=128) exam_time = models.CharField(verbose_name="考试时间",max_length=128) number = models.IntegerField(verbose_name="容量") entry_number = models.IntegerField(verbose_name="已报名数量",default=0) def __str__(self): return self.exam_point class Meta: # ordering = ['c_time'] verbose_name = '考点' verbose_name_plural = '考点信息表'class user_entry_table(models.Model): #用户考试信息表 email = models.EmailField(verbose_name="邮箱") exam_id = models.CharField(verbose_name="考试编号",max_length=10) exam_point = models.CharField(verbose_name="考点",max_length=128) def __str__(self): return self.email class Meta: # ordering = ['c_time'] verbose_name = '用户考试信息' verbose_name_plural = '用户考试信息表'class user_data(models.Model): #用户信息表 user_name = models.CharField(verbose_name="用户名",max_length=128, unique=True) user_true_name = models.CharField(verbose_name="真实姓名",max_length=128, null=True) user_id = models.CharField(verbose_name="身份证号",max_length=18, null=True) # user_img = user_school = models.CharField(verbose_name="在读学校",max_length=128) user_f_score = models.IntegerField(verbose_name="四级成绩", default=0) user_s_score = models.IntegerField(verbose_name="六级成绩", default=0) def __str__(self): return self.user_name class Meta: # ordering = ['c_time'] verbose_name = '用户名' verbose_name_plural = '用户信息表'

3.2 注册模型

#home/admin.pyfrom django.contrib import admin# Register your models here.from . import modelsadmin.site.register(models.exam_entry_table) #考点信息表admin.site.register(models.user_entry_table) #用户考试信息表admin.site.register(models.user_data)  #用户信息表

3.3 创建表单

#home/forms.pyfrom django import formsfrom captcha.fields import CaptchaFieldclass UserForm(forms.Form): username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) captcha = CaptchaField(label='验证码')class RegisterForm(forms.Form): gender = (  ('male', "男"),  ('female', "女"), ) username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) password1 = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) password2 = forms.CharField(label="确认密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) email = forms.EmailField(label="邮箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'})) sex = forms.ChoiceField(label='性别', choices=gender) captcha = CaptchaField(label='验证码')

3.4 路由及视图设计

3.4.1 路由设计

这里用到的路劲较为复杂,采用项目urls和app的urls结合起来避免项目的urls过于拥挤。

先修改项目的urls.py文件:

#项目名/urls.pypath('homepage/',include('home.urls')), #前面已经添加了,这里只做说明

再修改app中的urls.py文件:

#home/urls.pyfrom django.contrib import adminfrom django.urls import path,includefrom home import viewsurlpatterns = [ path('test/', views.test), path('my_data/',views.mydate),   #我的信息 path('query_results/',views.query), #查询报告信息 path('cet_4/',views.cet_4),   #四级报名 path('cet_6/',views.cet_6),   #六级报名 path('change_my_data/',views.updata),]

3.4.2 试图设计

#home/views.pyfrom django.shortcuts import render, redirectfrom . import modelsfrom .forms import user_entry, user_datasfrom django.http import HttpResponse,HttpResponseRedirect# from django.contrib.auth.models import Userfrom login.models import User# Create your views here.def test(request): pass return render(request, 'home/test.html')#我的信息def mydate(request): username = request.session.get('user_name', None) account = User.objects.get(name=username)#用户登录注册表 user = models.user_data.objects.get(user_name= username) return render(request,"home/myself.html",{"user":user, "account":account})#修改我的信息def updata(request): username = request.session.get('user_name', None) # print("-----------------------") # print(username) # print("-----------------------") user_da = models.user_data.objects.get(user_name=username) user = User.objects.get(name=username)#login_user表 if request.method == "POST":  userdata = user_datas(request.POST)  # print(userdata)  if userdata.is_valid():   user_das = userdata.cleaned_data   # user.user_name = user_da['user_name'] #用户无法修改用户名   user.email = user_das['email']   user_da.user_true_name = user_das['user_true_name']   user_da.user_id = user_das['user_id']   user_da.user_school = user_das['user_school']   user_da.save()   user.save()   # 四六级成绩无法修改   # user_datas.user_f_score = user_da['user_f_score']   # user_datas.user_s_score = user_da['user_s_score']   return redirect('/homepage/my_data/') else:  userdata = user_datas(initial={"email":user.email,"user_name":user_da.user_name,"user_true_name":user_da.user_true_name,"user_id":user_da.user_id,"user_school":user_da.user_school})  return render(request, 'home/change_mydata.html', {"userdata":userdata})#查询考试信息 还没完成,优化,条件判断def query(request): username = request.session.get('user_name',None) user = User.objects.get(name=username) #用于判断用户是否报名 user_en = models.user_entry_table.objects.filter(email=user.email) # print("********************") # print(user_en) # print(user_en[0]) if user_en:  # print(len(user_en))  if len(user_en)==1:   user_entry = models.user_entry_table.objects.get(email=user.email)   user_point = user_entry.exam_point   user_eid = user_entry.exam_id   exam_entry = models.exam_entry_table.objects.get(exam_point=user_point, exam_id=user_eid)   return render(request, 'home/query1.html', {"exam_entry": exam_entry})  else:   user_entry4 = models.user_entry_table.objects.get(email=user.email, exam_id=0)   user_entry6 = models.user_entry_table.objects.get(email=user.email, exam_id=1)   user_point4 = user_entry4.exam_point   user_point6 = user_entry6.exam_point   exam_entry4 = models.exam_entry_table.objects.get(exam_point=user_point4, exam_id=0)   exam_entry6 = models.exam_entry_table.objects.get(exam_point=user_point6, exam_id=1)   return render(request, 'home/query2.html', {"exam_entry4": exam_entry4, "exam_entry6":exam_entry6}) else:  # message = "你还未报名!请先报名之后再来查看!"  # return render(request, 'login/index.html', locals())  user_da = models.user_data.objects.get(user_name=user.name)  school = user_da.user_school  if school=='无':   message = "请先更新你的学校信息!"   return render(request, 'login/index.html', locals())  else:   point = models.exam_entry_table.objects.filter(exam_point=school)   if point:    if len(point)==1:     exam = models.exam_entry_table.objects.get(exam_point=school)     return render(request, 'home/exam1.html', {"exam": exam})    else:     exam4 = models.exam_entry_table.objects.get(exam_point=school, exam_id=0)     exam6 = models.exam_entry_table.objects.get(exam_point=school, exam_id=1)     return render(request, 'home/exam2.html', {"exam4": exam4, "exam6": exam6})   else:    message="你的学校还未开放报名!详情请咨询学校相关部门!"    return render(request, 'login/index.html', locals())#四级报名def cet_4(request): username = request.session.get('user_name', None) # 用户信息表,用户表,获取信息判断资格 user_da = models.user_data.objects.get(user_name=username) user = User.objects.get(name=username) if request.method == "POST":  cet4_form = user_entry(request.POST)  if cet4_form.is_valid():   # 四级考试对分数无要求   # 只需要获取报考考点信息即可   # email = cet4_form.cleaned_data['email']   exam_id = '0'   exam_point = cet4_form.cleaned_data['exam_point']   #用与下面的考点判断   point = models.exam_entry_table.objects.filter(exam_point=exam_point, exam_id='0')   # 用与下面的是否重复报名判断   entryer = models.user_entry_table.objects.filter(email=user.email, exam_id=exam_id)   #判断个人信息是否完善   if user_da.user_true_name=='无'or user_da.user_id=='无':    message="请先完善个人真实信息之后再来报名!"    return render(request, 'home/cet_4.html', locals())   # 判断是否重复报名   # print("判断是否重复报名")   elif entryer:    message = "请勿重复报名!"    return render(request, 'home/cet_4.html', locals())   elif point:    # 考点存在    # print("没有重复报名")    message = "报名成功!请按时参加考试!"    # 创建一条数据    new_user = models.user_entry_table.objects.create()    new_user.email = user.email    new_user.exam_id = exam_id    new_user.exam_point = exam_point    new_user.save()    # 考点容量减1,报考人数加1    exam_entry = models.exam_entry_table.objects.get(exam_point=exam_point, exam_id=exam_id)    exam_entry.number -= 1    exam_entry.entry_number += 1    exam_entry.save()    return render(request, 'home/cet_4.html', locals())   else:    message = "该学校还未开放报名!详情请咨询学校相关部门!"    return render(request, 'home/cet_4.html', locals()) cet4_form = user_entry(initial={"email": user.email, "user_name": user_da.user_name, "user_true_name": user_da.user_true_name,     "user_id": user_da.user_id}) return render(request, 'home/cet_4.html', locals())#六级报名:def cet_6(request): username = request.session.get('user_name', None) # 用户信息表,用户表,获取信息判断资格 user_da = models.user_data.objects.get(user_name=username) user = User.objects.get(name=username) if request.method == "POST":  cet6_form = user_entry(request.POST)  if cet6_form.is_valid():   # 只需要获取报考考点信息即可   # email = cet4_form.cleaned_data['email']   exam_id = '1'   exam_point = cet6_form.cleaned_data['exam_point']   f_score = user_da.user_f_score   # 用与下面的考点判断   point = models.exam_entry_table.objects.filter(exam_point=exam_point, exam_id=exam_id)   # 用与下面的是否重复报名判断   entryer = models.user_entry_table.objects.filter(email=user.email, exam_id=exam_id)   # 判断个人信息是否完善   if user_da.user_true_name=='无'or user_da.user_id=='无':    message="请先完善个人真实信息之后再来报名!"    return render(request, 'home/cet_6.html', locals())    # 判断是否重复报名   elif entryer:    # print("判断是否重复报名")    message = "请勿重复报名!"    return render(request, 'home/cet_6.html', locals())   # 判断考点是否存在   elif point:    # 考点存在    #判断四级成绩是否合格    if f_score >= 425:     # print("报名成功!请按时参加考试!")     # 创建一条数据     message = "报名成功!请按时参加考试!"     new_user = models.user_entry_table.objects.create()     new_user.email = user.email     new_user.exam_id = exam_id     new_user.exam_point = exam_point     new_user.save()     # 考点容量减1,报考人数加1     exam_entry = models.exam_entry_table.objects.get(exam_point=exam_point, exam_id=exam_id)     exam_entry.number -= 1     exam_entry.entry_number += 1     exam_entry.save()     return render(request, 'home/cet_6.html', locals())    else:     message = "四级成绩大于425才能报名六级考试!"     return render(request, 'home/cet_6.html', locals())   else:    message = "该学校还未开放报名!详情请咨询学校相关部门!"    return render(request, 'home/cet_6.html', locals()) cet6_form = user_entry(  initial={"email": user.email, "user_name": user_da.user_name, "user_true_name": user_da.user_true_name,     "user_id": user_da.user_id}) return render(request, 'home/cet_6.html', locals())

到这里基本的后端都实现了。

4 项目最终框架展示

5 总结

成果展示:

注册:

登录:

主页:

管理端:

数据库设计问题:

由于此次设计在开始之时没有设计完善,数据库各表之间的关联存在问题,例如本次报名系统而言,用户信息最好添加手机号码,我本打算使用邮箱即可,但是推荐使用联系更快的电话号码等等问题。

附录

整体项目包地址链接: https://pan.baidu.com/s/130AP7coMP_U2q_dzaazUWA 提取码: nywa

总结

以上所述是小编给大家介绍的Pycharm+django2.2+python3.6+MySQL实现简单的考试报名系统,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!


  • 上一条:
    Python图像处理模块ndimage用法实例分析
    下一条:
    浅谈Python_Openpyxl使用(最全总结)
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在python语言中Flask框架的学习及简单功能示例(0个评论)
    • 在Python语言中实现GUI全屏倒计时代码示例(0个评论)
    • Python + zipfile库实现zip文件解压自动化脚本示例(0个评论)
    • python爬虫BeautifulSoup快速抓取网站图片(1个评论)
    • vscode 配置 python3开发环境的方法(0个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2018-04
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2022-01
    • 2023-07
    • 2023-10
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客