342 lines
12 KiB
Go
342 lines
12 KiB
Go
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")
|
|
}
|