feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系

## P1 数据库基础
- zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu
- etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表
- 清理 assistant_abolish 残留数据

## P2 ETL/DWS 扩展
- 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution)
- 新增 assistant_order_contribution_task 任务及 RLS 视图
- member_consumption 增加充值字段、assistant_daily 增加处罚字段
- 更新 ODS/DWD/DWS 任务文档及业务规则文档
- 更新 consistency_checker、flow_runner、task_registry 等核心模块

## P3 小程序鉴权系统
- 新增 xcx_auth 路由/schema(微信登录 + JWT)
- 新增 wechat/role/matching/application 服务层
- zqyy_app 鉴权表迁移 + 角色权限种子数据
- auth/dependencies.py 支持小程序 JWT 鉴权

## 文档与审计
- 新增 DOCUMENTATION-MAP 文档导航
- 新增 7 份 BD_Manual 数据库变更文档
- 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth)
- 新增全栈集成审计记录、部署检查清单更新
- 新增 BACKLOG 路线图、FDW→Core 迁移计划

## Kiro 工程化
- 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务)
- 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan)
- 新增 6 个 Hook(合规检查/会话日志/提交审计等)
- 新增 doc-map steering 文件

## 运维与测试
- 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告
- 新增属性测试:test_dws_contribution / test_auth_system
- 清理过期 export 报告文件
- 更新 .gitignore 排除规则
This commit is contained in:
Neo
2026-02-26 08:03:53 +08:00
parent fafc95e64c
commit b25308c3f4
224 changed files with 17660 additions and 32198 deletions

230
docs/DOCUMENTATION-MAP.md Normal file
View File

@@ -0,0 +1,230 @@
# NeoZQYY 文档地图
> 本文档记录项目中所有文档资产的位置、类型和内容概要,方便快速定位。
---
## 一、根目录
| 文件 | 内容 |
|------|------|
| `README.md` | 项目总览:模块表(含文档链接)、技术栈、快速开始命令 |
| `.env.template` | 环境变量模板,列出所有可配置项及说明 |
---
## 二、项目级文档 `docs/`
### 2.1 文档中心首页
| 文件 | 内容 |
|------|------|
| `docs/README.md` | 项目架构图、模块文档索引、技术栈速览、认证体系概览、四库架构、常用命令 |
| `docs/etl-feiqiu-architecture.md` | ETL Connector 整体架构说明 |
### 2.2 数据库变更手册 `docs/database/`
每个 `BD_Manual_*.md` 是一次数据库结构变更的完整审计文档,包含:变更说明、字段定义、影响分析、回滚策略、验证 SQL。
| 文件 | 记录内容 |
|------|----------|
| `BD_Manual_auth_tables.md` | auth Schema 8 张认证表users、roles、permissions、user_applications 等) |
| `BD_Manual_auth_biz_schemas.md` | auth + biz Schema 创建 |
| `BD_Manual_fdw_etl_setup.md` | FDW 跨库访问配置zqyy_app → etl_feiqiu |
| `BD_Manual_app_schema_rls_views.md` | app Schema RLS 视图 |
| `BD_Manual_dws_assistant_order_contribution.md` | DWS 助教订单贡献表 |
| `BD_Manual_dws_goods_stock_summary.md` | DWS 商品库存汇总表 |
| `BD_Manual_dws_member_spending_power_index.md` | DWS 会员消费力指数表 |
| `BD_Manual_member_balance_changes.md` | 会员余额变动表 |
| `BD_Manual_recharge_settlements.md` | 充值结算表 |
| `BD_Manual_goods_stock_movements.md` | 商品库存流水表 |
| `BD_Manual_goods_stock_summary.md` | 商品库存汇总表 |
| `BD_Manual_goods_stock_warning_info.md` | 商品库存预警表 |
| `BD_Manual_store_goods_master.md` | 门店商品主表 |
| `BD_Manual_store_goods_sales_records.md` | 门店商品销售记录 |
| `BD_Manual_tenant_goods_master.md` | 租户商品主表 |
| `BD_Manual_assistant_accounts_master.md` | 助教账户主表 |
| `BD_Manual_assistant_service_records.md` | 助教服务记录表 |
| `BD_Manual_site_tables_master.md` | 门店台桌主表 |
| `README.md` | 数据库文档目录说明 |
子目录:
| 目录 | 内容 |
|------|------|
| `ddl/` | 9 个 DDL 基线文件,覆盖 etl_feiqiu 六层 Schemameta/ods/dwd/core/dws/app+ zqyy_app 两个 Schemaauth/public+ FDW |
| `_archived/` | 10 个已归档的历史变更文档(已废弃的表、已回滚的变更等) |
### 2.3 审计记录 `docs/audit/`
项目变更的完整审计追踪体系。
| 路径 | 内容 |
|------|------|
| `audit_dashboard.md` | 审计仪表盘,汇总所有变更审计记录 |
| `README.md` | 审计目录说明 |
| `changes/` | 31 份变更审计文档(`YYYY-MM-DD__<slug>.md` 格式),每份包含:变更原因、影响范围、回滚策略、验证 SQL |
| `prompt_logs/` | ~500 份 Prompt 日志(`prompt_log_YYYYMMDD_HHMMSS.md`),记录每次 AI 交互的输入输出 |
### 2.4 数据契约 `docs/contracts/`
| 路径 | 内容 |
|------|------|
| `openapi/backend-api.json` | 后端 API 的 OpenAPI 规范文件 |
| `data_dictionary/` | 数据字典(预留,待填充) |
| `schemas/` | 数据 Schema 定义(预留,待填充) |
### 2.5 部署文档 `docs/deployment/`
| 文件 | 内容 |
|------|------|
| `LAUNCH-CHECKLIST.md` | 上线检查清单:环境配置、数据库迁移、服务启动、验证步骤 |
| `EXPORT-PATHS.md` | 输出路径规范:环境变量映射表、目录结构、新增场景检查清单 |
### 2.6 产品需求 `docs/prd/`
| 路径 | 内容 |
|------|------|
| `小程序前后端.txt` | 小程序前后端原始需求描述 |
| `PRD审阅-Q&A.md` | PRD 审阅问答记录(第一轮) |
| `PRD审阅-Q&A-R2.md` | PRD 审阅问答记录(第二轮) |
| `SPI 消费力指数.md` | 消费力指数SPI算法需求说明 |
| `specs/00-数据依赖矩阵.md` | 各 SPEC 间的数据依赖关系矩阵 |
| `specs/01-SPEC任务拆分总览.md` | 11 个 SPEC 的任务拆分总览 |
| `specs/P1~P11` | 11 份 SPEC 拆分文档,覆盖:数据库基础(P1)、ETL DWS 扩展(P2)、认证系统(P3)、核心业务(P4)、AI 集成(P5)、前端任务/绩效/看板/详情(P6-P9)、租户管理后台(P10)、部署上线(P11) |
### 2.7 小程序 UI 原型 `docs/h5_ui/`
H5 静态原型页面,用于小程序 UI 设计参考。
| 路径 | 内容 |
|------|------|
| `index.html` | 原型首页入口 |
| `pages/` | 23 个页面原型,包括:登录(`login`)、申请(`apply`)、审核中(`reviewing`)、无权限(`no-permission`)、任务列表/详情(`task-list`/`task-detail`)、绩效(`performance`/`performance-records`)、助教详情(`coach-detail`)、客户详情(`customer-detail`)、客户服务记录(`customer-service-records`)、看板(`board-coach`/`board-customer`/`board-finance`)、聊天(`chat`/`chat-history`)、个人中心(`my-profile`)、首页设置(`home-settings`)、笔记(`notes`)、AI 图标演示(`ai-icon-demo`) |
| `css/` | 6 个样式文件 |
| `js/` | 8 个交互脚本 |
| `img/` | 图片资源 |
### 2.8 其他项目级文档目录
| 路径 | 内容 |
|------|------|
| `docs/architecture/` | 架构文档(预留,待填充) |
| `docs/roadmap/BACKLOG.md` | 项目待办事项 |
| `docs/roadmap/2026-02-24__fdw-dwd-to-core-migration-plan.md` | FDW + DWD→Core 迁移计划 |
| `docs/migrate/monorepo-migration-summary.md` | Monorepo 迁移总结 |
| `docs/migrate/oldworkspace-kiro-agent-config-summary.md` | 旧工作区 Kiro 配置迁移记录 |
| `docs/ops/` | 运维文档(预留,待填充) |
| `docs/permission_matrix/` | 权限矩阵(预留,待填充) |
| `docs/spec-input/2026-02-22__etl-aggregation-fix-spec-input.md` | ETL 聚合修复的 Spec 输入文档 |
| `docs/etl-feiqiu-architecture.md` | ETL Connector 整体架构说明 |
---
## 三、模块内部文档
### 3.1 FastAPI 后端 `apps/backend/`
| 文件 | 内容 |
|------|------|
| `README.md` | 架构概览、双库连接、认证系统、13 个路由模块摘要、服务层、配置加载 |
| `docs/API-REFERENCE.md` | 完整 API 参考13 个路由模块的所有端点、请求/响应示例、认证要求、错误码 |
### 3.2 ETL Connector `apps/etl/connectors/feiqiu/`
| 路径 | 内容 |
|------|------|
| `README.md` | Connector 总览、快速开始、CLI 用法 |
| `docs/README.md` | 文档目录索引 |
| `docs/CHANGELOG.md` | 变更日志 |
| `docs/api-reference/` | 上游飞球 API 接口文档(字段映射、请求参数、响应结构) |
| `docs/architecture/` | 架构设计文档数据流、分层设计、SCD 策略) |
| `docs/business-rules/` | 业务规则文档(金额精度、时区处理、去重逻辑) |
| `docs/database/` | ETL 数据库文档Schema 设计、表结构、索引策略) |
| `docs/etl_tasks/` | ETL 任务文档(每个任务的输入输出、依赖、调度配置) |
| `docs/operations/` | 运维文档(监控、告警、故障排查) |
| `docs/requirements/` | 需求文档(功能需求、非功能需求) |
### 3.3 微信小程序 `apps/miniprogram/`
| 文件 | 内容 |
|------|------|
| `README.md` | 后端 API 集成、认证流程、权限模型、关键端点说明 |
### 3.4 管理后台 `apps/admin-web/`
| 文件 | 内容 |
|------|------|
| `README.md` | 8 个页面、组件体系、API 层、状态管理、开发指南 |
### 3.5 MCP Server `apps/mcp-server/`
| 文件 | 内容 |
|------|------|
| `README.md` | MCP Server 功能说明、工具列表、配置方式 |
### 3.6 共享包 `packages/shared/`
| 文件 | 内容 |
|------|------|
| `README.md` | 3 个模块enums / money / datetime_utils的 API 文档及用法示例 |
---
## 四、数据库目录 `db/`
| 路径 | 内容 |
|------|------|
| `README.md` | 数据库目录总览、四库架构说明 |
| `zqyy_app/README.md` | 业务库文档auth Schema 8 张表字段说明、迁移顺序、FDW 跨库访问 |
| `zqyy_app/migrations/` | 业务库迁移脚本(日期前缀命名) |
| `etl_feiqiu/README.md` | ETL 库文档:六层 Schema 说明、表清单 |
| `etl_feiqiu/migrations/` | ETL 库迁移脚本(日期前缀命名) |
| `fdw/` | FDWForeign Data Wrapper跨库访问配置脚本 |
| `scripts/` | 数据库运维脚本 |
| `_archived/` | 已归档的历史数据库文件 |
---
## 五、Kiro 配置 `.kiro/`
### 5.1 Steering 文件(`.kiro/steering/`
13 个 Steering 文件,控制 AI 助手的行为规范:
| 文件 | 作用 |
|------|------|
| `language-zh.md` | 语言规范:输出简体中文,代码标识符保留英文 |
| `governance.md` | 治理规范:审计触发条件、执行方式、产物要求 |
| `product.md` / `product-full.md` | 产品概述(精简版 / 完整版) |
| `tech.md` / `tech-full.md` | 技术栈与构建(精简版 / 完整版) |
| `structure-lite.md` / `structure.md` | 项目结构(精简版 / 完整版) |
| `export-paths.md` / `export-paths-full.md` | 输出路径规范(精简版 / 完整版) |
| `testing-env.md` | 测试环境规范环境变量加载、cwd 要求、测试库使用 |
| `db-docs.md` | 数据库文档规范 |
| `steering-readme-maintainer.md` | README 维护者技能:变更影响审查与文档同步 |
### 5.2 Spec 文件(`.kiro/specs/`
17 个 Spec 目录,每个包含 `requirements.md``design.md``tasks.md` 三件套:
| Spec | 内容 |
|------|------|
| `01-miniapp-db-foundation` | P1小程序数据库基础建设 |
| `02-etl-dws-miniapp-extensions` | P2ETL DWS 小程序扩展 |
| `03-miniapp-auth-system` | P3小程序认证系统 |
| `[ETL]-fullstack-integration` | ETL 全栈集成 |
| `miniapp-core-business` | 小程序核心业务 |
| `miniapp-db-foundation` | 小程序数据库基础(早期版本) |
| `admin-web-console` | 管理后台控制台 |
| `etl-aggregation-fix` | ETL 聚合修复 |
| `etl-dws-flow-refactor` | ETL DWS 流程重构 |
| `etl-pipeline-debug` | ETL 管道调试 |
| `etl-staff-dimension` | ETL 员工维度 |
| `dwd-phase1-refactor` | DWD 第一阶段重构 |
| `ods-dedup-standardize` | ODS 去重标准化 |
| `spi-spending-power-index` | SPI 消费力指数 |
| `dataflow-field-completion` | 数据流字段补全 |
| `dataflow-structure-audit` | 数据流结构审计 |
| `assistant-abolish-cleanup` | 助教废除清理 |

View File

@@ -1,25 +1,153 @@
# docs/
# docs/ — 项目文档中心
## 作用说明
NeoZQYY Monorepo 的文档中心,存放产品需求、技术架构、数据契约、运维手册、审计记录等所有文档资产。
项目文档中心,存放产品需求、技术架构、数据契约、运维手册、审计记录等所有文档资产。
## 项目全貌
## 内部结构
NeoZQYY 是面向台球门店业务的全栈数据平台,包含 6 个子系统:
- `prd/` — 产品需求文档
- `contracts/` — 数据契约
- `openapi/` — OpenAPI 规范
- `schemas/` — JSON Schema
- `data_dictionary/` — 数据字典
- `permission_matrix/` — 权限矩阵
- `architecture/` — 架构设计文档
- `database/` — 数据库设计与变更文档
- `h5_ui/` — 小程序原型与 UI 设计稿
- `ops/` — 运维手册
- `audit/` — 项目级统一审计目录(变更记录 + Prompt 日志 + 审计一览表)
- `roadmap/` — 路线图
```
┌─────────────────────────────────────────────────────────────┐
│ 微信小程序C 端) │
│ apps/miniprogram/ │
│ TypeScript + TDesign + Donut 多端 │
└──────────────────────┬──────────────────────────────────────┘
│ REST API
┌──────────────────────▼──────────────────────────────────────┐
│ FastAPI 后端 │
│ apps/backend/ │
│ 13 个路由模块 · JWT 双认证 · WebSocket 日志 │
├──────────────┬───────────────────────┬──────────────────────┤
│ zqyy_app │ │ etl_feiqiu │
│ (业务库) │◄──── FDW 只读 ────────│ (ETL 数据仓库) │
│ auth/biz │ │ 6 层 Schema │
└──────────────┘ └──────────┬───────────┘
┌─────────────────────────────────────────────────▼───────────┐
│ ETL Connector │
│ apps/etl/connectors/feiqiu/ │
│ 飞球 SaaS API → ODS → DWD → DWS 三层处理 │
└─────────────────────────────────────────────────────────────┘
## Roadmap
┌──────────────────────┐ ┌──────────────────────┐
│ 管理后台 │ │ MCP Server │
│ apps/admin-web/ │ │ apps/mcp-server/ │
│ React+Vite+AntDesign │ │ AI 工具集成 │
└──────────────────────┘ └──────────────────────┘
- 补充 ADR架构决策记录模板
- 完善数据字典,覆盖所有 schema 表字段
┌─────────────────────────────────────────────────────────────┐
│ 共享包 packages/shared/ │
│ 枚举 · 金额精度 · 时间工具 │
└─────────────────────────────────────────────────────────────┘
```
## 模块文档索引
### 应用模块(各模块 README
| 模块 | 路径 | 说明 |
|------|------|------|
| ETL Connector | [`apps/etl/connectors/feiqiu/docs/`](../apps/etl/connectors/feiqiu/docs/) | 架构、API 参考、业务规则、运维文档 |
| FastAPI 后端 | [`apps/backend/README.md`](../apps/backend/README.md) | 架构、路由总览、认证体系、服务层 |
| 后端 API 参考 | [`apps/backend/docs/API-REFERENCE.md`](../apps/backend/docs/API-REFERENCE.md) | 全部 API 端点详细说明 |
| 微信小程序 | [`apps/miniprogram/README.md`](../apps/miniprogram/README.md) | 开发指南、认证流程、API 集成 |
| 管理后台 | [`apps/admin-web/README.md`](../apps/admin-web/README.md) | 页面功能、组件、状态管理 |
| MCP Server | [`apps/mcp-server/README.md`](../apps/mcp-server/README.md) | 工具说明、安全策略、配置 |
| 共享包 | [`packages/shared/README.md`](../packages/shared/README.md) | 枚举、金额精度、时间工具 API |
### 数据库文档
| 文档 | 路径 | 说明 |
|------|------|------|
| zqyy_app 架构 | [`db/zqyy_app/README.md`](../db/zqyy_app/README.md) | 业务库 Schema、表结构、迁移顺序 |
| etl_feiqiu 架构 | [`db/etl_feiqiu/README.md`](../db/etl_feiqiu/README.md) | ETL 六层 Schema 说明 |
| BD 手册 | [`docs/database/`](database/) | 各表的详细变更文档BD_Manual_*.md |
### 项目级文档
| 目录 | 说明 |
|------|------|
| `prd/` | 产品需求文档 |
| `contracts/` | 数据契约OpenAPI、JSON Schema、数据字典 |
| `permission_matrix/` | 权限矩阵 |
| `architecture/` | 架构设计文档 |
| `database/` | 数据库设计与变更文档BD 手册) |
| `deployment/` | 部署文档(启动清单、输出路径规范) |
| `h5_ui/` | 小程序原型与 UI 设计稿 |
| `ops/` | 运维手册 |
| `audit/` | 统一审计目录(变更记录 + Prompt 日志) |
| `roadmap/` | 路线图 |
| `migrate/` | 迁移指南 |
| `spec-input/` | Spec 输入文档 |
## 技术栈速览
| 层级 | 技术 |
|------|------|
| 后端 | Python 3.10+ / FastAPI / Uvicorn / psycopg2 |
| 前端(管理后台) | React 19 / Vite 6 / Ant Design 5 / Zustand / TypeScript |
| 前端(小程序) | 微信原生 + Donut + TDesign / TypeScript |
| 数据库 | PostgreSQL4 库etl_feiqiu / test_etl_feiqiu / zqyy_app / test_zqyy_app |
| 包管理 | Python: uv workspace / 前端: pnpm |
| AI 集成 | MCP ServerPostgreSQL 只读查询) |
## 数据库四库架构
| 库名 | 用途 | 连接变量 |
|------|------|----------|
| `etl_feiqiu` | ETL 数据仓库6 层 Schema | `PG_DSN` |
| `test_etl_feiqiu` | ETL 测试库 | `TEST_DB_DSN` |
| `zqyy_app` | 业务数据(认证、队列、调度) | `APP_DB_DSN` |
| `test_zqyy_app` | 业务测试库 | 默认连接 |
ETL 六层 Schema`meta``ods``dwd``core``dws``app`
## 认证体系概览
系统支持两套独立认证:
| 认证方式 | 入口 | 用户来源 | 令牌 |
|----------|------|----------|------|
| 管理后台 | `/api/auth/login` | `admin_users` 表 | JWT用户名+密码) |
| 小程序 | `/api/xcx-auth/login` | `auth.users` 表 | JWT微信 code |
小程序认证流程:微信登录 → 提交申请 → 管理员审批 → 正式使用
## 多门店隔离
- 业务数据通过 `site_id` 隔离
- ETL 数据库使用 RLSRow Level Security
- 后端 JWT 令牌携带 `site_id`,所有查询自动过滤
- 用户可关联多个门店,通过 API 切换
## 配置体系
优先级(低 → 高):根 `.env` < 应用 `.env.local` < 环境变量 < CLI 参数
关键环境变量见 `.env.template`
## 常用命令
```bash
# 安装依赖
uv sync --all-packages
# 启动后端
cd apps/backend && uv run uvicorn app.main:app --reload
# 启动管理后台
cd apps/admin-web && pnpm dev
# ETL 执行
cd apps/etl/connectors/feiqiu && python -m cli.main --dry-run --tasks DWD_LOAD_FROM_ODS
# 测试
cd apps/etl/connectors/feiqiu && pytest tests/unit
cd C:\NeoZQYY && pytest tests/ -v
```
## 文件归属规则
- 模块专属的 docs/tests/scripts → 放模块内部
- 项目级/跨模块的 docs/tests/scripts → 放根目录
- 审计产物统一写 `docs/audit/`,禁止写入子模块内部

