-- CSG 长期归档修复 (2026-09-21) -- 背景:TimescaleDB 任务 1008 `csg_daily_snapshot()` 自 2026-08-29 创建起 0 成功 / 294 失败。 -- 原因:函数签名是零参数 `csg_daily_snapshot()`,而 TimescaleDB 自定义 job 动作 -- 按名字调用 `schema.proc(job_id integer, config jsonb)`,解析不到 → 每次报 -- `function or procedure "public.csg_daily_snapshot(integer, jsonb)" does not exist`。 -- 后果:csg_history 的 day 行停在 2026-08-28、month 行停在 2026-08-01(且 2026-08 的 -- month 行是 08-29 当时的「本月至今」302.47 kWh / 180.28 元,不是月终值 331.22 / 198.65)。 -- -- 本文件的三个动作(幂等,可重复执行): -- 1. `csg_ladder_cost(kwh, month)` —— 阶梯电价助手(广州:夏季 5-10 月 260/600, -- 非夏季 200/400;0.589 / 0.639 / 0.889 元每 kWh)。归档侧的唯一计价来源。 -- 2. `csg_daily_snapshot(job_id integer, config jsonb)` v3 —— 兼容 TimescaleDB job 签名; -- 并把 month 行按「数据日期 d 所属月份」归属: -- · d 属于当前月 → usage 取集成本月累计,cost 用阶梯助手重算 -- · d 属于上月 → usage 由该月 day 行汇总(不读集成 last_month,避开翻月瞬态: -- 2026-08-31 16:32 实测 last_month_total_usage 曾读到 323.49 这种误值) -- 并加「不降级」保护:day 行汇总结果小于已记录的 month 行时不动。 -- 3. 回填 2026-08-29 → 2026-09-19 的 day 行(数据源:08-29..08-31 取 scribe -- states_raw 里 `yesterday_kwh` 观测值减一天;09-01..09-19 取 CSG 集成 -- `this_month_by_day`,其中 09-06 = 9.29 是 scribe 漏采的观测)。日费用一律用 -- 「当日用电 × 当日所处的边际档位」计算,使整月 day 费用之和 == 阶梯月费用。 -- -- 回滚(脚本内已自动建快照表 csg_history_bak_20260921): -- BEGIN; -- SELECT delete_job((SELECT job_id FROM timescaledb_information.jobs WHERE proc_name='csg_daily_snapshot')); -- DROP FUNCTION IF EXISTS public.csg_daily_snapshot(integer, jsonb); -- CREATE FUNCTION public.csg_daily_snapshot() RETURNS void LANGUAGE plpgsql AS $f$ ...原函数体... $f$; -- SELECT add_job('public.csg_daily_snapshot'::regproc, INTERVAL '1 day'); -- DROP FUNCTION IF EXISTS public.csg_ladder_cost(numeric, date); -- DELETE FROM csg_history; INSERT INTO csg_history SELECT * FROM csg_history_bak_20260921; -- COMMIT; -- (或整体放弃归档:DROP TABLE csg_history + 删任务,见 hosts/pgdb.md 回滚段) \set ON_ERROR_STOP on BEGIN; -- ---------- 0. 回滚快照(只建一次) ---------- CREATE TABLE IF NOT EXISTS public.csg_history_bak_20260921 AS TABLE public.csg_history; -- ---------- 1. 阶梯电价助手 ---------- CREATE OR REPLACE FUNCTION public.csg_ladder_cost(kwh numeric, month_date date) RETURNS numeric LANGUAGE sql IMMUTABLE AS $function$ SELECT CASE WHEN kwh IS NULL OR kwh <= 0 THEN 0::numeric WHEN kwh <= (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) THEN round(kwh * 0.589, 2) WHEN kwh <= (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END) THEN round((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) * 0.589 + (kwh - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)) * 0.639, 2) ELSE round((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) * 0.589 + ((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END) - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)) * 0.639 + (kwh - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)) * 0.889, 2) END; $function$; -- ---------- 2. 重建为 TimescaleDB 兼容签名的 v3 + 重挂 job ---------- -- 注意:不能直接 DROP 旧函数——TimescaleDB job 1008 持有依赖 -- (ERROR: cannot drop public.csg_daily_snapshot because background job 1008 depends on it)。 -- 因此:delete_job → 换签名重建函数 → add_job 重挂,并把排程对齐文档口径 -- (22:30 Asia/Shanghai = 14:30 UTC;原来实际跑在 22:00 UTC = 06:00 CST, -- 那个时点集成还没发布前一日数据,d 会晚一天)。 DO $do$ BEGIN IF EXISTS (SELECT 1 FROM timescaledb_information.jobs WHERE job_id = 1008) THEN PERFORM delete_job(1008); END IF; END $do$; DROP FUNCTION IF EXISTS public.csg_daily_snapshot(); CREATE OR REPLACE FUNCTION public.csg_daily_snapshot( job_id integer DEFAULT NULL, config jsonb DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $function$ DECLARE d date; v_usage numeric; v_cost numeric; v_ladder text; v_balance numeric; m_usage numeric; v_tariff numeric; p_month date; p_usage numeric; prev_usage numeric; BEGIN -- 数据日期 = CSG 集成「最新一天」(数据滞后一天);COALESCE 兜底昨天 SELECT COALESCE( (SELECT (attributes->>'latest_day_date')::date FROM states_raw s JOIN entities e ON e.id = s.metadata_id WHERE e.entity_id = 'sensor.0800041935246530_latest_day_kwh' ORDER BY s.time DESC LIMIT 1), CURRENT_DATE - 1) INTO d; -- 一律取「最新有值行」:轮询先写瞬态 unknown、~4s 后才写有值行 SELECT s.value INTO v_usage FROM states_raw s JOIN entities e ON e.id=s.metadata_id WHERE e.entity_id='sensor.0800041935246530_yesterday_kwh' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1; SELECT s.value INTO v_cost FROM states_raw s JOIN entities e ON e.id=s.metadata_id WHERE e.entity_id='sensor.0800041935246530_latest_day_cost' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1; SELECT s.state INTO v_ladder FROM states_raw s JOIN entities e ON e.id=s.metadata_id WHERE e.entity_id='sensor.csg_current_ladder' AND s.state IS NOT NULL AND s.state NOT IN ('unknown','unavailable') ORDER BY s.time DESC LIMIT 1; SELECT s.value INTO v_balance FROM states_raw s JOIN entities e ON e.id=s.metadata_id WHERE e.entity_id='sensor.0800041935246530_balance' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1; SELECT s.value INTO m_usage FROM states_raw s JOIN entities e ON e.id=s.metadata_id WHERE e.entity_id='sensor.0800041935246530_this_month_total_usage' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1; -- 日费用回退:原生 latest_day_cost 从未有值(集成侧该传感器恒为 unknown), -- 用「昨日用电 × 当前档费率」估计;账单口径可用时优先原生 SELECT s.value INTO v_tariff FROM states_raw s JOIN entities e ON e.id=s.metadata_id WHERE e.entity_id='sensor.csg_current_ladder_tariff' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1; IF v_cost IS NULL AND v_usage IS NOT NULL AND v_tariff IS NOT NULL THEN v_cost := v_usage * v_tariff; END IF; INSERT INTO csg_history (period, kind, usage_kwh, cost, ladder, balance) VALUES (d, 'day', v_usage, v_cost, v_ladder, v_balance) ON CONFLICT (period, kind) DO UPDATE SET usage_kwh = COALESCE(EXCLUDED.usage_kwh, csg_history.usage_kwh), cost = COALESCE(EXCLUDED.cost, csg_history.cost), ladder = COALESCE(EXCLUDED.ladder, csg_history.ladder), balance = COALESCE(EXCLUDED.balance, csg_history.balance), updated_at = now(); -- month 行按 d 所属月份归属(v3:修掉翻月时用错月份累计的 bug) p_month := date_trunc('month', d)::date; IF p_month = date_trunc('month', CURRENT_DATE)::date THEN -- 当月:集成本月累计优先,缺失则退回 day 行汇总;费用一律阶梯重算 SELECT COALESCE(m_usage, sum(h.usage_kwh)) INTO p_usage FROM csg_history h WHERE h.kind='day' AND h.period >= p_month AND h.period < (p_month + interval '1 month'); ELSE -- 上月/更早:只信该月 day 行(集成翻月时 last_month_* 有瞬态误值) SELECT sum(h.usage_kwh) INTO p_usage FROM csg_history h WHERE h.kind='day' AND h.period >= p_month AND h.period < (p_month + interval '1 month'); SELECT h.usage_kwh INTO prev_usage FROM csg_history h WHERE h.kind='month' AND h.period = p_month; IF p_usage IS NULL OR (prev_usage IS NOT NULL AND prev_usage > p_usage) THEN RETURN; -- day 行不完整,保留已有 month 行,不降级 END IF; END IF; IF p_usage IS NOT NULL AND p_usage > 0 THEN INSERT INTO csg_history (period, kind, usage_kwh, cost) VALUES (p_month, 'month', p_usage, public.csg_ladder_cost(p_usage, p_month)) ON CONFLICT (period, kind) DO UPDATE SET usage_kwh = EXCLUDED.usage_kwh, cost = EXCLUDED.cost, updated_at = now(); END IF; END $function$; -- 重挂每日任务:22:30 Asia/Shanghai(14:30 UTC) DO $do$ BEGIN IF NOT EXISTS (SELECT 1 FROM timescaledb_information.jobs WHERE proc_name = 'csg_daily_snapshot') THEN PERFORM add_job(proc => 'public.csg_daily_snapshot'::regproc, schedule_interval => INTERVAL '1 day', initial_start => date_trunc('day', now() AT TIME ZONE 'UTC') + INTERVAL '14 hours 30 minutes', timezone => 'UTC'); END IF; END $do$; -- ---------- 3. 回填 day 行 2026-08-29 .. 2026-09-19 + 重算月行 ---------- -- 现有 day 行(07-01..08-28 来自首批回填)与新行一起参与「月内累计」窗口, -- 使边际档位计算正确;已有非空 cost 不覆盖。 WITH newrows(period, usage_kwh) AS ( VALUES (DATE '2026-08-29', 10.99::numeric), (DATE '2026-08-30', 10.03::numeric), (DATE '2026-08-31', 7.73::numeric), (DATE '2026-09-01', 8.50::numeric), (DATE '2026-09-02', 6.68::numeric), (DATE '2026-09-03', 8.74::numeric), (DATE '2026-09-04', 6.76::numeric), (DATE '2026-09-05', 7.76::numeric), (DATE '2026-09-06', 9.29::numeric), (DATE '2026-09-07', 7.24::numeric), (DATE '2026-09-08', 13.00::numeric), (DATE '2026-09-09', 7.98::numeric), (DATE '2026-09-10', 8.16::numeric), (DATE '2026-09-11', 8.75::numeric), (DATE '2026-09-12', 10.48::numeric), (DATE '2026-09-13', 11.11::numeric), (DATE '2026-09-14', 7.22::numeric), (DATE '2026-09-15', 9.94::numeric), (DATE '2026-09-16', 7.19::numeric), (DATE '2026-09-17', 10.32::numeric), (DATE '2026-09-18', 7.77::numeric), (DATE '2026-09-19', 11.79::numeric) ), allrows AS ( SELECT h.period, h.usage_kwh FROM csg_history h WHERE h.kind='day' AND h.period BETWEEN DATE '2026-07-01' AND DATE '2026-09-19' UNION ALL SELECT n.period, n.usage_kwh FROM newrows n WHERE NOT EXISTS (SELECT 1 FROM csg_history h WHERE h.kind='day' AND h.period = n.period) ), calc AS ( SELECT period, usage_kwh, CASE WHEN extract(month FROM period) BETWEEN 5 AND 10 THEN 260 ELSE 200 END AS t1, CASE WHEN extract(month FROM period) BETWEEN 5 AND 10 THEN 600 ELSE 400 END AS t2, sum(usage_kwh) OVER (PARTITION BY date_trunc('month', period) ORDER BY period ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS c1 FROM allrows ), slab AS ( SELECT period, usage_kwh, t1, t2, c1, c1 - usage_kwh AS c0 FROM calc ), priced AS ( SELECT period, usage_kwh, round( (CASE WHEN c1 <= t1 THEN c1*0.589 WHEN c1 <= t2 THEN t1*0.589 + (c1-t1)*0.639 ELSE t1*0.589 + (t2-t1)*0.639 + (c1-t2)*0.889 END) - (CASE WHEN c0 <= t1 THEN c0*0.589 WHEN c0 <= t2 THEN t1*0.589 + (c0-t1)*0.639 ELSE t1*0.589 + (t2-t1)*0.639 + (c0-t2)*0.889 END), 2) AS cost, CASE WHEN c1 <= t1 THEN '一档' WHEN c1 <= t2 THEN '二档' ELSE '三档' END AS ladder FROM slab ) INSERT INTO csg_history (period, kind, usage_kwh, cost, ladder) SELECT period, 'day', usage_kwh, cost, ladder FROM priced ON CONFLICT (period, kind) DO UPDATE SET usage_kwh = EXCLUDED.usage_kwh, cost = COALESCE(csg_history.cost, EXCLUDED.cost), ladder = COALESCE(csg_history.ladder, EXCLUDED.ladder), updated_at = now(); -- 月行按 day 行汇总重算(只动 2026-08 / 2026-09;2026-07 及更早的月行来自集成 -- by_month,day 行并不覆盖整月,重算会把它算小,故不触碰) INSERT INTO csg_history (period, kind, usage_kwh, cost) SELECT date_trunc('month', h.period)::date, 'month', round(sum(h.usage_kwh),2), public.csg_ladder_cost(round(sum(h.usage_kwh),2), date_trunc('month', h.period)::date) FROM csg_history h WHERE h.kind='day' AND h.period BETWEEN DATE '2026-08-01' AND DATE '2026-09-30' GROUP BY date_trunc('month', h.period) ON CONFLICT (period, kind) DO UPDATE SET usage_kwh = EXCLUDED.usage_kwh, cost = EXCLUDED.cost, updated_at = now(); COMMIT;