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;

View File

@@ -0,0 +1,95 @@
-- AI_CHANGELOG
-- | 日期 | Prompt | 变更 |
-- |------|--------|------|
-- | 2026-03-23 | 角色体系隔离+店铺管理员 | 清理小程序 RBAC 中的 site_admin/tenant_admin新增 head_coach/managertenant_admins 加 admin_type |
-- 迁移:角色体系隔离 + 店铺管理员支持
-- 原因site_admin/tenant_admin 属于租户管理后台概念,不应出现在小程序 RBAC 体系中
-- 回滚:见文件末尾
BEGIN;
-- ═══════════════════════════════════════════════════════════
-- Part A: 清理小程序 RBAC 中的 site_admin / tenant_admin
-- ═══════════════════════════════════════════════════════════
-- A1. 安全检查:确认无用户绑定这两个角色(如有则中止)
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM auth.user_site_roles usr
JOIN auth.roles r ON r.id = usr.role_id
WHERE r.code IN ('site_admin', 'tenant_admin')
) THEN
RAISE EXCEPTION '存在用户绑定了 site_admin/tenant_admin 角色,需先迁移数据';
END IF;
END $$;
-- A2. 删除 role_permissions 关联
DELETE FROM auth.role_permissions
WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('site_admin', 'tenant_admin'));
-- A3. 删除角色记录
DELETE FROM auth.roles WHERE code IN ('site_admin', 'tenant_admin');
-- ═══════════════════════════════════════════════════════════
-- Part B: 新增 head_coach / manager 角色(幂等)
-- ═══════════════════════════════════════════════════════════
INSERT INTO auth.roles (code, name, description)
VALUES ('head_coach', '教练', '教练,可查看任务和看板')
ON CONFLICT (code) DO NOTHING;
INSERT INTO auth.roles (code, name, description)
VALUES ('manager', '管理员', '店铺管理员(小程序端),可查看所有板块')
ON CONFLICT (code) DO NOTHING;
-- B2. 为 head_coach 分配权限view_tasks, view_board
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code = 'head_coach' AND p.code IN ('view_tasks', 'view_board')
ON CONFLICT DO NOTHING;
-- B3. 为 manager 分配全部 5 个权限
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code = 'manager'
ON CONFLICT DO NOTHING;
-- ═══════════════════════════════════════════════════════════
-- Part C: tenant_admins 加 admin_type 列
-- ═══════════════════════════════════════════════════════════
ALTER TABLE auth.tenant_admins
ADD COLUMN IF NOT EXISTS admin_type VARCHAR(20) NOT NULL DEFAULT 'tenant_admin';
-- C2. CHECK 约束:只允许 tenant_admin / site_admin
ALTER TABLE auth.tenant_admins
ADD CONSTRAINT chk_admin_type CHECK (admin_type IN ('tenant_admin', 'site_admin'));
COMMIT;
-- ═══════════════════════════════════════════════════════════
-- 回滚
-- ═══════════════════════════════════════════════════════════
-- BEGIN;
-- ALTER TABLE auth.tenant_admins DROP CONSTRAINT IF EXISTS chk_admin_type;
-- ALTER TABLE auth.tenant_admins DROP COLUMN IF EXISTS admin_type;
-- DELETE FROM auth.role_permissions WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('head_coach', 'manager'));
-- DELETE FROM auth.roles WHERE code IN ('head_coach', 'manager');
-- INSERT INTO auth.roles (code, name, description) VALUES ('site_admin', '店铺管理员', '单店管理员,可查看所有看板和审核用户');
-- INSERT INTO auth.roles (code, name, description) VALUES ('tenant_admin', '租户管理员', '连锁管理员,可管理多店铺和所有功能');
-- -- 重新插入 role_permissionssite_admin/tenant_admin 各 5 条)
-- COMMIT;
-- ═══════════════════════════════════════════════════════════
-- 验证
-- ═══════════════════════════════════════════════════════════
-- 1. SELECT code, name FROM auth.roles ORDER BY id;
-- 期望coach, staff, head_coach, manager无 site_admin/tenant_admin
-- 2. SELECT r.code, array_agg(p.code ORDER BY p.code) FROM auth.role_permissions rp JOIN auth.roles r ON r.id = rp.role_id JOIN auth.permissions p ON p.id = rp.permission_id GROUP BY r.code;
-- 期望head_coach=[view_board, view_tasks], manager=[全部5个]
-- 3. SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema='auth' AND table_name='tenant_admins' AND column_name='admin_type';
-- 期望admin_type, varchar, 'tenant_admin'

View File

@@ -0,0 +1,23 @@
-- relationship_building 保底任务:扩大生成范围
-- 依赖biz.coach_tasks 表已存在P4 创建P17 扩展)
-- 回滚:见文件末尾 ROLLBACK 注释块
BEGIN;
-- ═══════════════════════════════════════════════════════════
-- 1. Partial unique index每个 (assistant, member) 对最多 1 条 active 的 relationship_building
-- ═══════════════════════════════════════════════════════════
CREATE UNIQUE INDEX IF NOT EXISTS idx_coach_tasks_rb_unique_active
ON biz.coach_tasks (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active';
COMMENT ON INDEX biz.idx_coach_tasks_rb_unique_active IS
'保证每个 (site_id, assistant_id, member_id) 最多 1 条 active 的 relationship_building 任务,支持 upsert';
COMMIT;
-- ═══════════════════════════════════════════════════════════
-- ROLLBACK
-- ═══════════════════════════════════════════════════════════
-- DROP INDEX IF EXISTS biz.idx_coach_tasks_rb_unique_active;

View File

@@ -0,0 +1,78 @@
-- AI_CHANGELOG
-- | 日期 | Prompt | 变更 |
-- |------|--------|------|
-- | 2026-03-27 | 权限改造 W3 | 修正角色-权限码映射coach 仅 view_tasksstaff 仅 view_board+customer+coachhead_coach/manager 全权限 |
-- 迁移:修正角色-权限码映射
-- 原因:前后端权限不一致导致"页面能进但数据全空403"
-- 目标映射:
-- coach: view_tasks
-- staff: view_board, view_board_customer, view_board_coach
-- head_coach: view_tasks, view_board, view_board_finance, view_board_customer, view_board_coach
-- manager: view_tasks, view_board, view_board_finance, view_board_customer, view_board_coach
-- 回滚:见文件末尾
BEGIN;
-- 1. 清空所有现有角色-权限关联(重建更安全,避免残留脏数据)
DELETE FROM auth.role_permissions
WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager'));
-- 2. coach → view_tasks
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code = 'coach' AND p.code IN ('view_tasks')
ON CONFLICT DO NOTHING;
-- 3. staff → view_board, view_board_customer, view_board_coach
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code = 'staff' AND p.code IN ('view_board', 'view_board_customer', 'view_board_coach')
ON CONFLICT DO NOTHING;
-- 4. head_coach → 全部 5 个权限
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code = 'head_coach'
ON CONFLICT DO NOTHING;
-- 5. manager → 全部 5 个权限
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code = 'manager'
ON CONFLICT DO NOTHING;
COMMIT;
-- ═══════════════════════════════════════════════════════════
-- 回滚(恢复到改造前状态)
-- ═══════════════════════════════════════════════════════════
-- BEGIN;
-- DELETE FROM auth.role_permissions WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager'));
-- -- coach: view_tasks, view_board_coach
-- INSERT INTO auth.role_permissions (role_id, permission_id) SELECT r.id, p.id FROM auth.roles r, auth.permissions p WHERE r.code = 'coach' AND p.code IN ('view_tasks', 'view_board_coach') ON CONFLICT DO NOTHING;
-- -- staff: view_board, view_tasks
-- INSERT INTO auth.role_permissions (role_id, permission_id) SELECT r.id, p.id FROM auth.roles r, auth.permissions p WHERE r.code = 'staff' AND p.code IN ('view_board', 'view_tasks') ON CONFLICT DO NOTHING;
-- -- head_coach: view_board, view_tasks
-- INSERT INTO auth.role_permissions (role_id, permission_id) SELECT r.id, p.id FROM auth.roles r, auth.permissions p WHERE r.code = 'head_coach' AND p.code IN ('view_board', 'view_tasks') ON CONFLICT DO NOTHING;
-- -- manager: 全部 5 个
-- INSERT INTO auth.role_permissions (role_id, permission_id) SELECT r.id, p.id FROM auth.roles r, auth.permissions p WHERE r.code = 'manager' ON CONFLICT DO NOTHING;
-- COMMIT;
-- ═══════════════════════════════════════════════════════════
-- 验证
-- ═══════════════════════════════════════════════════════════
-- SELECT r.code, array_agg(p.code ORDER BY p.code)
-- FROM auth.role_permissions rp
-- JOIN auth.roles r ON r.id = rp.role_id
-- JOIN auth.permissions p ON p.id = rp.permission_id
-- GROUP BY r.code ORDER BY r.code;
-- 期望:
-- coach = {view_tasks}
-- head_coach = {view_board,view_board_coach,view_board_customer,view_board_finance,view_tasks}
-- manager = {view_board,view_board_coach,view_board_customer,view_board_finance,view_tasks}
-- staff = {view_board,view_board_coach,view_board_customer}

View File

@@ -0,0 +1,18 @@
-- OS 分级分配:新增 3 条任务引擎参数
-- 依赖biz.cfg_task_generator_params 表已存在P17 创建)
BEGIN;
INSERT INTO biz.cfg_task_generator_params (site_id, param_key, param_value, description)
VALUES
(NULL, 'escalation_comanage_multiplier', 3.0, '升级到 COMANAGE 的倍数阈值(任务未完成时长 / 理想到店周期)'),
(NULL, 'escalation_pool_multiplier', 5.0, '转移到 POOL 的倍数阈值(任务未完成时长 / 理想到店周期)'),
(NULL, 'default_ideal_interval_days', 10.0, '无历史数据时的兜底到店周期(天)')
ON CONFLICT (site_id, param_key) DO NOTHING;
COMMIT;
-- ROLLBACK:
-- DELETE FROM biz.cfg_task_generator_params
-- WHERE site_id IS NULL
-- AND param_key IN ('escalation_comanage_multiplier', 'escalation_pool_multiplier', 'default_ideal_interval_days');

