Go语言项目实战班04 Go语言课程管理系统项目实战 20240807 课程笔记和上课代码

128 篇文章 0 订阅
108 篇文章 1 订阅

预览

在这里插入图片描述

课程特色

本教程录制于2024年8月8日,使用Go1.22版本,基于Goland2024进行开发,采用的技术栈比较新。

每节课控制在十分钟以内,课时精简,每节课都是一个独立的知识点,如果有遗忘,完全可以当做字典来查询,绝不浪费大家的时间。

整个课程从两行代码实现注册登录API接口讲起,以一个课程系统为实战,结合Vue3开发的前端,实现一个基本的前后端分离的课程管理系统,层层递进,学习路径平缓。

Golang是当前国内越来越多的企业正在全面转的一门系统级别的高性能的编程语言,比C语言写法更加的简单,比Python性能更加的好,是新时代的C语言,建议每个程序员都掌握!

视频课程

最近发现越来越多的公司在用Golang了,所以精心整理了一套视频教程给大家,这个是其中的第15部,后续还会有很多。

视频已经录制完成,完整目录截图如下:

在这里插入图片描述

本套录播课的售价是319元。

本套课程的特色是每节课都是一个核心知识点,每个视频控制在十分钟左右,精简不废话,拒绝浪费大家的时间。

课程目录

  • 01 概述
  • 02 搭建项目环境
  • 03 连接MySQL数据库
  • 04 课程表的设计和创建
  • 05 项目工程化
  • 06 整合gin框架
  • 07 两行代码自动生成注册和登录接口
  • 08 封装路由模块
  • 09 实现新增课程的接口
  • 10 实现分页查询的接口
  • 11 实现根据ID查询课程的接口
  • 12 解决根据ID查询不生效的问题
  • 13 实现根据ID修改课程的接口
  • 14 实现根据ID删除课程的接口
  • 15 前端界面的整体预览和开发思路
  • 16 实现登录的功能
  • 17 实现记录token和跳转首页的功能
  • 18 实现显示登录用户名的功能
  • 19 实现注销的功能
  • 20 解决注销按钮无法自动显示的BUG
  • 21 完善写作页面和双向绑定变量的设计
  • 22 给写作接口添加简单的权限校验
  • 23 实现添加文章的功能
  • 24 实现文章的请求和动态渲染
  • 25 将秒值转换为年月日字符串
  • 26 实现点击文章标题跳转详情页面的功能
  • 27 实现文章详情的渲染
  • 28 渲染课程的价格
  • 29 渲染编辑按钮
  • 30 实现编辑课程的功能
  • 31 给编辑文章的接口添加简单的权限校验
  • 32 总结

完整代码

03 连接MySQL数据库

package g

import (
	"api/model"
	ginLogin "github.com/zhangdapeng520/zdpgo_gin_login"
	gorm "github.com/zhangdapeng520/zdpgo_gorm"
	_ "github.com/zhangdapeng520/zdpgo_mysql"
)

var GDB *gorm.DB

func initMySQL() {
	var err error
	GDB, err = gorm.Open(
		"mysql",
		"root:root@tcp(127.0.0.1:3306)/blog?charset=utf8mb4&parseTime=True&loc=Local",
	)
	if err != nil {
		panic(err)
	}

	GDB.AutoMigrate(&model.CourseArticle{})
	GDB.AutoMigrate(&ginLogin.GinLoginUser{})
}

func closeMySQL() {
	GDB.Close()
}

04 课程表的设计和创建

package model

type CourseArticle struct {
	Id          int     `json:"id"`
	Title       string  `json:"title" gorm:"unique"`          // 标题
	Category    string  `json:"category"`                     // 分类
	Description string  `json:"description"`                  // 描述
	Content     string  `json:"content" gorm:"type:longtext"` // 内容
	Price       float64 `json:"price" gorm:"type:decimal"`    // 价格
	SaleNum     int     `json:"sale_num"`                     // 销量
	GoodNum     int     `json:"good_num"`                     // 点赞数量
	MoneyNum    int     `json:"money_num"`                    // 打赏数量
	ViewNum     int     `json:"view_num"`                     // 浏览量
	AddTime     int     `json:"add_time"`                     // 添加时间
	UpdateTime  int     `json:"update_time"`                  // 修改时间
}

07 两行代码自动生成注册和登录接口

package router

import (
	"api/g"
	gin "github.com/zhangdapeng520/zdpgo_gin"
	ginLogin "github.com/zhangdapeng520/zdpgo_gin_login"
)

