Beego

Beego 是一个开源的基于 Golang 的 MVC 框架,主要用于 Golang Web 开发。Beego 可以 用来快速开发 API、Web、后端服务等各种应用。

Golang 的 Web 开发框架有很多 Beego(star:22.8k)、Buffalo(5.6k)、Echo(17.2k)、Gin(37.9k)、 Iris(18.1k)、Revel(11.7k) ,从 github star 数量来看 Gin>Beego>Iris>Echo>Revel>Buffalo。

目前国内用的比较多的就是 Beego 和 gin 两个框架,如果项目比较小,个人开发,并且 只是用 Golang 来写一些 api 接口的话, gin 是不错的选择,如果你是团队开发或者不仅要用 golang 写 api,还要用 golang 写 web 后端,并且注重代码质量的话建议用 Beego。

Beego Github 地址
Beego 官网

init

经过前面两天的基础学习,对BeeGo有了大致了解
今天正式开始用GoLang写小米商城项目 beego new micom

项目模块化

由于这是一个企业级的项目比较大,为了使我们的项目更易于管理,首先需要对项目进行模块化
也就是实现控制器分组、视图分组、静态资源分组、路由分组

嵌套后台模板

在完成分组后,就可以进行模板嵌套
但是,为了后期管理路由方便,我们可以使用Beego当中的命名路由(namespace)
我们想的是后台所有的路由都配置到 Admin 下面,Api 中的所有路由都配置到 Api 下面

adminRouter.go

package routers

import (
    "begoxiaomi/controlers/admin"
    "fmt"
    "github.com/astaxie/bego"
    "github.com/astaxie/bego/context"
)

func init() {
    ns :=
        bego.NewNamespace("/admin",
            // 中间件
            bego.NSBefore(func(tx *context.Context) {
                fmt.Println("执行")
            }),
            bego.NSRouter("/login", &admin.LoginControler{}),
            bego.NSRouter("/main", &admin.MainControler{}),
        )
    bego.AdNamespace(ns)
}

apiRoute.go

