98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
// 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
|
|
}
|