- 新增 Feature Flag 模型、评估服务与管理 API(erp_sync/invoice_verification)\n- 完成 ERP 与验真灰度收口(路由/服务/作业统一判定)\n- 补齐 pay 与手动 sync 的 department_id 灰度上下文\n- 增加管理后台开关页、PWA 基础设施与移动拍照/审批页面\n- 增加回归脚本、安全基线与压测脚本,更新文档与执行合同\n\nIssue: G0,G1,G2,G3,G4,G5,G6,H1,M1,M2,M3,M4
234 行
6.6 KiB
Bash
可执行文件
234 行
6.6 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
|
|
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-verification}"
|
|
FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}"
|
|
FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}"
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
echo "[ERROR] curl not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "[ERROR] jq not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT_DIR"
|
|
|
|
if [[ -z "${DATABASE_URL:-}" && -f ".env" ]]; then
|
|
set -a
|
|
# shellcheck disable=SC1091
|
|
source .env
|
|
set +a
|
|
fi
|
|
|
|
if [[ -z "${DATABASE_URL:-}" ]]; then
|
|
echo "[ERROR] DATABASE_URL is required (export it or provide server/.env)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TMP_DIR="$(mktemp -d)"
|
|
RUN_SUFFIX="$(date +%s)-$RANDOM"
|
|
|
|
cleanup() {
|
|
rm -rf "$TMP_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
assert_status() {
|
|
local actual="$1"
|
|
local expected="$2"
|
|
local name="$3"
|
|
local body_file="${4:-}"
|
|
|
|
if [[ "$actual" != "$expected" ]]; then
|
|
echo "[ERROR] $name expected HTTP $expected but got $actual" >&2
|
|
if [[ -n "$body_file" && -f "$body_file" ]]; then
|
|
cat "$body_file" >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
login() {
|
|
local identifier="$1"
|
|
local password="$2"
|
|
local out_file="$3"
|
|
|
|
local status
|
|
status="$(curl -sS -o "$out_file" -w "%{http_code}" \
|
|
-X POST "$BASE_URL/api/auth/login" \
|
|
-H "content-type: application/json" \
|
|
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
|
|
-d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")"
|
|
assert_status "$status" "200" "login" "$out_file"
|
|
jq -er '.data.accessToken' "$out_file"
|
|
}
|
|
|
|
api_json() {
|
|
local method="$1"
|
|
local url="$2"
|
|
local token="$3"
|
|
local out_file="$4"
|
|
local body="${5:-}"
|
|
|
|
if [[ -n "$body" ]]; then
|
|
curl -sS -o "$out_file" -w "%{http_code}" \
|
|
-X "$method" "$url" \
|
|
-H "authorization: Bearer $token" \
|
|
-H "content-type: application/json" \
|
|
-d "$body"
|
|
else
|
|
curl -sS -o "$out_file" -w "%{http_code}" \
|
|
-X "$method" "$url" \
|
|
-H "authorization: Bearer $token"
|
|
fi
|
|
}
|
|
|
|
create_available_invoice() {
|
|
local marker="$1"
|
|
DATABASE_URL="$DATABASE_URL" RUN_SUFFIX="$RUN_SUFFIX" MARKER="$marker" node <<'NODE'
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const crypto = require('node:crypto');
|
|
const { createPrismaClient } = require('./src/prismaClient');
|
|
const { getConfig } = require('./src/config');
|
|
|
|
(async () => {
|
|
const marker = process.env.MARKER || 'VERIFY_OK';
|
|
const runSuffix = process.env.RUN_SUFFIX || 'default';
|
|
const config = getConfig();
|
|
const { prisma, pool } = createPrismaClient();
|
|
await prisma.$connect();
|
|
|
|
try {
|
|
const owner = await prisma.employee.findUnique({
|
|
where: { email: '[email protected]' },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!owner) {
|
|
throw new Error('finance_mgr not found');
|
|
}
|
|
|
|
const relPath = path.join('verification-regression', runSuffix, `${crypto.randomUUID()}.txt`);
|
|
const absPath = path.resolve(config.storageDir, relPath);
|
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
await fs.writeFile(absPath, marker, 'utf8');
|
|
|
|
const fileSha256 = crypto.createHash('sha256').update(marker).digest('hex');
|
|
const created = await prisma.invoicePool.create({
|
|
data: {
|
|
owner_employee_id: owner.id,
|
|
uploaded_by_employee_id: owner.id,
|
|
storage_path: relPath,
|
|
original_name: `verify-${runSuffix}.txt`,
|
|
mime_type: 'text/plain',
|
|
file_size: Buffer.byteLength(marker),
|
|
file_sha256: fileSha256,
|
|
status: 'available',
|
|
ocr_type: 'general',
|
|
invoice_code: 'MOCKPASS',
|
|
invoice_number: 'PASS',
|
|
remarks: marker.includes('FAIL') ? 'verify-fail' : null,
|
|
created_by: owner.id,
|
|
updated_by: owner.id,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
process.stdout.write(`${created.id}\n`);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
NODE
|
|
}
|
|
|
|
set_verification_flag() {
|
|
local mode="$1"
|
|
DATABASE_URL="$DATABASE_URL" MODE="$mode" node <<'NODE'
|
|
require("dotenv/config");
|
|
|
|
const { createPrismaClient } = require("./src/prismaClient");
|
|
|
|
(async () => {
|
|
const mode = process.env.MODE;
|
|
const { prisma, pool } = createPrismaClient();
|
|
await prisma.$connect();
|
|
|
|
try {
|
|
await prisma.featureFlag.upsert({
|
|
where: { flag_key: "invoice_verification" },
|
|
create: {
|
|
flag_key: "invoice_verification",
|
|
override_mode: mode,
|
|
rollout_type: "global",
|
|
department_ids: [],
|
|
note: "regression-verification",
|
|
},
|
|
update: {
|
|
override_mode: mode,
|
|
rollout_type: "global",
|
|
department_ids: [],
|
|
note: "regression-verification",
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
NODE
|
|
}
|
|
|
|
echo "[0/6] Ensure verification feature flag force_on"
|
|
set_verification_flag "force_on"
|
|
|
|
echo "[1/6] Login finance_mgr"
|
|
TOKEN="$(login "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/login.json")"
|
|
|
|
echo "[2/6] Create synthetic available invoice"
|
|
INVOICE_ID="$(create_available_invoice "VERIFY_OK")"
|
|
|
|
echo "[3/6] Request verification"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/verify" "$TOKEN" "$TMP_DIR/verify.json" '{}')"
|
|
assert_status "$status" "200" "invoice verify" "$TMP_DIR/verify.json"
|
|
VERIFICATION_ID="$(jq -er '.data.verificationId' "$TMP_DIR/verify.json")"
|
|
|
|
echo "[4/6] Run verification worker inline"
|
|
VERIFICATION_RECORD_ID="$VERIFICATION_ID" INVOICE_VERIFICATION_ENABLED=true VERIFICATION_PROVIDER=mock npm run --silent job:verification-once >"$TMP_DIR/verify-once.log"
|
|
|
|
sleep 0.2
|
|
|
|
echo "[5/6] Query verification records"
|
|
status="$(api_json GET "$BASE_URL/api/invoice-pool/$INVOICE_ID/verification?limit=5" "$TOKEN" "$TMP_DIR/records.json")"
|
|
assert_status "$status" "200" "list verification" "$TMP_DIR/records.json"
|
|
jq -e '.data.items | length >= 1' "$TMP_DIR/records.json" >/dev/null
|
|
jq -e '.data.items[0].status == "passed"' "$TMP_DIR/records.json" >/dev/null
|
|
|
|
echo "[6/6] Retry should be throttled immediately"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/verification/retry" "$TOKEN" "$TMP_DIR/retry.json" '{}')"
|
|
if [[ "$status" != "409" ]]; then
|
|
echo "[ERROR] expected retry throttled 409 but got $status" >&2
|
|
cat "$TMP_DIR/retry.json" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[OK] Verification regression passed"
|
|
|
|
echo "[post] Reset verification feature flag force_off"
|
|
set_verification_flag "force_off"
|