feat(db): payments 拆为 subscriptions 与 topups
订阅权益与充值入账分表;topups 对齐上游充值单(无 quota_granted), paid_at=complete_time;quota_derived 查询层派生。
This commit is contained in:
@@ -27,6 +27,9 @@ export const ERR = {
|
||||
errorBody("invalid_credentials", "用户名或密码错误"),
|
||||
session_expired: () =>
|
||||
errorBody("session_expired", "会话已过期,请重新登录"),
|
||||
/** 上游控制台 token 失效——**不是**管理端会话,禁止走 401 登出 */
|
||||
platform_token_invalid: () =>
|
||||
errorBody("platform_token_invalid", "平台登录已失效,请重新登录上游账号"),
|
||||
already_initialized: () =>
|
||||
errorBody("already_initialized", "系统已初始化,无法重复引导"),
|
||||
key_already_registered: (key_id: number) =>
|
||||
|
||||
@@ -130,17 +130,33 @@ export const memberMe = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const paymentItem = z.object({
|
||||
export const subscriptionItem = z.object({
|
||||
id: z.number().int(),
|
||||
kind: z.enum(["subscription", "topup"]),
|
||||
paid_at: z.number().int(),
|
||||
paid_amount: z.number(),
|
||||
remark: z.string(),
|
||||
period_start: z.number().int().nullable(),
|
||||
subscription_id: z.number().int(),
|
||||
period_start: z.number().int(),
|
||||
period_end: z.number().int().nullable(),
|
||||
subscription_id: z.number().int().nullable(),
|
||||
quota_granted: z.number().int().nullable(),
|
||||
quota_granted: z.number().int(),
|
||||
quota_usd: z.string(),
|
||||
paid_amount: z.number().nullable(),
|
||||
paid_at: z.number().int().nullable(),
|
||||
remark: z.string(),
|
||||
created_at: z.number().int(),
|
||||
});
|
||||
|
||||
export const topupItem = z.object({
|
||||
id: z.number().int(),
|
||||
upstream_id: z.number().int().nullable(),
|
||||
amount: z.number(),
|
||||
money: z.number(),
|
||||
trade_no: z.string().nullable(),
|
||||
payment_method: z.string().nullable(),
|
||||
payment_provider: z.string().nullable(),
|
||||
status: z.string().nullable(),
|
||||
paid_at: z.number().int(),
|
||||
quota_derived: z.number().int(),
|
||||
quota_usd: z.string(),
|
||||
remark: z.string(),
|
||||
created_at: z.number().int(),
|
||||
});
|
||||
|
||||
export const engineStatus = z.object({
|
||||
@@ -188,46 +204,33 @@ export const memberOverview = z.object({
|
||||
usage: usageTotals,
|
||||
}),
|
||||
),
|
||||
payments: z.array(z.looseObject({ id: z.number().int() })),
|
||||
subscriptions: z.array(subscriptionItem),
|
||||
topups: z.array(topupItem),
|
||||
});
|
||||
|
||||
export const createPaymentBody = z
|
||||
export const createSubscriptionBody = z
|
||||
.object({
|
||||
kind: z.enum(["subscription", "topup"]),
|
||||
paid_at: z.string().min(1),
|
||||
paid_amount: z.number(),
|
||||
remark: z.string().default(""),
|
||||
period_start: z.string().min(1).nullable().optional(),
|
||||
subscription_id: z.number().int().positive(),
|
||||
period_start: z.string().min(1),
|
||||
period_end: z.string().min(1).nullable().optional(),
|
||||
subscription_id: z.number().int().positive().nullable().optional(),
|
||||
quota_granted: z.number().int().nullable().optional(),
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (val.kind === "subscription") {
|
||||
if (!val.period_start) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "订阅必须填写期间起点",
|
||||
path: ["period_start"],
|
||||
});
|
||||
}
|
||||
if (val.subscription_id == null) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "订阅必须填写订阅编号",
|
||||
path: ["subscription_id"],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (val.quota_granted == null) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "必须填写本笔额度",
|
||||
path: ["quota_granted"],
|
||||
});
|
||||
}
|
||||
quota_granted: z.number().int().positive(),
|
||||
paid_amount: z.number().nullable().optional(),
|
||||
paid_at: z.string().min(1).nullable().optional(),
|
||||
remark: z.string().default(""),
|
||||
});
|
||||
|
||||
export const createTopupBody = z.object({
|
||||
upstream_id: z.number().int().nullable().optional(),
|
||||
amount: z.number().positive(),
|
||||
money: z.number().positive(),
|
||||
trade_no: z.string().nullable().optional(),
|
||||
payment_method: z.string().nullable().optional(),
|
||||
payment_provider: z.string().nullable().optional(),
|
||||
status: z.string().nullable().optional(),
|
||||
paid_at: z.string().min(1),
|
||||
remark: z.string().default(""),
|
||||
});
|
||||
|
||||
export {
|
||||
setupBody as setupBodySchema,
|
||||
loginBody as loginBodySchema,
|
||||
@@ -236,7 +239,10 @@ export {
|
||||
patchKeyBody as patchKeyBodySchema,
|
||||
createMemberBody as createMemberBodySchema,
|
||||
patchMemberBody as patchMemberBodySchema,
|
||||
createPaymentBody as createPaymentBodySchema,
|
||||
createSubscriptionBody as createSubscriptionBodySchema,
|
||||
createTopupBody as createTopupBodySchema,
|
||||
subscriptionItem as subscriptionItemSchema,
|
||||
topupItem as topupItemSchema,
|
||||
timeRangeQuery as timeRangeQuerySchema,
|
||||
pageQuery as pageQuerySchema,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ export type Quota = number;
|
||||
export type QuotaUsd = string;
|
||||
|
||||
export type BillingSource = "subscription" | "wallet";
|
||||
export type PaymentKind = "subscription" | "topup";
|
||||
export type KeyStatus = "active" | "disabled";
|
||||
export type MemberStatus = "active" | "disabled";
|
||||
|
||||
@@ -57,17 +56,35 @@ export interface KeyItem {
|
||||
expires_at: UnixSec | null;
|
||||
}
|
||||
|
||||
export interface PaymentItem {
|
||||
/** 订阅权益登记 */
|
||||
export interface SubscriptionItem {
|
||||
id: number;
|
||||
kind: PaymentKind;
|
||||
paid_at: UnixSec;
|
||||
paid_amount: PaidAmount;
|
||||
remark: string;
|
||||
period_start: UnixSec | null;
|
||||
subscription_id: number;
|
||||
period_start: UnixSec;
|
||||
period_end: UnixSec | null;
|
||||
subscription_id: number | null;
|
||||
quota_granted: number | null;
|
||||
quota_granted: Quota;
|
||||
quota_usd: QuotaUsd;
|
||||
paid_amount: PaidAmount | null;
|
||||
paid_at: UnixSec | null;
|
||||
remark: string;
|
||||
created_at: UnixSec;
|
||||
}
|
||||
|
||||
/** 充值入账(对齐上游充值单;无 quota_granted 入库列) */
|
||||
export interface TopupItem {
|
||||
id: number;
|
||||
upstream_id: number | null;
|
||||
amount: number;
|
||||
money: PaidAmount;
|
||||
trade_no: string | null;
|
||||
payment_method: string | null;
|
||||
payment_provider: string | null;
|
||||
status: string | null;
|
||||
paid_at: UnixSec;
|
||||
quota_derived: Quota;
|
||||
quota_usd: QuotaUsd;
|
||||
remark: string;
|
||||
created_at: UnixSec;
|
||||
}
|
||||
|
||||
export interface MemberItem {
|
||||
|
||||
@@ -241,3 +241,76 @@ export async function fetchPlatformSubscriptions(
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** 平台充值单(GET /api/user/topup/self 单页) */
|
||||
export interface PlatformTopup {
|
||||
id: number;
|
||||
user_id?: number;
|
||||
amount: number;
|
||||
money: number;
|
||||
trade_no?: string;
|
||||
payment_method?: string;
|
||||
payment_provider?: string;
|
||||
create_time?: number;
|
||||
complete_time?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/** 用控制台 access_token 分页拉充值单(仅 p / page_size,无时间参数) */
|
||||
export async function fetchPlatformTopups(
|
||||
baseUrl: string,
|
||||
accessToken: string,
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
timeoutMs = 8_000,
|
||||
): Promise<
|
||||
UpstreamResult<{
|
||||
items: PlatformTopup[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}>
|
||||
> {
|
||||
const ac = new AbortController();
|
||||
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
||||
try {
|
||||
const full = `${joinUrl(baseUrl, "/api/user/topup/self")}?p=${page}&page_size=${pageSize}`;
|
||||
const res = await fetch(full, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: ac.signal,
|
||||
});
|
||||
if (res.status === 401) return { ok: false, kind: "rejected" };
|
||||
if (!res.ok) return { ok: false, kind: "http", status: res.status };
|
||||
const body = (await res.json()) as {
|
||||
code?: boolean | number;
|
||||
success?: boolean;
|
||||
data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
total?: number;
|
||||
items?: PlatformTopup[];
|
||||
};
|
||||
};
|
||||
if (body.code === false || body.success === false) {
|
||||
return { ok: false, kind: "rejected" };
|
||||
}
|
||||
const data = body.data ?? {};
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
items: data.items ?? [],
|
||||
total: data.total ?? 0,
|
||||
page: data.page ?? page,
|
||||
page_size: data.page_size ?? pageSize,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { ok: false, kind: "unreachable" };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user