material_manage_system/internal/service/material.go

266 lines
7.4 KiB
Go
Raw Permalink 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"
"errors"
"log"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/internal/repository"
)
var ErrInvalidMaterialCategory = errors.New("请选择有效的小类类型")
// ErrInvalidMaterialUnit 表示物料引用的基本单位无效:要么不属于当前公司,
// 要么已停用。创建以及"改选"单位时都要求是本公司启用中的单位。
var ErrInvalidMaterialUnit = errors.New("请选择有效的单位")
type MaterialService struct {
repo *repository.MaterialRepo
logSvc *MaterialLogService
}
func NewMaterialService() *MaterialService {
return &MaterialService{
repo: repository.NewMaterialRepo(),
logSvc: NewMaterialLogService(),
}
}
func (s *MaterialService) validateCategory(categoryID *int64, organizationID int64) error {
if categoryID == nil || *categoryID <= 0 {
return ErrInvalidMaterialCategory
}
exists, err := s.repo.IsActiveSubcategory(*categoryID, organizationID)
if err != nil {
return err
}
if !exists {
return ErrInvalidMaterialCategory
}
return nil
}
// validateUnit 要求单位属于当前公司且启用。unitID 为 nil 表示本次不设置单位,
// 直接通过;更新时"保留原先已停用引用"的分支由调用方先判断后跳过本校验。
func (s *MaterialService) validateUnit(unitID *int64, organizationID int64) error {
if unitID == nil {
return nil
}
if *unitID <= 0 {
return ErrInvalidMaterialUnit
}
active, err := s.repo.IsActiveUnit(*unitID, organizationID)
if err != nil {
return err
}
if !active {
return ErrInvalidMaterialUnit
}
return nil
}
func (s *MaterialService) Create(req *model.CreateMaterialRequest, operator string) (*model.Material, error) {
organizationID := req.OrganizationID
if organizationID <= 0 {
return nil, ErrInvalidOrganizationID
}
if err := s.validateCategory(req.CategoryID, organizationID); err != nil {
return nil, err
}
// 单位校验必须在写库之前:引用非法时直接失败,既不落库也不推进编码号段。
if err := s.validateUnit(req.BaseUnitID, organizationID); err != nil {
return nil, err
}
unit := req.Unit
if unit == "" {
unit = "pcs"
}
var materialImage *string
if req.MaterialImage != nil {
materialImage = req.MaterialImage
}
m := &model.Material{
OrganizationID: organizationID,
MaterialCode: req.MaterialCode,
Name: req.Name,
MaterialImage: materialImage,
Spec: req.Spec,
CategoryID: req.CategoryID,
BaseUnitID: req.BaseUnitID,
Unit: unit,
Status: "active",
Version: 1,
}
if req.ManufacturerModel != nil {
m.ManufacturerModel = req.ManufacturerModel
}
// 编码由提交值和编码规则共同决定:值仍等于规则产物说明用户没改过,落库时
// 顺延号段;改过(或者小类没有规则)则原样保存,号段不动。
err := s.repo.CreateWithCodeRule(m, req.MaterialCode)
if err != nil {
return nil, err
}
// 记录日志
if err := s.logSvc.LogCreate(m, operator); err != nil {
log.Printf("Warning: failed to create material log: %v", err)
}
return m, nil
}
// Obsolete 作废本公司物料。物料读取错误不再忽略:不存在(含不属于本公司)会
// 以 sql.ErrNoRows 冒到接口,映射成 404。状态更新与变更日志在同一事务里完成,
// 日志写失败则整体回滚,不会出现"改了状态却没有日志"。
func (s *MaterialService) Obsolete(organizationID, id int64, operator string) error {
material, err := s.GetByID(organizationID, id)
if err != nil {
return err
}
tx, err := repository.BeginTx()
if err != nil {
return err
}
defer tx.Rollback()
if err := s.repo.ObsoleteTx(tx, organizationID, id); err != nil {
return err
}
updated := *material
updated.Status = "obsolete"
updated.Version++
if err := s.logSvc.LogUpdateTx(tx, &updated, material, operator); err != nil {
return err
}
return tx.Commit()
}
// Restore 恢复本公司物料,语义与 Obsolete 对称。
func (s *MaterialService) Restore(organizationID, id int64, operator string) error {
material, err := s.GetByID(organizationID, id)
if err != nil {
return err
}
tx, err := repository.BeginTx()
if err != nil {
return err
}
defer tx.Rollback()
if err := s.repo.RestoreTx(tx, organizationID, id); err != nil {
return err
}
updated := *material
updated.Status = "active"
updated.Version++
if err := s.logSvc.LogUpdateTx(tx, &updated, material, operator); err != nil {
return err
}
return tx.Commit()
}
func (s *MaterialService) GetByID(organizationID, id int64) (*model.Material, error) {
m := &model.Material{}
err := s.repo.GetByID(organizationID, id, m)
if err != nil {
return nil, err
}
return m, nil
}
func (s *MaterialService) List(organizationID int64, search, status string, limit, offset int) ([]model.Material, int64, error) {
return s.repo.List(organizationID, search, status, limit, offset)
}
func (s *MaterialService) Update(organizationID, id int64, req *model.UpdateMaterialRequest, operator string) (*model.Material, error) {
// 获取修改前的数据;不存在或不属于本公司时直接以 sql.ErrNoRows 结束。
oldMaterial, err := s.GetByID(organizationID, id)
if err != nil {
return nil, err
}
if req.CategoryID == nil || *req.CategoryID <= 0 {
return nil, ErrInvalidMaterialCategory
}
if oldMaterial.CategoryID == nil || *req.CategoryID != *oldMaterial.CategoryID {
if err := s.validateCategory(req.CategoryID, organizationID); err != nil {
return nil, err
}
}
// 单位:改选了就必须是本公司启用项;保持原值(哪怕原本已停用)不校验。
if req.BaseUnitID != nil && (oldMaterial.BaseUnitID == nil || *req.BaseUnitID != *oldMaterial.BaseUnitID) {
if err := s.validateUnit(req.BaseUnitID, organizationID); err != nil {
return nil, err
}
}
err = s.repo.Update(organizationID, id, req)
if err != nil {
return nil, err
}
// 获取修改后的数据
newMaterial, err := s.GetByID(organizationID, id)
if err != nil {
log.Printf("Warning: failed to get new material for logging: %v", err)
}
// 记录日志
if err := s.logSvc.LogUpdate(newMaterial, oldMaterial, operator); err != nil {
log.Printf("Warning: failed to create material log: %v", err)
}
return newMaterial, nil
}
func (s *MaterialService) Delete(organizationID, id int64, operator string) error {
// 获取删除前的数据;不存在或不属于本公司时不再继续。
material, err := s.GetByID(organizationID, id)
if err != nil {
return err
}
if err := s.repo.Delete(organizationID, id); err != nil {
return err
}
// 记录日志
if err := s.logSvc.LogDelete(material, operator); err != nil {
log.Printf("Warning: failed to create material log: %v", err)
}
return nil
}
func (s *MaterialService) BelongsToCompany(organizationID, id int64) (bool, error) {
return s.repo.Exists(organizationID, id)
}
func (s *MaterialService) Exists(organizationID, id int64) (bool, error) {
m := &model.Material{}
err := s.repo.GetByID(organizationID, id, m)
if err == sql.ErrNoRows {
return false, nil
}
return err == nil, err
}
// IsActive is intentionally separate from Exists: historical documents still
// need to resolve retired materials, while new warehouse operations do not.
func (s *MaterialService) IsActive(organizationID, id int64) (bool, error) {
m := &model.Material{}
err := s.repo.GetByID(organizationID, id, m)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return m.Status == "active", nil
}