package routers
import (
    "begoxiaomi/controlers/api"
    "github.com/astaxie/bego"
)
func int() {
    ns :=
        bego.NewNamespace("/api",
            bego.NSRouter("/, &api.IndexControler{}),
            )
    bego.AdNamespace(ns)
}

路由设置得差不多了,就可以进行模板嵌套了
我们首先实现一下后台登录页面

  1. 在controllers//admin//下新建一个控制器login.go
  2. 接着在routers//adminRouter.go中配置相关路由
  3. 将html文件放在view//admin文件夹中
  4. 将需要的静态资源放在static//admin文件中
  5. 最后,在controllers//admin//login.go 进行模板渲染(this.TplName="admin/login.html")

controllers//admin//login.go

func (this *LoginController) Get() {
    this.TplName="admin/login.html"
}

效果图


正如你所见,登录页面还实现了验证码功能
这个我们就运用到了,captcha 这个验证码插件

使用 captcha 生成验证码

1、引入 captcha 对应的两个模块

"github.com/astaxie/beego/cache" 
"github.com/astaxie/beego/utils/captcha"

2、生成验证码

var cpt *captcha.Captcha
// verify生成验证码
func init()  {
    store := cache.NewMemoryCache()
    cpt = captcha.NewWithFilter("/captcha/",store)
    cpt.ChallengeNums = 4
    cpt.StdHeight = 41
    cpt.StdWidth = 250
}

3、在模板中使用 {{create_captcha}}显示验证码

<div class="container">
            <h2>小米商城后台登录</h2>
            <h6>Powered By: Java_S</h6>
            <form class="form" action="/admin/login/doLogin" method="post">
                <input type="text" placeholder="用户名" name="username">
                <input type="text" placeholder="密码" name="password">
                <input type="text" placeholder="验证码" name="captcha">
                {{create_captcha}}
                <button type="submit" id="login-button">登录</button>
            </form>

        </div>

4、验证验证码

func (this *LoginController) Dologin() {
    flag := cpt.VerifyReq(this.Ctx.Request)
    if flag {
        this.Ctx.WriteString("验证码输入正确")
    }else {
        this.Ctx.WriteString("验证码输入错误")
    }
}


接着,就是进行后台内容模板的嵌套
这里我们用到了系统局部刷新架构
并将后台页面中,一些公共的部分代码提取了出来,方便后期的管理

{{template "../public/page_header.html" .}}
{{template "../public/page_nav.html" .}}
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-2">
            {{template "../public/page_aside.html" .}}
        </div>
        <div class="col-sm-10">
            <iframe width="100%" height="500px" id="rightMain" name="rightMain" src="/admin/welcome" frameborder="0"></iframe>
        </div>
    </div>
</div>
</body>
</html>

效果图


昨天已经完成了后台登录页面和后台页面的渲染
于是,今天就完成登录的操作
今天天气不错,外面大太阳,自从放寒假以来都没出过门,有幸出去转了转

连接数据库

要想完成登录操作,那么就得先有用户呀
所以,首先建了一张表,用于存放后台用户的数据信息
并在项目中创建了数据表模型
当然,需要先连接MySQL数据库(使用GORM,连接很简单,就不显示代码部分)

package models

import (
    _ "github.com/jinzhu/gorm"
)

type Manager struct {
    Id int
    Username string
    Password string
    Mobile string
    Email string
    Status string
    RoleId int
    AddTime int
    IsSuper int
}

func (Manager) TableName() string  {
    return "manager"
}

验证登录信息

数据库模型创建完成后,我们就可以通过前台传过来的登录数据,进行验证

需要注意的是,从前台传来的密码是明文,但是数据库里面存的是经过MD5算法加密过的密码,所以我们在与数据库中的密码进行对比的时候,需要先把密码加密后在进行对比

还有一个小细节就是,先验证输入的验证码是否正确,接着验证输入的密码是否正确,为什么这么做,我只能说dddd

验证密码的主要代码如下:

models.DB.Where("username=? AND password=?",username,password).Find(&manager)

这条代码的主要逻辑就是-----条件查询
在之前创建的manager模型中,查找是否username和password的值与表中的对应字段的值相同
若存在,即返回带有内容的切片;若不存在即返回空

验证登录信息完整代码

func (this *LoginController) Dologin() {
        // 1:验证用户输入的验证码是否正确
    flag := cpt.VerifyReq(this.Ctx.Request)
    if flag {
        // 2:获取表单传过来的用户名和密码
        username := strings.Trim(this.GetString("username"),"")
        password := models.Md5(strings.Trim(this.GetString("password"),""))
        // 3:根据表单里面的内容与数据库中的数据进行比较
        manager := []models.Manager{}
        // 条件查询,如果存在返回给manager数据
        models.DB.Where("username=? AND password=?",username,password).Find(&manager)
        if len(manager)>0 {
            // 登录成功
                // 1:保存用户信息
            this.SetSession("userinfo",manager[0])
                // 2:跳转到后台首页

            this.Success("密码输入正确","/"+beego.AppConfig.String("adminPath"))
        }else {
            this.Error("密码输入错误","/"+beego.AppConfig.String("adminPath")+"/login")
        }

    }else {
        this.Error("验证码输入错误","/"+beego.AppConfig.String("adminPath")+"/login")
    }
}

登录中间件

用户登录的功能实现后,我们会遇到一个问题
如果,我们不设置路由中间件的话,其他人就可以之间通过后台的链接,直接进入到后台
那么,我们所做的这个登录界面就毫无意义

这个时候,我们就可以通过路由+Session来实现
登录成功后,设置Session,当访问后台地址的时候,判断是否存在登录Session,来确实是否已登录

adminPath := beego.AppConfig.String("adminPath")

beego.NSBefore(func(ctx *context.Context) {
    userinfo,ok := ctx.Input.Session("userinfo").(models.Manager)
    pathname := ctx.Request.URL.String()
    if !(ok&&userinfo.Username != "") {
        if pathname!= "/"+adminPath+"/login" && pathname!="/"+adminPath+"/login/doLogin" {
            ctx.Redirect(302, "/"+adminPath+"/login")
        }
    }
}),

将Session存入Redis

当我们完成,登录中间件的时候,又会遇到一个问题
如果说,我们的beego关闭了,那么session也就丢失了,也就是需要重新输入登录后台的密码,这样会影响到我们后面做测试
所以,我们干脆把session直接保存到redis中,就可以解决这个问题

func init()  {
    gob.Register(models.Manager{})
}

func main() {
    // 配置session保存在redis里面
    beego.BConfig.WebConfig.Session.SessionProvider = "redis"
    beego.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:6379"
    beego.Run()
    defer models.DB.Close()
}


基于RBAC完成权限管理
今天完全就是增删改查的一天啊(枯燥且乏味)

RBAC

RBAC 是基于角色的权限访问控制(Role-Based Access Control)。在 RBAC 中,权限与角色相 关联,用户通过成为适当角色的成员而得到这些角色的权限。

RBAC 实现流程

  1. 实现角色的增加修改删除
  2. 实现用户的增加修改删除,增加修改用户的时候需要选择角色
  3. 实现权限的增加修改删除(页面菜单)
  4. 实现角色授权功能
  5. 判断当前登录的用户是否有访问菜单的权限
  6. 根据当前登录账户的角色信息动态显示左侧菜单

在实现权限管理之前,必须先理清楚权限控制相关的数据库表

角色管理前后端编写

要想实现角色管理,当然是要先连接数据库啦,那么就得先创建模型
Role模型

package models

import (
    _ "github.com/jinzhu/gorm"
)

type Role struct {
    Id          int
    Title       string
    Description string
    Status      int
    AddTime     int
}

func (Role) TableName() string {
    return "role"
}

模型创建完成后就是,先不忙着写控制器,我们先把路由设置好

// 角色管理
beego.NSRouter("/role",&admin.RoleController{}),
beego.NSRouter("/role/add",&admin.RoleController{},"get:Add"),
beego.NSRouter("/role/edit",&admin.RoleController{},"get:Edit"),
beego.NSRouter("/role/doAdd",&admin.RoleController{},"post:DoAdd"),
beego.NSRouter("/role/doEdit",&admin.RoleController{},"post:DoEdit"),
beego.NSRouter("/role/delete",&admin.RoleController{},"get:Delete"),

接着,就是最要的部分----RoleController
控制器中,通过编成不同的函数,完成对角色的增删查改
不仅要从数据库中读取数据,还要渲染到前台页面,当然也要能修改数据库中的内容

代码部分,就拿修改角色信息的函数DoEdit()演示

DoEdit()

func (this *RoleController) DoEdit() {
    id,err := this.GetInt("id")
    if err != nil {
        this.Error("传入参数错误","/role")
        return
    }
    title := this.GetString("title")
    description := this.GetString("description")

    if title == "" {
        this.Error("标题不能为空","/role/add")
        return
    }
    role := models.Role{Id:id}
    models.DB.Find(&role)

    role.Title = title
    role.Description = description

    err_save := models.DB.Save(&role).Error
    if err_save != nil {
        this.Error("修改数据失败","/role/edit?id="+strconv.Itoa(id))
    }else {
        this.Success("修改数据成功","/role")
    }
}


整个逻辑其实也挺简单的

  1. 首先前台通过POST的方式给指定路由发起请求,执行DoEdit()函数
  2. DoEdit()中通过GetInt()和GetString()接受前台传来的数据
  3. 通过一些简单的判断,返回一些错误信息
  4. 接着,通过Id值选择对应的角色
  5. 最后,通过给结构体里面的字段进行赋值,从而修改数据models.DB.Save()

管理员管理

角色部分完成后,就可以写用户部分了,也就是管理员部分
这个其实也没什么好写的,基本上和角色管理部分一样,都是增删改查,所以开头才说道:枯燥且乏味

在管理员管理页面中的编辑页面,关于前台页面渲染的部分还是有点意思

渲染前台页面

当我们,点击页面中的【编辑】时,会执行编辑路由和相关函数,也就是下面的代码

func (this *ManagerController) Edit() {
    // 获取所以角色
    role := []models.Role{}
    models.DB.Find(&role)
    this.Data["role"] = role

    // 获取管理员信息
    id,err := this.GetInt("id")
    if err != nil {
        this.Error("非法请求","/manager")
        return
    }
    manager := models.Manager{Id: id}
    models.DB.Find(&manager)
    this.Data["manager"] = manager

    this.TplName = "admin/manager/edit.html"
}

通过数据库模型,获取到数据库当中的数据
接着使用this.Data["role"] 将内容传给前台
当然我们要指定模板呀,那就要写这个啦 this.TplName = "admin/manager/edit.html"


前台部分,我们通过{{.role}}的方式结束后端传来的数据

<select name="role_id" id="role_id">
    {{$roleId := .manager.RoleId}}
    {{range $key,$value := .role}}
        {{if eq $value.Id $roleId}}
            <option selected value="{{$value.Id}}">{{$value.Title}}</option>                                    
        {{else}}
          <option value="{{$value.Id}}">{{$value.Title}}</option>                                    
        {{end}}
    {{end}}
</select>

我所说的有意思的部分,就是上面的代码啦
通过写判断,选择<option>里面的默认值
if eq $value.Id $roleId 的意思就是 if $value.Id == $roleId


其他地方的代码,我就不一一到来了,说白了就是增删改查,没意思没意思
明天写点有意思的内容,权限列表 权限增加 权限表和权限表的自关联


今天爷爷屋头杀猪,回老家吃好的了,就没怎么写代码
只完成了权限部分的增删查改,以及后台页面权限的显示
增删改查的部分,我就不在演示了,和昨天的内容都差不多

表的自身关联

有意思的地方时权限的数据库模型
实现的表的自身关联
Models//access.go

package models

import (
    _ "github.com/jinzhu/gorm"
)

type Access struct {
    Id          int
    ModuleName  string //模块名称
    ActionName  string //操作名称
    Type        int    //节点类型 :  1、表示模块    2、表示菜单     3、操作
    Url         string //路由跳转地址
    ModuleId    int    //此module_id和当前模型的_id关联      module_id= 0 表示模块
    Sort        int
    Description string
    Status      int
    AddTime     int
        // 表的自身关联
    AccessItem  []Access `gorm:"foreignkey:ModuleId;association_foreignkey:Id"`
}

func (Access) TableName() string {
    return "access"
}

通过自身关联实现的最终效果就是这样的

可以很明显的看出权限列表所展示出来的层次关系

而实现这个效果其实也很简单,在beego里面仅仅只需要两行代码

access := []models.Access{}
models.DB.Preload("AccessItem").Where("module_id=0").Find(&access)

可能,这样大家也看出个什么名堂,我把代码稍微改写一下,在浏览器中输出json格式的数据

access := []models.Access{}
models.DB.Preload("AccessItem").Where("module_id=0").Find(&access)
this.Data["json"] = access
this.ServeJSON()

再来看看数据库里面的数据,我相信你肯定一目了然

  1. 首先,通过models.DB.Preload("AccessItem")预加载先前设定好的自身关联
  2. 接着,通过Where("module_id=0")筛选关联完成的三大模块(角色,用户,权限)
  3. 最后,通过this.TplName = "admin/access/index.html" 将数据渲染到前台页面
  4. 前台模板中,再通过双层for循环,输出有层次的结果

今天总算是把RBAC权限管理功能写完了

  • 实现角色授权功能
  • 根据当前登录用户的角色信息动态显示左侧菜单
  • 根据当前登录用户的权限,动态设置路由

角色授权功能

效果图如下:

完成这个功能大致可以分为五步

  1. 获取角色ID

    // 当点击授权按键的时候,会跳转到授权页面,通过url即可获取ID值
    // http://127.0.0.1:8080/admin/role/auth?id=1
    roleId,err := this.GetInt("id")
    if err != nil {
    this.Error("传入参数错误","/role")
    return
    }
  2. 获取全部权限

    access := []models.Access{}
    models.DB.Preload("AccessItem").Where("module_id=0").Find(&access)
  3. 获取当前角色拥有的权限,并把权限id放在一个map对象里面

    roleAccess := []models.RoleAccess{}
    models.DB.Where("role_id=?",roleId).Find(&roleAccess)
    roleAccessMap := make(map[int]int)
    for _, v := range roleAccess {
    roleAccessMap[v.AccessId] = v.AccessId
    }
  4. 循环遍历所有的权限数据,判断当前权限的id是否在角色权限的map对象当中
    如果存在,将checked字段变为true

    for i := 0; i <len(access) ; i++ {
     if _,ok := roleAccessMap[access[i].Id];ok{
         access[i].Checked = true
        }
    for j := 0; j <len(access[i].AccessItem) ; j++ {
        if _,ok := roleAccessMap[access[i].AccessItem[j].Id];ok{
        access[i].AccessItem[j].Checked = true
        }
    }
    }
  5. 渲染数据到后台页面

    roleName := models.Role{Id: roleId}
    models.DB.Find(&roleName)
    this.Data["roleName"] = roleName.Title
    this.Data["accessList"] = access
    this.Data["roleId"] = roleId
    this.TplName = "admin/role/auth.html"

后台HTML的代码就不演示了,两个range循环就能把数据完整的显示出来

当我们需要修改角色权限的时候

// 获取需要修改的角色ID
role_id,err:= this.GetInt("role_id")
if err != nil {
    this.Error("传入参数错误","/role")
    return
}
// 获取后台表单传来的节点信息,也就是勾选的权限
accessNode := this.GetStrings("access_node")
// 通过获取的角色ID,查找出角色权限表中的数据,并将其删除
roleAccess := models.RoleAccess{}
models.DB.Where("role_id=?",role_id).Delete(&roleAccess)
// 最后通过for range循环,将节点信息,重新赋值给当前角色的角色权限表
for _, v := range accessNode {
    accessId, _ := strconv.Atoi(v)
    roleAccess.AccessId =accessId
    roleAccess.RoleId = role_id
    models.DB.Create(&roleAccess)
}
this.Success("授权成功","/role/auth?id="+strconv.Itoa(role_id))

动态显示左侧菜单

我们将角色授权页面完成后,就需要动态显示左侧菜单
跟显示角色权限的逻辑大同小易

  1. session里面获取登录信息,并获取角色id
  2. 获取全部权限
  3. 获取当前角色拥有的权限,并把权限id放在一个map对象里面
  4. 循环遍历所有的权限数据,判断当前权限的id是否在角色权限的map对象当中
    如果存在,将checked字段变为true
  5. 渲染数据到后台页面

但是,在管理员列表中,我们设置超级管理员
而超级管理员,可以拥有所有权限
那么,我们就给把当前登录用户是否为超级管理员的信息,传给后台的HTML代码中,进行逻辑处理

{{$isSuper := .isSuper}}
{{if eq $isSuper 1}}

通过判断是否为超级管理员,执行不同的range循环

HTML逻辑代码

<ul class="aside">
    {{$isSuper := .isSuper}}
    {{range $key, $val := .accessList}}
        {{if eq $isSuper 1}}
            <li>
                <h4>{{$val.ModuleName}}</h4>
                <ul>
                    {{range $k, $v := $val.AccessItem}}
                        {{if eq $v.Type 2}}
                            <li  class="list-group-item"> <a target="rightMain" href="/{{config "String" "adminPath" ""}}{{$v.Url}}"> {{$v.ActionName}}</a></li>
                        {{end}}
                    {{end}}
                </ul>
            </li>

        {{else}}
            {{if eq $val.Checked true }}
                <li>
                    <h4>{{$val.ModuleName}}</h4>
                    <ul>
                        {{range $k, $v := $val.AccessItem}}
                            {{if eq $v.Checked true}}
                                {{if eq $v.Type 2}}
                                    <li  class="list-group-item"> <a target="rightMain" href="/{{config "String" "adminPath" ""}}{{$v.Url}}"> {{$v.ActionName}}</a></li>
                                {{end}}
                            {{end}}
                        {{end}}
                    </ul>
                </li>
            {{end}}
        {{end}}
    {{end}}
</ul>


最后达到的效果就是酱紫的

设置权限路由

当我们完成动态显示左侧菜单,会发现一个问题
虽然,有些被限制权限的管理员,左侧只有一个或者两个管理模块
但是,通过在浏览器的地址栏中输入对应的地址,依然可以访问到他不应该到达的网页

所以,我们就需要根据登录用户的角色权限,动态设置路由地址


在设置路由前,由于涉及到写很多代码
我们就不在adminRouter.go里面编写路由逻辑
我们可以新建一个文件夹middleware(中间件),再创建adminAuth.go文件编成逻辑
其中的逻辑其实也不是很复杂,我就直接上代码了

func AdminAuth(ctx *context.Context)  {
    adminPath := beego.AppConfig.String("adminPath")
    userinfo,ok := ctx.Input.Session("userinfo").(models.Manager)
    pathname := ctx.Request.URL.String()
    // 判断进入后台的用户是否登录
    if !(ok&&userinfo.Username != "") { // 若没有登录重定向登录页面
        if pathname!= "/"+adminPath+"/login" && pathname!="/"+adminPath+"/login/doLogin" {
            ctx.Redirect(302, "/"+adminPath+"/login")
        }
    }else { // 若登录,判断此用户的权限是否可以访问相应的路由
        pathname = strings.Replace(pathname,"/"+adminPath,"",1)
        urlObj,_ := url.Parse(pathname)
        // 判断管理员是否是超级管理员
        if userinfo.IsSuper != 1 && !excludeAuthPath(urlObj.Path) {
            // 1:根据角色ID获取当前角色的权限列表,然后把权限ID放在一个map类型的对象里面
            roleId := userinfo.RoleId
            roleAccess := []models.RoleAccess{}
            models.DB.Where("role_id=?",roleId).Find(&roleAccess)
            roleAccessMap := make(map[int]int)
            for _, val := range roleAccess {
                roleAccessMap[val.AccessId] = val.AccessId
            }
            // 2:获取当前访问的url(row--->23-24),并获取对应的权限id
            access := models.Access{}
            models.DB.Where("url=?",urlObj.Path).Find(&access)
            // 3:判断当前用户访问的路由,是否在用户所拥有的权限列表中
            if _,ok := roleAccessMap[access.Id];!ok {
                ctx.WriteString("请注意素质,你没有权限访问!")
                return
            }
        }
    }
}


我以为完成RBAC权限管理后就不会怎么涉及到增删改查的内容,结果今天又是增删改查的一天啊啊啊...
当然,还是完了一些新花样,通过Ajax异步修改网页中的状态,并修改数据库当中的数据

今天主要完成的内容如下

  • 轮播图管理的增删改查
  • 封装上传图片方法
  • 异步修改网页中的数据
  • 通过sort字段的大小,改变左侧菜单的排列顺序
  • 商品分类管理的增删改查
  • 商品类型管理的增删改查

轮播图管理

代码部分其实没什么好讲,反正都是先定义数据模型,然后设置路由,最后写控制器里面的方法

封装上传图片方法

轮播图最重要的是什么,是轮播吗?不是!
而是图片呀,所以说我们就需要有一个上传图片的方法

  1. 获取图片,并设置关闭文件流

    // picName为HTML代码中图片上传表单的name值
    f, h, err := this.GetFile(picName)
    if err != nil {
        return "", err
    }
    defer f.Close()
  2. 获取后缀名 判断类型是否正确 .jpg .png .gif .jpeg

    extName := path.Ext(h.Filename)
    
    allowExtMap := map[string]bool{
    ".jpg":  true,
    ".png":  true,
    ".gif":  true,
    ".jpeg": true,
    }
    
    if _, ok := allowExtMap[extName]; !ok {
    return "", errors.New("图片后缀名不合法")
    }
  3. 创建图片保存目录 static/upload/20210201

    day := models.GetDay()
    dir := "static/upload/" + day
    
    if err := os.MkdirAll(dir, 0666); err != nil {
    return "", err
    }
  4. 生成文件名称(144325235235.png),并保存图片

    fileUnixName := strconv.FormatInt(models.GetUnix(), 10)
    //static/upload/20200623/144325235235.png
    saveDir := path.Join(dir, fileUnixName+extName)
    
    this.SaveToFile(picName, saveDir)

异步修改数据

增删改查真没什么好讲的了,我还是说说异步操作吧
首先定义一个API接口,用来操作数据库

  1. 通过url获取ID值,表名称,字段名称

    // ?id=10&table=focus&field=status
    id,err1 := this.GetInt("id")
    if err1 != nil {
    this.Data["json"] = map[string]interface{}{
        "success":false,
        "msg":"非法请求",
    }
    this.ServeJSON()
    return
    }
    table := this.GetString("table")
    field := this.GetString("field")
  2. 通过models.DB.Exec()方法,执行原生sql语句,完成对数据库中数据的操作
    最后通过this.ServeJSON()方法,返回JSON格式的信息

    err2 := models.DB.Exec("update "+table+" set "+field+"=ABS("+field+"-1) where id=?",id).Error
    if err2 != nil {
    beego.Error("错误")
    this.Data["json"] = map[string]interface{}{
        "success":false,
        "msg":"更新数据失败",
    }
    this.ServeJSON()
    return
    }
    
    this.Data["json"] = map[string]interface{}{
    "success":true,
    "msg":"更新数据成功",
    }
    this.ServeJSON()

接口完成后,就需要去JS代码了,完成对接口的调用操作

// 获取点击事件
$(".chStatus").click(function () {
    // 获取点击的标签中的自定义属性值
    var id = $(this).attr("data-id")
    var table = $(this).attr("data-table")
    var field = $(this).attr("data-field")
    var el = $(this)
    // 执行Ajax操作,调用上面定义的API接口
    $.get("/admin/main/changeStatus", {id: id, table: table, field: field}, function (response) {
        if (response.success) {
            if (el.attr("src").indexOf("yes") != -1){
                el.attr("src","/static/admin/images/no.gif")
            }else {
                el.attr("src","/static/admin/images/yes.gif")
            }
        }else{
            console.log(response)
        }
    })
})

使用Ajax执行API,就能很简单的完成网页中的异步操作

通过sort字段排序

当我们完成对状态的异步操作,同样的原理,还可以对网页显示出来的sort字段进行异步修改
而改变access表中的sort字段的就可以实现,改变左侧模块的排列顺序
怎么改变在网页中修改sort的值,我就不在赘述
重点讲一下,如果通过GORM完成降序输出数据
其实实现起来也很简单,就一行代码,GORM都为我们封装好了

models.DB.Preload("AccessItem", func(db *gorm.DB) *gorm.DB {
    return db.Order("access.sort DESC")
}).Order("sort desc").Where("module_id=0").Find(&access)
  1. 首先将模块进行降序排序
  2. 接着,通过表的自身关联,进行菜单的降序排序

初识商品模块

后台管理的用户权限和轮播图管理完成后,我也马不停蹄的接触到了商品模块
但是,当我看到商品模块所涉及到的数据库ER图的时候,我的内心是崩溃的
因为这是我迄今为止见到的最复杂的ER图,TM这增删改查起来......


商品分类管理和商品类型管理还是不是很复杂也就是一些增删改查
最后的列表效果如下

今天就到此为止吧,明天继续肝!


最近确实有点累了,下午接近五点的时候,实在是不想写代码了
胸口很闷,明天还是出去走走;项目嘛,慢慢做,反正有的是时间...

没写代码就去看了部电影《麦路人》,感触很深
本来心情就不是很好,结果看完更加压抑了

还是继续聊聊今天都做了些什么吧

  • 商品类型属性的增删查改
  • Golang图片处理,生成二维码
  • 商品模块增删改查
  • WYSIWYG富文本编辑器

商品类型属性的增删查改

昨天,完成了商品类型的增删改查
今天,这个商品类型属性
不用说,这两个数据表肯定涉及到关联,直接看ER图

最后的效果是这样的

可以看到,增加时间这里是存在一些问题的,但是问题不大,等会加个自定义模板函数就可以解决

{{$value.AddTime | unixToDate}}

Golang图片处理,生成二维码

这里我们需要用到两个第三方包

  • 图片处理:github.com/hunterhug/go_image

    //图像位置 
    filename := "static/upload/0.jpg" 
    //保存位置 
    savepath := "static/upload/0_600.jpg" 
    //按照宽度进行等比例缩放 
    err := ScaleF2F(filename, savepath, 20) 
    if err != nil { 
    fmt.Printf("%s\n", err.Error()) 
    } else {
    fmt.Printf("生成按宽度缩放图%s\n", savepath)
    } 
  • 生成二维码:github.com/skip2/go-qrcode

    err := qrcode.WriteFile("https://syjun.vip", qrcode.Medium, 256, "static/qr.png") 
    if err != nil {
    fmt.Println("write error") 
    }

商品增删改查

关于操作goods表,终于来了
goods表中一共27个字段
光去创建这个数据表都要创建好久,所以我选择直接复制粘贴,运行sql文件
老规矩嘛,接着定义goods表的模型

Goods表模型

package models

import (
    _ "github.com/jinzhu/gorm"
)

type Goods struct {
    Id            int
    Title         string
    SubTitle      string
    GoodsSn       string
    CateId        int
    ClickCount    int
    GoodsNumber   int
    Price         float64
    MarketPrice   float64
    RelationGoods string
    GoodsAttr     string
    GoodsVersion  string
    GoodsImg      string
    GoodsGift     string
    GoodsFitting  string
    GoodsColor    string
    GoodsKeywords string
    GoodsDesc     string
    GoodsContent  string
    IsDelete      int
    IsHot         int
    IsBest        int
    IsNew         int
    GoodsTypeId   int
    Sort          int
    Status        int
    AddTime       int
}

func (Goods) TableName() string {
    return "goods"
}


然后啊,就是定义路由,定义控制器,直到在控制器中把逻辑写完
当然,写完肯定是不容易的,毕竟字段就有27个...


所以,就先渲染了商品的首页和商品添加页面

func (this *GoodsController) Get() {
    this.TplName = "admin/goods/index.html"
}

func (this *GoodsController) Add() {
    this.TplName = "admin/goods/add.html"
}

因为还没有写添加商品的逻辑,所以商品首页是没有任何内容的
即使写了添加商品的逻辑,也没有办法显示,原因也很简单嘛,没有向后台页面传输数据呀
就先看看使用Bootstrap写的添加页面吧


可以看到,商品详述的编辑页面使用到了富文本编辑器

WYSIWYG富文本编辑器

有一说一,我今天第一次见到这个单词WYSIWYG
还专门去百度了一下是什么意思
WYSIWYG:What You See Is What You Get(所见即所得)牛啤

wysiwyg-editor

一个设计精美的基于 HTML5 的 WYSIWYG HTML 编辑器,它非常小但是非常强大。我们不仅 可以在golang中使用它,并还可以在 Nodejs, PHP, .NET, Java, and Python以及前端框架vue 、 react 、angular 中使用

  1. https://github.com/froala/wysiwyg-editor
  2. https://www.froala.com/wysiwyg-editor/docs/options

Beego 中使用 wysiwyg-editor

Beego 中汉化 wysiwyg-ed

https://www.froala.com/wysiwyg-editor/languages

  1. 引入 zh_cn 的语言包
  2. 配置 language: 'zh_cn'

Beego 中自定义 wysiwyg-editor 的导航条

https://www.froala.com/wysiwyg-editor/docs/options#toolbarBottom

new FroalaEditor('#content', {
    height: 300,
    language: 'zh_cn',
    toolbarButtons: [['bold', 'strikethrough', 'subscript', 'superscript', 'outdent', 'indent', 'clearFormatting', 'insertTable', 'html'], ['undo', 'redo']],
    toolbarButtonsXS: [['bold', 'strikethrough', 'subscript', 'superscript', 'outdent', 'indent', 'clearFormatting', 'insertTable', 'html'], ['undo', 'redo']]
});

Beego 中配置 wysiwyg-editor 上传图片

https://www.froala.com/wysiwyg-editor/docs/options#imageUploadURL

第一步:实例化imageUploadURL函数

new FroalaEditor('#content', {
    height: 200,
    language: 'zh_cn',
    imageUploadURL:'/{{config "String" "adminPath" ""}}/goods/doUpload'
});

第二步:使用GO编写API接口
注意:后台返回数据格式:{link: 'path/to/image.jpg'}

func (this *GoodsController) DoUpload() {
    savePath,err :=  this.UploadImg("file")
    if err != nil {
        beego.Error("上传图片失败")
        this.Data["json"] = map[string]interface{}{
            "link":"",
        }
        this.ServeJSON()
    }else {
        // 返回json数据
        this.Data["json"] = map[string]interface{}{
            "link":"/"+savePath,
        }
        this.ServeJSON()
    }
}

今天就到此为止吧,明天一定出去走走


今天完成了昨天立下的一个小小的要求,出门转转
生活了十几年的小县城,感觉这里真的好小,但是这里真的很好

今天就主要完成了对商品数据的表的操作,虽然说昨天把商品表的准备工作(建模,设置路由,初始化控制器)都弄完了,但是并没有开始写逻辑

  • 增加商品页面选择分类、颜色、关联商品类型
  • 动态生成商品类型属性表单
  • 配置批量上传图片插件
  • 实现增加商品,并开启协程完成操作
  • 实现修改商品

增加商品页面选择分类、颜色、关联商品类型

每个商品都有不同的颜色和不同的属性,所以我们又建了两个表分别是goods_attr和goods_color,用来存放这些数据
在数据库中建表完成后,当然是去项目当中创建数据库模型了,但是关于这两个表,我们不用创建路由和创建控制器
我们直接在goods的控制器中,修改相应的数据

  1. 获取商品分类,分类表中涉及到表的自身关联,所以我们又用到了Preload

    goodsCate := []models.GoodsCate{}
    models.DB.Where("pid=?", 0).Preload("GoodsCateItem").Find(&goodsCate)
    this.Data["goodsCateList"] = goodsCate
  2. 获取颜色信息

      goodsColor := []models.GoodsColor{}
      models.DB.Find(&goodsColor)
      this.Data["goodsColor"] = goodsColor
  3. 获取商品类型信息

    goodsType := []models.GoodsType{}
    models.DB.Find(&goodsType)
    this.Data["goodsType"] = goodsType
  4. 最后一步,不用多讲,我TM直接渲染

    this.TplName = "admin/goods/add.html"

动态生成商品类型属性表单

可能看这个标题,很难懂具体是什么操作,那就看看下面的图片吧


这个一看就涉及到了Javascript的知识,并且还涉及到从数据库中取数据
根据前面的学习,肯定是需要使用GO写个API接口的
关于这个接口,其实很简单,就是通过获取类型属性的ID,接着去数据库查询,最后返回Json数据给后台页面

func (this *GoodsController) GetGoodsTypeAttribute() {
    cate_id, err1 := this.GetInt("cate_id")
    GoodsTypeAttribute := []models.GoodsTypeAttribute{}
    err2 := models.DB.Where("cate_id=?", cate_id).Find(&GoodsTypeAttribute).Error

    if err1 != nil || err2 != nil {
    this.Data["json"] = map[string]interface{}{
        "result":  "",
        "success": true,
    }
    this.ServeJSON()
    } else {
        this.Data["json"] = map[string]interface{}{
        "result":  GoodsTypeAttribute,
        "success": true,
    }
    this.ServeJSON()
    }

}

难的就是JS部分的代码,主要是处理起来很麻烦,不得不说JS拼接字符串实在是太难受了...
逻辑其实也不难,就是通过for循环和if判断处理数据
也没什么好讲,主要就是拼接字符串的时候,一定要小心小心再小心

$(function(){
    $("#goods_type_id").change(function(){
       var cate_id = $(this).val()
       var str=""
       var data=""
       $.get('/{{config "String" "adminPath" ""}}/goods/getGoodsTypeAttribute',{"cate_id":cate_id},function(response){
            console.log(response)
            if(response.success){
                data=response.result;
                for (var i = 0; i < data.length; i++) {
                    if(data[i].attr_type==1){
                        str += '<li><span>' + data[i].title + ':  </span>  <input type="hidden" name="attr_id_list" value="' + data[i].id + '" />   <input type="text" name="attr_value_list" /></li>'
                    }else if (data[i].attr_type == 2) {
                        str += '<li><span>' + data[i].title + ':  </span> <input type="hidden" name="attr_id_list" value="' + data[i].id + '">  <textarea cols="50" rows="3" name="attr_value_list"></textarea></li>'
                    }else{
                        var attrArray=data[i].attr_value.split("\n")
                        str += '<li><span>' + data[i].title + ':  </span>  <input type="hidden" name="attr_id_list" value="' + data[i].id + '" />';
                        str += '<select name="attr_value_list">'
                        for (var j = 0; j < attrArray.length; j++) {
                            str += '<option value="' + attrArray[j] + '">' + attrArray[j] + '</option>';
                        }
                        str+='</select>'
                        str+='</li>' 
                    }
                }
                $("#goods_type_attribute").html(str);

            }
       })
    })
})

配置批量上传图片插件


要实现这么一个功能,我们需要用到一个插件diyUpload.js

  1. 在HTML文件中引入

    <link rel="stylesheet" type="text/css" href="/static/admin/diyUpload/css/diyUpload.css">
    <script type="text/javascript" src="/static/admin/diyUpload/js/diyUpload.js"></script>
  2. 定义一个div盒子,并自定义id值

    <div id="photoUploader"></div>
  3. 使用JS写逻辑

    $(function () {
        $("#photoUploader").diyUpload({
            url:'/{{config "String" "adminPath" ""}}/goods/doUpload',
            success:function (data) {
    console.info(data)
    var photoStr = '<input type="hidden" name="goods_image_list" value="' + data.link + '" />'
    $("#photoList").append(photoStr)
            },
            error:function (err) {
    console.info(err)
            },
            fileNumLimit : 50,// 最多同时上传50张图片
            fileSizeLimit : 51200 * 1024, //总文件最大上传50mb
            fileSingleSizeLimit : 5120 * 1024, // 最大上传5mb的图片
            })
    })
    函数里面调用的API,就是我们之前定义的上传图片的API,这里又用到了哟
    

实现增加商品,并开启协程完成操作

要实现增加商品的操作,大致可以分为六步

  1. 获取表单提交过来的数据

    title := this.GetString("title")
    subTitle := this.GetString("sub_title")
    goodsSn := this.GetString("goods_sn")
    cateId, _ := this.GetInt("cate_id")
    goodsNumber, _ := this.GetInt("goods_number")
    marketPrice, _ := this.GetFloat("market_price")
    price, _ := this.GetFloat("price")
    relationGoods := this.GetString("relation_goods")
    goodsAttr := this.GetString("goods_attr")
    goodsVersion := this.GetString("goods_version")
    goodsGift := this.GetString("goods_gift")
    goodsFitting := this.GetString("goods_fitting")
    goodsColor := this.GetStrings("goods_color")
    goodsKeywords := this.GetString("goods_keywords")
    goodsDesc := this.GetString("goods_desc")
    goodsContent := this.GetString("goods_content")
    isDelete, _ := this.GetInt("is_delete")
    isHot, _ := this.GetInt("is_hot")
    isBest, _ := this.GetInt("is_best")
    isNew, _ := this.GetInt("is_new")
    goodsTypeId, _ := this.GetInt("goods_type_id")
    sort, _ := this.GetInt("sort")
    status, _ := this.GetInt("status")
    addTime := int(models.GetUnix())

    没错,就是这么多,毕竟那么多字段呢,不是随便写着玩的

  2. 获取颜色信息 把颜色转化成字符串

    goodsColorStr := strings.Join(goodsColor, ",")
  3. 上传图片 生成缩略图

    goodsImg, _ := this.UploadImg("goods_img")
  4. 增加商品数据

    goods := models.Goods{
        Title:         title,
        SubTitle:      subTitle,
        GoodsSn:       goodsSn,
        CateId:        cateId,
        ClickCount:    100,
        GoodsNumber:   goodsNumber,
        MarketPrice:   marketPrice,
        Price:         price,
        RelationGoods: relationGoods,
        GoodsAttr:     goodsAttr,
        GoodsVersion:  goodsVersion,
        GoodsGift:     goodsGift,
        GoodsFitting:  goodsFitting,
        GoodsKeywords: goodsKeywords,
        GoodsDesc:     goodsDesc,
        GoodsContent:  goodsContent,
        IsDelete:      isDelete,
        IsHot:         isHot,
        IsBest:        isBest,
        IsNew:         isNew,
        GoodsTypeId:   goodsTypeId,
        Sort:          sort,
        Status:        status,
        AddTime:       addTime,
        GoodsColor:    goodsColorStr,
        GoodsImg:      goodsImg,
    }
    err1 := models.DB.Create(&goods).Error
    if err1 != nil {
        this.Error("增加失败", "/goods/add")
    }
  5. 增加图库 信息
    这里就是开启了一个协程,使用go关键字

    wg.Add(1)
    go func() {
        goodsImageList := this.GetStrings("goods_image_list")
        for _, v := range goodsImageList {
            goodsImgObj := models.GoodsImage{}
            goodsImgObj.GoodsId = goods.Id
            goodsImgObj.ImgUrl = v
            goodsImgObj.Sort = 10
            goodsImgObj.Status = 1
            goodsImgObj.AddTime = int(models.GetUnix())
            models.DB.Create(&goodsImgObj)
        }
        wg.Done()
    }()
  6. 增加商品属性,最后返回结果

    wg.Add(1)
    go func() {
    attrIdList := this.GetStrings("attr_id_list")
    attrValueList := this.GetStrings("attr_value_list")
    for i := 0; i < len(attrIdList); i++ {
        goodsTypeAttributeId, _ := strconv.Atoi(attrIdList[i])
        goodsTypeAttributeObj := models.GoodsTypeAttribute{Id: goodsTypeAttributeId}
        models.DB.Find(&goodsTypeAttributeObj)
    
        goodsAttrObj := models.GoodsAttr{}
        goodsAttrObj.GoodsId = goods.Id
        goodsAttrObj.AttributeTitle = goodsTypeAttributeObj.Title
        goodsAttrObj.AttributeType = goodsTypeAttributeObj.AttrType
        goodsAttrObj.AttributeId = goodsTypeAttributeObj.Id
        goodsAttrObj.AttributeCateId = goodsTypeAttributeObj.CateId
        goodsAttrObj.AttributeValue = attrValueList[i]
        goodsAttrObj.Status = 1
        goodsAttrObj.Sort = 10
        goodsAttrObj.AddTime = int(models.GetUnix())
        models.DB.Create(&goodsAttrObj)
    }
    wg.Done()
    }()
    wg.Wait()
    
    this.Success("操作成功","/goods")
    
    this.Ctx.WriteString("执行增加")

    可以发现,在使用go关键字开启协程的时候,前面还有一行代码wg.Add(1),并且在协程操作完成后,还有一段wg.Done()
    而这个wg,是在代码开头定义的一个变量var wg sync.WaitGroup,使用sync.WaitGroup,可以解决在多协程运行时的阻塞问题

实现修改商品

实现修改商品的操作,其实和增加商品差不多
主要的差别就是,在点击编辑的时候,这个商品的信息,会自动填写到后台对应的位置上
而这样的操作,其实就是通过商品ID在数据库查询到数据后,传递给后台页面,并渲染出来
当需要修改的时候,只需使用Save一下就可以了
但是,在某些操作上,需要先将以前的数据删除,接着保存新的数据才可行
就比如,修改商品类型属性数据

  1. 删除当前商品id对应的类型属性
  2. 获取新数据,接着执行增加
//删除当前商品id对应的类型属性
goodsAttrObj := models.GoodsAttr{}
models.DB.Where("goods_id=?", goods.Id).Delete(&goodsAttrObj)
//执行增加
wg.Add(1)
go func() {
    attrIdList := this.GetStrings("attr_id_list")
    attrValueList := this.GetStrings("attr_value_list")
    for i := 0; i < len(attrIdList); i++ {
        goodsTypeAttributeId, _ := strconv.Atoi(attrIdList[i])
        goodsTypeAttributeObj := models.GoodsTypeAttribute{Id: goodsTypeAttributeId}
        models.DB.Find(&goodsTypeAttributeObj)

        goodsAttrObj := models.GoodsAttr{}
        goodsAttrObj.GoodsId = goods.Id
        goodsAttrObj.AttributeTitle = goodsTypeAttributeObj.Title
        goodsAttrObj.AttributeType = goodsTypeAttributeObj.AttrType
        goodsAttrObj.AttributeId = goodsTypeAttributeObj.Id
        goodsAttrObj.AttributeCateId = goodsTypeAttributeObj.CateId
        goodsAttrObj.AttributeValue = attrValueList[i]
        goodsAttrObj.Status = 1
        goodsAttrObj.Sort = 10
        goodsAttrObj.AddTime = int(models.GetUnix())
        models.DB.Create(&goodsAttrObj)
    }
    wg.Done()
}()


在写这个项目之前,我在上个学期(大二上期)期末的时候,由于课程需求需要完成一个网站的开发.我使用的也是MVC的框架,PHP中的Laveran,做的是一个博客系统.当时,有一个搜索博客的功能,我一直觉得很复杂,就没去做,时间也比较有限,也就不敢去做.
但是,在今天的开发中,涉及到了搜索商品的操作,结果在Beego中其实很简单,可能一直是我想复杂了吧

今天完成的内容如下

  • 异步关联颜色、异步删除图库数据
  • 实现商品列表的分页
  • 商品列表的搜索功能
  • 导航模块的增删改查
  • 项目商店的设置

异步关联颜色、异步删除图库数据

提到异步,在这个项目中,其实就是通过Ajax配合Go写好的API,在网页中进行操作
首先就是写JavaScript代码,定义Ajax请求,就像这样

异步关联颜色

$(function () {
    $(".relation_goods_color").change(function () {
        var color_id = $(this).val()
        var goods_image_id = $(this).attr("goods_image_id")
        $.get('/{{config "String" "adminPath" ""}}/goods/changeGoodsImageColor',{color_id:color_id,goods_image_id:goods_image_id},function(response){
            console.log(response);
        });
    })

})

异步删除图库数据
在执行删除操作时,通过使用confirm()函数弹出一个弹窗,让用户选择
因为删除是一次不可逆的操作,删了就真没了

$(".goods_image_delete").click(function(){
    var goods_image_id=$(this).attr("goods_image_id");
    var flag = confirm("确定要删除吗?");
    var _that=this;
    if(flag){
        $.get('/{{config "String" "adminPath" ""}}/goods/removeGoodsImage',{goods_image_id:goods_image_id},function(response){
            if(response.success){
                $(_that).parent().remove()
            }
        });

    }
})

可以看到Ajax就是请求一条链接,所以在写完API的逻辑后,一定要记得去添加对应的路由地址

异步关联颜色

func (c *GoodsController) ChangeGoodsImageColor() {
    colorId, err1 := c.GetInt("color_id")
    goodsImageId, err2 := c.GetInt("goods_image_id")
    goodsImage := models.GoodsImage{Id: goodsImageId}
    models.DB.Find(&goodsImage)
    goodsImage.ColorId = colorId
    err3 := models.DB.Save(&goodsImage).Error

    if err1 != nil || err2 != nil || err3 != nil {
        c.Data["json"] = map[string]interface{}{
            "result":  "更新失败",
            "success": false,
        }
        c.ServeJSON()
    } else {
        c.Data["json"] = map[string]interface{}{
            "result":  "更新成功",
            "success": true,
        }
        c.ServeJSON()
    }
}

异步删除图库数据
在删除图库数据时,首先删除的是数据库存放的地址,但是图片还是保存在电脑上的磁盘上
这时,我们就可以在删除图片之前获取到地址,接着使用os.Remove()删除图片

func (c *GoodsController) RemoveGoodsImage() {
    goodsImageId, err1 := c.GetInt("goods_image_id")
    goodsImage := models.GoodsImage{Id: goodsImageId}
    models.DB.Find(&goodsImage)
    image_url := goodsImage.ImgUrl
    err2 := models.DB.Delete(&goodsImage).Error
    //   /static/upload/20200622/1592799750042592100.jpg

    if err1 != nil || err2 != nil {
        c.Data["json"] = map[string]interface{}{
            "result":  "删除失败",
            "success": false,
        }
        c.ServeJSON()
    } else {
        //删除图片
        os.Remove(strings.Trim(image_url, "/"))
        c.Data["json"] = map[string]interface{}{
            "result":  "删除",
            "success": true,
        }
        c.ServeJSON()
    }
}

实现商品列表的分页

在实现分页之前,我们需要了解一下数据库分页查询数据的原理算法

规则:规定每页 5 条数据的查询方式

  • 查询第一页(page=1):
    select * from userinfo limit 0,5
  • 查询第二页(page=2):
    select * from userinfo limit 5,5
  • 查询第三页(page=3):
    select * from userinfo limit 10,5

总结:分页查询的 sql 语句

select * from userinfo limit ((page-1)*pageSize),pageSize


在项目中,我们引入jqPaginator 实现分页

  1. 首先引入 jQuery 和 jqPaginator

    <script type="text/javascript" src="/static/admin/js/jqPaginator.js"></script>
  2. 定义一个空的 div 让这个 div 的 id,class = pagination

    <div id="pagination" class="pagination fr">
  3. 初始化分页

    $(function(){
        $('#pagination').jqPaginator({
            totalPages: {{.totalPages}},
            visiblePages: 10,
            currentPage: {{.page}},
            onPageChange: function (num, type) {
                console.log(num,type)
                   if(type=="change"){
                        location.href="/{{config "String" "adminPath" ""}}/goods?page="+num;
                    }
            }
        });
    })

接着,就是在控制器里面写逻辑了,从上面的js代码也可以看出来

//由于执行分页操作的时候,实际就是一个get请求
//所以我们可以通过GetInt()获取当前页
page, _ := this.GetInt("page")
if page == 0 {
    page = 1
}
//每一页显示的数量
pageSize := 3
//查询goods表里面的数量
var count int
models.DB.Where(where).Table("goods").Count(&count)
//最后传递数据给后台页面
this.Data["totalPages"] = math.Ceil(float64(count) / float64(pageSize))
this.Data["page"] = page

商品列表的搜索功能

要实现搜索功能,肯定需要一个搜索框呀,大概就是这个样子

这个搜索框,其实就是一个form表单,通过get方式传参,然后后端处理

<div class="panel-body">
    <form role="form" class="form-inline" method="get" action="/{{config "String" "adminPath" ""}}/goods">
        <div class="form-group">
            <label for="name">输入关键词</label>
            <input type="text" class="form-control" value="{{.keyword}}" id="keyword" name="keyword" placeholder="请输入名称">
        </div>

        <div class="form-group">
            <button type="submit" class="btn btn-default">开始搜索</button>
        </div>
    </form>
</div>

在Go当中,首先通过GetString()获取传来的参数,接着就是通过sql里面的like进行模糊查询,就是这么简单

//实现搜索功能
keyword := this.GetString("keyword")
where := "1=1"
if len(keyword) > 0 {
    where += " AND title like \"%" + keyword + "%\""
}
//查询数据
goodsList := []models.Goods{}
models.DB.Where(where).Offset((page - 1) * pageSize).Limit(pageSize).Find(&goodsList)

导航模块的增删改查 & 项目商店的设置

关于导航模块和商品设置模块,跟前面的东西都差不多
我也不想多讲了,我也讲累了,完全就是一个模子的东西,但是这些东西又不能缺少,就很...


果然,当项目写到一定程度的时候,如果想去修改一些内容,工作量真的不容小觑
所以,在写的时候,要有长远的考虑,不要讲究写得快,而忽视了整个项目的架构问题
即使,要做功能上的升级,也要尽可能的优化代码,把影响做得最小,要不然修改代码起来,真的喊恼火

今天完成的内容如下

  • 使用Golang上传图片到OSS云存储
  • 项目集成Oss云存储

Golang上传图片到OSS云存储

目前市场中的 OSS 对象存储服务有阿里云、腾讯云和七牛云等服务商提供,对象存储后台可以自己新建目录文件夹,存放不同类型的文件,而且会自动将系统的需求的文件按日期或分类进行存放。

在这个项目中,我们使用的是阿里云的OSS云存储
在Golang中使用阿里云的OSS很简单,就像这样

package main

import (
    "fmt"
    "os"
    "github.com/aliyun/aliyun-oss-go-sdk/oss"
)

func main() {
    // 创建OSSClient实例。
    client, err := oss.New("<yourEndpoint>", "<yourAccessKeyId>", "<yourAccessKeySecret>")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }

    // 获取存储空间。
    bucket, err := client.Bucket("<yourBucketName>")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }

    // 上传本地文件。
    err = bucket.PutObjectFromFile("<yourObjectName>", "<yourLocalFile>")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }
}
            

