预览
课程特色
本教程录制于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('/')">
« 返回
</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私教~