View File

@@ -0,0 +1,38 @@
-- 任务引擎参数调优2026-03-30
-- 更新 cfg_task_generator_params 全局默认参数
BEGIN;
UPDATE biz.cfg_task_generator_params SET param_value = 7.5
WHERE site_id IS NULL AND param_key = 'high_priority_recall_threshold';
UPDATE biz.cfg_task_generator_params SET param_value = 4.0
WHERE site_id IS NULL AND param_key = 'priority_recall_threshold';
UPDATE biz.cfg_task_generator_params SET param_value = 3.0
WHERE site_id IS NULL AND param_key = 'min_wbi_for_transfer';
UPDATE biz.cfg_task_generator_params SET param_value = 0.0
WHERE site_id IS NULL AND param_key = 'guard_assistant_coverage_ratio';
UPDATE biz.cfg_task_generator_params SET param_value = 4
WHERE site_id IS NULL AND param_key = 'max_transfer_count';
-- 新增/更新升级倍数参数
INSERT INTO biz.cfg_task_generator_params (site_id, param_key, param_value, description)
VALUES
(NULL, 'escalation_comanage_multiplier', 2.5, '升级到 COMANAGE 的倍数阈值'),
(NULL, 'escalation_pool_multiplier', 4.0, '转移到 POOL 的倍数阈值'),
(NULL, 'default_ideal_interval_days', 10.0, '无历史数据时的兜底到店周期')
ON CONFLICT (site_id, param_key) DO UPDATE SET param_value = EXCLUDED.param_value;
COMMIT;
-- ROLLBACK:
-- UPDATE biz.cfg_task_generator_params SET param_value = 7.0 WHERE site_id IS NULL AND param_key = 'high_priority_recall_threshold';
-- UPDATE biz.cfg_task_generator_params SET param_value = 5.0 WHERE site_id IS NULL AND param_key = 'priority_recall_threshold';
-- UPDATE biz.cfg_task_generator_params SET param_value = 5.0 WHERE site_id IS NULL AND param_key = 'min_wbi_for_transfer';
-- UPDATE biz.cfg_task_generator_params SET param_value = 0.5 WHERE site_id IS NULL AND param_key = 'guard_assistant_coverage_ratio';
-- UPDATE biz.cfg_task_generator_params SET param_value = 2 WHERE site_id IS NULL AND param_key = 'max_transfer_count';
-- UPDATE biz.cfg_task_generator_params SET param_value = 3.0 WHERE site_id IS NULL AND param_key = 'escalation_comanage_multiplier';
-- UPDATE biz.cfg_task_generator_params SET param_value = 5.0 WHERE site_id IS NULL AND param_key = 'escalation_pool_multiplier';

View File

@@ -0,0 +1,47 @@
-- 任务统计表2026-03-31
-- B: 按助教+月份汇总表
-- C: 关系指数表新增历史总计字段
BEGIN;
-- ═══════════════════════════════════════════════════════════
-- B: 新建 biz.dws_assistant_task_monthly按月汇总
-- ═══════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS biz.dws_assistant_task_monthly (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
assistant_id BIGINT NOT NULL,
stat_month DATE NOT NULL, -- 月份第一天,如 2026-03-01
-- 创建数
recall_created INT NOT NULL DEFAULT 0,
follow_up_created INT NOT NULL DEFAULT 0,
relationship_created INT NOT NULL DEFAULT 0,
total_created INT NOT NULL DEFAULT 0,
-- 完成数
recall_completed INT NOT NULL DEFAULT 0,
follow_up_completed INT NOT NULL DEFAULT 0,
total_completed INT NOT NULL DEFAULT 0,
-- 其他状态
abandoned_count INT NOT NULL DEFAULT 0,
transferred_count INT NOT NULL DEFAULT 0,
-- 时间戳
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (site_id, assistant_id, stat_month)
);
COMMENT ON TABLE biz.dws_assistant_task_monthly IS '助教任务月度统计汇总';
COMMENT ON COLUMN biz.dws_assistant_task_monthly.stat_month IS '统计月份(月初日期)';
COMMENT ON COLUMN biz.dws_assistant_task_monthly.recall_created IS '当月创建的召回任务数high_priority + priority';
COMMENT ON COLUMN biz.dws_assistant_task_monthly.recall_completed IS '当月完成的召回任务数';
CREATE INDEX IF NOT EXISTS idx_task_monthly_site_month
ON biz.dws_assistant_task_monthly (site_id, stat_month DESC);
CREATE INDEX IF NOT EXISTS idx_task_monthly_assistant
ON biz.dws_assistant_task_monthly (assistant_id, stat_month DESC);
COMMIT;
-- ROLLBACK:
-- DROP TABLE IF EXISTS biz.dws_assistant_task_monthly;

View File

@@ -0,0 +1,42 @@
-- =============================================================================
-- 迁移member_retention_clue 新增 is_hidden 列(维客线索隐藏功能)
-- 日期2026-03-20
-- 关联 SPECtenant-admin-web
-- 需求16.1, 16.2
-- 说明:
-- 租户管理后台可隐藏线索使其不在小程序端展示,同时保留在管理后台可见。
-- 已有数据通过 DEFAULT false 保证兼容(所有现有线索默认可见)。
-- =============================================================================
ALTER TABLE public.member_retention_clue
ADD COLUMN IF NOT EXISTS is_hidden BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN public.member_retention_clue.is_hidden
IS '是否隐藏true=管理后台保留但小程序不展示)';
-- =============================================================================
-- 回滚
-- =============================================================================
-- ALTER TABLE public.member_retention_clue DROP COLUMN IF EXISTS is_hidden;
-- =============================================================================
-- 验证
-- =============================================================================
-- 1) 字段存在性
-- SELECT column_name, data_type, column_default, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'public' AND table_name = 'member_retention_clue'
-- AND column_name = 'is_hidden';
-- 预期1 行boolean, false, NO
-- 2) 已有数据全部为 false
-- SELECT COUNT(*) FROM public.member_retention_clue WHERE is_hidden = true;
-- 预期0
-- 3) 列注释存在
-- SELECT col_description(
-- (SELECT oid FROM pg_class WHERE relname = 'member_retention_clue'),
-- (SELECT ordinal_position FROM information_schema.columns
-- WHERE table_name = 'member_retention_clue' AND column_name = 'is_hidden')
-- );
-- 预期:'是否隐藏true=管理后台保留但小程序不展示)'