从上面代码可以知道我们目前需要BucketName、Endpoint、AccessKeyId、AccessKeySecret

BucketName:就是创建 bucket 的时候写的名称
Endpoint:地域节点
AccessKeyId、AccessKeySecret:即账号信息,可在AccessKey 管理查看

项目集成Oss云存储

在之前的项目中,我们都是将上传的图片放在本地的磁盘上面
实现这个功能,我们封装了一个方法UploadImg()用于处理对应的逻辑
而现在,我们需要将上传的图片放在云存储上,为了尽可能减小对其他代码块的影响,我们将UploadImg()里面的代码全部复制出来,重新封装了一个方法,并且为实现Oss存储也写了一个方法,最后通过重写UploadImg()的代码,获取配置文件中配置的ossStatus = true,进行判断是否将图片上传到Oss云存储

本地上传

func  (this *BaseController) LoclUploadImg(picName string) (string, error) {
    //1、获取上传的文件
    f, h, err := this.GetFile(picName)
    if err != nil {
        return "", err
    }
    //2、关闭文件流
    defer f.Close()
    //3、获取后缀名 判断类型是否正确  .jpg .png .gif .jpeg
    extName := path.Ext(h.Filename)

    allowExtMap := map[string]bool{
        ".jpg":  true,
        ".png":  true,
        ".gif":  true,
        ".jpeg": true,
    }

    if _, ok := allowExtMap[extName]; !ok {

        return "", errors.New("图片后缀名不合法")
    }
    //4、创建图片保存目录  static/upload/20210201
    day := models.GetDay()
    dir := "static/upload/" + day

    if err := os.MkdirAll(dir, 0666); err != nil {
        return "", err
    }
    //5、生成文件名称   144325235235.png
    fileUnixName := strconv.FormatInt(models.GetUnixNano(), 10)
    //static/upload/20200623/144325235235.png
    saveDir := path.Join(dir, fileUnixName+extName)
    //6、保存图片
    this.SaveToFile(picName, saveDir)

    return saveDir, nil
}

