verify-mysql-rollback.sh
4.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/env bash
# 验证「事务回滚」在真实 MySQL(InnoDB) 上有效。
#
# 引擎唯一事务边界是 CommandEngine.execute 上的 @Transactional。CreateOrder(create 命令)先
# COMMIT_TRANSACTION 落库【订单主单+明细+付款条件+送货地址】并写 Outbox 事件,再由 ME 规则
# OrderCreated→StockDeduct 逐明细扣库存;带 guardField 的扣减 UPDATE 在库存不足时影响 0 行,
# 引擎调 setRollbackOnly()。若回滚有效:此前已插入的订单根+子实体+Outbox 应【全部消失】、库存不变。
#
# 前置:MySQL 容器 mysql-onto-rollback 在 3307;引擎默认即连 MySQL,正常起在 8080(无需额外 profile)。
set -euo pipefail
BASE="${BASE:-http://localhost:8080}"
DB=(mysql -h127.0.0.1 -P3307 -utrade -ptrade123 -N -B trade_db)
q() { "${DB[@]}" -e "$1" 2>/dev/null; } # 标量/多行查询
post() { curl -s -X POST "$BASE/api/scene/SCENE_CREATE_ORDER/execute" -H 'Content-Type: application/json' -d "$1"; }
jget() { python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('$1',''))"; }
bold() { printf "\n\033[1m%s\033[0m\n" "$1"; }
pass=0; fail=0
ok() { printf " \033[32m✔ PASS\033[0m %s\n" "$1"; pass=$((pass+1)); }
bad() { printf " \033[31m✘ FAIL\033[0m %s\n" "$1"; fail=$((fail+1)); }
eq() { if [ "$2" = "$3" ]; then ok "$1 (=$2)"; else bad "$1 期望[$3] 实得[$2]"; fi; }
curl -s -o /dev/null "$BASE/api/meta/models" || { echo "引擎未就绪:$BASE"; exit 1; }
# ---------- 基线快照 ----------
P2_0=$(q "SELECT stock_num FROM t_product_main WHERE product_id='P002'")
ORD_0=$(q "SELECT COUNT(*) FROM t_order_main")
ITM_0=$(q "SELECT COUNT(*) FROM t_order_item")
OBX_0=$(q "SELECT COUNT(*) FROM t_domain_outbox")
bold "基线:P002库存=$P2_0 订单数=$ORD_0 明细数=$ITM_0 Outbox=$OBX_0"
# ---------- 用例一:正向提交(对照,证明同一路径能落库)----------
bold "① 正向订单:C001 买 P002 ×3(3 ≤ 8 库存)应成功并落库、扣库存"
OK_ORDER='{"master":{"customerId":"C001","totalCurrency":"CNY","createTime":"2026-07-24T10:00:00"},
"details":{"OrderItem":[{"productId":"P002","quantity":3,"itemPrice":199,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"现金","dueDays":0}],
"DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"x","receiverName":"张三","receiverPhone":"13800001111"}]}}'
R=$(post "$OK_ORDER"); echo " 响应: success=$(echo "$R" | jget success) rootId=$(echo "$R" | jget rootId)"
eq "订单表 +1" "$(q "SELECT COUNT(*) FROM t_order_main")" "$((ORD_0+1))"
eq "明细表 +1" "$(q "SELECT COUNT(*) FROM t_order_item")" "$((ITM_0+1))"
eq "P002 库存扣至 5" "$(q "SELECT stock_num FROM t_product_main WHERE product_id='P002'")" "$((P2_0-3))"
# 更新基线到用例一之后
P2_1=$(q "SELECT stock_num FROM t_product_main WHERE product_id='P002'")
ORD_1=$(q "SELECT COUNT(*) FROM t_order_main")
ITM_1=$(q "SELECT COUNT(*) FROM t_order_item")
OBX_1=$(q "SELECT COUNT(*) FROM t_domain_outbox")
# ---------- 用例二:超卖 → 事务回滚(核心)----------
bold "② 超卖订单:C001 买 P002 ×99(99 > $P2_1 库存)应失败并【整单回滚】"
BAD_ORDER='{"master":{"customerId":"C001","totalCurrency":"CNY","createTime":"2026-07-24T10:05:00"},
"details":{"OrderItem":[{"productId":"P002","quantity":99,"itemPrice":199,"itemCurrency":"CNY"}],
"PaymentTerm":[{"paymentType":"现金","dueDays":0}],
"DeliveryAddressEntity":[{"province":"广东","city":"深圳","district":"南山","streetDetail":"x","receiverName":"张三","receiverPhone":"13800001111"}]}}'
R=$(post "$BAD_ORDER")
echo " 响应: success=$(echo "$R" | jget success) stage=$(echo "$R" | jget stage) message=$(echo "$R" | jget message)"
eq "命令应失败" "$(echo "$R" | jget success)" "False"
eq "订单表不新增(已回滚)" "$(q "SELECT COUNT(*) FROM t_order_main")" "$ORD_1"
eq "明细表不新增(已回滚)" "$(q "SELECT COUNT(*) FROM t_order_item")" "$ITM_1"
eq "Outbox不新增(已回滚)" "$(q "SELECT COUNT(*) FROM t_domain_outbox")" "$OBX_1"
eq "P002 库存不变" "$(q "SELECT stock_num FROM t_product_main WHERE product_id='P002'")" "$P2_1"
# ---------- 结论 ----------
bold "结论:PASS=$pass FAIL=$fail"
if [ "$fail" -eq 0 ]; then
printf "\033[32m事务回滚在真实 MySQL 上有效:超卖时订单根/明细/Outbox 与扣库存作为一个 InnoDB 事务整体回滚。\033[0m\n"
else
printf "\033[31m存在未通过断言,回滚可能失效,请查看上方明细。\033[0m\n"; exit 1
fi