View File

@@ -0,0 +1,217 @@
-- =============================================================================
-- 迁移NS4 租户管理后台 — 新建表
-- 日期2026-03-20
-- 关联 SPECtenant-admin-web
-- 需求15.1, 15.2, 15.3, 15.4
-- 说明:
-- 1. auth.tenant_admins — 租户管理员表(独立认证体系)
-- 2. biz.excel_upload_log — Excel 上传记录表
-- 3. biz.salary_adjustments — 助教奖罚明细表
-- 4. biz.stg_finance_expense — 财务支出暂存表
-- 5. biz.stg_platform_income — 团购收入暂存表
-- 6. biz.stg_recharge_commission — 充值业绩归属暂存表
-- 创建顺序excel_upload_log 先于 salary_adjustments 和 staging 表FK 依赖)
-- =============================================================================
-- 确保 schema 存在
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS biz;
-- ---------------------------------------------------------------------------
-- 1. auth.tenant_admins — 租户管理员表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS auth.tenant_admins (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, -- 登录用户名
password_hash VARCHAR(255) NOT NULL, -- bcrypt 哈希
display_name VARCHAR(100), -- 显示名称
tenant_id BIGINT NOT NULL, -- 所属租户
managed_site_ids BIGINT[] NOT NULL, -- 管辖门店 ID 列表
is_active BOOLEAN DEFAULT true, -- 账号状态(启用/禁用)
deleted_at TIMESTAMPTZ DEFAULT NULL, -- 软删除时间戳NULL=正常,非 NULL=已删除2026-03-22 新增,与 is_active 分离)
created_by BIGINT, -- 创建者Operator ID
created_at TIMESTAMPTZ DEFAULT NOW(), -- 创建时间
last_login_at TIMESTAMPTZ -- 最后登录时间
);
COMMENT ON TABLE auth.tenant_admins IS '租户管理员表NS4 独立认证体系,与小程序 auth.users 隔离)';
COMMENT ON COLUMN auth.tenant_admins.username IS '登录用户名,全局唯一';
COMMENT ON COLUMN auth.tenant_admins.password_hash IS 'bcrypt 密码哈希';
COMMENT ON COLUMN auth.tenant_admins.managed_site_ids IS '管辖门店 ID 数组,用于数据隔离';
COMMENT ON COLUMN auth.tenant_admins.tenant_id IS '所属租户 ID';
COMMENT ON COLUMN auth.tenant_admins.is_active IS '账号状态false=禁用,登录返回 403仅控制启用/禁用,与软删除无关)';
COMMENT ON COLUMN auth.tenant_admins.deleted_at IS '软删除时间戳NULL=正常,非 NULL=已删除。删除与禁用分离is_active 控制启用/禁用deleted_at 控制软删除2026-03-22';
CREATE INDEX IF NOT EXISTS idx_tenant_admin_tenant
ON auth.tenant_admins (tenant_id);
-- 部分索引加速列表和登录查询仅索引未删除记录2026-03-22 新增)
CREATE INDEX IF NOT EXISTS idx_tenant_admins_active_not_deleted
ON auth.tenant_admins (is_active)
WHERE deleted_at IS NULL;
-- ---------------------------------------------------------------------------
-- 2. biz.excel_upload_log — Excel 上传记录表(必须先于 salary_adjustments 和 staging 表)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.excel_upload_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 门店 ID
upload_type VARCHAR(30) NOT NULL
CHECK (upload_type IN ('expense', 'platform_income', 'salary_adj', 'recharge_commission')),
file_name VARCHAR(255) NOT NULL, -- 原始文件名
uploaded_by BIGINT NOT NULL, -- 上传人(管理员 ID
row_count INTEGER DEFAULT 0, -- 数据行数
conflict_count INTEGER DEFAULT 0, -- 冲突行数
resolved_count INTEGER DEFAULT 0, -- 已解决冲突数
status VARCHAR(20) NOT NULL
CHECK (status IN ('pending', 'confirmed', 'failed')),
error_detail JSONB, -- 错误详情
created_at TIMESTAMPTZ DEFAULT NOW(), -- 上传时间
confirmed_at TIMESTAMPTZ -- 确认时间
);
COMMENT ON TABLE biz.excel_upload_log IS 'Excel 上传记录表NS4 租户管理后台)';
COMMENT ON COLUMN biz.excel_upload_log.upload_type IS '模板类型expense/platform_income/salary_adj/recharge_commission';
COMMENT ON COLUMN biz.excel_upload_log.uploaded_by IS '上传人auth.tenant_admins.id';
COMMENT ON COLUMN biz.excel_upload_log.status IS '批次状态pending=待确认, confirmed=已写入, failed=写入失败';
COMMENT ON COLUMN biz.excel_upload_log.error_detail IS '写入失败时的错误详情 JSON';
CREATE INDEX IF NOT EXISTS idx_excel_log_site
ON biz.excel_upload_log (site_id, created_at DESC);
-- ---------------------------------------------------------------------------
-- 3. biz.salary_adjustments — 助教奖罚明细表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.salary_adjustments (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 门店 ID
assistant_id BIGINT, -- 匹配到的助教 ID可空
assistant_name VARCHAR(100) NOT NULL, -- 助教姓名
assistant_number VARCHAR(50) NOT NULL, -- 助教编号
salary_month VARCHAR(7) NOT NULL, -- 月份 YYYY-MM
adjustment_type VARCHAR(20) NOT NULL
CHECK (adjustment_type IN ('deduction', 'bonus')),
amount NUMERIC(12,2) NOT NULL
CHECK (amount > 0), -- 金额(必须 > 0
reason VARCHAR(200) NOT NULL, -- 原因
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
created_at TIMESTAMPTZ DEFAULT NOW(), -- 创建时间
created_by BIGINT -- 上传人
);
COMMENT ON TABLE biz.salary_adjustments IS '助教奖罚明细表(通过 Excel 上传写入,直接进入业务库)';
COMMENT ON COLUMN biz.salary_adjustments.assistant_id IS '匹配到的助教 ID匹配失败时为 NULL';
COMMENT ON COLUMN biz.salary_adjustments.adjustment_type IS '类型deduction=扣款, bonus=奖金';
COMMENT ON COLUMN biz.salary_adjustments.upload_batch_id IS '关联 excel_upload_log.id';
CREATE INDEX IF NOT EXISTS idx_salary_adj_site_month
ON biz.salary_adjustments (site_id, salary_month);
CREATE INDEX IF NOT EXISTS idx_salary_adj_assistant_month
ON biz.salary_adjustments (assistant_id, salary_month);
-- ---------------------------------------------------------------------------
-- 4. biz.stg_finance_expense — 财务支出暂存表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.stg_finance_expense (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 门店 ID
expense_month VARCHAR(7) NOT NULL, -- 月份 YYYY-MM
category VARCHAR(50) NOT NULL, -- 支出类别
amount NUMERIC(12,2) NOT NULL, -- 金额
remark TEXT, -- 备注
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ, -- ETL 同步时间NULL=未同步)
created_at TIMESTAMPTZ DEFAULT NOW() -- 创建时间
);
COMMENT ON TABLE biz.stg_finance_expense IS '财务支出暂存表Excel 上传 → ETL 同步到 DWS';
COMMENT ON COLUMN biz.stg_finance_expense.synced_at IS 'ETL 同步时间NULL 表示尚未同步';
-- ---------------------------------------------------------------------------
-- 5. biz.stg_platform_income — 团购收入暂存表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.stg_platform_income (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 门店 ID
income_month VARCHAR(7) NOT NULL, -- 月份 YYYY-MM
platform_name VARCHAR(100) NOT NULL, -- 平台名称
amount NUMERIC(12,2) NOT NULL, -- 收入金额
remark TEXT, -- 备注
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ, -- ETL 同步时间NULL=未同步)
created_at TIMESTAMPTZ DEFAULT NOW() -- 创建时间
);
COMMENT ON TABLE biz.stg_platform_income IS '团购收入暂存表Excel 上传 → ETL 同步到 DWS';
COMMENT ON COLUMN biz.stg_platform_income.synced_at IS 'ETL 同步时间NULL 表示尚未同步';
-- ---------------------------------------------------------------------------
-- 6. biz.stg_recharge_commission — 充值业绩归属暂存表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.stg_recharge_commission (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 门店 ID
recharge_date DATE NOT NULL, -- 充值日期
member_name VARCHAR(100) NOT NULL, -- 会员名称
recharge_amount NUMERIC(12,2) NOT NULL, -- 充值金额
assigned_assistant VARCHAR(100) NOT NULL, -- 归属助教
reward_amount NUMERIC(12,2) NOT NULL, -- 奖励金额
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ, -- ETL 同步时间NULL=未同步)
created_at TIMESTAMPTZ DEFAULT NOW() -- 创建时间
);
COMMENT ON TABLE biz.stg_recharge_commission IS '充值业绩归属暂存表Excel 上传 → ETL 同步到 DWS';
COMMENT ON COLUMN biz.stg_recharge_commission.synced_at IS 'ETL 同步时间NULL 表示尚未同步';
-- =============================================================================
-- 回滚
-- =============================================================================
-- DROP TABLE IF EXISTS biz.stg_recharge_commission;
-- DROP TABLE IF EXISTS biz.stg_platform_income;
-- DROP TABLE IF EXISTS biz.stg_finance_expense;
-- DROP TABLE IF EXISTS biz.salary_adjustments;
-- DROP TABLE IF EXISTS biz.excel_upload_log;
-- DROP TABLE IF EXISTS auth.tenant_admins;
-- =============================================================================
-- 验证
-- =============================================================================
-- 1) auth.tenant_admins 表存在性
-- SELECT column_name, data_type, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'auth' AND table_name = 'tenant_admins'
-- ORDER BY ordinal_position;
-- 预期11 行(含 2026-03-22 新增的 deleted_at
-- 2) biz.excel_upload_log 表存在性
-- SELECT column_name, data_type
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'excel_upload_log'
-- ORDER BY ordinal_position;
-- 预期12 行
-- 3) biz.salary_adjustments FK 约束
-- SELECT conname, pg_get_constraintdef(oid)
-- FROM pg_constraint
-- WHERE conrelid = 'biz.salary_adjustments'::regclass AND contype = 'f';
-- 预期1 行upload_batch_id → excel_upload_log
-- 4) 索引存在性
-- SELECT indexname FROM pg_indexes
-- WHERE schemaname IN ('auth', 'biz')
-- AND indexname IN (
-- 'idx_tenant_admin_tenant',
-- 'idx_excel_log_site',
-- 'idx_salary_adj_site_month',
-- 'idx_salary_adj_assistant_month'
-- );
-- 预期4 行
-- 5) staging 表存在性
-- SELECT table_schema, table_name
-- FROM information_schema.tables
-- WHERE table_schema = 'biz'
-- AND table_name IN ('stg_finance_expense', 'stg_platform_income', 'stg_recharge_commission');
-- 预期3 行

