material_manage_system/internal/service/application.go
2026-10-09 11:10:13 +08:00

255 lines
6.8 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 (
"errors"
"github.com/shopspring/decimal"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/internal/repository"
)
var (
ErrApplicationNotFound = errors.New("application not found")
ErrCannotCancelApplication = errors.New("cannot cancel application")
ErrApplicationNotPending = errors.New("application is not pending")
)
type ApplicationService struct {
appRepo *repository.ApplicationRepository
materialSvc *MaterialService
stockRepo *repository.StockRepo
}
func NewApplicationService() *ApplicationService {
return &ApplicationService{
appRepo: repository.NewApplicationRepository(),
materialSvc: NewMaterialService(),
stockRepo: repository.NewStockRepo(),
}
}
func (s *ApplicationService) Create(organizationID, userID int64, req *model.CreateApplicationRequest) (*model.MaterialApplication, error) {
// Build items from request
var items []model.ApplicationItem
for _, item := range req.Items {
exists, err := s.materialSvc.Exists(organizationID, item.MaterialID)
if err != nil || !exists {
return nil, ErrMaterialNotFound
}
active, err := s.materialSvc.IsActive(organizationID, item.MaterialID)
if err != nil {
return nil, err
}
if !active {
return nil, ErrMaterialObsolete
}
items = append(items, model.ApplicationItem{
MaterialID: item.MaterialID,
Quantity: item.Quantity,
})
}
app := &model.MaterialApplication{
UserID: userID,
Purpose: req.Purpose,
Status: model.ApplicationStatusPending,
Items: items,
}
if err := s.appRepo.Create(app); err != nil {
return nil, err
}
// Get full details
return s.appRepo.GetByID(app.ID)
}
func (s *ApplicationService) GetByID(id int64) (*model.MaterialApplication, error) {
app, err := s.appRepo.GetByID(id)
if err != nil {
return nil, ErrApplicationNotFound
}
return app, nil
}
func (s *ApplicationService) ListByUser(userID int64, status string, limit, offset int) ([]model.MaterialApplication, int, error) {
apps, total, err := s.appRepo.ListByUserID(userID, status, limit, offset)
if err != nil {
return nil, 0, err
}
if apps == nil {
apps = []model.MaterialApplication{}
}
return apps, total, nil
}
func (s *ApplicationService) ListAll(status string, limit, offset int) ([]model.MaterialApplication, int, error) {
apps, total, err := s.appRepo.ListAllApplications(status, limit, offset)
if err != nil {
return nil, 0, err
}
if apps == nil {
apps = []model.MaterialApplication{}
}
return apps, total, nil
}
func (s *ApplicationService) Approve(organizationID, id int64, approverID int64, autoOutbound bool) error {
app, err := s.appRepo.GetByID(id)
if err != nil {
return ErrApplicationNotFound
}
if app.Status != model.ApplicationStatusPending {
return ErrApplicationNotPending
}
// 库存维度由服务端按当前公司解析;审批自动出库会扣减本公司库存。
scope, err := ResolveInventoryScope(organizationID, true)
if err != nil {
return err
}
// Check stock for all items
for _, item := range app.Items {
stock, err := s.stockRepo.GetStock(scope, item.MaterialID)
if err != nil {
return ErrMaterialNotFound
}
if stock.LessThan(decimal.NewFromInt(int64(item.Quantity))) {
return ErrInsufficientStock
}
}
// Auto outbound if requested
if autoOutbound {
// Start transaction
tx, err := repository.BeginTx()
if err != nil {
return err
}
// 这里不能靠 err 判断是否回滚:事务内每个 if 里的 err 都是 := 声明的局部变量,
// 闭包捕获到的外层 err 始终为 nil,任何一步失败都不会回滚,事务和它持有的行锁
// 会一直挂在连接上不释放。改用标志位,只要没走到提交就一定回滚。
committed := false
defer func() {
if !committed {
tx.Rollback()
}
}()
// Get approver name for handler
approverName, _ := s.appRepo.GetApproverName(approverID)
// Create outbound order within transaction
order := &model.OutboundOrder{
Handler: approverName,
Operator: approverName,
Receiver: app.Username,
Status: "completed",
}
if err := s.appRepo.CreateOutboundOrderTx(tx, order); err != nil {
return err
}
// Process each item within transaction. 按物料 ID 升序条件扣减,余额不足时整体回滚。
itemIDs := make([]int64, len(app.Items))
for i := range app.Items {
itemIDs[i] = app.Items[i].MaterialID
}
for _, idx := range materialSortedIndexes(itemIDs) {
appItem := app.Items[idx]
// Deduct stock within transaction
if err := s.stockRepo.DeductStockTx(tx, scope, appItem.MaterialID, decimal.NewFromInt(int64(appItem.Quantity))); err != nil {
if errors.Is(err, repository.ErrInsufficientStock) {
return ErrInsufficientStock
}
return err
}
// Create outbound item within transaction
item := &model.OutboundItem{
OutboundOrderID: order.ID,
MaterialID: appItem.MaterialID,
Quantity: appItem.Quantity,
}
if err := s.appRepo.CreateOutboundItemTx(tx, item); err != nil {
return err
}
// Create stock log within transaction
stockLog := &model.StockLog{
OrganizationID: scope.OrganizationID,
WarehouseID: scope.WarehouseID,
LocationID: scope.LocationID,
MaterialID: appItem.MaterialID,
Type: "out",
Quantity: appItem.Quantity,
RelatedOrderType: "application",
RelatedOrderID: id,
}
if err := s.appRepo.CreateStockLogTx(tx, stockLog); err != nil {
return err
}
}
// Update status to approved within transaction
if err := s.appRepo.ApproveTx(tx, id, approverID); err != nil {
return err
}
// Commit transaction
if err = tx.Commit(); err != nil {
return err
}
committed = true
} else {
// Non-auto-outbound: just approve
if err := s.appRepo.Approve(id, approverID); err != nil {
return err
}
}
return nil
}
func (s *ApplicationService) Reject(id int64, reason string) error {
app, err := s.appRepo.GetByID(id)
if err != nil {
return ErrApplicationNotFound
}
if app.Status != model.ApplicationStatusPending {
return ErrApplicationNotPending
}
return s.appRepo.Reject(id, reason)
}
func (s *ApplicationService) Cancel(id int64, userID int64) error {
app, err := s.appRepo.GetByID(id)
if err != nil {
return ErrApplicationNotFound
}
// User can only cancel their own applications
if app.UserID != userID {
return errors.New("cannot cancel others' applications")
}
if app.Status != model.ApplicationStatusPending {
return ErrCannotCancelApplication
}
return s.appRepo.Cancel(id)
}
func (s *ApplicationService) GetMaterialStock(organizationID, materialID int64) (decimal.Decimal, error) {
// 只读查询:停用的仓库、库位仍返回其维度,便于核验历史余额。
scope, err := ResolveInventoryScope(organizationID, false)
if err != nil {
return decimal.Zero, err
}
return s.stockRepo.GetStock(scope, materialID)
}