package maintenance import ( "context" "errors" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrNotFound = errors.New("maintenance: window not found") ErrConflictOpen = errors.New("maintenance: an open window already exists") ErrInvalidWindow = errors.New("maintenance: invalid schedule") ErrBadStatus = errors.New("maintenance: invalid status transition") ) const ( StatusScheduled = "scheduled" StatusActive = "active" StatusEnded = "ended" StatusCancelled = "cancelled" ) // Window 维护窗口行。 type Window struct { ID int64 `json:"id"` ScheduledStart time.Time `json:"scheduledStart"` ScheduledEnd time.Time `json:"scheduledEnd"` Message string `json:"message"` Status string `json:"status"` CreatedBy *int64 `json:"createdBy,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` ActivatedAt *time.Time `json:"activatedAt,omitempty"` EndedAt *time.Time `json:"endedAt,omitempty"` } // Store 持久化接口。 type Store interface { GetOpen(ctx context.Context) (*Window, error) GetByID(ctx context.Context, id int64) (*Window, error) Insert(ctx context.Context, w Window) (Window, error) UpdateStatus(ctx context.Context, id int64, from, to string) (Window, error) ListRecent(ctx context.Context, limit int) ([]Window, error) } type pgStore struct { pool *pgxpool.Pool } func NewPGStore(pool *pgxpool.Pool) Store { return &pgStore{pool: pool} } func scanWindow(row pgx.Row) (Window, error) { var w Window var createdBy *int64 var activated, ended *time.Time err := row.Scan( &w.ID, &w.ScheduledStart, &w.ScheduledEnd, &w.Message, &w.Status, &createdBy, &w.CreatedAt, &w.UpdatedAt, &activated, &ended, ) if err != nil { return Window{}, err } w.CreatedBy = createdBy w.ActivatedAt = activated w.EndedAt = ended return w, nil } func (s *pgStore) GetOpen(ctx context.Context) (*Window, error) { w, err := scanWindow(s.pool.QueryRow(ctx, ` SELECT id, scheduled_start, scheduled_end, message, status, created_by, created_at, updated_at, activated_at, ended_at FROM maintenance_windows WHERE status IN ('scheduled', 'active') ORDER BY scheduled_start ASC LIMIT 1 `)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &w, nil } func (s *pgStore) GetByID(ctx context.Context, id int64) (*Window, error) { w, err := scanWindow(s.pool.QueryRow(ctx, ` SELECT id, scheduled_start, scheduled_end, message, status, created_by, created_at, updated_at, activated_at, ended_at FROM maintenance_windows WHERE id = $1 `, id)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &w, nil } func (s *pgStore) Insert(ctx context.Context, in Window) (Window, error) { w, err := scanWindow(s.pool.QueryRow(ctx, ` INSERT INTO maintenance_windows (scheduled_start, scheduled_end, message, status, created_by) VALUES ($1, $2, $3, $4, $5) RETURNING id, scheduled_start, scheduled_end, message, status, created_by, created_at, updated_at, activated_at, ended_at `, in.ScheduledStart, in.ScheduledEnd, in.Message, StatusScheduled, in.CreatedBy)) if err != nil { // unique partial index → conflict if isUniqueViolation(err) { return Window{}, ErrConflictOpen } return Window{}, err } return w, nil } func (s *pgStore) UpdateStatus(ctx context.Context, id int64, from, to string) (Window, error) { var q string switch to { case StatusActive: q = ` UPDATE maintenance_windows SET status = $2, updated_at = now(), activated_at = now() WHERE id = $1 AND status = $3 RETURNING id, scheduled_start, scheduled_end, message, status, created_by, created_at, updated_at, activated_at, ended_at` case StatusEnded, StatusCancelled: q = ` UPDATE maintenance_windows SET status = $2, updated_at = now(), ended_at = now() WHERE id = $1 AND status = $3 RETURNING id, scheduled_start, scheduled_end, message, status, created_by, created_at, updated_at, activated_at, ended_at` default: return Window{}, ErrBadStatus } w, err := scanWindow(s.pool.QueryRow(ctx, q, id, to, from)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Window{}, ErrBadStatus } return Window{}, err } return w, nil } func (s *pgStore) ListRecent(ctx context.Context, limit int) ([]Window, error) { if limit <= 0 { limit = 20 } rows, err := s.pool.Query(ctx, ` SELECT id, scheduled_start, scheduled_end, message, status, created_by, created_at, updated_at, activated_at, ended_at FROM maintenance_windows ORDER BY scheduled_start DESC LIMIT $1 `, limit) if err != nil { return nil, err } defer rows.Close() out := make([]Window, 0) for rows.Next() { w, err := scanWindow(rows) if err != nil { return nil, err } out = append(out, w) } return out, rows.Err() } func isUniqueViolation(err error) bool { return err != nil && (contains(err.Error(), "uq_maintenance_windows_one_open") || contains(err.Error(), "unique")) } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0) } func indexOf(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }