博客功能篇:博客的初始化处理和管理员的账号注册

457 阅读9分钟

前面思路篇和基础篇我们已经准备得差不多了,现在开始正式进入到功能实现步骤。第一步,是处理博客初始化工作。

这一步我们将完成博客的初始化工作,回写配置信息、注册管理员账号等操作。

当我们需要将博客分发和部署到服务器的时候,最好的操作是,运行博客程序后,类似其他cms、WordPress一样,初次访问,会要求输入数据库信息和管理员账号信息,通过简单的配置后,不可就可以开始正常使用了。我们也一样,也在博客开始运行的时候,来配置数据库信息、管理员账号。

定义常量

在开始初始化之前,我先定义几个常量,在config文件夹下,创建一个constant.go 文件,并在里面定义常量:

package config

const StatusOK = 0
const StatusFailed = -1
const StatusNoLogin = 1001

golang的常量使用const关键词来定义,我们现在定义了3中状态,StatusOK = 0代表正常状态,值为0;StatusFailed = -1代表错误状态,包括各种错误,这里不做细分,实际错误信息以文字直接显示,错误代码值为-1;StatusNoLogin = 1001代表未登录状态,未登录有点特殊,采用一个特殊的状态,可以明确告知前端可以在报错后引导到登录页面来处理登录信息。这些状态常量,我们将会在后续的开发中逐一使用到。下面我们开始初始化工作。

初始化界面html代码

我们在template目录下新建install目录,并创建一个index.html文件,写入下面的代码:

{% include "partial/header.html" %}
<div class="layui-container">
    <div class="install">
        <div class="layui-card">
            <div class="layui-card-header">{{SiteName}}初始化</div>
            <div class="layui-card-body">
                <form class="layui-form" onsubmit="return false;">
                    <div id="mysql-config" style="display: block;">
                        <div class="layui-form-item">
                            <label class="layui-form-label">数据库地址</label>
                            <div class="layui-input-inline">
                                <input type="text" name="host" required  lay-verify="required" placeholder="一般是localhost" autocomplete="off" class="layui-input">
                            </div>
                        </div>
                        <div class="layui-form-item">
                            <label class="layui-form-label">数据库端口</label>
                            <div class="layui-input-inline">
                                <input type="text" name="port" required  lay-verify="required" placeholder="一般是3306" autocomplete="off" class="layui-input">
                            </div>
                        </div>
                        <div class="layui-form-item">
                            <label class="layui-form-label">数据库名称</label>
                            <div class="layui-input-inline">
                                <input type="text" name="database" required  lay-verify="required" placeholder="安装到哪个数据库" autocomplete="off" class="layui-input">
                            </div>
                        </div>
                        <div class="layui-form-item">
                            <label class="layui-form-label">数据库用户</label>
                            <div class="layui-input-inline">
                                <input type="text" name="user" required  lay-verify="required" placeholder="填写数据库用户名" autocomplete="off" class="layui-input">
                            </div>
                        </div>
                        <div class="layui-form-item">
                            <label class="layui-form-label">数据库密码</label>
                            <div class="layui-input-inline">
                                <input type="password" name="password" required  lay-verify="required" placeholder="填写数据库密码" autocomplete="off" class="layui-input">
                            </div>
                        </div>
                    </div>
                    <hr>
                    <div class="layui-form-item">
                        <label class="layui-form-label">后台用户名</label>
                        <div class="layui-input-inline">
                            <input type="text" name="admin_user" required  lay-verify="required" placeholder="用于登录管理博客" autocomplete="off" class="layui-input">
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <label class="layui-form-label">后台密码</label>
                        <div class="layui-input-inline">
                            <input type="password" name="admin_password" required  lay-verify="required" placeholder="登录密码" autocomplete="off" class="layui-input">
                        </div>
                    </div>
                    <div class="layui-form-item">
                        <div class="layui-input-block">
                            <button class="layui-btn" lay-submit lay-filter="install">确认初始化</button>
                            <button type="reset" class="layui-btn layui-btn-primary">重置</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% include "partial/footer.html" %}

这里的代码包含了用户要输入的数据库地址、数据库端口、数据库名称、数据库用户、数据库密码等数据库信息,还要求用户输入管理员账号、管理员密码,来准备做后期博客管理账号使用。这里模板同样地使用了include引入了模板头部、模板尾部代码片段。

提交表单的js代码

前面章节我们交代了,为了方便js统一管理,我们将js代码存放在public/static/js/app.js 文件中。因为我们前端使用的是layui框架,因此form表单提交,也是需要使用layui的form表单提交组件来完成提交。我们在app.js 中, 最后的});前增加一行,并加入如下代码来实现:

