BBS 页面搭建知识点整理
表关系图及建表
from django.db import models
# Create your models here.
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
phone = models.BigIntegerField(null=True,blank=True) # 告诉django后台这个字段可以不填
# avatar 存的是用户的头像文件路径,用户上传的头像会自动保存到avatar文件下
avatar = models.FileField(upload_to='avatar/', default='media/avatar/default.jpg')
create_time = models.DateField(auto_now_add=True)
blog = models.OneToOneField(to='Blog', null=True)
# class Meta:
# verbose_name_plural = '用户表'
# verbose_name = '用户表'
def __str__(self):
return self.username
class Blog(models.Model):
site_title = models.CharField(max_length=32)
site_name = models.CharField(max_length=32)
site_theme = models.CharField(max_length=255)
# def __str__(self):
# return self.site_name
class Category(models.Model):
name = models.CharField(max_length=32)
blog = models.ForeignKey(to='Blog')
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=32)
blog = models.ForeignKey(to='Blog')
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=255)
desc = models.CharField(max_length=255)
content = models.TextField()
create_time = models.DateField(auto_now_add=True)
# 数据库优化字段
comment_num = models.IntegerField(default=0)
up_num = models.IntegerField(default=0)
down_num = models.IntegerField(default=0)
# 外键字段
blog = models.ForeignKey(to='Blog', null=True)
category = models.ForeignKey(to='Category', null=True)
tag = models.ManyToManyField(to='Tag', through='Article2Tag', through_fields=('article', 'tag'), )
def __str__(self):
return self.title
class Article2Tag(models.Model):
article = models.ForeignKey(to='Article')
tag = models.ForeignKey(to='Tag')
class UpAndDown(models.Model):
user = models.ForeignKey(to='UserInfo')
article = models.ForeignKey(to='Article')
is_up = models.BooleanField()
class Comment(models.Model):
user = models.ForeignKey(to='UserInfo')
article = models.ForeignKey(to='Article')
content = models.CharField(max_length=255)
create_time = models.DateField(auto_now_add=True)
parent = models.ForeignKey(to='self', null=True)
注册页面搭建
校验用户名、密码、邮箱、头像等格式是否正确
# coding:utf8
# myforms.py
from django import forms
from django.forms import widgets
from app01 import models
class MyRegForm(forms.Form):
username = forms.CharField(max_length=8, min_length=3, label='用户名',
error_messages={
'max_length': '用户名最大八位',
'min_length': '用户名最小三位',
'required': '用户名不能为空'
}, widget=widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(max_length=8, min_length=3, label='密码',
error_messages={
'max_length': '密码最大八位',
'min_length': '密码最小三位',
'required': '密码不能为空'
}, widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(max_length=8, min_length=3, label='确认密码',
error_messages={
'max_length': '确认密码最大八位',
'min_length': '确认密码最小三位',
'required': '确认密码不能为空'
}, widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱', error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式错误',
}, widget=widgets.EmailInput(attrs={'class': 'form-control'}))
# 局部钩子,校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
is_exist = models.UserInfo.objects.filter(username=username)
if is_exist:
self.add_error('username', '用户名已存在!')
return username
# 全局钩子,校验密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if password != confirm_password:
self.add_error('confirm_password', '两次密码不一致!')
return self.cleaned_data
注册后端代码
from app01 import myforms
def register(request):
form_obj = myforms.MyRegForm()
back_dic = {'code': 100, 'msg': ''}
# 校验用户信息是否合法
if request.method == 'POST':
# print(request.POST)
form_obj = myforms.MyRegForm(request.POST)
if form_obj.is_valid():
clean_data = form_obj.cleaned_data
clean_data.pop('confirm_password')
# 手动获取用户头像
user_avatar = request.FILES.get('myfile')
if user_avatar: # 判断用户是否传头像了,
clean_data['avatar'] = user_avatar
# 创建数据
models.UserInfo.objects.create_user(**clean_data)
back_dic['msg'] = '注册成功!'
back_dic['url'] = '/login/'
else:
back_dic['code'] = 101
back_dic['msg'] = form_obj.errors
return JsonResponse(back_dic)
return render(request, 'register.html', locals())
注册前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
<script src="{% static 'bootstrap-3.3.7/js/bootstrap.min.js' %}"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">注册</h2>
<form action="" id="myform" novalidate>
{% csrf_token %}
{% for foo in form_obj %}
<div class="form-group">
{#foo.auto_id获取foo渲染的input框的id值,点击input框提示信息,自动跳到input框内#}
<label for="{{ foo.auto_id }}">{{ foo.label }}</label>
{# 不同的input框#}
{{ foo }}
<span class="errors pull-right" style="color:red;"></span>
</div>
{% endfor %}
<div class="form-group">
{# for指定id=myfile(获取头像的input框),点击头像,就能跳进选择框,#}
<label for="myfile">头像
<img src="/static/img/default.jpg" alt="" id ="img" style="height: 60px;margin-left: 5px">
</label>
<input type="file" name="avatar" id="myfile" style="display: none">
</div>
<input type="button" class="btn btn-primary pull-right" value="注册" id="submit">
</form>
</div>
</div>
</div>
<script>
$('#myfile').change(function () {
// 获取用户上传的文件对象
var fileobj = $(this)[0].files[0];
// 利用内置对象FileReader
var fileReader = new FileReader();
// 将文件对象交由文件阅读器读取,文件内容
fileReader.readAsDataURL(fileobj);
// 找到img标签,修改src属性
// 等待文件阅读器完全读取完文件数据之后,才做下面的操作,使用onload。不然插入的图片在浏览器页面渲染不出来
fileReader.onload = function() {$('#img').attr('src',fileReader.result)};
});
$('#submit').click(function () {
// 产生内置对象formdata
var formData = new FormData();
// 循环添加form组件的普通键值对 username,password,confirm_password,email,还有一个是自动添加到formdata对象中的csrf组件
//console.log($('#myform').serializeArray()) [{…}, {…}, {…}, {…}, {…}]
$.each($("#myform").serializeArray(),function (index,obj) {
formData.append(obj.name,obj.value)
});
// 手动添加文件
formData.append('myfile',$('#myfile')[0].files[0]);
$.ajax({
url:'',
type:'post',
data:formData,
// 传文件需要指定两个参数
contentType:false,
processData:false,
success:function (data) {
if (data.code==100) {
location.href = data.url
}else{
// 如果没有成功,说明用户输入的数据不合法,需要展示错诶信息
{#console.log(data.msg)#}
// 能够将对应错误信息准确无误的渲染到对应的input框下面的span标签内
// 手动拼接input的id值
$.each(data.msg,function(index,obj){
{#console.log(index,obj)#}
var targetId = '#id_'+ index;
// 将父标签div添加一个has-error属性,该属性能将对应的input框动态渲染成红色,与错误信息展示相一致
$(targetId).next().text(obj[0]).parent().addClass('has-error')
})
}
}
})
});
$('input').focus(function () {
$(this).next().text('').parent().removeClass('has-error') // 清除格式错误信息,并去除父标签div的has-error属性
})
</script>
</body>
</html>
admin后台管理
是一个能够帮助你快速的实现注册了的模型表数据的增删改查,
使用:
在应用中找到admin.py文件,然后注册想要操作的默认表即可,使用超级管理员账户,即可登录后台进行数据的管理。
1.创建超级管理员用户;
在Django console内输入createsuperuser,并根据提示输入用户名、密码等,
2.注册要操作的表;
在应用app中的admin.py文件夹下,导入models,并注册;
from django.contrib import admin(自带的)
from app01 import models
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Tag)
admin.site.register(models.Category) 等等
3.用创建的超级管理员用户登录django后台,进行表操作;
http://127.0.0.1:8000/admin/
登入后台注册表表名都自动加上了's',
如果想以自己定义的中文展示对应
的表名,可以在models.py内的各
个类内部加上如下代码:
class Meta:
# verbose_name_plural = '用户表' # 修改成中文表名
verbose_name = '用户表' # 修改成中文并加上一个字母's'
admin 内部源码
http://127.0.0.1:8000/admin/app01/userinfo/ # 展示数据
http://127.0.0.1:8000/admin/app01/userinfo/add/ # 添加数据
http://127.0.0.1:8000/admin/app01/userinfo/3/change/ # 编辑数据
http://127.0.0.1:8000/admin/app01/userinfo/3/delete/ # 删除数据
http://127.0.0.1:8000/admin/app01/article/ # 展示数据
http://127.0.0.1:8000/admin/app01/article/add/ # 添加数据
http://127.0.0.1:8000/admin/app01/article/3/change/ # 编辑数据
http://127.0.0.1:8000/admin/app01/article/3/delete/ # 删除数据
用户头像展示(media配置)
博客园每个文章前都有用户的头像,通过模板语法{{article.blog.userinfo.avatar}},并不能将头像渲染到博客园页面上,这时候需要将我们avatar文件夹暴露给用户,才能添加上头像;然而django固用的暴露给外界资源的方式都是用文件夹media,通过media配置,暴露给用户任意的后端资源;
在settings文件夹下配置如下代码:
# 规定用户上传的所有的静态文件全部放到media文件夹下
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
## 暴露任意文件夹资源
MEDIA_ROOT1 = os.path.join(BASE_DIR,'app01')
之后用户在注册的时候会在项目文件夹下自动创建一个media文件夹,内部套一个我们类中指定的upload_to='avatar'文件夹,并把用户注册的头像自动添加该文件中;
然后再在urls.py文件中配置路由:
from django.views.static import serve
from BBS import settings
# 手动暴露后端的文件夹资源
urlpatterns = [url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT})]
# 暴露任意文件夹资源
urlpatterns = [url(r'^app01/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT1})]
个人主页搭建
404页面 直接拷贝博客园404页面即可
但是会发现图片加载不出来 是因为 人家设置了 图片防盗链
什么是图片防盗链?
实现原理:校验当前请求的url是否是我本网站的
如果是我正常给资源,如果不是 直接拒绝
Referer: http://127.0.0.1:8000/fsdafasfd/
Referer请求头 表示了你这个网页上一次是从哪里来的
爬虫
解决方式
1.爬虫直接爬取所有图片 保存到自己的数据库
2.如果图片需求量不大的情况下 你可以采用人工智能方式 手动下载到本地
侧边栏渲染
inclusion_tag
日期归档
id content create_time month
1 111 2019-1-1 2019-1
2 322 2019-2-11 2019-2
3 222 2019-1-21 2019-1
4 555 2019-1-22 2019-1
-官方提供
from django.db.models.functions import TruncMonth
models.Article.objects
.annotate(month=TruncMonth('create_time')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.values('month', 'c') # (might be redundant, haven't tested) select month and count
点赞点踩
1.前端页面点赞点踩样式拷贝
1.html和css代码 两者都需要拷贝
2.点赞点踩的图片设置了防盗链的 要想永久建议下载到本地
3.前端页面如何区分用户是点赞还是点踩
给赞和踩的图标加了一个公共的样式类
给这个样式类绑定了一个点击事件
再利用this指代的就是当前被操作对象
只需要通过判断当前被点击对象是否有某个独立的样式类
4.发送ajax请求
参数只需要两个 文章的主键 点赞或点踩
5.后端逻辑
1.后端接收到的用户点赞点踩参数是一个前端js的布尔值字符串
你可以直接利用json.loads转成后端的布尔值类型
2.验证当前请求是否是ajax请求
request.is_ajax()
3.验证当前用户是否登录
request.user.is_authenticated()
4.验证当前文章是否是当前用户自己写的
根据前端传过来的文章主键查询出文章对象
根据文章对象查询出作者与request.user当前登录用户做比对
5.验证当前文章是否已经被当前用户点过赞或踩了
根据文章主键和当前登录用户对象去点赞点踩表中筛选数据
一旦有数据 说明该用户已经给当前文章点过赞或者踩
6.操作数据库
一旦要注意在操作点赞点踩表的时候 一旦要去文章表中将
点赞或点踩普通字段同步修改
6.前端展示提示信息
点赞或点踩成功需要将前端页面对应的数字在原来的基础上加一
注意在加的时候一定要转换成数值类型相加
Number() 类似后端 int()
文章的评论
1.前端获取用户评论样式搭建
应该做到只有登录的用户才能看到评论框
2.前端利用ajax发送评论请求
当前文章的主键
评论的内容
3.后端
1.校验当前请求是否是ajax请求
获取主键 评论内容
2.操作数据库
利用事务完成两个地方数据的同步
1.文章表里面的评论普通字段
2.评论表记录
3.前端展示评论楼信息
后端获取当前文章所有的评论传递到前端
4.评论渲染
当用户点击提交按钮 除了渲染之外 你还应该将textarea框内容清空
1.DOM临时渲染
1.获取当前评论人的用户名和评论内容
2.利用es6新语法 模板字符串 完成字符串的替换
3.查找ul标签 将创建的li标签添加到ul标签内
2.render永久渲染
在渲染页面的时候 for循环文章所有的评论一一渲染即可
5.子评论的功能
子评论和根评论的唯一区别仅仅在于parent_id字段是否有值
突破口:点击回复按钮发送了几件事
1.获取当前登录用户想评论的那条评论人的用户名 拼接成 @用户名\r\n
利用标签可以支持任意多个自定义属性 给回复按钮标签 添加username={{comment.user.username}}
2.将拼接完成的内容的添加到textarea框中
3.textarea框自动聚焦
4.无论是提交根评论还是子评论都是点击的一个按钮
1.提交子评论和父评论唯一的差距就在于你是否传了parent_id
2.数据库中parent_id是可以为空的 也就意味你在创建数据的时候 如果传空也不会有影响
5.无论是根评论 子评论我都可以提交一个parent_id无论它是否有值
后端只需要接收parent_id然后在create方法中直接添加即可 至此后端代码一行都不需要再修改了
6.在点击回复按钮的时候 除了获取评论人的用户名之外 还应该回去当前评论数据的主键值
还需要给回复按钮 加一个自定义数据 pk = {{comment.pk}}
点击回复按钮能够获取到评论主键值 什么时候用的?
当你在点击提交评论按钮的时候才会用到评论主键值(一个函数需要引用林外一个函数中的某个名字)
应该在全局设置一个专门存储评论主键值的字段parent_id = null;默认等于None
点击完回复按钮之后才会对全局的parent_id进行修改
7.提交子评论内容中含有@用户名\r\n 这一段内容并不是用户自己写的 应该去除
前端处理
获取\n所在的索引值 然后你自己思考了一下 切片取值 是顾头不顾尾 所以应该给索引加1
利用slice切片操作(从0开始到索引结束的内容全部去除 保留剩下部分)
8.你会发现当你提交了一次子评论之后 页面不刷新的情况 永远无法提交根评论了 因为全局的parent_id字段一直有值
你应该在每一次提交完成后 清空parent_id字段
9.评论渲染的时候
如何渲染出子评论
利用orm表查询
{{ comment.parent.user.username }}
后台页面搭建
添加文章的前端页面渲染
类似于博客园添加随笔时的界面,在以下链接下载并解压,复制到BBS的static文件夹中
编辑页面下载官网:kindeditor编辑器
处理xss攻击
文章简介的获取
1.文章简介的获取
截取150个中文字符
2.防止用户写script脚本
1.获取用户输入的所有的script标签直接删除
2.给script转义
3.
处理xss攻击可以使用beautifulsoup4模块
beautifulsoup4 简称bs4
pip3 install beautifulsoup4
from bs4 import beautifulsoup
# 先生成一个beautifulsoup对象
soup =BeautifulSoup(content,'html.parser')
for tag in soup.find_all():
# print(tag.name) # 获取当前html页面所有的标签
# print(soup.text) # 获取当前html页面所有的文本
if tag.name == 'script': # 对于script标签,直接删除
tag.decompose() # 将符合条件的标签删除
desc = soup.text[0:150] # 给文章简介获取150个
article_obj = models.Article.objects.create(title=title,desc =desc,content=str(soup),category_id=category,blog=request.user.blog)
上传图片操作
在kindeditor编辑器官网上找到以下配置:
》上传文件
uploadJson : '../jsp/upload_json.jsp', # 匹配一个路由与视图函数的对应关系
仅仅只配置这个参数,上传图片还会报错(CSRF verification failed. Request aborted.),还需要额外配置以下参数:
》编辑器初始化参数》extraFileUploadParams:
extraFileUploadParams : {
'csrfmiddleware':'{{csrf_token}}',
}
所以文本编辑器加上传文件这一块一共应该配置的参数为:
<textarea name="content" cols="30" rows="10" id="id_content"></textarea>
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#id_content', {
width: '100%',
height: '450px',
resizeType: 1,
uploadJson : '/upload_img/',
extraFileUploadParams : { // 支持传额外的图片参数
'csrfmiddlewaretoken':'{{csrf_token}}',
}
});
});
</script>
图片传到后端操作:
def upload_img(request):
# 接受用户写文章上传的所有的图片资源
if request.method == "POST":
# print(request.FILES) # <MultiValueDict: {'imgFile': [<InMemoryUploadedFile: 44.jpg (image/jpeg)>]}>
# 编辑文章上传的图片传到后端全部都放在了request.FILES中了,且字典的键是它指定的imgFile
file_obj = request.FILES.get('imgFile')
#将文章图片存储在media文件下一个专门用来放文章图片的文件夹
# 手动拼接出图片所在的文件夹路径
base_path = os.path.join(settings.BASE_DIR,'media','article_img')
if not os.path.exists(base_path):
os.mkdir(base_path)
# 手动拼接文件的具体路径
file_path = os.path.join(base_path,file_obj)
# 将图片存放在该文件夹下
with open(file_path,'wb') as f:
for line in file_obj:
f.write(line)
back_dic = {"error": 0, "url": "/media/article_img/%s"%file_obj.name}
return JsonResponse(back_dic)
# 图片上传成功之后,返回的数据格式(JSON)
'''
》上传文件》返回格式(JSON)
//成功时
{
"error" : 0,
"url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
"error" : 1,
"message" : "错误信息"
}
'''
用户头像修改
BBS 功能汇总
1.数据库的设计 (7张表 一对一对一 一对多对多 其余的都是一对多)
2.forms组件完成注册功能
注册功能前端错误信息渲染 需要你自己找出每一个input id值的规律
3.登陆功能
图片验证码
4.首页展示
django admin后台管理
用户头像展示
media配置
5.个人站点
侧边栏展示
侧边栏筛选功能
inclusion_tag
6.文章详情页
点赞点踩
评论
7.后台管理
BBS 页面搭建知识点整理的更多相关文章
- JSP页面开发知识点整理
刚学JSP页面开发,把知识点整理一下. ----------------------------------------------------------------------- JSP语法htt ...
- 扩展auth_user字段、BBS需求分析、创建BBS数据库、注册页面搭建与用户头像展示及Ajax提交数据
昨日内容回顾 csrf跨站请求 1. SQL注入 2. xss攻击 3. csrf跨站请求 4. 密码加密(加盐) '''django中默认有一个中间件来验证csrf''' # 只针对post请求才验 ...
- springmvc 项目完整示例08 前台页面以及知识点总结
至此已经基本测试成功了,我们稍作完善,让它成为一个更加完整的项目 我们现在重新规划下逻辑 两个页面 一个登录页面 一个欢迎页面 登陆页面输入账号密码,登陆成功的话,跳转登陆成功 欢迎页面 并且,更新用 ...
- vue前端面试题知识点整理
vue前端面试题知识点整理 1. 说一下Vue的双向绑定数据的原理 vue 实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫 ...
- stark组件之显示页面搭建(四)
页面搭建包括第一如何获取前端传过来的数据,第二如何在前端渲染出对应标签. 一.后台获取数据并进行处理 在路由系统中,每一个路由都对应着一个处理函数,如下所示: def wrapper(self, fu ...
- web前端面试知识点整理
一.HTML5新特性 本地存储 webStorage websocket webworkers新增地理位置等API对css3的支持canvas多媒体标签新增表单元素类型结构标签:header nav ...
- HTML&&CSS基础知识点整理
HTML&&CSS基础知识点整理 一.WEB标准:一系列标准的集合 1. 结构(Structure):html 语言:XHTML[可扩展超文本标识语言]和XML[可扩展标记语言] 2. ...
- Python基础知识点整理(详细)
Python知识点整理(详细) 输出函数 print()可以向屏幕打印内容,或者在打开指定文件后,向文件中输入内容 输入函数 input([prompt])[prompt] 为输入的提示字符.该函数返 ...
- js页面跳转整理
js页面跳转整理 js方式的页面跳转1.window.location.href方式 <script language="javascript" type=" ...
随机推荐
- (六)Cookie 知识点总结 (来自那些年的笔记)
如果你想要转载话,可不可以不要删掉下面的 作者信息 呀!: 作者:淮左白衣 写于 2018年4月18日18:47:41 来源笔者自己之前学javaWeb的时候,写的笔记 : 目录 如果你想要转载话,可 ...
- PHP生成有背景的二维码图片
Hart QR Code 快速生产带背景的二维码,他为你提供了以下功能 生产原始二维码,可配置url或则text,以及二维码大小 生产带背景带二维码,背景大小是你传入带背景大小,可配置原始二维码大小, ...
- 2019php面试大全
一 .PHP基础部分 1.PHP语言的一大优势是跨平台,什么是跨平台? PHP的运行环境最优搭配为Apache+MySQL+PHP,此运行环境可以在不同操作系统(例如windows.Linux等)上配 ...
- 机器学习-EM算法的收敛证明
上一篇开头说过1983年,美国数学家吴建福(C.F. Jeff Wu)给出了EM算法在指数族分布以外的收敛性证明. EM算法的收敛性只要我们能够证明对数似然函数的值在迭代的过程中是增加的 即可: 证明 ...
- 选择排序——C语言
选择排序 1.算法描述 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾.以此类推,直到所有元素均排序完毕(放 ...
- ARM协处理器CP15寄存器详解
改自:https://blog.csdn.net/gameit/article/details/13169405 *C2描述的不对,bit[31-14]才是TTB,不是所有的bit去存储ttb.很明显 ...
- 使用RabbitMQ实现分布式事务
RabbitMQ解决分布式事务思路: 案例: 经典案例,以目前流行点外卖的案例,用户下单后,调用订单服务,让后订单服务调用派单系统通知送外卖人员送单,这时候订单系统与派单系统采用MQ异步通讯. Rab ...
- (七)Redis之Keys的通用操作
package myRedis01; import java.util.HashMap; import java.util.List; import java.util.Map; import jav ...
- (四)springmvc之获取servlet原生对象
一.使用DI注入的方式 <a href="<%=request.getContextPath()%>/servletObj_1">DI注入的方式</a ...
- 多节点bigchaindb集群部署
文章比较的长,安装下来大概4个小时左右,我个人使用的服务器,速度会快一点. 安装环境 ostname ip os node-admin 192.168.237.130 ubuntu 18.04.2 d ...