> 文章列表 > [golang gin框架] 20.Gin 商城项目-商品模块功能

[golang gin框架] 20.Gin 商城项目-商品模块功能

[golang gin框架] 20.Gin 商城项目-商品模块功能

一.商品模块数据表ER图关系分析

商品模块数据表相关功能关系见:[golang gin框架] 16.Gin 商城项目-商品模块数据表ER图关系分析

二.商品相关界面展示

商品列表

该商品列表有如下功能
1.增加商品按钮:跳转到增加商品页面
2.搜索功能:输入商品名称,点击搜索
3.修改商品字段(上架,精品,新平,热销)状态
4.修改排序数字
5.修改操作:点击修改跳转到修改页面
6.删除操作
7.分页操作

[golang gin框架] 20.Gin 商城项目-商品模块功能

添加商品

通用信息

功能如下:
1.输入商品相关信息
2.选择商品所属分类(从商品分类表goods_cate中选择)
3.上传商品logo图片
4.选择商品状态(单选)
5.选择商品(新品,热销,精品:复选框)

[golang gin框架] 20.Gin 商城项目-商品模块功能
[golang gin框架] 20.Gin 商城项目-商品模块功能

详细描述

功能:
1.引入富文本框,输入商品内容
2.上传商品相关图片到富文本
注意:这里图片上传后台要判断是否上传到云服务器

[golang gin框架] 20.Gin 商城项目-商品模块功能

商品属性

功能:
1.选择商品颜色(多选):从商品颜色表good_color中选择
2.输入商品其他相关属性

[golang gin框架] 20.Gin 商城项目-商品模块功能

规格与包装

功能:
1.选择商品类型:从商品类型表goods_type中选择
2.根据选择的商品类型,展示不同的商品类型属性:从商品类型属性表goods_type_attribute中选择
3.填写商品类型属性

[golang gin框架] 20.Gin 商城项目-商品模块功能

商品相册

功能:
1.选择商品相册,上传商品图片
注意:这里商品上传图片时,需要一个批量上传图片的插件,以及上传到云服务器还是本地服务器的判断

[golang gin框架] 20.Gin 商城项目-商品模块功能
[golang gin框架] 20.Gin 商城项目-商品模块功能
[golang gin框架] 20.Gin 商城项目-商品模块功能

总结:
1.用户点击添加商品按钮,进入添加商品页面
2.根据商品相关功能,布局商品页面
3.进入到通用信息页面,填写通用信息(一些基本的信息,以及选择商品分类,上传图片,单选,复选信息)
4.切换到详情描述,增加商品内容:引入富文本框,以及上传图片内容
5.切换到商品属性,填写相关属性以及选择颜色(复选框)
6.切换到规格与包装,选择商品类型,以及展示对应的商品类型属性,根据实际展示填写
7.切换到商品相册,选择上传图片,并上传
8.以上操作完成后,提交

修改商品

修改操作步骤和上面添加商品操作类似,只不过需要展示商品已有数据,然后进行处理,这里不一一介绍

删除商品

[golang gin框架] 20.Gin 商城项目-商品模块功能

修改属性状态,排序等

点击要修改的属性,进行状态修改
双击排序,生成焦点,修改数字,失去焦点,排序完成
该功能见:[golang gin框架] 15.Gin 商城项目-封装上传图片方法,轮播图的增删改查以及异步修改状态,数量

[golang gin框架] 20.Gin 商城项目-商品模块功能

三.代码展示

商品相关数据表见 [golang gin框架] 16.Gin 商城项目-商品模块数据表ER图关系分析

  1. tools.go工具类

项目所用到的工具类在models/tool.go下,代码如下:

