一.商品模块数据表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(`%v: `, v.AttributeTitle, v.AttributeId, v.AttributeValue)
} else if v.AttributeType == 2 { //当属性类型=2(多行文本框)时,显示的textareahtml
goodsAttrStr += fmt.Sprintf(`%v: `, 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(`%v: `, v.AttributeTitle, v.AttributeId)
goodsAttrStr += fmt.Sprintf(``)
//循环切片, 生成下拉框option
for i := 0; i < len(attrValueSlice); i++ {
if attrValueSlice[i] == v.AttributeValue { // 当前商品下拉属性 == 对应的商品下拉属性时, selected
goodsAttrStr += fmt.Sprintf(`%v `, attrValueSlice[i], attrValueSlice[i])
} else {
goodsAttrStr += fmt.Sprintf(`%v `, attrValueSlice[i], attrValueSlice[i])
}
}
goodsAttrStr += fmt.Sprintf(` `)
goodsAttrStr += fmt.Sprintf(` `)
}
}
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" .}}
商品名称
价格
原价
点击量
上架
精品
新品
热销
排序
库存
操作
{{range $key,$value := .goodsList}}
{{$value.Title}}
{{$value.Price}}
{{$value.MarketPrice}}
{{$value.ClickCount}}
{{if eq $value.Status 1}}
{{else}}
{{end}}
{{if eq $value.IsBest 1}}
{{else}}
{{end}}
{{if eq $value.IsNew 1}}
{{else}}
{{end}}
{{if eq $value.IsHot 1}}
{{else}}
{{end}}
{{$value.Sort}}
{{$value.GoodsNumber}}
修改
删除
{{end}}