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
+71 -23
View File
@@ -4,17 +4,19 @@ import {
auditLogs,
engineStatus,
members,
payments,
subscriptions,
qpuHistory,
sessions,
topups,
usageLogs,
users,
type ApiKeyRow,
type AuditLogRow,
type EngineStatusRow,
type MemberRow,
type PaymentRow,
type SubscriptionRow,
type SessionRow,
type TopupRow,
type UsageLogRow,
type UserRow,
} from "./schema.ts";
@@ -384,41 +386,87 @@ export function createRepos(db: Db) {
},
},
payments: {
async listAll(): Promise<PaymentRow[]> {
return db.select().from(payments).orderBy(desc(payments.paid_at));
subscriptions: {
async listAll(): Promise<SubscriptionRow[]> {
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
.select()
.from(payments)
.where(eq(payments.id, id))
.from(subscriptions)
.where(eq(subscriptions.id, id))
.limit(1);
return rows[0] ?? null;
},
async insert(values: {
kind: string;
paid_at: Date;
paid_amount: number;
remark: string;
period_start: Date | null;
period_end: Date | null;
subscription_id: number | null;
quota_granted: number | null;
}): Promise<PaymentRow> {
const [row] = await db.insert(payments).values(values).returning();
async findBySubscriptionId(
subscriptionId: number,
): Promise<SubscriptionRow | null> {
const rows = await db
.select()
.from(subscriptions)
.where(eq(subscriptions.subscription_id, subscriptionId))
.limit(1);
return rows[0] ?? null;
},
async insert(
values: typeof subscriptions.$inferInsert,
): Promise<SubscriptionRow> {
const [row] = await db.insert(subscriptions).values(values).returning();
return row!;
},
async update(id: number, patch: Partial<typeof payments.$inferInsert>) {
async update(
id: number,
patch: Partial<typeof subscriptions.$inferInsert>,
) {
const [row] = await db
.update(payments)
.update(subscriptions)
.set(patch)
.where(eq(payments.id, id))
.where(eq(subscriptions.id, id))
.returning();
return row!;
},
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(),
subscription_id: bigint("subscription_id", { mode: "number" }).notNull().unique(),
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", {
precision: 18,
scale: 2,
mode: "number",
}),
paid_at: timestamp("paid_at", { withTimezone: true, mode: "date" }),
remark: text("remark").notNull().default(""),
created_at: timestamp("created_at", {
withTimezone: true,
mode: "date",
})
.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(),
kind: text("kind").notNull(),
paid_at: timestamp("paid_at", { withTimezone: true, mode: "date" }).notNull(),
paid_amount: numeric("paid_amount", {
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(""),
period_start: timestamp("period_start", { withTimezone: true, mode: "date" }),
period_end: timestamp("period_end", { withTimezone: true, mode: "date" }),
subscription_id: bigint("subscription_id", { mode: "number" }),
quota_granted: bigint("quota_granted", { mode: "number" }),
created_at: timestamp("created_at", {
withTimezone: true,
mode: "date",
})
.notNull()
.defaultNow(),
});
export const qpuHistory = pgTable("qpu_history", {
@@ -207,7 +244,8 @@ export const engineStatus = pgTable("engine_status", {
export type MemberRow = typeof members.$inferSelect;
export type ApiKeyRow = typeof apiKeys.$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 UserRow = typeof users.$inferSelect;
export type SessionRow = typeof sessions.$inferSelect;