From 66ec4c7d1cfb131221e5c2a98435d4e6030b1bcd Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 26 Apr 2026 14:00:47 +0800 Subject: [PATCH] feat(account): define post-release notification preferences --- frontend/src/api/accountSettings.ts | 70 +++++++++++++++++++ .../account-settings/accountSettings.test.ts | 37 ++++++++++ 2 files changed, 107 insertions(+) diff --git a/frontend/src/api/accountSettings.ts b/frontend/src/api/accountSettings.ts index 5826aa6b..035df01a 100644 --- a/frontend/src/api/accountSettings.ts +++ b/frontend/src/api/accountSettings.ts @@ -6,6 +6,11 @@ export type AccountAnswerDepth = "concise" | "balanced" | "deep"; export type AccountEvidenceDisplay = "summary" | "inline" | "full"; export type AccountNotificationKey = "pendingConfirmation" | "executionWriteback" | "releaseGate" | "failureAlert"; export type AccountSettingsSaveScope = "local" | "account"; +export type AccountPostReleaseNotificationEvent = + | "feedback_received" + | "revenue_signal" + | "compliance_blocked" + | "iteration_recommendation"; export type AccountReleaseNotificationEvent = | "release_gate_blocked" | "release_gate_ready" @@ -59,6 +64,22 @@ export interface AccountReleaseGateNotificationPreference { reason: string; } +export interface AccountPostReleaseNotificationPreference { + event: AccountPostReleaseNotificationEvent; + notificationKey: AccountNotificationKey; + surface: "release_growth"; + spaceReadable: boolean; + inAppEnabled: boolean; + showInAppReminder: boolean; + desktopPreference: boolean; + emailDigestPreference: boolean; + title: string; + reason: string; + evidencePolicy: "preserve_business_evidence"; + reminderPolicy: "page_reminder_only"; + mutedDetail: string; +} + export interface AccountSettings { profile: AccountProfileSettings; aiPreferences: AccountAiPreferenceSettings; @@ -165,6 +186,28 @@ const releaseNotificationEventMeta: Record< }, }; +const postReleaseNotificationEventMeta: Record< + AccountPostReleaseNotificationEvent, + { notificationKey: AccountNotificationKey; reason: string } +> = { + feedback_received: { + notificationKey: "releaseGate", + reason: "Release/Growth 收到发布后用户反馈时使用发布门禁偏好控制页面内提醒;反馈证据本身不受偏好隐藏。", + }, + revenue_signal: { + notificationKey: "releaseGate", + reason: "Release/Growth 收到收入、转化或增长信号时使用发布门禁偏好控制页面内提醒;收入证据本身不受偏好隐藏。", + }, + compliance_blocked: { + notificationKey: "releaseGate", + reason: "合规、风控或发布后审核阻塞下一步时使用发布门禁偏好控制页面内提醒;阻塞状态与证据仍必须展示在业务页面。", + }, + iteration_recommendation: { + notificationKey: "pendingConfirmation", + reason: "AI 生成下一轮迭代建议、需要人类拍板时使用待确认偏好控制页面内提醒;建议对象和证据仍保留。", + }, +}; + export function createDefaultAccountSettings(seed: AccountSettingsSeed = {}): AccountSettings { return { profile: { @@ -303,6 +346,33 @@ export function getReleaseGateNotificationPreference( }; } +export function getPostReleaseNotificationPreference( + settings: AccountSettings | Partial | null | undefined, + event: AccountPostReleaseNotificationEvent +): AccountPostReleaseNotificationPreference { + const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); + const meta = postReleaseNotificationEventMeta[event]; + const preference = normalized.notifications[meta.notificationKey]; + const enabled = Boolean(preference.enabled); + return { + event, + notificationKey: meta.notificationKey, + surface: "release_growth", + spaceReadable: true, + inAppEnabled: enabled, + showInAppReminder: enabled, + desktopPreference: enabled && Boolean(preference.desktop), + emailDigestPreference: enabled && Boolean(preference.emailDigest), + title: accountNotificationLabels[meta.notificationKey].title, + reason: meta.reason, + evidencePolicy: "preserve_business_evidence", + reminderPolicy: "page_reminder_only", + mutedDetail: enabled + ? "Release/Growth 或 Space 可显示页面内提醒;桌面/邮件只记录偏好,不代表系统级推送已接入。" + : "仅静默页面内提醒;反馈、收入、合规、迭代建议及其证据仍必须按业务页面展示。", + }; +} + export async function getAccountSettings(seed?: AccountSettingsSeed): Promise { const defaults = createDefaultAccountSettings(seed); const localSettings = mergeAccountSettings(defaults, readStoredAccountSettings()); diff --git a/frontend/src/features/account-settings/accountSettings.test.ts b/frontend/src/features/account-settings/accountSettings.test.ts index 8c8e556e..130043eb 100644 --- a/frontend/src/features/account-settings/accountSettings.test.ts +++ b/frontend/src/features/account-settings/accountSettings.test.ts @@ -3,6 +3,7 @@ import { createDefaultAccountSettings, getAccountNotificationConsumption, getAccountSettings, + getPostReleaseNotificationPreference, getReleaseGateNotificationPreference, shouldShowAccountNotification, updateAccountSettings, @@ -132,6 +133,42 @@ describe("account settings api", () => { expect(failedEvent.reason).toContain("failed event"); }); + it("maps post-release feedback, revenue, compliance, and iteration events to local reminder preferences", () => { + const defaults = createDefaultAccountSettings(); + const settings = { + ...defaults, + notifications: { + ...defaults.notifications, + releaseGate: { enabled: true, desktop: false, emailDigest: true }, + pendingConfirmation: { enabled: false, desktop: true, emailDigest: true }, + }, + }; + + const feedback = getPostReleaseNotificationPreference(settings, "feedback_received"); + const revenue = getPostReleaseNotificationPreference(settings, "revenue_signal"); + const compliance = getPostReleaseNotificationPreference(settings, "compliance_blocked"); + const iteration = getPostReleaseNotificationPreference(settings, "iteration_recommendation"); + + expect(feedback.notificationKey).toBe("releaseGate"); + expect(feedback.surface).toBe("release_growth"); + expect(feedback.spaceReadable).toBe(true); + expect(feedback.showInAppReminder).toBe(true); + expect(feedback.desktopPreference).toBe(false); + expect(feedback.emailDigestPreference).toBe(true); + expect(feedback.reminderPolicy).toBe("page_reminder_only"); + expect(feedback.evidencePolicy).toBe("preserve_business_evidence"); + expect(feedback.mutedDetail).toContain("不代表系统级推送已接入"); + expect(revenue.notificationKey).toBe("releaseGate"); + expect(revenue.reason).toContain("收入"); + expect(compliance.notificationKey).toBe("releaseGate"); + expect(compliance.reason).toContain("合规"); + expect(iteration.notificationKey).toBe("pendingConfirmation"); + expect(iteration.showInAppReminder).toBe(false); + expect(iteration.desktopPreference).toBe(false); + expect(iteration.emailDigestPreference).toBe(false); + expect(iteration.mutedDetail).toContain("证据仍必须"); + }); + it("falls back missing notification fields to safe defaults", async () => { window.localStorage.setItem( "pmflow:account-settings:v1",