feat(imports): add real pricing and subscription collectors

Add plan catalog and subscription schema support, seed baselines, and real importers for core domestic subscriptions plus stable official pricing sources.

This commit also hardens the shared fetch layers so the importers can support live collection and database writes instead of relying on manual placeholders alone.
This commit is contained in:
phamnazage-jpg
2026-05-15 22:32:57 +08:00
parent dd58c18fe3
commit 958245537a
91 changed files with 10474 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
-- Phase 2: Token Plan / Coding Plan 基础目录清单
CREATE TABLE IF NOT EXISTS plan_catalog_inventory (
id BIGSERIAL PRIMARY KEY,
provider_id BIGINT REFERENCES model_provider(id) ON DELETE SET NULL,
operator_id BIGINT REFERENCES operator(id) ON DELETE SET NULL,
catalog_code TEXT NOT NULL UNIQUE,
platform_name TEXT NOT NULL,
platform_name_cn TEXT,
platform_type TEXT NOT NULL,
plan_family TEXT NOT NULL,
plan_status TEXT NOT NULL DEFAULT 'confirmed',
source_url TEXT NOT NULL,
source_title TEXT,
source_kind TEXT NOT NULL DEFAULT 'official_doc',
region TEXT NOT NULL DEFAULT 'global',
currency TEXT,
billing_cycle TEXT,
last_checked_at TIMESTAMP NOT NULL,
importer_key TEXT,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
CONSTRAINT chk_plan_catalog_platform_type
CHECK (platform_type IN ('official_vendor', 'cloud_operator', 'relay_platform')),
CONSTRAINT chk_plan_catalog_family
CHECK (plan_family IN ('token_plan', 'coding_plan', 'package_plan', 'pay_as_you_go', 'unknown')),
CONSTRAINT chk_plan_catalog_status
CHECK (plan_status IN ('confirmed', 'pending_verification', 'retired')),
CONSTRAINT chk_plan_catalog_source_kind
CHECK (source_kind IN ('official_doc', 'official_pricing', 'official_product_page', 'official_community', 'inferred')),
CONSTRAINT chk_plan_catalog_currency
CHECK (currency IS NULL OR currency IN ('CNY', 'USD', 'EUR'))
);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_provider_id ON plan_catalog_inventory(provider_id);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_operator_id ON plan_catalog_inventory(operator_id);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_family ON plan_catalog_inventory(plan_family);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_platform_type ON plan_catalog_inventory(platform_type);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_status ON plan_catalog_inventory(plan_status);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_last_checked_at ON plan_catalog_inventory(last_checked_at);
COMMENT ON TABLE plan_catalog_inventory IS 'Token Plan / Coding Plan / 套餐包 / 按量计费基础目录清单,用于后续 importer 排期与证据管理';
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'update_updated_at_column')
AND NOT EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = 'plan_catalog_inventory_updated_at'
) THEN
CREATE TRIGGER plan_catalog_inventory_updated_at
BEFORE UPDATE ON plan_catalog_inventory
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END
$$;

View File

@@ -0,0 +1,35 @@
-- Phase 2: 基础目录增加榜单分组与排名信息
ALTER TABLE plan_catalog_inventory
ADD COLUMN IF NOT EXISTS catalog_segment TEXT NOT NULL DEFAULT 'general',
ADD COLUMN IF NOT EXISTS market_rank INTEGER;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_plan_catalog_segment'
) THEN
ALTER TABLE plan_catalog_inventory
ADD CONSTRAINT chk_plan_catalog_segment
CHECK (catalog_segment IN ('general', 'vendor_top20', 'relay_top20plus', 'global_reference'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_plan_catalog_market_rank'
) THEN
ALTER TABLE plan_catalog_inventory
ADD CONSTRAINT chk_plan_catalog_market_rank
CHECK (market_rank IS NULL OR market_rank > 0);
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_plan_catalog_segment ON plan_catalog_inventory(catalog_segment);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_market_rank ON plan_catalog_inventory(market_rank);
COMMENT ON COLUMN plan_catalog_inventory.catalog_segment IS '目录分组general / vendor_top20 / relay_top20plus / global_reference';
COMMENT ON COLUMN plan_catalog_inventory.market_rank IS '榜单排序,数字越小优先级越高';

View File

@@ -0,0 +1,22 @@
-- 补齐 operator.type 字段,避免订阅与目录 importer 在新库中失败
ALTER TABLE operator
ADD COLUMN IF NOT EXISTS type TEXT NOT NULL DEFAULT 'reseller';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_operator_type'
) THEN
ALTER TABLE operator
ADD CONSTRAINT chk_operator_type
CHECK (type IN ('official', 'cloud', 'relay', 'reseller'));
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_operator_type ON operator(type);
COMMENT ON COLUMN operator.type IS '运营方类型official / cloud / relay / reseller';

View File

@@ -0,0 +1,8 @@
-- Phase 2: 订阅套餐表支持 package_plan
ALTER TABLE subscription_plan
DROP CONSTRAINT IF EXISTS subscription_plan_plan_family_check;
ALTER TABLE subscription_plan
ADD CONSTRAINT subscription_plan_plan_family_check
CHECK (plan_family IN ('token_plan', 'coding_plan', 'package_plan'));

View File

@@ -0,0 +1,41 @@
-- 第一模块:每日关键信号快照
CREATE TABLE IF NOT EXISTS daily_signal_snapshot (
id BIGSERIAL PRIMARY KEY,
signal_date DATE NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'generated',
new_models INTEGER NOT NULL DEFAULT 0,
price_changes INTEGER NOT NULL DEFAULT 0,
official_free INTEGER NOT NULL DEFAULT 0,
aggregator_free INTEGER NOT NULL DEFAULT 0,
unknown_free INTEGER NOT NULL DEFAULT 0,
event_count INTEGER NOT NULL DEFAULT 0,
page_mode TEXT NOT NULL DEFAULT 'standard',
event_type_counts JSONB NOT NULL DEFAULT '{}'::jsonb,
top_events JSONB NOT NULL DEFAULT '[]'::jsonb,
source_audit TEXT,
generated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_daily_signal_snapshot_date ON daily_signal_snapshot(signal_date);
CREATE INDEX IF NOT EXISTS idx_daily_signal_snapshot_status ON daily_signal_snapshot(status);
COMMENT ON TABLE daily_signal_snapshot IS '第一模块产出的每日关键信号快照,用于日报与其他下游形态消费';
COMMENT ON COLUMN daily_signal_snapshot.top_events IS '已筛选的关键事件数组JSONB 序列化 ModelEvent';
COMMENT ON COLUMN daily_signal_snapshot.event_type_counts IS '按事件类型聚合的数量统计';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = 'daily_signal_snapshot_updated_at'
) THEN
CREATE TRIGGER daily_signal_snapshot_updated_at
BEFORE UPDATE ON daily_signal_snapshot
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END
$$;

View File

@@ -0,0 +1,21 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_models_date_source_kind') THEN
ALTER TABLE models DROP CONSTRAINT chk_models_date_source_kind;
END IF;
ALTER TABLE models
ADD CONSTRAINT chk_models_date_source_kind
CHECK (
date_source_kind IN (
'official_announcement',
'official_product_page',
'official_pricing',
'secondary_authoritative_report',
'catalog_backfill',
'unknown'
)
);
END $$;
COMMENT ON COLUMN models.date_source_kind IS '发布日期证据来源类型official_announcement / official_product_page / official_pricing / secondary_authoritative_report / catalog_backfill / unknown';