Oss云存储

func  (this *BaseController) OssUploadImg(picName string) (string, error) {
    //0:获取系统信息
    setting := models.Setting{}
    models.DB.First(&setting)

    //1、获取上传的文件
    f, h, err := this.GetFile(picName)
    if err != nil {
        return "", err
    }
    //2、关闭文件流
    defer f.Close()
    //3、获取后缀名 判断类型是否正确  .jpg .png .gif .jpeg
    extName := path.Ext(h.Filename)

    allowExtMap := map[string]bool{
        ".jpg":  true,
        ".png":  true,
        ".gif":  true,
        ".jpeg": true,
    }

    if _, ok := allowExtMap[extName]; !ok {
        return "", errors.New("图片后缀名不合法")
    }

    // 把文件流上传到oss里面

    // 创建OSSClient实例
    client, err := oss.New(setting.EndPoint, setting.Appid, setting.AppSecret)
    if err != nil {
        return "", err
    }

    // 获取存储空间
    bucket, err := client.Bucket(setting.BucketName)
    if err != nil {
        return "", err
    }

    //创建图片保存目录
    day := models.GetDay()
    dir := "static/upload/" + day
    //生成文件名称   144325235235.png
    fileUnixName := strconv.FormatInt(models.GetUnixNano(), 10)
    //static/upload/20200623/144325235235.png
    saveDir := path.Join(dir, fileUnixName+extName)

    // 上传文件流
    err = bucket.PutObject(saveDir, f)
    if err != nil {
        return "", err
    }
    return saveDir, nil
}

UploadImg()

func (this *BaseController) UploadImg(picName string) (string, error) {
    ossStatus,_:= beego.AppConfig.Bool("ossStatus")
    if  ossStatus== true {
        return this.OssUploadImg(picName)
    }else {
        return this.LoclUploadImg(picName)
    }
}

由于将图片上传到云存储后,后台页面访问图片的链接地址会发生变化,导致部分图片不能正常显示
所以,我们也在html代码中也做出相应的改变,就像这样
通过判断配置文件中,是否开启Oss云存储,进行相应的处理

{{$ossStatus := config "Bool" "ossStatus" false}}
{{if ne .goodsCate.CateImg ""}}
    {{if eq $ossStatus true}}
        <img width="500px" src="{{config "String" "ossDomain" ""}}/{{.goodsCate.CateImg}}" size="60" />
    {{else}}
        <img width="500px" src="/{{.goodsCate.CateImg}}" size="60" />
    {{end}}
{{end}}


终于,后台的内容完成得差不多了,是时候进行前台页面的渲染啦
刚刚去数了一下日子,居然写后台就写了10天,时间过得太快了吧
现在开始写前台的内容,必然碰到恶心的CSS,这个写起来是真的麻烦
前台的部分页面,在我买的课程里面给出了静态页面,我们需要做的就是进行前台页面的渲染
看着挺简单的是吧,其实并不然,因为还是会涉及到后端逻辑处理,就比如精确的拿到数据库里面的数据,然后传输到前台页面
而用户直观感受到的就是前台的页面,他们并不关心商城的后端逻辑具体是怎么实现的,他们只关心网页的体验
所以说,在写前台的时候,需要比写后台的时候更加仔细才行,加油吧!

今天其实就做了几件事情,主要还是在上传商品到数据库的时候耗费了大量时间
如果数据库中,没有我们想要的数据的话,前台页面是根本渲染不出来的

  • 渲染静态页面
  • 模板分离
  • 首页顶部导航渲染
  • 首页轮播图数据渲染
  • 首页左侧分类数据渲染
  • 首页楼层数据渲染 以及封装根据分类获取商品的公共方法

渲染静态页面

今天Blog的开头,我提到了小米商城的前台页面是有静态代码的
我们要实现把数据库的内容渲染到前台页面,首先要看到模子对吧
那么,渲染一个静态页面,我们任然需要设置路由,控制器

// 路由地址
beego.Router("/", &mindex.IndexController{})
// 控制器
this.TplName = "mindex/index/index.html"
// 通过this.TplName 将静态页面与路由地址关联起来
// 在beego当中就是这么简单

模板分离


通过观察商城的前台静态页面,我们可以发现,跟写后台的时候一样
页面当中,是有公共的部分的,所以我们依然可以将公共的部分代码,提取出来放在命名为public的文件夹中,这样也方便我们后期的管理

首页顶部导航渲染

当分离工作完成后,我们就可以真正的开始渲染前台页面啦
首先,我们从最简单的开始,渲染首页顶部导航

在后台我们是有导航管理模块的,所以我们就可以通过查询导航管理的数据,将数据渲染到前台页面


在导航模块中的有这么一个字段position
1代表:顶部 2代表:中间 3代表:底部
我们就可以通过这个条件,获取数据

topNav := []models.Nav{}
models.DB.Where("status=1 AND position=1").Order("sort desc").Find(&topNav)
this.Data["topNavList"] = topNav

渲染数据

<ul>
    {{range $key,$value := .topNavList}}
        <li><a href="{{$value.Link}}" {{if eq $value.IsOpennew 2}} target="_blank" {{end}}>{{$value.Title}}</a></li>
    {{end}}
    <div class="clear"></div>
</ul>

渲染轮播图的部分,跟这个完全就是一模一样的,我就不多赘述

首页中间导航渲染

中间导航的渲染,跟顶部导航不一样,因为这里涉及到关联商品
完成的效果就是这样的

实现起来其实也挺简单的
首先,通过获取中间导航所关联的商品
接着,通过关联的商品ID,在goods表中进行查询
最后,传输数据到前台

middleNav := []models.Nav{}
models.DB.Where("status=1 AND position=2").Order("sort desc").Find(&middleNav)

for i := 0; i < len(middleNav); i++ {
    // 获取关联商品
    middleNav[i].Relation = strings.ReplaceAll(middleNav[i].Relation,",",",")
    relation := strings.Split(middleNav[i].Relation,",")
    goods := []models.Goods{}
    models.DB.Where("id in (?)",relation).Select("id,title,goods_img").Find(&goods)
    middleNav[i].GoodsItem = goods
}
this.Data["middleNavList"] = middleNav

首页左侧分类渲染,和这个也是差不多的,而且还有简单一点,只进行了一张表的查询,我也不在赘述了

首页楼层数据渲染 以及封装根据分类获取商品的公共方法

今天碰到比较复杂的地方就是楼层渲染的部分,那么什么是楼层呢,还是直接看图吧

这就是一个楼层,可以称为手机楼层

我们首先通过商品分类获取到手机商品
接着,通过判断是否是热销或者精品商品,如果不是的话,就不显示
最后,通过Limit设置返回的数据数量,通过Order进行降序排序输出结果

