156 lines
5.8 KiB
TypeScript
156 lines
5.8 KiB
TypeScript
export type MembershipProductType = "credit_pack" | "trial" | "subscription";
|
|
|
|
export type MembershipEntitlement = {
|
|
featureKey: string;
|
|
allowanceType: string;
|
|
allowanceCount: number | null;
|
|
resetPeriod: string;
|
|
modelTier: string | null;
|
|
fairUsePolicyId: string | null;
|
|
metadata: unknown;
|
|
};
|
|
|
|
export type MembershipProduct = {
|
|
id: string;
|
|
productId: string;
|
|
code: string;
|
|
version: number;
|
|
name: string;
|
|
description: string;
|
|
productType: MembershipProductType;
|
|
billingPeriod: "none" | "day" | "month" | "year";
|
|
intervalCount: number;
|
|
priceCents: number;
|
|
currency: string;
|
|
credits: number;
|
|
entitlements: MembershipEntitlement[];
|
|
};
|
|
|
|
export type MembershipPlanAlias = "trial" | "monthly" | "yearly";
|
|
|
|
export function membershipHref(source: string, options: { redeem?: boolean; plan?: MembershipPlanAlias } = {}) {
|
|
const params = new URLSearchParams({ source });
|
|
if (options.redeem) params.set("redeem", "1");
|
|
if (options.plan) params.set("plan", options.plan);
|
|
return `/membership?${params.toString()}`;
|
|
}
|
|
|
|
export function planDisplayLabel(alias: MembershipPlanAlias) {
|
|
const labels: Record<MembershipPlanAlias, string> = {
|
|
trial: "体验",
|
|
monthly: "月卡",
|
|
yearly: "年卡",
|
|
};
|
|
return labels[alias];
|
|
}
|
|
|
|
export function formatPrice(priceCents: number, currency = "CNY") {
|
|
return `${currency === "CNY" ? "¥" : `${currency} `}${(priceCents / 100).toFixed(2)}`;
|
|
}
|
|
|
|
export function formatMembershipDate(value: string | null | undefined) {
|
|
if (!value) return "";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
return date.toLocaleDateString("zh-CN");
|
|
}
|
|
|
|
export function orderStatusLabel(status: string | null | undefined) {
|
|
const labels: Record<string, string> = {
|
|
paid: "已支付",
|
|
pending: "待支付",
|
|
failed: "支付失败",
|
|
closed: "已关闭",
|
|
cancelled: "已取消",
|
|
};
|
|
return (status && labels[status]) || status || "未知";
|
|
}
|
|
|
|
export function membershipSourceNotice(source: string | null | undefined) {
|
|
const notices: Record<string, string> = {
|
|
credits: "来自对话页余额入口",
|
|
"account-menu": "来自账户菜单",
|
|
"insufficient-credits": "余额不足,请先补充点数或开通会员后再发送问题",
|
|
rectification: "生时校正需要点数或会员权益",
|
|
};
|
|
return notices[source ?? ""] ?? "";
|
|
}
|
|
|
|
export const BALANCE_SYNC_KEY = "jyotisha:balance:updated";
|
|
export const BALANCE_CHANGED_EVENT = "jyotisha:balance:changed";
|
|
|
|
export function notifyBalanceChanged(credits: number) {
|
|
try {
|
|
localStorage.setItem(BALANCE_SYNC_KEY, JSON.stringify({ credits, at: Date.now() }));
|
|
} catch {
|
|
// Storage can be unavailable in privacy modes; the window event still fires.
|
|
}
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(new CustomEvent(BALANCE_CHANGED_EVENT, { detail: { credits } }));
|
|
}
|
|
}
|
|
|
|
export function planAlias(product: Pick<MembershipProduct, "productType" | "billingPeriod">): MembershipPlanAlias | null {
|
|
if (product.productType === "trial") return "trial";
|
|
if (product.productType !== "subscription") return null;
|
|
if (product.billingPeriod === "month") return "monthly";
|
|
if (product.billingPeriod === "year") return "yearly";
|
|
return null;
|
|
}
|
|
|
|
export function selectMembershipPlans(products: readonly MembershipProduct[]) {
|
|
const aliases: MembershipPlanAlias[] = ["trial", "monthly", "yearly"];
|
|
return aliases.flatMap((alias) => {
|
|
const product = products.find((candidate) => planAlias(candidate) === alias);
|
|
return product ? [product] : [];
|
|
});
|
|
}
|
|
|
|
export function formatProductDuration(product: Pick<MembershipProduct, "productType" | "billingPeriod" | "intervalCount">) {
|
|
if (product.productType === "credit_pack") return "购买后立即到账";
|
|
const unit = product.billingPeriod === "day" ? "天" : product.billingPeriod === "month" ? "个月" : "年";
|
|
return `${product.intervalCount} ${unit}`;
|
|
}
|
|
|
|
export function productAudience(product: Pick<MembershipProduct, "productType" | "billingPeriod">) {
|
|
if (product.productType === "trial") return "适合初次体验完整咨询流程";
|
|
if (product.productType === "credit_pack") return "适合按需补充点数、灵活使用";
|
|
if (product.billingPeriod === "year") return "适合长期记录与持续复盘";
|
|
return "适合稳定使用咨询与报告功能";
|
|
}
|
|
|
|
const featureLabels: Record<string, string> = {
|
|
"chat.standard": "标准 AI 咨询",
|
|
"report.full": "完整个人报告",
|
|
rectification: "生时校正",
|
|
};
|
|
|
|
export function entitlementLabel(entitlement: MembershipEntitlement) {
|
|
const feature = featureLabels[entitlement.featureKey] ?? entitlement.featureKey;
|
|
if (entitlement.allowanceType === "unlimited") return `${feature}不限点数(遵循合理使用规则)`;
|
|
if (entitlement.allowanceType === "credits") return `${entitlement.allowanceCount ?? 0} 点咨询点数`;
|
|
if (typeof entitlement.allowanceCount === "number") return `${feature} ${entitlement.allowanceCount} 次`;
|
|
return feature;
|
|
}
|
|
|
|
export function redeemErrorMessage(status: number, payload: unknown) {
|
|
if (status === 429) return "请求过于频繁";
|
|
if (payload && typeof payload === "object" && "code" in payload) {
|
|
const code = String((payload as { code?: unknown }).code ?? "");
|
|
const messages: Record<string, string> = {
|
|
invalid_code: "兑换码不存在",
|
|
expired_code: "兑换码已过期",
|
|
already_redeemed: "兑换码已被使用",
|
|
account_not_eligible: "兑换码不适用于当前账户",
|
|
rate_limited: "请求过于频繁",
|
|
};
|
|
if (messages[code]) return messages[code];
|
|
}
|
|
if (status >= 500) return "系统异常,请稍后重试";
|
|
if (payload && typeof payload === "object" && "error" in payload) {
|
|
const error = (payload as { error?: unknown }).error;
|
|
if (typeof error === "string" && error) return error;
|
|
}
|
|
return "系统异常,请稍后重试";
|
|
}
|