feat(core,db): 业务核心与数据访问层
core:DTO/错误信封/时间口径/微分折算/dedupe_key/加密与 PBKDF2/契约 zod/logtape/上游客户端。 db:八表 Drizzle schema + 仓储 + drizzle 配置。
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* 契约 zod schema(字段权威 = API契约三篇;DTO 类型见 types.ts)。
|
||||
* OpenAPI 装饰(.openapi 组件名等)留在 apps/server,core 不依赖 hono。
|
||||
*/
|
||||
|
||||
export const errorShape = z.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
fields: z.record(z.string(), z.string()).optional(),
|
||||
key_id: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export const errorEnvelope = z.object({ error: errorShape });
|
||||
export type ErrorEnvelope = z.infer<typeof errorEnvelope>;
|
||||
|
||||
export const setupState = z.object({ initialized: z.boolean() });
|
||||
export const usernameBody = z.object({ username: z.string() });
|
||||
|
||||
export const setupBody = z.object({
|
||||
username: z.string().min(1).max(64),
|
||||
password: z.string().min(8).max(128),
|
||||
});
|
||||
|
||||
export const loginBody = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const changePasswordBody = z.object({
|
||||
old_password: z.string().min(1),
|
||||
new_password: z.string().min(8).max(128),
|
||||
});
|
||||
|
||||
export const timeRangeQuery = z.object({
|
||||
from: z.string().min(1),
|
||||
to: z.string().min(1),
|
||||
});
|
||||
|
||||
export const pageQuery = z.object({
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
page_size: z.coerce.number().int().min(1).max(200).default(50),
|
||||
});
|
||||
|
||||
export const usageTotals = z.object({
|
||||
count: z.number().int(),
|
||||
quota: z.number(),
|
||||
quota_usd: z.string(),
|
||||
});
|
||||
|
||||
export const logItem = z.object({
|
||||
id: z.number().int(),
|
||||
api_key_id: z.number().int(),
|
||||
created_at: z.string(),
|
||||
token_name: z.string(),
|
||||
model_name: z.string(),
|
||||
type: z.number().int(),
|
||||
content: z.string(),
|
||||
quota: z.number(),
|
||||
quota_usd: z.string(),
|
||||
prompt_tokens: z.number().int(),
|
||||
completion_tokens: z.number().int(),
|
||||
use_time: z.number().int(),
|
||||
is_stream: z.boolean(),
|
||||
billing_source: z.string().nullable(),
|
||||
subscription_id: z.number().int().nullable(),
|
||||
});
|
||||
|
||||
export const logPage = z.object({
|
||||
items: z.array(logItem),
|
||||
total: z.number().int(),
|
||||
});
|
||||
|
||||
export const keyItem = z.object({
|
||||
id: z.number().int(),
|
||||
token_name: z.string(),
|
||||
status: z.enum(["active", "disabled"]),
|
||||
member_id: z.number().int().nullable(),
|
||||
member_name: z.string().nullable(),
|
||||
registered_at: z.string(),
|
||||
log_synced_at: z.string().nullable(),
|
||||
usage_synced_at: z.string().nullable(),
|
||||
total_used: z.number(),
|
||||
total_available: z.number(),
|
||||
total_granted: z.number(),
|
||||
unlimited_quota: z.boolean(),
|
||||
expires_at: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const registerKeyBody = z.object({
|
||||
sk_key: z.string().min(8),
|
||||
member_id: z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
|
||||
export const patchKeyBody = z.object({
|
||||
status: z.enum(["active", "disabled"]).optional(),
|
||||
member_id: z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
|
||||
export const memberItem = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
remark: z.string(),
|
||||
status: z.enum(["active", "disabled"]),
|
||||
token_issued_at: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
key_count: z.number().int(),
|
||||
});
|
||||
|
||||
export const createMemberBody = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
remark: z.string().optional().default(""),
|
||||
});
|
||||
|
||||
export const patchMemberBody = z.object({
|
||||
status: z.enum(["active", "disabled"]),
|
||||
});
|
||||
|
||||
export const memberWithToken = z.object({
|
||||
member: memberItem,
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export const tokenOnly = z.object({ token: z.string() });
|
||||
|
||||
export const memberMe = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const paymentItem = z.object({
|
||||
id: z.number().int(),
|
||||
kind: z.enum(["subscription", "topup"]),
|
||||
paid_at: z.string(),
|
||||
paid_amount: z.number(),
|
||||
remark: z.string(),
|
||||
period_start: z.string().nullable(),
|
||||
period_end: z.string().nullable(),
|
||||
subscription_id: z.number().int().nullable(),
|
||||
quota_granted: z.number().int().nullable(),
|
||||
quota_usd: z.string(),
|
||||
});
|
||||
|
||||
export const engineStatus = z.object({
|
||||
running: z.boolean(),
|
||||
last_round: z
|
||||
.object({
|
||||
started_at: z.string(),
|
||||
keys_total: z.number().int(),
|
||||
succeeded: z.number().int(),
|
||||
failed: z.number().int(),
|
||||
failed_api_key_ids: z.array(z.number().int()),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const auditLogItem = z.object({
|
||||
id: z.number().int(),
|
||||
created_at: z.string(),
|
||||
operator: z.string().nullable(),
|
||||
domain: z.enum(["key", "payment", "member", "account"]),
|
||||
action: z.string(),
|
||||
summary: z.string(),
|
||||
});
|
||||
|
||||
export const memberOverview = z.object({
|
||||
member: z.object({
|
||||
id: z.number().int().nullable(),
|
||||
name: z.string().nullable(),
|
||||
}),
|
||||
usage: usageTotals,
|
||||
by_source: z.array(
|
||||
z.object({
|
||||
billing_source: z.string(),
|
||||
count: z.number().int(),
|
||||
quota: z.number(),
|
||||
quota_usd: z.string(),
|
||||
}),
|
||||
),
|
||||
keys: z.array(
|
||||
z.object({
|
||||
key_id: z.number().int(),
|
||||
token_name: z.string(),
|
||||
log_synced_at: z.string().nullable(),
|
||||
usage: usageTotals,
|
||||
}),
|
||||
),
|
||||
payments: z.array(z.looseObject({ id: z.number().int() })),
|
||||
});
|
||||
|
||||
export const createPaymentBody = 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(),
|
||||
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"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
setupBody as setupBodySchema,
|
||||
loginBody as loginBodySchema,
|
||||
changePasswordBody as changePasswordBodySchema,
|
||||
registerKeyBody as registerKeyBodySchema,
|
||||
patchKeyBody as patchKeyBodySchema,
|
||||
createMemberBody as createMemberBodySchema,
|
||||
patchMemberBody as patchMemberBodySchema,
|
||||
createPaymentBody as createPaymentBodySchema,
|
||||
timeRangeQuery as timeRangeQuerySchema,
|
||||
pageQuery as pageQuerySchema,
|
||||
};
|
||||
Reference in New Issue
Block a user