mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-28 08:08:32 +00:00
feat: add subscription expiry email toggle
This commit is contained in:
@@ -537,10 +537,11 @@ export interface SystemSettings {
|
||||
payment_visible_method_wxpay_enabled?: boolean;
|
||||
openai_advanced_scheduler_enabled?: boolean;
|
||||
|
||||
// Balance & quota notification
|
||||
// 余额、订阅到期与账号限额通知
|
||||
balance_low_notify_enabled: boolean;
|
||||
balance_low_notify_threshold: number;
|
||||
balance_low_notify_recharge_url: string;
|
||||
subscription_expiry_notify_enabled: boolean;
|
||||
account_quota_notify_enabled: boolean;
|
||||
account_quota_notify_emails: NotifyEmailEntry[];
|
||||
|
||||
@@ -756,10 +757,11 @@ export interface UpdateSettingsRequest {
|
||||
payment_visible_method_alipay_enabled?: boolean;
|
||||
payment_visible_method_wxpay_enabled?: boolean;
|
||||
openai_advanced_scheduler_enabled?: boolean;
|
||||
// Balance & quota notification
|
||||
// 余额、订阅到期与账号限额通知
|
||||
balance_low_notify_enabled?: boolean;
|
||||
balance_low_notify_threshold?: number;
|
||||
balance_low_notify_recharge_url?: string;
|
||||
subscription_expiry_notify_enabled?: boolean;
|
||||
account_quota_notify_enabled?: boolean;
|
||||
account_quota_notify_emails?: NotifyEmailEntry[];
|
||||
|
||||
@@ -862,6 +864,8 @@ export interface EmailTemplateOption {
|
||||
value: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
export type EmailTemplateEventOption = string | EmailTemplateOption;
|
||||
|
||||
@@ -5795,6 +5795,12 @@ export default {
|
||||
addEmail: 'Add Email',
|
||||
emailPlaceholder: 'Enter email address',
|
||||
},
|
||||
subscriptionExpiryNotify: {
|
||||
title: 'Subscription Expiry Reminder',
|
||||
description: 'Control whether users receive subscription expiry reminder emails.',
|
||||
enabled: 'Enable Subscription Expiry Reminder',
|
||||
enabledHint: 'When enabled, the system sends reminders 7, 3, and 1 day before expiry.'
|
||||
},
|
||||
smtp: {
|
||||
title: 'SMTP Settings',
|
||||
description: 'Configure email sending for verification codes',
|
||||
|
||||
@@ -5954,6 +5954,12 @@ export default {
|
||||
addEmail: '添加邮箱',
|
||||
emailPlaceholder: '输入邮箱地址',
|
||||
},
|
||||
subscriptionExpiryNotify: {
|
||||
title: '订阅到期提醒',
|
||||
description: '控制是否向用户发送订阅即将到期的邮件提醒。',
|
||||
enabled: '启用订阅到期提醒',
|
||||
enabledHint: '开启后,系统会在订阅到期前 7 天、3 天、1 天各发送一次提醒。'
|
||||
},
|
||||
smtp: {
|
||||
title: 'SMTP 设置',
|
||||
description: '配置用于发送验证码的邮件服务',
|
||||
|
||||
@@ -6283,6 +6283,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订阅到期提醒 -->
|
||||
<div class="card">
|
||||
<div
|
||||
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||
>
|
||||
<h3 class="text-base font-medium text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.subscriptionExpiryNotify.title") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.subscriptionExpiryNotify.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6 py-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label
|
||||
class="mb-0 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.subscriptionExpiryNotify.enabled") }}
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.subscriptionExpiryNotify.enabledHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.subscription_expiry_notify_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EmailTemplateEditor />
|
||||
|
||||
<!-- Balance Low Notification -->
|
||||
@@ -7005,10 +7034,11 @@ const form = reactive<SettingsForm>({
|
||||
rewrite_message_cache_control: false,
|
||||
antigravity_user_agent_version: "",
|
||||
openai_codex_user_agent: "",
|
||||
// Balance & quota notification
|
||||
// 余额、订阅到期与账号限额通知
|
||||
balance_low_notify_enabled: false,
|
||||
balance_low_notify_threshold: 0,
|
||||
balance_low_notify_recharge_url: "",
|
||||
subscription_expiry_notify_enabled: true,
|
||||
account_quota_notify_enabled: false,
|
||||
account_quota_notify_emails: [] as NotifyEmailEntry[],
|
||||
// Channel Monitor feature switch
|
||||
@@ -8137,12 +8167,14 @@ async function saveSettings() {
|
||||
form.payment_cancel_rate_limit_window_mode,
|
||||
payment_alipay_force_qrcode: form.payment_alipay_force_qrcode,
|
||||
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
|
||||
// Balance & quota notification
|
||||
// 余额、订阅到期与账号限额通知
|
||||
balance_low_notify_enabled: form.balance_low_notify_enabled,
|
||||
balance_low_notify_threshold:
|
||||
Number(form.balance_low_notify_threshold) || 0,
|
||||
balance_low_notify_recharge_url: (form.balance_low_notify_recharge_url =
|
||||
form.balance_low_notify_recharge_url || currentOrigin),
|
||||
subscription_expiry_notify_enabled:
|
||||
form.subscription_expiry_notify_enabled,
|
||||
account_quota_notify_enabled: form.account_quota_notify_enabled,
|
||||
account_quota_notify_emails: (
|
||||
form.account_quota_notify_emails || []
|
||||
|
||||
@@ -400,6 +400,7 @@ const baseSettingsResponse = {
|
||||
balance_low_notify_enabled: false,
|
||||
balance_low_notify_threshold: 0,
|
||||
balance_low_notify_recharge_url: "",
|
||||
subscription_expiry_notify_enabled: true,
|
||||
account_quota_notify_enabled: false,
|
||||
account_quota_notify_emails: [],
|
||||
};
|
||||
|
||||
@@ -67,12 +67,9 @@
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label || option.value }}
|
||||
{{ formatEventOptionLabel(option) }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="selectedEventDescription" class="input-hint">
|
||||
{{ selectedEventDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label" for="email-template-locale">
|
||||
@@ -95,6 +92,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedEventMeta"
|
||||
class="rounded-lg border border-primary-100 bg-primary-50/70 p-4 dark:border-primary-900/50 dark:bg-primary-950/20"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ selectedEventMeta.label }}
|
||||
</div>
|
||||
<span
|
||||
class="rounded-full bg-white px-2.5 py-1 text-xs font-medium text-gray-600 shadow-sm ring-1 ring-gray-200 dark:bg-dark-800 dark:text-gray-300 dark:ring-dark-600"
|
||||
>
|
||||
{{ selectedEventMeta.categoryLabel }}
|
||||
</span>
|
||||
<span
|
||||
class="rounded-full px-2.5 py-1 text-xs font-medium"
|
||||
:class="
|
||||
selectedEventMeta.optional
|
||||
? 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
"
|
||||
>
|
||||
{{ selectedEventMeta.optional ? localText("可退订通知", "Optional") : localText("事务邮件", "Transactional") }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ selectedEventMeta.timing }}
|
||||
</p>
|
||||
<p
|
||||
v-if="selectedEventDescription"
|
||||
class="mt-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{{ selectedEventDescription }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!eventOptions.length || !localeOptions.length"
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-300"
|
||||
@@ -274,6 +306,142 @@ const previewSubject = ref("");
|
||||
const previewHtml = ref("");
|
||||
const initializingSelection = ref(false);
|
||||
|
||||
interface EventDisplayMeta {
|
||||
label: string;
|
||||
timing: string;
|
||||
categoryLabel: string;
|
||||
}
|
||||
|
||||
function localText(zh: string, en: string): string {
|
||||
return locale.value.toLowerCase().startsWith("zh") ? zh : en;
|
||||
}
|
||||
|
||||
const eventDisplayMeta: Record<string, EventDisplayMeta> = {
|
||||
"auth.verify_code": {
|
||||
label: "邮箱验证码",
|
||||
timing: "注册、绑定邮箱、OAuth 补全邮箱或 TOTP 邮箱校验时发送。",
|
||||
categoryLabel: "认证安全",
|
||||
},
|
||||
"auth.password_reset": {
|
||||
label: "密码重置",
|
||||
timing: "用户请求密码重置链接时发送。",
|
||||
categoryLabel: "认证安全",
|
||||
},
|
||||
"notification_email.verify_code": {
|
||||
label: "通知邮箱验证码",
|
||||
timing: "用户添加并验证额外通知邮箱时发送。",
|
||||
categoryLabel: "认证安全",
|
||||
},
|
||||
"subscription.purchase_success": {
|
||||
label: "订阅开通成功",
|
||||
timing: "订阅订单完成支付并成功开通或续期后发送。",
|
||||
categoryLabel: "订阅",
|
||||
},
|
||||
"subscription.expiry_reminder": {
|
||||
label: "订阅到期提醒",
|
||||
timing: "后台任务在订阅仍有效且距离到期剩余 7 天、3 天、1 天时各发送一次,可通过邮件设置中的开关关闭。",
|
||||
categoryLabel: "订阅",
|
||||
},
|
||||
"balance.low": {
|
||||
label: "余额不足提醒",
|
||||
timing: "用户余额低于全局或个人配置的提醒阈值时发送。",
|
||||
categoryLabel: "计费",
|
||||
},
|
||||
"balance.recharge_success": {
|
||||
label: "余额充值成功",
|
||||
timing: "余额充值订单支付完成并入账后发送。",
|
||||
categoryLabel: "计费",
|
||||
},
|
||||
"account.quota_alert": {
|
||||
label: "账号限额告警",
|
||||
timing: "上游账号的用量达到配置的额度告警阈值时发送给管理员通知邮箱。",
|
||||
categoryLabel: "管理告警",
|
||||
},
|
||||
"content_moderation.violation_notice": {
|
||||
label: "内容审计违规提醒",
|
||||
timing: "用户请求命中内容审计或风控规则、但尚未被禁用时发送。",
|
||||
categoryLabel: "风控",
|
||||
},
|
||||
"content_moderation.account_disabled": {
|
||||
label: "内容审计禁用账号",
|
||||
timing: "内容审计违规次数达到封禁阈值并自动禁用用户账号时发送。",
|
||||
categoryLabel: "风控",
|
||||
},
|
||||
"ops.alert": {
|
||||
label: "运维告警",
|
||||
timing: "运维监控规则触发告警并满足邮件通知配置时发送给运维收件人。",
|
||||
categoryLabel: "运维",
|
||||
},
|
||||
"ops.scheduled_report": {
|
||||
label: "运维定时报表",
|
||||
timing: "运维日报、周报、错误摘要或账号健康报表到达配置的发送时间时发送。",
|
||||
categoryLabel: "运维",
|
||||
},
|
||||
};
|
||||
|
||||
const eventDisplayMetaEn: Record<string, EventDisplayMeta> = {
|
||||
"auth.verify_code": {
|
||||
label: "Email Verification Code",
|
||||
timing: "Sent for registration, email binding, OAuth pending email completion, or TOTP email verification.",
|
||||
categoryLabel: "Auth",
|
||||
},
|
||||
"auth.password_reset": {
|
||||
label: "Password Reset",
|
||||
timing: "Sent when a user requests a password reset link.",
|
||||
categoryLabel: "Auth",
|
||||
},
|
||||
"notification_email.verify_code": {
|
||||
label: "Notification Email Verification",
|
||||
timing: "Sent when a user adds and verifies an extra notification email address.",
|
||||
categoryLabel: "Auth",
|
||||
},
|
||||
"subscription.purchase_success": {
|
||||
label: "Subscription Activated",
|
||||
timing: "Sent after a subscription order is paid and the subscription is activated or extended.",
|
||||
categoryLabel: "Subscription",
|
||||
},
|
||||
"subscription.expiry_reminder": {
|
||||
label: "Subscription Expiry Reminder",
|
||||
timing: "Sent by the background job when an active subscription has 7, 3, or 1 day remaining. It can be disabled in Email settings.",
|
||||
categoryLabel: "Subscription",
|
||||
},
|
||||
"balance.low": {
|
||||
label: "Low Balance Alert",
|
||||
timing: "Sent when a user's balance drops below the global or personal reminder threshold.",
|
||||
categoryLabel: "Billing",
|
||||
},
|
||||
"balance.recharge_success": {
|
||||
label: "Balance Recharge Success",
|
||||
timing: "Sent after a balance recharge order is paid and credited.",
|
||||
categoryLabel: "Billing",
|
||||
},
|
||||
"account.quota_alert": {
|
||||
label: "Account Quota Alert",
|
||||
timing: "Sent to admin notification emails when an upstream account reaches the configured quota alert threshold.",
|
||||
categoryLabel: "Admin",
|
||||
},
|
||||
"content_moderation.violation_notice": {
|
||||
label: "Risk Control Violation Notice",
|
||||
timing: "Sent when a user request triggers content moderation or risk-control rules but the account is not disabled yet.",
|
||||
categoryLabel: "Risk Control",
|
||||
},
|
||||
"content_moderation.account_disabled": {
|
||||
label: "Risk Control Account Disabled",
|
||||
timing: "Sent when content moderation reaches the ban threshold and automatically disables the user account.",
|
||||
categoryLabel: "Risk Control",
|
||||
},
|
||||
"ops.alert": {
|
||||
label: "Ops Alert",
|
||||
timing: "Sent to ops recipients when an ops monitoring rule fires and email notification settings allow it.",
|
||||
categoryLabel: "Ops",
|
||||
},
|
||||
"ops.scheduled_report": {
|
||||
label: "Ops Scheduled Report",
|
||||
timing: "Sent when a configured daily, weekly, error digest, or account health report reaches its scheduled send time.",
|
||||
categoryLabel: "Ops",
|
||||
},
|
||||
};
|
||||
|
||||
function normalizeEventOption(option: EmailTemplateEventOption): EmailTemplateOption {
|
||||
if (typeof option === "string") {
|
||||
return { value: option };
|
||||
@@ -281,10 +449,58 @@ function normalizeEventOption(option: EmailTemplateEventOption): EmailTemplateOp
|
||||
return option;
|
||||
}
|
||||
|
||||
function eventMetaFor(option?: EmailTemplateOption | null) {
|
||||
if (!option) return null;
|
||||
const displayMeta = (
|
||||
locale.value.toLowerCase().startsWith("zh")
|
||||
? eventDisplayMeta
|
||||
: eventDisplayMetaEn
|
||||
)[option.value];
|
||||
const label = displayMeta?.label || option.label || option.value;
|
||||
const timing = displayMeta?.timing || option.description || "";
|
||||
const categoryLabel =
|
||||
displayMeta?.categoryLabel || formatCategory(option.category || "");
|
||||
return {
|
||||
label,
|
||||
timing,
|
||||
categoryLabel,
|
||||
optional: option.optional === true,
|
||||
};
|
||||
}
|
||||
|
||||
function formatEventOptionLabel(option: EmailTemplateOption): string {
|
||||
const meta = eventMetaFor(option);
|
||||
if (!meta) return option.label || option.value;
|
||||
return meta.label;
|
||||
}
|
||||
|
||||
function formatCategory(category: string): string {
|
||||
const normalized = category.trim().toLowerCase();
|
||||
if (!normalized) return localText("通知", "Notification");
|
||||
const labels: Record<string, { zh: string; en: string }> = {
|
||||
auth: { zh: "认证安全", en: "Auth" },
|
||||
subscription: { zh: "订阅", en: "Subscription" },
|
||||
billing: { zh: "计费", en: "Billing" },
|
||||
admin: { zh: "管理告警", en: "Admin" },
|
||||
risk_control: { zh: "风控", en: "Risk Control" },
|
||||
ops: { zh: "运维", en: "Ops" },
|
||||
};
|
||||
const item = labels[normalized];
|
||||
return item ? localText(item.zh, item.en) : category;
|
||||
}
|
||||
|
||||
const selectedEventOption = computed(() => {
|
||||
return (
|
||||
eventOptions.value.find((option) => option.value === selectedEvent.value) ||
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
const selectedEventMeta = computed(() => eventMetaFor(selectedEventOption.value));
|
||||
|
||||
const selectedEventDescription = computed(() => {
|
||||
return (
|
||||
eventOptions.value.find((option) => option.value === selectedEvent.value)
|
||||
?.description || ""
|
||||
selectedEventOption.value?.description || ""
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user