feat: product.

This commit is contained in:
2026-05-01 07:46:48 +08:00
parent 79515007b7
commit 7b43ccf42f
10 changed files with 1070 additions and 1 deletions
+83
View File
@@ -1,6 +1,7 @@
package controllers
import (
"encoding/json"
"errors"
"hr_receiver/config"
"hr_receiver/models"
@@ -122,6 +123,10 @@ func (gc *GatewayAdminController) Create(c *gin.Context) {
writeError(c, http.StatusInternalServerError, "保存网关失败")
return
}
if err := gc.syncGatewayInventory(record); err != nil {
writeError(c, http.StatusInternalServerError, err.Error())
return
}
writeSuccess(c, http.StatusCreated, "创建成功", record)
}
@@ -176,6 +181,10 @@ func (gc *GatewayAdminController) Update(c *gin.Context) {
writeError(c, http.StatusInternalServerError, "更新网关失败")
return
}
if err := gc.syncGatewayInventory(record); err != nil {
writeError(c, http.StatusInternalServerError, err.Error())
return
}
writeSuccess(c, http.StatusOK, "更新成功", record)
}
@@ -198,6 +207,10 @@ func (gc *GatewayAdminController) Delete(c *gin.Context) {
writeError(c, http.StatusInternalServerError, "删除网关失败")
return
}
if err := gc.deleteGatewayInventory(record); err != nil {
writeError(c, http.StatusInternalServerError, err.Error())
return
}
writeSuccess(c, http.StatusOK, "删除成功", nil)
}
@@ -232,6 +245,76 @@ func (gc *GatewayAdminController) findGatewayProjectType(projectTypeCode string)
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 = &regionID
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: &regionID,
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 {
mac := strings.TrimSpace(payload.MAC)
+199
View File
@@ -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")
}
+256
View File
@@ -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)
}