View File

@@ -0,0 +1,9 @@
-- 2026-03-22 | 为 task_execution_log 添加 config JSONB 列
-- 用途:存储完整 TaskConfig JSONrerun 时还原原始参数processing_mode、lookback_hours 等)
-- 旧记录 config 为 NULLrerun 时回退到默认配置
ALTER TABLE task_execution_log
ADD COLUMN IF NOT EXISTS config JSONB DEFAULT NULL;
COMMENT ON COLUMN task_execution_log.config
IS '完整 TaskConfig JSON用于 rerun 时还原原始参数';

View File

@@ -0,0 +1,173 @@
-- =============================================================================
-- 迁移NS4.1 注册体系 — 连接器/租户/店铺/简写ID历史 四张新表
-- 日期2026-03-22
-- 关联 SPECadmin-web-enhancement
-- 需求A1.1, A1.2, A1.3, A1.4, A1.5
-- 说明:
-- 1. biz.connectors — 连接器注册表(上游 SaaS 系统)
-- 2. biz.tenants — 租户注册表(连接器下的租户)
-- 3. biz.sites — 店铺注册表(合并原 auth.site_code_mapping
-- 4. biz.site_code_history — 简写ID 变更历史
-- 创建顺序connectors → tenants → sites → site_code_historyFK 依赖链)
-- 种子数据:飞球连接器 + 朗朗桌球租户
-- 数据迁移auth.site_code_mapping → biz.sites + biz.site_code_history
-- =============================================================================
-- 确保 biz schema 存在
CREATE SCHEMA IF NOT EXISTS biz;
-- ---------------------------------------------------------------------------
-- 1. biz.connectors — 连接器注册表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.connectors (
id SERIAL PRIMARY KEY,
connector_key VARCHAR(50) NOT NULL UNIQUE, -- 连接器标识(如 feiqiu
display_name VARCHAR(100) NOT NULL, -- 显示名称
is_active BOOLEAN NOT NULL DEFAULT true, -- 是否启用
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- 创建时间
);
COMMENT ON TABLE biz.connectors IS '连接器注册表:记录本项目接入的上游 SaaS 系统';
-- ---------------------------------------------------------------------------
-- 2. biz.tenants — 租户注册表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.tenants (
id SERIAL PRIMARY KEY,
connector_id INTEGER NOT NULL REFERENCES biz.connectors(id), -- 所属连接器
tenant_id BIGINT NOT NULL, -- 上游系统租户 ID
tenant_name VARCHAR(200), -- 租户名称
is_active BOOLEAN NOT NULL DEFAULT true, -- 是否启用
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 更新时间
UNIQUE (connector_id, tenant_id)
);
COMMENT ON TABLE biz.tenants IS '租户注册表连接器下的租户tenant_id 来自上游系统';
-- ---------------------------------------------------------------------------
-- 3. biz.sites — 店铺注册表
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.sites (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES biz.tenants(id), -- 所属租户biz.tenants.id
site_id BIGINT NOT NULL UNIQUE, -- 上游系统店铺 ID
site_name VARCHAR(200), -- 店铺名称
site_code VARCHAR(6) UNIQUE, -- 当前生效的简写ID6位3+3格式
site_label VARCHAR(50), -- 店铺标签
is_active BOOLEAN NOT NULL DEFAULT true, -- 是否启用
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- 更新时间
);
COMMENT ON TABLE biz.sites IS '店铺注册表:合并原 auth.site_code_mapping增加租户关联和简写ID管理';
COMMENT ON COLUMN biz.sites.site_code IS '当前生效的简写ID6位字符3+3格式全局唯一';
-- ---------------------------------------------------------------------------
-- 4. biz.site_code_history — 简写ID 变更历史
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.site_code_history (
id SERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 店铺 IDbiz.sites.site_id
site_code VARCHAR(6) NOT NULL UNIQUE, -- 简写ID全局唯一含历史
is_current BOOLEAN NOT NULL DEFAULT false, -- 是否当前生效
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 创建时间
retired_at TIMESTAMPTZ -- 退役时间NULL=未退役)
);
COMMENT ON TABLE biz.site_code_history IS '简写ID变更历史增量记录所有使用过的简写ID全局 UNIQUE 保护已提交申请';
COMMENT ON COLUMN biz.site_code_history.is_current IS 'true=当前生效的简写ID每个 site_id 最多一条 is_current=true';
-- =============================================================================
-- 种子数据
-- =============================================================================
-- 连接器:飞球
INSERT INTO biz.connectors (connector_key, display_name)
VALUES ('feiqiu', '飞球');
-- 租户:朗朗桌球
INSERT INTO biz.tenants (connector_id, tenant_id, tenant_name)
VALUES (1, 2790683160709957, '朗朗桌球');
-- =============================================================================
-- 数据迁移auth.site_code_mapping → biz.sites
-- =============================================================================
-- 从 auth.site_code_mapping 迁移真实数据到 biz.sites
INSERT INTO biz.sites (tenant_id, site_id, site_name, site_code)
SELECT t.id, scm.site_id, scm.site_name, scm.site_code
FROM auth.site_code_mapping scm
JOIN biz.tenants t ON t.tenant_id = scm.tenant_id
WHERE scm.tenant_id IS NOT NULL;
-- 为已有 site_code 创建历史记录(标记为当前生效)
INSERT INTO biz.site_code_history (site_id, site_code, is_current)
SELECT site_id, site_code, true
FROM biz.sites
WHERE site_code IS NOT NULL;
-- =============================================================================
-- === ROLLBACK ===
-- 按依赖逆序删除
-- DROP TABLE IF EXISTS biz.site_code_history;
-- DROP TABLE IF EXISTS biz.sites;
-- DROP TABLE IF EXISTS biz.tenants;
-- DROP TABLE IF EXISTS biz.connectors;
-- =============================================================================
-- =============================================================================
-- 验证
-- =============================================================================
-- 1) biz.connectors 表存在性
-- SELECT column_name, data_type, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'connectors'
-- ORDER BY ordinal_position;
-- 预期5 行id, connector_key, display_name, is_active, created_at
-- 2) biz.tenants 表存在性
-- SELECT column_name, data_type, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'tenants'
-- ORDER BY ordinal_position;
-- 预期7 行id, connector_id, tenant_id, tenant_name, is_active, created_at, updated_at
-- 3) biz.sites 表存在性及字段
-- SELECT column_name, data_type, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'sites'
-- ORDER BY ordinal_position;
-- 预期9 行id, tenant_id, site_id, site_name, site_code, site_label, is_active, created_at, updated_at
-- 4) biz.site_code_history 表存在性
-- SELECT column_name, data_type, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'site_code_history'
-- ORDER BY ordinal_position;
-- 预期6 行id, site_id, site_code, is_current, created_at, retired_at
-- 5) 种子数据验证
-- SELECT connector_key, display_name FROM biz.connectors;
-- 预期1 行feiqiu, 飞球)
-- SELECT tenant_id, tenant_name FROM biz.tenants;
-- 预期1 行2790683160709957, 朗朗桌球)
-- 6) 数据迁移验证 — biz.sites 行数应 ≥ auth.site_code_mapping 中 tenant_id IS NOT NULL 的行数
-- SELECT COUNT(*) FROM biz.sites;
-- SELECT COUNT(*) FROM auth.site_code_mapping WHERE tenant_id IS NOT NULL;
-- 7) site_code_history 记录数应等于 biz.sites 中 site_code IS NOT NULL 的行数
-- SELECT COUNT(*) FROM biz.site_code_history;
-- SELECT COUNT(*) FROM biz.sites WHERE site_code IS NOT NULL;
-- 8) UNIQUE 约束验证
-- SELECT conname, pg_get_constraintdef(oid)
-- FROM pg_constraint
-- WHERE conrelid IN (
-- 'biz.connectors'::regclass,
-- 'biz.tenants'::regclass,
-- 'biz.sites'::regclass,
-- 'biz.site_code_history'::regclass
-- ) AND contype IN ('u', 'p')
-- ORDER BY conrelid::text, conname;

