material_manage_system/internal/service/stock.go
2026-09-20 14:29:41 +08:00

173 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"database/sql"
"github.com/shopspring/decimal"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/internal/repository"
)
type StockService struct {
stockRepo *repository.StockRepo
}
func NewStockService() *StockService {
return &StockService{
stockRepo: repository.NewStockRepo(),
}
}
// ClassifyStock 与 repository.stockWarningHavingSQL 是同一套规则,后者负责筛出预警列表,
// 两者必须保持一致:只要预警处于启用状态、并且现存量已经触达阈值就是 low。阈值为 0 是
// 合法配置("库存降到 0 时预警"),不再被排除。
//
// 没配过规则和手动关闭预警的物料都由 warningEnabled=false 表达,因此在这里自然落到
// normal——那是操作者自己选择不被告知,不是告警。
func ClassifyStock(totalOnHand, warningStock decimal.Decimal, warningEnabled bool) string {
if !warningEnabled {
return model.StockStatusNormal
}
if totalOnHand.LessThanOrEqual(warningStock) {
return model.StockStatusLow
}
return model.StockStatusNormal
}
func classifyAll(stocks []model.Stock) {
for i := range stocks {
stocks[i].StockStatus = ClassifyStock(stocks[i].TotalOnHand, stocks[i].WarningStock, stocks[i].WarningEnabled)
}
}
func (s *StockService) GetAllStock(search string, limit, offset int) ([]model.Stock, int64, error) {
stocks, total, err := s.stockRepo.GetAllStock(search, limit, offset)
if err != nil {
return nil, 0, err
}
classifyAll(stocks)
return stocks, total, nil
}
func (s *StockService) GetByMaterialID(materialID int64) (*model.Stock, error) {
stock, err := s.stockRepo.GetByMaterialID(materialID)
if err != nil {
return nil, err
}
stock.StockStatus = ClassifyStock(stock.TotalOnHand, stock.WarningStock, stock.WarningEnabled)
return stock, nil
}
func (s *StockService) GetWarningStock(limit, offset int) ([]model.Stock, int64, error) {
stocks, total, err := s.stockRepo.GetWarningStock(limit, offset)
if err != nil {
return nil, 0, err
}
classifyAll(stocks)
return stocks, total, nil
}
func (s *StockService) UpdateWarningStock(materialID int64, minimumAvailable decimal.Decimal, enabled bool) error {
return s.stockRepo.UpdateWarningStock(materialID, minimumAvailable, enabled)
}
// signedChangeQuantity keeps the API contract consistent: inbound changes are
// positive and outbound changes are negative. The type still identifies the
// business operation for display and filtering.
func signedChangeQuantity(logType string, quantity decimal.Decimal) decimal.Decimal {
if logType == "in" {
return quantity
}
return quantity.Neg()
}
func (s *StockService) GetLogsByMaterialID(materialID int64, limit, offset int) ([]model.StockLogResponse, int64, error) {
logs, total, err := s.stockRepo.GetLogsByMaterialID(materialID, limit, offset)
if err != nil {
return nil, 0, err
}
var responses []model.StockLogResponse
balance := decimal.Zero
for _, log := range logs {
quantity := decimal.NewFromInt(int64(log.Quantity))
if log.Type == "in" {
balance = balance.Add(quantity)
} else {
balance = balance.Sub(quantity)
}
changeType := "outbound"
if log.Type == "in" {
changeType = "inbound"
}
beforeQuantity := balance.Sub(quantity)
if log.Type != "in" {
beforeQuantity = balance.Add(quantity)
}
responses = append(responses, model.StockLogResponse{
ID: log.ID,
MaterialID: log.MaterialID,
ChangeType: changeType,
ChangeQuantity: signedChangeQuantity(log.Type, quantity),
BeforeQuantity: beforeQuantity,
AfterQuantity: balance,
ReferenceID: log.RelatedOrderID,
ReferenceType: log.RelatedOrderType,
CreatedAt: log.CreatedAt,
})
}
return responses, total, nil
}
func (s *StockService) GetAllLogs(limit, offset int) ([]model.StockLogResponse, int64, error) {
logs, total, err := s.stockRepo.GetAllLogsWithBalance(limit, offset)
if err != nil {
return nil, 0, err
}
var responses []model.StockLogResponse
for _, log := range logs {
changeType := "outbound"
if log.Type == "in" {
changeType = "inbound"
}
quantity := decimal.NewFromInt(int64(log.Quantity))
beforeQuantity := log.RunningBalance.Sub(quantity)
if log.Type != "in" {
beforeQuantity = log.RunningBalance.Add(quantity)
}
responses = append(responses, model.StockLogResponse{
ID: log.ID,
MaterialID: log.MaterialID,
ChangeType: changeType,
ChangeQuantity: signedChangeQuantity(log.Type, quantity),
BeforeQuantity: beforeQuantity,
AfterQuantity: log.RunningBalance,
ReferenceID: log.RelatedOrderID,
ReferenceType: log.RelatedOrderType,
CreatedAt: log.CreatedAt,
})
}
return responses, total, nil
}
func (s *StockService) MaterialExists(materialID int64) (bool, error) {
_, err := s.stockRepo.GetByMaterialID(materialID)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func (s *StockService) GetInventoryBalanceDetails(materialID int64) ([]model.InventoryBalanceDetail, error) {
return s.stockRepo.GetInventoryBalanceDetails(materialID)
}