【Go项目】Go经典电子商城项目
原作者:https://github.com/congz666/cmall-go
Go实战-Gin-mall经典电子商城项目
视频地址:https://www.bilibili.com/video/BV1Zd4y1U7D8/
待完善:
Elasticsearch
redis查询商品 + 缓存
接口文件目录:
一、项目创建与读取配置文件
项目结构:
1 |
|
项目使用的所有驱动(依赖)
1 |
|
这一部分和todolist的配置差不多,①创建并读取配置文件,②配置并连接mysql数据库,额外添加mysql主从配置
①设置配置文件config.ini
1 |
|
②读取配置文件
1 |
|
③建立数据库连接
1 |
|
④实现数据库的迁移
1 |
|
⑤main方法调用配置文件读取
1 |
|
二、数据库创建
在model中创建所有结构体(数据库表),然后使用gorm的自动迁移生成表
1 |
|
在gorm迁移中开启自动迁移
1 |
|
四、新建路由
新建路由
1 |
|
中间使用了一个跨域中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// cors.go
package middleware
// 跨域
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method //请求方法
origin := c.Request.Header.Get("Origin") //请求头部
var headerKeys []string // 声明请求头keys
for k := range c.Request.Header {
headerKeys = append(headerKeys, k)
}
headerStr := strings.Join(headerKeys, ", ")
if headerStr != "" {
headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
} else {
headerStr = "access-control-allow-origin, access-control-allow-headers"
}
if origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
// header的类型
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
// 允许跨域设置 可以返回其他子段
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒
c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true
c.Set("content-type", "application/json") // 设置返回格式是json
}
//放行所有OPTIONS方法
if method == "OPTIONS" {
c.JSON(http.StatusOK, "Options Request!")
}
// 处理请求
c.Next() // 处理请求
}
}
五、添加日志模块
pkg.utils.logger
1 |
|
六、将error转换为json格式
api/v1/common.go
1 |
|
调用举例:
1 |
|
六、配置引入Redis
①读取Redis配置,②进行Redis链接
1 |
|
以获取商品点击数为例:
①创建Key:
1 |
|
- 下面这两个方法写在model的模型结构中(
model.product.go
)
②获取点击数
1 |
|
③增加点击数
1 |
|
接下来都是编写接口了
这次除了用户注册不写具体的包了,各别包会标注一下
一、用户操作
1、用户注册
① 首先编写路由routes.router.go
1 |
|
② Controller层:在api.v1.user.go中编写方法
1 |
|
③Service层:service.user.go
1、创建一个UserService结构体
2、实现注册方法
1 |
|
①
userDao := dao.NewUserDao(ctx)
- 将所有关于数据库操作的都封装在dao中,操作方式也是新建一个dao对象,然后调用其方法
② 将返回状态码和返回信息封装起来,封装在
pkg/e
包下,统一管理,返回信息可以根据返回状态码返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// e.go
// 存放所有状态码信息
const (
// 通用错误码
SUCCESS = 200
UpdatePasswordSuccess = 201
NotExistInentifier = 202
ERROR = 500
InvalidParams = 400
...
// 数据库错误
ErrorDatabase = 40001
)
// msg.go
// 定义所有错误信息
var MsgFlags = map[int]string{
SUCCESS: "ok",
UpdatePasswordSuccess: "修改密码成功",
NotExistInentifier: "该第三方账号未绑定",
ERROR: "fail",
InvalidParams: "请求参数错误",
...
ErrorDatabase: "数据库操作出错,请重试",
}
// GetMsg 获取状态码对应信息
func GetMsg(code int) string {
msg, ok := MsgFlags[code]
if ok {
return msg
}
return MsgFlags[ERROR]
}③构建通用返回类:
1
2
3
4
5
6
7
// 通用返回类(基础序列化器)
type Response struct {
Status int `json:"status"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
Error string `json:"error"`
}
④DAO层:与数据库操作的方法
1 |
|
新建DAO对象中复用了DB连接,创建对象后,根据
dao对象.db
进行访问
1
2
3
4
5
// NewDBClient 封装db连接,只暴露访问db连接的方法
func NewDBClient(ctx context.Context) *gorm.DB {
db := _db
return db.WithContext(ctx)
}
2、用户登录
1 |
|
1 |
|
1 |
|
token的签发:F:-gin_ mall_mall.go
成功返回的用户json序列器:
1
2
3
4
5
// TokenData 带有token的Data结构
type TokenData struct {
User interface{} `json:"user"`
Token string `json:"token"`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type User struct { // vo ;view object 传给视图的对象
ID uint `json:"id"`
UserName string `json:"user_name"`
NickName string `json:"nickname"`
Type int `json:"type"`
Email string `json:"email"`
Status string `json:"status"`
Avatar string `json:"avatar"`
CreateAt int64 `json:"create_at"`
}
//BuildUser 序列化用户
func BuildUser(user *model.User) User {
return User{
ID: user.ID,
UserName: user.UserName,
NickName: user.NickName,
Email: user.Email,
Status: user.Status,
Avatar: conf.PhotoHost + conf.HttpPort + conf.AvatarPath + user.AvatarURL(),
CreateAt: user.CreatedAt.Unix(),
}
}
1 |
|
3、用户修改
JWT验权(验证用户是否登录)
JWT验权:
1 |
|
middleware.JWT()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// JWT token验证中间件
func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
var code int
var data interface{}
code = 200
token := c.GetHeader("Authorization")
if token == "" {
code = 404
} else {
claims, err := utils.ParseToken(token)
if err != nil {
code = e.ErrorAuthCheckTokenFail
} else if time.Now().Unix() > claims.ExpiresAt {
code = e.ErrorAuthCheckTokenTimeout
}
}
if code != e.SUCCESS {
c.JSON(200, gin.H{
"status": code,
"msg": e.GetMsg(code),
"data": data,
})
c.Abort()
return
}
c.Next()
}
}utils.jwt工具类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var jwtSecret = []byte("SecretKey")
type Claims struct {
ID uint `json:"id"`
Username string `json:"username"`
Authority int `json:"authority"`
jwt.StandardClaims
}
// GenerateToken 签发用户Token
func GenerateToken(id uint, username string, authority int) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(24 * time.Hour)
claims := Claims{
ID: id,
Username: username,
Authority: authority,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: "mall",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret)
return token, err
}
// ParseToken 验证用户token
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
用户修改
1 |
|
1 |
|
1 |
|
4、更换头像
1 |
|
1 |
|
1 |
|
上传文件(JPG)
文件上传:创建一个新的
service.upload.go
(因为还有别的上传操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func UploadAvatarToLocalStatic(file multipart.File, userId uint, username string) (filepath string, err error) {
bId := strconv.Itoa(int(userId)) // int => string
basePath := "." + conf.AvatarPath + "user" + bId + "/"
// 如果路径不存在,则创建路径
if !DirExistOrNot(basePath) {
CreateDir(basePath)
}
avatarPath := basePath + username + ".jpg"
content, err := io.ReadAll(file)
if err != nil {
return "", err // 读取失败
}
err = os.WriteFile(avatarPath, content, 0666)
if err != nil {
return "", err
}
return "user" + bId + "/" + username + ".jpg", err
}
// DirExistOrNot 判断文件是否存在
func DirExistOrNot(fileAddr string) bool {
s, err := os.Stat(fileAddr)
if err != nil {
log.Println(err)
return false
}
return s.IsDir()
}
// CreateDir 创建文件夹
func CreateDir(dirName string) bool {
err := os.MkdirAll(dirName, 755)
if err != nil {
log.Println(err)
return false
}
return true
}
5、绑定邮箱(向用户发送邮件)
配置发送邮件的邮箱
QQ邮箱 => 设置 => 账户 => 开启POP3/SMTP服务
- config中
SmtpEmail
是发送邮箱,也就是用这个邮箱发给用户
1 |
|
在用户绑定邮箱的时候需要重新生成一个token,这个token携带了email信息,在验证email的时候需要验证该token
1
2
3
4
5
6
7
8
9
10
11
12
13
// EmailClaims
type EmailClaims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Password string `json:"password"`
OperationType uint `json:"operation_type"`
jwt.StandardClaims
}
// GenerateEmailToken 签发邮箱验证Token
func GenerateEmailToken(userID, Operation uint, email, password string) (string, error) {
...
}
绑定邮箱请求:(发送邮件进行确认)
1 |
|
1 |
|
1 |
|
关于邮件模板notice的dao和user.dao相似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package dao
type NoticeDao struct {
*gorm.DB
}
func NewNoticeDao(ctx context.Context) *NoticeDao {
return &NoticeDao{NewDBClient(ctx)}
}
// GetNoticeById 通过id获取notice
func (dao *NoticeDao) GetNoticeById(id uint) (notice *model.Notice, err error) {
err = dao.DB.Model(&model.Notice{}).Where("id = ?", id).First(¬ice).Error
return notice, err
}
6、验证邮箱(绑定邮箱、解绑邮箱、修改密码)
在上面绑定邮箱请求后,服务器会向用户邮箱发送邮件进行确认,用户点击生成的链接即触发验证邮箱
1 |
|
1 |
|
1 |
|
绑定邮箱结果:
7、获取用户金额
因为数据库中的钱是加密保存的,所有传请求中要带有加密的key,密钥一定要十六位
key : Ek1+Ep1==Ek2+Ep2
1 |
|
1 |
|
1 |
|
serializer.BuildMoney
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package serializer
type Money struct {
UserID uint `json:"user_id" form:"user_id"`
UserName string `json:"user_name" form:"user_name"`
UserMoney string `json:"user_money" form:"user_money"`
}
func BuildMoney(item *model.User, key string) Money {
utils.Encrypt.SetKey(key)
return Money{
UserID: item.ID,
UserName: item.UserName,
UserMoney: utils.Encrypt.AesDecoding(item.Money),
}
}
二、轮播图
8、获取轮播图
1 |
|
1 |
|
1 |
|
请求成功返回序列化后的结果:
serializer.BuildCarousels
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package serializer
import "example.com/unicorn-acc/model"
type Carousel struct {
ID uint `json:"id"`
ImgPath string `json:"img_path"`
ProductID uint `json:"product_id"`
CreatedAt int64 `json:"created_at"`
}
// BuildCarousel 序列化轮播图
func BuildCarousel(item *model.Carousel) Carousel {
return Carousel{
ID: item.ID,
ImgPath: item.ImgPath,
ProductID: item.ProductID,
CreatedAt: item.CreatedAt.Unix(),
}
}
// BuildCarousels 序列化轮播图列表
func BuildCarousels(items []*model.Carousel) (carousels []Carousel) {
for _, item := range items {
carousel := BuildCarousel(item)
carousels = append(carousels, carousel)
}
return carousels
}BuildListResponse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// DataList 带有总数的Data结构
type DataList struct {
Item interface{} `json:"item"`
Total uint `json:"total"`
}
func BuildListResponse(items interface{}, total uint) Response {
return Response{
Status: 200,
Data: DataList{
Item: items,
Total: total,
},
Msg: "ok",
}
}
1 |
|
三、商品模块
✨9、创建商品
1 |
|
1 |
|
1 |
|
保存商品信息的DAO
1 |
|
保存商品信息图片的DAO
1 |
|
并发保存商品图片
- 使用了
sync.WaitGroup
1 |
|
引入Redis保存点击数
①Redis配置
②编写key
③使用redis
model.product.go中创建两个方法(获取点击数)
1 |
|
10、获取商品列表
1 |
|
1 |
|
1 |
|
1 |
|
11、搜索商品
1 |
|
1 |
|
1 |
|
1 |
|
12、商品展示信息
1 |
|
1 |
|
1 |
|
1 |
|
13、获取图片地址
1 |
|
1 |
|
1 |
|
返回结果的序列化器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type ProductImg struct {
ProductID uint `json:"product_id" form:"product_id"`
ImgPath string `json:"img_path" form:"img_path"`
}
func BuildProductImg(item *model.ProductImg) ProductImg {
return ProductImg{
ProductID: item.ProductID,
ImgPath: conf.PhotoHost + conf.HttpPort + conf.ProductPhotoPath + item.ImgPath,
}
}
func BuildProductImgs(items []*model.ProductImg) (productImgs []ProductImg) {
for _, item := range items {
productimg := BuildProductImg(item)
productImgs = append(productImgs, productimg)
}
return
}
1 |
|
14、获取商品分类
1 |
|
1 |
|
目录列表json序列器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//vo
type Category struct {
ID uint `json:"id"`
CategoryName string `json:"category_name"`
CreateAt int64 `json:"create_at"`
}
func BuildCategory(item *model.Category) Category {
return Category{
ID: item.ID,
CategoryName: item.CategoryName,
CreateAt: item.CreatedAt.Unix(),
}
}
func BuildCategories(items []*model.Category) (categories []Category) {
for _, item := range items {
category := BuildCategory(item)
categories = append(categories, category)
}
return categories
}
1 |
|
四、收藏夹操作(将收藏夹与用户,做的不太好)
15、创建收藏夹
1 |
|
1 |
|
1 |
|
16、展示收藏夹
1 |
|
1 |
|
1 |
|
序列化vo对象
- 在这里根据bossid和商品ID查询信息不太好,应该在favoriteservice中调用userserice和productservice对象然后让他们查询返回id对应的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
type Favorite struct {
UserID uint `json:"user_id"`
ProductID uint `json:"product_id"`
CreatedAt int64 `json:"create_at"`
Name string `json:"name"`
CategoryID uint `json:"category_id"`
Title string `json:"title"`
Info string `json:"info"`
ImgPath string `json:"img_path"`
Price string `json:"price"`
DiscountPrice string `json:"discount_price"`
BossID uint `json:"boss_id"`
Num int `json:"num"`
OnSale bool `json:"on_sale"`
}
//序列化收藏夹
func BuildFavorite(item1 *model.Favorite, item2 *model.Product, item3 *model.User) Favorite {
return Favorite{
UserID: item1.UserID,
ProductID: item1.ProductID,
CreatedAt: item1.CreatedAt.Unix(),
Name: item2.Name,
CategoryID: item2.CategoryID,
Title: item2.Title,
Info: item2.Info,
ImgPath: item2.ImgPath,
Price: item2.Price,
DiscountPrice: item2.DiscountPrice,
BossID: item3.ID,
Num: item2.Num,
OnSale: item2.OnSale,
}
}
// 收藏夹列表
func BuildFavorites(ctx context.Context, items []*model.Favorite) (favorites []Favorite) {
productDao := dao.NewProductDao(ctx)
bossDao := dao.NewUserDao(ctx)
for _, fav := range items {
product, err := productDao.GetProductById(fav.ProductID)
if err != nil {
continue
}
boss, err := bossDao.GetUserById(fav.UserID)
if err != nil {
continue
}
favorite := BuildFavorite(fav, product, boss)
favorites = append(favorites, favorite)
}
return favorites
}
1 |
|
17、删除收藏夹
1 |
|
1 |
|
1 |
|
1 |
|
五、地址模块(简单CRUD)
路由:
1 |
|
Controller层:
1 |
|
Service层:
1 |
|
序列化vo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Address struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Name string `json:"name"`
Phone string `json:"phone"`
Address string `json:"address"`
Seen bool `json:"seen"`
CreateAt int64 `json:"create_at"`
}
//收货地址购物车
func BuildAddress(item *model.Address) Address {
return Address{
ID: item.ID,
UserID: item.UserID,
Name: item.Name,
Phone: item.Phone,
Address: item.Address,
Seen: false,
CreateAt: item.CreatedAt.Unix(),
}
}
//收货地址列表
func BuildAddresses(items []*model.Address) (addresses []Address) {
for _, item := range items {
address := BuildAddress(item)
addresses = append(addresses, address)
}
return addresses
}
DAO层:
1 |
|
六、购物车操作(简单CRUD)
18、创建购物车
1 |
|
1 |
|
1 |
|
购物车的vo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 购物车
type Cart struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
ProductID uint `json:"product_id"`
CreateAt int64 `json:"create_at"`
Num uint `json:"num"`
MaxNum uint `json:"max_num"`
Check bool `json:"check"`
Name string `json:"name"`
ImgPath string `json:"img_path"`
DiscountPrice string `json:"discount_price"`
BossId uint `json:"boss_id"`
BossName string `json:"boss_name"`
Desc string `json:"desc"`
}
func BuildCart(cart *model.Cart, product *model.Product, boss *model.User) Cart {
return Cart{
ID: cart.ID,
UserID: cart.UserID,
ProductID: cart.ProductID,
CreateAt: cart.CreatedAt.Unix(),
Num: cart.Num,
MaxNum: cart.MaxNum,
Check: cart.Check,
Name: product.Name,
ImgPath: conf.PhotoHost + conf.HttpPort + conf.ProductPhotoPath + product.ImgPath,
DiscountPrice: product.DiscountPrice,
BossId: boss.ID,
BossName: boss.UserName,
Desc: product.Info,
}
}
func BuildCarts(items []*model.Cart) (carts []Cart) {
for _, item1 := range items {
product, err := dao.NewProductDao(context.Background()).
GetProductById(item1.ProductID)
if err != nil {
continue
}
boss, err := dao.NewUserDao(context.Background()).
GetUserById(item1.BossID)
if err != nil {
continue
}
cart := BuildCart(item1, product, boss)
carts = append(carts, cart)
}
return carts
}
DAO
1 |
|
19、查看、更新、删除购物车
1 |
|
1 |
|
1 |
|
1 |
|
七、订单操作
20、创建、获取、获取详细信息、删除订单(简单CRUD)
1 |
|
1 |
|
1 |
|
1 |
|
序列化VO对象:
1 |
|
21、订单支付
1 |
|
1 |
|
1 |
|