✨ feat: impl accounts and channels
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user