diff --git a/.claude/commands/pre-change.md b/.claude/commands/pre-change.md new file mode 100644 index 0000000..59c2a9f --- /dev/null +++ b/.claude/commands/pre-change.md @@ -0,0 +1,65 @@ +# /pre-change — 逻辑改动前置调研 + +对即将修改的模块进行全面调研,输出上下文摘要供用户确认后再动手。 + +## 适用场景 + +任何逻辑改动(ETL/业务规则/API/数据模型/前端交互),写代码前执行。 + +## 执行步骤 + +### 第 1 步:识别改动范围 + +从用户需求中提取: +- 要修改的模块和文件 +- 涉及的数据表/API/页面 +- 预期的行为变化 + +### 第 2 步:委托 Explore 子代理调研 + +启动 Explore 子代理(thoroughness: very thorough),调研以下内容: + +1. **目标模块文件**:读取要修改的文件及其直接依赖 +2. **历史审计**:搜索 `docs/audit/changes/` 中相关模块的历史变更记录 +3. **相关文档**:README、PRD(`docs/prd/`)、BD 手册(`docs/database/`)、API 参考 +4. **调用关系**:要修改文件的调用方和被调用方 +5. **数据流向**:上游(数据从哪来)→ 当前模块 → 下游(数据到哪去) +6. **影响范围**:哪些模块/页面/任务可能受影响 + +### 第 3 步:输出「改动前上下文摘要」 + +格式: + +``` +## 改动前上下文摘要 + +### 模块职责 +<模块做什么,在系统中的角色> + +### 历史变更 +<近期审计记录中的相关改动,特别是踩坑记录> + +### 数据流向 +上游: <数据来源> +当前: <本模块处理> +下游: <消费方> + +### 影响范围 +- <受影响的模块/页面/任务列表> + +### 风险点 +- <可能的副作用、边界条件、兼容性问题> + +### 建议方案 +<基于调研结果的实施建议> +``` + +### 第 4 步:等待用户确认 + +输出摘要后,等待用户确认或调整方向,确认后再进入编码实施。 + +## 例外(无需执行此流程) + +- 纯格式调整、注释/文档纯文字修改 +- 用户明确说"直接改/跳过调研" +- 新建文件且不涉及已有逻辑 diff --git a/.claude/commands/spec-close.md b/.claude/commands/spec-close.md new file mode 100644 index 0000000..7042b3e --- /dev/null +++ b/.claude/commands/spec-close.md @@ -0,0 +1,63 @@ +# /spec-close — Spec 收尾通用流程 + +当一个功能 spec 开发完成时,执行此收尾检查清单确保质量闭环。 + +## 执行步骤 + +### 步骤 1:最终测试检查点(必选) + +- 运行 Monorepo 属性测试:`cd /c/NeoZQYY && pytest tests/ -v` +- 运行模块单元测试:`cd <模块路径> && pytest tests/ -v` +- 确保所有测试通过,有问题询问用户 + +### 步骤 2:前后端联调验证(涉及 API + 前端时必选) + +- 启动后端服务,使用测试库验证各端点完整请求-响应链路 +- 验证 JSON 响应结构与 Schema 定义一致(camelCase 序列化) +- 验证权限校验和数据隔离(`SET LOCAL app.current_site_id`)在真实请求中生效 +- 前端联调验证:确认前端页面能正确调用 API 并渲染数据 +- 验证空数据/降级场景下前端不崩溃 + +### 步骤 3:数据库变更审计与 DDL 合并(涉及 DB 改动时必选) + +- 审计本次实现中对数据库的所有改动(新建表、新增字段、新增索引、FDW 映射变更等) +- **必须通过 pg MCP 工具实际执行迁移 SQL**(禁止仅标记完成而不执行) +- 执行后用查询验证表/字段/索引已正确创建 +- RLS 视图双 schema:后端查询 `app.v_*` 视图,新建 DWS RLS 视图时必须同时在原 schema 和 `app` schema 下创建 +- 合并到主 DDL 基线文件(ETL → `docs/database/ddl/etl_feiqiu__.sql`,业务 → `docs/database/ddl/zqyy_app__.sql`) +- 编写回滚脚本(逆序 DROP/ALTER) + +### 步骤 4:BD 手册更新(涉及 DB 改动时必选) + +- 业务库 → `docs/database/BD_manual_*.md` +- ETL 库 → `apps/etl/connectors/feiqiu/docs/database/<层级>/main/BD_manual_*.md` +- FDW → `docs/database/BD_manual_fdw*.md` +- 每份手册必须包含:字段明细、约束与索引、验证 SQL(≥3 条)、兼容性影响、回滚策略 + +### 步骤 5:项目文档同步更新(按涉及范围裁剪) + +根据改动类型选择需要更新的文档: + +| 文档 | 更新条件 | +|------|----------| +| 模块 README | 模块内部结构变更时 | +| `apps/backend/docs/API-REFERENCE.md` | 新增/修改后端路由时 | +| `docs/contracts/openapi/backend-api.json` | 新增/修改 API 端点时 | +| `docs/DOCUMENTATION-MAP.md` | 新增任何文档条目时 | + +### 步骤 6:变更审计收口(涉及高风险路径时必选) + +执行 `/audit` 命令完成审计流程。 + +### 步骤 7:服务清理(启动了运行时服务时必选) + +- 关闭浏览器实例、停止后端和前端服务、清理资源 + +## 按 Spec 类型裁剪 + +| 类型 | 必选步骤 | +|------|---------| +| ETL 类(ODS/DWD/DWS) | 1, 3, 4, 5, 6 | +| 后端 API 类 | 1, 2, 5, 6 | +| 全栈类(前后端 + DB) | 1, 2, 3, 4, 5, 6 | +| 重构类 | 1, 5, 6 | diff --git a/.kiro/specs/01-miniapp-db-foundation/.config.kiro b/.kiro/specs/01-miniapp-db-foundation/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/01-miniapp-db-foundation/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/02-etl-dws-miniapp-extensions/.config.kiro b/.kiro/specs/02-etl-dws-miniapp-extensions/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/02-etl-dws-miniapp-extensions/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/03-miniapp-auth-system/.config.kiro b/.kiro/specs/03-miniapp-auth-system/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/03-miniapp-auth-system/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/04-miniapp-core-business/.config.kiro b/.kiro/specs/04-miniapp-core-business/.config.kiro deleted file mode 100644 index 4c04556..0000000 --- a/.kiro/specs/04-miniapp-core-business/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "27029642-a405-4932-8c22-5bc54fad5173", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/05-miniapp-ai-integration/.config.kiro b/.kiro/specs/05-miniapp-ai-integration/.config.kiro deleted file mode 100644 index e7b3c7c..0000000 --- a/.kiro/specs/05-miniapp-ai-integration/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "cf5c24d6-ec72-4c49-8650-264ef414e10e", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/[ETL]-fullstack-integration/.config.kiro b/.kiro/specs/[ETL]-fullstack-integration/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/[ETL]-fullstack-integration/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/admin-web-console/.config.kiro b/.kiro/specs/admin-web-console/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/admin-web-console/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/assistant-abolish-cleanup/.config.kiro b/.kiro/specs/assistant-abolish-cleanup/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/assistant-abolish-cleanup/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/business-day-cutoff/.config.kiro b/.kiro/specs/business-day-cutoff/.config.kiro deleted file mode 100644 index e19d9d7..0000000 --- a/.kiro/specs/business-day-cutoff/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "98a585de-82d9-4bbd-bed8-179208c12f8b", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/dataflow-field-completion/.config.kiro b/.kiro/specs/dataflow-field-completion/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/dataflow-field-completion/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/dataflow-structure-audit/.config.kiro b/.kiro/specs/dataflow-structure-audit/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/dataflow-structure-audit/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/dwd-business-panorama/.config.kiro b/.kiro/specs/dwd-business-panorama/.config.kiro deleted file mode 100644 index d0435f5..0000000 --- a/.kiro/specs/dwd-business-panorama/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "7e1dc63d-3dbd-4462-a43c-9ecaa9b1dd07", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/dwd-phase1-refactor/.config.kiro b/.kiro/specs/dwd-phase1-refactor/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/dwd-phase1-refactor/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/etl-coupon-detail/.config.kiro b/.kiro/specs/etl-coupon-detail/.config.kiro deleted file mode 100644 index d0ed1f2..0000000 --- a/.kiro/specs/etl-coupon-detail/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "cd79656c-9c23-4470-a147-d402b5f4b50b", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/etl-dws-flow-refactor/.config.kiro b/.kiro/specs/etl-dws-flow-refactor/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/etl-dws-flow-refactor/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/etl-pipeline-debug/.config.kiro b/.kiro/specs/etl-pipeline-debug/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/etl-pipeline-debug/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/etl-staff-dimension/.config.kiro b/.kiro/specs/etl-staff-dimension/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/etl-staff-dimension/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/etl-unified-pipeline/.config.kiro b/.kiro/specs/etl-unified-pipeline/.config.kiro deleted file mode 100644 index c4bf4fb..0000000 --- a/.kiro/specs/etl-unified-pipeline/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "a277a91a-b35c-4d48-b4a2-09df0e47b71b", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/gift-card-breakdown/.config.kiro b/.kiro/specs/gift-card-breakdown/.config.kiro deleted file mode 100644 index a5f0f1d..0000000 --- a/.kiro/specs/gift-card-breakdown/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "4b6736e7-40fc-40a9-82f7-809f80253fe2", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/h5-miniprogram-migration-subsequent/.config.kiro b/.kiro/specs/h5-miniprogram-migration-subsequent/.config.kiro deleted file mode 100644 index b702f79..0000000 --- a/.kiro/specs/h5-miniprogram-migration-subsequent/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "cd30e87b-ce7a-4ff5-8587-f5ae75013e58", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/h5-miniprogram-migration/.config.kiro b/.kiro/specs/h5-miniprogram-migration/.config.kiro deleted file mode 100644 index b702f79..0000000 --- a/.kiro/specs/h5-miniprogram-migration/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "cd30e87b-ce7a-4ff5-8587-f5ae75013e58", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/ods-dedup-standardize/.config.kiro b/.kiro/specs/ods-dedup-standardize/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/ods-dedup-standardize/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/p4-prerequisite-fixes/.config.kiro b/.kiro/specs/p4-prerequisite-fixes/.config.kiro deleted file mode 100644 index 151d8f3..0000000 --- a/.kiro/specs/p4-prerequisite-fixes/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "a7c3e1f2-9b84-4d6e-b5a1-3f8c2d7e9a04", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/rns1-board-apis/.config.kiro b/.kiro/specs/rns1-board-apis/.config.kiro deleted file mode 100644 index 0c7df12..0000000 --- a/.kiro/specs/rns1-board-apis/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "a3f7c2d1-8e4b-4f6a-9c5d-2b1e8f3a7d9c", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/rns1-chat-integration/.config.kiro b/.kiro/specs/rns1-chat-integration/.config.kiro deleted file mode 100644 index 19aa9dd..0000000 --- a/.kiro/specs/rns1-chat-integration/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "9eccc890-b6c3-41a3-8ba1-bb2f0e09f653", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/rns1-customer-coach-api/.config.kiro b/.kiro/specs/rns1-customer-coach-api/.config.kiro deleted file mode 100644 index a42a694..0000000 --- a/.kiro/specs/rns1-customer-coach-api/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "b2f4e8a1-3c7d-4f9b-a6e2-8d5c1b3f7a9e", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/rns1-infra-contract-rewrite/.config.kiro b/.kiro/specs/rns1-infra-contract-rewrite/.config.kiro deleted file mode 100644 index b4ede0d..0000000 --- a/.kiro/specs/rns1-infra-contract-rewrite/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "13cfd0bc-b6d6-408e-b943-aa11fb515478", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/rns1-task-performance-api/.config.kiro b/.kiro/specs/rns1-task-performance-api/.config.kiro deleted file mode 100644 index cbc72cf..0000000 --- a/.kiro/specs/rns1-task-performance-api/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "a7e3c1d4-8f2b-4e6a-b5d9-3c1f7a2e8b4d", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/spi-spending-power-index/.config.kiro b/.kiro/specs/spi-spending-power-index/.config.kiro deleted file mode 100644 index d30049b..0000000 --- a/.kiro/specs/spi-spending-power-index/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"generationMode": "requirements-first"} \ No newline at end of file diff --git a/.kiro/specs/tenant-admin-web/.config.kiro b/.kiro/specs/tenant-admin-web/.config.kiro deleted file mode 100644 index 19aa9dd..0000000 --- a/.kiro/specs/tenant-admin-web/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "9eccc890-b6c3-41a3-8ba1-bb2f0e09f653", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 91e49a2..3ba66e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,21 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +本文件为 Claude Code (claude.ai/code) 在本仓库工作时的指导规范。 + +## 语言(强制) + +始终使用中文,覆盖所有场景: + +- 对话回复、解释、提问、状态更新 → 中文 +- 代码注释 → 中文 +- Git commit message → 中文描述 + 英文 Co-Authored-By 签名行 +- PR 标题与正文 → 中文 +- 审计记录、文档、变更说明 → 中文 +- 错误提示、日志说明 → 中文 +- 变量名/函数名/类名 → 保持英文(编程惯例) +- 第三方 API 字段名、CLI 命令 → 保持原文 + +禁止在回复中使用英文段落或英文标题(技术术语、代码片段、专有名词内嵌除外)。 ## 项目概览 @@ -18,6 +33,8 @@ NeoZQYY Monorepo — 面向台球门店业务的全栈数据平台。多门店 | `apps/mcp-server/` | MCP Server(PostgreSQL 只读,AI 工具集成) | | `packages/shared/` | 跨项目共享包(enums, money, datetime_utils) | | `db/` | DDL / 迁移 / 种子数据 | +| `tools/` | 通用工具(db/reporting/health/h5-to-mp-checker) | +| `scripts/ops/` | 日常运维脚本(ETL 监控、数据回填、导出等) | ### 两个管理后台的区别 @@ -74,6 +91,8 @@ cd apps/miniprogram && npm test # 小程序 Jest ### ETL +> 完整规则见 `apps/etl/connectors/feiqiu/CLAUDE.md` + - 任务模式:继承 `BaseTask`(Extract → Transform → Load),在 `orchestration/task_registry.py` 注册 - 加载器模式:每张目标表一个 Loader,`upsert()` + 冲突处理 - SCD2 处理:`scd/` 模块 @@ -81,11 +100,12 @@ cd apps/miniprogram && npm test # 小程序 Jest ### 后端 -- 全局响应包装:`ResponseWrapperMiddleware` 把所有 2xx 响应包为 `{ "code": 0, "data": }` -- `CamelModel` 基类:snake_case → camelCase 自动转换(小程序 API 用) -- JWT 双认证:用户名密码(admin)+ 微信 code(小程序);待审核用户有 limited token -- AI 集成:8 个千问应用通过 DashScope SDK(chat/finance/clue/analysis/tactics/note/customer/consolidate),带熔断、限流、预算追踪 -- 后台服务(lifespan):`TaskQueue`(按 site_id 消费)、`Scheduler`(读 scheduled_tasks 自动入队)、4 个触发器 +> 完整规则见 `apps/backend/CLAUDE.md` + +- 全局响应包装:`ResponseWrapperMiddleware` → `{ "code": 0, "data": }` +- JWT 双认证:admin(用户名密码)+ miniapp(微信 code)+ tenant-admin(用户名密码) +- AI 集成:8 个千问应用(DashScope SDK),带熔断、限流、预算追踪 +- 后台服务:TaskQueue + Scheduler + 4 个触发器 ## 文件归属规则 @@ -96,9 +116,11 @@ cd apps/miniprogram && npm test # 小程序 Jest | 只验证本模块逻辑的测试 | 模块内 `tests/` | | 守护 monorepo 结构/约定的测试 | 根 `tests/` | | 只操作本模块数据的脚本 | 模块内 `scripts/` | -| 运维/全局工具脚本 | 根 `scripts/` | +| 日常运维脚本(回填/导出/种子/初始化) | `scripts/ops/` | +| 可复用通用工具(健康检查/数据库/报表/分析) | `tools/`(按类型分子目录) | | 审计记录(任何模块的变更) | 根 `docs/audit/` — 禁止写入子模块 | | 数据库文档(全局 schema 视角) | 根 `docs/database/` | +| 归档/待删除内容 | `_DEL/`(保持原路径结构,用户定期手动清理) | 审计产物路径: - 变更记录:`docs/audit/changes/__.md` @@ -106,38 +128,12 @@ cd apps/miniprogram && npm test # 小程序 Jest ## 飞球数据规范 -权威文档:`docs/reports/DWD-DOC/`(DWD 12 条)+ 同目录 DWS 权威规范。与 BD 手册、ETL 文档、DDL 注释冲突时以 DWD-DOC 为准。 +> 完整规则见 `apps/etl/connectors/feiqiu/CLAUDE.md`(进入 ETL 目录时自动加载)。 -### 硬规则速查 - -1. `consume_money` 禁止直接计算 → 用 `items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money` -2. 助教费用拆分:`assistant_pd_money`(陪打)+ `assistant_cx_money`(超休),禁止用 `service_fee` / `ASSISTANT_BASE` / `ASSISTANT_BONUS` -3. 支付恒等式:`balance_amount = recharge_card_amount + gift_card_amount`,三者不可重复计算 -4. `settle_type` 过滤:正向交易 `IN (1, 3)`,本表无 `is_delete` 字段 -5. 会员信息通过 `member_id` JOIN 维度表(`scd2_is_current=1`),结算单冗余字段不可靠(DQ-6/DQ-7) -6. 支付方式拆分来源是 `dwd_payment` 表(DQ-8):`payment_method=2` 现金,`payment_method=4` 扫码。`dwd_settlement_head_ex.cash_amount/online_amount` 不可靠 -7. 散客:`member_id ≤ 0`,全链路过滤入口加 `member_id > 0` -8. 课程类型/定价/绩效档位/奖金/指数权重 → 配置表读取,禁止硬编码 -9. DWS 汇总表 delete-before-insert,库存表 upsert -10. 折扣互斥:`discount_manual` + `discount_other` = `adjust_amount` -11. 现金流互斥:`platform_settlement_amount` 与 `groupbuy_pay_amount` 互斥 -12. 废单判断:`dwd_assistant_service_log_ex.is_trash` - -### 取数优先级 - -DWS > DWD(明细+维度表)> 禁止 ODS(API 快照表,同一 id 有 100+ 行重复,JOIN 会行膨胀) - -### 参考优先级 - -DWD-DOC > DWS 权威规范 > BD 手册 > ETL 任务文档 > 业务规则文档 > DDL 注释 - -## 废弃对象黑名单(禁止引用) - -- `dwd_assistant_trash_event` / `_ex`(2026-02-22 DROP)→ 用 `dwd_assistant_service_log_ex.is_trash` -- `ods.assistant_cancellation_records`(2026-02-22)→ 无需独立链路 -- `ODS_ASSISTANT_ABOLISH` / `ASSISTANT_ABOLISH`(2026-02-22)→ 无 -- `BILLIARD_VIP`(2026-03-07)→ V1-V4 归 `BILLIARD`,V5 归 `SNOOKER` -- `dws_member_recall_index` / `v_dws_member_recall_index`(2026-03-20)→ WBI + NCI +**核心速查**: +- `consume_money` 禁止直接计算 → 用 `items_sum` 拆分字段 +- 取数优先级:DWS > DWD > 禁止 ODS +- 参考优先级:DWD-DOC > DWS 权威规范 > BD 手册 > ETL 任务文档 > DDL 注释 - 所有 `_archived/` 目录禁止读取或参考 ## 编码前需求审问(强制) diff --git a/apps/backend/CLAUDE.md b/apps/backend/CLAUDE.md new file mode 100644 index 0000000..e6cd449 --- /dev/null +++ b/apps/backend/CLAUDE.md @@ -0,0 +1,52 @@ +# CLAUDE.md — Backend (FastAPI) + +进入本目录时自动加载。 + +## 架构模式 + +### 全局响应包装 + +`ResponseWrapperMiddleware` 把所有 2xx 响应包为 `{ "code": 0, "data": }`。 +非 2xx 响应保持原样。前端统一通过 `response.data` 解包。 + +### 序列化 + +`CamelModel` 基类:snake_case → camelCase 自动转换(小程序 API 用)。 +后端代码始终用 snake_case,JSON 输出自动转驼峰。 + +### JWT 双认证 + +| 认证方式 | 用途 | 表 | JWT aud | +|---------|------|-----|---------| +| 用户名+密码 | admin-web 登录 | `auth.admin_users` | `admin` | +| 微信 code | 小程序登录 | `auth.users` | `miniapp` | +| 用户名+密码 | tenant-admin 登录 | `auth.tenant_admins` | `tenant-admin` | + +待审核用户有 limited token(仅可访问审核状态接口)。 + +### AI 集成 + +8 个千问应用通过 DashScope SDK: +chat / finance / clue / analysis / tactics / note / customer / consolidate + +特性:熔断(连续失败自动断路)、限流(每分钟/每日)、预算追踪、对话缓存。 + +### 后台服务(lifespan) + +- `TaskQueue`:按 site_id 消费,FIFO 队列 +- `Scheduler`:读 `meta.scheduled_tasks` 自动入队 +- 4 个触发器:日结/月结/工资/关系指数 + +### 数据库访问 + +- 业务库通过 `APP_DB_DSN` 直连 `zqyy_app` +- ETL 数据通过 FDW 映射的 `app.v_*` RLS 视图访问 +- 查询前必须 `SET LOCAL app.current_site_id = :site_id` + +## 测试 + +```bash +cd apps/backend && pytest tests/ -v +``` + +使用测试库(`TEST_APP_DB_DSN`),禁止连正式库。 diff --git a/apps/etl/connectors/feiqiu/docs/database/DWS/config/BD_manual_cfg_skill_type.md b/apps/etl/connectors/feiqiu/docs/database/DWS/config/BD_manual_cfg_skill_type.md new file mode 100644 index 0000000..faa1a6e --- /dev/null +++ b/apps/etl/connectors/feiqiu/docs/database/DWS/config/BD_manual_cfg_skill_type.md @@ -0,0 +1,60 @@ +# cfg_skill_type 课程类型配置表 + +> 生成时间:2026-03-24 + +## 表信息 + +| 属性 | 值 | +|------|-----| +| Schema | dws | +| 表名 | cfg_skill_type | +| 主键 | skill_type_id(自增) | +| 唯一键 | skill_id | +| 数据来源 | 手工维护,对应飞球系统的课程/技能类型 | +| 更新频率 | 按需(新增课程类型时) | +| 说明 | 将飞球系统的 skill_id 映射到业务课程分类(BASE/BONUS),供 ETL 到店判定、工资计算、绩效统计使用 | + +## 字段说明 + +| 序号 | 字段名 | 类型 | 可空 | 说明 | +|------|--------|------|------|------| +| 1 | skill_type_id | SERIAL | NO | 自增主键 | +| 2 | skill_id | BIGINT | NO | 飞球系统技能 ID(唯一) | +| 3 | skill_name | VARCHAR | YES | 技能名称(来自飞球系统) | +| 4 | course_type_code | VARCHAR | NO | 课程分类代码:BASE(基础课)/ BONUS(附加课/超休/激励课) | +| 5 | course_type_name | VARCHAR | NO | 课程分类中文名 | +| 6 | is_active | BOOLEAN | NO | 是否启用(默认 true) | +| 7 | description | TEXT | YES | 备注说明 | +| 8 | created_at | TIMESTAMPTZ | NO | 创建时间 | +| 9 | updated_at | TIMESTAMPTZ | NO | 更新时间 | + +## 当前数据(截至 2026-03-24) + +| skill_id | skill_name | course_type_code | 来源说明 | +|----------|-----------|-----------------|---------| +| 2790683529513797 | 基础课 | BASE | 飞球系统原始课程类型(2026-03-24 补录) | +| 2790683529513798 | 附加课 | BONUS | 飞球系统原始课程类型(2026-03-24 补录) | +| 2791903611396869 | 台球基础陪打 | BASE | 初始种子数据 | +| 2807440316432197 | 台球超休服务 | BONUS | 初始种子数据 | +| 2807440316432198 | 包厢服务 | BASE | 初始种子数据 | +| 3039912271463941 | 包厢课 | BASE | 飞球系统原始课程类型(2026-03-24 补录) | + +## 业务口径 + +- `course_type_code = 'BONUS'` 用于 WBI/NCI 到店判定:settle_type=3 的商城订单,仅当关联了 BONUS 类型的助教服务记录时才算"到店" +- `course_type_code = 'BASE'` 用于基础课工资计算(按助教等级计价) +- `course_type_code = 'BONUS'` 用于附加课工资计算(固定 190 元/小时) + +## 下游依赖 + +| 消费方 | 用途 | +|--------|------| +| `member_index_base._build_visit_condition_sql()` | WBI/NCI 到店判定 | +| `index_verifier.visit_members` CTE | 指数验证器到店范围 | +| 助教工资计算任务 | 区分基础课/附加课计价 | +| 助教绩效统计 | 按课程类型分类统计服务时长 | + +## 维护注意事项 + +- 飞球系统新增课程类型时,必须同步在此表补录,否则相关订单会被 WBI 到店判定漏掉 +- 2026-03-24 发现 3 条缺失记录导致 113 名会员、3766 条服务记录的到店判定失效 diff --git a/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_cfg_area_category.md b/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_cfg_area_category.md index 55c3f68..16d1aaf 100644 --- a/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_cfg_area_category.md +++ b/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_cfg_area_category.md @@ -1,6 +1,6 @@ # cfg_area_category 台区分类映射表 -> 生成时间:2026-02-03 | 更新时间:2026-03-09 +> 生成时间:2026-02-03 | 更新时间:2026-03-20 ## 表信息 @@ -30,6 +30,7 @@ | 11 | description | TEXT | YES | | 说明 | | 12 | created_at | TIMESTAMPTZ | NO | | 创建时间 | | 13 | updated_at | TIMESTAMPTZ | NO | | 更新时间 | +| 14 | sort_order | INTEGER | NO | | 前端筛选器显示排序(值越小越靠前),DEFAULT 100 | ## 变更说明(2026-03-09) @@ -108,6 +109,7 @@ | 2026-02-03 | 初始创建,区域级精确 + LIKE 模糊匹配 | | 2026-03-07 | 新增 source_table_name 支持台桌级细分;废弃 BILLIARD_VIP | | 2026-03-09 | 改为纯台桌级精确映射,删除所有 LIKE 和区域级映射 | +| 2026-03-20 | 新增 sort_order 字段,控制前端筛选器分类显示排序 | ## 验证 SQL diff --git a/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_area_daily.md b/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_area_daily.md new file mode 100644 index 0000000..0699b36 --- /dev/null +++ b/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_area_daily.md @@ -0,0 +1,159 @@ +# dws_finance_area_daily 区域日粒度财务原子层表 + +> 生成时间:2026-03-28 +> 关联 SPEC:board-finance-dws-area-refactor + +## 表信息 + +| 属性 | 值 | +|------|-----| +| Schema | dws | +| 表名 | dws_finance_area_daily | +| 主键 | id | +| 唯一键 | (site_id, stat_date, area_code) | +| 数据来源 | dwd_settlement_head + dim_table + dws_finance_daily_summary | +| 更新频率 | 每小时更新当日数据 | +| 幂等策略 | delete-before-insert(按 site_id + stat_date 删除后插入 9 行) | +| 说明 | 按 (site_id, stat_date, area_code) 粒度存储 9 个区域的收入/优惠/现金流预计算数据 | + +## 字段说明 + +| 序号 | 字段名 | 类型 | 可空 | 说明 | +|------|--------|------|------|------| +| 1 | id | BIGSERIAL | NO | 自增主键 | +| 2 | site_id | BIGINT | NO | 门店ID | +| 3 | tenant_id | BIGINT | NO | 租户ID | +| 4 | stat_date | DATE | NO | 统计日期(营业日,按 BUSINESS_DAY_START_HOUR=8 切点) | +| 5 | area_code | VARCHAR(20) | NO | 区域编码:all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv | +| 6 | table_fee_amount | NUMERIC(14,2) | NO | 台费正价 | +| 7 | goods_amount | NUMERIC(14,2) | NO | 商品正价 | +| 8 | assistant_pd_amount | NUMERIC(14,2) | NO | 助教基础课正价(陪打) | +| 9 | assistant_cx_amount | NUMERIC(14,2) | NO | 助教激励课正价(超休) | +| 10 | gross_amount | NUMERIC(14,2) | NO | 毛收入 = 四项之和 | +| 11 | discount_groupbuy | NUMERIC(14,2) | NO | 团购优惠 | +| 12 | discount_vip | NUMERIC(14,2) | NO | 会员折扣 | +| 13 | discount_manual | NUMERIC(14,2) | NO | 手动调整(adjust_amount) | +| 14 | discount_gift_card | NUMERIC(14,2) | NO | 赠送卡消费金额口径 | +| 15 | discount_rounding | NUMERIC(14,2) | NO | 抹零 | +| 16 | discount_other | NUMERIC(14,2) | NO | 其他优惠 | +| 17 | discount_total | NUMERIC(14,2) | NO | 优惠合计 = 六项之和 | +| 18 | confirmed_income | NUMERIC(14,2) | NO | 确认收入 = gross_amount - discount_total | +| 19 | cash_pay_amount | NUMERIC(14,2) | NO | 收银实付(仅 all 行有效) | +| 20 | cash_paper_amount | NUMERIC(14,2) | NO | 纸币支付(仅 all 行有效) | +| 21 | scan_pay_amount | NUMERIC(14,2) | NO | 扫码支付(仅 all 行有效) | +| 22 | groupbuy_pay_amount | NUMERIC(14,2) | NO | 团购支付金额(仅 all 行有效) | +| 23 | recharge_cash_inflow | NUMERIC(14,2) | NO | 充值现金流入(仅 all 行有效) | +| 24 | cash_inflow_total | NUMERIC(14,2) | NO | 现金流入合计(仅 all 行有效) | +| 25 | cash_outflow_total | NUMERIC(14,2) | NO | 现金流出合计(仅 all 行有效) | +| 26 | cash_balance_change | NUMERIC(14,2) | NO | 现金余额变动(仅 all 行有效) | +| 27 | card_consume_total | NUMERIC(14,2) | NO | 卡消费合计(仅 all 行有效) | +| 28 | recharge_card_consume | NUMERIC(14,2) | NO | 充值卡消费(仅 all 行有效) | +| 29 | gift_card_consume | NUMERIC(14,2) | NO | 赠送卡消费(仅 all 行有效) | +| 30 | recharge_cash | NUMERIC(14,2) | NO | 充值现金(仅 all 行有效) | +| 31 | first_recharge_cash | NUMERIC(14,2) | NO | 首充现金(仅 all 行有效) | +| 32 | renewal_cash | NUMERIC(14,2) | NO | 续充现金(仅 all 行有效) | +| 33 | order_count | INTEGER | NO | 结账单数 | +| 34 | created_at | TIMESTAMPTZ | NO | 创建时间 | +| 35 | updated_at | TIMESTAMPTZ | NO | 更新时间 | + + +## 约束与索引 + +| 约束/索引 | 类型 | 列 | +|-----------|------|-----| +| dws_finance_area_daily_pkey | PRIMARY KEY | id | +| dws_finance_area_daily_site_id_stat_date_area_code_key | UNIQUE | (site_id, stat_date, area_code) | + +## RLS 视图 + +```sql +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; +``` + +## 数学恒等式 + +``` +gross_amount = table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount +discount_total = discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other +confirmed_income = gross_amount - discount_total +``` + +- `area_code ≠ 'all'` 时:现金流/卡消费/充值字段 = 0 +- `all` 行收入/优惠 = hallA~ktv 各行对应字段之和 +- `hall` 行 = hallA~ktv 各行之和(历史兼容) +- 每个 (site_id, stat_date) 恰好 9 行 + +## 变更原因 + +解决财务看板在 `area≠all` 时优惠数据从全局 DWS 表取数导致区域级优惠占比严重失真的 bug(如 B区优惠占比 417.9%)。 + +## 兼容性影响 + +| 组件 | 影响 | +|------|------| +| ETL 任务 | 新增 DWS_FINANCE_AREA_DAILY 任务,依赖 DWD_LOAD_FROM_ODS | +| 后端 API | board_service.py 改为从本表查询 overview/revenue 板块数据 | +| 小程序 | 无直接影响(API 签名不变) | +| dws_finance_daily_summary | 不改动,本表的 all 行现金流/充值/卡消费复用其数据 | + +## 回滚策略 + +```sql +DROP VIEW IF EXISTS dws.v_dws_finance_area_daily; +DROP TABLE IF EXISTS dws.dws_finance_area_daily; +``` + +回滚后后端需恢复到从 `dws_finance_daily_summary` 取数的旧逻辑。 + +## 验证 SQL + +```sql +-- 1. 验证表存在且有数据 +SELECT COUNT(*), COUNT(DISTINCT area_code), MIN(stat_date), MAX(stat_date) +FROM dws.dws_finance_area_daily +WHERE site_id = 1; + +-- 2. 验证每天恰好 9 行 +SELECT stat_date, COUNT(*) AS row_count +FROM dws.dws_finance_area_daily +WHERE site_id = 1 +GROUP BY stat_date +HAVING COUNT(*) != 9 +ORDER BY stat_date; + +-- 3. 验证收入恒等式 +SELECT stat_date, area_code, + gross_amount, + (table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount) AS calc_gross, + gross_amount - (table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount) AS diff +FROM dws.dws_finance_area_daily +WHERE site_id = 1 + AND gross_amount != (table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount); + +-- 4. 验证优惠恒等式 +SELECT stat_date, area_code, + discount_total, + (discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other) AS calc_disc, + discount_total - (discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other) AS diff +FROM dws.dws_finance_area_daily +WHERE site_id = 1 + AND discount_total != (discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other); + +-- 5. 验证非 all 行现金流为零 +SELECT stat_date, area_code, cash_inflow_total, cash_outflow_total +FROM dws.dws_finance_area_daily +WHERE site_id = 1 + AND area_code != 'all' + AND (cash_inflow_total != 0 OR cash_outflow_total != 0); +``` + +## 可回溯性 + +| 项目 | 说明 | +|------|------| +| 可回溯 | ✅ 完全可回溯(delete-before-insert) | +| 数据范围 | 2025-07-16 ~ 至今 | +| 依赖表 | dwd_settlement_head, dim_table, dws_finance_daily_summary | +| 回填脚本 | `scripts/ops/backfill_finance_area_daily.py` | diff --git a/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_board_cache.md b/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_board_cache.md new file mode 100644 index 0000000..33a7a8f --- /dev/null +++ b/apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_board_cache.md @@ -0,0 +1,121 @@ +# dws_finance_board_cache 看板缓存层表 + +> 生成时间:2026-03-28 +> 关联 SPEC:board-finance-dws-area-refactor + +## 表信息 + +| 属性 | 值 | +|------|-----| +| Schema | dws | +| 表名 | dws_finance_board_cache | +| 主键 | id | +| 唯一键 | (site_id, time_range, area_code) | +| 数据来源 | dws_finance_area_daily(日粒度原子层) | +| 更新频率 | 每天一次(营业日切点后) | +| 幂等策略 | ON CONFLICT (site_id, time_range, area_code) DO UPDATE | +| 说明 | 缓存已完成周期的 overview 聚合结果,避免重复 SUM 计算 | + +## 字段说明 + +| 序号 | 字段名 | 类型 | 可空 | 说明 | +|------|--------|------|------|------| +| 1 | id | BIGSERIAL | NO | 自增主键 | +| 2 | site_id | BIGINT | NO | 门店ID | +| 3 | time_range | VARCHAR(20) | NO | 时间范围:lastMonth/lastWeek/lastQuarter/quarter3/half6 | +| 4 | area_code | VARCHAR(20) | NO | 区域编码:all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv | +| 5 | start_date | DATE | NO | 当期起始日期 | +| 6 | end_date | DATE | NO | 当期结束日期 | +| 7 | prev_start_date | DATE | YES | 上期起始日期(环比用) | +| 8 | prev_end_date | DATE | YES | 上期结束日期(环比用) | +| 9 | occurrence | NUMERIC(14,2) | NO | 发生额(gross_amount 周期汇总) | +| 10 | discount | NUMERIC(14,2) | NO | 优惠合计(discount_total 周期汇总) | +| 11 | discount_rate | NUMERIC(8,4) | NO | 优惠占比 = discount / occurrence | +| 12 | confirmed_revenue | NUMERIC(14,2) | NO | 确认收入 = occurrence - discount | +| 13 | cash_in | NUMERIC(14,2) | NO | 现金流入合计(仅 area_code=all 有效) | +| 14 | cash_out | NUMERIC(14,2) | NO | 现金流出合计(仅 area_code=all 有效) | +| 15 | cash_balance | NUMERIC(14,2) | NO | 现金余额变动 = cash_in - cash_out | +| 16 | balance_rate | NUMERIC(8,4) | NO | 余额变动率 = cash_balance / cash_in | +| 17 | data_fingerprint | VARCHAR(64) | YES | 源数据 MD5 指纹,用于检测补录导致的数据变化 | +| 18 | computed_at | TIMESTAMPTZ | NO | 缓存计算时间 | +| 19 | created_at | TIMESTAMPTZ | NO | 创建时间 | +| 20 | updated_at | TIMESTAMPTZ | NO | 更新时间 | + +## 约束与索引 + +| 约束/索引 | 类型 | 列 | +|-----------|------|-----| +| dws_finance_board_cache_pkey | PRIMARY KEY | id | +| dws_finance_board_cache_site_id_time_range_area_code_key | UNIQUE | (site_id, time_range, area_code) | + +## RLS 视图 + +```sql +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; +``` + +## 缓存策略 + +- 已完成周期(缓存):lastMonth, lastWeek, lastQuarter, quarter3, half6 +- 当期周期(不缓存):month, week, quarter +- 失效条件:`data_fingerprint` 变化(补录导致源数据变化) +- 指纹算法:`MD5(sorted [(stat_date, gross_amount, discount_total), ...])` + +## 变更原因 + +为已完成周期的聚合结果提供缓存,避免每次查询都从日粒度表 SUM 计算。通过数据指纹机制自动检测补录导致的数据变化并重算。 + +## 兼容性影响 + +| 组件 | 影响 | +|------|------| +| ETL 任务 | 新增 DWS_FINANCE_BOARD_CACHE 任务,依赖 DWS_FINANCE_AREA_DAILY | +| 后端 API | board_service.py 已完成周期先查缓存,未命中从日粒度表 SUM | +| 小程序 | 无直接影响(API 签名不变) | + +## 回滚策略 + +```sql +DROP VIEW IF EXISTS dws.v_dws_finance_board_cache; +DROP TABLE IF EXISTS dws.dws_finance_board_cache; +``` + +回滚后后端需移除缓存查询逻辑,改为每次从日粒度表 SUM。 + +## 验证 SQL + +```sql +-- 1. 验证表存在且有数据 +SELECT COUNT(*), COUNT(DISTINCT time_range), COUNT(DISTINCT area_code) +FROM dws.dws_finance_board_cache +WHERE site_id = 1; + +-- 2. 验证唯一约束生效(应返回 0 行) +SELECT site_id, time_range, area_code, COUNT(*) +FROM dws.dws_finance_board_cache +GROUP BY site_id, time_range, area_code +HAVING COUNT(*) > 1; + +-- 3. 验证 confirmed_revenue = occurrence - discount +SELECT time_range, area_code, + occurrence, discount, confirmed_revenue, + (occurrence - discount) AS expected +FROM dws.dws_finance_board_cache +WHERE site_id = 1 + AND ABS(confirmed_revenue - (occurrence - discount)) > 0.01; + +-- 4. 验证当期周期不在缓存中 +SELECT * FROM dws.dws_finance_board_cache +WHERE time_range IN ('month', 'week', 'quarter'); +``` + +## 可回溯性 + +| 项目 | 说明 | +|------|------| +| 可回溯 | ✅ 完全可回溯(upsert 幂等) | +| 数据范围 | 取决于 dws_finance_area_daily 的数据范围 | +| 依赖表 | dws_finance_area_daily | +| 回填脚本 | `scripts/ops/backfill_finance_area_daily.py`(阶段 2 自动重算缓存) | diff --git a/db/README.md b/db/README.md index 08c4a1b..5a15733 100644 --- a/db/README.md +++ b/db/README.md @@ -1,11 +1,11 @@ # db/ — 数据库资产目录 -## 当前状态(2026-03-15 更新) +## 当前状态(2026-04-05 更新) -完整 DDL 基线已迁移至 `docs/database/ddl/`(按 schema 分文件,从测试库自动导出)。 -本目录保留运行时资产(迁移脚本、种子数据、FDW 配置、建库脚本)。 -2026-02-22 基线重置前的旧迁移已归档至 `_archived/`;之后的新迁移仍在 `migrations/` 中。 -DDL 基线最近一次刷新:2026-03-15(合并了截至 2026-03-09 的全部迁移)。 +完整 DDL 基线在 `docs/database/ddl/`(按 schema 分文件,从测试库自动导出)。 +本目录保留运行时资产:迁移脚本、FDW 配置、建库脚本。 + +DDL 基线最近一次刷新:**2026-04-05**(合并了截至 2026-03-31 的全部迁移)。 ## 目录结构 @@ -13,44 +13,57 @@ DDL 基线最近一次刷新:2026-03-15(合并了截至 2026-03-09 的全部 db/ ├── etl_feiqiu/ │ ├── schemas/ — 已清空(DDL 基线见 docs/database/ddl/etl_feiqiu__*.sql) -│ ├── migrations/ — 已清空(全部合并进 DDL 基线,2026-03-15) +│ ├── migrations/ — 11 个活跃迁移(2026-03-19 ~ 2026-03-31) │ ├── seeds/ — 已清空(合并进对应 DDL 文件末尾) │ └── scripts/ │ └── create_test_db.sql ├── zqyy_app/ │ ├── schemas/ — 已清空 -│ ├── migrations/ — 已清空(全部合并进 DDL 基线,2026-03-15) -│ ├── seeds/ — 已清空(合并进对应 DDL 文件末尾) +│ ├── migrations/ — 8 个活跃迁移(2026-03-20 ~ 2026-03-31)+ _archived/ +│ ├── seeds/ — 已清空 │ └── scripts/ │ └── create_test_db.sql -├── fdw/ — FDW 跨库映射配置(正式 + 测试 + 反向) -│ ├── setup_fdw.sql -│ ├── setup_fdw_test.sql -│ ├── setup_fdw_reverse.sql -│ └── setup_fdw_reverse_test.sql -└── _archived/ — 归档(旧 DDL + 基线重置前的迁移脚本,仅供历史参考) +├── fdw/ — FDW 跨库映射配置 +│ ├── setup_fdw.sql — 正向(zqyy_app → etl_feiqiu.app) +│ ├── setup_fdw_test.sql — 正向(测试环境) +│ ├── setup_fdw_reverse.sql — 反向(etl_feiqiu → zqyy_app.member_retention_clue) +│ └── setup_fdw_reverse_test.sql — 反向(测试环境) +└── _archived/ — 归档(2026-02-22 基线重置前的旧迁移,仅供历史参考) └── ddl_baseline_2026-02-22/ ``` ## DDL 基线 -新建库或 schema diff 请使用: -- `docs/database/ddl/etl_feiqiu__meta.sql` -- `docs/database/ddl/etl_feiqiu__ods.sql` -- `docs/database/ddl/etl_feiqiu__dwd.sql` -- `docs/database/ddl/etl_feiqiu__core.sql` -- `docs/database/ddl/etl_feiqiu__dws.sql` -- `docs/database/ddl/etl_feiqiu__app.sql`(仅视图) -- `docs/database/ddl/zqyy_app__public.sql` -- `docs/database/ddl/zqyy_app__auth.sql` -- `docs/database/ddl/zqyy_app__biz.sql` -- `docs/database/ddl/fdw.sql`(仅正向映射;反向映射见 `db/fdw/setup_fdw_reverse*.sql`) +新建库或 schema diff 请使用 `docs/database/ddl/` 下的文件: -重新生成:`python scripts/ops/gen_consolidated_ddl.py` +| 文件 | 数据库 | Schema | 对象数 | +|------|--------|--------|--------| +| `etl_feiqiu__meta.sql` | etl_feiqiu | meta | 3 表 | +| `etl_feiqiu__ods.sql` | etl_feiqiu | ods | 23 表 | +| `etl_feiqiu__dwd.sql` | etl_feiqiu | dwd | 42 表 | +| `etl_feiqiu__core.sql` | etl_feiqiu | core | 7 表 | +| `etl_feiqiu__dws.sql` | etl_feiqiu | dws | 38 表 | +| `etl_feiqiu__app.sql` | etl_feiqiu | app | 仅视图 | +| `zqyy_app__public.sql` | zqyy_app | public | 12 表 | +| `zqyy_app__auth.sql` | zqyy_app | auth | 9 表 | +| `zqyy_app__biz.sql` | zqyy_app | biz | 21 表 | +| `fdw.sql` | — | — | 正向 FDW 映射 | +| `fdw_reverse.sql` | — | — | 反向 FDW 映射 | + +重新生成:`PYTHONUTF8=1 python tools/db/gen_consolidated_ddl.py` ## 迁移管理 项目 1.0 尚未上线,DDL 基线已统一到 `docs/database/ddl/`(含种子数据)。 后续新增迁移脚本放 `migrations/`,文件名格式 `YYYY-MM-DD__描述.sql`。 -每次迁移执行后,重新运行 DDL 生成脚本刷新基线:`python scripts/ops/gen_consolidated_ddl.py` +每次迁移执行后,重新运行 DDL 生成脚本刷新基线。 种子数据已合并进对应 DDL 文件末尾,不再单独维护 `seeds/` 目录。 + +## 文档分工 + +| 位置 | 管辖范围 | +|------|---------| +| `docs/database/` | 业务库(zqyy_app) BD_Manual + 跨模块(FDW/RLS) + DDL 基线 | +| `apps/etl/connectors/feiqiu/docs/database/` | ETL 专属(ODS/DWD/DWS 表级文档) | +| `db/`(本目录) | 运行时资产(迁移脚本/FDW 配置/建库脚本) | +| `tools/db/` | 数据库工具(DDL 生成/验证/一致性检查) | diff --git a/docs/database/BD_Manual_ai_tables.md b/docs/database/BD_Manual_ai_tables.md index fefbee5..0f9504f 100644 --- a/docs/database/BD_Manual_ai_tables.md +++ b/docs/database/BD_Manual_ai_tables.md @@ -1,22 +1,26 @@ -# BD_Manual:biz Schema AI 表(对话记录 + 消息 + 缓存) +# BD_Manual:biz Schema AI 表(对话记录 + 消息 + 缓存 + 运行日志 + 调度记录) > 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接) > 迁移脚本: > - `db/zqyy_app/migrations/2026-03-08__create_ai_tables.sql`(初始建表) > - `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`(RNS1.4 CHAT 扩展) -> 关联 SPEC:`05-miniapp-ai-integration`(P5 AI 集成层)、`rns1-chat-integration`(RNS1.4 CHAT 对齐与联调收尾) +> - `db/zqyy_app/migrations/2026-03-22__p14_ai_module.sql`(P14 DashScope 迁移 + 调度器完善) +> - `db/zqyy_app/migrations/2026-03-23__p15_ai_monitoring.sql`(P15 AI 监控后台 — alert_status + BRIN 索引) +> 关联 SPEC:`05-miniapp-ai-integration`(P5 AI 集成层)、`rns1-chat-integration`(RNS1.4 CHAT 对齐与联调收尾)、`P14-ai-dashscope-migration`(P14 DashScope 迁移)、`ai-monitoring-testing`(P15 AI 监控后台) --- ## 1. 变更说明 -### 新增表(3 张,P5 初始建表) +### 新增表(5 张) -| # | 表名 | 用途 | 字段数(初始→当前) | -|---|------|------|---------------------| -| 1 | `biz.ai_conversations` | AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条 | 8 → 13 | -| 2 | `biz.ai_messages` | AI 消息记录:对话中的每条消息(输入/输出/系统) | 6 → 7 | -| 3 | `biz.ai_cache` | AI 应用缓存:各应用的结构化输出结果 | 9 | +| # | 表名 | 用途 | 字段数(初始→当前) | 来源 | +|---|------|------|---------------------|------| +| 1 | `biz.ai_conversations` | AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条 | 8 → 14 | P5 | +| 2 | `biz.ai_messages` | AI 消息记录:对话中的每条消息(输入/输出/系统) | 6 → 7 | P5 | +| 3 | `biz.ai_cache` | AI 应用缓存:各应用的结构化输出结果 | 9 → 10 | P5 | +| 4 | `biz.ai_run_logs` | **P14 新增** — AI 运行记录:每次 DashScope API 调用的详细日志 | 14 → 15 | P14, P15 | +| 5 | `biz.ai_trigger_jobs` | **P14 新增** — 调度运行记录:每次 AI 事件触发的编排执行记录 | 13 | P14 | ### RNS1.4 CHAT 模块扩展字段(2026-03-20) @@ -25,9 +29,26 @@ | 1 | `biz.ai_conversations` | `context_type`, `context_id`, `title`, `last_message`, `last_message_at` | 多入口对话复用 + 历史列表展示与排序 | | 2 | `biz.ai_messages` | `reference_card` | 引用卡片 JSON(客户概览等结构化上下文数据) | +### P14 DashScope 迁移扩展字段(2026-03-22) + +| # | 表名 | 新增字段 | 用途 | +|---|------|---------|------| +| 1 | `biz.ai_conversations` | `session_id` | 百炼 session_id(格式 `conv_{id}_{ts}`),仅 App1 使用 | +| 2 | `biz.ai_cache` | `status` | 缓存状态:valid / expired / invalidated / generating | + +### P15 AI 监控后台扩展(2026-03-23) + +| # | 表名 | 变更类型 | 说明 | +|---|------|---------|------| +| 1 | `biz.ai_run_logs` | 新增字段 `alert_status` | 告警处理状态:NULL / pending / acknowledged / ignored | +| 2 | `biz.ai_run_logs` | 新增约束 `chk_ai_run_logs_alert_status` | CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')) | +| 3 | `biz.ai_run_logs` | 新增部分索引 `idx_ai_run_logs_alert` | (alert_status, created_at DESC) WHERE status IN ('failed', 'timeout', 'circuit_open') — 告警列表查询 | +| 4 | `biz.ai_run_logs` | 新增 BRIN 索引 `idx_ai_run_logs_created_brin` | BRIN (created_at) WITH (pages_per_range = 32) — Dashboard 聚合优化 | +| 5 | `biz.ai_run_logs` | 回填 | 已有 status IN ('failed','timeout','circuit_open') 的记录 alert_status 设为 'pending' | + ### 表字段明细 -#### biz.ai_conversations(13 字段) +#### biz.ai_conversations(14 字段) | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| @@ -39,11 +60,12 @@ | `source_page` | VARCHAR(100) | 可空 | 来源页面标识 | | `source_context` | JSONB | 可空 | 页面上下文 JSON | | `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | -| `context_type` | VARCHAR(20) | 可空 | **RNS1.4 新增** — 对话关联上下文类型:task(任务)/ customer(客户)/ coach(助教)/ general(通用) | -| `context_id` | VARCHAR(50) | 可空 | **RNS1.4 新增** — 关联上下文 ID:task 入口为 taskId,customer 入口为 customerId,coach 入口为 coachId,general 为 NULL | -| `title` | VARCHAR(200) | 可空 | **RNS1.4 新增** — 对话标题:自定义 > 上下文名称 > 首条消息前20字 | +| `context_type` | VARCHAR(20) | 可空 | **RNS1.4 新增** — 对话关联上下文类型:task / customer / coach / general | +| `context_id` | VARCHAR(50) | 可空 | **RNS1.4 新增** — 关联上下文 ID | +| `title` | VARCHAR(200) | 可空 | **RNS1.4 新增** — 对话标题 | | `last_message` | TEXT | 可空 | **RNS1.4 新增** — 最后一条消息内容摘要(截断至100字) | -| `last_message_at` | TIMESTAMPTZ | 可空 | **RNS1.4 新增** — 最后消息时间,用于历史列表排序和对话复用时限判断 | +| `last_message_at` | TIMESTAMPTZ | 可空 | **RNS1.4 新增** — 最后消息时间 | +| `session_id` | VARCHAR(100) | 可空 | **P14 新增** — 百炼 session_id,格式 `conv_{conversation_id}_{created_timestamp}`,仅 App1 使用 | #### biz.ai_messages(7 字段) @@ -57,7 +79,7 @@ | `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | | `reference_card` | JSONB | 可空 | **RNS1.4 新增** — 引用卡片 JSON:`{type, title, summary, data}`,用于展示客户概览等结构化上下文数据 | -#### biz.ai_cache(9 字段) +#### biz.ai_cache(10 字段) | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| @@ -70,6 +92,7 @@ | `triggered_by` | VARCHAR(100) | 可空 | 触发来源标识 | | `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | | `expires_at` | TIMESTAMPTZ | 可空 | 可选过期时间 | +| `status` | VARCHAR(20) | DEFAULT 'valid', CHECK | **P14 新增** — 缓存状态:valid(有效)/ expired(已过期)/ invalidated(手动失效)/ generating(生成中) | ### cache_type 枚举值与 target_id 约定 @@ -89,14 +112,62 @@ |----|-----------|------|------| | `ai_conversations` | `idx_ai_conv_user_site` | INDEX | `(user_id, site_id, created_at DESC)` — 用户历史对话列表查询 | | `ai_conversations` | `idx_ai_conv_app_site` | INDEX | `(app_id, site_id, created_at DESC)` — 按应用查询对话 | +| `ai_conversations` | `idx_ai_conv_context` | INDEX(条件) | **RNS1.4** — `(user_id, site_id, context_type, context_id, last_message_at DESC) WHERE context_type IS NOT NULL` | +| `ai_conversations` | `idx_ai_conv_last_msg` | INDEX | **RNS1.4** — `(user_id, site_id, last_message_at DESC NULLS LAST)` | | `ai_messages` | FK `conversation_id` | FK | → `biz.ai_conversations(id)` ON DELETE CASCADE | | `ai_messages` | `chk_ai_msg_role` | CHECK | `role IN ('user', 'assistant', 'system')` | | `ai_messages` | `idx_ai_msg_conv` | INDEX | `(conversation_id, created_at)` — 对话消息列表 | | `ai_cache` | `chk_ai_cache_type` | CHECK | 7 个枚举值 | +| `ai_cache` | `chk_ai_cache_status` | CHECK | **P14** — `status IN ('valid', 'expired', 'invalidated', 'generating')` | | `ai_cache` | `idx_ai_cache_lookup` | INDEX | `(cache_type, site_id, target_id, created_at DESC)` — 查询最新缓存 | -| `ai_cache` | `idx_ai_cache_cleanup` | INDEX | `(cache_type, site_id, target_id, created_at)` — 清理超限记录(ASC 排序便于删除最旧) | -| `ai_conversations` | `idx_ai_conv_context` | INDEX(条件) | **RNS1.4 新增** — `(user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST) WHERE context_type IS NOT NULL` — 上下文对话查找(多入口复用) | -| `ai_conversations` | `idx_ai_conv_last_msg` | INDEX | **RNS1.4 新增** — `(user_id, site_id, last_message_at DESC NULLS LAST)` — 历史列表排序优化(CHAT-1 倒序) | +| `ai_cache` | `idx_ai_cache_cleanup` | INDEX | `(cache_type, site_id, target_id, created_at)` — 清理超限记录 | +| `ai_run_logs` | `idx_ai_run_logs_site_app` | INDEX | **P14** — `(site_id, app_type)` | +| `ai_run_logs` | `idx_ai_run_logs_created` | INDEX | **P14** — `(created_at)` — Token 预算聚合 | +| `ai_run_logs` | `idx_ai_run_logs_status` | INDEX | **P14** — `(status)` | +| `ai_run_logs` | `chk_ai_run_logs_alert_status` | CHECK | **P15** — `alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')` | +| `ai_run_logs` | `idx_ai_run_logs_alert` | INDEX(部分) | **P15** — `(alert_status, created_at DESC) WHERE status IN ('failed', 'timeout', 'circuit_open')` — 告警列表查询优化 | +| `ai_run_logs` | `idx_ai_run_logs_created_brin` | BRIN INDEX | **P15** — `BRIN (created_at) WITH (pages_per_range = 32)` — Dashboard 聚合查询优化,适合按时间顺序插入的表 | +| `ai_trigger_jobs` | `idx_ai_trigger_jobs_site` | INDEX | **P14** — `(site_id, event_type)` | +| `ai_trigger_jobs` | `idx_ai_trigger_jobs_dedup` | INDEX(条件) | **P14** — `(event_type, member_id, site_id, created_at) WHERE status NOT IN ('skipped_duplicate')` — 去重 | +| `ai_trigger_jobs` | `idx_ai_trigger_jobs_status` | INDEX | **P14** — `(status)` | + +#### biz.ai_run_logs(15 字段,P14 新增 + P15 扩展) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | BIGSERIAL | PK | 自增主键 | +| `site_id` | BIGINT | NOT NULL | 门店 ID | +| `app_type` | VARCHAR(30) | NOT NULL | 应用类型:app1_chat / app2_finance / ... / app8_consolidate | +| `trigger_type` | VARCHAR(20) | NOT NULL | 触发类型:user / scheduled / event / forced | +| `member_id` | BIGINT | 可空 | 会员 ID | +| `request_prompt` | TEXT | 可空 | 请求 prompt(截断前 2000 字符) | +| `response_text` | TEXT | 可空 | 响应文本 | +| `tokens_used` | INTEGER | DEFAULT 0 | 消耗 token 数 | +| `latency_ms` | INTEGER | 可空 | 调用延迟(毫秒) | +| `status` | VARCHAR(20) | NOT NULL DEFAULT 'pending' | 状态:pending / running / success / failed / timeout / budget_exceeded | +| `error_message` | TEXT | 可空 | 错误信息 | +| `session_id` | VARCHAR(100) | 可空 | 百炼 session_id(仅 App1) | +| `alert_status` | VARCHAR(20) | DEFAULT NULL, CHECK | **P15 新增** — 告警处理状态:NULL(非告警记录)/ pending(待处理)/ acknowledged(已确认)/ ignored(已忽略)。仅 status IN ('failed','timeout','circuit_open') 的记录才设置此字段 | +| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT now() | 创建时间 | +| `finished_at` | TIMESTAMPTZ | 可空 | 完成时间 | + +#### biz.ai_trigger_jobs(13 字段,P14 新增) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | BIGSERIAL | PK | 自增主键 | +| `site_id` | BIGINT | NOT NULL | 门店 ID | +| `event_type` | VARCHAR(30) | NOT NULL | 事件类型:consumption / dws_completed / note_created / task_assigned | +| `connector_type` | VARCHAR(30) | DEFAULT 'feiqiu' | 连接器类型 | +| `member_id` | BIGINT | 可空 | 会员 ID | +| `payload` | JSONB | 可空 | 附加数据 | +| `status` | VARCHAR(20) | NOT NULL DEFAULT 'pending' | 状态:pending / running / completed / failed / skipped_duplicate / budget_exceeded | +| `is_forced` | BOOLEAN | DEFAULT false | 是否强制执行(跳过去重检查) | +| `app_chain` | VARCHAR(100) | 可空 | 调用链描述,如 `app3→app8→app7` | +| `started_at` | TIMESTAMPTZ | 可空 | 开始时间 | +| `finished_at` | TIMESTAMPTZ | 可空 | 完成时间 | +| `error_message` | TEXT | 可空 | 错误信息 | +| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT now() | 创建时间 | --- @@ -104,19 +175,56 @@ | 组件 | 影响 | |------|------| -| ETL 任务 | 无影响。AI 表属于 `biz` Schema,不参与 ETL 流程 | -| 后端 API | 直接依赖。P5 AI 模块(`apps/backend/app/ai/`)将基于这三张表实现对话持久化、缓存读写、SSE 流式对话等功能 | -| 后端 API(RNS1.4) | **直接依赖**。CHAT 模块(`apps/backend/app/routers/xcx_chat.py`、`apps/backend/app/services/chat_service.py`)依赖 `ai_conversations` 的 5 个新字段(`context_type`/`context_id`/`title`/`last_message`/`last_message_at`)实现多入口对话复用、历史列表展示与排序;依赖 `ai_messages.reference_card` 存储引用卡片 JSON | +| ETL 任务 | **P14 新增**:DWS 任务完成后通过 `utils/ai_trigger.py` 发送 HTTP 触发事件到后端 `ai_trigger_jobs`,失败不中断 ETL 流程 | +| 后端 API | 直接依赖。P5 AI 模块(`apps/backend/app/ai/`)基于这些表实现对话持久化、缓存读写、SSE 流式对话等功能 | +| 后端 API(RNS1.4) | **直接依赖**。CHAT 模块依赖 `ai_conversations` 的 5 个 RNS1.4 字段实现多入口对话复用 | +| 后端 API(P14) | **直接依赖**。`ai_run_logs` 用于 Token 预算聚合(BudgetTracker);`ai_trigger_jobs` 用于事件去重和调度记录;`ai_conversations.session_id` 用于百炼会话管理;`ai_cache.status` 用于缓存状态控制 | | 小程序 | 间接依赖。小程序通过后端 AI API 间接使用对话和缓存数据 | -| 小程序(RNS1.4) | **间接依赖**。`pages/chat/chat.ts` 和 `pages/chat-history/chat-history.ts` 通过 CHAT-1/2/3/4 端点间接依赖新字段(`title`→对话标题、`lastMessage`→摘要、`timestamp`→排序、`referenceCard`→引用卡片渲染) | -| 管理后台 | 暂无影响。后续可能增加 AI 调用统计和缓存管理界面 | -| `member_retention_clue` | 间接关联。App8(维客线索整理)的结果同时写入 `ai_cache` 和 `member_retention_clue` 表 | -| 现有 `biz` Schema | 兼容。P5 新增 3 张表;RNS1.4 仅在已有表上 ADD COLUMN / CREATE INDEX,不修改已有字段或约束 | +| 管理后台 | **P15 直接依赖**。admin-web AI 监控后台(4 个页面)依赖 `ai_run_logs.alert_status` 实现告警管理(确认/忽略),依赖 BRIN 索引优化 Dashboard 聚合查询性能 | +| `member_retention_clue` | 间接关联。App8 结果同时写入 `ai_cache` 和 `member_retention_clue` 表(P14 实现幂等 DELETE+INSERT) | --- ## 3. 回滚策略 +### 3d. 回滚 P15 AI 监控后台(2026-03-23 迁移) + +按逆序 DROP 新增索引、约束和字段: + +```sql +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; +``` + +注意: +- 回滚后 admin-web AI 监控后台的告警管理功能将不可用 +- Dashboard 聚合查询性能可能下降(失去 BRIN 索引) +- 回滚不影响 P14 的核心功能(调度器、预算追踪等) + +### 3c. 回滚 P14 DashScope 迁移(2026-03-22 迁移) + +按逆序 DROP 新增表和字段: + +```sql +-- 删除 P14 新增约束和字段 +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; + +-- 删除 P14 新增表 +DROP TABLE IF EXISTS biz.ai_trigger_jobs; +DROP TABLE IF EXISTS biz.ai_run_logs; +``` + +注意: +- 回滚后 P14 AI 调度器(dispatcher)、Token 预算追踪(BudgetTracker)将无法正常工作 +- `ai_run_logs` 和 `ai_trigger_jobs` 中的数据将丢失,需先备份 +- 回滚不影响 P5 和 RNS1.4 的功能 + ### 3a. 回滚 RNS1.4 CHAT 扩展(2026-03-20 迁移) 按逆序 DROP 新增索引和字段: @@ -185,7 +293,7 @@ SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'biz' AND table_name = 'ai_conversations' ORDER BY ordinal_position; --- 预期:返回 13 行(P5 原始 8 字段 + RNS1.4 新增 5 字段) +-- 预期:返回 14 行(P5 原始 8 + RNS1.4 新增 5 + P14 session_id = 14) -- 3. 验证 ai_messages 的外键和 CHECK 约束 SELECT conname, contype, pg_get_constraintdef(oid) AS constraint_def @@ -198,7 +306,7 @@ ORDER BY conname; SELECT conname, pg_get_constraintdef(oid) AS constraint_def FROM pg_constraint WHERE conrelid = 'biz.ai_cache'::regclass AND contype = 'c'; --- 预期:返回 1 行 chk_ai_cache_type,包含 7 个枚举值 +-- 预期:返回 2 行(chk_ai_cache_type 含 7 个枚举值,chk_ai_cache_status 含 4 个状态值) -- 5. 验证 P5 初始索引全部存在(5 个) SELECT indexname @@ -244,3 +352,101 @@ WHERE schemaname = 'biz' AND tablename = 'ai_conversations' -- idx_ai_conv_context — 含 WHERE context_type IS NOT NULL 条件 -- idx_ai_conv_last_msg — (user_id, site_id, last_message_at DESC NULLS LAST) ``` + +### 4c. P14 DashScope 迁移验证 + +```sql +-- 9. 验证 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; +-- 预期:返回 15 行 + +-- 10. 验证 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 行 + +-- 11. 验证 ai_run_logs 索引(含 PK) +SELECT indexname FROM pg_indexes +WHERE schemaname = 'biz' AND tablename = 'ai_run_logs'; +-- 预期:6 行(PK + 3 P14 索引 + 2 P15 索引) + +-- 12. 验证 ai_trigger_jobs 索引(含去重部分索引) +SELECT indexname FROM pg_indexes +WHERE schemaname = 'biz' AND tablename = 'ai_trigger_jobs'; +-- 预期:4 行(PK + 3 个索引) + +-- 13. 验证 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) + +-- 14. 验证 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 行 +``` + +### 4d. P15 AI 监控后台验证 + +```sql +-- 15. 验证 ai_run_logs.alert_status 字段存在 +SELECT column_name, data_type, character_maximum_length, column_default +FROM information_schema.columns +WHERE table_schema = 'biz' AND table_name = 'ai_run_logs' + AND column_name = 'alert_status'; +-- 预期:1 行,varchar(20),default NULL + +-- 16. 验证 alert_status CHECK 约束 +SELECT conname, pg_get_constraintdef(oid) AS constraint_def +FROM pg_constraint +WHERE conrelid = 'biz.ai_run_logs'::regclass + AND conname = 'chk_ai_run_logs_alert_status'; +-- 预期:1 行,CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')) + +-- 17. 验证 P15 新增索引(部分索引 + BRIN 索引) +SELECT indexname, indexdef +FROM pg_indexes +WHERE schemaname = 'biz' AND tablename = 'ai_run_logs' + AND indexname IN ('idx_ai_run_logs_alert', 'idx_ai_run_logs_created_brin'); +-- 预期:2 行 +-- idx_ai_run_logs_alert — 含 WHERE status IN ('failed', 'timeout', 'circuit_open') +-- idx_ai_run_logs_created_brin — USING brin + +-- 18. 验证 ai_run_logs 总索引数(P14 3个 + P15 2个 + PK = 6) +SELECT indexname FROM pg_indexes +WHERE schemaname = 'biz' AND tablename = 'ai_run_logs'; +-- 预期:6 行 + +-- 19. 验证回填结果:失败/超时/熔断记录的 alert_status 应为 'pending' +SELECT COUNT(*) AS unset_alerts +FROM biz.ai_run_logs +WHERE status IN ('failed', 'timeout', 'circuit_open') + AND alert_status IS NULL; +-- 预期:0(所有失败记录已回填为 'pending') +``` + +### P15 admin API 查询模式说明 + +P15 admin-web AI 监控后台引入以下典型查询模式: + +| 查询场景 | SQL 模式 | 使用索引 | +|---------|---------|---------| +| Dashboard 今日统计 | `SELECT COUNT(*), AVG(latency_ms) FROM ai_run_logs WHERE created_at >= 今日零点` | `idx_ai_run_logs_created_brin` | +| Dashboard 7天趋势 | `SELECT date_trunc('day', created_at), COUNT(*) FROM ai_run_logs WHERE created_at >= 7天前 GROUP BY 1` | `idx_ai_run_logs_created_brin` | +| 告警列表 | `SELECT * FROM ai_run_logs WHERE status IN ('failed','timeout','circuit_open') AND alert_status = 'pending' ORDER BY created_at DESC` | `idx_ai_run_logs_alert` | +| 告警确认/忽略 | `UPDATE ai_run_logs SET alert_status = 'acknowledged' WHERE id = ?` | PK | +| 调用记录分页 | `SELECT * FROM ai_run_logs WHERE site_id = ? AND app_type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?` | `idx_ai_run_logs_site_app` | +| Token 预算聚合 | `SELECT SUM(tokens_used) FROM ai_run_logs WHERE created_at >= 今日零点 AND status = 'success'` | `idx_ai_run_logs_created` (B-tree) | diff --git a/docs/database/BD_Manual_app_schema_rls_views.md b/docs/database/BD_Manual_app_schema_rls_views.md index df23015..ce4d55b 100644 --- a/docs/database/BD_Manual_app_schema_rls_views.md +++ b/docs/database/BD_Manual_app_schema_rls_views.md @@ -15,7 +15,7 @@ ### 新增角色 - `app_reader`:只读角色(`LOGIN`),拥有 `app` Schema 的 `USAGE` + `SELECT` 权限 -### 新增视图(39 张) +### 新增视图(49 张) **DWD 层(11 张,全部含 `site_id` 过滤):** @@ -48,6 +48,7 @@ | `app.v_dws_assistant_salary_calc` | `dws.dws_assistant_salary_calc` | | `app.v_dws_assistant_customer_stats` | `dws.dws_assistant_customer_stats` | | `app.v_dws_assistant_finance_analysis` | `dws.dws_assistant_finance_analysis` | +| `app.v_dws_assistant_order_contribution` | `dws.dws_assistant_order_contribution` | | `app.v_dws_finance_daily_summary` | `dws.dws_finance_daily_summary` | | `app.v_dws_finance_income_structure` | `dws.dws_finance_income_structure` | | `app.v_dws_finance_recharge_summary` | `dws.dws_finance_recharge_summary` | @@ -59,6 +60,9 @@ | `app.v_dws_assistant_project_tag` | `dws.dws_assistant_project_tag` | | `app.v_dws_member_project_tag` | `dws.dws_member_project_tag` | | `app.v_dws_member_spending_power_index` | `dws.dws_member_spending_power_index` | +| `app.v_dws_coach_area_hours` | `dws.dws_coach_area_hours` | +| `app.v_dws_finance_area_daily` | `dws.dws_finance_area_daily` | +| `app.v_dws_finance_board_cache` | `dws.dws_finance_board_cache` | **DWS 层 — cfg_* 配置表(5 张,无 `site_id`):** @@ -70,15 +74,24 @@ | `app.v_cfg_index_parameters` | `dws.cfg_index_parameters` | 同上 | | `app.v_cfg_area_category` | `dws.cfg_area_category` | DISTINCT 去重到 category 级别,排除 SPECIAL/OTHER,按 sort_order 排序。用于项目类型筛选器(CONFIG-1)。2026-03-20 新增。 | +**快捷别名视图(7 张,简化常用查询路径):** + +| 视图 | 源视图 | 说明 | +|------|--------|------| +| `app.v_assistant` | `app.v_dim_assistant` | 助教维度快捷别名 | +| `app.v_assistant_daily` | `app.v_dws_assistant_daily_detail` | 助教日明细快捷别名 | +| `app.v_finance_daily` | `app.v_dws_finance_daily_summary` | 财务日汇总快捷别名 | +| `app.v_member` | `app.v_dim_member` | 会员维度快捷别名 | +| `app.v_member_consumption` | `app.v_dws_member_consumption_summary` | 会员消费汇总快捷别名 | +| `app.v_order_summary` | `app.v_dws_order_summary` | 订单汇总快捷别名 | +| `app.v_site` | `dwd.dim_site` | 门店维度快捷别名 | + ### 权限配置 | 角色 | Schema | 权限 | |------|--------|------| | `app_reader` | `app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` | -### P2 预留(注释形式,暂不创建) -- `dws.dws_assistant_order_contribution` → `app.v_dws_assistant_order_contribution` - > `v_dws_member_spending_power_index`、`v_dws_assistant_project_tag`、`v_dws_member_project_tag` 已于 2026-03-19 正式创建(迁移脚本 `2026-03-19_add_board_rls_views.sql`)。 > `v_dws_finance_recharge_summary` 已于 2026-03-20 重建,新增 6 个赠送卡细分字段(`gift_liquor_balance`、`gift_table_fee_balance`、`gift_voucher_balance`、`gift_liquor_recharge`、`gift_table_fee_recharge`、`gift_voucher_recharge`)。迁移脚本:`db/zqyy_app/migrations/2026-03-20_rebuild_rls_view_gift_breakdown.sql`。关联 SPEC:`gift-card-breakdown`。 @@ -119,7 +132,7 @@ DROP ROLE IF EXISTS app_reader; -- 1. 验证 app Schema 存在 SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app'; --- 2. 验证视图数量(应为 39 张:原 35 + 2026-03-19 新增 3 + 2026-03-20 新增 1) +-- 2. 验证视图数量(应为 49 张:11 DWD + 26 DWS + 5 cfg + 7 快捷别名) SELECT count(*) FROM information_schema.views WHERE table_schema = 'app'; -- 3. 验证 app_reader 角色存在且有 app Schema 权限 diff --git a/docs/database/BD_Manual_auth_tables.md b/docs/database/BD_Manual_auth_tables.md index 4f27cd4..9032509 100644 --- a/docs/database/BD_Manual_auth_tables.md +++ b/docs/database/BD_Manual_auth_tables.md @@ -4,24 +4,27 @@ > 迁移脚本: > - `db/zqyy_app/migrations/2026-02-25__p3_create_auth_tables.sql`(建表) > - `db/zqyy_app/migrations/2026-02-25__p3_seed_roles_permissions.sql`(种子数据) +> - `db/zqyy_app/migrations/2026-03-23__add_head_coach_manager_roles.sql`(新增 head_coach、manager 角色及权限映射) +> - `db/zqyy_app/migrations/2026-03-23__add_rejection_count_and_cancelled_status.sql`(申请审核流程增强:rejection_count + cancelled 状态) > 关联 SPEC:`miniapp-auth-system`(P3 小程序用户认证系统) --- ## 1. 变更说明 -### 新增表(8 张) +### 新增表(9 张) | # | 表名 | 用途 | 主要字段 | |---|------|------|---------| -| 1 | `auth.users` | 微信用户主表 | `id`(PK), `wx_openid`(UK), `wx_union_id`, `wx_avatar_url`, `nickname`, `phone`, `status`(默认 `pending`), `created_at`, `updated_at` | -| 2 | `auth.user_applications` | 用户入驻申请表 | `id`(PK), `user_id`(FK→users), `site_code`, `site_id`, `applied_role_text`, `employee_number`, `phone`, `status`(默认 `pending`), `reviewer_id`, `review_note`, `created_at`, `reviewed_at` | -| 3 | `auth.site_code_mapping` | 球房ID与门店映射表 | `id`(PK), `site_code`(UK), `site_id`(UK), `site_name`, `tenant_id`, `created_at` | +| 1 | `auth.users` | 微信用户主表 | `id`(PK), `wx_openid`(UK), `wx_union_id`, `wx_avatar_url`, `nickname`, `phone`, `avatar_url`, `status`(默认 `new`), `rejection_count`(默认 0,累计被拒次数), `created_at`, `updated_at` | +| 2 | `auth.user_applications` | 用户入驻申请表 | `id`(PK), `user_id`(FK→users), `site_code`, `site_id`, `applied_role_text`, `employee_number`, `phone`, `status`(默认 `pending`,可选值: pending/approved/rejected/cancelled), `reviewer_id`, `review_note`, `created_at`, `reviewed_at` | +| 3 | `auth._archived_site_code_mapping` | [已废弃] 球房ID与门店映射表(NS4.1 重命名,替代方案:`biz.sites` + `biz.site_code_history`) | `id`(PK), `site_code`(UK), `site_id`(UK), `site_name`, `tenant_id`, `created_at` | | 4 | `auth.roles` | 角色定义表 | `id`(PK), `code`(UK), `name`, `description`, `created_at` | | 5 | `auth.permissions` | 权限定义表 | `id`(PK), `code`(UK), `name`, `description`, `created_at` | | 6 | `auth.role_permissions` | 角色-权限关联表 | `role_id`(FK→roles), `permission_id`(FK→permissions),联合主键 | | 7 | `auth.user_site_roles` | 用户-门店-角色关联表 | `id`(PK), `user_id`(FK→users), `site_id`, `role_id`(FK→roles), `created_at`,`(user_id, site_id, role_id)` 唯一约束 | | 8 | `auth.user_assistant_binding` | 用户-人员绑定表 | `id`(PK), `user_id`(FK→users), `site_id`, `assistant_id`(可空), `staff_id`(可空), `binding_type`, `created_at` | +| 9 | `auth.tenant_admins` | 租户管理员表(详见 `BD_Manual_tenant_admin_tables.md`) | `id`(PK), `username`(UK), `password_hash`, `display_name`, `tenant_id`, `managed_site_ids`, `is_active`, `deleted_at`, `created_by`, `created_at`, `last_login_at` | ### 约束与索引 @@ -29,14 +32,15 @@ |----|-----------|------|------| | `users` | `uq_users_wx_openid` | UNIQUE | 微信 openid 唯一 | | `users` | `ix_users_wx_openid` | INDEX | openid 查询加速 | -| `site_code_mapping` | `uq_site_code_mapping_site_code` | UNIQUE | 球房ID 唯一 | -| `site_code_mapping` | `uq_site_code_mapping_site_id` | UNIQUE | site_id 唯一映射 | -| `site_code_mapping` | `ix_site_code_mapping_site_code` | INDEX | site_code 查询加速 | +| `site_code_mapping` | `uq_site_code_mapping_site_code` | UNIQUE | 球房ID 唯一(已废弃,表已重命名为 `_archived_site_code_mapping`) | +| `site_code_mapping` | `uq_site_code_mapping_site_id` | UNIQUE | site_id 唯一映射(已废弃) | +| `site_code_mapping` | `ix_site_code_mapping_site_code` | INDEX | site_code 查询加速(已废弃) | | `roles` | `uq_roles_code` | UNIQUE | 角色 code 唯一 | | `permissions` | `uq_permissions_code` | UNIQUE | 权限 code 唯一 | | `role_permissions` | PK `(role_id, permission_id)` | PRIMARY KEY | 联合主键 | | `role_permissions` | `fk_role_permissions_role_id` | FK | → `auth.roles(id)` CASCADE | | `role_permissions` | `fk_role_permissions_permission_id` | FK | → `auth.permissions(id)` CASCADE | +| `user_applications` | `user_applications_status_check` | CHECK | status IN ('pending', 'approved', 'rejected', 'cancelled') | | `user_applications` | `fk_user_applications_user_id` | FK | → `auth.users(id)` CASCADE | | `user_applications` | `ix_user_applications_user_id` | INDEX | user_id 查询加速 | | `user_applications` | `ix_user_applications_status` | INDEX | status 过滤加速 | @@ -65,17 +69,21 @@ |------|------|-------------| | `coach` | 助教 | 球房助教,可查看任务和助教看板 | | `staff` | 员工 | 球房员工,可查看任务和数据看板 | -| `site_admin` | 店铺管理员 | 单店管理员,可查看所有看板 | -| `tenant_admin` | 租户管理员 | 租户级管理员,拥有全部权限 | +| `head_coach` | 教练 | 主教练,负责训练助教,可查看任务和全部看板 | +| `manager` | 管理员 | 球房管理员,可查看任务和全部看板,未来将与 staff 进一步区分权限 | -#### 角色-权限映射(14 条) +> 注:`site_admin` 和 `tenant_admin` 已于 2026-03-23 从小程序 RBAC 体系中清理。租户/店铺管理员的区分通过 `auth.tenant_admins.admin_type` 列实现,不依赖 `auth.roles` 表。 + +#### 角色-权限映射(11 条) | 角色 | 权限列表 | 权限数 | |------|---------|--------| | `coach` | `view_tasks`, `view_board_coach` | 2 | | `staff` | `view_tasks`, `view_board` | 2 | -| `site_admin` | `view_tasks`, `view_board`, `view_board_finance`, `view_board_customer`, `view_board_coach` | 5 | -| `tenant_admin` | `view_tasks`, `view_board`, `view_board_finance`, `view_board_customer`, `view_board_coach` | 5 | +| `head_coach` | `view_tasks`, `view_board` | 2 | +| `manager` | `view_tasks`, `view_board`, `view_board_finance`, `view_board_customer`, `view_board_coach` | 5 | + +> 注:`site_admin`(5 条)和 `tenant_admin`(5 条)的权限映射已于 2026-03-23 删除,角色体系隔离后总映射从 24 条减为 11 条(head_coach 仅分配 view_tasks + view_board,非全部 5 个权限)。详见迁移脚本 `2026-03-23__cleanup_roles_add_admin_type.sql`。 --- @@ -100,10 +108,10 @@ ```sql -- 先删除种子数据(如需保留表结构) DELETE FROM auth.role_permissions -WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin')) +WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager')) AND permission_id IN (SELECT id FROM auth.permissions WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach')); -DELETE FROM auth.roles WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin'); +DELETE FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager'); DELETE FROM auth.permissions WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach'); -- 删除表(按逆序,CASCADE 处理外键依赖) @@ -113,7 +121,7 @@ DROP TABLE IF EXISTS auth.user_applications CASCADE; DROP TABLE IF EXISTS auth.role_permissions CASCADE; DROP TABLE IF EXISTS auth.permissions CASCADE; DROP TABLE IF EXISTS auth.roles CASCADE; -DROP TABLE IF EXISTS auth.site_code_mapping CASCADE; +DROP TABLE IF EXISTS auth._archived_site_code_mapping CASCADE; DROP TABLE IF EXISTS auth.users CASCADE; ``` @@ -124,17 +132,17 @@ DROP TABLE IF EXISTS auth.users CASCADE; ## 4. 验证 SQL ```sql --- 1. 验证 auth Schema 下 8 张认证表全部存在 +-- 1. 验证 auth Schema 下 9 张认证表全部存在 SELECT table_name FROM information_schema.tables WHERE table_schema = 'auth' AND table_name IN ( - 'users', 'user_applications', 'site_code_mapping', + 'users', 'user_applications', '_archived_site_code_mapping', 'roles', 'permissions', 'role_permissions', - 'user_site_roles', 'user_assistant_binding' + 'user_site_roles', 'user_assistant_binding', 'tenant_admins' ) ORDER BY table_name; --- 预期:返回 8 行 +-- 预期:返回 9 行 -- 2. 验证种子数据:5 条权限 SELECT COUNT(*) AS perm_count @@ -142,28 +150,111 @@ FROM auth.permissions WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach'); -- 预期:5 --- 3. 验证种子数据:4 条角色 +-- 3. 验证种子数据:4 条角色(site_admin/tenant_admin 已于 2026-03-23 删除) SELECT COUNT(*) AS role_count FROM auth.roles -WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin'); +WHERE code IN ('coach', 'staff', 'head_coach', 'manager'); -- 预期:4 --- 4. 验证角色-权限映射数量 +-- 4. 验证角色-权限映射数量(11 条) SELECT r.code AS role_code, COUNT(rp.permission_id) AS perm_count FROM auth.roles r JOIN auth.role_permissions rp ON r.id = rp.role_id GROUP BY r.code ORDER BY r.code; --- 预期:coach=2, site_admin=5, staff=2, tenant_admin=5(共 14 条映射) +-- 预期:coach=2, head_coach=2, manager=5, staff=2(共 11 条映射,无 site_admin/tenant_admin) -- 5. 验证关键约束存在 SELECT conname, contype FROM pg_constraint WHERE conrelid IN ( 'auth.users'::regclass, - 'auth.site_code_mapping'::regclass, + 'auth._archived_site_code_mapping'::regclass, 'auth.user_site_roles'::regclass ) ORDER BY conrelid::regclass::text, conname; --- 预期:包含 uq_users_wx_openid、uq_site_code_mapping_site_code、uq_site_code_mapping_site_id、uq_user_site_roles_user_site_role 等 +-- 预期:包含 uq_users_wx_openid、uq_site_code_mapping_site_code(已废弃表 _archived_site_code_mapping)、uq_site_code_mapping_site_id、uq_user_site_roles_user_site_role 等 ``` + +--- + +--- + +## 5. 变更记录:2026-03-23 申请审核流程增强 + +### 5.1 变更说明 + +| 对象 | 变更类型 | 说明 | +|------|---------|------| +| `auth.users.rejection_count` | 新增字段 | `integer NOT NULL DEFAULT 0`,累计被管理员拒绝的申请次数,达到 3 次自动将 `status` 设为 `disabled` | +| `auth.user_applications.status` CHECK 约束 | 修改 | 新增 `'cancelled'` 可选值(用户主动取消申请),约束名 `user_applications_status_check` | + +### 5.2 业务规则 + +- 管理员拒绝申请时:`rejection_count += 1`,第 3 次自动将 `users.status` 设为 `disabled` +- 用户主动取消(`cancelled`)不计入 `rejection_count` +- `cancelled` 状态的申请不在管理端申请列表中显示 +- `disabled` 用户不允许重新申请,需管理员手动解除(功能待开发) + +### 5.3 兼容性影响 + +| 组件 | 影响 | +|------|------| +| ETL | 无影响,不涉及 ETL 库 | +| 后端 API | 直接依赖。`reject_application` 增加 rejection_count 累加逻辑;新增 `cancel_application` 服务和 `/api/xcx/cancel-application` 端点;`/api/xcx/me` 返回 `latestApplication` 详情 | +| 小程序 | 间接依赖。reviewing 页展示申请详情+重新申请按钮;no-permission 页区分 rejected/disabled 状态 | +| 管理后台 | 间接依赖。拒绝端点自动累加 rejection_count 并触发禁用;申请列表排除 cancelled | + +### 5.4 回滚策略 + +```sql +-- 回滚 rejection_count 字段 +ALTER TABLE auth.users DROP COLUMN IF EXISTS rejection_count; + +-- 回滚 status CHECK 约束(恢复为不含 cancelled) +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')); + +-- 注意:回滚前需确认无 status='cancelled' 的记录,否则约束添加会失败 +-- UPDATE auth.user_applications SET status = 'pending' WHERE status = 'cancelled'; +``` + +### 5.5 验证 SQL + +```sql +-- 1. 验证 rejection_count 字段存在且默认值正确 +SELECT column_name, data_type, column_default, is_nullable +FROM information_schema.columns +WHERE table_schema = 'auth' AND table_name = 'users' AND column_name = 'rejection_count'; +-- 预期:integer, 0, NO + +-- 2. 验证 CHECK 约束包含 cancelled +SELECT conname, pg_get_constraintdef(oid) AS constraint_def +FROM pg_constraint +WHERE conrelid = 'auth.user_applications'::regclass + AND conname = 'user_applications_status_check'; +-- 预期:包含 'cancelled' + +-- 3. 验证现有数据 rejection_count 默认值 +SELECT COUNT(*) AS users_with_zero_rejection +FROM auth.users +WHERE rejection_count = 0; +-- 预期:等于 users 表总行数(所有现有用户默认 0) + +-- 4. 验证 cancelled 状态可正常写入(dry-run 验证) +-- INSERT INTO auth.user_applications (user_id, site_code, site_id, applied_role_text, phone, status) +-- VALUES (1, 'TEST', 1, '测试', '13800000000', 'cancelled'); +-- 预期:不报 CHECK 约束错误 +``` + +--- + + diff --git a/docs/database/BD_Manual_biz_tables.md b/docs/database/BD_Manual_biz_tables.md index 1f546a4..b359cd4 100644 --- a/docs/database/BD_Manual_biz_tables.md +++ b/docs/database/BD_Manual_biz_tables.md @@ -4,24 +4,28 @@ > 迁移脚本: > - `db/zqyy_app/migrations/2026-02-27__p4_create_biz_tables.sql`(建表) > - `db/zqyy_app/migrations/2026-02-27__p4_seed_trigger_jobs.sql`(种子数据) -> 关联 SPEC:`04-miniapp-core-business`(P4 小程序核心业务模块) +> - `db/zqyy_app/migrations/2026-03-24__p17_task_engine_ownership.sql`(P17 客户归属与转移) +> - `db/zqyy_app/migrations/2026-03-24__p18_task_engine_dashboard.sql`(P18 运营看板字段扩展) +> 关联 SPEC:`04-miniapp-core-business`(P4)、`P17-assistant-ownership-task-engine`(P17)、`P18-admin-task-engine-dashboard`(P18) --- ## 1. 变更说明 -### 新增表(4 张) +### 新增表(6 张) -| # | 表名 | 用途 | 字段数 | -|---|------|------|--------| -| 1 | `biz.coach_tasks` | 助教任务表:存储任务分配、状态、有效期、置顶、放弃原因等 | 15 | -| 2 | `biz.coach_task_history` | 任务变更历史表:记录任务关闭/新建/置顶/放弃的追溯链 | 9 | -| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 15 | -| 4 | `biz.trigger_jobs` | 触发器配置表:存储 cron/interval/event 三种触发方式的配置与执行状态 | 9 | +| # | 表名 | 用途 | 字段数 | 来源 | +|---|------|------|--------|------| +| 1 | `biz.coach_tasks` | 助教任务表:存储任务分配、状态、有效期、置顶、放弃原因、转移追踪等 | 18 | P4+P17 | +| 2 | `biz.coach_task_history` | 任务变更历史表:记录任务关闭/新建/置顶/放弃/转移的追溯链 | 9 | P4 | +| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 15 | P4 | +| 4 | `biz.trigger_jobs` | 触发器配置表:存储 cron/interval/event 三种触发方式的配置与执行状态 | 12 | P4+P18+P23 | +| 5 | `biz.cfg_task_generator_params` | 任务引擎参数配置表:支持全局默认 + 门店级覆盖 | 7 | P17+P18 | +| 6 | `biz.coach_task_transfer_log` | 客户转移日志表:记录每次转移的完整上下文 | 11 | P17 | ### 表字段明细 -#### biz.coach_tasks(15 字段) +#### biz.coach_tasks(18 字段) | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| @@ -30,7 +34,7 @@ | `assistant_id` | BIGINT | NOT NULL | 助教 ID | | `member_id` | BIGINT | NOT NULL | 客户 ID | | `task_type` | VARCHAR(50) | NOT NULL | 任务类型:`high_priority_recall` / `priority_recall` / `follow_up_visit` / `relationship_building` | -| `status` | VARCHAR(20) | NOT NULL DEFAULT 'active' | 状态:`active` / `inactive` / `completed` / `abandoned` | +| `status` | VARCHAR(20) | NOT NULL DEFAULT 'active' | 状态:`active` / `inactive` / `completed` / `abandoned` / `transferred`(P17)/ `pending_review`(P17) | | `priority_score` | NUMERIC(5,2) | 可空 | 优先级分数,取 `max(WBI, NCI)` 快照 | | `expires_at` | TIMESTAMPTZ | 可空 | 有效期时间戳,NULL 表示无限期 | | `is_pinned` | BOOLEAN | DEFAULT FALSE | 是否置顶 | @@ -38,6 +42,9 @@ | `completed_at` | TIMESTAMPTZ | 可空 | 完成时间 | | `completed_task_type` | VARCHAR(50) | 可空 | 完成时的任务类型快照 | | `parent_task_id` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 父任务 ID(自引用) | +| `transfer_count` | INTEGER | NOT NULL DEFAULT 0 | 该客户在此任务链上的累计转移次数(P17 新增) | +| `transferred_from` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 转移来源任务 ID(P17 新增) | +| `transferred_at` | TIMESTAMPTZ | 可空 | 转移发生时间(P17 新增) | | `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | | `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 | @@ -47,7 +54,7 @@ |------|------|------|------| | `id` | BIGSERIAL | PK | 自增主键 | | `task_id` | BIGINT | NOT NULL, FK → `biz.coach_tasks(id)` | 关联任务 | -| `action` | VARCHAR(50) | NOT NULL | 操作类型:`created` / `type_changed` / `pinned` / `abandoned` / `cancel_abandon` / `expired` / `completed` | +| `action` | VARCHAR(50) | NOT NULL | 操作类型:`created` / `type_changed` / `type_change_close` / `pinned` / `abandoned` / `cancel_abandon` / `expired` / `completed` / `expires_at_filled` / `transferred_out`(P17)/ `transferred_in`(P17) | | `old_status` | VARCHAR(20) | 可空 | 变更前状态 | | `new_status` | VARCHAR(20) | 可空 | 变更后状态 | | `old_task_type` | VARCHAR(50) | 可空 | 变更前任务类型 | @@ -75,20 +82,53 @@ | `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 | | `score` | SMALLINT | CHECK (1-5),可空 | 备注星星评分,助教创建备注时可选填写,不参与 AI 分析(RNS1.1 新增) | -#### biz.trigger_jobs(9 字段) +#### biz.trigger_jobs(12 字段) | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | `id` | SERIAL | PK | 自增主键 | -| `job_type` | VARCHAR(100) | NOT NULL | 任务类型标识,映射到 Python handler | -| `job_name` | VARCHAR(100) | NOT NULL, UNIQUE | 任务名称(唯一) | +| `job_type` | VARCHAR(100) | NOT NULL | 任务类型标识,映射到 Python handler(`_JOB_REGISTRY` 注册键) | +| `job_name` | VARCHAR(100) | NOT NULL, UNIQUE | 任务名称(唯一标识,如 `task_generator`) | | `trigger_condition` | VARCHAR(20) | NOT NULL | 触发方式:`cron` / `interval` / `event` | | `trigger_config` | JSONB | NOT NULL | 触发配置(cron 表达式 / 间隔秒数 / 事件名) | | `last_run_at` | TIMESTAMPTZ | 可空 | 上次运行时间 | | `next_run_at` | TIMESTAMPTZ | 可空 | 下次运行时间(event 类型为 NULL) | | `status` | VARCHAR(20) | NOT NULL DEFAULT 'enabled' | 状态:`enabled` / `disabled` | +| `description` | TEXT | 可空 | 任务中文描述,管理后台页面展示(P23 新增) | +| `last_error` | TEXT | 可空 | 最后一次执行异常的错误信息,成功后清空为 NULL(P23 新增) | +| `last_stats` | JSONB | 可空 | 最近一次执行的统计结果 JSON,如 `{"created":5,"replaced":2,"skipped":10,"transferred":1}`(P18 新增) | | `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | +#### biz.cfg_task_generator_params(7 字段,P17+P18) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | BIGSERIAL | PK | 自增主键 | +| `site_id` | BIGINT | 可空 | NULL=全局默认,非NULL=门店级覆盖 | +| `param_key` | VARCHAR(64) | NOT NULL | 参数键名 | +| `param_value` | NUMERIC | NOT NULL | 参数值 | +| `description` | TEXT | 可空 | 参数说明 | +| `updated_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 更新时间 | +| `updated_by` | BIGINT | 可空 | 最近修改人 user_id,用于审计追溯(P18 新增) | + +继承链:代码默认 → 全局默认(site_id IS NULL)→ 门店覆盖(site_id = ?) + +#### biz.coach_task_transfer_log(11 字段,P17 新增) + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | BIGSERIAL | PK | 自增主键 | +| `site_id` | BIGINT | NOT NULL | 门店 ID | +| `member_id` | BIGINT | NOT NULL | 客户 ID | +| `from_assistant_id` | BIGINT | NOT NULL | 原助教 ID | +| `to_assistant_id` | BIGINT | NOT NULL | 新助教 ID | +| `from_task_id` | BIGINT | NOT NULL, FK → `biz.coach_tasks(id)` | 原任务 ID | +| `to_task_id` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 新任务 ID | +| `transfer_reason` | TEXT | 可空 | 转移原因描述 | +| `guard_checks` | JSONB | 可空 | 三重保护检查结果快照 | +| `transfer_score` | NUMERIC | 可空 | 转移候选得分 | +| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | + ### 约束与索引 | 表 | 约束/索引名 | 类型 | 说明 | @@ -96,6 +136,7 @@ | `coach_tasks` | `idx_coach_tasks_site_assistant_member_type` | UNIQUE INDEX (partial) | `(site_id, assistant_id, member_id, task_type) WHERE status = 'active'`,保证同一组合下活跃任务最多一条 | | `coach_tasks` | `idx_coach_tasks_assistant_status` | INDEX | `(site_id, assistant_id, status)`,助教任务列表查询加速 | | `coach_tasks` | FK `parent_task_id` | FK | → `biz.coach_tasks(id)`,自引用 | +| `coach_tasks` | FK `fk_coach_tasks_transferred_from` | FK | → `biz.coach_tasks(id)`,转移来源任务(P17 新增) | | `coach_task_history` | FK `task_id` | FK | → `biz.coach_tasks(id)` | | `notes` | `idx_notes_target` | INDEX | `(site_id, target_type, target_id)`,按目标查询备注加速 | | `notes` | CHECK `rating_service_willingness` | CHECK | `BETWEEN 1 AND 5` | @@ -103,6 +144,31 @@ | `notes` | CHECK `score` | CHECK | `score IS NULL OR (score >= 1 AND score <= 5)`(RNS1.1 新增) | | `notes` | FK `task_id` | FK | → `biz.coach_tasks(id)` | | `trigger_jobs` | UNIQUE `job_name` | UNIQUE | 触发器名称唯一 | +| `cfg_task_generator_params` | UNIQUE `(site_id, param_key)` | UNIQUE | 全局+门店级参数唯一约束(P17 新增) | +| `coach_task_transfer_log` | FK `from_task_id` | FK | → `biz.coach_tasks(id)`(P17 新增) | +| `coach_task_transfer_log` | FK `to_task_id` | FK | → `biz.coach_tasks(id)`(P17 新增) | +| `coach_task_transfer_log` | `idx_transfer_log_site_created` | INDEX | `(site_id, created_at DESC)`(P17 新增) | +| `coach_task_transfer_log` | `idx_transfer_log_member` | INDEX | `(member_id, created_at DESC)`(P17 新增) | + +### P17 种子数据(13 条任务引擎参数) + +| param_key | param_value | description | +|-----------|-------------|-------------| +| `high_priority_recall_threshold` | 7.0 | max(WBI,NCI) 超过此值生成高优先召回 | +| `priority_recall_threshold` | 5.0 | max(WBI,NCI) 超过此值生成优先召回 | +| `rs_min_for_relationship` | 1.0 | RS ≤ 此值不生成关系构建 | +| `rs_max_for_relationship` | 6.0 | RS ≥ 此值不生成关系构建 | +| `consecutive_recall_fail_cycles` | 3 | 连续失败多少轮触发客户转移 | +| `min_wbi_for_transfer` | 5.0 | WBI 低于此值不触发转移 | +| `guard_assistant_coverage_ratio` | 0.5 | 绑定率低于此值禁用转移 | +| `guard_new_assistant_days` | 10 | 新助教入驻保护天数 | +| `transfer_score_w_rs` | 0.5 | 转移候选排序:RS 权重 | +| `transfer_score_w_ms` | 0.3 | 转移候选排序:MS 权重 | +| `transfer_score_w_ml` | 0.2 | 转移候选排序:ML 权重 | +| `max_transfer_count` | 2 | 单客户最大累计转移次数 | +| `follow_up_visit_retention_hours` | 48 | 回访任务最低保留时长(小时) | + +> 所有参数 `site_id IS NULL`(全局默认),门店可通过插入 `site_id = ?` 的行覆盖。 ### 种子数据(4 条触发器配置) @@ -120,13 +186,13 @@ | 组件 | 影响 | |------|------| | ETL 任务 | 无直接影响。`biz` Schema 表不参与 ETL 流程,但任务生成器通过 FDW 只读访问 ETL 库的 WBI/NCI/RS 指数数据 | -| 后端 API | 直接依赖。FastAPI 后端将基于这些表实现任务 CRUD(`/api/xcx/tasks`)、备注 CRUD(`/api/xcx/notes`)、触发器调度等功能 | +| 后端 API | 直接依赖。FastAPI 后端将基于这些表实现任务 CRUD(`/api/xcx/tasks`)、备注 CRUD(`/api/xcx/notes`)、触发器调度等功能。P17 新增:`task_generator.py` 完全重写,入口改为 OS 归属对;`fdw_queries.py` 新增 4 个批量查询方法 | | 小程序 | 间接依赖。小程序通过后端 API 间接使用任务列表、备注功能 | -| 管理后台 | 暂无影响。后续可能增加任务监控和触发器管理界面 | +| 管理后台 | P18 已实施。`admin_task_engine` router 提供 9 个端点(转移日志分页+历史、待审核任务分页+重新分配+关闭、参数管理 CRUD);前端 3 个页面(TransferLog/PendingReview/TaskEngineConfig)通过 `taskEngine.ts` API 层调用 | | FDW 配置 | 无影响。`fdw_etl` Schema 独立于 `biz`,任务生成器和召回检测器通过 FDW 只读查询 ETL 库 | -| `auth` Schema | 间接依赖。任务生成器通过 `auth.user_assistant_binding` 确定助教与小程序用户的映射关系 | +| `auth` Schema | 间接依赖。P17 仍通过 `auth.user_assistant_binding` 获取 site_ids 和助教绑定信息(转移保护检查) | | `public` Schema | 无影响。`member_retention_clue` 表独立于本次变更 | -| 现有 `biz` Schema | 兼容。`biz` Schema 已由 P1 迁移脚本创建,本次仅在其中新增 4 张表,不修改已有对象 | +| 现有 `biz` Schema | 兼容。`coach_tasks` 新增 3 字段均有默认值(`transfer_count DEFAULT 0`,其余可空),不影响现有查询。新增 `transferred`/`pending_review` 状态值,7 个下游模块均使用显式 status 过滤,不会误匹配 | --- @@ -170,6 +236,8 @@ RNS1.2(客户与助教接口)新增 3 个端点,引用了以下 biz/public ## 3. 回滚策略 +### P4 回滚(原始 4 张表) + 按逆序 `DROP TABLE IF EXISTS CASCADE`(迁移脚本末尾已包含注释形式的回滚语句): ```sql @@ -194,6 +262,32 @@ DROP TABLE IF EXISTS biz.coach_task_history CASCADE; DROP TABLE IF EXISTS biz.coach_tasks CASCADE; ``` +### P17 回滚(增量变更) + +```sql +-- 1. 删除 P17 种子数据 +DELETE FROM biz.cfg_task_generator_params WHERE site_id IS NULL; + +-- 2. 删除 P17 新增表 +DROP TABLE IF EXISTS biz.coach_task_transfer_log; +DROP TABLE IF EXISTS biz.cfg_task_generator_params; + +-- 3. 删除 P17 新增字段 +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 值(transferred/pending_review)一旦添加无法直接删除,需重建类型 +``` + +### P18 回滚(字段扩展) + +```sql +-- 删除 P18 新增字段 +ALTER TABLE biz.trigger_jobs DROP COLUMN IF EXISTS last_stats; +ALTER TABLE biz.cfg_task_generator_params DROP COLUMN IF EXISTS updated_by; +``` + 注意: - `CASCADE` 会级联删除依赖对象(外键引用的子表数据) - 如果表中已有业务数据,需先备份再执行回滚 @@ -204,52 +298,90 @@ DROP TABLE IF EXISTS biz.coach_tasks CASCADE; ## 4. 验证 SQL ```sql --- 1. 验证 biz Schema 下 4 张业务表全部存在 +-- 1. 验证 biz Schema 下 6 张业务表全部存在 SELECT table_name FROM information_schema.tables WHERE table_schema = 'biz' - AND table_name IN ('coach_tasks', 'coach_task_history', 'notes', 'trigger_jobs') + AND table_name IN ('coach_tasks', 'coach_task_history', 'notes', 'trigger_jobs', + 'cfg_task_generator_params', 'coach_task_transfer_log') ORDER BY table_name; --- 预期:返回 4 行(coach_task_history, coach_tasks, notes, trigger_jobs) +-- 预期:返回 6 行 --- 2. 验证 coach_tasks 表字段数量和关键字段 +-- 2. 验证 coach_tasks 表字段数量(P4 原 15 + P17 新增 3 = 18) SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'biz' AND table_name = 'coach_tasks' ORDER BY ordinal_position; --- 预期:返回 15 行,包含 id/site_id/assistant_id/member_id/task_type/status 等 +-- 预期:返回 18 行,包含 transfer_count/transferred_from/transferred_at --- 3. 验证部分唯一索引存在 +-- 3. 验证 P17 新增字段的默认值和约束 +SELECT column_name, data_type, is_nullable, column_default +FROM information_schema.columns +WHERE table_schema = 'biz' AND table_name = 'coach_tasks' + AND column_name IN ('transfer_count', 'transferred_from', 'transferred_at'); +-- 预期:transfer_count: integer, NOT NULL, DEFAULT 0 +-- transferred_from: bigint, YES (nullable) +-- transferred_at: timestamp with time zone, YES (nullable) + +-- 4. 验证 cfg_task_generator_params 表结构 +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'biz' AND table_name = 'cfg_task_generator_params' +ORDER BY ordinal_position; +-- 预期:6 行(id, site_id, param_key, param_value, description, updated_at) + +-- 5. 验证 P17 种子数据(13 条全局默认参数) +SELECT param_key, param_value, description +FROM biz.cfg_task_generator_params +WHERE site_id IS NULL +ORDER BY param_key; +-- 预期:返回 13 行 + +-- 6. 验证 coach_task_transfer_log 表结构 +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'biz' AND table_name = 'coach_task_transfer_log' +ORDER BY ordinal_position; +-- 预期:11 行(id, site_id, member_id, from_assistant_id, to_assistant_id, +-- from_task_id, to_task_id, transfer_reason, guard_checks, transfer_score, created_at) + +-- 7. 验证 P17 新增索引 +SELECT indexname +FROM pg_indexes +WHERE schemaname = 'biz' + AND indexname IN ('idx_transfer_log_site_created', 'idx_transfer_log_member') +ORDER BY indexname; +-- 预期:返回 2 行 + +-- 8. 验证 transferred_from 外键约束 +SELECT conname +FROM pg_constraint +WHERE conrelid = 'biz.coach_tasks'::regclass + AND conname = 'fk_coach_tasks_transferred_from'; +-- 预期:返回 1 行 + +-- 9. 验证部分唯一索引存在(P4 原有) SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'biz' AND indexname = 'idx_coach_tasks_site_assistant_member_type'; -- 预期:返回 1 行,indexdef 包含 "WHERE ((status)::text = 'active'::text)" --- 4. 验证 notes 表的 CHECK 约束(评分 1-5) -SELECT conname, pg_get_constraintdef(oid) AS constraint_def -FROM pg_constraint -WHERE conrelid = 'biz.notes'::regclass AND contype = 'c'; --- 预期:返回 2 行,分别约束 rating_service_willingness 和 rating_revisit_likelihood 在 1-5 范围 - --- 5. 验证种子数据:4 条触发器配置 -SELECT job_name, job_type, trigger_condition, - trigger_config->>'cron_expression' AS cron, - trigger_config->>'interval_seconds' AS interval_sec, - trigger_config->>'event_name' AS event +-- 10. 验证种子数据:4 条触发器配置(P4 原有) +SELECT job_name, job_type, trigger_condition FROM biz.trigger_jobs WHERE job_name IN ('task_generator', 'task_expiry_check', 'recall_completion_check', 'note_reclassify_backfill') ORDER BY job_name; -- 预期:返回 4 行 --- note_reclassify_backfill | event | recall_completed --- recall_completion_check | event | etl_data_updated --- task_expiry_check | interval| interval_seconds=3600 --- task_generator | cron | 0 4 * * * --- 6. 验证查询索引存在 -SELECT indexname -FROM pg_indexes -WHERE schemaname = 'biz' - AND indexname IN ('idx_coach_tasks_assistant_status', 'idx_notes_target') -ORDER BY indexname; --- 预期:返回 2 行 +-- 11. 验证 P18 新增字段:trigger_jobs.last_stats +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_schema = 'biz' AND table_name = 'trigger_jobs' AND column_name = 'last_stats'; +-- 预期:返回 1 行,data_type = 'jsonb',is_nullable = 'YES' + +-- 12. 验证 P18 新增字段:cfg_task_generator_params.updated_by +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_schema = 'biz' AND table_name = 'cfg_task_generator_params' AND column_name = 'updated_by'; +-- 预期:返回 1 行,data_type = 'bigint',is_nullable = 'YES' ``` diff --git a/docs/database/BD_Manual_fdw_etl_setup.md b/docs/database/BD_Manual_fdw_etl_setup.md index c70a5af..1aa3609 100644 --- a/docs/database/BD_Manual_fdw_etl_setup.md +++ b/docs/database/BD_Manual_fdw_etl_setup.md @@ -34,9 +34,11 @@ ### 导入的外部表 -通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 38 张,2026-03-19 新增 3 张 BOARD 看板视图): +通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 46 张,含 11 DWD + 23 DWS + 5 cfg + 7 快捷别名): - 11 张 DWD 视图:`v_dim_member`、`v_dim_assistant`、`v_dim_member_card_account`、`v_dim_table`、`v_dwd_settlement_head`、`v_dwd_table_fee_log`、`v_dwd_assistant_service_log`、`v_dwd_recharge_order`、`v_dwd_store_goods_sale`、`v_dim_staff`、`v_dim_staff_ex` -- 27 张 DWS 视图(含 4 张 cfg_* 配置表):`v_dws_member_consumption_summary`、`v_dws_member_visit_detail` 等 +- 23 张 DWS 视图(含 `v_dws_assistant_project_tag`、`v_dws_member_project_tag`、`v_dws_member_spending_power_index`、`v_dws_assistant_order_contribution` 等) +- 5 张 cfg_* 配置表视图(`v_cfg_performance_tier`、`v_cfg_assistant_level_price`、`v_cfg_bonus_rules`、`v_cfg_index_parameters`、`v_cfg_area_category`) +- 7 张快捷别名视图(`v_assistant`、`v_assistant_daily`、`v_finance_daily`、`v_member`、`v_member_consumption`、`v_order_summary`、`v_site`) ### 权限配置 diff --git a/docs/database/BD_Manual_fdw_reverse_retention_clue.md b/docs/database/BD_Manual_fdw_reverse_retention_clue.md index 2439816..76f5ca7 100644 --- a/docs/database/BD_Manual_fdw_reverse_retention_clue.md +++ b/docs/database/BD_Manual_fdw_reverse_retention_clue.md @@ -217,6 +217,7 @@ SELECT n.nspname AS schema_name, d.defaclacl AS default_acl | 项目 | 状态 | 说明 | |------|------|------| | `source` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `source VARCHAR(20) NOT NULL DEFAULT 'manual'`(2026-02-27),FDW 外部表定义未包含。当前无 ETL 任务需要此字段,但未来如需读取线索来源需先更新外部表 | +| `is_hidden` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `is_hidden BOOLEAN NOT NULL DEFAULT false`(2026-03-20,NS4 迁移),FDW 外部表定义未包含。如 ETL 任务需要过滤隐藏线索需先更新外部表 | | DWS 任务消费 | 📋 待规划 | 原 `member_birthday_manual` 的 DWS 消费逻辑已移除。维客线索的 DWS 聚合任务尚未规划 | ### source 列同步方法(备用) diff --git a/docs/database/BD_Manual_member_retention_clue.md b/docs/database/BD_Manual_member_retention_clue.md index cd3fc11..c889322 100644 --- a/docs/database/BD_Manual_member_retention_clue.md +++ b/docs/database/BD_Manual_member_retention_clue.md @@ -14,6 +14,7 @@ | zqyy_app / test_zqyy_app | public | member_retention_clue | 新建 | 维客线索表 | | zqyy_app / test_zqyy_app | public | member_retention_clue.source | 新增列 | 2026-02-27 补齐线索来源字段 | | zqyy_app / test_zqyy_app | public | member_retention_clue.category | 约束变更 | 2026-03-08 枚举对齐:`客户基础信息` → `客户基础` | +| zqyy_app / test_zqyy_app | public | member_retention_clue.is_hidden | 新增列 | 2026-03-20 NS4 租户管理后台:隐藏/显示控制 | ### 表结构 @@ -29,6 +30,7 @@ | recorded_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录时间 | | site_id | BIGINT | NOT NULL | 门店 ID(多门店隔离) | | source | VARCHAR(20) | NOT NULL DEFAULT 'manual' | 线索来源(2026-02-27 新增) | +| is_hidden | BOOLEAN | NOT NULL DEFAULT false | 是否隐藏(true=管理后台保留但小程序不展示)(2026-03-20 新增) | ### category 枚举值 @@ -65,6 +67,7 @@ - **后端 API**:`POST /api/member-birthday` 废弃,替换为 `POST /api/retention-clue`、`GET /api/retention-clue/{member_id}`、`DELETE /api/retention-clue/{clue_id}` - **source 字段**(2026-02-27):`POST /api/retention-clue` 接受可选 `source` 参数,默认 `manual`;`GET` 返回中包含 `source` 字段。已有数据自动填充 `DEFAULT 'manual'`,向后兼容 +- **is_hidden 字段**(2026-03-20 NS4):租户管理后台线索隐藏/显示控制。`DEFAULT false` 保证已有数据兼容。小程序端线索查询已追加 `WHERE is_hidden = false` 条件,隐藏线索仅在管理后台可见。租户管理后台路由 `tenant_clues.py` 提供 `PATCH /api/tenant/clues/{id}/visibility` 端点切换状态 - **ETL Connector**:DWS 任务移除 FDW 读取 `member_birthday_manual` 的逻辑,生日仅从 `dim_member.birthday`(API 来源)读取 - **FDW**:`fdw_app.member_birthday_manual` 外部表需在 ETL 库侧同步更新为 `fdw_app.member_retention_clue`(含 `source` 列) - **小程序**:助教端调用新 API 提交维客线索 @@ -102,13 +105,13 @@ SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'member_retention_clue'; -- 预期:1 行 --- 3. 确认列结构完整(10 列) +-- 3. 确认列结构完整(11 列) SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'member_retention_clue' ORDER BY ordinal_position; -- 预期:id, member_id, category, summary, detail, --- recorded_by_assistant_id, recorded_by_name, recorded_at, site_id, source +-- recorded_by_assistant_id, recorded_by_name, recorded_at, site_id, source, is_hidden -- 4. 确认 CHECK 约束 SELECT conname FROM pg_constraint @@ -143,6 +146,23 @@ SELECT col_description( -- 9. 确认已有数据的 source 分布 SELECT source, COUNT(*) FROM member_retention_clue GROUP BY source; -- 预期:全部为 'manual'(或空表) + +-- 10. 确认 is_hidden 列存在且默认值正确(2026-03-20) +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 + +-- 11. 确认已有数据的 is_hidden 分布(2026-03-20) +SELECT is_hidden, COUNT(*) FROM member_retention_clue GROUP BY is_hidden; +-- 预期:全部为 false(DEFAULT false 保证兼容) + +-- 12. 确认列结构完整(11 列,含 is_hidden) +SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'member_retention_clue'; +-- 预期:11 ``` ## 关联文件 @@ -150,9 +170,11 @@ SELECT source, COUNT(*) FROM member_retention_clue GROUP BY source; - 迁移脚本(建表):`db/zqyy_app/migrations/2026-02-26__refactor_birthday_to_retention_clue.sql` - 迁移脚本(source 列):`db/zqyy_app/migrations/2026-02-27__add_source_to_retention_clue.sql` - 迁移脚本(category 枚举对齐):`db/zqyy_app/migrations/2026-03-08__align_retention_clue_category_enum.sql` +- 迁移脚本(is_hidden 列):`db/zqyy_app/migrations/2026-03-xx__ns4_member_clue_is_hidden.sql` - FDW 反向映射(生产):`db/fdw/setup_fdw_reverse.sql` - FDW 反向映射(测试):`db/fdw/setup_fdw_reverse_test.sql` - 后端路由:`apps/backend/app/routers/member_retention_clue.py` +- 后端路由(租户管理后台):`apps/backend/app/routers/tenant_clues.py` - 后端模型:`apps/backend/app/schemas/member_retention_clue.py` - H5 原型:`docs/h5_ui/pages/customer-detail.html`、`docs/h5_ui/pages/task-detail.html` - 旧表文档(已归档):`docs/database/_archived/BD_Manual_member_birthday_manual.md` diff --git a/docs/database/BD_manual_auth_users_avatar.md b/docs/database/BD_manual_auth_users_avatar.md new file mode 100644 index 0000000..16ad666 --- /dev/null +++ b/docs/database/BD_manual_auth_users_avatar.md @@ -0,0 +1,53 @@ +# BD 手册:auth.users.avatar_url 字段 + +## 概述 + +`auth.users` 表新增 `avatar_url` 字段,存储用户头像的相对路径。 + +## 字段定义 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `avatar_url` | `VARCHAR(500)` | `NULL` | 头像相对路径,格式 `avatars/{user_id}.jpg` | + +## 数据流 + +1. 小程序端通过 `