material_manage_system/internal/service/application.go
2026-10-09 10:38:02 +08:00

243 lines
6.3 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(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
}
// Check stock for all items
for _, item := range app.Items {
stock, err := s.stockRepo.GetStock(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
for _, appItem := range app.Items {
// Re-check stock with row lock
lockedStock, lockErr := s.stockRepo.GetStockForUpdateTx(tx, appItem.MaterialID)
err = lockErr
if err != nil {
return ErrMaterialNotFound
}
if lockedStock.LessThan(decimal.NewFromInt(int64(appItem.Quantity))) {
return ErrInsufficientStock
}
// Deduct stock within transaction
if err := s.stockRepo.UpdateStockTx(tx, appItem.MaterialID, decimal.NewFromInt(int64(-appItem.Quantity))); err != nil {
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{
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(materialID int64) (decimal.Decimal, error) {
return s.stockRepo.GetStock(materialID)
}