因为,不仅仅只有手机这么一个楼层,所以我们将代码封装成了一个方法

func GetGoodsByCategory(cateId int, goodsType string, limitNum int) []Goods {
    goods := []Goods{}

    // 判断cateId是不是顶级分类
    goodsCate := []GoodsCate{}
    DB.Where("pid=?",cateId).Find(&goodsCate)
    var tempSlice []int
    if len(goodsCate) >0 {
        for i := 0; i <len(goodsCate) ; i++ {
            tempSlice = append(tempSlice, goodsCate[i].Id)
        }
    }
    tempSlice = append(tempSlice,cateId)
    where := "cate_id in (?)"
    switch goodsType {
    case "hot":
        where += " AND is_hot=1"
    case "best":
        where += " AND is_best=1"
    case "new":
        where += " AND is_new=1"
    default:
        break
    }

    DB.Where(where, tempSlice).Select("id,title,price,goods_img,sub_title").Limit(limitNum).Order("sort desc").Find(&goods)
    return goods
}

// 获取楼层数据
phone := models.GetGoodsByCategory(33,"hot",8)
this.Data["phoneList"] = phone

渲染数据

<div class="category_item_right">
    {{range $key, $val := .phoneList}}
        <div class="hot fl">
            <div class="xinpin"><span style="background:#fff"></span></div>
            <div class="tu"><a href=""><img src="{{$val.GoodsImg | formatImg}}"></a></div>
            <div class="miaoshu"><a href="">{{$val.Title}}</a></div>
            <div class="pingjia">{{$val.SubTitle}}</div>
            <div class="jiage">{{$val.Price}}元起</div>
        </div>
    {{end}}
</div>

修改CSS的操作,我就不展示出来了
它真的很烦,TMD写CSS烦死了


今天主要是把Redis的知识补学了一下,并在项目当中运用了Redis,其实之前也使用过Redis用于存储登录的Session信息,只不过当时只使用了它的一小部分,Redis其实是很强大的,而且性能极高;最后还是继续补充商品,完善主页的界面

Redis 的简单介绍

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库,它也属于 nosql

Redis 和 Memcached 类似,都是内存级别的数据缓存,主要用户数据缓存,它支持存储的 value 类型相对 更多,包括 string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和 hash(哈希类型)

Redis 不仅有丰富的特性(数据持久化到硬盘、 publish/subscribe、 key 过期),还有极高性能,经测试 Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s

Redis 的类型

值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型

Redis 字符串

Redis 字符串数据类型的相关命令用于管理 redis 字符串值

  • 查看所有的 key: keys *
  • 普通设置: set key value
  • 设置并加过期时间: set key value EX 30
  • 获取数据: get key
  • 删除指定数据: del key
  • 删除全部数据: flushall
  • 查看类型: type key
  • 设置过期时间: expire key 20

还有其他的Redis类型操作,我就不一一阐述了
主要还是讲一下,在Beego当中是如何操作Redis

Beego连接Redis

import (
    "github.com/astaxie/beego"
    "github.com/astaxie/beego/cache"
    _ "github.com/astaxie/beego/cache/redis"
)

var redisClient cache.Cache

func init() {
    if enableRedis {
        config := map[string]string{
            "key":      beego.AppConfig.String("redisKey"),
            "conn":     beego.AppConfig.String("redisConn"),
            "dbNum":    beego.AppConfig.String("redisDbNum"),
            "password": beego.AppConfig.String("redisPwd"),
        }
        bytes, _ := json.Marshal(config)
        redisClient, err = cache.NewCache("redis", string(bytes))
        if err != nil {
            fmt.Println(err)
            fmt.Println("连接redis数据库失败")
        } else {
            fmt.Println("连接redis数据库成功")
        }
    }
}

封装写入&读取Redis方法

写入

// 在实现方法之前,需要定义一个结构体,当然你也可以不定义
// 定义结构体
type cacheDb struct {}

// redis写入数据
func (c cacheDb) Set(key string,value interface{})  {
    if enableRedis {
        bytes,_ := json.Marshal(value)
        redisClient.Put(key,string(bytes),time.Second*time.Duration(redisTime))
    }
}

读取

// redis获取数据
func (c cacheDb) Get(key string,obj interface{}) bool {
    if enableRedis {
        if redisStr := redisClient.Get(key);redisStr != nil{
            redisValue,_ := redisStr.([]uint8)
            json.Unmarshal([]byte(redisValue),obj)
            return true
        }
        return false
    }
    return false
}

项目实际逻辑

我们完成Redis的基本操作后,就可以进行实际操作了
我们需要完成的就是,当需要对应数据的时候,第一次先去数据库中读取,并且将其数据经过一系列处理后存储在Redis当中,在下一次的请求中就不再从数据库中读取数据,这样就可以减小数据的压力,从而发挥Redis的作用

我们就已获取顶部导航的数据,进行演示

// 实例化导航模块的结构体
topNav := []models.Nav{}

// 通过调用上面写的获取数据的方法,进行判断是否Redis当中存在对应的数据
// 如果不存在就执行,else里面的代码,也就是首先向数据库中读取数据,并将其写入Redis
// 如果存在,就直接从Redis中读取
if hasTopNav := models.CacheDb.Get("topNav",&topNav); hasTopNav == true {
    this.Data["topNavList"] = topNav
} else {
    models.DB.Where("status=1 AND position=1").Order("sort desc").Find(&topNav)
    this.Data["topNavList"] = topNav
    models.CacheDb.Set("topNav",topNav)
}

Redis 分布式架构订阅发布

当然,Redis的作用不仅仅于此,那就再讲一个强大的功能

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
Redis 客户端可以订阅任意数量的频道

利用Redis的这个功能,我们就可以完成这样的操作
一台服务器负责爬取数据,作为发送者,并将数据上传到Redis服务器
而另外几台服务器,作为订阅者,通过获取Redis中的数据进行数据处理


下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端

代码实例
发送者

import (
    "context"
    "fmt"
    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
    //连接redis数据库
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })

    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        fmt.Println("redis数据库连接失败")
    } else {
        fmt.Println("redis数据库连接成功...")
    }

    //发布消息
    rdb.Publish(ctx, "ch3", "我是ch3的数据...")

    rdb.Publish(ctx, "ch4", "我是ch4的数据...")
}

接收者1

import (
    "context"
    "fmt"

    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
    //连接redis数据库
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })

    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        fmt.Println("redis数据库连接失败")
    } else {
        fmt.Println("redis数据库连接成功...")
    }

    //订阅消息
    pubsub := rdb.Subscribe(ctx, "ch3")
    ch := pubsub.Channel()
    for msg := range ch {
        fmt.Println(msg.Channel, msg.Payload)
    }

}

接收者2

import (
    "context"
    "fmt"

    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
    //连接redis数据库
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })

    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        fmt.Println("redis数据库连接失败")
    } else {
        fmt.Println("redis数据库连接成功...")
    }
    //订阅消息
    pubsub := rdb.Subscribe(ctx, "ch4")
    ch := pubsub.Channel()
    for msg := range ch {
        fmt.Println(msg.Channel, msg.Payload)
    }

}


今天又是被CSS折磨的一天,为了完成一个小小的细节,死磕CSS我TM直接呕吐
由于写CSS耗费了大量的时间,就只完成商品分类页的渲染

商品列表数据渲染

效果图如下

要达到这个的效果,我们首先需要设置动态路由

beego.Router("/category_:id([0-9]+).html", &mindex.ProductController{},"get:CategoryList")

而动态路由里面的id值就来自于,首页左侧的商品分类栏

通过id值,我们就可以获取商品分类的顶级分类,从而获取到顶级分类里面的商品信息

// 获取id值
id := this.Ctx.Input.Param(":id")
cateId, _ := strconv.Atoi(id)
models.DB.Where("id=?", cateId).Find(&curretGoodsCate)

获取到顶级分类的数据后,通过判断curretGoodsCate里面的Pid值是否等于0,判断是否为顶级分类
如果是顶级分类,就通过curretGoodsCate.Id在GoodsCate表中,获取顶级分类里面商品的id值,最后通过子类的id值在Goods表中获取到商品信息
如果不是顶级分类,那么就是二级分类,就可以通过curretGoodsCate.Pid获取兄弟商品的id值

// 定义一个切片用于存储商品的id值
var tempSlice []int
if curretGoodsCate.Pid == 0 { //顶级分类
    //二级分类
    models.DB.Where("pid=?", curretGoodsCate.Id).Find(&subGoodsCate)
    for i := 0; i < len(subGoodsCate); i++ {
        tempSlice = append(tempSlice, subGoodsCate[i].Id)
    }
} else {
    //获取当前二级分类对应的兄弟分类
    models.DB.Where("pid=?", curretGoodsCate.Pid).Find(&subGoodsCate)
}
tempSlice = append(tempSlice, cateId)

接着通过商品的id值,在goods表中获取数据

where := "cate_id in (?)"
goods := []models.Goods{}
models.DB.Where(where, tempSlice).Select("id,title,price,goods_img,sub_title").Offset((page - 1) * pageSize).Limit(pageSize).Order("sort desc").Find(&goods)

最后把需要的数据,传输给前台页面

this.Data["goodsList"] = goods
this.Data["subGoodsCate"] = subGoodsCate
this.Data["curretGoodsCate"] = curretGoodsCate


今天主要完成了对商品详情的制作,由于商品是我们这个项目的核心,所以在写商品详情页的时候,才是真正考验逻辑的时候,一个商品ID会关联很多数据;商品的名字,属性这些就不用说了,主要还是关联一些其他的内容,比如商品的图片,商品的颜色,商品的关联商品等等
像这种情况,一定要细心细心再细心;因为有的时候,项目虽然表明上看上去没有什么问题,但是一旦出现问题,那改起来真的要命,浪费了时间不说,主要是脑壳痛

今天写CSS的时候又碰到一些奇怪的问题,找错误就找了一会,结果是Bootstrap和自己写的样式冲突;对于这种情况,我算是长经验了;要是再碰到一些奇奇怪怪的问题,先把Bootstrap禁用了看看

效果图如下

昨天完成了商品列表的渲染,今天就再细化完成的内容如下:

  • 商品详情数据渲染
  • 选择商品版本、颜色
  • 选择颜色切换商品图片
  • 商品规格参数中使用Markdown语法

商品详情数据渲染

配置商品的路由,跟昨天配置商品列表的路由是一样的,也是配置动态路由
通过获取商品的id值,就获取到所需要商品的数据

id := this.Ctx.Input.Param(":id")
//1、获取商品信息
goods := models.Goods{}
models.DB.Where("id=?", id).Find(&goods)
this.Data["goods"] = goods

//2、获取关联商品  RelationGoods
relationGoods := []models.Goods{}
goods.RelationGoods = strings.ReplaceAll(goods.RelationGoods, ",", ",")
relationIds := strings.Split(goods.RelationGoods, ",")
models.DB.Where("id in (?)", relationIds).Select("id,title,price,goods_version").Find(&relationGoods)
this.Data["relationGoods"] = relationGoods

//3、获取关联赠品 GoodsGift
goodsGift := []models.Goods{}
goods.GoodsGift = strings.ReplaceAll(goods.GoodsGift, ",", ",")
giftIds := strings.Split(goods.GoodsGift, ",")
models.DB.Where("id in (?)", giftIds).Select("id,title,price,goods_img").Find(&goodsGift)
this.Data["goodsGift"] = goodsGift

获取商品颜色,图片等信息的操作跟上面都是差不多的,很简单
数据获取完成后,自然是将数据传输给前端页面,我们首先在项目渲染静态的商品详情页面,然后再一点一点的修改,部分代码如下

获取图片信息

<div class="swiper-wrapper item_focus" id="item_focus">
    {{range $key,$value := .goodsImage}}
        {{if eq $value.ColorId 0 }}
            <div class="swiper-slide">
                <img src="{{$value.ImgUrl | formatImg}}" />
            </div>
        {{end}}
    {{end}}
</div>

选择商品版本

关于选择商品版本、颜色的操作,还是先看图片示例比较容易理解

对于商品版本的数据,这是在定义当前商品的时候,关联的一些商品
通过获取关联商品的数据,就能获取到不同的版本信息
而点击不同版本的时候,就会通过不同的商品Id值得到不同的数据

//2、获取关联商品  RelationGoods
relationGoods := []models.Goods{}
goods.RelationGoods = strings.ReplaceAll(goods.RelationGoods, ",", ",")
relationIds := strings.Split(goods.RelationGoods, ",")
models.DB.Where("id in (?)", relationIds).Select("id,title,price,goods_version").Find(&relationGoods)
this.Data["relationGoods"] = relationGoods

选择颜色切换商品图片

对于商品颜色的数据,大致的逻辑也是这样的,只不过多了一些细节
每种颜色的按钮,对应着相同颜色的图片,而要完成这样的效果就需要Ajax和Api共同完成
首先肯定得获取到关联的图片信息

goodsImage := []models.GoodsImage{}
models.DB.Where("goods_id=?", goods.Id).Find(&goodsImage)
this.Data["goodsImage"] = goodsImage
beego.Info(goodsImage)

接着就是写API

func (this *ProductController) GetImgList() {
    goods_id,err1 := this.GetInt("goods_id")
    goods_color,err2 := this.GetInt("color_id")

    // 查询商品图库信息
    goodsImage := []models.GoodsImage{}
    err3 := models.DB.Where("color_id=? AND goods_id=?",goods_color,goods_id).Find(&goodsImage).Error
    if err1 != nil || err2 != nil || err3 != nil{
        this.Data["json"] = map[string]interface{}{
            "result":"失败",
            "success":false,
        }
        this.ServeJSON()
    }else {
        this.Data["json"] = map[string]interface{}{
            "result":goodsImage,
            "success":true,
        }
        this.ServeJSON()
    }
}

最后就是Ajax的部分
通过给指定API传递商品的id和商品的颜色id,达到获取指定数据的效果
获取到数据,通过for循环,拼接一串html的代码
最后通过 $("#item_focus").html(str) 替换显示图片标签中的html代码,完成图片的替换

initContentColor: function () {
    var _that = this
    $("#color-list .banben").click(function () {
        $(this).addClass("active").siblings().removeClass("active")
        $("#color_name").html($("#color-list .active .yanse").html())
        var color_id = $(this).attr("color_id")
        var goods_id = $(this).attr("goods_id")
        $.get("/product/getImgList", {"goods_id": goods_id, "color_id": color_id}, function (response) {
            if (response.success) {
                var data = response.result
                var str = ""
                for (var i = 0; i < data.length; i++) {
                    str += '<div class="swiper-slide"><img src="'+'http://bee.sunyj.xyz/'+data[i].img_url+'"> </div>'
                }
                $("#item_focus").html(str)
                _that.initSwiper()
            }
        })
    })
}

商品规格参数中使用Markdown语法

要想在Beego当中实现Markdown转HTMl,需要按照第三方包gomarkdown
实现转换的操作也很简单,几行代码就搞定了

func FormatAttr(str string) string {
    md := []byte(str)
    htmlByte := markdown.ToHTML(md,nil,nil)
    return string(htmlByte)
}

要想在模板中使用这个方法,不要忘记在main.go文件中配置自定义模板方法哦

beego.AddFuncMap("formatAttr",models.FormatAttr)


昨天完成了对商品详情页的制作,今天由于回老家过年,耽误了很多时间,也没怎么写项目,只是完成了对商品购物车的部分操作
我们要想把商品加入到购物车内,主要就是对cookie的操作,那么什么是cookie呢?

