投票 V1 版本
2023/10/1大约 10 分钟
1. 优化登录接口
将登录接口优化为AJAX
html css js ->** jquery** -> angularjs->** vue **(dva) react -> uniapp
gin-vue-admin
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>香香编程-投票项目</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<main class="main">
<input type="text" name="name" id="name" placeholder="Your name">
<input type="password" name="password" id="password" placeholder="Password">
<button type="submit" id="login_sub">Sign in</button>
</main>
<script>
$(document).ready(function(){
$("#login_sub").on("click",function () {
$.ajax({
//请求资源路径
url:"/login",
//请求参数
data:{
name:$("#name").val(),
password:$("#password").val()
},
//请求方式
type:"post",
//数据形式
dataType:"json",
//请求成功后调用的回调函数
success:function (data) {
console.log(data)
if (data.code !== 0){
alert(data.message)
}else{
alert("已登录")
setTimeout("pageRedirect()", 3000);
}
},
//请求失败后调用的回调函数
error:function () {
alert("登录成功")
}
});
});
});
function pageRedirect() {
window.location.replace("/index"); //实现跳转
}
</script>
</body>
</html>
同步与异步
阻塞与非阻塞
nginx redis 高并发
引入Session代替Cookie
正常情况下 session 保存登录态,是需要依赖一下cookie的。但,不绝对。
package model
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
)
var store = sessions.NewCookieStore([]byte("香香编程喵喵喵"))
var sessionName = "session-name"
func GetSession(c *gin.Context) map[interface{}]interface{} {
session, _ := store.Get(c.Request, sessionName)
fmt.Printf("session:%+v\n", session.Values)
return session.Values
}
func SetSession(c *gin.Context, name string, id int64) error {
session, _ := store.Get(c.Request, sessionName)
session.Values["name"] = name
session.Values["id"] = id
return session.Save(c.Request, c.Writer)
}
func FlushSession(c *gin.Context) error {
session, _ := store.Get(c.Request, sessionName)
fmt.Printf("session : %+v\n", session.Values)
session.Values["name"] = ""
session.Values["id"] = 0
return session.Save(c.Request, c.Writer)
}
cookie 是什么?session 是什么?他们两者之间有什么区别 有什么关系?
cookie 的跨域问题
2. 统一返回结构
HTTPCODE
大致可以分为5类:
- 1xx(信息性状态码):请求已经被接受,需要继续处理。**(全部不常用) **
- 100 Continue:服务器已经接收到请求的首部,并且客户端应该继续发送剩余的请求。
- 101 Switching Protocols:服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。
- 2xx(成功状态码):请求已成功被服务器接收、理解、并接受。
- 200 OK:请求成功。一般用于GET、POST请求。(仅200 常用)
- 201 Created:请求已经被实现,而且有一个新的资源已经依据请求的需要而建立。
- 204 No Content:服务器成功处理了请求,但不需要返回任何实体内容。
- 3xx(重定向状态码):需要客户端采取进一步的操作才能完成请求。(浏览器会响应)
- 301 Moved Permanently:被请求的资源已经永久移动到新位置。(某些浏览器会缓存这个状态,慎用)
- 302 Found:请求的资源现在临时从不同的URI响应请求。
- 304 Not Modified:客户端请求的资源未修改,可以使用缓存的版本
- 4xx(客户端错误状态码):请求包含错误语法或不能被服务器理解。
- 400 Bad Request:请求无效,服务器不理解请求的语法。
- 401 Unauthorized:请求要求身份验证。
- 403 Forbidden:服务器理解请求,但是拒绝执行它。
- 404 Not Found : 文件信息不存在。
- 5xx(服务器错误状态码):服务器在处理请求的过程中发生了错误。
- 500 Internal Server Error:服务器遇到了不知道如何处理的情况。
- 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到了无效的响应。
- 503 Service Unavailable:服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态。
客户端 -》网络-》服务器-》找资源
服务器-》网络-》客户端

