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", "用户名或密码错误"),
|
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) =>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791551016051,
|
||||||
|
"tag": "0000_loose_goblin_queen",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+71
-23
@@ -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));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user