View File

@@ -0,0 +1,164 @@
-- =============================================================================
-- 迁移P14 — AI 模块改造DashScope 迁移 + 调度器完善)
-- 日期2026-03-22
-- 关联 SPECP14-ai-dashscope-migration
-- 需求14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7
-- 说明:
-- 1. 新建 biz.ai_run_logsAI 运行记录,含 3 个索引)
-- 2. 新建 biz.ai_trigger_jobs调度运行记录含 3 个索引,含去重部分索引)
-- 3. ai_conversations 新增 session_id 字段(百炼会话 ID
-- 4. ai_cache 新增 status 字段 + CHECK 约束
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. 新建 biz.ai_run_logsAI 运行记录)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.ai_run_logs (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
app_type VARCHAR(30) NOT NULL,
trigger_type VARCHAR(20) NOT NULL,
member_id BIGINT,
request_prompt TEXT,
response_text TEXT,
tokens_used INTEGER DEFAULT 0,
latency_ms INTEGER,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
error_message TEXT,
session_id VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
finished_at TIMESTAMPTZ
);
COMMENT ON TABLE biz.ai_run_logs IS 'AI 运行记录:每次 DashScope Application API 调用的详细日志';
COMMENT ON COLUMN biz.ai_run_logs.app_type IS '应用类型app1_chat / app2_finance / app3_clue / ... / app8_consolidate';
COMMENT ON COLUMN biz.ai_run_logs.trigger_type IS '触发类型user / scheduled / event / forced';
COMMENT ON COLUMN biz.ai_run_logs.request_prompt IS '请求 prompt截断前 2000 字符)';
COMMENT ON COLUMN biz.ai_run_logs.status IS '状态pending / running / success / failed / timeout / budget_exceeded';
COMMENT ON COLUMN biz.ai_run_logs.session_id IS '百炼 session_id仅 App1 使用)';
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_site_app
ON biz.ai_run_logs (site_id, app_type);
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_created
ON biz.ai_run_logs (created_at);
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_status
ON biz.ai_run_logs (status);
-- ---------------------------------------------------------------------------
-- 2. 新建 biz.ai_trigger_jobs调度运行记录
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS biz.ai_trigger_jobs (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
event_type VARCHAR(30) NOT NULL,
connector_type VARCHAR(30) DEFAULT 'feiqiu',
member_id BIGINT,
payload JSONB,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
is_forced BOOLEAN DEFAULT false,
app_chain VARCHAR(100),
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE biz.ai_trigger_jobs IS '调度运行记录:每次 AI 事件触发的编排执行记录';
COMMENT ON COLUMN biz.ai_trigger_jobs.event_type IS '事件类型consumption / dws_completed / note_created / task_assigned';
COMMENT ON COLUMN biz.ai_trigger_jobs.status IS '状态pending / running / completed / failed / skipped_duplicate / budget_exceeded';
COMMENT ON COLUMN biz.ai_trigger_jobs.is_forced IS '是否强制执行(跳过去重检查)';
COMMENT ON COLUMN biz.ai_trigger_jobs.app_chain IS '调用链描述,如 app3→app8→app7';
CREATE INDEX IF NOT EXISTS idx_ai_trigger_jobs_site
ON biz.ai_trigger_jobs (site_id, event_type);
CREATE INDEX IF NOT EXISTS idx_ai_trigger_jobs_dedup
ON biz.ai_trigger_jobs (event_type, member_id, site_id, created_at)
WHERE status NOT IN ('skipped_duplicate');
CREATE INDEX IF NOT EXISTS idx_ai_trigger_jobs_status
ON biz.ai_trigger_jobs (status);
-- ---------------------------------------------------------------------------
-- 3. ai_conversations 新增 session_id 字段
-- ---------------------------------------------------------------------------
ALTER TABLE biz.ai_conversations
ADD COLUMN IF NOT EXISTS session_id VARCHAR(100);
COMMENT ON COLUMN biz.ai_conversations.session_id
IS '百炼 session_id格式 conv_{conversation_id}_{created_timestamp},仅 App1 使用';
-- ---------------------------------------------------------------------------
-- 4. ai_cache 新增 status 字段 + CHECK 约束
-- ---------------------------------------------------------------------------
ALTER TABLE biz.ai_cache
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'valid';
-- CHECK 约束需要先检查是否已存在IF NOT EXISTS 不适用于约束)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'chk_ai_cache_status'
AND conrelid = 'biz.ai_cache'::regclass
) THEN
ALTER TABLE biz.ai_cache
ADD CONSTRAINT chk_ai_cache_status
CHECK (status IN ('valid', 'expired', 'invalidated', 'generating'));
END IF;
END $$;
COMMENT ON COLUMN biz.ai_cache.status
IS '缓存状态valid有效/ expired已过期/ invalidated手动失效/ generating生成中';
-- =============================================================================
-- 回滚脚本(逆序执行)
-- =============================================================================
-- ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_status;
-- ALTER TABLE biz.ai_cache DROP COLUMN IF EXISTS status;
-- ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS session_id;
-- DROP TABLE IF EXISTS biz.ai_trigger_jobs;
-- DROP TABLE IF EXISTS biz.ai_run_logs;
-- =============================================================================
-- 验证 SQL
-- =============================================================================
-- 1) ai_run_logs 表存在且字段正确
-- SELECT column_name, data_type, character_maximum_length
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'ai_run_logs'
-- ORDER BY ordinal_position;
-- 预期14 行
-- 2) ai_trigger_jobs 表存在且字段正确
-- SELECT column_name, data_type, character_maximum_length
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'ai_trigger_jobs'
-- ORDER BY ordinal_position;
-- 预期13 行
-- 3) ai_run_logs 索引
-- SELECT indexname FROM pg_indexes
-- WHERE schemaname = 'biz' AND tablename = 'ai_run_logs';
-- 预期4 行(含 PK
-- 4) ai_trigger_jobs 索引(含去重部分索引)
-- SELECT indexname FROM pg_indexes
-- WHERE schemaname = 'biz' AND tablename = 'ai_trigger_jobs';
-- 预期4 行(含 PK
-- 5) ai_conversations.session_id 字段
-- SELECT column_name, data_type, character_maximum_length
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
-- AND column_name = 'session_id';
-- 预期1 行varchar(100)
-- 6) ai_cache.status 字段 + CHECK 约束
-- SELECT column_name, data_type, column_default
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'ai_cache'
-- AND column_name = 'status';
-- 预期1 行varchar(20)default 'valid'
-- SELECT conname FROM pg_constraint
-- WHERE conrelid = 'biz.ai_cache'::regclass AND conname = 'chk_ai_cache_status';
-- 预期1 行