package modelsimport ("context""crypto/md5""errors""fmt""github.com/aliyun/aliyun-oss-go-sdk/oss""github.com/tencentyun/cos-go-sdk-v5""github.com/gin-gonic/gin""gopkg.in/ini.v1""html/template""io""mime/multipart""net/http""net/url""os""path""reflect""strconv""strings""time"//引入模块的时候前面加个.表示可以直接使用模块里面的方法,无需加模块名称. "github.com/hunterhug/go_image"
)//时间戳转换成日期函数
func UnixToTime(timestamp int) string {t := time.Unix(int64(timestamp), 0)return t.Format("2006-01-02 15:04:05")
}//日期转换成时间戳
func DateToUnix(str string) int64 {template := "2006-01-02 15:04:05"t, err := time.ParseInLocation(template, str, time.Local)if err != nil {return 0}return t.Unix()
}//获取当前时间戳(毫秒)
func GetUnix() int64 {return time.Now().Unix()
}//获取当前时间戳(纳秒)
func GetUnixNano() int64 {return time.Now().UnixNano()
}//获取当前日期
func GetDate() string {template := "2006-01-02 15:04:05"return time.Now().Format(template)
}//获取年月日
func GetDay() string {template := "20060102"return time.Now().Format(template)
}//md5加密
func Md5(str string) string {//data := []byte(str)//return fmt.Sprintf("%x\\n", md5.Sum(data))h := md5.New()io.WriteString(h, str)return fmt.Sprintf("%x", h.Sum(nil))
}//把字符串解析成html
func Str2Html(str string) template.HTML {return template.HTML(str)
}//表示把string字符串转换成int
func Int(str string) (int, error) {n, err := strconv.Atoi(str)return n, err
}//表示把string字符串转换成Float
func Float(str string) (float64, error) {n, err := strconv.ParseFloat(str, 64)return n, err
}//表示把int转换成string字符串
func String(n int) string {str := strconv.Itoa(n)return str
}//通过列获取系统设置里面的值,columnName就是结构体的属性名称
func GetSettingFromColumn(columnName string) string {setting := Setting{}DB.First(&setting)//反射来获取v := reflect.ValueOf(setting)val := v.FieldByName(columnName).String()return val
}//获取oss的状态:是否开启上传到云服务器
func GetOssStatus() int  {cfg, err := ini.Load("./conf/app.ini")if err != nil {fmt.Printf("Fail to read file: %v", err)os.Exit(1)}ossStatus := cfg.Section("oss").Key("status").String()status, _:= Int(ossStatus)return status
}//格式化图片:判断是否开启了oss
func FormatImg(str string) string  {if GetOssStatus() == 1 {  // 开启了oss,则获取oss上的图片return GetSettingFromColumn("OssDomain") + str} else {return "/" + str  //获取本地图片}
}//图片上传方法
func UploadImg(c *gin.Context, picName string) (string, error) {//判断是否开启ossif GetOssStatus() == 1 {//return OssUploadImg(c, picName)return CosUploadImg(c, picName)} else {return LocalUploadImg(c, picName)}
}//图片上传:上传到cos
func CosUploadImg(c *gin.Context, picName string) (string, error) {//1.获取上传文件file, err := c.FormFile(picName)//判断上传文件上否存在if err != nil { //说明上传文件不存在return "", nil}//2.获取后缀名,判断后缀是否正确: .jpg,.png,.gif,.jpegextName := path.Ext(file.Filename)//设置后缀mapallowExtMap := map[string]bool{".jpg":  true,".png":  true,".gif":  true,".jpeg": true,}//判断后缀是否合法if _, ok := allowExtMap[extName]; !ok {return "", errors.New("文件后缀名不合法")}//3.创建图片保存目录 ./static/upload/20230203//获取日期day := GetDay()//拼接目录, 上传时,cos会自动创建对应文件目录dir := "./static/upload/" + day//4.生成文件名称和文件保存目录: models.GetUnixNano() 获取时间戳(int64) 纳秒:防止速度过快而上传图片失败; strconv.FormatInt() 把时间戳(int64)转换成字符串filename := strconv.FormatInt(GetUnixNano(), 10) + extName//5.执行上传dst := path.Join(dir, filename)//上传文件到指定目录_, err1 := CosUpload(file, dst)if err1 != nil {return "", err1}fmt.Println(dst)return dst, nil
}//图片上传:上传到OSS
func OssUploadImg(c *gin.Context, picName string) (string, error) {//1.获取上传文件file, err := c.FormFile(picName)//判断上传文件上否存在if err != nil { //说明上传文件不存在return "", nil}//2.获取后缀名,判断后缀是否正确: .jpg,.png,.gif,.jpegextName := path.Ext(file.Filename)//设置后缀mapallowExtMap := map[string]bool{".jpg":  true,".png":  true,".gif":  true,".jpeg": true,}//判断后缀是否合法if _, ok := allowExtMap[extName]; !ok {return "", errors.New("文件后缀名不合法")}//3.创建图片保存目录 ./static/upload/20230203//获取日期day := GetDay()//拼接目录, 上传时,oss会自动创建对应文件目录dir := "./static/upload/" + day//4.生成文件名称和文件保存目录: models.GetUnixNano() 获取时间戳(int64) 纳秒:防止速度过快而上传图片失败; strconv.FormatInt() 把时间戳(int64)转换成字符串filename := strconv.FormatInt(GetUnixNano(), 10) + extName//5.执行上传dst := path.Join(dir, filename)//上传文件到指定目录OssUpload(file, dst)return dst, nil
}//封装oss上传图片方法
func OssUpload(file *multipart.FileHeader, dst string) (string, error) {// 1.创建OSSClient实例。client, err := oss.New("oss-cn-beijing.aliyuncs.com", "xxx", "xxxKEe9")if err != nil {return "", err}// 2.获取存储空间。bucket, err := client.Bucket("beego")if err != nil {return "", err}// 3.读取本地文件: file.Open()返回的File最终的类型为:io.Reader, 这样下面的上传就可以用了src, err := file.Open()if err != nil {return "", err}defer src.Close()// 上传文件流 bucket.PutObjec方法第二个参数类型为io.Reader, src的类型为err = bucket.PutObject(dst, src)if err != nil {return "", err}return dst, nil
}//封装cos上传图片方法
func CosUpload(file *multipart.FileHeader, dst string) (string, error) {// 存储桶名称,由 bucketname-appid 组成,appid 必须填入,可以在 COS 控制台查看存储桶名称。 https://console.cloud.tencent.com/cos5/bucket// 替换为用户的 region,存储桶 region 可以在 COS 控制台“存储桶概览”查看 https://console.cloud.tencent.com/ ,关于地域的详情见 https://cloud.tencent.com/document/product/436/6224 。// 1.创建CosClient实例。//获取cos相关配置cfg, err := ini.Load("./conf/app.ini")if err != nil {fmt.Printf("Fail to read file: %v", err)os.Exit(1)}rawURL := cfg.Section("cos").Key("rawURL").String()secretID := cfg.Section("cos").Key("secretID").String()scretKey := cfg.Section("cos").Key("scretKey").String()u, _ := url.Parse(rawURL)b := &cos.BaseURL{BucketURL: u}client := cos.NewClient(b, &http.Client{Transport: &cos.AuthorizationTransport{// 通过环境变量获取密钥// 环境变量 SECRETID 表示用户的 SecretId,//登录访问管理控制台查看密钥,https://console.cloud.tencent.com/cam/capiSecretID: secretID,  // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140// 环境变量 SECRETKEY 表示用户的 SecretKey,//登录访问管理控制台查看密钥,https://console.cloud.tencent.com/cam/capiSecretKey: scretKey,  // 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140},})//对象在存储桶中的唯一标识key := dst//通过文件流上传对象// 3.读取本地文件: file.Open()返回的File最终的类型为:io.Reader, 这样下面的上传就可以用了fd, errOpen := file.Open()if errOpen != nil {return "", errOpen}defer fd.Close()_, err = client.Object.Put(context.Background(), key, fd, nil)if err != nil {return "", err}return dst, nil
}//图片上传:上传到本地
func LocalUploadImg(c *gin.Context, picName string) (string, error) {//1.获取上传文件file, err := c.FormFile(picName)//判断上传文件上否存在if err != nil { //说明上传文件不存在return "", nil}//2.获取后缀名,判断后缀是否正确: .jpg,.png,.gif,.jpegextName := path.Ext(file.Filename)//设置后缀mapallowExtMap := map[string]bool{".jpg":  true,".png":  true,".gif":  true,".jpeg": true,}//判断后缀是否合法if _, ok := allowExtMap[extName]; !ok {return "", errors.New("文件后缀名不合法")}//3.创建图片保存目录 ./static/upload/20230203//获取日期day := GetDay()//拼接目录dir := "./static/upload/" + day//创建目录:MkdirAll 目录不存在,会一次性创建多层err = os.MkdirAll(dir, 0666)if err != nil {return "", err}//4.生成文件名称和文件保存目录: models.GetUnixNano() 获取时间戳(int64) 纳秒:防止速度过快而上传图片失败; strconv.FormatInt() 把时间戳(int64)转换成字符串filename := strconv.FormatInt(GetUnixNano(), 10) + extName//5.执行上传dst := path.Join(dir, filename)//上传文件到指定目录c.SaveUploadedFile(file, dst)return dst, nil
}//生成商品缩略图
func ResizeGoodsImage(filename string) {//获取文件后缀名extname := path.Ext(filename)//获取缩略图尺寸thumbnailSizeSlice := strings.Split(GetSettingFromColumn("ThumbnailSize"), ",")//static/upload/tao_400.png//static/upload/tao_400.png_100x100.png//遍历尺寸,生成缩略图for i := 0; i < len(thumbnailSizeSlice); i++ {savepath := filename + "_" + thumbnailSizeSlice[i] + "x" + thumbnailSizeSlice[i] + extnamew, _ := Int(thumbnailSizeSlice[i])//调用github.com/hunterhug/go_image中的方法ThumbnailF2F(),生成缩略图err := ThumbnailF2F(filename, savepath, w, w)if err != nil {fmt.Println(err) //写个日志模块  处理日志}}
}
  1. 配置ini

项目所用到的app.ini配置在conf/app.ini文件下

app_name        = app测试
# 错误级别: DEBUG,INFO,WARNING,ERROR,FATAL
log_level       = DEBUG
app_mode        = production
# 管理后台,特殊的权限排除地址
excludeAuthPath = /,/welcome,/loginOut[mysql]
ip       = 127.0.0.1
port     = 3306
user     = root
password = 123456
database = ginshop[redis]
ip       = 127.0.0.1
port     = 9376
database = 1# 是否开启oss, 1 开启, 0 关闭, oss的配置需要在后台管理系统中配置
[oss]
status = 1# cos 文件存储密钥相关
[cos]
rawURL   = http://xxx.cos.ap-beijing.myqcloud.com
secretID = AKxxxXCaxxx
scretKey = 2xxxAsJTxxx6qIxxx
bucket   = xxx-xxx-13xxx8
region   = ap-beijing
  1. main.go

package mainimport ("fmt""github.com/gin-contrib/sessions"_ "github.com/gin-contrib/sessions/cookie""github.com/gin-contrib/sessions/redis""github.com/gin-gonic/gin""gopkg.in/ini.v1""goshop/models""goshop/routers""html/template""os""path/filepath""strings"
)func main() {//初始化路由,会设置默认中间件:engine.Use(Logger(), Recovery()),可以使用gin.New()来设置路由r := gin.Default()//初始化基于redis的存储引擎: 需要启动redis服务,不然会报错//参数说明://自第1个参数-redis最大的空闲连接数//第2个参数-数通信协议tcp或者udp//第3个参数-redis地址,格式,host:port 第4个参数-redis密码//第5个参数-session加密密钥store,_:=redis.NewStore(10,"tcp","localhost:6379","",[]byte("secret"))r.Use(sessions.Sessions("mysession",store))//自定义模板函数,必须在r.LoadHTMLGlob前面(只调用,不执行, 可以在html 中使用)r.SetFuncMap(template.FuncMap{"UnixToTime": models.UnixToTime, //注册模板函数"Str2Html": models.Str2Html, // 把字符串解析成html"FormatImg": models.FormatImg, //格式化图片:判断是否开启了oss})//加载templates中所有模板文件, 使用不同目录下名称相同的模板,注意:一定要放在配置路由之前才得行//如果模板在多级目录里面的话需要这样配置 r.LoadHTMLGlob("templates///*") / 表示目录//LoadHTMLGlob只能加载同一层级的文件//比如说使用router.LoadHTMLFile("/templates//*"),就只能加载/templates/admin/或者/templates/order/下面的文件//解决办法就是通过filepath.Walk来搜索/templates下的以.html结尾的文件,把这些html文件都加载一个数组中,然后用LoadHTMLFiles加载//r.LoadHTMLGlob("templates///*")var files []stringfilepath.Walk("./templates", func(path string, info os.FileInfo, err error) error {if strings.HasSuffix(path, ".html") {files = append(files, path)}return nil})r.LoadHTMLFiles(files...)//配置静态web目录 第一个参数表示路由,第二个参数表示映射的目录r.Static("/static", "./static")//分组路由文件routers.AdminRoutersInit(r)routers.ApiRoutersInit(r)routers.FrontendRoutersInit(r)r.Run() // 启动一个web服务
}

4.创建商品相关模型

在models下创建商品相关模型,汇总如下:
商品分类模型 GoodsCate.go
商品类型模型GoodType.go
商品类型属性模型 GoodsTypeAttribute.go
商品颜色模型 GoodsColor.go
商品模型 Goods.go
商品图片模型GoodsImage.go
商品属性模型GoodsAttr.go

package models//商品分类type GoodsCate struct {Id             intTitle          string  // 标题CateImg        string  // 分类图片Link           string  // 跳转地址Template       string  // 加载的模板: 为空的话加载默认模板, 不为空的话加载自定义模板Pid            int        // 上级id: 为0的话则是顶级分类SubTitle       string    // SEO标题Keywords       string    // SEO关键字Description    string    // SEO描述Sort           int    // 排序Status         int    // 状态: 1 显示, 0 隐藏AddTime        int    // 添加时间GoodsCateItems []GoodsCate `gorm:"foreignKey:pid;references:Id"` // 关联自身,下级分类
}func (GoodsCate) TableName() string {return "goods_cate"
}
package models//商品类型type GoodsType struct {Id          intTitle       string  // 类型名称Description string  // 介绍Status      int  // 状态AddTime     int  // 添加时间
}func (GoodsType) TableName() string {return "goods_type"
}
package models// 商品类型属性设置type GoodsTypeAttribute struct {Id        int `json:"id"`  // HTML页面使用名称CateId    int `json:"cate_id"`   //商品类型id:商品类型表goods_type.idTitle     string `json:"title"`   // 属性名称AttrType  int `json:"attr_type"`   //属性录入方式: 1 单行文本框, 2 多行文本框, 3 从下面列表中选择(一行代表一个可选值)AttrValue string `json:"attr_value"`   //可选值列表Status    int `json:"status"`   // 状态Sort      int `json:"sort"`   //排序AddTime   int `json:"add_time"`   //增加时间
}func (GoodsTypeAttribute) TableName() string {return "goods_type_attribute"
}
package models//商品颜色type GoodsColor struct {Id         intColorName  string // 颜色名称ColorValue string // 颜色值Status     int    // 状态Checked      bool   `gorm:"-"` //忽略该字段
}func (GoodsColor) TableName() string {return "goods_color"
}
package models//商品表type Goods struct {Id            intTitle         string    //商品标题SubTitle      string    //附属标题GoodsSn       string    //商品编号CateId        int    //商品分类id: goods_cate.idClickCount    int    //商品点击数量GoodsNumber   int    //商品库存Price         float64    //价格MarketPrice   float64    //商品市场价(原价)RelationGoods string    //关联商品id,如: 1, 23,55 ,商品id以逗号隔开GoodsAttr     string    //商品更多属性GoodsVersion  string    //商品版本GoodsImg      string    //图片GoodsGift     string    //商品赠品GoodsFitting  string    //商品配件GoodsColor    string    //颜色GoodsKeywords string    //SEO关键字GoodsDesc     string    //SEO商品描述GoodsContent       string    //商品详情IsDelete      int    //是否删除IsHot         int    //是否热销IsBest        int    //是否精品IsNew         int    //是否新品GoodsTypeId   int    //商品类型id,关联GoodsType.IdSort          int    //排序Status        int    //状态AddTime       int    //添加时间
}func (Goods) TableName() string {return "goods"
}
package models//商品图片type GoodsImage struct {Id      intGoodsId int        //商品idImgUrl  string    //图片保存路径:一般只会保存类似于/static/upload/20230313/xxx.png这种格式ColorId int        //颜色idSort    intAddTime intStatus  int
}func (GoodsImage) TableName() string {return "goods_image"
}
package models//商品属性type GoodsAttr struct {Id              intGoodsId         int        //商品idAttributeCateId int        //商品类型id,关联GoodsType.IdAttributeId     int        //商品类型属性id,关联GoodsTypeAttribute.IdAttributeTitle  string    //类型属性标题AttributeType   int        //类型属性录入方式:GoodsTypeAttribute.AttrTypeAttributeValue  string  //类型属性值Sort            intAddTime         intStatus          int
}func (GoodsAttr) TableName() string {return "goods_attr"
}

