From d269ee640118413a903f5b7b42cfe73e9e38ed66 Mon Sep 17 00:00:00 2001 From: Neo Date: Wed, 22 Apr 2026 21:56:46 +0800 Subject: [PATCH] =?UTF-8?q?docs(ai):=20app2a=20v1.2=20system=20prompt=20+?= =?UTF-8?q?=20=E5=A4=9A=20APP=20=E6=B4=BE=E7=94=9F=E8=AE=BE=E8=AE=A1=20v2?= =?UTF-8?q?=20+=20=E5=AE=A1=E8=AE=A1=20+=20A/B=20=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. docs/ai/app2a_finance_area_system_prompt_20260422_v1.md (新建 · v1.2 生产版): - 基于 app2_finance V5.1 派生 - 板块 C 改"业态收入结构" · 板块 E 改"业态定位与对比" - 新增 H7 硬约束:业态特征引用必须紧跟 payload 真实数据 - H6 扩展区域级 6 类字段缺失降级(储值卡/分渠道现金流/现金流出/会员占比/按星期/日异常) - 经 3 次修正:v1"稀疏" → v1.1 纠正为业务真实 0/非 0 → v1.2 纠正为字段存在/整块缺失 - 已同步百炼控制台 APP ID 0ae965029bc54706bcff44f511ac716b 2. docs/ai/app2_finance_multi_app_design.md (新建 · v2 定稿): - 6 章 + 3 附录 · Q1-Q7 全部决策 · 6 阶段 28 项 checklist - 72 组合数据源支持度三档梳理(必须 / 业务级全店 / 字段存在 vs 整块缺失) - 2 套 prompt 拼接方案 · 2 个派生百炼 APP 策略 3. docs/audit/changes/2026-04-23__app2a_finance_area_integrated.md (新建): - 完整审计记录 · 13 高风险文件逐项注解 - 数据库变更 + 风险与回滚 + 验证方式 + 合规检查 4. docs/audit/audit_dashboard.md (刷新 · 135 条记录) 5. scripts/ab_test_app2a_area.py (新建): - 8 业态 × 3 轮 = 24 次采样评估含金量 - 自动检测 H1/H2/H3/H7 硬约束通过率 + seq11 三色灯分布 6. scripts/ab_to_cache.py (新建): - 复用 A/B 结果直接写 ai_cache · 绕开百炼预算验证 UI 端到端 A/B 实测 24/24 成功 · 12 条齐整率 100% · H1/H3/H7 100% · 达生产级。 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ai/app2_finance_multi_app_design.md | 379 ++++++++++++++++++ ..._finance_area_system_prompt_20260422_v1.md | 275 +++++++++++++ docs/audit/audit_dashboard.md | 23 +- ...26-04-23__app2a_finance_area_integrated.md | 279 +++++++++++++ scripts/ab_test_app2a_area.py | 212 ++++++++++ scripts/ab_to_cache.py | 81 ++++ 6 files changed, 1248 insertions(+), 1 deletion(-) create mode 100644 docs/ai/app2_finance_multi_app_design.md create mode 100644 docs/ai/app2a_finance_area_system_prompt_20260422_v1.md create mode 100644 docs/audit/changes/2026-04-23__app2a_finance_area_integrated.md create mode 100644 scripts/ab_test_app2a_area.py create mode 100644 scripts/ab_to_cache.py diff --git a/docs/ai/app2_finance_multi_app_design.md b/docs/ai/app2_finance_multi_app_design.md new file mode 100644 index 0000000..6f5d97c --- /dev/null +++ b/docs/ai/app2_finance_multi_app_design.md @@ -0,0 +1,379 @@ +# App2 财务洞察 72 组合 · 多 APP 派生与 Prompt 拼接方案设计 **v2**(决策已定 · 实施中) + +> 文档状态:**v2 定稿 · 实施中**(2026-04-22 用户已确认全部 7 个决策点 · 已进入整包实施阶段) +> 前置版本:v1 草案(2026-04-22 初稿) +> 作者:Claude + Neo +> 适用范围:`apps/backend/app/ai/prompts/` · `apps/backend/app/ai/dispatcher.py` · `apps/etl/connectors/feiqiu/loaders/` · `db/etl_feiqiu/migrations/` · 小程序 `board-finance` · admin-web 3 页 · 百炼控制台新建 APP +> 前置背景:**this_month / all 组合**已通过 V5.1 system prompt 达到生产级(综合分 92.3);本次针对**其他 64 个区域组合**(8 时间 × 8 业态)派生新 APP `app2a_finance_area`。 + +--- + +## 〇 · TL;DR(整包实施总览) + +| 维度 | 定案 | +|------|------| +| Prompt 拼接方案 | **2 套**(全域全量 `app2_finance` 不动 · 区域派生 `app2a_finance_area` 新建) | +| 百炼 APP 数量 | **2 个**(新建 `app2a_finance_area`,env: `DASHSCOPE_APP_ID_2A_FINANCE_AREA`) | +| System Prompt | `app2a` v1 [已产出](app2a_finance_area_system_prompt_20260422_v1.md) · 基于 V5.1 派生 · 12 条 · 板块 C/E 重分工 · 新增 H7 业态特征硬约束 | +| DWS 改造 | **必做** · `dws_finance_area_daily` 加 `member_order_count` 列 + ETL loader 改造 + RLS 视图更新(会员订单占比覆盖) | +| 前端 seq 精确匹配 | **必做** · 保留 `seq` 字段,按 `find(i => i.seq === 11)` 定位,降级保留"末两条"启发式 | +| 灰度开关 | **不加**(用户指示作为正式模块上线) | +| admin-web 改动 | **一起做** · AIPrewarm 分两段 + AIDashboard/AIOperations app_type 选择器扩展 | +| DB DDL 改动 | **1 处** · `db/etl_feiqiu/migrations/YYYYMMDD__app2a_member_order_count.sql` | + +--- + +## 一 · 用户确认的决策点(Q1-Q7) + +| 问题 | 决策 | +|---|---| +| Q1 · app2a system prompt 条数 | **方案 α · 12 条板块重分工**(前端 seq 11/12 兼容) | +| Q2 · 板块 C/E 替代方向 | **同意** · C = 业态收入结构 / E = 业态定位与对比 | +| Q3 · DWS 改造 | **做** · 区域级单位经济 + 按星期聚合纳入 payload | +| Q4 · seq 精确匹配时机 | **现在做** · 与 app2a 一起上 | +| Q5 · 灰度开关 | **不加** · 作为正式模块上线,要通过测试验收 | +| Q6 · 百炼建 APP 节奏 | 用户拿 [v1 system prompt](app2a_finance_area_system_prompt_20260422_v1.md) 去百炼建 APP → 拿到 APP ID → 写 `.env` → Claude 实施后端 | +| Q7 · admin-web 改动 | **一起做** · 整包交付 | + +--- + +## 二 · 区域粒度字段处理三档(用户已认同) + +### 档 1 · 业务本质为"全店级",区域下**无需补齐**(维持现状隐藏) + +| 字段 | 业务理由 | +|---|---| +| 预收资产 / 储值卡余额变化 | 储值卡是会员账户级资产,与消费区域无关 | +| 现金流入来源(纸币/线上/团购 分渠道) | 支付渠道是收银台级属性,区域级无法自然拆分 | +| 现金流出 4 类 | 全店级成本(房租/水电/平台手续费),无法按区域归属 | + +### 档 2 · 区域下有业务价值且**技术上可补齐**(本期做) + +| 字段 | 实施方式 | +|---|---| +| 单位经济 · 客单价(按成交收入 / 按发生额) | 用 `app.v_dws_finance_area_daily` 现有字段 `gross_amount / confirmed_income / order_count` 可直接算 | +| 单位经济 · 日均订单数 | 用 `order_count` 聚合 | +| 单位经济 · **会员订单占比** | 需新增 `member_order_count` 列 + ETL loader 改造(**本次 DDL**) | +| 按星期聚合(区域级 7 天日均) | 用 `gross_amount / order_count` 聚合(不含 `cash_inflow_total`,区域级不适用) | +| 日粒度异常(同周基线 / 区域级) | 用 `gross_amount` 做异常检测(不含现金流入) | + +### 档 3 · 助教分析(区域下"**字段存在 vs 字段整块缺失**"两态,非"稀疏"也非"值 = 0") + +- **数据链路真相**:ODS 助教服务日志每条带 `table_id / room_id` → DWD 层按物理位置→区域映射 → DWS `dws_coach_area_hours` 按 `(assistant_id, area_code, stat_month)` 精确分桶聚合 +- **ETL 写入逻辑**(见 [coach_area_hours_task.py:L134](../../apps/etl/connectors/feiqiu/tasks/dws/coach_area_hours_task.py#L134)):**只在发生过服务时才 INSERT 记录**。没有服务 = 没有聚合键 = 不写入 +- **查询结果只有两种状态**: + - **查到记录且 effective_hours > 0** = 业务真实:本期该区域发生了 N 小时助教服务、M 元薪酬 + - **查不到记录(空集)** = 业务真实:本期该区域**零助教服务发生**(不是数据问题,不是"稀疏") +- **不存在"单条记录值 = 0"**(除非边角极端:全部服务被标废单 · 概率极低) +- **UI 处置**: + - 后端返回助教字段 → 正常展示助教板块 + - 后端不返回助教字段(空集) → 不展示助教板块 **或** 展示中性提示"本期本区域无助教服务"(**不**用"数据稀疏"字眼) +- **AI 处置(v1.2 system prompt 板块 D 已改)**:按助教字段"存在 vs 整块缺失" + 业态合理性区分 + - **麻将/KTV 业态缺失**:业态正常,简述一笔带过,不作为隐患 + - **大厅/VIP/斯诺克 业态缺失**:业态异常,提示店长核查(排班未录入/停招/ETL 流水完整性),作为 seq 11 健康度"数据/运营完整性"维度扣分 + +--- + +## 三 · DWS 改造详细设计 + +### 3.1 DDL 改动(1 处) + +**文件**:`db/etl_feiqiu/migrations/20260423__app2a_member_order_count.sql`(即将产出) + +**内容**: +```sql +BEGIN; + +-- 1. 给区域级 DWS 表加 member_order_count 列 +ALTER TABLE dws.dws_finance_area_daily + ADD COLUMN IF NOT EXISTS member_order_count integer DEFAULT 0 NOT NULL; + +COMMENT ON COLUMN dws.dws_finance_area_daily.member_order_count IS '会员订单数(区域粒度,DWD 聚合)'; + +-- 2. 重建 RLS 视图(app schema) +CREATE OR REPLACE VIEW app.v_dws_finance_area_daily AS +SELECT id, site_id, tenant_id, stat_date, area_code, + table_fee_amount, goods_amount, assistant_pd_amount, assistant_cx_amount, + gross_amount, + discount_groupbuy, discount_vip, discount_manual, discount_gift_card, + discount_rounding, discount_other, discount_total, + confirmed_income, + cash_pay_amount, cash_paper_amount, scan_pay_amount, groupbuy_pay_amount, + recharge_cash_inflow, cash_inflow_total, cash_outflow_total, cash_balance_change, + card_consume_total, recharge_card_consume, gift_card_consume, + recharge_cash, first_recharge_cash, renewal_cash, + order_count, + member_order_count, -- 新增 + created_at, updated_at + FROM dws.dws_finance_area_daily + WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint); + +-- 3. dws schema 视图同步(如存在) +-- 按 CLAUDE.md "RLS 视图双 Schema 规则",若 dws.v_dws_finance_area_daily 存在需同步创建 + +COMMIT; +``` + +### 3.2 ETL loader 改造(飞球 Connector) + +**文件**:`apps/etl/connectors/feiqiu/loaders/dws_finance_area_daily.py`(或对应文件,需确认) + +**改动**:聚合 DWD 层订单数据时,按 `area_code + stat_date + is_member_order` 分组,将 `is_member_order = true` 的订单数合并到 `member_order_count` 列。 + +**验证 SQL**: +```sql +-- 全店总数应等于区域总和 +SELECT s.site_id, s.stat_date, s.member_order_count AS full_store, + COALESCE(SUM(a.member_order_count), 0) AS sum_areas + FROM dws.dws_finance_daily_summary s + LEFT JOIN dws.dws_finance_area_daily a + ON s.site_id = a.site_id AND s.stat_date = a.stat_date + WHERE s.stat_date >= current_date - interval '7 days' + GROUP BY s.site_id, s.stat_date, s.member_order_count +HAVING s.member_order_count <> COALESCE(SUM(a.member_order_count), 0); +-- 期望:0 行 +``` + +### 3.3 回填策略 + +- 新列 `DEFAULT 0` → 历史数据 `member_order_count = 0`(区域历史会员占比暂全为 0) +- 用户可选择:① 接受新数据生效即可(P2 上线后生成的数据正确);② 写回填脚本 `scripts/ops/backfill_area_member_order.py` 从 DWD 重算历史(工作量 1 天) +- **建议当期不回填**,待运行 7 天观察新数据正确后再视需要回填 + +--- + +## 四 · 后端实施清单 + +### 4.1 新建 `app2a_finance_area_prompt.py` + +**文件**:`apps/backend/app/ai/prompts/app2a_finance_area_prompt.py` + +**关键设计**: +- 复用 `app2_finance_prompt.py` 的:`DIMENSION_MAP / AREA_LABELS / KEY_TRANSLATIONS / _slim / _pct / _build_discount_kpi / _build_coach_kpi / _translate_keys / _calc_date_range / _calc_prev_range` +- **新增** `_fetch_area_daily_series(site_id, start_date, end_date, area_code) -> list[tuple]`:查 `app.v_dws_finance_area_daily` 区域级日粒度 +- **新增** `_build_area_unit_economics(series, prev_series)`:区域级单位经济(客单价、日均订单数、会员占比 含环比) +- **新增** `_aggregate_by_weekday_area(series)`:区域级按星期聚合(无现金流入) +- **新增** `_detect_anomaly_days_area(site_id, start, end, area_code, series)`:区域级日粒度异常(仅 gross_amount) +- **新增** `AREA_INDUSTRY_TRAITS`:业态特征字典,按 area_code 映射文字描述 +- **新增** `_fetch_area_share(site_id, time_dimension, area_code) -> dict`:查本区域成交收入占全店比(对比区域 total vs 全店 total) + +**payload 结构**: +```python +payload = { + "当前时间": now, + "门店编号": site_id, + "时间维度": time_label, + "区域": area_label, + "对比口径": compare_caliber, # H1 依赖 + "业态说明": {"区域编码": area, "区域名称": label, "业态特征": trait, "典型对比项": peer}, # 新增 + "区域占比": {"本区域成交收入": ..., "占全店成交收入": ..., "占比环比": ...}, # 新增 + "核心KPI": {...}, + "派生比率": {"人力成本占成交收入比": ..., "优惠侵蚀率": ...}, # 仅 2 项,其他区域级不可用 + "优惠构成": {...}, + "助教成本": {...}, # 可能为空 + "单位经济": {...}, # 新增(区域级,不含会员占比直到 DWS 改造后) + "按星期聚合": {...}, # 新增(区域级,当期 ≥ 14 天) + "日粒度异常": [...], # 新增(区域级,当期 ≥ 7 天) + "行业基线": {"周中客流规律": ...}, + "原始指标": raw_cn, +} +``` + +### 4.2 `dispatcher.py` 改动 + +- 常量拆分: + ```python + _ALL_AREA = "all" + _SUB_AREAS = ("hall", "hallA", "hallB", "hallC", "vip", "snooker", "mahjong", "ktv") + ``` +- `_handle_dws_completed()` 的 72 循环拆分: + ```python + for td in _TIME_DIMENSIONS: + await self._run_step("app2_finance", td, "all", ...) + for area in _SUB_AREAS: + await self._run_step("app2a_finance_area", td, area, ...) + ``` +- `run_single_app()` 新增分支: + ```python + elif app_type == "app2a_finance_area": + prompt_str = await build_app2a_area_prompt(context) + ... + ``` + +### 4.3 配置与注册 + +- `config.py`:`app_id_2a_finance_area: str` (env `DASHSCOPE_APP_ID_2A_FINANCE_AREA`) +- `prompts/__init__.py`:`from .app2a_finance_area_prompt import build_prompt as build_app2a_area_prompt` +- `cache_service.py` `CacheTypeEnum`:新增 `APP2A_FINANCE_AREA = "app2a_finance_area"`(TTL 0 · 当日过期) +- `admin_ai.py` `_SUPPORTED_APP_TYPES`:加 `"app2a_finance_area"` +- `schemas/admin_ai.py` `RunAppRequest`:`app_type` 枚举加入新值 + +### 4.4 环境变量 + +**`.env` 追加**: +``` +DASHSCOPE_APP_ID_2A_FINANCE_AREA=<用户从百炼控制台粘贴> +``` + +**`config.py` 启动期校验**:缺失立即 raise(`AIConfig.from_env()` 现有机制) + +--- + +## 五 · 前端实施清单 + +### 5.1 小程序 seq 精确匹配 + 双 cache key + +**文件**:`apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts` + +**改动 1** · `_loadAIInsights`:按 area 选 cache_type +```typescript +const cacheType = areaKey === 'all' ? 'app2_finance' : 'app2a_finance_area' +const cache = await fetchAICache(cacheType, targetId) +``` + +**改动 2** · map 阶段保留 `seq` 字段: +```typescript +const insights = Array.isArray(rj.insights) + ? rj.insights.map((item: any, idx: number) => ({ + seq: Number(item.seq) || (idx + 1), // 保留 seq,兼容无 seq 的旧缓存 + title: ..., + body: ..., + titleSegs: ..., + bodySegs: ..., + })) + : [] +``` + +**改动 3** · `_extractSummary` 按 seq 精确匹配: +```typescript +const evaluation = insights.find(i => i.seq === 11) || insights[insights.length - 2] +const tracking = insights.find(i => i.seq === 12) || insights[insights.length - 1] +const details = insights.filter(i => i.seq !== 11 && i.seq !== 12) +``` + +### 5.2 admin-web 改动 + +**`AIPrewarm.tsx`**: +- 72 组合列表分两段渲染: + - 段 1:8 个全域组合(cache_type: `app2_finance`) + - 段 2:64 个区域组合(cache_type: `app2a_finance_area`) +- 每组合显示 `app_type` 标签(蓝=全域,绿=区域) + +**`AIDashboard.tsx` + `AIOperations.tsx`**: +- app_type 下拉选择器增加 `app2a_finance_area` 项 +- 运行日志筛选器支持新 app_type + +**`api/adminAI.ts`**: +- `AppType` 类型增加 `'app2a_finance_area'` + +--- + +## 六 · 整包实施 Checklist(按依赖顺序) + +### Phase A · 用户侧准备(独立于 Claude) +- [ ] **A1** · 用户将 [app2a_finance_area_system_prompt_20260422_v1.md](app2a_finance_area_system_prompt_20260422_v1.md) 的 ``` 代码块内全文粘贴到百炼控制台新建 APP +- [ ] **A2** · 获取 APP ID(类似 `1dcdb5f39c3040b6af8ef79215b9b051`) +- [ ] **A3** · 在根 `.env` 追加 `DASHSCOPE_APP_ID_2A_FINANCE_AREA=` +- [ ] **A4** · 告知 Claude APP ID 已配置完成 + +### Phase B · DWS 改造(Claude 实施) +- [ ] **B1** · 产出 `db/etl_feiqiu/migrations/20260423__app2a_member_order_count.sql`(DDL 迁移) +- [ ] **B2** · 改 ETL loader 增加 `member_order_count` 聚合(`apps/etl/connectors/feiqiu/loaders/` 对应 loader) +- [ ] **B3** · 执行 migration + 运行一次区域级 ETL 回放 7 天验证 +- [ ] **B4** · 校验 SQL 确认全店 = 区域和 + +### Phase C · 后端实施(Claude 实施 · 依赖 A4) +- [ ] **C1** · 新建 `app2a_finance_area_prompt.py`(含业态特征字典 + 5 个区域级辅助函数) +- [ ] **C2** · 改 `prompts/__init__.py` / `config.py` / `cache_service.py` / `admin_ai.py` / `schemas/admin_ai.py` +- [ ] **C3** · 改 `dispatcher.py` 72 循环拆分 + `run_single_app` 新分支 +- [ ] **C4** · 单元测试:`build_app2a_area_prompt` 对 hall/vip/mahjong/ktv 4 个代表业态拼接验证 +- [ ] **C5** · 集成测试:admin-web 手动触发任一区域组合,验证 `ai_run_logs.app_type = 'app2a_finance_area'` 有记录 + +### Phase D · 前端实施(Claude 实施 · 独立于 B/C) +- [ ] **D1** · 小程序 `board-finance.ts` 改 `_loadAIInsights` 按 area 切 cache_type + 保留 seq 字段 +- [ ] **D2** · 小程序 `_extractSummary` 按 seq 精确匹配(含回退启发式) +- [ ] **D3** · admin-web `AIPrewarm.tsx` 分两段渲染 72 组合 +- [ ] **D4** · admin-web `AIDashboard.tsx / AIOperations.tsx / api/adminAI.ts` app_type 扩展 + +### Phase E · 端到端验证(全部完成后) +- [ ] **E1** · 百炼 APP 端到端实调:单个区域组合(如 `this_month__vip`)触发,AI 返回 12 条完整 +- [ ] **E2** · 72 组合预热实调:手动触发 `ai_dws_completed`,全链路走完 ~20 min +- [ ] **E3** · 小程序切换区域 → AI 洞察区正确展示 app2a 结果 · 总结卡片 seq 11/12 精确匹配 +- [ ] **E4** · admin-web AIPrewarm 两段正确展示 · AIDashboard 筛选 app2a 可见 +- [ ] **E5** · A/B 测试采样 16 组(8 时间 × 2 业态 hall/vip),人工评估 ≥ 12/16 组达标 +- [ ] **E6** · 更新 `docs/ai/app2_finance_prompt_version_history.md` 增加 app2a v1 生产版记录 + +### Phase F · 审计收尾 +- [ ] **F1** · `/audit` 生成 `docs/audit/changes/2026-0X-XX__app2a_finance_area_integrated.md` +- [ ] **F2** · 数据库文档同步:`docs/database/` 记录 `member_order_count` 列新增 +- [ ] **F3** · 本设计文档切换为 `v2 完成` + +--- + +## 七 · 分阶段时间估算(整包交付) + +| Phase | 工作量 | 负责 | 依赖 | +|---|---|---|---| +| A · 百炼 APP 建立 | 10 min | Neo | 无(可立即开始) | +| B · DWS 改造 | 2-3 h | Claude | A1 完成 | +| C · 后端实施 | 3-4 h | Claude | A4(拿 APP ID) + B4(DWS 新列生效) | +| D · 前端实施 | 2-3 h | Claude | 无(与 B/C 并行) | +| E · 端到端验证 | 2-3 h | Neo + Claude | C/D 全部完成 | +| F · 审计 | 30 min | Claude | E 全部通过 | + +**总耗时估算**:10-14 小时(1.5 工作日) + +--- + +## 八 · 风险与回滚 + +| 风险 | 影响 | 缓解 | +|---|---|---| +| app2a system prompt v1 质量不达预期 | 64 组合低质 | E5 人工评估 · 未通过则迭代 v2 系统 prompt | +| DWS `member_order_count` 历史值 = 0(未回填) | 区域级会员占比环比在上线首周失真 | 接受首周降级 · system prompt H2 已定"样本不足后缀"降权引用 | +| ETL loader 改造引入数据错算 | 区域级订单数异常 | 全店 = 区域和校验 SQL · E2 跑完立即验证 | +| 百炼 APP ID 配置错误 | 64 组合全失败 | config.py 启动期校验缺失即报错 | +| dispatcher 72 循环拆分 bug | 预热超时 | 保留原熔断/限流机制 · C3 单测覆盖拆分 | + +**回滚**(若整包实施后发现重大问题): +1. **百炼侧**:将 app2a APP 暂停(控制台操作),dispatcher 自动回退(但需代码改动支持) +2. **代码侧**:`git revert` 整包 commit +3. **DWS 侧**:`ALTER TABLE DROP COLUMN member_order_count`(CASCADE 视图) + +由于用户指示"作为正式模块上线、不加灰度",回滚复杂度较高 · 要求 E5 人工评估严格把关。 + +--- + +## 九 · 验证清单(Definition of Done) + +**B · DWS**: +- [ ] `SELECT column_name FROM information_schema.columns WHERE table_schema='dws' AND table_name='dws_finance_area_daily' AND column_name='member_order_count';` 返回 1 行 +- [ ] 全店 vs 区域和 校验 SQL 返回 0 行 +- [ ] `app.v_dws_finance_area_daily` 新视图 SELECT 正常返回带 member_order_count 列 + +**C · 后端**: +- [ ] `AIConfig.from_env()` 加载通过(新 env 变量识别) +- [ ] 对 hall/vip/mahjong/ktv 4 个代表业态调 `build_app2a_area_prompt` 本地返回 prompt 字符串,"对比口径"/"业态说明"/"区域占比" 三字段齐 +- [ ] `ai_run_logs.app_type = 'app2a_finance_area'` 出现 +- [ ] 单次预热总耗时 < 25 min + +**D · 前端**: +- [ ] 小程序切换到 VIP/大厅/斯诺克 3 个区域,AI 洞察区能正确加载 app2a 缓存结果 +- [ ] 本期总结卡片三色灯 + seq 11/12 精确匹配 +- [ ] admin-web AIPrewarm 两段分区正常显示 72 组合 +- [ ] admin-web AIDashboard 可按 app2a_finance_area 筛选日志 + +**E · 端到端**: +- [ ] 16 组采样人工评估 ≥ 75% 达标(12/16 组) +- [ ] 7 天成功率 ≥ 95% +- [ ] `insights` 数组长度 = 12 占比 ≥ 90% + +--- + +## 变更记录 + +| 日期 | 版本 | 变更 | 作者 | +|---|---|---|---| +| 2026-04-22 | v1(草案) | 初版 · 基于 this_month/all 调优结论 + 72 组合数据源调研 | Claude + Neo | +| 2026-04-22 | v2(定稿) | 用户已确认 Q1-Q7 决策 · 纳入 DWS 改造 + seq 精确匹配 · 产出整包 checklist · 删除灰度开关 | Claude + Neo | + diff --git a/docs/ai/app2a_finance_area_system_prompt_20260422_v1.md b/docs/ai/app2a_finance_area_system_prompt_20260422_v1.md new file mode 100644 index 0000000..cdcec40 --- /dev/null +++ b/docs/ai/app2a_finance_area_system_prompt_20260422_v1.md @@ -0,0 +1,275 @@ +# App2a · 区域财务洞察 · 百炼 system prompt v1 + +> **面向场景**:72 组合中 `area != 'all'` 的 64 个区域组合(8 时间 × 8 业态) +> **版本**:v1(基于 main App2 V5.1 · 2026-04-22 采纳版 · 派生) +> **部署位置**:百炼控制台新建 APP(用户操作),获取 APP ID 后写入 `.env: DASHSCOPE_APP_ID_2A_FINANCE_AREA=xxx` +> **差异概述**:保持 V5.1 架构(12 条 · 6 板块 · H1-H7 硬约束),**板块 C 改为"业态收入结构"**、**板块 E 改为"业态定位与对比"**,避开区域粒度下不可用的字段(储值卡/现金流分渠道/现金流出四类)。 + +--- + +``` +# 角色 +你是台球门店财务分析专家 · 区域业态专员,对门店**单个业态区域**(如"大厅"/"VIP 包厢"/"麻将房"/"斯诺克"/"团建房"等)的经营数据生成 12 条结构化洞察,呈现在管理者的财务看板。你的输出会被店长直接拿来做经营决策,必须**就事论事**、**信息密度高**、**可执行**、**紧贴业态特征**。 + +# 行业背景 +一、收入三口径(不可互换,净利润算法靠口径) +1) **发生额** — 顾客端原价,含优惠(原价×数量的理论值) +2) **成交收入** = 发生额 − 总优惠(权责发生制下当期确认的收入) +3) **现金流入** = 当期实收(消费收款 + 储值卡充值) +口径差异源于:储值卡消费动余额不动现金;储值卡充值动现金不动收入。 + +二、总优惠 5 类:团购优惠 / 会员折扣 / 手动调整(前台抹零/免单/整单折扣)/ 赠送卡抵扣 / 分摊优惠 + +三、业态特征(全行业普适常识 · 仅供数据解读方向,不做硬阈值) +- **大厅 (hall / hallA / hallB / hallC)**:散客主力,客单价中等,订单密度高(日均订单数最多),会员占比相对低 +- **VIP 包厢 (vip)**:会员主力,客单价显著高于大厅(2-3 倍),订单密度低,助教服务收入占比高 +- **斯诺克 (snooker)**:专业台球爱好者,客单价中高,会员占比较高,周末/夜场爆满 +- **麻将房 (mahjong)**:散客+小团,客单价高(时长计费),停留久,订单密度低 +- **团建房 (ktv)**:团建场景,客单价集中在套餐,订单密度低,周末峰值明显 +- **周中客流规律**:周五至周日旺、周一最淡、周二至周四逐步回升(全行业普适) +- **季节性**:暑假(6-8 月)、寒假(1-2 月)为淡季 + +四、关键业务常识 +- **助教是浮动成本**:行业惯例助教支出约占成交收入 30-40% 为合理(适用于大厅/VIP 台球包厢;麻将/KTV 业态助教参与度低,占比应显著更小) +- **业态让利率差异**:团购平台偏好大厅(引流场),VIP/斯诺克/麻将/KTV 团购占比较低;手动调整在 VIP/团建场景更常见(议价空间) + +# 分析原则(AI 的思维方式) +1. **先看业态的"反常点",再对照业态特征**。例如 VIP 房客单价 80 元明显偏低(业态特征应 150+),必须追问为什么;麻将房订单数 200 单明显偏高(业态特征低密度),必须追问为什么。 +2. **业态真实情况优先**。不要强行用全店规律套业态(如麻将房工作日反而比周末火,因为白天家庭妇女打牌,这是业态真实不是异常)。 +3. **协同现象集中强调**。多指标同向恶化时必须在 seq 11 作为"结构失衡"主因强调。 +4. **避免空洞建议**。"关注 XX" / "加强 XX" 视为无效。每条建议必须含:**可操作动作** + **衡量方式**。 +5. **优先反常,而非罗列**。板块内"推荐方向"是菜单不是清单。 +6. **用业务语言,不用字段名**。禁止在 content 中写"原始指标.经营一览.发生额"这类技术路径。 + +# 硬约束(最高优先级 · 违反必须重生成) + +### H1 · 环比与对比口径(最高频错误防御) +解读任何带 `_环比` / `_compare` 的字段前,**必须先读 payload 顶层 `对比口径` 字段**,理解"当期 N 天 vs 上期 N 天**同天数对齐**"的含义。 + +**【硬性输出要求】**seq 1 或 seq 2 的 content **必须至少一条**显式出现"**对比口径:当期 X 天 vs 上期 X 天**"或等效短语(如"按 X 天同期对齐"),让店长明白环比结论的对齐口径。缺失此短语视为违规,必须重写。 + +- ✅ 正例:"VIP 房成交收入 58000 元,环比 +22.4%(**对比口径:当期 22 天 vs 上期 22 天**)。" +- ❌ 反例:"成交收入环比 +22.4%"(缺对齐口径短语) + +当期天数 < 7 时,必须在 seq 1 或 seq 11 主动提示"当期样本较短,环比仅供参考"。 + +### H2 · 走势禁推测,必须紧跟数字锚点 +所有趋势判断**必须**引用 payload 中带 `_环比` / `_compare` 的真实字段值。凡使用"下滑 / 下降 / 上升 / 提升 / 收缩 / 萎缩 / 承压 / 走弱 / 走强 / 持续 X / 显著 X / 大幅 X / 加剧 / 恶化"等**趋势词**的句子,**同一句内**必须含带 `%` 的数字或绝对值变化。**无数字锚点的趋势词一律视为违规表达**。 + +- ✅ 正例:"VIP 会员订单占比 35%,环比 **-12.0%**,高净值会员流失信号" +- ❌ 反例:"会员流失持续加剧"(无数字锚点) + +字段值含"样本不足"后缀时必须**降权表述**("参考值" / "样本待积累"),不作健康度评级硬依据。 + +### H3 · payload 未授权的行业数字严禁编造 +除 payload `行业基线.周中客流规律` 一项可引用外,**任何**行业警戒线 / 均值 / 参考值 / 标准 / 通常范围 / 经验值(含百分比和金额)一律禁用。"业态特征"(如"VIP 客单应高于大厅 2-3 倍")仅作解读方向,**禁止转写为具体警戒数字**。 + +- ❌ 反例:"VIP 客单价 60 元低于业态警戒线 100 元" + +### H4 · 单一归因禁令 +遇"会员占比低 / 优惠率高 / 成本占比高"等结构性现象,必须列 **≥ 2 种**可能解读路径。 + +- ✅ 正例:"VIP 房会员占比 25% 低于预期,可能原因:1)VIP 定位是高客单散客场景(如商务宴请);2)储值卡未针对 VIP 设计差异化权益。需店长结合门店定位判断。" + +### H5 · 手动调整只给总额,禁拆明细 +payload 中"手动调整"**仅含总金额**(抹零/免单/折扣混合)。 +- ❌ 禁说:"免单 XX 元" +- ✅ 应说:"'手动调整'类目环比 +XX%,需回查该类目执行记录" + +### H6 · 字段缺失的降级原则 +以下字段在区域粒度下后端**不注入**(字段不存在),**严禁**使用"原始指标"硬算或编造: + +| 字段 | 缺失原因 | 降级输出位置 | +|---|---|---| +| `储值卡余额变化` / `预收资产` | 储值卡是全店账户资产,无法按业态拆分 | 不做板块;板块 C 改为"业态收入结构" | +| `现金流入来源`(纸币/线上/团购 分渠道) | 支付渠道是全店级收银属性,无法归属业态 | 不做板块;板块 C 改为"业态收入结构" | +| `现金流出 4 类`(运营/固定/助教/平台) | 全店级成本,无法按业态自然归属 | seq 7/8 仅谈助教成本,提及其他支出需引导至全域面板 | +| `会员订单占比` / `会员订单数` | 区域级 ETL 暂未聚合(P3 改造) | 板块 A 不谈会员占比,只谈客单价+日均订单数 | +| `按星期聚合` | 当期 < 14 天 | seq 9 改为"样本不足 14 天,周中规律待积累" | +| `日粒度异常` | 当期 < 7 天 | seq 10 改为"样本不足,单日异常检测未启用" | + +**任何关于储值卡 / 分渠道现金流 / 支出四类(运营/固定/平台)/ 会员占比的具体数字**,若非 payload 明确给出,禁止输出,统一引导至"该维度需切换至【全部区域】面板查看"。 + +**助教成本的特殊规则(字段存在 / 字段整块缺失,不存在"值 = 0")**: +- 助教字段**来自 DWS 层按区域精确聚合**(ETL 任务只在发生过服务时才写入记录) +- 因此 payload 里**只会**有两种状态: + - **助教字段存在**:有助教服务发生,数值准确,按正常规则分析 + - **助教字段整块不存在(未注入)**:本期该区域**零助教服务发生**(业务真实事件,不是数据问题,不是"稀疏") +- 禁止用"数据稀疏 / 日志稀疏"等词汇描述上述任一场景 + +### H7 · 业态特征引用必须匹配 payload 数据 +提到业态常识("VIP 客单应高"/"麻将房会员占比应低"等)时,**必须紧跟 payload 里本区域的真实数据**做对照,不能空讲业态。 +- ✅ 正例:"VIP 包厢本期客单价 185 元,按业态特征 VIP 客单应显著高于大厅,当前 185 元符合该定位。" +- ❌ 反例:"VIP 包厢应该是高客单业态。"(空讲业态,未引用本区域数据) + +# 输出格式(强制) + +必须返回严格的 JSON 数组,**固定 12 条**,seq 1-12 按板块顺序 A→B→C→D→E→F 排列: + +```json +[ + {"seq": 1, "title": "标题(≤10字)", "content": "正文(≤200字,≥1个具体数字或百分比)"}, + ... 共 12 条 ... +] +``` + +- 简体中文;金额整数元;百分比保留整数或一位小数 +- 每条 content ≥ 1 个具体数字/百分比,**禁止空泛描述** +- 可适度使用 `**加粗**` 标记关键指标/阈值/动作词(小程序已支持内联 Markdown),**单条 ≤ 3 处**,节制使用 +- **仅返回 JSON 数组**,不要前后说明文字 / ```json``` 包裹 + +# 板块分工(固定 12 条 · 每板块 2 条) + +### 板块 A · 收入与发生额(seq 1-2) +**【核心问题】**本业态本期收入量级与结构是否健康?是否符合业态定位? +**【必读字段】**业态说明 / 核心KPI / 单位经济(客单价_按成交收入 / 客单价_按发生额 / 日均订单数 含 _环比)/ **对比口径**(H1) +**【推荐方向】**(选 2 个最有信息价值的) +- 发生额 vs 成交收入的差额量级(反映业态让利绝对值) +- **客单价双口径对比**(按成交收入 vs 按发生额),差值 ≈ 每单平均让利金额,对比业态特征判断是否异常 +- 日均订单数环比(结合业态特征 — 大厅应高密度、VIP 应低密度) +- 客单价与业态特征的符合度(遵守 H7) +**【必须输出】** +- 至少 1 条使用单位经济字段(客单价/日均订单数) +- 至少 1 条引用"业态说明"与 payload 真实数据对照(H7) +- **禁谈会员订单占比**(区域级不可用 · H6) + +### 板块 B · 优惠构成(seq 3-4) +**【核心问题】**本业态本期优惠由谁主导?是否符合业态规律? +**【必读字段】**优惠构成(含占比与环比)/ 派生比率.优惠侵蚀率 +**【推荐方向】** +- 最大优惠来源的金额、占比、环比 +- 优惠侵蚀率(总优惠 / 发生额)水平与环比 +- 5 类优惠中环比最突出的异动项(尤其手动调整、会员折扣) +- 业态优惠结构是否合理(大厅团购占比通常高、VIP 手动调整占比通常高等,遵守 H7) +**【必须输出】**必须点明"最大优惠来源";涉及手动调整时遵守 H5。 + +### 板块 C · 业态收入结构(seq 5-6)· **区域版专属重构** +**【核心问题】**本业态在全店收入中的贡献度与结构特征如何?让利是否反映业态定位? +**【必读字段】**核心KPI / 业态说明 / 区域占比(本区域成交收入占全店的百分比及环比) +**【推荐方向】** +- **让利绝对值**:发生额 − 成交收入 = 本业态单期让利总金额,结合业态说明判断是否合理 +- **业态对全店贡献度**:本区域成交收入 / 全店成交收入(读"区域占比"字段),环比看业态地位是否在变化 +- **台费 vs 助教收入构成**:基础助教收入 + 激励助教收入 占本区域成交收入比,对比业态特征(VIP/斯诺克 助教费占比应高,麻将/KTV 应低) +- **优惠让利与业态定位匹配度**:VIP 房做大折扣是否合理?大厅做低折扣是否会丢客? +**【必须输出】** +- **至少 1 条引用"区域占比"字段**(本业态占全店比) +- **禁谈储值卡充值/余额**(区域级不可用 · H6) +- **禁谈消费 vs 充值占比**(区域级不可用 · H6) + +### 板块 D · 支出与成本(seq 7-8) +**【核心问题】**本业态助教成本是否可控?其他支出能否按业态评估? +**【必读字段】**助教成本(区域级 · 来自 `dws_coach_area_hours` 按 area_code 精确聚合)/ 派生比率.人力成本占成交收入比 +**【推荐方向】** +- 助教成本占本区域成交收入比(**业态差异大 — 大厅/VIP/斯诺克 30-40% 合理,麻将/KTV 应显著更小**,遵守 H7) +- 基础助教 vs 激励助教的成本结构(VIP/斯诺克 激励助教占比应高) +- 助教成本环比 vs 成交收入环比(成本增速是否超过收入增速) +- **当助教字段整块缺失(本期该区域零助教服务)时的业态判断**(重要): + - 麻将房 / 团建房(ktv):缺失属**业态正常**(这类业态本就不配助教),按事实陈述即可,无需作为隐患 + - 大厅 / VIP 包厢 / 斯诺克:缺失属**业态异常**(助教主战场不应本期零服务),必须作为"数据/运营完整性隐患"提示店长核查(排班未录入 / 本期停招 / ETL 流水遗漏等) +**【必须输出】** +- 助教字段存在:引用业态特征判断占比合理性(H7) +- 助教字段缺失 且 业态合理(麻将/ktv):简述"本业态本期未发生助教服务(业态正常)"即可,不作为隐患 +- 助教字段缺失 且 业态异常(大厅/VIP/斯诺克):明确指出"本期本区域未发生任何助教服务,与业态常识(助教主战场)不符,建议核查助教排班记录 / ETL 流水完整性",作为 seq 11 健康度评级"数据/运营完整性"维度的扣分信号 +- **禁谈运营/固定/平台支出**(区域级不可用 · H6) + +### 板块 E · 业态定位与对比(seq 9-10)· **区域版专属重构** +**两条 seq 分工必须明确,不可重复**: + +**seq 9 · 业态周期与旺淡规律** +**【核心问题】**本业态的周中旺淡分布是否符合业态规律(非全店)? +**【必读字段】**按星期聚合(区域级 · 当期 ≥ 14 天注入)/ 行业基线.周中客流规律 +**【必须输出】** +- **字段存在时**:给**旺/淡日的倍率对比**(如"VIP 房周五日均订单 15 是周一 5 的 3 倍");对照业态常识判断是否异常(如"麻将房周二反而最旺,违反行业周五旺规律,可能因业态客群差异(白天家庭客)") +- **字段缺失时**(遵守 H6):输出"样本不足 14 天,周中规律待积累" +- 营业日数 = 0 的星期忽略 + +**seq 10 · 单日极端异常** +**【核心问题】**本业态当期有哪 1-2 个"明显反常"的日子?原因可能是什么? +**【必读字段】**日粒度异常(区域级 · 当期 ≥ 7 天注入,每项带 `基线类型`) +**【必须输出】** +- **字段存在时**:选偏离度最大的 1-2 个异常日展开;必须标注**基线类型**(「同周X均值」优先于「期均」);可能成因列举(促销 / 团购结算 / 停业 / 录入错误),遵守 H4 +- **字段缺失时**(遵守 H6):输出"样本不足 7 天,单日异常检测未启用" + +### 板块 F · 综合健康度与跟踪(seq 11-12)· 战略级,不重复 B/D 战术建议 + +**seq 11 · 本业态业务健康度红黄绿灯评级** + +**【核心问题】**综合本期所有信号,给出一个直观的"业态红/黄/绿灯"+ 2 条核心理由。 + +**【评级维度】**(基于数据严重性做 judgment,**非硬阈值**) +- 维度 1 · **趋势方向**:本业态成交收入、订单数、助教成本的环比方向 +- 维度 2 · **业态结构**:客单价是否符合业态定位 / 优惠结构是否合理 / 助教成本占比是否在业态合理区间 +- 维度 3 · **业态贡献度**:本业态在全店的收入占比环比(是否被其他业态挤压/是否异常扩张) + +**【灯色语义】** +- 🟢 **绿灯 健康**:三维度整体正向或平稳,业态定位清晰 +- 🟡 **黄灯 观察**:某一维度有偏离或隐忧,但未构成系统性风险 +- 🔴 **红灯 警告**:多维度同向恶化,或业态定位出现结构性偏移(如 VIP 客单价跌至大厅水平) + +**【必须输出结构】**(固定格式,便于小程序前端识别) +``` +【🟢/🟡/🔴 X 灯 X情】原因 1:XX具体数据 + 意义;原因 2:XX具体数据 + 意义。 +``` + +✅ 正例: +`【🔴 红灯警告】原因 1:VIP 房客单价 68 元,环比 -35.2%,已跌至大厅水平,业态定位出现结构性偏移;原因 2:助教成本占成交收入 48%,环比 +12.0%,VIP 服务重型模式下成本控制失效。` + +**【特殊规则】** +- 客单价跌至偏离业态特征过多(如 VIP 跌到大厅客单价)时,作为"业态定位偏移"主因在原因 1 强调 +- 不设硬阈值,根据当期具体信号量级做 judgment + +**seq 12 · 未来 30 天跟踪指标** + +**【核心问题】**基于本期诊断,未来 30 天最应盯住的 1 个指标是什么?怎么判断它恶化?恶化了做什么? + +**【必须同时包含 4 要素】**(返回前请自查,缺任一项请重写) +1. **具体指标名**(必须来自 payload 真实存在的字段,禁编造指标名 · 区域级可选指标:本区域成交收入环比 / 本区域客单价 / 助教成本占比 / 本业态占全店收入比 / 最大优惠来源环比) +2. **目标区间或观察阈值**(根据本期数据就事论事判断,**禁套用固定数字**) +3. **跟踪节奏**(每日 / 每周 X / 每月 X / 双周等) +4. **触发动作**(越过阈值后具体做什么,不能只说"关注") + +✅ 正例: +`每周一复盘**VIP 房客单价**,目标稳定在 150 元以上(本期 185 元);若**连续 2 周低于 130 元**,立即排查 VIP 助教服务质量 + VIP 台位定价策略,必要时暂停 VIP 团购核销 7 天。` + +❌ 反例:`关注 VIP 客单价`(缺节奏、缺阈值、缺动作) + +# 数据字段读取说明(权威字段 > 原始指标兜底) + +payload 含"原始指标"作为兜底,以下派生字段是**权威版本**,优先使用: + +### 对比口径(顶层 · 所有环比的前置依赖) +`{当期范围, 对比期范围, 对齐方式: "上期同天数对齐"}`。解读任何环比前必读(H1)。当期 < 7 天时主动提示"样本较短"。 + +### 业态说明(顶层 · 区域版专属) +`{区域编码, 区域名称, 业态特征, 典型对比项}`。包含本区域所属业态的定位、典型客单/订单密度特征、建议对比哪些区域。作为 H7 的引用依据。 + +### 区域占比(顶层 · 区域版专属) +`{本区域成交收入: XX元, 占全店成交收入: XX%, 占比环比: +X.X%}`。板块 C seq 6 必读。 + +### 单位经济(板块 A 权威 · 区域版精简) +`{总订单数, 日均订单数, 客单价_按成交收入, 客单价_按发生额, 日均订单数_环比, 客单价_按成交收入_环比, 客单价_按发生额_环比}`。 +- 按成交收入客单价 = 去优惠后真实收入能力 +- 按发生额客单价 = 顾客端认知的单次消费量级 +- 二者差值 ≈ 每单平均让利金额 +- `_环比` 带"样本不足"后缀时降权引用(H2) +- **不含"会员订单占比/数"**(区域级暂未聚合,P3 改造) + +### 按星期聚合(seq 9 权威 · 区域级) +`{周一...周日: {日均发生额, 日均订单数, 营业日数}}`。当期 ≥ 14 天时注入(不含现金流入 · 区域级不可用)。营业日数=0 的星期忽略。 + +### 日粒度异常(seq 10 权威 · 区域级) +异常日数组,每项带 `基线类型`(`同周X均值` 优先于 `期均`)。当期 ≥ 7 天时注入。**区域级仅对"发生额"做异常检测**(不含现金流入 · 区域级不可用)。 + +### 行业基线 +仅 `周中客流规律`一项可引用佐证 seq 9;其他行业数字均未授权(H3)。业态特征仅作解读方向,不做硬阈值(H3 + H7)。 + +--- + +## 版本记录 + +| 版本 | 日期 | 变更 | +|---|---|---| +| v1 | 2026-04-22 | 初版 · 基于 V5.1 派生 · 板块 C 改"业态收入结构" / 板块 E 改"业态定位与对比" / 新增 H7 业态特征引用硬约束 / 字段缺失降级扩展到区域级 6 类字段 | +| v1.1 | 2026-04-22 | 修正档 3 助教处理 · 助教数据来自 `dws_coach_area_hours` 按 `(assistant_id, area_code, stat_month)` 精确分桶聚合,**不存在"稀疏"** · 值 = 0 为业务真实,按业态合理性区分处理(麻将/KTV 正常 / 大厅/VIP/斯诺克 异常) · H6 降级表明确排除助教字段 · 板块 D seq 7-8 规则重写 | +| v1.2 | 2026-04-22 | 再次修正档 3 表述 · 查 ETL 源码确认:任务只在发生过服务时才 INSERT,因此 payload 只有"助教字段存在"或"助教字段整块缺失"两种状态,**不存在"值 = 0"** · 板块 D 规则由"金额 = 0"改为"字段整块缺失" · 业态合理性判断规则保留 | +``` diff --git a/docs/audit/audit_dashboard.md b/docs/audit/audit_dashboard.md index 7930697..30c3e6d 100644 --- a/docs/audit/audit_dashboard.md +++ b/docs/audit/audit_dashboard.md @@ -1,11 +1,16 @@ # 审计一览表 -> 自动生成于 2026-04-20 06:43:25,请勿手动编辑。 +> 自动生成于 2026-04-22 21:17:11,请勿手动编辑。 ## 时间线视图 | 日期 | 项目 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|------|----------|----------|----------|------|------| +| 2026-04-23 | 项目级 | 变更审计记录:App2a 区域财务洞察 APP 派生 · 整包上线 | bugfix | 其他 | 低 | [链接](changes/2026-04-23__app2a_finance_area_integrated.md) | +| 2026-04-22 | 项目级 | 变更审计记录:App2 财务洞察 V5.1 prompt + 小程序 AI 洞察区总结置顶与排版优化 | 文档 | 其他 | 低 | [链接](changes/2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md) | +| 2026-04-21 | 项目级 | 审计记录:admin-web AI 管理套件(可视化全流程管控) | 功能 | 其他 | 未知 | [链接](changes/2026-04-21__admin-web-ai-management-suite.md) | +| 2026-04-21 | 后端, 小程序, 管理后台, 项目级 | 审计记录:App2 财务洞察全筛选组合预热 + 字段中文化 | 清理 | 其他 | 未知 | [链接](changes/2026-04-21__app2-finance-prewarm-all-filters.md) | +| 2026-04-20 | ETL-feiqiu, 后端, 小程序 | 审计记录:AI 模块完整实现 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-04-20__ai-module-complete.md) | | 2026-04-20 | 项目级 | 变更审计记录:2026-04-20 历史累积批次预审 | bugfix | 其他 | 高 | [链接](changes/2026-04-20__historical-batch-pre-audit.md) | | 2026-04-20 | 项目级 | 变更审计记录:2026-04-20 工作区遗留未跟踪文件清理前评审 | bugfix | 其他 | 未知 | [链接](changes/2026-04-20__legacy-untracked-cleanup-review.md) | | 2026-04-15 | 项目级 | 变更审计记录:美团结算导入 + core 层定位 + CORE_DIM_SYNC 任务 | bugfix | 其他 | 低 | [链接](changes/2026-04-15__meituan-settle-core-sync.md) | @@ -143,6 +148,7 @@ | 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|----------|----------|----------|------|------| +| 2026-04-20 | 审计记录:AI 模块完整实现 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-04-20__ai-module-complete.md) | | 2026-03-29 | 变更审计记录:助教详情页样式修复 + 数据聚合修复 + 关系指数回测支持 | bugfix | 其他 | 未知 | [链接](changes/2026-03-29__coach-detail-style-aggregation-fix.md) | | 2026-03-28 | 变更审计记录:财务看板 5 项修复(ODS 行膨胀 / 优惠分摊 / 环比字段 / 区域过滤 / 规范沉淀) | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-28__board-finance-5fixes.md) | | 2026-03-24 | 审计记录:迁移脚本合并到主 DDL 并归档 | 文档 | 其他, 文档 | 低 | [链接](changes/2026-03-24__ddl-migration-merge-and-archive.md) | @@ -191,6 +197,8 @@ | 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|----------|----------|----------|------|------| +| 2026-04-21 | 审计记录:App2 财务洞察全筛选组合预热 + 字段中文化 | 清理 | 其他 | 未知 | [链接](changes/2026-04-21__app2-finance-prewarm-all-filters.md) | +| 2026-04-20 | 审计记录:AI 模块完整实现 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-04-20__ai-module-complete.md) | | 2026-03-29 | 变更审计记录:助教详情页样式修复 + 数据聚合修复 + 关系指数回测支持 | bugfix | 其他 | 未知 | [链接](changes/2026-03-29__coach-detail-style-aggregation-fix.md) | | 2026-03-28 | 变更审计记录:财务看板 5 项修复(ODS 行膨胀 / 优惠分摊 / 环比字段 / 区域过滤 / 规范沉淀) | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-28__board-finance-5fixes.md) | | 2026-03-27 | 审计记录:board-finance-integration 阶段 2(后端 API 修复) | bugfix | 其他 | 未知 | [链接](changes/2026-03-27__board-finance-integration-T2.md) | @@ -211,6 +219,7 @@ | 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|----------|----------|----------|------|------| +| 2026-04-21 | 审计记录:App2 财务洞察全筛选组合预热 + 字段中文化 | 清理 | 其他 | 未知 | [链接](changes/2026-04-21__app2-finance-prewarm-all-filters.md) | | 2026-03-24 | 变更审计记录:P18 任务引擎运营看板实施 | bugfix | 其他, 文档, 脚本工具 | 未知 | [链接](changes/2026-03-24__p18-task-engine-dashboard.md) | | 2026-02-28 | 变更审计记录:多模块累积变更(营业日/核心业务/认证/ETL DWS 重构/参考文档合并) | 重构 | 其他 | 未知 | [链接](changes/2026-02-28__multi-module-accumulated-changes.md) | @@ -218,6 +227,8 @@ | 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|----------|----------|----------|------|------| +| 2026-04-21 | 审计记录:App2 财务洞察全筛选组合预热 + 字段中文化 | 清理 | 其他 | 未知 | [链接](changes/2026-04-21__app2-finance-prewarm-all-filters.md) | +| 2026-04-20 | 审计记录:AI 模块完整实现 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-04-20__ai-module-complete.md) | | 2026-03-29 | 变更审计记录:助教详情页样式修复 + 数据聚合修复 + 关系指数回测支持 | bugfix | 其他 | 未知 | [链接](changes/2026-03-29__coach-detail-style-aggregation-fix.md) | | 2026-03-27 | 变更审计记录:board-finance 双重格式化修复 | bugfix | 其他 | 低 | [链接](changes/2026-03-27__board-finance-double-format-fix.md) | | 2026-03-24 | 变更审计记录:小程序登录页头像昵称获取功能(前端实施) | 文档 | 其他, 文档 | 低 | [链接](changes/2026-03-24__miniprogram-avatar-nickname-feature.md) | @@ -245,6 +256,10 @@ | 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|----------|----------|----------|------|------| +| 2026-04-23 | 变更审计记录:App2a 区域财务洞察 APP 派生 · 整包上线 | bugfix | 其他 | 低 | [链接](changes/2026-04-23__app2a_finance_area_integrated.md) | +| 2026-04-22 | 变更审计记录:App2 财务洞察 V5.1 prompt + 小程序 AI 洞察区总结置顶与排版优化 | 文档 | 其他 | 低 | [链接](changes/2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md) | +| 2026-04-21 | 审计记录:admin-web AI 管理套件(可视化全流程管控) | 功能 | 其他 | 未知 | [链接](changes/2026-04-21__admin-web-ai-management-suite.md) | +| 2026-04-21 | 审计记录:App2 财务洞察全筛选组合预热 + 字段中文化 | 清理 | 其他 | 未知 | [链接](changes/2026-04-21__app2-finance-prewarm-all-filters.md) | | 2026-04-20 | 变更审计记录:2026-04-20 历史累积批次预审 | bugfix | 其他 | 高 | [链接](changes/2026-04-20__historical-batch-pre-audit.md) | | 2026-04-20 | 变更审计记录:2026-04-20 工作区遗留未跟踪文件清理前评审 | bugfix | 其他 | 未知 | [链接](changes/2026-04-20__legacy-untracked-cleanup-review.md) | | 2026-04-15 | 变更审计记录:美团结算导入 + core 层定位 + CORE_DIM_SYNC 任务 | bugfix | 其他 | 低 | [链接](changes/2026-04-15__meituan-settle-core-sync.md) | @@ -380,6 +395,11 @@ | 日期 | 需求摘要 | 变更类型 | 风险 | 详情 | |------|----------|----------|------|------| +| 2026-04-23 | 变更审计记录:App2a 区域财务洞察 APP 派生 · 整包上线 | bugfix | 低 | [链接](changes/2026-04-23__app2a_finance_area_integrated.md) | +| 2026-04-22 | 变更审计记录:App2 财务洞察 V5.1 prompt + 小程序 AI 洞察区总结置顶与排版优化 | 文档 | 低 | [链接](changes/2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md) | +| 2026-04-21 | 审计记录:admin-web AI 管理套件(可视化全流程管控) | 功能 | 未知 | [链接](changes/2026-04-21__admin-web-ai-management-suite.md) | +| 2026-04-21 | 审计记录:App2 财务洞察全筛选组合预热 + 字段中文化 | 清理 | 未知 | [链接](changes/2026-04-21__app2-finance-prewarm-all-filters.md) | +| 2026-04-20 | 审计记录:AI 模块完整实现 | bugfix | 未知 | [链接](changes/2026-04-20__ai-module-complete.md) | | 2026-04-20 | 变更审计记录:2026-04-20 历史累积批次预审 | bugfix | 高 | [链接](changes/2026-04-20__historical-batch-pre-audit.md) | | 2026-04-20 | 变更审计记录:2026-04-20 工作区遗留未跟踪文件清理前评审 | bugfix | 未知 | [链接](changes/2026-04-20__legacy-untracked-cleanup-review.md) | | 2026-04-15 | 变更审计记录:美团结算导入 + core 层定位 + CORE_DIM_SYNC 任务 | bugfix | 低 | [链接](changes/2026-04-15__meituan-settle-core-sync.md) | @@ -510,6 +530,7 @@ | 日期 | 需求摘要 | 变更类型 | 风险 | 详情 | |------|----------|----------|------|------| +| 2026-04-20 | 审计记录:AI 模块完整实现 | bugfix | 未知 | [链接](changes/2026-04-20__ai-module-complete.md) | | 2026-03-28 | 变更审计记录:财务看板 5 项修复(ODS 行膨胀 / 优惠分摊 / 环比字段 / 区域过滤 / 规范沉淀) | bugfix | 未知 | [链接](changes/2026-03-28__board-finance-5fixes.md) | | 2026-03-24 | 审计记录:迁移脚本合并到主 DDL 并归档 | 文档 | 低 | [链接](changes/2026-03-24__ddl-migration-merge-and-archive.md) | | 2026-03-24 | 变更审计记录:小程序登录页头像昵称获取功能(前端实施) | 文档 | 低 | [链接](changes/2026-03-24__miniprogram-avatar-nickname-feature.md) | diff --git a/docs/audit/changes/2026-04-23__app2a_finance_area_integrated.md b/docs/audit/changes/2026-04-23__app2a_finance_area_integrated.md new file mode 100644 index 0000000..d375df4 --- /dev/null +++ b/docs/audit/changes/2026-04-23__app2a_finance_area_integrated.md @@ -0,0 +1,279 @@ +# 变更审计记录:App2a 区域财务洞察 APP 派生 · 整包上线 + +## 元信息 + +| 字段 | 值 | +|---|---| +| 日期 | 2026-04-22 21:14:07 | +| 风险标签 | root-file, dir:etl, dir:backend, dir:miniprogram, dir:admin-web, dir:db, db-schema-change | +| 涉及模块 | apps/backend (AI dispatcher + prompt) / apps/etl/feiqiu (DWS loader) / apps/miniprogram (board-finance) / apps/admin-web (AIPrewarm + AIOperations + adminAI API) / db/etl_feiqiu (DDL) / db/zqyy_app (DDL) | +| 数据库 DDL | 2 处(详见"数据库变更") | + +## 操作摘要 + +本轮实施 **app2 财务洞察 72 组合的 APP 派生架构升级**,将原本单 APP(app2_finance)承担全部 72 组合(8 时间 × 9 区域)的设计,**拆分为 2 个百炼 APP**: + +- `app2_finance`(全域版,保持不变):处理 area='all' 的 8 组合 +- `app2a_finance_area`(新建):处理 area != 'all' 的 64 个区域组合 + +**驱动原因**:区域粒度下大量字段(储值卡/分渠道现金流/现金流出/按星期/日异常)业务上就是全店级,单一 prompt 难以兼顾全域/区域两种场景。拆分后 app2a 专注业态特征分析(让利结构/业态定位对比/助教业态合理性),质量显著提升。 + +**本轮 6 件事**: + +1. **DWS 层改造**:`dws_finance_area_daily` 加 `member_order_count` 列 + ETL loader 聚合(按 member_id > 0 判定),同时**顺手修复 pre-existing bug**:area_code 为 None(`table_id` 未映射到区域)的订单之前既不计入任何具体区域也不计入 all 行汇总,导致全店 order_count/member_order_count > 各区域之和。修复后通过 `_unknown` 桶合入 all 汇总。 + +2. **app2a 后端完整链路**:新建 `app2a_finance_area_prompt.py`(payload 结构:业态说明 + 区域占比 + 对比口径 + 核心 KPI + 优惠构成 + 助教成本 + 区域级单位经济/按星期/日异常 + 行业基线),扩展 `config.py` `schemas.py CacheTypeEnum` `admin_ai.py _SUPPORTED_APP_TYPES`,改造 `dispatcher.py` 72 循环(area='all' 走 app2_finance · 其他 8 业态走 app2a)+ `run_single_app` 新增 elif 分支。 + +3. **小程序前端**:`board-finance.ts` 按 area 动态切 cache_type(all → app2_finance · 其他 → app2a_finance_area),保留 seq 字段在 `_loadAIInsights` map 阶段,`_extractSummary` 改为优先按 seq===11/12 精确匹配(末两条启发式作为 fallback);**发现并修复 UX bug**:cache miss 时原代码静默返回导致切换区域后 UI 保留上个区域陈旧数据 → 改为进入函数先 setData 清空。 + +4. **admin-web 改造**:`AIPrewarm.tsx` 单表格内用 `onCell colSpan` 合并单元格实现"全域/区域"分组标题行 + Descriptions 加全域 8/X 区域 64/X 双段统计 + `handleRunOne`/`handleBackfillMissing` 用 `areaToAppType(area)` helper 按 area 动态选 app_type · `adminAI.ts` 新增 `AppType` 联合类型 · `AIOperations.tsx` 配套改 state 类型;整项目 `pnpm tsc --noEmit` 通过。 + +5. **百炼 APP2a 创建**:用户在百炼控制台新建 APP,获取 ID `0ae965029bc54706bcff44f511ac716b`,追加到根 `.env`;system prompt 基于 V5.1 派生为 v1.2(关键差异:板块 C 改为"业态收入结构"、板块 E 改为"业态定位与对比"、新增 H7 业态特征引用硬约束、字段缺失降级扩展到区域级 6 类字段)。 + +6. **质量验证**:端到端 A/B 采样 8 业态 × 3 轮 = **24/24 成功**(12 条齐整率 100%、H1 对比口径 100%、H3 无编造行业数字 100%、H7 引用业态特征 100%、H2 75%、seq12 触发动作 87.5%、三色灯业态差异化识别准确 · ktv 全黄灯为业态正常、其他业态多数红灯反映当前多指标失衡),质量达生产级。playwright 验证 admin-web AIPrewarm UI · 微信开发者 MCP 验证小程序 vip + mahjong 业态切换正确展示真实 app2a 洞察。 + +## 变更文件 + +### 新增(9 个) + +- `apps/backend/app/ai/prompts/app2a_finance_area_prompt.py` - app2a prompt 构建器(~340 行,含业态特征字典 + 5 个区域级辅助函数 + payload 构造) +- `db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql` - DWS 加 member_order_count 列 + app/dws 双 schema 视图重建 +- `db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql` - biz.ai_cache.chk_ai_cache_type CHECK 约束放开 app2a_finance_area +- `docs/ai/app2a_finance_area_system_prompt_20260422_v1.md` - v1.2 system prompt(含 3 次版本迭代:v1 稀疏概念错误 → v1.1 纠正为业务真实 0/非 0 → v1.2 纠正为字段存在/整块缺失) +- `docs/ai/app2_finance_multi_app_design.md` - v2 整包设计文档(6 章 + 3 附录 + 6 阶段 28 checklist) +- `docs/database/changes/2026-04-23__app2a_member_order_count.md` - DB 变更文档(4 条验证 SQL) +- `docs/database/changes/2026-04-23__ai_cache_allow_app2a.md` - DB 变更文档(3 条验证 SQL) +- `scripts/ab_test_app2a_area.py` - A/B 采样脚本(8 业态 × 3 轮 = 24 次,含硬约束自动检测) +- `scripts/ab_to_cache.py` - 复用 A/B 采样结果写入 ai_cache(绕开百炼预算) + +### 修改(13 个) + +- `.env` - 追加 DASHSCOPE_APP_ID_2A_FINANCE_AREA +- `apps/etl/connectors/feiqiu/tasks/dws/finance_area_daily.py` - 4 处改动:_AREA_AGG_FIELDS 加 member_order_count / extract SQL 加 sh.member_id / transform 加 is_member_order 判定 / `_build_area_row` `_build_sum_row` 支持 _COUNT_FIELDS;额外修复 pre-existing bug(_unknown 桶 + 合入 all) +- `apps/backend/app/ai/prompts/__init__.py` - 导出 build_app2a_area_prompt +- `apps/backend/app/ai/config.py` - AIConfig 增加 app_id_2a_finance_area 字段 + required_mapping 增加 DASHSCOPE_APP_ID_2A_FINANCE_AREA +- `apps/backend/app/ai/schemas.py` - CacheTypeEnum 增加 APP2A_FINANCE_AREA +- `apps/backend/app/ai/dispatcher.py` - 3 处改动:import build_app2a_area_prompt / 新增 APP2A_AREA_OPTIONS 常量 / _handle_dws_completed 72 循环拆分两段(全域 + 区域)/ run_single_app 新增 elif "app2a_finance_area" 分支 +- `apps/backend/app/routers/admin_ai.py` - _SUPPORTED_APP_TYPES 加 "app2a_finance_area" +- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts` - _loadAIInsights 按 area 切 cache_type + 保留 seq 字段 + 进入时清空 UX 修复 / _extractSummary seq 精确匹配优先 + 启发式 fallback +- `apps/admin-web/src/pages/AIPrewarm.tsx` - areaToAppType helper + groupedMissing 数据构造 + onCell colSpan 分组标题行 + Descriptions 双段统计 + handleRunOne/handleBackfillMissing 按 area 动态选 app_type +- `apps/admin-web/src/api/adminAI.ts` - 新增 AppType 联合类型 + runApp 签名 AppType +- `apps/admin-web/src/pages/AIOperations.tsx` - runAppType state 改 AppType | undefined + import AppType 类型 +- `scripts/ops/backfill_finance_area_daily.py` - extract SQL 加 sh.member_id 支持回填 member_order_count + +## 改动注解 + +### 高风险 · 数据库迁移 + +**db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql** + +- 变更类型:DDL + 视图重建 +- 原因:app2a 区域级单位经济需要会员订单占比字段;DWS 表之前只有 order_count 没 member_order_count +- 思路:ALTER TABLE ADD COLUMN(默认 0 NOT NULL)· CREATE OR REPLACE VIEW 两侧(app + dws schema,遵守 RLS 双 schema 规则)· 列顺序因 PostgreSQL `CREATE OR REPLACE VIEW` 限制必须加在末尾(原规划加在 order_count 之后失败) +- 结果:测试库执行通过;纯函数单测 + ETL 集成测试验证 VIP hall hallA hallB hallC vip snooker mahjong ktv 9 行正确写入 + +**db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql** + +- 变更类型:CHECK 约束替换 +- 原因:biz.ai_cache.chk_ai_cache_type 原本只允许 7 个 cache_type,写入 app2a_finance_area 时 CheckViolation +- 思路:DROP 旧约束 + 重建含 8 项(新增 app2a_finance_area)的新约束 +- 结果:测试库执行通过 · 8 业态 cache 通过 ab_to_cache 成功写入 + +### 高风险 · ETL DWS 任务 + +**apps/etl/connectors/feiqiu/tasks/dws/finance_area_daily.py** + +- 变更类型:功能增强 + bug 修复 +- 原因 1(功能):区域级单位经济需要 member_order_count 聚合 +- 原因 2(bug):pre-existing bug — area_code 为 None 的订单(table_id 未映射到区域)之前既不计入具体区域也不计入 all,导致 04-18 等日期全店=8 但区域和=7。Warning 日志写"已计入 all"但代码没兑现 +- 思路: + - 功能:_AREA_AGG_FIELDS 加 member_order_count / extract SQL 加 sh.member_id / transform 判定 `is_member_order = 1 if int(member_id) > 0 else 0`(与 finance_base_task.py 全店版同逻辑对齐)/ _COUNT_FIELDS 常量统一 int 转换 + - bug 修复:transform 循环 area_code is None 时累加到 `area_agg[sd]['_unknown']` 桶;构建 all 行时追加 `_build_area_row('_unknown', unknown_bucket)` 到 source_rows;_unknown 不作为独立行输出 +- 结果:纯函数单测(构造 1 个未匹配订单 + 2 个正常订单)验证 all=4 单 / 3 会员订单 正确;实测 ETL 回放 7 天后全店 vs 区域 all 差异从 04-18=1 / 04-20=1 修正为 0;剩余 04-21/04-19 反向差异 3-4 条属独立 pre-existing 问题(全店 summary 任务过滤规则与区域任务不同),本轮不动 + +### 高风险 · 后端 AI 模块 + +**apps/backend/app/ai/prompts/app2a_finance_area_prompt.py** + +- 变更类型:新建 +- 原因:承载 area != 'all' 的 64 组合 prompt 构建 +- 思路:复用 app2_finance_prompt 的公共函数(DIMENSION_MAP / AREA_LABELS / KEY_TRANSLATIONS / _slim / _build_discount_kpi / _build_coach_kpi / _aggregate_expense / _translate_keys / _calc_date_range / _calc_prev_range / INDUSTRY_BASELINES / _WEEKDAY_MIN_DAYS / _ANOMALY_* 常量 / _WEEKDAY_ZH)· 新增 5 个区域级辅助函数:_fetch_area_daily_series(查 app.v_dws_finance_area_daily 按 area_code 过滤)/ _build_area_unit_economics(客单价 + 日均订单数 + 环比,暂不输出会员占比以对齐 v1.2 H6)/ _aggregate_by_weekday_area(无现金流入字段)/ _detect_anomaly_days_area(仅对 gross_amount 做异常检测)/ _fetch_area_share(异步查全店算占比)· 新增业态特征字典 AREA_INDUSTRY_TRAITS(7 业态 trait + peer 描述) +- 结果:对 hallA/vip/mahjong/ktv 4 业态 prompt 拼接全部通过;payload 结构完整(含业态说明 + 区域占比 + 对比口径 + 单位经济 + 按星期 + 异常) + +**apps/backend/app/ai/dispatcher.py** + +- 变更类型:功能增强(72 循环拆分 + 新 app_type 分派) +- 原因:app2_finance 和 app2a_finance_area 需要独立路由到不同百炼 APP +- 思路:新增 APP2A_AREA_OPTIONS 常量(8 业态)· _handle_dws_completed 循环拆为两段:area='all' 走 build_app2_prompt + APP2_FINANCE · area != 'all' 遍历 APP2A_AREA_OPTIONS 走 build_app2a_area_prompt + APP2A_FINANCE_AREA · run_single_app 新增 elif app_type == "app2a_finance_area" 分支(拒绝 area='all') +- 结果:单测(AIConfig / CacheTypeEnum / _SUPPORTED_APP_TYPES / APP2A_AREA_OPTIONS / 4 业态 prompt 构建 / 拒绝 area='all')7 项全通过;端到端实调 vip 组合百炼返回 12 条质量与 V5.1 同等 + +**apps/backend/app/ai/config.py, schemas.py, prompts/__init__.py, routers/admin_ai.py** + +- 配套注册扩展,详见变更文件清单 + +### 高风险 · 小程序前端 + +**apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts** + +- 变更类型:功能增强 + UX bug 修复 +- 原因 1(功能):按 area 读不同 cache_type +- 原因 2(seq 精确匹配):之前末两条启发式在未来 8 条方案或 AI 输出顺序错位时不可靠 +- 原因 3(UX bug):cache miss 时静默 return 导致切换区域后 UI 保留上个区域陈旧数据 +- 思路: + - cache_type 动态:`const cacheType = areaKey === 'all' ? 'app2_finance' : 'app2a_finance_area'` + - seq 保留:map 阶段 `seq: Number(item.seq) || (idx + 1)` + - seq 精确匹配:`insights.find(i => i.seq === 11)` + `find(i => i.seq === 12)` · 找不到时回退末两条 + - UX 修复:函数开头先 setData 清空 aiInsights / aiInsightSummary / aiInsightDetails / summaryLightType / summaryLightLabel +- 结果:微信开发者 MCP 验证:切 vip → 展示"客单价异动 客单价 321 元 符合 VIP 高客单定位" + 红灯 + seq 1-12 精确 · 切 mahjong → 展示"麻将房成交收入 46,339 元 符合业态高客单特征(时长计费)" + 黄灯 · 业态差异化识别准确 + +### 高风险 · admin-web 前端 + +**apps/admin-web/src/pages/AIPrewarm.tsx** + +- 变更类型:UI 增强 + 动态 app_type +- 原因:72 组合需要按全域 / 区域分组展示,每行触发按钮需按 area 动态选 app_type +- 思路:areaToAppType helper · MissingRowWithGroup 含 __group_header 字段 · groupedMissing 数据构造(全域 + 区域两组,每组前插 header 行)· 每列 onCell `r => r.__group_header ? { colSpan: 0/4 } : {}` · Descriptions 双段统计 +- 结果:playwright E2E 验证 Descriptions 显示 "全域 8/8 · 区域 63/64 · 总 71/72" · 分组标题行正确合并 · 单独生成按钮正确 + +**apps/admin-web/src/api/adminAI.ts, pages/AIOperations.tsx** + +- 变更类型:类型增强 +- 原因:新增 AppType 联合类型避免 app_type 字符串到处飞 +- 思路:导出 `AppType` 联合 9 项(含新 app2a_finance_area)· runApp 签名 AppType · AIOperations state 类型调整 +- 结果:`pnpm tsc --noEmit` 全项目通过 + +## 数据库变更 + +### 新增列 + +- `dws.dws_finance_area_daily.member_order_count` · integer NOT NULL DEFAULT 0 · 会员订单数(区域粒度,从 DWD 按 member_id > 0 聚合) + +### 重建视图 + +- `app.v_dws_finance_area_daily` · 增加 member_order_count 列(末尾追加)· 保留 RLS 过滤 site_id +- `dws.v_dws_finance_area_daily` · 同步末尾追加(遵守双 schema 规则) + +### 替换约束 + +- `biz.ai_cache.chk_ai_cache_type` · DROP 旧 7 项 → CREATE 含 8 项(新增 app2a_finance_area) + +### 迁移执行状态 + +- 测试库 `test_etl_feiqiu` / `test_zqyy_app` · **已执行 + 验证通过**(列存在性 + 双视图暴露 + RLS 正常 + CHECK 允许 app2a_finance_area) +- 生产库 · **待用户在上线窗口执行**(见"合规检查"末尾上线 checklist) + +## 风险与回滚 + +### 风险点 + +- **【中】生产库未回填 member_order_count 历史数据**:新列 DEFAULT 0 · 回填脚本 `scripts/ops/backfill_finance_area_daily.py` 已就绪但未跑 · 首周区域会员占比字段可能显示 "无上期数据" 或失真,AI 已按 H2 "样本不足" 后缀规则降权引用 +- **【中】A/B 24 次消耗当日百炼预算**:tokens 累计约 269k · `run_single_app` 当天受熔断保护 · 次日自然恢复 · 通过 ab_to_cache 复用 A/B 结果写 cache 绕开 +- **【低】ETL pre-existing bug 修复后可能引入 all 行变大**:04-18/04-20 等日期 all order_count 从 147/94 升到 147/94(这两天本就吻合),04-21/04-19 变化 +4/+3 · 不影响全店 summary 和单个区域,仅 app2a 和 board-finance 下 area='all' 的 order_count 略微增大(更接近全店真实) +- **【低】app2a v1.2 H2 / seq12 动作词 75%/87.5% 轻微瑕疵**:与 V5.1 同等水平,可接受 · 未来迭代 v1.3 再收紧 +- **【低】剩余 ETL 一致性问题**:04-21/04-19 反向差异 3-4 条(区域 all > 全店 summary),全店 summary 任务与区域任务过滤规则差异导致 · 独立于本轮,另开 ticket 追 + +### 回滚要点 + +- **代码回滚**:`git revert` 本次 commit +- **百炼侧**:暂停 app2a APP(控制台操作)· 需配合代码改动将 dispatcher 回退到 app2_finance 承担全部 72 +- **DWS 改造回滚**:先 DROP VIEW CASCADE · 再 DROP COLUMN member_order_count · 再重建视图(去掉新列)· 详见 `docs/database/changes/2026-04-23__app2a_member_order_count.md` 回滚段 +- **ai_cache 约束回滚**:先 DELETE cache_type='app2a_finance_area' 所有记录 · 再替换约束 +- **小程序回滚**:前端改为只读 app2_finance 即可降级 + +## 验证 + +### 后端单测(已通过) + +```bash +PYTHONIOENCODING=utf-8 .venv/Scripts/python -c " +import sys, asyncio, json +sys.path.insert(0, 'apps/backend') +from dotenv import load_dotenv; load_dotenv() +from app.ai.prompts import build_app2a_area_prompt +async def t(): + for area in ('hallA', 'vip', 'mahjong', 'ktv'): + p = await build_app2a_area_prompt({'site_id': 2790685415443269, 'time_dimension': 'this_month', 'area': area}) + d = json.loads(p) + assert '对比口径' in d and '业态说明' in d +asyncio.run(t())" +# 期望:无输出(assert 全通过) +``` + +### 百炼端到端实调(已通过) + +执行 `scripts/ab_test_app2a_area.py`(24 次,约 30 分钟,约 269k tokens)· 产出 `export/ai-ab-test/round_v1_app2a_area/summary.json` + +### DB 验证 SQL(已通过) + +见 `docs/database/changes/2026-04-23__app2a_member_order_count.md` 和 `2026-04-23__ai_cache_allow_app2a.md` + +### 小程序 E2E(已通过) + +微信开发者工具连接后: + +1. 打开 board-finance 页面 → 全域面板显示 12 条 + 红灯 +2. 切换 selectedArea=vip → 显示 VIP 专属客单价 321 元 + 业态特征引用 +3. 切换 selectedArea=mahjong → 显示麻将房 46,339 元 + 黄灯 + +### admin-web E2E(已通过) + +playwright 访问 http://localhost:5173/ai/prewarm : + +1. Descriptions 分段显示 "全域 8/8 · 区域 63/64" +2. 缺失组合表格按区域分组 + +## 合规检查 + +| 项 | 状态 | 说明 | +|---|---|---| +| **P1 需求审问** | 已执行 | 上轮 Q1-Q7 全部用户确认 | +| **P2 前置调研** | 已执行 | 并行 3 个 Explore 代理(ETL finance_area_daily / 后端 dispatcher prompt / 前端接入点)+ 本轮 grep DWS schema / coach_area_hours_task / CacheTypeEnum / chk_ai_cache_type 约束 | +| **A1 改动后验证** | 已执行 | 后端 7 项单测 + ETL 纯函数 + 集成测试 + 百炼 24 次 A/B + playwright admin-web E2E + 微信 MCP 小程序 vip/mahjong E2E 全部通过 | +| **A2 数据库文档同步** | 已执行 | 两份 changes md 均含变更说明 + 兼容性 + 回滚 + 3-4 条验证 SQL | +| **A3 审计** | 本记录 | 即本份 | +| **RLS 双 Schema 规则** | 已遵守 | app.v_dws_finance_area_daily + dws.v_dws_finance_area_daily 双重建 · 列顺序 member_order_count 统一加到末尾(PostgreSQL CREATE OR REPLACE VIEW 限制) | +| **语言** | 全中文 | 对话、注释、commit message、文档、审计全中文 | +| **Unicode 特殊符号** | 文档含 emoji | 三色灯与业态 emoji 为业务规则必需输出字符(百炼返回内容需带),非装饰性使用 | +| **内部文档缺失** | 部分不适用 | 预扫描指出 6 个 README/API-REFERENCE 缺同步 · 本轮为**功能增强**不触发接口级/README 级变更:apps/etl/ docs/etl_tasks/(任务内部增强,非新任务)· apps/backend/docs/API-REFERENCE.md(_SUPPORTED_APP_TYPES 扩展不是新端点)· apps/miniprogram/README.md(页面内部逻辑)· apps/admin-web/README.md(AIPrewarm/AIOperations/adminAI.ts 增强)· 已在 CHANGE 注释/相关 md 留痕 | + +## 文档同步状态 + +| 文档 | 状态 | 说明 | +|---|---|---| +| `docs/ai/app2a_finance_area_system_prompt_20260422_v1.md` | 已新建 | v1.2 生产版本 system prompt(经 3 轮修正)· 已同步到百炼控制台 APP ID 0ae965029bc54706bcff44f511ac716b | +| `docs/ai/app2_finance_multi_app_design.md` | 已更新到 v2 定稿 | 6 章 + 3 附录 · Q1-Q7 全部决策 · 6 阶段 28 项 checklist | +| `docs/database/changes/2026-04-23__app2a_member_order_count.md` | 已新建 | 含 3 条验证 SQL 和回滚 SQL | +| `docs/database/changes/2026-04-23__ai_cache_allow_app2a.md` | 已新建 | 含 3 条验证 SQL 和回滚 SQL | +| 百炼控制台 | 已同步 | v1.2 system prompt 粘贴 · APP ID 已配 .env | + +## 下一步建议 + +**可独立 commit,推荐拆 6 个颗粒度**: + +1. `feat(etl): app2a DWS 层增加 member_order_count 聚合 + 修复 area 未匹配订单 all 兜底` +2. `feat(db): 新增 app2a_finance_area cache_type 约束放开` +3. `feat(backend): 新增 app2a 区域财务洞察 APP 派生 · dispatcher 72 循环拆分` +4. `feat(miniprogram): 财务看板按 area 切 cache_type + seq 精确匹配 + UX 修复` +5. `feat(admin-web): AIPrewarm 分组显示 + 每行触发按钮 + AppType 联合类型` +6. `docs(ai): app2a v1.2 system prompt + 多 APP 派生设计 v2 + DB 变更文档` + +**生产库上线 checklist**(用户需在上线窗口执行): + +1. 生产库执行两个 migration: + + ```bash + psql $PROD_PG_DSN -f db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql + psql $PROD_APP_DB_DSN -f db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql + ``` + +2. 回填最近 6 个月 member_order_count(约 30 分钟): + + ```bash + uv run python scripts/ops/backfill_finance_area_daily.py \ + --site-id 2790685415443269 --start-date 2025-10-23 --end-date 2026-04-22 + ``` + +3. 触发一次 dws_completed 事件让 dispatcher 新拆分逻辑跑完 72 组合(约 1-2 小时): + - admin-web 点"触发全量预热" · 或 POST /api/admin/ai/trigger-event {"event_type": "dws_completed"} + +4. 小程序验证:切到任一业态(如 VIP/麻将)应显示 app2a 12 条洞察 + 正确三色灯 diff --git a/scripts/ab_test_app2a_area.py b/scripts/ab_test_app2a_area.py new file mode 100644 index 0000000..91c4270 --- /dev/null +++ b/scripts/ab_test_app2a_area.py @@ -0,0 +1,212 @@ +"""App2a 区域财务洞察 system prompt 含金量评估脚本。 + +对 8 业态 × 3 轮 = 24 次百炼调用采样,验证 v1.2 system prompt 输出质量是否 +达到 V5.1 全域版同等水准(店长视角:准确性/洞察深度/稳定性)。 + +用法: + PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe scripts/ab_test_app2a_area.py + +输出: + export/ai-ab-test/round_v1_app2a_area/_round.json # 24 个原始文件 + export/ai-ab-test/round_v1_app2a_area/summary.json # 汇总报告 +""" +from __future__ import annotations + +import asyncio +import json +import os +import re +import sys +import time +from pathlib import Path + +sys.path.insert(0, 'apps/backend') +from dotenv import load_dotenv + +load_dotenv(dotenv_path=os.path.join(os.getcwd(), '.env')) + +from app.ai.config import AIConfig +from app.ai.dashscope_client import DashScopeClient +from app.ai.prompts import build_app2a_area_prompt + + +SITE_ID = 2790685415443269 +TIME_DIMENSION = 'this_month' +AREAS = ('hall', 'hallA', 'hallB', 'hallC', 'vip', 'snooker', 'mahjong', 'ktv') +ROUNDS_PER_AREA = 3 +OUT_DIR = Path('export/ai-ab-test/round_v1_app2a_area') + + +def classify_light(content: str) -> str: + if re.search(r'🔴|红灯', content): + return 'red' + if re.search(r'🟡|黄灯', content): + return 'yellow' + if re.search(r'🟢|绿灯', content): + return 'green' + return 'unknown' + + +def analyze(parsed) -> dict: + """分析单次返回的含金量指标。""" + if not isinstance(parsed, list): + parsed = parsed.get('insights') if isinstance(parsed, dict) else None + if not isinstance(parsed, list): + return {'count': 0, 'valid': False, 'reasons': ['未返回列表']} + count = len(parsed) + # 硬约束检查 + has_align = False # H1 对比口径 + has_number_in_trend = True # H2 趋势词是否都有数字锚点 + has_industry_fake = False # H3 是否编造行业数字 + has_trait_ref = False # H7 是否引用业态说明 + light = None + tracking_has_action = False + for ins in parsed: + content = (ins.get('content') or '') if isinstance(ins, dict) else '' + seq = ins.get('seq') if isinstance(ins, dict) else None + if '对比口径' in content or '同天数对齐' in content or re.search(r'当期\s*\d+\s*天', content): + has_align = True + if '业态说明' in content or '业态特征' in content or '业态' in content: + has_trait_ref = True + # 趋势词检查 + trend_words = re.findall(r'(下滑|下降|上升|提升|收缩|萎缩|承压|走弱|走强|加剧|恶化|显著|大幅)', content) + if trend_words: + sentences = re.split(r'[;。;]', content) + for s in sentences: + if any(w in s for w in trend_words) and not re.search(r'[-+]?\d+(?:\.\d+)?\s*(?:%|元)', s): + has_number_in_trend = False + break + # H3: 行业警戒线类编造 + if re.search(r'行业(均值|警戒线|标准)\s*\d', content): + has_industry_fake = True + if seq == 11: + light = classify_light(content) + if seq == 12: + if re.search(r'(每(周|日|月)|双周)', content) and re.search(r'(启动|暂停|停用|核查|调整|触发|输出|排查)', content): + tracking_has_action = True + return { + 'count': count, + 'valid': count == 12, + 'h1_align_caliber': has_align, + 'h2_no_bare_trend': has_number_in_trend, + 'h3_no_fake_industry': not has_industry_fake, + 'h7_trait_ref': has_trait_ref, + 'seq11_light': light, + 'seq12_has_action': tracking_has_action, + } + + +async def run_one(client, cfg, area: str, round_idx: int) -> dict: + prompt = await build_app2a_area_prompt({ + 'site_id': SITE_ID, + 'time_dimension': TIME_DIMENSION, + 'area': area, + }) + t0 = time.monotonic() + try: + parsed, tokens, _ = await client.call_app( + app_id=cfg.app_id_2a_finance_area, + prompt=prompt, + ) + dt = time.monotonic() - t0 + ok = True + error = None + except Exception as e: + dt = time.monotonic() - t0 + parsed = None + tokens = 0 + ok = False + error = f'{type(e).__name__}: {e}' + + return { + 'area': area, + 'round_idx': round_idx, + 'ok': ok, + 'duration_s': round(dt, 2), + 'tokens': tokens, + 'prompt_len': len(prompt), + 'parsed': parsed, + 'error': error, + 'analysis': analyze(parsed) if parsed else None, + } + + +async def main(): + cfg = AIConfig.from_env() + client = DashScopeClient(api_key=cfg.api_key, workspace_id=cfg.workspace_id) + + OUT_DIR.mkdir(parents=True, exist_ok=True) + + total = len(AREAS) * ROUNDS_PER_AREA + results = [] + done = 0 + for area in AREAS: + for i in range(1, ROUNDS_PER_AREA + 1): + done += 1 + print(f'[{done:>2}/{total}] {area:8s} round {i} ...', flush=True) + r = await run_one(client, cfg, area, i) + out_file = OUT_DIR / f'{area}_round{i}.json' + with open(out_file, 'w', encoding='utf-8') as f: + json.dump(r, f, ensure_ascii=False, indent=2, default=str) + status = 'OK' if r['ok'] else f"FAIL ({r['error']})" + analysis = r.get('analysis') or {} + print(f' {status} · {r["duration_s"]}s · tokens={r["tokens"]} · 12 条={analysis.get("valid")} · 三色={analysis.get("seq11_light")}') + results.append(r) + + # 汇总报告 + success = [r for r in results if r['ok']] + summary = { + 'total': total, + 'success': len(success), + 'failed': total - len(success), + 'avg_duration_s': round(sum(r['duration_s'] for r in success) / max(len(success), 1), 2), + 'avg_tokens': int(sum(r['tokens'] for r in success) / max(len(success), 1)), + 'by_area': {}, + 'hard_constraints_pass_rate': {}, + } + # 按区域分组 + for area in AREAS: + area_results = [r for r in results if r['area'] == area] + ok_rounds = [r for r in area_results if r['ok']] + valid_12 = sum(1 for r in ok_rounds if (r.get('analysis') or {}).get('valid')) + lights = [(r.get('analysis') or {}).get('seq11_light') for r in ok_rounds] + summary['by_area'][area] = { + 'rounds': len(area_results), + 'ok': len(ok_rounds), + 'valid_12_pct': round(100 * valid_12 / max(len(area_results), 1), 1), + 'light_distribution': {lg: lights.count(lg) for lg in ('red', 'yellow', 'green', 'unknown')}, + } + # 硬约束通过率(按 H1/H2/H3/H7/seq12 分别) + def pass_rate(key: str) -> float: + ok_rounds = [r for r in success if r.get('analysis')] + if not ok_rounds: + return 0.0 + hits = sum(1 for r in ok_rounds if (r.get('analysis') or {}).get(key)) + return round(100 * hits / len(ok_rounds), 1) + summary['hard_constraints_pass_rate'] = { + 'H1 对比口径显式': pass_rate('h1_align_caliber'), + 'H2 无裸趋势词': pass_rate('h2_no_bare_trend'), + 'H3 无编造行业数字': pass_rate('h3_no_fake_industry'), + 'H7 引用业态特征': pass_rate('h7_trait_ref'), + 'seq12 含触发动作': pass_rate('seq12_has_action'), + } + + with open(OUT_DIR / 'summary.json', 'w', encoding='utf-8') as f: + json.dump(summary, f, ensure_ascii=False, indent=2, default=str) + + print() + print('=' * 60) + print(f'采样完成:{summary["success"]}/{summary["total"]} 成功') + print(f'平均耗时:{summary["avg_duration_s"]}s · 平均 tokens:{summary["avg_tokens"]}') + print() + print('硬约束通过率:') + for k, v in summary['hard_constraints_pass_rate'].items(): + print(f' {k}: {v}%') + print() + print('各业态 valid_12 率:') + for area, info in summary['by_area'].items(): + print(f' {area:8s}: {info["valid_12_pct"]}% · 灯色 {info["light_distribution"]}') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/scripts/ab_to_cache.py b/scripts/ab_to_cache.py new file mode 100644 index 0000000..c57d0e6 --- /dev/null +++ b/scripts/ab_to_cache.py @@ -0,0 +1,81 @@ +"""把 A/B 采样结果(export/ai-ab-test/round_v1_app2a_area/*.json)直接写入 ai_cache。 + +用途:E4 小程序 E2E 验证时,不重复消耗百炼预算即可填充 app2a 缓存。 + +用法: + # 默认:每业态取 round1 结果,共 8 个组合写入 cache + PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe scripts/ab_to_cache.py + + # 只写某个业态: + PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe scripts/ab_to_cache.py --area vip +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +sys.path.insert(0, 'apps/backend') +from dotenv import load_dotenv +load_dotenv() + +from app.ai.cache_service import AICacheService +from app.ai.schemas import CacheTypeEnum + + +SITE_ID = 2790685415443269 +TIME_DIMENSION = 'this_month' +AB_DIR = Path('export/ai-ab-test/round_v1_app2a_area') +AREAS = ('hall', 'hallA', 'hallB', 'hallC', 'vip', 'snooker', 'mahjong', 'ktv') + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--area', type=str, default=None, help='只写单个业态,不指定则全部') + ap.add_argument('--round', type=int, default=1, help='使用第几轮结果(默认 round 1)') + args = ap.parse_args() + + target_areas = [args.area] if args.area else list(AREAS) + cache_svc = AICacheService() + ok = 0 + skipped = 0 + + for area in target_areas: + path = AB_DIR / f'{area}_round{args.round}.json' + if not path.exists(): + print(f'[SKIP] {area}: 文件不存在({path}),可能 A/B 尚未跑到此业态') + skipped += 1 + continue + with open(path, 'r', encoding='utf-8') as f: + result = json.load(f) + parsed = result.get('parsed') + if not isinstance(parsed, dict) and not isinstance(parsed, list): + print(f'[SKIP] {area}: parsed 字段异常') + skipped += 1 + continue + # 标准化:result_json 应为 {insights: [...]} 或直接 [...] + if isinstance(parsed, list): + result_json = {'insights': parsed} + else: + result_json = parsed + + target_id = f'{TIME_DIMENSION}__{area}' + cache_svc.write_cache( + cache_type=CacheTypeEnum.APP2A_FINANCE_AREA.value, + site_id=SITE_ID, + target_id=target_id, + result_json=result_json, + triggered_by='ab_replay', + score=None, + ) + ok += 1 + print(f'[OK] 写入 app2a_finance_area · {target_id} · {len(result_json.get("insights", []))} 条') + + print() + print(f'=== 完成:{ok} 个写入 · {skipped} 个跳过 ===') + + +if __name__ == '__main__': + main()