#!/usr/bin/env bash set -u -o pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" CLIENT_DIR="$ROOT_DIR/client" PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}" REPORT_DIR="${FIN_PROD_ENABLE_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/fin-prod-enable-$(date +%Y%m%d-%H%M%S)}" SUMMARY_TXT="$REPORT_DIR/summary.txt" FLAG_SNAPSHOT_FILE="$REPORT_DIR/payment-batch-flag-before.json" STATUS=0 mkdir -p "$REPORT_DIR" : >"$SUMMARY_TXT" load_default_env() { local env_file="${FIN_PROD_ENABLE_ENV_FILE:-$HOME/.hyb-oa/live-smoke.env}" if [[ "${FIN_PROD_ENABLE_SKIP_ENV_FILE:-0}" == "1" || ! -f "$env_file" ]]; then return 0 fi set -a # shellcheck disable=SC1090 source "$env_file" set +a PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-$PUBLIC_BASE_URL}" echo "[fin-prod-enable] loaded masked env file" | tee -a "$SUMMARY_TXT" } run_step() { local name="$1" shift local log_file="$REPORT_DIR/${name}.log" echo ">>> [fin-prod-enable] $name" | tee -a "$SUMMARY_TXT" if "$@" > >(tee "$log_file") 2>&1; then echo "[fin-prod-enable][PASS] $name" | tee -a "$SUMMARY_TXT" else local code="$?" echo "[fin-prod-enable][FAIL] $name exit=$code" | tee -a "$SUMMARY_TXT" STATUS=1 fi } run_shell_step() { local name="$1" local command="$2" run_step "$name" bash -lc "$command" } fetch_feature_flags_node() { local expected="$1" EXPECTED_PAYMENT_BATCH_STATE="$expected" node <<'NODE' const expected = process.env.EXPECTED_PAYMENT_BATCH_STATE; const baseUrl = (process.env.LIVE_SMOKE_BASE_URL || "https://oa.hyinda.com").replace(/\/$/, ""); const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim(); const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim(); if (!identifier || !password) { console.error("[fin-prod-enable][flag-check] missing admin credentials"); process.exit(1); } async function fetchJson(url, init = {}) { const response = await fetch(url, { ...init, signal: AbortSignal.timeout(20_000), }); const text = await response.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { response, json }; } function errorSummary(json, status) { const error = json?.error ?? {}; return `${status} ${error.code ?? ""} ${error.message ?? ""}`.trim(); } const login = await fetchJson(`${baseUrl}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", "x-device-fingerprint": "fin-prod-enable-flag-check", }, body: JSON.stringify({ identifier, password }), }); if (!login.response.ok || !login.json?.data?.accessToken) { console.error(`[fin-prod-enable][flag-check] login failed: ${errorSummary(login.json, login.response.status)}`); process.exit(1); } const token = login.json.data.accessToken; const list = await fetchJson(`${baseUrl}/api/system/feature-flags`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" }, }); if (!list.response.ok) { console.error(`[fin-prod-enable][flag-check] list flags failed: ${errorSummary(list.json, list.response.status)}`); process.exit(1); } const items = list.json?.data?.items ?? []; const payment = items.find((item) => item.flagKey === "payment_batch_v1"); const erp = items.find((item) => item.flagKey === "erp_sync"); if (!payment || !erp) { console.error("[fin-prod-enable][flag-check] missing payment_batch_v1 or erp_sync"); process.exit(1); } console.log(`[fin-prod-enable][flag-check] payment_batch_v1 override=${payment.overrideMode} effective=${payment.effectiveEnabled} reason=${payment.effectiveReason}`); console.log(`[fin-prod-enable][flag-check] erp_sync override=${erp.overrideMode} effective=${erp.effectiveEnabled} reason=${erp.effectiveReason}`); const paymentOn = payment.overrideMode === "force_on" && payment.effectiveEnabled === true; const paymentOff = payment.overrideMode === "force_off" && payment.effectiveEnabled === false; const erpOff = erp.effectiveEnabled === false; if (expected === "force_on" && (!paymentOn || !erpOff)) { process.exit(1); } if (expected === "force_off" && (!paymentOff || !erpOff)) { process.exit(1); } NODE } restore_payment_batch_flag_snapshot() { P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" \ node "$ROOT_DIR/scripts/qa/payment-batch-flag-snapshot.mjs" restore \ --snapshot-file "$FLAG_SNAPSHOT_FILE" } write_summary_json() { local qa_status="PASS" local notes="FINPROD 正式启用:公网付款批次验收通过后保持 payment_batch_v1=force_on;失败时恢复调用前完整快照;不接真实支付、不接 ERP、不开放图片 OCR。" if [[ "$STATUS" -ne 0 ]]; then qa_status="FAIL" notes="FINPROD 正式启用失败或门禁失败:已尝试恢复 payment_batch_v1 调用前完整快照;不接真实支付、不接 ERP、不开放图片 OCR。" fi QA_REPORT_DIR="$REPORT_DIR" \ QA_ISSUE_ID="${QA_ISSUE_ID:-FINPROD}" \ QA_BASE_URL="$PUBLIC_BASE_URL" \ QA_MODE="fin-prod-enable" \ QA_STATUS="$qa_status" \ QA_SUMMARY_NOTES="$notes" \ node "$ROOT_DIR/scripts/qa/write-qa-summary.mjs" >/dev/null 2>&1 || true } finish() { if [[ "${P2_FIN_LIVE_APPROVED:-0}" == "1" && "${FIN_PROD_ENABLE_APPROVED:-0}" == "1" ]]; then if [[ "$STATUS" -eq 0 ]]; then echo ">>> [fin-prod-enable] final-force-on-check" | tee -a "$SUMMARY_TXT" if LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" fetch_feature_flags_node "force_on" > >(tee "$REPORT_DIR/final-force-on-check.log") 2>&1; then echo "[fin-prod-enable][PASS] final-force-on-check" | tee -a "$SUMMARY_TXT" else local code="$?" echo "[fin-prod-enable][FAIL] final-force-on-check exit=$code" | tee -a "$SUMMARY_TXT" STATUS=1 fi fi if [[ "$STATUS" -ne 0 && -f "$FLAG_SNAPSHOT_FILE" ]]; then echo ">>> [fin-prod-enable] restore-caller-flag-snapshot" | tee -a "$SUMMARY_TXT" if restore_payment_batch_flag_snapshot > >(tee "$REPORT_DIR/restore-caller-flag-snapshot.log") 2>&1; then echo "[fin-prod-enable][PASS] restore-caller-flag-snapshot" | tee -a "$SUMMARY_TXT" else local code="$?" echo "[fin-prod-enable][FAIL] restore-caller-flag-snapshot exit=$code" | tee -a "$SUMMARY_TXT" fi fi fi write_summary_json echo "[fin-prod-enable] report=$REPORT_DIR" | tee -a "$SUMMARY_TXT" exit "$STATUS" } trap finish EXIT load_default_env if [[ "${P2_FIN_LIVE_APPROVED:-0}" != "1" || "${FIN_PROD_ENABLE_APPROVED:-0}" != "1" ]]; then echo "[fin-prod-enable][ERROR] P2_FIN_LIVE_APPROVED=1 and FIN_PROD_ENABLE_APPROVED=1 are required for public write enablement" | tee -a "$SUMMARY_TXT" STATUS=2 exit "$STATUS" fi run_shell_step "live-env-strict" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-smoke-env.mjs' --strict" [[ "$STATUS" -eq 0 ]] || exit "$STATUS" run_shell_step "pre-flag-off-check" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-payment-batch-flag-off.mjs' --json" [[ "$STATUS" -eq 0 ]] || exit "$STATUS" run_shell_step "capture-caller-flag-snapshot" "P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/payment-batch-flag-snapshot.mjs' capture --snapshot-file '$FLAG_SNAPSHOT_FILE'" [[ "$STATUS" -eq 0 ]] || exit "$STATUS" run_shell_step "fin-prod-enable-spec" "cd '$CLIENT_DIR' && PLAYWRIGHT_MANUAL=1 E2E_MODE=live P2_FIN_LIVE_APPROVED=1 P2_FIN_LIVE_FINAL_STATE=force_on QA_ISSUE_ID='${QA_ISSUE_ID:-FINPROD}' P2_FIN_LIVE_MARKER_PREFIX='${P2_FIN_LIVE_MARKER_PREFIX:-${QA_ISSUE_ID:-FINPROD}}' P2_FIN_LIVE_REPORT_DIR='$REPORT_DIR' LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --workers=1 --reporter=list" if [[ "${FIN_PROD_ENABLE_SKIP_MOBILE:-0}" == "1" ]]; then echo "[fin-prod-enable][SKIP] mobile-wecom FIN_PROD_ENABLE_SKIP_MOBILE=1" | tee -a "$SUMMARY_TXT" else run_shell_step "public-mobile-matrix" "cd '$CLIENT_DIR' && E2E_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list" run_shell_step "wecom-rendering-matrix" "cd '$CLIENT_DIR' && E2E_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list" fi