5.创建商品控制器

在controllers下创建GoodsController.go控制器,该控制器中的方法功能详解:
(1).Index方法:
商品列表展示,分页查询,搜索条件判断查询,
(2).Add方法:
进入增加商品页面,提供商品分类,商品颜色,商品类型数据,以供选择
(3).DoAdd方法:
商品信息表单提交功能,获取表单提交过来的数据,并进行判断是否合法,商品颜色信息处理(把颜色转换成字符串),上传图片,生成缩略图,增加图库信息,增加规格包装
(4).Edit方法
编辑商品,获取要修改的商品信息,以及商品分类数据,商品颜色的转换(把商品颜色字符串转换成切片数组),商品颜色列表,商品图库信息,商品类型,规格信息(拼接规格信息表单html),以供商品编辑页面使用
(5).DoEdit方法
修改商品信息表单提交,获取表单提交过来的数据,并进行判断是否合法,上传图片,生成缩略图,增加图库信息,修改规格包装
(6).GoodsTypeAttribute方法
获取商品类型对应的属性,用于改变商品类型时,ajax获取对应类型的属性数据
(7).EditorImageUpload方法
富文本编辑器上传图片方法,当在富文本上传图片时,使用该方法
(8).GoodsImageUpload方法
商品上传图片方法,当上传商品logo,图集时,使用该方法
(9).ChangeGoodsImageColor方法
修改商品图库关联的颜色,当修改商品图库图片时,可以设置图片颜色
(10).RemoveGoodsImage方法
删除图库,在修改图片时,可以删除图片(数据库保存的图片信息,以及服务器上的图片)
(11).Delete方法
删除商品

