前端 stores/stock.js 一直在发 category,但 handler 只读 search,service 与 repository 的方法签名里也没有这个参数,整条链路把它静默丢弃,于是选完分类 点「查询」没有任何反应。前端本来就传对了,无需改动。 repository 侧按大类 code 过滤(material_categories 里 parent_id 为空的那层, 即 SQL 里的 pc.code),与列表「大类类型」列同口径,前端下拉的 value 正是大类 code。注意 GetAllStock 的 COUNT 查询原本没有 JOIN 分类表,加 pc.code 前必须 补上与主查询一致的 LEFT JOIN,否则分页总数会和实际行数对不上、末尾多出空白 页;两张表都按主键关联,实测计数不变。小类码不会误命中(category=A01 返回 0)。 顺带修掉同一功能里的耦合缺陷:updateWarningStock 刷新列表时不传筛选参数, 筛选一旦生效,保存预警就会把列表刷回全量、而下拉框仍显示选中的分类。store 新增 stockFilters 记住当前条件,刷新时沿用。 stock_test.go 里 COUNT 的正则随 SQL 变形同步放宽,新增 TestStockRepoGetAllStockAppliesCategoryToBothQueries 覆盖两条 SQL 的成对条件 与参数顺序;前端补 4 条用例(store 沿用/清空筛选、组件查询与翻页传递分类)。
173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
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, category string, limit, offset int) ([]model.Stock, int64, error) {
|
||
stocks, total, err := s.stockRepo.GetAllStock(search, category, 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)
|
||
}
|