Cookie

Cookie 并不是它的原意“甜饼”的意思, 而是一个保存在客户机中的简单的文本文件, 这个文件与特定的 Web 文档关联在一起, 保存了该客户机访问这个Web 文档时的信息, 当客户机再次访问这个 Web 文档时这些信息可供该文档使用。由于“Cookie”具有可以保存在客户机上的神奇特性, 因此它可以帮助我们实现记录用户个人信息的功能, 而这一切都不必使用复杂的CGI等程序
举例来说, 一个 Web 站点可能会为每一个访问者产生一个唯一的ID, 然后以 Cookie 文件的形式保存在每个用户的机器上。如果使用浏览器访问 Web, 会看到所有保存在硬盘上的 Cookie。在这个文件夹里每一个文件都是一个由“名/值”对组成的文本文件,另外还有一个文件保存有所有对应的 Web 站点的信息。在这里的每个 Cookie 文件都是一个简单而又普通的文本文件。透过文件名, 就可以看到是哪个 Web 站点在机器上放置了Cookie(当然站点信息在文件里也有保存)

简单理解就是,Cookie可以保存一些信息,并且拥有给服务器端提供这些信息的功能

封装操作 Cookie 的方法

在此项目中,我们可以将操作Cookie的操作封装起来,这样我们使用的时候也会方便很多

//定义结构体  缓存结构体 私有
type cookie struct{}

//写入数据的方法
func (c cookie) Set(ctx *context.Context, key string, value interface{}) {
    bytes, _ := json.Marshal(value)
    ctx.SetSecureCookie("micom", key, string(bytes), 3600*24*30)

}

//获取数据的方法
func (c cookie) Get(ctx *context.Context, key string, obj interface{}) bool {
    tempData, ok := ctx.GetSecureCookie("micom", key)
    if !ok {
        return false
    }
    json.Unmarshal([]byte(tempData), obj)
    return true

}

//实例化结构体
var Cookie = &cookie{}

使用封装的Cookie方法

func (this *CartController) AddCart() {
/*
购物车数据保持到哪里?:
    1、购物车数据保存在本地    (cookie)
    2、购物车数据保存到服务器(mysql)   (必须登录)
    3、没有登录 购物车数据保存到本地 , 登录成功后购物车数据保存到服务器  (用的最多)

增加购物车的实现逻辑:
    1、获取增加购物车的数据  (把哪一个商品加入到购物车)
    2、判断购物车有没有数据   (cookie)
    3、如果购物车没有任何数据  直接把当前数据写入cookie
    4、如果购物车有数据
        4、1、判断购物车有没有当前数据
            有当前数据让当前数据的数量加1,然后写入到cookie
            如果没有当前数据直接写入cookie
    */

    colorId, err1 := this.GetInt("color_id")
    goodsId, err2 := this.GetInt("goods_id")

    goods := models.Goods{}
    goodsColor := models.GoodsColor{}
    err3 := models.DB.Where("id=?", goodsId).Find(&goods).Error
    err4 := models.DB.Where("id=?", colorId).Find(&goodsColor).Error

    if err1 != nil || err2 != nil || err3 != nil || err4 != nil {

        this.Ctx.Redirect(302, "/item_"+strconv.Itoa(goods.Id)+".html")
        return
    }
    // 1、获取增加购物车的数据  (把哪一个商品加入到购物车)

    currentData := models.Cart{
        Id:           goodsId,
        Title:        goods.Title,
        Price:        goods.Price,
        GoodsVersion: goods.GoodsVersion,
        Num:          1,
        GoodsColor:   goodsColor.ColorName,
        GoodsImg:     goods.GoodsImg,
        GoodsGift:    goods.GoodsGift, /*赠品*/
        GoodsAttr:    "",              //根据自己的需求拓展
        Checked:      true,            /*默认选中*/
    }

    //  2、判断购物车有没有数据   (cookie)
    cartList := []models.Cart{}
    models.Cookie.Get(this.Ctx, "cartList", &cartList)
    if len(cartList) > 0 { //购物车有数据
        //4、判断购物车有没有当前数据
        if models.CartHasData(cartList, currentData) {
            for i := 0; i < len(cartList); i++ {
                if cartList[i].Id == currentData.Id && cartList[i].GoodsColor == currentData.GoodsColor && cartList[i].GoodsAttr == currentData.GoodsAttr {
                    cartList[i].Num = cartList[i].Num + 1
                }
            }
        } else {
            cartList = append(cartList, currentData)
        }
        models.Cookie.Set(this.Ctx, "cartList", cartList)

    } else {
        //3、如果购物车没有任何数据  直接把当前数据写入cookie
        cartList = append(cartList, currentData)
        models.Cookie.Set(this.Ctx, "cartList", cartList)
    }

    this.Ctx.WriteString("保存购物车成功")
}


春风吹十里 莺啼报新年
爆竹声声起 好运又一年
走过一片时间海 只为遇见对的爱
烟花聚又散 今夜共团圆

今天是大年三十,祭祖过除夕,最重要的事情完成后,当然是接着写代码,把昨天没完成的购物车部分,继续完善
还是先看看最终效果吧

可以看到购物车里面涉及的功能还是挺多的,其中包括:全选按钮的实现,商品数量的增加|减少,以及删除商品;而这些功能实现的同时,下方的总价格都是会跟随着改变的,包括商品的小计也会随着商品的数量而改变;而要实现这些功能,主要就是后端API结合ajax实现的

那么今天的任务清单就很清楚了呗

  • 商品数量的增加|减少
  • 删除商品
  • 全选按钮的实现

商品数量的增加|减少

昨天其实只是封装了使用Cookie的方法,正在从Cookie里面获取数据,并且渲染到前台的页面,实际上还是今天完成的
实现渲染数据的操作,我们已经在这个项目里面做过很多很多遍了,我就不展开讲了
只不过,购物车的页面,有一个显示商品总价的标签,这里面的数据,可以通过从后端获取,然后再传递给前台页面

cartList := []models.Cart{}
models.Cookie.Get(this.Ctx, "cartList", &cartList)

// 执行计算总价
var allPrice float64
for i := 0; i < len(cartList); i++ {
        // 判断是否单选框处于选中状态
    if cartList[i].Checked {
        allPrice += cartList[i].Price * float64(cartList[i].Num)
    }
}
this.Data["cartList"] = cartList
this.Data["allPrice"] = allPrice
this.TplName = "mindex/cart/cart.html"

实现商品数量增减的操作,从逻辑上来讲,几乎一模一样,只是有一个参数不一样罢了,那就商品数量的加减部分,我们完全可以讲两个操作写在同一个接口里面,但是我们为了后期维护起来方便易操作,最后还是选中放在两个方法里面
而ajax部分,也是通过调用两个不同的API实现的,逻辑几乎也一模一样
我们首先从后端的API开始讲起,这里用减少商品数量的方法演示

  1. 第一步当时是通过Cookie获取数据

    cartList := []models.Cart{}
    models.Cookie.Get(this.Ctx, "cartList", &cartList)
  2. 接下来就是通过链接获取商品的goodsid和goodscolor,确定当前商品
    成功获取商品相关的信息后,我们通过for循环cartList里面的数据
    并且,通过goodsid,goodscolor,进行if判断,进行商品数量减一的操作

    goodsId, _ := this.GetInt("goods_id")
    goodsColor := this.GetString("goods_color")
    for i := 0; i < len(cartList); i++ {
    if cartList[i].Id == goodsId && cartList[i].GoodsColor == goodsColor {
        if cartList[i].Num > 1 {
            cartList[i].Num = cartList[i].Num - 1
        }
    }
    }
  3. 要想商品数量和小计以及总计,发生改变,我们需要定义几个变量,用来接受这几个值
    并将它们写进Map对象里面,最后以Json的数据格式返回给前台,也就是给ajax处理

    if flag {
    models.Cookie.Set(this.Ctx, "cartList", cartList)
    this.Data["json"] = map[string]interface{}{
        "success":         true,
        "message":         "修改数量成功",
        "allPrice":        allPrice,
        "currentAllPrice": currentAllPrice,
        "num":             num,
    }
    } else {
    this.Data["json"] = map[string]interface{}{
        "success": false,
        "message": "传入参数错误",
    }
    }

商品减少数量API完整代码

func (this *CartController) DecCart() {
    var flag bool
    var allPrice float64
    var currentAllPrice float64
    var num int

    goodsId, _ := this.GetInt("goods_id")
    goodsColor := this.GetString("goods_color")
    cartList := []models.Cart{}
    models.Cookie.Get(this.Ctx, "cartList", &cartList)
    for i := 0; i < len(cartList); i++ {
        if cartList[i].Id == goodsId && cartList[i].GoodsColor == goodsColor {
            if cartList[i].Num > 1 {
                cartList[i].Num = cartList[i].Num - 1
            }
            flag = true
            num = cartList[i].Num
            currentAllPrice = cartList[i].Price * float64(cartList[i].Num)
        }
        if cartList[i].Checked {
            allPrice += cartList[i].Price * float64(cartList[i].Num)
        }
    }
    if flag {
        models.Cookie.Set(this.Ctx, "cartList", cartList)
        this.Data["json"] = map[string]interface{}{
            "success":         true,
            "message":         "修改数量成功",
            "allPrice":        allPrice,
            "currentAllPrice": currentAllPrice,
            "num":             num,
        }
    } else {
        this.Data["json"] = map[string]interface{}{
            "success": false,
            "message": "传入参数错误",
        }
    }
    this.ServeJSON()
}

后台的API写完后,ajax的部分就很简单啦,通过获取后端传来的数据,进行页面上内容的修改就行了

$('.decCart').click(function(){
    
    var goods_id=$(this).attr('goods_id');
    var goods_color=$(this).attr('goods_color');

    $.get('/cart/decCart?goods_id='+goods_id+'&goods_color='+goods_color,function(response){

        if(response.success){
            $("#allPrice").html(response.allPrice+"元")
            //注意this指向
            $(this).siblings(".input_center").find("input").val(response.num)
            $(this).parent().parent().siblings(".totalPrice").html(response.currentAllPrice+"元")
        }
    }.bind(this))

});

删除商品

删除商品的操作就不用到ajax了,直接通过获取对应的商品id和color就可以执行删除操作

func (this *CartController) DelCart() {

    goodsId, _ := this.GetInt("goods_id")
    goodsColor := this.GetString("goods_color")
    goodsAttr := ""

    cartList := []models.Cart{}
    models.Cookie.Get(this.Ctx, "cartList", &cartList)
    for i := 0; i < len(cartList); i++ {
        if cartList[i].Id == goodsId && cartList[i].GoodsColor == goodsColor && cartList[i].GoodsAttr == goodsAttr {
            //执行删除 切片再切片
            cartList = append(cartList[:i], cartList[(i+1):]...)
        }
    }
    models.Cookie.Set(this.Ctx, "cartList", cartList)

    this.Redirect("/cart", 302)

}

全选按钮的实现

对于全选按钮的实现主要逻辑是这样的

  1. 当点击全选按钮的时候,通过使用Jquery里面的函数,将其他单选框全部选中
    这时会触发一个ajax的请求,获取购物车里面的全部商品价格并返回
    最后通过改变网页中的数据达到效果
    反之,再次点击时,返回0元

    后端API

    func (this *CartController) ChangeAllCart() {
    flag, _ := this.GetInt("flag")
    var allPrice float64
    cartList := []models.Cart{}
    models.Cookie.Get(this.Ctx, "cartList", &cartList)
    for i := 0; i < len(cartList); i++ {
        if flag == 1 {
            cartList[i].Checked = true
        } else {
            cartList[i].Checked = false
        }
        //计算总价
        if cartList[i].Checked {
            allPrice += cartList[i].Price * float64(cartList[i].Num)
        }
    }
    models.Cookie.Set(this.Ctx, "cartList", cartList)
    
    this.Data["json"] = map[string]interface{}{
        "success":  true,
        "allPrice": allPrice,
    }
    this.ServeJSON()
    }

    javascript部分

    $("#checkAll").click(function() {
    if (this.checked) {
        $(":checkbox").prop("checked", true);
        $.get('/cart/changeAllCart?flag=1',function(response){
            if(response.success){
                $("#allPrice").html(response.allPrice+"元")
            }
        })
    }else {
        $(":checkbox").prop("checked", false);
        $.get('/cart/changeAllCart?flag=0',function(response){
            if(response.success){
                $("#allPrice").html(response.allPrice+"元")
            }
        })
    }
    });
  2. 当全选按钮下面的按钮点满时,全选按钮也很自动选中
    只要下面的按钮有其中一个没选中,全选按钮肯定说不会选中的

    当点击商品的单选框的时候,也会触发ajax请求,这个主要是为了改变当前商品的Checked属性
    因为Checked属性决定了,是否计算总价

    if cartList[i].Checked {
    allPrice += cartList[i].Price * float64(cartList[i].Num)
    }

今天就到此为止吧,去吃年夜饭啦


农历一月初一,清早八晨就被外面的爆竹声"炸"醒,遭不住遭不住
吵虽然是吵了点,但是春节喜庆的氛围还是很好的
晚上的时候一大家人还出去放了孔明灯,大家都在孔明灯上写上了自己的愿望,那么就期待一下2021年吧

我写的东西就非常符合自己的身份,我觉得也非常适合我

Python GoLang Java Line Line So Easy
Years Months Weeks Day Day No Bug


今天准备写注册用户的功能,当用户注册的时候需要给手机号发送验证码,但是需要用到第三方平台的SDK才能实现发送短信验证码的功能
我也去申请试了试云片平台的短信,结果就是通过不了
那么,只有通过STMP发送邮箱来实现注册的功能了
代码只写了一小部分,在这里就梳理一下思路,明天起来再写

  1. 当用户发起注册账号请求时,会获取前台表单传来的手机号,邮箱号,密码以及验证码
    验证码通过后,会先将用户的数据写入到数据库中
    而数据库中有status字段,这时会将此字段默认设置为0
    数据写入完成后,会给填写儿逗邮箱发送一份邮件
  2. 这份邮件中,会包含一条链接,当用户点击这条链接后,服务器端会进行验证
    验证通过后,将status字段设置为1,即用户注册成功
  3. 通过重定向的方式,跳转到登录页面,完成登录操作
func (this *LoginController) SetPass() {
    phone := this.GetString("phone")
    email := this.GetString("email")
    passwd := this.GetString("passwd")
    this.Data["json"] = map[string]string{
        "phone":   phone,
        "email":  email,
        "passwd": passwd,
    }
    flag := models.Cpt.VerifyReq(this.Ctx.Request)
    if flag {
        this.ServeJSON()
        userDb := models.User{
            Phone: phone,
            Email: email,
            Password: passwd,
            AddTime: int(models.GetUnix()),
            Status: 0,
            LastIp: this.Ctx.Request.RemoteAddr,
        }
        models.DB.Create(&userDb)

    } else {
        this.Error("验证码输入错误", "http://127.0.0.1:8080/pass")
    }
}


今天由于晚上吃完饭后就从老家回到城里面了,路上又堵车堵得不得了,所以今天也没完成什么内容
主要就是按照昨天梳理的思路把用户注册和用户登录的功能写完了,也就多写了退出登录的功能

用户注册

首先还是看看注册页面,我觉得还是挺好看的


