package service import ( "encoding/json" "fmt" "strconv" "strings" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" ) type MaterialLogService struct { logRepo *repository.MaterialLogRepo } func NewMaterialLogService() *MaterialLogService { return &MaterialLogService{ logRepo: repository.NewMaterialLogRepo(), } } func (s *MaterialLogService) LogCreate(material *model.Material, operator string) error { newValue, _ := json.Marshal(material) log := &model.MaterialLog{ OperationType: "CREATE", MaterialID: material.ID, NewValue: string(newValue), Operator: operator, Description: fmt.Sprintf("新增物料:%s", material.Name), } return s.logRepo.Create(log) } func (s *MaterialLogService) LogUpdate(material *model.Material, oldMaterial *model.Material, operator string) error { // 上层取「修改后」的物料失败时仍会走到这里,material 为 nil,取字段会 panic if material == nil { return nil } oldValue, _ := json.Marshal(oldMaterial) newValue, _ := json.Marshal(material) log := &model.MaterialLog{ OperationType: "UPDATE", MaterialID: material.ID, OldValue: string(oldValue), NewValue: string(newValue), Operator: operator, Description: updateDescription(material, oldMaterial), } return s.logRepo.Create(log) } // updateDescription 让日志详情直接说明改了什么,用户不必去比对 old_value/new_value 里的整段 JSON。 // 比对不出任何变化时退回只带物料名的简版。 func updateDescription(material, oldMaterial *model.Material) string { description := fmt.Sprintf("修改物料:%s", material.Name) if oldMaterial == nil { return description } changes := materialChanges(oldMaterial, material) if len(changes) == 0 { return description } return fmt.Sprintf("%s(%s)", description, strings.Join(changes, ";")) } // materialChanges 逐字段比对,返回「字段名:旧值 → 新值」形式的变更列表,顺序即展示顺序。 // 分类和基本单位在服务层拿不到名称,如实显示 ID;物料图片只报「已更新」, // 图片 URL 往往很长,整条塞进描述会把别的变更挤没。 func materialChanges(oldMaterial, newMaterial *model.Material) []string { fields := []struct { label string oldValue string newValue string }{ {"物料编码", oldMaterial.MaterialCode, newMaterial.MaterialCode}, {"名称", oldMaterial.Name, newMaterial.Name}, {"规格", oldMaterial.Spec, newMaterial.Spec}, {"厂商型号", derefString(oldMaterial.ManufacturerModel), derefString(newMaterial.ManufacturerModel)}, {"分类", derefID(oldMaterial.CategoryID), derefID(newMaterial.CategoryID)}, {"基本单位", derefID(oldMaterial.BaseUnitID), derefID(newMaterial.BaseUnitID)}, {"单位", oldMaterial.Unit, newMaterial.Unit}, // 作废和恢复也是走这里的:状态一变,日志就写明「状态:启用 → 作废」 {"状态", materialStatusLabel(oldMaterial.Status), materialStatusLabel(newMaterial.Status)}, } var changes []string for _, field := range fields { if field.oldValue != field.newValue { changes = append(changes, fmt.Sprintf("%s:%s → %s", field.label, orEmptyValue(field.oldValue), orEmptyValue(field.newValue))) } } if derefString(oldMaterial.MaterialImage) != derefString(newMaterial.MaterialImage) { changes = append(changes, "物料图片:已更新") } return changes } func derefString(value *string) string { if value == nil { return "" } return *value } func derefID(value *int64) string { if value == nil { return "" } return strconv.FormatInt(*value, 10) } // orEmptyValue 把空值显示成「空」,否则会写出「规格: → 6205」这种读不出是新增还是清空的描述。 func orEmptyValue(value string) string { if value == "" { return "空" } return value } // materialStatusLabel 把状态值转成界面用语,日志里直接写 active/obsolete 对用户没有意义。 func materialStatusLabel(status string) string { switch status { case "active": return "启用" case "obsolete": return "作废" default: return status } } func (s *MaterialLogService) LogDelete(material *model.Material, operator string) error { oldValue, _ := json.Marshal(material) log := &model.MaterialLog{ OperationType: "DELETE", MaterialID: material.ID, OldValue: string(oldValue), Operator: operator, Description: fmt.Sprintf("删除物料:%s", material.Name), } return s.logRepo.Create(log) } func (s *MaterialLogService) List(operationType, materialId, startDate, endDate string, limit, offset int) ([]model.MaterialLog, int64, error) { return s.logRepo.List(operationType, materialId, startDate, endDate, limit, offset) } func (s *MaterialLogService) GetByMaterialID(materialID int64) ([]model.MaterialLog, error) { return s.logRepo.GetByMaterialID(materialID) }