新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付 (C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件 经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空 历史支付继续按 payment_config_id 双读。凭证版本化加载与 ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部 引用。
This commit is contained in:
32
migrations/000210_add_payment_merchant_pools.down.sql
Normal file
32
migrations/000210_add_payment_merchant_pools.down.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- 回滚商户池支付路由 Schema。
|
||||
-- 任一新路由支付、成功累计或新配置事实存在时均禁止破坏性回滚。
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM tb_payment WHERE merchant_id IS NOT NULL OR merchant_pool_id IS NOT NULL)
|
||||
OR EXISTS (SELECT 1 FROM tb_payment_merchant_routing_success)
|
||||
OR EXISTS (SELECT 1 FROM tb_payment_merchant)
|
||||
OR EXISTS (SELECT 1 FROM tb_payment_merchant_pool)
|
||||
OR EXISTS (SELECT 1 FROM tb_payment_merchant_pool_member)
|
||||
OR EXISTS (SELECT 1 FROM tb_wechat_authorization) THEN
|
||||
RAISE EXCEPTION '存在商户池支付路由业务事实,禁止回滚商户池支付路由迁移';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DROP INDEX IF EXISTS idx_payment_merchant_pool_route;
|
||||
DROP INDEX IF EXISTS idx_payment_merchant_route;
|
||||
|
||||
ALTER TABLE tb_payment
|
||||
DROP COLUMN IF EXISTS routing_epoch,
|
||||
DROP COLUMN IF EXISTS routing_strategy_snapshot,
|
||||
DROP COLUMN IF EXISTS merchant_pool_name_snapshot,
|
||||
DROP COLUMN IF EXISTS merchant_provider_type_snapshot,
|
||||
DROP COLUMN IF EXISTS merchant_payment_method_snapshot,
|
||||
DROP COLUMN IF EXISTS merchant_name_snapshot,
|
||||
DROP COLUMN IF EXISTS merchant_pool_id,
|
||||
DROP COLUMN IF EXISTS merchant_id;
|
||||
|
||||
DROP TABLE IF EXISTS tb_payment_merchant_routing_success;
|
||||
DROP TABLE IF EXISTS tb_wechat_authorization;
|
||||
DROP TABLE IF EXISTS tb_payment_merchant_pool_member;
|
||||
DROP TABLE IF EXISTS tb_payment_merchant_pool;
|
||||
DROP TABLE IF EXISTS tb_payment_merchant;
|
||||
237
migrations/000210_add_payment_merchant_pools.up.sql
Normal file
237
migrations/000210_add_payment_merchant_pools.up.sql
Normal file
@@ -0,0 +1,237 @@
|
||||
-- 商户池支付路由:收款商户、商户池、微信授权、成功统计事实及支付快照。
|
||||
-- 不使用数据库外键;关联以 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 '支付创建时冻结的商户池统计世代';
|
||||
Reference in New Issue
Block a user