import { useEffect, useState } from "react"; import { adminSubPlans, saveSubPlan, adminSubscriptions, type SubPlan, type SubRow } from "../api"; // 支付 · 订阅:套餐配置(价格/时长/发放节奏)+ 全平台订阅观测。 // // 语义要在界面上讲清楚,否则配错了很难发现: // - 手动购买制,**不自动续费**(微信 Native 无代扣能力),到期即失效; // - 期内每 N 天发一次积分,发放是**累加**(不清零用户已有积分); // - 因此一个周期实际发放次数 = floor(时长 / 间隔),配的时候直接算给人看。 const MICRO = 1_000_000; const credits = (m: number) => (m / MICRO).toLocaleString("zh-CN", { maximumFractionDigits: 2 }); const yuan = (fen: number) => (fen / 100).toFixed(2); export function SubscriptionPage() { return (

支付 · 订阅

订阅套餐定价与发放节奏 · 全平台订阅状态。手动购买制,到期即失效,不自动续费

); } function PlansBlock() { const [rows, setRows] = useState([]); const [name, setName] = useState(""); const [price, setPrice] = useState(""); const [duration, setDuration] = useState("30"); const [refill, setRefill] = useState(""); const [interval, setIntervalDays] = useState("7"); const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); const load = () => adminSubPlans().then(setRows).catch((e) => setErr((e as Error).message)); useEffect(() => { void load(); }, []); const add = async () => { const p = Number(price), d = Number(duration), r = Number(refill), i = Number(interval); if (!name.trim() || !p || !d || !r || !i || busy) return; setBusy(true); setErr(""); try { await saveSubPlan({ name: name.trim(), price_fen: Math.round(p * 100), duration_days: d, refill_credits: r, refill_interval_days: i, active: true, sort: 0, }); setName(""); setPrice(""); setRefill(""); await load(); } catch (e) { setErr((e as Error).message); } finally { setBusy(false); } }; const toggle = async (p: SubPlan) => { try { await saveSubPlan({ id: p.id, name: p.name, price_fen: p.price_fen, duration_days: p.duration_days, refill_credits: p.refill_credits_micro / MICRO, refill_interval_days: p.refill_interval_days, active: !p.active, sort: p.sort, }); await load(); } catch (e) { setErr((e as Error).message); } }; // 一个周期发几次:让人在配的时候就看见后果,而不是上线后才发现只发了一次。 const times = (p: { duration_days: number; refill_interval_days: number }) => p.refill_interval_days > 0 ? Math.floor(p.duration_days / p.refill_interval_days) : 0; const previewTimes = times({ duration_days: Number(duration) || 0, refill_interval_days: Number(interval) || 0 }); const previewTotal = previewTimes * (Number(refill) || 0); return (

订阅套餐

积分按周期发放且累加,不清零用户已有积分
{/* 配置后果预览:省得上线后才发现节奏配错 */} {previewTimes > 0 && (

一个周期发 {previewTimes} 次,合计{" "} {previewTotal.toLocaleString("zh-CN")} 积分 {Number(duration) % Number(interval) !== 0 && ( ({Number(duration)} 不能被 {Number(interval)} 整除,最后一次发放后到期前会有空档) )}

)} {err &&

{err}

}
{rows.map((p) => ( ))} {rows.length === 0 && ( )}
名称 售价 时长 发放节奏 周期总积分 状态
{p.name} ¥{yuan(p.price_fen)} {p.duration_days} 天 每 {p.refill_interval_days} 天 · {credits(p.refill_credits_micro)} {credits(p.refill_credits_micro * times(p))} ({times(p)} 次)
还没有订阅套餐
); } function SubsBlock() { const [rows, setRows] = useState([]); const [err, setErr] = useState(""); useEffect(() => { adminSubscriptions().then(setRows).catch((e) => setErr((e as Error).message)); }, []); const now = Date.now(); const daysLeft = (iso: string) => Math.ceil((new Date(iso).getTime() - now) / 86400000); return (

全平台订阅

到期即失效,不自动续费;剩余天数少的排在前面
{err &&

{err}

} {rows.map((s) => { const left = daysLeft(s.expires_at); return ( ); })} {rows.length === 0 && ( )}
租户 套餐 状态 已发放 到期
{s.tenant_name || s.tenant_id} {s.plan_name || "—"} {s.status === "active" ? "生效中" : "已过期"} {s.refill_seq} 次 {new Date(s.expires_at).toLocaleDateString("zh-CN")} {s.status === "active" && ( // 快到期的要显眼:到期即失效,没有自动续费兜底 剩 {left} 天 )}
还没有人订阅
); }