feat(media-supply): add media resource supply marketplace
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Failing after 7m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m4s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Failing after 7m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m4s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s
Introduce an end-to-end media-supply feature: tenant-side resource sync service/worker backed by a Meijiequan supplier client, ops-side management APIs, and admin/ops web views for resources, orders, favorites and submission. Adds a shared digitocr helper, MediaSupply config blocks for tenant and ops, shared types, and migrations for supplier media resources, price overrides, customer visibility and order refunds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
DROP TABLE IF EXISTS media_supply_sync_jobs;
|
||||
DROP TABLE IF EXISTS media_supply_wallet_ledgers;
|
||||
DROP TABLE IF EXISTS media_supply_order_items;
|
||||
DROP TABLE IF EXISTS media_supply_orders;
|
||||
DROP TABLE IF EXISTS media_supply_user_wallets;
|
||||
DROP TABLE IF EXISTS supplier_media_price_overrides;
|
||||
DROP TABLE IF EXISTS supplier_media_resources;
|
||||
@@ -0,0 +1,173 @@
|
||||
CREATE TABLE IF NOT EXISTS supplier_media_resources (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
supplier VARCHAR(32) NOT NULL,
|
||||
supplier_resource_id VARCHAR(64) NOT NULL,
|
||||
model_id INT NOT NULL,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
||||
cost_price_cents BIGINT NOT NULL DEFAULT 0,
|
||||
cost_prices_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
sale_price_label VARCHAR(64),
|
||||
resource_url TEXT,
|
||||
baidu_weight INT,
|
||||
resource_remark TEXT,
|
||||
customer_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
channel_type VARCHAR(128),
|
||||
region VARCHAR(128),
|
||||
inclusion_effect VARCHAR(128),
|
||||
link_type VARCHAR(128),
|
||||
publish_rate VARCHAR(64),
|
||||
delivery_speed VARCHAR(128),
|
||||
supplier_updated_at TIMESTAMPTZ,
|
||||
last_synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
raw_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uk_supplier_media_resources_supplier_id UNIQUE (supplier, supplier_resource_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_media_resources_model_status
|
||||
ON supplier_media_resources (supplier, model_id, status, updated_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_media_resources_name_trgm_fallback
|
||||
ON supplier_media_resources (supplier, name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supplier_media_price_overrides (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
resource_id BIGINT NOT NULL REFERENCES supplier_media_resources(id) ON DELETE CASCADE,
|
||||
price_type VARCHAR(64) NOT NULL DEFAULT 'price',
|
||||
sell_price_cents BIGINT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
updated_by BIGINT REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_supplier_media_price_override_non_negative CHECK (sell_price_cents >= 0),
|
||||
CONSTRAINT uk_supplier_media_price_overrides_resource_type UNIQUE (resource_id, price_type)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_media_price_overrides_resource
|
||||
ON supplier_media_price_overrides (resource_id, enabled);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_supply_user_wallets (
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
balance_cents BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (tenant_id, user_id),
|
||||
CONSTRAINT chk_media_supply_user_wallet_balance_non_negative CHECK (balance_cents >= 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_supply_orders (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
workspace_id BIGINT NOT NULL REFERENCES workspaces(id),
|
||||
brand_id BIGINT NOT NULL REFERENCES brands(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
article_id BIGINT REFERENCES articles(id),
|
||||
supplier VARCHAR(32) NOT NULL,
|
||||
model_id INT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'queued',
|
||||
title VARCHAR(500) NOT NULL,
|
||||
content_snapshot TEXT NOT NULL,
|
||||
remark TEXT,
|
||||
order_brand VARCHAR(255),
|
||||
external_order_id VARCHAR(128),
|
||||
external_order_code VARCHAR(128),
|
||||
supplier_status VARCHAR(64),
|
||||
cost_total_cents BIGINT NOT NULL DEFAULT 0,
|
||||
sell_total_cents BIGINT NOT NULL DEFAULT 0,
|
||||
wallet_debit_cents BIGINT NOT NULL DEFAULT 0,
|
||||
wallet_debited_at TIMESTAMPTZ,
|
||||
wallet_refunded_at TIMESTAMPTZ,
|
||||
request_payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
response_payload_json JSONB,
|
||||
error_message TEXT,
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
submitted_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT chk_media_supply_order_totals_non_negative CHECK (cost_total_cents >= 0 AND sell_total_cents >= 0),
|
||||
CONSTRAINT chk_media_supply_order_no_loss CHECK (sell_total_cents >= cost_total_cents),
|
||||
CONSTRAINT chk_media_supply_order_wallet_debit_non_negative CHECK (wallet_debit_cents >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_orders_user_created
|
||||
ON media_supply_orders (tenant_id, user_id, created_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_orders_status_next
|
||||
ON media_supply_orders (supplier, status, next_attempt_at, created_at)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_orders_article
|
||||
ON media_supply_orders (tenant_id, article_id, created_at DESC)
|
||||
WHERE article_id IS NOT NULL AND deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_supply_order_items (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
order_id BIGINT NOT NULL REFERENCES media_supply_orders(id) ON DELETE CASCADE,
|
||||
resource_id BIGINT NOT NULL REFERENCES supplier_media_resources(id),
|
||||
supplier_resource_id VARCHAR(64) NOT NULL,
|
||||
price_type VARCHAR(64) NOT NULL DEFAULT 'price',
|
||||
resource_name_snapshot VARCHAR(500) NOT NULL,
|
||||
locked_cost_price_cents BIGINT NOT NULL,
|
||||
locked_sell_price_cents BIGINT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'queued',
|
||||
external_article_url TEXT,
|
||||
raw_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_media_supply_order_item_prices_non_negative CHECK (locked_cost_price_cents >= 0 AND locked_sell_price_cents >= 0),
|
||||
CONSTRAINT chk_media_supply_order_item_no_loss CHECK (locked_sell_price_cents >= locked_cost_price_cents)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_order_items_order
|
||||
ON media_supply_order_items (order_id, id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_supply_wallet_ledgers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
order_id BIGINT REFERENCES media_supply_orders(id) ON DELETE SET NULL,
|
||||
delta_cents BIGINT NOT NULL,
|
||||
balance_after_cents BIGINT NOT NULL,
|
||||
reason VARCHAR(128) NOT NULL,
|
||||
note TEXT,
|
||||
created_by BIGINT REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_media_supply_wallet_ledger_balance_non_negative CHECK (balance_after_cents >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_wallet_ledgers_user_created
|
||||
ON media_supply_wallet_ledgers (tenant_id, user_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_wallet_ledgers_user_recent
|
||||
ON media_supply_wallet_ledgers (tenant_id, user_id, created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_wallet_ledgers_order
|
||||
ON media_supply_wallet_ledgers (order_id)
|
||||
WHERE order_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_supply_sync_jobs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
supplier VARCHAR(32) NOT NULL,
|
||||
model_id INT,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'queued',
|
||||
requested_by BIGINT REFERENCES users(id),
|
||||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
error_message TEXT,
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
stats_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_supply_sync_jobs_status_created
|
||||
ON media_supply_sync_jobs (supplier, status, created_at);
|
||||
@@ -0,0 +1,6 @@
|
||||
DROP INDEX IF EXISTS idx_supplier_media_resources_remark;
|
||||
|
||||
ALTER TABLE supplier_media_resources
|
||||
DROP COLUMN IF EXISTS resource_remark,
|
||||
DROP COLUMN IF EXISTS baidu_weight,
|
||||
DROP COLUMN IF EXISTS resource_url;
|
||||
@@ -0,0 +1,17 @@
|
||||
ALTER TABLE supplier_media_resources
|
||||
ADD COLUMN IF NOT EXISTS resource_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS baidu_weight INT,
|
||||
ADD COLUMN IF NOT EXISTS resource_remark TEXT;
|
||||
|
||||
UPDATE supplier_media_resources
|
||||
SET resource_url = NULLIF(BTRIM(COALESCE(raw_json->>'meitianli', raw_json->>'resource_url', raw_json->>'url', raw_json->>'site_url', raw_json->>'link')), ''),
|
||||
baidu_weight = CASE
|
||||
WHEN BTRIM(COALESCE(raw_json->>'meitiquanzhong', raw_json->>'baidu_weight', raw_json->>'weight', raw_json->>'quanzhong')) ~ '^[0-9]+$'
|
||||
THEN BTRIM(COALESCE(raw_json->>'meitiquanzhong', raw_json->>'baidu_weight', raw_json->>'weight', raw_json->>'quanzhong'))::INT
|
||||
ELSE NULL
|
||||
END,
|
||||
resource_remark = NULLIF(BTRIM(COALESCE(raw_json->>'meitibeizhu', raw_json->>'remark', raw_json->>'beizhu', raw_json->>'note', raw_json->>'description')), '')
|
||||
WHERE resource_url IS NULL OR baidu_weight IS NULL OR resource_remark IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_media_resources_remark
|
||||
ON supplier_media_resources (supplier, resource_remark);
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_supplier_media_resources_customer_visible;
|
||||
|
||||
ALTER TABLE supplier_media_resources
|
||||
DROP COLUMN IF EXISTS customer_visible;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE supplier_media_resources
|
||||
ADD COLUMN IF NOT EXISTS customer_visible BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_supplier_media_resources_customer_visible
|
||||
ON supplier_media_resources (supplier, model_id, customer_visible, updated_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
-- Irreversible data repair: refunded terminal media supply orders should not be re-debited.
|
||||
@@ -0,0 +1,88 @@
|
||||
WITH terminal_orders AS (
|
||||
SELECT id,
|
||||
tenant_id,
|
||||
user_id,
|
||||
wallet_debit_cents,
|
||||
title,
|
||||
error_message
|
||||
FROM media_supply_orders
|
||||
WHERE supplier = 'meijiequan'
|
||||
AND status IN ('queued', 'submitting')
|
||||
AND wallet_debited_at IS NOT NULL
|
||||
AND wallet_refunded_at IS NULL
|
||||
AND wallet_debit_cents > 0
|
||||
AND deleted_at IS NULL
|
||||
AND (
|
||||
error_message ILIKE 'failed to refresh supplier price%'
|
||||
OR error_message ILIKE 'meijiequan create order failed:%'
|
||||
OR error_message ILIKE 'supplier cost increased above locked sell price%'
|
||||
)
|
||||
),
|
||||
wallet_snapshot AS (
|
||||
SELECT o.*,
|
||||
w.balance_cents AS balance_before,
|
||||
SUM(o.wallet_debit_cents) OVER (
|
||||
PARTITION BY o.tenant_id, o.user_id
|
||||
ORDER BY o.id
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS running_refund
|
||||
FROM terminal_orders o
|
||||
JOIN media_supply_user_wallets w
|
||||
ON w.tenant_id = o.tenant_id
|
||||
AND w.user_id = o.user_id
|
||||
),
|
||||
refund_totals AS (
|
||||
SELECT tenant_id,
|
||||
user_id,
|
||||
SUM(wallet_debit_cents) AS total_refund
|
||||
FROM terminal_orders
|
||||
GROUP BY tenant_id, user_id
|
||||
),
|
||||
updated_wallets AS (
|
||||
UPDATE media_supply_user_wallets w
|
||||
SET balance_cents = w.balance_cents + rt.total_refund,
|
||||
updated_at = NOW()
|
||||
FROM refund_totals rt
|
||||
WHERE w.tenant_id = rt.tenant_id
|
||||
AND w.user_id = rt.user_id
|
||||
RETURNING w.tenant_id, w.user_id
|
||||
),
|
||||
updated_orders AS (
|
||||
UPDATE media_supply_orders o
|
||||
SET status = 'failed',
|
||||
wallet_refunded_at = NOW(),
|
||||
next_attempt_at = NOW(),
|
||||
updated_at = NOW()
|
||||
FROM terminal_orders t
|
||||
WHERE o.id = t.id
|
||||
RETURNING o.id
|
||||
),
|
||||
updated_items AS (
|
||||
UPDATE media_supply_order_items i
|
||||
SET status = 'failed',
|
||||
updated_at = NOW()
|
||||
FROM terminal_orders t
|
||||
WHERE i.order_id = t.id
|
||||
AND i.status IN ('queued', 'submitting')
|
||||
RETURNING i.id
|
||||
)
|
||||
INSERT INTO media_supply_wallet_ledgers (
|
||||
tenant_id,
|
||||
user_id,
|
||||
order_id,
|
||||
delta_cents,
|
||||
balance_after_cents,
|
||||
reason,
|
||||
note,
|
||||
created_by
|
||||
)
|
||||
SELECT ws.tenant_id,
|
||||
ws.user_id,
|
||||
ws.id,
|
||||
ws.wallet_debit_cents,
|
||||
ws.balance_before + ws.running_refund,
|
||||
'order_refund',
|
||||
COALESCE(NULLIF(ws.error_message, ''), ws.title),
|
||||
ws.user_id
|
||||
FROM wallet_snapshot ws
|
||||
JOIN updated_orders uo ON uo.id = ws.id;
|
||||
Reference in New Issue
Block a user