恢复付款入口与常开基线(08-01-reimbursement-notification-progress-clarity)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-01 23:12:23 +08:00
父节点 aa6b9bc373
当前提交 37b4ef2d10
共修改 20 个文件,包含 655 行新增和 244 行删除
+1 -1
查看文件
@@ -65,7 +65,7 @@ export function resolveMobileNotificationTargetLink(item: NotificationTarget) {
}
if (item.targetType === "payment_batch") {
return `/payment-batches?id=${encodeSegment(item.targetId)}`;
return `/mobile/payment-batches?id=${encodeSegment(item.targetId)}`;
}
if (item.targetType === "leave_request") {
+4 -1
查看文件
@@ -118,7 +118,6 @@ export function resolveMobileEquivalentPath(pathname: string, search = "") {
normalizedPath.startsWith("/projects") ||
normalizedPath.startsWith("/contracts-registry") ||
normalizedPath.startsWith("/travel-requests") ||
normalizedPath.startsWith("/payment-batches") ||
normalizedPath.startsWith("/ap-operations") ||
normalizedPath.startsWith("/weekly-reports") ||
normalizedPath.startsWith("/todos") ||
@@ -130,6 +129,10 @@ export function resolveMobileEquivalentPath(pathname: string, search = "") {
return "/mobile";
}
if (normalizedPath === "/payment-batches") {
return `/mobile/payment-batches${search}`;
}
if (normalizedPath === "/purchase-requests") {
const purchaseRequestId = searchParams.get("highlight") ?? searchParams.get("id") ?? searchParams.get("requestId");
if (purchaseRequestId) {
+10 -2
查看文件
@@ -184,7 +184,10 @@ export function MobilePortalPage() {
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
const financePendingCount = financeSummary?.pendingBatchCount ?? 0;
const financeExceptionCount = (financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0);
const shouldShowFinanceCard = canUseFinance && !portalHomeQuery.isLoading && !portalHomeQuery.isError && (financePendingCount > 0 || financeExceptionCount > 0);
const shouldShowFinanceCard = canUseFinance &&
!portalHomeQuery.isLoading &&
!portalHomeQuery.isError &&
(paymentBatchEnabled || financePendingCount > 0 || financeExceptionCount > 0);
const financeCardBadge = paymentBatchEnabled ? "复核与付款" : financePendingCount > 0 ? "待付款" : "异常与复核";
async function openHomeNotification(item: NotificationItem) {
@@ -326,9 +329,14 @@ export function MobilePortalPage() {
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">处理</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">电脑端</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">OA</div>
</div>
</div>
{paymentBatchEnabled ? (
<Button asChild className="mt-3 w-full" size="sm" type="button" variant="secondary">
<Link to="/mobile/payment-batches">进入付款</Link>
</Button>
) : null}
</section>
) : null}
+9
查看文件
@@ -352,6 +352,15 @@ const routeElements = (
<Route path="mobile/notifications" element={withRouteFallback(<MobileNotificationsPage />)} />
<Route
path="mobile/payment-batches"
element={
<RequirePermission anyOf={["payment.batch.manage", "risk.review.manage", "payment.batch.review"]}>
{withRouteFallback(<PaymentBatchesPage />)}
</RequirePermission>
}
/>
<Route
path="mobile/wecom-approval-archive"
element={
@@ -24,6 +24,14 @@ type MockNotification = {
createdAt: string;
};
type FinanceSummaryOverride = {
pendingBatchCount: number;
failedItemCount: number;
riskCount: number;
paymentBatchEnabled: boolean;
href: string;
};
function envelope(data: unknown) {
return { requestId: "mock-dg7-mobile-information-dedupe", data };
}
@@ -453,7 +461,12 @@ function reimbursementDetail() {
};
}
async function mockApis(page: Page, role: RolePreset, notifications: MockNotification[]) {
async function mockApis(
page: Page,
role: RolePreset,
notifications: MockNotification[],
financeSummaryOverride?: FinanceSummaryOverride,
) {
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
const request = route.request();
const url = new URL(request.url());
@@ -544,7 +557,7 @@ async function mockApis(page: Page, role: RolePreset, notifications: MockNotific
isAdminOnly: role === "admin",
},
financeSummary: isFinance
? {
? financeSummaryOverride ?? {
pendingBatchCount: 2,
failedItemCount: 1,
riskCount: 3,
@@ -889,7 +902,7 @@ test.describe("MIF7 移动信息架构整改", () => {
const target = { targetType: "payment_batch", targetId: "batch/dg7" };
expect(resolveNotificationTargetLink(target)).toBe("/payment-batches?id=batch%2Fdg7");
expect(resolveMobileNotificationTargetLink(target)).toBe("/payment-batches?id=batch%2Fdg7");
expect(resolveMobileNotificationTargetLink(target)).toBe("/mobile/payment-batches?id=batch%2Fdg7");
});
test.skip(process.env.E2E_MODE === "live", "MIF7 本地专项使用 mock 数据验证信息架构。");
@@ -1161,6 +1174,27 @@ test.describe("MIF7 移动信息架构整改", () => {
await financeMobile.context.close();
}
const firstUseFinanceMobile = await newMobilePage(browser, 390);
try {
await mockApis(firstUseFinanceMobile.page, "finance", buildNotifications(), {
pendingBatchCount: 0,
failedItemCount: 0,
riskCount: 0,
paymentBatchEnabled: true,
href: "/payment-batches",
});
await loginForMobile(firstUseFinanceMobile.page, "finance");
await firstUseFinanceMobile.page.goto("/mobile");
const financeSummary = firstUseFinanceMobile.page.getByTestId("mobile-finance-summary");
await expect(financeSummary).toBeVisible();
await expect(financeSummary).toContainText("待付款");
await expect(financeSummary.getByRole("link", { name: "进入付款" })).toHaveAttribute("href", "/mobile/payment-batches");
await financeSummary.getByRole("link", { name: "进入付款" }).click();
await expect(firstUseFinanceMobile.page).toHaveURL(/\/mobile\/payment-batches$/);
} finally {
await firstUseFinanceMobile.context.close();
}
const adminMobile = await newMobilePage(browser, 390);
try {
await mockApis(adminMobile.page, "admin", buildNotifications());
@@ -19,7 +19,7 @@ type FeatureFlagItem = {
type FeatureFlagSnapshot = Pick<
FeatureFlagItem,
"flagKey" | "overrideMode" | "rolloutType" | "departmentIds" | "version" | "effectiveEnabled" | "effectiveReason"
"flagKey" | "overrideMode" | "rolloutType" | "departmentIds" | "note" | "version" | "effectiveEnabled" | "effectiveReason"
>;
type ReimbursementDetail = {
@@ -99,7 +99,7 @@ const baseUrl = process.env.LIVE_SMOKE_BASE_URL?.trim() || "https://oa.hyinda.co
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");
const finalFlagState = process.env.P2_FIN_LIVE_FINAL_STATE?.trim() === "force_on" ? "force_on" : "force_off";
const finalFlagState = process.env.P2_FIN_LIVE_FINAL_STATE?.trim() === "force_on" ? "force_on" : "restore";
const alreadyOnMode =
process.env.P2_FIN_LIVE_ALREADY_ON === "1" ||
process.env.LAUNCH_AUTO_PAYMENT_BATCH_ALREADY_ON === "1";
@@ -130,6 +130,7 @@ function snapshotFlag(item: FeatureFlagItem): FeatureFlagSnapshot {
overrideMode: item.overrideMode,
rolloutType: item.rolloutType,
departmentIds: item.departmentIds,
note: item.note,
version: item.version,
effectiveEnabled: item.effectiveEnabled,
effectiveReason: item.effectiveReason,
@@ -183,13 +184,15 @@ async function updateFlag(
flagKey: string,
overrideMode: FeatureFlagItem["overrideMode"],
note: string,
rolloutType: FeatureFlagItem["rolloutType"] = "global",
departmentIds: string[] = [],
) {
const current = await loadFlag(api, flagKey);
const response = await api.put(`/api/system/feature-flags/${flagKey}`, {
data: {
overrideMode,
rolloutType: "global",
departmentIds: [],
rolloutType,
departmentIds,
note,
expectedVersion: current.version,
},
@@ -198,12 +201,24 @@ async function updateFlag(
return data.item;
}
async function forcePaymentBatchOff(api: APIRequestContext, note: string) {
async function restoreFlagSnapshot(api: APIRequestContext, snapshot: FeatureFlagSnapshot) {
const current = await loadFlag(api, "payment_batch_v1");
if (current.overrideMode === "force_off" && !current.effectiveEnabled) {
const configurationMatches =
current.overrideMode === snapshot.overrideMode &&
current.rolloutType === snapshot.rolloutType &&
JSON.stringify(current.departmentIds) === JSON.stringify(snapshot.departmentIds) &&
current.note === snapshot.note;
if (configurationMatches) {
return current;
}
return updateFlag(api, "payment_batch_v1", "force_off", note);
return updateFlag(
api,
"payment_batch_v1",
snapshot.overrideMode,
snapshot.note,
snapshot.rolloutType,
snapshot.departmentIds,
);
}
function resolveLivePdfBuffer(marker: string) {
@@ -397,6 +412,7 @@ test.describe("公网付款批次受控验收", () => {
const clients: AuthenticatedApiClient[] = [];
let admin: AuthenticatedApiClient | null = null;
let paymentFlagBefore: FeatureFlagItem | null = null;
let caughtError: unknown = null;
let rollbackError: unknown = null;
@@ -408,7 +424,7 @@ test.describe("公网付款批次受控验收", () => {
const financeMgr = await createLiveClientWithRetry("financeMgr");
clients.push(admin, employee, manager, finance, financeMgr);
const paymentFlagBefore = await loadFlag(admin.api, "payment_batch_v1");
paymentFlagBefore = await loadFlag(admin.api, "payment_batch_v1");
const erpFlagBefore = await loadFlag(admin.api, "erp_sync");
summary.stages.preflight = {
paymentFlagBefore: snapshotFlag(paymentFlagBefore),
@@ -587,19 +603,7 @@ test.describe("公网付款批次受控验收", () => {
} finally {
if (admin) {
try {
if (alreadyOnMode) {
const finalPaymentFlag = await loadFlag(admin.api, "payment_batch_v1");
const finalErpFlag = await loadFlag(admin.api, "erp_sync");
summary.stages.finalState = {
paymentFlag: snapshotFlag(finalPaymentFlag),
erpSyncEffectiveEnabled: finalErpFlag.effectiveEnabled,
alreadyOnMode,
};
expect(finalPaymentFlag.effectiveEnabled).toBe(true);
expect(finalPaymentFlag.overrideMode).toBe("force_on");
expect(finalFlagState).toBe("force_on");
expect(finalErpFlag.effectiveEnabled).toBe(false);
} else if (finalFlagState === "force_on" && !caughtError) {
if (finalFlagState === "force_on" && !caughtError) {
const paymentFlag = await loadFlag(admin.api, "payment_batch_v1");
const finalPaymentFlag =
paymentFlag.overrideMode === "force_on" && paymentFlag.effectiveEnabled
@@ -609,14 +613,18 @@ test.describe("公网付款批次受控验收", () => {
summary.stages.finalState = {
paymentFlag: snapshotFlag(finalPaymentFlag),
erpSyncEffectiveEnabled: finalErpFlag.effectiveEnabled,
alreadyOnMode,
};
expect(finalPaymentFlag.effectiveEnabled).toBe(true);
expect(finalPaymentFlag.overrideMode).toBe("force_on");
expect(finalErpFlag.effectiveEnabled).toBe(false);
} else {
const rolledBack = await forcePaymentBatchOff(admin.api, `${issueId} rollback ${marker}`);
summary.stages.rollback = { paymentFlagAfterRollback: snapshotFlag(rolledBack) };
expect(rolledBack.effectiveEnabled).toBe(false);
} else if (paymentFlagBefore) {
const restored = await restoreFlagSnapshot(admin.api, snapshotFlag(paymentFlagBefore));
summary.stages.rollback = { paymentFlagAfterRestore: snapshotFlag(restored) };
expect(restored.overrideMode).toBe(paymentFlagBefore.overrideMode);
expect(restored.rolloutType).toBe(paymentFlagBefore.rolloutType);
expect(restored.departmentIds).toEqual(paymentFlagBefore.departmentIds);
expect(restored.note).toBe(paymentFlagBefore.note);
}
} catch (error) {
rollbackError = error;