package adminimport ("fmt""github.com/gin-gonic/gin""goshop/models""math""net/http""os""strings""sync"
)var wg sync.WaitGroup //可以实现主线程等待协程执行完毕type GoodsController struct {BaseController
}func (con GoodsController) Index(c *gin.Context) {//分页查询page, _ := models.Int(c.Query("page")) // 获取分页,当分页page数据格式不正确时, page == 0if page == 0 {page = 1}//每页查询的数量pageSize := 2//定义商品结构体goodsList := []models.Goods{}//条件where := "is_delete=0"//关键字查询keyword := c.Query("keyword")if len(keyword) > 0 {where += " and title like \\"%" + keyword + "%\\""//也可以使用下面的方式//where += ` and title like "%` + keyword +`%"`}//分页查询models.DB.Where(where).Offset((page - 1) * pageSize).Limit(pageSize).Find(&goodsList)//获取总数量var count int64models.DB.Table("goods").Where(where).Count(&count)//计算总页数:math.Ceil()向上取整,注意float64类型totalPages := math.Ceil(float64(count) / float64(pageSize))//判断最后一页有没有数据,如果没有则跳转到第一页if len(goodsList) > 0 {c.HTML(http.StatusOK, "admin/goods/index.html", gin.H{"goodsList":  goodsList,"totalPages": totalPages,"page":       page,"keyword":    keyword,})return}//最后一页没有数据,判断页码数是否等于1,如果不等于,则重定向到列表页if page != 1 {c.Redirect(http.StatusFound, "/admin/goods")return}c.HTML(http.StatusOK, "admin/goods/index.html", gin.H{"goodsList":  goodsList,"totalPages": totalPages,"page":       page,"keyword":    keyword,})
}func (con GoodsController) Add(c *gin.Context) {//获取商品分类goodsCateList := []models.GoodsCate{}models.DB.Where("pid = 0").Preload("GoodsCateItems").Find(&goodsCateList)//获取商品颜色goodsColorList := []models.GoodsColor{}models.DB.Where("status = 1").Find(&goodsColorList)//获取商品类型goodsTypeList := []models.GoodsType{}models.DB.Where("status = 1").Find(&goodsTypeList)c.HTML(http.StatusOK, "admin/goods/add.html", gin.H{"goodsCateList":  goodsCateList,"goodsColorList": goodsColorList,"goodsTypeList":  goodsTypeList,})
}//增加商品信息表单提交
func (con GoodsController) DoAdd(c *gin.Context) {//获取表单提交过来的数据,并进行判断是否合法title := c.PostForm("title")subTitle := c.PostForm("sub_title")goodsSn := c.PostForm("goods_sn")cateId, _ := models.Int(c.PostForm("cate_id"))goodsNumber, _ := models.Int(c.PostForm("goods_number"))//价格,注意小数点marketPrice, _ := models.Float(c.PostForm("market_price"))price, _ := models.Float(c.PostForm("price"))relationGoods := c.PostForm("relation_goods")goodsAttr := c.PostForm("goods_attr")goodsVersion := c.PostForm("goods_version")goodsGift := c.PostForm("goods_gift")goodsFitting := c.PostForm("goods_fitting")//颜色:获取的是切片goodsColorArr := c.PostFormArray("goods_color")goodsKeywords := c.PostForm("goods_keywords")goodsDesc := c.PostForm("goods_desc")goodsContent := c.PostForm("goods_content")isDelete, _ := models.Int(c.PostForm("is_delete"))isHot, _ := models.Int(c.PostForm("is_hot"))isBest, _ := models.Int(c.PostForm("is_best"))isNew, _ := models.Int(c.PostForm("is_new"))goodsTypeId, _ := models.Int(c.PostForm("goods_type_id"))sort, _ := models.Int(c.PostForm("sort"))status, _ := models.Int(c.PostForm("status"))addTime := int(models.GetUnix())//获取颜色信息,把颜色转换成字符串goodsColorStr := strings.Join(goodsColorArr, ",")//上传图片,生成缩略图(调用tools.go 工具中的方法)goodsImg, _ := models.UploadImg(c, "goods_img")if len(goodsImg) > 0 {//判断本地图片才需要处理缩略图(调用tools.go 工具中的方法)if models.GetOssStatus() != 1 {//开启协程wg.Add(1)go func() {models.ResizeGoodsImage(goodsImg)wg.Done()}()}}//增加商品数据//实例化商品结构体goods := models.Goods{Title:         title,SubTitle:      subTitle,GoodsSn:       goodsSn,CateId:        cateId,ClickCount:    100,GoodsNumber:   goodsNumber,MarketPrice:   marketPrice,Price:         price,RelationGoods: relationGoods,GoodsAttr:     goodsAttr,GoodsVersion:  goodsVersion,GoodsGift:     goodsGift,GoodsFitting:  goodsFitting,GoodsKeywords: goodsKeywords,GoodsDesc:     goodsDesc,GoodsContent:  goodsContent,IsDelete:      isDelete,IsHot:         isHot,IsBest:        isBest,IsNew:         isNew,GoodsTypeId:   goodsTypeId,Sort:          sort,Status:        status,AddTime:       addTime,GoodsColor:    goodsColorStr,GoodsImg:      goodsImg,}err := models.DB.Create(&goods).Errorif err != nil {con.Error(c, "增加失败", "/admin/goods/add")return}//增加图库信息//开启协程wg.Add(1)go func() {goodsImageList := c.PostFormArray("goods_image_list") //获取图片切片for _, v := range goodsImageList {goodsImgObj := models.GoodsImage{}goodsImgObj.GoodsId = goods.IdgoodsImgObj.ImgUrl = vgoodsImgObj.Sort = 10goodsImgObj.Status = 1goodsImgObj.AddTime = int(models.GetUnix())models.DB.Create(&goodsImgObj)}wg.Done()}()//增加规格包装wg.Add(1) //启动一个 goroutine 就登记+1//商品类型属性id和商品类型属性值一一对应go func() {attrIdList := c.PostFormArray("attr_id_list")attrValueList := c.PostFormArray("attr_value_list")for i := 0; i < len(attrIdList); i++ {//获取商品类型属性idgoodsTypeAttributeId, attributeIdErr := models.Int(attrIdList[i])if attributeIdErr == nil {//获取商品类型属性的数据goodsTypeAttributeObj := models.GoodsTypeAttribute{Id: goodsTypeAttributeId}models.DB.Find(&goodsTypeAttributeObj)//给商品属性里面增加数据  规格包装goodsAttrObj := models.GoodsAttr{}goodsAttrObj.GoodsId = goods.IdgoodsAttrObj.AttributeTitle = goodsTypeAttributeObj.TitlegoodsAttrObj.AttributeType = goodsTypeAttributeObj.AttrTypegoodsAttrObj.AttributeId = goodsTypeAttributeObj.IdgoodsAttrObj.AttributeCateId = goodsTypeAttributeObj.CateIdgoodsAttrObj.AttributeValue = attrValueList[i] //值从attrValueList中获取goodsAttrObj.Status = 1goodsAttrObj.Sort = 10goodsAttrObj.AddTime = int(models.GetUnix())models.DB.Create(&goodsAttrObj)}}wg.Done() //goroutine 结束就登记-1}()//等待所有登记的 goroutine 都结束wg.Wait()con.Success(c, "增加数据成功", "/admin/goods")
}func (con GoodsController) Edit(c *gin.Context) {//获取要修改的商品信息id, err := models.Int(c.Query("id"))if err != nil {con.Error(c, "传入参数错误", "/admin/goods")return}goods := models.Goods{Id: id}models.DB.Find(&goods)//获取商品分类goodsCateList := []models.GoodsCate{}models.DB.Where("pid = 0").Preload("GoodsCateItems").Find(&goodsCateList)//获取商品颜色,以及选择的颜色//把商品颜色字符串转换成切片数组goodsColorSlice := strings.Split(goods.GoodsColor, ",")//定义一个商品颜色MapgoodsColorMap := make(map[string]string)//循环颜色切片,把数据放入Map中for _, v := range goodsColorSlice {goodsColorMap[v] = v}//获取商品颜色列表goodsColorList := []models.GoodsColor{}models.DB.Where("status = 1").Find(&goodsColorList)//循环颜色列表,并与goodsColorMap比较,判断该商品是否有该颜色,并设置checkfor i := 0; i < len(goodsCateList); i++ {//断该商品是否有该颜色_, ok := goodsColorMap[models.String(goodsColorList[i].Id)]if ok { //该商品存在该颜色,设置Check=truegoodsColorList[i].Checked = true}}//获取商品图库信息goodsImageList := []models.GoodsImage{}models.DB.Where("goods_id = ?", goods.Id).Find(&goodsImageList)//获取商品类型goodsTypeList := []models.GoodsType{}models.DB.Where("status = 1").Find(&goodsTypeList)//获取规格信息goodsAttr := []models.GoodsAttr{}models.DB.Where("goods_id = ?", goods.Id).Find(&goodsAttr)//拼接规格信息表单htmlgoodsAttrStr := ""//循环规格信息数for _, v := range goodsAttr {if v.AttributeType == 1 { //当属性类型=1(单行文本框)时,显示的input html//fmt.Sprintf(): 拼接字符串goodsAttrStr += fmt.Sprintf(`<li><span>%v: </span> <input type="hidden" name="attr_id_list" value="%v" />   <input type="text" name="attr_value_list" value="%v" /></li>`, v.AttributeTitle, v.AttributeId, v.AttributeValue)} else if v.AttributeType == 2 { //当属性类型=2(多行文本框)时,显示的textareahtmlgoodsAttrStr += fmt.Sprintf(`<li><span>%v:  </span><input type="hidden" name="attr_id_list" value="%v" />  <textarea cols="50" rows="3" name="attr_value_list">%v</textarea></li>`, v.AttributeTitle, v.AttributeId, v.AttributeValue)} else if v.AttributeType == 3 { //当属性类型=3(下拉框选择)时,显示的select html//获取当前类型对应的值(下拉框应该有多个选择的值)goodsTypeAttribute := models.GoodsTypeAttribute{Id: v.AttributeId}models.DB.Find(&goodsTypeAttribute)//把下拉框中的值转换成切片attrValueSlice := strings.Split(goodsTypeAttribute.AttrValue, "\\n")//属性id input hiddengoodsAttrStr += fmt.Sprintf(`<li><span>%v:  </span>  <input type="hidden" name="attr_id_list" value="%v" /> `, v.AttributeTitle, v.AttributeId)goodsAttrStr += fmt.Sprintf(`<select name="attr_value_list">`)//循环切片, 生成下拉框optionfor i := 0; i < len(attrValueSlice); i++ {if attrValueSlice[i] == v.AttributeValue { // 当前商品下拉属性 == 对应的商品下拉属性时, selectedgoodsAttrStr += fmt.Sprintf(`<option value="%v" selected >%v</option>`, attrValueSlice[i], attrValueSlice[i])} else {goodsAttrStr += fmt.Sprintf(`<option value="%v">%v</option>`, attrValueSlice[i], attrValueSlice[i])}}goodsAttrStr += fmt.Sprintf(`</select>`)goodsAttrStr += fmt.Sprintf(`</li>`)}}c.HTML(http.StatusOK, "admin/goods/edit.html", gin.H{"goods":          goods,"goodsCateList":  goodsCateList,"goodsColorList": goodsColorList,"goodsTypeList":  goodsTypeList,"goodsAttrStr":   goodsAttrStr,"goodsImageList": goodsImageList,"prePage":        c.Request.Referer(), // 获取上一页的地址})
}//修改商品信息表单提交
func (con GoodsController) DoEdit(c *gin.Context) {//获取表单提交过来的数据,并进行判断是否合法id, err1 := models.Int(c.PostForm("id"))if err1 != nil {con.Error(c, "传入参数错误", "/admin/goods")return}prePage := c.PostForm("prePage") // 获取上一页地址title := c.PostForm("title")subTitle := c.PostForm("sub_title")goodsSn := c.PostForm("goods_sn")cateId, _ := models.Int(c.PostForm("cate_id"))goodsNumber, _ := models.Int(c.PostForm("goods_number"))//价格,注意小数点marketPrice, _ := models.Float(c.PostForm("market_price"))price, _ := models.Float(c.PostForm("price"))relationGoods := c.PostForm("relation_goods")goodsAttr := c.PostForm("goods_attr")goodsVersion := c.PostForm("goods_version")goodsGift := c.PostForm("goods_gift")goodsFitting := c.PostForm("goods_fitting")//颜色:获取的是切片goodsColorArr := c.PostFormArray("goods_color")goodsKeywords := c.PostForm("goods_keywords")goodsDesc := c.PostForm("goods_desc")goodsContent := c.PostForm("goods_content")isDelete, _ := models.Int(c.PostForm("is_delete"))isHot, _ := models.Int(c.PostForm("is_hot"))isBest, _ := models.Int(c.PostForm("is_best"))isNew, _ := models.Int(c.PostForm("is_new"))goodsTypeId, _ := models.Int(c.PostForm("goods_type_id"))sort, _ := models.Int(c.PostForm("sort"))status, _ := models.Int(c.PostForm("status"))//获取颜色信息,把颜色转换成字符串goodsColorStr := strings.Join(goodsColorArr, ",")//修改数据goods := models.Goods{Id: id}models.DB.Find(&goods)goods.Title = titlegoods.SubTitle = subTitlegoods.GoodsSn = goodsSngoods.CateId = cateIdgoods.GoodsNumber = goodsNumbergoods.MarketPrice = marketPricegoods.Price = pricegoods.RelationGoods = relationGoodsgoods.GoodsAttr = goodsAttrgoods.GoodsVersion = goodsVersiongoods.GoodsGift = goodsGiftgoods.GoodsFitting = goodsFittinggoods.GoodsKeywords = goodsKeywordsgoods.GoodsDesc = goodsDescgoods.GoodsContent = goodsContentgoods.IsDelete = isDeletegoods.IsHot = isHotgoods.IsBest = isBestgoods.IsNew = isNewgoods.GoodsTypeId = goodsTypeIdgoods.Sort = sortgoods.Status = statusgoods.GoodsColor = goodsColorStr//上传图片,生成缩略图goodsImg, err2 := models.UploadImg(c, "goods_img")if err2 == nil && len(goodsImg) > 0 { // 说明修改了图片,那么就要设置图片属性goods.GoodsImg = goodsImg//判断本地图片才需要处理缩略图if models.GetOssStatus() != 1 {//开启协程wg.Add(1)go func() {models.ResizeGoodsImage(goodsImg)wg.Done()}()}}err := models.DB.Save(&goods).Errorif err != nil {con.Error(c, "修改失败", "/admin/goods/edit?id="+models.String(id))return}//增加图库信息//开启协程wg.Add(1)go func() {goodsImageList := c.PostFormArray("goods_image_list") //获取图片切片for _, v := range goodsImageList {goodsImgObj := models.GoodsImage{}goodsImgObj.GoodsId = goods.IdgoodsImgObj.ImgUrl = vgoodsImgObj.Sort = 10goodsImgObj.Status = 1goodsImgObj.AddTime = int(models.GetUnix())models.DB.Create(&goodsImgObj)}wg.Done()}()//修改规格包装:1.删除当前商品下面的规格包装,2.重新执行增加//1.删除当前商品下面的规格包装goodsAttrObj := models.GoodsAttr{}models.DB.Where("goods_id = ?", goods.Id).Delete(&goodsAttrObj)//2.重新执行增加wg.Add(1) //启动一个 goroutine 就登记+1//商品类型属性id和商品类型属性值一一对应go func() {attrIdList := c.PostFormArray("attr_id_list")attrValueList := c.PostFormArray("attr_value_list")for i := 0; i < len(attrIdList); i++ {//获取商品类型属性idgoodsTypeAttributeId, attributeIdErr := models.Int(attrIdList[i])if attributeIdErr == nil {//获取商品类型属性的数据goodsTypeAttributeObj := models.GoodsTypeAttribute{Id: goodsTypeAttributeId}models.DB.Find(&goodsTypeAttributeObj)//给商品属性里面增加数据  规格包装goodsAttrObj := models.GoodsAttr{}goodsAttrObj.GoodsId = goods.IdgoodsAttrObj.AttributeTitle = goodsTypeAttributeObj.TitlegoodsAttrObj.AttributeType = goodsTypeAttributeObj.AttrTypegoodsAttrObj.AttributeId = goodsTypeAttributeObj.IdgoodsAttrObj.AttributeCateId = goodsTypeAttributeObj.CateIdgoodsAttrObj.AttributeValue = attrValueList[i] //值从attrValueList中获取goodsAttrObj.Status = 1goodsAttrObj.Sort = 10goodsAttrObj.AddTime = int(models.GetUnix())models.DB.Create(&goodsAttrObj)}}wg.Done() //goroutine 结束就登记-1}()//等待所有登记的 goroutine 都结束wg.Wait()if len(prePage) > 0 { //跳转到上一页con.Success(c, "修改数据成功", prePage)return}con.Success(c, "修改数据成功", "/admin/goods")
}//获取商品类型对应的属性
func (con GoodsController) GoodsTypeAttribute(c *gin.Context) {cateId, err1 := models.Int(c.Query("cateId"))goodsTypeAttributeList := []models.GoodsTypeAttribute{}err2 := models.DB.Where("cate_id = ?", cateId).Find(&goodsTypeAttributeList).Errorif err1 != nil || err2 != nil {c.JSON(http.StatusOK, gin.H{"success": false,"result":  "",})return}c.JSON(http.StatusOK, gin.H{"success": true,"result":  goodsTypeAttributeList,})
}//富文本编辑器上传图片方法
func (con GoodsController) EditorImageUpload(c *gin.Context) {imgDir, err := models.UploadImg(c, "file") //传递的参数默认是fileif err != nil {c.JSON(http.StatusOK, gin.H{"link": "", //富文本要求返回的格式: {link: 'path/to/image.jpg'}})return}//判断本地图片才需要处理缩略图if models.GetOssStatus() != 1 {//开启协程wg.Add(1)go func() {models.ResizeGoodsImage(imgDir)wg.Done()}()//本地图片,返回本地图片地址c.JSON(http.StatusOK, gin.H{"link": "/" + imgDir,})} else {//云服务器对象存储图片,返回云服务器图片地址c.JSON(http.StatusOK, gin.H{"link": models.GetSettingFromColumn("OssDomain") + imgDir,})}
}//商品上传图片方法
func (con GoodsController) GoodsImageUpload(c *gin.Context) {imgDir, err := models.UploadImg(c, "file") //传递的参数默认是fileif err != nil {c.JSON(http.StatusOK, gin.H{"link": "", //富文本要求返回的格式: {link: 'path/to/image.jpg'}})return}//判断本地图片才需要处理缩略图if models.GetOssStatus() != 1 {//开启协程wg.Add(1)go func() {models.ResizeGoodsImage(imgDir)wg.Done()}()}//返回图片地址c.JSON(http.StatusOK, gin.H{"link": imgDir,})
}//修改商品图库关联的颜色
func (con GoodsController) ChangeGoodsImageColor(c *gin.Context) {//获取图片id 获取颜色idgoodsImageId, err1 := models.Int(c.Query("goods_image_id"))colorId, err2 := models.Int(c.Query("color_id"))goodsImage := models.GoodsImage{Id: goodsImageId}models.DB.Find(&goodsImage)goodsImage.ColorId = colorIderr3 := models.DB.Save(&goodsImage).Errorif err1 != nil || err2 != nil || err3 != nil {c.JSON(http.StatusOK, gin.H{"result":  "更新失败","success": false,})return}c.JSON(http.StatusOK, gin.H{"result":  "更新成功","success": true,})
}//删除图库
func (con GoodsController) RemoveGoodsImage(c *gin.Context) {//获取图片idgoodsImageId, err1 := models.Int(c.Query("goods_image_id"))goodsImage := models.GoodsImage{Id: goodsImageId}//获取图片models.DB.Find(&goodsImage)fileName := goodsImage.ImgUrl//todo 是否删除服务器保存的图片?err3 := os.Remove(strings.TrimLeft(fileName, "/"))if err3 != nil {c.JSON(http.StatusOK, gin.H{"result":  err3.Error(),"success": false,})return}//删除数据库中的数据err2 := models.DB.Delete(&goodsImage).Errorif err1 != nil || err2 != nil {c.JSON(http.StatusOK, gin.H{"result":  "删除失败","success": false,})return}c.JSON(http.StatusOK, gin.H{"result":  "删除成功","success": true,})
}//删除
func (con GoodsController) Delete(c *gin.Context) {//获取提交的表单数据id, err := models.Int(c.Query("id"))if err != nil {con.Error(c, "传入数据错误", "/admin/goods")return}//查询商品goods := models.Goods{Id: id}models.DB.Find(&goods)//软删除goods.IsDelete = 1goods.Status = 0models.DB.Save(&goods)//获取上一页地址,判断是否存在,如果存在则跳转,不存在则跳转到列表首页prePage := c.Request.Referer()if len(prePage) > 0 {con.Success(c, "删除数据成功", prePage)return}con.Success(c, "删除数据成功", "/admin/goods")
}

