Files
fluxent-web/lib/db/schema.ts
T

245 lines
8.4 KiB
TypeScript

import {
pgTable,
text,
varchar,
uuid,
boolean,
timestamp,
jsonb,
uniqueIndex,
index,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// ---------------------------------------------------------------------------
// 基础时间戳辅助
// ---------------------------------------------------------------------------
export const timestamps = {
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
};
// ---------------------------------------------------------------------------
// 1. 用户与认证表
// ---------------------------------------------------------------------------
/**
* 用户表:本地密码用户与 OIDC 用户统一归属于此表
*/
export const users = pgTable(
"users",
{
id: uuid("id").defaultRandom().primaryKey(),
name: varchar("name", { length: 100 }).notNull(),
email: varchar("email", { length: 255 }).notNull(),
passwordHash: text("password_hash"), // OIDC 注册的用户可无密码
avatar: text("avatar"),
isActive: boolean("is_active").default(true).notNull(),
...timestamps,
},
(t) => [
uniqueIndex("users_email_unique").on(t.email),
]
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
/**
* 用户认证授权表 (支持 OIDC / OAuth 账号关联)
*/
export const userAccounts = pgTable(
"user_accounts",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
provider: varchar("provider", { length: 64 }).notNull(), // 如 'oidc'
providerAccountId: varchar("provider_account_id", { length: 255 }).notNull(),
refreshToken: text("refresh_token"),
accessToken: text("access_token"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
tokenType: varchar("token_type", { length: 64 }),
scope: text("scope"),
idToken: text("id_token"),
...timestamps,
},
(t) => [
uniqueIndex("user_accounts_provider_account_id_unique").on(
t.provider,
t.providerAccountId
),
index("user_accounts_user_id_idx").on(t.userId),
]
);
// ---------------------------------------------------------------------------
// 2. 资金账户表 (Accounts)
// ---------------------------------------------------------------------------
export type AccountType = "BANK" | "E_WALLET" | "CASH";
export type BalanceType = "ASSET" | "LIABILITY" | "EQUITY";
export const accounts = pgTable(
"accounts",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
name: varchar("name", { length: 150 }).notNull(),
accountType: varchar("account_type", { length: 32 }).$type<AccountType>().notNull(),
balanceType: varchar("balance_type", { length: 32 })
.$type<BalanceType>()
.default("ASSET")
.notNull(),
primaryCurrency: varchar("primary_currency", { length: 10 }),
supportedCurrencies: jsonb("supported_currencies").$type<string[] | null>(),
isActive: boolean("is_active").default(true).notNull(),
remark: text("remark"),
ext: text("ext"),
// 银行账户特定字段
issuerName: varchar("issuer_name", { length: 100 }),
accountNumber: varchar("account_number", { length: 100 }),
subAccounts: jsonb("sub_accounts").$type<string[]>(),
// 电子钱包特定字段
platform: varchar("platform", { length: 64 }),
accountId: varchar("account_id", { length: 100 }),
// 现金账户特定字段
location: varchar("location", { length: 150 }),
...timestamps,
},
(t) => [
index("accounts_user_id_idx").on(t.userId),
]
);
export type Account = typeof accounts.$inferSelect;
export type NewAccount = typeof accounts.$inferInsert;
// ---------------------------------------------------------------------------
// 3. 渠道表 (Channels)
// ---------------------------------------------------------------------------
export type ChannelType = "PAYMENT_CARD" | "E_WALLET" | "CASH" | "TRANSFER";
export type PaymentInstrumentType = "CREDIT" | "DEBIT";
export const channels = pgTable(
"channels",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
channelType: varchar("channel_type", { length: 32 }).$type<ChannelType>().notNull(),
refAccounts: jsonb("ref_accounts").$type<string[]>().notNull(), // 关联的账户 UUID 列表
isActive: boolean("is_active").default(true).notNull(),
desc: text("desc"),
ext: text("ext"),
// 支付卡渠道字段
region: varchar("region", { length: 10 }), // 发卡地如 HK, CN
issuerName: varchar("issuer_name", { length: 100 }),
cardType: varchar("card_type", { length: 32 }).$type<PaymentInstrumentType>(),
cardNumberFull: varchar("card_number_full", { length: 100 }),
cardNumberSuffix: varchar("card_number_suffix", { length: 10 }),
cardBrand: varchar("card_brand", { length: 32 }),
// 电子钱包渠道字段
platform: varchar("platform", { length: 64 }),
platformAccountId: varchar("platform_account_id", { length: 100 }),
subChannel: varchar("sub_channel", { length: 64 }),
subChannelType: varchar("sub_channel_type", { length: 32 }).$type<PaymentInstrumentType>(),
...timestamps,
},
(t) => [
index("channels_user_id_idx").on(t.userId),
]
);
export type Channel = typeof channels.$inferSelect;
export type NewChannel = typeof channels.$inferInsert;
// ---------------------------------------------------------------------------
// 4. 交易记账表 (Transactions)
// ---------------------------------------------------------------------------
export type TransactionStatus = "PENDING" | "COMPLETED" | "FAILED" | "REFUNDED";
export type DcFlag = "DEBIT" | "CREDIT";
export interface FxRateItem {
fromCcy: string;
toCcy: string;
rate: string;
}
export const transactions = pgTable(
"transactions",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
version: varchar("version", { length: 16 }).default("0").notNull(),
refTransactions: jsonb("ref_transactions").$type<string[]>(), // 关联交易 UUID 列表
txnDate: timestamp("txn_date", { withTimezone: true }).notNull(),
clearingDate: timestamp("clearing_date", { withTimezone: true }),
postingDate: timestamp("posting_date", { withTimezone: true }),
// 金额与币种
txnAmt: varchar("txn_amt", { length: 32 }).notNull(),
txnCcy: varchar("txn_ccy", { length: 10 }).notNull(),
postingAmt: varchar("posting_amt", { length: 32 }),
postingCcy: varchar("posting_ccy", { length: 10 }),
commAmt: varchar("comm_amt", { length: 32 }),
commCcy: varchar("comm_ccy", { length: 10 }),
surchargeAmt: varchar("surcharge_amt", { length: 32 }),
surchargeCcy: varchar("surcharge_ccy", { length: 10 }),
discAmt: varchar("disc_amt", { length: 32 }),
discCcy: varchar("disc_ccy", { length: 10 }),
fxRates: jsonb("fx_rates").$type<FxRateItem[]>(),
dcFlag: varchar("dc_flag", { length: 16 }).$type<DcFlag>().notNull(),
refChannels: jsonb("ref_channels").$type<string[]>().notNull(), // 渠道 UUID 列表
cp: varchar("cp", { length: 255 }), // 对手方
acqInst: varchar("acq_inst", { length: 150 }),
clearingNetwork: varchar("clearing_network", { length: 100 }),
txnSts: varchar("txn_sts", { length: 32 })
.$type<TransactionStatus>()
.default("COMPLETED")
.notNull(),
description: text("description"),
memo: text("memo"),
ext: text("ext"),
rawDescription: text("raw_description"),
rawData: jsonb("raw_data"),
// 场景类型
txnScene: varchar("txn_scene", { length: 64 }).notNull(), // MISC_IN, PAYMENT, ECOM_PAYMENT, POS_PAYMENT, ATM, TRANSFER 等
merchantName: varchar("merchant_name", { length: 255 }),
orderId: varchar("order_id", { length: 150 }),
geo: varchar("geo", { length: 64 }),
...timestamps,
},
(t) => [
index("transactions_user_id_idx").on(t.userId),
index("transactions_txn_date_idx").on(t.txnDate),
]
);
export type Transaction = typeof transactions.$inferSelect;
export type NewTransaction = typeof transactions.$inferInsert;