298 lines
7.4 KiB
Go
298 lines
7.4 KiB
Go
package service
|
||
|
||
import (
|
||
"time"
|
||
|
||
"github.com/shopspring/decimal"
|
||
"github.com/wmms/wmms/internal/model"
|
||
"github.com/wmms/wmms/internal/repository"
|
||
)
|
||
|
||
type InboundService struct {
|
||
inboundRepo *repository.InboundRepo
|
||
materialSvc *MaterialService
|
||
stockRepo *repository.StockRepo
|
||
}
|
||
|
||
func NewInboundService() *InboundService {
|
||
return &InboundService{
|
||
inboundRepo: repository.NewInboundRepo(),
|
||
materialSvc: NewMaterialService(),
|
||
stockRepo: repository.NewStockRepo(),
|
||
}
|
||
}
|
||
|
||
// resolveWarningEnabled 决定本次写入预警规则时是否启用。调用方已经确认阈值非 nil。
|
||
// 未显式传 warning_enabled 时默认启用,保持老客户端"带上阈值即启用"的行为。
|
||
func resolveWarningEnabled(enabled *bool) bool {
|
||
if enabled == nil {
|
||
return true
|
||
}
|
||
return *enabled
|
||
}
|
||
|
||
func (s *InboundService) Create(organizationID int64, req *model.CreateInboundRequest, operator string) (*model.InboundOrder, error) {
|
||
order := &model.InboundOrder{
|
||
InboundDate: time.Now(),
|
||
Operator: operator,
|
||
Handler: req.Handler,
|
||
Supplier: req.Supplier,
|
||
Remark: req.Remark,
|
||
Items: make([]model.InboundItem, len(req.Items)),
|
||
}
|
||
|
||
// Validate all items first (outside transaction)
|
||
for i, item := range req.Items {
|
||
exists, err := s.materialSvc.Exists(organizationID, item.MaterialID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !exists {
|
||
return nil, ErrMaterialNotFound
|
||
}
|
||
active, err := s.materialSvc.IsActive(organizationID, item.MaterialID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !active {
|
||
return nil, ErrMaterialObsolete
|
||
}
|
||
if item.WarningStock != nil && item.WarningStock.IsNegative() {
|
||
return nil, ErrInvalidWarningStock
|
||
}
|
||
order.Items[i] = model.InboundItem{
|
||
MaterialID: item.MaterialID,
|
||
Quantity: item.Quantity,
|
||
}
|
||
}
|
||
|
||
// Start transaction
|
||
tx, err := repository.BeginTx()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func() {
|
||
if err != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
// Create inbound order within transaction
|
||
err = s.inboundRepo.CreateTx(tx, order)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Update stock and create logs within transaction
|
||
for i := range order.Items {
|
||
item := &order.Items[i]
|
||
// 这里必须用赋值而不是 := ,否则 err 会被遮蔽,defer 里的回滚判断看不到它。
|
||
var material *model.Material
|
||
material, err = s.materialSvc.GetByID(organizationID, item.MaterialID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
item.MaterialCode = material.MaterialCode
|
||
item.MaterialName = material.Name
|
||
|
||
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(item.Quantity)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 入库明细上带的预警阈值在同一个事务里写入,保证物料的第一个阈值和第一行库存
|
||
// 同时落库。阈值为 nil 表示本次不改动已存的值;阈值为 0 是合法配置,含义是
|
||
// "库存降到 0 时预警"。
|
||
if warningStock := req.Items[i].WarningStock; warningStock != nil {
|
||
err = s.stockRepo.UpdateWarningStockTx(tx, item.MaterialID, *warningStock,
|
||
resolveWarningEnabled(req.Items[i].WarningEnabled))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
stockLog := &model.StockLog{
|
||
MaterialID: item.MaterialID,
|
||
Type: "in",
|
||
Quantity: item.Quantity,
|
||
RelatedOrderType: "inbound",
|
||
RelatedOrderID: order.ID,
|
||
}
|
||
err = s.stockRepo.CreateLogTx(tx, stockLog)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Commit transaction
|
||
if err = tx.Commit(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
order.Status = "completed"
|
||
return order, nil
|
||
}
|
||
|
||
func (s *InboundService) GetByID(id int64) (*model.InboundOrder, error) {
|
||
return s.inboundRepo.GetByID(id)
|
||
}
|
||
|
||
func (s *InboundService) List(search string, limit, offset int) ([]model.InboundOrder, int64, error) {
|
||
return s.inboundRepo.List(search, limit, offset)
|
||
}
|
||
|
||
func (s *InboundService) GetByMaterialID(materialID int64) ([]model.InboundRecord, error) {
|
||
return s.inboundRepo.GetByMaterialID(materialID)
|
||
}
|
||
|
||
func (s *InboundService) Delete(id int64) error {
|
||
// Get the inbound order with items
|
||
order, err := s.inboundRepo.GetByID(id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Start transaction
|
||
tx, err := repository.BeginTx()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() {
|
||
if err != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
// Reverse stock for each item within transaction
|
||
for _, item := range order.Items {
|
||
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(-item.Quantity)))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// Delete stock logs within transaction
|
||
err = s.stockRepo.DeleteLogsTx(tx, "inbound", id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Delete the inbound order (items will be cascade deleted)
|
||
err = s.inboundRepo.DeleteTx(tx, id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Commit transaction
|
||
if err = tx.Commit(); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *InboundService) Update(organizationID, id int64, req *model.UpdateInboundRequest) (*model.InboundOrder, error) {
|
||
// Get old order data
|
||
oldOrder, err := s.inboundRepo.GetByID(id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 阈值校验放在事务之外,避免为非法输入白开一个事务
|
||
for _, item := range req.Items {
|
||
active, err := s.materialSvc.IsActive(organizationID, item.MaterialID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !active {
|
||
return nil, ErrMaterialObsolete
|
||
}
|
||
if item.WarningStock != nil && item.WarningStock.IsNegative() {
|
||
return nil, ErrInvalidWarningStock
|
||
}
|
||
}
|
||
|
||
// Start transaction
|
||
tx, err := repository.BeginTx()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func() {
|
||
if err != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
// Reverse stock for old items (subtract what was previously added)
|
||
for _, item := range oldOrder.Items {
|
||
_, err = s.stockRepo.GetStockForUpdateTx(tx, item.MaterialID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(-item.Quantity)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Delete old stock logs
|
||
err = s.stockRepo.DeleteLogsTx(tx, "inbound", id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Update order in repository
|
||
err = s.inboundRepo.UpdateTx(tx, id, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Add stock for new items and create logs
|
||
for _, item := range req.Items {
|
||
_, err = s.stockRepo.GetStockForUpdateTx(tx, item.MaterialID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
err = s.stockRepo.UpdateStockTx(tx, item.MaterialID, decimal.NewFromInt(int64(item.Quantity)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 阈值随明细一起更新,nil 表示不改动已存的值,避免编辑旧入库单时把刚调好的
|
||
// 阈值覆盖回旧数;启用状态同理,nil 表示不改动。
|
||
if item.WarningStock != nil {
|
||
err = s.stockRepo.UpdateWarningStockTx(tx, item.MaterialID, *item.WarningStock,
|
||
resolveWarningEnabled(item.WarningEnabled))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
stockLog := &model.StockLog{
|
||
MaterialID: item.MaterialID,
|
||
Type: "in",
|
||
Quantity: item.Quantity,
|
||
RelatedOrderType: "inbound",
|
||
RelatedOrderID: id,
|
||
}
|
||
err = s.stockRepo.CreateLogTx(tx, stockLog)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Commit transaction
|
||
if err = tx.Commit(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Get updated order with material details
|
||
updatedOrder, err := s.inboundRepo.GetByID(id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
updatedOrder.Status = "completed"
|
||
|
||
return updatedOrder, nil
|
||
}
|