6.创建商品相关html

index.html

该商品列表有如下功能
1.增加商品按钮:跳转到增加商品页面
2.搜索功能:输入商品名称,点击搜索
3.修改商品字段(上架,精品,新平,热销)状态
4.修改排序数字
5.修改操作:点击修改跳转到修改页面
6.删除操作
7.分页操作
效果展示见:二.商品相关界面展示
这里要用到一个分页组件jqPaginator,下载链接:
链接:https://pan.baidu.com/s/1hype6KGLcYK2GBfiJZOCBA
提取码:5pze

{{ define "admin/goods/index.html" }}
{{ template "admin/public/page_header.html" .}}
<script type="text/javascript" src="/static/admin/js/jqPaginator.js"></script><div class="container-fluid"><div class="row"><div class="panel panel-default"><div class="panel-heading"><a href="/admin/goods/add" class="btn btn-primary">增加商品</a></div><div class="panel-body"><form role="form" class="form-inline" method="get" action="/admin/goods"><div class="form-group"><label for="name">输入关键词</label><input type="text" class="form-control" value="{{.keyword}}" id="keyword" name="keyword" placeholder="请输入名称"></div><div class="form-group"><button type="submit" class="btn btn-default">开始搜索</button></div></form></div><!--列表展示--><div class="table-responsive"><table class="table table-bordered"><thead><tr class="th"><th>商品名称</th><th>价格</th><th>原价</th><th>点击量</th><th>上架</th><th>精品</th><th>新品</th><th>热销</th><th>排序</th><th>库存</th><th class="text-center">操作</th></tr></thead><tbody>{{range $key,$value := .goodsList}}<tr><td>{{$value.Title}}</td><td>{{$value.Price}}</td><td>{{$value.MarketPrice}}</td><td>{{$value.ClickCount}}</td><td class="text-center">{{if eq $value.Status 1}}<img class="chStatus" src="/static/admin/images/yes.gif" data-id="{{$value.Id}}"data-table="goods" data-field="status"/>{{else}}<img class="chStatus" src="/static/admin/images/no.gif" data-id="{{$value.Id}}"data-table="goods" data-field="status"/>{{end}}</td><td class="text-center">{{if eq $value.IsBest 1}}<img class="chStatus" src="/static/admin/images/yes.gif" data-id="{{$value.Id}}"data-table="goods" data-field="is_best"/>{{else}}<img class="chStatus" src="/static/admin/images/no.gif" data-id="{{$value.Id}}"data-table="goods" data-field="is_best"/>{{end}}</td><td class="text-center">{{if eq $value.IsNew 1}}<img class="chStatus" src="/static/admin/images/yes.gif" data-id="{{$value.Id}}"data-table="goods" data-field="is_new"/>{{else}}<img class="chStatus" src="/static/admin/images/no.gif" data-id="{{$value.Id}}"data-table="goods" data-field="is_new"/>{{end}}</td><td class="text-center">{{if eq $value.IsHot 1}}<img class="chStatus" src="/static/admin/images/yes.gif" data-id="{{$value.Id}}"data-table="goods" data-field="is_hot"/>{{else}}<img class="chStatus" src="/static/admin/images/no.gif" data-id="{{$value.Id}}"data-table="goods" data-field="is_hot"/>{{end}}</td><td class="text-center"><span class="chSpanNum" data-id="{{$value.Id}}" data-table="goods"data-field="sort">{{$value.Sort}}</span></td><td class="text-center"><span class="chSpanNum" data-id="{{$value.Id}}" data-table="goods"data-field="goods_number">{{$value.GoodsNumber}}</span></td><td class="text-center"><a href="/admin/goods/edit?id={{$value.Id}}"/>修改</a>  <a class="delete" href="/admin/goods/delete?id={{$value.Id}}"/>删除</a></td></tr>{{end}}</tbody></table></div></div><div class="pagination" id="pagination"></div></div></div></body>
<script>$('#pagination').jqPaginator({totalPages: {{.totalPages}},visiblePages: 10,currentPage:  {{.page}},onPageChange: function (num, type) {if (type != "init") {location.href = "/admin/goods?page=" + num + "&keyword=" + {{.keyword}};}}});
</script>
</html>
{{end}}

