material_manage_system/internal/repository/material_log.go
2026-10-14 11:39:12 +08:00

244 lines
7.8 KiB
Go
Raw 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 repository
import (
"database/sql"
"errors"
"fmt"
"strings"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
type MaterialLogRepo struct{}
var ErrMaterialLogOperatorNotFound = errors.New("material log operator does not exist")
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 {
var result sql.Result
var err error
if log.OperatorID == nil {
query := `INSERT INTO material_logs (operation_type, material_id, operator_id, old_value, new_value, operator, description) VALUES (?, ?, NULL, ?, ?, ?, ?)`
result, err = execer.Exec(query, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Operator, log.Description)
} else {
// Take both identity fields from one users row so an old token username or
// caller-provided name can never be stored beside a different user ID.
query := `INSERT INTO material_logs (operation_type, material_id, operator_id, old_value, new_value, operator, description)
SELECT ?, ?, u.id, ?, ?, u.username, ? FROM users u WHERE u.id = ?`
result, err = execer.Exec(query, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Description, *log.OperatorID)
}
if err != nil {
return err
}
if log.OperatorID != nil {
rowsAffected, rowsErr := result.RowsAffected()
if rowsErr != nil {
return rowsErr
}
if rowsAffected != 1 {
return ErrMaterialLogOperatorNotFound
}
}
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.operator_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 operatorID sql.NullInt64
var oldValue, newValue []byte
err := rows.Scan(&log.ID, &log.OperationType, &log.MaterialID, &operatorID, &oldValue, &newValue, &log.Operator, &log.Description, &log.CreatedAt)
if err != nil {
return nil, 0, err
}
if oldValue != nil {
log.OldValue = string(oldValue)
}
if operatorID.Valid {
log.OperatorID = &operatorID.Int64
}
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, operator_id, old_value, new_value, operator, description) `)
args := make([]interface{}, 0, len(logs)*7)
withUserID := false
for _, log := range logs {
if log.OperatorID != nil {
withUserID = true
break
}
}
if !withUserID {
builder.WriteString(`VALUES `)
}
for i, log := range logs {
if i > 0 {
if withUserID {
builder.WriteString(" UNION ALL ")
} else {
builder.WriteString(", ")
}
}
if withUserID && log.OperatorID != nil {
builder.WriteString(`SELECT ?, ?, u.id, ?, ?, u.username, ? FROM users u WHERE u.id = ?`)
args = append(args, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Description, *log.OperatorID)
} else if withUserID {
builder.WriteString(`SELECT ?, ?, NULL, ?, ?, ?, ?`)
args = append(args, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Operator, log.Description)
} else {
builder.WriteString("(?, ?, NULL, ?, ?, ?, ?)")
args = append(args, log.OperationType, log.MaterialID, log.OldValue, log.NewValue, log.Operator, log.Description)
}
}
result, err := tx.Exec(builder.String(), args...)
if err != nil {
return err
}
if withUserID {
rowsAffected, rowsErr := result.RowsAffected()
if rowsErr != nil {
return rowsErr
}
if rowsAffected != int64(len(logs)) {
return fmt.Errorf("%w: expected %d rows, inserted %d", ErrMaterialLogOperatorNotFound, len(logs), rowsAffected)
}
}
return nil
}
// GetByMaterialID 只返回属于当前公司的该物料日志;物料不属于本公司时结果为空。
func (r *MaterialLogRepo) GetByMaterialID(organizationID, materialID int64) ([]model.MaterialLog, error) {
query := `SELECT ml.id, ml.operation_type, ml.material_id, ml.operator_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 operatorID sql.NullInt64
var oldValue, newValue []byte
err := rows.Scan(&log.ID, &log.OperationType, &log.MaterialID, &operatorID, &oldValue, &newValue, &log.Operator, &log.Description, &log.CreatedAt)
if err != nil {
return nil, err
}
if oldValue != nil {
log.OldValue = string(oldValue)
}
if operatorID.Valid {
log.OperatorID = &operatorID.Int64
}
if newValue != nil {
log.NewValue = string(newValue)
}
logs = append(logs, log)
}
return logs, nil
}