View File

@@ -0,0 +1,78 @@
-- =============================================================================
-- 迁移P16 调度任务最小运行间隔 — scheduled_tasks 新增字段
-- 日期2026-03-22
-- 关联 SPECadmin-web-enhancement
-- 需求B1.1, B1.2
-- 说明:
-- 为 public.scheduled_tasks 表新增 3 个字段:
-- 1. min_run_interval_value — 最小间隔数值0=无限制,向后兼容)
-- 2. min_run_interval_unit — 间隔单位minutes/hours/days
-- 3. last_success_at — 最后一次成功执行的时间
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. 新增字段
-- ---------------------------------------------------------------------------
-- 最小间隔数值0=无限制,与现有行为完全一致)
ALTER TABLE scheduled_tasks
ADD COLUMN IF NOT EXISTS min_run_interval_value INTEGER NOT NULL DEFAULT 0;
-- 间隔单位minutes/hours/days
ALTER TABLE scheduled_tasks
ADD COLUMN IF NOT EXISTS min_run_interval_unit VARCHAR(20) NOT NULL DEFAULT 'minutes';
-- 最后一次成功执行的时间NULL=从未成功执行)
ALTER TABLE scheduled_tasks
ADD COLUMN IF NOT EXISTS last_success_at TIMESTAMPTZ;
-- ---------------------------------------------------------------------------
-- 2. 字段注释
-- ---------------------------------------------------------------------------
COMMENT ON COLUMN scheduled_tasks.min_run_interval_value IS '最小间隔数值0=无限制)';
COMMENT ON COLUMN scheduled_tasks.min_run_interval_unit IS '间隔单位minutes/hours/days';
COMMENT ON COLUMN scheduled_tasks.last_success_at IS '最后一次成功执行的时间';
-- =============================================================================
-- === ROLLBACK ===
-- ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS min_run_interval_value;
-- ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS min_run_interval_unit;
-- ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS last_success_at;
-- =============================================================================
-- =============================================================================
-- 验证
-- =============================================================================
-- 1) 新增字段存在性
-- SELECT column_name, data_type, is_nullable, column_default
-- FROM information_schema.columns
-- WHERE table_schema = 'public' AND table_name = 'scheduled_tasks'
-- AND column_name IN ('min_run_interval_value', 'min_run_interval_unit', 'last_success_at')
-- ORDER BY ordinal_position;
-- 预期3 行
-- 2) 默认值验证
-- SELECT column_name, column_default
-- FROM information_schema.columns
-- WHERE table_schema = 'public' AND table_name = 'scheduled_tasks'
-- AND column_name IN ('min_run_interval_value', 'min_run_interval_unit')
-- ORDER BY column_name;
-- 预期min_run_interval_unit → 'minutes'::character varying, min_run_interval_value → 0
-- 3) 向后兼容验证 — 已有行的新字段值应为默认值
-- SELECT id, min_run_interval_value, min_run_interval_unit, last_success_at
-- FROM scheduled_tasks
-- LIMIT 5;
-- 预期min_run_interval_value=0, min_run_interval_unit='minutes', last_success_at=NULL
-- 4) 字段注释验证
-- SELECT a.attname, d.description
-- FROM pg_catalog.pg_attribute a
-- JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
-- JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
-- LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = a.attnum
-- WHERE n.nspname = 'public' AND c.relname = 'scheduled_tasks'
-- AND a.attname IN ('min_run_interval_value', 'min_run_interval_unit', 'last_success_at')
-- ORDER BY a.attnum;
-- 预期3 行,每行 description 非 NULL

View File

@@ -0,0 +1,22 @@
-- AI_CHANGELOG
-- | 日期 | Prompt | 变更 |
-- |------|--------|------|
-- | 2026-03-23 | 角色路由+页面权限守卫 | 新增 head_coach教练和 manager管理员角色及权限映射 |
-- 迁移:新增 head_coach教练和 manager管理员角色
-- 原因:小程序需要按角色区分页面入口和 tab-bar 可见性
-- 回滚DELETE FROM auth.roles WHERE code IN ('head_coach', 'manager');
-- 幂等插入code 有唯一约束,重复执行不报错
INSERT INTO auth.roles (code, name)
VALUES ('head_coach', '教练')
ON CONFLICT (code) DO NOTHING;
INSERT INTO auth.roles (code, name)
VALUES ('manager', '管理员')
ON CONFLICT (code) DO NOTHING;
-- 验证
-- 1. SELECT id, code, name FROM auth.roles ORDER BY id;
-- 2. SELECT count(*) FROM auth.roles WHERE code IN ('head_coach', 'manager');
-- 3. SELECT code, name FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager');

View File

@@ -0,0 +1,37 @@
-- 迁移:申请审核流程增强
-- 1. users 表增加 rejection_count 字段(累计被拒绝次数)
-- 2. user_applications.status 增加 'cancelled' 值(用户主动取消)
-- 日期2026-03-23
-- 1. 增加 rejection_count 字段(默认 0
ALTER TABLE auth.users
ADD COLUMN IF NOT EXISTS rejection_count integer NOT NULL DEFAULT 0;
COMMENT ON COLUMN auth.users.rejection_count
IS '累计被管理员拒绝的申请次数,达到 3 次自动禁用账号';
-- 2. user_applications.status 检查约束更新(增加 cancelled
-- 先删除旧约束(如果存在),再添加新约束
DO $$
BEGIN
-- 尝试删除旧的 CHECK 约束
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_schema = 'auth'
AND table_name = 'user_applications'
AND constraint_type = 'CHECK'
AND constraint_name = 'user_applications_status_check'
) THEN
ALTER TABLE auth.user_applications
DROP CONSTRAINT user_applications_status_check;
END IF;
END $$;
ALTER TABLE auth.user_applications
ADD CONSTRAINT user_applications_status_check
CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled'));
-- 回滚 SQL备用
-- ALTER TABLE auth.users DROP COLUMN IF EXISTS rejection_count;
-- ALTER TABLE auth.user_applications DROP CONSTRAINT IF EXISTS user_applications_status_check;
-- ALTER TABLE auth.user_applications ADD CONSTRAINT user_applications_status_check CHECK (status IN ('pending', 'approved', 'rejected'));

View File

@@ -0,0 +1,30 @@
-- 迁移:租户管理员用户名大小写不敏感
-- 日期2026-03-23
-- 原因:登录时用户名应不区分大小写,避免 Admin/admin 被视为不同账号
-- 影响auth.tenant_admins 表
-- 回滚:见文件末尾
-- 1. 将现有用户名统一转小写(幂等:已经是小写的不受影响)
UPDATE auth.tenant_admins
SET username = LOWER(username)
WHERE username != LOWER(username);
-- 2. 创建大小写不敏感的唯一索引(替代原 UNIQUE 约束的语义)
-- 原 UNIQUE 约束仍保留(防止完全相同的用户名),此索引额外覆盖大小写变体
CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_admins_username_lower
ON auth.tenant_admins (LOWER(username))
WHERE deleted_at IS NULL;
-- ── 回滚 SQL ──
-- DROP INDEX IF EXISTS auth.idx_tenant_admins_username_lower;
-- (用户名小写化不可逆,但不影响功能)
-- ── 验证 SQL ──
-- 1. 确认索引存在
-- SELECT indexname FROM pg_indexes WHERE tablename = 'tenant_admins' AND indexname = 'idx_tenant_admins_username_lower';
-- 2. 确认无大写用户名残留
-- SELECT username FROM auth.tenant_admins WHERE username != LOWER(username);
-- 3. 测试大小写不敏感插入冲突
-- INSERT INTO auth.tenant_admins (username, password_hash, tenant_id, managed_site_ids) VALUES ('TESTDUP', 'x', 1, '{1}');
-- INSERT INTO auth.tenant_admins (username, password_hash, tenant_id, managed_site_ids) VALUES ('testdup', 'x', 1, '{1}');
-- 第二条应报 unique violation

View File

@@ -0,0 +1,43 @@
-- P15AI 监控后台 — ai_run_logs 新增 alert_status 字段 + BRIN 索引
-- 依赖P14 迁移2026-03-22__p14_ai_module.sql
BEGIN;
-- 1. 新增 alert_status 字段
ALTER TABLE biz.ai_run_logs
ADD COLUMN IF NOT EXISTS alert_status VARCHAR(20) DEFAULT NULL;
ALTER TABLE biz.ai_run_logs
ADD CONSTRAINT chk_ai_run_logs_alert_status
CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored'));
COMMENT ON COLUMN biz.ai_run_logs.alert_status IS
'P15 — 告警处理状态NULL/pending/acknowledged/ignored';
-- 2. 告警查询部分索引
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_alert
ON biz.ai_run_logs(alert_status, created_at DESC)
WHERE status IN ('failed', 'timeout', 'circuit_open');
-- 3. BRIN 索引Dashboard 聚合优化)
CREATE INDEX IF NOT EXISTS idx_ai_run_logs_created_brin
ON biz.ai_run_logs USING BRIN (created_at)
WITH (pages_per_range = 32);
-- 4. 回填已有失败记录的 alert_status
UPDATE biz.ai_run_logs
SET alert_status = 'pending'
WHERE status IN ('failed', 'timeout', 'circuit_open')
AND alert_status IS NULL;
COMMIT;
-- ============================================================
-- P15 回滚
-- ============================================================
-- BEGIN;
-- DROP INDEX IF EXISTS biz.idx_ai_run_logs_created_brin;
-- DROP INDEX IF EXISTS biz.idx_ai_run_logs_alert;
-- ALTER TABLE biz.ai_run_logs DROP CONSTRAINT IF EXISTS chk_ai_run_logs_alert_status;
-- ALTER TABLE biz.ai_run_logs DROP COLUMN IF EXISTS alert_status;
-- COMMIT;

