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:
parent
c4cec836d9
commit
83f84b9d7c
|
|
@ -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");
|
||||||
|
|
@ -68,7 +68,7 @@ model Notification {
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
readRecords NotificationRead[]
|
readRecords NotificationRead[]
|
||||||
targetTags NotificationTagTarget[] // BY_TAG 时使用
|
targetTags NotificationTagTarget[] // BY_TAG 时使用
|
||||||
targetUsers NotificationUserTarget[] // SPECIFIC 时使用
|
targetUsers NotificationUserTarget[] // SPECIFIC 时使用
|
||||||
|
|
||||||
@@index([isEnabled, publishedAt])
|
@@index([isEnabled, publishedAt])
|
||||||
|
|
@ -110,8 +110,8 @@ enum NotificationPriority {
|
||||||
|
|
||||||
/// 目标用户类型
|
/// 目标用户类型
|
||||||
enum TargetType {
|
enum TargetType {
|
||||||
ALL // 所有用户
|
ALL // 所有用户
|
||||||
BY_TAG // 按标签匹配
|
BY_TAG // 按标签匹配
|
||||||
SPECIFIC // 指定用户列表
|
SPECIFIC // 指定用户列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,28 +180,28 @@ model TagCategory {
|
||||||
|
|
||||||
/// 标签类型
|
/// 标签类型
|
||||||
enum TagType {
|
enum TagType {
|
||||||
MANUAL // 手动打标 (管理员操作)
|
MANUAL // 手动打标 (管理员操作)
|
||||||
AUTO // 自动打标 (规则驱动)
|
AUTO // 自动打标 (规则驱动)
|
||||||
COMPUTED // 计算型 (实时计算,不存储关联)
|
COMPUTED // 计算型 (实时计算,不存储关联)
|
||||||
SYSTEM // 系统内置 (不可删除)
|
SYSTEM // 系统内置 (不可删除)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 标签值类型
|
/// 标签值类型
|
||||||
enum TagValueType {
|
enum TagValueType {
|
||||||
BOOLEAN // 布尔型: 有/无
|
BOOLEAN // 布尔型: 有/无
|
||||||
ENUM // 枚举型: 高/中/低
|
ENUM // 枚举型: 高/中/低
|
||||||
NUMBER // 数值型: 0-100分
|
NUMBER // 数值型: 0-100分
|
||||||
STRING // 字符串型
|
STRING // 字符串型
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 用户标签定义
|
/// 用户标签定义
|
||||||
model UserTag {
|
model UserTag {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
categoryId String? @map("category_id")
|
categoryId String? @map("category_id")
|
||||||
code String @unique @db.VarChar(50) // "vip", "new_user", "whale"
|
code String @unique @db.VarChar(50) // "vip", "new_user", "whale"
|
||||||
name String @db.VarChar(100) // "VIP用户", "新用户", "大客户"
|
name String @db.VarChar(100) // "VIP用户", "新用户", "大客户"
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
color String? @db.VarChar(20) // "#FF5722"
|
color String? @db.VarChar(20) // "#FF5722"
|
||||||
|
|
||||||
type TagType @default(MANUAL) // 标签类型
|
type TagType @default(MANUAL) // 标签类型
|
||||||
valueType TagValueType @default(BOOLEAN) @map("value_type") // 标签值类型
|
valueType TagValueType @default(BOOLEAN) @map("value_type") // 标签值类型
|
||||||
|
|
@ -211,7 +211,7 @@ model UserTag {
|
||||||
enumValues Json? @map("enum_values")
|
enumValues Json? @map("enum_values")
|
||||||
|
|
||||||
// 关联的自动规则 (type=AUTO 时使用)
|
// 关联的自动规则 (type=AUTO 时使用)
|
||||||
ruleId String? @unique @map("rule_id")
|
ruleId String? @unique @map("rule_id")
|
||||||
rule UserClassificationRule? @relation(fields: [ruleId], references: [id], onDelete: SetNull)
|
rule UserClassificationRule? @relation(fields: [ruleId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
// 广告相关
|
// 广告相关
|
||||||
|
|
@ -242,9 +242,9 @@ model UserTag {
|
||||||
|
|
||||||
/// 用户-标签关联
|
/// 用户-标签关联
|
||||||
model UserTagAssignment {
|
model UserTagAssignment {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
accountSequence String @map("account_sequence") @db.VarChar(12)
|
accountSequence String @map("account_sequence") @db.VarChar(12)
|
||||||
tagId String @map("tag_id")
|
tagId String @map("tag_id")
|
||||||
|
|
||||||
// 标签值 (根据 valueType)
|
// 标签值 (根据 valueType)
|
||||||
// BOOLEAN: null (存在即为true)
|
// BOOLEAN: null (存在即为true)
|
||||||
|
|
@ -344,16 +344,16 @@ model UserFeature {
|
||||||
|
|
||||||
/// 人群包用途
|
/// 人群包用途
|
||||||
enum SegmentUsageType {
|
enum SegmentUsageType {
|
||||||
GENERAL // 通用
|
GENERAL // 通用
|
||||||
NOTIFICATION // 通知定向
|
NOTIFICATION // 通知定向
|
||||||
ADVERTISING // 广告定向
|
ADVERTISING // 广告定向
|
||||||
ANALYTICS // 数据分析
|
ANALYTICS // 数据分析
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 人群包 - 多条件组合的用户群
|
/// 人群包 - 多条件组合的用户群
|
||||||
model AudienceSegment {
|
model AudienceSegment {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String @db.VarChar(100) // "高价值活跃用户"
|
name String @db.VarChar(100) // "高价值活跃用户"
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
|
|
||||||
// 定向条件 (JSON)
|
// 定向条件 (JSON)
|
||||||
|
|
@ -476,29 +476,29 @@ model UserQueryView {
|
||||||
/// 推荐关系查询视图 - 通过 Debezium CDC 从 referral-service 同步
|
/// 推荐关系查询视图 - 通过 Debezium CDC 从 referral-service 同步
|
||||||
/// 用于 admin-web 查看用户推荐关系,避免跨服务调用
|
/// 用于 admin-web 查看用户推荐关系,避免跨服务调用
|
||||||
model ReferralQueryView {
|
model ReferralQueryView {
|
||||||
id BigInt @id @map("relationship_id")
|
id BigInt @id @map("relationship_id")
|
||||||
userId BigInt @unique @map("user_id")
|
userId BigInt @unique @map("user_id")
|
||||||
accountSequence String @unique @map("account_sequence") @db.VarChar(12)
|
accountSequence String @unique @map("account_sequence") @db.VarChar(12)
|
||||||
|
|
||||||
// 推荐人信息
|
// 推荐人信息
|
||||||
referrerId BigInt? @map("referrer_id")
|
referrerId BigInt? @map("referrer_id")
|
||||||
rootUserId BigInt? @map("root_user_id")
|
rootUserId BigInt? @map("root_user_id")
|
||||||
|
|
||||||
// 推荐码
|
// 推荐码
|
||||||
myReferralCode String @unique @map("my_referral_code") @db.VarChar(20)
|
myReferralCode String @unique @map("my_referral_code") @db.VarChar(20)
|
||||||
usedReferralCode String? @map("used_referral_code") @db.VarChar(20)
|
usedReferralCode String? @map("used_referral_code") @db.VarChar(20)
|
||||||
|
|
||||||
// 推荐链信息
|
// 推荐链信息
|
||||||
ancestorPath BigInt[] @map("ancestor_path")
|
ancestorPath BigInt[] @map("ancestor_path")
|
||||||
depth Int @default(0) @map("depth")
|
depth Int @default(0) @map("depth")
|
||||||
|
|
||||||
// 直推统计
|
// 直推统计
|
||||||
directReferralCount Int @default(0) @map("direct_referral_count")
|
directReferralCount Int @default(0) @map("direct_referral_count")
|
||||||
activeDirectCount Int @default(0) @map("active_direct_count")
|
activeDirectCount Int @default(0) @map("active_direct_count")
|
||||||
|
|
||||||
// 时间戳
|
// 时间戳
|
||||||
createdAt DateTime @map("created_at")
|
createdAt DateTime @map("created_at")
|
||||||
syncedAt DateTime @default(now()) @map("synced_at")
|
syncedAt DateTime @default(now()) @map("synced_at")
|
||||||
|
|
||||||
@@index([referrerId])
|
@@index([referrerId])
|
||||||
@@index([accountSequence])
|
@@index([accountSequence])
|
||||||
|
|
@ -509,6 +509,474 @@ model ReferralQueryView {
|
||||||
@@map("referral_query_view")
|
@@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 (事件消费追踪)
|
// Kafka Event Tracking (事件消费追踪)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -562,16 +1030,16 @@ model SystemConfig {
|
||||||
|
|
||||||
/// 系统维护公告 - 用于系统升级/维护期间阻断用户操作
|
/// 系统维护公告 - 用于系统升级/维护期间阻断用户操作
|
||||||
model SystemMaintenance {
|
model SystemMaintenance {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
title String @db.VarChar(100) // 标题:如"系统升级中"
|
title String @db.VarChar(100) // 标题:如"系统升级中"
|
||||||
message String @db.Text // 说明:如"预计10:00恢复,请稍候"
|
message String @db.Text // 说明:如"预计10:00恢复,请稍候"
|
||||||
startTime DateTime @map("start_time") // 维护开始时间
|
startTime DateTime @map("start_time") // 维护开始时间
|
||||||
endTime DateTime @map("end_time") // 维护结束时间
|
endTime DateTime @map("end_time") // 维护结束时间
|
||||||
isActive Boolean @default(false) @map("is_active") // 是否激活
|
isActive Boolean @default(false) @map("is_active") // 是否激活
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
createdBy String @map("created_by") // 创建人ID
|
createdBy String @map("created_by") // 创建人ID
|
||||||
updatedBy String? @map("updated_by") // 更新人ID
|
updatedBy String? @map("updated_by") // 更新人ID
|
||||||
|
|
||||||
@@index([isActive])
|
@@index([isActive])
|
||||||
@@index([startTime, endTime])
|
@@index([startTime, endTime])
|
||||||
|
|
@ -584,12 +1052,12 @@ model SystemMaintenance {
|
||||||
|
|
||||||
/// 共管钱包会话状态
|
/// 共管钱包会话状态
|
||||||
enum WalletSessionStatus {
|
enum WalletSessionStatus {
|
||||||
WAITING // 等待参与方加入
|
WAITING // 等待参与方加入
|
||||||
READY // 所有参与方已就绪
|
READY // 所有参与方已就绪
|
||||||
PROCESSING // 密钥生成中
|
PROCESSING // 密钥生成中
|
||||||
COMPLETED // 创建完成
|
COMPLETED // 创建完成
|
||||||
FAILED // 创建失败
|
FAILED // 创建失败
|
||||||
CANCELLED // 已取消
|
CANCELLED // 已取消
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 共管钱包会话 - 钱包创建过程的会话记录
|
/// 共管钱包会话 - 钱包创建过程的会话记录
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,3 +2,6 @@ export * from './kafka.module';
|
||||||
export * from './user-event-consumer.service';
|
export * from './user-event-consumer.service';
|
||||||
export * from './cdc-consumer.service';
|
export * from './cdc-consumer.service';
|
||||||
export * from './referral-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';
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ import { USER_QUERY_REPOSITORY } from '../../domain/repositories/user-query.repo
|
||||||
import { UserEventConsumerService } from './user-event-consumer.service';
|
import { UserEventConsumerService } from './user-event-consumer.service';
|
||||||
import { CdcConsumerService } from './cdc-consumer.service';
|
import { CdcConsumerService } from './cdc-consumer.service';
|
||||||
import { ReferralCdcConsumerService } from './referral-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 模块
|
* Kafka 模块
|
||||||
|
|
@ -13,6 +16,9 @@ import { ReferralCdcConsumerService } from './referral-cdc-consumer.service';
|
||||||
* CDC 消费者 - 通过 Debezium 从各服务同步数据:
|
* CDC 消费者 - 通过 Debezium 从各服务同步数据:
|
||||||
* 1. CdcConsumerService - identity-service 用户数据
|
* 1. CdcConsumerService - identity-service 用户数据
|
||||||
* 2. ReferralCdcConsumerService - referral-service 推荐关系
|
* 2. ReferralCdcConsumerService - referral-service 推荐关系
|
||||||
|
* 3. WalletCdcConsumerService - wallet-service 钱包/提现数据
|
||||||
|
* 4. PlantingCdcConsumerService - planting-service 认种/持仓/合同数据
|
||||||
|
* 5. AuthorizationCdcConsumerService - authorization-service 授权/考核/系统账户数据
|
||||||
*
|
*
|
||||||
* Outbox 消费者 - 处理业务领域事件:
|
* Outbox 消费者 - 处理业务领域事件:
|
||||||
* 1. UserEventConsumerService - 用户相关业务事件
|
* 1. UserEventConsumerService - 用户相关业务事件
|
||||||
|
|
@ -31,11 +37,20 @@ import { ReferralCdcConsumerService } from './referral-cdc-consumer.service';
|
||||||
CdcConsumerService,
|
CdcConsumerService,
|
||||||
// CDC 消费者 - referral-service
|
// CDC 消费者 - referral-service
|
||||||
ReferralCdcConsumerService,
|
ReferralCdcConsumerService,
|
||||||
|
// CDC 消费者 - wallet-service
|
||||||
|
WalletCdcConsumerService,
|
||||||
|
// CDC 消费者 - planting-service
|
||||||
|
PlantingCdcConsumerService,
|
||||||
|
// CDC 消费者 - authorization-service
|
||||||
|
AuthorizationCdcConsumerService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
UserEventConsumerService,
|
UserEventConsumerService,
|
||||||
CdcConsumerService,
|
CdcConsumerService,
|
||||||
ReferralCdcConsumerService,
|
ReferralCdcConsumerService,
|
||||||
|
WalletCdcConsumerService,
|
||||||
|
PlantingCdcConsumerService,
|
||||||
|
AuthorizationCdcConsumerService,
|
||||||
USER_QUERY_REPOSITORY,
|
USER_QUERY_REPOSITORY,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -204,7 +204,7 @@ up() {
|
||||||
# Wait for Debezium Connect to be ready
|
# Wait for Debezium Connect to be ready
|
||||||
log_info "Waiting for Debezium Connect to be ready..."
|
log_info "Waiting for Debezium Connect to be ready..."
|
||||||
for i in {1..30}; do
|
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!"
|
log_info "Debezium Connect is ready!"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
@ -231,7 +231,7 @@ register_debezium_connectors() {
|
||||||
log_info "Registering Debezium connectors..."
|
log_info "Registering Debezium connectors..."
|
||||||
|
|
||||||
# Check existing 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
|
# Read database credentials from .env
|
||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
|
|
@ -276,7 +276,7 @@ register_debezium_connectors() {
|
||||||
RESULT=$(curl -s -X POST \
|
RESULT=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$IDENTITY_CONFIG" \
|
-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
|
if echo "$RESULT" | grep -q "identity-postgres-connector"; then
|
||||||
log_info "identity-postgres-connector registered successfully"
|
log_info "identity-postgres-connector registered successfully"
|
||||||
|
|
@ -325,7 +325,7 @@ register_debezium_connectors() {
|
||||||
RESULT=$(curl -s -X POST \
|
RESULT=$(curl -s -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$REFERRAL_CONFIG" \
|
-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
|
if echo "$RESULT" | grep -q "referral-postgres-connector"; then
|
||||||
log_info "referral-postgres-connector registered successfully"
|
log_info "referral-postgres-connector registered successfully"
|
||||||
|
|
@ -333,6 +333,153 @@ register_debezium_connectors() {
|
||||||
log_warn "Failed to register referral connector: $RESULT"
|
log_warn "Failed to register referral connector: $RESULT"
|
||||||
fi
|
fi
|
||||||
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() {
|
down() {
|
||||||
|
|
@ -610,10 +757,10 @@ infra_status() {
|
||||||
echo -e " ${RED}[FAIL]${NC} Kafka (port 9092)"
|
echo -e " ${RED}[FAIL]${NC} Kafka (port 9092)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if curl -s http://localhost:8083/ > /dev/null 2>&1; then
|
if curl -s http://localhost:8084/ > /dev/null 2>&1; then
|
||||||
echo -e " ${GREEN}[OK]${NC} Debezium Connect (port 8083)"
|
echo -e " ${GREEN}[OK]${NC} Debezium Connect (port 8084)"
|
||||||
# Check connector status
|
# 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
|
if echo "$CONNECTOR_STATUS" | grep -q "RUNNING"; then
|
||||||
echo -e " └─ ${GREEN}[RUNNING]${NC} identity-postgres-connector"
|
echo -e " └─ ${GREEN}[RUNNING]${NC} identity-postgres-connector"
|
||||||
elif [ -n "$CONNECTOR_STATUS" ]; then
|
elif [ -n "$CONNECTOR_STATUS" ]; then
|
||||||
|
|
@ -622,7 +769,7 @@ infra_status() {
|
||||||
echo -e " └─ ${YELLOW}[NOT REGISTERED]${NC} identity-postgres-connector"
|
echo -e " └─ ${YELLOW}[NOT REGISTERED]${NC} identity-postgres-connector"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e " ${RED}[FAIL]${NC} Debezium Connect (port 8083)"
|
echo -e " ${RED}[FAIL]${NC} Debezium Connect (port 8084)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -641,28 +788,28 @@ debezium_status() {
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
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"
|
log_error "Debezium Connect is not running"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Debezium Connect Version:"
|
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 ""
|
||||||
echo "Registered Connectors:"
|
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 ""
|
||||||
echo "Connector Details:"
|
echo "Connector Details:"
|
||||||
curl -s http://localhost:8083/connectors/identity-postgres-connector/status | python3 -m json.tool 2>/dev/null || \
|
curl -s http://localhost:8084/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
|
||||||
}
|
}
|
||||||
|
|
||||||
debezium_register() {
|
debezium_register() {
|
||||||
log_step "Registering Debezium connectors..."
|
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"
|
log_error "Debezium Connect is not running"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -673,12 +820,12 @@ debezium_register() {
|
||||||
debezium_restart_connector() {
|
debezium_restart_connector() {
|
||||||
log_step "Restarting Debezium 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"
|
log_error "Debezium Connect is not running"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
log_info "Connector restart requested"
|
||||||
sleep 3
|
sleep 3
|
||||||
|
|
@ -690,7 +837,7 @@ debezium_delete_connector() {
|
||||||
read -p "Are you sure? (y/N): " confirm
|
read -p "Are you sure? (y/N): " confirm
|
||||||
|
|
||||||
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
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"
|
log_info "Connector deleted"
|
||||||
else
|
else
|
||||||
log_info "Cancelled"
|
log_info "Cancelled"
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ services:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "8083:8083"
|
- "8084:8083"
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
GROUP_ID: debezium-connect
|
GROUP_ID: debezium-connect
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue