245 lines
8.4 KiB
TypeScript
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;
|