// Package orderstate 订单状态机:合法流转校验 + 乐观锁 + 审计日志(与状态修改在同一个事务里提交)。 package orderstate import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) const Schema = ` CREATE TABLE IF NOT EXISTS orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, status TEXT NOT NULL DEFAULT 'pending', version INT NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS order_events ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id), from_status TEXT NOT NULL, to_status TEXT NOT NULL, actor TEXT NOT NULL, reason TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() );` // 允许的状态流转:只有这里列出的才合法 var transitions = map[string][]string{ "pending": {"paid", "cancelled"}, "paid": {"shipped", "refunding"}, "shipped": {"completed"}, "refunding": {"refunded", "paid"}, } var ( ErrIllegal = errors.New("不允许的状态流转") ErrConflict = errors.New("订单已被其他操作修改,请刷新后重试") ErrNotFound = errors.New("订单不存在") ) func allowed(from, to string) bool { for _, s := range transitions[from] { if s == to { return true } } return false } type Order struct { ID int64 Status string Version int } func Get(ctx context.Context, db *pgxpool.Pool, id int64) (Order, error) { var o Order err := db.QueryRow(ctx, `SELECT id, status, version FROM orders WHERE id = $1`, id).Scan(&o.ID, &o.Status, &o.Version) if errors.Is(err, pgx.ErrNoRows) { return o, ErrNotFound } return o, err } // Transition 把订单从 cur 状态改成 to。cur 是调用方读到的版本(乐观锁)。 func Transition(ctx context.Context, db *pgxpool.Pool, cur Order, to, actor, reason string) (Order, error) { if !allowed(cur.Status, to) { return cur, fmt.Errorf("%s -> %s: %w", cur.Status, to, ErrIllegal) } tx, err := db.Begin(ctx) if err != nil { return cur, err } defer tx.Rollback(ctx) // 提交成功后再回滚是无害的 tag, err := tx.Exec(ctx, `UPDATE orders SET status = $1, version = version + 1, updated_at = now() WHERE id = $2 AND status = $3 AND version = $4`, to, cur.ID, cur.Status, cur.Version) if err != nil { return cur, err } if tag.RowsAffected() == 0 { return cur, ErrConflict } if _, err := tx.Exec(ctx, `INSERT INTO order_events (order_id, from_status, to_status, actor, reason) VALUES ($1, $2, $3, $4, $5)`, cur.ID, cur.Status, to, actor, reason); err != nil { return cur, err } if err := tx.Commit(ctx); err != nil { return cur, err } return Order{ID: cur.ID, Status: to, Version: cur.Version + 1}, nil }