func initUser(app *gin.Engine) {
	group := app.Group("/user")
	group.POST(
		"/register/",
		ginLogin.GetRegisterHandler(g.GDB, g.PasswordSalt),
	)
	group.POST(
		"/login/",
		ginLogin.GetLoginHandler(g.GDB, g.JwtKey, g.PasswordSalt),
	)
}

09 实现新增课程的接口

package course_article

import (
	"api/g"
	"api/model"
	gin "github.com/zhangdapeng520/zdpgo_gin"
	"time"
)

func add(c *gin.Context) {
	// 解析请求
	var req requestCourseArticle
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// 对作者做简单的限制,只有指定的作者才能拥有写作权限
	if req.Author != "zhangdapeng" {
		c.JSON(402, gin.H{"error": "you have not authorization"})
		return
	}

	// 新增
	now := int(time.Now().Unix())
	g.GDB.Create(&model.CourseArticle{
		Title:       req.Title,
		Category:    req.Category,
		Description: req.Description,
		Content:     req.Content,
		Price:       req.Price,
		SaleNum:     0,
		GoodNum:     0,
		MoneyNum:    0,
		ViewNum:     0,
		AddTime:     now,
		UpdateTime:  now,
	})

	c.JSON(200, nil)
}

10 实现分页查询的接口

package course_article

import (
	"api/g"
	"api/model"
	gin "github.com/zhangdapeng520/zdpgo_gin"
	"strconv"
)

func getAll(c *gin.Context) {
	pageStr := c.DefaultQuery("page", "1")
	sizeStr := c.DefaultQuery("size", "20")

	page, err := strconv.Atoi(pageStr)
	if err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	size, err := strconv.Atoi(sizeStr)
	if err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	var articles []model.CourseArticle
	g.GDB.
		Limit(size).
		Offset((page - 1) * size).
		Order("update_time desc").
		Find(&articles)
	c.JSON(200, articles)
}

func get(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	var article model.CourseArticle
	g.GDB.Find(&article, "id=?", id)
	c.JSON(200, article)
}

13 实现根据ID修改课程的接口

package course_article

import (
	"api/g"
	"api/model"
	gin "github.com/zhangdapeng520/zdpgo_gin"
	"strconv"
)

func update(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// 解析请求
	var req requestCourseArticle
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// 添加简单的权限校验
	if req.Author != "zhangdapeng" {
		c.JSON(402, gin.H{"error": "you have not authorization"})
		return
	}

	// 根据ID查询
	var article model.CourseArticle
	g.GDB.Find(&article, "id=?", id)
	if article.Id == 0 {
		c.JSON(404, gin.H{"error": "article not found"})
		return
	}

	// 修改
	article.Title = req.Title
	article.Category = req.Category
	article.Price = req.Price
	article.Description = req.Description
	article.Content = req.Content
	err = g.GDB.Save(&article).Error
	if err != nil {
		c.JSON(500, gin.H{"error": err.Error()})
		return
	}

	c.JSON(200, nil)
}

14 实现根据ID删除课程的接口

package course_article

import (
	"api/g"
	"api/model"
	gin "github.com/zhangdapeng520/zdpgo_gin"
	"strconv"
)

func deleteArticle(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	err = g.GDB.Delete(&model.CourseArticle{Id: id}).Error
	if err != nil {
		c.JSON(500, gin.H{"error": err.Error()})
		return
	}

	c.JSON(200, nil)
}

16 实现登录的功能

<template>
  <form class="p-3">
    <div class="form-group">
      <label for="username">账号</label>
      <input type="text" class="form-control" id="username" v-model="username">
    </div>
    <div class="form-group">
      <label for="password">密码</label>
      <input type="password" class="form-control" id="password" v-model="password">
    </div>
    <button class="btn btn-primary" @click="onLogin">立即登录</button>
  </form>

</template>

<script setup lang="ts">
import {ref} from "vue";
import axios from "redaxios";
import {useRouter} from "vue-router";

const username = ref("")
const password = ref("")
const router = useRouter()

