feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本

包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -0,0 +1,41 @@
-- 迁移:补录 cfg_skill_type 缺失的 3 条 skill_id
-- 原因dwd_assistant_service_log 中存在 5525 条记录引用了不在 cfg_skill_type 中的 skill_id
-- 导致 WBI/NCI 的到店判定settle_type=3 + BONUS EXISTS 检查)漏掉了这些到店记录。
-- 典型案例:会员"梅"实际 5 天前到店,但 WBI 显示 16 天未到店display_score 虚高至 9.42。
-- 影响WBI、NCI、关系指数、助教服务统计、工资计算等所有依赖 cfg_skill_type JOIN 的下游。
-- 回滚DELETE FROM dws.cfg_skill_type WHERE skill_id IN (2790683529513797, 2790683529513798, 3039912271463941);
BEGIN;
INSERT INTO dws.cfg_skill_type (skill_id, skill_name, course_type_code, course_type_name, is_active, description)
VALUES
(2790683529513797, '基础课', 'BASE', '基础课', TRUE,
'基础课:飞球系统原始课程类型,与"台球基础陪打"同类'),
(2790683529513798, '附加课', 'BONUS', '附加课', TRUE,
'附加课:飞球系统原始课程类型,与"台球超休服务"同类'),
(3039912271463941, '包厢课', 'BASE', '基础课', TRUE,
'包厢课:飞球系统原始课程类型,与"包厢服务"同类')
ON CONFLICT (skill_id) DO NOTHING;
COMMIT;
-- 验证 SQL执行后应返回 6 行,且无 NULL 的 course_type_code
-- SELECT skill_id, skill_name, course_type_code, is_active FROM dws.cfg_skill_type ORDER BY skill_id;
--
-- 验证受影响会员数(应 > 0
-- SELECT COUNT(DISTINCT asl.tenant_member_id)
-- FROM dwd.dwd_assistant_service_log asl
-- JOIN dws.cfg_skill_type st ON asl.skill_id = st.skill_id
-- WHERE asl.skill_id IN (2790683529513797, 2790683529513798, 3039912271463941)
-- AND asl.is_delete = 0;
--
-- 验证梅的到店记录是否被正确识别:
-- SELECT s.order_settle_id, s.pay_time, s.settle_type,
-- EXISTS (
-- SELECT 1 FROM dwd.dwd_assistant_service_log asl
-- JOIN dws.cfg_skill_type st ON asl.skill_id = st.skill_id AND st.course_type_code = 'BONUS' AND st.is_active = TRUE
-- WHERE asl.order_settle_id = s.order_settle_id AND asl.site_id = s.site_id AND asl.tenant_member_id = s.member_id AND asl.is_delete = 0
-- ) AS has_bonus_service
-- FROM dwd.dwd_settlement_head s
-- WHERE s.member_id = 2975065345119045 AND s.pay_time >= '2026-03-08'
-- ORDER BY s.pay_time DESC;

View File

@@ -0,0 +1,71 @@
-- =============================================================================
-- 迁移:补充 ETL 字段差异分析中识别的缺失字段
-- 日期2026-03-26
-- 来源field_gap_analysis.md 深度评估
-- 影响5 张 ODS 表 + 6 张 DWD 表
-- =============================================================================
BEGIN;
-- ── ODS 层新增列 ──
-- 1. member_profiles: 4 个 API 字段
ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS other_pay_money_sum numeric(18,2);
ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS last_consume_time timestamp without time zone;
ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS non_consume_day_num integer;
ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS first_consumption integer;
-- 2. assistant_service_records: 1 个 API 字段
ALTER TABLE ods.assistant_service_records ADD COLUMN IF NOT EXISTS deduct_leave_seconds integer DEFAULT 0;
-- 3. store_goods_sales_records: 2 个 API 字段
ALTER TABLE ods.store_goods_sales_records ADD COLUMN IF NOT EXISTS activity_amount numeric(18,2) DEFAULT 0;
ALTER TABLE ods.store_goods_sales_records ADD COLUMN IF NOT EXISTS activity_id bigint DEFAULT 0;
-- 4. goods_stock_summary: 1 个 API 字段(注意 ODS 列名全小写)
ALTER TABLE ods.goods_stock_summary ADD COLUMN IF NOT EXISTS createtime timestamp without time zone;
-- 5. table_fee_transactions: order_from
ALTER TABLE ods.table_fee_transactions ADD COLUMN IF NOT EXISTS order_from integer;
-- 6. assistant_service_records: order_from
ALTER TABLE ods.assistant_service_records ADD COLUMN IF NOT EXISTS order_from integer;
-- 7. store_goods_sales_records: order_from
ALTER TABLE ods.store_goods_sales_records ADD COLUMN IF NOT EXISTS order_from integer;
-- 8. settlement_records: order_from来自 settleList.orderFrom展开后为 orderfrom
-- 注意settlement_records 的 ODS 列名是全小写无下划线(与 API camelCase 对应)
ALTER TABLE ods.settlement_records ADD COLUMN IF NOT EXISTS orderfrom integer;
-- ── DWD 层新增列 ──
-- 1. dim_member_ex: 4 个字段 + birthday
ALTER TABLE dwd.dim_member_ex ADD COLUMN IF NOT EXISTS other_pay_money_sum numeric(18,2);
ALTER TABLE dwd.dim_member_ex ADD COLUMN IF NOT EXISTS last_consume_time timestamp with time zone;
ALTER TABLE dwd.dim_member_ex ADD COLUMN IF NOT EXISTS non_consume_day_num integer;
ALTER TABLE dwd.dim_member_ex ADD COLUMN IF NOT EXISTS first_consumption integer;
-- birthday 已在 dim_member 主表中存在,无需重复添加
-- 2. dim_member_card_account_ex: pdassisnatlevel + cxassisnatlevel 已存在于 DDL 中
-- 确认goodscategoryid, tableareaid, pdassisnatlevel, cxassisnatlevel 已在 DWD DDL 和 FACT_MAPPINGS 中
-- 3. dwd_assistant_service_log_ex: deduct_leave_seconds + order_from
ALTER TABLE dwd.dwd_assistant_service_log_ex ADD COLUMN IF NOT EXISTS deduct_leave_seconds integer DEFAULT 0;
ALTER TABLE dwd.dwd_assistant_service_log_ex ADD COLUMN IF NOT EXISTS order_from integer;
-- 4. dwd_store_goods_sale_ex: activity_amount + activity_id + order_from
ALTER TABLE dwd.dwd_store_goods_sale_ex ADD COLUMN IF NOT EXISTS activity_amount numeric(18,2) DEFAULT 0;
ALTER TABLE dwd.dwd_store_goods_sale_ex ADD COLUMN IF NOT EXISTS activity_id bigint DEFAULT 0;
ALTER TABLE dwd.dwd_store_goods_sale_ex ADD COLUMN IF NOT EXISTS order_from integer;
-- 5. dwd_goods_stock_summary: create_time
ALTER TABLE dwd.dwd_goods_stock_summary ADD COLUMN IF NOT EXISTS create_time timestamp with time zone;
-- 6. dwd_table_fee_log_ex: order_from
ALTER TABLE dwd.dwd_table_fee_log_ex ADD COLUMN IF NOT EXISTS order_from integer;
-- 7. dwd_settlement_head_ex: order_from
ALTER TABLE dwd.dwd_settlement_head_ex ADD COLUMN IF NOT EXISTS order_from integer;
COMMIT;

View File

@@ -0,0 +1,51 @@
-- 迁移:财务日报新增支付方式拆分字段
-- 关联board-finance-integration SPEC T1.1
-- 日期2026-03-27
-- 回滚:见文件末尾
-- ============================================================
-- 1. 新增字段
-- ============================================================
ALTER TABLE dws.dws_finance_daily_summary
ADD COLUMN IF NOT EXISTS cash_paper_amount NUMERIC(14,2) DEFAULT 0 NOT NULL,
ADD COLUMN IF NOT EXISTS scan_pay_amount NUMERIC(14,2) DEFAULT 0 NOT NULL;
COMMENT ON COLUMN dws.dws_finance_daily_summary.cash_paper_amount
IS '纸币现金收款dwd_payment.payment_method=2';
COMMENT ON COLUMN dws.dws_finance_daily_summary.scan_pay_amount
IS '扫码收款/离线支付dwd_payment.payment_method=4含微信/支付宝)';
-- ============================================================
-- 2. 更新 RLS 视图暴露新字段
-- ============================================================
CREATE OR REPLACE VIEW app.v_dws_finance_daily_summary AS
SELECT id, site_id, tenant_id, stat_date,
gross_amount, table_fee_amount, goods_amount,
assistant_pd_amount, assistant_cx_amount,
discount_total, discount_groupbuy, discount_vip,
discount_gift_card, discount_manual, discount_rounding, discount_other,
confirmed_income,
cash_inflow_total, cash_pay_amount,
cash_paper_amount, scan_pay_amount,
groupbuy_pay_amount, platform_settlement_amount, platform_fee_amount,
recharge_cash_inflow,
card_consume_total, recharge_card_consume AS cash_card_consume, gift_card_consume,
cash_outflow_total, cash_balance_change,
recharge_count, recharge_total, recharge_cash, recharge_gift,
first_recharge_count, first_recharge_amount,
renewal_count, renewal_amount,
order_count, member_order_count, guest_order_count, avg_order_amount,
created_at, updated_at
FROM dws.dws_finance_daily_summary
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
GRANT SELECT ON app.v_dws_finance_daily_summary TO app_reader;
-- ============================================================
-- 回滚
-- ============================================================
-- DROP VIEW IF EXISTS app.v_dws_finance_daily_summary;
-- ALTER TABLE dws.dws_finance_daily_summary
-- DROP COLUMN IF EXISTS cash_paper_amount,
-- DROP COLUMN IF EXISTS scan_pay_amount;
-- 然后重建原始视图(见 docs/database/ddl/etl_feiqiu__app.sql

View File

@@ -0,0 +1,106 @@
-- 迁移:创建区域日粒度财务原子层表 dws_finance_area_daily 及 RLS 视图
-- 关联board-finance-dws-area-refactor SPEC T2.1
-- 日期2026-03-28
-- 回滚:见文件末尾
-- ============================================================
-- 1. 创建 dws_finance_area_daily 表
-- ============================================================
CREATE TABLE dws.dws_finance_area_daily (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
stat_date DATE NOT NULL,
area_code VARCHAR(20) NOT NULL,
-- 收入结构4 项 + gross_amount
table_fee_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
goods_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
assistant_pd_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
assistant_cx_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
gross_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
-- 优惠拆分6 项 + discount_total
-- 恒等式discount_total = groupbuy + vip + manual + gift_card + rounding + other
discount_groupbuy NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_vip NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_manual NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_gift_card NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_rounding NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_other NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_total NUMERIC(14,2) NOT NULL DEFAULT 0,
-- 确认收入gross_amount - discount_total
confirmed_income NUMERIC(14,2) NOT NULL DEFAULT 0,
-- 现金流(仅 area_code='all' 时有效值,其余区域为 0
cash_pay_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_paper_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
scan_pay_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
groupbuy_pay_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
recharge_cash_inflow NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_inflow_total NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_outflow_total NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_balance_change NUMERIC(14,2) NOT NULL DEFAULT 0,
-- 卡消费(仅 area_code='all' 时有效值)
card_consume_total NUMERIC(14,2) NOT NULL DEFAULT 0,
recharge_card_consume NUMERIC(14,2) NOT NULL DEFAULT 0,
gift_card_consume NUMERIC(14,2) NOT NULL DEFAULT 0,
-- 充值(仅 area_code='all' 时有效值)
recharge_cash NUMERIC(14,2) NOT NULL DEFAULT 0,
first_recharge_cash NUMERIC(14,2) NOT NULL DEFAULT 0,
renewal_cash NUMERIC(14,2) NOT NULL DEFAULT 0,
-- 订单统计
order_count INTEGER NOT NULL DEFAULT 0,
-- 元数据
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 唯一约束:每个站点每天每区域一行
UNIQUE (site_id, stat_date, area_code)
);
COMMENT ON TABLE dws.dws_finance_area_daily
IS '区域日粒度财务原子层表,按 (site_id, stat_date, area_code) 存储 9 个区域的收入/优惠/现金流预计算数据';
COMMENT ON COLUMN dws.dws_finance_area_daily.area_code
IS '区域编码all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv';
COMMENT ON COLUMN dws.dws_finance_area_daily.gross_amount
IS '毛收入 = table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount';
COMMENT ON COLUMN dws.dws_finance_area_daily.discount_total
IS '优惠合计 = groupbuy + vip + manual + gift_card + rounding + other';
COMMENT ON COLUMN dws.dws_finance_area_daily.confirmed_income
IS '确认收入 = gross_amount - discount_total';
COMMENT ON COLUMN dws.dws_finance_area_daily.discount_gift_card
IS '赠送卡消费金额口径(非结算单 gift_card_amount';
-- ============================================================
-- 2. 创建 RLS 视图
-- ============================================================
CREATE OR REPLACE VIEW dws.v_dws_finance_area_daily AS
SELECT *
FROM dws.dws_finance_area_daily
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
GRANT SELECT ON dws.v_dws_finance_area_daily TO app_reader;
-- ============================================================
-- 3. 创建 app schema 导出视图(后端通过 app.v_* 访问)
-- ============================================================
CREATE OR REPLACE VIEW app.v_dws_finance_area_daily AS
SELECT *
FROM dws.dws_finance_area_daily
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
GRANT SELECT ON app.v_dws_finance_area_daily TO app_reader;
-- ============================================================
-- 回滚
-- ============================================================
-- DROP VIEW IF EXISTS app.v_dws_finance_area_daily;
-- DROP VIEW IF EXISTS dws.v_dws_finance_area_daily;
-- DROP TABLE IF EXISTS dws.dws_finance_area_daily;

View File

@@ -0,0 +1,102 @@
-- 迁移:创建看板缓存层表 dws_finance_board_cache 及 RLS 视图
-- 关联board-finance-dws-area-refactor SPEC T5.1
-- 日期2026-03-28
-- 回滚:见文件末尾
-- ============================================================
-- 1. 创建 dws_finance_board_cache 表
-- ============================================================
CREATE TABLE dws.dws_finance_board_cache (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
time_range VARCHAR(20) NOT NULL,
area_code VARCHAR(20) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
prev_start_date DATE,
prev_end_date DATE,
-- overview 8 项核心指标
occurrence NUMERIC(14,2) NOT NULL DEFAULT 0,
discount NUMERIC(14,2) NOT NULL DEFAULT 0,
discount_rate NUMERIC(8,4) NOT NULL DEFAULT 0,
confirmed_revenue NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_in NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_out NUMERIC(14,2) NOT NULL DEFAULT 0,
cash_balance NUMERIC(14,2) NOT NULL DEFAULT 0,
balance_rate NUMERIC(8,4) NOT NULL DEFAULT 0,
-- 数据指纹MD5用于检测源数据变化触发缓存失效
data_fingerprint VARCHAR(64),
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 元数据
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 唯一约束:每个站点每个时间范围每区域一行
UNIQUE (site_id, time_range, area_code)
);
COMMENT ON TABLE dws.dws_finance_board_cache
IS '看板缓存层表,缓存已完成周期的 overview 聚合结果,按 (site_id, time_range, area_code) 唯一';
COMMENT ON COLUMN dws.dws_finance_board_cache.time_range
IS '时间范围lastMonth/lastWeek/lastQuarter/quarter3/half6仅已完成周期缓存';
COMMENT ON COLUMN dws.dws_finance_board_cache.area_code
IS '区域编码all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv';
COMMENT ON COLUMN dws.dws_finance_board_cache.start_date
IS '当期起始日期';
COMMENT ON COLUMN dws.dws_finance_board_cache.end_date
IS '当期结束日期';
COMMENT ON COLUMN dws.dws_finance_board_cache.prev_start_date
IS '上期起始日期(环比用),无上期时为 NULL';
COMMENT ON COLUMN dws.dws_finance_board_cache.prev_end_date
IS '上期结束日期(环比用),无上期时为 NULL';
COMMENT ON COLUMN dws.dws_finance_board_cache.occurrence
IS '发生额(毛收入 gross_amount 的周期汇总)';
COMMENT ON COLUMN dws.dws_finance_board_cache.discount
IS '优惠合计discount_total 的周期汇总)';
COMMENT ON COLUMN dws.dws_finance_board_cache.discount_rate
IS '优惠占比 = discount / occurrence';
COMMENT ON COLUMN dws.dws_finance_board_cache.confirmed_revenue
IS '确认收入 = occurrence - discount';
COMMENT ON COLUMN dws.dws_finance_board_cache.cash_in
IS '现金流入合计(仅 area_code=all 时有效值)';
COMMENT ON COLUMN dws.dws_finance_board_cache.cash_out
IS '现金流出合计(仅 area_code=all 时有效值)';
COMMENT ON COLUMN dws.dws_finance_board_cache.cash_balance
IS '现金余额变动 = cash_in - cash_out';
COMMENT ON COLUMN dws.dws_finance_board_cache.balance_rate
IS '余额变动率';
COMMENT ON COLUMN dws.dws_finance_board_cache.data_fingerprint
IS '源数据指纹MD5用于检测补录导致的数据变化';
COMMENT ON COLUMN dws.dws_finance_board_cache.computed_at
IS '缓存计算时间';
-- ============================================================
-- 2. 创建 RLS 视图
-- ============================================================
CREATE OR REPLACE VIEW dws.v_dws_finance_board_cache AS
SELECT *
FROM dws.dws_finance_board_cache
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
GRANT SELECT ON dws.v_dws_finance_board_cache TO app_reader;
-- ============================================================
-- 3. 创建 app schema 导出视图(后端通过 app.v_* 访问)
-- ============================================================
CREATE OR REPLACE VIEW app.v_dws_finance_board_cache AS
SELECT *
FROM dws.dws_finance_board_cache
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
GRANT SELECT ON app.v_dws_finance_board_cache TO app_reader;
-- ============================================================
-- 回滚
-- ============================================================
-- DROP VIEW IF EXISTS app.v_dws_finance_board_cache;
-- DROP VIEW IF EXISTS dws.v_dws_finance_board_cache;
-- DROP TABLE IF EXISTS dws.dws_finance_board_cache;

View File

@@ -0,0 +1,13 @@
-- 回滚脚本board-finance-dws-area-refactor
-- 逆序 DROP VIEW → DROP TABLE
-- 日期2026-03-28
--
-- 执行顺序:先删缓存层(依赖日粒度),再删日粒度
-- 1. 删除缓存层 RLS 视图和表
DROP VIEW IF EXISTS dws.v_dws_finance_board_cache;
DROP TABLE IF EXISTS dws.dws_finance_board_cache;
-- 2. 删除日粒度 RLS 视图和表
DROP VIEW IF EXISTS dws.v_dws_finance_area_daily;
DROP TABLE IF EXISTS dws.dws_finance_area_daily;

View File

@@ -0,0 +1,30 @@
-- 关系指数表新增任务统计字段2026-03-31
-- C: 历史总计字段
BEGIN;
ALTER TABLE dws.dws_member_assistant_relation_index
ADD COLUMN IF NOT EXISTS recall_created_total INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS recall_completed_total INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS follow_up_created_total INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS follow_up_completed_total INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_created INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_completed INT NOT NULL DEFAULT 0;
COMMENT ON COLUMN dws.dws_member_assistant_relation_index.recall_created_total IS '历史累计召回任务创建数';
COMMENT ON COLUMN dws.dws_member_assistant_relation_index.recall_completed_total IS '历史累计召回任务完成数';
COMMENT ON COLUMN dws.dws_member_assistant_relation_index.follow_up_created_total IS '历史累计回访任务创建数';
COMMENT ON COLUMN dws.dws_member_assistant_relation_index.follow_up_completed_total IS '历史累计回访任务完成数';
COMMENT ON COLUMN dws.dws_member_assistant_relation_index.total_created IS '历史累计任务创建总数';
COMMENT ON COLUMN dws.dws_member_assistant_relation_index.total_completed IS '历史累计任务完成总数';
COMMIT;
-- ROLLBACK:
-- ALTER TABLE dws.dws_member_assistant_relation_index
-- DROP COLUMN IF EXISTS recall_created_total,
-- DROP COLUMN IF EXISTS recall_completed_total,
-- DROP COLUMN IF EXISTS follow_up_created_total,
-- DROP COLUMN IF EXISTS follow_up_completed_total,
-- DROP COLUMN IF EXISTS total_created,
-- DROP COLUMN IF EXISTS total_completed;