View File

@@ -0,0 +1,9 @@
-- 2026-03-23: 为 scheduled_tasks 添加 per-task-code 最小执行间隔
-- 格式: {"TASK_CODE": {"value": 30, "unit": "minutes"}, ...}
-- 空对象 {} 表示所有任务无独立间隔限制(回退到 schedule 级别的 min_run_interval_value/unit
ALTER TABLE scheduled_tasks
ADD COLUMN IF NOT EXISTS min_run_intervals JSONB NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN scheduled_tasks.min_run_intervals IS
'Per-task-code 最小执行间隔JSON 格式 {"TASK_CODE": {"value": N, "unit": "minutes|hours|days"}}';

View File

@@ -0,0 +1,47 @@
-- =============================================================================
-- 迁移trigger_jobs 新增 last_error 字段 + description 字段
-- 日期2026-03-23
-- 说明:
-- 1. last_error — 最后一次执行异常的错误信息NULL=无异常)
-- 2. description — 任务中文描述(用于管理页面展示)
-- =============================================================================
ALTER TABLE biz.trigger_jobs
ADD COLUMN IF NOT EXISTS last_error TEXT;
ALTER TABLE biz.trigger_jobs
ADD COLUMN IF NOT EXISTS description TEXT;
COMMENT ON COLUMN biz.trigger_jobs.last_error IS '最后一次执行异常的错误信息NULL=无异常)';
COMMENT ON COLUMN biz.trigger_jobs.description IS '任务中文描述(管理页面展示)';
-- 填充描述
UPDATE biz.trigger_jobs SET description = '每日凌晨4点根据客户指数为助教生成待办任务' WHERE job_name = 'task_generator';
UPDATE biz.trigger_jobs SET description = '每小时检查超时未处理的任务,标记为过期' WHERE job_name = 'task_expiry_check';
UPDATE biz.trigger_jobs SET description = 'ETL数据更新后检测客户是否已回店完成召回' WHERE job_name = 'recall_completion_check';
UPDATE biz.trigger_jobs SET description = '召回完成后,回溯检查备注是否需要重分类' WHERE job_name = 'note_reclassify_backfill';
-- =============================================================================
-- === ROLLBACK ===
-- ALTER TABLE biz.trigger_jobs DROP COLUMN IF EXISTS last_error;
-- ALTER TABLE biz.trigger_jobs DROP COLUMN IF EXISTS description;
-- =============================================================================
-- =============================================================================
-- 验证
-- =============================================================================
-- 1) 新增字段存在性
-- SELECT column_name, data_type, is_nullable
-- FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'trigger_jobs'
-- AND column_name IN ('last_error', 'description')
-- ORDER BY column_name;
-- 预期2 行
-- 2) description 已填充
-- SELECT job_name, description FROM biz.trigger_jobs ORDER BY id;
-- 预期4 行description 非 NULL
-- 3) last_error 默认 NULL
-- SELECT job_name, last_error FROM biz.trigger_jobs ORDER BY id;
-- 预期4 行last_error 全为 NULL

View File