const onLogin = () => {
  if (username.value == "") {
    alert("请输入用户名")
    return
  }
  if (username.value.length < 3) {
    alert("用户名长度最小为3")
    return
  }
  if (username.value.length > 36) {
    alert("用户名长度最大为36")
    return
  }
  if (password.value == "") {
    alert("请输入密码")
    return
  }
  if (password.value.length < 6) {
    alert("密码长度最小为6")
    return
  }
  if (password.value.length > 128) {
    alert("密码长度最大为128")
    return
  }

  axios({
    method: "POST",
    url: "/api/user/login/",
    data: {
      username: username.value,
      password: password.value,
    }
  }).then((res) => {
    let data =res.data
    if (data){
      localStorage.setItem("token", data.token)
      localStorage.setItem("username", data.username)
      router.push("/")
    }else{
      alert("登录失败!")
    }
  })
}
</script>

18 实现显示登录用户名的功能

<template>
  <nav
      class="navbar navbar-expand-md navbar-light mb-0"
      :style="`background-color: ${VUE_APP_NAVBAR_BG_CSS_COLOR}; color: ${VUE_APP_NAVBAR_TEXT_CSS_COLOR};`"
  >
    <router-link
        class="navbar-brand"
        :to="'/'"
        :style="`color: ${VUE_APP_NAVBAR_TEXT_CSS_COLOR};`"
    >
      {{ title }}
    </router-link>
    <button
        :class="`navbar-toggler collapsed`"
        type="button"
        aria-label="Toggle navigation"
        @click.prevent="showDropdown = !showDropdown"
    >
      <span
          class="navbar-toggler-icon"
          :style="`background-color: ${VUE_APP_NAVBAR_TEXT_CSS_COLOR};`"
      />
    </button>

    <div
        id="navbarNavDropdown"
        class="navbar-collapse"
    >
      <ul class="navbar-nav ml-auto top-right"
          @focusout="focusOut"
          tabindex="1">
        <li class="category">Python</li>
        <li class="category">Golang</li>
        <li class="nav-item" v-if="username">
          <a
              class="nav-link border rounded py-2 px-3 mr-2"
              style="color: white">
            {{ username }}
          </a>
        </li>
        <li class="nav-item" v-else>
          <router-link
              class="nav-link border rounded py-2 px-3 mr-2"
              :to="'/login'"
              :style="`color: ${VUE_APP_NAVBAR_TEXT_CSS_COLOR};`"
          >
            登录
          </router-link>
        </li>

        <li class="nav-item" v-if="username">
          <a
              class="nav-link border rounded py-2 px-3 mr-2 logout"
              @click.prevent="onLogout"
              style="color: white">
            注销
          </a>
        </li>

        <li v-if="router.currentRoute.value.path !== '/editor'" class="nav-item">
          <router-link
              class="nav-link border rounded py-2 px-3"
              :to="'/editor'"
              :style="`color: ${VUE_APP_NAVBAR_TEXT_CSS_COLOR};`"
          >
            写作
          </router-link>
        </li>
      </ul>
    </div>
  </nav>
</template>

<script setup lang="ts">
import {onMounted, ref} from 'vue';
import blogConfig from '../blog_config'
import router from '../router';
import {useRouter} from "vue-router";

const {VUE_APP_NAVBAR_BG_CSS_COLOR = 'black', VUE_APP_NAVBAR_TEXT_CSS_COLOR = 'white'} = blogConfig

defineProps({
  title: {
    type: String,
    default: ''
  },
  sections: {
    type: Object,
    default: () => ({})
  }
});

const showDropdown = ref(false)

const focusOut = ({relatedTarget}: any) => {
  if (!(Array.from(relatedTarget?.classList ?? []).includes('dropdown-item'))) {
    showDropdown.value = false
  }
}

const username = ref("")
const mrouter = useRouter()
const onLogout = () => {
  localStorage.removeItem("token")
  localStorage.removeItem("username")
  localStorage.removeItem("refresh")
  username.value = ""
  mrouter.push("/login")
}

onMounted(() => {
  username.value = localStorage.getItem("username") || ""
})
</script>

<style scoped>
.top-right {
  display: flex;
  align-items: center;
  justify-content: center;
}

.top-right .category {
  margin-right: 15px;
}

.top-right .category:hover {
  color: cornflowerblue;
  cursor: pointer;
}

.logout:hover {
  color: dodgerblue !important;
  cursor: pointer;
}
</style>

23 实现添加文章的功能

