feat(db): payments 拆为 subscriptions 与 topups

订阅权益与充值入账分表;topups 对齐上游充值单(无 quota_granted),
paid_at=complete_time;quota_derived 查询层派生。
This commit is contained in:
2026-10-09 22:02:10 +08:00
parent d4d740222e
commit c7cc6f6dfd
9 changed files with 1474 additions and 84 deletions
+3
View File
@@ -27,6 +27,9 @@ export const ERR = {
errorBody("invalid_credentials", "用户名或密码错误"), errorBody("invalid_credentials", "用户名或密码错误"),
session_expired: () => session_expired: () =>
errorBody("session_expired", "会话已过期,请重新登录"), errorBody("session_expired", "会话已过期,请重新登录"),
/** 上游控制台 token 失效——**不是**管理端会话,禁止走 401 登出 */
platform_token_invalid: () =>
errorBody("platform_token_invalid", "平台登录已失效,请重新登录上游账号"),
already_initialized: () => already_initialized: () =>
errorBody("already_initialized", "系统已初始化,无法重复引导"), errorBody("already_initialized", "系统已初始化,无法重复引导"),
key_already_registered: (key_id: number) => key_already_registered: (key_id: number) =>
+49 -43
View File
@@ -130,17 +130,33 @@ export const memberMe = z.object({
name: z.string(), name: z.string(),
}); });
export const paymentItem = z.object({ export const subscriptionItem = z.object({
id: z.number().int(), id: z.number().int(),
kind: z.enum(["subscription", "topup"]), subscription_id: z.number().int(),
paid_at: z.number().int(), period_start: z.number().int(),
paid_amount: z.number(),
remark: z.string(),
period_start: z.number().int().nullable(),
period_end: z.number().int().nullable(), period_end: z.number().int().nullable(),
subscription_id: z.number().int().nullable(), quota_granted: z.number().int(),
quota_granted: z.number().int().nullable(),
quota_usd: z.string(), 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({ export const engineStatus = z.object({
@@ -188,46 +204,33 @@ export const memberOverview = z.object({
usage: usageTotals, 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({ .object({
kind: z.enum(["subscription", "topup"]), subscription_id: z.number().int().positive(),
paid_at: z.string().min(1), period_start: z.string().min(1),
paid_amount: z.number(),
remark: z.string().default(""),
period_start: z.string().min(1).nullable().optional(),
period_end: z.string().min(1).nullable().optional(), period_end: z.string().min(1).nullable().optional(),
subscription_id: z.number().int().positive().nullable().optional(), quota_granted: z.number().int().positive(),
quota_granted: z.number().int().nullable().optional(), paid_amount: z.number().nullable().optional(),
}) paid_at: z.string().min(1).nullable().optional(),
.superRefine((val, ctx) => { remark: z.string().default(""),
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"],
});
}
}); });
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 { export {
setupBody as setupBodySchema, setupBody as setupBodySchema,
loginBody as loginBodySchema, loginBody as loginBodySchema,
@@ -236,7 +239,10 @@ export {
patchKeyBody as patchKeyBodySchema, patchKeyBody as patchKeyBodySchema,
createMemberBody as createMemberBodySchema, createMemberBody as createMemberBodySchema,
patchMemberBody as patchMemberBodySchema, patchMemberBody as patchMemberBodySchema,
createPaymentBody as createPaymentBodySchema, createSubscriptionBody as createSubscriptionBodySchema,
createTopupBody as createTopupBodySchema,
subscriptionItem as subscriptionItemSchema,
topupItem as topupItemSchema,
timeRangeQuery as timeRangeQuerySchema, timeRangeQuery as timeRangeQuerySchema,
pageQuery as pageQuerySchema, pageQuery as pageQuerySchema,
}; };
+26 -9
View File
@@ -11,7 +11,6 @@ export type Quota = number;
export type QuotaUsd = string; export type QuotaUsd = string;
export type BillingSource = "subscription" | "wallet"; export type BillingSource = "subscription" | "wallet";
export type PaymentKind = "subscription" | "topup";
export type KeyStatus = "active" | "disabled"; export type KeyStatus = "active" | "disabled";
export type MemberStatus = "active" | "disabled"; export type MemberStatus = "active" | "disabled";
@@ -57,17 +56,35 @@ export interface KeyItem {
expires_at: UnixSec | null; expires_at: UnixSec | null;
} }
export interface PaymentItem { /** 订阅权益登记 */
export interface SubscriptionItem {
id: number; id: number;
kind: PaymentKind; subscription_id: number;
paid_at: UnixSec; period_start: UnixSec;
paid_amount: PaidAmount;
remark: string;
period_start: UnixSec | null;
period_end: UnixSec | null; period_end: UnixSec | null;
subscription_id: number | null; quota_granted: Quota;
quota_granted: number | null;
quota_usd: QuotaUsd; 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 { export interface MemberItem {
+73
View File
@@ -241,3 +241,76 @@ export async function fetchPlatformSubscriptions(
clearTimeout(timer); 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);
}
}
@@ -0,0 +1,138 @@
CREATE TABLE "api_keys" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "api_keys_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"key_hash" text NOT NULL,
"key_ciphertext" "bytea" NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"member_id" bigint,
"registered_at" timestamp with time zone DEFAULT now() NOT NULL,
"log_synced_at" timestamp with time zone,
"usage_synced_at" timestamp with time zone,
"token_name" text DEFAULT '' NOT NULL,
"total_granted" bigint DEFAULT 0 NOT NULL,
"total_used" bigint DEFAULT 0 NOT NULL,
"total_available" bigint DEFAULT 0 NOT NULL,
"unlimited_quota" boolean DEFAULT false NOT NULL,
"model_limits" jsonb,
"model_limits_enabled" boolean DEFAULT false NOT NULL,
"expires_at" timestamp with time zone,
CONSTRAINT "api_keys_key_hash_unique" UNIQUE("key_hash")
);
--> statement-breakpoint
CREATE TABLE "audit_logs" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "audit_logs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"operator" text,
"domain" text NOT NULL,
"action" text NOT NULL,
"summary" text DEFAULT '' NOT NULL
);
--> statement-breakpoint
CREATE TABLE "engine_status" (
"id" integer PRIMARY KEY DEFAULT 1 NOT NULL,
"running" boolean DEFAULT false NOT NULL,
"sync_interval_ms" bigint DEFAULT 300000 NOT NULL,
"last_round" jsonb,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "members" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "members_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"name" text NOT NULL,
"remark" text DEFAULT '' NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"token_hash" text NOT NULL,
"token_issued_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "members_name_unique" UNIQUE("name"),
CONSTRAINT "members_token_hash_unique" UNIQUE("token_hash")
);
--> statement-breakpoint
CREATE TABLE "qpu_history" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "qpu_history_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"qpu" numeric(18, 6) NOT NULL,
"effective_from" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sessions" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "sessions_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"user_id" bigint NOT NULL,
"token_hash" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "sessions_token_hash_unique" UNIQUE("token_hash")
);
--> statement-breakpoint
CREATE TABLE "subscriptions" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "subscriptions_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"subscription_id" bigint NOT NULL,
"period_start" timestamp with time zone NOT NULL,
"period_end" timestamp with time zone,
"quota_granted" bigint NOT NULL,
"paid_amount" numeric(18, 2),
"paid_at" timestamp with time zone,
"remark" text DEFAULT '' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "subscriptions_subscription_id_unique" UNIQUE("subscription_id")
);
--> statement-breakpoint
CREATE TABLE "topups" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "topups_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"upstream_id" bigint,
"amount" numeric(18, 6) NOT NULL,
"money" numeric(18, 2) NOT NULL,
"trade_no" text,
"payment_method" text,
"payment_provider" text,
"status" text,
"paid_at" timestamp with time zone NOT NULL,
"remark" text DEFAULT '' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "usage_logs" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "usage_logs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"api_key_id" bigint NOT NULL,
"dedupe_key" text NOT NULL,
"billing_source" text,
"subscription_id" bigint,
"user_id" bigint DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone NOT NULL,
"type" smallint DEFAULT 2 NOT NULL,
"content" text DEFAULT '' NOT NULL,
"username" text DEFAULT '' NOT NULL,
"token_name" text DEFAULT '' NOT NULL,
"model_name" text DEFAULT '' NOT NULL,
"quota" bigint DEFAULT 0 NOT NULL,
"prompt_tokens" integer DEFAULT 0 NOT NULL,
"completion_tokens" integer DEFAULT 0 NOT NULL,
"use_time" integer DEFAULT 0 NOT NULL,
"is_stream" boolean DEFAULT false NOT NULL,
"channel" integer DEFAULT 0 NOT NULL,
"channel_name" text DEFAULT '' NOT NULL,
"token_id" bigint DEFAULT 0 NOT NULL,
"group_name" text DEFAULT '' NOT NULL,
"ip" text DEFAULT '' NOT NULL,
"request_id" text DEFAULT '' NOT NULL,
"upstream_request_id" text,
"other" jsonb,
CONSTRAINT "usage_logs_dedupe_key_unique" UNIQUE("dedupe_key")
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"username" text NOT NULL,
"password_hash" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "users_username_unique" UNIQUE("username")
);
--> statement-breakpoint
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_member_id_members_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."members"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "usage_logs" ADD CONSTRAINT "usage_logs_api_key_id_api_keys_id_fk" FOREIGN KEY ("api_key_id") REFERENCES "public"."api_keys"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "api_keys_member_id_idx" ON "api_keys" USING btree ("member_id");--> statement-breakpoint
CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "subscriptions_period_idx" ON "subscriptions" USING btree ("period_start","period_end");--> statement-breakpoint
CREATE INDEX "usage_logs_created_at_idx" ON "usage_logs" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "usage_logs_api_key_created_idx" ON "usage_logs" USING btree ("api_key_id","created_at");--> statement-breakpoint
CREATE INDEX "usage_logs_subscription_idx" ON "usage_logs" USING btree ("subscription_id");
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1791551016051,
"tag": "0000_loose_goblin_queen",
"breakpoints": true
}
]
}
+71 -23
View File
@@ -4,17 +4,19 @@ import {
auditLogs, auditLogs,
engineStatus, engineStatus,
members, members,
payments, subscriptions,
qpuHistory, qpuHistory,
sessions, sessions,
topups,
usageLogs, usageLogs,
users, users,
type ApiKeyRow, type ApiKeyRow,
type AuditLogRow, type AuditLogRow,
type EngineStatusRow, type EngineStatusRow,
type MemberRow, type MemberRow,
type PaymentRow, type SubscriptionRow,
type SessionRow, type SessionRow,
type TopupRow,
type UsageLogRow, type UsageLogRow,
type UserRow, type UserRow,
} from "./schema.ts"; } from "./schema.ts";
@@ -384,41 +386,87 @@ export function createRepos(db: Db) {
}, },
}, },
payments: { subscriptions: {
async listAll(): Promise<PaymentRow[]> { async listAll(): Promise<SubscriptionRow[]> {
return db.select().from(payments).orderBy(desc(payments.paid_at)); return db
.select()
.from(subscriptions)
.orderBy(desc(subscriptions.period_start));
}, },
async findById(id: number): Promise<PaymentRow | null> { async findById(id: number): Promise<SubscriptionRow | null> {
const rows = await db const rows = await db
.select() .select()
.from(payments) .from(subscriptions)
.where(eq(payments.id, id)) .where(eq(subscriptions.id, id))
.limit(1); .limit(1);
return rows[0] ?? null; return rows[0] ?? null;
}, },
async insert(values: { async findBySubscriptionId(
kind: string; subscriptionId: number,
paid_at: Date; ): Promise<SubscriptionRow | null> {
paid_amount: number; const rows = await db
remark: string; .select()
period_start: Date | null; .from(subscriptions)
period_end: Date | null; .where(eq(subscriptions.subscription_id, subscriptionId))
subscription_id: number | null; .limit(1);
quota_granted: number | null; return rows[0] ?? null;
}): Promise<PaymentRow> { },
const [row] = await db.insert(payments).values(values).returning(); async insert(
values: typeof subscriptions.$inferInsert,
): Promise<SubscriptionRow> {
const [row] = await db.insert(subscriptions).values(values).returning();
return row!; return row!;
}, },
async update(id: number, patch: Partial<typeof payments.$inferInsert>) { async update(
id: number,
patch: Partial<typeof subscriptions.$inferInsert>,
) {
const [row] = await db const [row] = await db
.update(payments) .update(subscriptions)
.set(patch) .set(patch)
.where(eq(payments.id, id)) .where(eq(subscriptions.id, id))
.returning(); .returning();
return row!; return row!;
}, },
async remove(id: number): Promise<void> { async remove(id: number): Promise<void> {
await db.delete(payments).where(eq(payments.id, id)); await db.delete(subscriptions).where(eq(subscriptions.id, id));
},
},
topups: {
async listAll(): Promise<TopupRow[]> {
return db.select().from(topups).orderBy(desc(topups.paid_at));
},
async findById(id: number): Promise<TopupRow | null> {
const rows = await db
.select()
.from(topups)
.where(eq(topups.id, id))
.limit(1);
return rows[0] ?? null;
},
async findByUpstreamId(upstreamId: number): Promise<TopupRow | null> {
const rows = await db
.select()
.from(topups)
.where(eq(topups.upstream_id, upstreamId))
.limit(1);
return rows[0] ?? null;
},
async insert(values: typeof topups.$inferInsert): Promise<TopupRow> {
const [row] = await db.insert(topups).values(values).returning();
return row!;
},
async update(id: number, patch: Partial<typeof topups.$inferInsert>) {
const [row] = await db
.update(topups)
.set(patch)
.where(eq(topups.id, id))
.returning();
return row!;
},
async remove(id: number): Promise<void> {
await db.delete(topups).where(eq(topups.id, id));
}, },
}, },
+47 -9
View File
@@ -117,20 +117,57 @@ export const usageLogs = pgTable(
], ],
); );
export const payments = pgTable("payments", { /** 订阅权益登记(原 payments.kind=subscription) */
export const subscriptions = pgTable(
"subscriptions",
{
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(), id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
kind: text("kind").notNull(), subscription_id: bigint("subscription_id", { mode: "number" }).notNull().unique(),
paid_at: timestamp("paid_at", { withTimezone: true, mode: "date" }).notNull(), period_start: timestamp("period_start", {
withTimezone: true,
mode: "date",
}).notNull(),
period_end: timestamp("period_end", { withTimezone: true, mode: "date" }),
quota_granted: bigint("quota_granted", { mode: "number" }).notNull(),
paid_amount: numeric("paid_amount", { paid_amount: numeric("paid_amount", {
precision: 18, precision: 18,
scale: 2, scale: 2,
mode: "number", mode: "number",
}).notNull(), }),
paid_at: timestamp("paid_at", { withTimezone: true, mode: "date" }),
remark: text("remark").notNull().default(""), remark: text("remark").notNull().default(""),
period_start: timestamp("period_start", { withTimezone: true, mode: "date" }), created_at: timestamp("created_at", {
period_end: timestamp("period_end", { withTimezone: true, mode: "date" }), withTimezone: true,
subscription_id: bigint("subscription_id", { mode: "number" }), mode: "date",
quota_granted: bigint("quota_granted", { mode: "number" }), })
.notNull()
.defaultNow(),
},
(t) => [index("subscriptions_period_idx").on(t.period_start, t.period_end)],
);
/** 充值入账事件(原 payments.kind=topup;无 quota_granted 列) */
export const topups = pgTable("topups", {
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
upstream_id: bigint("upstream_id", { mode: "number" }),
amount: numeric("amount", { precision: 18, scale: 6, mode: "number" }).notNull(),
money: numeric("money", {
precision: 18,
scale: 2,
mode: "number",
}).notNull(),
trade_no: text("trade_no"),
payment_method: text("payment_method"),
payment_provider: text("payment_provider"),
status: text("status"),
paid_at: timestamp("paid_at", { withTimezone: true, mode: "date" }).notNull(),
remark: text("remark").notNull().default(""),
created_at: timestamp("created_at", {
withTimezone: true,
mode: "date",
})
.notNull()
.defaultNow(),
}); });
export const qpuHistory = pgTable("qpu_history", { export const qpuHistory = pgTable("qpu_history", {
@@ -207,7 +244,8 @@ export const engineStatus = pgTable("engine_status", {
export type MemberRow = typeof members.$inferSelect; export type MemberRow = typeof members.$inferSelect;
export type ApiKeyRow = typeof apiKeys.$inferSelect; export type ApiKeyRow = typeof apiKeys.$inferSelect;
export type UsageLogRow = typeof usageLogs.$inferSelect; export type UsageLogRow = typeof usageLogs.$inferSelect;
export type PaymentRow = typeof payments.$inferSelect; export type SubscriptionRow = typeof subscriptions.$inferSelect;
export type TopupRow = typeof topups.$inferSelect;
export type QpuHistoryRow = typeof qpuHistory.$inferSelect; export type QpuHistoryRow = typeof qpuHistory.$inferSelect;
export type UserRow = typeof users.$inferSelect; export type UserRow = typeof users.$inferSelect;
export type SessionRow = typeof sessions.$inferSelect; export type SessionRow = typeof sessions.$inferSelect;