业务CODE
package tools
import "fmt"
var (
OK = ECode{Code: 0}
NotLogin = ECode{Code: 10001, Message: "用户未登录"}
ParamErr = ECode{Code: 10002, Message: "参数错误"}
)
type ECode struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
func (e *ECode) String() string {
return fmt.Sprintf("code:%d,message:%s", e.Code, e.Message)
}
3. 优化投票接口
使用事务优化接口
// DoVote 通用方式
func DoVote(userId, voteId int64, optIDs []int64) bool {
tx := Conn.Begin()
var ret Vote
if err := tx.Table("vote").Where("id = ?", voteId).First(&ret).Error; err != nil {
fmt.Printf("err:%s", err.Error())
tx.Rollback()
return false
}
for _, value := range optIDs {
if err := tx.Table("vote_opt").Where("id = ?", value).Update("count", gorm.Expr("count + ?", 1)).Error; err != nil {
fmt.Printf("err:%s", err.Error())
tx.Rollback()
return false
}
user := VoteOptUser{
VoteId: voteId,
UserId: userId,
VoteOptId: value,
CreatedTime: time.Now(),
}
err := tx.Create(&user).Error // 通过数据的指针来创建
if err != nil {
fmt.Printf("err:%s", err.Error())
tx.Rollback()
return false
}
}
tx.Commit()
return true
}
GORM事务的其他用法
// DoVoteV1 原生SQL
func DoVoteV1(userId, voteId int64, optIDs []int64) bool {
Conn.Exec("begin").
Exec("select * from vote where id = ?", voteId).
Exec("commit")
return false
}
// DoVoteV2 匿名函数
func DoVoteV2(userId, voteId int64, optIDs []int64) bool {
err := Conn.Transaction(func(tx *gorm.DB) error {
var ret Vote
if err := tx.Table("vote").Where("id = ?", voteId).First(&ret).Error; err != nil {
fmt.Printf("err:%s", err.Error())
return err //只要返回了err GORM会直接回滚,不会提交
}
for _, value := range optIDs {
if err := tx.Table("vote_opt").Where("id = ?", value).Update("count", gorm.Expr("count + ?", 1)).Error; err != nil {
fmt.Printf("err:%s", err.Error())
return err
}
user := VoteOptUser{
VoteId: voteId,
UserId: userId,
VoteOptId: value,
CreatedTime: time.Now(),
}
err := tx.Create(&user).Error // 通过数据的指针来创建
if err != nil {
fmt.Printf("err:%s", err.Error())
return err
}
}
return nil //如果返回nil 则直接commit
})
if err != nil {&
return false
}
return true
}
4. 将其他接口全部优化为异步
<!doctype html>
<html lang="en">
<head>
<title>香香编程-投票项目</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<main>
<div id="vote_table">
</div>
<!-- {{range $key,$value := .vote}}-->
<!-- <h2>{{$value.Title}}</h2>-->
<!-- {{end}}-->
</main>
<script>
$(document).ready(function(){
loadData()
});
function loadData() {
$.ajax({
//请求资源路径
url:"/votes",
//请求参数
data:{
},
//请求方式
type:"get",
//数据形式
dataType:"json",
//请求成功后调用的回调函数
success:function (data) {
console.log(data)
for (const datum of data.data) {
$("#vote_table").append('<a href=\"/vote?id='+datum.Id+'\">'+datum.Title+'</a ></h2><br>');
}
},
//请求失败后调用的回调函数
error:function () {
alert("数据加载失败!")
}
});
}
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<title>香香编程-投票项目</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<main>
<div id="vote_table">
</div>
<!-- {{range $key,$value := .vote}}-->
<!-- <h2>{{$value.Title}}</h2>-->
<!-- {{end}}-->
<h1>
投票详情
</h1>
<div id="vote_info">
</div>
</main>
<script>
$(document).ready(function(){
loadData()
// 当点击<a>标签时触发AJAX请求
$('body').on('click', '.ajax-trigger', function(event) {
// 阻止<a>标签的默认行为,即防止页面跳转
event.preventDefault();
$("#vote_info").empty();
$.ajax({
url: "/vote",
type: "GET",
data:{
id:$(this).attr("data"),
},
dataType: "json",
success: function(data) {
var vote = data.data.Vote
var h1 = $("<div></div>")
h1.append('<h2>title:'+vote.Title+'</h2>')
h1.append('<h2>id:'+vote.Id+'</h2>')
h1.append('<h2>Type:'+vote.Type+'</h2>')
h1.append('<h2>Status:'+vote.Status+'</h2>')
$("#vote_info").append(h1)
var form = $("<form method=\"post\" action=\"/vote\" ></form>")
form.append(' <input type="hidden" name="vote_id" value="'+vote.Id+'">')
for (const v of data.data.Opt) {
form.append('<input type="checkbox" name="opt[]" id="customCheck'+v.Id+'" value="'+v.Id+'">');
form.append('<label for="customCheck'+v.Id+'">'+v.Name+'</label>');
}
form.append('<button type="submit">Submit</button>')
$("#vote_info").append(form)
},
error: function(data) {
alert(data.message)
}
});
});
});
function loadData() {
$.ajax({
//请求资源路径
url:"/votes",
//请求参数
data:{
},
//请求方式
type:"get",
//数据形式
dataType:"json",
//请求成功后调用的回调函数
success:function (data) {
console.log(data)
for (const datum of data.data) {
$("#vote_table").append(''+datum.Title+'</h2><br>');
}
},
//请求失败后调用的回调函数
error:function () {
alert("数据加载失败!")
}
});
}
</script>
</body>
</html>
- 为后续的前后端分离做准备。公司里,前端后端一定是分离的。gin-vue-admin
- 为后续的编写接口文档做准备。设计-》开发-》前后端联调
5.为投票项目增加一些接口
新增投票接口
//logic
func AddVote(context *gin.Context) {
idStr := context.Query("title")
optStr, _ := context.GetPostFormArray("opt_name[]")
//构建结构体
vote := model.Vote{
Title: idStr,
Type: 0,
Status: 0,
CreatedTime: time.Now(),
}
opt := make([]model.VoteOpt, 0)
for _, v := range optStr {
opt = append(opt, model.VoteOpt{
Name: v,
CreatedTime: time.Now(),
})
}
if err := model.AddVote(vote, opt); err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10006,
Message: err.Error(),
})
return
}
context.JSON(http.StatusOK, tools.OK)
return
}
//model
func AddVote(vote Vote, opt []VoteOpt) error {
err := Conn.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&vote).Error; err != nil {
return err
}
for _, voteOpt := range opt {
voteOpt.VoteId = vote.Id
if err := tx.Create(&voteOpt).Error; err != nil {
return err
}
}
return nil
})
return err
}
修改投票接口
// logic 自己写
//model
func UpdateVote(vote Vote, opt []VoteOpt) error {
err := Conn.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&vote).Error; err != nil {
return err
}
for _, voteOpt := range opt {
if err := tx.Save(&voteOpt).Error; err != nil {
return err
}
}
return nil
})
return err
}
删除投票接口
//logic
// DelVote 新增一个投票
func DelVote(context *gin.Context) {
var id int64
idStr := context.Query("id")
id, _ = strconv.ParseInt(idStr, 10, 64)
if err := model.DelVote(id); err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10006,
Message: err.Error(),
})
return
}
context.JSON(http.StatusOK, tools.OK)
return
}
//model
func DelVote(id int64) bool {
if err := Conn.Transaction(func(tx *gorm.DB) error {
if err := tx.Delete(&Vote{}, id).Error; err != nil {
fmt.Printf("err:%s", err.Error())
return err
}
if err := tx.Where("vote_id = ?", id).Delete(&VoteOpt{}).Error; err != nil {
fmt.Printf("err:%s", err.Error())
return err
}
if err := tx.Where("vote_id = ?", id).Delete(&VoteOptUser{}).Error; err != nil {
fmt.Printf("err:%s", err.Error())
return err
}
return nil
}); err != nil {
fmt.Printf("err:%s", err.Error())
return false
}
return true
}
CURD,很多工作,本身就是CURD。
- 这个业务,能不能赚钱,怎么赚钱的。是否提高了公司工作效率。
- 当前的业务是否可以进一步优化,这个优化能否进一步提高效率,(1减少人工,2减少时间)
- 业务中有没有典型的问题,或者比较复杂的场景。
没有,是自己不知道?或者知道,但没有解决办法。
上述三条,你都已经非常清楚了。换工作,或者内部调岗。
技术是一方面,业务是另一方面。
6. 投票结束,看到投票结果
设计一个新的返回结构
注意:model里的结构体是不能直接返回给接口的。正常情况下,我们需要给API 定义一个标准的返回结构,这个接口可能是 model里结构体的子集,也可能是与其他结构体的并集。
// ResultData 新定义返回结构
type ResultData struct {
Title string
Count int64
Opt []*ResultVoteOpt
}
type ResultVoteOpt struct {
Name string
Count int64
}
// ResultVote 返回一个投票结果
func ResultVote(context *gin.Context) {
var id int64
idStr := context.Query("id")
id, _ = strconv.ParseInt(idStr, 10, 64)
ret := model.GetVote(id)
data := ResultData{
Title: ret.Vote.Title,
}
for _, v := range ret.Opt {
data.Count = data.Count + v.Count
tmp := ResultVoteOpt{
Name: v.Name,
Count: v.Count,
}
data.Opt = append(data.Opt, &tmp)
}
context.JSON(http.StatusOK, tools.ECode{
Data: data,
})
}
引入Echart 绘制饼状图
Echarts 文档:https://echarts.apache.org/handbook/zh/get-started/
<!doctype html>
<html lang="en">
<head>
<title>香香编程-投票项目</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<!-- 引入 echarts.js -->
<script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
</head>
<body>
<main>
<div id="echarts_main" style="width: 600px;height:400px;">
</div>
</main>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('echarts_main'));
// 指定图表的配置项和数据
var option = {
title: {
text: '第一个 ECharts 实例'
},
tooltip: {},
legend: {
data:['销量']
},
xAxis: {
data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<title>香香编程-投票项目</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<!-- 引入 echarts.js -->
<script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
</head>
<body>
<main>
<div id="vote_table">
</div>
<div id="echarts_main" style="width: 600px;height:400px;">
</div>
</main>
<script type="text/javascript">
$(document).ready(function(){
loadData()
$('body').on('click', '.ajax-trigger', function(event) {
// 阻止<a>标签的默认行为,即防止页面跳转
var myChart = echarts.init(document.getElementById('echarts_main'));
myChart.clear();
event.preventDefault();
$.ajax({
url: "/result/info",
type: "GET",
data:{
id:$(this).attr("data"),
},
dataType: "json",
success: function(data) {
// 指定图表的配置项和数据
var myArray = [];
for (const m of data.data.Opt) {
var tmp = {
value:m.Count,
name:m.Name,
}
myArray.push(tmp)
}
option = {
title: {
text: data.data.Title,
subtext: '投票详情',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
type: 'pie',
radius: '50%',
data: myArray,
}
]
};
myChart.setOption(option);
},
error: function(data) {
alert(data.message)
}
});
});
});
function loadData() {
$.ajax({
//请求资源路径
url:"/votes",
//请求参数
data:{
},
//请求方式
type:"get",
//数据形式
dataType:"json",
//请求成功后调用的回调函数
success:function (data) {
console.log(data)
for (const datum of data.data) {
$("#vote_table").append('<a href="#" class="ajax-trigger" data="'+datum.Id+'">'+datum.Title+'</a></h2><br>');
}
},
//请求失败后调用的回调函数
error:function () {
alert("数据加载失败!")
}
});
}
</script>
</body>
</html>