feat(server): 委派回收并发防重 + 最小门禁闸门
- delegationRevokeJob 改用 FOR UPDATE SKIP LOCKED + UPDATE RETURNING 原子认领 - regression-workflow-actions 新增 RUN_DELEGATION_CONCURRENCY_CHECK 并发校验开关 - README 补充最小门禁与并发语义说明
这个提交包含在:
@@ -12,6 +12,7 @@ FINANCE_PASSWORD="${FINANCE_PASSWORD:-FinancePassword1234}"
|
||||
CEO_IDENTIFIER="${CEO_IDENTIFIER:-ceo@hyinda.com}"
|
||||
CEO_PASSWORD="${CEO_PASSWORD:-CeoPassword1234}"
|
||||
RUN_PAY_CONCURRENCY_CHECK="${RUN_PAY_CONCURRENCY_CHECK:-0}"
|
||||
RUN_DELEGATION_CONCURRENCY_CHECK="${RUN_DELEGATION_CONCURRENCY_CHECK:-0}"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "[ERROR] curl not found" >&2
|
||||
@@ -864,6 +865,42 @@ const { createPrismaClient } = require('./src/prismaClient');
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_delegation_auto_revoke_audit_count() {
|
||||
local delegation_id="$1"
|
||||
local expected_count="$2"
|
||||
local scenario="$3"
|
||||
|
||||
DATABASE_URL="$DATABASE_URL" node - "$delegation_id" "$expected_count" "$scenario" <<'NODE'
|
||||
const [delegationId, expectedCountRaw, scenario] = process.argv.slice(2);
|
||||
const expectedCount = Number(expectedCountRaw);
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
const count = await prisma.auditLog.count({
|
||||
where: {
|
||||
action: 'DELEGATION_AUTO_REVOKED',
|
||||
target_id: delegationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (count !== expectedCount) {
|
||||
console.error(`[ERROR] ${scenario} expected audit count=${expectedCount} but got ${count}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
echo "[1/23] Health check + login baseline users"
|
||||
health_status="$(curl -sS -o "$TMP_DIR/health.json" -w "%{http_code}" "$BASE_URL/health")"
|
||||
assert_status "$health_status" "200" "health check" "$TMP_DIR/health.json"
|
||||
@@ -1137,13 +1174,45 @@ ceo_approve_expired_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/$
|
||||
assert_status "$ceo_approve_expired_status" "403" "expired delegation should be forbidden" "$TMP_DIR/delegation-expired-ceo-approve.json"
|
||||
assert_error_code "$TMP_DIR/delegation-expired-ceo-approve.json" "PERM_002" "expired delegation should be forbidden"
|
||||
|
||||
revoke_job_output="$(npm run --silent job:revoke-expired-delegations)"
|
||||
revoke_job_status="$(printf '%s\n' "$revoke_job_output" | tail -n 1)"
|
||||
if [[ "$revoke_job_status" != revoked=* ]]; then
|
||||
echo "[ERROR] revoke expired delegations script output invalid: $revoke_job_output" >&2
|
||||
exit 1
|
||||
if [[ "$RUN_DELEGATION_CONCURRENCY_CHECK" == "1" ]]; then
|
||||
echo "[18.1/23] optional delegation revoke concurrency check"
|
||||
(
|
||||
npm run --silent job:revoke-expired-delegations > "$TMP_DIR/delegation-revoke-a.out"
|
||||
) &
|
||||
revoke_pid_a=$!
|
||||
(
|
||||
npm run --silent job:revoke-expired-delegations > "$TMP_DIR/delegation-revoke-b.out"
|
||||
) &
|
||||
revoke_pid_b=$!
|
||||
wait "$revoke_pid_a"
|
||||
wait "$revoke_pid_b"
|
||||
|
||||
revoke_job_output_a="$(cat "$TMP_DIR/delegation-revoke-a.out")"
|
||||
revoke_job_output_b="$(cat "$TMP_DIR/delegation-revoke-b.out")"
|
||||
revoke_job_status_a="$(printf '%s\n' "$revoke_job_output_a" | tail -n 1)"
|
||||
revoke_job_status_b="$(printf '%s\n' "$revoke_job_output_b" | tail -n 1)"
|
||||
|
||||
if [[ "$revoke_job_status_a" != revoked=* ]]; then
|
||||
echo "[ERROR] revoke concurrency output A invalid: $revoke_job_output_a" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$revoke_job_status_b" != revoked=* ]]; then
|
||||
echo "[ERROR] revoke concurrency output B invalid: $revoke_job_output_b" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
assert_delegation_revoked_db "$DELEGATION_EXPIRED_ID" "delegation auto revoke concurrency"
|
||||
assert_delegation_auto_revoke_audit_count "$DELEGATION_EXPIRED_ID" "1" "delegation auto revoke concurrency"
|
||||
else
|
||||
revoke_job_output="$(npm run --silent job:revoke-expired-delegations)"
|
||||
revoke_job_status="$(printf '%s\n' "$revoke_job_output" | tail -n 1)"
|
||||
if [[ "$revoke_job_status" != revoked=* ]]; then
|
||||
echo "[ERROR] revoke expired delegations script output invalid: $revoke_job_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_delegation_revoked_db "$DELEGATION_EXPIRED_ID" "delegation auto revoke"
|
||||
fi
|
||||
assert_delegation_revoked_db "$DELEGATION_EXPIRED_ID" "delegation auto revoke"
|
||||
|
||||
manager_approve_after_expired_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/$TASK_DELEGATION_EXPIRED/approve" "$MANAGER_TOKEN" "$TMP_DIR/delegation-expired-manager-approve.json" '{}')"
|
||||
assert_status "$manager_approve_after_expired_status" "200" "manager approve after expired delegation" "$TMP_DIR/delegation-expired-manager-approve.json"
|
||||
|
||||
在新工单中引用
屏蔽一个用户