-- ============================================================================ -- affiliate_dash 初始化脚本(唯一迁移文件) -- 全平台统一使用积分(POINT)作为结算货币;商户手续费为"百分比或固定"二选一。 -- 本脚本幂等:所有 CREATE 均带 IF NOT EXISTS,种子数据均用 ON CONFLICT DO NOTHING。 -- ============================================================================ -- ---------------------------------------------------------------------------- -- 用户与商户 -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, username VARCHAR(64) NOT NULL, password_hash VARCHAR(255) NOT NULL, nickname VARCHAR(64), role VARCHAR(32) NOT NULL DEFAULT 'merchant', status BIGINT DEFAULT 1 ); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users (username); CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users (deleted_at); CREATE TABLE IF NOT EXISTS merchants ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', contact_name VARCHAR(64), contact_info VARCHAR(128), features TEXT NOT NULL DEFAULT 'products,orders,wallet,api,callbacks', fee_type VARCHAR(16) NOT NULL DEFAULT 'rate', fee_rate_bp BIGINT NOT NULL DEFAULT 0, fee_fixed_amount BIGINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS idx_merchants_code ON merchants (code); CREATE INDEX IF NOT EXISTS idx_merchants_deleted_at ON merchants (deleted_at); CREATE INDEX IF NOT EXISTS idx_merchants_status ON merchants (status); CREATE TABLE IF NOT EXISTS merchant_members ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, role VARCHAR(32) NOT NULL DEFAULT 'operator', status BIGINT NOT NULL DEFAULT 1, is_default BOOLEAN NOT NULL DEFAULT FALSE ); CREATE UNIQUE INDEX IF NOT EXISTS idx_merchant_member ON merchant_members (merchant_id, user_id); CREATE INDEX IF NOT EXISTS idx_merchant_members_merchant_id ON merchant_members (merchant_id); CREATE INDEX IF NOT EXISTS idx_merchant_members_user_id ON merchant_members (user_id); -- ---------------------------------------------------------------------------- -- 开放接口凭证 -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS api_clients ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, app_key VARCHAR(96) NOT NULL, secret_ciphertext TEXT NOT NULL, signature_version VARCHAR(16) NOT NULL DEFAULT 'v1', scopes TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', expires_at TIMESTAMPTZ, last_used_at TIMESTAMPTZ ); CREATE UNIQUE INDEX IF NOT EXISTS idx_api_clients_app_key ON api_clients (app_key); CREATE INDEX IF NOT EXISTS idx_api_clients_deleted_at ON api_clients (deleted_at); CREATE INDEX IF NOT EXISTS idx_api_clients_merchant_id ON api_clients (merchant_id); CREATE INDEX IF NOT EXISTS idx_api_clients_status ON api_clients (status); CREATE TABLE IF NOT EXISTS api_request_nonces ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, api_client_id BIGINT NOT NULL, nonce VARCHAR(96) NOT NULL, expires_at TIMESTAMPTZ NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_api_nonce ON api_request_nonces (api_client_id, nonce); CREATE INDEX IF NOT EXISTS idx_api_request_nonces_api_client_id ON api_request_nonces (api_client_id); CREATE INDEX IF NOT EXISTS idx_api_request_nonces_expires_at ON api_request_nonces (expires_at); CREATE TABLE IF NOT EXISTS source_api_nonces ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, api_key VARCHAR(96) NOT NULL, nonce VARCHAR(64) NOT NULL, expires_at TIMESTAMPTZ NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_source_api_nonce ON source_api_nonces (api_key, nonce); CREATE INDEX IF NOT EXISTS idx_source_api_nonces_expires_at ON source_api_nonces (expires_at); -- ---------------------------------------------------------------------------- -- 商品目录与商户可售商品 -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS products ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, code VARCHAR(96) NOT NULL, name VARCHAR(160) NOT NULL, category VARCHAR(64), description TEXT, attributes TEXT, status VARCHAR(16) NOT NULL DEFAULT 'active' ); CREATE UNIQUE INDEX IF NOT EXISTS idx_products_code ON products (code); CREATE INDEX IF NOT EXISTS idx_products_deleted_at ON products (deleted_at); CREATE INDEX IF NOT EXISTS idx_products_category ON products (category); CREATE INDEX IF NOT EXISTS idx_products_status ON products (status); CREATE TABLE IF NOT EXISTS merchant_products ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, product_id BIGINT NOT NULL, sku VARCHAR(96) NOT NULL, display_name VARCHAR(160), price_amount BIGINT NOT NULL DEFAULT 0, cost_amount BIGINT NOT NULL DEFAULT 0, currency VARCHAR(12) NOT NULL DEFAULT 'POINT', stock BIGINT NOT NULL DEFAULT -1, status VARCHAR(16) NOT NULL DEFAULT 'active', fulfillment_config TEXT ); CREATE UNIQUE INDEX IF NOT EXISTS idx_merchant_product_sku ON merchant_products (merchant_id, sku); CREATE INDEX IF NOT EXISTS idx_merchant_products_deleted_at ON merchant_products (deleted_at); CREATE INDEX IF NOT EXISTS idx_merchant_products_merchant_id ON merchant_products (merchant_id); CREATE INDEX IF NOT EXISTS idx_merchant_products_product_id ON merchant_products (product_id); CREATE INDEX IF NOT EXISTS idx_merchant_products_status ON merchant_products (status); -- ---------------------------------------------------------------------------- -- 钱包账户与流水 -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS wallet_accounts ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, currency VARCHAR(12) NOT NULL DEFAULT 'POINT', available_balance BIGINT NOT NULL DEFAULT 0, frozen_balance BIGINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS idx_wallet_accounts_merchant_id ON wallet_accounts (merchant_id); CREATE TABLE IF NOT EXISTS wallet_ledger_entries ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, wallet_account_id BIGINT NOT NULL, entry_no VARCHAR(64) NOT NULL, type VARCHAR(24) NOT NULL, amount BIGINT NOT NULL, balance_after BIGINT NOT NULL, reference_type VARCHAR(32) NOT NULL, reference_no VARCHAR(96) NOT NULL, idempotency_key VARCHAR(128), note VARCHAR(255) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_wallet_ledger_entries_entry_no ON wallet_ledger_entries (entry_no); CREATE UNIQUE INDEX IF NOT EXISTS idx_wallet_ledger_idempotency ON wallet_ledger_entries (merchant_id, idempotency_key); CREATE INDEX IF NOT EXISTS idx_wallet_ledger_entries_merchant_id ON wallet_ledger_entries (merchant_id); CREATE INDEX IF NOT EXISTS idx_wallet_ledger_entries_wallet_account_id ON wallet_ledger_entries (wallet_account_id); CREATE INDEX IF NOT EXISTS idx_wallet_ledger_entries_type ON wallet_ledger_entries (type); CREATE INDEX IF NOT EXISTS idx_wallet_ledger_entries_reference_no ON wallet_ledger_entries (reference_no); -- ---------------------------------------------------------------------------- -- 履约订单 -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS fulfillment_orders ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, client_order_no VARCHAR(96) NOT NULL, merchant_product_id BIGINT NOT NULL, product_sku VARCHAR(96) NOT NULL, product_name VARCHAR(160) NOT NULL, quantity BIGINT NOT NULL DEFAULT 1, base_amount BIGINT NOT NULL DEFAULT 0, fee_type VARCHAR(16) NOT NULL DEFAULT 'rate', fee_rate_bp BIGINT NOT NULL DEFAULT 0, fee_fixed_amount BIGINT NOT NULL DEFAULT 0, service_fee_amount BIGINT NOT NULL DEFAULT 0, amount BIGINT NOT NULL, currency VARCHAR(12) NOT NULL DEFAULT 'POINT', order_status VARCHAR(16) NOT NULL DEFAULT 'paid', buyer_reference VARCHAR(128), request_fingerprint VARCHAR(64) NOT NULL DEFAULT '', request_data TEXT, result_data TEXT, provider_order_no VARCHAR(96), failure_reason VARCHAR(512), cancelled_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, delivery_link_expires_at TIMESTAMPTZ, delivery_link_revoked_at TIMESTAMPTZ ); CREATE UNIQUE INDEX IF NOT EXISTS idx_fulfillment_orders_order_no ON fulfillment_orders (order_no); CREATE UNIQUE INDEX IF NOT EXISTS idx_order_client_no ON fulfillment_orders (merchant_id, client_order_no); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_deleted_at ON fulfillment_orders (deleted_at); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_merchant_id ON fulfillment_orders (merchant_id); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_merchant_product_id ON fulfillment_orders (merchant_product_id); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_order_status ON fulfillment_orders (order_status); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_request_fingerprint ON fulfillment_orders (request_fingerprint); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_provider_order_no ON fulfillment_orders (provider_order_no); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_delivery_link_expires_at ON fulfillment_orders (delivery_link_expires_at); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_delivery_link_revoked_at ON fulfillment_orders (delivery_link_revoked_at); -- ---------------------------------------------------------------------------- -- 回调订阅与投递(事务性 outbox) -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS callback_subscriptions ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, url VARCHAR(1024) NOT NULL, events TEXT NOT NULL, secret_ciphertext TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active' ); CREATE INDEX IF NOT EXISTS idx_callback_subscriptions_deleted_at ON callback_subscriptions (deleted_at); CREATE INDEX IF NOT EXISTS idx_callback_subscriptions_merchant_id ON callback_subscriptions (merchant_id); CREATE INDEX IF NOT EXISTS idx_callback_subscriptions_status ON callback_subscriptions (status); CREATE TABLE IF NOT EXISTS callback_deliveries ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, merchant_id BIGINT NOT NULL, callback_subscription_id BIGINT NOT NULL, event_id VARCHAR(64) NOT NULL, event VARCHAR(64) NOT NULL, payload TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', attempts BIGINT NOT NULL DEFAULT 0, next_attempt_at TIMESTAMPTZ NOT NULL, last_status_code BIGINT NOT NULL DEFAULT 0, last_response TEXT, delivered_at TIMESTAMPTZ ); CREATE UNIQUE INDEX IF NOT EXISTS idx_callback_deliveries_event_id ON callback_deliveries (event_id); CREATE INDEX IF NOT EXISTS idx_callback_deliveries_merchant_id ON callback_deliveries (merchant_id); CREATE INDEX IF NOT EXISTS idx_callback_deliveries_callback_subscription_id ON callback_deliveries (callback_subscription_id); CREATE INDEX IF NOT EXISTS idx_callback_deliveries_event ON callback_deliveries (event); CREATE INDEX IF NOT EXISTS idx_callback_deliveries_status ON callback_deliveries (status); CREATE INDEX IF NOT EXISTS idx_callback_deliveries_next_attempt_at ON callback_deliveries (next_attempt_at); -- ---------------------------------------------------------------------------- -- 审计日志 -- ---------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS audit_logs ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ, merchant_id BIGINT, actor_user_id BIGINT, api_client_id BIGINT, request_id VARCHAR(64), action VARCHAR(96) NOT NULL, entity_type VARCHAR(64) NOT NULL, entity_id VARCHAR(96) NOT NULL, metadata TEXT ); CREATE INDEX IF NOT EXISTS idx_audit_logs_merchant_id ON audit_logs (merchant_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_actor_user_id ON audit_logs (actor_user_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_api_client_id ON audit_logs (api_client_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_request_id ON audit_logs (request_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs (action); CREATE INDEX IF NOT EXISTS idx_audit_logs_entity_type ON audit_logs (entity_type); CREATE INDEX IF NOT EXISTS idx_audit_logs_entity_id ON audit_logs (entity_id); -- ============================================================================ -- 种子数据 -- ============================================================================ -- 1. 自营商户(平台默认租户,供 EnsureAdmin 关联成员) INSERT INTO merchants (code, name, status, features, fee_type) VALUES ('self-operated', '自营商户', 'active', 'products,orders,wallet,api,callbacks', 'rate') ON CONFLICT (code) DO NOTHING; -- 2. 自营商户积分钱包 INSERT INTO wallet_accounts (merchant_id, currency) SELECT id, 'POINT' FROM merchants WHERE code = 'self-operated' ON CONFLICT (merchant_id) DO NOTHING; -- 3. 平台商品目录(和平精英商品,幂等) INSERT INTO products (code, name, category, status) SELECT v.code, v.name, v.category, 'active' FROM (VALUES ('suit_alan_walker', '套装-Alan Walker', '套装'), ('suit_shadow_gothic', '套装-暗影哥特', '套装'), ('top_black_elite_trainer','黑色高级特训官上衣', '上衣'), ('m416_hamster_gray', 'M416-仓鼠灰灰', '枪械'), ('bag_cute_bear', '萌熊伴侣背包', '背包'), ('suit_dual_fluffy', '套装-双彩绵绵', '套装'), ('suit_pink_sheep', '套装-糯粉咩咩', '套装'), ('suit_first_peach', '套装-恋恋初桃', '套装'), ('suit_romantic_destiny', '套装-浪漫天命', '套装'), ('pack_western_cowboy', '西部牛仔大礼包', '礼包'), ('smoke_pink_sheep', '烟雾弹-糯粉咩咩', '投掷物'), ('frag_pink_sheep', '破片手榴弹-糯粉咩咩', '投掷物'), ('suit_hamster_gray', '套装-仓鼠灰灰', '套装'), ('suit_cute_bear', '套装-萌熊伴侣', '套装'), ('bag_pink_sheep', '糯粉咩咩背包', '背包'), ('helmet_pink_sheep', '糯粉咩咩头盔', '头盔'), ('bag_hamster_gray', '仓鼠灰灰背包', '背包'), ('helmet_hamster_gray', '仓鼠灰灰头盔', '头盔'), ('suit_western_mystery', '套装-西部谜踪', '套装'), ('helmet_panda_treasure', '国宝胖达头盔', '头盔'), ('suit_panda_round', '套装-胖达圆圆', '套装'), ('suit_panda_tuan', '套装-胖达团团', '套装'), ('pack_lava_ranger', '熔岩游骑兵礼包', '礼包'), ('suit_sand_dancer', '套装-狂沙舞者', '套装'), ('pack_star_roam_outfit', '星际漫游服装礼包', '礼包'), ('pack_star_roam_weapon', '星际漫游枪械礼包', '礼包'), ('suit_cloud_bear', '套装-绵云熊熊', '套装') ) AS v(code, name, category) ON CONFLICT (code) DO NOTHING; -- 4. 自营商户可售商品(价格一步到位,幂等,按 merchant_id + sku 唯一索引) INSERT INTO merchant_products (merchant_id, product_id, sku, display_name, price_amount, cost_amount, currency, stock, status) SELECT m.id, p.id, p.code, p.name, v.price_amount, v.cost_amount, 'POINT', -1, 'active' FROM (VALUES ('suit_alan_walker', 360, 360), ('suit_shadow_gothic', 888, 888), ('top_black_elite_trainer',360, 360), ('m416_hamster_gray', 1680, 1680), ('bag_cute_bear', 2880, 2880), ('suit_dual_fluffy', 1124, 1124), ('suit_pink_sheep', 1880, 1880), ('suit_first_peach', 988, 988), ('suit_romantic_destiny', 360, 360), ('pack_western_cowboy', 3200, 3200), ('smoke_pink_sheep', 488, 488), ('frag_pink_sheep', 488, 488), ('suit_hamster_gray', 1280, 1280), ('suit_cute_bear', 1680, 1680), ('bag_pink_sheep', 1680, 1680), ('helmet_pink_sheep', 1680, 1680), ('bag_hamster_gray', 1680, 1680), ('helmet_hamster_gray', 1680, 1680), ('suit_western_mystery', 1150, 1150), ('helmet_panda_treasure', 2340, 2340), ('suit_panda_round', 2340, 2340), ('suit_panda_tuan', 2340, 2340), ('pack_lava_ranger', 960, 960), ('suit_sand_dancer', 840, 840), ('pack_star_roam_outfit', 8360, 8360), ('pack_star_roam_weapon', 5160, 5160), ('suit_cloud_bear', 1348, 1348) ) AS v(sku, price_amount, cost_amount) JOIN products p ON p.code = v.sku JOIN merchants m ON m.code = 'self-operated' ON CONFLICT (merchant_id, sku) DO NOTHING;