A2-A4-A9 完善报销码值与查重回归
这个提交包含在:
+266
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SERVER_DIR="$ROOT_DIR/server"
|
||||
|
||||
QA_HOST="${QA_HOST:-127.0.0.1}"
|
||||
QA_PORT_INPUT="${QA_PORT:-}"
|
||||
QA_PORT="${QA_PORT_INPUT:-3000}"
|
||||
QA_READY_TIMEOUT_SECONDS="${QA_READY_TIMEOUT_SECONDS:-90}"
|
||||
QA_DB_CLIENT="${QA_DB_CLIENT:-host}"
|
||||
QA_DB_HOST="${QA_DB_HOST:-127.0.0.1}"
|
||||
QA_DB_PORT="${QA_DB_PORT:-5432}"
|
||||
QA_DB_USER="${QA_DB_USER:-hyboa_user}"
|
||||
QA_DB_PASSWORD="${QA_DB_PASSWORD:-hyboa_password}"
|
||||
QA_DB_ADMIN_DB="${QA_DB_ADMIN_DB:-postgres}"
|
||||
QA_DB_DOCKER_CONTAINER="${QA_DB_DOCKER_CONTAINER:-hyb-oa-trialrun-db}"
|
||||
QA_DOCKER_USE_SG="${QA_DOCKER_USE_SG:-0}"
|
||||
QA_DB_PREFIX="${QA_DB_PREFIX:-hyb_oa_dedup}"
|
||||
QA_DB_NAME="${QA_DB_NAME:-${QA_DB_PREFIX}_$(date +%Y%m%d%H%M%S)_$RANDOM}"
|
||||
QA_KEEP_DB="${QA_KEEP_DB:-0}"
|
||||
QA_ARTIFACT_DIR="${QA_ARTIFACT_DIR:-$ROOT_DIR/artifacts/qa-flow/isolated-dedup-$(date +%Y%m%d-%H%M%S)}"
|
||||
QA_SERVER_LOG="$QA_ARTIFACT_DIR/server.log"
|
||||
QA_SERVER_PID_FILE="$QA_ARTIFACT_DIR/server.pid"
|
||||
QA_BASE_URL=""
|
||||
TEST_FIELD_ENCRYPTION_KEY_DEFAULT="${FIELD_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}"
|
||||
|
||||
mkdir -p "$QA_ARTIFACT_DIR"
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "[ERROR] 缺少命令: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
docker_cmd() {
|
||||
if [[ "$QA_DOCKER_USE_SG" == "1" ]]; then
|
||||
local rendered="docker"
|
||||
local quoted
|
||||
for arg in "$@"; do
|
||||
printf -v quoted "%q" "$arg"
|
||||
rendered+=" $quoted"
|
||||
done
|
||||
sg docker -c "$rendered"
|
||||
return
|
||||
fi
|
||||
|
||||
docker "$@"
|
||||
}
|
||||
|
||||
require_db_client() {
|
||||
case "$QA_DB_CLIENT" in
|
||||
host)
|
||||
require_cmd psql
|
||||
require_cmd createdb
|
||||
require_cmd dropdb
|
||||
;;
|
||||
docker)
|
||||
require_cmd docker
|
||||
if [[ "$QA_DOCKER_USE_SG" == "1" ]]; then
|
||||
require_cmd sg
|
||||
fi
|
||||
if ! docker_cmd inspect "$QA_DB_DOCKER_CONTAINER" >/dev/null 2>&1; then
|
||||
echo "[ERROR] PostgreSQL 容器不可用: $QA_DB_DOCKER_CONTAINER" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] QA_DB_CLIENT 仅支持 host 或 docker,当前: $QA_DB_CLIENT" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
port_in_use() {
|
||||
python3 - "$QA_HOST" "$QA_PORT" <<'PY'
|
||||
import socket, sys
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(0.5)
|
||||
try:
|
||||
sock.connect((host, port))
|
||||
except OSError:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
PY
|
||||
}
|
||||
|
||||
find_available_port() {
|
||||
python3 - "$QA_HOST" "$QA_PORT" <<'PY'
|
||||
import socket, sys
|
||||
host = sys.argv[1]
|
||||
start = int(sys.argv[2])
|
||||
for port in range(start, start + 200):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind((host, port))
|
||||
except OSError:
|
||||
continue
|
||||
print(port)
|
||||
sys.exit(0)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
select_port() {
|
||||
if port_in_use; then
|
||||
if [[ -n "$QA_PORT_INPUT" ]]; then
|
||||
echo "[ERROR] 端口 ${QA_PORT} 已被占用,请改用 QA_PORT=<port> 后重试。" >&2
|
||||
exit 1
|
||||
fi
|
||||
QA_PORT="$(find_available_port)"
|
||||
echo "[INFO] 默认端口 3000 已被占用,自动切换到 ${QA_PORT}"
|
||||
fi
|
||||
QA_BASE_URL="http://${QA_HOST}:${QA_PORT}"
|
||||
}
|
||||
|
||||
psql_admin() {
|
||||
if [[ "$QA_DB_CLIENT" == "docker" ]]; then
|
||||
docker_cmd exec -e "PGPASSWORD=$QA_DB_PASSWORD" "$QA_DB_DOCKER_CONTAINER" \
|
||||
psql -v ON_ERROR_STOP=1 -U "$QA_DB_USER" -d "$QA_DB_ADMIN_DB" "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
PGPASSWORD="$QA_DB_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$QA_DB_HOST" -p "$QA_DB_PORT" -U "$QA_DB_USER" -d "$QA_DB_ADMIN_DB" "$@"
|
||||
}
|
||||
|
||||
create_db() {
|
||||
if [[ "$QA_DB_CLIENT" == "docker" ]]; then
|
||||
docker_cmd exec -e "PGPASSWORD=$QA_DB_PASSWORD" "$QA_DB_DOCKER_CONTAINER" \
|
||||
createdb -U "$QA_DB_USER" "$QA_DB_NAME"
|
||||
return
|
||||
fi
|
||||
|
||||
PGPASSWORD="$QA_DB_PASSWORD" createdb -h "$QA_DB_HOST" -p "$QA_DB_PORT" -U "$QA_DB_USER" "$QA_DB_NAME"
|
||||
}
|
||||
|
||||
drop_db() {
|
||||
if [[ "$QA_KEEP_DB" == "1" ]]; then
|
||||
echo "[INFO] QA_KEEP_DB=1,保留临时数据库:$QA_DB_NAME"
|
||||
return
|
||||
fi
|
||||
|
||||
psql_admin -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${QA_DB_NAME}' AND pid <> pg_backend_pid();" >/dev/null || true
|
||||
if [[ "$QA_DB_CLIENT" == "docker" ]]; then
|
||||
docker_cmd exec -e "PGPASSWORD=$QA_DB_PASSWORD" "$QA_DB_DOCKER_CONTAINER" \
|
||||
dropdb --if-exists -U "$QA_DB_USER" "$QA_DB_NAME" >/dev/null || true
|
||||
return
|
||||
fi
|
||||
|
||||
PGPASSWORD="$QA_DB_PASSWORD" dropdb --if-exists -h "$QA_DB_HOST" -p "$QA_DB_PORT" -U "$QA_DB_USER" "$QA_DB_NAME" >/dev/null || true
|
||||
}
|
||||
|
||||
stop_server() {
|
||||
if [[ -f "$QA_SERVER_PID_FILE" ]]; then
|
||||
local pid
|
||||
pid="$(cat "$QA_SERVER_PID_FILE")"
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 15); do
|
||||
if ! kill -0 "$pid" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
kill -0 "$pid" >/dev/null 2>&1 && kill -9 "$pid" >/dev/null 2>&1 || true
|
||||
wait "$pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -f "$QA_SERVER_PID_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
stop_server
|
||||
drop_db
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_ready() {
|
||||
local elapsed=0
|
||||
while [[ "$elapsed" -lt "$QA_READY_TIMEOUT_SECONDS" ]]; do
|
||||
if curl -fsS "$QA_BASE_URL/ready" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
echo "[ERROR] /ready 在 ${QA_READY_TIMEOUT_SECONDS}s 内未通过:$QA_BASE_URL/ready" >&2
|
||||
echo "[ERROR] server log: $QA_SERVER_LOG" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
export_app_env() {
|
||||
export NODE_ENV=test
|
||||
export HOST="$QA_HOST"
|
||||
export PORT="$QA_PORT"
|
||||
export APP_ORIGIN="$QA_BASE_URL"
|
||||
export BASE_URL="$QA_BASE_URL"
|
||||
export DATABASE_URL="postgresql://${QA_DB_USER}:${QA_DB_PASSWORD}@${QA_DB_HOST}:${QA_DB_PORT}/${QA_DB_NAME}?schema=public"
|
||||
export JWT_SECRET="${JWT_SECRET:-qa_isolated_dedup_secret_key}"
|
||||
export FIELD_ENCRYPTION_KEY="$TEST_FIELD_ENCRYPTION_KEY_DEFAULT"
|
||||
export JOBS_ENABLED=false
|
||||
export OCR_PROVIDER="${OCR_PROVIDER:-mock}"
|
||||
export OCR_MAX_ATTEMPTS="${OCR_MAX_ATTEMPTS:-3}"
|
||||
export METRICS_ENABLED="${METRICS_ENABLED:-false}"
|
||||
export RATE_LIMIT_ENABLED="${RATE_LIMIT_ENABLED:-false}"
|
||||
export PORTAL_HOME_ENABLED=true
|
||||
export GLOBAL_SEARCH_ENABLED=true
|
||||
export ANNOUNCEMENTS_ENABLED=true
|
||||
export PRESENCE_BOARD_ENABLED=true
|
||||
export ATTENDANCE_ENABLED=true
|
||||
export WEEKLY_REPORT_ENABLED=true
|
||||
export EMPLOYEE_LIFECYCLE_ENABLED=true
|
||||
export TODO_BOARD_ENABLED=true
|
||||
export PROJECT_DOMAIN_ENABLED=true
|
||||
export PROJECT_LINKING_ENABLED=true
|
||||
export PROJECT_COMMUNICATION_LOG_ENABLED=true
|
||||
export PROJECT_CLOSURE_CHECKLIST_ENABLED=true
|
||||
export CONFIDENTIAL_DOC_REFS_ENABLED=true
|
||||
export TRAVEL_SYNC_ENABLED=true
|
||||
}
|
||||
|
||||
require_cmd node
|
||||
require_cmd npm
|
||||
require_cmd curl
|
||||
require_cmd jq
|
||||
require_cmd python3
|
||||
require_db_client
|
||||
select_port
|
||||
export_app_env
|
||||
|
||||
echo ">>> [isolated-dedup] 创建临时数据库:$QA_DB_NAME"
|
||||
create_db
|
||||
|
||||
echo ">>> [isolated-dedup] Prisma generate + migrate deploy"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run prisma:generate
|
||||
npm run prisma:migrate:deploy
|
||||
)
|
||||
|
||||
echo ">>> [isolated-dedup] 写入开发种子数据"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run seed:dev
|
||||
)
|
||||
|
||||
echo ">>> [isolated-dedup] 启动临时服务:$QA_BASE_URL"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run dev >"$QA_SERVER_LOG" 2>&1 &
|
||||
echo "$!" >"$QA_SERVER_PID_FILE"
|
||||
)
|
||||
wait_ready
|
||||
|
||||
echo ">>> [isolated-dedup] 执行真实 HTTP 发票查重回归"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
BASE_URL="$QA_BASE_URL" DATABASE_URL="$DATABASE_URL" ./scripts/regression-dedup.sh
|
||||
)
|
||||
|
||||
echo "[isolated-dedup][PASS] regression-dedup 通过"
|
||||
echo "[isolated-dedup] 产物目录:$QA_ARTIFACT_DIR"
|
||||
在新工单中引用
屏蔽一个用户