FINPROD ACCTH 启用付款批次并收口代账交接
这个提交包含在:
@@ -0,0 +1,293 @@
|
||||
#!/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"
|
||||
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
|
||||
}
|
||||
|
||||
force_payment_batch_flag_off() {
|
||||
node <<'NODE'
|
||||
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();
|
||||
const issueId = process.env.QA_ISSUE_ID?.trim() || "FINPROD";
|
||||
|
||||
if (!identifier || !password) {
|
||||
console.error("[fin-prod-enable][rollback] 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-rollback",
|
||||
},
|
||||
body: JSON.stringify({ identifier, password }),
|
||||
});
|
||||
if (!login.response.ok || !login.json?.data?.accessToken) {
|
||||
console.error(`[fin-prod-enable][rollback] 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][rollback] list flags failed: ${errorSummary(list.json, list.response.status)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const flag = list.json?.data?.items?.find((item) => item.flagKey === "payment_batch_v1");
|
||||
if (!flag) {
|
||||
console.error("[fin-prod-enable][rollback] missing payment_batch_v1");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (flag.overrideMode === "force_off" && flag.effectiveEnabled === false) {
|
||||
console.log("[fin-prod-enable][rollback] payment_batch_v1 already force_off");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const updated = await fetchJson(`${baseUrl}/api/system/feature-flags/payment_batch_v1`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
overrideMode: "force_off",
|
||||
rolloutType: "global",
|
||||
departmentIds: [],
|
||||
note: `${issueId} failed final rollback`,
|
||||
expectedVersion: flag.version,
|
||||
}),
|
||||
});
|
||||
if (!updated.response.ok) {
|
||||
console.error(`[fin-prod-enable][rollback] update failed: ${errorSummary(updated.json, updated.response.status)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const item = updated.json?.data?.item;
|
||||
console.log(`[fin-prod-enable][rollback] payment_batch_v1 override=${item?.overrideMode ?? "unknown"} effective=${item?.effectiveEnabled ?? "unknown"}`);
|
||||
NODE
|
||||
}
|
||||
|
||||
write_summary_json() {
|
||||
local qa_status="PASS"
|
||||
local notes="FINPROD 正式启用:公网付款批次验收通过后保持 payment_batch_v1=force_on;失败时回滚 force_off;不接真实支付、不接 ERP、不开放图片 OCR。"
|
||||
if [[ "$STATUS" -ne 0 ]]; then
|
||||
qa_status="FAIL"
|
||||
notes="FINPROD 正式启用失败或门禁失败:已尝试将 payment_batch_v1 回滚到 force_off;不接真实支付、不接 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 ]]; then
|
||||
echo ">>> [fin-prod-enable] failure-force-flag-off" | tee -a "$SUMMARY_TXT"
|
||||
if force_payment_batch_flag_off > >(tee "$REPORT_DIR/failure-force-flag-off.log") 2>&1; then
|
||||
echo "[fin-prod-enable][PASS] failure-force-flag-off" | tee -a "$SUMMARY_TXT"
|
||||
else
|
||||
local code="$?"
|
||||
echo "[fin-prod-enable][FAIL] failure-force-flag-off exit=$code" | tee -a "$SUMMARY_TXT"
|
||||
fi
|
||||
|
||||
echo ">>> [fin-prod-enable] final-flag-off-check" | tee -a "$SUMMARY_TXT"
|
||||
if LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" fetch_feature_flags_node "force_off" > >(tee "$REPORT_DIR/final-flag-off-check.log") 2>&1; then
|
||||
echo "[fin-prod-enable][PASS] final-flag-off-check" | tee -a "$SUMMARY_TXT"
|
||||
else
|
||||
local code="$?"
|
||||
echo "[fin-prod-enable][FAIL] final-flag-off-check 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 "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
|
||||
在新工单中引用
屏蔽一个用户