#!/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;引擎以 --spring.profiles.active=mysql 运行(默认 8081)。 set -euo pipefail BASE="${BASE:-http://localhost:8081}" 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