//监听提交
form.on('submit(install)', function(data){
		let postData = data.field;
		postData.port = Number(postData.port)
		$.post("/install", postData, function (res) {
				if(res.code === 0) {
						layer.alert(res.msg, function(){
								window.location.href = "/";
						});
				} else {
						layer.msg(res.msg);
				}
		}, 'json');
		return false;
});

这段js的作用是,监听用户点击确认初始化按钮,因为我们在html中,给确认初始化按钮添加了lay-submit 属性和 lay-filter="install"属性。<button class="layui-btn" lay-submit lay-filter="install">确认初始化</button>

在页面提交前,我们还需要注意,port字段是整形,在提交之前,我们需要先转换它成为整形postData.port = Number(postData.port),否则,golang是强类型语言,直接按字符串格式提交到后台的话,它会无法解析而报错。

我们通过post方式提交,并定义返回的格式为json,方便直接处理。如果返回的数据res中,res.code为0,也就是上面我们定义的const StatusOK = 0,则表示安装完毕,跳转到首页,否则就弹出一个超时5秒自动消失的消息layer.msg(res.msg);来展示后端返回的错误信息。

初始化逻辑处理

我们接着在controller文件夹下,创建一个install.go文件,用来存放处理初始化过程的控制器。在install.go文件里,我们需要创建2个控制器函数,一个是用于显示初始化界面的控制器函数Install(),一个是用于接收处理初始化表单提交结果的控制器函数InstallForm()。

显示初始化界面的控制器函数Install()

func Install(ctx iris.Context) {
	if config.DB != nil {
		ctx.Redirect("/")
		return
	}

	ctx.View("install/index.html")
}

这里的判断很简单,判断数据库信息是否已经存在了,如果已经存在的话,表示已经初始化过了不能再次初始化,我们使用ctx.Redirect("/")跳转到首页,如果还未初始化过,则显示初始化界面信息。

定义接收初始化变量的结构体

我们在接收数据之前,先定义一个我们需要接收的变量的结构体。我们在request文件夹下,创建一个install.go文件,用来存放接收初始化变量的结构体代码:

package request

type Install struct {
	Database      string `form:"database" validate:"required"`
	User          string `form:"user" validate:"required"`
	Password      string `form:"password" validate:"required"`
	Host          string `form:"host" validate:"required"`
	Port          int    `form:"port" validate:"required"`
	AdminUser     string `form:"admin_user" validate:"required"`
	AdminPassword string `form:"admin_password" validate:"required"`
}

这个结构体,我们定义了DatabaseUserPasswordHostPortAdminUserAdminPassword这几个我们上面表单中存在字段,我们采用form表单形式提交数据,因此我们这里对结构体字段的标记为form,同时这些字段,都是必填字段,因此,我们在结构体字段的标记为validate:"required"。我们定义了对应的form字段,下面接收数据的时候,就可以将前端表单提交的数据都读入到这个结构体中了。

接收处理初始化表单提交结果的控制器函数InstallForm()

if config.DB != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  "Failed",
		})
		return
	}
	var req request.Install
	if err := ctx.ReadForm(&req); err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

	config.JsonData.DB.Database = req.Database
	config.JsonData.DB.User = req.User
	config.JsonData.DB.Password = req.Password
	config.JsonData.DB.Host = req.Host
	config.JsonData.DB.Port = req.Port

	err := config.InitDB(&config.JsonData.DB)
	if err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

	err = config.WriteConfig()
	if err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

	//创建管理员
	err = provider.InitAdmin(req.AdminUser, req.AdminPassword)
	if err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

	ctx.JSON(iris.Map{
		"code": config.StatusOK,
		"msg":  fmt.Sprintf("%s初始化成功", config.ServerConfig.SiteName),
	})

这一部分,同样的,需要先检查网站是否已经初始化过了,如果已经初始化过,则不能再次初始化,返回一个错误信息给前端。

当博客没有初始化过的话,我们就声明一个req变量,来接收用户提交的表单数据:

  var req request.Install
	if err := ctx.ReadForm(&req); err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

这里的判断是,如果用户提交的数据跟我们定义的一样,可以完整读入req变量的话,就继续,不能成功读入req变量的话,就返回一个错误信息。

接着,我们将接收到的mysql数据,赋值给配置变量,然后通过InitDB来验证这些提交的信息是否正确,如果正确,执行InitDB的逻辑代码,如果不正确,则返回一个错误。

  config.JsonData.DB.Database = req.Database
	config.JsonData.DB.User = req.User
	config.JsonData.DB.Password = req.Password
	config.JsonData.DB.Host = req.Host
	config.JsonData.DB.Port = req.Port

	err := config.InitDB(&config.JsonData.DB)
	if err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

