feat(admin): 积分包支持就地生成付款码,验证支付链路
管理端「支付 · 配置 → 积分包」每行加「生成付款码」:下单 → 二维码 → 盯到终态, 用来确认「渠道配置 → 下单 → 扫码 → 回调/查单 → 幂等入账 → 积分到账」在当前 环境真的通。 刻意走用户面的 /api/v1/billing/*,不做 admin 专用旁路 —— 造条测试专线的话, 验过了也不能说明线上用户那条路通。租户由服务端按登录用户解析,无需带租户头。 代价是这会真实扣款,所以: - 弹窗顶部显著提示是真单、会真扣钱,并指向「订单与对账」的退款入口; - 已下架的包按钮禁用(服务端本就会拒,别让人白扫)。 弹窗展示订单号/状态/轮询次数/到账积分,出问题时能直接判断卡在哪一环, 比只说一句"成功/失败"有用。轮询策略与 sundynix-web 支付弹窗保持一致: 递归 setTimeout、切后台暂停、失败指数退避、倒计时以服务端 expires_at 为准。 依赖:admin 新增 qrcode(懒加载进 PaymentConfigPage 分块,不进主包)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -301,6 +301,38 @@ export async function adminReconcile(): Promise<{ diffs: ReconcileDiff[]; ok: bo
|
||||
return { diffs: d.diffs ?? [], ok: !!d.ok };
|
||||
}
|
||||
|
||||
// ---- 支付联调:走「用户面」的真实下单/查单接口 ----
|
||||
// 刻意不做 admin 专用旁路:验的就是用户真实走的那条路(下单 → 渠道 → 回调/查单 → 入账),
|
||||
// 造条测试专线的话验过了也不代表线上通。因此这两个打的是 /api/v1/billing/*(非 /admin/*),
|
||||
// 租户由服务端按当前登录用户解析,无需带租户头。
|
||||
export interface TestOrder {
|
||||
order_id: string;
|
||||
code_url: string;
|
||||
amount_fen: number;
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
export async function createPayOrder(packId: string): Promise<TestOrder> {
|
||||
const res = guard(
|
||||
await fetch(`${GATEWAY}/api/v1/billing/orders`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify({ pack_id: packId }),
|
||||
}),
|
||||
);
|
||||
const d = (await res.json().catch(() => ({}))) as Partial<TestOrder> & { error?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `下单失败: ${res.status}`);
|
||||
return d as TestOrder;
|
||||
}
|
||||
|
||||
// warn:已付但金额与订单不符 —— 服务端不入账、挂起人工核对,订单会一直停在 pending。
|
||||
export async function payOrderStatus(orderId: string): Promise<{ order: PayOrder; warn?: string }> {
|
||||
const res = guard(await fetch(`${GATEWAY}/api/v1/billing/orders/${orderId}`, { headers: authHeaders() }));
|
||||
const d = (await res.json().catch(() => ({}))) as { order?: PayOrder; warn?: string; error?: string };
|
||||
if (!res.ok) throw new Error(d.error ?? `查询失败: ${res.status}`);
|
||||
return { order: d.order as PayOrder, warn: d.warn };
|
||||
}
|
||||
|
||||
// 人工退款:仅对已入账(paid)单——置 refunded + 记 adjust 负分录 + 回退余额(幂等)。
|
||||
// status="noop" 表示该单本就无需退(已退/未支付)。真渠道钱的原路退回需 admin 另在商户后台操作。
|
||||
export async function adminRefundOrder(id: string, memo: string): Promise<{ status: string; detail?: string }> {
|
||||
|
||||
Reference in New Issue
Block a user