-- 商户池支付路由:收款商户、商户池、微信授权、成功统计事实及支付快照。 -- 不使用数据库外键;关联以 ID 保存并由应用层显式校验。 CREATE TABLE tb_payment_merchant ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, payment_method VARCHAR(20) NOT NULL, provider_type VARCHAR(30) NOT NULL, merchant_identity VARCHAR(100) NOT NULL, credentials JSONB NOT NULL DEFAULT '{}'::jsonb, credential_version BIGINT NOT NULL DEFAULT 1, status SMALLINT NOT NULL DEFAULT 0, remark TEXT NOT NULL DEFAULT '', creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, CONSTRAINT chk_payment_merchant_method CHECK (payment_method IN ('wechat', 'alipay')), CONSTRAINT chk_payment_merchant_status CHECK (status IN (0, 1)), CONSTRAINT chk_payment_merchant_credential_version CHECK (credential_version >= 1) ); CREATE UNIQUE INDEX uk_payment_merchant_method_identity_active ON tb_payment_merchant (payment_method, merchant_identity) WHERE deleted_at IS NULL; CREATE INDEX idx_payment_merchant_method_status ON tb_payment_merchant (payment_method, status, id) WHERE deleted_at IS NULL; CREATE TABLE tb_payment_merchant_pool ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, payment_method VARCHAR(20) NOT NULL, status SMALLINT NOT NULL DEFAULT 0, strategy VARCHAR(20) NOT NULL, threshold_amount BIGINT, threshold_count BIGINT, statistic_cycle VARCHAR(20), time_period_value BIGINT, time_period_unit VARCHAR(10), time_period_started_at TIMESTAMPTZ, routing_epoch BIGINT NOT NULL DEFAULT 1, remark TEXT NOT NULL DEFAULT '', creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, CONSTRAINT chk_payment_merchant_pool_method CHECK (payment_method IN ('wechat', 'alipay')), CONSTRAINT chk_payment_merchant_pool_status CHECK (status IN (0, 1)), CONSTRAINT chk_payment_merchant_pool_strategy CHECK (strategy IN ('amount', 'count', 'time')), CONSTRAINT chk_payment_merchant_pool_epoch CHECK (routing_epoch >= 1), CONSTRAINT chk_payment_merchant_pool_statistic_cycle CHECK ( statistic_cycle IS NULL OR statistic_cycle IN ('round', 'day', 'month') ), CONSTRAINT chk_payment_merchant_pool_time_unit CHECK ( time_period_unit IS NULL OR time_period_unit IN ('minute', 'hour', 'day') ), CONSTRAINT chk_payment_merchant_pool_strategy_parameters CHECK ( (strategy = 'amount' AND threshold_amount IS NOT NULL AND threshold_amount > 0 AND threshold_count IS NULL AND statistic_cycle IS NOT NULL AND time_period_value IS NULL AND time_period_unit IS NULL AND time_period_started_at IS NULL) OR (strategy = 'count' AND threshold_count IS NOT NULL AND threshold_count > 0 AND threshold_amount IS NULL AND statistic_cycle IS NOT NULL AND time_period_value IS NULL AND time_period_unit IS NULL AND time_period_started_at IS NULL) OR (strategy = 'time' AND time_period_value IS NOT NULL AND time_period_value >= 1 AND time_period_unit IS NOT NULL AND time_period_started_at IS NOT NULL AND threshold_amount IS NULL AND threshold_count IS NULL AND statistic_cycle IS NULL) ) ); CREATE UNIQUE INDEX uk_payment_merchant_pool_name_active ON tb_payment_merchant_pool (name) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX uk_payment_merchant_pool_enabled_method ON tb_payment_merchant_pool (payment_method) WHERE status = 1 AND deleted_at IS NULL; CREATE INDEX idx_payment_merchant_pool_method_status ON tb_payment_merchant_pool (payment_method, status, id) WHERE deleted_at IS NULL; CREATE TABLE tb_payment_merchant_pool_member ( id BIGSERIAL PRIMARY KEY, pool_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, sort_order BIGINT NOT NULL, creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, CONSTRAINT chk_payment_merchant_pool_member_order CHECK (sort_order >= 0) ); CREATE UNIQUE INDEX uk_payment_merchant_pool_member_active ON tb_payment_merchant_pool_member (pool_id, merchant_id) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX uk_payment_merchant_pool_member_order_active ON tb_payment_merchant_pool_member (pool_id, sort_order) WHERE deleted_at IS NULL; CREATE INDEX idx_payment_merchant_pool_member_merchant ON tb_payment_merchant_pool_member (merchant_id, pool_id) WHERE deleted_at IS NULL; CREATE TABLE tb_wechat_authorization ( id BIGSERIAL PRIMARY KEY, oa_app_id VARCHAR(100) NOT NULL DEFAULT '', oa_app_secret VARCHAR(200) NOT NULL DEFAULT '', oa_token VARCHAR(200) NOT NULL DEFAULT '', oa_aes_key VARCHAR(200) NOT NULL DEFAULT '', oa_oauth_redirect_url VARCHAR(500) NOT NULL DEFAULT '', miniapp_app_id VARCHAR(100) NOT NULL DEFAULT '', miniapp_app_secret VARCHAR(200) NOT NULL DEFAULT '', credential_version BIGINT NOT NULL DEFAULT 1, status SMALLINT NOT NULL DEFAULT 0, creator BIGINT NOT NULL DEFAULT 0, updater BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, CONSTRAINT chk_wechat_authorization_status CHECK (status IN (0, 1)), CONSTRAINT chk_wechat_authorization_credential_version CHECK (credential_version >= 1) ); CREATE UNIQUE INDEX uk_wechat_authorization_enabled ON tb_wechat_authorization ((status)) WHERE status = 1 AND deleted_at IS NULL; CREATE TABLE tb_payment_merchant_routing_success ( id BIGSERIAL PRIMARY KEY, payment_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, pool_id BIGINT NOT NULL, routing_epoch BIGINT NOT NULL, amount BIGINT NOT NULL, paid_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uk_payment_merchant_routing_success_payment UNIQUE (payment_id), CONSTRAINT chk_payment_merchant_routing_success_epoch CHECK (routing_epoch >= 1), CONSTRAINT chk_payment_merchant_routing_success_amount CHECK (amount > 0) ); CREATE INDEX idx_payment_merchant_routing_success_select ON tb_payment_merchant_routing_success (pool_id, routing_epoch, merchant_id, paid_at, id); ALTER TABLE tb_payment ADD COLUMN merchant_id BIGINT, ADD COLUMN merchant_pool_id BIGINT, ADD COLUMN merchant_name_snapshot VARCHAR(100), ADD COLUMN merchant_payment_method_snapshot VARCHAR(20), ADD COLUMN merchant_provider_type_snapshot VARCHAR(30), ADD COLUMN merchant_pool_name_snapshot VARCHAR(100), ADD COLUMN routing_strategy_snapshot VARCHAR(20), ADD COLUMN routing_epoch BIGINT; CREATE INDEX idx_payment_merchant_route ON tb_payment (merchant_id, routing_epoch, status, id) WHERE deleted_at IS NULL AND merchant_id IS NOT NULL; CREATE INDEX idx_payment_merchant_pool_route ON tb_payment (merchant_pool_id, routing_epoch, id) WHERE deleted_at IS NULL AND merchant_pool_id IS NOT NULL; -- 从唯一生效综合配置复制完整凭证。单成员池使用一分钟时间轮询, -- 对单成员没有选择差异;后续成员或策略调整由管理配置显式产生新世代。 DO $$ DECLARE active_count BIGINT; legacy tb_wechat_config%ROWTYPE; copied_merchant_id BIGINT; copied_pool_id BIGINT; BEGIN SELECT COUNT(*) INTO active_count FROM tb_wechat_config WHERE is_active = TRUE AND deleted_at IS NULL; IF active_count > 1 THEN RAISE EXCEPTION '存在多条生效综合支付配置,无法安全迁移商户池'; END IF; IF active_count = 0 THEN RETURN; END IF; SELECT * INTO legacy FROM tb_wechat_config WHERE is_active = TRUE AND deleted_at IS NULL; IF legacy.provider_type IN ('wechat', 'wechat_v2') AND legacy.wx_mch_id <> '' AND ((legacy.provider_type = 'wechat' AND legacy.wx_api_v3_key <> '' AND legacy.wx_cert_content <> '' AND legacy.wx_key_content <> '' AND legacy.wx_serial_no <> '' AND legacy.wx_notify_url <> '') OR (legacy.provider_type = 'wechat_v2' AND legacy.wx_api_v2_key <> '' AND legacy.wx_notify_url <> '')) THEN INSERT INTO tb_payment_merchant (name, payment_method, provider_type, merchant_identity, credentials, credential_version, status) VALUES (legacy.name || '-微信商户', 'wechat', legacy.provider_type, legacy.wx_mch_id, jsonb_build_object('wx_mch_id', legacy.wx_mch_id, 'wx_api_v3_key', legacy.wx_api_v3_key, 'wx_api_v2_key', legacy.wx_api_v2_key, 'wx_cert_content', legacy.wx_cert_content, 'wx_key_content', legacy.wx_key_content, 'wx_serial_no', legacy.wx_serial_no, 'wx_notify_url', legacy.wx_notify_url), 1, 1) RETURNING id INTO copied_merchant_id; ELSIF legacy.provider_type = 'fuiou' AND legacy.fy_ins_cd <> '' AND legacy.fy_mchnt_cd <> '' AND legacy.fy_term_id <> '' AND legacy.fy_private_key <> '' AND legacy.fy_public_key <> '' AND legacy.fy_api_url <> '' AND legacy.fy_notify_url <> '' THEN INSERT INTO tb_payment_merchant (name, payment_method, provider_type, merchant_identity, credentials, credential_version, status) VALUES (legacy.name || '-富友商户', 'wechat', 'fuiou', legacy.fy_mchnt_cd, jsonb_build_object('fy_ins_cd', legacy.fy_ins_cd, 'fy_mchnt_cd', legacy.fy_mchnt_cd, 'fy_term_id', legacy.fy_term_id, 'fy_private_key', legacy.fy_private_key, 'fy_public_key', legacy.fy_public_key, 'fy_api_url', legacy.fy_api_url, 'fy_notify_url', legacy.fy_notify_url), 1, 1) RETURNING id INTO copied_merchant_id; END IF; IF copied_merchant_id IS NOT NULL THEN INSERT INTO tb_payment_merchant_pool (name, payment_method, status, strategy, time_period_value, time_period_unit, time_period_started_at, routing_epoch) VALUES ('迁移微信商户池', 'wechat', 1, 'time', 1, 'minute', NOW(), 1) RETURNING id INTO copied_pool_id; INSERT INTO tb_payment_merchant_pool_member (pool_id, merchant_id, sort_order) VALUES (copied_pool_id, copied_merchant_id, 0); END IF; copied_merchant_id := NULL; IF legacy.ali_app_id <> '' AND legacy.ali_private_key <> '' AND legacy.ali_public_key <> '' AND legacy.ali_notify_url <> '' AND legacy.ali_return_url <> '' THEN INSERT INTO tb_payment_merchant (name, payment_method, provider_type, merchant_identity, credentials, credential_version, status) VALUES (legacy.name || '-支付宝商户', 'alipay', 'alipay', legacy.ali_app_id, jsonb_build_object('ali_app_id', legacy.ali_app_id, 'ali_private_key', legacy.ali_private_key, 'ali_public_key', legacy.ali_public_key, 'ali_notify_url', legacy.ali_notify_url, 'ali_return_url', legacy.ali_return_url, 'ali_production', legacy.ali_production, 'ali_pay_expire_minutes', legacy.ali_pay_expire_minutes), 1, 1) RETURNING id INTO copied_merchant_id; INSERT INTO tb_payment_merchant_pool (name, payment_method, status, strategy, time_period_value, time_period_unit, time_period_started_at, routing_epoch) VALUES ('迁移支付宝商户池', 'alipay', 1, 'time', 1, 'minute', NOW(), 1) RETURNING id INTO copied_pool_id; INSERT INTO tb_payment_merchant_pool_member (pool_id, merchant_id, sort_order) VALUES (copied_pool_id, copied_merchant_id, 0); END IF; IF legacy.oa_app_id <> '' AND legacy.oa_app_secret <> '' AND legacy.miniapp_app_id <> '' AND legacy.miniapp_app_secret <> '' THEN INSERT INTO tb_wechat_authorization (oa_app_id, oa_app_secret, oa_token, oa_aes_key, oa_oauth_redirect_url, miniapp_app_id, miniapp_app_secret, credential_version, status) VALUES (legacy.oa_app_id, legacy.oa_app_secret, legacy.oa_token, legacy.oa_aes_key, legacy.oa_oauth_redirect_url, legacy.miniapp_app_id, legacy.miniapp_app_secret, 1, 1); END IF; END $$; COMMENT ON TABLE tb_payment_merchant IS '实际收款商户,凭证仅在受控字段保存'; COMMENT ON TABLE tb_payment_merchant_pool IS '按支付方式轮询实际收款商户的商户池'; COMMENT ON TABLE tb_payment_merchant_pool_member IS '商户池有序成员;支付方式一致性由应用事务校验'; COMMENT ON TABLE tb_wechat_authorization IS 'C端微信授权配置,不保存收款商户凭证'; COMMENT ON TABLE tb_payment_merchant_routing_success IS '支付首次成功的路由累计唯一事实'; COMMENT ON COLUMN tb_payment.routing_epoch IS '支付创建时冻结的商户池统计世代';