当提交的数据库信息验证通过后,我们需要将接收到的数据库信息,回写到配置文件中:

err = config.WriteConfig()
	if err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

WriteConfig()函数我们还没有定义,我们还需要到config中定义一下。我们现在打开config.go文件,添加WriteConfig函数:

func WriteConfig() error {
	//将现有配置写回文件
	configFile, err := os.OpenFile(fmt.Sprintf("%sconfig.json", ExecPath), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
	if err != nil {
		return err
	}

	defer configFile.Close()

	buff := &bytes.Buffer{}

	buf, err := json.MarshalIndent(JsonData, "", "\t")
	if err != nil {
		return err
	}
	buff.Write(buf)

	_, err = io.Copy(configFile, buff)
	if err != nil {
		return err
	}

	return nil
}

这个函数的意思是,读取项目根目录下的config.json文件,采用可写方式来读取,如果文件不存在的话,自动创建。然后将JsonData配置信息转换成json,再将json回写入congif.json文件。期间如果出错的话,比如文件创建不成功、json转换失败、文件写入不成功等错误的时候,就返回错误信息。

完成数据回写后,我们还需要根据用户提交的管理员账号和管理员密码,创建一个管理员用来做后续的登录管理操作:

  //创建管理员
	err = provider.InitAdmin(req.AdminUser, req.AdminPassword)
	if err != nil {
		ctx.JSON(iris.Map{
			"code": config.StatusFailed,
			"msg":  err.Error(),
		})
		return
	}

创建管理员的操作是跟model模型打交道的,在控制器到模型直接,我们再增加了一个中间层,这一层我们命名为provider。我们在provider文件夹加创建一个admin.go文件,用来处理创建管理员等操作逻辑。这里,我们先在admin.go中,增加一个InitAdmin()函数:

func InitAdmin(userName string, password string) error {
	if userName == "" || password == "" {
		return errors.New("请提供用户名和密码")
	}

	var exists int64
	db := config.DB
	db.Model(&model.Admin{}).Count(&exists)
	if exists > 0 {
		return errors.New("已有管理员不能再创建")
	}

	admin := &model.Admin{
		UserName: userName,
		Status:   1,
	}
	admin.Password = admin.EncryptPassword(password)
	err := admin.Save(db)
	if err !=  nil {
		return err
	}

	return nil
}

这个函数接收2个参数,一个是userName,一个是password。只有两个都提供的时候,才可以创建管理员账号,同样地,为了管理方便,简化博客管理流程,整个博客只允许创建一个管理员,如果管理员已经存在了,则不能再次创建。

var exists int64
	db := config.DB
	db.Model(&model.Admin{}).Count(&exists)
	if exists > 0 {
		return errors.New("已有管理员不能再创建")
	}

因为管理员有密码,但往往,我们建立网站的话,密码是不会明文存储的,因此,我们还需要增加一个密码加密函数EncryptPassword。我们将密码加密函数EncryptPassword存放在model/admin.go文件中:

func (admin *Admin) EncryptPassword(password string) string {
	if password == "" {
		return ""
	}
	pass := []byte(password)
	hash, err := bcrypt.GenerateFromPassword(pass, bcrypt.MinCost)
	if err != nil {
		return ""
	}

	return string(hash)
}

密码加密我们使用golang官方加密包golang.org/x/crypto/bcrypt。这样我们将得到一个被加密后的密码字符串。我们将这个字符串写入到数据库中,以方便以后登录的时候做比对。

最后,就是调用admin.Save(db)来将管理员信息写入数据库了。

配置初始化路由

初始化逻辑我们写完了,我们还需要将初始化的两个控制器注册到路由中,才能在浏览器中访问到。我们现在打开route/base.go文件,在Register中增加两个路由:

  app.Get("/install", controller.Install)
	app.Post("/install", controller.InstallForm)

添加完毕的base.go文件如下:

package route

import (
	"github.com/kataras/iris/v12"
	"irisweb/controller"
)

func Register(app *iris.Application) {
	app.OnErrorCode(iris.StatusNotFound, controller.NotFound)
	app.OnErrorCode(iris.StatusInternalServerError, controller.InternalServerError)
	app.Get("/", controller.IndexPage)

	app.Get("/install", controller.Install)
	app.Post("/install", controller.InstallForm)
}

至此,整个博客的mysql初始化、管理员用户注册就完成了。

验证结果

为了验证初始化界面,我们先将根目录下的config.json删掉,模拟未初始化状态。接着我们运行下看看,如果不出意外的话,我们将看到如下界面效果: 初始化界面

完整的项目示例代码托管在GitHub上,需要查看完整的项目代码可以到github.com/fesiong/gob… 上查看,也可以直接fork一份来在上面做修改。