View File

@@ -1,11 +1,12 @@
# 审计一览表
> 自动生成于 2026-02-16 03:44:50,请勿手动编辑。
> 自动生成于 2026-02-26 06:28:25,请勿手动编辑。
## 时间线视图
| 日期 | 项目 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|------|----------|----------|----------|------|------|
| 2026-02-26 | 项目级 | 变更审计P1/P2/P3 全栈集成DB 基础 + ETL DWS 扩展 + 小程序鉴权) | bugfix | 其他 | 低 | [链接](changes/2026-02-26__p1-p2-p3-fullstack-integration.md) |
| 2026-02-15 | 项目级 | 审计记录:管理后台全量实现 + DB Schema 迁移 + 审计产物重组 | 清理 | 其他 | 高 | [链接](changes/2026-02-15__admin-web-console-db-migration-audit-reorg.md) |
| 2026-02-15 | ETL-feiqiu, 项目级 | 变更审计记录Change Audit Record | 文档 | 其他, 文档, 质量校验 | 极低 | [链接](changes/2026-02-15__audit-consolidation-doc-reorg.md) |
| 2026-02-15 | 后端 | 审计记录:后端依赖补全使 FastAPI 可启动 | bugfix | 其他 | 未知 | [链接](changes/2026-02-15__backend-deps-bootstrap.md) |
@@ -85,6 +86,7 @@
| 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|----------|----------|----------|------|------|
| 2026-02-26 | 变更审计P1/P2/P3 全栈集成DB 基础 + ETL DWS 扩展 + 小程序鉴权) | bugfix | 其他 | 低 | [链接](changes/2026-02-26__p1-p2-p3-fullstack-integration.md) |
| 2026-02-15 | 审计记录:管理后台全量实现 + DB Schema 迁移 + 审计产物重组 | 清理 | 其他 | 高 | [链接](changes/2026-02-15__admin-web-console-db-migration-audit-reorg.md) |
| 2026-02-15 | 变更审计记录Change Audit Record | 文档 | 其他, 文档, 质量校验 | 极低 | [链接](changes/2026-02-15__audit-consolidation-doc-reorg.md) |
| 2026-02-15 | 审计记录docs/bd_manual + docs/dictionary → docs/database 合并 | 清理 | 其他, 文档, 脚本工具 | 极低 | [链接](changes/2026-02-15__docs-database-merge.md) |
@@ -132,6 +134,7 @@
| 日期 | 需求摘要 | 变更类型 | 风险 | 详情 |
|------|----------|----------|------|------|
| 2026-02-26 | 变更审计P1/P2/P3 全栈集成DB 基础 + ETL DWS 扩展 + 小程序鉴权) | bugfix | 低 | [链接](changes/2026-02-26__p1-p2-p3-fullstack-integration.md) |
| 2026-02-15 | 审计记录:管理后台全量实现 + DB Schema 迁移 + 审计产物重组 | 清理 | 高 | [链接](changes/2026-02-15__admin-web-console-db-migration-audit-reorg.md) |
| 2026-02-15 | 变更审计记录Change Audit Record | 文档 | 极低 | [链接](changes/2026-02-15__audit-consolidation-doc-reorg.md) |
| 2026-02-15 | 审计记录:后端依赖补全使 FastAPI 可启动 | bugfix | 未知 | [链接](changes/2026-02-15__backend-deps-bootstrap.md) |

View File

@@ -0,0 +1,135 @@
# BD_Manualapp Schema 与 RLS 视图层
> 目标库:`test_etl_feiqiu`(通过 `PG_DSN` 连接)
> 迁移脚本:`db/etl_feiqiu/migrations/2026-02-24__p1_create_app_schema_rls_views.sql`
> DDL 位置:`docs/database/ddl/etl_feiqiu__app.sql`(执行后需重新生成)
> 关联 SPEC`miniapp-db-foundation`P1 基础设施层)
---
## 1. 变更说明
### 新增 Schema
- `app`RLS 视图层,供业务库通过 `postgres_fdw` 只读访问 ETL 数据
### 新增角色
- `app_reader`:只读角色(`LOGIN`),拥有 `app` Schema 的 `USAGE` + `SELECT` 权限
### 新增视图35 张)
**DWD 层11 张,全部含 `site_id` 过滤):**
| 视图 | 源表 | 过滤条件 |
|------|------|---------|
| `app.v_dim_member` | `dwd.dim_member` | `site_id = current_setting('app.current_site_id')::bigint` |
| `app.v_dim_assistant` | `dwd.dim_assistant` | 同上 |
| `app.v_dim_member_card_account` | `dwd.dim_member_card_account` | 同上 |
| `app.v_dim_table` | `dwd.dim_table` | 同上 |
| `app.v_dwd_settlement_head` | `dwd.dwd_settlement_head` | 同上 |
| `app.v_dwd_table_fee_log` | `dwd.dwd_table_fee_log` | 同上 |
| `app.v_dwd_assistant_service_log` | `dwd.dwd_assistant_service_log` | 同上 |
| `app.v_dwd_recharge_order` | `dwd.dwd_recharge_order` | 同上 |
| `app.v_dwd_store_goods_sale` | `dwd.dwd_store_goods_sale` | 同上 |
| `app.v_dim_staff` | `dwd.dim_staff` | 同上 |
| `app.v_dim_staff_ex` | `dwd.dim_staff_ex` | 同上 |
**DWS 层 — 含 `site_id` 过滤20 张):**
| 视图 | 源表 |
|------|------|
| `app.v_dws_member_consumption_summary` | `dws.dws_member_consumption_summary` |
| `app.v_dws_member_visit_detail` | `dws.dws_member_visit_detail` |
| `app.v_dws_member_winback_index` | `dws.dws_member_winback_index` |
| `app.v_dws_member_newconv_index` | `dws.dws_member_newconv_index` |
| `app.v_dws_member_recall_index` | `dws.dws_member_recall_index` |
| `app.v_dws_member_assistant_relation_index` | `dws.dws_member_assistant_relation_index` |
| `app.v_dws_member_assistant_intimacy` | `dws.dws_member_assistant_intimacy` |
| `app.v_dws_assistant_daily_detail` | `dws.dws_assistant_daily_detail` |
| `app.v_dws_assistant_monthly_summary` | `dws.dws_assistant_monthly_summary` |
| `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_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` |
| `app.v_dws_finance_discount_detail` | `dws.dws_finance_discount_detail` |
| `app.v_dws_finance_expense_summary` | `dws.dws_finance_expense_summary` |
| `app.v_dws_platform_settlement` | `dws.dws_platform_settlement` |
| `app.v_dws_assistant_recharge_commission` | `dws.dws_assistant_recharge_commission` |
| `app.v_dws_order_summary` | `dws.dws_order_summary` |
**DWS 层 — cfg_* 配置表4 张,无 `site_id`,直接 `SELECT *`**
| 视图 | 源表 | 说明 |
|------|------|------|
| `app.v_cfg_performance_tier` | `dws.cfg_performance_tier` | 无 `site_id` 列,不加过滤 |
| `app.v_cfg_assistant_level_price` | `dws.cfg_assistant_level_price` | 同上 |
| `app.v_cfg_bonus_rules` | `dws.cfg_bonus_rules` | 同上 |
| `app.v_cfg_index_parameters` | `dws.cfg_index_parameters` | 同上 |
### 权限配置
| 角色 | Schema | 权限 |
|------|--------|------|
| `app_reader` | `app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` |
### P2 预留(注释形式,暂不创建)
- `dws.dws_member_spending_power_index``app.v_dws_member_spending_power_index`
- `dws.dws_assistant_order_contribution``app.v_dws_assistant_order_contribution`
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。视图仅读取 DWD/DWS 表,不影响 ETL 写入流程 |
| 后端 API | 前置依赖。后端通过 FDW 读取 `app` Schema 视图,本脚本是 FDW 配置的前提 |
| 小程序 | 无直接影响。小程序通过后端 API 间接访问 |
| 管理后台 | 无直接影响 |
| 现有 `app` Schema | 已有 7 个视图将被 `CREATE OR REPLACE` 覆盖更新,新增 28 个视图 |
---
## 3. 回滚策略
迁移脚本末尾已包含注释形式的回滚语句,按逆序执行:
```sql
ALTER DEFAULT PRIVILEGES IN SCHEMA app REVOKE SELECT ON TABLES FROM app_reader;
REVOKE SELECT ON ALL TABLES IN SCHEMA app FROM app_reader;
REVOKE USAGE ON SCHEMA app FROM app_reader;
DROP SCHEMA IF EXISTS app CASCADE; -- 会删除所有视图
DROP ROLE IF EXISTS app_reader;
```
注意:`DROP SCHEMA app CASCADE` 会级联删除所有视图和依赖的 FDW 外部表,需先回滚 FDW 配置。
---
## 4. 验证 SQL
```sql
-- 1. 验证 app Schema 存在
SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app';
-- 2. 验证视图数量(应为 35 张)
SELECT count(*) FROM information_schema.views WHERE table_schema = 'app';
-- 3. 验证 app_reader 角色存在且有 app Schema 权限
SELECT has_schema_privilege('app_reader', 'app', 'USAGE') AS has_usage;
-- 4. 验证含 site_id 的视图定义包含 current_setting 过滤
SELECT table_name, view_definition
FROM information_schema.views
WHERE table_schema = 'app'
AND view_definition LIKE '%current_setting%'
ORDER BY table_name;
-- 5. 验证 cfg_* 视图不含 current_setting 过滤
SELECT table_name, view_definition
FROM information_schema.views
WHERE table_schema = 'app'
AND table_name LIKE 'v_cfg_%'
AND view_definition NOT LIKE '%current_setting%';
```

View File

@@ -0,0 +1,87 @@
# BD_Manualauth/biz Schema 与权限配置
> 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接)
> 迁移脚本:`db/zqyy_app/migrations/2026-02-24__p1_create_auth_biz_schemas.sql`
> 关联 SPEC`miniapp-db-foundation`P1 基础设施层)
---
## 1. 变更说明
### 新增 Schema
| Schema | 用途 |
|--------|------|
| `auth` | 用户认证、权限、微信 OpenID 映射等 |
| `biz` | 业务数据任务、备注、AI 分析、Excel 导出等) |
### 权限配置
| 角色 | Schema | 权限 |
|------|--------|------|
| `app_user` | `auth` | `USAGE` + `SELECT, INSERT, UPDATE, DELETE ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` |
| `app_user` | `biz` | `USAGE` + `SELECT, INSERT, UPDATE, DELETE ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` |
### 未操作的 Schema
- `public`:保留现有系统管理表(`admin_users``roles``permissions` 等)不受影响,脚本不包含任何对 `public` Schema 的操作
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。本脚本仅操作业务库,不涉及 ETL 库 |
| 后端 API | 前置依赖。后续业务表将创建在 `auth`/`biz` Schema 中,后端需使用 `auth.` / `biz.` 前缀或设置 `search_path` |
| 小程序 | 无直接影响。小程序通过后端 API 间接访问 |
| 管理后台 | 无直接影响 |
| FDW 配置 | 无影响。`fdw_etl` Schema 独立于 `auth`/`biz` |
| `public` Schema | 无影响。脚本不包含任何对 `public` 的操作 |
---
## 3. 回滚策略
迁移脚本末尾已包含注释形式的回滚语句,按逆序执行:
```sql
ALTER DEFAULT PRIVILEGES IN SCHEMA biz REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA auth REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM app_user;
REVOKE SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA biz FROM app_user;
REVOKE USAGE ON SCHEMA biz FROM app_user;
REVOKE SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth FROM app_user;
REVOKE USAGE ON SCHEMA auth FROM app_user;
DROP SCHEMA IF EXISTS biz CASCADE;
DROP SCHEMA IF EXISTS auth CASCADE;
```
注意:`DROP SCHEMA CASCADE` 会级联删除 Schema 内所有表和依赖对象。如果 `auth`/`biz` 中已有业务表,需先备份数据再执行回滚。
---
## 4. 验证 SQL
```sql
-- 1. 验证 auth 和 biz Schema 存在
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name IN ('auth', 'biz')
ORDER BY schema_name;
-- 2. 验证 app_user 对 auth Schema 有 USAGE 权限
SELECT has_schema_privilege('app_user', 'auth', 'USAGE') AS auth_usage,
has_schema_privilege('app_user', 'biz', 'USAGE') AS biz_usage;
-- 3. 验证 ALTER DEFAULT PRIVILEGES 已设置(查询 pg_default_acl
SELECT n.nspname AS schema_name,
d.defaclacl AS default_acl
FROM pg_default_acl d
JOIN pg_namespace n ON n.oid = d.defaclnamespace
WHERE n.nspname IN ('auth', 'biz');
-- 4. 验证 public Schema 中现有表未受影响
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
```

View File