当用户在前台页面正确填写完所有信息后,点击注册时,我们看看后端到底发送了什么

func (this *LoginController) SetPass() {
    // 1:获取前台的信息
    phone := this.GetString("phone")
    email := this.GetString("email")
    passwd := this.GetString("passwd")
    
    // 2: 判断输入的验证码是否正确
    flag := models.Cpt.VerifyReq(this.Ctx.Request)
    if flag {
        // 3:验证码输入正确后,首先会在数据库中查询是否存在此用户
        userVarify := []models.User{}
        models.DB.Where("email=? AND phone=?",email,phone).Find(&userVarify)
        // 3.1:如果当前注册的用户存在,会弹出提示界面,并直接跳转到登录页面
        if len(userVarify) > 0 {
            this.Error("用户存在请直接登录","http://127.0.0.1:8080/login.html")
            return
        // 4:如果不存在,会将前台获取到的数据保存到数据中,并设置status为0        
        }else {
            userDb := models.User{
                Phone: phone,
                Email: email,
                Password: models.Md5(passwd+"micom"),
                AddTime: int(models.GetUnix()),
                Status: 0,
                LastIp: strings.Split(this.Ctx.Request.RemoteAddr,":")[0],
            }
            models.DB.Create(&userDb)
            
            // 5:定义验证url
            verifyUrl := "http://127.0.0.1:8080/pass/verifyUrl&verify="+models.Md5(passwd+"micom")+"&email="+email

            //邮件主题为"小米商城GoLang注册验证"
            subject := "小米商城GoLang注册验证"
            // 6:定义邮件正文
            body := `<table width="100%"style="background-color: rgb(31,200,153);  border-radius: 45px"bgcolor="#1FC899"><tbody><tr><td width="100%"valign="top"align="center"style="border: 0px none transparent; background-color: rgb(31,200,153); color: rgb(56, 56, 56);border-radius: 45px"bgcolor="#1fc899"><div><table width="600"border="0"cellpadding="0"cellspacing="0"><tr><td><table width="100%"cellspacing="0"cellpadding="0"border="0"><tbody><tr><td style="padding: 20px; border-collapse: collapse; display: block"align="center"><img src="https://syjun.vip/image/Logo.png"style="max-width: 140px; display: block;"width="140px"></td><td style="padding: 20px; border-collapse: collapse; display: block"align="center"><strong style="font-weight: 200;font-size: 48px; font-family: Impact, Chicago; color: #ffffff; line-height: 125%;">小米商城GoLang注册验证</strong></td><td style=" margin-bottom: 10px; border-collapse: collapse; display: block"align="center"><span style="font-size: 24px; font-family: Tahoma, Arial, Helvetica, sans-serif; color: #ffffff; line-height: 150%;">点击下方链接即可完成验证</span></td><td style="margin-bottom: 10px; border-collapse: collapse; display: block"align="center"><span style="font-size: 24px; font-family: Tahoma, Arial, Helvetica, sans-serif; color: #ffffff; line-height: 150%;"></span></td><td style="margin-bottom: 50px; border-collapse: collapse; display: block"align="center"><a href="`+verifyUrl+`"style="list-style: none"><span style="font-size: 24px; font-family: Tahoma, Arial, Helvetica, sans-serif; color: #ffffff; line-height: 150%;">戳我戳我</span></a></td><td style="margin-bottom: 10px; border-collapse: collapse; display: block"align="center"><img src="https://syjun.vip/usr/uploads/2021/01/525314553.png"class="mobile-img-large"width="560"style="max-width: 1120px; display: block; width: 560px;"alt=""border="0"></td></tr></tbody></table></td></tr></table></div></td></tr></tbody></table>`
            // 7:发送给用户注册邮件
            err := SendMail(email, subject, body)
            if err != nil {
                this.Error("验证码发送失败","http://127.0.0.1:8080/pass")
            }else {
                this.TplName = "admin/public/email.html"
            }

        }
    // 2.1:验证码如果输入错误,重新加载注册页面
    } else {
        this.Error("验证码输入错误", "http://127.0.0.1:8080/pass")
    }
}

用户验证

当用户点击注册后,会给目标邮箱发送一份验证邮箱,就是下图的样子

邮件里面需要点击的链接,就是上面定义的verifyUrl
当点击这条链接后,我们看看后端又发生了什么

func (this *LoginController) VerifyUrl()  {
    // 1:通过url获取注册信息
    email := this.GetString("email")
    verify := this.GetString("verify")
    // 2:通过邮箱在数据库找到对应的数据
    user := models.User{Email: email}
    // 3:如果verify与数据库保存的密码一样
    //   那么就表示验证成功,并将数据表中的status字段设置为1,表示注册成功
    if user.Password == verify {
        user.Status = 1
        models.DB.Save(&user)
        // 4:验证成功后,会直接跳转到登录页面,即可进行登录
        this.Success("验证成功,即将跳转登录界面","http://127.0.0.1:8080/login.html")
    }else {
        // 5:如果验证失败,会删除数据库中的数据,并且重新跳转到注册页面
        models.DB.Delete(&user)
        this.Error("验证失败,请重新注册","http://127.0.0.1:8080/pass.html")
    }
}

用户登录

用户注册验证完成后,会直接跳转到登录页面
那么我们继续看看登录的时候,后端又发生了什么

func (this *LoginController) Login() {
    // 1:获取用户登录信息
    email := this.GetString("email")
    passwd := models.Md5(this.GetString("passwd")+"micom")
    // 2:通过email在数据中查询数据
    user := []models.User{}
    models.DB.Where("email=?",email).Find(&user)
    // 3:如果User的结构体切片长度大于的0的话,表示用户存在
    if len(user) >0 {
        // 4:当前如果用户存在,就继续判断用户是否通过验证,即验证status字段是否为1
        if user[0].Status ==1 {
            // 5:当用户通过验证时,判断密码是否正确
            if user[0].Password == passwd {
                // 6:密码输入正确,就将用户信息写入到cookie当中保存起来,并跳转到首页
                models.Cookie.Set(this.Ctx,"userinfo",user[0])
                this.Redirect("http://127.0.0.1:8080/",302)
            // 5.1:当密码输入错误时,首先会弹出提示页面,接着会跳转到登录页面    
            }else {
                this.Error("密码输入错误,请重新输入", "http://127.0.0.1:8080/login.html")
            }
        // 4.1:如果不等于1即表示用户未激活,会跳转到注册页面    
        }else {
            this.Error("账号未激活,请登录邮箱激活", "http://127.0.0.1:8080/pass.html")
        }
    // 3.1:如果不等于0即表示,用户不存在,会跳转到注册页面    
    }else {
        this.Error("账号不存在,请先注册", "http://127.0.0.1:8080/pass.html")
    }
}

退出登录

退出登录的逻辑其实很简单,就是将设置的Cookie到期时间重新设为-1,即可立即实现

//删除指定Cookie
func (c cookie) Remove(ctx *context.Context, key string, value interface{}) {
    bytes, _ := json.Marshal(value)
    ctx.SetSecureCookie("micom", key, string(bytes), -1)
}

登录首页显示

登录成功后,首页的右上角以前的登录|注册 就会发生变化
而后台的逻辑就是判断Cookie当中是否存在userinfo
代码就不展示了,真的很简单


二月十四日,西方人的节日,关我南方人什么事

还是家里面写代码写着舒适,在老家写代码不是颈子痛,而是屁股疼;那个凳子坐起真的不舒服啊
有着舒适的环境,当然生产力刷刷的上升,今天完成了不少内容

  1. 订单结算页面制作
  2. 订单结算页面收获地址增删改查
  3. 结算页面提交订单 防止订单重复提交
  4. 支付页面制作

订单结束页面制作

首先,我们还是欣赏一下页面吧

昨天完成了用户登录的操作,那么我们就可以获取购物车里面的数据与用户结合起来,完成订单页面的制作
在制作订单页面的时候,我们需要完成的就是中间件的实现
中间件主要就是判断用户是否登录对指定的链接进行处理

func DefaultAuth(ctx *context.Context) {
    // 判断前端用户是否登录
    user := models.User{}
    models.Cookie.Get(ctx, "userinfo", &user)
    if len(user.Phone) != 11 {
        ctx.Redirect(302,"/login")
    }
}

// 使用到中间件的路由
beego.InsertFilter("/buy/*",beego.BeforeRouter,middleware.DefaultAuth)
beego.Router("/buy/checkout", &mindex.BuyController{}, "get:CheckOut")
beego.Router("/buy/doOrder", &mindex.BuyController{}, "post:DoOrder")
beego.Router("/buy/confirm", &mindex.BuyController{}, "get:Confirm")

关于订单页面其实很简单,就是对购物车里面的数据进行判断,查看Check字段是否为true,如果是true就将它绑定到订单中,最后渲染到前台页面

// 获取结算的商品
cartList := []models.Cart{}
orderList := []models.Cart{} // 需要结算的商品
models.Cookie.Get(this.Ctx, "cartList", &cartList)
var allPrice float64

for i := 0; i < len(cartList); i++ {
    if cartList[i].Checked {
        allPrice += cartList[i].Price * float64(cartList[i].Num)
        orderList = append(orderList, cartList[i])
    }
}
this.Data["orderList"] = orderList
this.Data["allPrice"] = allPrice

订单结算页面收获地址增删改查

今天任务比较重的就是收获地址的增删改查,比较麻烦需要配合Ajax完成
这里就拿增加收获地址举例,其他的操作都是类似的
那么,完成增加操作主要是这5步

  1. 获取表单提交的数据
  2. 判断收货地址的数量
  3. 更新当前用户的所有收货地址的默认收货地址状态为0
  4. 增加当前收货地址,让默认收货地址状态是1
  5. 返回当前用户的所有收货地址返回
func (this *AddressController) AddAddress() {
    // 1、获取表单提交的数据
    user := models.User{}
    models.Cookie.Get(this.Ctx, "userinfo", &user)

    name := this.GetString("name")
    phone := this.GetString("phone")
    address := this.GetString("address")
    zipcode := this.GetString("zipcode")

    // 2、判断收货地址的数量
    var addressCount int
    models.DB.Where("uid=?", user.Id).Table("address").Count(&addressCount)
    if addressCount > 10 {
        this.Data["json"] = map[string]interface{}{
            "success": false,
            "message": "增加收货地址失败 收货地址数量超过限制",
        }
        this.ServeJSON()
        return
    }

    // 3、更新当前用户的所有收货地址的默认收货地址状态为0
    models.DB.Table("address").Where("uid=?", user.Id).Updates(map[string]interface{}{"default_address": 0})

    // 4、增加当前收货地址,让默认收货地址状态是1

    addressResult := models.Address{
        Uid:            user.Id,
        Name:           name,
        Phone:          phone,
        Address:        address,
        Zipcode:        zipcode,
        DefaultAddress: 1,
    }
    models.DB.Create(&addressResult)

    // 5、返回当前用户的所有收货地址返回
    allAddressResult := []models.Address{}
    models.DB.Where("uid=?", user.Id).Find(&allAddressResult)

    this.Data["json"] = map[string]interface{}{
        "success": true,
        "result":  allAddressResult,
    }
    this.ServeJSON()

}

接下来就是Ajax的部分,由于代码有点多,我就梳理思路,然后把代码放在折叠栏里面

  1. 当点击创建地址的时候,触发对应的JS代码,会弹出模态框,进行信息填写
  2. 获取表单的信息,判断手机格式是否正确,提示姓名、电话、地址不能为空
  3. 通过$.post执行Ajax请求,并把表单的信息传输给后端
  4. 后端处理完成后,返回数据,进行判断
  5. 提取信息,并且拼接html代码,通过 $("#addressList").html(str) 是页面出现新增的地址

Ajax代码部分

addAddress: function () {
            $("#addAddress").click(function () {
                var name = $('#add_name').val();
                var phone = $('#add_phone').val();
                var address = $('#add_address').val();
                var zipcode = $('#add_zipcode').val();
                if (name == '' || phone == "" || address == "") {
                    alert('姓名、电话、地址不能为空')
                    return false;
                }
                var reg = /^[\d]{11}$/;
                if (!reg.test(phone)) {
                    alert('手机号格式不正确');
                    return false;
                }
                $.post('/address/addAddress', {
                    name: name,
                    phone: phone,
                    address: address,
                    zipcode: zipcode
                }, function (response) {
                    console.log(response)
                    if (response.success) {
                        var addressList = response.result;
                        var str = ""
                        for (var i = 0; i < addressList.length; i++) {
                            if (addressList[i].default_address) {
                                str += '<div class="address-item J_addressItem selected" data-id="' + addressList[i].id + '" data-name="' + addressList[i].name + '" data-phone="' + addressList[i].phone + '" data-address="' + addressList[i].address + '" > ';
                                str += '<dl>';
                                str += '<dt> <em class="uname">' + addressList[i].name + '</em> </dt>';
                                str += '<dd class="utel">' + addressList[i].phone + '</dd>';
                                str += '<dd class="uaddress">' + addressList[i].address + '</dd>';
                                str += '</dl>';
                                str += '<div class="actions">';
                                str += '<a href="javascript:void(0);" data-id="' + addressList[i].id + '" class="modify addressModify">修改</a>';
                                str += '</div>';
                                str += '</div>';

                            } else {
                                str += '<div class="address-item J_addressItem" data-id="' + addressList[i].id + '" data-name="' + addressList[i].name + '" data-phone="' + addressList[i].phone + '" data-address="' + addressList[i].address + '" > ';
                                str += '<dl>';
                                str += '<dt> <em class="uname">' + addressList[i].name + '</em> </dt>';
                                str += '<dd class="utel">' + addressList[i].phone + '</dd>';
                                str += '<dd class="uaddress">' + addressList[i].address + '</dd>';
                                str += '</dl>';
                                str += '<div class="actions">';
                                str += '<a href="javascript:void(0);" data-id="' + addressList[i].id + '" class="modify addressModify">修改</a>';
                                str += '</div>';
                                str += '</div>';
                            }
                        }

                        $("#addressList").html(str)
                    }

                    $('#addModal').modal('hide')
                });
            })
        }

结算页面提交订单

提交订单其实跟渲染显示前台结算页面很类似
都是会去购物车中Check字段选择的商品数据
不同的是结算页面只是显示就完了,但是提交订单的时候,需要将其信息写入到订单数据表和订单商品数据表中
大致的步骤如下

  1. 获取收获地址信息
  2. 获取购物商品的信息
  3. 把订单信息放在订单表中,把商品信息放在商品表中
  4. 删除购物车里面的选中数据
  5. 重定向支付页面
    代码也比较多,我就不演示了,清楚逻辑就行

防止订单重复提交

为什么要防止订单重复,就是为了防止用户在支付页面的时候,不小心按了返回上一页的按钮,然后又提交了一下订单;这样就会造成订单重复的功能,数据库中的数据也会跟着重复,这也属于一个小bug,其实解决起来也不是很难

  1. 当我们访问结算页面的时候,生成一个签名,并且保存在session当中,绑定在前台表单中

    orderSign := models.Md5(models.GetRandomNum())
    this.SetSession("orderSign", orderSign)
    this.Data["orderSign"] = orderSign
  2. 当提交订单时,通过获取表单绑定的签名进行对session的判断,session的信息只会生效一次,因为第一次判断后,就执行删除操作,这样就可以完美避免重复订单

    orderSign := this.GetString("orderSign")
    sessionOrderSign := this.GetString("orderSign")
    if sessionOrderSign != orderSign {
    this.Redirect("/", 302)
    return
    }
    this.DelSession("orderSign")

