feat(core,db): 业务核心与数据访问层
core:DTO/错误信封/时间口径/微分折算/dedupe_key/加密与 PBKDF2/契约 zod/logtape/上游客户端。 db:八表 Drizzle schema + 仓储 + drizzle 配置。
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: "./src/schema.ts",
|
||||
out: "./drizzle",
|
||||
dbCredentials: {
|
||||
url:
|
||||
process.env.DATABASE_URL ??
|
||||
"postgres://postgres:postgres@127.0.0.1:5432/new_api_key_usage",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@new-api-key-usage/db",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.45.3",
|
||||
"postgres": "^3.4.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.23.1",
|
||||
"drizzle-kit": "^0.31.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema.ts";
|
||||
|
||||
export function createDb(connectionString: string) {
|
||||
const client = postgres(connectionString, { max: 10 });
|
||||
return drizzle(client, { schema });
|
||||
}
|
||||
|
||||
export type Db = ReturnType<typeof createDb>;
|
||||
|
||||
export * from "./schema.ts";
|
||||
export * from "./repos.ts";
|
||||
@@ -0,0 +1,575 @@
|
||||
import type { Db } from "./index.ts";
|
||||
import {
|
||||
apiKeys,
|
||||
auditLogs,
|
||||
members,
|
||||
payments,
|
||||
qpuHistory,
|
||||
sessions,
|
||||
usageLogs,
|
||||
users,
|
||||
type ApiKeyRow,
|
||||
type AuditLogRow,
|
||||
type MemberRow,
|
||||
type PaymentRow,
|
||||
type SessionRow,
|
||||
type UsageLogRow,
|
||||
type UserRow,
|
||||
} from "./schema.ts";
|
||||
import { and, asc, count, desc, eq, gt, gte, lt, or, sql } from "drizzle-orm";
|
||||
|
||||
export interface SessionUser {
|
||||
user_id: number;
|
||||
username: string;
|
||||
expires_at: Date;
|
||||
}
|
||||
|
||||
export interface KeyListRow extends ApiKeyRow {
|
||||
member_name: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓储层(R14):数据访问只出现在 packages/db。
|
||||
* apps/server 不 import drizzle-orm / postgres 驱动。
|
||||
*/
|
||||
export function createRepos(db: Db) {
|
||||
return {
|
||||
users: {
|
||||
async countAll(): Promise<number> {
|
||||
const rows = await db.select({ id: users.id }).from(users).limit(1);
|
||||
return rows[0] ? 1 : 0;
|
||||
},
|
||||
async findByUsername(username: string): Promise<UserRow | null> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async findById(id: number): Promise<UserRow | null> {
|
||||
const rows = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async insert(username: string, password_hash: string): Promise<UserRow> {
|
||||
const [row] = await db
|
||||
.insert(users)
|
||||
.values({ username, password_hash })
|
||||
.returning();
|
||||
return row!;
|
||||
},
|
||||
async updatePassword(id: number, password_hash: string): Promise<void> {
|
||||
await db.update(users).set({ password_hash }).where(eq(users.id, id));
|
||||
},
|
||||
},
|
||||
|
||||
sessions: {
|
||||
async findValid(
|
||||
token_hash: string,
|
||||
now: Date,
|
||||
): Promise<SessionUser | null> {
|
||||
const rows = await db
|
||||
.select({
|
||||
user_id: sessions.user_id,
|
||||
username: users.username,
|
||||
expires_at: sessions.expires_at,
|
||||
})
|
||||
.from(sessions)
|
||||
.innerJoin(users, eq(sessions.user_id, users.id))
|
||||
.where(
|
||||
and(eq(sessions.token_hash, token_hash), gt(sessions.expires_at, now)),
|
||||
)
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async insert(
|
||||
user_id: number,
|
||||
token_hash: string,
|
||||
expires_at: Date,
|
||||
): Promise<SessionRow> {
|
||||
const [row] = await db
|
||||
.insert(sessions)
|
||||
.values({ user_id, token_hash, expires_at })
|
||||
.returning();
|
||||
return row!;
|
||||
},
|
||||
async deleteByTokenHash(token_hash: string): Promise<void> {
|
||||
await db.delete(sessions).where(eq(sessions.token_hash, token_hash));
|
||||
},
|
||||
async listByUser(user_id: number) {
|
||||
return db
|
||||
.select({ id: sessions.id, token_hash: sessions.token_hash })
|
||||
.from(sessions)
|
||||
.where(eq(sessions.user_id, user_id));
|
||||
},
|
||||
async deleteById(id: number): Promise<void> {
|
||||
await db.delete(sessions).where(eq(sessions.id, id));
|
||||
},
|
||||
},
|
||||
|
||||
keys: {
|
||||
async listAll(filter?: {
|
||||
member_id?: number | "unassigned";
|
||||
status?: string;
|
||||
}): Promise<KeyListRow[]> {
|
||||
const conds = [];
|
||||
if (filter?.member_id === "unassigned") {
|
||||
conds.push(sql`${apiKeys.member_id} is null`);
|
||||
} else if (typeof filter?.member_id === "number") {
|
||||
conds.push(eq(apiKeys.member_id, filter.member_id));
|
||||
}
|
||||
if (filter?.status) {
|
||||
conds.push(eq(apiKeys.status, filter.status));
|
||||
}
|
||||
const base = db
|
||||
.select({
|
||||
id: apiKeys.id,
|
||||
key_hash: apiKeys.key_hash,
|
||||
key_ciphertext: apiKeys.key_ciphertext,
|
||||
status: apiKeys.status,
|
||||
member_id: apiKeys.member_id,
|
||||
registered_at: apiKeys.registered_at,
|
||||
log_synced_at: apiKeys.log_synced_at,
|
||||
usage_synced_at: apiKeys.usage_synced_at,
|
||||
token_name: apiKeys.token_name,
|
||||
total_granted: apiKeys.total_granted,
|
||||
total_used: apiKeys.total_used,
|
||||
total_available: apiKeys.total_available,
|
||||
unlimited_quota: apiKeys.unlimited_quota,
|
||||
model_limits: apiKeys.model_limits,
|
||||
model_limits_enabled: apiKeys.model_limits_enabled,
|
||||
expires_at: apiKeys.expires_at,
|
||||
member_name: members.name,
|
||||
})
|
||||
.from(apiKeys)
|
||||
.leftJoin(members, eq(apiKeys.member_id, members.id));
|
||||
if (conds.length > 0) {
|
||||
return base.where(and(...conds));
|
||||
}
|
||||
return base;
|
||||
},
|
||||
async findById(id: number): Promise<ApiKeyRow | null> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.id, id))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async findByHash(key_hash: string): Promise<{ id: number } | null> {
|
||||
const rows = await db
|
||||
.select({ id: apiKeys.id })
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.key_hash, key_hash))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async insert(values: {
|
||||
key_hash: string;
|
||||
key_ciphertext: Uint8Array;
|
||||
member_id: number | null;
|
||||
token_name?: string;
|
||||
}): Promise<ApiKeyRow> {
|
||||
const [row] = await db
|
||||
.insert(apiKeys)
|
||||
.values({
|
||||
key_hash: values.key_hash,
|
||||
key_ciphertext: values.key_ciphertext,
|
||||
member_id: values.member_id,
|
||||
token_name: values.token_name ?? "",
|
||||
})
|
||||
.returning();
|
||||
return row!;
|
||||
},
|
||||
async update(
|
||||
id: number,
|
||||
patch: Partial<typeof apiKeys.$inferInsert>,
|
||||
): Promise<ApiKeyRow> {
|
||||
const [row] = await db
|
||||
.update(apiKeys)
|
||||
.set(patch)
|
||||
.where(eq(apiKeys.id, id))
|
||||
.returning();
|
||||
return row!;
|
||||
},
|
||||
},
|
||||
|
||||
members: {
|
||||
async listAll(): Promise<MemberRow[]> {
|
||||
return db.select().from(members);
|
||||
},
|
||||
async findById(id: number): Promise<MemberRow | null> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(members)
|
||||
.where(eq(members.id, id))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async findByName(name: string): Promise<{ id: number } | null> {
|
||||
const rows = await db
|
||||
.select({ id: members.id })
|
||||
.from(members)
|
||||
.where(eq(members.name, name))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async findByTokenHash(token_hash: string): Promise<{
|
||||
id: number;
|
||||
status: string;
|
||||
} | null> {
|
||||
const rows = await db
|
||||
.select({ id: members.id, status: members.status })
|
||||
.from(members)
|
||||
.where(eq(members.token_hash, token_hash))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
async insert(values: {
|
||||
name: string;
|
||||
remark: string;
|
||||
token_hash: string;
|
||||
token_issued_at: Date;
|
||||
}): Promise<MemberRow> {
|
||||
const [row] = await db.insert(members).values(values).returning();
|
||||
return row!;
|
||||
},
|
||||
async updateStatus(id: number, status: string): Promise<MemberRow> {
|
||||
const [row] = await db
|
||||
.update(members)
|
||||
.set({ status })
|
||||
.where(eq(members.id, id))
|
||||
.returning();
|
||||
return row!;
|
||||
},
|
||||
async reissueToken(
|
||||
id: number,
|
||||
token_hash: string,
|
||||
token_issued_at: Date,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(members)
|
||||
.set({ token_hash, token_issued_at })
|
||||
.where(eq(members.id, id));
|
||||
},
|
||||
async countKeys(member_id: number): Promise<number> {
|
||||
const rows = await db
|
||||
.select({ n: count() })
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.member_id, member_id));
|
||||
return rows[0]?.n ?? 0;
|
||||
},
|
||||
},
|
||||
|
||||
audit: {
|
||||
async insert(values: {
|
||||
operator: string | null;
|
||||
domain: string;
|
||||
action: string;
|
||||
summary: string;
|
||||
}): Promise<AuditLogRow> {
|
||||
const [row] = await db.insert(auditLogs).values(values).returning();
|
||||
return row!;
|
||||
},
|
||||
async listPage(params: {
|
||||
domain?: string;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}): Promise<{ rows: AuditLogRow[]; total: number }> {
|
||||
const conds = params.domain ? [eq(auditLogs.domain, params.domain)] : [];
|
||||
const totalRows = await db
|
||||
.select({ n: count() })
|
||||
.from(auditLogs)
|
||||
.where(conds.length ? and(...conds) : undefined);
|
||||
const total = totalRows[0]?.n ?? 0;
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(auditLogs)
|
||||
.where(conds.length ? and(...conds) : undefined)
|
||||
.orderBy(desc(auditLogs.created_at), desc(auditLogs.id))
|
||||
.limit(params.page_size)
|
||||
.offset((params.page - 1) * params.page_size);
|
||||
return { rows, total };
|
||||
},
|
||||
},
|
||||
|
||||
payments: {
|
||||
async listAll(): Promise<PaymentRow[]> {
|
||||
return db.select().from(payments).orderBy(desc(payments.paid_at));
|
||||
},
|
||||
async findById(id: number): Promise<PaymentRow | null> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq(payments.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();
|
||||
return row!;
|
||||
},
|
||||
async update(id: number, patch: Partial<typeof payments.$inferInsert>) {
|
||||
const [row] = await db
|
||||
.update(payments)
|
||||
.set(patch)
|
||||
.where(eq(payments.id, id))
|
||||
.returning();
|
||||
return row!;
|
||||
},
|
||||
async remove(id: number): Promise<void> {
|
||||
await db.delete(payments).where(eq(payments.id, id));
|
||||
},
|
||||
},
|
||||
|
||||
qpu: {
|
||||
async listTimeline(): Promise<
|
||||
Array<{ qpu: number; effective_from: Date }>
|
||||
> {
|
||||
return db
|
||||
.select({
|
||||
qpu: qpuHistory.qpu,
|
||||
effective_from: qpuHistory.effective_from,
|
||||
})
|
||||
.from(qpuHistory)
|
||||
.orderBy(asc(qpuHistory.effective_from));
|
||||
},
|
||||
async latest(): Promise<{ qpu: number; effective_from: Date } | null> {
|
||||
const rows = await db
|
||||
.select({
|
||||
qpu: qpuHistory.qpu,
|
||||
effective_from: qpuHistory.effective_from,
|
||||
})
|
||||
.from(qpuHistory)
|
||||
.orderBy(desc(qpuHistory.effective_from))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
},
|
||||
|
||||
logs: {
|
||||
/** 明细分页(时间范围左闭右开) */
|
||||
async listPage(params: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
api_key_id?: number;
|
||||
member_id?: number;
|
||||
billing_source?: string;
|
||||
subscription_id?: number;
|
||||
type?: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}): Promise<{ rows: Array<UsageLogRow & { token_name: string }>; total: number }> {
|
||||
const conds = [
|
||||
gte(usageLogs.created_at, params.from),
|
||||
lt(usageLogs.created_at, params.to),
|
||||
];
|
||||
if (params.api_key_id != null) {
|
||||
conds.push(eq(usageLogs.api_key_id, params.api_key_id));
|
||||
}
|
||||
if (params.billing_source) {
|
||||
conds.push(eq(usageLogs.billing_source, params.billing_source));
|
||||
}
|
||||
if (params.subscription_id != null) {
|
||||
conds.push(eq(usageLogs.subscription_id, params.subscription_id));
|
||||
}
|
||||
if (params.type != null) {
|
||||
conds.push(eq(usageLogs.type, params.type));
|
||||
}
|
||||
if (params.member_id != null) {
|
||||
conds.push(eq(apiKeys.member_id, params.member_id));
|
||||
}
|
||||
|
||||
const totalRows = await db
|
||||
.select({ n: count() })
|
||||
.from(usageLogs)
|
||||
.leftJoin(apiKeys, eq(usageLogs.api_key_id, apiKeys.id))
|
||||
.where(and(...conds));
|
||||
const total = totalRows[0]?.n ?? 0;
|
||||
|
||||
const offset = (params.page - 1) * params.page_size;
|
||||
const rows = await db
|
||||
.select({
|
||||
id: usageLogs.id,
|
||||
api_key_id: usageLogs.api_key_id,
|
||||
dedupe_key: usageLogs.dedupe_key,
|
||||
billing_source: usageLogs.billing_source,
|
||||
subscription_id: usageLogs.subscription_id,
|
||||
user_id: usageLogs.user_id,
|
||||
created_at: usageLogs.created_at,
|
||||
type: usageLogs.type,
|
||||
content: usageLogs.content,
|
||||
username: usageLogs.username,
|
||||
token_name: apiKeys.token_name,
|
||||
model_name: usageLogs.model_name,
|
||||
quota: usageLogs.quota,
|
||||
prompt_tokens: usageLogs.prompt_tokens,
|
||||
completion_tokens: usageLogs.completion_tokens,
|
||||
use_time: usageLogs.use_time,
|
||||
is_stream: usageLogs.is_stream,
|
||||
channel: usageLogs.channel,
|
||||
channel_name: usageLogs.channel_name,
|
||||
token_id: usageLogs.token_id,
|
||||
group_name: usageLogs.group_name,
|
||||
ip: usageLogs.ip,
|
||||
request_id: usageLogs.request_id,
|
||||
upstream_request_id: usageLogs.upstream_request_id,
|
||||
other: usageLogs.other,
|
||||
})
|
||||
.from(usageLogs)
|
||||
.leftJoin(apiKeys, eq(usageLogs.api_key_id, apiKeys.id))
|
||||
.where(and(...conds))
|
||||
.orderBy(desc(usageLogs.created_at), desc(usageLogs.id))
|
||||
.limit(params.page_size)
|
||||
.offset(offset);
|
||||
|
||||
return {
|
||||
rows: rows.map((r) => ({
|
||||
...r,
|
||||
token_name: r.token_name ?? "",
|
||||
})) as Array<UsageLogRow & { token_name: string }>,
|
||||
total,
|
||||
};
|
||||
},
|
||||
|
||||
/** 总览聚合用:type=2、按维字段 */
|
||||
async listForAggregate(from: Date, to: Date, type = 2) {
|
||||
return db
|
||||
.select({
|
||||
api_key_id: usageLogs.api_key_id,
|
||||
member_id: apiKeys.member_id,
|
||||
billing_source: usageLogs.billing_source,
|
||||
subscription_id: usageLogs.subscription_id,
|
||||
quota: usageLogs.quota,
|
||||
created_at: usageLogs.created_at,
|
||||
token_name: apiKeys.token_name,
|
||||
status: apiKeys.status,
|
||||
log_synced_at: apiKeys.log_synced_at,
|
||||
member_name: members.name,
|
||||
})
|
||||
.from(usageLogs)
|
||||
.leftJoin(apiKeys, eq(usageLogs.api_key_id, apiKeys.id))
|
||||
.leftJoin(members, eq(apiKeys.member_id, members.id))
|
||||
.where(
|
||||
and(
|
||||
gte(usageLogs.created_at, from),
|
||||
lt(usageLogs.created_at, to),
|
||||
eq(usageLogs.type, type),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
/** 成员视图聚合(名下 key、type=2) */
|
||||
async listMemberForAggregate(
|
||||
member_id: number,
|
||||
from: Date,
|
||||
to: Date,
|
||||
type = 2,
|
||||
) {
|
||||
return db
|
||||
.select({
|
||||
api_key_id: usageLogs.api_key_id,
|
||||
billing_source: usageLogs.billing_source,
|
||||
subscription_id: usageLogs.subscription_id,
|
||||
quota: usageLogs.quota,
|
||||
created_at: usageLogs.created_at,
|
||||
token_name: apiKeys.token_name,
|
||||
log_synced_at: apiKeys.log_synced_at,
|
||||
})
|
||||
.from(usageLogs)
|
||||
.innerJoin(apiKeys, eq(usageLogs.api_key_id, apiKeys.id))
|
||||
.where(
|
||||
and(
|
||||
eq(apiKeys.member_id, member_id),
|
||||
gte(usageLogs.created_at, from),
|
||||
lt(usageLogs.created_at, to),
|
||||
eq(usageLogs.type, type),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
/** 成员视图明细分页(恒 type=2) */
|
||||
async listMemberPage(params: {
|
||||
member_id: number;
|
||||
from: Date;
|
||||
to: Date;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}) {
|
||||
const conds = [
|
||||
eq(apiKeys.member_id, params.member_id),
|
||||
gte(usageLogs.created_at, params.from),
|
||||
lt(usageLogs.created_at, params.to),
|
||||
eq(usageLogs.type, 2),
|
||||
];
|
||||
const totalRows = await db
|
||||
.select({ n: count() })
|
||||
.from(usageLogs)
|
||||
.innerJoin(apiKeys, eq(usageLogs.api_key_id, apiKeys.id))
|
||||
.where(and(...conds));
|
||||
const total = totalRows[0]?.n ?? 0;
|
||||
const rows = await db
|
||||
.select({
|
||||
id: usageLogs.id,
|
||||
api_key_id: usageLogs.api_key_id,
|
||||
created_at: usageLogs.created_at,
|
||||
token_name: apiKeys.token_name,
|
||||
model_name: usageLogs.model_name,
|
||||
type: usageLogs.type,
|
||||
content: usageLogs.content,
|
||||
quota: usageLogs.quota,
|
||||
prompt_tokens: usageLogs.prompt_tokens,
|
||||
completion_tokens: usageLogs.completion_tokens,
|
||||
use_time: usageLogs.use_time,
|
||||
is_stream: usageLogs.is_stream,
|
||||
billing_source: usageLogs.billing_source,
|
||||
subscription_id: usageLogs.subscription_id,
|
||||
log_synced_at: apiKeys.log_synced_at,
|
||||
})
|
||||
.from(usageLogs)
|
||||
.innerJoin(apiKeys, eq(usageLogs.api_key_id, apiKeys.id))
|
||||
.where(and(...conds))
|
||||
.orderBy(desc(usageLogs.created_at), desc(usageLogs.id))
|
||||
.limit(params.page_size)
|
||||
.offset((params.page - 1) * params.page_size);
|
||||
return { rows, total };
|
||||
},
|
||||
},
|
||||
|
||||
keysExtra: {
|
||||
/** 全池数据截止 = 最慢 log_synced_at */
|
||||
async slowestLogSyncedAt(): Promise<Date | null> {
|
||||
const rows = await db
|
||||
.select({ t: sql<string | null>`min(${apiKeys.log_synced_at})::text` })
|
||||
.from(apiKeys)
|
||||
.where(sql`${apiKeys.log_synced_at} is not null`);
|
||||
const raw = rows[0]?.t;
|
||||
if (!raw) return null;
|
||||
const d = new Date(raw);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
},
|
||||
async listIdsWithCutoff() {
|
||||
return db
|
||||
.select({
|
||||
id: apiKeys.id,
|
||||
member_id: apiKeys.member_id,
|
||||
token_name: apiKeys.token_name,
|
||||
status: apiKeys.status,
|
||||
log_synced_at: apiKeys.log_synced_at,
|
||||
})
|
||||
.from(apiKeys);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type Repos = ReturnType<typeof createRepos>;
|
||||
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
customType,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
pgTable,
|
||||
smallint,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
/** bytea:AES-GCM 打包密文(nonce||ct||tag) */
|
||||
const bytea = customType<{ data: Uint8Array; driverData: Buffer }>({
|
||||
dataType: () => "bytea",
|
||||
toDriver: (v) => Buffer.from(v),
|
||||
fromDriver: (v) => new Uint8Array(v),
|
||||
});
|
||||
|
||||
export const members = pgTable("members", {
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
name: text("name").notNull().unique(),
|
||||
remark: text("remark").notNull().default(""),
|
||||
status: text("status").notNull().default("active"),
|
||||
token_hash: text("token_hash").notNull().unique(),
|
||||
token_issued_at: timestamp("token_issued_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}),
|
||||
created_at: timestamp("created_at", { withTimezone: true, mode: "date" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
export const apiKeys = pgTable(
|
||||
"api_keys",
|
||||
{
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
key_hash: text("key_hash").notNull().unique(),
|
||||
key_ciphertext: bytea("key_ciphertext").notNull(),
|
||||
status: text("status").notNull().default("active"),
|
||||
member_id: bigint("member_id", { mode: "number" }).references(
|
||||
() => members.id,
|
||||
),
|
||||
registered_at: timestamp("registered_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
log_synced_at: timestamp("log_synced_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}),
|
||||
usage_synced_at: timestamp("usage_synced_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}),
|
||||
token_name: text("token_name").notNull().default(""),
|
||||
total_granted: bigint("total_granted", { mode: "number" })
|
||||
.notNull()
|
||||
.default(0),
|
||||
total_used: bigint("total_used", { mode: "number" }).notNull().default(0),
|
||||
total_available: bigint("total_available", { mode: "number" })
|
||||
.notNull()
|
||||
.default(0),
|
||||
unlimited_quota: boolean("unlimited_quota").notNull().default(false),
|
||||
model_limits: jsonb("model_limits"),
|
||||
model_limits_enabled: boolean("model_limits_enabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
expires_at: timestamp("expires_at", { withTimezone: true, mode: "date" }),
|
||||
},
|
||||
(t) => [index("api_keys_member_id_idx").on(t.member_id)],
|
||||
);
|
||||
|
||||
export const usageLogs = pgTable(
|
||||
"usage_logs",
|
||||
{
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
api_key_id: bigint("api_key_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => apiKeys.id),
|
||||
dedupe_key: text("dedupe_key").notNull().unique(),
|
||||
billing_source: text("billing_source"),
|
||||
subscription_id: bigint("subscription_id", { mode: "number" }),
|
||||
user_id: bigint("user_id", { mode: "number" }).notNull().default(0),
|
||||
created_at: timestamp("created_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}).notNull(),
|
||||
type: smallint("type").notNull().default(2),
|
||||
content: text("content").notNull().default(""),
|
||||
username: text("username").notNull().default(""),
|
||||
token_name: text("token_name").notNull().default(""),
|
||||
model_name: text("model_name").notNull().default(""),
|
||||
quota: bigint("quota", { mode: "number" }).notNull().default(0),
|
||||
prompt_tokens: integer("prompt_tokens").notNull().default(0),
|
||||
completion_tokens: integer("completion_tokens").notNull().default(0),
|
||||
use_time: integer("use_time").notNull().default(0),
|
||||
is_stream: boolean("is_stream").notNull().default(false),
|
||||
channel: integer("channel").notNull().default(0),
|
||||
channel_name: text("channel_name").notNull().default(""),
|
||||
token_id: bigint("token_id", { mode: "number" }).notNull().default(0),
|
||||
group_name: text("group_name").notNull().default(""),
|
||||
ip: text("ip").notNull().default(""),
|
||||
request_id: text("request_id").notNull().default(""),
|
||||
upstream_request_id: text("upstream_request_id"),
|
||||
other: jsonb("other"),
|
||||
},
|
||||
(t) => [
|
||||
index("usage_logs_created_at_idx").on(t.created_at),
|
||||
index("usage_logs_api_key_created_idx").on(t.api_key_id, t.created_at),
|
||||
index("usage_logs_subscription_idx").on(t.subscription_id),
|
||||
],
|
||||
);
|
||||
|
||||
export const payments = pgTable("payments", {
|
||||
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", {
|
||||
precision: 18,
|
||||
scale: 2,
|
||||
mode: "number",
|
||||
}).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" }),
|
||||
});
|
||||
|
||||
export const qpuHistory = pgTable("qpu_history", {
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
qpu: numeric("qpu", { precision: 18, scale: 6, mode: "number" }).notNull(),
|
||||
effective_from: timestamp("effective_from", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
username: text("username").notNull().unique(),
|
||||
password_hash: text("password_hash").notNull(),
|
||||
created_at: timestamp("created_at", { withTimezone: true, mode: "date" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
export const sessions = pgTable(
|
||||
"sessions",
|
||||
{
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
user_id: bigint("user_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
token_hash: text("token_hash").notNull().unique(),
|
||||
expires_at: timestamp("expires_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}).notNull(),
|
||||
created_at: timestamp("created_at", { withTimezone: true, mode: "date" })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
},
|
||||
(t) => [index("sessions_user_id_idx").on(t.user_id)],
|
||||
);
|
||||
|
||||
/** 审计日志(管理面写操作留痕)——结构对齐 API契约-管理面 §9 */
|
||||
export const auditLogs = pgTable(
|
||||
"audit_logs",
|
||||
{
|
||||
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
||||
created_at: timestamp("created_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
operator: text("operator"),
|
||||
domain: text("domain").notNull(),
|
||||
action: text("action").notNull(),
|
||||
summary: text("summary").notNull().default(""),
|
||||
},
|
||||
(t) => [index("audit_logs_created_at_idx").on(t.created_at)],
|
||||
);
|
||||
|
||||
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 QpuHistoryRow = typeof qpuHistory.$inferSelect;
|
||||
export type UserRow = typeof users.$inferSelect;
|
||||
export type SessionRow = typeof sessions.$inferSelect;
|
||||
export type AuditLogRow = typeof auditLogs.$inferSelect;
|
||||
Reference in New Issue
Block a user