feat: product.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"hr_receiver/config"
|
"hr_receiver/config"
|
||||||
"hr_receiver/models"
|
"hr_receiver/models"
|
||||||
@@ -122,6 +123,10 @@ func (gc *GatewayAdminController) Create(c *gin.Context) {
|
|||||||
writeError(c, http.StatusInternalServerError, "保存网关失败")
|
writeError(c, http.StatusInternalServerError, "保存网关失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := gc.syncGatewayInventory(record); err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
writeSuccess(c, http.StatusCreated, "创建成功", record)
|
writeSuccess(c, http.StatusCreated, "创建成功", record)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +181,10 @@ func (gc *GatewayAdminController) Update(c *gin.Context) {
|
|||||||
writeError(c, http.StatusInternalServerError, "更新网关失败")
|
writeError(c, http.StatusInternalServerError, "更新网关失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := gc.syncGatewayInventory(record); err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
writeSuccess(c, http.StatusOK, "更新成功", record)
|
writeSuccess(c, http.StatusOK, "更新成功", record)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +207,10 @@ func (gc *GatewayAdminController) Delete(c *gin.Context) {
|
|||||||
writeError(c, http.StatusInternalServerError, "删除网关失败")
|
writeError(c, http.StatusInternalServerError, "删除网关失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := gc.deleteGatewayInventory(record); err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
writeSuccess(c, http.StatusOK, "删除成功", nil)
|
writeSuccess(c, http.StatusOK, "删除成功", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +245,76 @@ func (gc *GatewayAdminController) findGatewayProjectType(projectTypeCode string)
|
|||||||
return projectType, nil
|
return projectType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gc *GatewayAdminController) syncGatewayInventory(record models.Gateway) error {
|
||||||
|
mac := strings.ToUpper(strings.TrimSpace(record.MAC))
|
||||||
|
if mac == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parameterValuesBytes, err := json.Marshal(map[string]interface{}{
|
||||||
|
"mac": mac,
|
||||||
|
"location": strings.TrimSpace(record.Location),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := models.ProductInventoryStatusInStock
|
||||||
|
if record.IsSold {
|
||||||
|
status = models.ProductInventoryStatusSold
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumber := mac
|
||||||
|
regionID := record.RegionID
|
||||||
|
|
||||||
|
var inventory models.ProductInventory
|
||||||
|
err = gc.DB.Where("source_type = ? AND source_ref = ?", "gateway", mac).First(&inventory).Error
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
inventory.ProductCode = "collection_gateway"
|
||||||
|
inventory.ProjectTypeCode = strings.TrimSpace(strings.ToLower(record.ProjectType))
|
||||||
|
inventory.SerialNumber = &serialNumber
|
||||||
|
inventory.AssetName = strings.TrimSpace(record.Name)
|
||||||
|
inventory.Status = status
|
||||||
|
inventory.RegionID = ®ionID
|
||||||
|
inventory.StorageLocation = strings.TrimSpace(record.Location)
|
||||||
|
inventory.ParameterValues = string(parameterValuesBytes)
|
||||||
|
inventory.SourceType = "gateway"
|
||||||
|
inventory.SourceRef = mac
|
||||||
|
if record.IsSold {
|
||||||
|
inventory.SoldAt = record.SoldAt
|
||||||
|
} else {
|
||||||
|
inventory.SoldAt = nil
|
||||||
|
}
|
||||||
|
return gc.DB.Save(&inventory).Error
|
||||||
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
|
inventory = models.ProductInventory{
|
||||||
|
ProductCode: "collection_gateway",
|
||||||
|
ProjectTypeCode: strings.TrimSpace(strings.ToLower(record.ProjectType)),
|
||||||
|
SerialNumber: &serialNumber,
|
||||||
|
AssetName: strings.TrimSpace(record.Name),
|
||||||
|
Status: status,
|
||||||
|
RegionID: ®ionID,
|
||||||
|
StorageLocation: strings.TrimSpace(record.Location),
|
||||||
|
SoldAt: record.SoldAt,
|
||||||
|
ParameterValues: string(parameterValuesBytes),
|
||||||
|
SourceType: "gateway",
|
||||||
|
SourceRef: mac,
|
||||||
|
}
|
||||||
|
return gc.DB.Create(&inventory).Error
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GatewayAdminController) deleteGatewayInventory(record models.Gateway) error {
|
||||||
|
mac := strings.ToUpper(strings.TrimSpace(record.MAC))
|
||||||
|
if mac == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return gc.DB.Where("source_type = ? AND source_ref = ?", "gateway", mac).Delete(&models.ProductInventory{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
// 辅助方法:验证输入数据
|
// 辅助方法:验证输入数据
|
||||||
func validateGatewayPayload(payload gatewayPayload) error {
|
func validateGatewayPayload(payload gatewayPayload) error {
|
||||||
mac := strings.TrimSpace(payload.MAC)
|
mac := strings.TrimSpace(payload.MAC)
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
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 ProductDefinitionAdminController struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type productDefinitionPayload struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ParameterSchema string `json:"parameterSchema"`
|
||||||
|
TrackSerialNumber bool `json:"trackSerialNumber"`
|
||||||
|
IsActive bool `json:"isActive"`
|
||||||
|
Sort int `json:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductDefinitionAdminController() *ProductDefinitionAdminController {
|
||||||
|
return &ProductDefinitionAdminController{DB: config.DB}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductDefinitionAdminController) List(c *gin.Context) {
|
||||||
|
var items []models.ProductDefinition
|
||||||
|
query := pc.DB.Model(&models.ProductDefinition{}).Order("sort ASC, id ASC")
|
||||||
|
|
||||||
|
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
|
||||||
|
likeValue := "%" + keyword + "%"
|
||||||
|
query = query.Where("code LIKE ? OR name LIKE ? OR description LIKE ?", likeValue, likeValue, likeValue)
|
||||||
|
}
|
||||||
|
if category := strings.TrimSpace(c.Query("category")); category != "" {
|
||||||
|
query = query.Where("category = ?", strings.ToLower(category))
|
||||||
|
}
|
||||||
|
if isActiveStr := strings.TrimSpace(c.Query("isActive")); isActiveStr != "" {
|
||||||
|
isActive, err := strconv.ParseBool(isActiveStr)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, "invalid isActive")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query = query.Where("is_active = ?", isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&items).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to query product definitions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusOK, "query success", items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductDefinitionAdminController) Create(c *gin.Context) {
|
||||||
|
var payload productDefinitionPayload
|
||||||
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateProductDefinitionPayload(payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record := models.ProductDefinition{
|
||||||
|
Code: payload.Code,
|
||||||
|
Name: payload.Name,
|
||||||
|
Category: payload.Category,
|
||||||
|
Description: payload.Description,
|
||||||
|
ParameterSchema: payload.ParameterSchema,
|
||||||
|
TrackSerialNumber: payload.TrackSerialNumber,
|
||||||
|
IsActive: payload.IsActive,
|
||||||
|
Sort: payload.Sort,
|
||||||
|
}
|
||||||
|
if err := pc.DB.Create(&record).Error; err != nil {
|
||||||
|
writeProductDefinitionDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusCreated, "create success", record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductDefinitionAdminController) Update(c *gin.Context) {
|
||||||
|
record, err := pc.findByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
respondProductDefinitionLookupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload productDefinitionPayload
|
||||||
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateProductDefinitionPayload(payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Code = payload.Code
|
||||||
|
record.Name = payload.Name
|
||||||
|
record.Category = payload.Category
|
||||||
|
record.Description = payload.Description
|
||||||
|
record.ParameterSchema = payload.ParameterSchema
|
||||||
|
record.TrackSerialNumber = payload.TrackSerialNumber
|
||||||
|
record.IsActive = payload.IsActive
|
||||||
|
record.Sort = payload.Sort
|
||||||
|
|
||||||
|
if err := pc.DB.Save(&record).Error; err != nil {
|
||||||
|
writeProductDefinitionDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusOK, "update success", record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductDefinitionAdminController) Delete(c *gin.Context) {
|
||||||
|
record, err := pc.findByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
respondProductDefinitionLookupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var inventoryCount int64
|
||||||
|
if err := pc.DB.Model(&models.ProductInventory{}).Where("product_code = ?", record.Code).Count(&inventoryCount).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to check inventory references")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if inventoryCount > 0 {
|
||||||
|
writeError(c, http.StatusConflict, "current product definition is referenced by inventory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateCount int64
|
||||||
|
if err := pc.DB.Model(&models.ProjectProductTemplate{}).Where("product_code = ?", record.Code).Count(&templateCount).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to check project template references")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if templateCount > 0 {
|
||||||
|
writeError(c, http.StatusConflict, "current product definition is referenced by project templates")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pc.DB.Delete(&record).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to delete product definition")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusOK, "delete success", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductDefinitionAdminController) findByID(id string) (models.ProductDefinition, error) {
|
||||||
|
var record models.ProductDefinition
|
||||||
|
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 validateProductDefinitionPayload(payload productDefinitionPayload) 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.Category) == "" {
|
||||||
|
return errors.New("category is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(payload.ParameterSchema) != "" && !json.Valid([]byte(strings.TrimSpace(payload.ParameterSchema))) {
|
||||||
|
return errors.New("parameterSchema must be valid JSON")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondProductDefinitionLookupError(c *gin.Context, err error) {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
writeError(c, http.StatusNotFound, "product definition not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to query product definition")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeProductDefinitionDBError(c *gin.Context, err error) {
|
||||||
|
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
||||||
|
writeError(c, http.StatusConflict, "product code already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to persist product definition")
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"hr_receiver/config"
|
||||||
|
"hr_receiver/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductInventoryAdminController struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type productInventoryPayload struct {
|
||||||
|
ProductCode string `json:"productCode"`
|
||||||
|
ProjectTypeCode string `json:"projectTypeCode"`
|
||||||
|
SuiteCode string `json:"suiteCode"`
|
||||||
|
SerialNumber *string `json:"serialNumber"`
|
||||||
|
AssetName string `json:"assetName"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RegionID *uint32 `json:"regionId"`
|
||||||
|
StorageLocation string `json:"storageLocation"`
|
||||||
|
SoldTargetType string `json:"soldTargetType"`
|
||||||
|
SoldTargetName string `json:"soldTargetName"`
|
||||||
|
SoldTo string `json:"soldTo"`
|
||||||
|
SoldAt *time.Time `json:"soldAt"`
|
||||||
|
ParameterValues string `json:"parameterValues"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductInventoryAdminController() *ProductInventoryAdminController {
|
||||||
|
return &ProductInventoryAdminController{DB: config.DB}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductInventoryAdminController) List(c *gin.Context) {
|
||||||
|
var items []models.ProductInventory
|
||||||
|
query := pc.DB.Model(&models.ProductInventory{}).Order("updated_at DESC, id DESC")
|
||||||
|
|
||||||
|
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
|
||||||
|
likeValue := "%" + keyword + "%"
|
||||||
|
query = query.Where("asset_name LIKE ? OR serial_number LIKE ? OR sold_to LIKE ? OR sold_target_name LIKE ? OR suite_code LIKE ? OR notes LIKE ?", likeValue, likeValue, likeValue, likeValue, 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 suiteCode := strings.TrimSpace(c.Query("suiteCode")); suiteCode != "" {
|
||||||
|
query = query.Where("suite_code = ?", suiteCode)
|
||||||
|
}
|
||||||
|
if status := strings.TrimSpace(c.Query("status")); status != "" {
|
||||||
|
query = query.Where("status = ?", strings.ToLower(status))
|
||||||
|
}
|
||||||
|
if soldTargetType := strings.TrimSpace(c.Query("soldTargetType")); soldTargetType != "" {
|
||||||
|
query = query.Where("sold_target_type = ?", strings.ToLower(soldTargetType))
|
||||||
|
}
|
||||||
|
if regionIDStr := strings.TrimSpace(c.Query("regionId")); regionIDStr != "" {
|
||||||
|
regionID, err := strconv.ParseUint(regionIDStr, 10, 32)
|
||||||
|
if err != nil || regionID == 0 {
|
||||||
|
writeError(c, http.StatusBadRequest, "invalid regionId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query = query.Where("region_id = ?", uint32(regionID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&items).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to query product inventory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusOK, "query success", items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductInventoryAdminController) Create(c *gin.Context) {
|
||||||
|
var payload productInventoryPayload
|
||||||
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := pc.validateInventoryPayload(payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record := models.ProductInventory{
|
||||||
|
ProductCode: payload.ProductCode,
|
||||||
|
ProjectTypeCode: payload.ProjectTypeCode,
|
||||||
|
SuiteCode: payload.SuiteCode,
|
||||||
|
SerialNumber: payload.SerialNumber,
|
||||||
|
AssetName: payload.AssetName,
|
||||||
|
Status: models.ProductInventoryStatus(payload.Status),
|
||||||
|
RegionID: payload.RegionID,
|
||||||
|
StorageLocation: payload.StorageLocation,
|
||||||
|
SoldTargetType: payload.SoldTargetType,
|
||||||
|
SoldTargetName: payload.SoldTargetName,
|
||||||
|
SoldTo: payload.SoldTo,
|
||||||
|
SoldAt: payload.SoldAt,
|
||||||
|
ParameterValues: payload.ParameterValues,
|
||||||
|
Notes: payload.Notes,
|
||||||
|
}
|
||||||
|
pc.applySoldDefaults(&record)
|
||||||
|
|
||||||
|
if err := pc.DB.Create(&record).Error; err != nil {
|
||||||
|
writeProductInventoryDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusCreated, "create success", record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductInventoryAdminController) Update(c *gin.Context) {
|
||||||
|
record, err := pc.findByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
respondProductInventoryLookupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload productInventoryPayload
|
||||||
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := pc.validateInventoryPayload(payload); err != nil {
|
||||||
|
writeError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record.ProductCode = payload.ProductCode
|
||||||
|
record.ProjectTypeCode = payload.ProjectTypeCode
|
||||||
|
record.SuiteCode = payload.SuiteCode
|
||||||
|
record.SerialNumber = payload.SerialNumber
|
||||||
|
record.AssetName = payload.AssetName
|
||||||
|
record.Status = models.ProductInventoryStatus(payload.Status)
|
||||||
|
record.RegionID = payload.RegionID
|
||||||
|
record.StorageLocation = payload.StorageLocation
|
||||||
|
record.SoldTargetType = payload.SoldTargetType
|
||||||
|
record.SoldTargetName = payload.SoldTargetName
|
||||||
|
record.SoldTo = payload.SoldTo
|
||||||
|
record.SoldAt = payload.SoldAt
|
||||||
|
record.ParameterValues = payload.ParameterValues
|
||||||
|
record.Notes = payload.Notes
|
||||||
|
pc.applySoldDefaults(&record)
|
||||||
|
|
||||||
|
if err := pc.DB.Save(&record).Error; err != nil {
|
||||||
|
writeProductInventoryDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusOK, "update success", record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductInventoryAdminController) Delete(c *gin.Context) {
|
||||||
|
record, err := pc.findByID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
respondProductInventoryLookupError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if record.Status == models.ProductInventoryStatusSold {
|
||||||
|
writeError(c, http.StatusConflict, "sold inventory record cannot be deleted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := pc.DB.Delete(&record).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to delete product inventory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(c, http.StatusOK, "delete success", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductInventoryAdminController) findByID(id string) (models.ProductInventory, error) {
|
||||||
|
var record models.ProductInventory
|
||||||
|
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 *ProductInventoryAdminController) validateInventoryPayload(payload productInventoryPayload) error {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(payload.ParameterValues) != "" && !json.Valid([]byte(strings.TrimSpace(payload.ParameterValues))) {
|
||||||
|
return errors.New("parameterValues is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProductInventoryAdminController) applySoldDefaults(record *models.ProductInventory) {
|
||||||
|
if record == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if record.Status == models.ProductInventoryStatusSold {
|
||||||
|
if record.SoldAt == nil {
|
||||||
|
now := time.Now()
|
||||||
|
record.SoldAt = &now
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(record.SoldTo) == "" {
|
||||||
|
record.SoldTo = strings.TrimSpace(record.SoldTargetName)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record.SoldAt = nil
|
||||||
|
record.SoldTo = ""
|
||||||
|
record.SoldTargetType = ""
|
||||||
|
record.SoldTargetName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondProductInventoryLookupError(c *gin.Context, err error) {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
writeError(c, http.StatusNotFound, "product inventory not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to query product inventory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeProductInventoryDBError(c *gin.Context, err error) {
|
||||||
|
if strings.Contains(strings.ToLower(err.Error()), "unique") {
|
||||||
|
writeError(c, http.StatusConflict, "serial number already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to persist product inventory")
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hr_receiver/config"
|
||||||
|
"hr_receiver/models"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectProductTemplateAdminController struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type projectProductTemplateItem struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
ProjectTypeCode string `json:"projectTypeCode"`
|
||||||
|
ProjectTypeName string `json:"projectTypeName"`
|
||||||
|
ProductCode string `json:"productCode"`
|
||||||
|
ProductName string `json:"productName"`
|
||||||
|
Quantity *int `json:"quantity"`
|
||||||
|
QuantityRule string `json:"quantityRule"`
|
||||||
|
IsOptional bool `json:"isOptional"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Sort int `json:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectProductTemplateAdminController() *ProjectProductTemplateAdminController {
|
||||||
|
return &ProjectProductTemplateAdminController{DB: config.DB}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *ProjectProductTemplateAdminController) List(c *gin.Context) {
|
||||||
|
var items []models.ProjectProductTemplate
|
||||||
|
query := pc.DB.Model(&models.ProjectProductTemplate{}).Order("project_type_code ASC, sort ASC, id ASC")
|
||||||
|
|
||||||
|
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 project product templates")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projectTypeCodes := make([]string, 0)
|
||||||
|
projectTypeSet := make(map[string]struct{})
|
||||||
|
productCodes := make([]string, 0)
|
||||||
|
productSet := make(map[string]struct{})
|
||||||
|
for _, item := range items {
|
||||||
|
if _, ok := projectTypeSet[item.ProjectTypeCode]; !ok {
|
||||||
|
projectTypeSet[item.ProjectTypeCode] = struct{}{}
|
||||||
|
projectTypeCodes = append(projectTypeCodes, item.ProjectTypeCode)
|
||||||
|
}
|
||||||
|
if _, ok := productSet[item.ProductCode]; !ok {
|
||||||
|
productSet[item.ProductCode] = struct{}{}
|
||||||
|
productCodes = append(productCodes, item.ProductCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projectTypeNameMap := make(map[string]string)
|
||||||
|
if len(projectTypeCodes) > 0 {
|
||||||
|
var projectTypes []models.ProjectType
|
||||||
|
if err := pc.DB.Where("code IN ?", projectTypeCodes).Find(&projectTypes).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to query project types")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, item := range projectTypes {
|
||||||
|
projectTypeNameMap[item.Code] = item.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productNameMap := make(map[string]string)
|
||||||
|
if len(productCodes) > 0 {
|
||||||
|
var productDefinitions []models.ProductDefinition
|
||||||
|
if err := pc.DB.Where("code IN ?", productCodes).Find(&productDefinitions).Error; err != nil {
|
||||||
|
writeError(c, http.StatusInternalServerError, "failed to query product definitions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, item := range productDefinitions {
|
||||||
|
productNameMap[item.Code] = item.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]projectProductTemplateItem, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
result = append(result, projectProductTemplateItem{
|
||||||
|
ID: item.ID,
|
||||||
|
ProjectTypeCode: item.ProjectTypeCode,
|
||||||
|
ProjectTypeName: projectTypeNameMap[item.ProjectTypeCode],
|
||||||
|
ProductCode: item.ProductCode,
|
||||||
|
ProductName: productNameMap[item.ProductCode],
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
QuantityRule: item.QuantityRule,
|
||||||
|
IsOptional: item.IsOptional,
|
||||||
|
Notes: item.Notes,
|
||||||
|
Sort: item.Sort,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccess(c, http.StatusOK, "query success", result)
|
||||||
|
}
|
||||||
@@ -31,6 +31,9 @@ func main() {
|
|||||||
&models.UserRegionBinding{},
|
&models.UserRegionBinding{},
|
||||||
&models.Kindergarten{},
|
&models.Kindergarten{},
|
||||||
&models.ProjectType{},
|
&models.ProjectType{},
|
||||||
|
&models.ProductDefinition{},
|
||||||
|
&models.ProjectProductTemplate{},
|
||||||
|
&models.ProductInventory{},
|
||||||
&models.AppFile{},
|
&models.AppFile{},
|
||||||
&models.AppFileShareCode{},
|
&models.AppFileShareCode{},
|
||||||
&models.MqttHeartRateRecord{},
|
&models.MqttHeartRateRecord{},
|
||||||
@@ -53,6 +56,12 @@ func main() {
|
|||||||
if err := models.EnsureDefaultProjectTypes(config.DB); err != nil {
|
if err := models.EnsureDefaultProjectTypes(config.DB); err != nil {
|
||||||
log.Printf("default project types init failed: %v", err)
|
log.Printf("default project types init failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := models.EnsureDefaultProductDefinitions(config.DB); err != nil {
|
||||||
|
log.Printf("default product definitions init failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := models.EnsureDefaultProjectProductTemplates(config.DB); err != nil {
|
||||||
|
log.Printf("default project product templates init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := mqtt.Start(config.DB, config.App.MQTT); err != nil {
|
if err := mqtt.Start(config.DB, config.App.MQTT); err != nil {
|
||||||
log.Printf("mqtt listener start failed: %v", err)
|
log.Printf("mqtt listener start failed: %v", err)
|
||||||
|
|||||||
+4
-1
@@ -7,7 +7,10 @@ import (
|
|||||||
|
|
||||||
// Gateway 代表一个物联网网关设备
|
// Gateway 代表一个物联网网关设备
|
||||||
type Gateway struct {
|
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地址是网关的唯一标识
|
||||||
MAC string `gorm:"size:32;uniqueIndex;not null" 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
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ func SetupRouter() *gin.Engine {
|
|||||||
lessonPlanController := controllers.NewLessonPlanController()
|
lessonPlanController := controllers.NewLessonPlanController()
|
||||||
kindergartenAdminController := controllers.NewKindergartenAdminController()
|
kindergartenAdminController := controllers.NewKindergartenAdminController()
|
||||||
projectTypeAdminController := controllers.NewProjectTypeAdminController()
|
projectTypeAdminController := controllers.NewProjectTypeAdminController()
|
||||||
|
productDefinitionAdminController := controllers.NewProductDefinitionAdminController()
|
||||||
|
projectProductTemplateAdminController := controllers.NewProjectProductTemplateAdminController()
|
||||||
|
productInventoryAdminController := controllers.NewProductInventoryAdminController()
|
||||||
userAdminController := controllers.NewUserAdminController()
|
userAdminController := controllers.NewUserAdminController()
|
||||||
gatewayController := controllers.NewGatewayAdminController()
|
gatewayController := controllers.NewGatewayAdminController()
|
||||||
systemDebugController := controllers.NewSystemDebugController()
|
systemDebugController := controllers.NewSystemDebugController()
|
||||||
@@ -79,6 +82,18 @@ func SetupRouter() *gin.Engine {
|
|||||||
admin.PUT("/project-types/:id", projectTypeAdminController.Update)
|
admin.PUT("/project-types/:id", projectTypeAdminController.Update)
|
||||||
admin.DELETE("/project-types/:id", projectTypeAdminController.Delete)
|
admin.DELETE("/project-types/:id", projectTypeAdminController.Delete)
|
||||||
|
|
||||||
|
admin.GET("/product-definitions", productDefinitionAdminController.List)
|
||||||
|
admin.POST("/product-definitions", productDefinitionAdminController.Create)
|
||||||
|
admin.PUT("/product-definitions/:id", productDefinitionAdminController.Update)
|
||||||
|
admin.DELETE("/product-definitions/:id", productDefinitionAdminController.Delete)
|
||||||
|
|
||||||
|
admin.GET("/project-product-templates", projectProductTemplateAdminController.List)
|
||||||
|
|
||||||
|
admin.GET("/product-inventories", productInventoryAdminController.List)
|
||||||
|
admin.POST("/product-inventories", productInventoryAdminController.Create)
|
||||||
|
admin.PUT("/product-inventories/:id", productInventoryAdminController.Update)
|
||||||
|
admin.DELETE("/product-inventories/:id", productInventoryAdminController.Delete)
|
||||||
|
|
||||||
admin.GET("/users", userAdminController.List)
|
admin.GET("/users", userAdminController.List)
|
||||||
admin.POST("/users", userAdminController.Create)
|
admin.POST("/users", userAdminController.Create)
|
||||||
admin.PUT("/users/:id", userAdminController.Update)
|
admin.PUT("/users/:id", userAdminController.Update)
|
||||||
|
|||||||
Reference in New Issue
Block a user