@@ -0,0 +1,169 @@
# BD_Manualauth Schema 认证业务表
> 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接)
> 迁移脚本:
> - `db/zqyy_app/migrations/2026-02-25__p3_create_auth_tables.sql`(建表)
> - `db/zqyy_app/migrations/2026-02-25__p3_seed_roles_permissions.sql`(种子数据)
> 关联 SPEC`miniapp-auth-system`P3 小程序用户认证系统)
---
## 1. 变更说明
### 新增表8 张)
| # | 表名 | 用途 | 主要字段 |
|---|------|------|---------|
| 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` |
| 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` |
### 约束与索引
| 表 | 约束/索引名 | 类型 | 说明 |
|----|-----------|------|------|
| `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 查询加速 |
| `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` | `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 过滤加速 |
| `user_site_roles` | `uq_user_site_roles_user_site_role` | UNIQUE | 防止重复分配 |
| `user_site_roles` | `fk_user_site_roles_user_id` | FK | → `auth.users(id)` CASCADE |
| `user_site_roles` | `fk_user_site_roles_role_id` | FK | → `auth.roles(id)` CASCADE |
| `user_site_roles` | `ix_user_site_roles_user_site` | INDEX | (user_id, site_id) 查询加速 |
| `user_assistant_binding` | `fk_user_assistant_binding_user_id` | FK | → `auth.users(id)` CASCADE |
### 种子数据
#### 权限5 条)
| code | name | description |
|------|------|-------------|
| `view_tasks` | 查看任务 | 允许查看任务列表和任务详情 |
| `view_board` | 查看看板 | 允许查看数据看板概览 |
| `view_board_finance` | 查看财务看板 | 允许查看财务相关的数据看板 |
| `view_board_customer` | 查看客户看板 | 允许查看客户相关的数据看板 |
| `view_board_coach` | 查看助教看板 | 允许查看助教相关的数据看板 |
#### 角色4 条)
| code | name | description |
|------|------|-------------|
| `coach` | 助教 | 球房助教,可查看任务和助教看板 |
| `staff` | 员工 | 球房员工,可查看任务和数据看板 |
| `site_admin` | 店铺管理员 | 单店管理员,可查看所有看板 |
| `tenant_admin` | 租户管理员 | 租户级管理员,拥有全部权限 |
#### 角色-权限映射14 条)
| 角色 | 权限列表 | 权限数 |
|------|---------|--------|
| `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 |
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。本次变更仅操作业务库 `auth` Schema不涉及 ETL 库 |
| 后端 API | 直接依赖。FastAPI 后端将基于这些表实现微信登录、用户申请、审核、权限中间件等认证功能 |
| 小程序 | 间接依赖。小程序通过后端 API 间接使用这些表(登录、申请、状态查询) |
| 管理后台 | 间接依赖。管理端通过后端 API 进行申请审核操作 |
| FDW 配置 | 无影响。`fdw_etl` Schema 独立于 `auth`,但人员匹配服务会通过 FDW 查询 ETL 库的助教/员工表 |
| `public` Schema | 无影响。脚本不包含任何对 `public` 的操作 |
| 现有 `auth` Schema | 兼容。`auth` Schema 已由 P1 迁移脚本创建,本次仅在其中新增表,不修改已有对象 |
---
## 3. 回滚策略
按逆序 `DROP TABLE IF EXISTS CASCADE`(迁移脚本末尾已包含注释形式的回滚语句):
```sql
-- 先删除种子数据(如需保留表结构)
DELETE FROM auth.role_permissions
WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin'))
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.permissions WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach');
-- 删除表按逆序CASCADE 处理外键依赖)
DROP TABLE IF EXISTS auth.user_assistant_binding CASCADE;
DROP TABLE IF EXISTS auth.user_site_roles CASCADE;
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.users CASCADE;
```
注意:`CASCADE` 会级联删除依赖对象。如果表中已有业务数据,需先备份再执行回滚。
---
## 4. 验证 SQL
```sql
-- 1. 验证 auth Schema 下 8 张认证表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'auth'
AND table_name IN (
'users', 'user_applications', 'site_code_mapping',
'roles', 'permissions', 'role_permissions',
'user_site_roles', 'user_assistant_binding'
)
ORDER BY table_name;
-- 预期:返回 8 行
-- 2. 验证种子数据5 条权限
SELECT COUNT(*) AS perm_count
FROM auth.permissions
WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach');
-- 预期5
-- 3. 验证种子数据4 条角色
SELECT COUNT(*) AS role_count
FROM auth.roles
WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin');
-- 预期4
-- 4. 验证角色-权限映射数量
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 条映射)
-- 5. 验证关键约束存在
SELECT conname, contype
FROM pg_constraint
WHERE conrelid IN (
'auth.users'::regclass,
'auth.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 等
```

View File

@@ -0,0 +1,266 @@
# BD_Manualdws_assistant_order_contribution助教订单流水四项统计
> DWS 表:`dws.dws_assistant_order_contribution`
> DWD 数据源:`dwd.dwd_settlement_head`(结算主表)、`dwd.dwd_table_fee_log`(台费明细)、`dwd.dwd_assistant_service_log`(助教服务记录)
> 任务代码:`DWS_ASSISTANT_ORDER_CONTRIBUTION`
> 代码位置:`apps/etl/connectors/feiqiu/tasks/dws/assistant_order_contribution_task.py`
> DDL 位置:`docs/database/ddl/etl_feiqiu__dws.sql`
> 迁移脚本:`db/etl_feiqiu/migrations/2025-02-24__create_dws_assistant_order_contribution.sql`
> RLS 视图:`db/etl_feiqiu/migrations/2025-02-24__create_rls_view_assistant_order_contribution.sql`
> FDW 映射:`db/zqyy_app/migrations/2025-02-24__add_fdw_dws_extensions.sql`
---
## 1. 表结构
| 列名 | 类型 | 默认值 | 业务含义 | 取值范围/示例 |
|------|------|--------|---------|-------------|
| `contribution_id` | BIGINT (SERIAL) | nextval 序列 | 自增主键PK | 自增 |
| `site_id` | INTEGER NOT NULL | — | 门店 ID | 飞球门店 ID |
| `tenant_id` | INTEGER NOT NULL | — | 租户 ID | 飞球租户 ID |
| `assistant_id` | BIGINT NOT NULL | — | 助教 ID | 飞球助教 ID |
| `assistant_nickname` | VARCHAR(100) | NULL | 助教昵称 | 中文昵称 |
| `stat_date` | DATE NOT NULL | — | 统计日期 | `2025-01-15` |
| `order_gross_revenue` | NUMERIC(14,2) | 0 | 订单总流水 = 台费 + 酒水食品 + 所有助教服务费 | `0.00` ~ 金额值 |
| `order_net_revenue` | NUMERIC(14,2) | 0 | 订单净流水 = 订单总流水 - 所有助教服务分成 | `0.00` ~ 金额值 |
| `time_weighted_revenue` | NUMERIC(14,2) | 0 | 时效贡献流水 = 台费按时长分摊 + 个人服务费 + 酒水食品按时长比例 | `0.00` ~ 金额值 |
| `time_weighted_net_revenue` | NUMERIC(14,2) | 0 | 时效净贡献 = 时效贡献流水 - 个人服务分成 | `0.00` ~ 金额值 |
| `order_count` | INTEGER | 0 | 当日参与订单数 | `0` ~ 正整数 |
| `total_service_seconds` | INTEGER | 0 | 当日总服务时长(秒) | `0` ~ 正整数 |
| `created_at` | TIMESTAMPTZ | NOW() | 记录创建时间 | ISO 时间戳 |
| `updated_at` | TIMESTAMPTZ | NOW() | 记录最后更新时间 | ISO 时间戳 |
---
## 2. 主键与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|----|------|
| `dws_assistant_order_contribution_pkey` | PRIMARY KEY | `contribution_id` | 物理主键(自增序列) |
| `idx_aoc_site_assistant_date` | UNIQUE INDEX | `(site_id, assistant_id, stat_date)` | 业务主键:每个门店每个助教每天唯一一条记录 |
| `idx_aoc_stat_date` | INDEX | `(site_id, stat_date)` | 按门店+日期查询,支持日度报表 |
---
## 3. 数据写入策略
- **delete-before-insert**:每次执行按 `site_id` + 日期窗口全量刷新
1. `DELETE FROM dws.dws_assistant_order_contribution WHERE site_id = %s AND stat_date BETWEEN %s AND %s`
2. 批量 `INSERT` 新计算结果
- 继承 `BaseDwsTask` 默认 load 实现,幂等可重跑
---
## 4. 算法概要
### 4.1 数据来源
| 来源表 | 筛选条件 | 提取内容 |
|--------|---------|---------|
| `dwd.dwd_settlement_head` | 日期窗口内,`settle_type IN (1, 3)` | 结算单信息、酒水食品金额 |
| `dwd.dwd_table_fee_log` | 关联结算单 | 台桌使用时长、台费金额、区域 |
| `dwd.dwd_assistant_service_log` | 关联结算单 | 助教服务时长、服务流水、分成、课程类型 |
### 4.2 四项统计公式
**订单总流水order_gross_revenue**
```
order_gross_revenue = total_table_fee + total_goods_amount + SUM(所有助教 ledger_amount)
```
每个参与助教获得相同值。
**订单净流水order_net_revenue**
```
order_net_revenue = order_gross_revenue - SUM(所有助教 commission)
```
每个参与助教获得相同值。
**时效贡献流水time_weighted_revenue**
```
对于台桌 t
billable_seconds = MAX(SUM(助教服务时长), 台桌使用时长)
台费分摊_a = table_fee_t × (service_seconds_a / billable_seconds)
酒水食品分摊_a = total_goods_amount × (助教 a 总服务时长 / 所有助教总服务时长)
time_weighted_revenue_a = SUM(各台桌台费分摊_a) + ledger_amount_a + 酒水食品分摊_a
```
**时效净贡献time_weighted_net_revenue**
```
time_weighted_net_revenue_a = time_weighted_revenue_a - commission_a
```
### 4.3 超休/打赏课特殊处理
`course_type = BONUS` 时,四项统计均等于个人服务流水和分成,不参与订单级分摊。
---
## 5. 前置依赖
- 任务依赖:`DWD_LOAD_FROM_ODS`(需先完成 DWD 层数据加载)
- 数据源表:`dwd.dwd_settlement_head``dwd.dwd_table_fee_log``dwd.dwd_assistant_service_log` 必须已有数据
---
## 6. 验证 SQL
### 6.1 检查表是否存在且有数据
```sql
SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT site_id) AS site_count,
COUNT(DISTINCT assistant_id) AS assistant_count,
MIN(stat_date) AS earliest_date,
MAX(stat_date) AS latest_date
FROM dws.dws_assistant_order_contribution;
```
### 6.2 检查业务主键唯一性(不应有重复)
```sql
SELECT site_id, assistant_id, stat_date, COUNT(*) AS cnt
FROM dws.dws_assistant_order_contribution
GROUP BY site_id, assistant_id, stat_date
HAVING COUNT(*) > 1;
-- 预期:无结果返回
```
### 6.3 检查四项统计数值合理性(非负)
```sql
SELECT
COUNT(*) FILTER (WHERE order_gross_revenue < 0) AS neg_gross,
COUNT(*) FILTER (WHERE order_net_revenue < 0) AS neg_net,
COUNT(*) FILTER (WHERE time_weighted_revenue < 0) AS neg_twr,
COUNT(*) FILTER (WHERE time_weighted_net_revenue < 0) AS neg_twnr
FROM dws.dws_assistant_order_contribution;
-- 预期:所有列均为 0
```
### 6.4 按门店查看统计概况
```sql
SELECT
site_id,
COUNT(*) AS record_count,
SUM(order_count) AS total_orders,
ROUND(AVG(order_gross_revenue), 2) AS avg_gross,
ROUND(AVG(order_net_revenue), 2) AS avg_net,
ROUND(AVG(time_weighted_revenue), 2) AS avg_twr,
ROUND(AVG(time_weighted_net_revenue), 2) AS avg_twnr
FROM dws.dws_assistant_order_contribution
GROUP BY site_id
ORDER BY site_id;
```
---
## 7. RLS 视图与 FDW 映射
### 7.1 RLS 视图ETL 库 app schema
```sql
-- 视图名app.v_dws_assistant_order_contribution
CREATE OR REPLACE VIEW app.v_dws_assistant_order_contribution AS
SELECT * FROM dws.dws_assistant_order_contribution
WHERE site_id = current_setting('app.current_site_id')::bigint;
```
### 7.2 FDW 外部表(业务库 fdw_etl schema
```sql
-- 外部表名fdw_etl.v_dws_assistant_order_contribution
-- 通过 app schema RLS 视图访问,非直接访问 dws schema
CREATE FOREIGN TABLE fdw_etl.v_dws_assistant_order_contribution (...)
SERVER etl_server
OPTIONS (schema_name 'app', table_name 'v_dws_assistant_order_contribution');
```
---
## 8. 兼容性说明
| 影响范围 | 说明 |
|---------|------|
| ETL 任务 | 新增任务 `DWS_ASSISTANT_ORDER_CONTRIBUTION`,依赖 `DWD_LOAD_FROM_ODS`。不影响现有 DWS 任务 |
| 后端 API | 当前无 API 直接读取此表。后续小程序助教看板需新增接口 |
| 管理后台 | 当前无前端页面展示。后续可在助教详情页新增流水统计展示 |
| 小程序 | 小程序助教端将通过后端 API 读取此表数据展示四项统计 |
| 其他 DWS 表 | 独立于现有 `dws_assistant_daily_detail`,不修改任何已有表或任务逻辑 |
---
## 9. 回滚策略
### 9.1 删除数据(保留表结构)
```sql
DELETE FROM dws.dws_assistant_order_contribution;
```
### 9.2 完整回滚(删除表 + 视图 + FDW
```sql
-- 1. 删除 FDW 外部表(业务库)
DROP FOREIGN TABLE IF EXISTS fdw_etl.v_dws_assistant_order_contribution;
-- 2. 删除 RLS 视图ETL 库)
DROP VIEW IF EXISTS app.v_dws_assistant_order_contribution;
-- 3. 删除表和索引ETL 库)
DROP INDEX IF EXISTS dws.idx_aoc_stat_date;
DROP INDEX IF EXISTS dws.idx_aoc_site_assistant_date;
DROP TABLE IF EXISTS dws.dws_assistant_order_contribution;
```
### 9.3 回滚任务注册
`orchestration/task_registry.py` 中移除 `DWS_ASSISTANT_ORDER_CONTRIBUTION` 注册行,并从 `tasks/dws/__init__.py` 中移除 `AssistantOrderContributionTask` 导出。
---
## 10. 代码引用
- 任务类:`tasks/dws/assistant_order_contribution_task.py``AssistantOrderContributionTask`
- 数据结构:`TableUsage``AssistantService``OrderData`(同文件)
- 继承:`BaseDwsTask`
- 任务注册:`orchestration/task_registry.py``DWS_ASSISTANT_ORDER_CONTRIBUTION`
- 属性测试:`tests/test_dws_contribution_properties.py`
- 迁移脚本:`db/etl_feiqiu/migrations/2025-02-24__create_dws_assistant_order_contribution.sql`
- RLS 视图:`db/etl_feiqiu/migrations/2025-02-24__create_rls_view_assistant_order_contribution.sql`
- FDW 映射:`db/zqyy_app/migrations/2025-02-24__add_fdw_dws_extensions.sql`
- 验证脚本:`apps/etl/connectors/feiqiu/scripts/verify_dws_extensions.py`
---
## 11. 关联扩展字段说明
本次 Spec02-etl-dws-miniapp-extensions同时扩展了两张已有表的字段简要说明如下
### 11.1 dws_member_consumption_summary 新增字段
| 列名 | 类型 | 默认值 | 业务含义 |
|------|------|--------|---------|
| `recharge_count_30d` | INTEGER | 0 | 近 30 天充值次数 |
| `recharge_count_60d` | INTEGER | 0 | 近 60 天充值次数 |
| `recharge_count_90d` | INTEGER | 0 | 近 90 天充值次数 |
| `recharge_amount_30d` | NUMERIC(14,2) | 0 | 近 30 天充值金额 |
| `recharge_amount_60d` | NUMERIC(14,2) | 0 | 近 60 天充值金额 |
| `recharge_amount_90d` | NUMERIC(14,2) | 0 | 近 90 天充值金额 |
| `avg_ticket_amount` | NUMERIC(14,2) | 0 | 次均消费 = total_consume_amount / MAX(total_visit_count, 1) |
迁移脚本:`db/etl_feiqiu/migrations/2025-02-24__alter_member_consumption_add_recharge_fields.sql`
### 11.2 dws_assistant_daily_detail 新增字段
| 列名 | 类型 | 默认值 | 业务含义 |
|------|------|--------|---------|
| `penalty_minutes` | NUMERIC(10,2) | 0 | 定档折算惩罚分钟数,无惩罚时为 0 |
| `penalty_reason` | TEXT | NULL | 惩罚原因描述,无惩罚时为 NULL |
| `is_exempt` | BOOLEAN | FALSE | 是否豁免惩罚 |
| `per_hour_contribution` | NUMERIC(14,2) | NULL | 单人每小时贡献流水 = 台费每小时单价 / 助教人数 |
迁移脚本:`db/etl_feiqiu/migrations/2025-02-24__alter_assistant_daily_add_penalty_fields.sql`

View File

@@ -0,0 +1,251 @@
# BD_Manualdws_member_spending_power_indexSPI 消费力指数)
> DWS 表:`dws.dws_member_spending_power_index`
> DWD 数据源:`dwd.dwd_settlement_head`(消费订单)、`dwd.dwd_recharge_order`(充值订单)
> 配置表:`dws.cfg_index_parameters``index_type='SPI'`
> 任务代码:`DWS_SPENDING_POWER_INDEX`
> 代码位置:`apps/etl/connectors/feiqiu/tasks/dws/index/spending_power_index_task.py`
> DDL 位置:`docs/database/ddl/etl_feiqiu__dws.sql`
> 迁移脚本:`db/etl_feiqiu/migrations/2026-02-23_create_dws_member_spending_power_index.sql`
> 种子数据:`db/etl_feiqiu/seeds/seed_index_parameters.sql``index_type='SPI'` 部分)
---
## 1. 表结构
| 列名 | 类型 | 默认值 | 业务含义 | 取值范围/示例 |
|------|------|--------|---------|-------------|
| `spi_id` | BIGINT (SERIAL) | nextval 序列 | 自增主键PK | 自增 |
| `site_id` | INTEGER NOT NULL | — | 门店 ID | 飞球门店 ID |
| `member_id` | BIGINT NOT NULL | — | 会员 ID | 飞球会员 ID |
| `spend_30` | NUMERIC(14,2) | 0 | 近 30 天消费总额(元) | `0.00` ~ 金额值 |
| `spend_90` | NUMERIC(14,2) | 0 | 近 90 天消费总额(元) | `0.00` ~ 金额值 |
| `recharge_90` | NUMERIC(14,2) | 0 | 近 90 天充值总额(元) | `0.00` ~ 金额值 |
| `orders_30` | INTEGER | 0 | 近 30 天消费笔数 | `0` ~ 正整数 |
| `orders_90` | INTEGER | 0 | 近 90 天消费笔数 | `0` ~ 正整数 |
| `visit_days_30` | INTEGER | 0 | 近 30 天消费日数(按天去重) | `0` ~ `30` |
| `visit_days_90` | INTEGER | 0 | 近 90 天消费日数(按天去重) | `0` ~ `90` |
| `avg_ticket_90` | NUMERIC(14,2) | 0 | 90 天客单价(= spend_90 / max(orders_90, 1) | `0.00` ~ 金额值 |
| `active_weeks_90` | INTEGER | 0 | 近 90 天有消费的自然周数 | `0` ~ `13` |
| `daily_spend_ewma_90` | NUMERIC(14,2) | 0 | 日消费 EWMA指数加权移动平均 | `0.00` ~ 金额值 |
| `score_level_raw` | NUMERIC(10,4) | 0 | Level 子分原始分(消费水平) | ≥ 0 |
| `score_speed_raw` | NUMERIC(10,4) | 0 | Speed 子分原始分(消费速度) | ≥ 0 |
| `score_stability_raw` | NUMERIC(10,4) | 0 | Stability 子分原始分(消费稳定性) | `0.0000` ~ `1.0000` |
| `score_level_display` | NUMERIC(5,2) | 0 | Level 子分展示分 | `0.00` ~ `10.00` |
| `score_speed_display` | NUMERIC(5,2) | 0 | Speed 子分展示分 | `0.00` ~ `10.00` |
| `score_stability_display` | NUMERIC(5,2) | 0 | Stability 子分展示分 | `0.00` ~ `10.00` |
| `raw_score` | NUMERIC(10,4) | 0 | SPI 总分原始分(加权合成) | ≥ 0 |
| `display_score` | NUMERIC(5,2) | 0 | SPI 总分展示分 | `0.00` ~ `10.00` |
| `calc_time` | TIMESTAMPTZ | NOW() | 本次计算时间 | ISO 时间戳 |
| `created_at` | TIMESTAMPTZ | NOW() | 记录创建时间 | ISO 时间戳 |
| `updated_at` | TIMESTAMPTZ | NOW() | 记录最后更新时间 | ISO 时间戳 |
---
## 2. 主键与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|----|------|
| `dws_member_spending_power_index_pkey` | PRIMARY KEY | `spi_id` | 物理主键(自增序列) |
| `idx_spi_site_member` | UNIQUE INDEX | `(site_id, member_id)` | 业务主键:每个门店每个会员唯一一条记录 |
| `idx_spi_display_score` | INDEX | `(site_id, display_score DESC)` | 按门店查询展示分排名,支持 TOP-N 查询 |
---
## 3. 数据写入策略
- **delete-before-insert**:每次执行按 `site_id` 全量刷新
1. `DELETE FROM dws.dws_member_spending_power_index WHERE site_id = %s`
2. 批量 `INSERT` 新计算结果
- 无数据时跳过(不删除、不插入),返回 `{'status': 'skipped', 'reason': 'no_data'}`
---
## 4. 算法概要
### 4.1 数据来源
| 来源表 | 筛选条件 | 提取内容 |
|--------|---------|---------|
| `dwd.dwd_settlement_head` | 近 90 天,`settle_type IN (1, 3)` | 消费金额、笔数、消费日数、周覆盖、日消费序列 |
| `dwd.dwd_recharge_order` | 近 90 天,`settle_type = 5` | 充值总额 |
### 4.2 子分公式
- **Level**(消费水平,权重 0.60
`L = w_s30 × ln(1 + spend_30/M30) + w_s90 × ln(1 + spend_90/M90) + w_ticket × ln(1 + avg_ticket_90/T0) + w_r90 × ln(1 + recharge_90/R90)`
- **Speed**(消费速度,权重 0.30
`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
- `V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`
- `V_rel = ln((v_30 + ε) / (v_90 + ε))`,仅加速加分
- `V_ewma = ln(1 + daily_spend_ewma_90 / E0)`
- **Stability**(消费稳定性,权重 0.10
`P = active_weeks_90 / 13`,取值 [0, 1]
### 4.3 总分合成
`SPI_raw = w_L × L + w_S × S + w_P × P`(默认 0.60 / 0.30 / 0.10
### 4.4 展示分映射
Raw → Winsorize(P5, P95) → 可选压缩(log1p/asinh) → MinMax [0, 10] → 可选 EWMA 平滑
子分Level/Speed/Stability各自独立映射使用 `SPI_LEVEL` / `SPI_SPEED` / `SPI_STABILITY` 隔离分位历史。
---
## 5. 配置参数
所有参数存储在 `dws.cfg_index_parameters``index_type='SPI'`),缺失时回退到代码中 `DEFAULT_PARAMS`
| 参数名 | 默认值 | 说明 |
|--------|--------|------|
| `spend_window_short_days` | 30 | 短窗口天数 |
| `spend_window_long_days` | 90 | 长窗口天数 |
| `ewma_alpha_daily_spend` | 0.3 | 日消费 EWMA 平滑系数 |
| `amount_base_spend_30` | 500.0 | 30 天消费金额压缩基数 |
| `amount_base_spend_90` | 1500.0 | 90 天消费金额压缩基数 |
| `amount_base_ticket_90` | 200.0 | 客单价压缩基数 |
| `amount_base_recharge_90` | 1000.0 | 充值金额压缩基数 |
| `amount_base_speed_abs` | 100.0 | 绝对速度压缩基数 |
| `amount_base_ewma_90` | 50.0 | EWMA 速度压缩基数 |
| `w_level_spend_30` | 0.30 | Level 子分中 spend_30 权重 |
| `w_level_spend_90` | 0.35 | Level 子分中 spend_90 权重 |
| `w_level_ticket_90` | 0.20 | Level 子分中 avg_ticket_90 权重 |
| `w_level_recharge_90` | 0.15 | Level 子分中 recharge_90 权重 |
| `w_speed_abs` | 0.50 | Speed 子分中绝对速度权重 |
| `w_speed_rel` | 0.30 | Speed 子分中相对速度权重 |
| `w_speed_ewma` | 0.20 | Speed 子分中 EWMA 速度权重 |
| `weight_level` | 0.60 | 总分中 Level 权重 |
| `weight_speed` | 0.30 | 总分中 Speed 权重 |
| `weight_stability` | 0.10 | 总分中 Stability 权重 |
| `stability_window_days` | 90 | 稳定性计算窗口 |
| `use_stability` | 1 | 是否启用稳定性子分0=禁用) |
| `percentile_lower` | 5 | Winsorize 下分位 |
| `percentile_upper` | 95 | Winsorize 上分位 |
| `compression_mode` | 1 | 压缩模式0=无1=log1p2=asinh |
| `use_smoothing` | 1 | 是否启用 EWMA 分位平滑 |
| `ewma_alpha` | 0.2 | 分位平滑 EWMA 系数 |
| `speed_epsilon` | 1e-6 | 速度计算防除零小量 |
---
## 6. 前置依赖
- 任务依赖:`DWS_MEMBER_CONSUMPTION`(需先完成会员消费汇总)
- 数据源表:`dwd.dwd_settlement_head``dwd.dwd_recharge_order` 必须已有数据
- 配置表:`dws.cfg_index_parameters``index_type='SPI'` 种子数据已插入(缺失时使用默认值)
- 分位历史表:`dws.dws_index_percentile_history`(首次执行时无历史,不平滑)
---
## 7. 验证 SQL
### 7.1 检查表是否存在且有数据
```sql
SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT site_id) AS site_count,
MIN(calc_time) AS earliest_calc,
MAX(calc_time) AS latest_calc
FROM dws.dws_member_spending_power_index;
```
### 7.2 检查展示分范围是否合规(应全部在 [0, 10]
```sql
SELECT
COUNT(*) FILTER (WHERE display_score < 0 OR display_score > 10) AS spi_out_of_range,
COUNT(*) FILTER (WHERE score_level_display < 0 OR score_level_display > 10) AS level_out_of_range,
COUNT(*) FILTER (WHERE score_speed_display < 0 OR score_speed_display > 10) AS speed_out_of_range,
COUNT(*) FILTER (WHERE score_stability_display < 0 OR score_stability_display > 10) AS stability_out_of_range
FROM dws.dws_member_spending_power_index;
-- 预期:所有列均为 0
```
### 7.3 检查业务主键唯一性(不应有重复)
```sql
SELECT site_id, member_id, COUNT(*) AS cnt
FROM dws.dws_member_spending_power_index
GROUP BY site_id, member_id
HAVING COUNT(*) > 1;
-- 预期:无结果返回
```
### 7.4 按门店查看 SPI 分布概况
```sql
SELECT
site_id,
COUNT(*) AS member_count,
ROUND(AVG(display_score), 2) AS avg_spi,
ROUND(MIN(display_score), 2) AS min_spi,
ROUND(MAX(display_score), 2) AS max_spi,
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY display_score), 2) AS median_spi
FROM dws.dws_member_spending_power_index
GROUP BY site_id
ORDER BY site_id;
```
### 7.5 检查 Stability 子分原始分范围(应在 [0, 1]
```sql
SELECT COUNT(*) AS out_of_range
FROM dws.dws_member_spending_power_index
WHERE score_stability_raw < 0 OR score_stability_raw > 1;
-- 预期0
```
---
## 8. 兼容性说明
| 影响范围 | 说明 |
|---------|------|
| ETL 任务 | 新增任务 `DWS_SPENDING_POWER_INDEX`,依赖 `DWS_MEMBER_CONSUMPTION`。不影响现有 WBI/NCI/RS/OS/MS/ML 指数任务 |
| 后端 API | 当前无 API 直接读取此表。后续如需暴露 SPI 数据,需新增接口 |
| 管理后台 | 当前无前端页面展示 SPI。后续可在会员详情页新增 SPI 展示 |
| 小程序 | 无影响 |
| 其他指数 | SPI 独立于现有指数体系,不修改任何已有表或任务逻辑 |
| 分位历史 | SPI 会向 `dws.dws_index_percentile_history` 写入 `index_type='SPI'`/`SPI_LEVEL`/`SPI_SPEED`/`SPI_STABILITY` 的分位记录 |
---
## 9. 回滚策略
### 9.1 删除数据(保留表结构)
```sql
DELETE FROM dws.dws_member_spending_power_index;
DELETE FROM dws.dws_index_percentile_history WHERE index_type LIKE 'SPI%';
DELETE FROM dws.cfg_index_parameters WHERE index_type = 'SPI';
```
### 9.2 完整回滚(删除表)
```sql
DROP INDEX IF EXISTS dws.idx_spi_display_score;
DROP INDEX IF EXISTS dws.idx_spi_site_member;
DROP TABLE IF EXISTS dws.dws_member_spending_power_index;
DROP SEQUENCE IF EXISTS dws.dws_member_spending_power_index_spi_id_seq;
```
### 9.3 回滚任务注册
`orchestration/task_registry.py` 中移除 `DWS_SPENDING_POWER_INDEX` 注册行,并从 `tasks/dws/index/__init__.py``tasks/dws/__init__.py` 中移除 `SpendingPowerIndexTask` 导出。
---
## 10. 代码引用
- 任务类:`tasks/dws/index/spending_power_index_task.py``SpendingPowerIndexTask`
- 继承:`BaseIndexTask``tasks/dws/index/base_index_task.py`
- 任务注册:`orchestration/task_registry.py``DWS_SPENDING_POWER_INDEX`
- 属性测试:`tests/test_spi_properties.py`
- 单元测试:`apps/etl/connectors/feiqiu/tests/unit/test_spi_task.py`
- 迁移脚本:`db/etl_feiqiu/migrations/2026-02-23_create_dws_member_spending_power_index.sql`
- 种子数据:`db/etl_feiqiu/seeds/seed_index_parameters.sql`

