项目简介
该项目是一个商城项目的后台管理系统,我主要负责后台的pc端商品管理和轮播图管理模块,下面给出项目部署的服务器端地址(到2022年10.5号服务器到期)
项目地址:http://47.92.163.212:9999/
写在最前面
本次项目后端最大的失误就是数据库表的设计有缺陷。数据库表的设计一定要提前考虑好,考虑需要的各种字段,宁可多花一天设计好表,也不要匆匆建表盲目开发,不然后面的开发过程会出现意想不到的bug,或者可能由于表设计的不合理导致项目开发到一半就无法继续进行,本次项目一开始设计表的时候太仓促,没有给需要读写的表设计删除时间,更新时间等字段,导致使用gorm框架操作数据库遇到麻烦,中途不得不重新使用gorm框架重新建表。此外,在分类表中使用上级分类名替代了上级分类id,存储商品信息的时候用分类名字代替了分类id,这种设计会导致查询某一分类下的分类信息变慢,(例如查询家电分类下的所有分类信息select * from category where category.parentname = "家电"
),商品表也是如此,商品应该存储的是所属的某一分类的分类id,设计表的时候存储的是分类名,所以查询某一分类下的商品也是根据分类名查的(查询分类是手机的所有商品信息select * from goods where goods.category ="手机"
),这种sql的效率是很低的,当然也是不合理的。
遇到的难点
三级分类信息的查询
问题描述:印象最深的就是,需要给前端提供一个返回三级嵌套的商品信息的接口。具体要求就是:前端传递一个一级分类的分类名,该接口需要返回一级分类下的所有分类信息。
解决方案:分析返回信息的结构,设计一个拥有相同嵌套结构的结构体大概是下面这个样子
// Data 第一层返回数据
type Data struct {
CategoryID int `json:"categoryId" gorm:"column:id"`
CategoryLevel int `json:"categoryLevel" gorm:"column:cur_category"`
CategoryName string `json:"categoryName" gorm:"column:class_name"`
SecondLevelCategoryVOS []SecondLevelCategory `json:"secondLevelCategoryVOS" gorm:"-"`
}
// SecondLevelCategory 第二层返回数据
type SecondLevelCategory struct {
CategoryID int `json:"categoryId" gorm:"column:id"`
CategoryLevel int `json:"categoryLevel" gorm:"column:cur_category"`
CategoryName string `json:"categoryName" gorm:"column:class_name"`
ParentID int `json:"parentId" `
ThirdLevelCategoryVOS []ThirdLevelCategory `json:"thirdLevelCategoryVOS" gorm:"-"`
}
// ThirdLevelCategory 第三层返回数据
type ThirdLevelCategory struct {
CategoryID int `json:"categoryId" gorm:"column:id"`
CategoryLevel int `json:"categoryLevel" gorm:"column:cur_category"`
CategoryName string `json:"categoryName" gorm:"column:class_name"`
}
可以看出最外层的是结构体是一级分类信息,一级分类信息中包含了一个存储所有二级类目信息的结构体切片,该结构体有一个包含了所有三级类目信息的切片。
所以写出的代码大概就是下面这个样子,简单描述一下思路就是:
- 根据一级分类名查询其下的所有二级分类名,并放入一个切片secondLevelName中
- 遍历secondLevelName,根据每一个二级分类名查询对应的三级分类名,放入切片ThirdLevelName
- 遍历ThirdLevelName,根据每个三级类目名查询对应的三级类目信息,并将三级类目信息放入ThirdLevelCategory切片中,这时填上id类名和当前分类id就完成了一个二级类目信息的查询
- 每完成一个二级类目信息的查询将其放入SecondLevelCategory切片中,遍历完所有二级类目切片就完成了所有二级类目信息的查询,填充上另外三个字段,ID,ClassName,CurCategory也就完成了一级类目所有信息的查询。
代码:
//QueryClassInfo 根据本级分类名和要查询的下级分类级别获取该分类级别的所有分类类信息
func QueryClassInfo(className string, level int) (model.ClassificationInfo, error) {
var classInfo []string
err := GOLAB_DB.Debug().Model(&model.Category{}).Select("class_name").Where("cur_category=? AND pre_name=?", level, className).Find(&classInfo).Error
if err != nil {
zap.L().Error("查询类别信息出错", zap.Error(err))
return model.ClassificationInfo{}, err
}
return model.ClassificationInfo{ClassificationName: classInfo}, nil
}
// GetFirstLevelInfoByName 获取一级分类下所有信息
func GetFirstLevelInfoByName(name string) (model.Data, error) {
var res model.Data
err := GOLAB_DB.Debug().Model(&model.Category{}).Where("class_name=?", name).Select("id", "cur_category", "class_name").First(&res).Error
if err != nil {
zap.L().Info("查找id等基本信息时出错", zap.Error(err))
return model.Data{}, err
}
//查询出二级分类信息
var secCategory model.ClassificationInfo
secCategory, err = QueryClassInfo(name, 2)
fmt.Println("二级分类信息第一次查询:", secCategory)
if err != nil {
zap.L().Info("查找二级分类名字数组时出错", zap.Error(err))
return model.Data{}, err
}
res.SecondLevelCategoryVOS, err = GetSecLevelInfoByNames(secCategory.ClassificationName)
if err != nil {
zap.L().Info("查找二级分类具体信息时出错", zap.Error(err))
return model.Data{}, err
}
//var thirdCategory model.ClassificationInfo
return res, nil
}
// GetSecLevelInfoByNames 通过分类名获取二级分类具体信息
func GetSecLevelInfoByNames(secCategory []string) ([]model.SecondLevelCategory, error) {
fmt.Println("GetSecLevelInfoByNames里的分类名", secCategory)
length := len(secCategory)
var secInfos []model.SecondLevelCategory
//secInfos = secCategory
fmt.Println("二级分类名:", secCategory)
for i := 0; i < length; i++ {
//每一条二级信息记录
var oneSecInfo model.SecondLevelCategory
var temp struct {
CategoryID int `json:"categoryId" gorm:"column:id"`
CategoryLevel int `json:"categoryLevel" gorm:"column:cur_category"`
CategoryName string `json:"categoryName" gorm:"column:class_name"`
}
//oneSecInfo = secInfos[i]
err := GOLAB_DB.Model(&model.Category{}).Where("class_name=?", secCategory[i]).Select("id", "cur_category", "class_name").Find(&temp).Error
if err != nil {
zap.L().Info("查找id等基本信息时出错", zap.Error(err))
secInfos = append(secInfos, model.SecondLevelCategory{})
continue
}
fmt.Println("285行二级分类的基本信息", temp)
fmt.Println("第", i, "次调用GetCategoryIdByName前的分类名:", temp.CategoryName)
//把查询到的二级基本信息插入一条二级信息结构体里
oneSecInfo.CategoryID = temp.CategoryID
oneSecInfo.CategoryName = temp.CategoryName
oneSecInfo.CategoryLevel = temp.CategoryLevel
//通过二级分类名获取parentID
oneSecInfo.ParentID, err = GetCategoryIdByName(oneSecInfo.CategoryName)
fmt.Println("293行一条二级分类信息", oneSecInfo)
if err != nil {
zap.L().Info("查找ParentID时出错", zap.Error(err))
secInfos = append(secInfos, model.SecondLevelCategory{})
continue
}
//查询二级分类下所有三级分类名 ThirdLevelCategoryVOS []ThirdLevelCategory
thirdCategoryNames, err := QueryClassInfo(secCategory[i], 3)
if err != nil {
zap.L().Info("查找三级分类名信息时出错", zap.Error(err))
secInfos = append(secInfos, model.SecondLevelCategory{})
return nil, err
}
//根据每一个三级分类名查询对应的三级分类信息并添加到二级分类信息中
for _, v := range thirdCategoryNames.ClassificationName {
thirdCategoryInfo, err := GetThirdLevelInfoByName(v)
if err != nil {
zap.L().Info("根据分类名查找三级分类信息时出错", zap.Error(err))
return nil, err
}
oneSecInfo.ThirdLevelCategoryVOS = append(oneSecInfo.ThirdLevelCategoryVOS, thirdCategoryInfo)
}
secInfos = append(secInfos, oneSecInfo)
}
return secInfos, nil
}
// GetThirdLevelInfoByName 通过分类名获取具体三级分类信息
func GetThirdLevelInfoByName(thirdCategoryName string) (model.ThirdLevelCategory, error) {
var oneThirdInfo model.ThirdLevelCategory
err := GOLAB_DB.Debug().Model(&model.Category{}).Where("class_name=?", thirdCategoryName).Select("id", "cur_category", "class_name").First(&oneThirdInfo).Error
if err != nil {
zap.L().Info("查找id等基本信息时出错", zap.Error(err))
return model.ThirdLevelCategory{}, err
}
return oneThirdInfo, nil
}
这段代码逻辑比较复杂,实现过程也很繁琐,并且又调用了一些其它处理函数非常不方便阅读。但好在最终成功返回了需要的信息。以下是查询的信息
{
"code": 1000,
"msg": "success",
"data": {
"categoryId": 2,
"categoryLevel": 1,
"categoryName": "男女穿搭",
"secondLevelCategoryVOS": [
{
"categoryId": 22,
"categoryLevel": 2,
"categoryName": "男装",
"parentId": 22,
"thirdLevelCategoryVOS": [
{
"categoryId": 29,
"categoryLevel": 3,
"categoryName": "皮孩"
},
{
"categoryId": 46,
"categoryLevel": 3,
"categoryName": "男装衬衫"
},
{
"categoryId": 47,
"categoryLevel": 3,
"categoryName": "男装裤子"
},
{
"categoryId": 64,
"categoryLevel": 3,
"categoryName": "皮带"
},
{
"categoryId": 65,
"categoryLevel": 3,
"categoryName": "男士短裤"
},
{
"categoryId": 66,
"categoryLevel": 3,
"categoryName": "男士长裤"
}
]
},
{
"categoryId": 23,
"categoryLevel": 2,
"categoryName": "女装",
"parentId": 23,
"thirdLevelCategoryVOS": [
{
"categoryId": 30,
"categoryLevel": 3,
"categoryName": "女装上衣"
},
{
"categoryId": 48,
"categoryLevel": 3,
"categoryName": "女装裙子"
},
{
"categoryId": 70,
"categoryLevel": 3,
"categoryName": "高跟鞋"
},
{
"categoryId": 71,
"categoryLevel": 3,
"categoryName": "绣花鞋"
}
]
},
{
"categoryId": 34,
"categoryLevel": 2,
"categoryName": "童装",
"parentId": 34,
"thirdLevelCategoryVOS": [
{
"categoryId": 45,
"categoryLevel": 3,
"categoryName": "袜子"
},
{
"categoryId": 67,
"categoryLevel": 3,
"categoryName": "小帽子"
},
{
"categoryId": 68,
"categoryLevel": 3,
"categoryName": "运动鞋"
},
{
"categoryId": 69,
"categoryLevel": 3,
"categoryName": "虎头鞋"
}
]
}
]
}
}
使用gin框架部署静态资源
这个内容比较多,参考我的另一篇博客https://juejin.cn/post/7143961105939824676
转化RFC3339时间格式
下面是项目中的代码片段,基本功能就是:把查询到的所有图片信息的时间格式由RFC3339转化为标准格式
var res []model.OneImgInfo//图片信息的切片
for k, v := range res {
rfcTime := v.CreatedAt//获取RFC时间
t, err := time.Parse(time.RFC3339, rfcTime)//解析RFC时间
if err != nil {
zap.L().Info("解析图片rfc时间出错")
return model.ImgFirstPage{}, err
}
stdTime := t.Local().Format("2006-01-02 15:04:05")//转化为标准时间格式
fmt.Println(stdTime)
res[k].CreatedAt = stdTime
}
总结
项目开始要设计好表,不要以varchar类型的字段作为唯一标识。还有就是听说最好手写sql语句,这一点是大佬给我的建议,因为这样可以避免sql出错难以排查。