支付页面制作

支付页面其实挺简单的,就是通过Cookie获取用户信息后,在Order表和OrderItem查询数据后,进行前台页面的数据渲染
真正实现支付操作,还得明天再写
代码不展示,那就看看页面吧


实现支付功能,可以采用支付宝和微信支付两个平台的服务
但是,由于我是个人开发者无法正常申请到网站支付的功能,必须需要商家用户的账号才能使用
就这样,实现不了支付的功能,还是有点小小的遗憾

利用空出来的时间,我把前端页面进行了一次优化,比之前的更工整了
还仿照小米官网,写了小米秒杀中的倒计时模块,如下图

这里面主要就是使用了window.setInterval(),不停地调用函数

var intDiff = parseInt(4800);//倒计时总秒数量
function timer(intDiff){
    window.setInterval(function(){
        var hour=0, minute=0, second=0;//时间默认值
        if(intDiff > 0){
            hour = Math.floor(intDiff / (60 * 60)) ;
            minute = Math.floor(intDiff / 60) - (hour * 60);
            second = Math.floor(intDiff) - (hour * 60 * 60) - (minute * 60);
        }
        if (minute <= 9) minute = '0' + minute;
        if (second <= 9) second = '0' + second;
        if (hour <= 9) hour = '0' + hour;
        $('#hour_show').text(hour);
        $('#minute_show').text(minute);
        $('#second_show').text(second);
        intDiff--;
}, 1000);

下就被朋友拉去看了《唐人街探案3》,回来的时候接近六点了,就没怎么写代码
有一说一,《唐人街探案3》总感觉比前两部少了些什么,没有前两部那么惊艳
总得来说,东鹏特饮不行,补充能量还得Red bull,毕竟今年牛年


由于支付的功能做不了,所以我就准备直接跳过这个一步,直接做订单页面
关于订单页面,肯定是放在个人信息里面的,所以要想做订单页面就得先搞定个人中心
今天的任务清单就很清晰了

  • 个人中心
  • 我的订单
  • 订单详情

个人中心

惯例,先欣赏页面

路由设置这就不用多说了,必须先设置好,不能没法访问
对于这个页面的数据都是通过后端渲染出来,并没有写死,也很简单,直接看代码吧
因为这个显示用户数据的页面,所以可以通过cookie获取

user := models.User{}
models.Cookie.Get(this.Ctx,"userinfo",&user)
order := []models.Order{}
models.DB.Where("uid=? And order_status=0", user.Id).Preload("OrderItem").Find(&order)
this.Data["noPay"] = len(order)
this.Data["user"] = user
this.TplName = "mindex/user/welcome.html"

我的订单

订单页面就比较有意思,因为就这一个页面就涉及到搜索,筛选,分页等功能

其实这些所有的功能,归根到底就是对数据库的操作
整个代码片段最重要的就是这段代码

models.DB.Where(where, user.Id).Offset((page - 1) * pageSize).Limit(pageSize).Preload("OrderItem").Order("add_time desc").Find(&order)
  • where 通过user.Id查询指定数据
  • Offset 偏移量,里面的数值决定了取值范围,主要用于分页功能
  • Limit 限制,限制一次查询的数据
  • Preload 预加载,通过外键读取两张表的数据即关联查询
  • Order 排序,desc降序排序
  • Find 查询数据

完整代码

func (this *UserController) OrderList() {
    this.SuperInit()

    // 1:获取当前用户
    user := models.User{}
    models.Cookie.Get(this.Ctx, "userinfo", &user)

    // 2:获取当前用户下面的订单信息
    page, _ := this.GetInt("page")
    if page == 0 {
        page = 1
    }
    pageSize := 3

    // 3:获取关键词,搜索功能
    where := "uid=?"
    keywords := this.GetString("keywords")
    if keywords != "" {
        orderItem := []models.OrderItem{}
        models.DB.Where("product_title like ?", "%"+keywords+"%").Find(&orderItem)

        var str string
        for i := 0; i < len(orderItem); i++ {
            str += "," + strconv.Itoa(orderItem[i].OrderId)
        }
        str = strings.Trim(str, ",")
        where += " And id in (" + str + ")"
    }

    // 4:获取筛选条件
    orderStatus,stasusError := this.GetInt("order_status")
    if stasusError == nil {
        where += " And order_status="+strconv.Itoa(orderStatus)
        this.Data["orderStatus"] = orderStatus
    }else {
        this.Data["orderStatus"] = ""
    }


    var count int // 商品总数量
    models.DB.Where(where, user.Id).Table("order").Count(&count)

    order := []models.Order{}
    models.DB.Where(where, user.Id).Offset((page - 1) * pageSize).Limit(pageSize).Preload("OrderItem").Order("add_time desc").Find(&order)
    this.Data["order"] = order
    this.Data["page"] = page
    this.Data["totalPages"] = math.Ceil(float64(count) / float64(pageSize))
    this.Data["keywords"] = keywords

    this.TplName = "mindex/user/order.html"

}

订单详情

在渲染订单页面的时候,其实就已经把每一个的订单id渲染到指定标签里面了,即订单详情里面
我们只需点击[订单详情],就能直接跳转到对应页面
代码部分就很简单了,先查询后渲染完事

func (this *UserController) OrderInfo() {
    this.SuperInit()
    id,_ := this.GetInt("id")
    user := models.User{}
    models.Cookie.Get(this.Ctx,"userinfo",&user)
    order := models.Order{}
    models.DB.Where("id=? And uid=?",id,user.Id).Preload("OrderItem").Find(&order)
    this.Data["order"] = order
    if order.OrderId =="" {
        this.Redirect("/",302)
    }
    this.TplName = "mindex/user/order_info.html"
}

这里面比较有意思的是,页面当中的状态显示
主要是通过数据表中的order_status字段进行判断
订单状态: 0 已下单 1 已付款 2 已配货 3、发货 4、交易成功 5、退货 6、取消
通过给li标签写入step-done属性就能显示为绿色状态

<ol class="progress-list clearfix progress-list-5">
    <li class="step step-first {{if ge .order.OrderStatus 0}}step-done{{end}}">
        <div class="progress"><span class="text">下单</span></div>
        <div class="info">2021年02月20日 18:45</div>
    </li>
    <li class="step {{if ge .order.OrderStatus 1}}step-done{{end}}">
        <div class="progress"><span class="text">付款</span></div>
        <div class="info">2021年02月20日 18:45</div>
    </li>
    <li class="step {{if ge .order.OrderStatus 2}}step-done{{end}}">
        <div class="progress"><span class="text">配货</span></div>
        <div class="info">2021年02月20日 18:50</div>
    </li>
    <li class="step {{if ge .order.OrderStatus 3}}step-done{{end}}">
        <div class="progress"><span class="text">出库</span></div>
        <div class="info">2021年02月20日 19:00</div>
    </li>
    <li class="step {{if ge .order.OrderStatus 4}}step-done{{end}} step-active step-last">
        <div class="progress"><span class="text">交易成功</span></div>
        <div class="info">2021年02月21日 08:16</div>
    </li>
</ol>


今天任务比较轻,主要任务就是继续优化整个项目其次了解了RESTful API 设计指南

RESTful API 设计指南

网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、 平板、桌面电脑、其他专用设备......)

因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致 API 构架的流行,甚至出现"API First"的设计思想。RESTful API 是目前比较成熟的一套互联网应用程序的 API 设计理论。RESTful API 在企业中用的也非常多

RESTful API 也有不一些不足:(字段冗余,扩展性差、无法聚合 api、无法定义数据类型、网络请求次数多)等不足。
所以大家也可以了解一下 GraphQL Api ,GraphQL 继承了 RESTful 的优点也弥补了 RESTful 的不足

一个好的 RESTful API 我们从以下几个方面考虑

  1. 协议:建议使用更安全的 https 协议
  2. 域名:尽量部署在专属域名下面,比如 https://a.syjun.vip
  3. 应该将 api 的版本号放入 URl 中:
    https://a1.syjun.vip https://a2.syjun.vip
  4. 路径:在 RESTful 架构中,每个网址代表一种资源(resource),所以网址中建议不能有 动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表 都是同种记录的"集合"(collection),所以 API 中的名词也应该使用复数
  5. http 请求数据的方式:(7 个 HTTP 方法:GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS)

    • GET(SELECT):从服务器取出资源(一项或多项)
    • POST(CREATE):在服务器新建一个资源
    • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)
    • DELETE(DELETE):从服务器删除资源
      还有三个不常用的 HTTP 请求方式
    • HEAD:获取资源的元数据
    • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的
    • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性

Beego 中配置服务器端允许跨域

在 main.go 中配置如下代码

beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
    AllowOrigins: []string{"*"}, 
    AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, 
    AllowHeaders: []string{"Origin", "Authorization", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"}, 
    ExposeHeaders: []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"}, 
    AllowCredentials: true, 
}))

参数说明:

  • AllowAllOrigins 允许全部来源设置为 true 则所有域名都可以访问本网站接口,可以将此配 置换成为 AllowOrigins:[“允许访问的域名”]
  • AllowMethods :允许的请求类型
  • AllowHeaders:允许的头部信息
  • ExposeHeaders:允许暴露的头信息
  • AllowCredentials:如果设置,允许共享 AuthTuffic 证书,如 Cookie
  • AllowOrigins:[“允许访问的域名”]

示例代码

beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{ 
    AllowOrigins: []string{"http://10.*.*.*:*", "http://localhost:*", "http://127.0.0.1:*"}, 
    AllowMethods: []string{"*"}, 
    AllowHeaders: []string{"Origin", "Authorization", "Access-Control-Allow-Origin", "content-type"}, 
    ExposeHeaders: []string{"Content-Length", "Access-Control-Allow-Origin"}, 
    AllowCredentials: true,
}))

游客权限

在后台设置了游客权限,可以看到后台管理中心的管理员列表,商品列表等
但是只有只读的权限,并没有修改的权限,如果涉及到了没有权限的操作,则会重定向这样的页面,还是有点好看


项目总算完成得差不多了,就差部署到服务器了
但是在这之前,还有一个东西需要了解一下---ElasticSearch

ElasticSearch

ElasticSearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索。稳定,可靠,快速,安装使用方便

电脑上面必须安装 java jdk 以及配置对应的环境变量

Windows 下面下载并启动 ElasticSearch

  1. elasticsearch 下载:https://www.elastic.co/downloads/elasticsearch
  2. 运行 elasticsearch:
    下载完成 elasticsearch 包后,把 elasticsearch 包放在一个固定目录,然后从命令窗口 cd 到 elasticsearch 包对应的目录,运行位于 bin 文件夹中的 elasticsearch.bat。这将会启动 ElasticSearch 在控制台的前台运行,这意味着我们可在控制台中看到运行信息或一些错误信息。并可以使用 ctrl + c 停止或关闭它
  3. 访问 ElasticSearch Api
    当 ElasticSearch 的实例并运行,您可以使用 localhost:9200,基于 JSON 的 REST API 与 ElasticSearch 进行通信,如果输入 http://localhost:9200/ 出来如下界面,说明我们的 ElasticSearch 配置并启动成功


ElasticSearch 默认情况只适用于英文分词,如果要做中文分词的话我们要安装 elasticsearch-analysis-ik 插件

安装配置中文分词工具

  1. 下载中文分词工具
  2. 在分词工具根目录创建 plugins/ik 文件
  3. 把分词工具包的内容复制到 plugins/ik 文件里面
  4. 修改配置文件的版本
  5. 重启ElasticSearch

Elasticsearch 中的一些概念概念

集群(cluster)

  • 代表一个集群,集群中有多个节点(node),其中有一个为主节点,这个主节点是 可以通过选举产生的,主从节点是对于集群内部来说的。es 的一个概念就是去中心 化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看 es 集 群,在逻辑上是个整体,你与任何一个节点的通信和与整个 es 集群通信是等价的

索引(index)

  • ElasticSearch 将它的数据存储在一个或多个索引(index)中。用 SQL 领域的术 语来类比,索引就像数据库,可以向索引写入文档或者从索引中读取文档,并通过 ElasticSearch 内部使用 Lucene 将数据写入索引或从索引中检索数据

文档(document)

  • 文档(document)是 ElasticSearch 中的主要实体。对所有使用 ElasticSearch 的案例来说,他们最终都可以归结为对文档的搜索。文档由字段构成

映射(mapping)

  • 所有文档写进索引之前都会先进行分析,如何将输入的文本分割为词条、哪些词条 又会被过滤,这种行为叫做映射(mapping)。一般由用户自己定义规则


终于写完了,昨天把一些遗留的小Bug修复完成后,今天将项目上传到了腾讯云服务器,项目总算是上线了
大家可以访问 http://mi.sunyj.xyz/ 这个网址进行体验

今天就记录最后一点内容,linux服务器部署beego项目

linux服务器部署beego项目

要想项目跑在服务器上,那么肯定需要准备一台服务器,这里我已Linux为例
如果说,你想通过指定的网址进行访问,那么还需要一个域名
如果没有的话也没关系,可以通过IP地址加端口号也可以进行访问

1:将本地数据库文件复制到服务器的数据中

这里我使用的是Navicat这个软件进行操作

  1. 首先将本地数据库中的数据加结构转储为SQL文件
  2. 使用Navicat连接服务器的数据库
  3. 连接成功后,运行刚才转储的SQL文件
    需要注意的是,SQL文件中的字符集和排序规则一定要和服务器数据的类型一样,不然会导入数据失败
    如果类型不一样,先查看服务器中的类型是什么,接着用notepad++或者其他工具打开刚才的SQL文件,进行批量替换就行了

2:打包BeeGO项目

在打包之前,我们需要更改一些项目连接数据库的参数
在腾讯云服务器中,包含数据库文件的数据库为micom,用户为micom,密码为124212,跟本地数据库的信息不同,所以需要在core.go文件中进行修改

func init() {
    DB,err = gorm.Open("mysql", "micom:124212@/micom?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        beego.Error(err)
    }
}

在GoLand中,打开终端,输入 bee pack -be GOOS=linux,即可将BeeGo项目打包成Linux平台可运行的程序

3:运行项目

打包成功后,会在当前项目的根目录下面生成.tar.gz文件,我们将这个文件上传到服务器的文件夹中进行解压即可
关于文件夹,建议放在www/wwwroot/常见一个文件夹 这样的目录下,我自己的目录:www/wwwroot/micom

接着,进入到micom中,输入 nohub ./项目名称 & 即可运行项目 nohub ./micom &

项目中用到了redis,需要在服务器中安装redis

项目跑起来后,可以通过服务器的IP地址,加上你在项目中.conf配置文件中设置的端口号进行访问

4:DNS解析

如果我们想用域名进行访问网站,需要进行域名解析,和Nginx配置
域名解析就不用多解释了,经常玩服务器的同学应该很老练

关于Nginx,我用到了宝塔面板,在面板进行了配置
在/www/server/panel/vhost/nginx 这个目录下,新建了一个配置文件 micom.conf
将IP地址指向了刚刚解析的域名,这样我们就可以通过域名进行访问


完结撒花,芜湖起飞~~~

世界因代码而改变 Peace Out
最后修改:2021 年 03 月 22 日 02 : 18 PM
如果觉得我的文章对你有用,请随意赞赏