View File

@@ -0,0 +1,123 @@
# BD_ManualFDW 跨库映射配置fdw_etl
> 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接)
> 迁移脚本:`db/zqyy_app/migrations/2026-02-24__p1_setup_fdw_etl.sql`
> 关联 SPEC`miniapp-db-foundation`P1 基础设施层)
---
## 1. 变更说明
### 新增扩展
| 扩展 | 用途 |
|------|------|
| `postgres_fdw` | PostgreSQL 外部数据包装器,支持跨库查询 |
### 新增外部服务器
| 服务器名 | 目标库 | 说明 |
|----------|--------|------|
| `etl_feiqiu_server` | ETL 库(通过 `PG_DSN` 连接) | 通用名称,通过 host/dbname/port 参数区分环境 |
### 新增用户映射
| 本地角色 | 远程角色 | 服务器 |
|----------|----------|--------|
| `app_user` | `app_reader` | `etl_feiqiu_server` |
### 新增 Schema
| Schema | 用途 |
|--------|------|
| `fdw_etl` | 存放从 ETL 库 `app` Schema 导入的外部表(只读) |
### 导入的外部表
通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 35 张):
- 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`
- 24 张 DWS 视图:`v_dws_member_consumption_summary``v_dws_member_visit_detail`
### 权限配置
| 角色 | Schema | 权限 |
|------|--------|------|
| `app_user` | `fdw_etl` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` |
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。本脚本仅在业务库创建外部表映射,不修改 ETL 库 |
| 后端 API | 前置依赖。后端可通过 `fdw_etl.v_dim_member` 等外部表读取 ETL 数据,无需直连 ETL 库 |
| 小程序 | 无直接影响。小程序通过后端 API 间接访问 |
| 管理后台 | 无直接影响 |
| `auth`/`biz` Schema | 无影响。FDW 配置独立于业务 Schema |
| `public` Schema | 无影响 |
| 现有 `db/fdw/setup_fdw_test.sql` | 功能重叠。本迁移脚本使用通用服务器名 `etl_feiqiu_server`(不含环境前缀),与旧脚本的 `test_etl_feiqiu_server` 共存但独立 |
### 幂等性说明
`IMPORT FOREIGN SCHEMA` 不支持 `IF NOT EXISTS`,重复执行会因外部表已存在而报错。本脚本采用 `DROP SCHEMA IF EXISTS fdw_etl CASCADE` + 重建的方式确保幂等性。副作用是每次执行会重建所有外部表,但由于外部表不存储数据,无数据丢失风险。
---
## 3. 回滚策略
迁移脚本末尾已包含注释形式的回滚语句,按逆序执行:
```sql
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_etl REVOKE SELECT ON TABLES FROM app_user;
REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_etl FROM app_user;
REVOKE USAGE ON SCHEMA fdw_etl FROM app_user;
DROP SCHEMA IF EXISTS fdw_etl CASCADE;
DROP USER MAPPING IF EXISTS FOR app_user SERVER etl_feiqiu_server;
DROP SERVER IF EXISTS etl_feiqiu_server CASCADE;
DROP EXTENSION IF EXISTS postgres_fdw;
```
注意:
- `DROP SERVER CASCADE` 会级联删除依赖的用户映射和外部表
- 如果其他 Schema 也使用 `postgres_fdw` 扩展,不要执行最后一行 `DROP EXTENSION`
- 回滚不影响 ETL 库侧的 `app` Schema 和 RLS 视图
---
## 4. 验证 SQL
```sql
-- 1. 验证 postgres_fdw 扩展已安装
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'postgres_fdw';
-- 2. 验证外部服务器已创建
SELECT srvname, srvowner::regrole, srvoptions
FROM pg_foreign_server
WHERE srvname = 'etl_feiqiu_server';
-- 3. 验证用户映射已创建
SELECT um.umid, r.rolname AS local_role, s.srvname, um.umoptions
FROM pg_user_mappings um
JOIN pg_foreign_server s ON s.srvname = um.srvname
JOIN pg_roles r ON r.rolname = um.usename
WHERE s.srvname = 'etl_feiqiu_server';
-- 4. 验证 fdw_etl Schema 存在且包含外部表
SELECT foreign_table_schema, foreign_table_name, foreign_server_name
FROM information_schema.foreign_tables
WHERE foreign_table_schema = 'fdw_etl'
ORDER BY foreign_table_name;
-- 5. 验证 app_user 对 fdw_etl 有 USAGE 权限
SELECT has_schema_privilege('app_user', 'fdw_etl', 'USAGE') AS fdw_etl_usage;
-- 6. 验证 ALTER DEFAULT PRIVILEGES 已设置
SELECT n.nspname AS schema_name,
d.defaclacl AS default_acl
FROM pg_default_acl d
JOIN pg_namespace n ON n.oid = d.defaclnamespace
WHERE n.nspname = 'fdw_etl';
```

View File

@@ -0,0 +1,102 @@
# BD Manual: goodsStockWarningInfo库存预警信息
## 变更概述
- 日期2026-02-24
- 触发:一致性检查报告发现 API 独有嵌套字段 `goodsStockWarningInfo` 未映射到 ODS/DWD
- 迁移脚本:`db/etl_feiqiu/migrations/2026-02-24__add_goods_stock_warning_info.sql`
## 字段来源
API 端点 `/TenantGoods/GetGoodsInventoryList` 返回的 `store_goods_master` 记录中包含嵌套对象:
```json
{
"goodsStockWarningInfo": {
"tenant_goods_id": 0, // 冗余,已有同名顶层字段
"site_goods_id": 0, // 冗余,对应顶层 id
"sales_day": 0.0, // → warning_sales_day
"warning_day_max": 0, // → warning_day_max
"warning_day_min": 0 // → warning_day_min
}
}
```
仅提取 3 个有效字段,冗余 ID 不重复收录。
## 新增字段
| 层 | 表 | 列名 | 类型 | 说明 |
|---|---|---|---|---|
| ODS | `ods.store_goods_master` | `warning_sales_day` | NUMERIC(18,2) | 库存预警参考的日均销量 |
| ODS | `ods.store_goods_master` | `warning_day_max` | INTEGER | 预警天数上限 |
| ODS | `ods.store_goods_master` | `warning_day_min` | INTEGER | 预警天数下限 |
| DWD | `dwd.dim_store_goods_ex` | `warning_sales_day` | NUMERIC(18,2) | 同 ODS直接映射 |
| DWD | `dwd.dim_store_goods_ex` | `warning_day_max` | INTEGER | 同 ODS直接映射 |
| DWD | `dwd.dim_store_goods_ex` | `warning_day_min` | INTEGER | 同 ODS直接映射 |
## 数据流
```
API goodsStockWarningInfo (嵌套 JSON)
↓ _merge_record_layers 扁平化_STOCK_WARNING_FIELD_MAP
ODS ods.store_goods_master (warning_sales_day / warning_day_max / warning_day_min)
↓ DWD FACT_MAPPINGS 直接映射
DWD dwd.dim_store_goods_ex (同名列)
```
## 代码变更
| 文件 | 变更 |
|---|---|
| `apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py` | `_merge_record_layers` 增加 `goodsStockWarningInfo` 扁平化逻辑 |
| `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py` | `FACT_MAPPINGS["dwd.dim_store_goods_ex"]` 增加 3 个映射 |
| `db/_archived/ddl_baseline_2026-02-22/db/etl_feiqiu/schemas/ods.sql` | baseline 同步 |
| `db/_archived/ddl_baseline_2026-02-22/db/etl_feiqiu/schemas/dwd.sql` | baseline 同步 |
## 兼容性
- 后端 API无影响后端不直接读取 ODS/DWD 层这些字段)
- 小程序:无影响
- ETLODS schema-aware 插入自动识别新列DWD 通过 FACT_MAPPINGS 映射
- 管理后台:如需展示库存预警信息,可从 `dwd.dim_store_goods_ex` 读取
## 回滚策略
```sql
ALTER TABLE ods.store_goods_master
DROP COLUMN IF EXISTS warning_sales_day,
DROP COLUMN IF EXISTS warning_day_max,
DROP COLUMN IF EXISTS warning_day_min;
ALTER TABLE dwd.dim_store_goods_ex
DROP COLUMN IF EXISTS warning_sales_day,
DROP COLUMN IF EXISTS warning_day_max,
DROP COLUMN IF EXISTS warning_day_min;
```
## 验证 SQL
```sql
-- 1. 确认 ODS 新列存在
SELECT column_name, data_type FROM information_schema.columns
WHERE table_schema = 'ods' AND table_name = 'store_goods_master'
AND column_name IN ('warning_sales_day', 'warning_day_max', 'warning_day_min')
ORDER BY column_name;
-- 预期3 行
-- 2. 确认 DWD 新列存在
SELECT column_name, data_type FROM information_schema.columns
WHERE table_schema = 'dwd' AND table_name = 'dim_store_goods_ex'
AND column_name IN ('warning_sales_day', 'warning_day_max', 'warning_day_min')
ORDER BY column_name;
-- 预期3 行
-- 3. 确认注释已设置
SELECT c.column_name, pgd.description
FROM information_schema.columns c
JOIN pg_catalog.pg_statio_all_tables st ON st.schemaname = c.table_schema AND st.relname = c.table_name
JOIN pg_catalog.pg_description pgd ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
WHERE c.table_schema = 'ods' AND c.table_name = 'store_goods_master'
AND c.column_name LIKE 'warning_%';
-- 预期3 行description 非空
```