<template>
  <form class="p-3">
    <div class="form-group">
      <label for="title">标题</label>
      <input type="text" class="form-control" id="title" v-model="title">
    </div>
    <div class="form-group">
      <label for="category">分类</label>
      <input type="text" class="form-control" id="category" v-model="category">
    </div>
    <div class="form-group">
      <label for="price">价格</label>
      <input type="number" class="form-control" id="price" v-model="price">
    </div>
    <div class="form-group">
      <label for="description">描述</label>
      <textarea class="form-control" id="description" rows="3" v-model="description"></textarea>
    </div>
    <div class="form-group">
      <label for="content">内容</label>
      <v-md-editor v-model="content" @save="onSave" height="100vh"
                   :right-toolbar="'preview sync-scroll fullscreen'"></v-md-editor>
    </div>
    <button class="btn btn-primary" @click="onSubmit">保存</button>
  </form>

</template>

<script setup>
import {onMounted, ref} from "vue"
import axios from "redaxios";
import {useRouter} from "vue-router";

const router = useRouter()
const editArticle = ref({}) // 编辑的文章
const author = ref("")
const title = ref("")
const category = ref("")
const price = ref(0)
const description = ref("")
const content = ref("支持Markdown语法")

const onSave = (text, html) => {
  alert("保存成功")
}
const onSubmit = () => {
  if (title.value.length < 3) {
    alert("标题最小长度是3")
    return
  }
  if (category.value.length < 3) {
    alert("分类最小长度是3")
    return
  }
  if (price.value < 0) {
    alert("价格不能小于0")
    return
  }
  if (description.value.length < 6) {
    alert("描述最小长度是6")
    return
  }
  if (content.value.length < 6) {
    alert("内容最小长度是6")
    return
  }

  // 提交
  if (editArticle.value && editArticle.value.id){
    axios({
      method: "put",
      url: `/api/course_article/${editArticle.value.id}/`,
      data: {
        author: author.value,
        title: title.value,
        category: category.value,
        price: price.value,
        description: description.value,
        content: content.value,
      }
    }).then(() => {
      localStorage.removeItem("edit_article")
      router.push("/")
    }).catch(err => {
      alert(err)
    })
  }else{
    axios({
      method: "post",
      url: "/api/course_article/",
      data: {
        author: author.value,
        title: title.value,
        category: category.value,
        price: price.value,
        description: description.value,
        content: content.value,
      }
    }).then(() => {
      title.value = ""
      category.value = ""
      price.value = 0
      description.value = ""
      content.value = ""

      router.push("/")
    }).catch(err => {
      alert(err)
    })
  }

}

onMounted(() => {
  // 获取作者信息
  author.value = localStorage.getItem("username") || ""

  // 尝试获取编辑文章信息
  try {
    editArticle.value = JSON.parse(localStorage.getItem("edit_article"))
    title.value = editArticle.value.title
    category.value = editArticle.value.category
    price.value = editArticle.value.price
    description.value = editArticle.value.description
    content.value = editArticle.value.content
  } catch {
  }
})
</script>

24 实现文章的请求和动态渲染

<template>
  <div :style="`background-color: ${VUE_APP_MAIN_BG_CSS_COLOR}; color: ${VUE_APP_MAIN_TEXT_CSS_COLOR};`">
    <div
        v-for="article in articles"
        :key="article.id"
        class="container markdown-body p-3 p-md-4 my-3"
    >
      <!-- 标题 -->
      <a class="text-reset link-title" @click.prevent="onOpenDetail(article)">
        <h3 class="text-left m-0 p-0">
          {{ article.title }}
        </h3>
      </a>

      <!-- 日期 -->
      <p class="font-weight-light  m-0 p-0 text-right">
        {{ secondsToDateStr(article.update_time) }}
      </p>
      <!--分类-->
      <div class="text-right">
        <a class="m-0 p-0 text-right font-weight-bold" style="cursor: pointer">
          #{{ article.category }}
        </a>
        <span class="text-right font-weight-bold" style="margin: 0 5px; color: red;">
          {{ article.price }} 元
        </span>
        <a
            class="m-0 p-0 text-right font-weight-bold"
            style="cursor: pointer"
            @click.prevent="onEdit(article)"
            v-if="username==='zhangdapeng'">
          编辑
        </a>
      </div>

      <p class="font-weight-light mt-1">
        {{ article.description }}
      </p>
    </div>
  </div>
</template>

<script setup>
import {ref, computed, inject, onMounted} from 'vue'
import blogConfig from '../blog_config'
import axios from "redaxios";
import {useRouter} from "vue-router";

const {VUE_APP_POSTS_PER_PAGE, VUE_APP_MAIN_BG_CSS_COLOR, VUE_APP_MAIN_TEXT_CSS_COLOR} = blogConfig


const router = useRouter()
const username = ref("")

