166 lines
5.5 KiB
Go
166 lines
5.5 KiB
Go
package service
|
||
|
||
import (
|
||
"database/sql"
|
||
"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
|
||
}
|
||
return s.logRepo.Create(buildUpdateLog(material, oldMaterial, operator))
|
||
}
|
||
|
||
// LogUpdateTx 在调用方事务内写修改日志,供作废、恢复等要求主记录与日志原子提交的
|
||
// 场景使用;material 为 nil 时同样跳过,避免解引用 panic。
|
||
func (s *MaterialLogService) LogUpdateTx(tx *sql.Tx, material *model.Material, oldMaterial *model.Material, operator string) error {
|
||
if material == nil {
|
||
return nil
|
||
}
|
||
return s.logRepo.CreateTx(tx, buildUpdateLog(material, oldMaterial, operator))
|
||
}
|
||
|
||
func buildUpdateLog(material, oldMaterial *model.Material, operator string) *model.MaterialLog {
|
||
oldValue, _ := json.Marshal(oldMaterial)
|
||
newValue, _ := json.Marshal(material)
|
||
return &model.MaterialLog{
|
||
OperationType: "UPDATE",
|
||
MaterialID: material.ID,
|
||
OldValue: string(oldValue),
|
||
NewValue: string(newValue),
|
||
Operator: operator,
|
||
Description: updateDescription(material, oldMaterial),
|
||
}
|
||
}
|
||
|
||
// 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(organizationID int64, operationType, materialId, startDate, endDate string, limit, offset int) ([]model.MaterialLog, int64, error) {
|
||
return s.logRepo.List(organizationID, operationType, materialId, startDate, endDate, limit, offset)
|
||
}
|
||
|
||
func (s *MaterialLogService) GetByMaterialID(organizationID, materialID int64) ([]model.MaterialLog, error) {
|
||
return s.logRepo.GetByMaterialID(organizationID, materialID)
|
||
}
|