material_manage_system/internal/service/admin_master.go

358 lines
12 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 (
"errors"
"strings"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/internal/repository"
)
// 本文件里的「编码」字段——分类/单位/仓库/库位的 code、伙伴的 partner_code——
// 一律不可变。它们是编码规则前缀的来源(init.sql 里 code_rules 的种子数据直接取
// material_categories.code)或者对外的业务编号,改掉会让已经生成的历史编码、单据
// 与之脱节,而且事后查不回来。前端编辑时这些输入框都是 :disabled 的,与之对应:
// 各 Update* 既不校验也不使用请求体里的编码,repository 的 UPDATE 也不写这些列。
// 因此 Update* 只校验名称等可改字段——从前这里复用 Create* 的校验,要求编码非空,
// 形成「必须传、传了却被丢弃」的怪要求。
var errInvalidMasterData = errors.New("code and name are required")
var errUpdateRequiresID = errors.New("id is required for update")
var ErrProtectedRole = repository.ErrProtectedRole
type OrganizationService struct {
repo *repository.OrganizationRepository
}
func NewOrganizationService() *OrganizationService {
return &OrganizationService{repo: repository.NewOrganizationRepository()}
}
func (s *OrganizationService) List() ([]model.Organization, error) { return s.repo.List() }
func (s *OrganizationService) ListRoles() ([]model.Role, error) { return s.repo.ListRoles() }
func (s *OrganizationService) ListPermissions() ([]model.Permission, error) {
return s.repo.ListPermissions()
}
func (s *OrganizationService) UpdateRole(roleID int64, req *model.UpdateRoleRequest) error {
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
return errors.New("role name is required")
}
return s.repo.UpdateRole(roleID, req.Name, req.Enabled, req.PermissionCodes)
}
func (s *OrganizationService) Create(item *model.Organization) error {
item.Code = strings.TrimSpace(item.Code)
item.Name = strings.TrimSpace(item.Name)
if item.Code == "" || item.Name == "" {
return errInvalidMasterData
}
applyDefaults(&item.Status, &item.DefaultTimezone)
return s.repo.Create(item)
}
func (s *OrganizationService) Update(item *model.Organization) error {
item.Name = strings.TrimSpace(item.Name)
if item.Name == "" {
return errInvalidMasterData
}
applyDefaults(&item.Status, &item.DefaultTimezone)
return s.repo.Update(item)
}
func applyDefaults(status, timezone *string) {
if *status == "" {
*status = "active"
}
if *timezone == "" {
*timezone = "Asia/Shanghai"
}
}
type MasterDataService struct {
repo *repository.MasterDataRepository
}
func NewMasterDataService() *MasterDataService {
return &MasterDataService{repo: repository.NewMasterDataRepository()}
}
func (s *MasterDataService) ListCategories(orgID int64) ([]model.MaterialCategory, error) {
return s.repo.ListCategories(orgID)
}
// CreateCategory 新建分类。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。
func (s *MasterDataService) CreateCategory(item *model.MaterialCategory) error {
normalizeCategory(item)
if item.Code == "" || item.Name == "" {
return errInvalidMasterData
}
return s.repo.CreateCategory(item)
}
// UpdateCategory 更新分类。id 来自路由参数,必须显式存在。
// code 不可变,理由见文件头的约定:这里只认名称、描述、上级和状态。
func (s *MasterDataService) UpdateCategory(item *model.MaterialCategory) error {
if item.ID == 0 {
return errUpdateRequiresID
}
normalizeCategory(item)
if item.Name == "" {
return errInvalidMasterData
}
return s.repo.UpdateCategory(item)
}
// normalizeCategory 是新建与更新共用的字段清理和默认值补全。
// 编码是否必填由两个入口各自判断:新建必填,更新不看。
func normalizeCategory(item *model.MaterialCategory) {
item.Code = strings.TrimSpace(item.Code)
item.Name = strings.TrimSpace(item.Name)
item.Description = strings.TrimSpace(item.Description)
if item.Status == "" {
item.Status = "active"
}
}
func (s *MasterDataService) ListUnits(orgID int64) ([]model.Unit, error) {
return s.repo.ListUnits(orgID)
}
// CreateUnit 新建单位。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。
func (s *MasterDataService) CreateUnit(item *model.Unit) error {
if err := normalizeUnit(item); err != nil {
return err
}
if item.Code == "" || item.Name == "" {
return errInvalidMasterData
}
return s.repo.CreateUnit(item)
}
// UpdateUnit 更新单位。id 来自路由参数,必须显式存在。
// code 不可变,理由见文件头的约定;小数位是可以改的。
func (s *MasterDataService) UpdateUnit(item *model.Unit) error {
if item.ID == 0 {
return errUpdateRequiresID
}
if err := normalizeUnit(item); err != nil {
return err
}
if item.Name == "" {
return errInvalidMasterData
}
return s.repo.UpdateUnit(item)
}
// normalizeUnit 是新建与更新共用的字段清理、默认值补全和小数位值域校验。
// 小数位在编辑时也能改,所以两个入口都要查。编码是否必填由两个入口各自判断。
func normalizeUnit(item *model.Unit) error {
item.Code = strings.TrimSpace(item.Code)
item.Name = strings.TrimSpace(item.Name)
if item.Status == "" {
item.Status = "active"
}
if item.PrecisionScale < 0 || item.PrecisionScale > 6 {
return errors.New("precision_scale must be between 0 and 6")
}
return nil
}
func (s *MasterDataService) ListCodeRules(orgID int64) ([]model.CodeRule, error) {
return s.repo.ListCodeRules(orgID)
}
// NextMaterialCode 预览新增物料时按小类规则自动生成的编码。这里只读不落库,
// 返回的编码只是候选值;真正占号发生在创建物料的事务里。
func (s *MasterDataService) NextMaterialCode(orgID, categoryID int64) (*model.NextMaterialCode, error) {
rule, err := s.repo.FindActiveCodeRule(orgID, categoryID)
if err != nil {
return nil, err
}
if rule == nil {
return &model.NextMaterialCode{Available: false}, nil
}
return &model.NextMaterialCode{Available: true, Code: rule.FormatCode(rule.NextSequence)}, nil
}
// CreateCodeRule 新建编码规则。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。
func (s *MasterDataService) CreateCodeRule(item *model.CodeRule) error {
if err := prepareCodeRule(item); err != nil {
return err
}
return s.repo.CreateCodeRule(item)
}
// UpdateCodeRule 更新编码规则。id 来自路由参数,必须显式存在。
func (s *MasterDataService) UpdateCodeRule(item *model.CodeRule) error {
if item.ID == 0 {
return errUpdateRequiresID
}
if err := prepareCodeRule(item); err != nil {
return err
}
return s.repo.UpdateCodeRule(item)
}
// prepareCodeRule 是新建与更新共用的字段清理、校验和默认值补全。
func prepareCodeRule(item *model.CodeRule) error {
item.Prefix = strings.TrimSpace(item.Prefix)
if item.CategoryID <= 0 || item.Prefix == "" {
return errors.New("category_id and prefix are required")
}
if item.SequenceLength < 1 || item.SequenceLength > 12 {
return errors.New("sequence_length must be between 1 and 12")
}
if item.NextSequence <= 0 {
item.NextSequence = 1
}
if item.Status == "" {
item.Status = "active"
}
return nil
}
type PartnerService struct{ repo *repository.PartnerRepository }
func NewPartnerService() *PartnerService {
return &PartnerService{repo: repository.NewPartnerRepository()}
}
func (s *PartnerService) List(orgID int64, role string, limit, offset int, category string) ([]model.BusinessPartner, int64, error) {
if role != "" && role != "supplier" && role != "customer" {
return nil, 0, errors.New("role must be supplier or customer")
}
return s.repo.List(orgID, role, limit, offset, category)
}
// Create 新建伙伴。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。
func (s *PartnerService) Create(item *model.BusinessPartner) error {
if err := normalizePartner(item); err != nil {
return err
}
if item.PartnerCode == "" || item.LegalName == "" {
return errors.New("partner_code and legal_name are required")
}
return s.repo.Create(item)
}
// Update 更新伙伴。id 来自路由参数,必须显式存在。
// partner_code 不可变,理由见文件头的约定:它是伙伴对外的业务编号,
// 单据引用的就是它。这里只认企业名称、角色、结算等可改字段。
func (s *PartnerService) Update(item *model.BusinessPartner) error {
if item.ID == 0 {
return errUpdateRequiresID
}
if err := normalizePartner(item); err != nil {
return err
}
if item.LegalName == "" {
return errors.New("legal_name is required")
}
return s.repo.Update(item)
}
// normalizePartner 是新建与更新共用的字段清理、默认值补全和角色校验:
// 至少要有供应商或客户其中一个角色,编辑时也可能把两个都取消掉。
// 编码是否必填由两个入口各自判断。
func normalizePartner(item *model.BusinessPartner) error {
item.PartnerCode = strings.TrimSpace(item.PartnerCode)
item.LegalName = strings.TrimSpace(item.LegalName)
if !item.IsSupplier && !item.IsCustomer {
return errors.New("at least one partner role is required")
}
for _, categories := range []*[]string{&item.SupplierCategories, &item.CustomerCategories} {
if *categories == nil {
continue // 更新时未传分类保留原值;显式空数组表示清空。
}
normalized := make([]string, 0, 2)
seen := make(map[string]bool)
for _, category := range *categories {
if category != "material" && category != "finished_product" {
return errors.New("业务分类只能选择物料级(生产)或成品级(经营买卖)")
}
if !seen[category] {
normalized = append(normalized, category)
seen[category] = true
}
}
*categories = normalized
}
if item.IsSupplier && item.SupplierStatus == "" {
item.SupplierStatus = "active"
}
if item.IsCustomer && item.CustomerStatus == "" {
item.CustomerStatus = "active"
}
return nil
}
type WarehouseService struct {
repo *repository.WarehouseRepository
}
func NewWarehouseService() *WarehouseService {
return &WarehouseService{repo: repository.NewWarehouseRepository()}
}
func (s *WarehouseService) List(orgID int64) ([]model.Warehouse, error) {
return s.repo.List(orgID)
}
// Create 新建仓库。不读取 item.ID,脏 id 不会把新建变成覆盖。
func (s *WarehouseService) Create(item *model.Warehouse) error {
normalizeWarehouse(item)
if item.Code == "" || item.Name == "" {
return errInvalidMasterData
}
return s.repo.Create(item)
}
// Update 更新仓库。id 来自路由参数,必须显式存在。
// code 不可变,理由见文件头的约定。
func (s *WarehouseService) Update(item *model.Warehouse) error {
if item.ID == 0 {
return errUpdateRequiresID
}
normalizeWarehouse(item)
if item.Name == "" {
return errInvalidMasterData
}
return s.repo.Update(item)
}
// normalizeWarehouse 是新建与更新共用的字段清理和默认值补全。
// 编码是否必填由两个入口各自判断:新建必填,更新不看。
func normalizeWarehouse(item *model.Warehouse) {
item.Code = strings.TrimSpace(item.Code)
item.Name = strings.TrimSpace(item.Name)
if item.Status == "" {
item.Status = "active"
}
}
// CreateLocation 新建库位。orgID 用于校验仓库归属,item.ID 不参与。
func (s *WarehouseService) CreateLocation(item *model.WarehouseLocation, orgID int64) error {
normalizeLocation(item)
if item.Code == "" || item.Name == "" || item.WarehouseID <= 0 {
return errInvalidMasterData
}
return s.repo.CreateLocation(item, orgID)
}
// UpdateLocation 更新库位。id 来自路由参数,必须显式存在。
// code 不可变,理由见文件头的约定;warehouse_id 由路由给出,不能搬到别的仓库去。
func (s *WarehouseService) UpdateLocation(item *model.WarehouseLocation, orgID int64) error {
if item.ID == 0 {
return errUpdateRequiresID
}
normalizeLocation(item)
if item.Name == "" || item.WarehouseID <= 0 {
return errInvalidMasterData
}
return s.repo.UpdateLocation(item, orgID)
}
// normalizeLocation 是新建与更新共用的字段清理和默认值补全。
// 编码是否必填由两个入口各自判断:新建必填,更新不看。
func normalizeLocation(item *model.WarehouseLocation) {
item.Code = strings.TrimSpace(item.Code)
item.Name = strings.TrimSpace(item.Name)
if item.Status == "" {
item.Status = "active"
}
}