// 打开详情页面
const onOpenDetail = (article) => {
  localStorage.setItem("article", JSON.stringify(article))
  router.push("/detail")
}

// 打开编辑页面
const onEdit = (article) => {
  localStorage.setItem("edit_article", JSON.stringify(article))
  router.push("/editor")
}

// 将时间的秒值转化为年月日字符串
const secondsToDateStr = (seconds) => {
  const date = new Date(seconds * 1000)

  // 月份
  const month = date.getMonth() + 1
  let monthStr = month.toString();
  if (month < 10) {
    monthStr = "0" + monthStr
  }

  // 日期
  const mdate = date.getDate()
  let mdateStr = mdate.toString();
  if (mdate < 10) {
    mdateStr = "0" + mdateStr
  }

  // 时
  const hour = date.getHours()
  let hourStr = hour.toString();
  if (hour < 10) {
    hourStr = "0" + hourStr
  }

  // 分
  const minute = date.getMinutes()
  let minuteStr = minute.toString();
  if (minute < 10) {
    minuteStr = "0" + minuteStr
  }

  // 秒
  const second = date.getSeconds()
  let secondStr = second.toString();
  if (second < 10) {
    secondStr = "0" + secondStr
  }

  // 返回
  return `${date.getFullYear()}-${monthStr}-${mdateStr} ${hourStr}:${minuteStr}:${secondStr}`
}
const articles = ref([])
onMounted(() => {
  // 获取当前登录用户
  username.value = localStorage.getItem("username") || ""

  // 解决注销按钮无法自动显示的BUG
  if (!localStorage.getItem("refresh")) {
    window.location.href = "/"
    localStorage.setItem("refresh", "true")
  }
  // 加载数据
  axios({
    method: 'get',
    url: "/api/course_article/",
    params: {
      page: 1,
      size: 20,
    }
  }).then(res => {
    articles.value = res.data
  })
})
</script>

<style scoped>
.link-title {
  cursor: pointer;
}

.link-title:hover {
  color: dodgerblue !important;
}
</style>

27 实现文章详情的渲染

<template>
  <div class="container my-4 my-md-5">
    <h1>{{ article.title}}</h1>
    <!--渲染富文本-->
    <v-md-editor
        mode="preview"
        v-model="article.content"
        :style="`background-color: ${VUE_APP_MAIN_BG_CSS_COLOR}; color: ${VUE_APP_MAIN_TEXT_CSS_COLOR};`"/>
    <!--返回按钮-->
    <button type="button" :style="`color: ${VUE_APP_MAIN_TEXT_CSS_COLOR};`" class="border btn mt-4"
            @click="hasHistory() ? router.go(-1) : router.push('/')">
      &laquo; 返回
    </button>
  </div>
</template>

<script setup lang="ts">
import {inject, onMounted, ref} from 'vue'
import {onBeforeRouteUpdate} from 'vue-router'
import router from '../router'
import axios from 'redaxios'
import {type PostIndex} from '../types/PostIndex'
import blogConfig from '../blog_config'

const {VUE_APP_MAIN_BG_CSS_COLOR, VUE_APP_MAIN_TEXT_CSS_COLOR} = blogConfig

const props = defineProps({
  id: {
    type: String,
    default: ''
  }
})

/* Hacky navigation when a href link is clicked within the compiled html Post */
onBeforeRouteUpdate(() => {
  location.reload()
})

// Fetch Post markdown and compile it to html
const postsIndex: PostIndex[] = inject<PostIndex[]>('postsIndex', [])
const {url = ''} = postsIndex.find(({id}) => id === props.id) || {}
const {data: markDownSource} = await axios.get(url)

// Patch page title
const [, title] = markDownSource.split('#')

// Back button helper
const hasHistory = () => window.history?.length > 2

const article = ref({}) // 文章
onMounted(() => {
  try {
    article.value = JSON.parse(localStorage.getItem("article"))
    console.log(article.value)
  } catch {
    console.error("读取文章信息失败")
  }
})
</script>

总结

整个课程从两行代码实现注册登录API接口讲起,以一个课程系统为实战,结合Vue3开发的前端,实现一个基本的前后端分离的课程管理系统,层层递进,学习路径平缓。

通过本套课程,能帮你入门gin+gorm+vue3开发前后端分离管理系统,积累实际的前后端分离开发经验。

如果您需要完整的源码,打赏20元即可。

人生苦短,我用PyGo,我是您身边的Python私教~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Python私教

创业不易,请打赏支持我一点吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值