一.商品模块数据表ER图关系分析
商品模块数据表相关功能关系见: [golang gin框架] 16.Gin 商城项目-商品模块数据表ER图关系分析
二.商品相关界面展示
商品列表
该商品列表有如下功能
1.增加商品按钮:跳转到增加商品页面
2.搜索功能:输入商品名称,点击搜索
3.修改商品字段(上架,精品,新平,热销)状态
4.修改排序数字
5.修改操作:点击修改跳转到修改页面
6.删除操作
7.分页操作
添加商品
通用信息
功能如下:
1.输入商品相关信息
2.选择商品所属分类(从商品分类表goods_cate中选择)
3.上传商品logo图片
4.选择商品状态(单选)
5.选择商品(新品,热销,精品:复选框)
详细描述
功能:
1.引入富文本框,输入商品内容
2.上传商品相关图片到富文本
注意:这里图片上传后台要判断是否上传到云服务器
商品属性
功能:
1.选择商品颜色(多选):从商品颜色表good_color中选择
2.输入商品其他相关属性
规格与包装
功能:
1.选择商品类型:从商品类型表goods_type中选择
2.根据选择的商品类型,展示不同的商品类型属性:从商品类型属性表goods_type_attribute中选择
3.填写商品类型属性
商品相册
功能:
1.选择商品相册,上传商品图片
注意:这里商品上传图片时,需要一个批量上传图片的插件,以及上传到云服务器还是本地服务器的判断
总结:
1.用户点击添加商品按钮,进入添加商品页面
2.根据商品相关功能,布局商品页面
3.进入到通用信息页面,填写通用信息(一些基本的信息,以及选择商品分类,上传图片,单选,复选信息)
4.切换到详情描述,增加商品内容:引入富文本框,以及上传图片内容
5.切换到商品属性,填写相关属性以及选择颜色(复选框)
6.切换到规格与包装,选择商品类型,以及展示对应的商品类型属性,根据实际展示填写
7.切换到商品相册,选择上传图片,并上传
8.以上操作完成后,提交
修改商品
修改操作步骤和上面添加商品操作类似,只不过需要展示商品已有数据,然后进行处理,这里不一一介绍
删除商品
修改属性状态,排序等
点击要修改的属性,进行状态修改
双击排序,生成焦点,修改数字,失去焦点,排序完成
该功能见: [golang gin框架] 15.Gin 商城项目-封装上传图片方法,轮播图的增删改查以及异步修改状态,数量
三.代码展示
商品相关数据表见 [golang gin框架] 16.Gin 商城项目-商品模块数据表ER图关系分析
tools.go工具类
项目所用到的工具类在models/tool.go下,代码如下:
package models
import (
"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) {
//判断是否开启oss
if 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,.jpeg
extName := path.Ext(file.Filename)
//设置后缀map
allowExtMap := 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,.jpeg
extName := path.Ext(file.Filename)
//设置后缀map
allowExtMap := 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/capi
SecretID: secretID, // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
// 环境变量 SECRETKEY 表示用户的 SecretKey,
//登录访问管理控制台查看密钥,https://console.cloud.tencent.com/cam/capi
SecretKey: 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,.jpeg
extName := path.Ext(file.Filename)
//设置后缀map
allowExtMap := 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] + extname
w, _ := Int(thumbnailSizeSlice[i])
//调用github.com/hunterhug/go_image中的方法ThumbnailF2F(),生成缩略图
err := ThumbnailF2F(filename, savepath, w, w)
if err != nil {
fmt.Println(err) //写个日志模块 处理日志
}
}
}
配置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
main.go
package main
import (
"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 []string
filepath.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 int
Title 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 int
Title 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.id
Title 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 int
ColorName string // 颜色名称
ColorValue string // 颜色值
Status int // 状态
Checked bool `gorm:"-"` //忽略该字段
}
func (GoodsColor) TableName() string {
return "goods_color"
}
package models
//商品表
type Goods struct {
Id int
Title string //商品标题
SubTitle string //附属标题
GoodsSn string //商品编号
CateId int //商品分类id: goods_cate.id
ClickCount 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.Id
Sort int //排序
Status int //状态
AddTime int //添加时间
}
func (Goods) TableName() string {
return "goods"
}
package models
//商品图片
type GoodsImage struct {
Id int
GoodsId int //商品id
ImgUrl string //图片保存路径:一般只会保存类似于/static/upload/20230313/xxx.png这种格式
ColorId int //颜色id
Sort int
AddTime int
Status int
}
func (GoodsImage) TableName() string {
return "goods_image"
}
package models
//商品属性
type GoodsAttr struct {
Id int
GoodsId int //商品id
AttributeCateId int //商品类型id,关联GoodsType.Id
AttributeId int //商品类型属性id,关联GoodsTypeAttribute.Id
AttributeTitle string //类型属性标题
AttributeType int //类型属性录入方式:GoodsTypeAttribute.AttrType
AttributeValue string //类型属性值
Sort int
AddTime int
Status 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 admin
import (
"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 == 0
if 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 int64
models.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).Error
if 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.Id
goodsImgObj.ImgUrl = v
goodsImgObj.Sort = 10
goodsImgObj.Status = 1
goodsImgObj.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++ {
//获取商品类型属性id
goodsTypeAttributeId, attributeIdErr := models.Int(attrIdList[i])
if attributeIdErr == nil {
//获取商品类型属性的数据
goodsTypeAttributeObj := models.GoodsTypeAttribute{Id: goodsTypeAttributeId}
models.DB.Find(&goodsTypeAttributeObj)
//给商品属性里面增加数据 规格包装
goodsAttrObj := models.GoodsAttr{}
goodsAttrObj.GoodsId = goods.Id
goodsAttrObj.AttributeTitle = goodsTypeAttributeObj.Title
goodsAttrObj.AttributeType = goodsTypeAttributeObj.AttrType
goodsAttrObj.AttributeId = goodsTypeAttributeObj.Id
goodsAttrObj.AttributeCateId = goodsTypeAttributeObj.CateId
goodsAttrObj.AttributeValue = attrValueList[i] //值从attrValueList中获取
goodsAttrObj.Status = 1
goodsAttrObj.Sort = 10
goodsAttrObj.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, ",")
//定义一个商品颜色Map
goodsColorMap := make(map[string]string)
//循环颜色切片,把数据放入Map中
for _, v := range goodsColorSlice {
goodsColorMap[v] = v
}
//获取商品颜色列表
goodsColorList := []models.GoodsColor{}
models.DB.Where("status = 1").Find(&goodsColorList)
//循环颜色列表,并与goodsColorMap比较,判断该商品是否有该颜色,并设置check
for i := 0; i < len(goodsCateList); i++ {
//断该商品是否有该颜色
_, ok := goodsColorMap[models.String(goodsColorList[i].Id)]
if ok { //该商品存在该颜色,设置Check=true
goodsColorList[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)
//拼接规格信息表单html
goodsAttrStr := ""
//循环规格信息数
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(多行文本框)时,显示的textareahtml
goodsAttrStr += 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 hidden
goodsAttrStr += 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">`)
//循环切片, 生成下拉框option
for i := 0; i < len(attrValueSlice); i++ {
if attrValueSlice[i] == v.AttributeValue { // 当前商品下拉属性 == 对应的商品下拉属性时, selected
goodsAttrStr += 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 = title
goods.SubTitle = subTitle
goods.GoodsSn = goodsSn
goods.CateId = cateId
goods.GoodsNumber = goodsNumber
goods.MarketPrice = marketPrice
goods.Price = price
goods.RelationGoods = relationGoods
goods.GoodsAttr = goodsAttr
goods.GoodsVersion = goodsVersion
goods.GoodsGift = goodsGift
goods.GoodsFitting = goodsFitting
goods.GoodsKeywords = goodsKeywords
goods.GoodsDesc = goodsDesc
goods.GoodsContent = goodsContent
goods.IsDelete = isDelete
goods.IsHot = isHot
goods.IsBest = isBest
goods.IsNew = isNew
goods.GoodsTypeId = goodsTypeId
goods.Sort = sort
goods.Status = status
goods.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).Error
if 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.Id
goodsImgObj.ImgUrl = v
goodsImgObj.Sort = 10
goodsImgObj.Status = 1
goodsImgObj.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++ {
//获取商品类型属性id
goodsTypeAttributeId, attributeIdErr := models.Int(attrIdList[i])
if attributeIdErr == nil {
//获取商品类型属性的数据
goodsTypeAttributeObj := models.GoodsTypeAttribute{Id: goodsTypeAttributeId}
models.DB.Find(&goodsTypeAttributeObj)
//给商品属性里面增加数据 规格包装
goodsAttrObj := models.GoodsAttr{}
goodsAttrObj.GoodsId = goods.Id
goodsAttrObj.AttributeTitle = goodsTypeAttributeObj.Title
goodsAttrObj.AttributeType = goodsTypeAttributeObj.AttrType
goodsAttrObj.AttributeId = goodsTypeAttributeObj.Id
goodsAttrObj.AttributeCateId = goodsTypeAttributeObj.CateId
goodsAttrObj.AttributeValue = attrValueList[i] //值从attrValueList中获取
goodsAttrObj.Status = 1
goodsAttrObj.Sort = 10
goodsAttrObj.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).Error
if 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") //传递的参数默认是file
if 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") //传递的参数默认是file
if 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 获取颜色id
goodsImageId, 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 = colorId
err3 := models.DB.Save(&goodsImage).Error
if 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) {
//获取图片id
goodsImageId, 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).Error
if 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 = 1
goods.Status = 0
models.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']
// ],
//上传图片url
imageUploadURL: '/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>
{{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 商城项目-导航模块功能