feat(admin-service): 添加 CDC 分类账流水同步

新增 wallet/planting/authorization 服务的 CDC 数据同步:

状态表同步:
- WalletAccountQueryView: 钱包账户余额状态
- WithdrawalOrderQueryView: 提现订单状态
- FiatWithdrawalOrderQueryView: 法币提现订单
- PlantingOrderQueryView: 认种订单状态
- PlantingPositionQueryView: 持仓状态
- ContractSigningTaskQueryView: 合同签约任务
- AuthorizationRoleQueryView: 授权角色
- MonthlyAssessmentQueryView: 月度考核
- SystemAccountQueryView: 系统账户余额

分类账流水同步:
- WalletLedgerEntryView: 钱包流水分类账
- FundAllocationView: 认种资金分配记录
- SystemAccountLedgerView: 系统账户流水

其他:
- Debezium Connect 端口改为 8084 避免冲突
- 更新连接器配置添加流水表

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-07 19:27:22 -08:00
parent c4cec836d9
commit 83f84b9d7c
12 changed files with 3134 additions and 70 deletions

View File

@ -0,0 +1,354 @@
-- =============================================================================
-- CDC Query Views Migration
-- 创建用于 Debezium CDC 数据同步的查询视图表
-- =============================================================================
-- -----------------------------------------------------------------------------
-- Wallet Query Views (钱包查询视图)
-- -----------------------------------------------------------------------------
-- 钱包账户查询视图
CREATE TABLE "wallet_account_query_view" (
"wallet_id" BIGINT NOT NULL,
"account_sequence" VARCHAR(20) NOT NULL,
"user_id" BIGINT NOT NULL,
"usdt_available" DECIMAL(20,8) NOT NULL DEFAULT 0,
"usdt_frozen" DECIMAL(20,8) NOT NULL DEFAULT 0,
"dst_available" DECIMAL(20,8) NOT NULL DEFAULT 0,
"dst_frozen" DECIMAL(20,8) NOT NULL DEFAULT 0,
"bnb_available" DECIMAL(20,8) NOT NULL DEFAULT 0,
"bnb_frozen" DECIMAL(20,8) NOT NULL DEFAULT 0,
"og_available" DECIMAL(20,8) NOT NULL DEFAULT 0,
"og_frozen" DECIMAL(20,8) NOT NULL DEFAULT 0,
"rwad_available" DECIMAL(20,8) NOT NULL DEFAULT 0,
"rwad_frozen" DECIMAL(20,8) NOT NULL DEFAULT 0,
"hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
"pending_usdt" DECIMAL(20,8) NOT NULL DEFAULT 0,
"pending_hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
"settleable_usdt" DECIMAL(20,8) NOT NULL DEFAULT 0,
"settleable_hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
"settled_total_usdt" DECIMAL(20,8) NOT NULL DEFAULT 0,
"settled_total_hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
"expired_total_usdt" DECIMAL(20,8) NOT NULL DEFAULT 0,
"expired_total_hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
"has_planted" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "wallet_account_query_view_pkey" PRIMARY KEY ("wallet_id")
);
CREATE UNIQUE INDEX "wallet_account_query_view_account_sequence_key" ON "wallet_account_query_view"("account_sequence");
CREATE UNIQUE INDEX "wallet_account_query_view_user_id_key" ON "wallet_account_query_view"("user_id");
CREATE INDEX "wallet_account_query_view_account_sequence_idx" ON "wallet_account_query_view"("account_sequence");
CREATE INDEX "wallet_account_query_view_usdt_available_idx" ON "wallet_account_query_view"("usdt_available" DESC);
CREATE INDEX "wallet_account_query_view_hashpower_idx" ON "wallet_account_query_view"("hashpower" DESC);
CREATE INDEX "wallet_account_query_view_status_idx" ON "wallet_account_query_view"("status");
-- 提现订单查询视图
CREATE TABLE "withdrawal_order_query_view" (
"order_id" BIGINT NOT NULL,
"order_no" VARCHAR(50) NOT NULL,
"account_sequence" VARCHAR(20) NOT NULL,
"user_id" BIGINT NOT NULL,
"amount" DECIMAL(20,8) NOT NULL,
"fee" DECIMAL(20,8) NOT NULL,
"chain_type" VARCHAR(20) NOT NULL,
"to_address" VARCHAR(100) NOT NULL,
"tx_hash" VARCHAR(100),
"is_internal_transfer" BOOLEAN NOT NULL DEFAULT false,
"to_account_sequence" VARCHAR(20),
"to_user_id" BIGINT,
"status" VARCHAR(20) NOT NULL DEFAULT 'PENDING',
"error_message" VARCHAR(500),
"frozen_at" TIMESTAMP(3),
"broadcasted_at" TIMESTAMP(3),
"confirmed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "withdrawal_order_query_view_pkey" PRIMARY KEY ("order_id")
);
CREATE UNIQUE INDEX "withdrawal_order_query_view_order_no_key" ON "withdrawal_order_query_view"("order_no");
CREATE INDEX "withdrawal_order_query_view_account_sequence_idx" ON "withdrawal_order_query_view"("account_sequence");
CREATE INDEX "withdrawal_order_query_view_user_id_idx" ON "withdrawal_order_query_view"("user_id");
CREATE INDEX "withdrawal_order_query_view_status_idx" ON "withdrawal_order_query_view"("status");
CREATE INDEX "withdrawal_order_query_view_chain_type_idx" ON "withdrawal_order_query_view"("chain_type");
CREATE INDEX "withdrawal_order_query_view_tx_hash_idx" ON "withdrawal_order_query_view"("tx_hash");
CREATE INDEX "withdrawal_order_query_view_created_at_idx" ON "withdrawal_order_query_view"("created_at");
-- 法币提现订单查询视图
CREATE TABLE "fiat_withdrawal_order_query_view" (
"order_id" BIGINT NOT NULL,
"order_no" VARCHAR(50) NOT NULL,
"account_sequence" VARCHAR(20) NOT NULL,
"user_id" BIGINT NOT NULL,
"amount" DECIMAL(20,8) NOT NULL,
"fee" DECIMAL(20,8) NOT NULL,
"payment_method" VARCHAR(20) NOT NULL,
"bank_name" VARCHAR(100),
"bank_card_no_masked" VARCHAR(50),
"card_holder_name" VARCHAR(100),
"alipay_account_masked" VARCHAR(100),
"wechat_account_masked" VARCHAR(100),
"status" VARCHAR(20) NOT NULL DEFAULT 'PENDING',
"error_message" VARCHAR(500),
"reviewed_by" VARCHAR(100),
"reviewed_at" TIMESTAMP(3),
"review_remark" VARCHAR(500),
"paid_by" VARCHAR(100),
"paid_at" TIMESTAMP(3),
"frozen_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "fiat_withdrawal_order_query_view_pkey" PRIMARY KEY ("order_id")
);
CREATE UNIQUE INDEX "fiat_withdrawal_order_query_view_order_no_key" ON "fiat_withdrawal_order_query_view"("order_no");
CREATE INDEX "fiat_withdrawal_order_query_view_account_sequence_idx" ON "fiat_withdrawal_order_query_view"("account_sequence");
CREATE INDEX "fiat_withdrawal_order_query_view_user_id_idx" ON "fiat_withdrawal_order_query_view"("user_id");
CREATE INDEX "fiat_withdrawal_order_query_view_status_idx" ON "fiat_withdrawal_order_query_view"("status");
CREATE INDEX "fiat_withdrawal_order_query_view_payment_method_idx" ON "fiat_withdrawal_order_query_view"("payment_method");
CREATE INDEX "fiat_withdrawal_order_query_view_reviewed_by_idx" ON "fiat_withdrawal_order_query_view"("reviewed_by");
CREATE INDEX "fiat_withdrawal_order_query_view_created_at_idx" ON "fiat_withdrawal_order_query_view"("created_at");
-- -----------------------------------------------------------------------------
-- Planting Query Views (认种查询视图)
-- -----------------------------------------------------------------------------
-- 认种订单查询视图
CREATE TABLE "planting_order_query_view" (
"order_id" BIGINT NOT NULL,
"order_no" VARCHAR(50) NOT NULL,
"user_id" BIGINT NOT NULL,
"account_sequence" VARCHAR(20) NOT NULL,
"tree_count" INTEGER NOT NULL,
"total_amount" DECIMAL(20,8) NOT NULL,
"selected_province" VARCHAR(10),
"selected_city" VARCHAR(10),
"status" VARCHAR(30) NOT NULL DEFAULT 'CREATED',
"pool_injection_batch_id" BIGINT,
"created_at" TIMESTAMP(3) NOT NULL,
"paid_at" TIMESTAMP(3),
"fund_allocated_at" TIMESTAMP(3),
"mining_enabled_at" TIMESTAMP(3),
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "planting_order_query_view_pkey" PRIMARY KEY ("order_id")
);
CREATE UNIQUE INDEX "planting_order_query_view_order_no_key" ON "planting_order_query_view"("order_no");
CREATE INDEX "planting_order_query_view_user_id_idx" ON "planting_order_query_view"("user_id");
CREATE INDEX "planting_order_query_view_account_sequence_idx" ON "planting_order_query_view"("account_sequence");
CREATE INDEX "planting_order_query_view_status_idx" ON "planting_order_query_view"("status");
CREATE INDEX "planting_order_query_view_province_city_idx" ON "planting_order_query_view"("selected_province", "selected_city");
CREATE INDEX "planting_order_query_view_created_at_idx" ON "planting_order_query_view"("created_at");
CREATE INDEX "planting_order_query_view_paid_at_idx" ON "planting_order_query_view"("paid_at");
-- 用户持仓查询视图
CREATE TABLE "planting_position_query_view" (
"position_id" BIGINT NOT NULL,
"user_id" BIGINT NOT NULL,
"total_tree_count" INTEGER NOT NULL DEFAULT 0,
"effective_tree_count" INTEGER NOT NULL DEFAULT 0,
"pending_tree_count" INTEGER NOT NULL DEFAULT 0,
"first_mining_start_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "planting_position_query_view_pkey" PRIMARY KEY ("position_id")
);
CREATE UNIQUE INDEX "planting_position_query_view_user_id_key" ON "planting_position_query_view"("user_id");
CREATE INDEX "planting_position_query_view_total_tree_count_idx" ON "planting_position_query_view"("total_tree_count");
-- 合同签署任务查询视图
CREATE TABLE "contract_signing_task_query_view" (
"task_id" BIGINT NOT NULL,
"order_no" VARCHAR(50) NOT NULL,
"contract_no" VARCHAR(30) NOT NULL,
"user_id" BIGINT NOT NULL,
"account_sequence" VARCHAR(20) NOT NULL,
"contract_version" VARCHAR(20) NOT NULL,
"tree_count" INTEGER NOT NULL,
"total_amount" DECIMAL(20,8) NOT NULL,
"province_code" VARCHAR(10) NOT NULL,
"province_name" VARCHAR(50) NOT NULL,
"city_code" VARCHAR(10) NOT NULL,
"city_name" VARCHAR(50) NOT NULL,
"status" VARCHAR(30) NOT NULL DEFAULT 'PENDING',
"expires_at" TIMESTAMP(3) NOT NULL,
"signed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "contract_signing_task_query_view_pkey" PRIMARY KEY ("task_id")
);
CREATE UNIQUE INDEX "contract_signing_task_query_view_order_no_key" ON "contract_signing_task_query_view"("order_no");
CREATE UNIQUE INDEX "contract_signing_task_query_view_contract_no_key" ON "contract_signing_task_query_view"("contract_no");
CREATE INDEX "contract_signing_task_query_view_user_id_idx" ON "contract_signing_task_query_view"("user_id");
CREATE INDEX "contract_signing_task_query_view_status_idx" ON "contract_signing_task_query_view"("status");
CREATE INDEX "contract_signing_task_query_view_expires_at_idx" ON "contract_signing_task_query_view"("expires_at");
-- -----------------------------------------------------------------------------
-- Authorization Query Views (授权查询视图)
-- -----------------------------------------------------------------------------
-- 授权角色查询视图
CREATE TABLE "authorization_role_query_view" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"account_sequence" TEXT NOT NULL,
"role_type" VARCHAR(30) NOT NULL,
"region_code" TEXT NOT NULL,
"region_name" TEXT NOT NULL,
"status" VARCHAR(20) NOT NULL DEFAULT 'PENDING',
"display_title" TEXT NOT NULL,
"authorized_at" TIMESTAMP(3),
"authorized_by" TEXT,
"revoked_at" TIMESTAMP(3),
"revoked_by" TEXT,
"deleted_at" TIMESTAMP(3),
"initial_target_tree_count" INTEGER NOT NULL,
"monthly_target_type" VARCHAR(20) NOT NULL,
"benefit_active" BOOLEAN NOT NULL DEFAULT false,
"benefit_activated_at" TIMESTAMP(3),
"last_assessment_month" TEXT,
"monthly_trees_added" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "authorization_role_query_view_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "authorization_role_query_view_account_sequence_idx" ON "authorization_role_query_view"("account_sequence");
CREATE INDEX "authorization_role_query_view_user_id_idx" ON "authorization_role_query_view"("user_id");
CREATE INDEX "authorization_role_query_view_role_type_region_code_idx" ON "authorization_role_query_view"("role_type", "region_code");
CREATE INDEX "authorization_role_query_view_status_idx" ON "authorization_role_query_view"("status");
CREATE INDEX "authorization_role_query_view_deleted_at_idx" ON "authorization_role_query_view"("deleted_at");
-- 月度考核查询视图
CREATE TABLE "monthly_assessment_query_view" (
"id" TEXT NOT NULL,
"authorization_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"account_sequence" TEXT NOT NULL,
"role_type" VARCHAR(30) NOT NULL,
"region_code" TEXT NOT NULL,
"assessment_month" TEXT NOT NULL,
"month_index" INTEGER NOT NULL,
"monthly_target" INTEGER NOT NULL,
"cumulative_target" INTEGER NOT NULL,
"monthly_completed" INTEGER NOT NULL DEFAULT 0,
"cumulative_completed" INTEGER NOT NULL DEFAULT 0,
"completed_at" TIMESTAMP(3),
"result" VARCHAR(20) NOT NULL DEFAULT 'NOT_ASSESSED',
"ranking_in_region" INTEGER,
"is_first_place" BOOLEAN NOT NULL DEFAULT false,
"is_bypassed" BOOLEAN NOT NULL DEFAULT false,
"assessed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "monthly_assessment_query_view_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "monthly_assessment_query_view_account_month_idx" ON "monthly_assessment_query_view"("account_sequence", "assessment_month");
CREATE INDEX "monthly_assessment_query_view_user_month_idx" ON "monthly_assessment_query_view"("user_id", "assessment_month");
CREATE INDEX "monthly_assessment_query_view_role_region_month_idx" ON "monthly_assessment_query_view"("role_type", "region_code", "assessment_month");
CREATE INDEX "monthly_assessment_query_view_month_result_idx" ON "monthly_assessment_query_view"("assessment_month", "result");
-- 系统账户查询视图
CREATE TABLE "system_account_query_view" (
"account_id" BIGINT NOT NULL,
"account_type" VARCHAR(30) NOT NULL,
"region_code" VARCHAR(10),
"region_name" VARCHAR(50),
"wallet_address" VARCHAR(42),
"usdt_balance" DECIMAL(20,8) NOT NULL DEFAULT 0,
"hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
"total_received" DECIMAL(20,8) NOT NULL DEFAULT 0,
"total_transferred" DECIMAL(20,8) NOT NULL DEFAULT 0,
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "system_account_query_view_pkey" PRIMARY KEY ("account_id")
);
CREATE INDEX "system_account_query_view_account_type_idx" ON "system_account_query_view"("account_type");
CREATE INDEX "system_account_query_view_wallet_address_idx" ON "system_account_query_view"("wallet_address");
-- -----------------------------------------------------------------------------
-- Ledger Views (分类账视图)
-- -----------------------------------------------------------------------------
-- 钱包流水分类账
CREATE TABLE "wallet_ledger_entry_view" (
"entry_id" BIGINT NOT NULL,
"account_sequence" VARCHAR(20) NOT NULL,
"user_id" BIGINT NOT NULL,
"entry_type" VARCHAR(50) NOT NULL,
"amount" DECIMAL(20,8) NOT NULL,
"asset_type" VARCHAR(20) NOT NULL,
"balance_after" DECIMAL(20,8),
"ref_order_id" VARCHAR(100),
"ref_tx_hash" VARCHAR(100),
"memo" TEXT,
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "wallet_ledger_entry_view_pkey" PRIMARY KEY ("entry_id")
);
CREATE INDEX "wallet_ledger_entry_view_account_created_idx" ON "wallet_ledger_entry_view"("account_sequence", "created_at" DESC);
CREATE INDEX "wallet_ledger_entry_view_user_created_idx" ON "wallet_ledger_entry_view"("user_id", "created_at" DESC);
CREATE INDEX "wallet_ledger_entry_view_entry_type_idx" ON "wallet_ledger_entry_view"("entry_type");
CREATE INDEX "wallet_ledger_entry_view_asset_type_idx" ON "wallet_ledger_entry_view"("asset_type");
CREATE INDEX "wallet_ledger_entry_view_ref_order_id_idx" ON "wallet_ledger_entry_view"("ref_order_id");
CREATE INDEX "wallet_ledger_entry_view_ref_tx_hash_idx" ON "wallet_ledger_entry_view"("ref_tx_hash");
CREATE INDEX "wallet_ledger_entry_view_created_at_idx" ON "wallet_ledger_entry_view"("created_at");
-- 认种资金分配分类账
CREATE TABLE "fund_allocation_view" (
"allocation_id" BIGINT NOT NULL,
"order_id" BIGINT NOT NULL,
"target_type" VARCHAR(50) NOT NULL,
"amount" DECIMAL(20,8) NOT NULL,
"target_account_id" VARCHAR(100),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "fund_allocation_view_pkey" PRIMARY KEY ("allocation_id")
);
CREATE INDEX "fund_allocation_view_order_id_idx" ON "fund_allocation_view"("order_id");
CREATE INDEX "fund_allocation_view_target_idx" ON "fund_allocation_view"("target_type", "target_account_id");
CREATE INDEX "fund_allocation_view_created_at_idx" ON "fund_allocation_view"("created_at");
-- 系统账户流水分类账
CREATE TABLE "system_account_ledger_view" (
"ledger_id" BIGINT NOT NULL,
"account_id" BIGINT NOT NULL,
"entry_type" VARCHAR(30) NOT NULL,
"amount" DECIMAL(20,8) NOT NULL,
"balance_after" DECIMAL(20,8) NOT NULL,
"source_order_id" BIGINT,
"source_reward_id" BIGINT,
"tx_hash" VARCHAR(66),
"memo" VARCHAR(500),
"created_at" TIMESTAMP(3) NOT NULL,
"synced_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "system_account_ledger_view_pkey" PRIMARY KEY ("ledger_id")
);
CREATE INDEX "system_account_ledger_view_account_created_idx" ON "system_account_ledger_view"("account_id", "created_at" DESC);
CREATE INDEX "system_account_ledger_view_source_order_idx" ON "system_account_ledger_view"("source_order_id");
CREATE INDEX "system_account_ledger_view_tx_hash_idx" ON "system_account_ledger_view"("tx_hash");
CREATE INDEX "system_account_ledger_view_created_at_idx" ON "system_account_ledger_view"("created_at");

View File

@ -68,7 +68,7 @@ model Notification {
// 关联
readRecords NotificationRead[]
targetTags NotificationTagTarget[] // BY_TAG 时使用
targetTags NotificationTagTarget[] // BY_TAG 时使用
targetUsers NotificationUserTarget[] // SPECIFIC 时使用
@@index([isEnabled, publishedAt])
@ -110,8 +110,8 @@ enum NotificationPriority {
/// 目标用户类型
enum TargetType {
ALL // 所有用户
BY_TAG // 按标签匹配
ALL // 所有用户
BY_TAG // 按标签匹配
SPECIFIC // 指定用户列表
}
@ -180,28 +180,28 @@ model TagCategory {
/// 标签类型
enum TagType {
MANUAL // 手动打标 (管理员操作)
AUTO // 自动打标 (规则驱动)
MANUAL // 手动打标 (管理员操作)
AUTO // 自动打标 (规则驱动)
COMPUTED // 计算型 (实时计算,不存储关联)
SYSTEM // 系统内置 (不可删除)
SYSTEM // 系统内置 (不可删除)
}
/// 标签值类型
enum TagValueType {
BOOLEAN // 布尔型: 有/无
ENUM // 枚举型: 高/中/低
NUMBER // 数值型: 0-100分
STRING // 字符串型
ENUM // 枚举型: 高/中/低
NUMBER // 数值型: 0-100分
STRING // 字符串型
}
/// 用户标签定义
model UserTag {
id String @id @default(uuid())
categoryId String? @map("category_id")
code String @unique @db.VarChar(50) // "vip", "new_user", "whale"
name String @db.VarChar(100) // "VIP用户", "新用户", "大客户"
description String? @db.Text
color String? @db.VarChar(20) // "#FF5722"
id String @id @default(uuid())
categoryId String? @map("category_id")
code String @unique @db.VarChar(50) // "vip", "new_user", "whale"
name String @db.VarChar(100) // "VIP用户", "新用户", "大客户"
description String? @db.Text
color String? @db.VarChar(20) // "#FF5722"
type TagType @default(MANUAL) // 标签类型
valueType TagValueType @default(BOOLEAN) @map("value_type") // 标签值类型
@ -211,7 +211,7 @@ model UserTag {
enumValues Json? @map("enum_values")
// 关联的自动规则 (type=AUTO 时使用)
ruleId String? @unique @map("rule_id")
ruleId String? @unique @map("rule_id")
rule UserClassificationRule? @relation(fields: [ruleId], references: [id], onDelete: SetNull)
// 广告相关
@ -242,9 +242,9 @@ model UserTag {
/// 用户-标签关联
model UserTagAssignment {
id String @id @default(uuid())
accountSequence String @map("account_sequence") @db.VarChar(12)
tagId String @map("tag_id")
id String @id @default(uuid())
accountSequence String @map("account_sequence") @db.VarChar(12)
tagId String @map("tag_id")
// 标签值 (根据 valueType)
// BOOLEAN: null (存在即为true)
@ -344,16 +344,16 @@ model UserFeature {
/// 人群包用途
enum SegmentUsageType {
GENERAL // 通用
GENERAL // 通用
NOTIFICATION // 通知定向
ADVERTISING // 广告定向
ANALYTICS // 数据分析
ADVERTISING // 广告定向
ANALYTICS // 数据分析
}
/// 人群包 - 多条件组合的用户群
model AudienceSegment {
id String @id @default(uuid())
name String @db.VarChar(100) // "高价值活跃用户"
id String @id @default(uuid())
name String @db.VarChar(100) // "高价值活跃用户"
description String? @db.Text
// 定向条件 (JSON)
@ -476,29 +476,29 @@ model UserQueryView {
/// 推荐关系查询视图 - 通过 Debezium CDC 从 referral-service 同步
/// 用于 admin-web 查看用户推荐关系,避免跨服务调用
model ReferralQueryView {
id BigInt @id @map("relationship_id")
userId BigInt @unique @map("user_id")
accountSequence String @unique @map("account_sequence") @db.VarChar(12)
id BigInt @id @map("relationship_id")
userId BigInt @unique @map("user_id")
accountSequence String @unique @map("account_sequence") @db.VarChar(12)
// 推荐人信息
referrerId BigInt? @map("referrer_id")
rootUserId BigInt? @map("root_user_id")
referrerId BigInt? @map("referrer_id")
rootUserId BigInt? @map("root_user_id")
// 推荐码
myReferralCode String @unique @map("my_referral_code") @db.VarChar(20)
usedReferralCode String? @map("used_referral_code") @db.VarChar(20)
myReferralCode String @unique @map("my_referral_code") @db.VarChar(20)
usedReferralCode String? @map("used_referral_code") @db.VarChar(20)
// 推荐链信息
ancestorPath BigInt[] @map("ancestor_path")
depth Int @default(0) @map("depth")
ancestorPath BigInt[] @map("ancestor_path")
depth Int @default(0) @map("depth")
// 直推统计
directReferralCount Int @default(0) @map("direct_referral_count")
activeDirectCount Int @default(0) @map("active_direct_count")
directReferralCount Int @default(0) @map("direct_referral_count")
activeDirectCount Int @default(0) @map("active_direct_count")
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([referrerId])
@@index([accountSequence])
@ -509,6 +509,474 @@ model ReferralQueryView {
@@map("referral_query_view")
}
// =============================================================================
// Wallet Query Views (钱包查询视图 - 通过 CDC 同步)
// =============================================================================
/// 钱包账户查询视图 - 通过 Debezium CDC 从 wallet-service 同步
model WalletAccountQueryView {
id BigInt @id @map("wallet_id")
accountSequence String @unique @map("account_sequence") @db.VarChar(20)
userId BigInt @unique @map("user_id")
// USDT 余额
usdtAvailable Decimal @default(0) @map("usdt_available") @db.Decimal(20, 8)
usdtFrozen Decimal @default(0) @map("usdt_frozen") @db.Decimal(20, 8)
// DST 余额
dstAvailable Decimal @default(0) @map("dst_available") @db.Decimal(20, 8)
dstFrozen Decimal @default(0) @map("dst_frozen") @db.Decimal(20, 8)
// BNB 余额
bnbAvailable Decimal @default(0) @map("bnb_available") @db.Decimal(20, 8)
bnbFrozen Decimal @default(0) @map("bnb_frozen") @db.Decimal(20, 8)
// OG 余额
ogAvailable Decimal @default(0) @map("og_available") @db.Decimal(20, 8)
ogFrozen Decimal @default(0) @map("og_frozen") @db.Decimal(20, 8)
// RWAD 余额
rwadAvailable Decimal @default(0) @map("rwad_available") @db.Decimal(20, 8)
rwadFrozen Decimal @default(0) @map("rwad_frozen") @db.Decimal(20, 8)
// 算力
hashpower Decimal @default(0) @map("hashpower") @db.Decimal(20, 8)
// 待领取/可结算/已结算收益
pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8)
pendingHashpower Decimal @default(0) @map("pending_hashpower") @db.Decimal(20, 8)
settleableUsdt Decimal @default(0) @map("settleable_usdt") @db.Decimal(20, 8)
settleableHashpower Decimal @default(0) @map("settleable_hashpower") @db.Decimal(20, 8)
settledTotalUsdt Decimal @default(0) @map("settled_total_usdt") @db.Decimal(20, 8)
settledTotalHashpower Decimal @default(0) @map("settled_total_hashpower") @db.Decimal(20, 8)
expiredTotalUsdt Decimal @default(0) @map("expired_total_usdt") @db.Decimal(20, 8)
expiredTotalHashpower Decimal @default(0) @map("expired_total_hashpower") @db.Decimal(20, 8)
// 状态
status String @default("ACTIVE") @map("status") @db.VarChar(20)
hasPlanted Boolean @default(false) @map("has_planted")
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence])
@@index([usdtAvailable(sort: Desc)])
@@index([hashpower(sort: Desc)])
@@index([status])
@@map("wallet_account_query_view")
}
/// 提现订单查询视图 - 通过 Debezium CDC 从 wallet-service 同步
model WithdrawalOrderQueryView {
id BigInt @id @map("order_id")
orderNo String @unique @map("order_no") @db.VarChar(50)
accountSequence String @map("account_sequence") @db.VarChar(20)
userId BigInt @map("user_id")
// 提现信息
amount Decimal @map("amount") @db.Decimal(20, 8)
fee Decimal @map("fee") @db.Decimal(20, 8)
chainType String @map("chain_type") @db.VarChar(20)
toAddress String @map("to_address") @db.VarChar(100)
// 交易信息
txHash String? @map("tx_hash") @db.VarChar(100)
// 内部转账标识
isInternalTransfer Boolean @default(false) @map("is_internal_transfer")
toAccountSequence String? @map("to_account_sequence") @db.VarChar(20)
toUserId BigInt? @map("to_user_id")
// 状态
status String @default("PENDING") @map("status") @db.VarChar(20)
errorMessage String? @map("error_message") @db.VarChar(500)
// 时间戳
frozenAt DateTime? @map("frozen_at")
broadcastedAt DateTime? @map("broadcasted_at")
confirmedAt DateTime? @map("confirmed_at")
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence])
@@index([userId])
@@index([status])
@@index([chainType])
@@index([txHash])
@@index([createdAt])
@@map("withdrawal_order_query_view")
}
/// 法币提现订单查询视图 - 通过 Debezium CDC 从 wallet-service 同步
model FiatWithdrawalOrderQueryView {
id BigInt @id @map("order_id")
orderNo String @unique @map("order_no") @db.VarChar(50)
accountSequence String @map("account_sequence") @db.VarChar(20)
userId BigInt @map("user_id")
// 金额
amount Decimal @map("amount") @db.Decimal(20, 8)
fee Decimal @map("fee") @db.Decimal(20, 8)
// 收款方式
paymentMethod String @map("payment_method") @db.VarChar(20)
// 银行卡信息 (脱敏)
bankName String? @map("bank_name") @db.VarChar(100)
bankCardNoMasked String? @map("bank_card_no_masked") @db.VarChar(50)
cardHolderName String? @map("card_holder_name") @db.VarChar(100)
// 支付宝/微信 (脱敏)
alipayAccountMasked String? @map("alipay_account_masked") @db.VarChar(100)
wechatAccountMasked String? @map("wechat_account_masked") @db.VarChar(100)
// 状态
status String @default("PENDING") @map("status") @db.VarChar(20)
errorMessage String? @map("error_message") @db.VarChar(500)
// 审核信息
reviewedBy String? @map("reviewed_by") @db.VarChar(100)
reviewedAt DateTime? @map("reviewed_at")
reviewRemark String? @map("review_remark") @db.VarChar(500)
// 打款信息
paidBy String? @map("paid_by") @db.VarChar(100)
paidAt DateTime? @map("paid_at")
// 时间戳
frozenAt DateTime? @map("frozen_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence])
@@index([userId])
@@index([status])
@@index([paymentMethod])
@@index([reviewedBy])
@@index([createdAt])
@@map("fiat_withdrawal_order_query_view")
}
// =============================================================================
// Planting Query Views (认种查询视图 - 通过 CDC 同步)
// =============================================================================
/// 认种订单查询视图 - 通过 Debezium CDC 从 planting-service 同步
model PlantingOrderQueryView {
id BigInt @id @map("order_id")
orderNo String @unique @map("order_no") @db.VarChar(50)
userId BigInt @map("user_id")
accountSequence String @map("account_sequence") @db.VarChar(20)
// 认种信息
treeCount Int @map("tree_count")
totalAmount Decimal @map("total_amount") @db.Decimal(20, 8)
// 省市选择
selectedProvince String? @map("selected_province") @db.VarChar(10)
selectedCity String? @map("selected_city") @db.VarChar(10)
// 订单状态
status String @default("CREATED") @map("status") @db.VarChar(30)
// 底池信息
poolInjectionBatchId BigInt? @map("pool_injection_batch_id")
// 时间戳
createdAt DateTime @map("created_at")
paidAt DateTime? @map("paid_at")
fundAllocatedAt DateTime? @map("fund_allocated_at")
miningEnabledAt DateTime? @map("mining_enabled_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([userId])
@@index([accountSequence])
@@index([status])
@@index([selectedProvince, selectedCity])
@@index([createdAt])
@@index([paidAt])
@@map("planting_order_query_view")
}
/// 用户持仓查询视图 - 通过 Debezium CDC 从 planting-service 同步
model PlantingPositionQueryView {
id BigInt @id @map("position_id")
userId BigInt @unique @map("user_id")
// 持仓统计
totalTreeCount Int @default(0) @map("total_tree_count")
effectiveTreeCount Int @default(0) @map("effective_tree_count")
pendingTreeCount Int @default(0) @map("pending_tree_count")
// 挖矿状态
firstMiningStartAt DateTime? @map("first_mining_start_at")
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([totalTreeCount])
@@map("planting_position_query_view")
}
/// 合同签署任务查询视图 - 通过 Debezium CDC 从 planting-service 同步
model ContractSigningTaskQueryView {
id BigInt @id @map("task_id")
// 关联信息
orderNo String @unique @map("order_no") @db.VarChar(50)
contractNo String @unique @map("contract_no") @db.VarChar(30)
userId BigInt @map("user_id")
accountSequence String @map("account_sequence") @db.VarChar(20)
// 合同信息
contractVersion String @map("contract_version") @db.VarChar(20)
// 订单信息快照
treeCount Int @map("tree_count")
totalAmount Decimal @map("total_amount") @db.Decimal(20, 8)
provinceCode String @map("province_code") @db.VarChar(10)
provinceName String @map("province_name") @db.VarChar(50)
cityCode String @map("city_code") @db.VarChar(10)
cityName String @map("city_name") @db.VarChar(50)
// 签署状态
status String @default("PENDING") @map("status") @db.VarChar(30)
expiresAt DateTime @map("expires_at")
// 签署时间戳
signedAt DateTime? @map("signed_at")
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([userId])
@@index([status])
@@index([expiresAt])
@@map("contract_signing_task_query_view")
}
// =============================================================================
// Authorization Query Views (授权查询视图 - 通过 CDC 同步)
// =============================================================================
/// 授权角色查询视图 - 通过 Debezium CDC 从 authorization-service 同步
model AuthorizationRoleQueryView {
id String @id
userId String @map("user_id")
accountSequence String @map("account_sequence")
roleType String @map("role_type") @db.VarChar(30)
regionCode String @map("region_code")
regionName String @map("region_name")
status String @default("PENDING") @db.VarChar(20)
displayTitle String @map("display_title")
// 授权信息
authorizedAt DateTime? @map("authorized_at")
authorizedBy String? @map("authorized_by")
revokedAt DateTime? @map("revoked_at")
revokedBy String? @map("revoked_by")
// 软删除
deletedAt DateTime? @map("deleted_at")
// 考核配置
initialTargetTreeCount Int @map("initial_target_tree_count")
monthlyTargetType String @map("monthly_target_type") @db.VarChar(20)
// 权益状态
benefitActive Boolean @default(false) @map("benefit_active")
benefitActivatedAt DateTime? @map("benefit_activated_at")
// 月度考核追踪
lastAssessmentMonth String? @map("last_assessment_month")
monthlyTreesAdded Int @default(0) @map("monthly_trees_added")
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence])
@@index([userId])
@@index([roleType, regionCode])
@@index([status])
@@index([deletedAt])
@@map("authorization_role_query_view")
}
/// 月度考核查询视图 - 通过 Debezium CDC 从 authorization-service 同步
model MonthlyAssessmentQueryView {
id String @id
authorizationId String @map("authorization_id")
userId String @map("user_id")
accountSequence String @map("account_sequence")
roleType String @map("role_type") @db.VarChar(30)
regionCode String @map("region_code")
// 考核月份
assessmentMonth String @map("assessment_month")
monthIndex Int @map("month_index")
// 考核目标
monthlyTarget Int @map("monthly_target")
cumulativeTarget Int @map("cumulative_target")
// 完成情况
monthlyCompleted Int @default(0) @map("monthly_completed")
cumulativeCompleted Int @default(0) @map("cumulative_completed")
completedAt DateTime? @map("completed_at")
// 考核结果
result String @default("NOT_ASSESSED") @db.VarChar(20)
// 排名
rankingInRegion Int? @map("ranking_in_region")
isFirstPlace Boolean @default(false) @map("is_first_place")
// 豁免
isBypassed Boolean @default(false) @map("is_bypassed")
// 时间戳
assessedAt DateTime? @map("assessed_at")
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence, assessmentMonth])
@@index([userId, assessmentMonth])
@@index([roleType, regionCode, assessmentMonth])
@@index([assessmentMonth, result])
@@map("monthly_assessment_query_view")
}
/// 系统账户查询视图 - 通过 Debezium CDC 从 authorization-service 同步
model SystemAccountQueryView {
id BigInt @id @map("account_id")
accountType String @map("account_type") @db.VarChar(30)
// 区域信息
regionCode String? @map("region_code") @db.VarChar(10)
regionName String? @map("region_name") @db.VarChar(50)
// 钱包地址
walletAddress String? @map("wallet_address") @db.VarChar(42)
// 余额
usdtBalance Decimal @default(0) @map("usdt_balance") @db.Decimal(20, 8)
hashpower Decimal @default(0) @map("hashpower") @db.Decimal(20, 8)
// 累计统计
totalReceived Decimal @default(0) @map("total_received") @db.Decimal(20, 8)
totalTransferred Decimal @default(0) @map("total_transferred") @db.Decimal(20, 8)
// 状态
status String @default("ACTIVE") @map("status") @db.VarChar(20)
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountType])
@@index([walletAddress])
@@map("system_account_query_view")
}
// =============================================================================
// Ledger Views (分类账视图 - 通过 CDC 同步)
// =============================================================================
/// 钱包流水分类账 - 通过 Debezium CDC 从 wallet-service 同步
/// 记录所有用户钱包的资金变动(充值/提现/收益/扣款等)
model WalletLedgerEntryView {
id BigInt @id @map("entry_id")
accountSequence String @map("account_sequence") @db.VarChar(20)
userId BigInt @map("user_id")
// 流水类型
entryType String @map("entry_type") @db.VarChar(50)
// 金额变动 (正数入账, 负数支出)
amount Decimal @map("amount") @db.Decimal(20, 8)
assetType String @map("asset_type") @db.VarChar(20)
// 余额快照 (操作后余额)
balanceAfter Decimal? @map("balance_after") @db.Decimal(20, 8)
// 关联引用
refOrderId String? @map("ref_order_id") @db.VarChar(100)
refTxHash String? @map("ref_tx_hash") @db.VarChar(100)
// 备注
memo String? @map("memo") @db.Text
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence, createdAt(sort: Desc)])
@@index([userId, createdAt(sort: Desc)])
@@index([entryType])
@@index([assetType])
@@index([refOrderId])
@@index([refTxHash])
@@index([createdAt])
@@map("wallet_ledger_entry_view")
}
/// 认种资金分配分类账 - 通过 Debezium CDC 从 planting-service 同步
/// 记录每笔认种订单的资金分配明细
model FundAllocationView {
id BigInt @id @map("allocation_id")
orderId BigInt @map("order_id")
// 分配信息
targetType String @map("target_type") @db.VarChar(50)
amount Decimal @map("amount") @db.Decimal(20, 8)
targetAccountId String? @map("target_account_id") @db.VarChar(100)
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([orderId])
@@index([targetType, targetAccountId])
@@index([createdAt])
@@map("fund_allocation_view")
}
/// 系统账户流水分类账 - 通过 Debezium CDC 从 authorization-service 同步
/// 记录系统账户(成本账户、运营账户、区域账户)的资金变动
model SystemAccountLedgerView {
id BigInt @id @map("ledger_id")
accountId BigInt @map("account_id")
// 流水类型
entryType String @map("entry_type") @db.VarChar(30)
// 金额
amount Decimal @map("amount") @db.Decimal(20, 8)
balanceAfter Decimal @map("balance_after") @db.Decimal(20, 8)
// 关联信息
sourceOrderId BigInt? @map("source_order_id")
sourceRewardId BigInt? @map("source_reward_id")
txHash String? @map("tx_hash") @db.VarChar(66)
memo String? @map("memo") @db.VarChar(500)
// 时间戳
createdAt DateTime @map("created_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountId, createdAt(sort: Desc)])
@@index([sourceOrderId])
@@index([txHash])
@@index([createdAt])
@@map("system_account_ledger_view")
}
// =============================================================================
// Kafka Event Tracking (事件消费追踪)
// =============================================================================
@ -562,16 +1030,16 @@ model SystemConfig {
/// 系统维护公告 - 用于系统升级/维护期间阻断用户操作
model SystemMaintenance {
id String @id @default(uuid())
title String @db.VarChar(100) // 标题:如"系统升级中"
message String @db.Text // 说明:如"预计10:00恢复请稍候"
startTime DateTime @map("start_time") // 维护开始时间
endTime DateTime @map("end_time") // 维护结束时间
isActive Boolean @default(false) @map("is_active") // 是否激活
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String @map("created_by") // 创建人ID
updatedBy String? @map("updated_by") // 更新人ID
id String @id @default(uuid())
title String @db.VarChar(100) // 标题:如"系统升级中"
message String @db.Text // 说明:如"预计10:00恢复请稍候"
startTime DateTime @map("start_time") // 维护开始时间
endTime DateTime @map("end_time") // 维护结束时间
isActive Boolean @default(false) @map("is_active") // 是否激活
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String @map("created_by") // 创建人ID
updatedBy String? @map("updated_by") // 更新人ID
@@index([isActive])
@@index([startTime, endTime])
@ -584,12 +1052,12 @@ model SystemMaintenance {
/// 共管钱包会话状态
enum WalletSessionStatus {
WAITING // 等待参与方加入
READY // 所有参与方已就绪
WAITING // 等待参与方加入
READY // 所有参与方已就绪
PROCESSING // 密钥生成中
COMPLETED // 创建完成
FAILED // 创建失败
CANCELLED // 已取消
COMPLETED // 创建完成
FAILED // 创建失败
CANCELLED // 已取消
}
/// 共管钱包会话 - 钱包创建过程的会话记录

View File

@ -0,0 +1,655 @@
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs';
import { PrismaService } from '../persistence/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
/**
* Debezium CDC ( ExtractNewRecordState )
*/
// authorization_roles 表
interface CdcAuthorizationRolePayload {
id: string;
user_id: string;
account_sequence: string;
role_type: string;
region_code: string;
region_name: string;
status: string;
display_title: string;
authorized_at?: string | null;
authorized_by?: string | null;
revoked_at?: string | null;
revoked_by?: string | null;
deleted_at?: string | null;
initial_target_tree_count: number;
monthly_target_type: string;
benefit_active: boolean;
benefit_activated_at?: string | null;
last_assessment_month?: string | null;
monthly_trees_added: number;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// monthly_assessments 表
interface CdcMonthlyAssessmentPayload {
id: string;
authorization_id: string;
user_id: string;
account_sequence: string;
role_type: string;
region_code: string;
assessment_month: string;
month_index: number;
monthly_target: number;
cumulative_target: number;
monthly_completed: number;
cumulative_completed: number;
completed_at?: string | null;
result: string;
ranking_in_region?: number | null;
is_first_place: boolean;
is_bypassed: boolean;
assessed_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// system_account_ledgers 表 (系统账户分类账流水)
interface CdcSystemAccountLedgerPayload {
ledger_id: string;
account_id: string;
entry_type: string;
amount: string;
balance_after: string;
source_order_id?: string | null;
source_reward_id?: string | null;
tx_hash?: string | null;
memo?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// system_accounts 表
interface CdcSystemAccountPayload {
account_id: string;
account_type: string;
region_code?: string | null;
region_name?: string | null;
wallet_address?: string | null;
usdt_balance: string;
hashpower: string;
total_received: string;
total_transferred: string;
status: string;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
type CdcAuthorizationPayload = CdcAuthorizationRolePayload | CdcMonthlyAssessmentPayload | CdcSystemAccountLedgerPayload | CdcSystemAccountPayload;
/**
* Authorization CDC
*
* Debezium authorization-service PostgreSQL
*
* Topics:
* - cdc.authorization.public.authorization_roles
* - cdc.authorization.public.monthly_assessments
* - cdc.authorization.public.system_accounts
*/
@Injectable()
export class AuthorizationCdcConsumerService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(AuthorizationCdcConsumerService.name);
private kafka: Kafka;
private consumer: Consumer;
private isRunning = false;
// CDC Topics
private readonly cdcTopics = [
'cdc.authorization.public.authorization_roles',
'cdc.authorization.public.monthly_assessments',
'cdc.authorization.public.system_accounts',
'cdc.authorization.public.system_account_ledgers',
];
private readonly consumerGroup: string;
constructor(
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
) {
const brokers = (this.configService.get<string>('KAFKA_BROKERS', 'localhost:9092')).split(',');
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID', 'admin-service');
this.consumerGroup = this.configService.get<string>('KAFKA_AUTHORIZATION_CDC_GROUP', 'admin-service-authorization-cdc');
this.kafka = new Kafka({
clientId: `${clientId}-authorization-cdc`,
brokers,
logLevel: logLevel.WARN,
});
this.consumer = this.kafka.consumer({ groupId: this.consumerGroup });
this.logger.log(`[Authorization-CDC] Configured to consume topics: ${this.cdcTopics.join(', ')}`);
}
async onModuleInit() {
await this.start();
}
async onModuleDestroy() {
await this.stop();
}
async start(): Promise<void> {
if (this.isRunning) {
this.logger.warn('[Authorization-CDC] Consumer already running');
return;
}
try {
this.logger.log('[Authorization-CDC] Connecting to Kafka...');
await this.consumer.connect();
for (const topic of this.cdcTopics) {
await this.consumer.subscribe({
topic,
fromBeginning: false,
});
}
this.logger.log(`[Authorization-CDC] Subscribed to topics: ${this.cdcTopics.join(', ')}`);
await this.consumer.run({
eachMessage: async (payload: EachMessagePayload) => {
await this.handleMessage(payload);
},
});
this.isRunning = true;
this.logger.log('[Authorization-CDC] Consumer started successfully');
} catch (error) {
this.logger.error('[Authorization-CDC] Failed to start consumer:', error);
}
}
async stop(): Promise<void> {
if (!this.isRunning) return;
try {
await this.consumer.disconnect();
this.isRunning = false;
this.logger.log('[Authorization-CDC] Consumer stopped');
} catch (error) {
this.logger.error('[Authorization-CDC] Failed to stop consumer:', error);
}
}
private async handleMessage(payload: EachMessagePayload): Promise<void> {
const { topic, partition, message } = payload;
if (!message.value) {
this.logger.warn(`[Authorization-CDC] Empty message from ${topic}:${partition}`);
return;
}
try {
const data = JSON.parse(message.value.toString()) as CdcAuthorizationPayload;
const operation = data.__op;
const table = data.__table;
this.logger.debug(
`[Authorization-CDC] Received ${operation} event for table ${table}`
);
// 幂等性检查
const eventId = `authorization-cdc:${topic}:${partition}:${message.offset}`;
if (await this.isEventProcessed(eventId)) {
this.logger.debug(`[Authorization-CDC] Event ${eventId} already processed, skipping`);
return;
}
// 根据表名处理不同的事件
switch (table) {
case 'authorization_roles':
await this.processAuthorizationRoleEvent(data as CdcAuthorizationRolePayload);
break;
case 'monthly_assessments':
await this.processMonthlyAssessmentEvent(data as CdcMonthlyAssessmentPayload);
break;
case 'system_accounts':
await this.processSystemAccountEvent(data as CdcSystemAccountPayload);
break;
case 'system_account_ledgers':
await this.processSystemAccountLedgerEvent(data as CdcSystemAccountLedgerPayload);
break;
default:
this.logger.warn(`[Authorization-CDC] Unknown table: ${table}`);
}
// 记录已处理
await this.markEventProcessed(eventId, `authorization-cdc:${table}:${operation}`);
this.logger.log(
`[Authorization-CDC] ✓ Processed ${operation} for table: ${table}`
);
} catch (error) {
this.logger.error(`[Authorization-CDC] Failed to process message:`, error);
throw error; // 让 KafkaJS 重试
}
}
// ==================== authorization_roles 处理 ====================
private async processAuthorizationRoleEvent(data: CdcAuthorizationRolePayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleAuthorizationRoleDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleAuthorizationRoleCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleAuthorizationRoleUpdate(data);
}
}
private async handleAuthorizationRoleCreateOrSnapshot(data: CdcAuthorizationRolePayload): Promise<void> {
await this.prisma.authorizationRoleQueryView.upsert({
where: { id: data.id },
create: {
id: data.id,
userId: data.user_id,
accountSequence: data.account_sequence,
roleType: data.role_type,
regionCode: data.region_code,
regionName: data.region_name,
status: data.status,
displayTitle: data.display_title,
authorizedAt: data.authorized_at ? new Date(data.authorized_at) : null,
authorizedBy: data.authorized_by || null,
revokedAt: data.revoked_at ? new Date(data.revoked_at) : null,
revokedBy: data.revoked_by || null,
deletedAt: data.deleted_at ? new Date(data.deleted_at) : null,
initialTargetTreeCount: data.initial_target_tree_count,
monthlyTargetType: data.monthly_target_type,
benefitActive: data.benefit_active,
benefitActivatedAt: data.benefit_activated_at ? new Date(data.benefit_activated_at) : null,
lastAssessmentMonth: data.last_assessment_month || null,
monthlyTreesAdded: data.monthly_trees_added,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
userId: data.user_id,
accountSequence: data.account_sequence,
roleType: data.role_type,
regionCode: data.region_code,
regionName: data.region_name,
status: data.status,
displayTitle: data.display_title,
authorizedAt: data.authorized_at ? new Date(data.authorized_at) : null,
authorizedBy: data.authorized_by || null,
revokedAt: data.revoked_at ? new Date(data.revoked_at) : null,
revokedBy: data.revoked_by || null,
deletedAt: data.deleted_at ? new Date(data.deleted_at) : null,
initialTargetTreeCount: data.initial_target_tree_count,
monthlyTargetType: data.monthly_target_type,
benefitActive: data.benefit_active,
benefitActivatedAt: data.benefit_activated_at ? new Date(data.benefit_activated_at) : null,
lastAssessmentMonth: data.last_assessment_month || null,
monthlyTreesAdded: data.monthly_trees_added,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Created/Snapshot authorization role: ${data.id}`);
}
private async handleAuthorizationRoleUpdate(data: CdcAuthorizationRolePayload): Promise<void> {
const exists = await this.prisma.authorizationRoleQueryView.findUnique({
where: { id: data.id },
select: { id: true },
});
if (!exists) {
await this.handleAuthorizationRoleCreateOrSnapshot(data);
return;
}
await this.prisma.authorizationRoleQueryView.update({
where: { id: data.id },
data: {
status: data.status,
authorizedAt: data.authorized_at ? new Date(data.authorized_at) : null,
authorizedBy: data.authorized_by || null,
revokedAt: data.revoked_at ? new Date(data.revoked_at) : null,
revokedBy: data.revoked_by || null,
deletedAt: data.deleted_at ? new Date(data.deleted_at) : null,
benefitActive: data.benefit_active,
benefitActivatedAt: data.benefit_activated_at ? new Date(data.benefit_activated_at) : null,
lastAssessmentMonth: data.last_assessment_month || null,
monthlyTreesAdded: data.monthly_trees_added,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Updated authorization role: ${data.id}`);
}
private async handleAuthorizationRoleDelete(data: CdcAuthorizationRolePayload): Promise<void> {
try {
await this.prisma.authorizationRoleQueryView.delete({
where: { id: data.id },
});
this.logger.log(`[Authorization-CDC] Deleted authorization role: ${data.id}`);
} catch {
this.logger.warn(`[Authorization-CDC] Authorization role not found for delete: ${data.id}`);
}
}
// ==================== monthly_assessments 处理 ====================
private async processMonthlyAssessmentEvent(data: CdcMonthlyAssessmentPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleMonthlyAssessmentDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleMonthlyAssessmentCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleMonthlyAssessmentUpdate(data);
}
}
private async handleMonthlyAssessmentCreateOrSnapshot(data: CdcMonthlyAssessmentPayload): Promise<void> {
await this.prisma.monthlyAssessmentQueryView.upsert({
where: { id: data.id },
create: {
id: data.id,
authorizationId: data.authorization_id,
userId: data.user_id,
accountSequence: data.account_sequence,
roleType: data.role_type,
regionCode: data.region_code,
assessmentMonth: data.assessment_month,
monthIndex: data.month_index,
monthlyTarget: data.monthly_target,
cumulativeTarget: data.cumulative_target,
monthlyCompleted: data.monthly_completed,
cumulativeCompleted: data.cumulative_completed,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
result: data.result,
rankingInRegion: data.ranking_in_region || null,
isFirstPlace: data.is_first_place,
isBypassed: data.is_bypassed,
assessedAt: data.assessed_at ? new Date(data.assessed_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
authorizationId: data.authorization_id,
userId: data.user_id,
accountSequence: data.account_sequence,
roleType: data.role_type,
regionCode: data.region_code,
assessmentMonth: data.assessment_month,
monthIndex: data.month_index,
monthlyTarget: data.monthly_target,
cumulativeTarget: data.cumulative_target,
monthlyCompleted: data.monthly_completed,
cumulativeCompleted: data.cumulative_completed,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
result: data.result,
rankingInRegion: data.ranking_in_region || null,
isFirstPlace: data.is_first_place,
isBypassed: data.is_bypassed,
assessedAt: data.assessed_at ? new Date(data.assessed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Created/Snapshot monthly assessment: ${data.id}`);
}
private async handleMonthlyAssessmentUpdate(data: CdcMonthlyAssessmentPayload): Promise<void> {
const exists = await this.prisma.monthlyAssessmentQueryView.findUnique({
where: { id: data.id },
select: { id: true },
});
if (!exists) {
await this.handleMonthlyAssessmentCreateOrSnapshot(data);
return;
}
await this.prisma.monthlyAssessmentQueryView.update({
where: { id: data.id },
data: {
monthlyCompleted: data.monthly_completed,
cumulativeCompleted: data.cumulative_completed,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
result: data.result,
rankingInRegion: data.ranking_in_region || null,
isFirstPlace: data.is_first_place,
isBypassed: data.is_bypassed,
assessedAt: data.assessed_at ? new Date(data.assessed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Updated monthly assessment: ${data.id}`);
}
private async handleMonthlyAssessmentDelete(data: CdcMonthlyAssessmentPayload): Promise<void> {
try {
await this.prisma.monthlyAssessmentQueryView.delete({
where: { id: data.id },
});
this.logger.log(`[Authorization-CDC] Deleted monthly assessment: ${data.id}`);
} catch {
this.logger.warn(`[Authorization-CDC] Monthly assessment not found for delete: ${data.id}`);
}
}
// ==================== system_accounts 处理 ====================
private async processSystemAccountEvent(data: CdcSystemAccountPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleSystemAccountDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleSystemAccountCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleSystemAccountUpdate(data);
}
}
private async handleSystemAccountCreateOrSnapshot(data: CdcSystemAccountPayload): Promise<void> {
await this.prisma.systemAccountQueryView.upsert({
where: { id: BigInt(data.account_id) },
create: {
id: BigInt(data.account_id),
accountType: data.account_type,
regionCode: data.region_code || null,
regionName: data.region_name || null,
walletAddress: data.wallet_address || null,
usdtBalance: new Decimal(data.usdt_balance || '0'),
hashpower: new Decimal(data.hashpower || '0'),
totalReceived: new Decimal(data.total_received || '0'),
totalTransferred: new Decimal(data.total_transferred || '0'),
status: data.status,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
accountType: data.account_type,
regionCode: data.region_code || null,
regionName: data.region_name || null,
walletAddress: data.wallet_address || null,
usdtBalance: new Decimal(data.usdt_balance || '0'),
hashpower: new Decimal(data.hashpower || '0'),
totalReceived: new Decimal(data.total_received || '0'),
totalTransferred: new Decimal(data.total_transferred || '0'),
status: data.status,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Created/Snapshot system account: ${data.account_id} (${data.account_type})`);
}
private async handleSystemAccountUpdate(data: CdcSystemAccountPayload): Promise<void> {
const id = BigInt(data.account_id);
const exists = await this.prisma.systemAccountQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleSystemAccountCreateOrSnapshot(data);
return;
}
await this.prisma.systemAccountQueryView.update({
where: { id },
data: {
walletAddress: data.wallet_address || null,
usdtBalance: new Decimal(data.usdt_balance || '0'),
hashpower: new Decimal(data.hashpower || '0'),
totalReceived: new Decimal(data.total_received || '0'),
totalTransferred: new Decimal(data.total_transferred || '0'),
status: data.status,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Updated system account: ${data.account_id} (${data.account_type})`);
}
private async handleSystemAccountDelete(data: CdcSystemAccountPayload): Promise<void> {
const id = BigInt(data.account_id);
try {
await this.prisma.systemAccountQueryView.delete({
where: { id },
});
this.logger.log(`[Authorization-CDC] Deleted system account: ${data.account_id}`);
} catch {
this.logger.warn(`[Authorization-CDC] System account not found for delete: ${data.account_id}`);
}
}
// ==================== system_account_ledgers 处理 ====================
private async processSystemAccountLedgerEvent(data: CdcSystemAccountLedgerPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
// 系统账户流水是 append-only通常只有 create 和 snapshot
if (operation === 'd' || isDeleted) {
await this.handleSystemAccountLedgerDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleSystemAccountLedgerCreate(data);
} else if (operation === 'u') {
await this.handleSystemAccountLedgerUpdate(data);
}
}
private async handleSystemAccountLedgerCreate(data: CdcSystemAccountLedgerPayload): Promise<void> {
await this.prisma.systemAccountLedgerView.upsert({
where: { id: BigInt(data.ledger_id) },
create: {
id: BigInt(data.ledger_id),
accountId: BigInt(data.account_id),
entryType: data.entry_type,
amount: new Decimal(data.amount),
balanceAfter: new Decimal(data.balance_after),
sourceOrderId: data.source_order_id ? BigInt(data.source_order_id) : null,
sourceRewardId: data.source_reward_id ? BigInt(data.source_reward_id) : null,
txHash: data.tx_hash || null,
memo: data.memo || null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
accountId: BigInt(data.account_id),
entryType: data.entry_type,
amount: new Decimal(data.amount),
balanceAfter: new Decimal(data.balance_after),
sourceOrderId: data.source_order_id ? BigInt(data.source_order_id) : null,
sourceRewardId: data.source_reward_id ? BigInt(data.source_reward_id) : null,
txHash: data.tx_hash || null,
memo: data.memo || null,
syncedAt: new Date(),
},
});
this.logger.log(`[Authorization-CDC] Created system account ledger: ${data.ledger_id} (${data.entry_type})`);
}
private async handleSystemAccountLedgerUpdate(data: CdcSystemAccountLedgerPayload): Promise<void> {
await this.handleSystemAccountLedgerCreate(data);
this.logger.log(`[Authorization-CDC] Updated system account ledger: ${data.ledger_id}`);
}
private async handleSystemAccountLedgerDelete(data: CdcSystemAccountLedgerPayload): Promise<void> {
const id = BigInt(data.ledger_id);
try {
await this.prisma.systemAccountLedgerView.delete({
where: { id },
});
this.logger.log(`[Authorization-CDC] Deleted system account ledger: ${data.ledger_id}`);
} catch {
this.logger.warn(`[Authorization-CDC] System account ledger not found for delete: ${data.ledger_id}`);
}
}
// ==================== Helper Methods ====================
private async isEventProcessed(eventId: string): Promise<boolean> {
const count = await this.prisma.processedEvent.count({
where: { eventId },
});
return count > 0;
}
private async markEventProcessed(eventId: string, eventType: string): Promise<void> {
await this.prisma.processedEvent.create({
data: {
eventId,
eventType,
processedAt: new Date(),
},
});
}
/**
*
*/
getStatus(): { isRunning: boolean; topics: string[]; consumerGroup: string } {
return {
isRunning: this.isRunning,
topics: this.cdcTopics,
consumerGroup: this.consumerGroup,
};
}
}

View File

@ -2,3 +2,6 @@ export * from './kafka.module';
export * from './user-event-consumer.service';
export * from './cdc-consumer.service';
export * from './referral-cdc-consumer.service';
export * from './wallet-cdc-consumer.service';
export * from './planting-cdc-consumer.service';
export * from './authorization-cdc-consumer.service';

View File

@ -6,6 +6,9 @@ import { USER_QUERY_REPOSITORY } from '../../domain/repositories/user-query.repo
import { UserEventConsumerService } from './user-event-consumer.service';
import { CdcConsumerService } from './cdc-consumer.service';
import { ReferralCdcConsumerService } from './referral-cdc-consumer.service';
import { WalletCdcConsumerService } from './wallet-cdc-consumer.service';
import { PlantingCdcConsumerService } from './planting-cdc-consumer.service';
import { AuthorizationCdcConsumerService } from './authorization-cdc-consumer.service';
/**
* Kafka
@ -13,6 +16,9 @@ import { ReferralCdcConsumerService } from './referral-cdc-consumer.service';
* CDC - Debezium
* 1. CdcConsumerService - identity-service
* 2. ReferralCdcConsumerService - referral-service
* 3. WalletCdcConsumerService - wallet-service /
* 4. PlantingCdcConsumerService - planting-service //
* 5. AuthorizationCdcConsumerService - authorization-service //
*
* Outbox -
* 1. UserEventConsumerService -
@ -31,11 +37,20 @@ import { ReferralCdcConsumerService } from './referral-cdc-consumer.service';
CdcConsumerService,
// CDC 消费者 - referral-service
ReferralCdcConsumerService,
// CDC 消费者 - wallet-service
WalletCdcConsumerService,
// CDC 消费者 - planting-service
PlantingCdcConsumerService,
// CDC 消费者 - authorization-service
AuthorizationCdcConsumerService,
],
exports: [
UserEventConsumerService,
CdcConsumerService,
ReferralCdcConsumerService,
WalletCdcConsumerService,
PlantingCdcConsumerService,
AuthorizationCdcConsumerService,
USER_QUERY_REPOSITORY,
],
})

View File

@ -0,0 +1,600 @@
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs';
import { PrismaService } from '../persistence/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
/**
* Debezium CDC ( ExtractNewRecordState )
*/
// planting_orders 表
interface CdcPlantingOrderPayload {
order_id: string;
order_no: string;
user_id: string;
account_sequence: string;
tree_count: number;
total_amount: string;
selected_province?: string | null;
selected_city?: string | null;
status: string;
pool_injection_batch_id?: string | null;
created_at: string;
paid_at?: string | null;
fund_allocated_at?: string | null;
mining_enabled_at?: string | null;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// planting_positions 表
interface CdcPlantingPositionPayload {
position_id: string;
user_id: string;
total_tree_count: number;
effective_tree_count: number;
pending_tree_count: number;
first_mining_start_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// fund_allocations 表 (资金分配分类账)
interface CdcFundAllocationPayload {
allocation_id: string;
order_id: string;
target_type: string;
amount: string;
target_account_id?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// contract_signing_tasks 表
interface CdcContractSigningTaskPayload {
task_id: string;
order_no: string;
contract_no: string;
user_id: string;
account_sequence: string;
template_id: number;
contract_version: string;
tree_count: number;
total_amount: string;
province_code: string;
province_name: string;
city_code: string;
city_name: string;
status: string;
expires_at: string;
signed_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
type CdcPlantingPayload = CdcPlantingOrderPayload | CdcPlantingPositionPayload | CdcFundAllocationPayload | CdcContractSigningTaskPayload;
/**
* Planting CDC
*
* Debezium planting-service PostgreSQL
*
* Topics:
* - cdc.planting.public.planting_orders
* - cdc.planting.public.planting_positions
* - cdc.planting.public.contract_signing_tasks
*/
@Injectable()
export class PlantingCdcConsumerService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PlantingCdcConsumerService.name);
private kafka: Kafka;
private consumer: Consumer;
private isRunning = false;
// CDC Topics
private readonly cdcTopics = [
'cdc.planting.public.planting_orders',
'cdc.planting.public.planting_positions',
'cdc.planting.public.contract_signing_tasks',
'cdc.planting.public.fund_allocations',
];
private readonly consumerGroup: string;
constructor(
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
) {
const brokers = (this.configService.get<string>('KAFKA_BROKERS', 'localhost:9092')).split(',');
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID', 'admin-service');
this.consumerGroup = this.configService.get<string>('KAFKA_PLANTING_CDC_GROUP', 'admin-service-planting-cdc');
this.kafka = new Kafka({
clientId: `${clientId}-planting-cdc`,
brokers,
logLevel: logLevel.WARN,
});
this.consumer = this.kafka.consumer({ groupId: this.consumerGroup });
this.logger.log(`[Planting-CDC] Configured to consume topics: ${this.cdcTopics.join(', ')}`);
}
async onModuleInit() {
await this.start();
}
async onModuleDestroy() {
await this.stop();
}
async start(): Promise<void> {
if (this.isRunning) {
this.logger.warn('[Planting-CDC] Consumer already running');
return;
}
try {
this.logger.log('[Planting-CDC] Connecting to Kafka...');
await this.consumer.connect();
for (const topic of this.cdcTopics) {
await this.consumer.subscribe({
topic,
fromBeginning: false,
});
}
this.logger.log(`[Planting-CDC] Subscribed to topics: ${this.cdcTopics.join(', ')}`);
await this.consumer.run({
eachMessage: async (payload: EachMessagePayload) => {
await this.handleMessage(payload);
},
});
this.isRunning = true;
this.logger.log('[Planting-CDC] Consumer started successfully');
} catch (error) {
this.logger.error('[Planting-CDC] Failed to start consumer:', error);
}
}
async stop(): Promise<void> {
if (!this.isRunning) return;
try {
await this.consumer.disconnect();
this.isRunning = false;
this.logger.log('[Planting-CDC] Consumer stopped');
} catch (error) {
this.logger.error('[Planting-CDC] Failed to stop consumer:', error);
}
}
private async handleMessage(payload: EachMessagePayload): Promise<void> {
const { topic, partition, message } = payload;
if (!message.value) {
this.logger.warn(`[Planting-CDC] Empty message from ${topic}:${partition}`);
return;
}
try {
const data = JSON.parse(message.value.toString()) as CdcPlantingPayload;
const operation = data.__op;
const table = data.__table;
this.logger.debug(
`[Planting-CDC] Received ${operation} event for table ${table}`
);
// 幂等性检查
const eventId = `planting-cdc:${topic}:${partition}:${message.offset}`;
if (await this.isEventProcessed(eventId)) {
this.logger.debug(`[Planting-CDC] Event ${eventId} already processed, skipping`);
return;
}
// 根据表名处理不同的事件
switch (table) {
case 'planting_orders':
await this.processPlantingOrderEvent(data as CdcPlantingOrderPayload);
break;
case 'planting_positions':
await this.processPlantingPositionEvent(data as CdcPlantingPositionPayload);
break;
case 'contract_signing_tasks':
await this.processContractSigningTaskEvent(data as CdcContractSigningTaskPayload);
break;
case 'fund_allocations':
await this.processFundAllocationEvent(data as CdcFundAllocationPayload);
break;
default:
this.logger.warn(`[Planting-CDC] Unknown table: ${table}`);
}
// 记录已处理
await this.markEventProcessed(eventId, `planting-cdc:${table}:${operation}`);
this.logger.log(
`[Planting-CDC] ✓ Processed ${operation} for table: ${table}`
);
} catch (error) {
this.logger.error(`[Planting-CDC] Failed to process message:`, error);
throw error; // 让 KafkaJS 重试
}
}
// ==================== planting_orders 处理 ====================
private async processPlantingOrderEvent(data: CdcPlantingOrderPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handlePlantingOrderDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handlePlantingOrderCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handlePlantingOrderUpdate(data);
}
}
private async handlePlantingOrderCreateOrSnapshot(data: CdcPlantingOrderPayload): Promise<void> {
await this.prisma.plantingOrderQueryView.upsert({
where: { id: BigInt(data.order_id) },
create: {
id: BigInt(data.order_id),
orderNo: data.order_no,
userId: BigInt(data.user_id),
accountSequence: data.account_sequence,
treeCount: data.tree_count,
totalAmount: new Decimal(data.total_amount),
selectedProvince: data.selected_province || null,
selectedCity: data.selected_city || null,
status: data.status,
poolInjectionBatchId: data.pool_injection_batch_id ? BigInt(data.pool_injection_batch_id) : null,
createdAt: new Date(data.created_at),
paidAt: data.paid_at ? new Date(data.paid_at) : null,
fundAllocatedAt: data.fund_allocated_at ? new Date(data.fund_allocated_at) : null,
miningEnabledAt: data.mining_enabled_at ? new Date(data.mining_enabled_at) : null,
syncedAt: new Date(),
},
update: {
orderNo: data.order_no,
userId: BigInt(data.user_id),
accountSequence: data.account_sequence,
treeCount: data.tree_count,
totalAmount: new Decimal(data.total_amount),
selectedProvince: data.selected_province || null,
selectedCity: data.selected_city || null,
status: data.status,
poolInjectionBatchId: data.pool_injection_batch_id ? BigInt(data.pool_injection_batch_id) : null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
fundAllocatedAt: data.fund_allocated_at ? new Date(data.fund_allocated_at) : null,
miningEnabledAt: data.mining_enabled_at ? new Date(data.mining_enabled_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Created/Snapshot planting order: ${data.order_no}`);
}
private async handlePlantingOrderUpdate(data: CdcPlantingOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
const exists = await this.prisma.plantingOrderQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handlePlantingOrderCreateOrSnapshot(data);
return;
}
await this.prisma.plantingOrderQueryView.update({
where: { id },
data: {
selectedProvince: data.selected_province || null,
selectedCity: data.selected_city || null,
status: data.status,
poolInjectionBatchId: data.pool_injection_batch_id ? BigInt(data.pool_injection_batch_id) : null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
fundAllocatedAt: data.fund_allocated_at ? new Date(data.fund_allocated_at) : null,
miningEnabledAt: data.mining_enabled_at ? new Date(data.mining_enabled_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Updated planting order: ${data.order_no}`);
}
private async handlePlantingOrderDelete(data: CdcPlantingOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
try {
await this.prisma.plantingOrderQueryView.delete({
where: { id },
});
this.logger.log(`[Planting-CDC] Deleted planting order: ${data.order_no}`);
} catch {
this.logger.warn(`[Planting-CDC] Planting order not found for delete: ${data.order_no}`);
}
}
// ==================== planting_positions 处理 ====================
private async processPlantingPositionEvent(data: CdcPlantingPositionPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handlePlantingPositionDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handlePlantingPositionCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handlePlantingPositionUpdate(data);
}
}
private async handlePlantingPositionCreateOrSnapshot(data: CdcPlantingPositionPayload): Promise<void> {
await this.prisma.plantingPositionQueryView.upsert({
where: { id: BigInt(data.position_id) },
create: {
id: BigInt(data.position_id),
userId: BigInt(data.user_id),
totalTreeCount: data.total_tree_count,
effectiveTreeCount: data.effective_tree_count,
pendingTreeCount: data.pending_tree_count,
firstMiningStartAt: data.first_mining_start_at ? new Date(data.first_mining_start_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
userId: BigInt(data.user_id),
totalTreeCount: data.total_tree_count,
effectiveTreeCount: data.effective_tree_count,
pendingTreeCount: data.pending_tree_count,
firstMiningStartAt: data.first_mining_start_at ? new Date(data.first_mining_start_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Created/Snapshot planting position for user: ${data.user_id}`);
}
private async handlePlantingPositionUpdate(data: CdcPlantingPositionPayload): Promise<void> {
const id = BigInt(data.position_id);
const exists = await this.prisma.plantingPositionQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handlePlantingPositionCreateOrSnapshot(data);
return;
}
await this.prisma.plantingPositionQueryView.update({
where: { id },
data: {
totalTreeCount: data.total_tree_count,
effectiveTreeCount: data.effective_tree_count,
pendingTreeCount: data.pending_tree_count,
firstMiningStartAt: data.first_mining_start_at ? new Date(data.first_mining_start_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Updated planting position for user: ${data.user_id}`);
}
private async handlePlantingPositionDelete(data: CdcPlantingPositionPayload): Promise<void> {
const id = BigInt(data.position_id);
try {
await this.prisma.plantingPositionQueryView.delete({
where: { id },
});
this.logger.log(`[Planting-CDC] Deleted planting position for user: ${data.user_id}`);
} catch {
this.logger.warn(`[Planting-CDC] Planting position not found for delete: ${data.user_id}`);
}
}
// ==================== contract_signing_tasks 处理 ====================
private async processContractSigningTaskEvent(data: CdcContractSigningTaskPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleContractSigningTaskDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleContractSigningTaskCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleContractSigningTaskUpdate(data);
}
}
private async handleContractSigningTaskCreateOrSnapshot(data: CdcContractSigningTaskPayload): Promise<void> {
await this.prisma.contractSigningTaskQueryView.upsert({
where: { id: BigInt(data.task_id) },
create: {
id: BigInt(data.task_id),
orderNo: data.order_no,
contractNo: data.contract_no,
userId: BigInt(data.user_id),
accountSequence: data.account_sequence,
contractVersion: data.contract_version,
treeCount: data.tree_count,
totalAmount: new Decimal(data.total_amount),
provinceCode: data.province_code,
provinceName: data.province_name,
cityCode: data.city_code,
cityName: data.city_name,
status: data.status,
expiresAt: new Date(data.expires_at),
signedAt: data.signed_at ? new Date(data.signed_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
orderNo: data.order_no,
contractNo: data.contract_no,
userId: BigInt(data.user_id),
accountSequence: data.account_sequence,
contractVersion: data.contract_version,
treeCount: data.tree_count,
totalAmount: new Decimal(data.total_amount),
provinceCode: data.province_code,
provinceName: data.province_name,
cityCode: data.city_code,
cityName: data.city_name,
status: data.status,
expiresAt: new Date(data.expires_at),
signedAt: data.signed_at ? new Date(data.signed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Created/Snapshot contract signing task: ${data.contract_no}`);
}
private async handleContractSigningTaskUpdate(data: CdcContractSigningTaskPayload): Promise<void> {
const id = BigInt(data.task_id);
const exists = await this.prisma.contractSigningTaskQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleContractSigningTaskCreateOrSnapshot(data);
return;
}
await this.prisma.contractSigningTaskQueryView.update({
where: { id },
data: {
status: data.status,
signedAt: data.signed_at ? new Date(data.signed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Updated contract signing task: ${data.contract_no}`);
}
private async handleContractSigningTaskDelete(data: CdcContractSigningTaskPayload): Promise<void> {
const id = BigInt(data.task_id);
try {
await this.prisma.contractSigningTaskQueryView.delete({
where: { id },
});
this.logger.log(`[Planting-CDC] Deleted contract signing task: ${data.contract_no}`);
} catch {
this.logger.warn(`[Planting-CDC] Contract signing task not found for delete: ${data.contract_no}`);
}
}
// ==================== fund_allocations 处理 ====================
private async processFundAllocationEvent(data: CdcFundAllocationPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
// 资金分配是 append-only通常只有 create 和 snapshot
if (operation === 'd' || isDeleted) {
await this.handleFundAllocationDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleFundAllocationCreate(data);
} else if (operation === 'u') {
await this.handleFundAllocationUpdate(data);
}
}
private async handleFundAllocationCreate(data: CdcFundAllocationPayload): Promise<void> {
await this.prisma.fundAllocationView.upsert({
where: { id: BigInt(data.allocation_id) },
create: {
id: BigInt(data.allocation_id),
orderId: BigInt(data.order_id),
targetType: data.target_type,
amount: new Decimal(data.amount),
targetAccountId: data.target_account_id || null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
orderId: BigInt(data.order_id),
targetType: data.target_type,
amount: new Decimal(data.amount),
targetAccountId: data.target_account_id || null,
syncedAt: new Date(),
},
});
this.logger.log(`[Planting-CDC] Created fund allocation: ${data.allocation_id} (${data.target_type})`);
}
private async handleFundAllocationUpdate(data: CdcFundAllocationPayload): Promise<void> {
await this.handleFundAllocationCreate(data);
this.logger.log(`[Planting-CDC] Updated fund allocation: ${data.allocation_id}`);
}
private async handleFundAllocationDelete(data: CdcFundAllocationPayload): Promise<void> {
const id = BigInt(data.allocation_id);
try {
await this.prisma.fundAllocationView.delete({
where: { id },
});
this.logger.log(`[Planting-CDC] Deleted fund allocation: ${data.allocation_id}`);
} catch {
this.logger.warn(`[Planting-CDC] Fund allocation not found for delete: ${data.allocation_id}`);
}
}
// ==================== Helper Methods ====================
private async isEventProcessed(eventId: string): Promise<boolean> {
const count = await this.prisma.processedEvent.count({
where: { eventId },
});
return count > 0;
}
private async markEventProcessed(eventId: string, eventType: string): Promise<void> {
await this.prisma.processedEvent.create({
data: {
eventId,
eventType,
processedAt: new Date(),
},
});
}
/**
*
*/
getStatus(): { isRunning: boolean; topics: string[]; consumerGroup: string } {
return {
isRunning: this.isRunning,
topics: this.cdcTopics,
consumerGroup: this.consumerGroup,
};
}
}

View File

@ -0,0 +1,738 @@
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs';
import { PrismaService } from '../persistence/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
/**
* Debezium CDC ( ExtractNewRecordState )
*/
// wallet_accounts 表
interface CdcWalletAccountPayload {
wallet_id: string;
account_sequence: string;
user_id: string;
usdt_available: string;
usdt_frozen: string;
dst_available: string;
dst_frozen: string;
bnb_available: string;
bnb_frozen: string;
og_available: string;
og_frozen: string;
rwad_available: string;
rwad_frozen: string;
hashpower: string;
pending_usdt: string;
pending_hashpower: string;
settleable_usdt: string;
settleable_hashpower: string;
settled_total_usdt: string;
settled_total_hashpower: string;
expired_total_usdt: string;
expired_total_hashpower: string;
status: string;
has_planted: boolean;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// withdrawal_orders 表
interface CdcWithdrawalOrderPayload {
order_id: string;
order_no: string;
account_sequence: string;
user_id: string;
amount: string;
fee: string;
chain_type: string;
to_address: string;
tx_hash?: string | null;
is_internal_transfer: boolean;
to_account_sequence?: string | null;
to_user_id?: string | null;
status: string;
error_message?: string | null;
frozen_at?: string | null;
broadcasted_at?: string | null;
confirmed_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// fiat_withdrawal_orders 表
interface CdcFiatWithdrawalOrderPayload {
order_id: string;
order_no: string;
account_sequence: string;
user_id: string;
amount: string;
fee: string;
payment_method: string;
bank_name?: string | null;
bank_card_no?: string | null;
card_holder_name?: string | null;
alipay_account?: string | null;
alipay_real_name?: string | null;
wechat_account?: string | null;
wechat_real_name?: string | null;
status: string;
error_message?: string | null;
reviewed_by?: string | null;
reviewed_at?: string | null;
review_remark?: string | null;
paid_by?: string | null;
paid_at?: string | null;
frozen_at?: string | null;
completed_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// wallet_ledger_entries 表 (分类账流水)
interface CdcWalletLedgerEntryPayload {
entry_id: string;
account_sequence: string;
user_id: string;
entry_type: string;
amount: string;
asset_type: string;
balance_after?: string | null;
ref_order_id?: string | null;
ref_tx_hash?: string | null;
memo?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
type CdcWalletPayload = CdcWalletAccountPayload | CdcWithdrawalOrderPayload | CdcFiatWithdrawalOrderPayload | CdcWalletLedgerEntryPayload;
/**
* Wallet CDC
*
* Debezium wallet-service PostgreSQL
*
* Topics:
* - cdc.wallet.public.wallet_accounts
* - cdc.wallet.public.withdrawal_orders
* - cdc.wallet.public.fiat_withdrawal_orders
*/
@Injectable()
export class WalletCdcConsumerService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(WalletCdcConsumerService.name);
private kafka: Kafka;
private consumer: Consumer;
private isRunning = false;
// CDC Topics
private readonly cdcTopics = [
'cdc.wallet.public.wallet_accounts',
'cdc.wallet.public.withdrawal_orders',
'cdc.wallet.public.fiat_withdrawal_orders',
'cdc.wallet.public.wallet_ledger_entries',
];
private readonly consumerGroup: string;
constructor(
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
) {
const brokers = (this.configService.get<string>('KAFKA_BROKERS', 'localhost:9092')).split(',');
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID', 'admin-service');
this.consumerGroup = this.configService.get<string>('KAFKA_WALLET_CDC_GROUP', 'admin-service-wallet-cdc');
this.kafka = new Kafka({
clientId: `${clientId}-wallet-cdc`,
brokers,
logLevel: logLevel.WARN,
});
this.consumer = this.kafka.consumer({ groupId: this.consumerGroup });
this.logger.log(`[Wallet-CDC] Configured to consume topics: ${this.cdcTopics.join(', ')}`);
}
async onModuleInit() {
await this.start();
}
async onModuleDestroy() {
await this.stop();
}
async start(): Promise<void> {
if (this.isRunning) {
this.logger.warn('[Wallet-CDC] Consumer already running');
return;
}
try {
this.logger.log('[Wallet-CDC] Connecting to Kafka...');
await this.consumer.connect();
for (const topic of this.cdcTopics) {
await this.consumer.subscribe({
topic,
fromBeginning: false,
});
}
this.logger.log(`[Wallet-CDC] Subscribed to topics: ${this.cdcTopics.join(', ')}`);
await this.consumer.run({
eachMessage: async (payload: EachMessagePayload) => {
await this.handleMessage(payload);
},
});
this.isRunning = true;
this.logger.log('[Wallet-CDC] Consumer started successfully');
} catch (error) {
this.logger.error('[Wallet-CDC] Failed to start consumer:', error);
}
}
async stop(): Promise<void> {
if (!this.isRunning) return;
try {
await this.consumer.disconnect();
this.isRunning = false;
this.logger.log('[Wallet-CDC] Consumer stopped');
} catch (error) {
this.logger.error('[Wallet-CDC] Failed to stop consumer:', error);
}
}
private async handleMessage(payload: EachMessagePayload): Promise<void> {
const { topic, partition, message } = payload;
if (!message.value) {
this.logger.warn(`[Wallet-CDC] Empty message from ${topic}:${partition}`);
return;
}
try {
const data = JSON.parse(message.value.toString()) as CdcWalletPayload;
const operation = data.__op;
const table = data.__table;
this.logger.debug(
`[Wallet-CDC] Received ${operation} event for table ${table}`
);
// 幂等性检查
const eventId = `wallet-cdc:${topic}:${partition}:${message.offset}`;
if (await this.isEventProcessed(eventId)) {
this.logger.debug(`[Wallet-CDC] Event ${eventId} already processed, skipping`);
return;
}
// 根据表名处理不同的事件
switch (table) {
case 'wallet_accounts':
await this.processWalletAccountEvent(data as CdcWalletAccountPayload);
break;
case 'withdrawal_orders':
await this.processWithdrawalOrderEvent(data as CdcWithdrawalOrderPayload);
break;
case 'fiat_withdrawal_orders':
await this.processFiatWithdrawalOrderEvent(data as CdcFiatWithdrawalOrderPayload);
break;
case 'wallet_ledger_entries':
await this.processWalletLedgerEntryEvent(data as CdcWalletLedgerEntryPayload);
break;
default:
this.logger.warn(`[Wallet-CDC] Unknown table: ${table}`);
}
// 记录已处理
await this.markEventProcessed(eventId, `wallet-cdc:${table}:${operation}`);
this.logger.log(
`[Wallet-CDC] ✓ Processed ${operation} for table: ${table}`
);
} catch (error) {
this.logger.error(`[Wallet-CDC] Failed to process message:`, error);
throw error; // 让 KafkaJS 重试
}
}
// ==================== wallet_accounts 处理 ====================
private async processWalletAccountEvent(data: CdcWalletAccountPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleWalletAccountDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleWalletAccountCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleWalletAccountUpdate(data);
}
}
private async handleWalletAccountCreateOrSnapshot(data: CdcWalletAccountPayload): Promise<void> {
await this.prisma.walletAccountQueryView.upsert({
where: { id: BigInt(data.wallet_id) },
create: {
id: BigInt(data.wallet_id),
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
usdtAvailable: new Decimal(data.usdt_available || '0'),
usdtFrozen: new Decimal(data.usdt_frozen || '0'),
dstAvailable: new Decimal(data.dst_available || '0'),
dstFrozen: new Decimal(data.dst_frozen || '0'),
bnbAvailable: new Decimal(data.bnb_available || '0'),
bnbFrozen: new Decimal(data.bnb_frozen || '0'),
ogAvailable: new Decimal(data.og_available || '0'),
ogFrozen: new Decimal(data.og_frozen || '0'),
rwadAvailable: new Decimal(data.rwad_available || '0'),
rwadFrozen: new Decimal(data.rwad_frozen || '0'),
hashpower: new Decimal(data.hashpower || '0'),
pendingUsdt: new Decimal(data.pending_usdt || '0'),
pendingHashpower: new Decimal(data.pending_hashpower || '0'),
settleableUsdt: new Decimal(data.settleable_usdt || '0'),
settleableHashpower: new Decimal(data.settleable_hashpower || '0'),
settledTotalUsdt: new Decimal(data.settled_total_usdt || '0'),
settledTotalHashpower: new Decimal(data.settled_total_hashpower || '0'),
expiredTotalUsdt: new Decimal(data.expired_total_usdt || '0'),
expiredTotalHashpower: new Decimal(data.expired_total_hashpower || '0'),
status: data.status,
hasPlanted: data.has_planted,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
usdtAvailable: new Decimal(data.usdt_available || '0'),
usdtFrozen: new Decimal(data.usdt_frozen || '0'),
dstAvailable: new Decimal(data.dst_available || '0'),
dstFrozen: new Decimal(data.dst_frozen || '0'),
bnbAvailable: new Decimal(data.bnb_available || '0'),
bnbFrozen: new Decimal(data.bnb_frozen || '0'),
ogAvailable: new Decimal(data.og_available || '0'),
ogFrozen: new Decimal(data.og_frozen || '0'),
rwadAvailable: new Decimal(data.rwad_available || '0'),
rwadFrozen: new Decimal(data.rwad_frozen || '0'),
hashpower: new Decimal(data.hashpower || '0'),
pendingUsdt: new Decimal(data.pending_usdt || '0'),
pendingHashpower: new Decimal(data.pending_hashpower || '0'),
settleableUsdt: new Decimal(data.settleable_usdt || '0'),
settleableHashpower: new Decimal(data.settleable_hashpower || '0'),
settledTotalUsdt: new Decimal(data.settled_total_usdt || '0'),
settledTotalHashpower: new Decimal(data.settled_total_hashpower || '0'),
expiredTotalUsdt: new Decimal(data.expired_total_usdt || '0'),
expiredTotalHashpower: new Decimal(data.expired_total_hashpower || '0'),
status: data.status,
hasPlanted: data.has_planted,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created/Snapshot wallet account: ${data.account_sequence}`);
}
private async handleWalletAccountUpdate(data: CdcWalletAccountPayload): Promise<void> {
const id = BigInt(data.wallet_id);
const exists = await this.prisma.walletAccountQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleWalletAccountCreateOrSnapshot(data);
return;
}
await this.prisma.walletAccountQueryView.update({
where: { id },
data: {
usdtAvailable: new Decimal(data.usdt_available || '0'),
usdtFrozen: new Decimal(data.usdt_frozen || '0'),
dstAvailable: new Decimal(data.dst_available || '0'),
dstFrozen: new Decimal(data.dst_frozen || '0'),
bnbAvailable: new Decimal(data.bnb_available || '0'),
bnbFrozen: new Decimal(data.bnb_frozen || '0'),
ogAvailable: new Decimal(data.og_available || '0'),
ogFrozen: new Decimal(data.og_frozen || '0'),
rwadAvailable: new Decimal(data.rwad_available || '0'),
rwadFrozen: new Decimal(data.rwad_frozen || '0'),
hashpower: new Decimal(data.hashpower || '0'),
pendingUsdt: new Decimal(data.pending_usdt || '0'),
pendingHashpower: new Decimal(data.pending_hashpower || '0'),
settleableUsdt: new Decimal(data.settleable_usdt || '0'),
settleableHashpower: new Decimal(data.settleable_hashpower || '0'),
settledTotalUsdt: new Decimal(data.settled_total_usdt || '0'),
settledTotalHashpower: new Decimal(data.settled_total_hashpower || '0'),
expiredTotalUsdt: new Decimal(data.expired_total_usdt || '0'),
expiredTotalHashpower: new Decimal(data.expired_total_hashpower || '0'),
status: data.status,
hasPlanted: data.has_planted,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Updated wallet account: ${data.account_sequence}`);
}
private async handleWalletAccountDelete(data: CdcWalletAccountPayload): Promise<void> {
const id = BigInt(data.wallet_id);
try {
await this.prisma.walletAccountQueryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted wallet account: ${data.account_sequence}`);
} catch {
this.logger.warn(`[Wallet-CDC] Wallet account not found for delete: ${data.account_sequence}`);
}
}
// ==================== withdrawal_orders 处理 ====================
private async processWithdrawalOrderEvent(data: CdcWithdrawalOrderPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleWithdrawalOrderDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleWithdrawalOrderCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleWithdrawalOrderUpdate(data);
}
}
private async handleWithdrawalOrderCreateOrSnapshot(data: CdcWithdrawalOrderPayload): Promise<void> {
await this.prisma.withdrawalOrderQueryView.upsert({
where: { id: BigInt(data.order_id) },
create: {
id: BigInt(data.order_id),
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
chainType: data.chain_type,
toAddress: data.to_address,
txHash: data.tx_hash || null,
isInternalTransfer: data.is_internal_transfer,
toAccountSequence: data.to_account_sequence || null,
toUserId: data.to_user_id ? BigInt(data.to_user_id) : null,
status: data.status,
errorMessage: data.error_message || null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
broadcastedAt: data.broadcasted_at ? new Date(data.broadcasted_at) : null,
confirmedAt: data.confirmed_at ? new Date(data.confirmed_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
chainType: data.chain_type,
toAddress: data.to_address,
txHash: data.tx_hash || null,
isInternalTransfer: data.is_internal_transfer,
toAccountSequence: data.to_account_sequence || null,
toUserId: data.to_user_id ? BigInt(data.to_user_id) : null,
status: data.status,
errorMessage: data.error_message || null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
broadcastedAt: data.broadcasted_at ? new Date(data.broadcasted_at) : null,
confirmedAt: data.confirmed_at ? new Date(data.confirmed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created/Snapshot withdrawal order: ${data.order_no}`);
}
private async handleWithdrawalOrderUpdate(data: CdcWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
const exists = await this.prisma.withdrawalOrderQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleWithdrawalOrderCreateOrSnapshot(data);
return;
}
await this.prisma.withdrawalOrderQueryView.update({
where: { id },
data: {
txHash: data.tx_hash || null,
status: data.status,
errorMessage: data.error_message || null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
broadcastedAt: data.broadcasted_at ? new Date(data.broadcasted_at) : null,
confirmedAt: data.confirmed_at ? new Date(data.confirmed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Updated withdrawal order: ${data.order_no}`);
}
private async handleWithdrawalOrderDelete(data: CdcWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
try {
await this.prisma.withdrawalOrderQueryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted withdrawal order: ${data.order_no}`);
} catch {
this.logger.warn(`[Wallet-CDC] Withdrawal order not found for delete: ${data.order_no}`);
}
}
// ==================== fiat_withdrawal_orders 处理 ====================
private async processFiatWithdrawalOrderEvent(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleFiatWithdrawalOrderDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleFiatWithdrawalOrderCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleFiatWithdrawalOrderUpdate(data);
}
}
private maskBankCardNo(cardNo: string | null | undefined): string | null {
if (!cardNo) return null;
if (cardNo.length <= 8) return cardNo;
return cardNo.slice(0, 4) + '****' + cardNo.slice(-4);
}
private maskAccount(account: string | null | undefined): string | null {
if (!account) return null;
if (account.length <= 4) return account;
const visible = Math.min(3, Math.floor(account.length / 3));
return account.slice(0, visible) + '****' + account.slice(-visible);
}
private async handleFiatWithdrawalOrderCreateOrSnapshot(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
await this.prisma.fiatWithdrawalOrderQueryView.upsert({
where: { id: BigInt(data.order_id) },
create: {
id: BigInt(data.order_id),
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
paymentMethod: data.payment_method,
bankName: data.bank_name || null,
bankCardNoMasked: this.maskBankCardNo(data.bank_card_no),
cardHolderName: data.card_holder_name || null,
alipayAccountMasked: this.maskAccount(data.alipay_account),
wechatAccountMasked: this.maskAccount(data.wechat_account),
status: data.status,
errorMessage: data.error_message || null,
reviewedBy: data.reviewed_by || null,
reviewedAt: data.reviewed_at ? new Date(data.reviewed_at) : null,
reviewRemark: data.review_remark || null,
paidBy: data.paid_by || null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
paymentMethod: data.payment_method,
bankName: data.bank_name || null,
bankCardNoMasked: this.maskBankCardNo(data.bank_card_no),
cardHolderName: data.card_holder_name || null,
alipayAccountMasked: this.maskAccount(data.alipay_account),
wechatAccountMasked: this.maskAccount(data.wechat_account),
status: data.status,
errorMessage: data.error_message || null,
reviewedBy: data.reviewed_by || null,
reviewedAt: data.reviewed_at ? new Date(data.reviewed_at) : null,
reviewRemark: data.review_remark || null,
paidBy: data.paid_by || null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created/Snapshot fiat withdrawal order: ${data.order_no}`);
}
private async handleFiatWithdrawalOrderUpdate(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
const exists = await this.prisma.fiatWithdrawalOrderQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleFiatWithdrawalOrderCreateOrSnapshot(data);
return;
}
await this.prisma.fiatWithdrawalOrderQueryView.update({
where: { id },
data: {
status: data.status,
errorMessage: data.error_message || null,
reviewedBy: data.reviewed_by || null,
reviewedAt: data.reviewed_at ? new Date(data.reviewed_at) : null,
reviewRemark: data.review_remark || null,
paidBy: data.paid_by || null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Updated fiat withdrawal order: ${data.order_no}`);
}
private async handleFiatWithdrawalOrderDelete(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
try {
await this.prisma.fiatWithdrawalOrderQueryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted fiat withdrawal order: ${data.order_no}`);
} catch {
this.logger.warn(`[Wallet-CDC] Fiat withdrawal order not found for delete: ${data.order_no}`);
}
}
// ==================== wallet_ledger_entries 处理 ====================
private async processWalletLedgerEntryEvent(data: CdcWalletLedgerEntryPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
// 分类账流水是 append-only通常只有 create 和 snapshot
// 但也处理更新和删除情况以保持完整性
if (operation === 'd' || isDeleted) {
await this.handleWalletLedgerEntryDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleWalletLedgerEntryCreate(data);
} else if (operation === 'u') {
await this.handleWalletLedgerEntryUpdate(data);
}
}
private async handleWalletLedgerEntryCreate(data: CdcWalletLedgerEntryPayload): Promise<void> {
await this.prisma.walletLedgerEntryView.upsert({
where: { id: BigInt(data.entry_id) },
create: {
id: BigInt(data.entry_id),
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
entryType: data.entry_type,
amount: new Decimal(data.amount),
assetType: data.asset_type,
balanceAfter: data.balance_after ? new Decimal(data.balance_after) : null,
refOrderId: data.ref_order_id || null,
refTxHash: data.ref_tx_hash || null,
memo: data.memo || null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
entryType: data.entry_type,
amount: new Decimal(data.amount),
assetType: data.asset_type,
balanceAfter: data.balance_after ? new Decimal(data.balance_after) : null,
refOrderId: data.ref_order_id || null,
refTxHash: data.ref_tx_hash || null,
memo: data.memo || null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created ledger entry: ${data.entry_id} (${data.entry_type})`);
}
private async handleWalletLedgerEntryUpdate(data: CdcWalletLedgerEntryPayload): Promise<void> {
// 理论上分类账不应该被更新,但为了完整性还是处理
await this.handleWalletLedgerEntryCreate(data);
this.logger.log(`[Wallet-CDC] Updated ledger entry: ${data.entry_id}`);
}
private async handleWalletLedgerEntryDelete(data: CdcWalletLedgerEntryPayload): Promise<void> {
const id = BigInt(data.entry_id);
try {
await this.prisma.walletLedgerEntryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted ledger entry: ${data.entry_id}`);
} catch {
this.logger.warn(`[Wallet-CDC] Ledger entry not found for delete: ${data.entry_id}`);
}
}
// ==================== Helper Methods ====================
private async isEventProcessed(eventId: string): Promise<boolean> {
const count = await this.prisma.processedEvent.count({
where: { eventId },
});
return count > 0;
}
private async markEventProcessed(eventId: string, eventType: string): Promise<void> {
await this.prisma.processedEvent.create({
data: {
eventId,
eventType,
processedAt: new Date(),
},
});
}
/**
*
*/
getStatus(): { isRunning: boolean; topics: string[]; consumerGroup: string } {
return {
isRunning: this.isRunning,
topics: this.cdcTopics,
consumerGroup: this.consumerGroup,
};
}
}

View File

@ -204,7 +204,7 @@ up() {
# Wait for Debezium Connect to be ready
log_info "Waiting for Debezium Connect to be ready..."
for i in {1..30}; do
if curl -s http://localhost:8083/ > /dev/null 2>&1; then
if curl -s http://localhost:8084/ > /dev/null 2>&1; then
log_info "Debezium Connect is ready!"
break
fi
@ -231,7 +231,7 @@ register_debezium_connectors() {
log_info "Registering Debezium connectors..."
# Check existing connectors
EXISTING=$(curl -s http://localhost:8083/connectors 2>/dev/null || echo "[]")
EXISTING=$(curl -s http://localhost:8084/connectors 2>/dev/null || echo "[]")
# Read database credentials from .env
source "$ENV_FILE"
@ -276,7 +276,7 @@ register_debezium_connectors() {
RESULT=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$IDENTITY_CONFIG" \
"http://localhost:8083/connectors" 2>/dev/null || echo "failed")
"http://localhost:8084/connectors" 2>/dev/null || echo "failed")
if echo "$RESULT" | grep -q "identity-postgres-connector"; then
log_info "identity-postgres-connector registered successfully"
@ -325,7 +325,7 @@ register_debezium_connectors() {
RESULT=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$REFERRAL_CONFIG" \
"http://localhost:8083/connectors" 2>/dev/null || echo "failed")
"http://localhost:8084/connectors" 2>/dev/null || echo "failed")
if echo "$RESULT" | grep -q "referral-postgres-connector"; then
log_info "referral-postgres-connector registered successfully"
@ -333,6 +333,153 @@ register_debezium_connectors() {
log_warn "Failed to register referral connector: $RESULT"
fi
fi
# Register wallet-postgres-connector
if echo "$EXISTING" | grep -q "wallet-postgres-connector"; then
log_info "wallet-postgres-connector already registered"
else
log_info "Registering wallet-postgres-connector..."
WALLET_CONFIG='{
"name": "wallet-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"tasks.max": "1",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "'${POSTGRES_USER:-rwa_user}'",
"database.password": "'${POSTGRES_PASSWORD:-rwa_secure_password}'",
"database.dbname": "rwa_wallet",
"topic.prefix": "cdc.wallet",
"table.include.list": "public.wallet_accounts,public.withdrawal_orders,public.fiat_withdrawal_orders,public.wallet_ledger_entries",
"plugin.name": "pgoutput",
"publication.name": "debezium_wallet_publication",
"publication.autocreate.mode": "filtered",
"slot.name": "debezium_wallet_slot",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.drop.tombstones": "true",
"transforms.unwrap.delete.handling.mode": "rewrite",
"transforms.unwrap.add.fields": "op,table,source.ts_ms",
"heartbeat.interval.ms": "10000",
"snapshot.mode": "initial",
"decimal.handling.mode": "string",
"time.precision.mode": "connect"
}
}'
RESULT=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$WALLET_CONFIG" \
"http://localhost:8084/connectors" 2>/dev/null || echo "failed")
if echo "$RESULT" | grep -q "wallet-postgres-connector"; then
log_info "wallet-postgres-connector registered successfully"
else
log_warn "Failed to register wallet connector: $RESULT"
fi
fi
# Register planting-postgres-connector
if echo "$EXISTING" | grep -q "planting-postgres-connector"; then
log_info "planting-postgres-connector already registered"
else
log_info "Registering planting-postgres-connector..."
PLANTING_CONFIG='{
"name": "planting-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"tasks.max": "1",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "'${POSTGRES_USER:-rwa_user}'",
"database.password": "'${POSTGRES_PASSWORD:-rwa_secure_password}'",
"database.dbname": "rwa_planting",
"topic.prefix": "cdc.planting",
"table.include.list": "public.planting_orders,public.planting_positions,public.contract_signing_tasks,public.fund_allocations",
"plugin.name": "pgoutput",
"publication.name": "debezium_planting_publication",
"publication.autocreate.mode": "filtered",
"slot.name": "debezium_planting_slot",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.drop.tombstones": "true",
"transforms.unwrap.delete.handling.mode": "rewrite",
"transforms.unwrap.add.fields": "op,table,source.ts_ms",
"heartbeat.interval.ms": "10000",
"snapshot.mode": "initial",
"decimal.handling.mode": "string",
"time.precision.mode": "connect"
}
}'
RESULT=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$PLANTING_CONFIG" \
"http://localhost:8084/connectors" 2>/dev/null || echo "failed")
if echo "$RESULT" | grep -q "planting-postgres-connector"; then
log_info "planting-postgres-connector registered successfully"
else
log_warn "Failed to register planting connector: $RESULT"
fi
fi
# Register authorization-postgres-connector
if echo "$EXISTING" | grep -q "authorization-postgres-connector"; then
log_info "authorization-postgres-connector already registered"
else
log_info "Registering authorization-postgres-connector..."
AUTHORIZATION_CONFIG='{
"name": "authorization-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"tasks.max": "1",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "'${POSTGRES_USER:-rwa_user}'",
"database.password": "'${POSTGRES_PASSWORD:-rwa_secure_password}'",
"database.dbname": "rwa_authorization",
"topic.prefix": "cdc.authorization",
"table.include.list": "public.authorization_roles,public.monthly_assessments,public.system_accounts,public.system_account_ledgers",
"plugin.name": "pgoutput",
"publication.name": "debezium_authorization_publication",
"publication.autocreate.mode": "filtered",
"slot.name": "debezium_authorization_slot",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.drop.tombstones": "true",
"transforms.unwrap.delete.handling.mode": "rewrite",
"transforms.unwrap.add.fields": "op,table,source.ts_ms",
"heartbeat.interval.ms": "10000",
"snapshot.mode": "initial",
"decimal.handling.mode": "string",
"time.precision.mode": "connect"
}
}'
RESULT=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$AUTHORIZATION_CONFIG" \
"http://localhost:8084/connectors" 2>/dev/null || echo "failed")
if echo "$RESULT" | grep -q "authorization-postgres-connector"; then
log_info "authorization-postgres-connector registered successfully"
else
log_warn "Failed to register authorization connector: $RESULT"
fi
fi
}
down() {
@ -610,10 +757,10 @@ infra_status() {
echo -e " ${RED}[FAIL]${NC} Kafka (port 9092)"
fi
if curl -s http://localhost:8083/ > /dev/null 2>&1; then
echo -e " ${GREEN}[OK]${NC} Debezium Connect (port 8083)"
if curl -s http://localhost:8084/ > /dev/null 2>&1; then
echo -e " ${GREEN}[OK]${NC} Debezium Connect (port 8084)"
# Check connector status
CONNECTOR_STATUS=$(curl -s http://localhost:8083/connectors/identity-postgres-connector/status 2>/dev/null | grep -o '"state":"[^"]*"' | head -1 || echo "")
CONNECTOR_STATUS=$(curl -s http://localhost:8084/connectors/identity-postgres-connector/status 2>/dev/null | grep -o '"state":"[^"]*"' | head -1 || echo "")
if echo "$CONNECTOR_STATUS" | grep -q "RUNNING"; then
echo -e " └─ ${GREEN}[RUNNING]${NC} identity-postgres-connector"
elif [ -n "$CONNECTOR_STATUS" ]; then
@ -622,7 +769,7 @@ infra_status() {
echo -e " └─ ${YELLOW}[NOT REGISTERED]${NC} identity-postgres-connector"
fi
else
echo -e " ${RED}[FAIL]${NC} Debezium Connect (port 8083)"
echo -e " ${RED}[FAIL]${NC} Debezium Connect (port 8084)"
fi
}
@ -641,28 +788,28 @@ debezium_status() {
echo "============================================"
echo ""
if ! curl -s http://localhost:8083/ > /dev/null 2>&1; then
if ! curl -s http://localhost:8084/ > /dev/null 2>&1; then
log_error "Debezium Connect is not running"
exit 1
fi
echo "Debezium Connect Version:"
curl -s http://localhost:8083/ | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8083/
curl -s http://localhost:8084/ | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8084/
echo ""
echo "Registered Connectors:"
curl -s http://localhost:8083/connectors | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8083/connectors
curl -s http://localhost:8084/connectors | python3 -m json.tool 2>/dev/null || curl -s http://localhost:8084/connectors
echo ""
echo "Connector Details:"
curl -s http://localhost:8083/connectors/identity-postgres-connector/status | python3 -m json.tool 2>/dev/null || \
curl -s http://localhost:8083/connectors/identity-postgres-connector/status
curl -s http://localhost:8084/connectors/identity-postgres-connector/status | python3 -m json.tool 2>/dev/null || \
curl -s http://localhost:8084/connectors/identity-postgres-connector/status
}
debezium_register() {
log_step "Registering Debezium connectors..."
if ! curl -s http://localhost:8083/ > /dev/null 2>&1; then
if ! curl -s http://localhost:8084/ > /dev/null 2>&1; then
log_error "Debezium Connect is not running"
exit 1
fi
@ -673,12 +820,12 @@ debezium_register() {
debezium_restart_connector() {
log_step "Restarting Debezium connector..."
if ! curl -s http://localhost:8083/ > /dev/null 2>&1; then
if ! curl -s http://localhost:8084/ > /dev/null 2>&1; then
log_error "Debezium Connect is not running"
exit 1
fi
curl -X POST http://localhost:8083/connectors/identity-postgres-connector/restart
curl -X POST http://localhost:8084/connectors/identity-postgres-connector/restart
log_info "Connector restart requested"
sleep 3
@ -690,7 +837,7 @@ debezium_delete_connector() {
read -p "Are you sure? (y/N): " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
curl -X DELETE http://localhost:8083/connectors/identity-postgres-connector
curl -X DELETE http://localhost:8084/connectors/identity-postgres-connector
log_info "Connector deleted"
else
log_info "Cancelled"

View File

@ -131,7 +131,7 @@ services:
postgres:
condition: service_healthy
ports:
- "8083:8083"
- "8084:8083"
environment:
TZ: Asia/Shanghai
GROUP_ID: debezium-connect

View File

@ -0,0 +1,28 @@
{
"name": "authorization-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium",
"database.password": "debezium_password",
"database.dbname": "rwa_authorization",
"database.server.name": "authorization",
"topic.prefix": "cdc.authorization",
"plugin.name": "pgoutput",
"publication.name": "authorization_cdc_publication",
"slot.name": "authorization_cdc_slot",
"table.include.list": "public.authorization_roles,public.monthly_assessments,public.system_accounts,public.system_account_ledgers",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.add.fields": "op,table,source.ts_ms",
"transforms.unwrap.delete.handling.mode": "rewrite",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
"snapshot.mode": "initial",
"decimal.handling.mode": "string",
"time.precision.mode": "connect"
}
}

View File

@ -0,0 +1,28 @@
{
"name": "planting-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium",
"database.password": "debezium_password",
"database.dbname": "rwa_planting",
"database.server.name": "planting",
"topic.prefix": "cdc.planting",
"plugin.name": "pgoutput",
"publication.name": "planting_cdc_publication",
"slot.name": "planting_cdc_slot",
"table.include.list": "public.planting_orders,public.planting_positions,public.contract_signing_tasks,public.fund_allocations",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.add.fields": "op,table,source.ts_ms",
"transforms.unwrap.delete.handling.mode": "rewrite",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
"snapshot.mode": "initial",
"decimal.handling.mode": "string",
"time.precision.mode": "connect"
}
}

View File

@ -0,0 +1,28 @@
{
"name": "wallet-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium",
"database.password": "debezium_password",
"database.dbname": "rwa_wallet",
"database.server.name": "wallet",
"topic.prefix": "cdc.wallet",
"plugin.name": "pgoutput",
"publication.name": "wallet_cdc_publication",
"slot.name": "wallet_cdc_slot",
"table.include.list": "public.wallet_accounts,public.withdrawal_orders,public.fiat_withdrawal_orders,public.wallet_ledger_entries",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.add.fields": "op,table,source.ts_ms",
"transforms.unwrap.delete.handling.mode": "rewrite",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",
"snapshot.mode": "initial",
"decimal.handling.mode": "string",
"time.precision.mode": "connect"
}
}