P2FLIVE 收口公网付款批次短窗口测试

这个提交包含在:
2026-05-17 00:19:39 +08:00
父节点 08853c7f03
当前提交 20a783543b
共修改 6 个文件,包含 746 行新增和 10 行删除
@@ -0,0 +1,522 @@
import { expect, test, type APIRequestContext } from "@playwright/test";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { apiGetData, apiPostData, type AuthenticatedApiClient } from "../helpers/auth";
import { createLiveApiClient, type LiveSmokeRole } from "../helpers/liveAuth";
import { approveWorkflowTaskViaApi, waitForWorkflowTask } from "../helpers/workflow";
type FeatureFlagItem = {
flagKey: string;
overrideMode: "inherit" | "force_on" | "force_off";
rolloutType: "global" | "department_allowlist";
departmentIds: string[];
note: string;
version: number;
effectiveEnabled: boolean;
effectiveReason: string;
};
type FeatureFlagSnapshot = Pick<
FeatureFlagItem,
"flagKey" | "overrideMode" | "rolloutType" | "departmentIds" | "version" | "effectiveEnabled" | "effectiveReason"
>;
type ReimbursementDetail = {
id: string;
formNumber: string | null;
status: string;
totalAmount: string | null;
};
type InvoiceDetail = {
id: string;
status: string;
linkedReimbursementId: string | null;
archivedAt: string | null;
};
type PaymentBatch = {
id: string;
name: string;
status: string;
reviewRequirement: {
completedReviewCount: number;
requiredReviewCount: number;
nextStep: string;
};
};
type FinanceLedgerResponse = {
total: number;
items: Array<{
id: string;
formNumber: string | null;
status: string;
payment: {
status: string;
paymentVoucherNo: string | null;
paidAt: string | null;
};
}>;
};
type AuditLogListResponse = {
items: Array<{
action: string;
targetId: string | null;
actorId: string | null;
extra: Record<string, unknown> | null;
}>;
};
type LiveSummary = {
issueId: string;
mode: string;
status: "RUNNING" | "PASS" | "FAIL";
baseUrl: string;
marker: string;
startedAt: string;
completedAt?: string;
stages: Record<string, unknown>;
error?: string;
};
const issueId = "P2FLIVE";
const baseUrl = process.env.LIVE_SMOKE_BASE_URL?.trim() || "https://oa.hyinda.com";
const reportDir = process.env.P2_FIN_LIVE_REPORT_DIR?.trim() ||
path.resolve(process.cwd(), "..", "artifacts/qa-flow/p2-fin-live-local");
const summaryPath = path.join(reportDir, "live-result.json");
function nowMarker() {
return `P2FLIVE-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function today() {
return new Date().toISOString().slice(0, 10);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function sanitizeError(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return message
.replace(/Bearer\s+[A-Za-z0-9._-]+/g, "Bearer <redacted>")
.replace(/password[^,}\n]*/gi, "password=<redacted>")
.slice(0, 1000);
}
function snapshotFlag(item: FeatureFlagItem): FeatureFlagSnapshot {
return {
flagKey: item.flagKey,
overrideMode: item.overrideMode,
rolloutType: item.rolloutType,
departmentIds: item.departmentIds,
version: item.version,
effectiveEnabled: item.effectiveEnabled,
effectiveReason: item.effectiveReason,
};
}
async function parseData<T>(response: { ok(): boolean; status(): number; text(): Promise<string> }, label: string) {
const text = await response.text();
let payload: { data?: T; error?: { code?: string; message?: string; details?: unknown } } = {};
try {
payload = text ? JSON.parse(text) : {};
} catch {
payload = {};
}
if (!response.ok()) {
const error = payload.error;
throw new Error(`${label} failed: HTTP ${response.status()} ${error?.code ?? ""} ${error?.message ?? ""}`.trim());
}
return payload.data as T;
}
async function createLiveClientWithRetry(role: LiveSmokeRole): Promise<AuthenticatedApiClient> {
let lastError: unknown = null;
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
return await createLiveApiClient(role);
} catch (error) {
lastError = error;
if (!/429|请求过于频繁|rate.?limit|too many requests/i.test(sanitizeError(error)) || attempt > 0) {
break;
}
await sleep(Number.parseInt(process.env.P2_FIN_LIVE_LOGIN_RETRY_WAIT_MS ?? "65000", 10));
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
async function loadFlag(api: APIRequestContext, flagKey: string) {
const data = await apiGetData<{ items: FeatureFlagItem[] }>(api, "/api/system/feature-flags");
const item = data.items.find((candidate) => candidate.flagKey === flagKey);
if (!item) {
throw new Error(`Missing feature flag: ${flagKey}`);
}
return item;
}
async function updateFlag(
api: APIRequestContext,
flagKey: string,
overrideMode: FeatureFlagItem["overrideMode"],
note: string,
) {
const current = await loadFlag(api, flagKey);
const response = await api.put(`/api/system/feature-flags/${flagKey}`, {
data: {
overrideMode,
rolloutType: "global",
departmentIds: [],
note,
expectedVersion: current.version,
},
});
const data = await parseData<{ item: FeatureFlagItem }>(response, `update ${flagKey}`);
return data.item;
}
async function forcePaymentBatchOff(api: APIRequestContext, note: string) {
const current = await loadFlag(api, "payment_batch_v1");
if (current.overrideMode === "force_off" && !current.effectiveEnabled) {
return current;
}
return updateFlag(api, "payment_batch_v1", "force_off", note);
}
function resolveLivePdfBuffer(marker: string) {
const sample = process.env.PLAYWRIGHT_OCR_LIVE_FILE?.trim() || process.env.LIVE_OCR_INVOICE_FILE?.trim() || "";
if (!sample || !existsSync(sample) || path.extname(sample).toLowerCase() !== ".pdf") {
throw new Error("missing_authorized_pdf_sample");
}
const source = readFileSync(sample);
return Buffer.concat([source, Buffer.from(`\n% ${marker}\n`, "utf8")]);
}
async function firstEnabledExpenseTypeId(api: APIRequestContext) {
const data = await apiGetData<{ items: Array<{ id: string; enabled?: boolean }> }>(api, "/api/expense-types?enabled=true");
const item = data.items.find((candidate) => candidate.enabled !== false) ?? data.items[0];
if (!item?.id) {
throw new Error("missing_enabled_expense_type");
}
return item.id;
}
async function createTestReimbursement(api: APIRequestContext, marker: string) {
const reimbursement = await apiPostData<ReimbursementDetail>(api, "/api/reimbursements", {
description: `P2FINLIVE 公网付款测试 ${marker}`,
reimbursementDate: today(),
});
const expenseTypeId = await firstEnabledExpenseTypeId(api);
await apiPostData<Record<string, unknown>>(api, `/api/reimbursements/${reimbursement.id}/items`, {
expenseTypeId,
expenseDate: today(),
amount: "168.30",
description: `P2FINLIVE 小额测试 ${marker}`,
});
return apiGetData<ReimbursementDetail>(api, `/api/reimbursements/${reimbursement.id}`);
}
async function uploadAndBindPdfInvoice(api: APIRequestContext, reimbursementId: string, marker: string) {
const buffer = resolveLivePdfBuffer(marker);
const response = await api.post("/api/invoice-pool/upload", {
multipart: {
ocr_type: "general",
file: {
name: `p2-fin-live-${marker}.pdf`,
mimeType: "application/pdf",
buffer,
},
},
});
const upload = await parseData<{
invoicePoolId: string | null;
status: string;
uploadAction?: string;
unavailableReason?: string | null;
}>(response, "upload invoice");
if (!upload.invoicePoolId || upload.status !== "available") {
throw new Error(`invoice_not_bindable:${upload.status}:${upload.uploadAction ?? "-"}:${upload.unavailableReason ?? "-"}`);
}
await apiPostData<Record<string, unknown>>(api, `/api/invoice-pool/${upload.invoicePoolId}/bind`, { reimbursementId });
return upload.invoicePoolId;
}
async function approveReimbursementToApproved(input: {
employeeApi: APIRequestContext;
managerApi: APIRequestContext;
financeApi: APIRequestContext;
reimbursementId: string;
}) {
const submit = await apiPostData<{ currentTaskId?: string }>(
input.employeeApi,
`/api/reimbursements/${input.reimbursementId}/submit`,
{},
);
const managerTaskId =
submit.currentTaskId ??
(await waitForWorkflowTask(input.managerApi, { entityType: "reimbursement", entityId: input.reimbursementId })).id;
await approveWorkflowTaskViaApi(input.managerApi, managerTaskId);
let financeTaskId = "";
try {
financeTaskId = (await waitForWorkflowTask(input.financeApi, {
entityType: "reimbursement",
entityId: input.reimbursementId,
attempts: 12,
intervalMs: 1000,
})).id;
} catch {
financeTaskId = "";
}
if (financeTaskId) {
await approveWorkflowTaskViaApi(input.financeApi, financeTaskId);
}
await expect
.poll(async () => {
const detail = await apiGetData<ReimbursementDetail>(input.financeApi, `/api/reimbursements/${input.reimbursementId}`);
return detail.status;
}, { timeout: 30_000 })
.toBe("approved");
return { managerTaskId, financeTaskId };
}
async function createPaymentBatch(api: APIRequestContext, input: { reimbursementId: string; name: string; marker: string }) {
const data = await apiPostData<{ item: PaymentBatch }>(api, "/api/payment-batches", {
entityType: "reimbursement",
name: input.name,
entityIds: [input.reimbursementId],
defaultPaymentDate: today(),
defaultPaymentChannel: "bank",
voucherPrefix: `P2LIVE-${input.marker.slice(-6)}`,
});
return data.item;
}
async function expectPostRejected(api: APIRequestContext, pathName: string, data: unknown, label: string) {
const response = await api.post(pathName, { data });
expect(response.ok(), `${label} should be rejected`).toBe(false);
expect([403, 409, 404]).toContain(response.status());
return response.status();
}
function isWildcardSuperAdmin(client: AuthenticatedApiClient) {
const roleCodes = client.me.roleCodes ?? [];
return roleCodes.includes("admin");
}
async function fetchFinanceLedger(api: APIRequestContext, query: string) {
const params = new URLSearchParams({ page: "1", pageSize: "20", q: query });
return apiGetData<FinanceLedgerResponse>(api, `/api/ap-operations/reimbursement-ledger?${params.toString()}`);
}
async function expectAuditAction(
api: APIRequestContext,
action: string,
predicate: (item: AuditLogListResponse["items"][number]) => boolean,
) {
await expect
.poll(async () => {
const data = await apiGetData<AuditLogListResponse>(
api,
`/api/audit-logs?action=${encodeURIComponent(action)}&page=1&pageSize=50`,
);
return data.items.some(predicate);
}, { timeout: 20_000 })
.toBe(true);
}
test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
test.skip(process.env.E2E_MODE !== "live", "仅在 live 模式下执行");
test.describe.configure({ mode: "serial" });
test("公网付款批次 flag-on 闭环后强制回滚到 flag-off", async ({}, testInfo) => {
test.setTimeout(420_000);
if (process.env.P2_FIN_LIVE_APPROVED !== "1") {
throw new Error("P2_FIN_LIVE_APPROVED=1 is required for public write test");
}
mkdirSync(reportDir, { recursive: true });
const marker = nowMarker();
const summary: LiveSummary = {
issueId,
mode: "p2-fin-live",
status: "RUNNING",
baseUrl,
marker,
startedAt: new Date().toISOString(),
stages: {},
};
const clients: AuthenticatedApiClient[] = [];
let admin: AuthenticatedApiClient | null = null;
let caughtError: unknown = null;
let rollbackError: unknown = null;
try {
admin = await createLiveClientWithRetry("admin");
const employee = await createLiveClientWithRetry("employee");
const manager = await createLiveClientWithRetry("manager");
const finance = await createLiveClientWithRetry("finance");
const financeMgr = await createLiveClientWithRetry("financeMgr");
clients.push(admin, employee, manager, finance, financeMgr);
const paymentFlagBefore = await loadFlag(admin.api, "payment_batch_v1");
const erpFlagBefore = await loadFlag(admin.api, "erp_sync");
summary.stages.preflight = {
paymentFlagBefore: snapshotFlag(paymentFlagBefore),
erpSyncEffectiveEnabled: erpFlagBefore.effectiveEnabled,
};
expect(paymentFlagBefore.effectiveEnabled, "payment_batch_v1 should be off before public live window").toBe(false);
expect(erpFlagBefore.effectiveEnabled, "erp_sync must remain off for this test").toBe(false);
const reimbursement = await createTestReimbursement(employee.api, marker);
const invoiceId = await uploadAndBindPdfInvoice(employee.api, reimbursement.id, marker);
const approval = await approveReimbursementToApproved({
employeeApi: employee.api,
managerApi: manager.api,
financeApi: finance.api,
reimbursementId: reimbursement.id,
});
const approved = await apiGetData<ReimbursementDetail>(finance.api, `/api/reimbursements/${reimbursement.id}`);
summary.stages.reimbursement = {
reimbursementId: reimbursement.id,
formNumber: approved.formNumber,
status: approved.status,
invoiceId,
managerTaskId: approval.managerTaskId,
financeTaskApproved: Boolean(approval.financeTaskId),
};
const paymentFlagOn = await updateFlag(
admin.api,
"payment_batch_v1",
"force_on",
`P2FLIVE short window ${marker}`,
);
summary.stages.flagOn = { paymentFlagOn: snapshotFlag(paymentFlagOn) };
expect(paymentFlagOn.effectiveEnabled).toBe(true);
const erpFlagAfterPaymentOn = await loadFlag(admin.api, "erp_sync");
expect(erpFlagAfterPaymentOn.effectiveEnabled, "erp_sync must not be opened by P2FLIVE").toBe(false);
const batchName = `P2FINLIVE 公网付款批次 ${marker}`;
const adminWildcard = isWildcardSuperAdmin(admin);
const rejectedCreate: Record<string, string | number> = {
employee: await expectPostRejected(employee.api, "/api/payment-batches", {
entityType: "reimbursement",
name: `${batchName} employee denied`,
entityIds: [reimbursement.id],
}, "employee create payment batch"),
manager: await expectPostRejected(manager.api, "/api/payment-batches", {
entityType: "reimbursement",
name: `${batchName} manager denied`,
entityIds: [reimbursement.id],
}, "manager create payment batch"),
};
if (adminWildcard) {
rejectedCreate.admin = "SKIP_WILDCARD_SUPERADMIN";
} else {
rejectedCreate.admin = await expectPostRejected(admin.api, "/api/payment-batches", {
entityType: "reimbursement",
name: `${batchName} admin denied`,
entityIds: [reimbursement.id],
}, "admin-only create payment batch");
}
let batch = await createPaymentBatch(finance.api, {
reimbursementId: reimbursement.id,
name: batchName,
marker,
});
const adminExecuteBoundary = adminWildcard
? "SKIP_WILDCARD_SUPERADMIN"
: await expectPostRejected(admin.api, `/api/payment-batches/${batch.id}/execute`, {}, "admin-only execute payment batch");
batch = (await apiPostData<{ item: PaymentBatch }>(financeMgr.api, `/api/payment-batches/${batch.id}/review`, {
comment: `P2FLIVE 复核 ${marker}`,
})).item;
expect(batch.reviewRequirement.completedReviewCount).toBeGreaterThanOrEqual(1);
batch = (await apiPostData<{ item: PaymentBatch }>(finance.api, `/api/payment-batches/${batch.id}/execute`, {})).item;
expect(batch.status).toBe("executed");
const paid = await apiGetData<ReimbursementDetail>(finance.api, `/api/reimbursements/${reimbursement.id}`);
expect(paid.status).toBe("paid");
const invoice = await apiGetData<InvoiceDetail>(employee.api, `/api/invoice-pool/${invoiceId}`);
expect(invoice.status).toBe("archived");
expect(invoice.linkedReimbursementId).toBe(reimbursement.id);
const ledger = await fetchFinanceLedger(finance.api, paid.formNumber ?? reimbursement.id);
const ledgerItem = ledger.items.find((item) => item.id === reimbursement.id || item.formNumber === paid.formNumber);
expect(ledgerItem?.payment.status).toBe("paid");
const exportResponse = await finance.api.post("/api/exports/ap-operations", {
data: { format: "csv", ledgerSearch: paid.formNumber ?? reimbursement.id },
});
expect(exportResponse.status(), "finance AP export should succeed while flag is on").toBe(200);
await expectAuditAction(admin.api, "FEATURE_FLAG_UPDATED", (item) =>
item.extra?.flagKey === "payment_batch_v1" &&
(item.extra?.to as Record<string, unknown> | undefined)?.overrideMode === "force_on",
);
await expectAuditAction(admin.api, "PAYMENT_BATCH_CREATED", (item) => item.targetId === batch.id);
await expectAuditAction(admin.api, "PAYMENT_BATCH_REVIEWED", (item) => item.targetId === batch.id);
await expectAuditAction(admin.api, "PAYMENT_BATCH_EXECUTED", (item) => item.targetId === batch.id);
await expectAuditAction(admin.api, "REIMBURSEMENT_PAYMENT_MARKED", (item) => item.targetId === reimbursement.id);
await expectAuditAction(admin.api, "EXPORT_AP_OPERATIONS", (item) => item.actorId === finance.me.id);
summary.stages.payment = {
batchId: batch.id,
batchStatus: batch.status,
reimbursementStatus: paid.status,
invoiceStatus: invoice.status,
ledgerPaymentStatus: ledgerItem?.payment.status ?? "missing",
rejectedCreate,
adminExecuteBoundary,
exportStatus: exportResponse.status(),
};
summary.status = "PASS";
} catch (error) {
caughtError = error;
summary.status = "FAIL";
summary.error = sanitizeError(error);
} finally {
if (admin) {
try {
const rolledBack = await forcePaymentBatchOff(admin.api, `P2FLIVE rollback ${marker}`);
summary.stages.rollback = { paymentFlagAfterRollback: snapshotFlag(rolledBack) };
expect(rolledBack.effectiveEnabled).toBe(false);
} catch (error) {
rollbackError = error;
summary.status = "FAIL";
summary.stages.rollback = { error: sanitizeError(error) };
}
}
summary.completedAt = new Date().toISOString();
writeFileSync(summaryPath, JSON.stringify(summary, null, 2), "utf8");
await testInfo.attach("p2-fin-live-summary.json", {
path: summaryPath,
contentType: "application/json",
});
await Promise.all(clients.map((client) => client.dispose().catch(() => undefined)));
}
if (caughtError) {
throw caughtError;
}
if (rollbackError) {
throw rollbackError;
}
});
});
+6 -3
查看文件
@@ -24,7 +24,8 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
- 2026-05-16 P2Q 已完成 P2OCRS 后续任务队列规划;P2AF 后图片 OCR 不再作为默认主线等待样票,后续默认按 UX8 真实反馈池、P2-FIN-LIVE 公网启用窗口、P2-WF、P2-BIZ 和持续上线门禁拆独立合同推进。 - 2026-05-16 P2Q 已完成 P2OCRS 后续任务队列规划;P2AF 后图片 OCR 不再作为默认主线等待样票,后续默认按 UX8 真实反馈池、P2-FIN-LIVE 公网启用窗口、P2-WF、P2-BIZ 和持续上线门禁拆独立合同推进。
- 2026-05-16 P2AUTO 已完成自动化优先回归:后端 focused、前端 typecheck/build、高风险确认、隔离核心业务流、隔离付款批次闭环、公网 readiness/PDF live OCR、公网移动矩阵和企业微信 UA/渲染自动化均通过;PDF 样票和 live smoke 凭据可用,P2AF 后 provider-only 默认结论为 `FREEZE_BY_DEFAULT`,不开放图片 OCR,不启用公网 `payment_batch_v1`,未部署。 - 2026-05-16 P2AUTO 已完成自动化优先回归:后端 focused、前端 typecheck/build、高风险确认、隔离核心业务流、隔离付款批次闭环、公网 readiness/PDF live OCR、公网移动矩阵和企业微信 UA/渲染自动化均通过;PDF 样票和 live smoke 凭据可用,P2AF 后 provider-only 默认结论为 `FREEZE_BY_DEFAULT`,不开放图片 OCR,不启用公网 `payment_batch_v1`,未部署。
- 2026-05-16 P2AF 已完成可自动实施收口:图片 OCR 默认 `FREEZE_BY_DEFAULT`,PDF-only OCR 继续维护;新增 QA-only 回归聚合和付款批次公网关闭态只读门禁,公网 `payment_batch_v1` 继续 `flag_force_off`,本轮不做业务实现、不部署。 - 2026-05-16 P2AF 已完成可自动实施收口:图片 OCR 默认 `FREEZE_BY_DEFAULT`,PDF-only OCR 继续维护;新增 QA-only 回归聚合和付款批次公网关闭态只读门禁,公网 `payment_batch_v1` 继续 `flag_force_off`,本轮不做业务实现、不部署。
- 2026-05-16 P2-FIN-LIVE-DECISION 已完成公网付款批次启用窗口决策:live smoke/PDF OCR readiness 通过,付款批次公网关闭态只读门禁通过;由于尚未明确公网启用窗口、财务/复核参与人、flag 启停人、回滚联系人、暂停/继续决策人和测试报销单来源,结论为 `HOLD_FOR_WINDOW`,公网 `payment_batch_v1` 继续关闭。 - 2026-05-16 P2-FIN-LIVE-DECISION 已完成公网付款批次启用窗口决策:live smoke/PDF OCR readiness 通过,付款批次公网关闭态只读门禁通过;初始结论为 `HOLD_FOR_WINDOW`,随后用户批准立即短窗口、现有 live smoke 测试账号和新建测试报销,进入 `P2-FIN-LIVE`。
- 2026-05-17 P2-FIN-LIVE 已按用户批准完成公网付款批次短窗口受控测试:使用现有 live smoke 测试账号新建专用测试报销,临时打开 `payment_batch_v1`,完成付款批次创建、复核、执行、AP 导出和审计核验后强制回到 `flag_force_off`;未部署,不接真实支付,不写 ERP 实时凭证,不开放图片 OCR。
## 关键证据索引 ## 关键证据索引
@@ -56,7 +57,8 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
| P2Q | 已收口:P2OCRS 后续队列和启动门槛完成;图片 OCR 默认冻结,所有实现或公网启用均需另建独立合同 | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | | P2Q | 已收口:P2OCRS 后续队列和启动门槛完成;图片 OCR 默认冻结,所有实现或公网启用均需另建独立合同 | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` |
| P2AUTO | 已收口:自动化优先回归和样票 provider 门禁完成;核心隔离业务流通过、付款批次隔离闭环 1 passed、公网 readiness/PDF live OCR 通过、公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed;provider 默认结论改为 `FREEZE_BY_DEFAULT` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv`、`artifacts/qa-flow/business-flow-20260516-203001/summary.txt`、`artifacts/qa-flow/business-flow-20260516-203856/summary.txt`、`artifacts/qa-flow/live-smoke-readiness-20260516-204157/` | | P2AUTO | 已收口:自动化优先回归和样票 provider 门禁完成;核心隔离业务流通过、付款批次隔离闭环 1 passed、公网 readiness/PDF live OCR 通过、公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed;provider 默认结论改为 `FREEZE_BY_DEFAULT` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv`、`artifacts/qa-flow/business-flow-20260516-203001/summary.txt`、`artifacts/qa-flow/business-flow-20260516-203856/summary.txt`、`artifacts/qa-flow/live-smoke-readiness-20260516-204157/` |
| P2AF | 已收口:可自动实施收口完成;安全回归聚合 PASS,付款批次公网关闭态只读门禁 PASS,UX8 自动矩阵通过且 UX8-1..5 保持真实反馈驱动 | `issues/2026-05-16_22-11-10-p2-automatable-followup.csv`、`artifacts/qa-flow/p2-safe-regression-20260516-222222/summary.json` | | P2AF | 已收口:可自动实施收口完成;安全回归聚合 PASS,付款批次公网关闭态只读门禁 PASS,UX8 自动矩阵通过且 UX8-1..5 保持真实反馈驱动 | `issues/2026-05-16_22-11-10-p2-automatable-followup.csv`、`artifacts/qa-flow/p2-safe-regression-20260516-222222/summary.json` |
| P2-FIN-LIVE-DECISION | 已收口:公网付款批次启用窗口决策完成,结论 `HOLD_FOR_WINDOW`;readiness 和 flag-off 证据通过,等待明确窗口、参与人、回滚联系人和测试报销单来源 | `issues/2026-05-16_22-49-05-p2-fin-payment-batch-live-decision.csv` | | P2-FIN-LIVE-DECISION | 已收口:公网付款批次启用窗口决策完成,初始结论 `HOLD_FOR_WINDOW`;readiness 和 flag-off 证据通过,后续由用户批准进入 P2-FIN-LIVE 短窗口 | `issues/2026-05-16_22-49-05-p2-fin-payment-batch-live-decision.csv` |
| P2-FIN-LIVE | 已收口:公网短窗口受控测试 PASS;专用测试报销 `BX-20260517-0022` 付款闭环到 paid,票据 archived,AP 台账 paid,导出和审计核验通过;公网移动矩阵 6 passed,企业微信渲染矩阵 8 passed;最终 `payment_batch_v1=flag_force_off` | `issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv`、`artifacts/qa-flow/p2-fin-live-20260517-001022/summary.json`、`artifacts/qa-flow/p2-fin-live-20260517-001022/live-result.json` |
## 当前门禁 ## 当前门禁
@@ -78,7 +80,8 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
| P2Q 后续队列 | 已收口,未部署 | 图片 OCR 不再作为默认主线等待样票;UX8 等待真实反馈;P2-FIN-LIVE 等待公网启用窗口批准;后续 P2-WF/P2-BIZ 均需独立合同 | | P2Q 后续队列 | 已收口,未部署 | 图片 OCR 不再作为默认主线等待样票;UX8 等待真实反馈;P2-FIN-LIVE 等待公网启用窗口批准;后续 P2-WF/P2-BIZ 均需独立合同 |
| P2AUTO 自动化优先回归 | 通过,未部署 | 后端 focused 43 tests passed;`client` typecheck/build 通过;核心隔离业务流 auth/reimbursement/approval/leave/procurement/governance PASS;付款批次隔离 flag-on 1 passed;公网 `/ready`、public bundle、live smoke 3 passed、PDF live OCR 1 passed;公网移动矩阵 6 passed;企业微信渲染矩阵 8 passed;P2AF 后 provider-only 默认结论为 `FREEZE_BY_DEFAULT` | | P2AUTO 自动化优先回归 | 通过,未部署 | 后端 focused 43 tests passed;`client` typecheck/build 通过;核心隔离业务流 auth/reimbursement/approval/leave/procurement/governance PASS;付款批次隔离 flag-on 1 passed;公网 `/ready`、public bundle、live smoke 3 passed、PDF live OCR 1 passed;公网移动矩阵 6 passed;企业微信渲染矩阵 8 passed;P2AF 后 provider-only 默认结论为 `FREEZE_BY_DEFAULT` |
| P2AF 可自动实施收口 | 通过,未部署 | `run-p2-safe-regression.sh` PASS:后端 focused 43 tests passed,`client` typecheck/build 通过,核心隔离业务流 auth/reimbursement/approval/leave/procurement/governance PASS,公网 readiness/PDF live OCR 通过,付款批次公网关闭态 409 `payment_batch_v1_disabled / flag_force_off`,公网移动矩阵 6 passed,企业微信渲染矩阵 8 passed,OCR provider-only 默认 `FREEZE_BY_DEFAULT`;不启用公网 `payment_batch_v1`,不开放图片 OCR,不做业务实现 | | P2AF 可自动实施收口 | 通过,未部署 | `run-p2-safe-regression.sh` PASS:后端 focused 43 tests passed,`client` typecheck/build 通过,核心隔离业务流 auth/reimbursement/approval/leave/procurement/governance PASS,公网 readiness/PDF live OCR 通过,付款批次公网关闭态 409 `payment_batch_v1_disabled / flag_force_off`,公网移动矩阵 6 passed,企业微信渲染矩阵 8 passed,OCR provider-only 默认 `FREEZE_BY_DEFAULT`;不启用公网 `payment_batch_v1`,不开放图片 OCR,不做业务实现 |
| P2-FIN-LIVE-DECISION | HOLD,未部署 | `check-live-smoke-env --strict` 通过,`check-payment-batch-flag-off --json` 通过,财务/员工/admin 对付款批次公网只读访问均为 409 `payment_batch_v1_disabled / flag_force_off`;未提供启用窗口、财务/复核参与人、flag 启停人、回滚联系人、暂停/继续决策人和测试报销单来源,因此不启动 P2-FIN-LIVE | | P2-FIN-LIVE-DECISION | 已完成,未部署 | `check-live-smoke-env --strict` 通过,`check-payment-batch-flag-off --json` 通过,财务/员工/admin 对付款批次公网只读访问均为 409 `payment_batch_v1_disabled / flag_force_off`;初始 `HOLD_FOR_WINDOW`,后续用户批准立即短窗口并由 P2-FIN-LIVE 承接 |
| P2-FIN-LIVE 公网短窗口 | 通过,未部署,最终关闭 | `run-p2-fin-live-window.sh` PASS:live-env strict PASS,pre flag-off PASS,公网付款批次 live spec 1 passed,移动矩阵 6 passed,企业微信渲染矩阵 8 passed,final flag-off check PASS;测试中只临时开启 `payment_batch_v1`,未开启 `erp_sync`,未接真实支付,最终恢复 409 `payment_batch_v1_disabled / flag_force_off` |
## R7EP / OPS8 / QAF8 / UX8 / P2T 预检推进 ## R7EP / OPS8 / QAF8 / UX8 / P2T 预检推进
+5 -4
查看文件
@@ -45,7 +45,8 @@
| `P2Q` | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | `EVIDENCE` | P2OCRS 后续任务队列已锁定;图片 OCR 默认冻结,后续实现均需另建独立合同,当前不部署、不改业务代码 | | `P2Q` | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | `EVIDENCE` | P2OCRS 后续任务队列已锁定;图片 OCR 默认冻结,后续实现均需另建独立合同,当前不部署、不改业务代码 |
| `P2AUTO` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv` | `EVIDENCE` | 自动化优先回归已收口;后端/前端/隔离业务流/公网 readiness/PDF live OCR/移动与企业微信自动化均有证据,provider-only 默认决策为 `FREEZE_BY_DEFAULT` | | `P2AUTO` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv` | `EVIDENCE` | 自动化优先回归已收口;后端/前端/隔离业务流/公网 readiness/PDF live OCR/移动与企业微信自动化均有证据,provider-only 默认决策为 `FREEZE_BY_DEFAULT` |
| `P2AF` | `issues/2026-05-16_22-11-10-p2-automatable-followup.csv` | `EVIDENCE` | 可自动实施收口已完成;纠正图片 OCR 默认冻结口径,补 QA-only 安全回归、付款批次公网关闭态只读门禁和 UX8 自动矩阵扫尾;报告 `artifacts/qa-flow/p2-safe-regression-20260516-222222/` | | `P2AF` | `issues/2026-05-16_22-11-10-p2-automatable-followup.csv` | `EVIDENCE` | 可自动实施收口已完成;纠正图片 OCR 默认冻结口径,补 QA-only 安全回归、付款批次公网关闭态只读门禁和 UX8 自动矩阵扫尾;报告 `artifacts/qa-flow/p2-safe-regression-20260516-222222/` |
| `P2-FIN-LIVE-DECISION` | `issues/2026-05-16_22-49-05-p2-fin-payment-batch-live-decision.csv` | `EVIDENCE` | 公网付款批次启用窗口决策已收口;readiness 和 flag-off 只读门禁通过,但缺启用窗口/参与人/回滚联系人/测试数据前置,结论 `HOLD_FOR_WINDOW` | | `P2-FIN-LIVE-DECISION` | `issues/2026-05-16_22-49-05-p2-fin-payment-batch-live-decision.csv` | `EVIDENCE` | 公网付款批次启用窗口决策已收口;readiness 和 flag-off 只读门禁通过,初始结论 `HOLD_FOR_WINDOW`,随后用户批准进入 `P2-FIN-LIVE` |
| `P2-FIN-LIVE` | `issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv` | `EVIDENCE` | 公网付款批次短窗口受控测试已收口;测试报销、付款批次、AP 导出和审计核验通过,最终 `payment_batch_v1=flag_force_off`,未部署 |
旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。 旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。
@@ -53,8 +54,8 @@
| Doc_ID | Path | Category | Authority | Status | Owner | Related_Issues | Evidence | Supersedes | Last_Audited | Audit_Method | Risk | Notes | | Doc_ID | Path | Category | Authority | Status | Owner | Related_Issues | Evidence | Supersedes | Last_Audited | Audit_Method | Risk | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION` | `rg` | none | 2026-05-16 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 | | `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE` | `rg` | none | 2026-05-16 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 |
| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION` | `audit-docs` | none | 2026-05-16 | `audit-docs --check-registry` | medium | 判断文档权威性 | | `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE` | `audit-docs` | none | 2026-05-16 | `audit-docs --check-registry` | medium | 判断文档权威性 |
| `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8` | Issue CSV | `docs/requirements-specification.md` | 2026-05-13 | `rg` | high | 定义做什么、不做什么和验收口径 | | `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8` | Issue CSV | `docs/requirements-specification.md` | 2026-05-13 | `rg` | high | 定义做什么、不做什么和验收口径 |
| `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T` | `rg` | none | 2026-05-13 | `rg` | high | 记录已拍板产品边界 | | `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T` | `rg` | none | 2026-05-13 | `rg` | high | 记录已拍板产品边界 |
| `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9` | Playwright/视觉审查/DG8/DG9 | none | 2026-05-13 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型 | | `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9` | Playwright/视觉审查/DG8/DG9 | none | 2026-05-13 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型 |
@@ -159,4 +160,4 @@
- `ME6-3` 代表性企业微信真机门禁已关闭。 - `ME6-3` 代表性企业微信真机门禁已关闭。
- MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。 - MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。
- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口和 QAOPS9 上线硬化连续合同均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同;P2 功能开发必须另建或延续对应连续合同。 - R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口和 QAOPS9 上线硬化连续合同均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同;P2-FIN-LIVE 已完成公网付款批次短窗口受控测试并回到 `payment_batch_v1=flag_force_off`;P2 功能开发必须另建或延续对应连续合同。
+5 -3
查看文件
@@ -20,7 +20,7 @@
## 最近事实源链 ## 最近事实源链
R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION。 R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE。
- R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。 - R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。
- ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。 - ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。
@@ -39,7 +39,8 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
- P2Q 是 P2OCRS 后续任务队列合同:只锁定 `UX8`、`P2-FIN-LIVE`、审批通知、轻业务增强和持续上线门禁等启动条件;图片 OCR 不进入默认主线,只有用户明确重启评估才另建 provider 回归或实现合同。 - P2Q 是 P2OCRS 后续任务队列合同:只锁定 `UX8`、`P2-FIN-LIVE`、审批通知、轻业务增强和持续上线门禁等启动条件;图片 OCR 不进入默认主线,只有用户明确重启评估才另建 provider 回归或实现合同。
- P2AUTO 是 P2Q 后自动化优先回归合同:后端 focused、前端构建、高风险确认、隔离核心业务流、隔离付款批次闭环、公网 readiness/PDF live OCR、公网移动矩阵和企业微信 UA/渲染自动化均已完成;PDF 样票和 live smoke 凭据可用,图片 OCR 默认冻结。 - P2AUTO 是 P2Q 后自动化优先回归合同:后端 focused、前端构建、高风险确认、隔离核心业务流、隔离付款批次闭环、公网 readiness/PDF live OCR、公网移动矩阵和企业微信 UA/渲染自动化均已完成;PDF 样票和 live smoke 凭据可用,图片 OCR 默认冻结。
- P2AF 是 P2AUTO 后可自动实施收口合同:纠正图片 OCR 默认冻结口径,补 QA-only provider 决策语义、安全回归聚合、付款批次公网关闭态只读门禁和 UX8 自动矩阵扫尾;不部署、不启用公网付款批次、不开放图片 OCR。 - P2AF 是 P2AUTO 后可自动实施收口合同:纠正图片 OCR 默认冻结口径,补 QA-only provider 决策语义、安全回归聚合、付款批次公网关闭态只读门禁和 UX8 自动矩阵扫尾;不部署、不启用公网付款批次、不开放图片 OCR。
- P2-FIN-LIVE-DECISION 是 P2AF 后付款批次公网启用窗口决策合同:公网 readiness 和 flag-off 只读门禁通过,但缺明确启用窗口、财务/复核参与人、flag 启停人、回滚联系人、暂停/继续决策人和测试报销单来源,结论为 `HOLD_FOR_WINDOW`;公网 `payment_batch_v1` 继续关闭。 - P2-FIN-LIVE-DECISION 是 P2AF 后付款批次公网启用窗口决策合同:公网 readiness 和 flag-off 只读门禁通过,初始结论为 `HOLD_FOR_WINDOW`;随后用户批准立即短窗口、现有 live smoke 测试账号和新建测试报销,已进入并完成 `P2-FIN-LIVE`。
- P2-FIN-LIVE 已按用户批准完成公网付款批次短窗口受控测试:使用 live smoke 测试账号新建专用测试报销,临时打开 `payment_batch_v1`,完成创建/复核/执行、AP 导出和审计核验后强制回到 `flag_force_off`;不部署、不接真实支付、不写 ERP 实时凭证,不开放图片 OCR。
## 当前活跃治理流 ## 当前活跃治理流
@@ -65,7 +66,8 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
| `P2Q` | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | 已收口:P2OCRS 后续队列和启动门槛完成;图片 OCR 默认冻结 | UX8、付款批次公网启用、审批通知、轻业务增强和持续上线门禁的后续合同顺序;图片 OCR 仅在用户明确重启评估时另建合同 | 不直接启动 HOLD 项、不新建综合规划文档、不改业务代码、不部署 | | `P2Q` | `issues/2026-05-16_10-35-22-p2-post-p2ocrs-followup-queue.csv` | 已收口:P2OCRS 后续队列和启动门槛完成;图片 OCR 默认冻结 | UX8、付款批次公网启用、审批通知、轻业务增强和持续上线门禁的后续合同顺序;图片 OCR 仅在用户明确重启评估时另建合同 | 不直接启动 HOLD 项、不新建综合规划文档、不改业务代码、不部署 |
| `P2AUTO` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv` | 已收口:自动化优先回归和样票 provider 门禁完成;P2AF 后 provider 默认结论为 `FREEZE_BY_DEFAULT` | 凭据/样票掩码核对、后端 focused、前端构建、高风险确认、隔离业务流、隔离付款批次、公网 readiness/PDF live OCR、移动/企业微信自动化、OCR provider-only 脚本 | 不部署、不开放图片 OCR、不打开公网 `payment_batch_v1`、不自动选择未登记图片样票 | | `P2AUTO` | `issues/2026-05-16_20-09-49-p2-automated-regression-sweep.csv` | 已收口:自动化优先回归和样票 provider 门禁完成;P2AF 后 provider 默认结论为 `FREEZE_BY_DEFAULT` | 凭据/样票掩码核对、后端 focused、前端构建、高风险确认、隔离业务流、隔离付款批次、公网 readiness/PDF live OCR、移动/企业微信自动化、OCR provider-only 脚本 | 不部署、不开放图片 OCR、不打开公网 `payment_batch_v1`、不自动选择未登记图片样票 |
| `P2AF` | `issues/2026-05-16_22-11-10-p2-automatable-followup.csv` | 已收口:可自动实施收口完成 | 图片 OCR 默认冻结纠偏、QA 脚本语义、安全回归聚合、付款批次公网关闭态只读门禁、UX8 自动矩阵扫尾;报告 `artifacts/qa-flow/p2-safe-regression-20260516-222222/` | 不部署、不开放图片 OCR、不启用公网 `payment_batch_v1`、不做业务实现 | | `P2AF` | `issues/2026-05-16_22-11-10-p2-automatable-followup.csv` | 已收口:可自动实施收口完成 | 图片 OCR 默认冻结纠偏、QA 脚本语义、安全回归聚合、付款批次公网关闭态只读门禁、UX8 自动矩阵扫尾;报告 `artifacts/qa-flow/p2-safe-regression-20260516-222222/` | 不部署、不开放图片 OCR、不启用公网 `payment_batch_v1`、不做业务实现 |
| `P2-FIN-LIVE-DECISION` | `issues/2026-05-16_22-49-05-p2-fin-payment-batch-live-decision.csv` | 已收口:公网启用窗口决策完成,结论 `HOLD_FOR_WINDOW` | 公网 readiness、付款批次 flag-off 只读门禁、窗口/参与人/回滚联系人/测试数据前置、后续 P2-FIN-LIVE 启动条件 | 不打开公网 `payment_batch_v1`、不写付款批次、不部署、不接真实支付、不写 ERP 实时凭证 | | `P2-FIN-LIVE-DECISION` | `issues/2026-05-16_22-49-05-p2-fin-payment-batch-live-decision.csv` | 已收口:公网启用窗口决策完成,初始结论 `HOLD_FOR_WINDOW`,后续用户批准并进入 `P2-FIN-LIVE` | 公网 readiness、付款批次 flag-off 只读门禁、窗口/参与人/回滚联系人/测试数据前置、后续 P2-FIN-LIVE 启动条件 | 不在决策合同中打开公网 `payment_batch_v1`、不写付款批次、不部署、不接真实支付、不写 ERP 实时凭证 |
| `P2-FIN-LIVE` | `issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv` | 已收口:公网短窗口付款批次受控测试通过,最终 `payment_batch_v1=flag_force_off` | 公网测试报销、PDF 样票绑定、审批到 approved、付款批次创建/复核/执行、AP 导出审计、权限边界、移动/企业微信自动化 | 不保持公网开启、不部署、不接真实支付、不写 ERP 实时凭证、不开放图片 OCR |
旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。 旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。
@@ -0,0 +1,11 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"P2FLIVE-0","P2-FIN-LIVE 合同建档","建立公网付款批次受控短窗口测试合同;用户已批准立即短窗口、现有 live smoke 测试账号和新建专用测试报销;默认测试结束后关回 payment_batch_v1。","1) CSV 校验通过;2) 不部署、不改业务 API/schema/UI;3) 不接真实支付、不写 ERP 实时凭证、不开放图片 OCR;4) 公网写入仅限测试报销、付款批次、审计和 AP 导出审计;5) 最终 payment_batch_v1 回到 force_off。","受影响流程:P2-FIN-V1 启用前硬化 -> 公网短窗口付款批次测试 -> 强制 flag 回滚;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv;人工核对本合同范围和用户批准。","python3|manual","DONE","DONE","DONE","issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv","[]","DONE: user approved immediate public short window; executed report=artifacts/qa-flow/p2-fin-live-20260517-001022; no deploy/API/schema/UI change; no real payment/ERP/image OCR; final payment_batch_v1=force_off."
"P2FLIVE-1","公网前置与关闭态复核","执行公网 readiness、PDF live OCR readiness 和 payment_batch_v1 关闭态只读复核,确认进入短窗口前系统可用且付款批次仍未打开。","1) check-live-smoke-env --strict 通过;2) check-payment-batch-flag-off --json 通过;3) 工作区无非本轮未解释改动;4) 不打印 secret、cookie、样票路径或远端 env;5) 不执行付款批次写入。","受影响流程:公网 /ready、live smoke、PDF OCR readiness、付款批次关闭态;验证步骤:set -a && source ~/.hyb-oa/live-smoke.env && set +a;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-smoke-env.mjs --strict;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-payment-batch-flag-off.mjs --json;git status --short。","bash|node|git","DONE","DONE","DONE","scripts/qa/check-live-smoke-env.mjs|scripts/qa/check-payment-batch-flag-off.mjs|external","[""P2FLIVE-0""]","DONE: live-env-strict PASS and pre-flag-off-check PASS before flag-on; workspace dirty only with this P2FLIVE contract/QA files; no secrets or sample paths printed."
"P2FLIVE-2","公网安全 runner 和 live spec","新增 QA-only runner 与 Playwright live spec,只有 P2_FIN_LIVE_APPROVED=1 时才允许公网写入,并在 finally 中执行 flag 回滚。","1) scripts/qa/run-p2-fin-live-window.sh 存在且 bash -n 通过;2) client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts 使用 helpers/liveAuth.ts 的 live 账号;3) 缺 P2_FIN_LIVE_APPROVED=1 直接拒绝;4) 证据落 artifacts/qa-flow/p2-fin-live-*;5) 不打印 secret、cookie、PDF 路径或导出内容。","受影响流程:公网短窗口自动化编排;验证步骤:bash -n scripts/qa/run-p2-fin-live-window.sh;cd client && npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --list;人工核对脚本强制审批变量和 finally 回滚。","bash|playwright|manual","DONE","DONE","DONE","scripts/qa/run-p2-fin-live-window.sh|client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts","[""P2FLIVE-1""]","DONE: added QA-only runner/spec with P2_FIN_LIVE_APPROVED=1 guard and final rollback; bash -n/list/typecheck/build passed; evidence report=artifacts/qa-flow/p2-fin-live-20260517-001022."
"P2FLIVE-3","flag-off 下创建测试报销并审批","在打开付款批次前,用员工账号新建专用测试报销、上传并绑定授权 PDF 样票、提交审批,并由 manager/finance 链路审批到 approved;样票不可绑定则停止。","1) 生成专用测试报销单号或 ID;2) 报销金额为小额测试值;3) 发票为 PDF-only 授权样票且成功绑定;4) 报销状态进入 approved;5) payment_batch_v1 仍处于关闭态。","受影响流程:员工新建报销 -> PDF 发票绑定 -> 提交 -> manager/finance 审批到 approved;验证步骤:P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-p2-fin-live-window.sh;检查 summary.json 中 reimbursement.status=approved before flag-on。","playwright|node","DONE","DONE","DONE","client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|artifacts/qa-flow","[""P2FLIVE-2""]","DONE: created dedicated small test reimbursement BX-20260517-0022, bound authorized PDF sample, and approved to approved before flag-on; payment_batch_v1 was force_off before opening."
"P2FLIVE-4","公网短窗口 flag-on","由 admin 读取 payment_batch_v1 当前快照并临时设置 force_on;仅打开 payment_batch_v1,不打开 erp_sync。","1) 原始 feature flag 快照记录到证据摘要但不含 token;2) payment_batch_v1 设置为 force_on 且 effectiveEnabled=true;3) erp_sync 不被本轮改为 force_on;4) 操作写入 FEATURE_FLAG_UPDATED 审计;5) 失败时立即执行回滚。","受影响流程:系统功能开关 -> 公网付款批次入口短窗口开启;验证步骤:live spec 通过 /api/system/feature-flags 读取 version 并 PUT /api/system/feature-flags/payment_batch_v1;随后 GET /api/payment-batches 返回 200。","playwright|node","DONE","DONE","DONE","client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|server/src/routes/featureFlags.js","[""P2FLIVE-3""]","DONE: payment_batch_v1 moved force_off -> force_on for short window only; erp_sync remained disabled; FEATURE_FLAG_UPDATED audit verified; final rollback force_off."
"P2FLIVE-5","付款批次公网闭环","在短窗口内由 finance 创建付款批次,finance_mgr 复核,finance 执行,并验证报销 paid、票据 archived、AP 台账已付款。","1) 付款批次创建成功;2) finance_mgr 复核成功;3) finance 执行成功;4) 报销状态为 paid;5) 关联发票为 archived 且 AP 台账 payment.status=paid。","受影响流程:approved 报销 -> 付款批次创建 -> 复核 -> 执行 -> 报销 paid -> 发票 archived -> AP 台账已付款;验证步骤:P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-p2-fin-live-window.sh;检查 summary.json 中 batch/reimbursement/invoice/ledger 终态。","playwright|node","DONE","DONE","DONE","client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|artifacts/qa-flow","[""P2FLIVE-4""]","DONE: finance created batch c7eefe90-7164-4232-a307-bfbf3b823065; finance_mgr reviewed; finance executed; reimbursement paid, invoice archived, AP ledger paid."
"P2FLIVE-6","权限审计导出核验","核验员工、manager、admin-only 不可越权,审计含 FEATURE_FLAG_UPDATED、PAYMENT_BATCH_*、REIMBURSEMENT_PAYMENT_MARKED 和 EXPORT_AP_OPERATIONS;AP 导出只记录状态和审计,不打印内容。","1) employee/manager 创建或执行付款被拒绝;2) admin-only 账号若可用则创建或执行付款被拒绝,若 LIVE_SMOKE_ADMIN 为 wildcard superadmin 则记录 SKIP_WILDCARD_SUPERADMIN;3) PAYMENT_BATCH_CREATED/REVIEWED/EXECUTED 可查;4) REIMBURSEMENT_PAYMENT_MARKED 可查;5) EXPORT_AP_OPERATIONS 可查;6) 导出内容不输出到日志或报告。","受影响流程:付款批次权限边界、AP 导出权限、审计追溯;验证步骤:live spec 用 API 探测 employee/manager 越权 POST;若 live admin 是 wildcard superadmin 则跳过 admin-only 写负例并记录 SKIP_WILDCARD_SUPERADMIN;查询 /api/audit-logs;finance POST /api/exports/ap-operations 只检查 200/审计,不输出响应正文。","playwright|node","DONE","DONE","DONE","client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|server/src/routes/paymentBatches.js|server/src/routes/exports.js","[""P2FLIVE-5""]","DONE: employee/manager create boundaries returned 403; LIVE_SMOKE_ADMIN is wildcard superadmin so admin-only negative write boundary recorded SKIP_WILDCARD_SUPERADMIN; PAYMENT_BATCH_*, REIMBURSEMENT_PAYMENT_MARKED and EXPORT_AP_OPERATIONS audits verified; export content not printed."
"P2FLIVE-7","强制回滚与关闭态复核","无论 P2FLIVE-3..6 成功或失败,恢复 payment_batch_v1 到原始关闭态,并复跑公网 flag-off 只读门禁。","1) live spec finally 执行回滚;2) runner 结束后 check-payment-batch-flag-off --json 通过;3) 财务/员工/admin 对 payment-batches/AP 返回 409 flag_force_off 或权限拒绝;4) 回滚失败时 runner 返回非 0 并突出风险;5) 不执行生产恢复或数据库重置。","受影响流程:公网付款批次 feature flag 回滚;验证步骤:P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-p2-fin-live-window.sh;随后 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-payment-batch-flag-off.mjs --json。","bash|node|playwright","DONE","DONE","DONE","scripts/qa/run-p2-fin-live-window.sh|scripts/qa/check-payment-batch-flag-off.mjs|client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts","[""P2FLIVE-6""]","DONE: spec finally and runner final hook restored payment_batch_v1 to force_off; final check returned 409 payment_batch_v1_disabled / flag_force_off for finance/employee/admin."
"P2FLIVE-8","移动和企业微信自动化复核","付款闭环后跑公网移动矩阵和 WeCom UA 自动化;真机测试机器只确认入口、免登和状态查看,不执行付款动作。","1) 公网移动矩阵通过或有可解释失败;2) WeCom UA 自动化通过或有可解释失败;3) 真机如执行只记录查看确认;4) 不新增移动付款能力;5) 不因真机未执行付款阻塞短窗口自动化结论。","受影响流程:移动首页、移动财务入口、企业微信 UA 渲染和 deeplink 查看;验证步骤:cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。","playwright|manual","DONE","DONE","DONE","client/tests/e2e/dg7-mobile-information-dedupe.spec.ts|client/tests/e2e/r124-mobile-entry-matrix.spec.ts|client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts|external","[""P2FLIVE-7""]","DONE: public mobile matrix 6 passed and WeCom UA rendering matrix 8 passed in report artifacts/qa-flow/p2-fin-live-20260517-001022; no mobile payment action added."
"P2FLIVE-9","收口回填与提交","回填 P2-FIN-LIVE CSV、文档索引、事实源矩阵和人工试运行报告,执行最终门禁并提交中文 commit。","1) P2FLIVE 各行状态按真实证据回填;2) docs/文档索引.md、docs/文档事实源状态矩阵.md、docs/trialrun/人工试运行报告.md 必要状态更新;3) validate、audit-docs、rg、git diff --check 通过;4) 中文 commit 引用 P2FLIVE;5) 明确未部署和公网最终 flag 状态。","流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv;node scripts/qa/audit-docs.mjs --check-registry;rg -n 'P2FLIVE|P2-FIN-LIVE|payment_batch_v1|PAYMENT_BATCH|EXPORT_AP_OPERATIONS|flag_force_off' docs issues scripts client/tests;git diff --check;git status --short。","python3|node|rg|git","DONE","DONE","DONE","issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv|docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md","[""P2FLIVE-7"",""P2FLIVE-8""]","DONE: CSV/docs updated; validate_issues_csv passed; audit-docs errors=0 warnings=1 existing unregistered docs files=24; keyword sweep explainable; git diff --check passed; final public flag-off check PASS."
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 P2FLIVE-0 P2-FIN-LIVE 合同建档 建立公网付款批次受控短窗口测试合同;用户已批准立即短窗口、现有 live smoke 测试账号和新建专用测试报销;默认测试结束后关回 payment_batch_v1。 1) CSV 校验通过;2) 不部署、不改业务 API/schema/UI;3) 不接真实支付、不写 ERP 实时凭证、不开放图片 OCR;4) 公网写入仅限测试报销、付款批次、审计和 AP 导出审计;5) 最终 payment_batch_v1 回到 force_off。 受影响流程:P2-FIN-V1 启用前硬化 -> 公网短窗口付款批次测试 -> 强制 flag 回滚;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv;人工核对本合同范围和用户批准。 python3|manual DONE DONE DONE issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv [] DONE: user approved immediate public short window; executed report=artifacts/qa-flow/p2-fin-live-20260517-001022; no deploy/API/schema/UI change; no real payment/ERP/image OCR; final payment_batch_v1=force_off.
3 P2FLIVE-1 公网前置与关闭态复核 执行公网 readiness、PDF live OCR readiness 和 payment_batch_v1 关闭态只读复核,确认进入短窗口前系统可用且付款批次仍未打开。 1) check-live-smoke-env --strict 通过;2) check-payment-batch-flag-off --json 通过;3) 工作区无非本轮未解释改动;4) 不打印 secret、cookie、样票路径或远端 env;5) 不执行付款批次写入。 受影响流程:公网 /ready、live smoke、PDF OCR readiness、付款批次关闭态;验证步骤:set -a && source ~/.hyb-oa/live-smoke.env && set +a;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-smoke-env.mjs --strict;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-payment-batch-flag-off.mjs --json;git status --short。 bash|node|git DONE DONE DONE scripts/qa/check-live-smoke-env.mjs|scripts/qa/check-payment-batch-flag-off.mjs|external ["P2FLIVE-0"] DONE: live-env-strict PASS and pre-flag-off-check PASS before flag-on; workspace dirty only with this P2FLIVE contract/QA files; no secrets or sample paths printed.
4 P2FLIVE-2 公网安全 runner 和 live spec 新增 QA-only runner 与 Playwright live spec,只有 P2_FIN_LIVE_APPROVED=1 时才允许公网写入,并在 finally 中执行 flag 回滚。 1) scripts/qa/run-p2-fin-live-window.sh 存在且 bash -n 通过;2) client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts 使用 helpers/liveAuth.ts 的 live 账号;3) 缺 P2_FIN_LIVE_APPROVED=1 直接拒绝;4) 证据落 artifacts/qa-flow/p2-fin-live-*;5) 不打印 secret、cookie、PDF 路径或导出内容。 受影响流程:公网短窗口自动化编排;验证步骤:bash -n scripts/qa/run-p2-fin-live-window.sh;cd client && npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --list;人工核对脚本强制审批变量和 finally 回滚。 bash|playwright|manual DONE DONE DONE scripts/qa/run-p2-fin-live-window.sh|client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts ["P2FLIVE-1"] DONE: added QA-only runner/spec with P2_FIN_LIVE_APPROVED=1 guard and final rollback; bash -n/list/typecheck/build passed; evidence report=artifacts/qa-flow/p2-fin-live-20260517-001022.
5 P2FLIVE-3 flag-off 下创建测试报销并审批 在打开付款批次前,用员工账号新建专用测试报销、上传并绑定授权 PDF 样票、提交审批,并由 manager/finance 链路审批到 approved;样票不可绑定则停止。 1) 生成专用测试报销单号或 ID;2) 报销金额为小额测试值;3) 发票为 PDF-only 授权样票且成功绑定;4) 报销状态进入 approved;5) payment_batch_v1 仍处于关闭态。 受影响流程:员工新建报销 -> PDF 发票绑定 -> 提交 -> manager/finance 审批到 approved;验证步骤:P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-p2-fin-live-window.sh;检查 summary.json 中 reimbursement.status=approved before flag-on。 playwright|node DONE DONE DONE client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|artifacts/qa-flow ["P2FLIVE-2"] DONE: created dedicated small test reimbursement BX-20260517-0022, bound authorized PDF sample, and approved to approved before flag-on; payment_batch_v1 was force_off before opening.
6 P2FLIVE-4 公网短窗口 flag-on 由 admin 读取 payment_batch_v1 当前快照并临时设置 force_on;仅打开 payment_batch_v1,不打开 erp_sync。 1) 原始 feature flag 快照记录到证据摘要但不含 token;2) payment_batch_v1 设置为 force_on 且 effectiveEnabled=true;3) erp_sync 不被本轮改为 force_on;4) 操作写入 FEATURE_FLAG_UPDATED 审计;5) 失败时立即执行回滚。 受影响流程:系统功能开关 -> 公网付款批次入口短窗口开启;验证步骤:live spec 通过 /api/system/feature-flags 读取 version 并 PUT /api/system/feature-flags/payment_batch_v1;随后 GET /api/payment-batches 返回 200。 playwright|node DONE DONE DONE client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|server/src/routes/featureFlags.js ["P2FLIVE-3"] DONE: payment_batch_v1 moved force_off -> force_on for short window only; erp_sync remained disabled; FEATURE_FLAG_UPDATED audit verified; final rollback force_off.
7 P2FLIVE-5 付款批次公网闭环 在短窗口内由 finance 创建付款批次,finance_mgr 复核,finance 执行,并验证报销 paid、票据 archived、AP 台账已付款。 1) 付款批次创建成功;2) finance_mgr 复核成功;3) finance 执行成功;4) 报销状态为 paid;5) 关联发票为 archived 且 AP 台账 payment.status=paid。 受影响流程:approved 报销 -> 付款批次创建 -> 复核 -> 执行 -> 报销 paid -> 发票 archived -> AP 台账已付款;验证步骤:P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-p2-fin-live-window.sh;检查 summary.json 中 batch/reimbursement/invoice/ledger 终态。 playwright|node DONE DONE DONE client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|artifacts/qa-flow ["P2FLIVE-4"] DONE: finance created batch c7eefe90-7164-4232-a307-bfbf3b823065; finance_mgr reviewed; finance executed; reimbursement paid, invoice archived, AP ledger paid.
8 P2FLIVE-6 权限审计导出核验 核验员工、manager、admin-only 不可越权,审计含 FEATURE_FLAG_UPDATED、PAYMENT_BATCH_*、REIMBURSEMENT_PAYMENT_MARKED 和 EXPORT_AP_OPERATIONS;AP 导出只记录状态和审计,不打印内容。 1) employee/manager 创建或执行付款被拒绝;2) admin-only 账号若可用则创建或执行付款被拒绝,若 LIVE_SMOKE_ADMIN 为 wildcard superadmin 则记录 SKIP_WILDCARD_SUPERADMIN;3) PAYMENT_BATCH_CREATED/REVIEWED/EXECUTED 可查;4) REIMBURSEMENT_PAYMENT_MARKED 可查;5) EXPORT_AP_OPERATIONS 可查;6) 导出内容不输出到日志或报告。 受影响流程:付款批次权限边界、AP 导出权限、审计追溯;验证步骤:live spec 用 API 探测 employee/manager 越权 POST;若 live admin 是 wildcard superadmin 则跳过 admin-only 写负例并记录 SKIP_WILDCARD_SUPERADMIN;查询 /api/audit-logs;finance POST /api/exports/ap-operations 只检查 200/审计,不输出响应正文。 playwright|node DONE DONE DONE client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts|server/src/routes/paymentBatches.js|server/src/routes/exports.js ["P2FLIVE-5"] DONE: employee/manager create boundaries returned 403; LIVE_SMOKE_ADMIN is wildcard superadmin so admin-only negative write boundary recorded SKIP_WILDCARD_SUPERADMIN; PAYMENT_BATCH_*, REIMBURSEMENT_PAYMENT_MARKED and EXPORT_AP_OPERATIONS audits verified; export content not printed.
9 P2FLIVE-7 强制回滚与关闭态复核 无论 P2FLIVE-3..6 成功或失败,恢复 payment_batch_v1 到原始关闭态,并复跑公网 flag-off 只读门禁。 1) live spec finally 执行回滚;2) runner 结束后 check-payment-batch-flag-off --json 通过;3) 财务/员工/admin 对 payment-batches/AP 返回 409 flag_force_off 或权限拒绝;4) 回滚失败时 runner 返回非 0 并突出风险;5) 不执行生产恢复或数据库重置。 受影响流程:公网付款批次 feature flag 回滚;验证步骤:P2_FIN_LIVE_APPROVED=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-p2-fin-live-window.sh;随后 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-payment-batch-flag-off.mjs --json。 bash|node|playwright DONE DONE DONE scripts/qa/run-p2-fin-live-window.sh|scripts/qa/check-payment-batch-flag-off.mjs|client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts ["P2FLIVE-6"] DONE: spec finally and runner final hook restored payment_batch_v1 to force_off; final check returned 409 payment_batch_v1_disabled / flag_force_off for finance/employee/admin.
10 P2FLIVE-8 移动和企业微信自动化复核 付款闭环后跑公网移动矩阵和 WeCom UA 自动化;真机测试机器只确认入口、免登和状态查看,不执行付款动作。 1) 公网移动矩阵通过或有可解释失败;2) WeCom UA 自动化通过或有可解释失败;3) 真机如执行只记录查看确认;4) 不新增移动付款能力;5) 不因真机未执行付款阻塞短窗口自动化结论。 受影响流程:移动首页、移动财务入口、企业微信 UA 渲染和 deeplink 查看;验证步骤:cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。 playwright|manual DONE DONE DONE client/tests/e2e/dg7-mobile-information-dedupe.spec.ts|client/tests/e2e/r124-mobile-entry-matrix.spec.ts|client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts|external ["P2FLIVE-7"] DONE: public mobile matrix 6 passed and WeCom UA rendering matrix 8 passed in report artifacts/qa-flow/p2-fin-live-20260517-001022; no mobile payment action added.
11 P2FLIVE-9 收口回填与提交 回填 P2-FIN-LIVE CSV、文档索引、事实源矩阵和人工试运行报告,执行最终门禁并提交中文 commit。 1) P2FLIVE 各行状态按真实证据回填;2) docs/文档索引.md、docs/文档事实源状态矩阵.md、docs/trialrun/人工试运行报告.md 必要状态更新;3) validate、audit-docs、rg、git diff --check 通过;4) 中文 commit 引用 P2FLIVE;5) 明确未部署和公网最终 flag 状态。 流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv;node scripts/qa/audit-docs.mjs --check-registry;rg -n 'P2FLIVE|P2-FIN-LIVE|payment_batch_v1|PAYMENT_BATCH|EXPORT_AP_OPERATIONS|flag_force_off' docs issues scripts client/tests;git diff --check;git status --short。 python3|node|rg|git DONE DONE DONE issues/2026-05-16_23-42-08-p2-fin-payment-batch-live-enable.csv|docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md ["P2FLIVE-7","P2FLIVE-8"] DONE: CSV/docs updated; validate_issues_csv passed; audit-docs errors=0 warnings=1 existing unregistered docs files=24; keyword sweep explainable; git diff --check passed; final public flag-off check PASS.
+197
查看文件
@@ -0,0 +1,197 @@
#!/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="${P2_FIN_LIVE_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/p2-fin-live-$(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="${P2_FIN_LIVE_ENV_FILE:-$HOME/.hyb-oa/live-smoke.env}"
if [[ "${P2_FIN_LIVE_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 "[p2-fin-live] loaded masked env file" | tee -a "$SUMMARY_TXT"
}
run_step() {
local name="$1"
shift
local log_file="$REPORT_DIR/${name}.log"
echo ">>> [p2-fin-live] $name" | tee -a "$SUMMARY_TXT"
if "$@" > >(tee "$log_file") 2>&1; then
echo "[p2-fin-live][PASS] $name" | tee -a "$SUMMARY_TXT"
else
local code="$?"
echo "[p2-fin-live][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"
}
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();
if (!identifier || !password) {
console.error("[p2-fin-live][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": "p2-fin-live-runner-rollback",
},
body: JSON.stringify({ identifier, password }),
});
if (!login.response.ok || !login.json?.data?.accessToken) {
console.error(`[p2-fin-live][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(`[p2-fin-live][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("[p2-fin-live][rollback] missing payment_batch_v1");
process.exit(1);
}
if (flag.overrideMode === "force_off" && flag.effectiveEnabled === false) {
console.log("[p2-fin-live][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: "P2FLIVE runner final rollback",
expectedVersion: flag.version,
}),
});
if (!updated.response.ok) {
console.error(`[p2-fin-live][rollback] update failed: ${errorSummary(updated.json, updated.response.status)}`);
process.exit(1);
}
const item = updated.json?.data?.item;
console.log(`[p2-fin-live][rollback] payment_batch_v1 override=${item?.overrideMode ?? "unknown"} effective=${item?.effectiveEnabled ?? "unknown"}`);
NODE
}
write_summary_json() {
local qa_status="PASS"
if [[ "$STATUS" -ne 0 ]]; then
qa_status="FAIL"
fi
QA_REPORT_DIR="$REPORT_DIR" \
QA_ISSUE_ID="${QA_ISSUE_ID:-P2FLIVE}" \
QA_BASE_URL="$PUBLIC_BASE_URL" \
QA_MODE="p2-fin-live-window" \
QA_STATUS="$qa_status" \
QA_SUMMARY_NOTES="P2-FIN-LIVE 公网短窗口:临时启用 payment_batch_v1,完成测试付款闭环后强制回滚到 flag_force_off;不部署、不接真实支付、不开放图片 OCR。" \
node "$ROOT_DIR/scripts/qa/write-qa-summary.mjs" >/dev/null 2>&1 || true
}
finish() {
if [[ "${P2_FIN_LIVE_APPROVED:-0}" == "1" ]]; then
echo ">>> [p2-fin-live] final-force-flag-off" | tee -a "$SUMMARY_TXT"
if force_payment_batch_flag_off > >(tee "$REPORT_DIR/final-force-flag-off.log") 2>&1; then
echo "[p2-fin-live][PASS] final-force-flag-off" | tee -a "$SUMMARY_TXT"
else
local code="$?"
echo "[p2-fin-live][FAIL] final-force-flag-off exit=$code" | tee -a "$SUMMARY_TXT"
STATUS=1
fi
echo ">>> [p2-fin-live] final-flag-off-check" | tee -a "$SUMMARY_TXT"
if LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" node "$ROOT_DIR/scripts/qa/check-payment-batch-flag-off.mjs" --json > >(tee "$REPORT_DIR/final-flag-off-check.log") 2>&1; then
echo "[p2-fin-live][PASS] final-flag-off-check" | tee -a "$SUMMARY_TXT"
else
local code="$?"
echo "[p2-fin-live][FAIL] final-flag-off-check exit=$code" | tee -a "$SUMMARY_TXT"
STATUS=1
fi
fi
write_summary_json
echo "[p2-fin-live] report=$REPORT_DIR" | tee -a "$SUMMARY_TXT"
exit "$STATUS"
}
trap finish EXIT
load_default_env
if [[ "${P2_FIN_LIVE_APPROVED:-0}" != "1" ]]; then
echo "[p2-fin-live][ERROR] P2_FIN_LIVE_APPROVED=1 is required for public write test" | 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"
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"
run_shell_step "p2-fin-live-spec" "cd '$CLIENT_DIR' && PLAYWRIGHT_MANUAL=1 E2E_MODE=live P2_FIN_LIVE_APPROVED=1 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 [[ "${P2_FIN_LIVE_SKIP_MOBILE:-0}" == "1" ]]; then
echo "[p2-fin-live][SKIP] mobile-wecom P2_FIN_LIVE_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