244 lines
7.8 KiB
Go
244 lines
7.8 KiB
Go
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
|
||
}
|