verify-mysql-rollback.sh 4.49 KB
#!/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