完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)

汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
2026-09-19 13:11:04 +08:00
父节点 729d90576f
当前提交 1f8d5b658a
共修改 367 个文件,包含 21079 行新增和 12287 行删除
+193 -48
查看文件
@@ -7,7 +7,7 @@ CLIENT_DIR="$ROOT_DIR/client"
source "$ROOT_DIR/scripts/qa/pwa-cross-version-fixture.sh"
MODE="${1:-}"
if [[ "$MODE" == "full-local" || "$MODE" == "full-local-diagnostic" ]]; then
if [[ "$MODE" == "full-local" || "$MODE" == "full-local-diagnostic" || "$MODE" == "retained-local" || "$MODE" == "retained-server-local" ]]; then
if [[ -z "${QA_ENVIRONMENT_CONTEXT:-}" ]]; then
exec node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" "$MODE"
fi
@@ -43,6 +43,7 @@ QA_READY_TIMEOUT_SECONDS="${QA_READY_TIMEOUT_SECONDS:-90}"
QA_BASE_URL=""
QA_DB_NAME="${QA_DB_NAME:-${QA_DB_PREFIX}_$(date +%Y%m%d%H%M%S)_$RANDOM}"
QA_DB_CREATED=0
QA_FULL_STAGE_FILE=""
QA_SERVER_STARTED=0
QA_SERVER_OWNED=0
DATABASE_URL="postgresql://${QA_DB_USER}:${QA_DB_PASSWORD}@${QA_DB_HOST}:${QA_DB_PORT}/${QA_DB_NAME}?schema=public"
@@ -57,7 +58,7 @@ qa_require_cmd() {
qa_usage() {
cat <<USAGE
Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|auth-security-local|payment-concurrency-local|business-flow-local|project-wide-visual-local|qg6-visual-matrix-local|r70-assist-local|r106-visual-local|r108-mobile-local|r109-mobile-local|r110-mobile-local|smoke-live>
Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|retained-local|retained-server-local|auth-security-local|payment-concurrency-local|business-flow-local|project-wide-visual-local|qg6-visual-matrix-local|r70-assist-local|r106-visual-local|r108-mobile-local|r109-mobile-local|r110-mobile-local|smoke-live>
可选环境变量:
QA_PORT / QA_HOST
@@ -66,6 +67,9 @@ Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|auth-security-lo
QA_DB_DOCKER_CONTAINER / QA_DOCKER_USE_SG=1
QA_DB_PREFIX / QA_DB_NAME / QA_KEEP_DB
QA_FULL_REPORT_DIR full-local 独立发现、构建日志与浏览器结果目录
retained-local 受管限定重验,无普通发布资格;必填 QA_RETAINED_SPECS
QA_RETAINED_GREP 可选历史用例标题筛选,不改变 full-local 分母
QA_RETAINED_SERVER_SUITE retained-server-local 必填 workflow-actions 或 server-regressions
full-local-diagnostic 仅本地诊断,始终 PARTIAL/退出 2,无发布资格
QA_DIAGNOSTIC_REASON / QA_DIAGNOSTIC_OPERATOR / QA_DIAGNOSTIC_APPROVAL_REF
QA_DIAGNOSTIC_REPORT_DIR 本次诊断专用空目录;禁止覆盖历史报告
@@ -305,6 +309,13 @@ qa_cleanup() {
fi
fi
if [[ -n "$QA_FULL_STAGE_FILE" ]]; then
local stage_status="$original_status"
if [[ "$stage_status" == "0" ]]; then stage_status="$cleanup_status"; fi
if ! node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" finish "$QA_FULL_STAGE_FILE" "$stage_status"; then
cleanup_status=1
fi
fi
if [[ "$original_status" != "0" ]]; then
exit "$original_status"
fi
@@ -513,6 +524,17 @@ qa_start_server() {
tail -n 200 "$QA_SERVER_LOG" >&2 || true
exit 1
}
# Derive browser capabilities from this successfully started server, never
# from a caller's PLAYWRIGHT_OCR_MOCK assertion. A restart recalculates it.
export PLAYWRIGHT_OCR_MOCK=0
if [[ "$OCR_PROVIDER" == "mock" && "$JOBS_ENABLED" == "true" ]]; then
export PLAYWRIGHT_OCR_MOCK=1
fi
if [[ -n "${QA_FULL_REPORT_DIR:-}" ]]; then
mkdir -p "$QA_FULL_REPORT_DIR"
node -e 'require("node:fs").appendFileSync(process.argv[1], JSON.stringify({provider:process.env.OCR_PROVIDER,jobsEnabled:process.env.JOBS_ENABLED === "true",mockCorrectionEnabled:process.env.PLAYWRIGHT_OCR_MOCK === "1",source:"owned-server-start"})+"\n")' "$QA_FULL_REPORT_DIR/server-capabilities.jsonl"
fi
}
qa_build_client() {
@@ -530,7 +552,7 @@ qa_build_client() {
(
cd "$CLIENT_DIR"
npx tsc --noEmit --pretty false --extendedDiagnostics
npx vite build
NODE_ENV=production npx vite build
)
}
@@ -563,75 +585,182 @@ qa_prepare_full_inputs() {
pwa_with_fixture env E2E_MODE=local \
PLAYWRIGHT_JSON_OUTPUT_FILE="$QA_FULL_REPORT_DIR/discovery.json" \
QA_VISUAL_OUTPUT_DIR="$QA_FULL_REPORT_DIR/visual" \
npx playwright test --list --retries=0 --reporter=json \
npx playwright test --list --retries=0 --forbid-only --reporter=json \
> "$QA_FULL_REPORT_DIR/discovery.stdout.log" 2> "$QA_FULL_REPORT_DIR/discovery.stderr.log"
)
node "$ROOT_DIR/scripts/qa/verify-full-local-discovery.mjs" "$QA_FULL_REPORT_DIR/discovery.json" \
> "$QA_FULL_REPORT_DIR/discovery-coverage.json"
node "$ROOT_DIR/scripts/qa/full-local-coverage.mjs" plan "$QA_FULL_REPORT_DIR" \
> "$QA_FULL_REPORT_DIR/coverage-plan.json"
}
# One owner prepares synthetic accounts before any business suite runs.
qa_seed_synthetic_credentials() {
export QA_SYNTHETIC_INITIAL_PASSWORD="Init!$(node -e 'process.stdout.write(require("node:crypto").randomBytes(24).toString("hex"))')"
local role
for role in CEO ADMIN MANAGER EMPLOYEE FINANCE FINANCE_MGR HR ADMINISTRATION; do
export "SEED_${role}_PASSWORD=$QA_SYNTHETIC_INITIAL_PASSWORD"
done
}
qa_prepare_synthetic_accounts() {
mkdir -p "$QA_FULL_REPORT_DIR"
export SEED_DEV_DATABASE_CONFIRM="$QA_DB_NAME"
node "$ROOT_DIR/scripts/qa/prepare-local-e2e-accounts.mjs" "$QA_FULL_REPORT_DIR/synthetic-account-preparation.json"
unset QA_SYNTHETIC_INITIAL_PASSWORD
for role in CEO ADMIN MANAGER EMPLOYEE FINANCE FINANCE_MGR HR ADMINISTRATION; do unset "SEED_${role}_PASSWORD"; done
}
# Focused historical-case diagnosis; never a normal full-local release receipt.
qa_run_retained_local() {
[[ -n "${QA_RETAINED_SPECS:-}" ]] || { echo "retained-local requires an explicit spec list" >&2; return 1; }
local -a retained_specs
read -r -a retained_specs <<< "$QA_RETAINED_SPECS"
local spec
for spec in "${retained_specs[@]}"; do
[[ "$spec" =~ ^[a-zA-Z0-9_-]+\.spec\.ts$ && -f "$CLIENT_DIR/tests/e2e/$spec" ]] || { echo "invalid local spec selector" >&2; return 1; }
done
mkdir -p "$QA_FULL_REPORT_DIR"
node -e 'require("node:fs").writeFileSync(process.argv[1], JSON.stringify({scope:"focused-historical-retest",normalReleaseEligible:false,specs:process.argv.slice(2),grep:process.env.QA_RETAINED_GREP||null})+"\n",{flag:"wx"})' "$QA_FULL_REPORT_DIR/selection.json" "${retained_specs[@]}"
qa_select_port
export JOBS_ENABLED=true RATE_LIMIT_ENABLED=false IM_NOTIFIER_ENABLED=false
qa_export_app_env
export IM_NOTIFIER_ENABLED=false
qa_seed_synthetic_credentials
qa_prepare_db
qa_build_client
qa_start_server
mkdir -p "$QA_FULL_REPORT_DIR"
qa_prepare_synthetic_accounts
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" report-guest > "$QA_FULL_REPORT_DIR/environment-before-browser.json"
local -a pwa_env=()
if [[ " $QA_RETAINED_SPECS " == *" pwa-cross-version-upgrade.spec.ts "* ]]; then
pwa_prepare_fixture "$CLIENT_DIR" "$QA_FULL_REPORT_DIR"
pwa_env=("PWA_BUILD_A_DIR=$PWA_BUILD_A_DIR" "PWA_BUILD_B_DIR=$PWA_BUILD_B_DIR" "PWA_BUILD_A_SHA=$PWA_BUILD_A_SHA" "PWA_BUILD_B_SHA=$PWA_BUILD_B_SHA")
fi
local -a grep_args=()
if [[ -n "${QA_RETAINED_GREP:-}" ]]; then grep_args=(--grep "$QA_RETAINED_GREP"); fi
(
cd "$CLIENT_DIR"
env "${pwa_env[@]}" E2E_MODE=local PLAYWRIGHT_JSON_OUTPUT_FILE="$QA_FULL_REPORT_DIR/playwright-results.json" \
QA_VISUAL_OUTPUT_DIR="$QA_FULL_REPORT_DIR/visual" QA_VISUAL_ARTIFACT_DIR="$QA_FULL_REPORT_DIR/visual-artifacts" \
node node_modules/@playwright/test/cli.js test "${retained_specs[@]}" "${grep_args[@]}" \
--retries=0 --reporter=line,json --output="$QA_FULL_REPORT_DIR/test-results"
)
}
# A fixed server suite in the same managed environment; never full-local coverage.
qa_run_retained_server_local() {
case "${QA_RETAINED_SERVER_SUITE:-}" in
workflow-actions|server-regressions) ;;
*) echo "retained-server-local requires an allowed server suite" >&2; return 1 ;;
esac
mkdir -p "$QA_FULL_REPORT_DIR"
node -e 'require("node:fs").writeFileSync(process.argv[1], JSON.stringify({scope:"focused-server-regression",normalReleaseEligible:false,suite:process.argv[2]})+"\n",{flag:"wx"})' "$QA_FULL_REPORT_DIR/selection.json" "$QA_RETAINED_SERVER_SUITE"
qa_select_port
qa_export_app_env
export JOBS_ENABLED=false RATE_LIMIT_ENABLED=false IM_NOTIFIER_ENABLED=false
if [[ "$QA_RETAINED_SERVER_SUITE" == "server-regressions" ]]; then export IM_NOTIFIER_ENABLED=true; fi
qa_seed_synthetic_credentials
qa_prepare_db
qa_build_client
qa_start_server
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
qa_prepare_synthetic_accounts
(
cd "$SERVER_DIR"
case "$QA_RETAINED_SERVER_SUITE" in
workflow-actions) ./scripts/regression-workflow-actions.sh ;;
server-regressions) ./scripts/regression-all.sh ;;
esac
)
}
# Plain calls preserve errexit inside compound helpers. Failure is sealed by
# qa_cleanup; wrapping "$@" in an if/|| would suppress their inner failures.
qa_full_stage() {
local stage="$1"
shift
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" start "$QA_FULL_STAGE_FILE" "$stage"
echo ">>> [full-local] $stage"
"$@"
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" end "$QA_FULL_STAGE_FILE" "$stage" 0
}
qa_full_in_directory() (
cd "$1"
shift
"$@"
)
qa_full_database() {
qa_seed_synthetic_credentials
qa_prepare_db
}
qa_full_server_start() {
qa_start_server
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
}
qa_full_browser_server() {
qa_cleanup_server
export JOBS_ENABLED="${JOBS_ENABLED_FOR_E2E:-true}"
qa_start_server
}
qa_run_full_local() {
qa_select_port
export JOBS_ENABLED="${JOBS_ENABLED:-false}"
qa_export_app_env
qa_prepare_full_inputs
qa_prepare_db
echo ">>> 执行 server Prisma 校验"
(
cd "$SERVER_DIR"
npm run prisma:validate
)
echo ">>> 执行 server 单元测试"
(
cd "$SERVER_DIR"
npm test
)
echo ">>> 执行 client typecheck"
(
cd "$CLIENT_DIR"
npx tsc --noEmit --pretty false --extendedDiagnostics
)
qa_build_client
qa_start_server
echo ">>> 执行 workflow-actions 回归"
(
cd "$SERVER_DIR"
./scripts/regression-workflow-actions.sh
)
echo ">>> 执行 server 全量回归"
(
cd "$SERVER_DIR"
./scripts/regression-all.sh
)
echo ">>> 切换 server 到 Playwright E2E 模式(启用后台 job)"
qa_cleanup_server
export JOBS_ENABLED="${JOBS_ENABLED_FOR_E2E:-true}"
qa_start_server
qa_run_full_browser
export RATE_LIMIT_ENABLED=false
QA_FULL_REPORT_DIR="${QA_FULL_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/full-local/$QA_RUN_ID}"
mkdir -p "$QA_FULL_REPORT_DIR"
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" init "$QA_FULL_REPORT_DIR/full-local-stages.json"
QA_FULL_STAGE_FILE="$QA_FULL_REPORT_DIR/full-local-stages.json"
qa_full_stage inputs qa_prepare_full_inputs
qa_full_stage database qa_full_database
qa_full_stage prisma qa_full_in_directory "$SERVER_DIR" npm run prisma:validate
qa_full_stage server-unit qa_full_in_directory "$SERVER_DIR" npm test
qa_full_stage typecheck qa_full_in_directory "$CLIENT_DIR" npx tsc --noEmit --pretty false --extendedDiagnostics
qa_full_stage client-build qa_build_client
qa_full_stage server-start qa_full_server_start
qa_full_stage accounts qa_prepare_synthetic_accounts
qa_full_stage workflow-actions qa_full_in_directory "$SERVER_DIR" ./scripts/regression-workflow-actions.sh
qa_full_stage server-regressions qa_full_in_directory "$SERVER_DIR" ./scripts/regression-all.sh
qa_full_stage browser-server qa_full_browser_server
qa_full_stage browser qa_run_full_browser
}
qa_run_full_browser() {
# Only the diagnostic owner may add tracing; ordinary callers cannot narrow coverage.
if (( $# )) && [[ "${QA_DIAGNOSTIC_STAGE:-}" != "browser" || "$*" != "--trace=on" ]]; then
echo "[ERROR] full-local browser scope override rejected" >&2
return 1
fi
local -a local_specs
mapfile -t local_specs < "$QA_FULL_REPORT_DIR/local-spec-args.txt"
(( ${#local_specs[@]} > 0 )) || return 1
if [[ -n "${QA_DIAGNOSTIC_STAGE:-}" ]]; then QA_DIAGNOSTIC_PHASE=environment; fi
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" report-guest > "$QA_FULL_REPORT_DIR/environment-before-browser.json"
if [[ -n "${QA_DIAGNOSTIC_STAGE:-}" ]]; then QA_DIAGNOSTIC_PHASE=test; fi
echo ">>> 执行 Playwright full E2E"
local browser_exit=0 coverage_exit=0
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" selection "$QA_FULL_REPORT_DIR/execution-selection.json" "${#local_specs[@]}" "${local_specs[@]}" "$@"
(
cd "$CLIENT_DIR"
pwa_with_fixture env E2E_MODE=local \
PLAYWRIGHT_JSON_OUTPUT_FILE="$QA_FULL_REPORT_DIR/playwright-results.json" \
QA_VISUAL_OUTPUT_DIR="$QA_FULL_REPORT_DIR/visual" \
PLAYWRIGHT_HTML_OUTPUT_DIR="$QA_FULL_REPORT_DIR/playwright-report" PLAYWRIGHT_HTML_OPEN=never \
npm run test:e2e:full -- "$@" --retries=0 --reporter=list,json,html --output="$QA_FULL_REPORT_DIR/test-results"
)
npm run test:e2e:full -- "${local_specs[@]}" "$@" --retries=0 --forbid-only --reporter=list,json,html --output="$QA_FULL_REPORT_DIR/test-results"
) || browser_exit=$?
node "$ROOT_DIR/scripts/qa/full-local-coverage.mjs" verify-local "$QA_FULL_REPORT_DIR" \
> "$QA_FULL_REPORT_DIR/local-coverage-result.json" || coverage_exit=$?
if (( browser_exit )); then return "$browser_exit"; fi
return "$coverage_exit"
}
qa_run_r70_assist_local() {
@@ -815,6 +944,22 @@ case "$MODE" in
source "$ROOT_DIR/scripts/qa/full-local-diagnostic-stage.sh"
qa_run_diagnostic_stage "${2:-}"
;;
retained-local)
qa_require_cmd curl
qa_require_db_client
qa_require_cmd node
qa_require_cmd npm
trap qa_cleanup EXIT
qa_run_retained_local
;;
retained-server-local)
qa_require_cmd curl
qa_require_db_client
qa_require_cmd node
qa_require_cmd npm
trap qa_cleanup EXIT
qa_run_retained_server_local
;;
full-local)
qa_require_cmd curl
qa_require_db_client