feat(web): 用户面订阅 —— 购买/续订入口 + 到期提醒
账单页新增:当前订阅状态(套餐/发放节奏/已发放次数/到期倒计时)+ 可购套餐。 已有订阅时购买区标题自动变成「续订(在当前到期时间上顺延)」,与后端的顺延 语义一致,免得用户以为会新开一条。 到期倒计时 ≤7 天转琥珀、≤3 天转红:到期即失效且**没有任何扣款或续费通知**, 用户只能从这里看见,不显眼等于没有。 支付弹窗改为对「买什么」中立(PayItem:积分包 | 订阅),下单/轮询/超时/warn 处理全共用。给订阅复制一份弹窗的话,两边迟早漂移——之前修过的轮询问题就得修 两遍。 真环境验证:账单页显示真实订阅(已发放 3 次 = 首笔 + 补发 2 笔)、余额 3.0K (NULL 回填后的 2981)、套餐卡片算出「周期内共 3 次,合计 3.0K 积分」。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+37
-2
@@ -274,18 +274,53 @@ export async function billingPacks(): Promise<{ packs: Pack[]; channels: string[
|
||||
// createWechatOrder 微信 Native 下单:返回订单号 + code_url(渲染成二维码扫码付)
|
||||
// + expires_at(二维码有效期,由服务端 orderTTL 决定,前端只负责倒计时展示)。
|
||||
export async function createWechatOrder(
|
||||
packId: string,
|
||||
target: { packId: string } | { planId: string },
|
||||
): Promise<{ order_id: string; code_url: string; amount_fen: number; expires_at: string }> {
|
||||
// 积分包与订阅走同一条支付链路(下单/回调/查单/掉单补偿全复用),只是订单内容不同。
|
||||
const body = "packId" in target ? { pack_id: target.packId } : { plan_id: target.planId };
|
||||
const res = guard401(
|
||||
await fetch(`${GATEWAY}/api/v1/billing/orders`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...bearer() },
|
||||
body: JSON.stringify({ pack_id: packId }),
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
return jsonOrThrow(res, "下单失败");
|
||||
}
|
||||
|
||||
// ---- 订阅(手动购买制:买一个周期,期内每 N 天发一次积分,到期即失效)----
|
||||
export interface SubPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
price_fen: number;
|
||||
duration_days: number;
|
||||
refill_credits_micro: number;
|
||||
refill_interval_days: number;
|
||||
}
|
||||
|
||||
export interface MySub {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
status: string;
|
||||
started_at: string;
|
||||
expires_at: string;
|
||||
refill_seq: number;
|
||||
}
|
||||
|
||||
export async function subPlans(): Promise<SubPlan[]> {
|
||||
const res = guard401(await fetch(`${GATEWAY}/api/v1/billing/sub-plans`, { headers: bearer() }));
|
||||
if (!res.ok) return [];
|
||||
return ((await res.json()) as { plans?: SubPlan[] }).plans ?? [];
|
||||
}
|
||||
|
||||
// 我的订阅(无则 null)。到期即失效、不自动续费,所以到期时间要显眼地告诉用户。
|
||||
export async function mySubscription(): Promise<{ subscription: MySub | null; plan: SubPlan | null }> {
|
||||
const res = guard401(await fetch(`${GATEWAY}/api/v1/billing/subscription`, { headers: bearer() }));
|
||||
if (!res.ok) return { subscription: null, plan: null };
|
||||
const d = (await res.json()) as { subscription?: MySub | null; plan?: SubPlan | null };
|
||||
return { subscription: d.subscription ?? null, plan: d.plan ?? null };
|
||||
}
|
||||
|
||||
// orderStatus 轮询订单态(pending 时服务端顺路主动查单,本地也能确认到账)。
|
||||
// warn 必须一并返回:服务端在「已付但金额与订单不符」时不入账、挂起人工核对,
|
||||
// 订单会一直停在 pending —— 丢掉 warn 的话用户付了钱、界面却只会一直转圈等下去。
|
||||
|
||||
Reference in New Issue
Block a user