From f30cc1ea46a7b2150b3a8f56f4be728463f2b9db Mon Sep 17 00:00:00 2001 From: laoboli <1293528695@qq.com> Date: Fri, 1 May 2026 09:26:41 +0800 Subject: [PATCH] feat: product proto type. --- controllers/product_inventory_admin.go | 43 ++++- controllers/product_prototype_admin.go | 191 +++++++++++++++++++ controllers/product_suite_admin.go | 244 +++++++++++++++++++++++++ main.go | 5 + models/product_inventory.go | 7 + models/product_prototype.go | 114 ++++++++++++ models/product_suite.go | 49 +++++ routes/routes.go | 12 ++ 8 files changed, 663 insertions(+), 2 deletions(-) create mode 100644 controllers/product_prototype_admin.go create mode 100644 controllers/product_suite_admin.go create mode 100644 models/product_prototype.go create mode 100644 models/product_suite.go diff --git a/controllers/product_inventory_admin.go b/controllers/product_inventory_admin.go index 4927034..3c12469 100644 --- a/controllers/product_inventory_admin.go +++ b/controllers/product_inventory_admin.go @@ -21,6 +21,7 @@ type ProductInventoryAdminController struct { type productInventoryPayload struct { ProductCode string `json:"productCode"` ProjectTypeCode string `json:"projectTypeCode"` + SuiteID *uint `json:"suiteId"` SuiteCode string `json:"suiteCode"` SerialNumber *string `json:"serialNumber"` AssetName string `json:"assetName"` @@ -88,11 +89,17 @@ func (pc *ProductInventoryAdminController) Create(c *gin.Context) { writeError(c, http.StatusBadRequest, err.Error()) return } + resolvedSuiteID, resolvedSuiteCode, err := pc.resolveSuite(payload.SuiteID, payload.SuiteCode) + if err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } record := models.ProductInventory{ ProductCode: payload.ProductCode, ProjectTypeCode: payload.ProjectTypeCode, - SuiteCode: payload.SuiteCode, + SuiteID: resolvedSuiteID, + SuiteCode: resolvedSuiteCode, SerialNumber: payload.SerialNumber, AssetName: payload.AssetName, Status: models.ProductInventoryStatus(payload.Status), @@ -130,10 +137,16 @@ func (pc *ProductInventoryAdminController) Update(c *gin.Context) { writeError(c, http.StatusBadRequest, err.Error()) return } + resolvedSuiteID, resolvedSuiteCode, err := pc.resolveSuite(payload.SuiteID, payload.SuiteCode) + if err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } record.ProductCode = payload.ProductCode record.ProjectTypeCode = payload.ProjectTypeCode - record.SuiteCode = payload.SuiteCode + record.SuiteID = resolvedSuiteID + record.SuiteCode = resolvedSuiteCode record.SerialNumber = payload.SerialNumber record.AssetName = payload.AssetName record.Status = models.ProductInventoryStatus(payload.Status) @@ -219,6 +232,32 @@ func (pc *ProductInventoryAdminController) validateInventoryPayload(payload prod return nil } +func (pc *ProductInventoryAdminController) resolveSuite(suiteID *uint, suiteCode string) (*uint, string, error) { + if suiteID != nil && *suiteID > 0 { + var suite models.ProductSuite + if err := pc.DB.First(&suite, *suiteID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, "", errors.New("suiteId is invalid") + } + return nil, "", errors.New("failed to query product suite") + } + return &suite.ID, suite.Code, nil + } + + if trimmedCode := strings.TrimSpace(suiteCode); trimmedCode != "" { + var suite models.ProductSuite + if err := pc.DB.Where("code = ?", trimmedCode).First(&suite).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, "", errors.New("suiteCode is invalid") + } + return nil, "", errors.New("failed to query product suite") + } + return &suite.ID, suite.Code, nil + } + + return nil, "", nil +} + func (pc *ProductInventoryAdminController) applySoldDefaults(record *models.ProductInventory) { if record == nil { return diff --git a/controllers/product_prototype_admin.go b/controllers/product_prototype_admin.go new file mode 100644 index 0000000..0d56fb3 --- /dev/null +++ b/controllers/product_prototype_admin.go @@ -0,0 +1,191 @@ +package controllers + +import ( + "encoding/json" + "errors" + "hr_receiver/config" + "hr_receiver/models" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type ProductPrototypeAdminController struct { + DB *gorm.DB +} + +type productPrototypePayload struct { + Name string `json:"name"` + ProductCode string `json:"productCode"` + ProjectTypeCode string `json:"projectTypeCode"` + AssetName string `json:"assetName"` + Status string `json:"status"` + RegionID *uint32 `json:"regionId"` + StorageLocation string `json:"storageLocation"` + ParameterValues string `json:"parameterValues"` + Notes string `json:"notes"` +} + +func NewProductPrototypeAdminController() *ProductPrototypeAdminController { + return &ProductPrototypeAdminController{DB: config.DB} +} + +func (pc *ProductPrototypeAdminController) List(c *gin.Context) { + var items []models.ProductPrototype + query := pc.DB.Model(&models.ProductPrototype{}).Order("updated_at DESC, id DESC") + + if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" { + likeValue := "%" + keyword + "%" + query = query.Where("name LIKE ? OR notes LIKE ?", likeValue, likeValue) + } + if productCode := strings.TrimSpace(c.Query("productCode")); productCode != "" { + query = query.Where("product_code = ?", strings.ToLower(productCode)) + } + if projectTypeCode := strings.TrimSpace(c.Query("projectTypeCode")); projectTypeCode != "" { + query = query.Where("project_type_code = ?", strings.ToLower(projectTypeCode)) + } + + if err := query.Find(&items).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to query product prototypes") + return + } + writeSuccess(c, http.StatusOK, "query success", items) +} + +func (pc *ProductPrototypeAdminController) Create(c *gin.Context) { + var payload productPrototypePayload + if err := c.ShouldBindJSON(&payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + if err := pc.validatePayload(payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + + record := models.ProductPrototype{ + Name: payload.Name, + ProductCode: payload.ProductCode, + ProjectTypeCode: payload.ProjectTypeCode, + AssetName: payload.AssetName, + Status: payload.Status, + RegionID: payload.RegionID, + StorageLocation: payload.StorageLocation, + ParameterValues: payload.ParameterValues, + Notes: payload.Notes, + } + if err := pc.DB.Create(&record).Error; err != nil { + if strings.Contains(strings.ToLower(err.Error()), "unique") { + writeError(c, http.StatusConflict, "prototype already exists") + return + } + writeError(c, http.StatusInternalServerError, "failed to create prototype") + return + } + writeSuccess(c, http.StatusCreated, "create success", record) +} + +func (pc *ProductPrototypeAdminController) Update(c *gin.Context) { + record, err := pc.findByID(c.Param("id")) + if err != nil { + respondProductPrototypeLookupError(c, err) + return + } + + var payload productPrototypePayload + if err := c.ShouldBindJSON(&payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + if err := pc.validatePayload(payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + + record.Name = payload.Name + record.ProductCode = payload.ProductCode + record.ProjectTypeCode = payload.ProjectTypeCode + record.AssetName = payload.AssetName + record.Status = payload.Status + record.RegionID = payload.RegionID + record.StorageLocation = payload.StorageLocation + record.ParameterValues = payload.ParameterValues + record.Notes = payload.Notes + + if err := pc.DB.Save(&record).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to update prototype") + return + } + writeSuccess(c, http.StatusOK, "update success", record) +} + +func (pc *ProductPrototypeAdminController) Delete(c *gin.Context) { + record, err := pc.findByID(c.Param("id")) + if err != nil { + respondProductPrototypeLookupError(c, err) + return + } + if err := pc.DB.Delete(&record).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to delete prototype") + return + } + writeSuccess(c, http.StatusOK, "delete success", nil) +} + +func (pc *ProductPrototypeAdminController) findByID(id string) (models.ProductPrototype, error) { + var record models.ProductPrototype + numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64) + if err != nil { + return record, gorm.ErrRecordNotFound + } + if err := pc.DB.First(&record, numericID).Error; err != nil { + return record, err + } + return record, nil +} + +func (pc *ProductPrototypeAdminController) validatePayload(payload productPrototypePayload) error { + if strings.TrimSpace(payload.Name) == "" { + return errors.New("name is required") + } + productCode := strings.TrimSpace(strings.ToLower(payload.ProductCode)) + if productCode == "" { + return errors.New("productCode is required") + } + var product models.ProductDefinition + if err := pc.DB.Where("code = ? AND is_active = ?", productCode, true).First(&product).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("productCode is invalid") + } + return errors.New("failed to query product definition") + } + if strings.TrimSpace(payload.ProjectTypeCode) != "" { + var projectType models.ProjectType + if err := pc.DB.Where("code = ? AND is_active = ?", strings.ToLower(strings.TrimSpace(payload.ProjectTypeCode)), true).First(&projectType).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("projectTypeCode is invalid") + } + return errors.New("failed to query project type") + } + } + switch models.ProductInventoryStatus(strings.ToLower(strings.TrimSpace(payload.Status))) { + case "", models.ProductInventoryStatusInStock, models.ProductInventoryStatusSold, models.ProductInventoryStatusMaintenance, models.ProductInventoryStatusRetired: + default: + return errors.New("status is invalid") + } + if strings.TrimSpace(payload.ParameterValues) != "" && !json.Valid([]byte(strings.TrimSpace(payload.ParameterValues))) { + return errors.New("parameterValues is invalid") + } + return nil +} + +func respondProductPrototypeLookupError(c *gin.Context, err error) { + if errors.Is(err, gorm.ErrRecordNotFound) { + writeError(c, http.StatusNotFound, "product prototype not found") + return + } + writeError(c, http.StatusInternalServerError, "failed to query product prototype") +} diff --git a/controllers/product_suite_admin.go b/controllers/product_suite_admin.go new file mode 100644 index 0000000..defeb10 --- /dev/null +++ b/controllers/product_suite_admin.go @@ -0,0 +1,244 @@ +package controllers + +import ( + "errors" + "hr_receiver/config" + "hr_receiver/models" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type ProductSuiteAdminController struct { + DB *gorm.DB +} + +type productSuitePayload struct { + Code string `json:"code"` + Name string `json:"name"` + ProjectTypeCode string `json:"projectTypeCode"` + RegionID *uint32 `json:"regionId"` + SoldTargetType string `json:"soldTargetType"` + SoldTargetName string `json:"soldTargetName"` + Notes string `json:"notes"` + InventoryIDs []uint `json:"inventoryIds"` +} + +type productSuiteListItem struct { + models.ProductSuite + Inventories []models.ProductInventory `json:"inventories"` +} + +func NewProductSuiteAdminController() *ProductSuiteAdminController { + return &ProductSuiteAdminController{DB: config.DB} +} + +func (pc *ProductSuiteAdminController) List(c *gin.Context) { + var suites []models.ProductSuite + query := pc.DB.Model(&models.ProductSuite{}).Order("updated_at DESC, id DESC") + + if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" { + likeValue := "%" + keyword + "%" + query = query.Where("code LIKE ? OR name LIKE ? OR notes LIKE ?", likeValue, likeValue, likeValue) + } + if projectTypeCode := strings.TrimSpace(c.Query("projectTypeCode")); projectTypeCode != "" { + query = query.Where("project_type_code = ?", strings.ToLower(projectTypeCode)) + } + + if err := query.Find(&suites).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to query product suites") + return + } + + result := make([]productSuiteListItem, 0, len(suites)) + for _, suite := range suites { + var inventories []models.ProductInventory + if err := pc.DB.Where("suite_id = ?", suite.ID).Order("updated_at DESC, id DESC").Find(&inventories).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to query suite inventories") + return + } + result = append(result, productSuiteListItem{ + ProductSuite: suite, + Inventories: inventories, + }) + } + + writeSuccess(c, http.StatusOK, "query success", result) +} + +func (pc *ProductSuiteAdminController) Create(c *gin.Context) { + var payload productSuitePayload + if err := c.ShouldBindJSON(&payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + if err := pc.validatePayload(payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + + record := models.ProductSuite{ + Code: payload.Code, + Name: payload.Name, + ProjectTypeCode: payload.ProjectTypeCode, + RegionID: payload.RegionID, + SoldTargetType: payload.SoldTargetType, + SoldTargetName: payload.SoldTargetName, + Notes: payload.Notes, + } + + if err := pc.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&record).Error; err != nil { + return err + } + return pc.syncSuiteInventories(tx, record, payload.InventoryIDs) + }); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "unique") { + writeError(c, http.StatusConflict, "suite code already exists") + return + } + writeError(c, http.StatusInternalServerError, "failed to create suite") + return + } + + writeSuccess(c, http.StatusCreated, "create success", record) +} + +func (pc *ProductSuiteAdminController) Update(c *gin.Context) { + record, err := pc.findByID(c.Param("id")) + if err != nil { + respondProductSuiteLookupError(c, err) + return + } + + var payload productSuitePayload + if err := c.ShouldBindJSON(&payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + if err := pc.validatePayload(payload); err != nil { + writeError(c, http.StatusBadRequest, err.Error()) + return + } + + record.Code = payload.Code + record.Name = payload.Name + record.ProjectTypeCode = payload.ProjectTypeCode + record.RegionID = payload.RegionID + record.SoldTargetType = payload.SoldTargetType + record.SoldTargetName = payload.SoldTargetName + record.Notes = payload.Notes + + if err := pc.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(&record).Error; err != nil { + return err + } + return pc.syncSuiteInventories(tx, record, payload.InventoryIDs) + }); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "unique") { + writeError(c, http.StatusConflict, "suite code already exists") + return + } + writeError(c, http.StatusInternalServerError, "failed to update suite") + return + } + + writeSuccess(c, http.StatusOK, "update success", record) +} + +func (pc *ProductSuiteAdminController) Delete(c *gin.Context) { + record, err := pc.findByID(c.Param("id")) + if err != nil { + respondProductSuiteLookupError(c, err) + return + } + + if err := pc.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&models.ProductInventory{}).Where("suite_id = ?", record.ID).Updates(map[string]interface{}{ + "suite_id": nil, + "suite_code": "", + }).Error; err != nil { + return err + } + return tx.Delete(&record).Error + }); err != nil { + writeError(c, http.StatusInternalServerError, "failed to delete suite") + return + } + + writeSuccess(c, http.StatusOK, "delete success", nil) +} + +func (pc *ProductSuiteAdminController) findByID(id string) (models.ProductSuite, error) { + var record models.ProductSuite + numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64) + if err != nil { + return record, gorm.ErrRecordNotFound + } + if err := pc.DB.First(&record, numericID).Error; err != nil { + return record, err + } + return record, nil +} + +func (pc *ProductSuiteAdminController) validatePayload(payload productSuitePayload) error { + if strings.TrimSpace(payload.Code) == "" { + return errors.New("code is required") + } + if strings.TrimSpace(payload.Name) == "" { + return errors.New("name is required") + } + if strings.TrimSpace(payload.ProjectTypeCode) != "" { + var projectType models.ProjectType + if err := pc.DB.Where("code = ? AND is_active = ?", strings.ToLower(strings.TrimSpace(payload.ProjectTypeCode)), true).First(&projectType).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("projectTypeCode is invalid") + } + return errors.New("failed to query project type") + } + } + if strings.TrimSpace(payload.SoldTargetType) != "" { + switch strings.ToLower(strings.TrimSpace(payload.SoldTargetType)) { + case "kindergarten", "school", "dealer", "person", "other": + default: + return errors.New("soldTargetType is invalid") + } + } + return nil +} + +func (pc *ProductSuiteAdminController) syncSuiteInventories(tx *gorm.DB, suite models.ProductSuite, inventoryIDs []uint) error { + if err := tx.Model(&models.ProductInventory{}).Where("suite_id = ?", suite.ID).Updates(map[string]interface{}{ + "suite_id": nil, + "suite_code": "", + }).Error; err != nil { + return err + } + if len(inventoryIDs) == 0 { + return nil + } + + var count int64 + if err := tx.Model(&models.ProductInventory{}).Where("id IN ?", inventoryIDs).Count(&count).Error; err != nil { + return err + } + if count != int64(len(inventoryIDs)) { + return errors.New("some inventory records do not exist") + } + + return tx.Model(&models.ProductInventory{}).Where("id IN ?", inventoryIDs).Updates(map[string]interface{}{ + "suite_id": suite.ID, + "suite_code": suite.Code, + }).Error +} + +func respondProductSuiteLookupError(c *gin.Context, err error) { + if errors.Is(err, gorm.ErrRecordNotFound) { + writeError(c, http.StatusNotFound, "product suite not found") + return + } + writeError(c, http.StatusInternalServerError, "failed to query product suite") +} diff --git a/main.go b/main.go index 7d2efee..c82d220 100644 --- a/main.go +++ b/main.go @@ -32,8 +32,10 @@ func main() { &models.Kindergarten{}, &models.ProjectType{}, &models.ProductDefinition{}, + &models.ProductPrototype{}, &models.ProjectProductTemplate{}, &models.ProductInventory{}, + &models.ProductSuite{}, &models.AppFile{}, &models.AppFileShareCode{}, &models.MqttHeartRateRecord{}, @@ -59,6 +61,9 @@ func main() { if err := models.EnsureDefaultProductDefinitions(config.DB); err != nil { log.Printf("default product definitions init failed: %v", err) } + if err := models.EnsureDefaultProductPrototypes(config.DB); err != nil { + log.Printf("default product prototypes init failed: %v", err) + } if err := models.EnsureDefaultProjectProductTemplates(config.DB); err != nil { log.Printf("default project product templates init failed: %v", err) } diff --git a/models/product_inventory.go b/models/product_inventory.go index c6e23b6..0e25912 100644 --- a/models/product_inventory.go +++ b/models/product_inventory.go @@ -20,6 +20,7 @@ type ProductInventory struct { ID uint `gorm:"primaryKey" json:"id"` ProductCode string `gorm:"size:64;not null;index" json:"productCode"` ProjectTypeCode string `gorm:"size:64;index" json:"projectTypeCode"` + SuiteID *uint `gorm:"index" json:"suiteId"` SuiteCode string `gorm:"size:128;index" json:"suiteCode"` SerialNumber *string `gorm:"size:128;uniqueIndex" json:"serialNumber"` AssetName string `gorm:"size:255" json:"assetName"` @@ -46,6 +47,9 @@ func (p *ProductInventory) BeforeCreate(tx *gorm.DB) (err error) { now := time.Now().UnixMilli() p.ProductCode = normalizeProductCodeValue(p.ProductCode) p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode)) + if p.SuiteID != nil && *p.SuiteID == 0 { + p.SuiteID = nil + } p.SuiteCode = strings.TrimSpace(p.SuiteCode) p.SerialNumber = normalizeOptionalProductString(p.SerialNumber) p.AssetName = strings.TrimSpace(p.AssetName) @@ -66,6 +70,9 @@ func (p *ProductInventory) BeforeCreate(tx *gorm.DB) (err error) { func (p *ProductInventory) BeforeUpdate(tx *gorm.DB) (err error) { p.ProductCode = normalizeProductCodeValue(p.ProductCode) p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode)) + if p.SuiteID != nil && *p.SuiteID == 0 { + p.SuiteID = nil + } p.SuiteCode = strings.TrimSpace(p.SuiteCode) p.SerialNumber = normalizeOptionalProductString(p.SerialNumber) p.AssetName = strings.TrimSpace(p.AssetName) diff --git a/models/product_prototype.go b/models/product_prototype.go new file mode 100644 index 0000000..6e7574f --- /dev/null +++ b/models/product_prototype.go @@ -0,0 +1,114 @@ +package models + +import ( + "errors" + "strings" + "time" + + "gorm.io/gorm" +) + +type ProductPrototype struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + ProductCode string `gorm:"size:64;not null;index" json:"productCode"` + ProjectTypeCode string `gorm:"size:64;index" json:"projectTypeCode"` + AssetName string `gorm:"size:255" json:"assetName"` + Status string `gorm:"size:32;not null;default:'in_stock'" json:"status"` + RegionID *uint32 `gorm:"index" json:"regionId"` + StorageLocation string `gorm:"size:255" json:"storageLocation"` + ParameterValues string `gorm:"type:text" json:"parameterValues"` + Notes string `gorm:"size:1024" json:"notes"` + CreatedAt int64 `gorm:"not null" json:"created_at"` + UpdatedAt int64 `gorm:"not null" json:"updated_at"` +} + +func (ProductPrototype) TableName() string { + return "product_prototypes" +} + +func (p *ProductPrototype) BeforeCreate(tx *gorm.DB) (err error) { + now := time.Now().UnixMilli() + p.Name = strings.TrimSpace(p.Name) + p.ProductCode = normalizeProductCodeValue(p.ProductCode) + p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode)) + p.AssetName = strings.TrimSpace(p.AssetName) + p.Status = string(normalizeProductInventoryStatus(ProductInventoryStatus(p.Status))) + if p.RegionID != nil && *p.RegionID == 0 { + p.RegionID = nil + } + p.StorageLocation = strings.TrimSpace(p.StorageLocation) + p.ParameterValues = normalizeJSONTextValue(p.ParameterValues) + p.Notes = strings.TrimSpace(p.Notes) + p.CreatedAt = now + p.UpdatedAt = now + return nil +} + +func (p *ProductPrototype) BeforeUpdate(tx *gorm.DB) (err error) { + p.Name = strings.TrimSpace(p.Name) + p.ProductCode = normalizeProductCodeValue(p.ProductCode) + p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode)) + p.AssetName = strings.TrimSpace(p.AssetName) + p.Status = string(normalizeProductInventoryStatus(ProductInventoryStatus(p.Status))) + if p.RegionID != nil && *p.RegionID == 0 { + p.RegionID = nil + } + p.StorageLocation = strings.TrimSpace(p.StorageLocation) + p.ParameterValues = normalizeJSONTextValue(p.ParameterValues) + p.Notes = strings.TrimSpace(p.Notes) + p.UpdatedAt = time.Now().UnixMilli() + return nil +} + +func EnsureDefaultProductPrototypes(db *gorm.DB) error { + defaults := []ProductPrototype{ + { + Name: "心率手环默认原型", + ProductCode: "collection_band", + ProjectTypeCode: "heartrate", + AssetName: "智能心率采集手环", + Status: string(ProductInventoryStatusInStock), + ParameterValues: mustJSON(map[string]interface{}{ + "bandSize": "M", + "firmwareVersion": "default", + }), + Notes: "用于批量新增智能心率采集手环时复用通用参数", + }, + { + Name: "50米手环默认原型", + ProductCode: "collection_band", + ProjectTypeCode: "run50", + AssetName: "50米往返跑手环", + Status: string(ProductInventoryStatusInStock), + ParameterValues: mustJSON(map[string]interface{}{ + "bandSize": "M", + "firmwareVersion": "default", + }), + Notes: "用于50米往返跑手环的默认参数", + }, + } + + for _, item := range defaults { + var existing ProductPrototype + err := db.Where("name = ? AND product_code = ? AND project_type_code = ?", item.Name, item.ProductCode, item.ProjectTypeCode).First(&existing).Error + switch { + case err == nil: + existing.AssetName = item.AssetName + existing.Status = item.Status + existing.ParameterValues = item.ParameterValues + existing.Notes = item.Notes + if err := db.Save(&existing).Error; err != nil { + return err + } + case errors.Is(err, gorm.ErrRecordNotFound): + if err := db.Create(&item).Error; err != nil { + return err + } + default: + return err + } + } + + return nil +} diff --git a/models/product_suite.go b/models/product_suite.go new file mode 100644 index 0000000..495572f --- /dev/null +++ b/models/product_suite.go @@ -0,0 +1,49 @@ +package models + +import ( + "strings" + "time" + + "gorm.io/gorm" +) + +type ProductSuite struct { + ID uint `gorm:"primaryKey" json:"id"` + Code string `gorm:"size:128;not null;uniqueIndex" json:"code"` + Name string `gorm:"size:255;not null" json:"name"` + ProjectTypeCode string `gorm:"size:64;index" json:"projectTypeCode"` + RegionID *uint32 `gorm:"index" json:"regionId"` + SoldTargetType string `gorm:"size:64;index" json:"soldTargetType"` + SoldTargetName string `gorm:"size:255" json:"soldTargetName"` + Notes string `gorm:"size:1024" json:"notes"` + CreatedAt int64 `gorm:"not null" json:"created_at"` + UpdatedAt int64 `gorm:"not null" json:"updated_at"` +} + +func (ProductSuite) TableName() string { + return "product_suites" +} + +func (p *ProductSuite) BeforeCreate(tx *gorm.DB) (err error) { + now := time.Now().UnixMilli() + p.Code = strings.TrimSpace(p.Code) + p.Name = strings.TrimSpace(p.Name) + p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode)) + p.SoldTargetType = strings.TrimSpace(strings.ToLower(p.SoldTargetType)) + p.SoldTargetName = strings.TrimSpace(p.SoldTargetName) + p.Notes = strings.TrimSpace(p.Notes) + p.CreatedAt = now + p.UpdatedAt = now + return nil +} + +func (p *ProductSuite) BeforeUpdate(tx *gorm.DB) (err error) { + p.Code = strings.TrimSpace(p.Code) + p.Name = strings.TrimSpace(p.Name) + p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode)) + p.SoldTargetType = strings.TrimSpace(strings.ToLower(p.SoldTargetType)) + p.SoldTargetName = strings.TrimSpace(p.SoldTargetName) + p.Notes = strings.TrimSpace(p.Notes) + p.UpdatedAt = time.Now().UnixMilli() + return nil +} diff --git a/routes/routes.go b/routes/routes.go index 8b6eee5..f4471a3 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -17,8 +17,10 @@ func SetupRouter() *gin.Engine { kindergartenAdminController := controllers.NewKindergartenAdminController() projectTypeAdminController := controllers.NewProjectTypeAdminController() productDefinitionAdminController := controllers.NewProductDefinitionAdminController() + productPrototypeAdminController := controllers.NewProductPrototypeAdminController() projectProductTemplateAdminController := controllers.NewProjectProductTemplateAdminController() productInventoryAdminController := controllers.NewProductInventoryAdminController() + productSuiteAdminController := controllers.NewProductSuiteAdminController() userAdminController := controllers.NewUserAdminController() gatewayController := controllers.NewGatewayAdminController() systemDebugController := controllers.NewSystemDebugController() @@ -87,6 +89,11 @@ func SetupRouter() *gin.Engine { admin.PUT("/product-definitions/:id", productDefinitionAdminController.Update) admin.DELETE("/product-definitions/:id", productDefinitionAdminController.Delete) + admin.GET("/product-prototypes", productPrototypeAdminController.List) + admin.POST("/product-prototypes", productPrototypeAdminController.Create) + admin.PUT("/product-prototypes/:id", productPrototypeAdminController.Update) + admin.DELETE("/product-prototypes/:id", productPrototypeAdminController.Delete) + admin.GET("/project-product-templates", projectProductTemplateAdminController.List) admin.GET("/product-inventories", productInventoryAdminController.List) @@ -94,6 +101,11 @@ func SetupRouter() *gin.Engine { admin.PUT("/product-inventories/:id", productInventoryAdminController.Update) admin.DELETE("/product-inventories/:id", productInventoryAdminController.Delete) + admin.GET("/product-suites", productSuiteAdminController.List) + admin.POST("/product-suites", productSuiteAdminController.Create) + admin.PUT("/product-suites/:id", productSuiteAdminController.Update) + admin.DELETE("/product-suites/:id", productSuiteAdminController.Delete) + admin.GET("/users", userAdminController.List) admin.POST("/users", userAdminController.Create) admin.PUT("/users/:id", userAdminController.Update)