View File

@@ -10,10 +10,11 @@
| `etl_feiqiu__ods.sql` | etl_feiqiu | ods | 原始数据层23 表) |
| `etl_feiqiu__dwd.sql` | etl_feiqiu | dwd | 明细数据层44 表) |
| `etl_feiqiu__core.sql` | etl_feiqiu | core | 跨门店标准化7 表) |
| `etl_feiqiu__dws.sql` | etl_feiqiu | dws | 汇总数据层32 表 + 1 视图 + 8 物化视图) |
| `etl_feiqiu__app.sql` | etl_feiqiu | app | RLS 视图层(7 视图,无表) |
| `etl_feiqiu__dws.sql` | etl_feiqiu | dws | 汇总数据层34 表 + 1 视图 + 8 物化视图) |
| `etl_feiqiu__app.sql` | etl_feiqiu | app | RLS 视图层(43 视图,无表) |
| `zqyy_app__public.sql` | zqyy_app | public | 小程序业务表12 表) |
| `fdw.sql` | — | — | FDW 跨库映射配置 |
| `zqyy_app__auth.sql` | zqyy_app | auth | 用户认证与权限8 表) |
| `fdw.sql` | — | — | FDW 正向跨库映射配置etl→app |
## 数据字典BD_Manual — ODS→DWD 字段映射)
@@ -41,9 +42,15 @@
- `etl_feiqiu_schema_migration.md`(旧迁移汇总)
- `zqyy_app_admin_web_tables.md`(建表记录)
## 注意事项
- `fdw.sql` 仅包含正向映射etl_feiqiu → zqyy_app反向映射zqyy_app → etl_feiqiu的可执行脚本在 `db/fdw/setup_fdw_reverse*.sql`
- DDL 基线中的统计数字以文件实际内容为准,本 README 的表格数字可能滞后于最新导出
## 相关资源
- 种子数据:`db/etl_feiqiu/seeds/``db/zqyy_app/seeds/`
- FDW 配置:`db/fdw/`
- FDW 配置(可执行)`db/fdw/`(含正向 + 反向 + 测试环境版本)
- DDL 生成脚本:`scripts/ops/gen_consolidated_ddl.py`
- 迁移脚本(活跃):`db/etl_feiqiu/migrations/``db/zqyy_app/migrations/`
- 迁移脚本归档:`db/_archived/ddl_baseline_2026-02-22/`

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / core跨门店标准化维度/事实)
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / dwd明细数据层
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -449,7 +449,10 @@ CREATE TABLE dwd.dim_store_goods_ex (
scd2_is_current integer,
scd2_version integer,
batch_stock_quantity numeric,
time_slot_sale integer
time_slot_sale integer,
warning_sales_day numeric(18,2),
warning_day_max integer,
warning_day_min integer
);
CREATE TABLE dwd.dim_table (

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / dws汇总数据层
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -17,6 +17,7 @@ CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_customer_stats_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_daily_detail_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_finance_analysis_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_monthly_summary_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_order_contribution_contribution_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_recharge_commission_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_salary_calc_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_finance_daily_summary_id_seq AS bigint;
@@ -194,7 +195,11 @@ CREATE TABLE dws.dws_assistant_daily_detail (
trashed_seconds integer DEFAULT 0 NOT NULL,
trashed_count integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
updated_at timestamp with time zone DEFAULT now() NOT NULL,
penalty_minutes numeric(10,2) DEFAULT 0,
penalty_reason text,
is_exempt boolean DEFAULT false,
per_hour_contribution numeric(14,2)
);
CREATE TABLE dws.dws_assistant_finance_analysis (
@@ -258,6 +263,23 @@ CREATE TABLE dws.dws_assistant_monthly_summary (
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE dws.dws_assistant_order_contribution (
contribution_id bigint DEFAULT nextval('dws.dws_assistant_order_contribution_contribution_id_seq'::regclass) NOT NULL,
site_id integer NOT NULL,
tenant_id integer NOT NULL,
assistant_id bigint NOT NULL,
assistant_nickname character varying(100),
stat_date date NOT NULL,
order_gross_revenue numeric(14,2) DEFAULT 0,
order_net_revenue numeric(14,2) DEFAULT 0,
time_weighted_revenue numeric(14,2) DEFAULT 0,
time_weighted_net_revenue numeric(14,2) DEFAULT 0,
order_count integer DEFAULT 0,
total_service_seconds integer DEFAULT 0,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
);
CREATE TABLE dws.dws_assistant_recharge_commission (
id bigint DEFAULT nextval('dws.dws_assistant_recharge_commission_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
@@ -622,7 +644,14 @@ CREATE TABLE dws.dws_member_consumption_summary (
is_active_90d boolean DEFAULT false NOT NULL,
customer_tier character varying(20),
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
updated_at timestamp with time zone DEFAULT now() NOT NULL,
recharge_count_30d integer DEFAULT 0,
recharge_count_60d integer DEFAULT 0,
recharge_count_90d integer DEFAULT 0,
recharge_amount_30d numeric(14,2) DEFAULT 0,
recharge_amount_60d numeric(14,2) DEFAULT 0,
recharge_amount_90d numeric(14,2) DEFAULT 0,
avg_ticket_amount numeric(14,2) DEFAULT 0
);
CREATE TABLE dws.dws_member_newconv_index (
@@ -918,6 +947,7 @@ ALTER TABLE dws.dws_assistant_finance_analysis ADD CONSTRAINT dws_assistant_fina
ALTER TABLE dws.dws_assistant_finance_analysis ADD CONSTRAINT uk_dws_assistant_finance UNIQUE (site_id, stat_date, assistant_id);
ALTER TABLE dws.dws_assistant_monthly_summary ADD CONSTRAINT dws_assistant_monthly_summary_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_assistant_monthly_summary ADD CONSTRAINT uk_dws_assistant_monthly UNIQUE (site_id, assistant_id, stat_month, assistant_level_code);
ALTER TABLE dws.dws_assistant_order_contribution ADD CONSTRAINT dws_assistant_order_contribution_pkey PRIMARY KEY (contribution_id);
ALTER TABLE dws.dws_assistant_recharge_commission ADD CONSTRAINT dws_assistant_recharge_commission_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_assistant_salary_calc ADD CONSTRAINT dws_assistant_salary_calc_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_assistant_salary_calc ADD CONSTRAINT uk_dws_assistant_salary UNIQUE (site_id, assistant_id, salary_month, assistant_level_code);
@@ -978,6 +1008,8 @@ CREATE INDEX idx_dws_assistant_finance_date ON dws.dws_assistant_finance_analysi
CREATE INDEX idx_dws_assistant_monthly_asst ON dws.dws_assistant_monthly_summary USING btree (assistant_id, stat_month);
CREATE INDEX idx_dws_assistant_monthly_month ON dws.dws_assistant_monthly_summary USING btree (stat_month);
CREATE INDEX idx_dws_assistant_monthly_tier ON dws.dws_assistant_monthly_summary USING btree (tier_code);
CREATE UNIQUE INDEX idx_aoc_site_assistant_date ON dws.dws_assistant_order_contribution USING btree (site_id, assistant_id, stat_date);
CREATE INDEX idx_aoc_stat_date ON dws.dws_assistant_order_contribution USING btree (site_id, stat_date);
CREATE INDEX idx_dws_assistant_commission_asst ON dws.dws_assistant_recharge_commission USING btree (assistant_id, commission_month);
CREATE INDEX idx_dws_assistant_commission_batch ON dws.dws_assistant_recharge_commission USING btree (import_batch_no);
CREATE INDEX idx_dws_assistant_commission_month ON dws.dws_assistant_recharge_commission USING btree (commission_month);

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / metaETL 调度元数据)
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / ods原始数据层
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -873,7 +873,10 @@ CREATE TABLE ods.store_goods_master (
source_file text,
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now(),
time_slot_sale integer
time_slot_sale integer,
warning_sales_day numeric(18,2),
warning_day_max integer,
warning_day_min integer
);
CREATE TABLE ods.store_goods_sales_records (

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- FDW 跨库映射(在 zqyy_app 中执行)
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源db/fdw/setup_fdw.sql
-- =============================================================================

View File

@@ -0,0 +1,135 @@
-- =============================================================================
-- zqyy_app / auth用户认证与权限
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
CREATE SCHEMA IF NOT EXISTS auth;
-- 序列
CREATE SEQUENCE IF NOT EXISTS auth.permissions_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.roles_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.site_code_mapping_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.user_applications_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.user_assistant_binding_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.user_site_roles_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.users_id_seq AS integer;
-- 表
CREATE TABLE auth.permissions (
id integer DEFAULT nextval('auth.permissions_id_seq'::regclass) NOT NULL,
code character varying(100) NOT NULL,
name character varying(200) NOT NULL,
description text,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.role_permissions (
role_id integer NOT NULL,
permission_id integer NOT NULL
);
CREATE TABLE auth.roles (
id integer DEFAULT nextval('auth.roles_id_seq'::regclass) NOT NULL,
code character varying(50) NOT NULL,
name character varying(100) NOT NULL,
description text,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.site_code_mapping (
id integer DEFAULT nextval('auth.site_code_mapping_id_seq'::regclass) NOT NULL,
site_code character varying(10) NOT NULL,
site_id bigint NOT NULL,
site_name character varying(200),
tenant_id integer,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.user_applications (
id integer DEFAULT nextval('auth.user_applications_id_seq'::regclass) NOT NULL,
user_id integer NOT NULL,
site_code character varying(10) NOT NULL,
site_id bigint,
applied_role_text character varying(100) NOT NULL,
employee_number character varying(50),
phone character varying(20) NOT NULL,
status character varying(20) DEFAULT 'pending'::character varying NOT NULL,
reviewer_id integer,
review_note text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
reviewed_at timestamp with time zone
);
CREATE TABLE auth.user_assistant_binding (
id integer DEFAULT nextval('auth.user_assistant_binding_id_seq'::regclass) NOT NULL,
user_id integer NOT NULL,
site_id bigint NOT NULL,
assistant_id bigint,
staff_id bigint,
binding_type character varying(20) NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.user_site_roles (
id integer DEFAULT nextval('auth.user_site_roles_id_seq'::regclass) NOT NULL,
user_id integer NOT NULL,
site_id bigint NOT NULL,
role_id integer NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.users (
id integer DEFAULT nextval('auth.users_id_seq'::regclass) NOT NULL,
wx_openid character varying(100),
wx_union_id character varying(100),
wx_avatar_url text,
nickname character varying(100),
phone character varying(20),
status character varying(20) DEFAULT 'pending'::character varying NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
-- 约束(主键 / 唯一 / 外键)
ALTER TABLE auth.permissions ADD CONSTRAINT permissions_pkey PRIMARY KEY (id);
ALTER TABLE auth.permissions ADD CONSTRAINT permissions_code_key UNIQUE (code);
ALTER TABLE auth.permissions ADD CONSTRAINT uq_permissions_code UNIQUE (code);
ALTER TABLE auth.role_permissions ADD CONSTRAINT fk_role_permissions_permission_id FOREIGN KEY (permission_id) REFERENCES auth.permissions(id) ON DELETE CASCADE;
ALTER TABLE auth.role_permissions ADD CONSTRAINT fk_role_permissions_role_id FOREIGN KEY (role_id) REFERENCES auth.roles(id) ON DELETE CASCADE;
ALTER TABLE auth.role_permissions ADD CONSTRAINT role_permissions_permission_id_fkey FOREIGN KEY (permission_id) REFERENCES auth.permissions(id) ON DELETE CASCADE;
ALTER TABLE auth.role_permissions ADD CONSTRAINT role_permissions_role_id_fkey FOREIGN KEY (role_id) REFERENCES auth.roles(id) ON DELETE CASCADE;
ALTER TABLE auth.role_permissions ADD CONSTRAINT role_permissions_pkey PRIMARY KEY (role_id, permission_id);
ALTER TABLE auth.roles ADD CONSTRAINT roles_pkey PRIMARY KEY (id);
ALTER TABLE auth.roles ADD CONSTRAINT roles_code_key UNIQUE (code);
ALTER TABLE auth.roles ADD CONSTRAINT uq_roles_code UNIQUE (code);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT site_code_mapping_pkey PRIMARY KEY (id);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT site_code_mapping_site_code_key UNIQUE (site_code);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT site_code_mapping_site_id_key UNIQUE (site_id);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT uq_site_code_mapping_site_code UNIQUE (site_code);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT uq_site_code_mapping_site_id UNIQUE (site_id);
ALTER TABLE auth.user_applications ADD CONSTRAINT fk_user_applications_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_applications ADD CONSTRAINT user_applications_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_applications ADD CONSTRAINT user_applications_pkey PRIMARY KEY (id);
ALTER TABLE auth.user_assistant_binding ADD CONSTRAINT fk_user_assistant_binding_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_assistant_binding ADD CONSTRAINT user_assistant_binding_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_assistant_binding ADD CONSTRAINT user_assistant_binding_pkey PRIMARY KEY (id);
ALTER TABLE auth.user_site_roles ADD CONSTRAINT fk_user_site_roles_role_id FOREIGN KEY (role_id) REFERENCES auth.roles(id) ON DELETE CASCADE;
ALTER TABLE auth.user_site_roles ADD CONSTRAINT fk_user_site_roles_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_site_roles ADD CONSTRAINT user_site_roles_role_id_fkey FOREIGN KEY (role_id) REFERENCES auth.roles(id) ON DELETE CASCADE;
ALTER TABLE auth.user_site_roles ADD CONSTRAINT user_site_roles_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_site_roles ADD CONSTRAINT user_site_roles_pkey PRIMARY KEY (id);
ALTER TABLE auth.user_site_roles ADD CONSTRAINT uq_user_site_roles_user_site_role UNIQUE (user_id, site_id, role_id);
ALTER TABLE auth.user_site_roles ADD CONSTRAINT user_site_roles_user_id_site_id_role_id_key UNIQUE (user_id, site_id, role_id);
ALTER TABLE auth.users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE auth.users ADD CONSTRAINT uq_users_wx_openid UNIQUE (wx_openid);
ALTER TABLE auth.users ADD CONSTRAINT users_wx_openid_key UNIQUE (wx_openid);
-- 索引
CREATE INDEX ix_site_code_mapping_site_code ON auth.site_code_mapping USING btree (site_code);
CREATE INDEX ix_user_applications_status ON auth.user_applications USING btree (status);
CREATE INDEX ix_user_applications_user_id ON auth.user_applications USING btree (user_id);
CREATE INDEX ix_user_site_roles_user_site ON auth.user_site_roles USING btree (user_id, site_id);
CREATE INDEX ix_users_status ON auth.users USING btree (status);
CREATE INDEX ix_users_wx_openid ON auth.users USING btree (wx_openid);

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- zqyy_app / public小程序业务表
-- 生成日期2026-02-23
-- 生成日期2026-02-25
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================

View File

@@ -23,12 +23,12 @@
| 状态 | 项目 |
|------|------|
| 20260220 | 在 Windows Server 上创建目录结构 |
| 20260220 | 克隆仓库并切换分支 |
| | 配置环境变量文件 |
| | 安装 Python 依赖 |
| | 运行 `setup-server-git.py` 配置 Git 排除规则 |
| | 运行 `init-server-env.ps1` 删除排除文件 + 创建 export 目录 |
| 完成 20260220 | 在 Windows Server 上创建目录结构 |
| 完成 20260220 | 克隆仓库并切换分支 |
| 完成 20260224 | 配置环境变量文件 |
| 完成 20260224 | 安装 Python 依赖 |
| 完成 20260224 | 运行 `setup-server-git.py` 配置 Git 排除规则 |
| 完成 20260224 | 运行 `init-server-env.py` 删除排除文件 + 创建 export 目录 |
在 Windows Server 上执行:
@@ -125,11 +125,11 @@ python scripts/server/setup-server-git.py
```powershell
# 删除排除文件 + 创建 export 目录树test + prod 一次搞定)
cd D:\NeoZQYY
.\test\repo\scripts\server\init-server-env.ps1
python test\repo\scripts\server\init-server-env.py
# 也可以只初始化单个环境
.\test\repo\scripts\server\init-server-env.ps1 -Envs test
.\prod\repo\scripts\server\init-server-env.ps1 -Envs prod
python test\repo\scripts\server\init-server-env.py --envs test
python prod\repo\scripts\server\init-server-env.py --envs prod
```
Git 排除方案说明(统一 .gitignore + skip-worktree
@@ -172,8 +172,8 @@ Git 排除方案说明(统一 .gitignore + skip-worktree
| 状态 | 项目 |
|------|------|
| | 将 bat 脚本放到服务器 `D:\NeoZQYY\scripts\` |
| | 登录服务器手动运行对应脚本启动服务 |
| 完成 20260224 | 将 bat 脚本放到服务器 `D:\NeoZQYY\scripts\` |
| 完成 20260224 | 登录服务器手动运行对应脚本启动服务 |
> 后续将由监控系统(见 7.2)统一管理所有服务的启停和状态监控。
> 在监控系统上线之前,登录 Windows Server 手动双击 bat 脚本启动。

185
docs/h5_ui/css/ai-icons.css Normal file
View File

@@ -0,0 +1,185 @@
/* ========== AI 标识通用样式 ========== */
/* --- 配色系渐变背景badge 用更深的 --ai-from-deep / --ai-to-deep --- */
.ai-color-red { --ai-from: #e74c3c; --ai-to: #f39c9c; --ai-from-deep: #c0392b; --ai-to-deep: #e74c3c; }
.ai-color-orange { --ai-from: #e67e22; --ai-to: #f5c77e; --ai-from-deep: #ca6c17; --ai-to-deep: #e67e22; }
.ai-color-yellow { --ai-from: #d4a017; --ai-to: #f7dc6f; --ai-from-deep: #b8860b; --ai-to-deep: #d4a017; }
.ai-color-blue { --ai-from: #2980b9; --ai-to: #7ec8e3; --ai-from-deep: #1a5276; --ai-to-deep: #2980b9; }
.ai-color-indigo { --ai-from: #667eea; --ai-to: #a78bfa; --ai-from-deep: #4a5fc7; --ai-to-deep: #667eea; }
.ai-color-purple { --ai-from: #764ba2; --ai-to: #c084fc; --ai-from-deep: #5b3080; --ai-to-deep: #764ba2; }
/* --- 1. 嵌入 Icon行首小图标轻量化 --- */
.ai-inline-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background: linear-gradient(135deg, color-mix(in srgb, var(--ai-from) 45%, white), color-mix(in srgb, var(--ai-to) 50%, white));
border-radius: 4px;
font-size: 10px;
line-height: 1;
vertical-align: middle;
margin-right: 3px;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
/* 嵌入 Icon 内的机器人 SVG */
.ai-inline-icon svg {
width: 13px;
height: 13px;
position: relative;
z-index: 1;
flex-shrink: 0;
}
/* 微光动画(慢速、柔和、宽光带) */
.ai-inline-icon::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent 30%,
rgba(255,255,255,0.18) 45%,
rgba(255,255,255,0.22) 50%,
rgba(255,255,255,0.18) 55%,
transparent 70%
);
animation: ai-shimmer 12s ease-in-out infinite;
}
@keyframes ai-shimmer {
0%, 100% { transform: translateX(-100%) rotate(45deg); }
50% { transform: translateX(100%) rotate(45deg); }
}
/* --- 2. Title AI 标识(标题行右侧,浅色背景+主题色文字+边框) --- */
.ai-title-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 8px 2px 3px;
background: linear-gradient(135deg, color-mix(in srgb, var(--ai-from) 8%, white), color-mix(in srgb, var(--ai-to) 10%, white));
border: 1px solid color-mix(in srgb, var(--ai-from) 35%, transparent);
border-radius: 10px;
font-size: 11px;
font-weight: 500;
color: var(--ai-from-deep);
white-space: nowrap;
position: relative;
overflow: hidden;
line-height: 1.4;
/* 呼吸脉冲:模拟 AI 思考进度感 */
animation: ai-pulse 3s ease-in-out infinite;
}
@keyframes ai-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(var(--ai-pulse-r, 102), var(--ai-pulse-g, 126), var(--ai-pulse-b, 234), 0); }
50% { box-shadow: 0 0 8px 2px rgba(var(--ai-pulse-r, 102), var(--ai-pulse-g, 126), var(--ai-pulse-b, 234), 0.35); }
}
/* 各配色的脉冲 RGB 值 */
.ai-color-red .ai-title-badge,
.ai-title-badge.ai-color-red { --ai-pulse-r: 231; --ai-pulse-g: 76; --ai-pulse-b: 60; }
.ai-color-orange .ai-title-badge,
.ai-title-badge.ai-color-orange { --ai-pulse-r: 230; --ai-pulse-g: 126; --ai-pulse-b: 34; }
.ai-color-yellow .ai-title-badge,
.ai-title-badge.ai-color-yellow { --ai-pulse-r: 212; --ai-pulse-g: 160; --ai-pulse-b: 23; }
.ai-color-blue .ai-title-badge,
.ai-title-badge.ai-color-blue { --ai-pulse-r: 41; --ai-pulse-g: 128; --ai-pulse-b: 185; }
.ai-color-indigo .ai-title-badge,
.ai-title-badge.ai-color-indigo { --ai-pulse-r: 102; --ai-pulse-g: 126; --ai-pulse-b: 234; }
.ai-color-purple .ai-title-badge,
.ai-title-badge.ai-color-purple { --ai-pulse-r: 118; --ai-pulse-g: 75; --ai-pulse-b: 162; }
.ai-title-badge-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
/* 主题色发光 */
filter: drop-shadow(0 0 2px var(--ai-from));
}
.ai-title-badge-icon svg {
width: 16px;
height: 16px;
}
/* 高光扫过(柔和、微弱) */
.ai-title-badge::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent 30%,
rgba(255,255,255,0.15) 43%,
rgba(255,255,255,0.22) 50%,
rgba(255,255,255,0.15) 57%,
transparent 70%
);
animation: ai-shimmer 14s ease-in-out infinite;
}
/* --- 星级评价备注卡片右上角14px --- */
.star-rating {
display: inline-flex;
align-items: center;
gap: 1px;
}
.star-rating .star {
position: relative;
width: 14px;
height: 14px;
cursor: default;
}
.star-rating .star svg {
width: 14px;
height: 14px;
}
/* 空星 */
.star-rating .star .star-empty {
color: #e8e8e8;
}
/* 满星 / 半星用 clip 实现 */
.star-rating .star .star-fill {
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
/* 颜色方案浅黄→黄→橙→深红橙0-10 分) */
.star-rating[data-score="0"] { --star-color: #d4d4d4; }
.star-rating[data-score="1"] { --star-color: #f5e6a3; }
.star-rating[data-score="2"] { --star-color: #f0d86e; }
.star-rating[data-score="3"] { --star-color: #f0c93a; }
.star-rating[data-score="4"] { --star-color: #f0b429; }
.star-rating[data-score="5"] { --star-color: #f09c1a; }
.star-rating[data-score="6"] { --star-color: #ed8a0a; }
.star-rating[data-score="7"] { --star-color: #e67e22; }
.star-rating[data-score="8"] { --star-color: #e05d1a; }
.star-rating[data-score="9"] { --star-color: #d44a12; }
.star-rating[data-score="10"] { --star-color: #c0392b; }
.star-rating .star .star-fill svg {
color: var(--star-color);
}
/* 高分微光 */
.star-rating[data-score="9"] .star .star-fill svg,
.star-rating[data-score="10"] .star .star-fill svg {
filter: drop-shadow(0 0 2px rgba(192,57,43,0.4));
}
/* 备注卡片内星级评价:删除按钮左侧,垂直居中 */
.note-card-wrap {
position: relative;
}
.note-card-wrap .star-rating {
/* 不再绝对定位,改为在 flex 布局中自然排列 */
flex-shrink: 0;
align-self: center;
}

View File

@@ -234,6 +234,22 @@
<iframe src="pages/chat.html" class="phone-screen"></iframe>
</div>
</div>
<!-- 客户服务记录页 -->
<div>
<div class="page-label">客户服务记录页</div>
<div class="phone-frame">
<iframe src="pages/customer-service-records.html" class="phone-screen"></iframe>
</div>
</div>
<!-- 业绩记录页 -->
<div>
<div class="page-label">业绩记录页</div>
<div class="phone-frame">
<iframe src="pages/performance-records.html" class="phone-screen"></iframe>
</div>
</div>
</div>
</div>
</body>

49
docs/h5_ui/js/ai-icons.js Normal file
View File

@@ -0,0 +1,49 @@
/**
* AI Icon 通用初始化脚本
* - 页面加载时随机分配配色
* - 嵌入 Icon 注入机器人 SVG
* - 渲染星级评价组件
*/
(function () {
var ROBOT_SVG = '<svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg>';
var COLOR_CLASSES = [
'ai-color-red', 'ai-color-orange', 'ai-color-yellow',
'ai-color-blue', 'ai-color-indigo', 'ai-color-purple'
];
// 随机选一个配色(同一页面统一)
var pick = COLOR_CLASSES[Math.floor(Math.random() * COLOR_CLASSES.length)];
// 给所有嵌入 Icon 和 title badge 加上配色 class
document.querySelectorAll('.ai-inline-icon, .ai-title-badge').forEach(function (el) {
el.classList.add(pick);
});
// 嵌入 Icon如果内部没有 SVG自动注入机器人
document.querySelectorAll('.ai-inline-icon').forEach(function (el) {
if (!el.querySelector('svg')) {
el.innerHTML = ROBOT_SVG;
}
});
// 渲染星级评价
document.querySelectorAll('.star-rating').forEach(function (container) {
var score = parseInt(container.getAttribute('data-score') || '0', 10);
score = Math.max(0, Math.min(10, score));
var fullStars = Math.floor(score / 2);
var halfStar = score % 2 === 1;
var html = '';
for (var i = 0; i < 5; i++) {
var fillWidth = '0%';
if (i < fullStars) fillWidth = '100%';
else if (i === fullStars && halfStar) fillWidth = '50%';
html += '<span class="star">'
+ '<svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>'
+ '<span class="star-fill" style="width:' + fillWidth + '">'
+ '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>'
+ '</span></span>';
}
container.innerHTML = html;
});
})();

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 标识配色演示</title>
<link href="../css/ai-icons.css" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Noto Sans SC', -apple-system, sans-serif; background: #f5f5f5; padding: 20px; }
h1 { font-size: 18px; color: #242424; margin-bottom: 20px; text-align: center; }
h2 { font-size: 15px; color: #5e5e5e; margin: 24px 0 12px; padding-left: 8px; border-left: 3px solid #667eea; }
.demo-row { display: flex; align-items: center; gap: 16px; padding: 14px 16px; background: #fff; border-radius: 12px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.demo-row .label { font-size: 13px; color: #8b8b8b; min-width: 36px; }
.demo-row .sample-text { font-size: 14px; color: #5e5e5e; display: flex; align-items: center; }
.note { font-size: 12px; color: #a6a6a6; text-align: center; margin-top: 8px; }
</style>
</head>
<body>
<h1>AI 标识配色方案演示</h1>
<!-- ===== 嵌入 Icon ===== -->
<h2>嵌入 Icon行首小图标 · 机器人)</h2>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-red"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-orange"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-yellow"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-blue"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-indigo"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="sample-text"><span class="ai-inline-icon ai-color-purple"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white"/><path d="M12 7V4" stroke="white" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white"/></svg></span>高流失风险,建议尽快联系</span>
</div>
<p class="note">每个页面所有嵌入 Icon 统一使用一种配色,刷新后随机分配</p>
<!-- ===== Title AI 标识 ===== -->
<h2>Title AI 标识(标题行右侧 · 机器人 · 浅色背景+边框)</h2>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-red"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-orange"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-yellow"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-blue"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-indigo"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="demo-row">
<span class="label"></span>
<span class="ai-title-badge ai-color-purple"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<p class="note">每个页面所有 Title AI 标识统一使用一种配色,刷新后随机分配</p>
</body>
</html>

View File

@@ -11,24 +11,24 @@
theme: {
extend: {
colors: {
primary: '#333333',
'primary-light': '#f5f5f5',
success: '#2d2d2d',
warning: '#666666',
error: '#444444',
'gray-1': '#fafafa',
'gray-2': '#f5f5f5',
'gray-3': '#eeeeee',
'gray-4': '#e0e0e0',
'gray-5': '#bdbdbd',
'gray-6': '#9e9e9e',
'gray-7': '#757575',
'gray-8': '#616161',
'gray-9': '#424242',
'gray-10': '#333333',
'gray-11': '#212121',
'gray-12': '#1a1a1a',
'gray-13': '#111111',
primary: '#0052d9',
'primary-light': '#ecf2fe',
success: '#00a870',
warning: '#ed7b2f',
error: '#e34d59',
'gray-1': '#f3f3f3',
'gray-2': '#eeeeee',
'gray-3': '#e7e7e7',
'gray-4': '#dcdcdc',
'gray-5': '#c5c5c5',
'gray-6': '#a6a6a6',
'gray-7': '#8b8b8b',
'gray-8': '#777777',
'gray-9': '#5e5e5e',
'gray-10': '#4b4b4b',
'gray-11': '#393939',
'gray-12': '#2c2c2c',
'gray-13': '#242424',
},
fontFamily: {
sans: ['Noto Sans SC', 'sans-serif'],

View File

@@ -7,6 +7,7 @@
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="../css/banner.css" rel="stylesheet">
<link href="../css/ai-icons.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
@@ -84,7 +85,10 @@
<div class="p-4 space-y-4">
<!-- 消费习惯 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="st green text-sm font-semibold text-gray-13 mb-4">消费习惯</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="st green text-sm font-semibold text-gray-13">消费习惯</h2>
<span class="ai-title-badge ai-color-red"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<span class="px-3 py-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 text-primary text-xs rounded-full border border-blue-100">🌙 常来夜场</span>
<span class="px-3 py-1.5 bg-gradient-to-r from-green-50 to-emerald-50 text-success text-xs rounded-full border border-green-100">🎱 偏爱中式</span>

View File

@@ -1,13 +0,0 @@
{
"folders": [
{
"path": "../../../../dev/LLTQ/ETL/feiqiu-ETL"
},
{
"path": "../.."
}
],
"settings": {
"liveServer.settings.multiRootWorkspaceName": "LLZQ-1"
}
}

View File

@@ -100,21 +100,6 @@
</svg>
</a>
<a href="home-settings.html" class="flex items-center justify-between bg-white px-4 py-4 border-b border-gray-1" style="display:none">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-warning/10 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
</svg>
</div>
<span class="text-sm text-gray-13">首页设置</span>
</div>
<svg class="w-4 h-4 text-gray-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
</a>
<button onclick="showLogoutModal()" class="w-full flex items-center justify-between bg-white px-4 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-error/10 rounded-lg flex items-center justify-center">

View File

@@ -8,6 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="../css/banner.css" rel="stylesheet">
<link href="../css/task-detail.css" rel="stylesheet">
<link href="../css/ai-icons.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
@@ -78,7 +79,10 @@
<div class="p-4 space-y-4">
<!-- 消费习惯 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title green text-sm font-semibold text-gray-13 mb-4">消费习惯</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title green text-sm font-semibold text-gray-13">消费习惯</h2>
<span class="ai-title-badge ai-color-blue"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<span class="px-3 py-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 text-primary text-xs rounded-full border border-blue-100">🎱 斯诺克爱好者</span>
<span class="px-3 py-1.5 bg-gradient-to-r from-green-50 to-emerald-50 text-success text-xs rounded-full border border-green-100">⭐ 高满意度</span>
@@ -91,7 +95,10 @@
<!-- 与我的关系 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title blue text-sm font-semibold text-gray-13 mb-4">与我的关系</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title blue text-sm font-semibold text-gray-13">与我的关系</h2>
<span class="ai-title-badge ai-color-blue"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<span class="px-4 py-2 bg-gradient-to-r from-pink-500 to-rose-500 text-white text-sm font-semibold rounded-xl shadow-sm">💖 非常好</span>
@@ -164,7 +171,10 @@
<h2 class="section-title purple text-sm font-semibold text-gray-13 mb-4">任务建议</h2>
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4 border border-blue-100">
<p class="text-sm text-primary leading-relaxed font-medium mb-3">
📞 常规回访要点
<span class="flex items-center justify-between">
<span>📞 常规回访要点</span>
<span class="ai-title-badge ai-color-blue"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</span>
</p>
<p class="text-sm text-gray-9 leading-relaxed mb-2">
该客户上次到店是 3 天前,关系良好,进行常规关怀回访:
@@ -176,10 +186,27 @@
</ul>
</div>
<div class="mt-4 p-4 bg-gray-50 rounded-xl border border-gray-100">
<p class="text-sm text-gray-9 leading-relaxed">
<span class="font-medium text-gray-13">💬 话术参考</span><br/>
"赵姐您好!上次打球感觉怎么样?新到的球杆手感还习惯吗?这周末您有空的话,可以提前帮您预留老位置~"
</p>
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-gray-13 text-sm">💬 话术参考</span>
<span class="ai-title-badge ai-color-blue"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="space-y-3">
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"赵姐您好!上次打球感觉怎么样?新到的球杆手感还习惯吗?这周末您有空的话,可以提前帮您预留老位置~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"赵姐,最近店里新进了一批斯诺克专用巧克粉,手感特别好,下次来的时候可以试试~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"赵姐好呀,上次您说想学几个高级杆法,我最近整理了一些教学视频,要不要发给您先看看?"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"赵姐这周六下午VIP包厢有空位要不要帮您提前预留可以叫上朋友一起来打球~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"赵姐您好,我们下个月有个会员积分兑换活动,您的积分可以换不少好东西,到时候提醒您哦~"
</p>
</div>
</div>
</div>
@@ -190,20 +217,22 @@
<span class="text-xs text-gray-6">2 条备注</span>
</div>
<div id="noteList" class="space-y-3">
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="note-card-wrap flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-02-07</p>
<p class="text-sm text-gray-9 leading-relaxed">赵姐反馈上次体验很满意新球杆手感不错希望下次能预留VIP包厢。</p>
</div>
<div class="star-rating" data-score="9"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="note-card-wrap flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-01-25</p>
<p class="text-sm text-gray-9 leading-relaxed">已预约本周六下午到店,需要提前安排靠窗位置。</p>
</div>
<div class="star-rating" data-score="9"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>

View File

@@ -8,6 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="../css/banner.css" rel="stylesheet">
<link href="../css/task-detail.css" rel="stylesheet">
<link href="../css/ai-icons.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
@@ -78,7 +79,10 @@
<div class="p-4 space-y-4">
<!-- 消费习惯 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title green text-sm font-semibold text-gray-13 mb-4">消费习惯</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title green text-sm font-semibold text-gray-13">消费习惯</h2>
<span class="ai-title-badge ai-color-orange"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<span class="px-3 py-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 text-primary text-xs rounded-full border border-blue-100">🌙 偏好夜场</span>
<span class="px-3 py-1.5 bg-gradient-to-r from-green-50 to-emerald-50 text-success text-xs rounded-full border border-green-100">🎱 中式八球</span>
@@ -91,7 +95,10 @@
<!-- 与我的关系 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title blue text-sm font-semibold text-gray-13 mb-4">与我的关系</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title blue text-sm font-semibold text-gray-13">与我的关系</h2>
<span class="ai-title-badge ai-color-orange"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<span class="px-4 py-2 bg-gradient-to-r from-amber-400 to-yellow-500 text-white text-sm font-semibold rounded-xl shadow-sm">💛 一般</span>
@@ -164,7 +171,10 @@
<h2 class="section-title orange text-sm font-semibold text-gray-13 mb-4">任务建议</h2>
<div class="bg-gradient-to-br from-orange-50 to-amber-50 rounded-xl p-4 border border-orange-100">
<p class="text-sm text-warning leading-relaxed font-medium mb-3">
💡 建议执行
<span class="flex items-center justify-between">
<span>💡 建议执行</span>
<span class="ai-title-badge ai-color-orange"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</span>
</p>
<p class="text-sm text-gray-9 leading-relaxed mb-2">
该客户消费频率从月均 4 次下降到近月仅 1 次,需要关注原因:
@@ -176,10 +186,27 @@
</ul>
</div>
<div class="mt-4 p-4 bg-gray-50 rounded-xl border border-gray-100">
<p class="text-sm text-gray-9 leading-relaxed">
<span class="font-medium text-gray-13">💬 话术参考</span><br/>
"张哥,好久没见您来打球了,最近忙吗?店里这周六有个球友聚会活动,想邀请您来玩,顺便认识一些新球友~"
</p>
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-gray-13 text-sm">💬 话术参考</span>
<span class="ai-title-badge ai-color-orange"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="space-y-3">
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"张哥,好久没见您来打球了,最近忙吗?店里这周六有个球友聚会活动,想邀请您来玩,顺便认识一些新球友~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"张哥好呀,最近工作还顺利吧?周末有空的话过来放松一下,我帮您约几个水平差不多的球友一起切磋~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"张哥,店里最近新上了几款精酿啤酒,打完球来一杯特别爽,周末要不要来试试?"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"张哥,上次您说想练练组合球,我最近研究了几个不错的训练方法,下次来的时候教您~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"张哥您好,这个月会员充值有额外赠送活动,力度挺大的,要不要了解一下?"
</p>
</div>
</div>
</div>
@@ -190,20 +217,22 @@
<span class="text-xs text-gray-6">2 条备注</span>
</div>
<div id="noteList" class="space-y-3">
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="note-card-wrap flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-02-03</p>
<p class="text-sm text-gray-9 leading-relaxed">张先生说最近换了工作,下班时间不固定,周末可能更方便。</p>
</div>
<div class="star-rating" data-score="5"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="note-card-wrap flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-01-15</p>
<p class="text-sm text-gray-9 leading-relaxed">推荐了周末球友聚会活动,客户表示有兴趣但还没确认。</p>
</div>
<div class="star-rating" data-score="5"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>

View File

@@ -8,6 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="../css/banner.css" rel="stylesheet">
<link href="../css/task-detail.css" rel="stylesheet">
<link href="../css/ai-icons.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
@@ -78,7 +79,10 @@
<div class="p-4 space-y-4">
<!-- 消费习惯 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title green text-sm font-semibold text-gray-13 mb-4">消费习惯</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title green text-sm font-semibold text-gray-13">消费习惯</h2>
<span class="ai-title-badge ai-color-purple"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<span class="px-3 py-1.5 bg-gradient-to-r from-amber-50 to-yellow-50 text-amber-600 text-xs rounded-full border border-amber-100">☀️ 偏好下午</span>
<span class="px-3 py-1.5 bg-gradient-to-r from-green-50 to-emerald-50 text-success text-xs rounded-full border border-green-100">🎱 初学者</span>
@@ -91,7 +95,10 @@
<!-- 与我的关系 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title blue text-sm font-semibold text-gray-13 mb-4">与我的关系</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title blue text-sm font-semibold text-gray-13">与我的关系</h2>
<span class="ai-title-badge ai-color-purple"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<span class="px-4 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white text-sm font-semibold rounded-xl shadow-sm">💙 待发展</span>
@@ -164,7 +171,10 @@
<h2 class="section-title pink text-sm font-semibold text-gray-13 mb-4">任务建议</h2>
<div class="bg-gradient-to-br from-pink-50 to-rose-50 rounded-xl p-4 border border-pink-100">
<p class="text-sm text-pink-600 leading-relaxed font-medium mb-3">
💝 关系构建重点
<span class="flex items-center justify-between">
<span>💝 关系构建重点</span>
<span class="ai-title-badge ai-color-purple"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</span>
</p>
<p class="text-sm text-gray-9 leading-relaxed mb-2">
该客户消费潜力大但关系指数较低,建议重点培养:
@@ -176,10 +186,27 @@
</ul>
</div>
<div class="mt-4 p-4 bg-gray-50 rounded-xl border border-gray-100">
<p class="text-sm text-gray-9 leading-relaxed">
<span class="font-medium text-gray-13">💬 话术参考</span><br/>
"陈哥您好,上次看您打球进步很快呀!我们这周有个初学者交流会,可以认识一些同水平的球友一起练习,您有兴趣参加吗?"
</p>
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-gray-13 text-sm">💬 话术参考</span>
<span class="ai-title-badge ai-color-purple"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="space-y-3">
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"陈哥您好,上次看您打球进步很快呀!我们这周有个初学者交流会,可以认识一些同水平的球友一起练习,您有兴趣参加吗?"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"陈哥好呀,上次教您的那个发力技巧练得怎么样了?下次来的时候我再帮您看看,争取早日突破~"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"陈哥,我们店里新推出了一个入门课程套餐,专门针对想快速提升的球友,性价比很高,要不要了解一下?"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"陈哥您好,这周六下午有几位球友约了练习赛,水平都差不多,要不要一起来切磋切磋?"
</p>
<p class="text-sm text-gray-9 leading-relaxed pl-3 border-l-2 border-primary/30">
"陈哥,最近天气不错,下午来打打球放松一下吧,我帮您留个好位置~"
</p>
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="../css/banner.css" rel="stylesheet">
<link href="../css/task-detail.css" rel="stylesheet">
<link href="../css/ai-icons.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
@@ -39,6 +40,32 @@
}
}
</script>
<style>
/* 话术气泡 */
.speech-bubble {
position: relative;
background: #eef2ff;
border: 1px solid #b4c0f0;
border-radius: 14px;
padding: 12px 16px;
font-size: 14px;
line-height: 1.7;
color: #5e5e5e;
}
/* 引出角:底部偏右,用旋转正方形模拟 */
.speech-bubble::after {
content: '';
position: absolute;
bottom: -7px;
right: 20px;
width: 12px;
height: 12px;
background: #eef2ff;
border-right: 1px solid #b4c0f0;
border-bottom: 1px solid #b4c0f0;
transform: rotate(45deg);
}
</style>
</head>
<body class="bg-gray-1 min-h-screen">
<!-- 通栏 Banner - 客户信息 -->
@@ -79,7 +106,10 @@
<div class="p-4 space-y-4">
<!-- 消费习惯 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title green text-sm font-semibold text-gray-13 mb-4">消费习惯</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title green text-sm font-semibold text-gray-13">消费习惯</h2>
<span class="ai-title-badge ai-color-indigo"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<span class="px-3 py-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 text-primary text-xs rounded-full border border-blue-100">🌙 常来夜场</span>
<span class="px-3 py-1.5 bg-gradient-to-r from-green-50 to-emerald-50 text-success text-xs rounded-full border border-green-100">🎱 偏爱中式</span>
@@ -92,7 +122,10 @@
<!-- 与我的关系 -->
<div class="bg-white rounded-2xl p-5 shadow-sm">
<h2 class="section-title blue text-sm font-semibold text-gray-13 mb-4">与我的关系</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title blue text-sm font-semibold text-gray-13">与我的关系</h2>
<span class="ai-title-badge ai-color-indigo"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<span class="px-4 py-2 bg-gradient-to-r from-pink-500 to-rose-500 text-white text-sm font-semibold rounded-xl shadow-sm">💖 非常好</span>
@@ -165,7 +198,10 @@
<h2 class="section-title orange text-sm font-semibold text-gray-13 mb-4">任务建议</h2>
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4 border border-blue-100">
<p class="text-sm text-primary leading-relaxed font-medium mb-3">
💡 建议执行
<span class="flex items-center justify-between">
<span>💡 建议执行</span>
<span class="ai-title-badge ai-color-indigo"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</span>
</p>
<p class="text-sm text-gray-9 leading-relaxed mb-2">
该客户已有 15 天未到店,存在流失风险。建议通过微信联系:
@@ -176,11 +212,18 @@
<li>根据其偏好时段(晚间)推荐合适的时间</li>
</ul>
</div>
<div class="mt-4 p-4 bg-gray-50 rounded-xl border border-gray-100">
<p class="text-sm text-gray-9 leading-relaxed">
<span class="font-medium text-gray-13">💬 话术参考</span><br/>
"王哥您好,好久不见!最近店里新到了几张国际标准的斯诺克球桌,知道您是斯诺克爱好者,想邀请您有空来体验一下~"
</p>
<div class="mt-4">
<div class="flex items-center justify-between mb-3">
<span class="font-medium text-gray-13 text-sm">💬 话术参考</span>
<span class="ai-title-badge ai-color-indigo"><span class="ai-title-badge-icon"><svg viewBox="0 0 24 24" fill="none"><rect x="5" y="7" width="14" height="12" rx="4" fill="white" stroke="currentColor" stroke-width="0.8"/><path d="M12 7V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="3" r="1.5" fill="white" stroke="currentColor" stroke-width="0.7"/><circle cx="9" cy="11.5" r="2" fill="#667eea"/><circle cx="15" cy="11.5" r="2" fill="#667eea"/><circle cx="9.5" cy="11" r="0.7" fill="white"/><circle cx="15.5" cy="11" r="0.7" fill="white"/><path d="M9.5 15C10 16 14 16 14.5 15" stroke="#667eea" stroke-width="1.5" stroke-linecap="round"/><circle cx="7" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><circle cx="17" cy="13.5" r="1" fill="#f5a0c0" opacity="0.6"/><rect x="3" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/><rect x="19" y="10" width="2" height="4" rx="1" fill="white" stroke="currentColor" stroke-width="0.7"/></svg></span>AI智能洞察</span>
</div>
<div class="flex flex-col gap-5">
<div class="speech-bubble">王哥您好,好久不见!最近店里新到了几张国际标准的斯诺克球桌,知道您是斯诺克爱好者,想邀请您有空来体验一下~</div>
<div class="speech-bubble">王哥,最近忙吗?这周末我们有个老客户专属的球友交流赛,奖品还挺丰富的,您要不要来参加?</div>
<div class="speech-bubble">王哥好呀,上次您提到想练练斯诺克的走位,我最近研究了一些新的训练方法,下次来的时候可以一起试试~</div>
<div class="speech-bubble">王哥,好久没见您了,您的老位置 A12 号台一直给您留着呢!最近晚上人不多,环境特别好,随时欢迎您来~</div>
<div class="speech-bubble">王哥您好,我们这个月推出了储值会员专属的夜场优惠套餐,包含球台+酒水,性价比很高,给您留意着呢~</div>
</div>
</div>
</div>
@@ -191,32 +234,41 @@
<span class="text-xs text-gray-6">3 条备注</span>
</div>
<div id="noteList" class="space-y-3">
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-02-05</p>
<p class="text-sm text-gray-9 leading-relaxed">已通过微信联系王先生,表示对新到的斯诺克球桌感兴趣,周末可能来体验。</p>
<div class="note-card-wrap p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex items-center justify-between mb-1.5">
<p class="text-xs text-gray-6">2026-02-05</p>
<div class="flex items-center gap-2">
<div class="star-rating" data-score="7"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
</div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
<p class="text-sm text-gray-9 leading-relaxed">已通过微信联系王先生,表示对新到的斯诺克球桌感兴趣,周末可能来体验。</p>
</div>
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-01-20</p>
<p class="text-sm text-gray-9 leading-relaxed">王先生最近出差较多,到店频率降低。建议等他回来后再约。</p>
<div class="note-card-wrap p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex items-center justify-between mb-1.5">
<p class="text-xs text-gray-6">2026-01-20</p>
<div class="flex items-center gap-2">
<div class="star-rating" data-score="7"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
</div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
<p class="text-sm text-gray-9 leading-relaxed">王先生最近出差较多,到店频率降低。建议等他回来后再约。</p>
</div>
<div class="flex items-start gap-3 p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-6 mb-1.5">2026-01-08</p>
<p class="text-sm text-gray-9 leading-relaxed">上次到店时推荐了会员续费活动,客户说考虑一下。</p>
<div class="note-card-wrap p-3.5 bg-gray-50 rounded-xl border border-gray-100">
<div class="flex items-center justify-between mb-1.5">
<p class="text-xs text-gray-6">2026-01-08</p>
<div class="flex items-center gap-2">
<div class="star-rating" data-score="7"><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:100%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:50%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span><span class="star"><svg class="star-empty" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span class="star-fill" style="width:0%"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span></span></div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
</div>
<button onclick="confirmDeleteNote()" class="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-5">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
<p class="text-sm text-gray-9 leading-relaxed">上次到店时推荐了会员续费活动,客户说考虑一下。</p>
</div>
</div>
<div id="noteEmpty" class="text-center py-6 hidden">

View File

@@ -7,6 +7,7 @@
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="../css/banner.css" rel="stylesheet">
<link href="../css/ai-icons.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
@@ -514,7 +515,7 @@
<span class="note-indicator" title="有备注">📝</span>
</div>
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店15天前 · 余额:非常多</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc">高流失风险,建议尽快联系</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>高流失风险,建议尽快联系</p>
</div>
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
@@ -532,7 +533,7 @@
<span class="text-sm">🧡</span>
</div>
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店20天前 · 余额:非常多</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc">VIP客户储值余额较多</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>VIP客户储值余额较多</p>
</div>
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
@@ -560,7 +561,7 @@
<span class="note-indicator" title="有备注">📝</span>
</div>
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店10天前 · 余额:一般</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc">消费频率下降,需关注</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>消费频率下降,需关注</p>
</div>
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
@@ -578,7 +579,7 @@
<span class="text-sm">💙</span>
</div>
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店8天前 · 余额:一般</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc">偏好晚间时段,可推荐夜场套餐</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>偏好晚间时段,可推荐夜场套餐</p>
</div>
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
@@ -596,7 +597,7 @@
<span class="text-sm">💙</span>
</div>
<p class="text-sm text-gray-7 leading-relaxed task-desc">最近到店5天前 · 余额:无</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc">潜力客户,建议加强互动</p>
<p class="text-sm text-gray-6 leading-relaxed task-desc"><span class="ai-inline-icon"></span>潜力客户,建议加强互动</p>
</div>
<svg class="w-5 h-5 text-gray-5 flex-shrink-0 ml-2 mt-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
@@ -746,7 +747,7 @@
card.addEventListener('touchend', function(e) {
clearTimeout(pressTimer);
if (!isLongPress && !isMoved && !this.classList.contains('abandoned')) {
if (!isLongPress && !isMoved) {
navigateCard(this);
}
});
@@ -784,7 +785,7 @@
card.addEventListener('mouseup', function() {
clearTimeout(pressTimer);
if (!isLongPress && !isMoved && !this.classList.contains('abandoned')) {
if (!isLongPress && !isMoved) {
navigateCard(this);
}
});
@@ -951,5 +952,3 @@
})();
</script>
</body>
</html>

View File

@@ -164,7 +164,7 @@
| 功能 | 数据源 | 状态 |
|------|--------|------|
| 用户审核列表 | `zqyy_app.auth.user_applications` + `zqyy_app.auth.users` | 📱 新建 |
| 用户-助教关联建议 | `dwd.dim_assistant`通过球房ID+手机号匹配) | ✅ FDW |
| 用户-助教关联建议 | `dwd.dim_assistant`通过球房ID+手机号匹配)+ `dwd.dim_staff` / `dwd.dim_staff_ex`(员工信息表匹配) | ✅ FDW |
| 球房ID映射 | `zqyy_app.auth.site_code_mapping` | 📱 新建 |
| Excel 上传-财务支出 | `dws.dws_finance_expense_summary`(或新建 staging 表) | 🔧/📱 |
| Excel 上传-团购收入 | `dws.dws_platform_settlement`(或新建 staging 表) | 🔧/📱 |
@@ -263,7 +263,14 @@
| `dws.cfg_performance_tier` | 定档配置 |
| `dws.cfg_assistant_level_price` | 助教等级单价 |
| `dws.cfg_bonus_rules` | 奖金规则 |
| `dws.cfg_index_parameters` | 指数参数配置 |
| `dws.cfg_index_parameters` | 指数参数配置(含 SPI 26 个参数 ✅) |
| `dws.dws_order_summary` | 订单汇总 |
### 新增 FDW 映射(员工信息表)
| 来源表 | 用途 |
|--------|------|
| `dwd.dim_staff` | 员工基础维度(姓名、手机、岗位、在职状态等),用于用户申请人员匹配 |
| `dwd.dim_staff_ex` | 员工扩展维度(工号、头像、职级、分组等),用于用户申请人员匹配 |
---

View File

@@ -87,14 +87,15 @@ P11 部署与上线(环境配置 + 监控 + 灰度)
### SPEC 名称建议:`miniapp-auth-system`
### 需求概述
构建小程序的完整用户认证体系包括微信登录、用户申请、审核流程、RBAC 权限、用户-助教绑定。
构建小程序的完整用户认证体系包括微信登录、用户申请、审核流程、RBAC 权限、用户-助教/员工绑定。
### 关键交付物
1. 新建表:`auth.users`(重构,增加 wx_openid、status、site_id 等)、`auth.user_applications``auth.site_code_mapping``auth.user_assistant_binding`
1. 新建表:`auth.users`(重构,增加 wx_openid、status、site_id 等)、`auth.user_applications``auth.site_code_mapping``auth.user_assistant_binding`(增加 `staff_id` 字段支持员工绑定)
2. 重构现有 `public.roles``public.permissions``public.user_roles``public.role_permissions` 迁移至 `auth` Schema
3. 后端 API微信 code2Session 登录、用户申请提交、JWT 签发
4. 后端 API用户状态查询审核中/通过/拒绝)
5. 权限中间件:基于角色的 API 访问控制
6. 人员匹配:用户申请时同时在助教表(`dim_assistant`)和员工信息表(`dim_staff` + `dim_staff_ex`)中匹配
### 依赖
- P1`auth` Schema 已创建)

View File

@@ -19,7 +19,7 @@
- AC1租户管理后台是独立 Web 应用,独立登录入口
- AC2租户管理员只能看到自己租户下的店铺和用户
- AC3用户审核页面申请列表、状态筛选、球房ID+手机号关联建议、审核操作
- AC3用户审核页面申请列表、状态筛选、球房ID+手机号关联建议(同时匹配助教表+员工信息表)、审核操作
- AC4Excel 上传校验:必填、金额精度、表头格式、类型合法
- AC5主键冲突时前端展示 diff 交互,逐条确认后保存
- AC6助教奖罚支持同一助教同月多笔
@@ -130,4 +130,4 @@ biz.excel_upload_log
- [ ] T6实现 Excel 上传后端(解析 + 校验 + 冲突检测)
- [ ] T7实现 Excel 上传前端(模板下载 + 上传 + diff 交互 + 确认)
- [ ] T8实现 4 种 Excel 模板的校验规则
- [ ] T9实现助教姓名+编号匹配校验(对照 `dim_assistant`
- [ ] T9实现人员姓名+编号匹配校验(同时对照 `dim_assistant` 助教表和 `dim_staff` + `dim_staff_ex` 员工信息表

View File

@@ -21,7 +21,7 @@
- AC3`dws_member_consumption_summary` 新增字段有值(`recharge_count_30d/60d/90d``recharge_amount_30d/60d/90d``avg_ticket_amount`
- AC4`dws_assistant_daily_detail` 新增字段在符合惩罚条件的订单上正确填充
- AC5新表的 RLS 视图和 FDW 映射已同步创建
- AC6`cfg_index_parameters` 中新增 `SPI` 类型的配置行
- AC6`cfg_index_parameters` 中新增 `SPI` 类型的配置行已完成26 个参数effective_from=2026-02-23
---
@@ -56,9 +56,9 @@
## 任务清单
- [ ] T1设计并创建 `dws_member_spending_power_index`
- [ ] T2实现 `SpendingPowerIndexTask`(含 Level/Speed/Stability 子分)
- [ ] T3`cfg_index_parameters` 中插入 SPI 默认参数
- [x] T1设计并创建 `dws_member_spending_power_index`已完成24 字段 + 2 索引,迁移脚本 `2026-02-23_create_dws_member_spending_power_index.sql`测试库已建表0 行待跑数)
- [x] T2实现 `SpendingPowerIndexTask`(含 Level/Speed/Stability 子分)✅(已完成,代码 `tasks/dws/index/spending_power_index_task.py`,已注册 task_registry含单元测试 + 属性测试 + BD_Manual 文档)
- [x] T3`cfg_index_parameters` 中插入 SPI 默认参数已完成27 个参数含 Level/Speed/Stability 权重、压缩基数、窗口天数等)
- [ ] T4设计并创建 `dws_assistant_order_contribution`
- [ ] T5实现 `AssistantOrderContributionTask`
- [ ] T6扩展 `dws_member_consumption_summary`(充值窗口 + 次均消费字段)

View File

@@ -11,7 +11,7 @@
1. 作为球房工作人员我需要通过微信登录小程序首次登录时填写申请表单球房ID、申请身份、手机号、编号、昵称
2. 作为租户管理员,我需要审核用户申请,将用户关联到对应的助教/员工,并分配身份权限。
3. 作为系统我需要通过球房ID+手机号自动建议用户与助教的对应关系。
3. 作为系统我需要通过球房ID+手机号自动建议用户与助教/员工的对应关系(同时匹配 `dwd.dim_assistant` 助教表和 `dwd.dim_staff` + `dwd.dim_staff_ex` 员工信息表)
4. 作为用户,我需要看到自己的申请状态(审核中/通过/拒绝)。
5. 作为用户,我可以同时属于多个店铺(连锁场景),权限按店铺独立。
@@ -21,6 +21,7 @@
- AC2新用户首次登录后进入申请页面提交后状态为 pending
- AC3管理员审核通过后用户状态变为 approved可正常访问小程序
- AC4球房ID 不存在时,申请仍可提交,管理端显示"未找到关联信息"
- AC7用户申请信息比对时系统同时在助教表`dwd.dim_assistant`)和员工信息表(`dwd.dim_staff` / `dwd.dim_staff_ex`)中匹配,返回所有匹配结果供管理员选择
- AC5权限中间件正确拦截无权请求如助教不能看财务看板
- AC6一个用户可关联多个店铺切换店铺后数据正确隔离
@@ -51,10 +52,22 @@ auth.user_site_roles
auth.user_assistant_binding
- id, user_id, site_id, assistant_id (ETL dim_assistant)
- staff_id (ETL dim_staff可选员工绑定时填写)
- binding_type (assistant/staff/manager)
- created_at
```
### 人员匹配逻辑(用户申请 → 信息比对)
用户提交申请时填写球房ID + 手机号(或编号),系统自动在以下两张表中匹配:
1. 助教表 `dwd.dim_assistant`(通过 FDW `fdw_etl.v_assistant`):按 `site_id` + `mobile``alias_name` 匹配
2. 员工信息表 `dwd.dim_staff` + `dwd.dim_staff_ex`(通过 FDW`site_id` + `mobile``staff_name` / `alias_name` / `job_num` 匹配
匹配结果合并后返回给管理端,管理员从候选列表中选择正确的关联对象。若两张表均无匹配,显示"未找到关联信息",管理员可手动填写。
> 员工信息表数据链路:上游 API → `ods.staff_info_master` → `dwd.dim_staff`基础维度SCD2+ `dwd.dim_staff_ex`扩展维度SCD2
### 权限列表(固定)
| 权限 code | 说明 |

View File

@@ -0,0 +1,85 @@
# FDW 数据源迁移方案DWD → Core
> 创建日期2026-02-24
> 状态:已决策(上线后迭代)
> 关联 SPECminiapp-db-foundation
---
## 背景
当前 `miniapp-db-foundation` SPEC 中FDW 外部表映射的数据源链路为:
```
ETL 库 dwd.dim_* / dws.dws_*
→ app.v_dim_* / app.v_dws_*RLS 视图site_id 过滤)
→ 业务库 fdw_etl.v_dim_* / fdw_etl.v_dws_*FDW 外部表)
```
ETL 六层 Schema 中,`core` 层的定位是"跨门店标准化维度/事实",是 DWD 层的精简子集。长远来看,小程序读取的维度数据应从 `core` 层获取,而非直接读 `dwd` 层。
## 决策
上线时保持现状(`app` 视图指向 `dwd.*` / `dws.*`),上线后再迭代切换到 `core`
## 理由
1. Core 层当前仅 7 张表(`dim_site``dim_member``dim_assistant``dim_table``dim_goods_category``fact_settlement``fact_payment`),小程序需要的 11 张 DWD + 24 张 DWS 远超 Core 覆盖范围
2. Core 层字段是精简子集(如 `core.dim_member``card_balance``total_consumption` 等),小程序前期需要 DWD 的完整字段
3. `app` 视图层本身就是抽象层——后端通过 `fdw_etl.v_dim_member` 访问数据,底层视图定义可随时切换,后端代码零改动
4. 上线关键路径是 P0 阶段(基础设施 → 微信配置 → 后端核心功能Core 层完善属于 P2 范畴
## 迁移路径
### Phase 0上线前当前
按 SPEC 原样执行:
- `app.v_dim_member``SELECT * FROM dwd.dim_member WHERE site_id = ...`
- `app.v_dws_*``SELECT * FROM dws.dws_* WHERE site_id = ...`
- FDW 通过 `IMPORT FOREIGN SCHEMA app` 映射
### Phase 1上线后 — 补齐 Core 层表
- 盘点小程序实际用到的维度表,确认 Core 层需要补齐哪些字段
- 对照 DWD 层的 `dim_member``dim_assistant` 等,在 Core 层补全小程序所需的业务字段
- 新增 Core 层缺失的维度表(如 `core.dim_member_card_account``core.dim_staff` 等)
### Phase 2上线后 — ETL 填充任务
- 在 ETL Connector 中新增 DWD → Core 的填充任务
- 确保 Core 层数据与 DWD 层保持同步
- 验证 Core 层数据完整性和正确性
### Phase 3上线后 — 切换视图定义
- 修改 `app` 视图定义,将维度表从 `dwd.*` 切换到 `core.*`
```sql
-- 修改前
CREATE OR REPLACE VIEW app.v_dim_member AS
SELECT * FROM dwd.dim_member
WHERE site_id = current_setting('app.current_site_id')::bigint;
-- 修改后
CREATE OR REPLACE VIEW app.v_dim_member AS
SELECT * FROM core.dim_member
WHERE site_id = current_setting('app.current_site_id')::bigint;
```
- DWS 汇总表保持不变(它们本身就在 DWS 层Core 层不涉及汇总)
- 重新执行 `IMPORT FOREIGN SCHEMA app INTO fdw_etl` 同步外部表
- 后端和小程序代码无需任何改动
## 风险与缓解
| 风险 | 缓解措施 |
|------|---------|
| Core 层字段不全导致小程序功能缺失 | Phase 1 先盘点再补齐,确保字段覆盖 |
| DWD → Core 填充任务引入数据延迟 | Core 填充任务纳入 ETL 调度链,与 DWD 同步执行 |
| 切换视图时短暂不可用 | 使用 `CREATE OR REPLACE VIEW` 原子替换,无停机 |
| Core 层数据与 DWD 不一致 | 切换前运行数据一致性校验脚本 |
## 关键设计优势
`app` 视图层是整个方案的"切换开关"
- 后端只看到 `fdw_etl.v_dim_member`,不关心底层是 DWD 还是 Core
- 切换只需修改视图 SQL + 重新 IMPORT零代码改动
- 可以逐表切换(先切 `dim_member`,验证通过后再切其他表),降低风险

186
docs/roadmap/BACKLOG.md Normal file
View File

@@ -0,0 +1,186 @@
# 项目待办总览Backlog
> 最后更新2026-02-24
> 本文档汇总项目中所有已识别的待办事项,按子系统和优先级分类。
> 来源LAUNCH-CHECKLIST、SPEC 文档、字段审计报告、PRD、代码注释等。
---
## 阅读指南
- P0 = 不做就上不了线
- P1 = 上线前必须做
- P2 = 可上线后迭代
- ✅ = 已完成
- 🔲 = 待办
- 📋 = 有 SPEC 但未执行
---
## 一、小程序上线关键路径P0
来源:`docs/deployment/LAUNCH-CHECKLIST.md`
### 1.1 基础设施
| 状态 | 项目 | 说明 |
|------|------|------|
| ✅ | 服务器目录结构 + 仓库克隆 | 20260220 完成 |
| 🔲 | 配置环境变量文件 | 服务器 `.env` 手动创建 |
| 🔲 | 安装 Python 依赖 | `uv sync --all-packages` |
| 🔲 | 运行 `setup-server-git.py` | Git 排除规则 |
| 🔲 | 运行 `init-server-env.py` | 删除排除文件 + 创建 export 目录 |
| 🔲 | bat 脚本部署到服务器 | `D:\NeoZQYY\scripts\` |
| 🔲 | 确认 Nginx 反代规则 | 测试 8001 / 正式 8000 |
| 🔲 | SSL 证书自动续期 | |
| 🔲 | 数据库备份方案 | pg_dump + Windows 计划任务 |
### 1.2 微信侧配置
| 状态 | 项目 | 说明 |
|------|------|------|
| ✅ | 合法域名 + HTTPS | 已配置 |
| 🔲 | 消息推送配置提交验证 | 需服务器后端在线 |
| 🔲 | 用户隐私保护指引 | 微信后台填写 |
| 🔲 | 小程序名称/图标/简介/类目 | 审核必需 |
| 🔲 | 体验成员配置 | 内部测试必需 |
### 1.3 后端核心功能
| 状态 | 项目 | 说明 |
|------|------|------|
| 🔲 | 微信登录接口 | `POST /api/auth/wechat_login` |
| 🔲 | 权限中间件 | JWT site_id + role 校验 |
| 🔲 | 至少一个有实际功能的首页 | 审核要求 |
| 🔲 | 密钥配置 | `WX_APP_ID``WX_APP_SECRET``JWT_SECRET_KEY` |
### 1.4 数据库基础设施
| 状态 | 项目 | 说明 |
|------|------|------|
| 📋 | miniapp-db-foundation SPEC | 6 个任务全部待执行 |
| 🔲 | auth/biz Schema 创建 | SPEC 任务 2 |
| 🔲 | ETL RLS 视图层35 张) | SPEC 任务 1 |
| 🔲 | FDW 跨库映射 | SPEC 任务 3 |
| 🔲 | 端到端验证脚本 | SPEC 任务 5 |
---
## 二、安全与审计P1 — 上线前必须做)
来源:`docs/deployment/LAUNCH-CHECKLIST.md` 第五阶段
| 状态 | 项目 | 说明 |
|------|------|------|
| 🔲 | 用户申请/审核流 | `user_application` 表 + 审核 API |
| 🔲 | 审计日志 | `audit_log` 表 + 审计中间件 |
| 🔲 | 后端结构化日志 | 替代 uvicorn 默认日志 |
| 🔲 | 服务器防火墙确认 | Tailscale 网卡入站限制 |
| 🔲 | PostgreSQL 监听确认 | 仅内网/本机 |
| 🔲 | 消息推送切安全模式 | AES 加解密 |
---
## 三、审核准备P1 — 提交审核前)
来源:`docs/deployment/LAUNCH-CHECKLIST.md` 第六阶段
| 状态 | 项目 | 说明 |
|------|------|------|
| 🔲 | 主要页面功能截图 | |
| 🔲 | 测试账号 | |
| 🔲 | 类目资质文件 | 营业执照等 |
| 🔲 | 功能介绍文案 | |
---
## 四、ETL 待办
### 4.1 字段补全(来源:`field_review_for_user.md`
| 状态 | 项目 | 优先级 | 说明 |
|------|------|--------|------|
| 🔲 | 映射错误修复4 处) | 高 | site_assistant_id、discount_price 列名、batch_stock_qty、provisional_total_cost |
| 🔲 | 待新增/补映射字段40 个) | 中 | 分布在 12 张 ODS 表,含 A/B/C 三类 |
| 🔲 | 新建 DWD 表2 张) | 中 | `dwd_goods_stock_movement`(优先)、`settlement_ticket`(待定) |
| 🔲 | ODS 配置修改1 处) | 中 | `goods_stock_summary``requires_window=True` |
| 🔲 | ODS 平层化映射修复 | 低 | `table_area_id_list` 字段名映射错位 |
### 4.2 DWS 层待建表
| 状态 | 项目 | 来源 |
|------|------|------|
| 🔲 | `dws_member_spending_power_index` RLS 视图 | miniapp-db-foundation P2 预留 |
| 🔲 | `dws_assistant_order_contribution` | miniapp-db-foundation P2 预留 |
| 🔲 | `dws_goods_stock_summary`(日/周/月粒度) | dataflow-field-completion SPEC |
### 4.3 Core 层迁移
| 状态 | 项目 | 来源 |
|------|------|------|
| 🔲 | 补齐 Core 层维度表字段 | `docs/roadmap/2026-02-24__fdw-dwd-to-core-migration-plan.md` |
| 🔲 | DWD → Core 填充任务 | 同上 |
| 🔲 | app 视图从 DWD 切换到 Core | 同上 |
### 4.4 架构优化
| 状态 | 项目 | 来源 |
|------|------|------|
| 📋 | 冷数据归档(方案 5 | `ods_taskspec_refactor_proposal.md` 中长期待办 |
| 🔲 | ETL SDK 抽象 | LAUNCH-CHECKLIST 7.4,飞球 Connector → 通用基类 |
---
## 五、后端待办P2 — 上线后迭代)
来源:`docs/deployment/LAUNCH-CHECKLIST.md` 第七阶段
| 状态 | 项目 | 说明 |
|------|------|------|
| 🔲 | xlsx 导入/导出 | 上传、解析、校验、落库、错误报告 |
| 🔲 | 运维监控系统 | BS 架构,集成管理后台,取代 bat 脚本 |
| 🔲 | 租户模型 | tenant 层 + RLS Policy DDL |
| 🔲 | 后端 API 集成测试 | |
| 🔲 | 小程序端自动化测试 | |
| 🔲 | 依赖版本 pin 上限 | 当前 `>=0.115` 等范围较宽松 |
---
## 六、SPEC 状态总览
| SPEC | 状态 | 说明 |
|------|------|------|
| `admin-web-console` | ✅ 已完成 | Web 管理后台 |
| `assistant-abolish-cleanup` | ✅ 已完成 | 助教废除链路清理 |
| `dataflow-field-completion` | ✅ 已完成 | 字段补全与联调 |
| `dataflow-structure-audit` | ✅ 已完成 | 数据流结构分析重构 |
| `dwd-phase1-refactor` | ✅ 已完成 | DWD 第一阶段重构 |
| `etl-aggregation-fix` | ✅ 已完成 | ETL 聚合修复 |
| `etl-dws-flow-refactor` | ✅ 已完成 | DWS 流程重构 |
| `etl-fullstack-integration` | ✅ 已完成 | 全栈集成测试 |
| `etl-pipeline-debug` | ✅ 已完成 | ETL 全链路调试 |
| `etl-staff-dimension` | ✅ 已完成 | 员工维度表 |
| `ods-dedup-standardize` | ✅ 已完成 | ODS 去重标准化 |
| `spi-spending-power-index` | ✅ 已完成 | 消费力指数 |
| `miniapp-db-foundation` | 📋 待执行 | 小程序数据库基础设施6 个任务) |
---
## 七、文档待办
| 状态 | 项目 | 位置 |
|------|------|------|
| 🔲 | ADR架构决策记录模板 | `docs/architecture/` |
| 🔲 | 数据字典完善 | `docs/contracts/data_dictionary/` |
| 🔲 | 权限矩阵 | `docs/permission_matrix/` |
| 🔲 | 运维手册 | `docs/ops/` |
| ✅ | FDW DWD→Core 迁移方案 | `docs/roadmap/2026-02-24__fdw-dwd-to-core-migration-plan.md` |
---
## 维护说明
本文档为项目待办的单一汇总入口。新增待办时:
1. 先在对应的来源文档中记录LAUNCH-CHECKLIST、SPEC、字段审计报告等
2. 同步更新本文档对应分类
3. 完成后标记 ✅ 并注明日期