add.html

效果展示与功能介绍见:二.商品相关界面展示
这里需要用到一个富文本编辑器(wysiwyg-editor),以及对应的语言包(zh_cn.js),下载链接:https://pan.baidu.com/s/1HgUUhMBemHmH-gXchBBOXg ,提取码:1b3n,批量上传图片插(diyUpload)),下载链接:https://pan.baidu.com/s/1Q0If0tygsKU3d6PM4hs1GA ,提取码:xq3m

{{ define "admin/goods/add.html" }}{{ template "admin/public/page_header.html" .}}<!-- 富文本编辑器start. --><link href="/static/wysiwyg-editor/css/froala_editor.pkgd.min.css"rel="stylesheet" type="text/css"/><script type="text/javascript"src="/static/wysiwyg-editor/js/froala_editor.pkgd.min.js"></script><!--语言包--><script type="text/javascript" src="/static/admin/js/zh_cn.js"></script><!-- 富文本编辑器end. --><!-- 批量上传图片插件start. --><link rel="stylesheet" type="text/css" href="/static/diyUpload/css/webuploader.css"><link rel="stylesheet" type="text/css" href="/static/diyUpload/css/diyUpload.css"><script type="text/javascript" src="/static/diyUpload/js/webuploader.html5only.min.js"></script><script type="text/javascript" src="/static/diyUpload/js/diyUpload.js"></script><!-- 批量上传图片插件end. --><div class="container-fluid"><div class="row"><div class="panel panel-default"><div class="panel-heading">增加商品</div><div class="panel-body"><div class="table-responsive goods-content input-form"><form action="/admin/goods/doAdd" method="post" enctype="multipart/form-data"><!-- Nav tabs --><ul class="nav nav-tabs" role="tablist"><li role="presentation" class="active"><a href="#general" role="tab" data-toggle="tab">通用信息</a></li><li role="presentation"><a href="#detail" role="tab" data-toggle="tab">详细描述</a></li><li role="presentation"><a href="#mix" role="tab" data-toggle="tab">商品属性</a></li><li role="presentation"><a href="#attribute" role="tab" data-toggle="tab">规格与包装</a></li><li role="presentation"><a href="#photo" role="tab" data-toggle="tab">商品相册</a></li></ul><!-- Tab panes --><div class="tab-content"><div role="tabpanel" class="tab-pane active" id="general"><ul class="form_input"><li><span> 商品标题:</span> <input type="text" name="title" class="input"/></li><li><span> 附属标题:</span> <input type="text" name="sub_title" class="input"/></li><li><span>商品版本:</span> <input type="text" name="goods_version" class="input"/></li><li><span>所属分类:</span><select name="cate_id" id="cate_id">{{range $key, $value := .goodsCateList}}<option value="{{$value.Id}}">{{$value.Title}}</option>{{range $k,$v := $value.GoodsCateItems}}<option value="{{$v.Id}}"> ----{{$v.Title}}</option>{{end}}{{end}}</select></li><li><span> 商品图片:</span> <input type="file" name="goods_img"/></li><li><span>商品价格:</span> <input type="text" name="price"/></li><li><span>商品原价:</span> <input type="text" name="market_price"/></li><li><span>商品状态:</span> <input type="radio" value="1" name="status" checked/> 显示  <input type="radio" value="0" name="status"/> 隐藏</li><li><span>加入推荐:</span> <input type="checkbox" value="1" name="is_best"/> 精品<input type="checkbox" value="1" name="is_hot"/> 热销<input type="checkbox" value="1" name="is_new"/> 新品</li></ul></div><div role="tabpanel" class="tab-pane" id="detail"><textarea name="goods_content" id="goods_content" cols="100" rows="8"></textarea></div><div role="tabpanel" class="tab-pane" id="mix"><ul class="form_input"><li><span>商品颜色:</span>{{range $key, $value := .goodsColorList}}<input type="checkbox" name="goods_color" id="color_{{$value.Id}}"value="{{$value.Id}}"/><label for="color_{{$value.Id}}">{{$value.ColorName}}</label>{{end}}</li><li><span>关联商品:</span><input type="text" name="relation_goods" class="relation_goods"/> <i>填写关联商品的id多个以逗号隔开 格式:23,24,39</i></li><li><span>关联赠品:</span><input type="text" name="goods_gift" class="goods_gift"/> <i>可为空格式:23-2,39-5 说明:例如23-2 中的23表示商品id,2表示商品数量</i></li><li><span>关联配件:</span><input type="text" name="goods_fitting" class="goods_fitting"/> <i>可为空格式:23-2,39-5 说明:例如23-2 中的23表示商品id,2表示商品数量</i></li><li><span>更多属性:</span><input type="text" name="goods_attr" class="goods_attr"/> <i> 格式:颜色:红色,白色,黄色 | 尺寸:41,42,43</i></li><li><span>SEO关键词:</span><input type="text" name="goods_keywords" class="goods_keywords"/></li><li><span>SEO描述:</span><textarea  name="goods_desc" class="goods_desc"  cols="100" rows="8"></textarea></li></ul></div><div role="tabpanel" class="tab-pane" id="attribute"><ul class="form_input"><li><span>商品类型: </span><select name="goods_type_id" id="goods_type_id"><option value="0">--请选择商品类型--</option>{{range $key, $value := .goodsTypeList}}<option value="{{$value.Id}}">{{$value.Title}}</option>{{end}}</select></li></ul><ul class="form_input" id="goods_type_attribute"></ul></div><div role="tabpanel" class="tab-pane" id="photo"><div id="photoUploader"></div><div id="photoList"></div></div></div><br/><button type="submit" class="btn btn-primary">提交</button></form></div></div></div></div></div><script>、、富文本编辑器new FroalaEditor('#goods_content', {height: 300,language: "zh_cn", //需要引入语言包,注意名称// //自定义导航条// toolbarButtons: [//     ['bold', 'insertTable', 'html'],//     ['undo', 'redo']// ],// //< 768px使用// toolbarButtonsXS: [//     ['bold', 'strikethrough', 'subscript', 'superscript', 'outdent', 'indent',//     'clearFormatting', 'insertTable', 'html'],//     ['undo', 'redo']// ],//上传图片urlimageUploadURL: '/admin/goods/editorImageUpload',});$(function () {//获取商品类型属性$("#goods_type_id").change(function () {var cateId = $(this).val()$.get("/admin/goods/goodsTypeAttribute", {"cateId": cateId}, function (response) {var str = ""if (response.success) {var attrData = response.result;for (var i = 0; i < attrData.length; i++) {if (attrData[i].attr_type == 1) { //1 单行文本框str += '<li><span>' + attrData[i].title + ':  </span> <input type="hidden" name="attr_id_list" value="' + attrData[i].id + '" />   <input type="text" name="attr_value_list" /></li>'} else if (attrData[i].attr_type == 2) { //2 多行文本框str += '<li><span>' + attrData[i].title + ':  </span> <input type="hidden" name="attr_id_list" value="' + attrData[i].id + '">  <textarea cols="50" rows="3" name="attr_value_list"></textarea></li>'} else {//3 从下面列表中选择(一行代表一个可选值)var attrArray = attrData[i].attr_value.split("\\n")str += '<li><span>' + attrData[i].title + ':  </span>  <input type="hidden" name="attr_id_list" value="' + attrData[i].id + '" />';str += '<select name="attr_value_list">'for (var j = 0; j < attrArray.length; j++) {str += '<option value="' + attrArray[j] + '">' + attrArray[j] + '</option>';}str += '</select>'str += '</li>'}}$("#goods_type_attribute").html(str);}})})//批量上传图片,使用diyUpload上传图片插件$('#photoUploader').diyUpload({url:'/admin/goods/goodsImageUpload',success:function( response ) {var photoStr='<input type="hidden" name="goods_image_list" value='+response.link+' />';$("#photoList").append(photoStr)},error:function( err ) {console.info( err );}});})</script></body></html>
{{end}}

edit.html

{{ define "admin/goods/edit.html" }}
{{ template "admin/public/page_header.html" .}}
<!-- 富文本编辑器 -->
<link href="/static/wysiwyg-editor/css/froala_editor.pkgd.min.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="/static/wysiwyg-editor/js/froala_editor.pkgd.min.js"></script>
<script type="text/javascript" src="/static/wysiwyg-editor/js/zh_cn.js"></script>
<!-- 上传图片的js css -->
<link rel="stylesheet" type="text/css" href="/static/diyUpload/css/webuploader.css">
<link rel="stylesheet" type="text/css" href="/static/diyUpload/css/diyUpload.css">
<script type="text/javascript" src="/static/diyUpload/js/webuploader.html5only.min.js"></script>
<script type="text/javascript" src="/static/diyUpload/js/diyUpload.js"></script><div class="container-fluid"><div class="row"><div class="panel panel-default"><div class="panel-heading">修改商品</div><div class="panel-body"><div class="table-responsive goods-content input-form"><form action="/admin/goods/doEdit" method="post" enctype="multipart/form-data"><input type="hidden" name="id" class="input" value="{{.goods.Id}}" /><input type="hidden" name="prePage" class="input" value="{{.prePage}}" /><!-- Nav tabs --><ul class="nav nav-tabs" role="tablist"><li role="presentation" class="active"><a href="#general" role="tab"data-toggle="tab">通用信息</a></li><li role="presentation"><a href="#detail" role="tab" data-toggle="tab">详细描述</a></li><li role="presentation"><a href="#mix" role="tab" data-toggle="tab">商品属性</a></li><li role="presentation"><a href="#attribute" role="tab" data-toggle="tab">规格与包装</a></li><li role="presentation"><a href="#photo" role="tab" data-toggle="tab">商品相册</a></li></ul><!-- Tab panes --><div class="tab-content"><div role="tabpanel" class="tab-pane active" id="general"><ul class="form_input"><li> <span> 商品标题:</span> <input type="text" name="title" class="input"value="{{.goods.Title}}" /></li><li> <span> 附属标题:</span> <input type="text" name="sub_title" class="input"value="{{.goods.SubTitle}}" /></li><li> <span> 商品版本:</span> <input type="text" name="goods_version" class="input"value="{{.goods.GoodsVersion}}" /></li><li> <span>所属分类:</span><select name="cate_id" id="cid">{{$cateId := .goods.CateId}}{{range $key,$value := .goodsCateList}}<option  {{if eq $cateId $value.Id}}selected{{end}} value="{{$value.Id}}" >{{$value.Title}}</option>{{range $k,$v := $value.GoodsCateItems}}<option {{if eq $cateId $v.Id}}selected{{end}} value="{{$v.Id}}"> ----{{$v.Title}}</option>{{end}}{{end}}</select></li><li> <span> 商品图片:</span> <input type="file" name="goods_img" />{{if ne .goods.GoodsImg ""}}<img src="{{.goods.GoodsImg | FormatImg}}" width="80"/>{{end}}</li><li> <span>商品价格:</span> <input type="text" name="price" value="{{.goods.Price}}"/></li><li> <span>商品原价:</span> <input type="text" name="market_price"  value="{{.goods.MarketPrice}}" /></li><li> <span>商品库存:</span> <input type="text" name="goods_number" value="{{.goods.GoodsNumber}}" /></li><li> <span>商品排序:</span> <input type="text" name="sort" value="{{.goods.Sort}}"/></li><li> <span>商品状态:</span> <input type="radio" value="1" {{if eq .goods.Status 1}}checked{{end}} name="status" checked /> 显示  <input type="radio" value="0" {{if eq .goods.Status 0}}checked{{end}} name="status" /> 隐藏</li><li> <span>加入推荐:</span> <input type="checkbox" value="1" name="is_best" {{if eq .goods.IsBest 1}}checked{{end}}/> 精品<input type="checkbox" value="1" name="is_hot" {{if eq .goods.IsHot 1}}checked{{end}}/> 热销<input type="checkbox" value="1" name="is_new" {{if eq .goods.IsNew 1}}checked{{end}} /> 新品</li></ul></div><div role="tabpanel" class="tab-pane" id="detail"><textarea name="goods_content" id="content" cols="100" rows="8">{{.goods.GoodsContent}}</textarea></div><div role="tabpanel" class="tab-pane" id="mix"><ul class="form_input"><li> <span>商品颜色:</span>{{range $key,$value := .goodsColorList}}<input type="checkbox" {{if eq $value.Checked true}}checked{{end}} value="{{$value.Id}}" name="goods_color"id="color_{{$value.Id}}" /><label for="color_{{$value.Id}}">{{$value.ColorName}}</label>&nbsp;{{end}}</li><li> <span>关联商品:</span><input type="text" name="relation_goods" class="relation_goods" value="{{.goods.RelationGoods}}"/> <i>填写关联商品的id多个以逗号隔开 格式:23,24,39</i></li><li> <span>关联赠品:</span><input type="text" name="goods_gift" class="goods_gift" value="{{.goods.GoodsGift}}"/> <i>可为空 格式:23-2,39-5说明:例如23-2 中的23表示商品id,2表示商品数量</i></li><li> <span>关联配件:</span><input type="text" name="goods_fitting" class="goods_fitting" value="{{.goods.GoodsFitting}}" /> <i>可为空格式:23-2,39-5 说明:例如23-2 中的23表示商品id,2表示商品数量</i></li><li> <span>更多属性:</span><input type="text" name="goods_attr" class="goods_attr" value="{{.goods.GoodsAttr}}" /> <i> 格式: 颜色:红色,白色,黄色 |尺寸:41,42,43</i></li><li> <span>Seo关键词:</span><input type="text" name="goods_keywords" class="input" value="{{.goods.GoodsKeywords}}" />  </li><li> <span>Seo描述:</span>                                                <textarea name="goods_desc" id="goods_desc" cols="100" rows="2">{{.goods.GoodsDesc}}</textarea></li></ul></div><div role="tabpanel" class="tab-pane" id="attribute"><ul class="form_input"><li> <span>商品类型: </span><select name="goods_type_id" id="goods_type_id"><option value="0">--请选择商品类型--</option>{{$goodsTypeId := .goods.GoodsTypeId}}{{range $key,$value := .goodsTypeList}}<option  {{if eq $value.Id $goodsTypeId}}selected{{end}} value="{{$value.Id}}">{{$value.Title}}</option>{{end}}</select></li></ul><ul class="form_input" id="goods_type_attribute">{{.goodsAttrStr | Str2Html}}</ul></div><div role="tabpanel" class="tab-pane" id="photo"><div id="photoList"><ul id="goods_image_list" class="goods_image_list clear">{{$goodsColor:=.goodsColorList}}{{range $key,$value := .goodsImageList}}<li><img  src="{{$value.ImgUrl | FormatImg}}" class="pic" /><div class="color_list"><select class="relation_goods_color" goods_image_id="{{$value.Id}}"><option value="0">关联颜色</option>{{range $k,$v := $goodsColor}}{{if eq $v.Checked true}}                                                                    <option value="{{$v.Id}}" {{if eq $value.ColorId $v.Id}}selected{{end}}>{{$v.ColorName}}</option>{{end}}{{end}}</select>                                                        </div>    <div class="goods_image_delete" goods_image_id="{{$value.Id}}"></div>                                          </li>{{end}}</ul></div><div id="photoUploader"></div></div></div><br /><button type="submit" class="btn btn-primary">提交</button></form></div></div></div></div>
</div>
<script>new FroalaEditor('#content', {height: 300,language: 'zh_cn',  //要使用语言包首先需要引入 ,还要注意下划线// toolbarButtons: [ ['undo', 'redo'], ['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'outdent', 'indent', 'clearFormatting', 'insertTable', 'html'] ],// toolbarButtonsXS: [ ['undo', 'redo'], ['bold', 'italic', 'underline'] ]imageUploadURL: '/admin/goods/editorImageUpload'});//获取商品类型属性$(function () {$("#goods_type_id").change(function () {var cateId = $(this).val()$.get("/admin/goods/goodsTypeAttribute", { "cateId": cateId }, function (response) {console.log(response)var str = ""if (response.success) {var attrData = response.result;for (var i = 0; i < attrData.length; i++) {if (attrData[i].attr_type == 1) {str += '<li><span>' + attrData[i].title + ':  </span> <input type="hidden" name="attr_id_list" value="' + attrData[i].id + '" />   <input type="text" name="attr_value_list" /></li>'} else if (attrData[i].attr_type == 2) {str += '<li><span>' + attrData[i].title + ':  </span> <input type="hidden" name="attr_id_list" value="' + attrData[i].id + '">  <textarea cols="50" rows="3" name="attr_value_list"></textarea></li>'} else {var attrArray = attrData[i].attr_value.split("\\n")str += '<li><span>' + attrData[i].title + ':  </span>  <input type="hidden" name="attr_id_list" value="' + attrData[i].id + '" />';str += '<select name="attr_value_list">'for (var j = 0; j < attrArray.length; j++) {str += '<option value="' + attrArray[j] + '">' + attrArray[j] + '</option>';}str += '</select>'str += '</li>'}}$("#goods_type_attribute").html(str);}})})})//批量上传图片$(function () {$('#photoUploader').diyUpload({url: '/admin/goods/goodsImageUpload',success: function (response) {// console.info( data );var photoStr = '<input type="hidden" name="goods_image_list" value=' + response.link + ' />';$("#photoList").append(photoStr)},error: function (err) {console.info(err);}});})$(function(){$(".relation_goods_color").change(function(){var goods_image_id=$(this).attr("goods_image_id")var color_id=$(this).val()$.get("/admin/goods/changeGoodsImageColor",{"goods_image_id":goods_image_id,"color_id":color_id},function(response){if(response.success){alert("操作成功")}})})$(".goods_image_delete").click(function(){var goods_image_id=$(this).attr("goods_image_id")   var _that=this;         var flag = confirm("确定要删除吗?");if (flag){$.get("/admin/goods/removeGoodsImage",{"goods_image_id":goods_image_id},function(response){// console.log(response)if(response.success){//删除当前显示的图片$(_that).parent().remove()}})}})})
</script>
</body></html>
{{end}}

7.配置商品相关路由

在routers/adminRouters.go下配置商品相关路由

//商品路由
adminRouters.GET("/goods", admin.GoodsController{}.Index)
adminRouters.GET("/goods/add", admin.GoodsController{}.Add)
adminRouters.POST("/goods/doAdd", admin.GoodsController{}.DoAdd)
adminRouters.GET("/goods/edit", admin.GoodsController{}.Edit)
adminRouters.POST("/goods/doEdit", admin.GoodsController{}.DoEdit)
adminRouters.GET("/goods/delete", admin.GoodsController{}.Delete)//上传商品图片(商品头图,商品图片相册)
adminRouters.POST("/goods/goodsImageUpload", admin.GoodsController{}.GoodsImageUpload)
//商品富文本编辑器图片上传
adminRouters.POST("/goods/editorImageUpload", admin.GoodsController{}.EditorImageUpload)
//获取商品类型对应的属性
adminRouters.GET("/goods/goodsTypeAttribute", admin.GoodsController{}.GoodsTypeAttribute)
//改变图库中相关图片颜色
adminRouters.GET("/goods/changeGoodsImageColor", admin.GoodsController{}.ChangeGoodsImageColor)
//删除图片
adminRouters.GET("/goods/removeGoodsImage", admin.GoodsController{}.RemoveGoodsImage)

[上一节][golang gin框架] 19.Gin 图片上传到云服务器(腾讯云,阿里云)

[下一节][golang gin框架] 21.Gin 商城项目-导航模块功能