package repository import ( "database/sql" "strings" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/pkg/database" ) type MaterialLogRepo struct{} func NewMaterialLogRepo() *MaterialLogRepo { return &MaterialLogRepo{} } // normalizeEndDate ensures endDate has a time component for proper date range filtering func normalizeEndDate(endDate string) string { if endDate == "" { return "" } // If already contains a time component (has : in time part like 23:59:59), return as-is if strings.Contains(endDate, " ") && strings.Contains(strings.Split(endDate, " ")[1], ":") { return endDate } // Append end of day time return endDate + " 23:59:59" } func (r *MaterialLogRepo) insertLog(execer sqlExecer, log *model.MaterialLog) error { query := `INSERT INTO material_logs (operation_type, material_id, old_value, new_value, operator, description) VALUES (?, ?, ?, ?, ?, ?)` result, err := execer.Exec(query, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Operator, log.Description) if err != nil { return err } id, err := result.LastInsertId() if err != nil { return err } log.ID = id return nil } func (r *MaterialLogRepo) Create(log *model.MaterialLog) error { return r.insertLog(database.DB, log) } // CreateTx 在调用方事务内写入日志,便于和物料主记录的修改一起提交或回滚。 func (r *MaterialLogRepo) CreateTx(tx *sql.Tx, log *model.MaterialLog) error { return r.insertLog(tx, log) } // List 只返回当前公司物料的日志:material_logs 本身不带组织列,经 materials // 关联过滤,避免 A 看到 B 的物料变更记录。 func (r *MaterialLogRepo) List(organizationID int64, operationType, materialId string, startDate, endDate string, limit, offset int) ([]model.MaterialLog, int64, error) { countQuery := `SELECT COUNT(*) FROM material_logs ml JOIN materials m ON m.id = ml.material_id WHERE m.organization_id = ?` countArgs := []interface{}{organizationID} if operationType != "" { countQuery += ` AND ml.operation_type = ?` countArgs = append(countArgs, operationType) } if materialId != "" { countQuery += ` AND ml.material_id = ?` countArgs = append(countArgs, materialId) } if startDate != "" { countQuery += ` AND ml.created_at >= ?` countArgs = append(countArgs, startDate) } if endDate != "" { countQuery += ` AND ml.created_at <= ?` countArgs = append(countArgs, normalizeEndDate(endDate)) } var total int64 err := database.DB.QueryRow(countQuery, countArgs...).Scan(&total) if err != nil { return nil, 0, err } query := `SELECT ml.id, ml.operation_type, ml.material_id, ml.old_value, ml.new_value, ml.operator, ml.description, ml.created_at FROM material_logs ml JOIN materials m ON m.id = ml.material_id WHERE m.organization_id = ?` args := []interface{}{organizationID} if operationType != "" { query += ` AND ml.operation_type = ?` args = append(args, operationType) } if materialId != "" { query += ` AND ml.material_id = ?` args = append(args, materialId) } if startDate != "" { query += ` AND ml.created_at >= ?` args = append(args, startDate) } if endDate != "" { query += ` AND ml.created_at <= ?` args = append(args, normalizeEndDate(endDate)) } query += ` ORDER BY ml.id DESC LIMIT ? OFFSET ?` args = append(args, limit, offset) rows, err := database.DB.Query(query, args...) if err != nil { return nil, 0, err } defer rows.Close() var logs []model.MaterialLog for rows.Next() { var log model.MaterialLog var oldValue, newValue []byte err := rows.Scan(&log.ID, &log.OperationType, &log.MaterialID, &oldValue, &newValue, &log.Operator, &log.Description, &log.CreatedAt) if err != nil { return nil, 0, err } if oldValue != nil { log.OldValue = string(oldValue) } if newValue != nil { log.NewValue = string(newValue) } logs = append(logs, log) } return logs, total, nil } // CreateBatchTx 在同一事务里批量写审计日志。导入场景一条物料一条 CREATE 日志, // 逐条往返太慢,这里折叠成多值 INSERT。 func (r *MaterialLogRepo) CreateBatchTx(tx *sql.Tx, logs []*model.MaterialLog) error { if len(logs) == 0 { return nil } var builder strings.Builder builder.WriteString(`INSERT INTO material_logs (operation_type, material_id, old_value, new_value, operator, description) VALUES `) args := make([]interface{}, 0, len(logs)*6) for i, log := range logs { if i > 0 { builder.WriteString(", ") } builder.WriteString("(?, ?, ?, ?, ?, ?)") args = append(args, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Operator, log.Description) } _, err := tx.Exec(builder.String(), args...) return err } // GetByMaterialID 只返回属于当前公司的该物料日志;物料不属于本公司时结果为空。 func (r *MaterialLogRepo) GetByMaterialID(organizationID, materialID int64) ([]model.MaterialLog, error) { query := `SELECT ml.id, ml.operation_type, ml.material_id, ml.old_value, ml.new_value, ml.operator, ml.description, ml.created_at FROM material_logs ml JOIN materials m ON m.id = ml.material_id WHERE m.organization_id = ? AND ml.material_id = ? ORDER BY ml.id DESC` rows, err := database.DB.Query(query, organizationID, materialID) if err != nil { return nil, err } defer rows.Close() var logs []model.MaterialLog for rows.Next() { var log model.MaterialLog var oldValue, newValue []byte err := rows.Scan(&log.ID, &log.OperationType, &log.MaterialID, &oldValue, &newValue, &log.Operator, &log.Description, &log.CreatedAt) if err != nil { return nil, err } if oldValue != nil { log.OldValue = string(oldValue) } if newValue != nil { log.NewValue = string(newValue) } logs = append(logs, log) } return logs, nil }