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"` SuiteID *uint `json:"suiteId"` 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} } // @Summary 获取产品库存列表 // @Description 查询产品库存列表,支持多条件筛选 // @Tags 产品库存管理 // @Produce json // @Param keyword query string false "关键词(资产名称/序列号/购买方等模糊搜索)" // @Param productCode query string false "产品代码筛选" // @Param projectTypeCode query string false "项目类型代码筛选" // @Param suiteCode query string false "套件代码筛选" // @Param status query string false "状态筛选" // @Param soldTargetType query string false "购买方类型筛选" // @Param regionId query int false "区域ID" // @Security BearerAuth // @Success 200 {object} SwagAPIResponse "查询成功" // @Router /admin/product-inventories [get] 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) } // @Summary 创建产品库存记录 // @Description 创建新的产品库存记录 // @Tags 产品库存管理 // @Accept json // @Produce json // @Param inventory body productInventoryPayload true "库存信息" // @Security BearerAuth // @Success 201 {object} SwagAPIResponse "创建成功" // @Failure 400 {object} SwagAPIResponse "请求参数错误" // @Router /admin/product-inventories [post] 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 } 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, SuiteID: resolvedSuiteID, SuiteCode: resolvedSuiteCode, 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) } // @Summary 更新产品库存 // @Description 更新指定产品库存记录 // @Tags 产品库存管理 // @Accept json // @Produce json // @Param id path int true "库存ID" // @Param inventory body productInventoryPayload true "更新信息" // @Security BearerAuth // @Success 200 {object} SwagAPIResponse "更新成功" // @Failure 400 {object} SwagAPIResponse "请求参数错误" // @Failure 404 {object} SwagAPIResponse "库存记录不存在" // @Router /admin/product-inventories/{id} [put] 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 } 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.SuiteID = resolvedSuiteID record.SuiteCode = resolvedSuiteCode 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) } // @Summary 删除产品库存 // @Description 删除指定产品库存记录(已售出的无法删除) // @Tags 产品库存管理 // @Produce json // @Param id path int true "库存ID" // @Security BearerAuth // @Success 200 {object} SwagAPIResponse "删除成功" // @Failure 404 {object} SwagAPIResponse "库存记录不存在" // @Failure 409 {object} SwagAPIResponse "已售出无法删除" // @Router /admin/product-inventories/{id} [delete] 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) 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 } 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") }