package service import ( "fmt" "mime/multipart" "os" "path" "strings" "time" "github.com/rovina/zsp-backend/internal/dto" "github.com/rovina/zsp-backend/internal/model" "github.com/rovina/zsp-backend/internal/repository" ) type ZSPContractService interface { CreateFolder(req *dto.ZSPFolderCreateRequest) error GetFolders() ([]model.ZSPContractFolder, error) GetFolderById(folderID string) (*model.ZSPContractFolder, error) DeleteFolderById(folderID string) error UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error GetContractById(contractID string) (*model.ZSPContractFile, error) GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) ListContracts(folderID string) ([]model.ZSPContractFile, error) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) DeleteContractById(contractID string) error UpdateContract(contract *model.ZSPContractFile) error UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error // 合同详情相关 CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) DeleteContractDetailById(detailID string) error BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error } type zspContractService struct { repo repository.ZSPContractRepository } func NewZSPContractService(repo repository.ZSPContractRepository) (ZSPContractService, error) { return &zspContractService{repo: repo}, nil } func (s *zspContractService) CreateFolder(req *dto.ZSPFolderCreateRequest) error { var folder model.ZSPContractFolder folder.Name = req.Name folder.Description = req.Description folder.Count = 0 folder.CreateTime = time.Now() return s.repo.CreateFolder(&folder) } func (s *zspContractService) GetFolders() ([]model.ZSPContractFolder, error) { folders, err := s.repo.GetFolders() if err != nil { return nil, err } return folders, nil } func (s *zspContractService) GetFolderById(folderID string) (*model.ZSPContractFolder, error) { return s.repo.GetFolderById(folderID) } func (s *zspContractService) DeleteFolderById(folderID string) error { return s.repo.DeleteFolderById(folderID) } func (s *zspContractService) UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error { folder, err := s.repo.GetFolderById(folderID) if err != nil { return err } folder.Name = req.Name folder.Description = req.Description return s.repo.UpdateFolder(folder) } func (s *zspContractService) UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error { timeNow := time.Now() var contract model.ZSPContractFile contract.FolderID = req.FolderID contract.Commodity = req.Commodity contract.CompanyName = req.CompanyName contract.ContractNumber = req.ContractNumber contract.ContractType = req.ContractType contract.FileName = fileHeader.Filename // 提取扩展名便于前端展示(如 pdf、jpg) if ext := path.Ext(fileHeader.Filename); ext != "" { contract.FileType = strings.TrimPrefix(strings.ToLower(ext), ".") } else { contract.FileType = fileHeader.Header.Get("Content-Type") } if req.SignDate == "" { return fmt.Errorf("签订日期为必填项") } signDate, err := time.Parse("2006-01-02", req.SignDate) if err != nil { return fmt.Errorf("签订日期格式不正确,请使用 YYYY-MM-DD: %w", err) } contract.SignDate = signDate contract.UploadTime = timeNow // 确保目录存在后保存文件 saveDir := "./data/zsp-contract/files" if err := os.MkdirAll(saveDir, os.ModePerm); err != nil { return fmt.Errorf("create upload dir: %w", err) } storedName := fmt.Sprintf("%d_%s", timeNow.UnixNano(), fileHeader.Filename) savePath := path.Join(saveDir, storedName) if err := saveUploadedFile(fileHeader, savePath); err != nil { return err } contract.FileURL = "files/" + storedName contract.FileSize = fileHeader.Size return s.repo.CreateContract(&contract) } func (s *zspContractService) GetContractById(contractID string) (*model.ZSPContractFile, error) { return s.repo.GetContractById(contractID) } func (s *zspContractService) ListContracts(folderID string) ([]model.ZSPContractFile, error) { return s.repo.ListContracts(folderID) } func (s *zspContractService) DeleteContractById(contractID string) error { return s.repo.DeleteContractById(contractID) } func (s *zspContractService) UpdateContract(contract *model.ZSPContractFile) error { return s.repo.UpdateContract(contract) } func (s *zspContractService) UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error { contract, err := s.repo.GetContractById(contractID) if err != nil { return err } if req.ContractNumber != "" { contract.ContractNumber = req.ContractNumber } if req.CompanyName != "" { contract.CompanyName = req.CompanyName } if req.Commodity != "" { contract.Commodity = req.Commodity } if req.ContractType != "" { contract.ContractType = req.ContractType } if req.FolderID > 0 { contract.FolderID = req.FolderID } if req.SignDate != "" { signDate, err := time.Parse("2006-01-02", req.SignDate) if err != nil { return fmt.Errorf("签订日期格式不正确: %w", err) } contract.SignDate = signDate } return s.repo.UpdateContract(contract) } func (s *zspContractService) GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) { contract, err := s.repo.GetContractById(contractID) if err != nil { return nil, err } // 获取详情列表 details, err := s.repo.ListContractDetails(contractID) if err != nil { return nil, err } contract.Details = details return contract, nil } func (s *zspContractService) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) { return s.repo.ListContractsWithDetails(folderID) } // ============ 合同详情相关方法 ============ func (s *zspContractService) CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error { var detail model.ZSPContractDetail detail.ContractFileID = req.ContractFileID detail.CommodityName = req.CommodityName detail.CommodityCode = req.CommodityCode detail.TotalQuantity = req.TotalQuantity detail.Unit = req.Unit detail.UnitPrice = req.UnitPrice detail.DeliveredQty = req.DeliveredQty detail.BillOfLading = req.BillOfLading detail.Remark = req.Remark // PendingQty 会在 BeforeCreate 钩子中自动计算 return s.repo.CreateContractDetail(&detail) } func (s *zspContractService) UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error { detail, err := s.repo.GetContractDetailById(fmt.Sprintf("%d", req.ID)) if err != nil { return err } if req.CommodityName != "" { detail.CommodityName = req.CommodityName } if req.CommodityCode != "" { detail.CommodityCode = req.CommodityCode } if req.TotalQuantity > 0 { detail.TotalQuantity = req.TotalQuantity } if req.Unit != "" { detail.Unit = req.Unit } if req.UnitPrice > 0 { detail.UnitPrice = req.UnitPrice } // DeliveredQty 可以为 0,所以需要特别处理 detail.DeliveredQty = req.DeliveredQty if req.BillOfLading != "" { detail.BillOfLading = req.BillOfLading } detail.Remark = req.Remark // PendingQty 会在 BeforeUpdate 钩子中自动计算 return s.repo.UpdateContractDetail(detail) } func (s *zspContractService) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) { return s.repo.GetContractDetailById(detailID) } func (s *zspContractService) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) { return s.repo.ListContractDetails(contractFileID) } func (s *zspContractService) DeleteContractDetailById(detailID string) error { return s.repo.DeleteContractDetailById(detailID) } func (s *zspContractService) BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error { details := make([]model.ZSPContractDetail, 0, len(req.Details)) for _, detailReq := range req.Details { detail := model.ZSPContractDetail{ ContractFileID: req.ContractFileID, CommodityName: detailReq.CommodityName, CommodityCode: detailReq.CommodityCode, TotalQuantity: detailReq.TotalQuantity, Unit: detailReq.Unit, UnitPrice: detailReq.UnitPrice, DeliveredQty: detailReq.DeliveredQty, BillOfLading: detailReq.BillOfLading, Remark: detailReq.Remark, } // PendingQty 会在 BeforeCreate 钩子中自动计算 details = append(details, detail) } return s.repo.BatchCreateContractDetails(details) }