feat: product.
This commit is contained in:
+4
-1
@@ -7,7 +7,10 @@ import (
|
||||
|
||||
// Gateway 代表一个物联网网关设备
|
||||
type Gateway struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
// MAC地址是网关的唯一标识
|
||||
MAC string `gorm:"size:32;uniqueIndex;not null" json:"mac"`
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductDefinition struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Code string `gorm:"size:64;not null;uniqueIndex" json:"code"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Category string `gorm:"size:64;not null;default:'device';index" json:"category"`
|
||||
Description string `gorm:"size:1024" json:"description"`
|
||||
ParameterSchema string `gorm:"type:text" json:"parameterSchema"`
|
||||
TrackSerialNumber bool `gorm:"not null;default:false" json:"trackSerialNumber"`
|
||||
IsActive bool `gorm:"not null;default:true;index" json:"isActive"`
|
||||
Sort int `gorm:"not null;default:0;index" json:"sort"`
|
||||
CreatedAt int64 `gorm:"not null" json:"created_at"`
|
||||
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (ProductDefinition) TableName() string {
|
||||
return "product_definitions"
|
||||
}
|
||||
|
||||
func (p *ProductDefinition) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
now := time.Now().UnixMilli()
|
||||
p.Code = normalizeProductCodeValue(p.Code)
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
p.Category = normalizeProductCategoryValue(p.Category)
|
||||
p.Description = strings.TrimSpace(p.Description)
|
||||
p.ParameterSchema = normalizeJSONTextValue(p.ParameterSchema)
|
||||
p.CreatedAt = now
|
||||
p.UpdatedAt = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProductDefinition) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
p.Code = normalizeProductCodeValue(p.Code)
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
p.Category = normalizeProductCategoryValue(p.Category)
|
||||
p.Description = strings.TrimSpace(p.Description)
|
||||
p.ParameterSchema = normalizeJSONTextValue(p.ParameterSchema)
|
||||
p.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeProductCodeValue(code string) string {
|
||||
return strings.TrimSpace(strings.ToLower(code))
|
||||
}
|
||||
|
||||
func normalizeProductCategoryValue(category string) string {
|
||||
value := strings.TrimSpace(strings.ToLower(category))
|
||||
if value == "" {
|
||||
return "device"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeJSONTextValue(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if !json.Valid([]byte(trimmed)) {
|
||||
return ""
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
type ProductParameterSchemaField struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
func mustJSON(value interface{}) string {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func EnsureDefaultProductDefinitions(db *gorm.DB) error {
|
||||
defaults := []ProductDefinition{
|
||||
{
|
||||
Code: "charging_case_large",
|
||||
Name: "充电收纳箱(20位)",
|
||||
Category: "container",
|
||||
Description: "可收纳20个手环的大充电收纳箱",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "capacity", Label: "容量", Type: "number", Required: false}, {Key: "powerAdapter", Label: "适配器型号", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 10,
|
||||
},
|
||||
{
|
||||
Code: "charging_case_small",
|
||||
Name: "小充电收纳箱(10位)",
|
||||
Category: "container",
|
||||
Description: "可收纳10个手环的小充电收纳箱",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "capacity", Label: "容量", Type: "number", Required: false}, {Key: "powerAdapter", Label: "适配器型号", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 20,
|
||||
},
|
||||
{
|
||||
Code: "collection_band",
|
||||
Name: "采集手环",
|
||||
Category: "wearable",
|
||||
Description: "用于智能心率采集、50米往返跑等项目的采集手环",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "bandSize", Label: "尺码", Type: "string", Required: false}, {Key: "firmwareVersion", Label: "固件版本", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 30,
|
||||
},
|
||||
{
|
||||
Code: "control_tablet",
|
||||
Name: "控制平板",
|
||||
Category: "tablet",
|
||||
Description: "项目控制与操作使用的平板设备",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "screenSize", Label: "屏幕尺寸", Type: "string", Required: false}, {Key: "osVersion", Label: "系统版本", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 40,
|
||||
},
|
||||
{
|
||||
Code: "collection_gateway",
|
||||
Name: "采集网关",
|
||||
Category: "gateway",
|
||||
Description: "用于设备数据采集与上传的网关",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "mac", Label: "MAC地址", Type: "string", Required: true}, {Key: "location", Label: "安装位置", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 50,
|
||||
},
|
||||
{
|
||||
Code: "display_stand_screen",
|
||||
Name: "闺蜜机",
|
||||
Category: "display",
|
||||
Description: "用于投屏展示的移动显示设备",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "screenSize", Label: "屏幕尺寸", Type: "string", Required: false}, {Key: "resolution", Label: "分辨率", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 60,
|
||||
},
|
||||
{
|
||||
Code: "led_light_strip",
|
||||
Name: "LED灯带",
|
||||
Category: "accessory",
|
||||
Description: "心肺耐力测试配套的LED灯带",
|
||||
ParameterSchema: mustJSON([]ProductParameterSchemaField{{Key: "length", Label: "长度", Type: "string", Required: false}, {Key: "colorMode", Label: "颜色模式", Type: "string", Required: false}}),
|
||||
TrackSerialNumber: true,
|
||||
IsActive: true,
|
||||
Sort: 70,
|
||||
},
|
||||
}
|
||||
|
||||
for _, item := range defaults {
|
||||
var existing ProductDefinition
|
||||
err := db.Where("code = ?", item.Code).First(&existing).Error
|
||||
switch {
|
||||
case err == nil:
|
||||
existing.Name = item.Name
|
||||
existing.Category = item.Category
|
||||
existing.Description = item.Description
|
||||
existing.ParameterSchema = item.ParameterSchema
|
||||
existing.TrackSerialNumber = item.TrackSerialNumber
|
||||
existing.IsActive = item.IsActive
|
||||
existing.Sort = item.Sort
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductInventoryStatus string
|
||||
|
||||
const (
|
||||
ProductInventoryStatusInStock ProductInventoryStatus = "in_stock"
|
||||
ProductInventoryStatusSold ProductInventoryStatus = "sold"
|
||||
ProductInventoryStatusMaintenance ProductInventoryStatus = "maintenance"
|
||||
ProductInventoryStatusRetired ProductInventoryStatus = "retired"
|
||||
)
|
||||
|
||||
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"`
|
||||
SuiteCode string `gorm:"size:128;index" json:"suiteCode"`
|
||||
SerialNumber *string `gorm:"size:128;uniqueIndex" json:"serialNumber"`
|
||||
AssetName string `gorm:"size:255" json:"assetName"`
|
||||
Status ProductInventoryStatus `gorm:"size:32;not null;default:'in_stock';index" json:"status"`
|
||||
RegionID *uint32 `gorm:"index" json:"regionId"`
|
||||
StorageLocation string `gorm:"size:255" json:"storageLocation"`
|
||||
SoldTargetType string `gorm:"size:64;index" json:"soldTargetType"`
|
||||
SoldTargetName string `gorm:"size:255" json:"soldTargetName"`
|
||||
SoldTo string `gorm:"size:255" json:"soldTo"`
|
||||
SoldAt *time.Time `json:"soldAt"`
|
||||
ParameterValues string `gorm:"type:text" json:"parameterValues"`
|
||||
SourceType string `gorm:"size:64;index" json:"sourceType"`
|
||||
SourceRef string `gorm:"size:128;index" json:"sourceRef"`
|
||||
Notes string `gorm:"size:1024" json:"notes"`
|
||||
CreatedAt int64 `gorm:"not null" json:"created_at"`
|
||||
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (ProductInventory) TableName() string {
|
||||
return "product_inventories"
|
||||
}
|
||||
|
||||
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))
|
||||
p.SuiteCode = strings.TrimSpace(p.SuiteCode)
|
||||
p.SerialNumber = normalizeOptionalProductString(p.SerialNumber)
|
||||
p.AssetName = strings.TrimSpace(p.AssetName)
|
||||
p.StorageLocation = strings.TrimSpace(p.StorageLocation)
|
||||
p.SoldTargetType = strings.TrimSpace(strings.ToLower(p.SoldTargetType))
|
||||
p.SoldTargetName = strings.TrimSpace(p.SoldTargetName)
|
||||
p.SoldTo = strings.TrimSpace(p.SoldTo)
|
||||
p.ParameterValues = normalizeJSONTextValue(p.ParameterValues)
|
||||
p.SourceType = strings.TrimSpace(strings.ToLower(p.SourceType))
|
||||
p.SourceRef = strings.TrimSpace(p.SourceRef)
|
||||
p.Notes = strings.TrimSpace(p.Notes)
|
||||
p.Status = normalizeProductInventoryStatus(p.Status)
|
||||
p.CreatedAt = now
|
||||
p.UpdatedAt = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProductInventory) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
p.ProductCode = normalizeProductCodeValue(p.ProductCode)
|
||||
p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode))
|
||||
p.SuiteCode = strings.TrimSpace(p.SuiteCode)
|
||||
p.SerialNumber = normalizeOptionalProductString(p.SerialNumber)
|
||||
p.AssetName = strings.TrimSpace(p.AssetName)
|
||||
p.StorageLocation = strings.TrimSpace(p.StorageLocation)
|
||||
p.SoldTargetType = strings.TrimSpace(strings.ToLower(p.SoldTargetType))
|
||||
p.SoldTargetName = strings.TrimSpace(p.SoldTargetName)
|
||||
p.SoldTo = strings.TrimSpace(p.SoldTo)
|
||||
p.ParameterValues = normalizeJSONTextValue(p.ParameterValues)
|
||||
p.SourceType = strings.TrimSpace(strings.ToLower(p.SourceType))
|
||||
p.SourceRef = strings.TrimSpace(p.SourceRef)
|
||||
p.Notes = strings.TrimSpace(p.Notes)
|
||||
p.Status = normalizeProductInventoryStatus(p.Status)
|
||||
p.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeOptionalProductString(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(*value)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
return &trimmed
|
||||
}
|
||||
|
||||
func normalizeProductInventoryStatus(status ProductInventoryStatus) ProductInventoryStatus {
|
||||
switch ProductInventoryStatus(strings.TrimSpace(strings.ToLower(string(status)))) {
|
||||
case ProductInventoryStatusSold:
|
||||
return ProductInventoryStatusSold
|
||||
case ProductInventoryStatusMaintenance:
|
||||
return ProductInventoryStatusMaintenance
|
||||
case ProductInventoryStatusRetired:
|
||||
return ProductInventoryStatusRetired
|
||||
default:
|
||||
return ProductInventoryStatusInStock
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProjectProductTemplate struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ProjectTypeCode string `gorm:"size:64;not null;index;uniqueIndex:idx_project_product_template" json:"projectTypeCode"`
|
||||
ProductCode string `gorm:"size:64;not null;index;uniqueIndex:idx_project_product_template" json:"productCode"`
|
||||
Quantity *int `json:"quantity"`
|
||||
QuantityRule string `gorm:"size:32;not null;default:'exact'" json:"quantityRule"`
|
||||
IsOptional bool `gorm:"not null;default:false" json:"isOptional"`
|
||||
Notes string `gorm:"size:1024" json:"notes"`
|
||||
Sort int `gorm:"not null;default:0;index" json:"sort"`
|
||||
CreatedAt int64 `gorm:"not null" json:"created_at"`
|
||||
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (ProjectProductTemplate) TableName() string {
|
||||
return "project_product_templates"
|
||||
}
|
||||
|
||||
func (p *ProjectProductTemplate) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
now := time.Now().UnixMilli()
|
||||
p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode))
|
||||
p.ProductCode = normalizeProductCodeValue(p.ProductCode)
|
||||
p.QuantityRule = normalizeQuantityRuleValue(p.QuantityRule)
|
||||
p.Notes = strings.TrimSpace(p.Notes)
|
||||
p.CreatedAt = now
|
||||
p.UpdatedAt = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProjectProductTemplate) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
p.ProjectTypeCode = strings.TrimSpace(strings.ToLower(p.ProjectTypeCode))
|
||||
p.ProductCode = normalizeProductCodeValue(p.ProductCode)
|
||||
p.QuantityRule = normalizeQuantityRuleValue(p.QuantityRule)
|
||||
p.Notes = strings.TrimSpace(p.Notes)
|
||||
p.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeQuantityRuleValue(rule string) string {
|
||||
value := strings.TrimSpace(strings.ToLower(rule))
|
||||
if value == "" {
|
||||
return "exact"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func intPtr(value int) *int {
|
||||
return &value
|
||||
}
|
||||
|
||||
func EnsureDefaultProjectProductTemplates(db *gorm.DB) error {
|
||||
defaults := []ProjectProductTemplate{
|
||||
{ProjectTypeCode: "heartrate", ProductCode: "charging_case_large", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: false, Notes: "可收纳20个手环", Sort: 10},
|
||||
{ProjectTypeCode: "heartrate", ProductCode: "charging_case_small", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: true, Notes: "可选,可收纳10个手环", Sort: 20},
|
||||
{ProjectTypeCode: "heartrate", ProductCode: "collection_band", Quantity: nil, QuantityRule: "flexible", IsOptional: false, Notes: "数量按班级规模或实际采购数量配置", Sort: 30},
|
||||
{ProjectTypeCode: "heartrate", ProductCode: "control_tablet", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: false, Notes: "", Sort: 40},
|
||||
{ProjectTypeCode: "heartrate", ProductCode: "collection_gateway", Quantity: intPtr(1), QuantityRule: "minimum", IsOptional: false, Notes: "至少1个", Sort: 50},
|
||||
{ProjectTypeCode: "heartrate", ProductCode: "display_stand_screen", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: true, Notes: "可选,用于投屏展示", Sort: 60},
|
||||
|
||||
{ProjectTypeCode: "light", ProductCode: "charging_case_large", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: false, Notes: "可收纳20个手环", Sort: 110},
|
||||
{ProjectTypeCode: "light", ProductCode: "charging_case_small", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: true, Notes: "可选,可收纳10个手环", Sort: 120},
|
||||
{ProjectTypeCode: "light", ProductCode: "collection_band", Quantity: nil, QuantityRule: "flexible", IsOptional: false, Notes: "数量按班级规模或实际采购数量配置", Sort: 130},
|
||||
{ProjectTypeCode: "light", ProductCode: "control_tablet", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: false, Notes: "", Sort: 140},
|
||||
{ProjectTypeCode: "light", ProductCode: "collection_gateway", Quantity: intPtr(1), QuantityRule: "minimum", IsOptional: false, Notes: "至少1个", Sort: 150},
|
||||
{ProjectTypeCode: "light", ProductCode: "display_stand_screen", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: true, Notes: "可选,用于投屏展示", Sort: 160},
|
||||
{ProjectTypeCode: "light", ProductCode: "led_light_strip", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: false, Notes: "心肺耐力测试附加LED灯带", Sort: 170},
|
||||
|
||||
{ProjectTypeCode: "run50", ProductCode: "control_tablet", Quantity: intPtr(1), QuantityRule: "exact", IsOptional: false, Notes: "", Sort: 210},
|
||||
{ProjectTypeCode: "run50", ProductCode: "collection_band", Quantity: nil, QuantityRule: "flexible", IsOptional: false, Notes: "数量按测试规模配置", Sort: 220},
|
||||
}
|
||||
|
||||
for _, item := range defaults {
|
||||
var existing ProjectProductTemplate
|
||||
err := db.Where("project_type_code = ? AND product_code = ?", item.ProjectTypeCode, item.ProductCode).First(&existing).Error
|
||||
switch {
|
||||
case err == nil:
|
||||
existing.Quantity = item.Quantity
|
||||
existing.QuantityRule = item.QuantityRule
|
||||
existing.IsOptional = item.IsOptional
|
||||
existing.Notes = item.Notes
|
||||
existing.Sort = item.Sort
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user