@@ -0,0 +1,139 @@
-- P17助教客户归属与任务生成引擎 — 数据库变更
-- 依赖biz.coach_tasks 表已存在P4 创建)
-- 回滚:见文件末尾 ROLLBACK 注释块
BEGIN;
-- ═══════════════════════════════════════════════════════════
-- 1. 新增任务状态枚举值
-- ═══════════════════════════════════════════════════════════
-- 检查 coach_tasks.status 是否使用 enum 类型
-- 现有值: active, inactive, completed, abandoned
-- 新增: transferred已转移, pending_review待人工审核
-- 注意: 如果 status 是 VARCHAR 类型则无需 ALTER TYPE直接使用即可
-- 以下 DO 块兼容 enum 和 varchar 两种情况
DO $$
BEGIN
-- 尝试添加 enum 值(如果 status 列使用 enum 类型)
BEGIN
EXECUTE 'ALTER TYPE task_status ADD VALUE IF NOT EXISTS ''transferred''';
EXCEPTION WHEN undefined_object THEN
-- status 列不是 enum 类型,跳过
NULL;
END;
BEGIN
EXECUTE 'ALTER TYPE task_status ADD VALUE IF NOT EXISTS ''pending_review''';
EXCEPTION WHEN undefined_object THEN
NULL;
END;
END $$;
-- ═══════════════════════════════════════════════════════════
-- 2. coach_tasks 表新增转移追踪字段
-- ═══════════════════════════════════════════════════════════
ALTER TABLE biz.coach_tasks
ADD COLUMN IF NOT EXISTS transfer_count INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS transferred_from BIGINT,
ADD COLUMN IF NOT EXISTS transferred_at TIMESTAMPTZ;
-- transferred_from 外键(指向自身,记录转移来源任务)
-- 使用 DO 块避免重复添加约束
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_coach_tasks_transferred_from'
AND table_schema = 'biz'
AND table_name = 'coach_tasks'
) THEN
ALTER TABLE biz.coach_tasks
ADD CONSTRAINT fk_coach_tasks_transferred_from
FOREIGN KEY (transferred_from) REFERENCES biz.coach_tasks(id);
END IF;
END $$;
COMMENT ON COLUMN biz.coach_tasks.transfer_count IS '该客户在此任务链上的累计转移次数';
COMMENT ON COLUMN biz.coach_tasks.transferred_from IS '转移来源任务 ID指向原助教的任务';
COMMENT ON COLUMN biz.coach_tasks.transferred_at IS '转移发生时间';
-- ═══════════════════════════════════════════════════════════
-- 3. 新增表biz.cfg_task_generator_params任务引擎参数配置
-- ═══════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS biz.cfg_task_generator_params (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT, -- NULL 表示全局默认值
param_key VARCHAR(64) NOT NULL,
param_value NUMERIC NOT NULL,
description TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (site_id, param_key)
);
COMMENT ON TABLE biz.cfg_task_generator_params IS 'P17 任务引擎参数配置,支持全局默认 + 门店级覆盖';
COMMENT ON COLUMN biz.cfg_task_generator_params.site_id IS 'NULL=全局默认非NULL=门店级覆盖';
-- ═══════════════════════════════════════════════════════════
-- 4. 新增表biz.coach_task_transfer_log客户转移日志
-- ═══════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS biz.coach_task_transfer_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
from_assistant_id BIGINT NOT NULL,
to_assistant_id BIGINT NOT NULL,
from_task_id BIGINT NOT NULL REFERENCES biz.coach_tasks(id),
to_task_id BIGINT REFERENCES biz.coach_tasks(id),
transfer_reason TEXT,
guard_checks JSONB, -- 三重保护检查结果快照
transfer_score NUMERIC, -- 转移候选得分
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.coach_task_transfer_log IS 'P17 客户转移日志,记录每次转移的完整上下文';
-- 索引:按门店+时间查询转移日志
CREATE INDEX IF NOT EXISTS idx_transfer_log_site_created
ON biz.coach_task_transfer_log (site_id, created_at DESC);
-- 索引:按客户查询转移历史
CREATE INDEX IF NOT EXISTS idx_transfer_log_member
ON biz.coach_task_transfer_log (member_id, created_at DESC);
-- ═══════════════════════════════════════════════════════════
-- 5. 插入全局默认参数P17 第 5 节定义)
-- ═══════════════════════════════════════════════════════════
INSERT INTO biz.cfg_task_generator_params (site_id, param_key, param_value, description)
VALUES
(NULL, 'high_priority_recall_threshold', 7.0, 'max(WBI,NCI) 超过此值生成高优先召回'),
(NULL, 'priority_recall_threshold', 5.0, 'max(WBI,NCI) 超过此值生成优先召回'),
(NULL, 'rs_min_for_relationship', 1.0, 'RS ≤ 此值不生成关系构建'),
(NULL, 'rs_max_for_relationship', 6.0, 'RS ≥ 此值不生成关系构建'),
(NULL, 'consecutive_recall_fail_cycles', 3, '连续失败多少轮触发客户转移'),
(NULL, 'min_wbi_for_transfer', 5.0, 'WBI 低于此值不触发转移'),
(NULL, 'guard_assistant_coverage_ratio', 0.5, '绑定率低于此值禁用转移'),
(NULL, 'guard_new_assistant_days', 10, '新助教入驻保护天数'),
(NULL, 'transfer_score_w_rs', 0.5, '转移候选排序RS 权重'),
(NULL, 'transfer_score_w_ms', 0.3, '转移候选排序MS 权重'),
(NULL, 'transfer_score_w_ml', 0.2, '转移候选排序ML 权重'),
(NULL, 'max_transfer_count', 2, '单客户最大累计转移次数'),
(NULL, 'follow_up_visit_retention_hours', 48, '回访任务最低保留时长(小时)')
ON CONFLICT (site_id, param_key) DO NOTHING;
COMMIT;
-- ═══════════════════════════════════════════════════════════
-- ROLLBACK手动执行不在事务内
-- ═══════════════════════════════════════════════════════════
-- DELETE FROM biz.cfg_task_generator_params WHERE site_id IS NULL;
-- DROP TABLE IF EXISTS biz.coach_task_transfer_log;
-- DROP TABLE IF EXISTS biz.cfg_task_generator_params;
-- ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS transfer_count;
-- ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS transferred_from;
-- ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS transferred_at;
-- 注意enum 值一旦添加无法直接删除,需重建类型

View File

@@ -0,0 +1,24 @@
-- P18管理后台任务引擎运营看板 — DDL 迁移
-- 日期2026-03-24
-- 依赖P17 已完成的表结构
-- 1. trigger_jobs 新增 last_stats 字段(记录最近一次执行统计)
ALTER TABLE biz.trigger_jobs
ADD COLUMN IF NOT EXISTS last_stats JSONB;
COMMENT ON COLUMN biz.trigger_jobs.last_stats
IS '最近一次执行的统计结果 JSON如 {"created":5,"replaced":2,"skipped":10,"transferred":1}';
-- 2. cfg_task_generator_params 新增 updated_by 字段(审计追溯)
ALTER TABLE biz.cfg_task_generator_params
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
COMMENT ON COLUMN biz.cfg_task_generator_params.updated_by
IS '最近修改人 user_id用于审计追溯';
-- 验证
-- SELECT column_name, data_type FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'trigger_jobs' AND column_name = 'last_stats';
--
-- SELECT column_name, data_type FROM information_schema.columns
-- WHERE table_schema = 'biz' AND table_name = 'cfg_task_generator_params' AND column_name = 'updated_by';

View File

@@ -0,0 +1,13 @@
-- 迁移admin_users 表新增 roles 字段
-- 原因:登录签发 JWT 时需要携带角色信息,供管理端权限检查使用
-- 回滚ALTER TABLE admin_users DROP COLUMN IF EXISTS roles;
ALTER TABLE admin_users
ADD COLUMN IF NOT EXISTS roles text[] NOT NULL DEFAULT '{site_admin}';
COMMENT ON COLUMN admin_users.roles IS '用户角色列表,如 site_admin / tenant_admin';
-- 验证
-- 1. SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_name='admin_users' AND column_name='roles';
-- 2. SELECT id, username, roles FROM admin_users LIMIT 5;
-- 3. SELECT count(*) FROM admin_users WHERE 'site_admin' = ANY(roles);

View File

@@ -0,0 +1,33 @@
-- 迁移auth.users 新增 avatar_url 字段
-- 日期2026-03-24
-- 原因:小程序登录时获取微信头像,保存到服务器后存储相对路径
-- 影响:后端 /api/xcx/me 返回 avatar_url小程序个人页、审核页显示头像
BEGIN;
ALTER TABLE auth.users
ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(500);
COMMENT ON COLUMN auth.users.avatar_url IS '用户头像相对路径(如 avatars/123.jpg由 chooseAvatar 上传后保存';
COMMIT;
-- ── 回滚 ──
-- BEGIN;
-- ALTER TABLE auth.users DROP COLUMN IF EXISTS avatar_url;
-- COMMIT;
-- ── 验证 ──
-- 1. SELECT column_name, data_type, character_maximum_length
-- FROM information_schema.columns
-- WHERE table_schema='auth' AND table_name='users' AND column_name='avatar_url';
-- 期望avatar_url, character varying, 500
--
-- 2. SELECT id, nickname, avatar_url FROM auth.users LIMIT 5;
-- 期望avatar_url 列存在,值为 NULL
--
-- 3. SELECT col_description(
-- (SELECT oid FROM pg_class WHERE relname='users' AND relnamespace=(SELECT oid FROM pg_namespace WHERE nspname='auth')),
-- (SELECT ordinal_position FROM information_schema.columns WHERE table_schema='auth' AND table_name='users' AND column_name='avatar_url')
-- );
-- 期望:包含 'chooseAvatar' 关键词

View File

@@ -0,0 +1,35 @@
-- 迁移user_site_roles 和 user_assistant_binding 软删除支持
-- 日期2026-03-24
-- 原因:租户后台"移除用户"改为软删除,保留历史记录可追溯
-- 影响:后端所有查询需加 is_removed = false 过滤
BEGIN;
-- 1. user_site_roles 加软删除字段
ALTER TABLE auth.user_site_roles
ADD COLUMN IF NOT EXISTS is_removed boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS removed_at timestamptz;
-- 2. user_assistant_binding 加软删除字段
ALTER TABLE auth.user_assistant_binding
ADD COLUMN IF NOT EXISTS is_removed boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS removed_at timestamptz;
-- 3. 部分索引:加速常规查询(只查未移除的记录)
CREATE INDEX IF NOT EXISTS idx_user_site_roles_active
ON auth.user_site_roles (user_id, site_id)
WHERE is_removed = false;
CREATE INDEX IF NOT EXISTS idx_user_assistant_binding_active
ON auth.user_assistant_binding (user_id, site_id)
WHERE is_removed = false;
COMMIT;
-- ── 回滚 ──
-- BEGIN;
-- DROP INDEX IF EXISTS auth.idx_user_site_roles_active;
-- DROP INDEX IF EXISTS auth.idx_user_assistant_binding_active;
-- ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS removed_at, DROP COLUMN IF EXISTS is_removed;
-- ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS removed_at, DROP COLUMN IF EXISTS is_removed;
-- COMMIT;