feat(backend): 新增商品属性字典表(产地/保质期/储存方式/描述文档)

- model/product_attr.go: 新增 4 张字典表 struct(TenantBase 范式)
- model/product.go: Product 增加 4 个可空外键 + belongs-to 关联
- handler/product_attr.go: 4 组 16 个 CRUD handler(shop_id 隔离)
- handler/product.go: FindOrCreate/Update 支持写入 4 个属性 ID
- handler/public.go: Preload 4 表 + 三级回退介绍 + 默认话术常量
- router/router.go: /product-options/{origins,shelf-lives,storages,description-docs} 路由
- main.go: AutoMigrate 注册 4 个新 model
- testutil/setup.go: SQLite 测试库补齐新列与新表

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-08 06:56:16 +08:00
parent 0e2a4086e1
commit ba127826b2
8 changed files with 543 additions and 44 deletions
+56 -27
View File
@@ -127,22 +127,26 @@ func (h *ProductHandler) Update(c *gin.Context) {
namePinyin, nameInitials := util.ToPinyin(req.Name)
// 只更新业务字段,防止 Save() 覆盖 shop_id / created_at 等系统字段
if err := h.db.Model(&product).Updates(map[string]interface{}{
"code": req.Code,
"barcode": req.Barcode,
"name": req.Name,
"series": req.Series,
"spec": req.Spec,
"unit": req.Unit,
"category_id": req.CategoryID,
"brand": req.Brand,
"purchase_price": req.PurchasePrice,
"sale_price": req.SalePrice,
"min_stock": req.MinStock,
"description": req.Description,
"remark": req.Remark,
"custom_fields": req.CustomFields,
"name_pinyin": namePinyin,
"name_initials": nameInitials,
"code": req.Code,
"barcode": req.Barcode,
"name": req.Name,
"series": req.Series,
"spec": req.Spec,
"unit": req.Unit,
"category_id": req.CategoryID,
"brand": req.Brand,
"purchase_price": req.PurchasePrice,
"sale_price": req.SalePrice,
"min_stock": req.MinStock,
"description": req.Description,
"remark": req.Remark,
"custom_fields": req.CustomFields,
"name_pinyin": namePinyin,
"name_initials": nameInitials,
"origin_id": req.OriginID,
"shelf_life_id": req.ShelfLifeID,
"storage_id": req.StorageID,
"description_doc_id": req.DescriptionDocID,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -208,9 +212,13 @@ func (h *ProductHandler) QRCode(c *gin.Context) {
func (h *ProductHandler) FindOrCreate(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Name string `json:"name" binding:"required"`
Series string `json:"series"`
Spec string `json:"spec"`
Name string `json:"name" binding:"required"`
Series string `json:"series"`
Spec string `json:"spec"`
OriginID *uint64 `json:"origin_id"`
ShelfLifeID *uint64 `json:"shelf_life_id"`
StorageID *uint64 `json:"storage_id"`
DescriptionDocID *uint64 `json:"description_doc_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -221,6 +229,23 @@ func (h *ProductHandler) FindOrCreate(c *gin.Context) {
err := h.db.Where("shop_id = ? AND name = ? AND series = ? AND spec = ? AND deleted_at IS NULL",
shopID, req.Name, req.Series, req.Spec).First(&product).Error
if err == nil {
// 商品已存在:如果传入了属性 ID,则更新属性关联
attrUpdates := map[string]interface{}{}
if req.OriginID != nil {
attrUpdates["origin_id"] = req.OriginID
}
if req.ShelfLifeID != nil {
attrUpdates["shelf_life_id"] = req.ShelfLifeID
}
if req.StorageID != nil {
attrUpdates["storage_id"] = req.StorageID
}
if req.DescriptionDocID != nil {
attrUpdates["description_doc_id"] = req.DescriptionDocID
}
if len(attrUpdates) > 0 {
h.db.Model(&product).Updates(attrUpdates)
}
c.JSON(http.StatusOK, gin.H{"data": product})
return
}
@@ -233,14 +258,18 @@ func (h *ProductHandler) FindOrCreate(c *gin.Context) {
h.db.Model(&model.Product{}).Where("shop_id = ? AND deleted_at IS NULL", shopID).Count(&count)
namePinyin, nameInitials := util.ToPinyin(req.Name)
product = model.Product{
TenantBase: model.TenantBase{ShopID: shopID},
PublicID: uuid.New().String(),
Name: req.Name,
Series: req.Series,
Spec: req.Spec,
Code: fmt.Sprintf("P%03d", count+1),
NamePinyin: namePinyin,
NameInitials: nameInitials,
TenantBase: model.TenantBase{ShopID: shopID},
PublicID: uuid.New().String(),
Name: req.Name,
Series: req.Series,
Spec: req.Spec,
Code: fmt.Sprintf("P%03d", count+1),
NamePinyin: namePinyin,
NameInitials: nameInitials,
OriginID: req.OriginID,
ShelfLifeID: req.ShelfLifeID,
StorageID: req.StorageID,
DescriptionDocID: req.DescriptionDocID,
}
if createErr := h.db.Create(&product).Error; createErr != nil {
// Race condition: try to find the record created by another request
+268
View File
@@ -0,0 +1,268 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
)
// ProductAttrHandler 处理商品属性字典(产地/保质期/储存方式/描述文档)
type ProductAttrHandler struct {
db *gorm.DB
}
func NewProductAttrHandler(db *gorm.DB) *ProductAttrHandler {
return &ProductAttrHandler{db: db}
}
// ── 产地(origins) ──────────────────────────────────────────────
func (h *ProductAttrHandler) ListOrigins(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductOriginOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateOrigin(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductOriginOption{
TenantBase: model.TenantBase{ShopID: shopID},
Code: req.Code,
Name: req.Name,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateOrigin(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductOriginOption
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Code = req.Code
item.Name = req.Name
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteOrigin(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductOriginOption{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ── 保质期(shelf-lives) ─────────────────────────────────────────
func (h *ProductAttrHandler) ListShelfLives(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductShelfLifeOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateShelfLife(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductShelfLifeOption{
TenantBase: model.TenantBase{ShopID: shopID},
Code: req.Code,
Name: req.Name,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateShelfLife(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductShelfLifeOption
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Code = req.Code
item.Name = req.Name
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteShelfLife(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductShelfLifeOption{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ── 储存方式(storages) ──────────────────────────────────────────
func (h *ProductAttrHandler) ListStorages(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductStorageOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateStorage(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductStorageOption{
TenantBase: model.TenantBase{ShopID: shopID},
Code: req.Code,
Name: req.Name,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateStorage(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductStorageOption
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Code = req.Code
item.Name = req.Name
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteStorage(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductStorageOption{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ── 描述文档(description-docs) ─────────────────────────────────
func (h *ProductAttrHandler) ListDescriptionDocs(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductDescriptionDoc, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateDescriptionDoc(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Title string `json:"title" binding:"required"`
Content string `json:"content"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductDescriptionDoc{
TenantBase: model.TenantBase{ShopID: shopID},
Title: req.Title,
Content: req.Content,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateDescriptionDoc(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Title string `json:"title" binding:"required"`
Content string `json:"content"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductDescriptionDoc
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Title = req.Title
item.Content = req.Content
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteDescriptionDoc(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductDescriptionDoc{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
+98 -10
View File
@@ -1,7 +1,9 @@
package handler
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -9,6 +11,12 @@ import (
"github.com/wangjia/jiu/backend/internal/model"
)
// 默认话术(保质期/储存方式空时使用)
const (
defaultShelfLife = "无限期(适饮)"
defaultStorage = "阴凉干燥、避光保存"
)
type PublicHandler struct {
db *gorm.DB
}
@@ -24,6 +32,10 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
var product model.Product
if err := h.db.Where("public_id = ? AND deleted_at IS NULL", publicID).
Preload("Images").
Preload("Origin").
Preload("ShelfLife").
Preload("Storage").
Preload("DescriptionDoc").
First(&product).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -60,22 +72,98 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
}
}
// 产地:空串表示无,前端不展示该行
origin := ""
if product.Origin != nil {
origin = product.Origin.Name
}
// 保质期:无关联时用默认话术
shelfLife := defaultShelfLife
if product.ShelfLife != nil {
shelfLife = product.ShelfLife.Name
}
// 储存方式:无关联时用默认话术
storage := defaultStorage
if product.Storage != nil {
storage = product.Storage.Name
}
// 介绍三级回退:描述文档 → 旧 Description → 通用中性兜底
descTitle, descBody, descKeywords := buildDescription(product)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"id": product.ID,
"name": product.Name,
"series": product.Series,
"spec": product.Spec,
"brand": product.Brand,
"unit": product.Unit,
"description": product.Description,
"images": product.Images,
"shop": shopData,
"batch": batchData,
"id": product.ID,
"name": product.Name,
"series": product.Series,
"spec": product.Spec,
"brand": product.Brand,
"unit": product.Unit,
"description": descBody,
"description_title": descTitle,
"description_keywords": descKeywords,
"origin": origin,
"shelf_life": shelfLife,
"storage": storage,
"images": product.Images,
"shop": shopData,
"batch": batchData,
},
})
}
// buildDescription 商品介绍三级回退逻辑
// 1. DescriptionDoc.Content(结构化介绍文档)
// 2. Product.Description(旧自由文本)
// 3. 通用中性兜底(用真实字段拼,不含品牌专属词)
func buildDescription(p model.Product) (title, body string, keywords []string) {
// 1. 描述文档
if p.DescriptionDoc != nil && p.DescriptionDoc.Content != "" {
return p.DescriptionDoc.Title, p.DescriptionDoc.Content, buildKeywords(p)
}
// 2. 旧 Description 自由文本
if p.Description != "" {
return "商品介绍", p.Description, buildKeywords(p)
}
// 3. 通用中性兜底
parts := make([]string, 0, 4)
if p.Brand != "" && p.Name != "" {
parts = append(parts, fmt.Sprintf("%s%s", p.Brand, p.Name))
} else if p.Name != "" {
parts = append(parts, p.Name)
}
if p.Series != "" {
parts = append(parts, fmt.Sprintf("%s系列", p.Series))
}
if p.Spec != "" {
parts = append(parts, fmt.Sprintf("规格%s", p.Spec))
}
intro := strings.Join(parts, "")
if intro != "" {
intro += "。"
}
intro += "本商品由门店正品供应,支持扫码验真,请认准官方渠道。"
return "商品介绍", intro, buildKeywords(p)
}
// buildKeywords 从真实商品属性派生关键词 chips
func buildKeywords(p model.Product) []string {
kws := make([]string, 0, 4)
if p.Brand != "" {
kws = append(kws, p.Brand)
}
if p.Series != "" {
kws = append(kws, p.Series)
}
kws = append(kws, "正品保障", "扫码验真")
return kws
}
// GetRelease GET /api/v1/public/release (no auth)
func (h *PublicHandler) GetRelease(c *gin.Context) {
cfg, err := loadVersionConfig()
+16 -6
View File
@@ -22,11 +22,21 @@ type Product struct {
SalePrice float64 `gorm:"type:decimal(12,2)" json:"sale_price"`
MinStock int `gorm:"default:0" json:"min_stock"`
Description string `gorm:"type:text" json:"description"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
Category *ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
// 商品属性字典外键(可空,公开页展示用)
OriginID *uint64 `json:"origin_id"`
ShelfLifeID *uint64 `json:"shelf_life_id"`
StorageID *uint64 `json:"storage_id"`
DescriptionDocID *uint64 `json:"description_doc_id"`
Category *ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
Origin *ProductOriginOption `gorm:"foreignKey:OriginID" json:"origin,omitempty"`
ShelfLife *ProductShelfLifeOption `gorm:"foreignKey:ShelfLifeID" json:"shelf_life,omitempty"`
Storage *ProductStorageOption `gorm:"foreignKey:StorageID" json:"storage,omitempty"`
DescriptionDoc *ProductDescriptionDoc `gorm:"foreignKey:DescriptionDocID" json:"description_doc,omitempty"`
}
+34
View File
@@ -0,0 +1,34 @@
package model
// ProductOriginOption 产地字典(门店级)
type ProductOriginOption struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name"`
Remark string `gorm:"size:500" json:"remark"`
}
// ProductShelfLifeOption 保质期字典(门店级)
type ProductShelfLifeOption struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name"` // 如「无限期(适饮)」「24个月」
Remark string `gorm:"size:500" json:"remark"`
}
// ProductStorageOption 储存方式字典(门店级)
type ProductStorageOption struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name"` // 如「阴凉干燥、避光保存」
Remark string `gorm:"size:500" json:"remark"`
}
// ProductDescriptionDoc 商品描述文档字典(门店级)
// Title = 酒类型(如「酱香型白酒」),Content = 商品介绍正文
type ProductDescriptionDoc struct {
TenantBase
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text" json:"content"`
Remark string `gorm:"size:500" json:"remark"`
}
+22
View File
@@ -27,6 +27,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
importH := handler.NewImportHandler(db)
userH := handler.NewUserHandler(db)
productOptH := handler.NewProductOptionHandler(db)
productAttrH := handler.NewProductAttrHandler(db)
productImageH := handler.NewProductImageHandler(db)
financeH := handler.NewFinanceHandler(db)
numberRuleH := handler.NewNumberRuleHandler(db)
@@ -217,6 +218,27 @@ func Setup(r *gin.Engine, db *gorm.DB) {
opts.POST("/specs", productOptH.CreateSpec)
opts.PUT("/specs/:id", productOptH.UpdateSpec)
opts.DELETE("/specs/:id", productOptH.DeleteSpec)
// 商品属性字典(产地/保质期/储存方式/描述文档)
opts.GET("/origins", productAttrH.ListOrigins)
opts.POST("/origins", productAttrH.CreateOrigin)
opts.PUT("/origins/:id", productAttrH.UpdateOrigin)
opts.DELETE("/origins/:id", productAttrH.DeleteOrigin)
opts.GET("/shelf-lives", productAttrH.ListShelfLives)
opts.POST("/shelf-lives", productAttrH.CreateShelfLife)
opts.PUT("/shelf-lives/:id", productAttrH.UpdateShelfLife)
opts.DELETE("/shelf-lives/:id", productAttrH.DeleteShelfLife)
opts.GET("/storages", productAttrH.ListStorages)
opts.POST("/storages", productAttrH.CreateStorage)
opts.PUT("/storages/:id", productAttrH.UpdateStorage)
opts.DELETE("/storages/:id", productAttrH.DeleteStorage)
opts.GET("/description-docs", productAttrH.ListDescriptionDocs)
opts.POST("/description-docs", productAttrH.CreateDescriptionDoc)
opts.PUT("/description-docs/:id", productAttrH.UpdateDescriptionDoc)
opts.DELETE("/description-docs/:id", productAttrH.DeleteDescriptionDoc)
}
// 超级管理员专属
+4
View File
@@ -107,6 +107,10 @@ func autoMigrate(db *gorm.DB) {
&model.ProductNameOption{},
&model.ProductSeriesOption{},
&model.ProductSpecOption{},
&model.ProductOriginOption{},
&model.ProductShelfLifeOption{},
&model.ProductStorageOption{},
&model.ProductDescriptionDoc{},
&model.ProductImage{},
&model.ErrorReport{},
&model.Feedback{},
+45 -1
View File
@@ -124,7 +124,51 @@ func SetupTestDB() *gorm.DB {
custom_fields TEXT,
remark TEXT,
name_pinyin TEXT DEFAULT '',
name_initials TEXT DEFAULT ''
name_initials TEXT DEFAULT '',
origin_id INTEGER,
shelf_life_id INTEGER,
storage_id INTEGER,
description_doc_id INTEGER
)`,
`CREATE TABLE IF NOT EXISTS product_origin_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_shelf_life_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_storage_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_description_docs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS warehouses (
id INTEGER PRIMARY KEY AUTOINCREMENT,