Compare commits

..

5 Commits

Author SHA1 Message Date
Neo
a3f4d04335 Updata2 2026-02-04 21:39:01 +08:00
Neo
ee773a9b52 确认 1 2026-02-04 21:38:22 +08:00
Neo
15948cbd64 同意 2026-02-04 21:19:34 +08:00
Neo
294c6edbc9 更新数据库文档 20260201-2 2026-02-01 23:42:18 +08:00
Neo
9b2c2c5c78 更新20260201-1 2026-02-01 22:04:15 +08:00
167 changed files with 65326 additions and 631 deletions

13
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.5",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

157
README.md
View File

@@ -78,6 +78,46 @@ python -m cli.main \
- `FETCH_ONLY`:仅在线抓取落盘,不入库 - `FETCH_ONLY`:仅在线抓取落盘,不入库
- `INGEST_ONLY`:仅从本地 JSON 回放入库(适合离线回放/补跑) - `INGEST_ONLY`:仅从本地 JSON 回放入库(适合离线回放/补跑)
## DWS 层(汇总/财务)
### 建表与初始化
- 建表:`INIT_DWS_SCHEMA`
- 配置:`SEED_DWS_CONFIG`
- 订单汇总(可选):`DWS_BUILD_ORDER_SUMMARY`
### 任务与调度建议
- **每小时**`DWS_ASSISTANT_DAILY`、`DWS_FINANCE_DAILY`、`DWS_FINANCE_INCOME_STRUCTURE`
- **每日**`DWS_ASSISTANT_MONTHLY`、`DWS_ASSISTANT_CUSTOMER`、`DWS_MEMBER_CONSUMPTION`、`DWS_MEMBER_VISIT`、`DWS_FINANCE_DISCOUNT_DETAIL`、`DWS_FINANCE_RECHARGE`、`DWS_ASSISTANT_FINANCE`
- **每月(月初)**`DWS_ASSISTANT_SALARY`
- **维护清理(按需)**`DWS_RETENTION_CLEANUP`
调度配置默认保存在 `etl_billiards/scheduled_tasks.json`GUI 调度器会读取该文件。
### 时间口径
- 周起始日:周一
- 月/季度起始:第一天 0 点
- 环比:对比上一个等长区间
- 窗口类型:本周/上周/本月/上月/前3个月不含本月/前3个月含本月/本季度/上季度/最近半年不含本月
### Excel 导入(支出/平台回款/充值提成)
脚本:`etl_billiards/scripts/import_dws_excel.py`
- 支出结构:`--type expense`,按月导入(房租/水电/物业/工资/报销/平台服务费等)
- 平台回款:`--type platform`,按回款日期导入(回款金额、佣金、服务费、订单号等)
- 充值提成:`--type commission`按月份导入助教ID、提成金额、充值订单金额等
### 大客户优惠拆分(可选)
用于将手动调整拆分为“大客户优惠/其他优惠”,可在配置中指定:
- `dws.discount.big_customer_member_ids`会员ID列表逗号分隔
- `dws.discount.big_customer_order_ids`订单ID列表逗号分隔
未配置时,大客户金额为 0手动调整全部计入“其他优惠”。
### 时间分层清理(可选)
任务:`DWS_RETENTION_CLEANUP`,按配置清理历史数据
- `dws.retention.enabled`:是否启用
- `dws.retention.layer`:分层(如 `LAST_3_MONTHS`
- `dws.retention.tables`:需要清理的表列表(逗号分隔)
- `dws.retention.table_layers`表级分层覆盖JSON 字符串)
## 目录结构与关键文件 ## 目录结构与关键文件
- 仓库根目录:`etl_billiards/` 主代码;`app/` 示例 runner`开发笔记/` 项目笔记;`tmp/` 草稿/调试归档;`requirements.txt`(仓库根)依赖;`run_etl.sh/.bat` 启动脚本。 - 仓库根目录:`etl_billiards/` 主代码;`app/` 示例 runner`开发笔记/` 项目笔记;`tmp/` 草稿/调试归档;`requirements.txt`(仓库根)依赖;`run_etl.sh/.bat` 启动脚本。
- 注意:根目录的 `run_etl.sh/.bat` 运行时要求当前目录为 `etl_billiards/`(因为入口是 `python -m cli.main`)。 - 注意:根目录的 `run_etl.sh/.bat` 运行时要求当前目录为 `etl_billiards/`(因为入口是 `python -m cli.main`)。
@@ -367,6 +407,95 @@ python -m cli.main \
6) 去嵌套:数组展开为子表/子行,重复 profile 提炼为维度。 6) 去嵌套:数组展开为子表/子行,重复 profile 提炼为维度。
7) 长期演进:优先加列/加表,减少对已有表结构的破坏。 7) 长期演进:优先加列/加表,减少对已有表结构的破坏。
## DWS 数据层(汇总层)
DWSData Warehouse Service层基于 DWD 明细层数据构建,提供预聚合的数据服务。
### 表结构概览
| 分类 | 表数量 | 说明 |
|------|--------|------|
| 配置表 | 5 | 绩效档位、等级定价、奖金规则、区域分类、技能映射 |
| 助教维度 | 5 | 日度/月度业绩、客户统计、工资计算、充值提成 |
| 客户维度 | 2 | 消费汇总、来店明细 |
| 财务维度 | 7 | 日度汇总、收入结构、优惠明细、充值统计、支出、助教收支、平台结算 |
| 订单汇总 | 1 | 订单级别聚合 |
### 核心特性
- **时间分层**支持近2天/近1月/近3月/全量的时间窗口筛选
- **滚动窗口**支持7/10/15/30/60/90天的滚动统计
- **SCD2 as-of**:维度取值支持按时间点获取历史值(如助教等级)
- **幂等更新**:采用 delete-before-insert 策略,支持重复执行
- **Excel导入**:支出/平台结算/充值提成支持手动导入
### 助教工资计算
**绩效档位6档 + 新入职)**
| 档位 | 业绩阈值 | 专业课抽成 | 打赏课抽成 | 休假 |
|------|----------|-----------|-----------|------|
| T0 | H < 100 | 28元/时 | 50% | 3天 |
| T1 | 100 ≤ H < 130 | 18元/时 | 40% | 4天 |
| T2 | 130 ≤ H < 160 | 15元/时 | 38% | 4天 |
| T3 | 160 ≤ H < 190 | 13元/时 | 35% | 5天 |
| T4 | 190 ≤ H < 220 | 10元/时 | 33% | 6天 |
| T5 | H ≥ 220 | 8元/时 | 30% | 休假自由 |
**工资计算公式**
```
基础课收入 = 基础课小时数 × (客户支付价格 - 专业课抽成)
附加课收入 = 附加课小时数 × 190 × (1 - 打赏课抽成比例)
应发工资 = 课时收入 + 奖金
```
**计算示例中级助教185小时3档**
- 基础课170小时: 170 × (108 - 13) = 16,150元
- 附加课15小时: 15 × 190 × (1 - 0.35) = 1,852.5元
- 课时收入: 18,002.5元
**等级定价(客户支付价格)**
| 等级 | 基础课价格 | 附加课价格 |
|------|-----------|-----------|
| 初级 | 98元/时 | 190元/时 |
| 中级 | 108元/时 | 190元/时 |
| 高级 | 118元/时 | 190元/时 |
| 星级 | 138元/时 | 190元/时 |
### 运行 DWS 任务
```bash
# 初始化 DWS Schema
python -m cli.main --tasks INIT_DWS_SCHEMA
# 执行配置数据初始化
psql -f etl_billiards/database/seed_dws_config.sql
# 执行 DWS 订单汇总构建
python -m cli.main --tasks DWS_BUILD_ORDER_SUMMARY
```
### Excel 数据导入
```bash
# 导入支出数据
python etl_billiards/scripts/import_dws_excel.py --type expense --file expenses.xlsx
# 导入平台结算
python etl_billiards/scripts/import_dws_excel.py --type platform --file platform.xlsx
# 导入充值提成
python etl_billiards/scripts/import_dws_excel.py --type commission --file commission.xlsx
```
### 相关文档
- `etl_billiards/docs/dws_tables_dictionary.md`DWS 数据字典
- `etl_billiards/database/schema_dws.sql`DWS DDL
- `etl_billiards/database/seed_dws_config.sql`:配置初始数据
## 常用 CLI ## 常用 CLI
```bash ```bash
cd etl_billiards cd etl_billiards
@@ -474,11 +603,37 @@ python scripts/test_db_connection.py --dsn "postgresql://user:pwd@host:5432/db"
> 完整字段级映射见 `etl_billiards/docs/` 与 ODS/DWD DDL。 > 完整字段级映射见 `etl_billiards/docs/` 与 ODS/DWD DDL。
## 当前状态2025-12-09 ## 当前状态2026-02-02
- 示例 JSON 已全量灌入DWD 行数与 ODS 对齐。 - 示例 JSON 已全量灌入DWD 行数与 ODS 对齐。
- 分类维度已展平大类+子类:`dim_goods_category` 26 行category_level/leaf 已赋值)。 - 分类维度已展平大类+子类:`dim_goods_category` 26 行category_level/leaf 已赋值)。
- 部分空字段源数据即为空,如需补值请先确认上游。 - 部分空字段源数据即为空,如需补值请先确认上游。
### 2026-02-02 更新:字段补全
本次更新完成了 API → ODS → DWD 全链路字段补全:
**ODS 新增字段**16 张表,共 50+ 字段):
- `settlement_records`/`recharge_settlements`:电费相关字段(`electricitymoney`、`realelectricitymoney`、`electricityadjustmoney`)、券销售额、结算明细列表
- `table_fee_transactions`:活动折扣金额、订单消费类型、实际服务费
- `assistant_service_records`:助教团队名称、实际服务费
- `group_buy_redemption_records`:会员折扣、各类分摊金额(台费/商品/助教/充值)
- `table_fee_discount_records`:台区信息、台桌名称/价格、免费标记
- `member_stored_value_cards`:本金余额、会员等级、电费相关配置
- `member_profiles`:累计支付/充值金额、注册来源
- `member_balance_changes`:本金变动(前/后/数据)
- `group_buy_packages`排序、首单限制、租户券销售订单项ID
- 其他:商品编码/停售、租户ID等
**DWD 新增字段**
- 主表新增核心业务字段金额、ID、状态
- 扩展表新增配置/明细字段
**数据补全脚本**
- `scripts/backfill_202507_to_now.bat`:从 2025-07-01 重新抓取并装载数据
**文档更新**
- `etl_billiards/docs/bd_manual/` 下所有相关表文档已同步更新
## 可精简/归档 ## 可精简/归档
- `tmp/`、`tmp/etl_billiards_misc/` 中草稿、旧备份、调试脚本仅供参考,不影响运行。 - `tmp/`、`tmp/etl_billiards_misc/` 中草稿、旧备份、调试脚本仅供参考,不影响运行。
- 根级保留必要文件README、requirements、run_etl.*),其余临时文件按需归档至 `tmp/`。 - 根级保留必要文件README、requirements、run_etl.*),其余临时文件按需归档至 `tmp/`。

View File

@@ -32,7 +32,7 @@ SCHEMA_ETL=etl_admin
# API 配置 # API 配置
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
API_BASE=https://pc.ficoo.vip/apiprod/admin/v1/ API_BASE=https://pc.ficoo.vip/apiprod/admin/v1/
API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnQtdHlwZSI6IjQiLCJ1c2VyLXR5cGUiOiIxIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiMTIiLCJyb2xlLWlkIjoiMTIiLCJ0ZW5hbnQtaWQiOiIyNzkwNjgzMTYwNzA5OTU3Iiwibmlja25hbWUiOiLnp5_miLfnrqHnkIblkZjvvJrmganmgakxIiwic2l0ZS1pZCI6IjAiLCJtb2JpbGUiOiIxMzgxMDUwMjMwNCIsInNpZCI6IjI5NTA0ODk2NTgzOTU4NDUiLCJzdGFmZi1pZCI6IjMwMDk5MTg2OTE1NTkwNDUiLCJvcmctaWQiOiIwIiwicm9sZS10eXBlIjoiMyIsInJlZnJlc2hUb2tlbiI6Ik1oKzFpTitjclRHMTY3cUp5SzFXYllteVBaaUhjdDI2ZTZDZkJvd1pxSVk9IiwicmVmcmVzaEV4cGlyeVRpbWUiOiIyMDI2LzIvNyDkuIvljYg5OjU2OjE4IiwibmVlZENoZWNrVG9rZW4iOiJmYWxzZSIsImV4cCI6MTc3MDQ3MjU3OCwiaXNzIjoidGVzdCIsImF1ZCI6IlVzZXIifQ.rY03o82SKznD7NOktXKzTOI1btl2FHsklMCChOlZUeY API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnQtdHlwZSI6IjQiLCJ1c2VyLXR5cGUiOiIxIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiMTIiLCJyb2xlLWlkIjoiMTIiLCJ0ZW5hbnQtaWQiOiIyNzkwNjgzMTYwNzA5OTU3Iiwibmlja25hbWUiOiLnp5_miLfnrqHnkIblkZjvvJrmganmgakxIiwic2l0ZS1pZCI6IjAiLCJtb2JpbGUiOiIxMzgxMDUwMjMwNCIsInNpZCI6IjI5NTA0ODk2NTgzOTU4NDUiLCJzdGFmZi1pZCI6IjMwMDk5MTg2OTE1NTkwNDUiLCJvcmctaWQiOiIwIiwicm9sZS10eXBlIjoiMyIsInJlZnJlc2hUb2tlbiI6IjlES1lWcEVkYWw1bEc5cTMrdFptMkJXeTlyMkVMeEY5MHZuUWRyRnNYVFU9IiwicmVmcmVzaEV4cGlyeVRpbWUiOiIyMDI2LzIvOSDkuIrljYgyOjQzOjU0IiwibmVlZENoZWNrVG9rZW4iOiJmYWxzZSIsImV4cCI6MTc3MDU3NjIzNCwiaXNzIjoidGVzdCIsImF1ZCI6IlVzZXIifQ._1gnWcJHw8O26pcfiT1x8tgQRGn3g56vv2IZP8shgGU
# API 请求超时(秒) # API 请求超时(秒)
API_TIMEOUT=20 API_TIMEOUT=20

View File

@@ -4,7 +4,9 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import time
from typing import Any, Iterable, Tuple from typing import Any, Iterable, Tuple
from zoneinfo import ZoneInfo
from api.client import APIClient from api.client import APIClient
from api.endpoint_routing import plan_calls from api.endpoint_routing import plan_calls
@@ -128,3 +130,57 @@ class RecordingAPIClient:
"pages": len(pages), "pages": len(pages),
"records": total_records, "records": total_records,
} }
def _cfg_get(cfg, key: str, default=None):
if isinstance(cfg, dict):
cur = cfg
for part in key.split("."):
if not isinstance(cur, dict) or part not in cur:
return default
cur = cur[part]
return cur
getter = getattr(cfg, "get", None)
if callable(getter):
return getter(key, default)
return default
def build_recording_client(
cfg,
*,
task_code: str,
output_dir: Path | str | None = None,
run_id: int | None = None,
write_pretty: bool | None = None,
):
"""Build RecordingAPIClient from AppConfig or dict config."""
base_client = APIClient(
base_url=_cfg_get(cfg, "api.base_url") or "",
token=_cfg_get(cfg, "api.token"),
timeout=int(_cfg_get(cfg, "api.timeout_sec", 20) or 20),
retry_max=int(_cfg_get(cfg, "api.retries.max_attempts", 3) or 3),
headers_extra=_cfg_get(cfg, "api.headers_extra") or {},
)
if write_pretty is None:
write_pretty = bool(_cfg_get(cfg, "io.write_pretty_json", False))
if run_id is None:
run_id = int(time.time())
if output_dir is None:
tz_name = _cfg_get(cfg, "app.timezone", "Asia/Taipei") or "Asia/Taipei"
tz = ZoneInfo(tz_name)
ts = datetime.now(tz).strftime("%Y%m%d-%H%M%S")
fetch_root = _cfg_get(cfg, "pipeline.fetch_root") or _cfg_get(cfg, "io.export_root") or "export/JSON"
task_upper = str(task_code).upper()
output_dir = Path(fetch_root) / task_upper / f"{task_upper}-{run_id}-{ts}"
return RecordingAPIClient(
base_client=base_client,
output_dir=output_dir,
task_code=str(task_code),
run_id=int(run_id),
write_pretty=bool(write_pretty),
)

View File

@@ -19,6 +19,11 @@ CREATE TABLE IF NOT EXISTS billiards_ods.member_profiles (
status INT, status INT,
user_status INT, user_status INT,
create_time TIMESTAMP, create_time TIMESTAMP,
pay_money_sum NUMERIC(18,2),
person_tenant_org_id BIGINT,
person_tenant_org_name TEXT,
recharge_money_sum NUMERIC(18,2),
register_source TEXT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -75,6 +80,9 @@ CREATE TABLE IF NOT EXISTS billiards_ods.member_balance_changes (
operator_name TEXT, operator_name TEXT,
is_delete INT, is_delete INT,
create_time TIMESTAMP, create_time TIMESTAMP,
principal_after NUMERIC(18,2),
principal_before NUMERIC(18,2),
principal_data TEXT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -185,6 +193,13 @@ CREATE TABLE IF NOT EXISTS billiards_ods.member_stored_value_cards (
tenantName TEXT, tenantName TEXT,
pdAssisnatLevel TEXT, pdAssisnatLevel TEXT,
cxAssisnatLevel TEXT, cxAssisnatLevel TEXT,
able_share_member_discount BOOLEAN,
electricity_deduct_radio NUMERIC(18,4),
electricity_discount NUMERIC(18,4),
electricitycarddeduct BOOLEAN,
member_grade BIGINT,
principal_balance NUMERIC(18,2),
rechargefreezebalance NUMERIC(18,2),
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -331,6 +346,12 @@ CREATE TABLE IF NOT EXISTS billiards_ods.recharge_settlements (
isfirst INT, isfirst INT,
rechargecardamount NUMERIC(18,2), rechargecardamount NUMERIC(18,2),
giftcardamount NUMERIC(18,2), giftcardamount NUMERIC(18,2),
electricityadjustmoney NUMERIC(18,2),
electricitymoney NUMERIC(18,2),
mervousalesamount NUMERIC(18,2),
plcouponsaleamount NUMERIC(18,2),
realelectricitymoney NUMERIC(18,2),
settlelist JSONB,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -469,6 +490,12 @@ CREATE TABLE IF NOT EXISTS billiards_ods.settlement_records (
isfirst INT, isfirst INT,
rechargecardamount NUMERIC(18,2), rechargecardamount NUMERIC(18,2),
giftcardamount NUMERIC(18,2), giftcardamount NUMERIC(18,2),
electricityadjustmoney NUMERIC(18,2),
electricitymoney NUMERIC(18,2),
mervousalesamount NUMERIC(18,2),
plcouponsaleamount NUMERIC(18,2),
realelectricitymoney NUMERIC(18,2),
settlelist JSONB,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -559,6 +586,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.assistant_cancellation_records (
tableName TEXT, tableName TEXT,
trashReason TEXT, trashReason TEXT,
createTime TIMESTAMP, createTime TIMESTAMP,
tenant_id BIGINT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -792,6 +820,8 @@ CREATE TABLE IF NOT EXISTS billiards_ods.assistant_service_records (
get_grade_times INT, get_grade_times INT,
is_not_responding INT, is_not_responding INT,
is_confirm INT, is_confirm INT,
assistantteamname TEXT,
real_service_money NUMERIC(18,2),
payload JSONB NOT NULL, payload JSONB NOT NULL,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
@@ -897,6 +927,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.site_tables_master (
table_status INT, table_status INT,
temporary_light_second INT, temporary_light_second INT,
virtual_table INT, virtual_table INT,
order_id BIGINT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -957,6 +988,14 @@ CREATE TABLE IF NOT EXISTS billiards_ods.table_fee_discount_records (
order_trade_no TEXT, order_trade_no TEXT,
is_delete INT, is_delete INT,
create_time TIMESTAMP, create_time TIMESTAMP,
area_type_id BIGINT,
charge_free BOOLEAN,
site_table_area_id BIGINT,
site_table_area_name TEXT,
sitename TEXT,
table_name TEXT,
table_price NUMERIC(18,2),
tenant_name TEXT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -1032,6 +1071,9 @@ CREATE TABLE IF NOT EXISTS billiards_ods.table_fee_transactions (
salesman_org_id BIGINT, salesman_org_id BIGINT,
salesman_user_id BIGINT, salesman_user_id BIGINT,
create_time TIMESTAMP, create_time TIMESTAMP,
activity_discount_amount NUMERIC(18,2),
order_consumption_type INT,
real_service_money NUMERIC(18,2),
payload JSONB NOT NULL, payload JSONB NOT NULL,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
@@ -1234,6 +1276,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.payment_transactions (
create_time TIMESTAMP, create_time TIMESTAMP,
payment_method INT, payment_method INT,
online_pay_channel INT, online_pay_channel INT,
tenant_id BIGINT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -1440,6 +1483,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.tenant_goods_master (
remark_name TEXT, remark_name TEXT,
create_time TIMESTAMP, create_time TIMESTAMP,
update_time TIMESTAMP, update_time TIMESTAMP,
not_sale BOOLEAN,
payload JSONB NOT NULL, payload JSONB NOT NULL,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
@@ -1522,6 +1566,9 @@ CREATE TABLE IF NOT EXISTS billiards_ods.group_buy_packages (
area_tag_type INT, area_tag_type INT,
creator_name TEXT, creator_name TEXT,
create_time TIMESTAMP, create_time TIMESTAMP,
is_first_limit BOOLEAN,
sort INT,
tenantcouponsaleorderitemid BIGINT,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
source_endpoint TEXT, source_endpoint TEXT,
@@ -1616,6 +1663,15 @@ CREATE TABLE IF NOT EXISTS billiards_ods.group_buy_redemption_records (
is_single_order INT, is_single_order INT,
is_delete INT, is_delete INT,
create_time TIMESTAMP, create_time TIMESTAMP,
assistant_service_share_money NUMERIC(18,2),
assistant_share_money NUMERIC(18,2),
coupon_sale_id BIGINT,
good_service_share_money NUMERIC(18,2),
goods_share_money NUMERIC(18,2),
member_discount_money NUMERIC(18,2),
recharge_share_money NUMERIC(18,2),
table_service_share_money NUMERIC(18,2),
table_share_money NUMERIC(18,2),
payload JSONB NOT NULL, payload JSONB NOT NULL,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
@@ -1812,6 +1868,8 @@ CREATE TABLE IF NOT EXISTS billiards_ods.store_goods_master (
goods_cover TEXT, goods_cover TEXT,
create_time TIMESTAMP, create_time TIMESTAMP,
update_time TIMESTAMP, update_time TIMESTAMP,
commodity_code TEXT,
not_sale INTEGER,
payload JSONB NOT NULL, payload JSONB NOT NULL,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,
@@ -1923,6 +1981,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.store_goods_sales_records (
tenant_goods_business_id BIGINT, tenant_goods_business_id BIGINT,
tenant_goods_category_id BIGINT, tenant_goods_category_id BIGINT,
create_time TIMESTAMP, create_time TIMESTAMP,
coupon_share_money NUMERIC(18,2),
payload JSONB NOT NULL, payload JSONB NOT NULL,
content_hash TEXT NOT NULL, content_hash TEXT NOT NULL,
source_file TEXT, source_file TEXT,

View File

@@ -107,7 +107,7 @@ COMMENT ON COLUMN billiards_dwd.dim_site.scd2_is_current IS '【说明】SCD2
COMMENT ON COLUMN billiards_dwd.dim_site.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】table_fee_transactions - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_site.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】table_fee_transactions - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_site_Ex ( CREATE TABLE IF NOT EXISTS dim_site_ex (
site_id BIGINT, site_id BIGINT,
avatar TEXT, avatar TEXT,
address TEXT, address TEXT,
@@ -172,6 +172,7 @@ CREATE TABLE IF NOT EXISTS dim_table (
site_table_area_name TEXT, site_table_area_name TEXT,
tenant_table_area_id BIGINT, tenant_table_area_id BIGINT,
table_price NUMERIC(18,2), table_price NUMERIC(18,2),
order_id BIGINT,
SCD2_start_time TIMESTAMPTZ DEFAULT now(), SCD2_start_time TIMESTAMPTZ DEFAULT now(),
SCD2_end_time TIMESTAMPTZ DEFAULT '9999-12-31', SCD2_end_time TIMESTAMPTZ DEFAULT '9999-12-31',
SCD2_is_current INT DEFAULT 1, SCD2_is_current INT DEFAULT 1,
@@ -193,7 +194,7 @@ COMMENT ON COLUMN billiards_dwd.dim_table.scd2_is_current IS '【说明】SCD2
COMMENT ON COLUMN billiards_dwd.dim_table.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】site_tables_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_table.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】site_tables_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_table_Ex ( CREATE TABLE IF NOT EXISTS dim_table_ex (
table_id BIGINT, table_id BIGINT,
show_status INTEGER, show_status INTEGER,
is_online_reservation INTEGER, is_online_reservation INTEGER,
@@ -265,7 +266,7 @@ COMMENT ON COLUMN billiards_dwd.dim_assistant.scd2_is_current IS '【说明】SC
COMMENT ON COLUMN billiards_dwd.dim_assistant.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】assistant_accounts_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_assistant.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】assistant_accounts_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_assistant_Ex ( CREATE TABLE IF NOT EXISTS dim_assistant_ex (
assistant_id BIGINT, assistant_id BIGINT,
gender INTEGER, gender INTEGER,
birth_date TIMESTAMPTZ, birth_date TIMESTAMPTZ,
@@ -379,6 +380,8 @@ CREATE TABLE IF NOT EXISTS dim_member (
member_card_grade_name TEXT, member_card_grade_name TEXT,
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
update_time TIMESTAMPTZ, update_time TIMESTAMPTZ,
pay_money_sum NUMERIC(18,2),
recharge_money_sum NUMERIC(18,2),
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -403,7 +406,7 @@ COMMENT ON COLUMN billiards_dwd.dim_member.scd2_is_current IS '【说明】SCD2
COMMENT ON COLUMN billiards_dwd.dim_member.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】member_profiles - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_member.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】member_profiles - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_member_Ex ( CREATE TABLE IF NOT EXISTS dim_member_ex (
member_id BIGINT, member_id BIGINT,
referrer_member_id BIGINT, referrer_member_id BIGINT,
point NUMERIC(18,2), point NUMERIC(18,2),
@@ -411,6 +414,9 @@ CREATE TABLE IF NOT EXISTS dim_member_Ex (
growth_value NUMERIC(18,2), growth_value NUMERIC(18,2),
user_status INTEGER, user_status INTEGER,
status INTEGER, status INTEGER,
person_tenant_org_id BIGINT,
person_tenant_org_name TEXT,
register_source TEXT,
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -450,6 +456,8 @@ CREATE TABLE IF NOT EXISTS dim_member_card_account (
last_consume_time TIMESTAMPTZ, last_consume_time TIMESTAMPTZ,
status INTEGER, status INTEGER,
is_delete INTEGER, is_delete INTEGER,
principal_balance NUMERIC(18,2),
member_grade BIGINT,
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -481,7 +489,7 @@ COMMENT ON COLUMN billiards_dwd.dim_member_card_account.scd2_is_current IS '【
COMMENT ON COLUMN billiards_dwd.dim_member_card_account.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】member_stored_value_cards - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_member_card_account.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】member_stored_value_cards - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_member_card_account_Ex ( CREATE TABLE IF NOT EXISTS dim_member_card_account_ex (
member_card_id BIGINT, member_card_id BIGINT,
site_name TEXT, site_name TEXT,
tenant_name VARCHAR(64), tenant_name VARCHAR(64),
@@ -534,6 +542,11 @@ CREATE TABLE IF NOT EXISTS dim_member_card_account_Ex (
goodsCategoryId TEXT, goodsCategoryId TEXT,
pdAssisnatLevel TEXT, pdAssisnatLevel TEXT,
cxAssisnatLevel TEXT, cxAssisnatLevel TEXT,
able_share_member_discount BOOLEAN,
electricity_deduct_radio NUMERIC(18,4),
electricity_discount NUMERIC(18,4),
electricity_card_deduct BOOLEAN,
recharge_freeze_balance NUMERIC(18,2),
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -615,6 +628,7 @@ CREATE TABLE IF NOT EXISTS dim_tenant_goods (
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
update_time TIMESTAMPTZ, update_time TIMESTAMPTZ,
is_delete INTEGER, is_delete INTEGER,
not_sale INTEGER,
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -643,7 +657,7 @@ COMMENT ON COLUMN billiards_dwd.dim_tenant_goods.scd2_is_current IS '【说明
COMMENT ON COLUMN billiards_dwd.dim_tenant_goods.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】tenant_goods_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_tenant_goods.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】tenant_goods_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_tenant_goods_Ex ( CREATE TABLE IF NOT EXISTS dim_tenant_goods_ex (
tenant_goods_id BIGINT, tenant_goods_id BIGINT,
remark_name VARCHAR(128), remark_name VARCHAR(128),
pinyin_initial VARCHAR(128), pinyin_initial VARCHAR(128),
@@ -715,6 +729,8 @@ CREATE TABLE IF NOT EXISTS dim_store_goods (
enable_status INTEGER, enable_status INTEGER,
send_state INTEGER, send_state INTEGER,
is_delete INTEGER, is_delete INTEGER,
commodity_code TEXT,
not_sale INTEGER,
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -749,7 +765,7 @@ COMMENT ON COLUMN billiards_dwd.dim_store_goods.scd2_is_current IS '【说明】
COMMENT ON COLUMN billiards_dwd.dim_store_goods.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】store_goods_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_store_goods.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】store_goods_master - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_store_goods_Ex ( CREATE TABLE IF NOT EXISTS dim_store_goods_ex (
site_goods_id BIGINT, site_goods_id BIGINT,
site_name TEXT, site_name TEXT,
unit TEXT, unit TEXT,
@@ -872,6 +888,8 @@ CREATE TABLE IF NOT EXISTS dim_groupbuy_package (
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
tenant_table_area_id_list VARCHAR(512), tenant_table_area_id_list VARCHAR(512),
card_type_ids VARCHAR(255), card_type_ids VARCHAR(255),
sort INTEGER,
is_first_limit BOOLEAN,
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -902,7 +920,7 @@ COMMENT ON COLUMN billiards_dwd.dim_groupbuy_package.scd2_is_current IS '【说
COMMENT ON COLUMN billiards_dwd.dim_groupbuy_package.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】group_buy_packages - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。'; COMMENT ON COLUMN billiards_dwd.dim_groupbuy_package.scd2_version IS '【说明】SCD2 版本号(自增),用于与时间段一起避免版本重叠。 【示例】1SCD2 版本号(自增),用于与时间段一起避免版本重叠)。 【ODS来源】group_buy_packages - 无DWD慢变元数据。 【JSON字段】无 - DWD慢变元数据 - 无。';
CREATE TABLE IF NOT EXISTS dim_groupbuy_package_Ex ( CREATE TABLE IF NOT EXISTS dim_groupbuy_package_ex (
groupbuy_package_id BIGINT, groupbuy_package_id BIGINT,
site_name VARCHAR(100), site_name VARCHAR(100),
usable_count INTEGER, usable_count INTEGER,
@@ -923,6 +941,7 @@ CREATE TABLE IF NOT EXISTS dim_groupbuy_package_Ex (
effective_status INTEGER, effective_status INTEGER,
max_selectable_categories INTEGER, max_selectable_categories INTEGER,
creator_name VARCHAR(100), creator_name VARCHAR(100),
tenant_coupon_sale_order_item_id BIGINT,
SCD2_start_time TIMESTAMPTZ, SCD2_start_time TIMESTAMPTZ,
SCD2_end_time TIMESTAMPTZ, SCD2_end_time TIMESTAMPTZ,
SCD2_is_current INT, SCD2_is_current INT,
@@ -990,6 +1009,11 @@ CREATE TABLE IF NOT EXISTS dwd_settlement_head (
coupon_amount NUMERIC(18,2), coupon_amount NUMERIC(18,2),
rounding_amount NUMERIC(18,2), rounding_amount NUMERIC(18,2),
point_amount NUMERIC(18,2), point_amount NUMERIC(18,2),
electricity_money NUMERIC(18,2),
real_electricity_money NUMERIC(18,2),
electricity_adjust_money NUMERIC(18,2),
pl_coupon_sale_amount NUMERIC(18,2),
mervou_sales_amount NUMERIC(18,2),
PRIMARY KEY (order_settle_id) PRIMARY KEY (order_settle_id)
); );
@@ -1028,7 +1052,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_settlement_head.rounding_amount IS '【说
COMMENT ON COLUMN billiards_dwd.dwd_settlement_head.point_amount IS '【说明】金额字段,用于计费/结算/核算等金额计算。 【示例】NULL金额字段用于计费/结算/核算等金额计算)。 【ODS来源】settlement_records - pointamount。 【JSON字段】settlement_records.json - $ - pointamount。'; COMMENT ON COLUMN billiards_dwd.dwd_settlement_head.point_amount IS '【说明】金额字段,用于计费/结算/核算等金额计算。 【示例】NULL金额字段用于计费/结算/核算等金额计算)。 【ODS来源】settlement_records - pointamount。 【JSON字段】settlement_records.json - $ - pointamount。';
CREATE TABLE IF NOT EXISTS dwd_settlement_head_Ex ( CREATE TABLE IF NOT EXISTS dwd_settlement_head_ex (
order_settle_id BIGINT, order_settle_id BIGINT,
serial_number INTEGER, serial_number INTEGER,
settle_status INTEGER, settle_status INTEGER,
@@ -1059,6 +1083,7 @@ CREATE TABLE IF NOT EXISTS dwd_settlement_head_Ex (
order_remark VARCHAR(255), order_remark VARCHAR(255),
operator_id BIGINT, operator_id BIGINT,
salesman_user_id BIGINT, salesman_user_id BIGINT,
settle_list JSONB,
PRIMARY KEY (order_settle_id) PRIMARY KEY (order_settle_id)
); );
@@ -1123,6 +1148,8 @@ CREATE TABLE IF NOT EXISTS dwd_table_fee_log (
ledger_status INTEGER, ledger_status INTEGER,
is_single_order INTEGER, is_single_order INTEGER,
is_delete INTEGER, is_delete INTEGER,
activity_discount_amount NUMERIC(18,2),
real_service_money NUMERIC(18,2),
PRIMARY KEY (table_fee_log_id) PRIMARY KEY (table_fee_log_id)
); );
@@ -1156,7 +1183,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_table_fee_log.is_single_order IS '【说明
COMMENT ON COLUMN billiards_dwd.dwd_table_fee_log.is_delete IS '【说明】布尔/开关字段,用于表示是否/可用性等业务开关。 【示例】0布尔/开关字段,用于表示是否/可用性等业务开关)。 【ODS来源】table_fee_transactions - is_delete。 【JSON字段】table_fee_transactions.json - data.siteTableUseDetailsList - is_delete。'; COMMENT ON COLUMN billiards_dwd.dwd_table_fee_log.is_delete IS '【说明】布尔/开关字段,用于表示是否/可用性等业务开关。 【示例】0布尔/开关字段,用于表示是否/可用性等业务开关)。 【ODS来源】table_fee_transactions - is_delete。 【JSON字段】table_fee_transactions.json - data.siteTableUseDetailsList - is_delete。';
CREATE TABLE IF NOT EXISTS dwd_table_fee_log_Ex ( CREATE TABLE IF NOT EXISTS dwd_table_fee_log_ex (
table_fee_log_id BIGINT, table_fee_log_id BIGINT,
operator_name VARCHAR(64), operator_name VARCHAR(64),
salesman_name VARCHAR(64), salesman_name VARCHAR(64),
@@ -1169,6 +1196,7 @@ CREATE TABLE IF NOT EXISTS dwd_table_fee_log_Ex (
operator_id BIGINT, operator_id BIGINT,
salesman_user_id BIGINT, salesman_user_id BIGINT,
salesman_org_id BIGINT, salesman_org_id BIGINT,
order_consumption_type INTEGER,
PRIMARY KEY (table_fee_log_id) PRIMARY KEY (table_fee_log_id)
); );
@@ -1201,6 +1229,9 @@ CREATE TABLE IF NOT EXISTS dwd_table_fee_adjust (
ledger_status INTEGER, ledger_status INTEGER,
is_delete INTEGER, is_delete INTEGER,
adjust_time TIMESTAMPTZ, adjust_time TIMESTAMPTZ,
table_name TEXT,
table_price NUMERIC(18,2),
charge_free BOOLEAN,
PRIMARY KEY (table_fee_adjust_id) PRIMARY KEY (table_fee_adjust_id)
); );
@@ -1220,7 +1251,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_table_fee_adjust.is_delete IS '【说明】
COMMENT ON COLUMN billiards_dwd.dwd_table_fee_adjust.adjust_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:25:11时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】table_fee_discount_records - create_time。 【JSON字段】table_fee_discount_records.json - data.taiFeeAdjustInfos - create_time。'; COMMENT ON COLUMN billiards_dwd.dwd_table_fee_adjust.adjust_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:25:11时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】table_fee_discount_records - create_time。 【JSON字段】table_fee_discount_records.json - data.taiFeeAdjustInfos - create_time。';
CREATE TABLE IF NOT EXISTS dwd_table_fee_adjust_Ex ( CREATE TABLE IF NOT EXISTS dwd_table_fee_adjust_ex (
table_fee_adjust_id BIGINT, table_fee_adjust_id BIGINT,
adjust_type INTEGER, adjust_type INTEGER,
ledger_count INTEGER, ledger_count INTEGER,
@@ -1229,6 +1260,11 @@ CREATE TABLE IF NOT EXISTS dwd_table_fee_adjust_Ex (
operator_name VARCHAR(64), operator_name VARCHAR(64),
applicant_id BIGINT, applicant_id BIGINT,
operator_id BIGINT, operator_id BIGINT,
area_type_id BIGINT,
site_table_area_id BIGINT,
site_table_area_name TEXT,
site_name TEXT,
tenant_name TEXT,
PRIMARY KEY (table_fee_adjust_id) PRIMARY KEY (table_fee_adjust_id)
); );
@@ -1267,6 +1303,7 @@ CREATE TABLE IF NOT EXISTS dwd_store_goods_sale (
ledger_status INTEGER, ledger_status INTEGER,
is_delete INTEGER, is_delete INTEGER,
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
coupon_share_money NUMERIC(18,2),
PRIMARY KEY (store_goods_sale_id) PRIMARY KEY (store_goods_sale_id)
); );
@@ -1296,7 +1333,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_store_goods_sale.is_delete IS '【说明】
COMMENT ON COLUMN billiards_dwd.dwd_store_goods_sale.create_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:35:57时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】store_goods_sales_records - create_time。 【JSON字段】store_goods_sales_records.json - data.orderGoodsLedgers - create_time。'; COMMENT ON COLUMN billiards_dwd.dwd_store_goods_sale.create_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:35:57时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】store_goods_sales_records - create_time。 【JSON字段】store_goods_sales_records.json - data.orderGoodsLedgers - create_time。';
CREATE TABLE IF NOT EXISTS dwd_store_goods_sale_Ex ( CREATE TABLE IF NOT EXISTS dwd_store_goods_sale_ex (
store_goods_sale_id BIGINT, store_goods_sale_id BIGINT,
legacy_order_goods_id BIGINT, legacy_order_goods_id BIGINT,
site_name TEXT, site_name TEXT,
@@ -1392,6 +1429,7 @@ CREATE TABLE IF NOT EXISTS dwd_assistant_service_log (
start_use_time TIMESTAMPTZ, start_use_time TIMESTAMPTZ,
last_use_time TIMESTAMPTZ, last_use_time TIMESTAMPTZ,
is_delete INTEGER, is_delete INTEGER,
real_service_money NUMERIC(18,2),
PRIMARY KEY (assistant_service_id) PRIMARY KEY (assistant_service_id)
); );
@@ -1430,7 +1468,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_assistant_service_log.last_use_time IS '【
COMMENT ON COLUMN billiards_dwd.dwd_assistant_service_log.is_delete IS '【说明】布尔/开关字段,用于表示是否/可用性等业务开关。 【示例】0布尔/开关字段,用于表示是否/可用性等业务开关)。 【ODS来源】assistant_service_records - is_delete。 【JSON字段】assistant_service_records.json - data.orderAssistantDetails - is_delete。'; COMMENT ON COLUMN billiards_dwd.dwd_assistant_service_log.is_delete IS '【说明】布尔/开关字段,用于表示是否/可用性等业务开关。 【示例】0布尔/开关字段,用于表示是否/可用性等业务开关)。 【ODS来源】assistant_service_records - is_delete。 【JSON字段】assistant_service_records.json - data.orderAssistantDetails - is_delete。';
CREATE TABLE IF NOT EXISTS dwd_assistant_service_log_Ex ( CREATE TABLE IF NOT EXISTS dwd_assistant_service_log_ex (
assistant_service_id BIGINT, assistant_service_id BIGINT,
table_name VARCHAR(64), table_name VARCHAR(64),
assistant_name VARCHAR(64), assistant_name VARCHAR(64),
@@ -1461,6 +1499,7 @@ CREATE TABLE IF NOT EXISTS dwd_assistant_service_log_Ex (
get_grade_times INTEGER, get_grade_times INTEGER,
grade_status INTEGER, grade_status INTEGER,
composite_grade_time TIMESTAMPTZ, composite_grade_time TIMESTAMPTZ,
assistant_team_name TEXT,
PRIMARY KEY (assistant_service_id) PRIMARY KEY (assistant_service_id)
); );
@@ -1508,6 +1547,7 @@ CREATE TABLE IF NOT EXISTS dwd_assistant_trash_event (
abolish_amount NUMERIC(18,2), abolish_amount NUMERIC(18,2),
trash_reason VARCHAR(255), trash_reason VARCHAR(255),
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
tenant_id BIGINT,
PRIMARY KEY (assistant_trash_event_id) PRIMARY KEY (assistant_trash_event_id)
); );
@@ -1524,7 +1564,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_assistant_trash_event.trash_reason IS '【
COMMENT ON COLUMN billiards_dwd.dwd_assistant_trash_event.create_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 19:23:29时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】assistant_cancellation_records - createTime。 【JSON字段】assistant_cancellation_records.json - data.abolitionAssistants - createTime。'; COMMENT ON COLUMN billiards_dwd.dwd_assistant_trash_event.create_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 19:23:29时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】assistant_cancellation_records - createTime。 【JSON字段】assistant_cancellation_records.json - data.abolitionAssistants - createTime。';
CREATE TABLE IF NOT EXISTS dwd_assistant_trash_event_Ex ( CREATE TABLE IF NOT EXISTS dwd_assistant_trash_event_ex (
assistant_trash_event_id BIGINT, assistant_trash_event_id BIGINT,
table_name VARCHAR(64), table_name VARCHAR(64),
table_area_name VARCHAR(64), table_area_name VARCHAR(64),
@@ -1557,6 +1597,8 @@ CREATE TABLE IF NOT EXISTS dwd_member_balance_change (
change_time TIMESTAMPTZ, change_time TIMESTAMPTZ,
is_delete INTEGER, is_delete INTEGER,
remark VARCHAR(255), remark VARCHAR(255),
principal_before NUMERIC(18,2),
principal_after NUMERIC(18,2),
PRIMARY KEY (balance_change_id) PRIMARY KEY (balance_change_id)
); );
@@ -1582,13 +1624,14 @@ COMMENT ON COLUMN billiards_dwd.dwd_member_balance_change.is_delete IS '【说
COMMENT ON COLUMN billiards_dwd.dwd_member_balance_change.remark IS '【说明】明细字段,用于记录事实取值。 【示例】充值退款(明细字段,用于记录事实取值)。 【ODS来源】member_balance_changes - remark。 【JSON字段】member_balance_changes.json - data.tenantMemberCardLogs - remark。'; COMMENT ON COLUMN billiards_dwd.dwd_member_balance_change.remark IS '【说明】明细字段,用于记录事实取值。 【示例】充值退款(明细字段,用于记录事实取值)。 【ODS来源】member_balance_changes - remark。 【JSON字段】member_balance_changes.json - data.tenantMemberCardLogs - remark。';
CREATE TABLE IF NOT EXISTS dwd_member_balance_change_EX ( CREATE TABLE IF NOT EXISTS dwd_member_balance_change_ex (
balance_change_id BIGINT, balance_change_id BIGINT,
pay_site_name VARCHAR(64), pay_site_name VARCHAR(64),
register_site_name VARCHAR(64), register_site_name VARCHAR(64),
refund_amount NUMERIC(18,2), refund_amount NUMERIC(18,2),
operator_id BIGINT, operator_id BIGINT,
operator_name VARCHAR(64), operator_name VARCHAR(64),
principal_data TEXT,
PRIMARY KEY (balance_change_id) PRIMARY KEY (balance_change_id)
); );
@@ -1625,6 +1668,8 @@ CREATE TABLE IF NOT EXISTS dwd_groupbuy_redemption (
is_delete INTEGER, is_delete INTEGER,
ledger_name VARCHAR(128), ledger_name VARCHAR(128),
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
member_discount_money NUMERIC(18,2),
coupon_sale_id BIGINT,
PRIMARY KEY (redemption_id) PRIMARY KEY (redemption_id)
); );
@@ -1654,7 +1699,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_groupbuy_redemption.ledger_name IS '【说
COMMENT ON COLUMN billiards_dwd.dwd_groupbuy_redemption.create_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:35:57时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】group_buy_redemption_records - create_time。 【JSON字段】group_buy_redemption_records.json - data.siteTableUseDetailsList - create_time。'; COMMENT ON COLUMN billiards_dwd.dwd_groupbuy_redemption.create_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:35:57时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】group_buy_redemption_records - create_time。 【JSON字段】group_buy_redemption_records.json - data.siteTableUseDetailsList - create_time。';
CREATE TABLE IF NOT EXISTS dwd_groupbuy_redemption_Ex ( CREATE TABLE IF NOT EXISTS dwd_groupbuy_redemption_ex (
redemption_id BIGINT, redemption_id BIGINT,
site_name VARCHAR(64), site_name VARCHAR(64),
table_name VARCHAR(64), table_name VARCHAR(64),
@@ -1676,6 +1721,13 @@ CREATE TABLE IF NOT EXISTS dwd_groupbuy_redemption_Ex (
salesman_role_id BIGINT, salesman_role_id BIGINT,
salesman_org_id BIGINT, salesman_org_id BIGINT,
ledger_group_name VARCHAR(128), ledger_group_name VARCHAR(128),
table_share_money NUMERIC(18,2),
table_service_share_money NUMERIC(18,2),
goods_share_money NUMERIC(18,2),
good_service_share_money NUMERIC(18,2),
assistant_share_money NUMERIC(18,2),
assistant_service_share_money NUMERIC(18,2),
recharge_share_money NUMERIC(18,2),
PRIMARY KEY (redemption_id) PRIMARY KEY (redemption_id)
); );
@@ -1750,7 +1802,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_platform_coupon_redemption.create_time IS '
COMMENT ON COLUMN billiards_dwd.dwd_platform_coupon_redemption.consume_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:41:04时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】platform_coupon_redemption_records - consume_time。 【JSON字段】platform_coupon_redemption_records.json - $ - consume_time。'; COMMENT ON COLUMN billiards_dwd.dwd_platform_coupon_redemption.consume_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】2025-11-09 23:41:04时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】platform_coupon_redemption_records - consume_time。 【JSON字段】platform_coupon_redemption_records.json - $ - consume_time。';
CREATE TABLE IF NOT EXISTS dwd_platform_coupon_redemption_Ex ( CREATE TABLE IF NOT EXISTS dwd_platform_coupon_redemption_ex (
platform_coupon_redemption_id BIGINT, platform_coupon_redemption_id BIGINT,
coupon_cover VARCHAR(255), coupon_cover VARCHAR(255),
coupon_remark VARCHAR(255), coupon_remark VARCHAR(255),
@@ -1814,7 +1866,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_recharge_order.create_time IS '【说明】
COMMENT ON COLUMN billiards_dwd.dwd_recharge_order.pay_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】NULL时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】recharge_settlements - paytime。 【JSON字段】recharge_settlements.json - $ - paytime。'; COMMENT ON COLUMN billiards_dwd.dwd_recharge_order.pay_time IS '【说明】时间/日期字段,用于记录业务时间与统计口径对齐。 【示例】NULL时间/日期字段,用于记录业务时间与统计口径对齐)。 【ODS来源】recharge_settlements - paytime。 【JSON字段】recharge_settlements.json - $ - paytime。';
CREATE TABLE IF NOT EXISTS dwd_recharge_order_Ex ( CREATE TABLE IF NOT EXISTS dwd_recharge_order_ex (
recharge_order_id BIGINT, recharge_order_id BIGINT,
site_name_snapshot TEXT, site_name_snapshot TEXT,
settle_status INTEGER, settle_status INTEGER,
@@ -1919,6 +1971,7 @@ CREATE TABLE IF NOT EXISTS dwd_payment (
create_time TIMESTAMPTZ, create_time TIMESTAMPTZ,
pay_time TIMESTAMPTZ, pay_time TIMESTAMPTZ,
pay_date DATE, pay_date DATE,
tenant_id BIGINT,
PRIMARY KEY (payment_id) PRIMARY KEY (payment_id)
); );
@@ -1967,7 +2020,7 @@ COMMENT ON COLUMN billiards_dwd.dwd_refund.member_id IS '【说明】标识类 I
COMMENT ON COLUMN billiards_dwd.dwd_refund.member_card_id IS '【说明】标识类 ID 字段,用于关联/定位相关实体。 【示例】0标识类 ID 字段,用于关联/定位相关实体)。 【ODS来源】refund_transactions - member_card_id。 【JSON字段】refund_transactions.json - $ - member_card_id。'; COMMENT ON COLUMN billiards_dwd.dwd_refund.member_card_id IS '【说明】标识类 ID 字段,用于关联/定位相关实体。 【示例】0标识类 ID 字段,用于关联/定位相关实体)。 【ODS来源】refund_transactions - member_card_id。 【JSON字段】refund_transactions.json - $ - member_card_id。';
CREATE TABLE IF NOT EXISTS dwd_refund_Ex ( CREATE TABLE IF NOT EXISTS dwd_refund_ex (
refund_id BIGINT, refund_id BIGINT,
tenant_name VARCHAR(64), tenant_name VARCHAR(64),
pay_sn BIGINT, pay_sn BIGINT,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
-- =============================================================================
-- DWS 配置表初始数据
-- 版本: v3.0
-- 创建日期: 2026-02-01
-- 描述: 初始化配置表数据,包含绩效档位、等级定价、奖金规则、区域分类、技能映射
-- =============================================================================
-- =============================================================================
-- 1. cfg_performance_tier - 绩效档位配置6档 + 新入职)
-- 数据来源DWS 数据库处理需求.md 第35-41行
-- 档位原因考虑 总业绩小时数阈值 专业课抽成(元/小时) 打赏课抽成 次月休假(天)
-- 0档 淘汰压力 H <100 28 50% 3
-- 1档 及格档(重点激励) 100≤ H <130 18 40% 4
-- 2档 良好档(重点激励) 130≤ H <160 15 38% 4
-- 3档 优秀档 160≤ H <190 13 35% 5
-- 4档 卓越加速档(高端人才倾斜) 190≤ H <220 10 33% 6
-- 5档 冠军加速档(高端人才倾斜) H ≥220 8 30% 休假自由
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_performance_tier RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_performance_tier (
tier_code, tier_name, tier_level,
min_hours, max_hours,
base_deduction, bonus_deduction_ratio, vacation_days, vacation_unlimited,
is_new_hire_tier, effective_from, effective_to, description
) VALUES
-- 0档 淘汰压力: H<100, 专业课抽成28元/小时, 打赏课抽成50%, 休假3天
('T0', '0档-淘汰压力', 0,
0, 100,
28.00, 0.50, 3, FALSE,
FALSE, '2000-01-01', '9999-12-31',
'淘汰压力档H<100专业课抽成28元/小时打赏课抽成50%休假3天'),
-- 1档 及格档: 100≤H<130, 专业课抽成18元/小时, 打赏课抽成40%, 休假4天
('T1', '1档-及格档', 1,
100, 130,
18.00, 0.40, 4, FALSE,
FALSE, '2000-01-01', '9999-12-31',
'及格档重点激励100≤H<130专业课抽成18元/小时打赏课抽成40%休假4天'),
-- 2档 良好档: 130≤H<160, 专业课抽成15元/小时, 打赏课抽成38%, 休假4天
('T2', '2档-良好档', 2,
130, 160,
15.00, 0.38, 4, FALSE,
FALSE, '2000-01-01', '9999-12-31',
'良好档重点激励130≤H<160专业课抽成15元/小时打赏课抽成38%休假4天'),
-- 3档 优秀档: 160≤H<190, 专业课抽成13元/小时, 打赏课抽成35%, 休假5天
('T3', '3档-优秀档', 3,
160, 190,
13.00, 0.35, 5, FALSE,
FALSE, '2000-01-01', '9999-12-31',
'优秀档160≤H<190专业课抽成13元/小时打赏课抽成35%休假5天'),
-- 4档 卓越加速档: 190≤H<220, 专业课抽成10元/小时, 打赏课抽成33%, 休假6天
('T4', '4档-卓越加速档', 4,
190, 220,
10.00, 0.33, 6, FALSE,
FALSE, '2000-01-01', '9999-12-31',
'卓越加速档高端人才倾斜190≤H<220专业课抽成10元/小时打赏课抽成33%休假6天'),
-- 5档 冠军加速档: H≥220, 专业课抽成8元/小时, 打赏课抽成30%, 休假自由
('T5', '5档-冠军加速档', 5,
220, NULL,
8.00, 0.30, 0, TRUE,
FALSE, '2000-01-01', '9999-12-31',
'冠军加速档高端人才倾斜H≥220专业课抽成8元/小时打赏课抽成30%,休假自由'),
-- 新入职档位: 首月特殊处理按1档标准但不参与排名
('NEW', '新入职档位', -1,
0, NULL,
18.00, 0.40, 4, FALSE,
TRUE, '2000-01-01', '9999-12-31',
'新入职月1日0点后入职者首月使用按1档抽成标准不参与排名奖金');
-- =============================================================================
-- 2. cfg_assistant_level_price - 助教等级定价
-- 说明:
-- - level_code 来自 dim_assistant.assistant_level
-- - 8=助教管理, 10=初级, 20=中级, 30=高级, 40=星级
-- - 价格为客户支付价格(对外价格),助教收入=客户支付-档位抽成
-- - 数据来源DWS 数据库处理需求.md
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_assistant_level_price RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_assistant_level_price (
level_code, level_name,
base_course_price, bonus_course_price,
effective_from, effective_to, description
) VALUES
-- 初级助教基础课对客户收费98元/小时
(10, '初级',
98.00, 190.00,
'2000-01-01', '9999-12-31',
'初级助教基础课98元/时附加课190元/时(客户支付价格)'),
-- 中级助教基础课对客户收费108元/小时
(20, '中级',
108.00, 190.00,
'2000-01-01', '9999-12-31',
'中级助教基础课108元/时附加课190元/时(客户支付价格)'),
-- 高级助教基础课对客户收费118元/小时
(30, '高级',
118.00, 190.00,
'2000-01-01', '9999-12-31',
'高级助教基础课118元/时附加课190元/时(客户支付价格)'),
-- 星级助教基础课对客户收费138元/小时
(40, '星级',
138.00, 190.00,
'2000-01-01', '9999-12-31',
'星级助教基础课138元/时附加课190元/时(客户支付价格)'),
-- 助教管理level_code=8通常不参与客户服务计费此处设置默认值
(8, '助教管理',
98.00, 190.00,
'2000-01-01', '9999-12-31',
'助教管理:不参与客户服务计费,默认按初级价格');
-- =============================================================================
-- 3. cfg_bonus_rules - 奖金规则配置
-- 说明:
-- - SPRINT: 冲刺奖金,按业绩小时数阈值,不累计取最高档
-- - TOP_RANK: Top3排名奖金按有效业绩排名并列都算
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_bonus_rules RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_bonus_rules (
rule_type, rule_code, rule_name,
threshold_hours, rank_position, bonus_amount,
is_cumulative, priority,
effective_from, effective_to, description
) VALUES
-- 冲刺奖金: H>=190 得300元
('SPRINT', 'SPRINT_190', '冲刺奖金190',
190.00, NULL, 300.00,
FALSE, 1,
'2000-01-01', '9999-12-31',
'业绩≥190小时获得300元冲刺奖金不累计'),
-- 冲刺奖金: H>=220 得800元优先级更高覆盖190档
('SPRINT', 'SPRINT_220', '冲刺奖金220',
220.00, NULL, 800.00,
FALSE, 2,
'2000-01-01', '9999-12-31',
'业绩≥220小时获得800元冲刺奖金覆盖190档'),
-- Top1排名奖金: 1000元
('TOP_RANK', 'TOP_1', 'Top1排名奖金',
NULL, 1, 1000.00,
FALSE, 0,
'2000-01-01', '9999-12-31',
'月度排名第一获得1000元并列都算'),
-- Top2排名奖金: 600元
('TOP_RANK', 'TOP_2', 'Top2排名奖金',
NULL, 2, 600.00,
FALSE, 0,
'2000-01-01', '9999-12-31',
'月度排名第二获得600元并列都算'),
-- Top3排名奖金: 400元
('TOP_RANK', 'TOP_3', 'Top3排名奖金',
NULL, 3, 400.00,
FALSE, 0,
'2000-01-01', '9999-12-31',
'月度排名第三获得400元并列都算');
-- =============================================================================
-- 4. cfg_area_category - 台区分类映射
-- 说明:
-- - 将 dim_table.site_table_area_name 映射到财务报表区域分类
-- - 映射规则: 精确匹配 > 模糊匹配 > 默认兜底
-- - 数据来源: BD_manual_dim_table.md 中的 site_table_area_name 实际分布
-- 分类设计:
-- - BILLIARD: 台球散台A区/B区/C区/TV台
-- - BILLIARD_VIP: 台球VIP包厢
-- - SNOOKER: 斯诺克区
-- - MAHJONG: 麻将区
-- - KTV: K歌/KTV
-- - SPECIAL: 特殊(补时长等)
-- - OTHER: 其他
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_area_category RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_area_category (
source_area_name, category_code, category_name,
match_type, match_priority, is_active, description
) VALUES
-- ============ 台球散台区(精确匹配)============
('A区', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台A区18台- 中八/追分'),
('B区', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台B区15台- 中八/追分'),
('C区', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台C区6台- 中八/追分'),
('TV台', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台TV台1台- 中八/追分'),
-- ============ 台球VIP包厢精确匹配============
('VIP包厢', 'BILLIARD_VIP', '台球VIP',
'EXACT', 10, TRUE, '台球VIPVIP包厢4台- V1-V4中八, V5斯诺克'),
-- ============ 斯诺克区(精确匹配)============
('斯诺克区', 'SNOOKER', '斯诺克',
'EXACT', 10, TRUE, '斯诺克斯诺克区4台'),
-- ============ 麻将区(精确匹配)============
('麻将房', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌麻将房5台'),
('M7', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌M72台'),
('M8', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌M81台'),
('666', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌6662台'),
('发财', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌发财1台'),
-- ============ KTV/K包精确匹配============
('K包', 'KTV', 'K歌娱乐',
'EXACT', 10, TRUE, 'K歌娱乐K包4台'),
('k包活动区', 'KTV', 'K歌娱乐',
'EXACT', 10, TRUE, 'K歌娱乐k包活动区2台'),
('幸会158', 'KTV', 'K歌娱乐',
'EXACT', 10, TRUE, 'K歌娱乐幸会1582台'),
-- ============ 特殊区域(精确匹配)============
('补时长', 'SPECIAL', '补时长',
'EXACT', 10, TRUE, '特殊补时长7台- 用于时长补录'),
-- ============ 模糊匹配规则(优先级较低)============
('%VIP%', 'BILLIARD_VIP', '台球VIP',
'LIKE', 50, TRUE, '模糊匹配:包含"VIP"的区域'),
('%斯诺克%', 'SNOOKER', '斯诺克',
'LIKE', 50, TRUE, '模糊匹配:包含"斯诺克"的区域'),
('%麻将%', 'MAHJONG', '麻将棋牌',
'LIKE', 50, TRUE, '模糊匹配:包含"麻将"的区域'),
('%K包%', 'KTV', 'K歌娱乐',
'LIKE', 50, TRUE, '模糊匹配:包含"K包"的区域'),
('%KTV%', 'KTV', 'K歌娱乐',
'LIKE', 50, TRUE, '模糊匹配:包含"KTV"的区域'),
-- ============ 默认兜底(优先级最低)============
('DEFAULT', 'OTHER', '其他',
'DEFAULT', 999, TRUE, '兜底规则:无法匹配的区域归入其他');
-- =============================================================================
-- 5. cfg_skill_type - 技能→课程类型映射
-- 说明:
-- - 将 skill_id 映射到课程类型
-- - 基础课/陪打: skill_id = 2791903611396869
-- - 附加课/超休: skill_id = 2807440316432197
-- - 避免依赖 skill_name 文本匹配
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_skill_type RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_skill_type (
skill_id, skill_name,
course_type_code, course_type_name,
is_active, description
) VALUES
-- 基础课/陪打
(2791903611396869, '台球基础陪打',
'BASE', '基础课',
TRUE, '基础课:陪打服务,按助教等级计价'),
-- 附加课/超休
(2807440316432197, '台球超休服务',
'BONUS', '附加课',
TRUE, '附加课:超休/激励课固定50元/小时'),
-- 包厢课(如有)
(2807440316432198, '包厢服务',
'BASE', '基础课',
TRUE, '包厢服务:归入基础课统计');
-- =============================================================================
-- 6. 优惠类型配置(用于财务优惠明细分析)
-- 说明: 定义各类优惠的代码和名称,便于后续分析
-- =============================================================================
-- 此配置作为代码常量使用,不单独建表
-- GROUPBUY - 团购优惠
-- VIP - 会员折扣
-- GIFT_CARD - 赠送卡抵扣
-- MANUAL - 手动调整
-- ROUNDING - 抹零
-- BIG_CUSTOMER - 大客户优惠(待抽样分析确认)
-- OTHER - 其他优惠
-- =============================================================================
-- 7. 支出类型配置用于Excel导入
-- 说明: 定义各类支出的代码和名称
-- =============================================================================
-- 此配置作为代码常量使用,不单独建表
-- RENT - 房租
-- UTILITY - 水电费
-- PROPERTY - 物业费
-- SALARY - 工资
-- REIMBURSE - 报销
-- PLATFORM_FEE - 平台服务费
-- OTHER - 其他支出
-- =============================================================================
-- 8. 平台类型配置用于Excel导入
-- 说明: 定义各平台的代码和名称
-- =============================================================================
-- 此配置作为代码常量使用,不单独建表
-- MEITUAN - 美团
-- DOUYIN - 抖音
-- DIANPING - 大众点评
-- OTHER - 其他平台
-- =============================================================================
-- 验证数据插入
-- =============================================================================
DO $$
DECLARE
v_tier_count INTEGER;
v_price_count INTEGER;
v_bonus_count INTEGER;
v_area_count INTEGER;
v_skill_count INTEGER;
BEGIN
SELECT COUNT(*) INTO v_tier_count FROM billiards_dws.cfg_performance_tier;
SELECT COUNT(*) INTO v_price_count FROM billiards_dws.cfg_assistant_level_price;
SELECT COUNT(*) INTO v_bonus_count FROM billiards_dws.cfg_bonus_rules;
SELECT COUNT(*) INTO v_area_count FROM billiards_dws.cfg_area_category;
SELECT COUNT(*) INTO v_skill_count FROM billiards_dws.cfg_skill_type;
RAISE NOTICE '配置数据初始化完成:';
RAISE NOTICE ' - cfg_performance_tier: % 条', v_tier_count;
RAISE NOTICE ' - cfg_assistant_level_price: % 条', v_price_count;
RAISE NOTICE ' - cfg_bonus_rules: % 条', v_bonus_count;
RAISE NOTICE ' - cfg_area_category: % 条', v_area_count;
RAISE NOTICE ' - cfg_skill_type: % 条', v_skill_count;
END;
$$;

View File

@@ -0,0 +1,118 @@
-- =============================================================================
-- 指数算法参数初始化脚本
-- 版本: v1.0
-- 创建日期: 2026-02-03
-- 描述: 为客户召回指数和客户-助教亲密指数插入默认参数
-- =============================================================================
-- 清空旧数据(如果需要重新初始化)
-- DELETE FROM billiards_dws.cfg_index_parameters WHERE index_type IN ('RECALL', 'INTIMACY');
-- =============================================================================
-- 客户召回指数RECALL参数
-- =============================================================================
INSERT INTO billiards_dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
-- 基础参数
('RECALL', 'lookback_days', 60, '回溯窗口分析近60天的数据', CURRENT_DATE),
('RECALL', 'sigma_min', 2.0, '波动下限(天):避免σ过小导致超期过敏', CURRENT_DATE),
-- 半衰期参数
('RECALL', 'halflife_new', 7, '新客户半衰期7天后新客户加分衰减到一半', CURRENT_DATE),
('RECALL', 'halflife_recharge', 10, '刚充值半衰期10天后充值加分衰减到一半', CURRENT_DATE),
-- 权重参数
('RECALL', 'weight_overdue', 3.0, '超期紧急性权重主导因子建议3.0', CURRENT_DATE),
('RECALL', 'weight_new', 1.0, '新客户权重建议1.0', CURRENT_DATE),
('RECALL', 'weight_recharge', 1.0, '刚充值权重建议1.0', CURRENT_DATE),
('RECALL', 'weight_hot', 1.0, '热度断档权重建议1.0', CURRENT_DATE),
-- 映射参数
('RECALL', 'percentile_lower', 5, '下锚分位数5分位', CURRENT_DATE),
('RECALL', 'percentile_upper', 95, '上锚分位数95分位', CURRENT_DATE),
('RECALL', 'ewma_alpha', 0.2, 'EWMA平滑系数越小越平滑建议0.2', CURRENT_DATE)
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- 客户-助教亲密指数INTIMACY参数
-- =============================================================================
INSERT INTO billiards_dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
-- 基础参数
('INTIMACY', 'lookback_days', 60, '回溯窗口分析近60天的数据', CURRENT_DATE),
('INTIMACY', 'session_merge_hours', 4, '会话合并间隔小时4小时内的服务算同次', CURRENT_DATE),
('INTIMACY', 'recharge_attribute_hours', 1, '充值归因窗口小时服务结束后1小时内', CURRENT_DATE),
('INTIMACY', 'amount_base', 500, '金额压缩基准(元):选门店常见充值档位', CURRENT_DATE),
('INTIMACY', 'incentive_weight', 1.5, '附加课权重倍数:附加课=基础课的1.5倍', CURRENT_DATE),
-- 半衰期参数
('INTIMACY', 'halflife_session', 14, '会话衰减半衰期14天后权重衰减到一半', CURRENT_DATE),
('INTIMACY', 'halflife_last', 10, '最近一次半衰期10天后温度衰减到一半', CURRENT_DATE),
('INTIMACY', 'halflife_recharge', 21, '充值衰减半衰期21天后充值贡献衰减到一半', CURRENT_DATE),
('INTIMACY', 'halflife_short', 7, '短期激增检测半衰期用于Burst检测', CURRENT_DATE),
('INTIMACY', 'halflife_long', 30, '长期激增检测半衰期用于Burst检测', CURRENT_DATE),
-- 权重参数
('INTIMACY', 'weight_frequency', 2.0, '频次权重建议2.0', CURRENT_DATE),
('INTIMACY', 'weight_recency', 1.5, '最近一次权重建议1.5', CURRENT_DATE),
('INTIMACY', 'weight_recharge', 2.0, '归因充值权重建议2.0', CURRENT_DATE),
('INTIMACY', 'weight_duration', 0.5, '时长权重次要因素建议0.5', CURRENT_DATE),
('INTIMACY', 'burst_gamma', 0.6, '激增放大系数γ建议0.6', CURRENT_DATE),
-- 映射参数
('INTIMACY', 'percentile_lower', 5, '下锚分位数5分位', CURRENT_DATE),
('INTIMACY', 'percentile_upper', 95, '上锚分位数95分位', CURRENT_DATE),
('INTIMACY', 'ewma_alpha', 0.2, 'EWMA平滑系数越小越平滑建议0.2', CURRENT_DATE)
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- 验证
-- =============================================================================
-- 检查参数数量
DO $$
DECLARE
recall_count INTEGER;
intimacy_count INTEGER;
BEGIN
SELECT COUNT(*) INTO recall_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'RECALL';
SELECT COUNT(*) INTO intimacy_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'INTIMACY';
RAISE NOTICE '召回指数参数数量: %', recall_count;
RAISE NOTICE '亲密指数参数数量: %', intimacy_count;
IF recall_count < 10 THEN
RAISE WARNING '召回指数参数不完整期望至少10个';
END IF;
IF intimacy_count < 15 THEN
RAISE WARNING '亲密指数参数不完整期望至少15个';
END IF;
END $$;
-- 显示所有参数
SELECT
index_type,
param_name,
param_value,
description,
effective_from
FROM billiards_dws.cfg_index_parameters
ORDER BY index_type, param_name;

View File

@@ -5,17 +5,91 @@
本文档描述在ETL已完成的DWD层数据基础上对DWS层的数据处理 本文档描述在ETL已完成的DWD层数据基础上对DWS层的数据处理
- 完成对DWS层数据库的处理即数据库设计成果为DDL的SQL语句。 - 完成对DWS层数据库的处理即数据库设计成果为DDL的SQL语句。
- 数据读取处理到落库即DWD读取Python处理SQL写入。 - 数据读取处理到落库即DWD读取Python处理SQL写入。
- 在动手之前,先出一个任务计划文档,写明事实的具体技术方案细节。
文档更多聚焦业务描述你需要使用专业技能使用面向对象编程OOP思想完成程序设计直至代码完成 文档更多聚焦业务描述你需要使用专业技能使用面向对象编程OOP思想完成程序设计直至代码完成
- 参考.\README.md 了解现在项目现状。 - 参考.\README.md 了解现在项目现状。
- 参考.\etl_billiards\docs\dwd_main_tables_dictionary.md 了解 DWD的schema的表和字段若与数据库有出路则以当前数据库为准。 - 参考.\etl_billiards\docs 了解 DWD的schema的表和字段。
- SQL和Python代码需要详尽的高密度的中文注释。 - SQL和Python代码需要详尽的高密度的中文注释。
- 完成内容,需要详尽高密度的补充至.\README.md以方便后续维护。 - 完成内容,需要详尽高密度的补充至.\README.md以方便后续维护。
- DWS的表与表的字段 参考.\etl_billiards\docs\dwd_main_tables_dictionary.md 完成类似的数据库文档,方便后续维护。 - DWS的表与表的字段 参考.\etl_billiards\docs\dwd_main_tables_dictionary.md 完成类似的数据库文档,方便后续维护。
- 注意中文编码需求。 - 注意中文编码需求。
## 具体需求 ## 通用需求
### 助教视角 ### 数据分层
- 需要 我希望使用互联网软件的业内通用方法将数据按照更新时间分为4层以符合业务层面的查询效率速度。
- 第一层:回溯两天前到当前数据。
- 第二层回溯1个月前到当前数据。
- 第三层回溯3个月前到当前数据。
- 第四层:全量数据。
- 需要有配套的机制及时添加删除整理数据。
### 统计注意
当统计一些数据时,注意口径,数据有效性标识。举例:
- 计算助教业绩/工资时,需要参考助教废除表,相关业务数据的影响。
- 计算助教业绩/工资时,注意辨别 助教课 附加课影响。
## 业务需求
### 系统设置
- 助教新的绩效考核和工资结算方式更新为以下算法,影响工资结算和财务账务方面的统计核算,相关内容需要落库,以方便后续调整。还要标记执行时间(如哪个月执行哪个标准等),执行相关结算和计算逻辑。:
档位原因考虑 总业绩小时数阈值 专业课抽成(元/小时) 打赏课抽成 次月休假(天)
0档 淘汰压力 H <100 28 50% 3
1档 及格档(重点激励) 100≤ H <130 18 40% 4
2档 良好档(重点激励) 130≤ H <160 15 38% 4
3档 优秀档 160≤ H <190 13 35% 5
4档 卓越加速档(高端人才倾斜) 190≤ H <220 10 33% 6
5档 冠军加速档(高端人才倾斜) H ≥220 8 30% 休假自由
*课程分为2种dwd_assistant_service_log表的skill_name
基础课:又名 专业课 上桌 上钟,是为客户提供台球助教陪练的课程,按时长统计。精确到分钟。
附加课:又名 超休 激励 打赏,是客户支付较为高昂的价格,买断整小时与助教外出。
总业绩小时数阈值指基础课和附加课总和。
各级别助教dim_assistant表的level基础课对客户收费初级 98元/小时;中级 108元/小时;高级 118元/小时;星级 138元/小时;
附加课对客户收费统一为190元/小时。
充值提成:
冲刺奖 达成奖金
当月 H ≥ 190300 元
当月 H ≥ 220800 元(与上条不叠加,取高)
额外奖金:
冲刺奖 达成奖金
当月 H ≥ 190300 元
当月 H ≥ 220800 元(与上条不叠加,取高)
Top3 奖金:
第1名1000 元
第2名600 元
第3名400 元
规则:
1、过档后所有时长按新档位进行计算。
举例当前某中级助教已完成185小时基础课占170小时附加课15小时。则该月工资计算方法
170*108-13+15*1-0.35
2、本月新入职助教定档方案
按照日均*30的总业绩小时数定档。
在该25日之后入职的新助教最高定档至3档。
该折算仅用于定档不适用于“冲刺奖”和“Top3奖”的计算口径。
### 助教维度
以每个助教个体的视角
- 我要知道我的业绩档位,历史月份与本月档位进度,档位影响的收入单价。及相邻月份的变化。
- 我要知道我的有效业绩:历史月份与本月的 基础课课时,激励课课时,全部课课时。相邻月份的变化。
- 我要知道我的收入:历史月份与本月的收入(注意助教等级,业绩档位,课程种类等因素的总和计算)。相邻月份的变化。
- 我要知道我的客户情况过去7天、10天、15天、30天、60天、90天 的跨度进行统计,我服务过(基础课+附加课)的客户数据,并关联每次服务的 时间 时长 台桌 分类 等详细信息。
### 客户维度
统计每个客户的信息
- 我要知道每个客户过去7天、10天、15天、30天、60天、90天 的跨度进行统计,来店消费情况,并关联每次服务的 时间 食品饮品 时长 台桌 分类 助教服务 等详细信息。
### 财务维度
财务维度的需求(已经落到原型图需求级别了),见财务页面需求.md

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -69,6 +68,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_assistant_ex
WHERE assistant_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.*, e.* SELECT m.*, e.*

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -38,10 +37,11 @@
| 18 | effective_status | INTEGER | YES | | 生效状态。**枚举值**: 1(24)=有效, 3(10)=失效 **[待确认]** | | 18 | effective_status | INTEGER | YES | | 生效状态。**枚举值**: 1(24)=有效, 3(10)=失效 **[待确认]** |
| 19 | max_selectable_categories | INTEGER | YES | | 最大可选分类数(当前数据全为 0 | | 19 | max_selectable_categories | INTEGER | YES | | 最大可选分类数(当前数据全为 0 |
| 20 | creator_name | VARCHAR(100) | YES | | 创建人。**样本值**: "店长:郑丽珊", "管理员:郑丽珊" | | 20 | creator_name | VARCHAR(100) | YES | | 创建人。**样本值**: "店长:郑丽珊", "管理员:郑丽珊" |
| 21 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 21 | tenant_coupon_sale_order_item_id | BIGINT | YES | | 租户券销售订单项 ID |
| 22 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本效时间 | | 22 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本效时间 |
| 23 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 23 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 24 | scd2_version | INTEGER | YES | | 版本号 | | 24 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 25 | scd2_version | INTEGER | YES | | 版本号 |
## 样本数据 ## 样本数据
@@ -53,6 +53,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_groupbuy_package_ex
WHERE groupbuy_package_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.package_name, m.duration_seconds, e.start_clock, e.end_clock, e.effective_status SELECT m.package_name, m.duration_seconds, e.start_clock, e.end_clock, e.effective_status

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -70,13 +69,33 @@
| 50 | goodscategoryid | TEXT | YES | | 可用商品分类 ID 列表(当前数据全为空) | | 50 | goodscategoryid | TEXT | YES | | 可用商品分类 ID 列表(当前数据全为空) |
| 51 | pdassisnatlevel | TEXT | YES | | 陪打助教等级限制。**当前值**: "{}" | | 51 | pdassisnatlevel | TEXT | YES | | 陪打助教等级限制。**当前值**: "{}" |
| 52 | cxassisnatlevel | TEXT | YES | | 促销助教等级限制。**当前值**: "{}" | | 52 | cxassisnatlevel | TEXT | YES | | 促销助教等级限制。**当前值**: "{}" |
| 53 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 53 | able_share_member_discount | BOOLEAN | YES | | 是否可共享会员折扣 |
| 54 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 | | 54 | electricity_deduct_radio | NUMERIC(18,4) | YES | | 电费扣减比例 |
| 55 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 55 | electricity_discount | NUMERIC(18,4) | YES | | 电费折扣 |
| 56 | scd2_version | INTEGER | YES | | 版本号 | | 56 | electricity_card_deduct | BOOLEAN | YES | | 电费卡扣 |
| 57 | recharge_freeze_balance | NUMERIC(18,2) | YES | | 充值冻结余额 |
| 58 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 |
| 59 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 60 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 61 | scd2_version | INTEGER | YES | | 版本号 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_member_card_account_ex
WHERE member_card_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联查询卡片及折扣配置 -- 关联查询卡片及折扣配置
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -25,10 +24,13 @@
| 5 | growth_value | NUMERIC(18,2) | YES | | 成长值 | | 5 | growth_value | NUMERIC(18,2) | YES | | 成长值 |
| 6 | user_status | INTEGER | YES | | 用户状态。**枚举值**: 1(556)=正常 | | 6 | user_status | INTEGER | YES | | 用户状态。**枚举值**: 1(556)=正常 |
| 7 | status | INTEGER | YES | | 账户状态。**枚举值**: 1(490)=正常, 3(66)=**[含义待确认]** | | 7 | status | INTEGER | YES | | 账户状态。**枚举值**: 1(490)=正常, 3(66)=**[含义待确认]** |
| 8 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 8 | person_tenant_org_id | BIGINT | YES | | 人员租户组织 ID |
| 9 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 | | 9 | person_tenant_org_name | TEXT | YES | | 人员租户组织名称 |
| 10 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 10 | register_source | TEXT | YES | | 注册来源 |
| 11 | scd2_version | INTEGER | YES | | 版本号 | | 11 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 |
| 12 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 13 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 14 | scd2_version | INTEGER | YES | | 版本号 |
## 样本数据 ## 样本数据
@@ -40,6 +42,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_member_ex
WHERE member_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.*, e.point, e.growth_value, e.status SELECT m.*, e.point, e.growth_value, e.status

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -46,6 +45,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_site_ex
WHERE site_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.*, e.* SELECT m.*, e.*

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -51,6 +50,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_store_goods_ex
WHERE site_goods_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.goods_name, m.sale_price, m.sale_qty, e.unit, e.stock_qty, e.cost_price SELECT m.goods_name, m.sale_price, m.sale_qty, e.unit, e.stock_qty, e.cost_price

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -39,6 +38,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_table_ex
WHERE table_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.table_name, m.site_table_area_name, e.show_status, e.table_status SELECT m.table_name, m.site_table_area_name, e.show_status, e.table_status

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -43,6 +42,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_tenant_goods_ex
WHERE tenant_goods_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.goods_name, m.market_price, e.cost_price, e.min_discount_price SELECT m.goods_name, m.market_price, e.cost_price, e.min_discount_price

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -48,9 +47,23 @@
| 28 | get_grade_times | INTEGER | YES | | 评分次数(当前数据全为 0 | | 28 | get_grade_times | INTEGER | YES | | 评分次数(当前数据全为 0 |
| 29 | grade_status | INTEGER | YES | | 评分状态。**枚举值**: 0(216)=未评分, 1(4787)=已评分 **[待确认]** | | 29 | grade_status | INTEGER | YES | | 评分状态。**枚举值**: 0(216)=未评分, 1(4787)=已评分 **[待确认]** |
| 30 | composite_grade_time | TIMESTAMPTZ | YES | | 评分时间 | | 30 | composite_grade_time | TIMESTAMPTZ | YES | | 评分时间 |
| 31 | assistant_team_name | TEXT | YES | | 助教团队名称 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段ledger_start_time, ledger_end_time, composite_grade_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_assistant_service_log_ex
ORDER BY ledger_start_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT m.nickname, m.ledger_amount, e.table_name, e.assistant_name, e.grade_status SELECT m.nickname, m.ledger_amount, e.table_name, e.assistant_name, e.grade_status

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -46,4 +45,18 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段create_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_assistant_trash_event m
JOIN billiards_dwd.dwd_assistant_trash_event_ex e ON m.assistant_trash_event_id = e.assistant_trash_event_id
ORDER BY m.create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_assistant_trash_event` 通过 `assistant_trash_event_id` 关联,提供台桌和台区名称信息。 与主表 `dwd_assistant_trash_event` 通过 `assistant_trash_event_id` 关联,提供台桌和台区名称信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -39,6 +38,13 @@
| 19 | salesman_role_id | BIGINT | YES | | 销售员角色 ID当前数据全为 0 | | 19 | salesman_role_id | BIGINT | YES | | 销售员角色 ID当前数据全为 0 |
| 20 | salesman_org_id | BIGINT | YES | | 销售员组织 ID当前数据全为 0 | | 20 | salesman_org_id | BIGINT | YES | | 销售员组织 ID当前数据全为 0 |
| 21 | ledger_group_name | VARCHAR(128) | YES | | 账本分组名称(当前数据全为 NULL | | 21 | ledger_group_name | VARCHAR(128) | YES | | 账本分组名称(当前数据全为 NULL |
| 22 | table_share_money | NUMERIC(18,2) | YES | | 台费分摊金额 |
| 23 | table_service_share_money | NUMERIC(18,2) | YES | | 台费服务分摊金额 |
| 24 | goods_share_money | NUMERIC(18,2) | YES | | 商品分摊金额 |
| 25 | good_service_share_money | NUMERIC(18,2) | YES | | 商品服务分摊金额 |
| 26 | assistant_share_money | NUMERIC(18,2) | YES | | 助教分摊金额 |
| 27 | assistant_service_share_money | NUMERIC(18,2) | YES | | 助教服务分摊金额 |
| 28 | recharge_share_money | NUMERIC(18,2) | YES | | 充值分摊金额 |
## 台区核销分布 ## 台区核销分布
@@ -59,4 +65,18 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段create_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_groupbuy_redemption m
JOIN billiards_dwd.dwd_groupbuy_redemption_ex e ON m.redemption_id = e.redemption_id
ORDER BY m.create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_groupbuy_redemption` 通过 `redemption_id` 关联,提供门店、台桌名称、操作员等扩展信息。 与主表 `dwd_groupbuy_redemption` 通过 `redemption_id` 关联,提供门店、台桌名称、操作员等扩展信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -24,6 +23,7 @@
| 4 | refund_amount | NUMERIC(18,2) | YES | | 退款金额 | | 4 | refund_amount | NUMERIC(18,2) | YES | | 退款金额 |
| 5 | operator_id | BIGINT | YES | | 操作员 ID | | 5 | operator_id | BIGINT | YES | | 操作员 ID |
| 6 | operator_name | VARCHAR(64) | YES | | 操作员名称。**枚举值**: "收银员:郑丽珊"(4101), "店长:郑丽珊"(223), "管理员:郑丽珊"(153), "店长:蒋雨轩"(124), "店长:谢晓洪"(115), "店长:黄月柳"(29) | | 6 | operator_name | VARCHAR(64) | YES | | 操作员名称。**枚举值**: "收银员:郑丽珊"(4101), "店长:郑丽珊"(223), "管理员:郑丽珊"(153), "店长:蒋雨轩"(124), "店长:谢晓洪"(115), "店长:黄月柳"(29) |
| 7 | principal_data | TEXT | YES | | 本金变动数据 |
## 操作员分布 ## 操作员分布
@@ -46,4 +46,18 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段change_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_member_balance_change m
JOIN billiards_dwd.dwd_member_balance_change_ex e ON m.balance_change_id = e.balance_change_id
ORDER BY m.change_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_member_balance_change` 通过 `balance_change_id` 关联,提供操作员和门店名称等扩展信息。 与主表 `dwd_member_balance_change` 通过 `balance_change_id` 关联,提供操作员和门店名称等扩展信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -42,6 +41,19 @@
## 使用说明 ## 使用说明
与主表 `dwd_platform_coupon_redemption` 通过 `platform_coupon_redemption_id` 关联,提供操作员等扩展信息。 **版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段coupon_free_time, create_time, consume_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_platform_coupon_redemption m
JOIN billiards_dwd.dwd_platform_coupon_redemption_ex e ON m.platform_coupon_redemption_id = e.platform_coupon_redemption_id
ORDER BY m.create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_platform_coupon_redemption` 通过 `platform_coupon_redemption_id` 关联,提供操作员等扩展信息。
**注意**: `coupon_remark` 字段在抖音渠道的核销记录中包含核验信息。 **注意**: `coupon_remark` 字段在抖音渠道的核销记录中包含核验信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -64,6 +63,18 @@
## 使用说明 ## 使用说明
与主表 `dwd_recharge_order` 通过 `recharge_order_id` 关联,提供操作员、各类金额明细等扩展信息。 **版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段revoke_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_recharge_order_ex
ORDER BY revoke_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_recharge_order` 通过 `recharge_order_id` 关联,提供操作员、各类金额明细等扩展信息。
**注意**: 样本数据获取时因日期解析错误未能获取。 **注意**: 样本数据获取时因日期解析错误未能获取。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -48,4 +47,18 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段pay_time, create_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_refund m
JOIN billiards_dwd.dwd_refund_ex e ON m.refund_id = e.refund_id
ORDER BY m.pay_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_refund` 通过 `refund_id` 关联,提供退款状态和渠道等扩展信息。 与主表 `dwd_refund` 通过 `refund_id` 关联,提供退款状态和渠道等扩展信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -48,9 +47,23 @@
| 28 | order_remark | VARCHAR(255) | YES | | 订单备注。**样本值**: "五折"(42), "轩哥"(24), "陈德韩"(7), "免台费"(3) | | 28 | order_remark | VARCHAR(255) | YES | | 订单备注。**样本值**: "五折"(42), "轩哥"(24), "陈德韩"(7), "免台费"(3) |
| 29 | operator_id | BIGINT | YES | | 操作员 ID | | 29 | operator_id | BIGINT | YES | | 操作员 ID |
| 30 | salesman_user_id | BIGINT | YES | | 销售员用户 ID当前数据全为 0 | | 30 | salesman_user_id | BIGINT | YES | | 销售员用户 ID当前数据全为 0 |
| 31 | settle_list | JSONB | YES | | 结算明细列表JSON数组 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段revoke_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_settlement_head_ex
ORDER BY revoke_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 关联主表与扩展表 -- 关联主表与扩展表
SELECT SELECT
@@ -59,7 +72,6 @@ SELECT
FROM billiards_dwd.dwd_settlement_head m FROM billiards_dwd.dwd_settlement_head m
JOIN billiards_dwd.dwd_settlement_head_ex e JOIN billiards_dwd.dwd_settlement_head_ex e
ON m.order_settle_id = e.order_settle_id; ON m.order_settle_id = e.order_settle_id;
-- 统计备注订单 -- 统计备注订单
SELECT order_remark, COUNT(*) SELECT order_remark, COUNT(*)
FROM billiards_dwd.dwd_settlement_head_ex FROM billiards_dwd.dwd_settlement_head_ex

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -56,4 +55,18 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段create_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_store_goods_sale m
JOIN billiards_dwd.dwd_store_goods_sale_ex e ON m.store_goods_sale_id = e.store_goods_sale_id
ORDER BY m.create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_store_goods_sale` 通过 `store_goods_sale_id` 关联,提供销售详情、折扣优惠等扩展信息。 与主表 `dwd_store_goods_sale` 通过 `store_goods_sale_id` 关联,提供销售详情、折扣优惠等扩展信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -26,6 +25,11 @@
| 6 | operator_name | VARCHAR(64) | YES | | 操作员名称。**枚举值**: "收银员:郑丽珊"(2849) | | 6 | operator_name | VARCHAR(64) | YES | | 操作员名称。**枚举值**: "收银员:郑丽珊"(2849) |
| 7 | applicant_id | BIGINT | YES | | 申请人 ID | | 7 | applicant_id | BIGINT | YES | | 申请人 ID |
| 8 | operator_id | BIGINT | YES | | 操作员 ID | | 8 | operator_id | BIGINT | YES | | 操作员 ID |
| 9 | area_type_id | BIGINT | YES | | 区域类型 ID |
| 10 | site_table_area_id | BIGINT | YES | | 门店台区 ID |
| 11 | site_table_area_name | TEXT | YES | | 门店台区名称 |
| 12 | site_name | TEXT | YES | | 门店名称 |
| 13 | tenant_name | TEXT | YES | | 租户名称 |
## 样本数据 ## 样本数据
@@ -36,4 +40,18 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 主表可用时间字段adjust_time
```sql
-- 取最新一条(按主表时间字段)
SELECT e.*
FROM billiards_dwd.dwd_table_fee_adjust m
JOIN billiards_dwd.dwd_table_fee_adjust_ex e ON m.table_fee_adjust_id = e.table_fee_adjust_id
ORDER BY m.adjust_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_table_fee_adjust` 通过 `table_fee_adjust_id` 关联,提供调整类型、申请人、操作员等扩展信息。 与主表 `dwd_table_fee_adjust` 通过 `table_fee_adjust_id` 关联,提供调整类型、申请人、操作员等扩展信息。

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -30,6 +29,7 @@
| 10 | operator_id | BIGINT | YES | | 操作员 ID。**枚举值**: 3个不同ID | | 10 | operator_id | BIGINT | YES | | 操作员 ID。**枚举值**: 3个不同ID |
| 11 | salesman_user_id | BIGINT | YES | | 销售员用户 ID当前数据全为 0 | | 11 | salesman_user_id | BIGINT | YES | | 销售员用户 ID当前数据全为 0 |
| 12 | salesman_org_id | BIGINT | YES | | 销售员组织 ID当前数据全为 0 | | 12 | salesman_org_id | BIGINT | YES | | 销售员组织 ID当前数据全为 0 |
| 13 | order_consumption_type | INTEGER | YES | | 订单消费类型 |
## 样本数据 ## 样本数据
@@ -41,4 +41,17 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段ledger_start_time, last_use_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_table_fee_log_ex
ORDER BY ledger_start_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
与主表 `dwd_table_fee_log` 通过 `table_fee_log_id` 关联,提供操作员和时间相关的扩展信息。 与主表 `dwd_table_fee_log` 通过 `table_fee_log_id` 关联,提供操作员和时间相关的扩展信息。

View File

@@ -7,22 +7,21 @@
## 概述 ## 概述
`billiards_dwd` 是台球门店数据仓库的明细层(DWD),包含维度表(DIM)和事实表(DWD)。本 Schema 基于 SCD2 缓慢变化维度设计,支持历史数据追溯。 `billiards_dwd` 是台球门店数据仓库的明细层(DWD),包含维度表(DIM)和事实表(DWD)。本 Schema 基于 SCD2 缓慢变化维度设计,支持历史数据追溯。
--- ---
## 维度表 (Dimension Tables) ## 维度表 (Dimension Tables)
| 序号 | 表名 | 说明 | 主键 | 扩展表 | 文档链接 | | 序号 | 表名 | 说明 | 主键 | 扩展表 | 文档链接 |
|------|------|------|------|--------|----------| |------|------|------|------|--------|----------|
| 1 | dim_assistant | 助教信息 | assistant_id | dim_assistant_ex | [主表](BD_manual_dim_assistant.md) / [扩展表](BD_manual_dim_assistant_ex.md) | | 1 | dim_assistant | 助教信息 | assistant_id, scd2_start_time | dim_assistant_ex | [主表](BD_manual_dim_assistant.md) / [扩展表](BD_manual_dim_assistant_ex.md) |
| 2 | dim_goods_category | 商品分类 | goods_category_id | 无 | [主表](BD_manual_dim_goods_category.md) | | 2 | dim_goods_category | 商品分类 | category_id, scd2_start_time | 无 | [主表](BD_manual_dim_goods_category.md) |
| 3 | dim_groupbuy_package | 团购套餐 | groupbuy_package_id | dim_groupbuy_package_ex | [主表](BD_manual_dim_groupbuy_package.md) / [扩展表](BD_manual_dim_groupbuy_package_ex.md) | | 3 | dim_groupbuy_package | 团购套餐 | groupbuy_package_id, scd2_start_time | dim_groupbuy_package_ex | [主表](BD_manual_dim_groupbuy_package.md) / [扩展表](BD_manual_dim_groupbuy_package_ex.md) |
| 4 | dim_member | 会员信息 | member_id | dim_member_ex | [主表](BD_manual_dim_member.md) / [扩展表](BD_manual_dim_member_ex.md) | | 4 | dim_member | 会员信息 | member_id, scd2_start_time | dim_member_ex | [主表](BD_manual_dim_member.md) / [扩展表](BD_manual_dim_member_ex.md) |
| 5 | dim_member_card_account | 会员卡账户 | member_card_account_id | dim_member_card_account_ex | [主表](BD_manual_dim_member_card_account.md) / [扩展表](BD_manual_dim_member_card_account_ex.md) | | 5 | dim_member_card_account | 会员卡账户 | member_card_id, scd2_start_time | dim_member_card_account_ex | [主表](BD_manual_dim_member_card_account.md) / [扩展表](BD_manual_dim_member_card_account_ex.md) |
| 6 | dim_site | 门店信息 | site_id | dim_site_ex | [主表](BD_manual_dim_site.md) / [扩展表](BD_manual_dim_site_ex.md) | | 6 | dim_site | 门店信息 | site_id, scd2_start_time | dim_site_ex | [主表](BD_manual_dim_site.md) / [扩展表](BD_manual_dim_site_ex.md) |
| 7 | dim_store_goods | 门店商品 | store_goods_id | dim_store_goods_ex | [主表](BD_manual_dim_store_goods.md) / [扩展表](BD_manual_dim_store_goods_ex.md) | | 7 | dim_store_goods | 门店商品 | site_goods_id, scd2_start_time | dim_store_goods_ex | [主表](BD_manual_dim_store_goods.md) / [扩展表](BD_manual_dim_store_goods_ex.md) |
| 8 | dim_table | 台桌信息 | table_id | dim_table_ex | [主表](BD_manual_dim_table.md) / [扩展表](BD_manual_dim_table_ex.md) | | 8 | dim_table | 台桌信息 | table_id, scd2_start_time | dim_table_ex | [主表](BD_manual_dim_table.md) / [扩展表](BD_manual_dim_table_ex.md) |
| 9 | dim_tenant_goods | 租户商品 | tenant_goods_id | dim_tenant_goods_ex | [主表](BD_manual_dim_tenant_goods.md) / [扩展表](BD_manual_dim_tenant_goods_ex.md) | | 9 | dim_tenant_goods | 租户商品 | tenant_goods_id, scd2_start_time | dim_tenant_goods_ex | [主表](BD_manual_dim_tenant_goods.md) / [扩展表](BD_manual_dim_tenant_goods_ex.md) |
--- ---
@@ -30,7 +29,7 @@
| 序号 | 表名 | 说明 | 主键 | 扩展表 | 文档链接 | | 序号 | 表名 | 说明 | 主键 | 扩展表 | 文档链接 |
|------|------|------|------|--------|----------| |------|------|------|------|--------|----------|
| 1 | dwd_assistant_service_log | 助教服务流水 | assistant_service_log_id | dwd_assistant_service_log_ex | [主表](BD_manual_dwd_assistant_service_log.md) / [扩展表](BD_manual_dwd_assistant_service_log_ex.md) | | 1 | dwd_assistant_service_log | 助教服务流水 | assistant_service_id | dwd_assistant_service_log_ex | [主表](BD_manual_dwd_assistant_service_log.md) / [扩展表](BD_manual_dwd_assistant_service_log_ex.md) |
| 2 | dwd_assistant_trash_event | 助教服务作废 | assistant_trash_event_id | dwd_assistant_trash_event_ex | [主表](BD_manual_dwd_assistant_trash_event.md) / [扩展表](BD_manual_dwd_assistant_trash_event_ex.md) | | 2 | dwd_assistant_trash_event | 助教服务作废 | assistant_trash_event_id | dwd_assistant_trash_event_ex | [主表](BD_manual_dwd_assistant_trash_event.md) / [扩展表](BD_manual_dwd_assistant_trash_event_ex.md) |
| 3 | dwd_groupbuy_redemption | 团购券核销 | redemption_id | dwd_groupbuy_redemption_ex | [主表](BD_manual_dwd_groupbuy_redemption.md) / [扩展表](BD_manual_dwd_groupbuy_redemption_ex.md) | | 3 | dwd_groupbuy_redemption | 团购券核销 | redemption_id | dwd_groupbuy_redemption_ex | [主表](BD_manual_dwd_groupbuy_redemption.md) / [扩展表](BD_manual_dwd_groupbuy_redemption_ex.md) |
| 4 | dwd_member_balance_change | 会员余额变动 | balance_change_id | dwd_member_balance_change_ex | [主表](BD_manual_dwd_member_balance_change.md) / [扩展表](BD_manual_dwd_member_balance_change_ex.md) | | 4 | dwd_member_balance_change | 会员余额变动 | balance_change_id | dwd_member_balance_change_ex | [主表](BD_manual_dwd_member_balance_change.md) / [扩展表](BD_manual_dwd_member_balance_change_ex.md) |
@@ -38,7 +37,7 @@
| 6 | dwd_platform_coupon_redemption | 平台券核销 | platform_coupon_redemption_id | dwd_platform_coupon_redemption_ex | [主表](BD_manual_dwd_platform_coupon_redemption.md) / [扩展表](BD_manual_dwd_platform_coupon_redemption_ex.md) | | 6 | dwd_platform_coupon_redemption | 平台券核销 | platform_coupon_redemption_id | dwd_platform_coupon_redemption_ex | [主表](BD_manual_dwd_platform_coupon_redemption.md) / [扩展表](BD_manual_dwd_platform_coupon_redemption_ex.md) |
| 7 | dwd_recharge_order | 充值订单 | recharge_order_id | dwd_recharge_order_ex | [主表](BD_manual_dwd_recharge_order.md) / [扩展表](BD_manual_dwd_recharge_order_ex.md) | | 7 | dwd_recharge_order | 充值订单 | recharge_order_id | dwd_recharge_order_ex | [主表](BD_manual_dwd_recharge_order.md) / [扩展表](BD_manual_dwd_recharge_order_ex.md) |
| 8 | dwd_refund | 退款流水 | refund_id | dwd_refund_ex | [主表](BD_manual_dwd_refund.md) / [扩展表](BD_manual_dwd_refund_ex.md) | | 8 | dwd_refund | 退款流水 | refund_id | dwd_refund_ex | [主表](BD_manual_dwd_refund.md) / [扩展表](BD_manual_dwd_refund_ex.md) |
| 9 | dwd_settlement_head | 结账单 | settlement_head_id | dwd_settlement_head_ex | [主表](BD_manual_dwd_settlement_head.md) / [扩展表](BD_manual_dwd_settlement_head_ex.md) | | 9 | dwd_settlement_head | 结账单 | order_settle_id | dwd_settlement_head_ex | [主表](BD_manual_dwd_settlement_head.md) / [扩展表](BD_manual_dwd_settlement_head_ex.md) |
| 10 | dwd_store_goods_sale | 商品销售流水 | store_goods_sale_id | dwd_store_goods_sale_ex | [主表](BD_manual_dwd_store_goods_sale.md) / [扩展表](BD_manual_dwd_store_goods_sale_ex.md) | | 10 | dwd_store_goods_sale | 商品销售流水 | store_goods_sale_id | dwd_store_goods_sale_ex | [主表](BD_manual_dwd_store_goods_sale.md) / [扩展表](BD_manual_dwd_store_goods_sale_ex.md) |
| 11 | dwd_table_fee_adjust | 台费调整 | table_fee_adjust_id | dwd_table_fee_adjust_ex | [主表](BD_manual_dwd_table_fee_adjust.md) / [扩展表](BD_manual_dwd_table_fee_adjust_ex.md) | | 11 | dwd_table_fee_adjust | 台费调整 | table_fee_adjust_id | dwd_table_fee_adjust_ex | [主表](BD_manual_dwd_table_fee_adjust.md) / [扩展表](BD_manual_dwd_table_fee_adjust_ex.md) |
| 12 | dwd_table_fee_log | 台费计费流水 | table_fee_log_id | dwd_table_fee_log_ex | [主表](BD_manual_dwd_table_fee_log.md) / [扩展表](BD_manual_dwd_table_fee_log_ex.md) | | 12 | dwd_table_fee_log | 台费计费流水 | table_fee_log_id | dwd_table_fee_log_ex | [主表](BD_manual_dwd_table_fee_log.md) / [扩展表](BD_manual_dwd_table_fee_log_ex.md) |
@@ -58,6 +57,18 @@
--- ---
## 最新值获取
- 维度表SCD2使用 scd2_is_current = 1 获取当前版本。
- 事实表:无 SCD2 版本字段,按业务时间字段倒序获取最新记录(如 pay_time / create_time / update_time 等)。
```sql
-- 维度表取当前版本
SELECT * FROM billiards_dwd.dim_member WHERE scd2_is_current = 1;
-- 事实表取最新记录(示例:按 pay_time
SELECT * FROM billiards_dwd.dwd_payment ORDER BY pay_time DESC NULLS LAST LIMIT 1;
```
## 常见 ID 关联说明 ## 常见 ID 关联说明
| ID 字段 | 关联表 | 说明 | | ID 字段 | 关联表 | 说明 |
@@ -79,6 +90,7 @@
### 主表 + 扩展表模式 ### 主表 + 扩展表模式
大部分表采用"主表 + 扩展表"的设计模式: 大部分表采用"主表 + 扩展表"的设计模式:
- **主表**:包含核心业务字段(如金额、状态、关键 ID - **主表**:包含核心业务字段(如金额、状态、关键 ID
- **扩展表**:包含附属信息(如操作员、门店名称快照、各类详细字段) - **扩展表**:包含附属信息(如操作员、门店名称快照、各类详细字段)
- 两表通过主键一对一关联 - 两表通过主键一对一关联
@@ -86,6 +98,7 @@
### 枚举值说明 ### 枚举值说明
文档中的枚举值格式为 `值(数量)=含义`,例如: 文档中的枚举值格式为 `值(数量)=含义`,例如:
- `1(100)=有效` 表示值为 1 的记录有 100 条,含义为"有效" - `1(100)=有效` 表示值为 1 的记录有 100 条,含义为"有效"
- **[待确认]** 表示该值的含义无法从数据中确定 - **[待确认]** 表示该值的含义无法从数据中确定

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -40,6 +39,7 @@
## 使用说明 ## 使用说明
使用 scd2_is_current = 1 获取当前版本。
```sql ```sql
-- 查询当前在职助教 -- 查询当前在职助教
SELECT * FROM billiards_dwd.dim_assistant SELECT * FROM billiards_dwd.dim_assistant

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -20,10 +19,10 @@
|------|--------|------|------|------|------| |------|--------|------|------|------|------|
| 1 | category_id | BIGINT | NO | PK | 分类唯一标识 | | 1 | category_id | BIGINT | NO | PK | 分类唯一标识 |
| 2 | tenant_id | BIGINT | YES | | 租户 ID当前值: 2790683160709957 | | 2 | tenant_id | BIGINT | YES | | 租户 ID当前值: 2790683160709957 |
| 3 | category_name | VARCHAR | YES | | 分类名称。**样本值**: "槟榔", "皮头" 等 | | 3 | category_name | VARCHAR(50) | YES | | 分类名称。**样本值**: "槟榔", "皮头" 等 |
| 4 | alias_name | VARCHAR | YES | | 分类别名(当前数据大部分为空) | | 4 | alias_name | VARCHAR(50) | YES | | 分类别名(当前数据大部分为空) |
| 5 | parent_category_id | BIGINT | YES | | 父级分类 ID0=一级分类)→ 自关联 | | 5 | parent_category_id | BIGINT | YES | | 父级分类 ID0=一级分类)→ 自关联 |
| 6 | business_name | VARCHAR | YES | | 业务大类名称。**样本值**: "酒水", "器材" 等 | | 6 | business_name | VARCHAR(50) | YES | | 业务大类名称。**样本值**: "酒水", "器材" 等 |
| 7 | tenant_goods_business_id | BIGINT | YES | | 业务大类 ID | | 7 | tenant_goods_business_id | BIGINT | YES | | 业务大类 ID |
| 8 | category_level | INTEGER | YES | | 分类层级。**枚举值**: 1=一级大类, 2=二级子类 | | 8 | category_level | INTEGER | YES | | 分类层级。**枚举值**: 1=一级大类, 2=二级子类 |
| 9 | is_leaf | INTEGER | YES | | 是否叶子节点。**枚举值**: 0=非叶子, 1=叶子 | | 9 | is_leaf | INTEGER | YES | | 是否叶子节点。**枚举值**: 0=非叶子, 1=叶子 |
@@ -40,12 +39,10 @@
``` ```
槟榔(一级) 槟榔(一级)
├── 槟榔(二级) ├── 槟榔(二级)
器材(一级) 器材(一级)
├── 皮头 ├── 皮头
├── 球杆 ├── 球杆
├── 其他 ├── 其他
酒水(一级) 酒水(一级)
├── 饮料 ├── 饮料
├── 酒水 ├── 酒水
@@ -57,11 +54,25 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_goods_category
WHERE category_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询一级分类 -- 查询一级分类
SELECT * FROM billiards_dwd.dim_goods_category SELECT * FROM billiards_dwd.dim_goods_category
WHERE scd2_is_current = 1 AND parent_category_id = 0; WHERE scd2_is_current = 1 AND parent_category_id = 0;
-- 查询某一级分类下的二级分类 -- 查询某一级分类下的二级分类
SELECT * FROM billiards_dwd.dim_goods_category SELECT * FROM billiards_dwd.dim_goods_category
WHERE scd2_is_current = 1 AND parent_category_id = <一级分类ID>; WHERE scd2_is_current = 1 AND parent_category_id = <一级分类ID>;

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -21,29 +20,45 @@
| 1 | groupbuy_package_id | BIGINT | NO | PK | 团购套餐 ID | | 1 | groupbuy_package_id | BIGINT | NO | PK | 团购套餐 ID |
| 2 | tenant_id | BIGINT | YES | | 租户 ID当前值: 2790683160709957 | | 2 | tenant_id | BIGINT | YES | | 租户 ID当前值: 2790683160709957 |
| 3 | site_id | BIGINT | YES | | 门店 ID → dim_site当前值: 2790685415443269 | | 3 | site_id | BIGINT | YES | | 门店 ID → dim_site当前值: 2790685415443269 |
| 4 | package_name | VARCHAR | YES | | 套餐名称。**样本值**: "中八、斯诺克包厢两小时", "斯诺克两小时"等 | | 4 | package_name | VARCHAR(200) | YES | | 套餐名称。**样本值**: "中八、斯诺克包厢两小时", "斯诺克两小时"等 |
| 5 | package_template_id | BIGINT | YES | | 套餐模板 ID | | 5 | package_template_id | BIGINT | YES | | 套餐模板 ID |
| 6 | selling_price | NUMERIC(10,2) | YES | | 售卖价格每笔订单不同从核销记录中dwd_groupbuy_redemption获取 | | 6 | selling_price | NUMERIC(10,2) | YES | | 售卖价格每笔订单不同从核销记录中dwd_groupbuy_redemption获取 |
| 7 | coupon_face_value | NUMERIC(10,2) | YES | | 券面值每笔订单不同从核销记录中dwd_groupbuy_redemption获取 | | 7 | coupon_face_value | NUMERIC(10,2) | YES | | 券面值每笔订单不同从核销记录中dwd_groupbuy_redemption获取 |
| 8 | duration_seconds | INTEGER | YES | | 套餐时长(秒)。**样本值**: 3600=1小时, 7200=2小时, 14400=4小时 等 | | 8 | duration_seconds | INTEGER | YES | | 套餐时长(秒)。**样本值**: 3600=1小时, 7200=2小时, 14400=4小时 等 |
| 9 | start_time | TIMESTAMPTZ | YES | | 套餐生效开始时间 | | 9 | start_time | TIMESTAMPTZ | YES | | 套餐生效开始时间 |
| 10 | end_time | TIMESTAMPTZ | YES | | 套餐生效结束时间 | | 10 | end_time | TIMESTAMPTZ | YES | | 套餐生效结束时间 |
| 11 | table_area_name | VARCHAR | YES | | 适用台区名称。**枚举值**: "A区", "VIP包厢", "斯诺克区", "B区", "麻将房", "888" | | 11 | table_area_name | VARCHAR(100) | YES | | 适用台区名称。**枚举值**: "A区", "VIP包厢", "斯诺克区", "B区", "麻将房", "888" |
| 12 | is_enabled | INTEGER | YES | | 启用状态。**枚举值**: 1=启用, 2=停用 | | 12 | is_enabled | INTEGER | YES | | 启用状态。**枚举值**: 1=启用, 2=停用 |
| 13 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 13 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 14 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 14 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 15 | tenant_table_area_id_list | VARCHAR | YES | | 租户级台区 ID 列表 | | 15 | tenant_table_area_id_list | VARCHAR(512) | YES | | 租户级台区 ID 列表 |
| 16 | card_type_ids | VARCHAR | YES | | 允许使用的卡类型 ID 列表(当前数据为 "0" | | 16 | card_type_ids | VARCHAR(255) | YES | | 允许使用的卡类型 ID 列表(当前数据为 "0" |
| 17 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 17 | sort | INTEGER | YES | | 排序 |
| 18 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 | | 18 | is_first_limit | BOOLEAN | YES | | 是否首单限制 |
| 19 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 19 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 |
| 20 | scd2_version | INTEGER | YES | | 版本号 | | 20 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 21 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 22 | scd2_version | INTEGER | YES | | 版本号 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_groupbuy_package
WHERE groupbuy_package_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询当前启用的套餐 -- 查询当前启用的套餐
SELECT * FROM billiards_dwd.dim_groupbuy_package SELECT * FROM billiards_dwd.dim_groupbuy_package
WHERE scd2_is_current = 1 AND is_delete = 0 AND is_enabled = 1; WHERE scd2_is_current = 1 AND is_delete = 0 AND is_enabled = 1;
``` ```

View File

@@ -27,20 +27,34 @@
| 8 | member_card_grade_name | TEXT | YES | | 卡等级名称。**枚举值**: "储值卡", "台费卡", "年卡", "活动抵用券", "月卡" | | 8 | member_card_grade_name | TEXT | YES | | 卡等级名称。**枚举值**: "储值卡", "台费卡", "年卡", "活动抵用券", "月卡" |
| 9 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 9 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 10 | update_time | TIMESTAMPTZ | YES | | 更新时间 | | 10 | update_time | TIMESTAMPTZ | YES | | 更新时间 |
| 11 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 11 | pay_money_sum | NUMERIC(18,2) | YES | | 累计支付金额 |
| 12 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 | | 12 | recharge_money_sum | NUMERIC(18,2) | YES | | 累计充值金额 |
| 13 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 13 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 |
| 14 | scd2_version | INTEGER | YES | | 版本号 | | 14 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 15 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 16 | scd2_version | INTEGER | YES | | 版本号 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_member
WHERE member_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询当前有效会员 -- 查询当前有效会员
SELECT * FROM billiards_dwd.dim_member SELECT * FROM billiards_dwd.dim_member
WHERE scd2_is_current = 1; WHERE scd2_is_current = 1;
-- 按卡类型统计会员数 -- 按卡类型统计会员数
SELECT member_card_grade_name, COUNT(*) SELECT member_card_grade_name, COUNT(*)
FROM billiards_dwd.dim_member FROM billiards_dwd.dim_member

View File

@@ -34,10 +34,12 @@
| 15 | last_consume_time | TIMESTAMPTZ | YES | | 最近消费时间 | | 15 | last_consume_time | TIMESTAMPTZ | YES | | 最近消费时间 |
| 16 | status | INTEGER | YES | | 卡状态。**枚举值**: 1=正常, 4=过期 | | 16 | status | INTEGER | YES | | 卡状态。**枚举值**: 1=正常, 4=过期 |
| 17 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 17 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 18 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 18 | principal_balance | NUMERIC(18,2) | YES | | 本金余额 |
| 19 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 | | 19 | member_grade | INTEGER | YES | | 会员等级 |
| 20 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 20 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 |
| 21 | scd2_version | INTEGER | YES | | 版本号 | | 21 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 22 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 23 | scd2_version | INTEGER | YES | | 版本号 |
## 卡种分布 ## 卡种分布
@@ -50,9 +52,23 @@
| 2793306611533637 | 月卡 | 充值获得,时长卡,仅可抵扣台费 | | 2793306611533637 | 月卡 | 充值获得,时长卡,仅可抵扣台费 |
| 2791987095408517 | 年卡 | 充值获得,时长卡,仅可抵扣台费 | | 2791987095408517 | 年卡 | 充值获得,时长卡,仅可抵扣台费 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_member_card_account
WHERE member_card_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询有效的储值卡 -- 查询有效的储值卡
SELECT * FROM billiards_dwd.dim_member_card_account SELECT * FROM billiards_dwd.dim_member_card_account

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -44,6 +43,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_site
WHERE site_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询当前有效门店 -- 查询当前有效门店
SELECT * FROM billiards_dwd.dim_site SELECT * FROM billiards_dwd.dim_site

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -38,10 +37,12 @@
| 18 | enable_status | INTEGER | YES | | 启用状态。**枚举值**: 1=启用 | | 18 | enable_status | INTEGER | YES | | 启用状态。**枚举值**: 1=启用 |
| 19 | send_state | INTEGER | YES | | 配送状态。暂无作用 | | 19 | send_state | INTEGER | YES | | 配送状态。暂无作用 |
| 20 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 20 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 21 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 21 | commodity_code | TEXT | YES | | 商品编码 |
| 22 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 | | 22 | not_sale | INTEGER | YES | | 是否停售 |
| 23 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 23 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 |
| 24 | scd2_version | INTEGER | YES | | 版本号 | | 24 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 25 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 26 | scd2_version | INTEGER | YES | | 版本号 |
## 样本数据 ## 样本数据
@@ -54,6 +55,21 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_store_goods
WHERE site_goods_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询当前上架商品 -- 查询当前上架商品
SELECT * FROM billiards_dwd.dim_store_goods SELECT * FROM billiards_dwd.dim_store_goods

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -25,10 +24,11 @@
| 5 | site_table_area_name | TEXT | YES | | 台区名称。**样本值**: "A区", "B区", "补时长", "C区", "麻将房", "K包", "VIP包厢", "斯诺克区", "666", "k包活动区", "M7" 等 | | 5 | site_table_area_name | TEXT | YES | | 台区名称。**样本值**: "A区", "B区", "补时长", "C区", "麻将房", "K包", "VIP包厢", "斯诺克区", "666", "k包活动区", "M7" 等 |
| 6 | tenant_table_area_id | BIGINT | YES | | 租户级台区 ID | | 6 | tenant_table_area_id | BIGINT | YES | | 租户级台区 ID |
| 7 | table_price | NUMERIC(18,2) | YES | | 台桌单价(当前数据全为 0.00 | | 7 | table_price | NUMERIC(18,2) | YES | | 台桌单价(当前数据全为 0.00 |
| 8 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 8 | order_id | BIGINT | YES | | 订单 ID |
| 9 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本效时间 | | 9 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本效时间 |
| 10 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 10 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 11 | scd2_version | INTEGER | YES | | 版本号 | | 11 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 12 | scd2_version | INTEGER | YES | | 版本号 |
## 台区分布 ## 台区分布
@@ -52,11 +52,25 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_table
WHERE table_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询当前有效台桌 -- 查询当前有效台桌
SELECT * FROM billiards_dwd.dim_table SELECT * FROM billiards_dwd.dim_table
WHERE scd2_is_current = 1; WHERE scd2_is_current = 1;
-- 按台区统计台桌数 -- 按台区统计台桌数
SELECT site_table_area_name, COUNT(*) SELECT site_table_area_name, COUNT(*)
FROM billiards_dwd.dim_table FROM billiards_dwd.dim_table

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -21,25 +20,40 @@
| 1 | tenant_goods_id | BIGINT | NO | PK | 租户商品 IDSKU | | 1 | tenant_goods_id | BIGINT | NO | PK | 租户商品 IDSKU |
| 2 | tenant_id | BIGINT | YES | | 租户 ID | | 2 | tenant_id | BIGINT | YES | | 租户 ID |
| 3 | supplier_id | BIGINT | YES | | 供应商 ID当前数据全为 0 | | 3 | supplier_id | BIGINT | YES | | 供应商 ID当前数据全为 0 |
| 4 | category_name | VARCHAR | YES | | 分类名称(二级分类)。**样本值**: "零食", "饮料", "香烟"等 | | 4 | category_name | VARCHAR(64) | YES | | 分类名称(二级分类)。**样本值**: "零食", "饮料", "香烟"等 |
| 5 | goods_category_id | BIGINT | YES | | 一级分类 ID | | 5 | goods_category_id | BIGINT | YES | | 一级分类 ID |
| 6 | goods_second_category_id | BIGINT | YES | | 二级分类 ID | | 6 | goods_second_category_id | BIGINT | YES | | 二级分类 ID |
| 7 | goods_name | VARCHAR | YES | | 商品名称。**样本值**: "海之言", "西梅多多饮品", "美汁源果粒橙", "三诺橙汁"等 | | 7 | goods_name | VARCHAR(128) | YES | | 商品名称。**样本值**: "海之言", "西梅多多饮品", "美汁源果粒橙", "三诺橙汁"等 |
| 8 | goods_number | VARCHAR | YES | | 商品编号(序号) | | 8 | goods_number | VARCHAR(64) | YES | | 商品编号(序号) |
| 9 | unit | VARCHAR | YES | | 商品单位。**枚举值**: "包", "瓶", "个", "份"等 | | 9 | unit | VARCHAR(16) | YES | | 商品单位。**枚举值**: "包", "瓶", "个", "份"等 |
| 10 | market_price | NUMERIC(18,2) | YES | | 市场价/吊牌价(元) | | 10 | market_price | NUMERIC(18,2) | YES | | 市场价/吊牌价(元) |
| 11 | goods_state | INTEGER | YES | | 商品状态。**枚举值**: 1=上架, 2=下架 | | 11 | goods_state | INTEGER | YES | | 商品状态。**枚举值**: 1=上架, 2=下架 |
| 12 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 12 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 13 | update_time | TIMESTAMPTZ | YES | | 更新时间 | | 13 | update_time | TIMESTAMPTZ | YES | | 更新时间 |
| 14 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 14 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 15 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本生效时间 | | 15 | not_sale | INTEGER | YES | | 是否停售 |
| 16 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本效时间 | | 16 | scd2_start_time | TIMESTAMPTZ | NO | PK | SCD2 版本效时间 |
| 17 | scd2_is_current | INTEGER | YES | | 当前版本标记 | | 17 | scd2_end_time | TIMESTAMPTZ | YES | | SCD2 版本失效时间 |
| 18 | scd2_version | INTEGER | YES | | 版本号 | | 18 | scd2_is_current | INTEGER | YES | | 当前版本标记 |
| 19 | scd2_version | INTEGER | YES | | 版本号 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为 SCD2 维度表版本字段scd2_start_time / scd2_end_time / scd2_is_current / scd2_version。
- 最新版本scd2_is_current = 1
- 按业务主键取最新:按 scd2_start_time 倒序
```sql
-- 取某业务主键的最新版本
SELECT *
FROM billiards_dwd.dim_tenant_goods
WHERE tenant_goods_id = <value>
ORDER BY scd2_start_time DESC
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 查询当前有效的租户商品 -- 查询当前有效的租户商品
SELECT * FROM billiards_dwd.dim_tenant_goods SELECT * FROM billiards_dwd.dim_tenant_goods

View File

@@ -28,16 +28,16 @@
| 9 | site_table_id | BIGINT | YES | | 台桌 ID → dim_table0=非台桌服务) | | 9 | site_table_id | BIGINT | YES | | 台桌 ID → dim_table0=非台桌服务) |
| 10 | tenant_member_id | BIGINT | YES | | 会员 ID → dim_member0=散客) | | 10 | tenant_member_id | BIGINT | YES | | 会员 ID → dim_member0=散客) |
| 11 | system_member_id | BIGINT | YES | | 系统会员 ID0=散客) | | 11 | system_member_id | BIGINT | YES | | 系统会员 ID0=散客) |
| 12 | assistant_no | VARCHAR | YES | | 助教工号。**样本值**: "2", "9"等 | | 12 | assistant_no | VARCHAR(64) | YES | | 助教工号。**样本值**: "2", "9"等 |
| 13 | nickname | VARCHAR | YES | | 助教昵称。**样本值**: "佳怡", "婉婉", "七七"等 | | 13 | nickname | VARCHAR(64) | YES | | 助教昵称。**样本值**: "佳怡", "婉婉", "七七"等 |
| 14 | site_assistant_id | BIGINT | YES | | 助教 ID → dim_assistant | | 14 | site_assistant_id | BIGINT | YES | | 助教 ID → dim_assistant |
| 15 | user_id | BIGINT | YES | | 助教用户 ID | | 15 | user_id | BIGINT | YES | | 助教用户 ID |
| 16 | assistant_team_id | BIGINT | YES | | 助教团队 ID。**枚举值**: 2792011585884037=1组, 2959085810992645=2组 | | 16 | assistant_team_id | BIGINT | YES | | 助教团队 ID。**枚举值**: 2792011585884037=1组, 2959085810992645=2组 |
| 17 | person_org_id | BIGINT | YES | | 人事组织 ID | | 17 | person_org_id | BIGINT | YES | | 人事组织 ID |
| 18 | assistant_level | INTEGER | YES | | 助教等级。**枚举值**: 8=助教管理, 10=初级, 20=中级, 30=高级, 40=星级 | | 18 | assistant_level | INTEGER | YES | | 助教等级。**枚举值**: 8=助教管理, 10=初级, 20=中级, 30=高级, 40=星级 |
| 19 | level_name | VARCHAR | YES | | 等级名称。**枚举值**: "助教管理", "初级", "中级", "高级", "星级" | | 19 | level_name | VARCHAR(64) | YES | | 等级名称。**枚举值**: "助教管理", "初级", "中级", "高级", "星级" |
| 20 | skill_id | BIGINT | YES | | 技能 ID | | 20 | skill_id | BIGINT | YES | | 技能 ID **枚举值**: 2790683529513797 = 基础课 , 2790683529513798 = 附加课/激励课, 3039912271463941 = 包厢课 |
| 21 | skill_name | VARCHAR | YES | | 技能名称。**枚举值**: "基础课", "附加课/激励课", "包厢课" | | 21 | skill_name | VARCHAR(64) | YES | | 技能名称。 **枚举值**: "基础课","附加课","包厢课"|
| 22 | ledger_unit_price | NUMERIC(10,2) | YES | | 单价(元/小时),**样本值**: 98.00/108.00/190.00 等 | | 22 | ledger_unit_price | NUMERIC(10,2) | YES | | 单价(元/小时),**样本值**: 98.00/108.00/190.00 等 |
| 23 | ledger_amount | NUMERIC(10,2) | YES | | 计费金额 | | 23 | ledger_amount | NUMERIC(10,2) | YES | | 计费金额 |
| 24 | projected_income | NUMERIC(10,2) | YES | | 预估收入 | | 24 | projected_income | NUMERIC(10,2) | YES | | 预估收入 |
@@ -49,10 +49,23 @@
| 30 | start_use_time | TIMESTAMPTZ | YES | | 服务开始时间 | | 30 | start_use_time | TIMESTAMPTZ | YES | | 服务开始时间 |
| 31 | last_use_time | TIMESTAMPTZ | YES | | 服务结束时间 | | 31 | last_use_time | TIMESTAMPTZ | YES | | 服务结束时间 |
| 32 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 32 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 33 | real_service_money | NUMERIC(18,2) | YES | | 实际服务费金额 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time, start_use_time, last_use_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_assistant_service_log
ORDER BY create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 统计助教服务收入 -- 统计助教服务收入
SELECT SELECT

View File

@@ -21,16 +21,29 @@
| 2 | site_id | BIGINT | YES | | 门店 ID | | 2 | site_id | BIGINT | YES | | 门店 ID |
| 3 | table_id | BIGINT | YES | | 台桌 ID → dim_table | | 3 | table_id | BIGINT | YES | | 台桌 ID → dim_table |
| 4 | table_area_id | BIGINT | YES | | 台区 ID | | 4 | table_area_id | BIGINT | YES | | 台区 ID |
| 5 | assistant_no | VARCHAR | YES | | 助教工号/昵称。**样本值**: "七七", "乔西", "球球"等 | | 5 | assistant_no | VARCHAR(32) | YES | | 助教工号/昵称。**样本值**: "七七", "乔西", "球球"等 |
| 6 | assistant_name | VARCHAR | YES | | 助教名称,与 assistant_no 相同 | | 6 | assistant_name | VARCHAR(64) | YES | | 助教名称,与 assistant_no 相同 |
| 7 | charge_minutes_raw | INTEGER | YES | | 原计费时长(秒)。**样本值**: 0, 3600=1h, 10800=3h 等 | | 7 | charge_minutes_raw | INTEGER | YES | | 原计费时长(秒)。**样本值**: 0, 3600=1h, 10800=3h 等 |
| 8 | abolish_amount | NUMERIC(18,2) | YES | | 作废金额(元)。**样本值**: 0.00, 190.00, 570.00 等 | | 8 | abolish_amount | NUMERIC(18,2) | YES | | 作废金额(元)。**样本值**: 0.00, 190.00, 570.00 等 |
| 9 | trash_reason | VARCHAR | YES | | 作废原因(当前数据全为 NULL | | 9 | trash_reason | VARCHAR(255) | YES | | 作废原因(当前数据全为 NULL |
| 10 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 10 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 11 | tenant_id | BIGINT | YES | | 租户 ID |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_assistant_trash_event
ORDER BY create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 助教作废金额统计 -- 助教作废金额统计
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -36,14 +35,29 @@
| 16 | ledger_amount | NUMERIC(18,2) | YES | | 账本金额(元)。**样本值**: 48.00, 96.00, 68.00 等 | | 16 | ledger_amount | NUMERIC(18,2) | YES | | 账本金额(元)。**样本值**: 48.00, 96.00, 68.00 等 |
| 17 | coupon_money | NUMERIC(18,2) | YES | | 券面额(元)。**样本值**: 48.00, 116.00, 96.00, 68.00 等 | | 17 | coupon_money | NUMERIC(18,2) | YES | | 券面额(元)。**样本值**: 48.00, 116.00, 96.00, 68.00 等 |
| 18 | promotion_seconds | INTEGER | YES | | 促销时长(秒)。**样本值**: 3600=1h, 7200=2h, 14400=4h 等 | | 18 | promotion_seconds | INTEGER | YES | | 促销时长(秒)。**样本值**: 3600=1h, 7200=2h, 14400=4h 等 |
| 19 | coupon_code | VARCHAR | YES | | 券码 | | 19 | coupon_code | VARCHAR(64) | YES | | 券码 |
| 20 | is_single_order | INTEGER | YES | | 是否独立订单。**枚举值**: 0=否, 1=是 | | 20 | is_single_order | INTEGER | YES | | 是否独立订单。**枚举值**: 0=否, 1=是 |
| 21 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 21 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 22 | ledger_name | VARCHAR | YES | | 套餐名称。**样本值**: "全天A区中八一小时", "中八A区新人特惠一小时" 等 | | 22 | ledger_name | VARCHAR(128) | YES | | 套餐名称。**样本值**: "全天A区中八一小时", "中八A区新人特惠一小时" 等 |
| 23 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 23 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 24 | member_discount_money | NUMERIC(18,2) | YES | | 会员折扣金额 |
| 25 | coupon_sale_id | BIGINT | YES | | 优惠券销售 ID |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_groupbuy_redemption
ORDER BY create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 各套餐核销统计 -- 各套餐核销统计
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -26,9 +25,9 @@
| 6 | system_member_id | BIGINT | YES | | 系统会员 ID | | 6 | system_member_id | BIGINT | YES | | 系统会员 ID |
| 7 | tenant_member_card_id | BIGINT | YES | | 会员卡 ID → dim_member_card_account | | 7 | tenant_member_card_id | BIGINT | YES | | 会员卡 ID → dim_member_card_account |
| 8 | card_type_id | BIGINT | YES | | 卡类型 ID | | 8 | card_type_id | BIGINT | YES | | 卡类型 ID |
| 9 | card_type_name | VARCHAR | YES | | 卡类型名称。**枚举值**: "储值卡", "活动抵用券", "台费卡", "酒水卡", "年卡", "月卡" | | 9 | card_type_name | VARCHAR(32) | YES | | 卡类型名称。**枚举值**: "储值卡", "活动抵用券", "台费卡", "酒水卡", "年卡", "月卡" |
| 10 | member_name | VARCHAR | YES | | 会员名称快照 | | 10 | member_name | VARCHAR(64) | YES | | 会员名称快照 |
| 11 | member_mobile | VARCHAR | YES | | 会员手机号快照 | | 11 | member_mobile | VARCHAR(20) | YES | | 会员手机号快照 |
| 12 | balance_before | NUMERIC(18,2) | YES | | 变动前余额 | | 12 | balance_before | NUMERIC(18,2) | YES | | 变动前余额 |
| 13 | change_amount | NUMERIC(18,2) | YES | | 变动金额(正=充值/赠送,负=消费) | | 13 | change_amount | NUMERIC(18,2) | YES | | 变动金额(正=充值/赠送,负=消费) |
| 14 | balance_after | NUMERIC(18,2) | YES | | 变动后余额 | | 14 | balance_after | NUMERIC(18,2) | YES | | 变动后余额 |
@@ -36,7 +35,10 @@
| 16 | payment_method | INTEGER | YES | | 支付方式,暂未启用。 | | 16 | payment_method | INTEGER | YES | | 支付方式,暂未启用。 |
| 17 | change_time | TIMESTAMPTZ | YES | | 变动时间 | | 17 | change_time | TIMESTAMPTZ | YES | | 变动时间 |
| 18 | is_delete | INTEGER | YES | | 删除标记 | | 18 | is_delete | INTEGER | YES | | 删除标记 |
| 19 | remark | VARCHAR | YES | | 备注。**样本值**: "注销会员", "充值退款" 等 | | 19 | remark | VARCHAR(255) | YES | | 备注。**样本值**: "注销会员", "充值退款" 等 |
| 20 | principal_before | NUMERIC(18,2) | YES | | 变动前本金 |
| 21 | principal_after | NUMERIC(18,2) | YES | | 变动后本金 |
| 22 | principal_change_amount | NUMERIC(18,2) | YES | | 本金变动金额(正=增加,负=减少) |
## 卡类型余额变动分布 ## 卡类型余额变动分布
@@ -58,6 +60,19 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段change_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_member_balance_change
ORDER BY change_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 会员消费总额排行 -- 会员消费总额排行
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -29,11 +28,23 @@
| 9 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 9 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 10 | pay_time | TIMESTAMPTZ | YES | | 支付时间 | | 10 | pay_time | TIMESTAMPTZ | YES | | 支付时间 |
| 11 | pay_date | DATE | YES | | 支付日期 | | 11 | pay_date | DATE | YES | | 支付日期 |
| 12 | tenant_id | BIGINT | YES | | 租户 ID |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time, pay_time, pay_date
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_payment
ORDER BY pay_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 每日支付金额统计 -- 每日支付金额统计
SELECT SELECT

View File

@@ -20,9 +20,9 @@
| 1 | platform_coupon_redemption_id | BIGINT | NO | PK | 核销 ID | | 1 | platform_coupon_redemption_id | BIGINT | NO | PK | 核销 ID |
| 2 | tenant_id | BIGINT | YES | | 租户 ID | | 2 | tenant_id | BIGINT | YES | | 租户 ID |
| 3 | site_id | BIGINT | YES | | 门店 ID | | 3 | site_id | BIGINT | YES | | 门店 ID |
| 4 | coupon_code | VARCHAR | YES | | 券码 | | 4 | coupon_code | VARCHAR(64) | YES | | 券码 |
| 5 | coupon_channel | INTEGER | YES | | 券渠道。**枚举值**: 1=美团, 2=抖音 | | 5 | coupon_channel | INTEGER | YES | | 券渠道。**枚举值**: 1=美团, 2=抖音 |
| 6 | coupon_name | VARCHAR | YES | | 券名称。**样本值**: "【全天可用】中八桌球一小时A区", "【全天可用】中八桌球两小时A区" 等 | | 6 | coupon_name | VARCHAR(200) | YES | | 券名称。**样本值**: "【全天可用】中八桌球一小时A区", "【全天可用】中八桌球两小时A区" 等 |
| 7 | sale_price | NUMERIC(10,2) | YES | | 售卖价(元)。**样本值**: 29.90, 69.90, 59.90, 39.90, 19.90 等 | | 7 | sale_price | NUMERIC(10,2) | YES | | 售卖价(元)。**样本值**: 29.90, 69.90, 59.90, 39.90, 19.90 等 |
| 8 | coupon_money | NUMERIC(10,2) | YES | | 券面额(元)。**样本值**: 48.00, 96.00, 116.00, 68.00 等 | | 8 | coupon_money | NUMERIC(10,2) | YES | | 券面额(元)。**样本值**: 48.00, 96.00, 116.00, 68.00 等 |
| 9 | coupon_free_time | INTEGER | YES | | 券赠送时长(当前数据全为 0 | | 9 | coupon_free_time | INTEGER | YES | | 券赠送时长(当前数据全为 0 |
@@ -31,8 +31,8 @@
| 12 | group_package_id | BIGINT | YES | | 团购套餐 ID当前数据全为 0 | | 12 | group_package_id | BIGINT | YES | | 团购套餐 ID当前数据全为 0 |
| 13 | site_order_id | BIGINT | YES | | 门店订单 ID | | 13 | site_order_id | BIGINT | YES | | 门店订单 ID |
| 14 | table_id | BIGINT | YES | | 台桌 ID → dim_table | | 14 | table_id | BIGINT | YES | | 台桌 ID → dim_table |
| 15 | certificate_id | VARCHAR | YES | | 凭证 ID | | 15 | certificate_id | VARCHAR(64) | YES | | 凭证 ID |
| 16 | verify_id | VARCHAR | YES | | 核验 ID仅抖音券有值 | | 16 | verify_id | VARCHAR(64) | YES | | 核验 ID仅抖音券有值 |
| 17 | use_status | INTEGER | YES | | 使用状态。**枚举值**: 1=已使用, 2=已撤销 | | 17 | use_status | INTEGER | YES | | 使用状态。**枚举值**: 1=已使用, 2=已撤销 |
| 18 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 18 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 19 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 19 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
@@ -40,6 +40,19 @@
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段coupon_free_time, create_time, consume_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_platform_coupon_redemption
ORDER BY create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 各渠道核销统计 -- 各渠道核销统计
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -38,9 +37,21 @@
| 18 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 18 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 19 | pay_time | TIMESTAMPTZ | YES | | 支付时间 | | 19 | pay_time | TIMESTAMPTZ | YES | | 支付时间 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time, pay_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_recharge_order
ORDER BY pay_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 充值总额统计(不含撤销) -- 充值总额统计(不含撤销)
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -31,9 +30,21 @@
| 11 | member_id | BIGINT | YES | | 会员 ID当前数据全为 0 | | 11 | member_id | BIGINT | YES | | 会员 ID当前数据全为 0 |
| 12 | member_card_id | BIGINT | YES | | 会员卡 ID当前数据全为 0 | | 12 | member_card_id | BIGINT | YES | | 会员卡 ID当前数据全为 0 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段pay_time, create_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_refund
ORDER BY pay_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 退款统计 -- 退款统计
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -21,19 +20,19 @@
| 1 | order_settle_id | BIGINT | NO | PK | 结账单 ID | | 1 | order_settle_id | BIGINT | NO | PK | 结账单 ID |
| 2 | tenant_id | BIGINT | YES | | 租户 ID | | 2 | tenant_id | BIGINT | YES | | 租户 ID |
| 3 | site_id | BIGINT | YES | | 门店 ID → dim_site | | 3 | site_id | BIGINT | YES | | 门店 ID → dim_site |
| 4 | site_name | VARCHAR | YES | | 门店名称。**当前值**: "朗朗桌球" | | 4 | site_name | VARCHAR(100) | YES | | 门店名称。**当前值**: "朗朗桌球" |
| 5 | table_id | BIGINT | YES | | 台桌 ID → dim_table0=非台桌订单,如商城订单) | | 5 | table_id | BIGINT | YES | | 台桌 ID → dim_table0=非台桌订单,如商城订单) |
| 6 | settle_name | VARCHAR | YES | | 结账名称。**样本值**: "商城订单", "A区 A3", "A区 A4", "斯诺克区 S1" | | 6 | settle_name | VARCHAR(100) | YES | | 结账名称。**样本值**: "商城订单", "A区 A3", "A区 A4", "斯诺克区 S1" |
| 7 | order_trade_no | BIGINT | YES | | 订单号 | | 7 | order_trade_no | BIGINT | YES | | 订单号 |
| 8 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 8 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 9 | pay_time | TIMESTAMPTZ | YES | | 支付时间 | | 9 | pay_time | TIMESTAMPTZ | YES | | 支付时间 |
| 10 | settle_type | INTEGER | YES | | 结账类型。**枚举值**: 1=台桌结账, 3=商城订单, 6=退货订单, 7=退款订单 | | 10 | settle_type | INTEGER | YES | | 结账类型。**枚举值**: 1=台桌结账, 3=商城订单, 6=退货订单, 7=退款订单 |
| 11 | revoke_order_id | BIGINT | YES | | 撤销订单 ID当前数据全为 0 | | 11 | revoke_order_id | BIGINT | YES | | 撤销订单 ID当前数据全为 0 |
| 12 | member_id | BIGINT | YES | | 会员 ID → dim_member0=散客,占比约 82.8% | | 12 | member_id | BIGINT | YES | | 会员 ID → dim_member0=散客,占比约 82.8% |
| 13 | member_name | VARCHAR | YES | | 会员名称 | | 13 | member_name | VARCHAR(100) | YES | | 会员名称 |
| 14 | member_phone | VARCHAR | YES | | 会员电话 | | 14 | member_phone | VARCHAR(50) | YES | | 会员电话 |
| 15 | member_card_account_id | BIGINT | YES | | 会员卡账户 ID当前数据全为 0 | | 15 | member_card_account_id | BIGINT | YES | | 会员卡账户 ID当前数据全为 0 |
| 16 | member_card_type_name | VARCHAR | YES | | 卡类型名称(当前数据全为空) | | 16 | member_card_type_name | VARCHAR(100) | YES | | 卡类型名称(当前数据全为空) |
| 17 | is_bind_member | BOOLEAN | YES | | 是否绑定会员。**枚举值**: False=否 | | 17 | is_bind_member | BOOLEAN | YES | | 是否绑定会员。**枚举值**: False=否 |
| 18 | member_discount_amount | NUMERIC(18,2) | YES | | 会员折扣金额 | | 18 | member_discount_amount | NUMERIC(18,2) | YES | | 会员折扣金额 |
| 19 | consume_money | NUMERIC(18,2) | YES | | 消费总金额(元) | | 19 | consume_money | NUMERIC(18,2) | YES | | 消费总金额(元) |
@@ -41,7 +40,7 @@
| 21 | goods_money | NUMERIC(18,2) | YES | | 商品金额 | | 21 | goods_money | NUMERIC(18,2) | YES | | 商品金额 |
| 22 | real_goods_money | NUMERIC(18,2) | YES | | 实收商品金额 | | 22 | real_goods_money | NUMERIC(18,2) | YES | | 实收商品金额 |
| 23 | assistant_pd_money | NUMERIC(18,2) | YES | | 助教陪打费用 | | 23 | assistant_pd_money | NUMERIC(18,2) | YES | | 助教陪打费用 |
| 24 | assistant_cx_money | NUMERIC(18,2) | YES | | 助教促销费用 | | 24 | assistant_cx_money | NUMERIC(18,2) | YES | | 助教超休费用 |
| 25 | adjust_amount | NUMERIC(18,2) | YES | | 调整金额 | | 25 | adjust_amount | NUMERIC(18,2) | YES | | 调整金额 |
| 26 | pay_amount | NUMERIC(18,2) | YES | | 实付金额 | | 26 | pay_amount | NUMERIC(18,2) | YES | | 实付金额 |
| 27 | balance_amount | NUMERIC(18,2) | YES | | 余额支付金额 | | 27 | balance_amount | NUMERIC(18,2) | YES | | 余额支付金额 |
@@ -50,10 +49,27 @@
| 30 | coupon_amount | NUMERIC(18,2) | YES | | 券抵扣金额 | | 30 | coupon_amount | NUMERIC(18,2) | YES | | 券抵扣金额 |
| 31 | rounding_amount | NUMERIC(18,2) | YES | | 抹零金额 | | 31 | rounding_amount | NUMERIC(18,2) | YES | | 抹零金额 |
| 32 | point_amount | NUMERIC(18,2) | YES | | 积分抵扣等值金额 | | 32 | point_amount | NUMERIC(18,2) | YES | | 积分抵扣等值金额 |
| 33 | electricity_money | NUMERIC(18,2) | YES | | 电费金额 |
| 34 | real_electricity_money | NUMERIC(18,2) | YES | | 实际电费金额 |
| 35 | electricity_adjust_money | NUMERIC(18,2) | YES | | 电费调整金额 |
| 36 | pl_coupon_sale_amount | NUMERIC(18,2) | YES | | 平台券销售额 |
| 37 | mervou_sales_amount | NUMERIC(18,2) | YES | | 商户券销售额 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time, pay_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_settlement_head
ORDER BY pay_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 每日营收统计 -- 每日营收统计
SELECT SELECT
@@ -64,7 +80,6 @@ SELECT
FROM billiards_dwd.dwd_settlement_head FROM billiards_dwd.dwd_settlement_head
GROUP BY DATE(pay_time) GROUP BY DATE(pay_time)
ORDER BY pay_date DESC; ORDER BY pay_date DESC;
-- 台费 vs 商品 vs 助教收入 -- 台费 vs 商品 vs 助教收入
SELECT SELECT
SUM(table_charge_money) AS table_revenue, SUM(table_charge_money) AS table_revenue,

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -30,8 +29,8 @@
| 10 | tenant_goods_category_id | BIGINT | YES | | 商品分类 ID | | 10 | tenant_goods_category_id | BIGINT | YES | | 商品分类 ID |
| 11 | tenant_goods_business_id | BIGINT | YES | | 业务大类 ID | | 11 | tenant_goods_business_id | BIGINT | YES | | 业务大类 ID |
| 12 | site_table_id | BIGINT | YES | | 台桌 ID0=商城订单,非台桌消费) | | 12 | site_table_id | BIGINT | YES | | 台桌 ID0=商城订单,非台桌消费) |
| 13 | ledger_name | VARCHAR | YES | | 商品名称。**样本值**: "哇哈哈矿泉水", "东方树叶", "可乐" 等 | | 13 | ledger_name | VARCHAR(200) | YES | | 商品名称。**样本值**: "哇哈哈矿泉水", "东方树叶", "可乐" 等 |
| 14 | ledger_group_name | VARCHAR | YES | | 商品分类。**样本值**: "酒水", "零食", "香烟" 等 | | 14 | ledger_group_name | VARCHAR(100) | YES | | 商品分类。**样本值**: "酒水", "零食", "香烟" 等 |
| 15 | ledger_unit_price | NUMERIC(18,2) | YES | | 单价(元) | | 15 | ledger_unit_price | NUMERIC(18,2) | YES | | 单价(元) |
| 16 | ledger_count | INTEGER | YES | | 购买数量。**样本值**: 1, 2, 3, 4 等 | | 16 | ledger_count | INTEGER | YES | | 购买数量。**样本值**: 1, 2, 3, 4 等 |
| 17 | ledger_amount | NUMERIC(18,2) | YES | | 销售金额(元) | | 17 | ledger_amount | NUMERIC(18,2) | YES | | 销售金额(元) |
@@ -41,10 +40,23 @@
| 21 | ledger_status | INTEGER | YES | | 账本状态。**枚举值**: 1=已结算 | | 21 | ledger_status | INTEGER | YES | | 账本状态。**枚举值**: 1=已结算 |
| 22 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 22 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 23 | create_time | TIMESTAMPTZ | YES | | 创建时间 | | 23 | create_time | TIMESTAMPTZ | YES | | 创建时间 |
| 24 | coupon_share_money | NUMERIC(18,2) | YES | | 优惠券分摊金额 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段create_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_store_goods_sale
ORDER BY create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 热销商品排行 -- 热销商品排行
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -25,14 +24,31 @@
| 5 | site_id | BIGINT | YES | | 门店 ID | | 5 | site_id | BIGINT | YES | | 门店 ID |
| 6 | table_id | BIGINT | YES | | 台桌 ID → dim_table | | 6 | table_id | BIGINT | YES | | 台桌 ID → dim_table |
| 7 | table_area_id | BIGINT | YES | | 台区 ID | | 7 | table_area_id | BIGINT | YES | | 台区 ID |
| 8 | table_area_name | VARCHAR | YES | | 台区名称(当前数据全为 NULL | | 8 | table_area_name | VARCHAR(64) | YES | | 台区名称(当前数据全为 NULL |
| 9 | tenant_table_area_id | BIGINT | YES | | 租户台区 ID | | 9 | tenant_table_area_id | BIGINT | YES | | 租户台区 ID |
| 10 | ledger_amount | NUMERIC(18,2) | YES | | 调整金额(元) | | 10 | ledger_amount | NUMERIC(18,2) | YES | | 调整金额(元) |
| 11 | ledger_status | INTEGER | YES | | 账本状态。**枚举值**: 0=待确认, 1=已确认 | | 11 | ledger_status | INTEGER | YES | | 账本状态。**枚举值**: 0=待确认, 1=已确认 |
| 12 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 12 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 13 | table_name | TEXT | YES | | 台桌名称 |
| 14 | table_price | NUMERIC(18,2) | YES | | 台桌价格 |
| 15 | charge_free | BOOLEAN | YES | | 是否免费 |
| 16 | adjust_time | TIMESTAMPTZ | YES | | 调整时间 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段adjust_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_table_fee_adjust
ORDER BY adjust_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 台费调整统计 -- 台费调整统计
SELECT SELECT

View File

@@ -2,7 +2,6 @@
> 生成时间2026-01-28 > 生成时间2026-01-28
## 表信息 ## 表信息
| 属性 | 值 | | 属性 | 值 |
@@ -26,10 +25,10 @@
| 6 | site_id | BIGINT | YES | | 门店 ID | | 6 | site_id | BIGINT | YES | | 门店 ID |
| 7 | site_table_id | BIGINT | YES | | 台桌 ID → dim_table | | 7 | site_table_id | BIGINT | YES | | 台桌 ID → dim_table |
| 8 | site_table_area_id | BIGINT | YES | | 台区 ID | | 8 | site_table_area_id | BIGINT | YES | | 台区 ID |
| 9 | site_table_area_name | VARCHAR | YES | | 台区名称。**枚举值**: "A区", "B区", "斯诺克区", "麻将房", "C区", "补时长", "VIP包厢" 等 | | 9 | site_table_area_name | VARCHAR(64) | YES | | 台区名称。**枚举值**: "A区", "B区", "斯诺克区", "麻将房", "C区", "补时长", "VIP包厢" 等 |
| 10 | tenant_table_area_id | BIGINT | YES | | 租户级台区 ID | | 10 | tenant_table_area_id | BIGINT | YES | | 租户级台区 ID |
| 11 | member_id | BIGINT | YES | | 会员 ID0=散客,占比约 82.4% | | 11 | member_id | BIGINT | YES | | 会员 ID0=散客,占比约 82.4% |
| 12 | ledger_name | VARCHAR | YES | | 台桌名称。**样本值**: "A3", "A5", "A4", "S1", "B5", "M3" 等 | | 12 | ledger_name | VARCHAR(64) | YES | | 台桌名称。**样本值**: "A3", "A5", "A4", "S1", "B5", "M3" 等 |
| 13 | ledger_unit_price | NUMERIC(18,2) | YES | | 单价(元/小时),如 48.00/58.00/68.00 | | 13 | ledger_unit_price | NUMERIC(18,2) | YES | | 单价(元/小时),如 48.00/58.00/68.00 |
| 14 | ledger_count | INTEGER | YES | | 计费时长(秒)。**样本值**: 3600=1h, 7200=2h, 10800=3h 等 | | 14 | ledger_count | INTEGER | YES | | 计费时长(秒)。**样本值**: 3600=1h, 7200=2h, 10800=3h 等 |
| 15 | ledger_amount | NUMERIC(18,2) | YES | | 计费金额(元) | | 15 | ledger_amount | NUMERIC(18,2) | YES | | 计费金额(元) |
@@ -45,10 +44,24 @@
| 25 | ledger_status | INTEGER | YES | | 账本状态。**枚举值**: 1=已结算 | | 25 | ledger_status | INTEGER | YES | | 账本状态。**枚举值**: 1=已结算 |
| 26 | is_single_order | INTEGER | YES | | 是否独立订单。**枚举值**: 0=合并订单, 1=独立订单 | | 26 | is_single_order | INTEGER | YES | | 是否独立订单。**枚举值**: 0=合并订单, 1=独立订单 |
| 27 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 | | 27 | is_delete | INTEGER | YES | | 删除标记。**枚举值**: 0=未删除 |
| 28 | activity_discount_amount | NUMERIC(18,2) | YES | | 活动折扣金额 |
| 29 | real_service_money | NUMERIC(18,2) | YES | | 实际服务费金额 |
## 使用说明 ## 使用说明
**版本与最新值**
本表为事实表,无 SCD2 版本字段。
- 可用时间字段start_use_time, ledger_end_time, create_time
```sql
-- 取最新一条(按时间字段倒序)
SELECT *
FROM billiards_dwd.dwd_table_fee_log
ORDER BY create_time DESC NULLS LAST
LIMIT 1;
```
**使用示例**
```sql ```sql
-- 各台区台费收入统计 -- 各台区台费收入统计
SELECT SELECT

View File

@@ -0,0 +1,74 @@
# cfg_area_category 台区分类映射表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | cfg_area_category |
| 主键 | category_id |
| 数据来源 | 手工维护/seed脚本基于dim_table实际数据 |
| 说明 | 将dim_table.site_table_area_name映射到财务报表区域分类 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 主键 | 说明 |
|------|--------|------|------|------|------|
| 1 | category_id | SERIAL | NO | PK | 分类ID自增 |
| 2 | source_area_name | VARCHAR(100) | NO | UK | 源区域名称来自dim_table.site_table_area_name |
| 3 | category_code | VARCHAR(20) | NO | | 分类代码。**枚举值**: BILLIARD, BILLIARD_VIP, SNOOKER, MAHJONG, KTV, SPECIAL, OTHER |
| 4 | category_name | VARCHAR(50) | NO | | 分类名称 |
| 5 | match_type | VARCHAR(10) | NO | | 匹配类型。**枚举值**: EXACT精确, LIKE模糊, DEFAULT兜底 |
| 6 | match_priority | INTEGER | NO | | 匹配优先级(数字越小优先级越高) |
| 7 | is_active | BOOLEAN | NO | | 是否启用 |
| 8 | description | TEXT | YES | | 说明 |
| 9 | created_at | TIMESTAMPTZ | NO | | 创建时间 |
| 10 | updated_at | TIMESTAMPTZ | NO | | 更新时间 |
## 分类映射示例
| 源区域名称 | 分类代码 | 分类名称 |
|------------|----------|----------|
| A区 | BILLIARD | 台球散台 |
| B区 | BILLIARD | 台球散台 |
| C区 | BILLIARD | 台球散台 |
| TV台 | BILLIARD | 台球散台 |
| VIP包厢 | BILLIARD_VIP | 台球VIP |
| 斯诺克区 | SNOOKER | 斯诺克 |
| 麻将房 | MAHJONG | 麻将棋牌 |
| M7 | MAHJONG | 麻将棋牌 |
| M8 | MAHJONG | 麻将棋牌 |
| 666 | MAHJONG | 麻将棋牌 |
| 发财 | MAHJONG | 麻将棋牌 |
| K包 | KTV | K歌娱乐 |
| k包活动区 | KTV | K歌娱乐 |
| 幸会158 | KTV | K歌娱乐 |
| 补时长 | SPECIAL | 补时长 |
## 使用说明
**取值方式**
```sql
-- 将台区名称映射到分类
SELECT
dt.site_table_area_name,
COALESCE(ac.category_code, 'OTHER') AS category_code,
COALESCE(ac.category_name, '其他') AS category_name
FROM billiards_dwd.dim_table dt
LEFT JOIN billiards_dws.cfg_area_category ac
ON dt.site_table_area_name = ac.source_area_name
AND ac.is_active = TRUE
WHERE dt.scd2_is_current = 1;
-- 按分类汇总收入
SELECT
COALESCE(ac.category_name, '其他') AS category_name,
SUM(tfl.ledger_amount) AS total_amount
FROM billiards_dwd.dwd_table_fee_log tfl
LEFT JOIN billiards_dwd.dim_table dt ON dt.table_id = tfl.site_table_id
LEFT JOIN billiards_dws.cfg_area_category ac ON dt.site_table_area_name = ac.source_area_name
GROUP BY COALESCE(ac.category_name, '其他');
```

View File

@@ -0,0 +1,56 @@
# cfg_assistant_level_price 助教等级定价表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | cfg_assistant_level_price |
| 主键 | price_id |
| 数据来源 | 手工维护/seed脚本 |
| 说明 | 助教等级对应的基础课和附加课单价配置 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 主键 | 说明 |
|------|--------|------|------|------|------|
| 1 | price_id | SERIAL | NO | PK | 定价ID自增 |
| 2 | level_code | INTEGER | NO | | 等级代码。**枚举值**: 8=助教管理, 10=初级, 20=中级, 30=高级, 40=星级 |
| 3 | level_name | VARCHAR(20) | NO | | 等级名称 |
| 4 | base_course_price | NUMERIC(10,2) | NO | | 基础课单价(元/小时) |
| 5 | bonus_course_price | NUMERIC(10,2) | NO | | 附加课单价(元/小时固定50元 |
| 6 | effective_from | DATE | NO | | 生效起始日期(含) |
| 7 | effective_to | DATE | NO | | 生效截止日期(含) |
| 8 | description | TEXT | YES | | 说明 |
| 9 | created_at | TIMESTAMPTZ | NO | | 创建时间 |
| 10 | updated_at | TIMESTAMPTZ | NO | | 更新时间 |
## 定价配置示例
| 等级代码 | 等级名称 | 基础课单价 | 附加课单价 |
|----------|----------|------------|------------|
| 8 | 助教管理 | 98元/小时 | 50元/小时 |
| 10 | 初级 | 98元/小时 | 50元/小时 |
| 20 | 中级 | 108元/小时 | 50元/小时 |
| 30 | 高级 | 118元/小时 | 50元/小时 |
| 40 | 星级 | 138元/小时 | 50元/小时 |
## 使用说明
**取值方式**
SCD2口径助教等级来自dim_assistant取数时需按有效期as-of join
```sql
-- 获取助教在指定日期的等级定价
SELECT p.*
FROM billiards_dws.cfg_assistant_level_price p
JOIN billiards_dwd.dim_assistant a ON p.level_code = a.level
WHERE a.assistant_id = 123
AND a.scd2_start_time <= '2026-01-15'
AND (a.scd2_end_time IS NULL OR a.scd2_end_time > '2026-01-15')
AND p.effective_from <= '2026-01-15'
AND p.effective_to >= '2026-01-15';
```

View File

@@ -0,0 +1,73 @@
# cfg_bonus_rules 奖金规则配置表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | cfg_bonus_rules |
| 主键 | rule_id |
| 数据来源 | 手工维护/seed脚本 |
| 说明 | 冲刺奖金按小时阈值和Top3排名奖金配置 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 主键 | 说明 |
|------|--------|------|------|------|------|
| 1 | rule_id | SERIAL | NO | PK | 规则ID自增 |
| 2 | rule_type | VARCHAR(20) | NO | | 规则类型。**枚举值**: SPRINT冲刺奖金, TOP_RANKTop排名奖金 |
| 3 | rule_code | VARCHAR(30) | NO | | 规则代码。**枚举值**: SPRINT_190, SPRINT_220, TOP_1, TOP_2, TOP_3 |
| 4 | rule_name | VARCHAR(50) | NO | | 规则名称 |
| 5 | threshold_hours | NUMERIC(10,2) | YES | | 小时数阈值(冲刺奖金用) |
| 6 | rank_position | INTEGER | YES | | 排名位置Top奖金用 |
| 7 | bonus_amount | NUMERIC(12,2) | NO | | 奖金金额(元) |
| 8 | is_cumulative | BOOLEAN | NO | | 是否可累计冲刺奖金为FALSE取最高档 |
| 9 | priority | INTEGER | NO | | 优先级(数字越大优先级越高) |
| 10 | effective_from | DATE | NO | | 生效起始日期(含) |
| 11 | effective_to | DATE | NO | | 生效截止日期(含) |
| 12 | description | TEXT | YES | | 说明 |
| 13 | created_at | TIMESTAMPTZ | NO | | 创建时间 |
| 14 | updated_at | TIMESTAMPTZ | NO | | 更新时间 |
## 奖金规则示例
### 冲刺奖金(不累计,取最高档)
| 规则代码 | 小时阈值 | 奖金金额 | 优先级 |
|----------|----------|----------|--------|
| SPRINT_190 | 190小时 | 300元 | 1 |
| SPRINT_220 | 220小时 | 800元 | 2 |
### Top3排名奖金独立发放
| 规则代码 | 排名 | 奖金金额 |
|----------|------|----------|
| TOP_1 | 第1名 | 1000元 |
| TOP_2 | 第2名 | 600元 |
| TOP_3 | 第3名 | 400元 |
## 使用说明
**取值方式**
```sql
-- 获取冲刺奖金(取最高档)
SELECT * FROM billiards_dws.cfg_bonus_rules
WHERE rule_type = 'SPRINT'
AND threshold_hours <= 200 -- 实际小时数
AND effective_from <= '2026-01-01'
AND effective_to >= '2026-01-01'
ORDER BY priority DESC
LIMIT 1;
-- 获取Top3排名奖金
SELECT * FROM billiards_dws.cfg_bonus_rules
WHERE rule_type = 'TOP_RANK'
AND rank_position = 1 -- 排名
AND effective_from <= '2026-01-01'
AND effective_to >= '2026-01-01';
```
**排名口径说明**
- Top3排名按有效业绩小时数effective_hours降序排列
- 如遇并列则都算如2个第一则记为2个第一下一个是第三

View File

@@ -0,0 +1,71 @@
# cfg_performance_tier 绩效档位配置表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | cfg_performance_tier |
| 主键 | tier_id |
| 数据来源 | 手工维护/seed脚本 |
| 说明 | 助教绩效档位配置包含6档阈值、抽成比例、假期天数 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 主键 | 说明 |
|------|--------|------|------|------|------|
| 1 | tier_id | SERIAL | NO | PK | 档位ID自增 |
| 2 | tier_code | VARCHAR(20) | NO | | 档位代码。**枚举值**: T0, T1, T2, T3, T4, T5, NEW |
| 3 | tier_name | VARCHAR(50) | NO | | 档位名称 |
| 4 | tier_level | INTEGER | NO | | 档位等级(数字越大档位越高) |
| 5 | min_hours | NUMERIC(10,2) | NO | | 最低业绩小时数阈值(>= |
| 6 | max_hours | NUMERIC(10,2) | YES | | 最高业绩小时数阈值(<NULL表示无上限 |
| 7 | base_deduction | NUMERIC(10,2) | NO | | 专业课抽成(元/小时),球房从基础课扣除 |
| 8 | bonus_deduction_ratio | NUMERIC(5,4) | NO | | 打赏课抽成比例0-1 |
| 9 | vacation_days | INTEGER | NO | | 次月可休假天数 |
| 10 | vacation_unlimited | BOOLEAN | NO | | 是否休假自由5档为TRUE |
| 11 | is_new_hire_tier | BOOLEAN | NO | | 是否为新入职专用档位 |
| 12 | effective_from | DATE | NO | | 生效起始日期(含) |
| 13 | effective_to | DATE | NO | | 生效截止日期(含) |
| 14 | description | TEXT | YES | | 档位说明 |
| 15 | created_at | TIMESTAMPTZ | NO | | 创建时间 |
| 16 | updated_at | TIMESTAMPTZ | NO | | 更新时间 |
## 档位配置示例
| 档位代码 | 档位名称 | 小时数范围 | 专业课抽成 | 打赏课抽成 | 假期 |
|----------|----------|------------|------------|------------|------|
| T0 | 0档 | 0-100 | 23元/小时 | 45% | 4天 |
| T1 | 1档 | 100-130 | 20元/小时 | 42% | 5天 |
| T2 | 2档 | 130-160 | 17元/小时 | 40% | 6天 |
| T3 | 3档 | 160-190 | 13元/小时 | 35% | 7天 |
| T4 | 4档 | 190-230 | 8元/小时 | 30% | 8天 |
| T5 | 5档 | 230+ | 0元/小时 | 0% | 自由 |
| NEW | 新入职 | 任意 | 23元/小时 | 45% | 4天 |
## 使用说明
**取值方式**
按月份匹配生效的配置:
```sql
-- 获取指定月份的档位配置
SELECT * FROM billiards_dws.cfg_performance_tier
WHERE effective_from <= '2026-01-01'
AND effective_to >= '2026-01-01'
ORDER BY min_hours;
-- 根据有效业绩小时数匹配档位
SELECT * FROM billiards_dws.cfg_performance_tier
WHERE effective_from <= '2026-01-01'
AND effective_to >= '2026-01-01'
AND min_hours <= 185 -- 有效小时数
AND (max_hours IS NULL OR max_hours > 185)
LIMIT 1;
```
**薪资计算公式**
- 基础课收入 = 基础课小时数 × (客户支付价格 - base_deduction)
- 附加课收入 = 附加课小时数 × 190 × (1 - bonus_deduction_ratio)

View File

@@ -0,0 +1,62 @@
# cfg_skill_type 技能→课程类型映射表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | cfg_skill_type |
| 主键 | skill_type_id |
| 数据来源 | 手工维护/seed脚本 |
| 说明 | 将skill_id映射到课程类型基础课/附加课避免依赖skill_name文本匹配 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 主键 | 说明 |
|------|--------|------|------|------|------|
| 1 | skill_type_id | SERIAL | NO | PK | 映射ID自增 |
| 2 | skill_id | BIGINT | NO | UK | 技能ID来自dwd_assistant_service_log.skill_id |
| 3 | skill_name | VARCHAR(50) | YES | | 技能名称(仅用于展示和校验) |
| 4 | course_type_code | VARCHAR(10) | NO | | 课程类型代码。**枚举值**: BASE基础课, BONUS附加课 |
| 5 | course_type_name | VARCHAR(20) | NO | | 课程类型名称 |
| 6 | is_active | BOOLEAN | NO | | 是否启用 |
| 7 | description | TEXT | YES | | 说明 |
| 8 | created_at | TIMESTAMPTZ | NO | | 创建时间 |
| 9 | updated_at | TIMESTAMPTZ | NO | | 更新时间 |
## 技能映射示例
| skill_id | skill_name | 课程类型代码 | 课程类型名称 |
|----------|------------|--------------|--------------|
| 2791903611396869 | 陪打/PD | BASE | 基础课 |
| 2807440316432197 | 超休/CX | BONUS | 附加课 |
## 使用说明
**取值方式**
```sql
-- 将服务记录分类为基础课/附加课
SELECT
asl.*,
COALESCE(st.course_type_code, 'BASE') AS course_type_code,
COALESCE(st.course_type_name, '基础课') AS course_type_name
FROM billiards_dwd.dwd_assistant_service_log asl
LEFT JOIN billiards_dws.cfg_skill_type st
ON asl.skill_id = st.skill_id
AND st.is_active = TRUE;
-- 按课程类型汇总小时数
SELECT
COALESCE(st.course_type_code, 'BASE') AS course_type,
SUM(asl.income_seconds) / 3600.0 AS total_hours
FROM billiards_dwd.dwd_assistant_service_log asl
LEFT JOIN billiards_dws.cfg_skill_type st ON asl.skill_id = st.skill_id
GROUP BY COALESCE(st.course_type_code, 'BASE');
```
**说明**
- 基础课(陪打/PD: 按等级定价客户支付98-138元/小时
- 附加课(超休/CX: 固定客户支付190元/小时助教收入50元/小时

View File

@@ -0,0 +1,98 @@
# dws_assistant_customer_stats 助教服务客户统计表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_assistant_customer_stats |
| 主键 | id |
| 唯一键 | (site_id, assistant_id, member_id, stat_date) |
| 数据来源 | dwd_assistant_service_log |
| 更新频率 | 每日更新 |
| 说明 | 以"助教+客户"为粒度,统计服务关系和滚动窗口指标 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | assistant_id | BIGINT | NO | 助教ID |
| 5 | assistant_nickname | VARCHAR(50) | YES | 助教花名 |
| 6 | member_id | BIGINT | NO | 客户IDmember_id=0散客不入此表 |
| 7 | member_nickname | VARCHAR(100) | YES | 客户昵称 |
| 8 | member_mobile | VARCHAR(20) | YES | 客户手机号(脱敏) |
| 9 | stat_date | DATE | NO | 统计基准日期 |
| 10 | first_service_date | DATE | YES | 首次服务日期 |
| 11 | last_service_date | DATE | YES | 最近服务日期 |
| 12 | total_service_count | INTEGER | NO | 累计服务次数 |
| 13 | total_service_hours | NUMERIC(10,2) | NO | 累计服务小时数 |
| 14 | total_service_amount | NUMERIC(12,2) | NO | 累计服务金额 |
| 15-20 | service_count_7d/10d/15d/30d/60d/90d | INTEGER | NO | 近N天服务次数 |
| 21-26 | service_hours_7d/10d/15d/30d/60d/90d | NUMERIC(10,2) | NO | 近N天服务小时数 |
| 27-32 | service_amount_7d/10d/15d/30d/60d/90d | NUMERIC(12,2) | NO | 近N天服务金额 |
| 33 | days_since_last | INTEGER | YES | 距离最近服务的天数 |
| 34 | is_active_7d | BOOLEAN | NO | 近7天是否活跃 |
| 35 | is_active_30d | BOOLEAN | NO | 近30天是否活跃 |
| 36 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 37 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 滚动窗口计算
```sql
-- 统计每个助教-客户组合的滚动窗口指标
WITH service_data AS (
SELECT
site_id,
site_assistant_id AS assistant_id,
tenant_member_id AS member_id,
DATE(create_time) AS service_date,
COUNT(*) AS service_count,
SUM(income_seconds) / 3600.0 AS service_hours,
SUM(ledger_amount) AS service_amount
FROM billiards_dwd.dwd_assistant_service_log
WHERE is_delete = 0
AND tenant_member_id != 0 -- 排除散客
GROUP BY site_id, site_assistant_id, tenant_member_id, DATE(create_time)
)
SELECT
assistant_id,
member_id,
:stat_date AS stat_date,
MIN(service_date) AS first_service_date,
MAX(service_date) AS last_service_date,
SUM(service_count) AS total_service_count,
SUM(CASE WHEN service_date >= :stat_date - 6 THEN service_count ELSE 0 END) AS service_count_7d,
SUM(CASE WHEN service_date >= :stat_date - 29 THEN service_count ELSE 0 END) AS service_count_30d,
-- ... 其他窗口
FROM service_data
GROUP BY assistant_id, member_id;
```
## 使用说明
**散客处理**
- member_id=0 的散客不进入此表统计
- 仅统计有会员身份的客户
**活跃度判断**
```sql
-- 近7天活跃 = 近7天有服务记录
is_active_7d = (service_count_7d > 0)
-- 近30天活跃 = 近30天有服务记录
is_active_30d = (service_count_30d > 0)
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-21 ~ 至今 |
| 依赖表 | dwd_assistant_service_log, dim_member |
| 注意事项 | 滚动窗口需要足够的历史数据支撑 |

View File

@@ -0,0 +1,109 @@
# dws_assistant_daily_detail 助教日度业绩明细表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_assistant_daily_detail |
| 主键 | id |
| 唯一键 | (site_id, assistant_id, stat_date) |
| 数据来源 | dwd_assistant_service_log + dwd_assistant_trash_event |
| 更新频率 | 每小时增量更新 |
| 说明 | 以"助教+日期"为粒度,汇总每日业绩明细 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | assistant_id | BIGINT | NO | 助教IDdim_assistant.assistant_id |
| 5 | assistant_nickname | VARCHAR(50) | YES | 助教花名(冗余,便于查询展示) |
| 6 | stat_date | DATE | NO | 统计日期 |
| 7 | assistant_level_code | INTEGER | YES | 助教等级代码SCD2口径取stat_date当日生效的等级 |
| 8 | assistant_level_name | VARCHAR(20) | YES | 助教等级名称 |
| 9 | total_service_count | INTEGER | NO | 总服务次数 |
| 10 | base_service_count | INTEGER | NO | 基础课服务次数 |
| 11 | bonus_service_count | INTEGER | NO | 附加课服务次数 |
| 12 | total_seconds | INTEGER | NO | 总计费时长(秒) |
| 13 | base_seconds | INTEGER | NO | 基础课计费时长(秒) |
| 14 | bonus_seconds | INTEGER | NO | 附加课计费时长(秒) |
| 15 | total_hours | NUMERIC(10,2) | NO | 总计费小时数 |
| 16 | base_hours | NUMERIC(10,2) | NO | 基础课小时数 |
| 17 | bonus_hours | NUMERIC(10,2) | NO | 附加课小时数 |
| 18 | total_ledger_amount | NUMERIC(12,2) | NO | 总计费金额(元) |
| 19 | base_ledger_amount | NUMERIC(12,2) | NO | 基础课计费金额 |
| 20 | bonus_ledger_amount | NUMERIC(12,2) | NO | 附加课计费金额 |
| 21 | unique_customers | INTEGER | NO | 服务客户数(去重) |
| 22 | unique_tables | INTEGER | NO | 服务台桌数(去重) |
| 23 | trashed_seconds | INTEGER | NO | 被废除的服务时长(秒) |
| 24 | trashed_count | INTEGER | NO | 被废除的服务次数 |
| 25 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 26 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 主要来源dwd_assistant_service_log
```sql
SELECT
site_id,
DATE(create_time) AS stat_date,
site_assistant_id AS assistant_id,
nickname AS assistant_nickname,
COUNT(*) AS total_service_count,
SUM(income_seconds) AS total_seconds,
SUM(ledger_amount) AS total_ledger_amount,
COUNT(DISTINCT tenant_member_id) AS unique_customers,
COUNT(DISTINCT site_table_id) AS unique_tables
FROM billiards_dwd.dwd_assistant_service_log
WHERE is_delete = 0
GROUP BY site_id, DATE(create_time), site_assistant_id, nickname;
```
### 废除记录dwd_assistant_trash_event
```sql
SELECT
site_id,
DATE(create_time) AS stat_date,
assistant_no,
assistant_name,
SUM(charge_minutes_raw * 60) AS trashed_seconds,
COUNT(*) AS trashed_count
FROM billiards_dwd.dwd_assistant_trash_event
GROUP BY site_id, DATE(create_time), assistant_no, assistant_name;
```
## 使用说明
**时间分层查询**
```sql
-- 近2天
SELECT * FROM billiards_dws.dws_assistant_daily_detail
WHERE stat_date >= CURRENT_DATE - 1;
-- 近1月
SELECT * FROM billiards_dws.dws_assistant_daily_detail
WHERE stat_date >= CURRENT_DATE - INTERVAL '1 month';
-- 月度汇总
SELECT
assistant_id,
DATE_TRUNC('month', stat_date) AS stat_month,
SUM(total_hours) AS total_hours,
SUM(base_hours) AS base_hours,
SUM(bonus_hours) AS bonus_hours
FROM billiards_dws.dws_assistant_daily_detail
GROUP BY assistant_id, DATE_TRUNC('month', stat_date);
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-21 ~ 至今 |
| 依赖表 | dwd_assistant_service_log, dwd_assistant_trash_event, dim_assistant |

View File

@@ -0,0 +1,88 @@
# dws_assistant_finance_analysis 助教收支分析表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_assistant_finance_analysis |
| 主键 | id |
| 唯一键 | (site_id, stat_date, assistant_id) |
| 数据来源 | dwd_assistant_service_log + dws_assistant_salary_calc |
| 更新频率 | 每日更新 |
| 说明 | 以"日期+助教"为粒度,分析助教产出的收入和成本 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | stat_date | DATE | NO | 统计日期 |
| 5 | assistant_id | BIGINT | NO | 助教ID |
| 6 | assistant_nickname | VARCHAR(50) | YES | 助教花名 |
| 7 | revenue_total | NUMERIC(14,2) | NO | 助教产出收入ledger_amount汇总 |
| 8 | revenue_base | NUMERIC(14,2) | NO | 基础课收入 |
| 9 | revenue_bonus | NUMERIC(14,2) | NO | 附加课收入 |
| 10 | cost_daily | NUMERIC(14,2) | NO | 日均工资成本(月工资/工作天数) |
| 11 | gross_profit | NUMERIC(14,2) | NO | 毛利 = 收入 - 成本 |
| 12 | gross_margin | NUMERIC(5,4) | NO | 毛利率 |
| 13 | service_count | INTEGER | NO | 服务次数 |
| 14 | service_hours | NUMERIC(10,2) | NO | 服务小时数 |
| 15 | unique_customers | INTEGER | NO | 服务客户数 |
| 16 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 17 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 收入来源dwd_assistant_service_log
```sql
SELECT
DATE(create_time) AS stat_date,
site_assistant_id AS assistant_id,
SUM(ledger_amount) AS revenue_total,
SUM(CASE WHEN skill_id = 2791903611396869 THEN ledger_amount ELSE 0 END) AS revenue_base,
SUM(CASE WHEN skill_id = 2807440316432197 THEN ledger_amount ELSE 0 END) AS revenue_bonus,
COUNT(*) AS service_count,
SUM(income_seconds) / 3600.0 AS service_hours,
COUNT(DISTINCT tenant_member_id) AS unique_customers
FROM billiards_dwd.dwd_assistant_service_log
WHERE is_delete = 0
GROUP BY DATE(create_time), site_assistant_id;
```
### 成本来源dws_assistant_salary_calc
```sql
-- 日均成本 = 月度应发工资 / 当月工作天数
SELECT
assistant_id,
salary_month,
gross_salary / NULLIF(work_days, 0) AS cost_daily
FROM billiards_dws.dws_assistant_salary_calc sc
JOIN billiards_dws.dws_assistant_monthly_summary ms
ON sc.assistant_id = ms.assistant_id AND sc.salary_month = ms.stat_month;
```
## 使用说明
**毛利计算**
```
gross_profit = revenue_total - cost_daily
gross_margin = gross_profit / NULLIF(revenue_total, 0)
```
**注意事项**
- cost_daily 基于月度工资分摊,非实际日薪
- 当月数据在月末工资计算前 cost_daily 可能不准确
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ⚠️ 部分可回溯 |
| 数据范围 | 2025-07-21 ~ 至今 |
| 依赖表 | dwd_assistant_service_log, dws_assistant_salary_calc |
| 限制 | cost_daily 依赖 salary_calc需先完成薪资计算 |

View File

@@ -0,0 +1,110 @@
# dws_assistant_monthly_summary 助教月度业绩汇总表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_assistant_monthly_summary |
| 主键 | id |
| 唯一键 | (site_id, assistant_id, stat_month) |
| 数据来源 | dws_assistant_daily_detail 聚合 + cfg_performance_tier |
| 更新频率 | 每日更新当月数据 |
| 说明 | 以"助教+月份"为粒度,汇总月度业绩及档位计算 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | assistant_id | BIGINT | NO | 助教ID |
| 5 | assistant_nickname | VARCHAR(50) | YES | 助教花名 |
| 6 | stat_month | DATE | NO | 统计月份月第一天如2026-01-01 |
| 7 | assistant_level_code | INTEGER | YES | 助教等级代码(月末时点) |
| 8 | assistant_level_name | VARCHAR(20) | YES | 助教等级名称 |
| 9 | hire_date | DATE | YES | 入职日期 |
| 10 | is_new_hire | BOOLEAN | NO | 是否新入职(入职日期 >= 月1日0点 |
| 11 | work_days | INTEGER | NO | 有服务天数 |
| 12 | total_service_count | INTEGER | NO | 总服务次数 |
| 13 | base_service_count | INTEGER | NO | 基础课服务次数 |
| 14 | bonus_service_count | INTEGER | NO | 附加课服务次数 |
| 15 | total_hours | NUMERIC(10,2) | NO | 总计费小时数 |
| 16 | base_hours | NUMERIC(10,2) | NO | 基础课小时数 |
| 17 | bonus_hours | NUMERIC(10,2) | NO | 附加课小时数 |
| 18 | effective_hours | NUMERIC(10,2) | NO | 有效业绩小时数(影响档位)= total_hours - trashed_hours |
| 19 | trashed_hours | NUMERIC(10,2) | NO | 被废除小时数 |
| 20 | total_ledger_amount | NUMERIC(12,2) | NO | 总计费金额 |
| 21 | base_ledger_amount | NUMERIC(12,2) | NO | 基础课计费金额 |
| 22 | bonus_ledger_amount | NUMERIC(12,2) | NO | 附加课计费金额 |
| 23 | unique_customers | INTEGER | NO | 月度服务客户数(去重) |
| 24 | unique_tables | INTEGER | NO | 月度服务台桌数(去重) |
| 25 | avg_service_seconds | NUMERIC(10,2) | NO | 平均单次服务时长(秒) |
| 26 | tier_id | INTEGER | YES | 匹配的档位ID |
| 27 | tier_code | VARCHAR(20) | YES | 档位代码T0-T5/NEW |
| 28 | tier_name | VARCHAR(50) | YES | 档位名称 |
| 29 | rank_by_hours | INTEGER | YES | 月度排名按effective_hours降序 |
| 30 | rank_with_ties | INTEGER | YES | 考虑并列的排名 |
| 31 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 32 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 从日度明细聚合
```sql
SELECT
site_id,
tenant_id,
assistant_id,
DATE_TRUNC('month', stat_date)::DATE AS stat_month,
COUNT(DISTINCT stat_date) AS work_days,
SUM(total_service_count) AS total_service_count,
SUM(base_service_count) AS base_service_count,
SUM(bonus_service_count) AS bonus_service_count,
SUM(total_hours) AS total_hours,
SUM(base_hours) AS base_hours,
SUM(bonus_hours) AS bonus_hours,
SUM(trashed_seconds) / 3600.0 AS trashed_hours
FROM billiards_dws.dws_assistant_daily_detail
GROUP BY site_id, tenant_id, assistant_id, DATE_TRUNC('month', stat_date);
```
### 档位匹配
```sql
-- 根据有效业绩匹配档位
SELECT * FROM billiards_dws.cfg_performance_tier
WHERE min_hours <= :effective_hours
AND (max_hours IS NULL OR max_hours > :effective_hours)
AND effective_from <= :stat_month
AND effective_to >= :stat_month
AND is_new_hire_tier = :is_new_hire
LIMIT 1;
```
## 使用说明
**新入职判断**
- 入职日期 >= 统计月1日0点 则为新入职
- 新入职使用NEW档位配置
**排名计算**
```sql
-- rank_with_ties: 并列排名如2个第一则都是1下一个是3
SELECT
assistant_id,
effective_hours,
RANK() OVER (ORDER BY effective_hours DESC) AS rank_with_ties
FROM billiards_dws.dws_assistant_monthly_summary
WHERE stat_month = '2026-01-01';
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025年8月起需要完整月数据 |
| 依赖表 | dws_assistant_daily_detail, cfg_performance_tier, dim_assistant |

View File

@@ -0,0 +1,84 @@
# dws_assistant_recharge_commission 助教充值提成表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_assistant_recharge_commission |
| 主键 | id |
| 数据来源 | Excel手动导入 |
| 更新频率 | 按需导入 |
| 说明 | 以"助教+月份+充值订单"为粒度,记录充值提成 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | assistant_id | BIGINT | NO | 助教ID |
| 5 | assistant_nickname | VARCHAR(50) | YES | 助教花名 |
| 6 | commission_month | DATE | NO | 提成月份(月第一天) |
| 7 | recharge_order_id | BIGINT | YES | 充值订单ID |
| 8 | recharge_order_no | VARCHAR(50) | YES | 充值订单号 |
| 9 | recharge_amount | NUMERIC(12,2) | NO | 充值订单金额 |
| 10 | commission_amount | NUMERIC(12,2) | NO | 提成金额 |
| 11 | commission_ratio | NUMERIC(5,4) | YES | 提成比例 |
| 12 | import_batch_no | VARCHAR(50) | YES | 导入批次号 |
| 13 | import_file_name | VARCHAR(200) | YES | 导入文件名 |
| 14 | import_time | TIMESTAMPTZ | YES | 导入时间 |
| 15 | import_user | VARCHAR(50) | YES | 导入操作人 |
| 16 | remark | TEXT | YES | 备注 |
| 17 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 18 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## Excel导入模板
| 月份 | 助教号 | 助教花名 | 充值订单金额 | 提成金额 | 备注 |
|------|--------|----------|--------------|----------|------|
| 2026-01 | 1 | 小燕 | 5000.00 | 300.00 | ge |
| 2026-01 | 2 | 小明 | 3000.00 | 180.00 | 续充 |
### 导入规则
- **月份**: 必填,格式 2026-01 或 2026/01/01
- **助教号**: 必填,数字(如 1, 2, 31
- **助教花名**: 必填,与助教号组合确定唯一助教
- **充值订单金额**: 选填,单位:元
- **提成金额**: 必填,单位:元
- **备注**: 选填
### 助教匹配逻辑
```sql
-- 通过 assistant_no + nickname 查找 assistant_id
SELECT assistant_id
FROM billiards_dwd.dim_assistant
WHERE assistant_no = :assistant_no
AND nickname = :nickname
AND scd2_is_current = 1;
```
## 使用说明
**汇总到薪资计算**
```sql
-- 获取助教某月的充值提成总额
SELECT
assistant_id,
commission_month,
SUM(commission_amount) AS total_commission
FROM billiards_dws.dws_assistant_recharge_commission
WHERE commission_month = '2026-01-01'
GROUP BY assistant_id, commission_month;
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ❌ 不可自动回溯 |
| 原因 | 数据来源为Excel手工导入DWD层无此数据 |
| 处理 | 需要人工补录历史数据 |

View File

@@ -0,0 +1,98 @@
# dws_assistant_salary_calc 助教工资计算详情表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_assistant_salary_calc |
| 主键 | id |
| 唯一键 | (site_id, assistant_id, salary_month) |
| 数据来源 | dws_assistant_monthly_summary + cfg_* 配置表 |
| 更新频率 | 月初计算上月工资 |
| 说明 | 以"助教+月份"为粒度,计算月度工资明细 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | assistant_id | BIGINT | NO | 助教ID |
| 5 | assistant_nickname | VARCHAR(50) | YES | 助教花名 |
| 6 | salary_month | DATE | NO | 工资月份(月第一天) |
| 7 | assistant_level_code | INTEGER | YES | 助教等级代码 |
| 8 | assistant_level_name | VARCHAR(20) | YES | 助教等级名称 |
| 9 | hire_date | DATE | YES | 入职日期 |
| 10 | is_new_hire | BOOLEAN | NO | 是否新入职 |
| 11 | effective_hours | NUMERIC(10,2) | NO | 有效业绩小时数 |
| 12 | base_hours | NUMERIC(10,2) | NO | 基础课小时数 |
| 13 | bonus_hours | NUMERIC(10,2) | NO | 附加课小时数 |
| 14 | tier_id | INTEGER | YES | 档位ID |
| 15 | tier_code | VARCHAR(20) | YES | 档位代码 |
| 16 | tier_name | VARCHAR(50) | YES | 档位名称 |
| 17 | rank_with_ties | INTEGER | YES | 月度排名(考虑并列) |
| 18 | base_course_price | NUMERIC(10,2) | NO | 基础课客户支付价格 |
| 19 | bonus_course_price | NUMERIC(10,2) | NO | 附加课客户支付价格固定190 |
| 20 | base_deduction | NUMERIC(10,2) | NO | 专业课抽成(元/小时) |
| 21 | bonus_deduction_ratio | NUMERIC(5,4) | NO | 打赏课抽成比例 |
| 22 | base_income | NUMERIC(12,2) | NO | 基础课收入 |
| 23 | bonus_income | NUMERIC(12,2) | NO | 附加课收入 |
| 24 | total_course_income | NUMERIC(12,2) | NO | 课时收入合计 |
| 25 | sprint_bonus | NUMERIC(12,2) | NO | 冲刺奖金 |
| 26 | top_rank_bonus | NUMERIC(12,2) | NO | Top3排名奖金 |
| 27 | recharge_commission | NUMERIC(12,2) | NO | 充值提成 |
| 28 | other_bonus | NUMERIC(12,2) | NO | 其他奖金 |
| 29 | total_bonus | NUMERIC(12,2) | NO | 奖金合计 |
| 30 | gross_salary | NUMERIC(12,2) | NO | 应发工资 |
| 31 | vacation_days | INTEGER | NO | 次月可休假天数 |
| 32 | vacation_unlimited | BOOLEAN | NO | 休假自由标记 |
| 33 | calc_notes | TEXT | YES | 计算备注 |
| 34 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 35 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 工资计算公式
### 课时收入
```
基础课收入 = base_hours × (base_course_price - base_deduction)
附加课收入 = bonus_hours × 190 × (1 - bonus_deduction_ratio)
课时收入合计 = 基础课收入 + 附加课收入
```
### 奖金
```
冲刺奖金: H>=190得300元, H>=220得800元不累计取最高
Top3奖金: 1st=1000元, 2nd=600元, 3rd=400元
充值提成: 来自dws_assistant_recharge_commission
```
### 应发工资
```
gross_salary = total_course_income + total_bonus
```
## 计算示例
| 项目 | 数值 | 计算过程 |
|------|------|----------|
| 基础课小时数 | 170 | 来自monthly_summary |
| 附加课小时数 | 15 | 来自monthly_summary |
| 等级 | 中级(20) | base_course_price=108 |
| 档位 | T3 | base_deduction=13, bonus_ratio=0.35 |
| 基础课收入 | 16,150 | 170 × (108-13) |
| 附加课收入 | 1,852.5 | 15 × 190 × 0.65 |
| 冲刺奖金 | 300 | 185>=190 |
| 应发工资 | 18,302.5 | 16,150 + 1,852.5 + 300 |
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ⚠️ 部分可回溯 |
| 数据范围 | 2025年8月起 |
| 依赖表 | dws_assistant_monthly_summary, cfg_*, dws_assistant_recharge_commission |
| 限制 | 充值提成需手工导入历史数据 |

View File

@@ -0,0 +1,125 @@
# dws_finance_daily_summary 财务日度汇总表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_finance_daily_summary |
| 主键 | id |
| 唯一键 | (site_id, stat_date) |
| 数据来源 | dwd_settlement_head + 多个DWD事实表 |
| 更新频率 | 每小时更新当日数据 |
| 说明 | 以"日期"为粒度,汇总当日财务数据 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | stat_date | DATE | NO | 统计日期 |
| 5 | gross_amount | NUMERIC(14,2) | NO | 发生额合计 |
| 6 | table_fee_amount | NUMERIC(14,2) | NO | 台费正价 |
| 7 | goods_amount | NUMERIC(14,2) | NO | 商品正价 |
| 8 | assistant_pd_amount | NUMERIC(14,2) | NO | 助教基础课正价(陪打) |
| 9 | assistant_cx_amount | NUMERIC(14,2) | NO | 助教激励课正价(超休) |
| 10 | discount_total | NUMERIC(14,2) | NO | 优惠合计 |
| 11 | discount_groupbuy | NUMERIC(14,2) | NO | 团购优惠 |
| 12 | discount_vip | NUMERIC(14,2) | NO | 会员折扣 |
| 13 | discount_gift_card | NUMERIC(14,2) | NO | 赠送卡抵扣 |
| 14 | discount_manual | NUMERIC(14,2) | NO | 手动调整 |
| 15 | discount_rounding | NUMERIC(14,2) | NO | 抹零 |
| 16 | discount_other | NUMERIC(14,2) | NO | 其他优惠 |
| 17 | confirmed_income | NUMERIC(14,2) | NO | 确认收入 = 发生额 - 优惠 |
| 18 | cash_inflow_total | NUMERIC(14,2) | NO | 现金流入合计 |
| 19 | cash_pay_amount | NUMERIC(14,2) | NO | 收银实付 |
| 20 | groupbuy_pay_amount | NUMERIC(14,2) | NO | 团购支付金额 |
| 21 | platform_settlement_amount | NUMERIC(14,2) | NO | 平台回款金额(导入) |
| 22 | platform_fee_amount | NUMERIC(14,2) | NO | 平台佣金+服务费(导入) |
| 23 | recharge_cash_inflow | NUMERIC(14,2) | NO | 充值现金流入 |
| 24 | card_consume_total | NUMERIC(14,2) | NO | 卡消费合计 |
| 25 | cash_card_consume | NUMERIC(14,2) | NO | 储值卡消费 |
| 26 | gift_card_consume | NUMERIC(14,2) | NO | 赠送卡消费 |
| 27 | cash_outflow_total | NUMERIC(14,2) | NO | 现金流出合计 |
| 28 | cash_balance_change | NUMERIC(14,2) | NO | 现金余额变动 |
| 29 | recharge_count | INTEGER | NO | 充值笔数 |
| 30 | recharge_total | NUMERIC(14,2) | NO | 充值总额(含赠送) |
| 31 | recharge_cash | NUMERIC(14,2) | NO | 充值现金部分 |
| 32 | recharge_gift | NUMERIC(14,2) | NO | 充值赠送部分 |
| 33 | first_recharge_count | INTEGER | NO | 首充笔数 |
| 34 | first_recharge_amount | NUMERIC(14,2) | NO | 首充金额 |
| 35 | renewal_count | INTEGER | NO | 续充笔数 |
| 36 | renewal_amount | NUMERIC(14,2) | NO | 续充金额 |
| 37 | order_count | INTEGER | NO | 结账单数 |
| 38 | member_order_count | INTEGER | NO | 会员订单数 |
| 39 | guest_order_count | INTEGER | NO | 散客订单数 |
| 40 | avg_order_amount | NUMERIC(12,2) | NO | 平均客单价 |
| 41 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 42 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 结账汇总dwd_settlement_head
```sql
SELECT
DATE(pay_time) AS stat_date,
SUM(table_charge_money) AS table_fee_amount,
SUM(goods_money) AS goods_amount,
SUM(assistant_pd_money) AS assistant_pd_amount,
SUM(assistant_cx_money) AS assistant_cx_amount,
SUM(member_discount_amount) AS discount_vip,
SUM(adjust_amount) AS discount_manual,
SUM(rounding_amount) AS discount_rounding,
SUM(pay_amount) AS cash_pay_amount,
SUM(balance_amount) AS cash_card_consume,
SUM(gift_card_amount) AS gift_card_consume,
COUNT(*) AS order_count
FROM billiards_dwd.dwd_settlement_head
WHERE settle_type = 1
GROUP BY DATE(pay_time);
```
### 团购核销dwd_groupbuy_redemption
```sql
SELECT
DATE(create_time) AS stat_date,
SUM(coupon_money) AS groupbuy_pay_amount
FROM billiards_dwd.dwd_groupbuy_redemption
WHERE is_delete = 0
GROUP BY DATE(create_time);
```
### 充值订单dwd_recharge_order
```sql
SELECT
DATE(pay_time) AS stat_date,
COUNT(*) AS recharge_count,
SUM(pay_amount) AS recharge_cash,
SUM(point_amount) AS recharge_gift,
SUM(CASE WHEN is_first = 1 THEN 1 ELSE 0 END) AS first_recharge_count
FROM billiards_dwd.dwd_recharge_order
GROUP BY DATE(pay_time);
```
## 使用说明
**计算公式**
```
gross_amount = table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount
discount_total = discount_groupbuy + discount_vip + discount_gift_card + discount_manual + discount_rounding + discount_other
confirmed_income = gross_amount - discount_total
cash_inflow_total = cash_pay_amount + groupbuy_pay_amount + platform_settlement_amount + recharge_cash_inflow
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-16 ~ 至今 |
| 依赖表 | dwd_settlement_head, dwd_groupbuy_redemption, dwd_recharge_order, dws_platform_settlement |
| 注意 | platform_settlement需Excel导入 |

View File

@@ -0,0 +1,90 @@
# dws_finance_discount_detail 优惠明细表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_finance_discount_detail |
| 主键 | id |
| 唯一键 | (site_id, stat_date, discount_type_code) |
| 数据来源 | dwd_settlement_head + dwd_groupbuy_redemption |
| 更新频率 | 每日更新 |
| 说明 | 以"日期+优惠类型"为粒度,分析优惠构成 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | stat_date | DATE | NO | 统计日期 |
| 5 | discount_type_code | VARCHAR(30) | NO | 优惠类型代码 |
| 6 | discount_type_name | VARCHAR(50) | NO | 优惠类型名称 |
| 7 | discount_amount | NUMERIC(14,2) | NO | 优惠金额 |
| 8 | discount_ratio | NUMERIC(5,4) | NO | 优惠占比(占总优惠) |
| 9 | usage_count | INTEGER | NO | 使用次数 |
| 10 | affected_orders | INTEGER | NO | 影响订单数 |
| 11 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 12 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 优惠类型说明
| discount_type_code | discount_type_name | 数据来源 |
|--------------------|--------------------|----------|
| GROUPBUY | 团购优惠 | dwd_settlement_head.coupon_amount |
| VIP | 会员折扣 | dwd_settlement_head.member_discount_amount |
| GIFT_CARD | 赠送卡抵扣 | dwd_settlement_head.gift_card_amount |
| MANUAL | 手动调整 | dwd_settlement_head.adjust_amount |
| ROUNDING | 抹零 | dwd_settlement_head.rounding_amount |
| BIG_CUSTOMER | 大客户优惠 | dwd_settlement_head特定会员优惠 |
| OTHER | 其他优惠 | 其他无法归类的优惠 |
## 数据来源
```sql
-- 从结账头表提取各类优惠
SELECT
DATE(pay_time) AS stat_date,
'VIP' AS discount_type_code,
'会员折扣' AS discount_type_name,
SUM(member_discount_amount) AS discount_amount,
COUNT(CASE WHEN member_discount_amount > 0 THEN 1 END) AS usage_count,
COUNT(DISTINCT CASE WHEN member_discount_amount > 0 THEN order_settle_id END) AS affected_orders
FROM billiards_dwd.dwd_settlement_head
WHERE settle_type = 1
GROUP BY DATE(pay_time)
UNION ALL
SELECT
DATE(pay_time) AS stat_date,
'GROUPBUY' AS discount_type_code,
'团购优惠' AS discount_type_name,
SUM(coupon_amount) AS discount_amount,
COUNT(CASE WHEN coupon_amount > 0 THEN 1 END) AS usage_count,
COUNT(DISTINCT CASE WHEN coupon_amount > 0 THEN order_settle_id END) AS affected_orders
FROM billiards_dwd.dwd_settlement_head
WHERE settle_type = 1
GROUP BY DATE(pay_time)
-- ... 其他优惠类型
```
## 使用说明
**占比计算**
```sql
discount_ratio = discount_amount / SUM(discount_amount) OVER (PARTITION BY stat_date)
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-16 ~ 至今 |
| 依赖表 | dwd_settlement_head, dwd_groupbuy_redemption |

View File

@@ -0,0 +1,87 @@
# dws_finance_expense_summary 支出结构表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_finance_expense_summary |
| 主键 | id |
| 唯一键 | (site_id, expense_month, expense_type_code, import_batch_no) |
| 数据来源 | Excel手动导入 |
| 更新频率 | 按需导入 |
| 说明 | 以"月份+支出类型"为粒度,记录支出数据 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | expense_month | DATE | NO | 支出月份(月第一天) |
| 5 | expense_type_code | VARCHAR(30) | NO | 支出类型代码 |
| 6 | expense_type_name | VARCHAR(50) | NO | 支出类型名称 |
| 7 | expense_category | VARCHAR(20) | YES | 支出大类 |
| 8 | expense_amount | NUMERIC(14,2) | NO | 支出金额 |
| 9 | expense_detail | TEXT | YES | 支出明细说明 |
| 10 | import_batch_no | VARCHAR(50) | YES | 导入批次号 |
| 11 | import_file_name | VARCHAR(200) | YES | 导入文件名 |
| 12 | import_time | TIMESTAMPTZ | YES | 导入时间 |
| 13 | import_user | VARCHAR(50) | YES | 导入操作人 |
| 14 | remark | TEXT | YES | 备注 |
| 15 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 16 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 支出类型说明
| expense_type_code | expense_type_name | expense_category |
|-------------------|-------------------|------------------|
| RENT | 房租 | FIXED_COST |
| UTILITY | 水电费 | FIXED_COST |
| PROPERTY | 物业费 | FIXED_COST |
| SALARY | 工资 | VARIABLE_COST |
| REIMBURSE | 报销 | VARIABLE_COST |
| PLATFORM_FEE | 平台费用 | VARIABLE_COST |
| MAINTENANCE | 维修保养 | VARIABLE_COST |
| CONSUMABLES | 耗材 | VARIABLE_COST |
| MARKETING | 营销费用 | VARIABLE_COST |
| OTHER | 其他 | OTHER |
## Excel导入模板
| 月份 | 支出类型 | 支出金额 | 明细说明 | 备注 |
|------|----------|----------|----------|------|
| 2026-01 | 房租 | 50000.00 | 1月房租 | |
| 2026-01 | 水电费 | 8000.00 | 1月水电 | |
| 2026-01 | 工资 | 120000.00 | 员工工资 | |
### 导入规则
- **月份**: 必填,格式 2026-01 或 2026/01/01
- **支出类型**: 必填,需匹配支出类型名称
- **支出金额**: 必填,单位:元
- **明细说明**: 选填
- **备注**: 选填
## 使用说明
**月度支出汇总**
```sql
SELECT
expense_month,
expense_category,
SUM(expense_amount) AS total_expense
FROM billiards_dws.dws_finance_expense_summary
GROUP BY expense_month, expense_category
ORDER BY expense_month, expense_category;
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ❌ 不可自动回溯 |
| 原因 | 数据来源为Excel手工导入DWD层无此数据 |
| 处理 | 需要人工补录历史数据 |

View File

@@ -0,0 +1,88 @@
# dws_finance_income_structure 收入结构分析表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_finance_income_structure |
| 主键 | id |
| 唯一键 | (site_id, stat_date, structure_type, category_code) |
| 数据来源 | dwd_table_fee_log + dwd_assistant_service_log + cfg_area_category |
| 更新频率 | 每日更新 |
| 说明 | 以"日期+区域/类型"为粒度,分析收入结构 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | stat_date | DATE | NO | 统计日期 |
| 5 | structure_type | VARCHAR(20) | NO | 结构类型。**枚举值**: AREA区域, INCOME_TYPE收入类型 |
| 6 | category_code | VARCHAR(30) | NO | 分类代码 |
| 7 | category_name | VARCHAR(50) | NO | 分类名称 |
| 8 | income_amount | NUMERIC(14,2) | NO | 收入金额 |
| 9 | income_ratio | NUMERIC(5,4) | NO | 收入占比 |
| 10 | order_count | INTEGER | NO | 订单数 |
| 11 | duration_minutes | INTEGER | NO | 时长(分钟) |
| 12 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 13 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 分类代码说明
### 按区域分析 (structure_type = 'AREA')
| category_code | category_name | 来源 |
|---------------|---------------|------|
| BILLIARD | 台球散台 | A区/B区/C区/TV台 |
| BILLIARD_VIP | 台球VIP | VIP包厢 |
| SNOOKER | 斯诺克 | 斯诺克区 |
| MAHJONG | 麻将棋牌 | 麻将房/M7/M8/666/发财 |
| KTV | K歌娱乐 | K包/k包活动区/幸会158 |
| SPECIAL | 补时长 | 补时长 |
| OTHER | 其他 | 未映射区域 |
### 按收入类型分析 (structure_type = 'INCOME_TYPE')
| category_code | category_name |
|---------------|---------------|
| TABLE_FEE | 台费收入 |
| GOODS | 商品收入 |
| ASSISTANT_BASE | 助教基础课收入 |
| ASSISTANT_BONUS | 助教附加课收入 |
## 数据来源
### 按区域汇总台费
```sql
SELECT
DATE(tfl.ledger_end_time) AS stat_date,
COALESCE(ac.category_code, 'OTHER') AS category_code,
COALESCE(ac.category_name, '其他') AS category_name,
SUM(tfl.ledger_amount) AS income_amount,
SUM(tfl.ledger_count) AS duration_seconds,
COUNT(DISTINCT tfl.order_settle_id) AS order_count
FROM billiards_dwd.dwd_table_fee_log tfl
LEFT JOIN billiards_dwd.dim_table dt ON dt.table_id = tfl.site_table_id
LEFT JOIN billiards_dws.cfg_area_category ac ON dt.site_table_area_name = ac.source_area_name
WHERE tfl.is_delete = 0
GROUP BY DATE(tfl.ledger_end_time), COALESCE(ac.category_code, 'OTHER'), COALESCE(ac.category_name, '其他');
```
## 使用说明
**占比计算**
```sql
-- income_ratio = 当前分类收入 / 当日总收入
income_ratio = income_amount / SUM(income_amount) OVER (PARTITION BY stat_date, structure_type)
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-21 ~ 至今 |
| 依赖表 | dwd_table_fee_log, dwd_assistant_service_log, dim_table, cfg_area_category |

View File

@@ -0,0 +1,95 @@
# dws_finance_recharge_summary 充值统计表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_finance_recharge_summary |
| 主键 | id |
| 唯一键 | (site_id, stat_date) |
| 数据来源 | dwd_recharge_order |
| 更新频率 | 每日更新 |
| 说明 | 以"日期"为粒度,统计充值数据,区分首充/续充 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | stat_date | DATE | NO | 统计日期 |
| 5 | recharge_count | INTEGER | NO | 充值笔数 |
| 6 | recharge_total | NUMERIC(14,2) | NO | 充值总额(含赠送) |
| 7 | recharge_cash | NUMERIC(14,2) | NO | 现金充值金额 |
| 8 | recharge_gift | NUMERIC(14,2) | NO | 赠送金额 |
| 9 | first_recharge_count | INTEGER | NO | 首充笔数 |
| 10 | first_recharge_cash | NUMERIC(14,2) | NO | 首充现金 |
| 11 | first_recharge_gift | NUMERIC(14,2) | NO | 首充赠送 |
| 12 | first_recharge_total | NUMERIC(14,2) | NO | 首充总额 |
| 13 | renewal_count | INTEGER | NO | 续充笔数 |
| 14 | renewal_cash | NUMERIC(14,2) | NO | 续充现金 |
| 15 | renewal_gift | NUMERIC(14,2) | NO | 续充赠送 |
| 16 | renewal_total | NUMERIC(14,2) | NO | 续充总额 |
| 17 | recharge_member_count | INTEGER | NO | 充值会员数(去重) |
| 18 | new_member_count | INTEGER | NO | 新增会员数 |
| 19 | total_card_balance | NUMERIC(14,2) | NO | 全部会员卡余额(当日末) |
| 20 | cash_card_balance | NUMERIC(14,2) | NO | 储值卡余额 |
| 21 | gift_card_balance | NUMERIC(14,2) | NO | 赠送卡余额 |
| 22 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 23 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 充值订单dwd_recharge_order
```sql
SELECT
DATE(pay_time) AS stat_date,
COUNT(*) AS recharge_count,
SUM(pay_amount + point_amount) AS recharge_total,
SUM(pay_amount) AS recharge_cash,
SUM(point_amount) AS recharge_gift,
-- 首充
SUM(CASE WHEN is_first = 1 THEN 1 ELSE 0 END) AS first_recharge_count,
SUM(CASE WHEN is_first = 1 THEN pay_amount ELSE 0 END) AS first_recharge_cash,
SUM(CASE WHEN is_first = 1 THEN point_amount ELSE 0 END) AS first_recharge_gift,
-- 续充
SUM(CASE WHEN is_first = 0 THEN 1 ELSE 0 END) AS renewal_count,
SUM(CASE WHEN is_first = 0 THEN pay_amount ELSE 0 END) AS renewal_cash,
-- 会员数
COUNT(DISTINCT member_id) AS recharge_member_count
FROM billiards_dwd.dwd_recharge_order
GROUP BY DATE(pay_time);
```
### 卡余额快照dim_member_card_account
```sql
-- 截至stat_date当日末的卡余额
SELECT
SUM(balance) AS total_card_balance,
SUM(CASE WHEN card_type_id = 2793249295533893 THEN balance ELSE 0 END) AS cash_card_balance,
SUM(CASE WHEN card_type_id != 2793249295533893 THEN balance ELSE 0 END) AS gift_card_balance
FROM billiards_dwd.dim_member_card_account
WHERE scd2_start_time <= :stat_date + INTERVAL '1 day'
AND (scd2_end_time IS NULL OR scd2_end_time > :stat_date + INTERVAL '1 day');
```
## 使用说明
**首充判断**
- is_first = 1: 首充
- is_first = 0: 续充
**储值卡ID**
- 储值卡 card_type_id = 2793249295533893
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-21 ~ 至今 |
| 依赖表 | dwd_recharge_order, dim_member_card_account |

View File

@@ -0,0 +1,102 @@
# dws_member_consumption_summary 会员消费汇总表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_member_consumption_summary |
| 主键 | id |
| 唯一键 | (site_id, member_id, stat_date) |
| 数据来源 | dwd_settlement_head + 关联明细表 |
| 更新频率 | 每日更新 |
| 说明 | 以"会员"为粒度,统计消费行为和滚动窗口指标 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | member_id | BIGINT | NO | 会员IDmember_id=0散客不入此表 |
| 5 | stat_date | DATE | NO | 统计基准日期 |
| 6 | member_nickname | VARCHAR(100) | YES | 会员昵称 |
| 7 | member_mobile | VARCHAR(20) | YES | 手机号(脱敏) |
| 8 | card_grade_name | VARCHAR(50) | YES | 卡等级名称 |
| 9 | register_date | DATE | YES | 注册日期 |
| 10 | first_consume_date | DATE | YES | 首次消费日期 |
| 11 | last_consume_date | DATE | YES | 最近消费日期 |
| 12 | total_visit_count | INTEGER | NO | 累计到店次数 |
| 13 | total_consume_amount | NUMERIC(14,2) | NO | 累计消费金额 |
| 14 | total_recharge_amount | NUMERIC(14,2) | NO | 累计充值金额 |
| 15 | total_table_fee | NUMERIC(14,2) | NO | 累计台费 |
| 16 | total_goods_amount | NUMERIC(14,2) | NO | 累计商品消费 |
| 17 | total_assistant_amount | NUMERIC(14,2) | NO | 累计助教服务消费 |
| 18-23 | visit_count_7d/10d/15d/30d/60d/90d | INTEGER | NO | 近N天到店次数 |
| 24-29 | consume_amount_7d/10d/15d/30d/60d/90d | NUMERIC(14,2) | NO | 近N天消费金额 |
| 30 | cash_card_balance | NUMERIC(14,2) | NO | 储值卡余额 |
| 31 | gift_card_balance | NUMERIC(14,2) | NO | 赠送卡余额 |
| 32 | total_card_balance | NUMERIC(14,2) | NO | 总卡余额 |
| 33 | days_since_last | INTEGER | YES | 距离最近消费的天数 |
| 34 | is_active_7d | BOOLEAN | NO | 近7天是否活跃 |
| 35 | is_active_30d | BOOLEAN | NO | 近30天是否活跃 |
| 36 | is_active_90d | BOOLEAN | NO | 近90天是否活跃 |
| 37 | customer_tier | VARCHAR(20) | YES | 客户分层(高价值/中等/低活跃/流失) |
| 38 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 39 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 消费统计来源dwd_settlement_head
```sql
SELECT
site_id,
member_id,
DATE(pay_time) AS consume_date,
COUNT(*) AS visit_count,
SUM(consume_money) AS consume_amount,
SUM(table_charge_money) AS table_fee,
SUM(goods_money) AS goods_amount,
SUM(assistant_pd_money + assistant_cx_money) AS assistant_amount
FROM billiards_dwd.dwd_settlement_head
WHERE member_id != 0 -- 排除散客
AND settle_type = 1 -- 已结账
GROUP BY site_id, member_id, DATE(pay_time);
```
### 卡余额来源dim_member_card_account
```sql
SELECT
tenant_member_id AS member_id,
SUM(CASE WHEN card_type_id = 2793249295533893 THEN balance ELSE 0 END) AS cash_card_balance,
SUM(CASE WHEN card_type_id != 2793249295533893 THEN balance ELSE 0 END) AS gift_card_balance
FROM billiards_dwd.dim_member_card_account
WHERE scd2_is_current = 1
GROUP BY tenant_member_id;
```
## 使用说明
**散客处理**
- member_id=0 的散客不进入此表统计
**客户分层规则**
```sql
customer_tier = CASE
WHEN consume_amount_30d >= 1000 THEN '高价值'
WHEN consume_amount_30d >= 300 THEN '中等'
WHEN is_active_30d THEN '低活跃'
ELSE '流失'
END
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-16 ~ 至今 |
| 依赖表 | dwd_settlement_head, dim_member, dim_member_card_account |

View File

@@ -0,0 +1,119 @@
# dws_member_visit_detail 会员来店明细表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_member_visit_detail |
| 主键 | id |
| 唯一键 | (site_id, member_id, order_settle_id) |
| 数据来源 | dwd_settlement_head + 关联明细表 |
| 更新频率 | 每日增量更新 |
| 说明 | 以"会员+订单"为粒度,记录每次来店消费明细 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | member_id | BIGINT | NO | 会员ID散客不入此表 |
| 5 | order_settle_id | BIGINT | NO | 结账单ID |
| 6 | visit_date | DATE | NO | 来店日期 |
| 7 | visit_time | TIMESTAMPTZ | YES | 来店时间 |
| 8 | member_nickname | VARCHAR(100) | YES | 会员昵称 |
| 9 | member_mobile | VARCHAR(20) | YES | 手机号 |
| 10 | member_birthday | DATE | YES | 会员生日 |
| 11 | table_id | BIGINT | YES | 台桌ID |
| 12 | table_name | VARCHAR(50) | YES | 台桌名称 |
| 13 | area_name | VARCHAR(50) | YES | 区域名称(原始) |
| 14 | area_category | VARCHAR(20) | YES | 区域分类 |
| 15 | table_fee | NUMERIC(12,2) | NO | 台费 |
| 16 | goods_amount | NUMERIC(12,2) | NO | 商品金额 |
| 17 | assistant_amount | NUMERIC(12,2) | NO | 助教服务金额 |
| 18 | total_consume | NUMERIC(12,2) | NO | 消费总额(正价) |
| 19 | total_discount | NUMERIC(12,2) | NO | 优惠总额 |
| 20 | actual_pay | NUMERIC(12,2) | NO | 实付金额 |
| 21 | cash_pay | NUMERIC(12,2) | NO | 现金/刷卡支付 |
| 22 | cash_card_pay | NUMERIC(12,2) | NO | 储值卡支付 |
| 23 | gift_card_pay | NUMERIC(12,2) | NO | 赠送卡支付 |
| 24 | groupbuy_pay | NUMERIC(12,2) | NO | 团购券支付 |
| 25 | table_duration_min | INTEGER | NO | 台桌使用时长(分钟) |
| 26 | assistant_duration_min | INTEGER | NO | 助教服务时长(分钟) |
| 27 | assistant_services | JSONB | YES | 助教服务列表 |
| 28 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 29 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
### 主表来源dwd_settlement_head
```sql
SELECT
site_id,
tenant_id,
member_id,
order_settle_id,
DATE(pay_time) AS visit_date,
pay_time AS visit_time,
member_name AS member_nickname,
member_phone AS member_mobile,
table_id,
table_charge_money AS table_fee,
goods_money AS goods_amount,
assistant_pd_money + assistant_cx_money AS assistant_amount,
consume_money AS total_consume,
member_discount_amount + coupon_amount + adjust_amount AS total_discount,
pay_amount AS actual_pay,
balance_amount AS cash_card_pay,
gift_card_amount AS gift_card_pay
FROM billiards_dwd.dwd_settlement_head
WHERE member_id != 0
AND settle_type = 1;
```
### 助教服务明细dwd_assistant_service_log
```sql
-- 聚合为JSONB格式
SELECT
order_settle_id,
jsonb_agg(jsonb_build_object(
'assistant_id', site_assistant_id,
'nickname', nickname,
'duration_min', income_seconds / 60,
'amount', ledger_amount
)) AS assistant_services
FROM billiards_dwd.dwd_assistant_service_log
GROUP BY order_settle_id;
```
## 使用说明
**assistant_services JSON格式**
```json
[
{"assistant_id": 123, "nickname": "小燕", "duration_min": 60, "amount": 108.00},
{"assistant_id": 456, "nickname": "小明", "duration_min": 30, "amount": 54.00}
]
```
**区域分类映射**
```sql
-- 通过cfg_area_category映射
area_category = COALESCE(
(SELECT category_name FROM billiards_dws.cfg_area_category
WHERE source_area_name = dim_table.site_table_area_name),
'其他'
)
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-16 ~ 至今 |
| 依赖表 | dwd_settlement_head, dwd_assistant_service_log, dim_table, dim_member |

View File

@@ -0,0 +1,100 @@
# dws_platform_settlement 平台回款/服务费表
> 生成时间2026-02-03
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards_dws |
| 表名 | dws_platform_settlement |
| 主键 | id |
| 数据来源 | Excel手动导入 |
| 更新频率 | 按需导入 |
| 说明 | 以"回款日期+平台+订单"为粒度,记录平台结算数据 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | settlement_date | DATE | NO | 回款日期 |
| 5 | platform_type | VARCHAR(30) | NO | 平台类型。**枚举值**: MEITUAN, DOUYIN, DIANPING, OTHER |
| 6 | platform_name | VARCHAR(50) | YES | 平台名称 |
| 7 | platform_order_no | VARCHAR(100) | YES | 平台订单号 |
| 8 | order_settle_id | BIGINT | YES | 关联的结账单ID |
| 9 | settlement_amount | NUMERIC(14,2) | NO | 回款金额(实际入账) |
| 10 | commission_amount | NUMERIC(14,2) | NO | 佣金(平台抽成) |
| 11 | service_fee | NUMERIC(14,2) | NO | 服务费 |
| 12 | gross_amount | NUMERIC(14,2) | NO | 订单原始金额 |
| 13 | import_batch_no | VARCHAR(50) | YES | 导入批次号 |
| 14 | import_file_name | VARCHAR(200) | YES | 导入文件名 |
| 15 | import_time | TIMESTAMPTZ | YES | 导入时间 |
| 16 | import_user | VARCHAR(50) | YES | 导入操作人 |
| 17 | remark | TEXT | YES | 备注 |
| 18 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 19 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 平台类型说明
| platform_type | platform_name | 说明 |
|---------------|---------------|------|
| MEITUAN | 美团 | 美团团购/外卖 |
| DOUYIN | 抖音 | 抖音团购 |
| DIANPING | 大众点评 | 大众点评团购 |
| OTHER | 其他 | 其他平台 |
## Excel导入模板
| 回款日期 | 平台 | 平台订单号 | 订单金额 | 回款金额 | 佣金 | 服务费 | 备注 |
|----------|------|------------|----------|----------|------|--------|------|
| 2026-01-15 | 美团 | MT202601150001 | 200.00 | 186.00 | 12.00 | 2.00 | |
| 2026-01-15 | 抖音 | DY202601150001 | 150.00 | 142.50 | 6.00 | 1.50 | |
### 导入规则
- **回款日期**: 必填,实际到账日期
- **平台**: 必填,美团/抖音/大众点评/其他
- **平台订单号**: 选填,用于追溯
- **订单金额**: 必填,订单原始金额
- **回款金额**: 必填,实际到账金额
- **佣金**: 选填,平台抽成
- **服务费**: 选填
### 金额关系
```
settlement_amount = gross_amount - commission_amount - service_fee
```
## 使用说明
**日度平台回款汇总**
```sql
SELECT
settlement_date,
platform_type,
SUM(settlement_amount) AS total_settlement,
SUM(commission_amount) AS total_commission,
SUM(service_fee) AS total_service_fee
FROM billiards_dws.dws_platform_settlement
GROUP BY settlement_date, platform_type
ORDER BY settlement_date, platform_type;
```
**关联到财务日度汇总**
```sql
-- dws_finance_daily_summary.platform_settlement_amount
SELECT stat_date, SUM(settlement_amount)
FROM billiards_dws.dws_platform_settlement
WHERE settlement_date = :stat_date
GROUP BY stat_date;
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ❌ 不可自动回溯 |
| 原因 | 数据来源为Excel手工导入需从平台后台导出 |
| 处理 | 需要人工补录历史平台结算数据 |

View File

@@ -0,0 +1,585 @@
# DWS 数据字典
## 概述
DWSData Warehouse Service层是数据仓库的汇总层基于DWD明细层数据构建为上层应用和报表提供预聚合的数据服务。
### 表清单
| 分类 | 表名 | 说明 | 更新频率 |
|------|------|------|----------|
| **配置表** | cfg_performance_tier | 绩效档位配置 | 手动维护 |
| | cfg_assistant_level_price | 助教等级定价 | 手动维护 |
| | cfg_bonus_rules | 奖金规则配置 | 手动维护 |
| | cfg_area_category | 台区分类映射 | 手动维护 |
| | cfg_skill_type | 技能课程类型映射 | 手动维护 |
| **助教维度** | dws_assistant_daily_detail | 助教日度业绩明细 | 每小时 |
| | dws_assistant_monthly_summary | 助教月度业绩汇总 | 每日 |
| | dws_assistant_customer_stats | 助教服务客户统计 | 每日 |
| | dws_assistant_salary_calc | 助教工资计算详情 | 月初 |
| | dws_assistant_recharge_commission | 助教充值提成 | Excel导入 |
| **客户维度** | dws_member_consumption_summary | 会员消费汇总 | 每日 |
| | dws_member_visit_detail | 会员来店明细 | 每日 |
| **财务维度** | dws_finance_daily_summary | 财务日度汇总 | 每小时 |
| | dws_finance_income_structure | 收入结构分析 | 每日 |
| | dws_finance_discount_detail | 优惠明细 | 每日 |
| | dws_finance_recharge_summary | 充值统计 | 每日 |
| | dws_finance_expense_summary | 支出结构 | Excel导入 |
| | dws_assistant_finance_analysis | 助教收支分析 | 每日 |
| | dws_platform_settlement | 平台回款/服务费 | Excel导入 |
| **订单汇总** | dws_order_summary | 订单汇总 | 每日 |
---
## 一、配置表
### 1.1 cfg_performance_tier - 绩效档位配置
| 字段 | 类型 | 说明 |
|------|------|------|
| tier_id | SERIAL | 档位ID主键 |
| tier_code | VARCHAR(20) | 档位代码T0-T5, NEW |
| tier_name | VARCHAR(50) | 档位名称 |
| tier_level | INTEGER | 档位等级(-1=新入职, 0-5=正常档位) |
| min_hours | NUMERIC(10,2) | 最低业绩小时数阈值(>= |
| max_hours | NUMERIC(10,2) | 最高业绩小时数阈值(<NULL=无上限 |
| base_deduction | NUMERIC(10,2) | 专业课抽成(元/小时),球房从基础课扣除 |
| bonus_deduction_ratio | NUMERIC(5,4) | 打赏课抽成比例0-1球房从附加课扣除 |
| vacation_days | INTEGER | 次月可休假天数 |
| vacation_unlimited | BOOLEAN | 休假自由标记5档为TRUE |
| is_new_hire_tier | BOOLEAN | 是否为新入职专用档位 |
| effective_from | DATE | 生效起始日期 |
| effective_to | DATE | 生效截止日期 |
**档位配置来自DWS数据库处理需求.md**
| tier_code | tier_name | 业绩阈值 | 专业课抽成 | 打赏课抽成 | 休假 |
|-----------|-----------|----------|-----------|-----------|------|
| T0 | 0档-淘汰压力 | H < 100 | 28元/时 | 50% | 3天 |
| T1 | 1档-及格档 | 100 ≤ H < 130 | 18元/时 | 40% | 4天 |
| T2 | 2档-良好档 | 130 ≤ H < 160 | 15元/时 | 38% | 4天 |
| T3 | 3档-优秀档 | 160 ≤ H < 190 | 13元/时 | 35% | 5天 |
| T4 | 4档-卓越加速档 | 190 ≤ H < 220 | 10元/时 | 33% | 6天 |
| T5 | 5档-冠军加速档 | H ≥ 220 | 8元/时 | 30% | 休假自由 |
| NEW | 新入职档位 | - | 18元/时 | 40% | 4天 |
**业务规则:**
- 6档绩效T0-T5根据有效业绩小时数基础课+附加课)匹配
- 新入职档位月1日0点后入职者首月使用NEW档位按1档抽成标准
- 支持按时间生效,历史月份使用历史规则
### 1.2 cfg_assistant_level_price - 助教等级定价
| 字段 | 类型 | 说明 |
|------|------|------|
| price_id | SERIAL | 定价ID主键 |
| level_code | INTEGER | 等级代码8/10/20/30/40 |
| level_name | VARCHAR(20) | 等级名称 |
| base_course_price | NUMERIC(10,2) | 基础课客户支付价格(元/小时) |
| bonus_course_price | NUMERIC(10,2) | 附加课客户支付价格固定190元 |
| effective_from | DATE | 生效起始日期 |
| effective_to | DATE | 生效截止日期 |
**等级定价(客户支付价格):**
| level_code | level_name | 基础课价格 | 附加课价格 |
|------------|------------|-----------|-----------|
| 8 | 助教管理 | 98元/时 | 190元/时 |
| 10 | 初级 | 98元/时 | 190元/时 |
| 20 | 中级 | 108元/时 | 190元/时 |
| 30 | 高级 | 118元/时 | 190元/时 |
| 40 | 星级 | 138元/时 | 190元/时 |
**注意:** 此价格为客户支付价格,助教实际收入需减去档位抽成
### 1.3 cfg_bonus_rules - 奖金规则配置
| 字段 | 类型 | 说明 |
|------|------|------|
| rule_id | SERIAL | 规则ID主键 |
| rule_type | VARCHAR(20) | 规则类型SPRINT/TOP_RANK |
| rule_code | VARCHAR(30) | 规则代码 |
| rule_name | VARCHAR(50) | 规则名称 |
| threshold_hours | NUMERIC(10,2) | 小时数阈值(冲刺奖金) |
| rank_position | INTEGER | 排名位置Top奖金 |
| bonus_amount | NUMERIC(12,2) | 奖金金额(元) |
| is_cumulative | BOOLEAN | 是否可累计 |
| priority | INTEGER | 优先级 |
| effective_from | DATE | 生效起始日期 |
| effective_to | DATE | 生效截止日期 |
**业务规则:**
- 冲刺奖金H>=190得300元H>=220得800元不累计取最高档
- Top3奖金1st=1000元2nd=600元3rd=400元并列都算
### 1.4 cfg_area_category - 台区分类映射
| 字段 | 类型 | 说明 |
|------|------|------|
| category_id | SERIAL | 分类ID主键 |
| source_area_name | VARCHAR(100) | 源区域名称来自dim_table.site_table_area_name |
| category_code | VARCHAR(20) | 分类代码 |
| category_name | VARCHAR(50) | 分类名称 |
| match_type | VARCHAR(10) | 匹配类型exact/like/default |
| match_priority | INTEGER | 匹配优先级(数字越小优先级越高) |
| is_active | BOOLEAN | 是否启用 |
**分类代码基于BD_manual_dim_table.md实际数据**
| category_code | category_name | 匹配规则 |
|---------------|---------------|----------|
| BILLIARD | 普通台球区 | A区, B区, C区 |
| BILLIARD_VIP | VIP台球包厢 | VIP包厢 |
| SNOOKER | 斯诺克区 | 斯诺克区 |
| MAHJONG | 麻将房 | 麻将房 |
| KTV | KTV包间 | K包 |
| SPECIAL | 补时长专用 | 补时长 |
| OTHER | 其他区域 | 默认匹配 |
### 1.5 cfg_skill_type - 技能课程类型映射
| 字段 | 类型 | 说明 |
|------|------|------|
| skill_type_id | SERIAL | 映射ID主键 |
| skill_id | BIGINT | 技能ID |
| skill_name | VARCHAR(50) | 技能名称 |
| course_type_code | VARCHAR(10) | 课程类型代码 |
| course_type_name | VARCHAR(20) | 课程类型名称 |
| is_active | BOOLEAN | 是否启用 |
**课程类型:**
- BASE = 基础课/陪打
- BONUS = 附加课/超休
---
## 二、助教维度表
### 2.1 dws_assistant_daily_detail - 助教日度业绩明细
**粒度:** 助教 + 日期
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| site_id | BIGINT | 门店ID |
| tenant_id | BIGINT | 租户ID |
| assistant_id | BIGINT | 助教ID |
| assistant_nickname | VARCHAR(50) | 助教花名 |
| stat_date | DATE | 统计日期 |
| assistant_level_code | INTEGER | 助教等级代码SCD2 as-of |
| assistant_level_name | VARCHAR(20) | 助教等级名称 |
| total_service_count | INTEGER | 总服务次数 |
| base_service_count | INTEGER | 基础课服务次数 |
| bonus_service_count | INTEGER | 附加课服务次数 |
| total_seconds | INTEGER | 总计费时长(秒) |
| base_seconds | INTEGER | 基础课计费时长 |
| bonus_seconds | INTEGER | 附加课计费时长 |
| total_hours | NUMERIC(10,2) | 总计费小时数 |
| base_hours | NUMERIC(10,2) | 基础课小时数 |
| bonus_hours | NUMERIC(10,2) | 附加课小时数 |
| total_ledger_amount | NUMERIC(12,2) | 总计费金额 |
| base_ledger_amount | NUMERIC(12,2) | 基础课计费金额 |
| bonus_ledger_amount | NUMERIC(12,2) | 附加课计费金额 |
| unique_customers | INTEGER | 服务客户数(去重) |
| unique_tables | INTEGER | 服务台桌数(去重) |
| trashed_seconds | INTEGER | 被废除的服务时长 |
| trashed_count | INTEGER | 被废除的服务次数 |
**数据来源:** dwd_assistant_service_log + dwd_assistant_trash_event
### 2.2 dws_assistant_monthly_summary - 助教月度业绩汇总
**粒度:** 助教 + 月份
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| site_id | BIGINT | 门店ID |
| assistant_id | BIGINT | 助教ID |
| stat_month | DATE | 统计月份(月第一天) |
| hire_date | DATE | 入职日期 |
| is_new_hire | BOOLEAN | 是否新入职 |
| work_days | INTEGER | 有服务天数 |
| total_hours | NUMERIC(10,2) | 总计费小时数 |
| base_hours | NUMERIC(10,2) | 基础课小时数 |
| bonus_hours | NUMERIC(10,2) | 附加课小时数 |
| effective_hours | NUMERIC(10,2) | 有效业绩小时数 |
| trashed_hours | NUMERIC(10,2) | 被废除小时数 |
| tier_id | INTEGER | 档位ID |
| tier_code | VARCHAR(20) | 档位代码 |
| tier_name | VARCHAR(50) | 档位名称 |
| rank_by_hours | INTEGER | 月度排名 |
| rank_with_ties | INTEGER | 考虑并列的排名 |
**业务规则:**
- 有效业绩 = total_hours - trashed_hours
- 新入职判断:入职日期 >= 月1日0点
- 排名按effective_hours降序并列都算
### 2.3 dws_assistant_customer_stats - 助教服务客户统计
**粒度:** 助教 + 客户 + 统计日期
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| assistant_id | BIGINT | 助教ID |
| member_id | BIGINT | 客户ID |
| stat_date | DATE | 统计基准日期 |
| first_service_date | DATE | 首次服务日期 |
| last_service_date | DATE | 最近服务日期 |
| total_service_count | INTEGER | 累计服务次数 |
| total_service_hours | NUMERIC(10,2) | 累计服务小时数 |
| service_count_7d | INTEGER | 近7天服务次数 |
| service_count_30d | INTEGER | 近30天服务次数 |
| service_count_90d | INTEGER | 近90天服务次数 |
| is_active_7d | BOOLEAN | 近7天是否活跃 |
| is_active_30d | BOOLEAN | 近30天是否活跃 |
**业务规则:**
- 散客member_id=0不进入此表
- 滚动窗口7/10/15/30/60/90天
### 2.4 dws_assistant_salary_calc - 助教工资计算详情
**粒度:** 助教 + 工资月份
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| assistant_id | BIGINT | 助教ID |
| assistant_nickname | VARCHAR(50) | 助教花名 |
| salary_month | DATE | 工资月份(月第一天) |
| assistant_level_code | INTEGER | 助教等级代码8/10/20/30/40 |
| assistant_level_name | VARCHAR(20) | 助教等级名称 |
| hire_date | DATE | 入职日期 |
| is_new_hire | BOOLEAN | 是否新入职 |
| effective_hours | NUMERIC(10,2) | 有效业绩小时数(基础课+附加课-废除) |
| base_hours | NUMERIC(10,2) | 基础课/专业课小时数 |
| bonus_hours | NUMERIC(10,2) | 附加课/打赏课小时数 |
| tier_id | INTEGER | 档位ID |
| tier_code | VARCHAR(20) | 档位代码T0-T5/NEW |
| tier_name | VARCHAR(50) | 档位名称 |
| rank_with_ties | INTEGER | 月度排名考虑并列用于Top3奖金 |
| base_course_price | NUMERIC(10,2) | 基础课客户支付价格98/108/118/138 |
| bonus_course_price | NUMERIC(10,2) | 附加课客户支付价格固定190 |
| base_deduction | NUMERIC(10,2) | 专业课抽成(元/小时),档位决定 |
| bonus_deduction_ratio | NUMERIC(5,4) | 打赏课抽成比例0-1档位决定 |
| base_income | NUMERIC(12,2) | 基础课收入 |
| bonus_income | NUMERIC(12,2) | 附加课收入 |
| total_course_income | NUMERIC(12,2) | 课时收入合计 |
| sprint_bonus | NUMERIC(12,2) | 冲刺奖金H>=190:300, H>=220:800 |
| top_rank_bonus | NUMERIC(12,2) | Top3排名奖金1st:1000, 2nd:600, 3rd:400 |
| recharge_commission | NUMERIC(12,2) | 充值提成 |
| other_bonus | NUMERIC(12,2) | 其他奖金(手动调整) |
| total_bonus | NUMERIC(12,2) | 奖金合计 |
| gross_salary | NUMERIC(12,2) | 应发工资 |
| vacation_days | INTEGER | 次月可休假天数 |
| vacation_unlimited | BOOLEAN | 休假自由标记5档为TRUE |
| calc_notes | TEXT | 计算备注(异常说明等) |
**工资计算公式来自DWS数据库处理需求.md**
```
基础课收入 = 基础课小时数 × (客户支付价格 - 专业课抽成)
附加课收入 = 附加课小时数 × 190 × (1 - 打赏课抽成比例)
应发工资 = 课时收入 + 奖金
```
**计算示例中级助教185小时3档**
- 基础课170小时: 170 × (108 - 13) = 16,150元
- 附加课15小时: 15 × 190 × (1 - 0.35) = 1,852.5元
- 课时收入: 18,002.5元
- 冲刺奖金H≥190未达到: 0元
- 应发工资: 18,002.5元
---
## 三、客户维度表
### 3.1 dws_member_consumption_summary - 会员消费汇总
**粒度:** 会员 + 统计日期
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| member_id | BIGINT | 会员ID |
| stat_date | DATE | 统计基准日期 |
| first_consume_date | DATE | 首次消费日期 |
| last_consume_date | DATE | 最近消费日期 |
| total_visit_count | INTEGER | 累计到店次数 |
| total_consume_amount | NUMERIC(14,2) | 累计消费金额 |
| visit_count_7d | INTEGER | 近7天到店次数 |
| visit_count_30d | INTEGER | 近30天到店次数 |
| consume_amount_30d | NUMERIC(14,2) | 近30天消费金额 |
| cash_card_balance | NUMERIC(14,2) | 储值卡余额 |
| gift_card_balance | NUMERIC(14,2) | 赠送卡余额 |
| customer_tier | VARCHAR(20) | 客户分层 |
**客户分层规则:**
- 高价值90天内消费>=3次 且 消费金额>=1000
- 中等30天内有消费
- 低活跃90天内有消费但30天内无消费
- 流失90天内无消费
### 3.2 dws_member_visit_detail - 会员来店明细
**粒度:** 会员 + 订单
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| member_id | BIGINT | 会员ID |
| order_settle_id | BIGINT | 结账单ID |
| visit_date | DATE | 来店日期 |
| table_name | VARCHAR(50) | 台桌名称 |
| area_category | VARCHAR(20) | 区域分类 |
| table_fee | NUMERIC(12,2) | 台费 |
| goods_amount | NUMERIC(12,2) | 商品金额 |
| assistant_amount | NUMERIC(12,2) | 助教服务金额 |
| total_consume | NUMERIC(12,2) | 消费总额 |
| actual_pay | NUMERIC(12,2) | 实付金额 |
| assistant_services | JSONB | 助教服务明细JSON |
---
## 四、财务维度表
### 4.1 dws_finance_daily_summary - 财务日度汇总
**粒度:** 日期
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| stat_date | DATE | 统计日期 |
| gross_amount | NUMERIC(14,2) | 发生额合计 |
| table_fee_amount | NUMERIC(14,2) | 台费正价 |
| goods_amount | NUMERIC(14,2) | 商品正价 |
| assistant_pd_amount | NUMERIC(14,2) | 助教基础课正价 |
| assistant_cx_amount | NUMERIC(14,2) | 助教激励课正价 |
| discount_total | NUMERIC(14,2) | 优惠合计 |
| discount_groupbuy | NUMERIC(14,2) | 团购优惠 |
| discount_vip | NUMERIC(14,2) | 会员折扣 |
| discount_gift_card | NUMERIC(14,2) | 赠送卡抵扣 |
| discount_manual | NUMERIC(14,2) | 手动调整 |
| discount_rounding | NUMERIC(14,2) | 抹零 |
| discount_other | NUMERIC(14,2) | 其他优惠(手动调整拆分) |
| confirmed_income | NUMERIC(14,2) | 确认收入 |
| cash_inflow_total | NUMERIC(14,2) | 现金流入合计 |
| cash_pay_amount | NUMERIC(14,2) | 收银实付 |
| groupbuy_pay_amount | NUMERIC(14,2) | 团购支付金额 |
| platform_settlement_amount | NUMERIC(14,2) | 平台回款金额 |
| platform_fee_amount | NUMERIC(14,2) | 平台服务费+佣金 |
| recharge_cash_inflow | NUMERIC(14,2) | 充值现金流入 |
| card_consume_total | NUMERIC(14,2) | 卡消费合计 |
| cash_card_consume | NUMERIC(14,2) | 储值卡消费 |
| gift_card_consume | NUMERIC(14,2) | 赠送卡消费 |
| cash_outflow_total | NUMERIC(14,2) | 现金流出合计 |
| cash_balance_change | NUMERIC(14,2) | 现金结余 |
| recharge_count | INTEGER | 充值笔数 |
| recharge_total | NUMERIC(14,2) | 充值总额 |
| recharge_cash | NUMERIC(14,2) | 充值现金部分 |
| recharge_gift | NUMERIC(14,2) | 充值赠送部分 |
| first_recharge_count | INTEGER | 首充笔数 |
| renewal_count | INTEGER | 续充笔数 |
| order_count | INTEGER | 结账单数 |
| member_order_count | INTEGER | 会员订单数 |
| guest_order_count | INTEGER | 散客订单数 |
| avg_order_amount | NUMERIC(12,2) | 平均客单价 |
**计算公式:**
- 发生额 = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money
- 团购支付金额 = pl_coupon_sale_amount > 0 ? pl_coupon_sale_amount : groupbuy_redemption.ledger_unit_price
- 团购优惠 = coupon_amount - 团购支付金额
- 优惠合计 = 团购优惠 + 会员折扣 + 赠送卡抵扣 + 手动调整 + 抹零
- 其他优惠 = adjust_amount - 大客户优惠不足0按0处理
- 确认收入 = 发生额 - 优惠合计
- 平台回款金额 = dws_platform_settlement.settlement_amount若无导入则使用团购支付金额
- 平台服务费 = commission_amount + service_fee
- 现金流入合计 = 收银实付 + 平台回款金额 + 充值现金流入
- 现金流出合计 = 支出汇总 + 平台服务费
- 现金结余 = 现金流入合计 - 现金流出合计
**财务指标数据来源矩阵(字段 → 来源 → 口径)**
| 字段 | 来源表 | 口径说明 |
|------|--------|----------|
| gross_amount | dwd_settlement_head | table_charge_money + goods_money + assistant_pd_money + assistant_cx_money |
| discount_groupbuy | dwd_settlement_head + dwd_groupbuy_redemption | coupon_amount - 团购支付金额 |
| discount_vip | dwd_settlement_head | member_discount_amount |
| discount_gift_card | dwd_settlement_head | gift_card_amount |
| discount_manual | dwd_settlement_head | adjust_amount手动调整总额 |
| discount_rounding | dwd_settlement_head | rounding_amount |
| discount_other | dwd_settlement_head | adjust_amount - 大客户优惠(配置映射) |
| confirmed_income | dwd_settlement_head | gross_amount - discount_total |
| cash_pay_amount | dwd_settlement_head | pay_amount收银实付 |
| groupbuy_pay_amount | dwd_settlement_head + dwd_groupbuy_redemption | pl_coupon_sale_amount 或 ledger_unit_price |
| platform_settlement_amount | dws_platform_settlement | settlement_amountExcel导入 |
| platform_fee_amount | dws_platform_settlement | commission_amount + service_fee |
| recharge_cash_inflow | dwd_recharge_order | pay_money现金充值 |
| cash_inflow_total | dwd_settlement_head + dws_platform_settlement + dwd_recharge_order | 收银实付 + 平台回款 + 充值现金 |
| cash_outflow_total | dws_finance_expense_summary + dws_platform_settlement | 支出汇总 + 平台服务费 |
| cash_balance_change | dws_finance_daily_summary | cash_inflow_total - cash_outflow_total |
### 4.2 dws_finance_recharge_summary - 充值统计
**粒度:** 日期
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| stat_date | DATE | 统计日期 |
| recharge_count | INTEGER | 充值笔数 |
| recharge_total | NUMERIC(14,2) | 充值总额(含赠送) |
| recharge_cash | NUMERIC(14,2) | 现金充值金额 |
| recharge_gift | NUMERIC(14,2) | 赠送金额 |
| first_recharge_count | INTEGER | 首充笔数 |
| first_recharge_cash | NUMERIC(14,2) | 首充现金 |
| renewal_count | INTEGER | 续充笔数 |
| renewal_cash | NUMERIC(14,2) | 续充现金 |
| cash_card_balance | NUMERIC(14,2) | 储值卡余额 |
| gift_card_balance | NUMERIC(14,2) | 赠送卡余额 |
**数据来源:** dwd_recharge_orderis_first字段区分首充/续充)
### 4.3 dws_finance_income_structure - 收入结构分析
**粒度:** 日期 + 结构类型 + 分类
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| stat_date | DATE | 统计日期 |
| structure_type | VARCHAR(20) | 结构类型INCOME_TYPE/AREA |
| category_code | VARCHAR(30) | 分类代码 |
| category_name | VARCHAR(50) | 分类名称 |
| income_amount | NUMERIC(14,2) | 收入金额 |
| income_ratio | NUMERIC(5,4) | 收入占比 |
| order_count | INTEGER | 订单数 |
| duration_minutes | INTEGER | 时长(分钟) |
**结构类型说明:**
1. **INCOME_TYPE按收入类型**
- TABLE_FEE = 台费收入
- GOODS = 商品收入
- ASSISTANT_BASE = 助教基础课
- ASSISTANT_BONUS = 助教附加课
2. **AREA按区域**
- 使用cfg_area_category映射BILLIARD/BILLIARD_VIP/SNOOKER/MAHJONG/KTV/OTHER
**数据来源:** dwd_settlement_head, dwd_table_fee_log, dwd_assistant_service_log
### 4.4 dws_finance_discount_detail - 优惠明细
**粒度:** 日期 + 优惠类型
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| stat_date | DATE | 统计日期 |
| discount_type_code | VARCHAR(30) | 优惠类型代码 |
| discount_type_name | VARCHAR(50) | 优惠类型名称 |
| discount_amount | NUMERIC(14,2) | 优惠金额 |
| discount_ratio | NUMERIC(5,4) | 优惠占比(占总优惠) |
| usage_count | INTEGER | 使用次数 |
| affected_orders | INTEGER | 影响订单数 |
**优惠类型:**
- GROUPBUY = 团购优惠coupon_amount - 团购实付金额)
- VIP = 会员折扣member_discount_amount
- GIFT_CARD = 赠送卡抵扣gift_card_amount
- ROUNDING = 抹零rounding_amount
- BIG_CUSTOMER = 大客户优惠(基于配置映射的手动调整)
- OTHER = 其他优惠(手动调整中除大客户外部分)
**数据来源:** dwd_settlement_head, dwd_groupbuy_redemption
### 4.5 dws_finance_expense_summary - 支出结构Excel导入
**粒度:** 月份 + 支出类型
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| expense_month | DATE | 支出月份 |
| expense_type_code | VARCHAR(30) | 支出类型代码 |
| expense_type_name | VARCHAR(50) | 支出类型名称 |
| expense_category | VARCHAR(20) | 支出大类 |
| expense_amount | NUMERIC(14,2) | 支出金额 |
| import_batch_no | VARCHAR(50) | 导入批次号 |
**支出类型:**
- RENT = 房租
- UTILITY = 水电费
- PROPERTY = 物业费
- SALARY = 工资
- REIMBURSE = 报销
- PLATFORM_FEE = 平台服务费
- OTHER = 其他
### 4.6 dws_platform_settlement - 平台回款Excel导入
**粒度:** 回款日期 + 平台 + 订单
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| settlement_date | DATE | 回款日期 |
| platform_type | VARCHAR(30) | 平台类型 |
| platform_name | VARCHAR(50) | 平台名称 |
| platform_order_no | VARCHAR(100) | 平台订单号 |
| order_settle_id | BIGINT | 关联的结账单ID |
| settlement_amount | NUMERIC(14,2) | 回款金额 |
| commission_amount | NUMERIC(14,2) | 佣金 |
| service_fee | NUMERIC(14,2) | 服务费 |
| gross_amount | NUMERIC(14,2) | 订单原始金额 |
| import_batch_no | VARCHAR(50) | 导入批次号 |
---
## 五、时间分层机制
### 5.1 时间口径定义
| 时间窗口 | 说明 | 边界规则 |
|----------|------|----------|
| 本周 | 从本周一到今天 | 周起始日为周一 |
| 上周 | 上周一到上周日 | 完整7天 |
| 本月 | 从月1日到今天 | 月第一天0点起 |
| 上月 | 上月完整月份 | 完整自然月 |
| 前3个月不含本月 | 三个月前月初到上月末 | 不含当前月 |
| 前3个月含本月 | 两个月前月初到今天 | 含当前月 |
| 本季度 | 季度第一月1日到今天 | 季度起始 |
| 上季度 | 上季度完整三个月 | 完整自然季 |
| 最近半年 | 往前6个月不含本月 | 不含当前月 |
### 5.2 滚动窗口
支持以下滚动窗口统计:
- 近7天
- 近10天
- 近15天
- 近30天
- 近60天
- 近90天
### 5.3 环比计算
环比规则:对比上一个等长区间
- 如查询1月1日-1月15日环比为12月17日-12月31日
---
## 六、数据更新策略
| 表类型 | 更新频率 | 幂等方式 |
|--------|----------|----------|
| 日度明细表 | 每小时 | delete-before-insert按日期窗口 |
| 日度汇总表 | 每小时 | delete-before-insert按日期 |
| 月度汇总表 | 每日 | delete-before-insert按月份 |
| 客户统计表 | 每日 | delete-before-insert按统计日期 |
| Excel导入表 | 手动 | 按import_batch_no去重 |

View File

@@ -0,0 +1,494 @@
## 1. 客户召回表
| 客户姓名 | 召回指数 |
|----------|----------|
| 陈腾鑫 | 10.00 |
| 章先生 | 10.00 |
| 孙总 | 10.00 |
| 梅 | 10.00 |
| 胡先生 | 10.00 |
| 黄先生 | 9.63 |
| 小熊 | 9.52 |
| 周先生 | 9.41 |
| 李先生 | 9.39 |
| 王 | 9.27 |
| 张无忌 | 9.20 |
| 黄先生 | 8.96 |
| 陈德韩 | 8.94 |
| 胡总 | 8.93 |
| T | 8.89 |
| 候 | 8.88 |
| 孙先生 | 8.87 |
| 王先生 | 8.86 |
| 清 | 8.86 |
| amy | 8.84 |
| 林先生 | 8.84 |
| 张先生 | 8.79 |
| 刘先生 | 8.79 |
| 黄国磊 | 8.79 |
| 游 | 8.79 |
| 陈先生 | 8.79 |
| 陈 | 8.79 |
| 大G | 8.79 |
| 李先生 | 8.79 |
| 孙启明 | 8.79 |
| 陈先生 | 8.79 |
| 罗先生 | 8.79 |
| 刘哥 | 8.79 |
| 杨 | 8.79 |
| 枫先生 | 8.79 |
| 老宋 | 8.79 |
| 黄先生 | 8.79 |
| 刘女士 | 8.79 |
| 彭先生 | 8.79 |
| 李 | 8.79 |
| 桂先生 | 8.79 |
| 王先生 | 8.79 |
| 潘先生 | 8.79 |
| 方先生 | 8.79 |
| 郑先生 | 8.79 |
| 阿亮 | 8.79 |
| 孟紫龙 | 8.79 |
| 林总 | 8.78 |
| 林志铭 | 8.64 |
| 罗超 | 8.63 |
| 张丹逸 | 8.52 |
| 谢俊 | 8.07 |
| 王龙 | 7.80 |
| 唐先生 | 7.79 |
| 周周 | 7.47 |
| 曾巧明 | 6.83 |
| 昌哥 | 6.17 |
| 江先生 | 5.84 |
| 袁 | 5.24 |
| 蔡总 | 4.73 |
| 胡先生 | 4.51 |
| 陈先生 | 4.45 |
| 明哥 | 3.92 |
| 公孙先生 | 3.57 |
| 曾先生 | 3.47 |
| 黄生 | 3.46 |
| 葛先生 | 3.35 |
| 轩哥 | 3.32 |
| 张先生 | 2.73 |
| 叶先生 | 2.61 |
| 小燕 | 2.39 |
| 罗先生 | 2.38 |
| 李先生 | 2.23 |
| 陈淑涛 | 2.23 |
| 肖先生 | 2.23 |
| 范先生 | 2.14 |
| 常总 | 1.47 |
| 董贝 | 1.04 |
| 陈小姐 | 1.04 |
| 林先生 | 0.90 |
| 柳先生 | 0.61 |
| 林先生 | 0.20 |
| 潘先生 | 0.20 |
| 曾丹烨 | 0.07 |
| 魏先生 | 0.00 |
| 艾宇民 | 0.00 |
| 吴生 | 0.00 |
| 卢广贤 | 0.00 |
| 陈泽斌 | 0.00 |
| 李先生 | 0.00 |
共 90 条记录
## 2. 助教客户关系表
| 助教花名 | 客户姓名 | 关系指数 |
|----------|----------|----------|
| 卡顿 | 葛先生 | 10.00 |
| 小燕 | 葛先生 | 10.00 |
| 七七 | 轩哥 | 10.00 |
| 佳怡 | 罗先生 | 10.00 |
| 璇子 | 轩哥 | 10.00 |
| 阿清 | 张先生 | 10.00 |
| 璇子 | 江先生 | 10.00 |
| CC | 周周 | 10.00 |
| 周周 | 周周 | 10.00 |
| 小燕 | 小燕 | 10.00 |
| 卡顿 | 小燕 | 10.00 |
| 姜姜 | 张先生 | 10.00 |
| 小侯 | 张先生 | 10.00 |
| 渔渔 | 张先生 | 10.00 |
| 欣欣 | 张先生 | 10.00 |
| 千千 | 张先生 | 10.00 |
| 小A | 张先生 | 10.00 |
| 甜甜 | 张先生 | 10.00 |
| 小A | 周先生 | 10.00 |
| 欣欣 | 周先生 | 10.00 |
| 千千 | 周先生 | 10.00 |
| 甜甜 | 周先生 | 10.00 |
| 涛涛 | 蔡总 | 10.00 |
| 婉婉 | 吴先生 | 10.00 |
| 千千 | 梅 | 10.00 |
| 甜甜 | 梅 | 10.00 |
| 小A | 梅 | 10.00 |
| 欣欣 | 梅 | 10.00 |
| 球球 | 周周 | 10.00 |
| 涛涛 | 轩哥 | 10.00 |
| 小不点 | 周周 | 10.00 |
| 小柔 | 蔡总 | 10.00 |
| 年糕 | 葛先生 | 10.00 |
| 佳怡 | 陈腾鑫 | 10.00 |
| 小不点 | 罗先生 | 10.00 |
| 球球 | 罗先生 | 10.00 |
| 小柔 | 轩哥 | 10.00 |
| 阿清 | 梅 | 10.00 |
| 阿清 | 胡先生 | 10.00 |
| 佳怡 | 陈先生 | 10.00 |
| 小不点 | 轩哥 | 10.00 |
| 佳怡 | 小熊 | 10.00 |
| 球球 | 轩哥 | 10.00 |
| 阿清 | 孙总 | 10.00 |
| CC | 罗先生 | 10.00 |
| 周周 | 罗先生 | 10.00 |
| 小柔 | 明哥 | 9.96 |
| 渔渔 | 李先生 | 9.88 |
| 姜姜 | 李先生 | 9.88 |
| 小侯 | 李先生 | 9.88 |
| 年糕 | 常总 | 9.85 |
| 婉婉 | 明哥 | 9.61 |
| 乔西 | 陈先生 | 9.59 |
| 璇子 | 蔡总 | 9.46 |
| CC | 常总 | 9.40 |
| 周周 | 常总 | 9.40 |
| 七七 | 蔡总 | 9.24 |
| 甜甜 | 孙总 | 9.24 |
| 小A | 孙总 | 9.24 |
| 欣欣 | 孙总 | 9.24 |
| 千千 | 孙总 | 9.24 |
| 七七 | 胡先生 | 9.20 |
| 千千 | 小熊 | 9.02 |
| 甜甜 | 小熊 | 9.02 |
| 欣欣 | 小熊 | 9.02 |
| 小A | 小熊 | 9.02 |
| 佳怡 | 胡先生 | 9.02 |
| 涛涛 | 小燕 | 8.66 |
| 阿清 | 轩哥 | 8.53 |
| 年糕 | 叶先生 | 8.51 |
| 小不点 | 张先生 | 8.39 |
| 球球 | 张先生 | 8.39 |
| 阿清 | 葛先生 | 8.36 |
| 周周 | 张先生 | 8.30 |
| CC | 张先生 | 8.30 |
| 甜甜 | 胡先生 | 8.04 |
| 千千 | 胡先生 | 8.04 |
| 欣欣 | 胡先生 | 8.04 |
| 小A | 胡先生 | 8.04 |
| 小不点 | 小熊 | 7.88 |
| 球球 | 小熊 | 7.88 |
| 小侯 | 胡先生 | 7.86 |
| 姜姜 | 胡先生 | 7.86 |
| 渔渔 | 胡先生 | 7.86 |
| 乔西 | 罗先生 | 7.86 |
| 小不点 | 胡先生 | 7.65 |
| 球球 | 胡先生 | 7.65 |
| 球球 | 孙总 | 7.56 |
| 小不点 | 孙总 | 7.56 |
| 璇子 | 孙总 | 7.46 |
| 阿清 | 清 | 7.35 |
| 小A | 小燕 | 7.09 |
| 甜甜 | 小燕 | 7.09 |
| 欣欣 | 小燕 | 7.09 |
| 千千 | 小燕 | 7.09 |
| 甜甜 | 公孙先生 | 7.07 |
| 千千 | 公孙先生 | 7.07 |
| 欣欣 | 公孙先生 | 7.07 |
| 小A | 公孙先生 | 7.07 |
| 婉婉 | 孙总 | 7.03 |
| 菲菲 | 陈腾鑫 | 6.92 |
| 橙子 | 陈腾鑫 | 6.92 |
| 希希 | 陈腾鑫 | 6.92 |
| 婉婉 | 章先生 | 6.91 |
| 婉婉 | 公孙先生 | 6.86 |
| CC | 林先生 | 6.77 |
| 周周 | 林先生 | 6.77 |
| 阿清 | 小燕 | 6.76 |
| 苏苏 | 蔡总 | 6.64 |
| 七七 | 小燕 | 6.60 |
| 小不点 | 江先生 | 6.59 |
| 球球 | 江先生 | 6.59 |
| 涛涛 | 罗先生 | 6.52 |
| 凤梨 | 葛先生 | 6.51 |
| 佳怡 | 轩哥 | 6.49 |
| 年糕 | 轩哥 | 6.44 |
| 年糕 | 小燕 | 6.43 |
| CC | 轩哥 | 6.26 |
| 周周 | 轩哥 | 6.26 |
| yy | 公孙先生 | 6.13 |
| 阿清 | 陈腾鑫 | 6.04 |
| 佳怡 | 周周 | 6.03 |
| 七七 | 江先生 | 5.93 |
| CC | 林先生 | 5.87 |
| 周周 | 林先生 | 5.87 |
| 年糕 | 王 | 5.76 |
| 年糕 | 李先生 | 5.72 |
| 七七 | 孙总 | 5.69 |
| 苏苏 | 黄先生 | 5.66 |
| 婉婉 | 叶先生 | 5.55 |
| 涛涛 | 叶先生 | 5.55 |
| 凤梨 | 叶先生 | 5.54 |
| 小A | 黄先生 | 5.53 |
| 甜甜 | 黄先生 | 5.53 |
| 千千 | 黄先生 | 5.53 |
| 欣欣 | 黄先生 | 5.53 |
| yy | 叶先生 | 5.53 |
| 苏苏 | 罗先生 | 5.48 |
| 小侯 | 葛先生 | 5.47 |
| 渔渔 | 葛先生 | 5.47 |
| 姜姜 | 葛先生 | 5.47 |
| 佳怡 | 林志铭 | 5.45 |
| 婉婉 | 葛先生 | 5.37 |
| CC | 小熊 | 5.29 |
| 周周 | 小熊 | 5.29 |
| 涛涛 | 孙总 | 5.20 |
| 小敌 | 李先生 | 5.09 |
| 吱吱 | 李先生 | 5.09 |
| 周周 | 葛先生 | 5.08 |
| CC | 葛先生 | 5.08 |
| 甜甜 | 蔡总 | 5.04 |
| 千千 | 蔡总 | 5.04 |
| 欣欣 | 蔡总 | 5.04 |
| 小A | 蔡总 | 5.04 |
| 婉婉 | 轩哥 | 5.03 |
| 年糕 | 胡先生 | 5.02 |
| 吱吱 | 葛先生 | 4.88 |
| 小敌 | 葛先生 | 4.88 |
| 婉婉 | 王 | 4.87 |
| yy | 张先生 | 4.66 |
| 璇子 | 罗先生 | 4.65 |
| yy | 葛先生 | 4.59 |
| 苏苏 | 柳先生 | 4.58 |
| 乔西 | 蔡总 | 4.50 |
| 七七 | 张先生 | 4.36 |
| 乔西 | 葛先生 | 4.33 |
| 乔西 | 小熊 | 4.33 |
| 周周 | 江先生 | 4.32 |
| CC | 江先生 | 4.32 |
| Amy | 轩哥 | 4.31 |
| 年糕 | 罗超 | 4.20 |
| yy | 林志铭 | 4.19 |
| 年糕 | 艾宇民 | 4.16 |
| 阿清 | 黄先生 | 4.14 |
| 七七 | 罗超 | 4.12 |
| 年糕 | 范先生 | 4.08 |
| 凤梨 | 林先生 | 4.07 |
| 璇子 | 张先生 | 4.06 |
| 球球 | 常总 | 4.05 |
| 小不点 | 常总 | 4.05 |
| yy | 孙总 | 3.99 |
| 七七 | 葛先生 | 3.93 |
| 乔西 | 轩哥 | 3.90 |
| 年糕 | 小熊 | 3.85 |
| 千千 | 李先生 | 3.73 |
| 欣欣 | 李先生 | 3.73 |
| 小A | 李先生 | 3.73 |
| 甜甜 | 李先生 | 3.73 |
| 姜姜 | 轩哥 | 3.62 |
| 渔渔 | 轩哥 | 3.62 |
| 小侯 | 轩哥 | 3.62 |
| 迟迟 | 轩哥 | 3.60 |
| 泡芙 | 轩哥 | 3.60 |
| 小琳 | 轩哥 | 3.60 |
| 七七 | 罗先生 | 3.57 |
| 年糕 | 胡总 | 3.47 |
| 欣欣 | 葛先生 | 3.43 |
| 甜甜 | 葛先生 | 3.43 |
| 千千 | 葛先生 | 3.43 |
| 小A | 葛先生 | 3.43 |
| 七七 | 林总 | 3.43 |
| 乔西 | 陈德韩 | 3.38 |
| 泡芙 | 林总 | 3.31 |
| 迟迟 | 林总 | 3.31 |
| 小琳 | 林总 | 3.31 |
| 涛涛 | 葛先生 | 3.27 |
| 阿清 | 罗先生 | 3.16 |
| 璇子 | 周周 | 3.16 |
| 阿清 | 王先生 | 3.14 |
| 小柳 | 轩哥 | 3.06 |
| 迟迟 | 陈腾鑫 | 3.04 |
| 小琳 | 陈腾鑫 | 3.04 |
| 泡芙 | 陈腾鑫 | 3.04 |
| 瑶瑶 | 蔡总 | 2.92 |
| 图图 | 蔡总 | 2.92 |
| 小A | 轩哥 | 2.91 |
| 千千 | 轩哥 | 2.91 |
| 欣欣 | 轩哥 | 2.91 |
| 甜甜 | 轩哥 | 2.91 |
| 年糕 | 罗先生 | 2.84 |
| 小不点 | 黄先生 | 2.73 |
| 球球 | 黄先生 | 2.73 |
| 渔渔 | 梅 | 2.72 |
| 姜姜 | 梅 | 2.72 |
| 小侯 | 梅 | 2.72 |
| 欣欣 | 陈先生 | 2.68 |
| 千千 | 陈先生 | 2.68 |
| 小A | 陈先生 | 2.68 |
| 甜甜 | 陈先生 | 2.68 |
| 婉婉 | 江先生 | 2.67 |
| 千千 | 枫先生 | 2.67 |
| 欣欣 | 枫先生 | 2.67 |
| 小A | 枫先生 | 2.67 |
| 甜甜 | 枫先生 | 2.67 |
| 阿清 | 枫先生 | 2.67 |
| 乔西 | 张无忌 | 2.55 |
| 甜甜 | 范先生 | 2.51 |
| 千千 | 范先生 | 2.51 |
| 小A | 范先生 | 2.51 |
| 欣欣 | 范先生 | 2.51 |
| 七七 | 林先生 | 2.45 |
| CC | T | 2.36 |
| 周周 | T | 2.36 |
| 苏苏 | 周周 | 2.36 |
| 小侯 | 周先生 | 2.28 |
| 渔渔 | 周先生 | 2.28 |
| 姜姜 | 周先生 | 2.28 |
| 涛涛 | 胡总 | 2.28 |
| 苏苏 | 林先生 | 2.14 |
| 渔渔 | 彭先生 | 2.07 |
| 小侯 | 彭先生 | 2.07 |
| 姜姜 | 彭先生 | 2.07 |
| 小侯 | 清 | 2.03 |
| 甜甜 | 清 | 2.03 |
| 小A | 清 | 2.03 |
| 欣欣 | 清 | 2.03 |
| 千千 | 清 | 2.03 |
| 渔渔 | 清 | 2.03 |
| 姜姜 | 清 | 2.03 |
| 苏苏 | 张先生 | 1.94 |
| 千千 | 林总 | 1.88 |
| 甜甜 | 林总 | 1.88 |
| 欣欣 | 林总 | 1.88 |
| 小A | 林总 | 1.88 |
| 甜甜 | 陈腾鑫 | 1.82 |
| 欣欣 | 陈腾鑫 | 1.82 |
| 千千 | 陈腾鑫 | 1.82 |
| 小A | 陈腾鑫 | 1.82 |
| 佳怡 | 彭先生 | 1.80 |
| 婉婉 | 周先生 | 1.77 |
| 苏苏 | 周先生 | 1.68 |
| CC | 昌哥 | 1.64 |
| 周周 | 昌哥 | 1.64 |
| 球球 | 蔡总 | 1.57 |
| 小不点 | 蔡总 | 1.57 |
| 苏苏 | 李先生 | 1.53 |
| 吱吱 | 李先生 | 1.50 |
| 小敌 | 李先生 | 1.50 |
| 婉婉 | 刘哥 | 1.46 |
| CC | 林总 | 1.39 |
| 周周 | 林总 | 1.39 |
| 小不点 | T | 1.38 |
| 球球 | T | 1.38 |
| 悠悠 | 张先生 | 1.38 |
| 布丁 | 张先生 | 1.38 |
| 小怡 | 周先生 | 1.37 |
| 雯雯 | 周先生 | 1.37 |
| 素素 | 周先生 | 1.37 |
| 嘉嘉 | 轩哥 | 1.31 |
| 小柔 | 葛先生 | 1.30 |
| 乔西 | 张先生 | 1.29 |
| 小不点 | 候 | 1.23 |
| 球球 | 候 | 1.23 |
| 嘉嘉 | 罗先生 | 1.22 |
| 小侯 | T | 1.19 |
| 渔渔 | T | 1.19 |
| 姜姜 | T | 1.19 |
| 小侯 | 黄先生 | 1.19 |
| 小敌 | 林先生 | 1.19 |
| 姜姜 | 黄先生 | 1.19 |
| 吱吱 | 林先生 | 1.19 |
| 渔渔 | 黄先生 | 1.19 |
| 球球 | 葛先生 | 1.16 |
| 小不点 | 葛先生 | 1.16 |
| Amy | amy | 1.15 |
| 乔西 | T | 1.12 |
| 球球 | 老宋 | 1.10 |
| 小不点 | 老宋 | 1.10 |
| 乔西 | 林先生 | 1.01 |
| 素素 | 张先生 | 0.98 |
| 小怡 | 张先生 | 0.98 |
| 雯雯 | 张先生 | 0.98 |
| 佳怡 | T | 0.96 |
| 年糕 | 张先生 | 0.94 |
| 小侯 | 陈腾鑫 | 0.88 |
| 渔渔 | 陈腾鑫 | 0.88 |
| 姜姜 | 陈腾鑫 | 0.88 |
| 阿清 | 李先生 | 0.85 |
| 球球 | 林总 | 0.83 |
| 小不点 | 林总 | 0.83 |
| 婉婉 | 常总 | 0.77 |
| 小侯 | 艾宇民 | 0.76 |
| 姜姜 | 艾宇民 | 0.76 |
| 渔渔 | 艾宇民 | 0.76 |
| 小敌 | 郑先生 | 0.74 |
| 吱吱 | 郑先生 | 0.74 |
| 千千 | 罗先生 | 0.72 |
| 甜甜 | 罗先生 | 0.72 |
| 小A | 罗先生 | 0.72 |
| 欣欣 | 罗先生 | 0.72 |
| 球球 | 小燕 | 0.67 |
| 小不点 | 小燕 | 0.67 |
| 年糕 | 周先生 | 0.65 |
| 卡顿 | 罗先生 | 0.62 |
| 小燕 | 罗先生 | 0.62 |
| 小敌 | 刘哥 | 0.60 |
| 吱吱 | 刘哥 | 0.60 |
| 小柔 | 孟紫龙 | 0.56 |
| 阿清 | 候 | 0.54 |
| 乔西 | 候 | 0.49 |
| 小敌 | 张先生 | 0.46 |
| 甜甜 | T | 0.46 |
| 小A | T | 0.46 |
| 欣欣 | T | 0.46 |
| 千千 | T | 0.46 |
| 吱吱 | 张先生 | 0.46 |
| 小A | 游 | 0.38 |
| 千千 | 游 | 0.38 |
| 甜甜 | 游 | 0.38 |
| 欣欣 | 游 | 0.38 |
| 苏苏 | 葛先生 | 0.34 |
| 渔渔 | 候 | 0.32 |
| 小侯 | 候 | 0.32 |
| 姜姜 | 候 | 0.32 |
| 苏苏 | T | 0.31 |
| 婉婉 | 罗先生 | 0.26 |
| 涛涛 | 候 | 0.24 |
| 苏苏 | 候 | 0.23 |
| 阿清 | 常总 | 0.23 |
| 小不点 | 李先生 | 0.22 |
| 球球 | 李先生 | 0.22 |
| 小柔 | T | 0.19 |
| 年糕 | 潘先生 | 0.19 |
| 婉婉 | 候 | 0.18 |
| 小柔 | 罗先生 | 0.17 |
| 梦梦 | 葛先生 | 0.14 |
| 欣怡 | 葛先生 | 0.14 |
| 大姚 | 葛先生 | 0.14 |
| 椰子 | 葛先生 | 0.14 |
| 璇子 | 林先生 | 0.11 |
| 年糕 | 明哥 | 0.09 |
| 涛涛 | 张先生 | 0.08 |
| 周周 | 大G | 0.02 |
| 佳怡 | 大G | 0.02 |
| CC | 大G | 0.02 |
| 周周 | 明哥 | 0.00 |
| Amy | 明哥 | 0.00 |
| 小怡 | 叶先生 | 0.00 |
| 乔西 | 林先生 | 0.00 |
| 素素 | 叶先生 | 0.00 |
| 雯雯 | 叶先生 | 0.00 |
| 梦梦 | 蔡总 | 0.00 |
| 欣怡 | 蔡总 | 0.00 |
| 椰子 | 蔡总 | 0.00 |
| 大姚 | 蔡总 | 0.00 |
| 周周 | 游 | 0.00 |
| 小柔 | 昌哥 | 0.00 |
| CC | 游 | 0.00 |
| 佳怡 | 游 | 0.00 |
| CC | 明哥 | 0.00 |
| 小柔 | 江先生 | 0.00 |
共 391 条记录

Binary file not shown.

View File

@@ -0,0 +1,167 @@
# 补充更多信息:
## DWD数据库更新
DWD的数据库若干表中新增了若干表可能会对整个DWS层设计有影响/优化,重新思考可用的字段。
## 支出/成本数据缺失
财务页需要房租、水电、物业、工资、报销、平台服务费等现金支出与“支出结构”DWD 里只有商品成本 dwd_store_goods_sale.cost_money但价格也不对。缺少费用/薪酬/平台服务费等表,导致“现金支出/现金结余/结余率/支出结构”无法落地。
### 更新:
- 这些内容先在数据库结构中预留后期会通过Excel等方式手动导入。
## 平台回款与团购差价口径不足
需求有“平台回款”“团购差价”DWD 只有团购核销/验券记录dwd_groupbuy_redemption/dwd_platform_coupon_redemption没有平台结算/回款/佣金/服务费明细,无法算“平台回款”与“平台服务费”。
### 更新:
- 确认的平台服务费与回款金额先在数据库结构中预留后期会通过Excel等方式手动导入。
## 优惠分类无法分拆
财务页要区分“团购优惠/大客户优惠/赠送卡抵扣/其他优惠”DWD 仅有 member_discount_amount / coupon_amount / adjust_amount / rounding_amount / gift_card_amount / recharge_card_amount 等汇总字段,且没有“大客户”标识或优惠原因维表,无法稳定拆分口径。
### 更新:
- 赠送卡抵扣 指的就是 酒水卡+台费卡+活动抵用券 结账 抵扣的。
- 团购优惠: ledger_amount + assistant_promotion_money - ledger_unit_price
- 大客户优惠和其他优惠就是手动调账产生的优惠订单中的折扣、台桌折扣、商品折扣、手动优惠这几项关系需要确认下找100个样本进行分析
## “发生额/正价”口径不清
- 结账记录中的正价: tableChargeMoney台费正价goodsMoney商品正价assistantPdMoney助教基础课正价assistantCxMoney助教激励课正价
- 团购中的正价ledger_amount(台桌正价) + assistant_promotion_money(助教正价)
- 团购中的核销价ledger_unit_price
## 区域/房型维度不规范
筛选要“大厅A/B/C、麻将房、团建房/包厢”DWD 只有 site_table_area_name 等自由文本,没有规范维表映射,容易导致前端筛选不可控。
### 更新
BD_manual_dim_table.md 中,有台区分布的对应关系
## 充值与赠送卡口径缺口
需求中“储值卡充值实收(首充/续费、不含赠送)”与“赠送卡新增/消费/余额”细分酒水卡/台费卡/抵用券。DWD 里 dwd_recharge_order 没有明确“赠送金额”字段dim_member_card_account / dwd_member_balance_change 仅有卡类型名称,缺少“是否赠送”“卡类别标准枚举”,需要补充规则/维表。
### 更新
- 酒水卡,台费卡活动抵用券,台费卡 是赠送卡 分类在dim_member_card_account 的card_type_id对应的数据库说明书中有介绍。
- 储值卡是充值的“现金卡”
## 助教薪酬规则未闭合
DWS 需求里“充值提成”空缺,且“冲刺奖/额外奖金”重复;没有助教工资/结算流水表,财务页“助教分成/奖惩”无法核算。
### 更新
- 充值提成数据库结构中预留后期会通过Excel等方式手动导入。会记录时间充值金额储值卡卡关联充值提成金额。
- “冲刺奖/额外奖金”重复:按照薪资说明进行相应调整。
- 没有助教工资/结算流水表:为我增加相应的表。满足业务逻辑。
## 时间分层与筛选不匹配
### 更新
- UI 需要“最近半年不含本月、上季度”等时间维度并且满足上葛周期的环比。DWS 分层仅到 3 个月,可能导致查询性能或需要额外聚合层。财务方面需要特殊处理。
## 缺失 DDL
方案里列出的表没有全部给出结构定义,包括 cfg_tier_effective_period、dws_assistant_salary_calc、dws_member_visit_detail、dws_finance_discount_detail、dws_finance_recharge_summary、dws_finance_expense_summary。这些在 DWS_任务计划_v1.md 中仅出现在清单里,但没有 DDL会导致实施阶段卡住。
### 更新
- 补全DLL。
## SCD2 维度取数口径
助教等级在 dws_assistant_monthly_summary 用了 SCD2_is_current=1这是否会把“当前等级”套到历史月份能否满足需求中的“历史月份”统计是否要加一些数据筛选条件是否需按业务时间点做 as-of join基于有效期
## 附加课/基础课口径
方案中用 skill_name 判断“超休/激励/打赏”为附加课但我希望换成skill_id进行枚举避免漏记或误记落在库中可以使用名称。
## 财务指标可追溯口径
dws_finance_daily_summary 已覆盖“发生额/优惠/确认收入/现金流/充值”等字段但缺少“数据来源矩阵”字段→DWD表→公式。财务需求对“发生额(正价)”和“优惠”拆分非常细,需明确“正价”来源(台费价、助教等级价、商品原价)与“优惠”拆分口径(团购差价、大客户折扣、赠送卡抵扣、免单/抹零、手动调整)。
### 更新
- 增加 数据来源矩阵,记录数据的来龙去脉
我觉得还不够全,给你一些我整理的内容。
# 1.2 DWD 核心表与关键字段
还差好多,举例:
## 助教服务相关:
dwd_assistant_service_log
| `order_assistant_type` | 服务类型 | 1=基础课或包厢课, 2=附加课/激励课 | 这个不重要用skill_id判断就好。
另外服务时keh长服务的助教ID与花名客户关联台桌号台桌分类关联等也很重要。
## 客户相关:
客户姓名手机号生日以及关联的会员卡。
## 财务:
还有从结账记录出发关联的台桌流水助教流水
结算路径
充值流水等。
以上是否要补充?
---------------
## 订单获取的字段更新
### 订单各项正价小计
- 台费正价table_charge_money
- 商品正价goods_money
- 助教基础课/陪打正价assistant_pd_money
- 助教激励课/超休正价assistant_cx_money
### 支付信息
- 会员卡支付金额recharge_card_amount。卡类型还要从dwd_settlement_head的order_settle_id 去dwd_member_balance_change表找到卡的类型。
- 收银实付pay_amount。
- 团购抵消的台费coupon_amount。
- 团购支付的金额2条路径若pl_coupon_sale_amount非0 则使用pl_coupon_sale_amount。若pl_coupon_sale_amount为0且coupon_amount不为0那么需要到dwd_groupbuy_redemption找到对应的订单的ledger_unit_price。
### 订单优惠与打折
- 台费打折adjust_amount
- 团购券优惠:团购抵消的台费 - 团购支付的金额
-----------------
单独任务:
大客户优惠;抹零;其他优惠 需要抽样分析,当作一个单独任务为我分析执行。
| **会员折扣** | dwd_settlement_head | `member_discount_amount` | 会员身份折扣 | 这个貌似没有启用过,也为我作为单独任务分析处理吧。。
---------------
时间分层机制需求明确“四层时间分层近2天/近1月/近3月/全量)”,方案只写了更新频率,需补齐具体实现(分区策略/分层表或物化汇总层/定期归档与清理作业)。
DDL 完整性:补充说明中提到缺失的表(如 cfg_tier_effective_period、dws_assistant_salary_calc、dws_member_visit_detail、dws_finance_discount_detail、dws_finance_recharge_summary、dws_finance_expense_summary需要在 schema_dws.sql 里落全方案里写了“更新DDL”但应明确完整DDL清单与字段级定义。
薪酬规则与生效期:档位、奖金、规则有“按月/按时间生效”的要求,方案目前只有 cfg_performance_tier/cfg_bonus_rules需要补充生效期字段或独立“规则生效期配置表”否则历史月份口径会错。
SCD2 / as-of 口径助教等级是SCD2维度历史月份不能直接用“当前等级”。方案需明确“按有效期 as-of join”的取数规则。
技能枚举规范:需求要求用 skill_id 判断基础课/附加课;方案应明确 skill_id→课程类型映射可用配置表避免 skill_name 漏记。
滚动区间统计:需求中明确 7/10/15/30/60/90 天窗口,方案未明确存储方式(建议在 dws_assistant_customer_stats、dws_member_consumption_summary 中直接落多窗口字段,或新增滚动汇总表)。
财务口径矩阵需全覆盖:方案已有“数据来源矩阵”,但需扩展至财务页面每一项指标(发生额/优惠拆分/确认收入/现金流/充值/平台回款/支出结构),确保每一项都有明确字段+公式+来源表。
手工导入表规范:支出/平台回款/充值提成的Excel导入要补“字段定义、时间粒度、门店维度、去重与校验规则”否则实现阶段会反复返工。
区域/房型维表:方案已有 cfg_area_category但需落地“具体映射规则 + 默认兜底 + 异常值处理”,并与 BD_manual_dim_table.md 一致。
# 更新
时间口径定义:本周/上周/本季度/上季度/最近半年不含本月 等窗口的“起止边界”为月第一天0点。周起始日为周一。
环比规则:开启对比时,是“对比上一个等长区间”相比。
有效业绩的排除规则:仅对“助教废除表”的记录进行处理排除。其影响绩效。
新入职定档规则月1日0点之后入住的计算为新入职。入职日以助教表入职时间为准。
Top3 奖金排名口径按绩效总小时数。如遇并列则都算比如2个第一则记为2个第一一个第三。
充值提成规则:比例/阶梯/时间口径缺失:通过手动导入表格,表格中会明确月份,提成关联充值订单金额和助教获得的提成金额。
大客户优惠/其他优惠划分规则:目前需要抽样分析。
平台回款/服务费口径:明确导入数据字段包含:回款金额、佣金、服务费、回款日期、平台类型、订单关联键。
散客处理member_id=0 的客户是散客。不进入客户维度统计。
门店/租户范围:现在只有一个门店,一个租户。

View File

@@ -0,0 +1,198 @@
# 筛选
- 按时间范围 本月/上个月/前3个月不加本月/前3个月+本月/最近半年不加本月/本季度含本月/上个季度/本周/上周
- 按区域筛选 大厅A区/B区/C区 /麻将房/团建房
# 新增功能
- 一个开关,打开后,可以与紧邻前一个等长区间进行对比(用上下箭头表示增/跌,并跟随百分比。)
- 对比数值的UI需要设计关闭状态和开启状态。
- 问号icon点击会有相应的弹窗显示内容。将弹出放在页面底部存在关闭按钮且默认5秒后自动消失。不影响滚动等操
# 数据展示调整
## 黑色banner 经营状况一览
### 行1收入概览 即 经营链:
- 发生额/正价。 点击提示icon
"
按台桌/包厢/助教/酒水的“正价”计算出的理论销售额,反映经营规模与业务量。
计算方式 = 各收入项目按正价 × 数量/时长汇总计算。
**不是最终收到了多少钱。**
"
- 总优惠 | 优惠比例。点击提示icon
"
本期因团购差价、大客户折扣、赠送卡抵扣、免单/抹零等导致的让利总额,用于解释“发生额”与“成交/确认收入”的差异。
计算方式 = 发生额 成交/确认收入
或 = 团购优惠 + 大客户优惠 + 赠送抵扣 + 其他优惠/免单/抹零(汇总)
"
- 成交/确认收入。点击提示icon
"
扣除各种优惠后的成交金额,**按记账规则统计的营业收入**。
计算方式 = 发生额 团购优惠 大客户优惠 赠送抵扣(及其他优惠)。
**不含充值营业收入** 充值是预收/负债,但会影响现金流。**
"
### 行2现金概览 注:往期为已结算,本期为预估:
- 实收/现金流入
"
统计真实进账的资金,包括现金 + 线上支付 + 平台回款。
计算方式 = 消费实收 + 平台团购 - 各类退款/冲正。
**此为现金口径,不等于营业收入。**区别为:充值属于预收款的现金流入,属于预存行为,球房债务。
"
- 现金支出。点击提示icon
"
本期所有支出项目的合计。
计算方式 = 房租 + 水电 + 进货成本支出 + 耗材 + 报销 + 助教分成 + 固定人员工资 + 平台服务费 + 其他费用
"
- 现金结余 | 结余率。点击提示icon
"
本期营业收入扣除全部成本后的利润,用于衡量经营质量。
计算方式= 实收/现金流入 总支出。
"
## AI分析
以下内容先占位真实内容会通过AI接口调用展示此处为标准Markdown内容排版。
优惠率Top团购(%) / 大客户(%) / 赠送卡(%)
差异最大项目:酒水 / 台桌 / 包厢 ...
财务分析:充值高但消耗低(或相反)提示
## 充值与预收
### 行1 会员卡概览
- 储值卡充值实收 首充 | 续费 | 合计。点击提示icon
"
本期储值卡充值到账的新增金额。
按照首充,续费,合计路径进行统计。
计算方式 = 本期储值卡充值订单的实收金额。
不含赠送金额
"
- 全类别会员卡余额合计 **仅经营参考,非财务属性**。点击提示icon
"
截至本期末,顾客充值后尚未消费的储值余额,包括赠送的台费卡酒水卡等类别,用于判断未来可转化的消费规模。
计算方式 = 各类会员卡往期余额 + 本期充值到账与赠送到账 本期卡消耗 ± 调整(退款/冲正/手工修正)
"
### 行2 储值卡统计详情
- 储值卡充值。点击提示icon
"
本期储值卡充值到账的新增金额。
"
- 储值卡消耗。点击提示icon
"
余额卡在查询周期内消耗金额。
计算方式 = 本期消耗 ± 调整
"
- 储值卡总余额。点击提示icon
"
截至本期末,余额卡可用的余额。
计算方式 = 期初余额卡余额 + 本期新增 本期消耗 ± 调整
"
### 行3 赠送卡统计详情
需要设计下页面,主要字段是合计,且细分的也要展示。
- 赠送卡新增合计;细分 酒水卡|台费卡|抵用券。点击提示icon
"
本期各类型赠送卡的新增金额。
"
- 赠送卡消费合计;细分酒水卡|台费卡|抵用券。点击提示icon
"
本期各类型赠送卡在查询周期内消耗金额。
计算方式 = 本期消耗 ± 调整
"
- 赠送卡总余额合计;细分酒水卡|台费卡|抵用券。点击提示icon
"
截至本期末,各类型赠送卡可用的余额。
计算方式 = 期初余额 + 本期新增 本期消耗 ± 调整
"
## 发生额 → 入账收入 及 优惠影响
页面字段结构:
### 收入确认(损益链)
发生额(正价) ¥123,456
├─ 团购优惠 -¥ 6,200
├─ 手动调整 + 大客户优惠 -¥ 4,800
├─ 赠送卡抵扣(台桌卡+酒水卡+抵用券) -¥ 2,336
└─ 其他优惠 免单+抹零 -¥ 0
成交/确认收入 ¥110,120
支付方式构成
├─ 由储值卡结算冲销 ¥60,120
├─ 现金/线上支付 ¥60,120
└─ 团购核销确认收入(团购成交价) ¥60,120
现金流
消费现金流入:现金+线上+平台回款−退款 ¥60,120
充值到账(首充/续费) ¥60,120
现金流入合计 ¥60,120
### 收入结构
收入结构(发生额 | 优惠 | 入账
开台与包厢 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
├─ A区 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
├─ B区 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
├─ C区 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
├─ 团建区 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
└─ 麻将区 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
助教(基础课) ¥xx,xxx | -¥ | ¥xx,xxx
助教(激励课) ¥xx,xxx | -¥ | ¥xx,xxx
食品酒水 ¥xx,xxx | -¥x,xxx | ¥xx,xxx
## 支出结构
助教分成基础¥x,xxx 附加¥x,xxx 充值提成¥x,xxx
助教额外奖金¥x,xxx
食品饮料进货¥x,xxx 耗材¥x,xxx 报销¥x,xxx
房租¥x,xxx 水电¥x,xxx 物业¥x,xxx
固定人员工资¥x,xxx
汇来米平台服务费¥x,xxx
美团服务费¥x,xxx 抖音服务费¥x,xxx
支出合计 ¥ xx,xxx
## 助教收支分析
助教基础课 客户支付 | 球房抽成 | 球房均小时抽成
├─ 初级 客户支付 | 球房抽成 | 球房均小时抽成
├─ 中级 客户支付 | 球房抽成 | 球房均小时抽成
├─ 高级 客户支付 | 球房抽成 | 球房均小时抽成
└─ 星级 客户支付 | 球房抽成 | 球房均小时抽成
助教激励课 客户支付 | 球房抽成 | 球房均小时抽成

View File

@@ -26,7 +26,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path: if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from api.client import APIClient from api.recording_client import build_recording_client
from api.endpoint_routing import derive_former_endpoint as derive_former_endpoint_shared from api.endpoint_routing import derive_former_endpoint as derive_former_endpoint_shared
from config.settings import AppConfig from config.settings import AppConfig
from models.parsers import TypeParser from models.parsers import TypeParser
@@ -265,13 +265,7 @@ def main() -> int:
if not cfg["api"].get("token"): if not cfg["api"].get("token"):
raise SystemExit("缺少 api.token请在 .env 配置 API_TOKEN 或 FICOO_TOKEN") raise SystemExit("缺少 api.token请在 .env 配置 API_TOKEN 或 FICOO_TOKEN")
client = APIClient( client = build_recording_client(cfg, task_code="FETCH_TEST_COMPARE")
base_url=cfg["api"]["base_url"],
token=cfg["api"]["token"],
timeout=int(cfg["api"].get("timeout_sec") or 20),
retry_max=int(cfg["api"].get("retries", {}).get("max_attempts") or 3),
headers_extra=cfg["api"].get("headers_extra") or {},
)
common_params = cfg["api"].get("params", {}) or {} common_params = cfg["api"].get("params", {}) or {}
if not isinstance(common_params, dict): if not isinstance(common_params, dict):

View File

@@ -5,6 +5,11 @@ from .task_model import TaskItem, TaskStatus, TaskHistory, TaskConfig, QueuedTas
from .schedule_model import ( from .schedule_model import (
ScheduledTask, ScheduleConfig, ScheduleType, IntervalUnit, ScheduleStore ScheduledTask, ScheduleConfig, ScheduleType, IntervalUnit, ScheduleStore
) )
from .task_registry import (
TaskRegistry, TaskDefinition, BusinessDomain, DOMAIN_LABELS,
task_registry, get_ods_task_codes, get_fact_ods_task_codes,
get_dimension_ods_task_codes, get_all_task_tuples
)
__all__ = [ __all__ = [
"TaskItem", "TaskItem",
@@ -17,4 +22,14 @@ __all__ = [
"ScheduleType", "ScheduleType",
"IntervalUnit", "IntervalUnit",
"ScheduleStore", "ScheduleStore",
# 任务注册表
"TaskRegistry",
"TaskDefinition",
"BusinessDomain",
"DOMAIN_LABELS",
"task_registry",
"get_ods_task_codes",
"get_fact_ods_task_codes",
"get_dimension_ods_task_codes",
"get_all_task_tuples",
] ]

View File

@@ -0,0 +1,353 @@
# -*- coding: utf-8 -*-
"""任务注册表:定义所有可用任务及其业务域分组。
从后端 ods_tasks 动态获取任务定义,并按业务域分组,供 UI 使用。
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Tuple
# 尝试从后端导入 ODS 任务定义
try:
from tasks.ods_tasks import ENABLED_ODS_CODES, ODS_TASK_SPECS
_HAS_BACKEND = True
except ImportError:
_HAS_BACKEND = False
ENABLED_ODS_CODES = set()
ODS_TASK_SPECS = ()
class BusinessDomain(Enum):
"""业务域枚举"""
MEMBER = "member" # 会员
SETTLEMENT = "settlement" # 结算/支付
ASSISTANT = "assistant" # 助教
GOODS = "goods" # 商品/销售
TABLE = "table" # 台桌
PROMOTION = "promotion" # 团购/优惠券
INVENTORY = "inventory" # 库存
SCHEMA = "schema" # Schema 初始化
DWD = "dwd" # DWD 装载
QUALITY = "quality" # 质量检查
OTHER = "other" # 其他
# 业务域显示名称
DOMAIN_LABELS: Dict[BusinessDomain, str] = {
BusinessDomain.MEMBER: "会员",
BusinessDomain.SETTLEMENT: "结算/支付",
BusinessDomain.ASSISTANT: "助教",
BusinessDomain.GOODS: "商品/销售",
BusinessDomain.TABLE: "台桌",
BusinessDomain.PROMOTION: "团购/优惠券",
BusinessDomain.INVENTORY: "库存",
BusinessDomain.SCHEMA: "Schema 初始化",
BusinessDomain.DWD: "DWD 装载",
BusinessDomain.QUALITY: "质量检查",
BusinessDomain.OTHER: "其他",
}
@dataclass
class TaskDefinition:
"""任务定义"""
code: str # 任务编码
name: str # 显示名称
description: str # 描述
domain: BusinessDomain # 业务域
requires_window: bool = True # 是否需要时间窗口
is_ods: bool = False # 是否为 ODS 任务
is_dimension: bool = False # 是否为维度类任务(校验时区分)
default_enabled: bool = True # 默认是否选中
# ODS 任务到业务域的映射
ODS_DOMAIN_MAP: Dict[str, BusinessDomain] = {
# 会员相关
"ODS_MEMBER": BusinessDomain.MEMBER,
"ODS_MEMBER_CARD": BusinessDomain.MEMBER,
"ODS_MEMBER_BALANCE": BusinessDomain.MEMBER,
# 结算/支付相关
"ODS_PAYMENT": BusinessDomain.SETTLEMENT,
"ODS_REFUND": BusinessDomain.SETTLEMENT,
"ODS_SETTLEMENT_RECORDS": BusinessDomain.SETTLEMENT,
"ODS_RECHARGE_SETTLE": BusinessDomain.SETTLEMENT,
"ODS_SETTLEMENT_TICKET": BusinessDomain.SETTLEMENT,
# 助教相关
"ODS_ASSISTANT_ACCOUNT": BusinessDomain.ASSISTANT,
"ODS_ASSISTANT_LEDGER": BusinessDomain.ASSISTANT,
"ODS_ASSISTANT_ABOLISH": BusinessDomain.ASSISTANT,
# 商品/销售相关
"ODS_TENANT_GOODS": BusinessDomain.GOODS,
"ODS_STORE_GOODS": BusinessDomain.GOODS,
"ODS_STORE_GOODS_SALES": BusinessDomain.GOODS,
"ODS_GOODS_CATEGORY": BusinessDomain.GOODS,
# 台桌相关
"ODS_TABLES": BusinessDomain.TABLE,
"ODS_TABLE_USE": BusinessDomain.TABLE,
"ODS_TABLE_FEE_DISCOUNT": BusinessDomain.TABLE,
# 团购/优惠券相关
"ODS_GROUP_PACKAGE": BusinessDomain.PROMOTION,
"ODS_GROUP_BUY_REDEMPTION": BusinessDomain.PROMOTION,
"ODS_PLATFORM_COUPON": BusinessDomain.PROMOTION,
# 库存相关
"ODS_INVENTORY_STOCK": BusinessDomain.INVENTORY,
"ODS_INVENTORY_CHANGE": BusinessDomain.INVENTORY,
}
# ODS 任务显示名称(中文)
ODS_DISPLAY_NAMES: Dict[str, str] = {
"ODS_MEMBER": "会员档案",
"ODS_MEMBER_CARD": "会员储值卡",
"ODS_MEMBER_BALANCE": "会员余额变动",
"ODS_PAYMENT": "支付流水",
"ODS_REFUND": "退款流水",
"ODS_SETTLEMENT_RECORDS": "结账记录",
"ODS_RECHARGE_SETTLE": "充值结算",
"ODS_SETTLEMENT_TICKET": "结账小票",
"ODS_ASSISTANT_ACCOUNT": "助教账号",
"ODS_ASSISTANT_LEDGER": "助教流水",
"ODS_ASSISTANT_ABOLISH": "助教作废",
"ODS_TENANT_GOODS": "租户商品",
"ODS_STORE_GOODS": "门店商品",
"ODS_STORE_GOODS_SALES": "商品销售流水",
"ODS_GOODS_CATEGORY": "商品分类",
"ODS_TABLES": "台桌维表",
"ODS_TABLE_USE": "台费计费流水",
"ODS_TABLE_FEE_DISCOUNT": "台费折扣调账",
"ODS_GROUP_PACKAGE": "团购套餐",
"ODS_GROUP_BUY_REDEMPTION": "团购核销",
"ODS_PLATFORM_COUPON": "平台券核销",
"ODS_INVENTORY_STOCK": "库存汇总",
"ODS_INVENTORY_CHANGE": "库存变化",
}
# 维度类 ODS 任务(校验时通常单独处理)
DIMENSION_ODS_CODES = {
"ODS_MEMBER",
"ODS_MEMBER_CARD",
"ODS_ASSISTANT_ACCOUNT",
"ODS_TENANT_GOODS",
"ODS_STORE_GOODS",
"ODS_GOODS_CATEGORY",
"ODS_TABLES",
"ODS_GROUP_PACKAGE",
}
# 事实类 ODS 任务(需要时间窗口)
FACT_ODS_CODES = {
"ODS_MEMBER_BALANCE",
"ODS_PAYMENT",
"ODS_REFUND",
"ODS_SETTLEMENT_RECORDS",
"ODS_RECHARGE_SETTLE",
"ODS_SETTLEMENT_TICKET",
"ODS_ASSISTANT_LEDGER",
"ODS_ASSISTANT_ABOLISH",
"ODS_STORE_GOODS_SALES",
"ODS_TABLE_USE",
"ODS_TABLE_FEE_DISCOUNT",
"ODS_GROUP_BUY_REDEMPTION",
"ODS_PLATFORM_COUPON",
"ODS_INVENTORY_CHANGE",
}
# 非 ODS 任务定义
NON_ODS_TASKS: List[TaskDefinition] = [
# DWD 装载
TaskDefinition(
code="DWD_LOAD_FROM_ODS",
name="ODS→DWD 装载",
description="从 ODS 增量装载到 DWD",
domain=BusinessDomain.DWD,
requires_window=True,
),
TaskDefinition(
code="DWD_QUALITY_CHECK",
name="DWD 质量检查",
description="执行 DWD 数据质量检查",
domain=BusinessDomain.QUALITY,
requires_window=False,
),
TaskDefinition(
code="DWS_BUILD_ORDER_SUMMARY",
name="构建订单汇总",
description="重算 DWS 订单汇总表",
domain=BusinessDomain.DWD,
requires_window=False,
),
# Schema 初始化
TaskDefinition(
code="INIT_ODS_SCHEMA",
name="初始化 ODS Schema",
description="创建/重建 ODS 表结构",
domain=BusinessDomain.SCHEMA,
requires_window=False,
default_enabled=False,
),
TaskDefinition(
code="INIT_DWD_SCHEMA",
name="初始化 DWD Schema",
description="创建/重建 DWD 表结构",
domain=BusinessDomain.SCHEMA,
requires_window=False,
default_enabled=False,
),
TaskDefinition(
code="INIT_DWS_SCHEMA",
name="初始化 DWS Schema",
description="创建/重建 DWS 表结构",
domain=BusinessDomain.SCHEMA,
requires_window=False,
default_enabled=False,
),
# 其他
TaskDefinition(
code="MANUAL_INGEST",
name="手工数据灌入",
description="从本地 JSON 回放入库",
domain=BusinessDomain.OTHER,
requires_window=False,
default_enabled=False,
),
TaskDefinition(
code="CHECK_CUTOFF",
name="检查 Cutoff",
description="查看各表数据截止时间",
domain=BusinessDomain.QUALITY,
requires_window=False,
),
TaskDefinition(
code="DATA_INTEGRITY_CHECK",
name="数据完整性检查",
description="检查 ODS/DWD 数据完整性",
domain=BusinessDomain.QUALITY,
requires_window=True,
),
]
def _build_ods_task_definition(code: str) -> TaskDefinition:
"""根据 ODS 任务编码构建任务定义"""
domain = ODS_DOMAIN_MAP.get(code, BusinessDomain.OTHER)
name = ODS_DISPLAY_NAMES.get(code, code)
is_dimension = code in DIMENSION_ODS_CODES
# 从后端获取描述(如果可用)
description = f"抓取{name}到 ODS"
if _HAS_BACKEND:
for spec in ODS_TASK_SPECS:
if spec.code == code:
# 尝试解码描述(可能是乱码)
desc = spec.description
if desc and not any(ord(c) > 0x4e00 for c in desc[:10] if desc):
description = f"抓取{name}到 ODS"
break
return TaskDefinition(
code=code,
name=name,
description=description,
domain=domain,
requires_window=code not in DIMENSION_ODS_CODES,
is_ods=True,
is_dimension=is_dimension,
)
class TaskRegistry:
"""任务注册表:管理所有可用任务"""
_instance: Optional["TaskRegistry"] = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._tasks: Dict[str, TaskDefinition] = {}
self._load_tasks()
def _load_tasks(self):
"""加载所有任务定义"""
# 加载 ODS 任务
ods_codes = ENABLED_ODS_CODES if _HAS_BACKEND else set(ODS_DOMAIN_MAP.keys())
for code in ods_codes:
self._tasks[code] = _build_ods_task_definition(code)
# 加载非 ODS 任务
for task_def in NON_ODS_TASKS:
self._tasks[task_def.code] = task_def
def get_task(self, code: str) -> Optional[TaskDefinition]:
"""获取任务定义"""
return self._tasks.get(code)
def get_all_tasks(self) -> List[TaskDefinition]:
"""获取所有任务"""
return list(self._tasks.values())
def get_ods_tasks(self) -> List[TaskDefinition]:
"""获取所有 ODS 任务"""
return [t for t in self._tasks.values() if t.is_ods]
def get_fact_ods_tasks(self) -> List[TaskDefinition]:
"""获取事实类 ODS 任务(需要时间窗口)"""
return [t for t in self._tasks.values() if t.is_ods and not t.is_dimension]
def get_dimension_ods_tasks(self) -> List[TaskDefinition]:
"""获取维度类 ODS 任务"""
return [t for t in self._tasks.values() if t.is_ods and t.is_dimension]
def get_tasks_by_domain(self, domain: BusinessDomain) -> List[TaskDefinition]:
"""按业务域获取任务"""
return [t for t in self._tasks.values() if t.domain == domain]
def get_ods_tasks_grouped(self) -> Dict[BusinessDomain, List[TaskDefinition]]:
"""获取按业务域分组的 ODS 任务"""
grouped: Dict[BusinessDomain, List[TaskDefinition]] = {}
for task in self.get_ods_tasks():
if task.domain not in grouped:
grouped[task.domain] = []
grouped[task.domain].append(task)
return grouped
def get_non_ods_tasks(self) -> List[TaskDefinition]:
"""获取非 ODS 任务"""
return [t for t in self._tasks.values() if not t.is_ods]
# 全局注册表实例
task_registry = TaskRegistry()
# 便捷函数
def get_ods_task_codes() -> List[str]:
"""获取所有 ODS 任务编码"""
return [t.code for t in task_registry.get_ods_tasks()]
def get_fact_ods_task_codes() -> List[str]:
"""获取事实类 ODS 任务编码"""
return [t.code for t in task_registry.get_fact_ods_tasks()]
def get_dimension_ods_task_codes() -> List[str]:
"""获取维度类 ODS 任务编码"""
return [t.code for t in task_registry.get_dimension_ods_tasks()]
def get_all_task_tuples() -> List[Tuple[str, str, str]]:
"""获取所有任务的 (code, name, description) 元组列表"""
return [(t.code, t.name, t.description) for t in task_registry.get_all_tasks()]
def get_ods_tasks_for_ui() -> List[Tuple[str, str, BusinessDomain]]:
"""获取 ODS 任务列表供 UI 使用:(code, display_name, domain)"""
return [(t.code, t.name, t.domain) for t in task_registry.get_ods_tasks()]

View File

@@ -7,6 +7,7 @@ from .log_viewer import LogViewer
from .db_viewer import DBViewer from .db_viewer import DBViewer
from .status_panel import StatusPanel from .status_panel import StatusPanel
from .task_manager import TaskManager from .task_manager import TaskManager
from .task_selector import TaskSelectorWidget, CompactTaskSelector
__all__ = [ __all__ = [
"TaskPanel", "TaskPanel",
@@ -15,4 +16,6 @@ __all__ = [
"DBViewer", "DBViewer",
"StatusPanel", "StatusPanel",
"TaskManager", "TaskManager",
"TaskSelectorWidget",
"CompactTaskSelector",
] ]

View File

@@ -26,9 +26,24 @@ from ..utils.app_settings import app_settings
from ..workers.task_worker import TaskWorker from ..workers.task_worker import TaskWorker
# 可调度的任务列表(包含所有 ODS 任务 + DWD/质量检查任务) # 动态获取可调度的任务列表
SCHEDULABLE_TASKS = [ def _get_schedulable_tasks():
# ODS 数据抓取任务(与 task_panel.AUTO_UPDATE_TASKS 保持一致) """从任务注册表动态获取可调度任务列表"""
try:
from ..models.task_registry import task_registry
tasks = []
# 添加所有 ODS 任务
for task_def in task_registry.get_ods_tasks():
tasks.append((task_def.code, task_def.name))
# 添加非 ODS 任务(排除 Schema 初始化和手工灌入)
exclude_codes = {"INIT_ODS_SCHEMA", "INIT_DWD_SCHEMA", "INIT_DWS_SCHEMA", "MANUAL_INGEST"}
for task_def in task_registry.get_non_ods_tasks():
if task_def.code not in exclude_codes:
tasks.append((task_def.code, task_def.name))
return tasks
except ImportError:
# 回退到静态列表
return [
("ODS_PAYMENT", "支付流水"), ("ODS_PAYMENT", "支付流水"),
("ODS_MEMBER", "会员档案"), ("ODS_MEMBER", "会员档案"),
("ODS_MEMBER_CARD", "会员储值卡"), ("ODS_MEMBER_CARD", "会员储值卡"),
@@ -42,7 +57,6 @@ SCHEDULABLE_TASKS = [
("ODS_PLATFORM_COUPON", "平台券核销"), ("ODS_PLATFORM_COUPON", "平台券核销"),
("ODS_RECHARGE_SETTLE", "充值结算"), ("ODS_RECHARGE_SETTLE", "充值结算"),
("ODS_SETTLEMENT_TICKET", "结账小票"), ("ODS_SETTLEMENT_TICKET", "结账小票"),
# DWD 和质量检查任务
("DWD_LOAD_FROM_ODS", "ODS→DWD 装载"), ("DWD_LOAD_FROM_ODS", "ODS→DWD 装载"),
("DWD_QUALITY_CHECK", "DWD 质量检查"), ("DWD_QUALITY_CHECK", "DWD 质量检查"),
("DATA_INTEGRITY_CHECK", "数据完整性检查"), ("DATA_INTEGRITY_CHECK", "数据完整性检查"),
@@ -50,6 +64,9 @@ SCHEDULABLE_TASKS = [
] ]
SCHEDULABLE_TASKS = _get_schedulable_tasks()
class TaskLogDialog(QDialog): class TaskLogDialog(QDialog):
"""任务日志查看对话框""" """任务日志查看对话框"""
@@ -1584,6 +1601,7 @@ class TaskManager(QWidget):
# 统计关键数据 # 统计关键数据
total_inserted = 0 total_inserted = 0
total_updated = 0
total_missing = 0 total_missing = 0
total_records = 0 total_records = 0
@@ -1596,11 +1614,30 @@ class TaskManager(QWidget):
import json import json
stats_str = match.group(1).replace("'", '"') stats_str = match.group(1).replace("'", '"')
stats = json.loads(stats_str) stats = json.loads(stats_str)
if 'tables' in stats: if 'tables' in stats:
for tbl in stats['tables']: for tbl in stats['tables']:
inserted = tbl.get('inserted', 0)
processed = tbl.get('processed', 0) inserted = int(tbl.get('inserted', 0) or 0)
updated = int(tbl.get('updated', 0) or 0)
processed = int(tbl.get('processed', 0) or 0)
has_new_counts = ('inserted' in tbl) or ('updated' in tbl)
if has_new_counts:
total_inserted += inserted
total_updated += updated
else:
total_inserted += inserted + processed total_inserted += inserted + processed
except Exception: except Exception:
pass pass
@@ -1622,8 +1659,11 @@ class TaskManager(QWidget):
total_records += int(match.group(1)) total_records += int(match.group(1))
# 构建摘要 # 构建摘要
if total_inserted > 0: if total_inserted > 0 or total_updated > 0:
summary_parts.append(f"处理 {total_inserted}") if total_updated > 0:
summary_parts.append(f"?? {total_inserted} ?, ?? {total_updated} ?")
else:
summary_parts.append(f"?? {total_inserted} ?")
if total_records > 0: if total_records > 0:
if total_missing > 0: if total_missing > 0:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,398 @@
# -*- coding: utf-8 -*-
"""可复用的 ODS 任务选择组件:按业务域分组显示,支持全选/反选。"""
from typing import Dict, List, Optional, Set
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
QCheckBox, QPushButton, QScrollArea, QFrame,
QLabel, QSizePolicy
)
from PySide6.QtCore import Signal, Qt
from ..models.task_registry import (
TaskRegistry, TaskDefinition, BusinessDomain, DOMAIN_LABELS,
task_registry, get_fact_ods_task_codes, get_dimension_ods_task_codes
)
class TaskSelectorWidget(QWidget):
"""ODS 任务选择组件:按业务域分组显示"""
# 选择变化信号
selection_changed = Signal(list) # 选中的任务编码列表
def __init__(
self,
parent: Optional[QWidget] = None,
show_dimensions: bool = True,
show_facts: bool = True,
default_select_facts: bool = True,
default_select_dimensions: bool = False,
compact: bool = False,
max_height: int = 0,
):
"""
初始化任务选择器
Args:
parent: 父组件
show_dimensions: 是否显示维度类任务
show_facts: 是否显示事实类任务
default_select_facts: 默认选中事实类任务
default_select_dimensions: 默认选中维度类任务
compact: 紧凑模式(更小的间距)
max_height: 最大高度0 表示不限制)
"""
super().__init__(parent)
self.show_dimensions = show_dimensions
self.show_facts = show_facts
self.default_select_facts = default_select_facts
self.default_select_dimensions = default_select_dimensions
self.compact = compact
self.max_height = max_height
# 任务复选框映射code -> QCheckBox
self._checkboxes: Dict[str, QCheckBox] = {}
# 业务域分组框映射domain -> QGroupBox
self._domain_groups: Dict[BusinessDomain, QGroupBox] = {}
self._init_ui()
self._apply_default_selection()
def _init_ui(self):
"""初始化界面"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
spacing = 4 if self.compact else 8
layout.setSpacing(spacing)
# 顶部工具栏
toolbar = QHBoxLayout()
toolbar.setSpacing(8)
self.select_all_btn = QPushButton("全选")
self.select_all_btn.setProperty("secondary", True)
self.select_all_btn.setFixedWidth(60)
self.select_all_btn.clicked.connect(self._select_all)
toolbar.addWidget(self.select_all_btn)
self.deselect_all_btn = QPushButton("全不选")
self.deselect_all_btn.setProperty("secondary", True)
self.deselect_all_btn.setFixedWidth(60)
self.deselect_all_btn.clicked.connect(self._deselect_all)
toolbar.addWidget(self.deselect_all_btn)
self.select_facts_btn = QPushButton("选事实表")
self.select_facts_btn.setProperty("secondary", True)
self.select_facts_btn.setFixedWidth(70)
self.select_facts_btn.setToolTip("选中所有事实类任务(需要时间窗口的任务)")
self.select_facts_btn.clicked.connect(self._select_facts_only)
toolbar.addWidget(self.select_facts_btn)
toolbar.addStretch()
self.selected_count_label = QLabel("已选: 0")
self.selected_count_label.setProperty("subheading", True)
toolbar.addWidget(self.selected_count_label)
layout.addLayout(toolbar)
# 滚动区域
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.NoFrame)
if self.max_height > 0:
scroll_area.setMaximumHeight(self.max_height)
# 内容容器
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(spacing)
# 按业务域分组创建复选框
grouped_tasks = task_registry.get_ods_tasks_grouped()
# 定义业务域显示顺序
domain_order = [
BusinessDomain.MEMBER,
BusinessDomain.SETTLEMENT,
BusinessDomain.ASSISTANT,
BusinessDomain.GOODS,
BusinessDomain.TABLE,
BusinessDomain.PROMOTION,
BusinessDomain.INVENTORY,
]
for domain in domain_order:
if domain not in grouped_tasks:
continue
tasks = grouped_tasks[domain]
# 过滤任务
filtered_tasks = []
for task in tasks:
if task.is_dimension and not self.show_dimensions:
continue
if not task.is_dimension and not self.show_facts:
continue
filtered_tasks.append(task)
if not filtered_tasks:
continue
# 创建业务域分组
group_box = self._create_domain_group(domain, filtered_tasks)
self._domain_groups[domain] = group_box
content_layout.addWidget(group_box)
content_layout.addStretch()
scroll_area.setWidget(content_widget)
layout.addWidget(scroll_area, 1)
def _create_domain_group(self, domain: BusinessDomain, tasks: List[TaskDefinition]) -> QGroupBox:
"""创建业务域分组框"""
group_box = QGroupBox(DOMAIN_LABELS.get(domain, str(domain.value)))
group_layout = QVBoxLayout(group_box)
group_layout.setContentsMargins(8, 4, 8, 4)
group_layout.setSpacing(2)
for task in tasks:
checkbox = QCheckBox(f"{task.name}")
checkbox.setToolTip(f"{task.code}: {task.description}")
checkbox.setProperty("task_code", task.code)
checkbox.setProperty("is_dimension", task.is_dimension)
checkbox.stateChanged.connect(self._on_selection_changed)
self._checkboxes[task.code] = checkbox
group_layout.addWidget(checkbox)
return group_box
def _apply_default_selection(self):
"""应用默认选择"""
for code, checkbox in self._checkboxes.items():
is_dimension = checkbox.property("is_dimension")
if is_dimension:
checkbox.setChecked(self.default_select_dimensions)
else:
checkbox.setChecked(self.default_select_facts)
self._update_count_label()
def _on_selection_changed(self):
"""选择变化时"""
self._update_count_label()
self.selection_changed.emit(self.get_selected_codes())
def _update_count_label(self):
"""更新选中计数标签"""
count = len(self.get_selected_codes())
total = len(self._checkboxes)
self.selected_count_label.setText(f"已选: {count}/{total}")
def _select_all(self):
"""全选"""
for checkbox in self._checkboxes.values():
checkbox.blockSignals(True)
checkbox.setChecked(True)
checkbox.blockSignals(False)
self._on_selection_changed()
def _deselect_all(self):
"""全不选"""
for checkbox in self._checkboxes.values():
checkbox.blockSignals(True)
checkbox.setChecked(False)
checkbox.blockSignals(False)
self._on_selection_changed()
def _select_facts_only(self):
"""只选事实表任务"""
for code, checkbox in self._checkboxes.items():
checkbox.blockSignals(True)
is_dimension = checkbox.property("is_dimension")
checkbox.setChecked(not is_dimension)
checkbox.blockSignals(False)
self._on_selection_changed()
def get_selected_codes(self) -> List[str]:
"""获取选中的任务编码列表"""
selected = []
for code, checkbox in self._checkboxes.items():
if checkbox.isChecked():
selected.append(code)
return selected
def set_selected_codes(self, codes: List[str]):
"""设置选中的任务编码"""
codes_set = set(codes)
for code, checkbox in self._checkboxes.items():
checkbox.blockSignals(True)
checkbox.setChecked(code in codes_set)
checkbox.blockSignals(False)
self._on_selection_changed()
def get_all_codes(self) -> List[str]:
"""获取所有任务编码"""
return list(self._checkboxes.keys())
def is_any_selected(self) -> bool:
"""是否有任何任务被选中"""
return len(self.get_selected_codes()) > 0
class CompactTaskSelector(QWidget):
"""紧凑型任务选择器:单行显示业务域,点击展开选择"""
selection_changed = Signal(list)
def __init__(
self,
parent: Optional[QWidget] = None,
show_dimensions: bool = True,
show_facts: bool = True,
default_select_facts: bool = True,
default_select_dimensions: bool = False,
):
super().__init__(parent)
self.show_dimensions = show_dimensions
self.show_facts = show_facts
self.default_select_facts = default_select_facts
self.default_select_dimensions = default_select_dimensions
# 业务域复选框
self._domain_checkboxes: Dict[BusinessDomain, QCheckBox] = {}
# 业务域下的任务编码
self._domain_tasks: Dict[BusinessDomain, List[str]] = {}
self._init_ui()
self._apply_default_selection()
def _init_ui(self):
"""初始化界面"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
# 工具栏
toolbar = QHBoxLayout()
toolbar.setSpacing(8)
self.select_all_btn = QPushButton("全选")
self.select_all_btn.setProperty("secondary", True)
self.select_all_btn.setFixedWidth(50)
self.select_all_btn.clicked.connect(self._select_all)
toolbar.addWidget(self.select_all_btn)
self.deselect_all_btn = QPushButton("清空")
self.deselect_all_btn.setProperty("secondary", True)
self.deselect_all_btn.setFixedWidth(50)
self.deselect_all_btn.clicked.connect(self._deselect_all)
toolbar.addWidget(self.deselect_all_btn)
toolbar.addStretch()
self.count_label = QLabel("已选: 0")
self.count_label.setProperty("subheading", True)
toolbar.addWidget(self.count_label)
layout.addLayout(toolbar)
# 业务域复选框(横向排列)
domains_layout = QHBoxLayout()
domains_layout.setSpacing(12)
grouped_tasks = task_registry.get_ods_tasks_grouped()
domain_order = [
BusinessDomain.MEMBER,
BusinessDomain.SETTLEMENT,
BusinessDomain.ASSISTANT,
BusinessDomain.GOODS,
BusinessDomain.TABLE,
BusinessDomain.PROMOTION,
BusinessDomain.INVENTORY,
]
for domain in domain_order:
if domain not in grouped_tasks:
continue
tasks = grouped_tasks[domain]
# 过滤任务
task_codes = []
for task in tasks:
if task.is_dimension and not self.show_dimensions:
continue
if not task.is_dimension and not self.show_facts:
continue
task_codes.append(task.code)
if not task_codes:
continue
self._domain_tasks[domain] = task_codes
checkbox = QCheckBox(DOMAIN_LABELS.get(domain, str(domain.value)))
checkbox.setToolTip(f"包含: {', '.join(task_codes)}")
checkbox.stateChanged.connect(self._on_selection_changed)
self._domain_checkboxes[domain] = checkbox
domains_layout.addWidget(checkbox)
domains_layout.addStretch()
layout.addLayout(domains_layout)
def _apply_default_selection(self):
"""应用默认选择"""
# 默认选中所有业务域
for domain, checkbox in self._domain_checkboxes.items():
checkbox.setChecked(True)
self._update_count_label()
def _on_selection_changed(self):
"""选择变化时"""
self._update_count_label()
self.selection_changed.emit(self.get_selected_codes())
def _update_count_label(self):
"""更新计数标签"""
count = len(self.get_selected_codes())
self.count_label.setText(f"已选: {count} 个任务")
def _select_all(self):
"""全选所有业务域"""
for checkbox in self._domain_checkboxes.values():
checkbox.blockSignals(True)
checkbox.setChecked(True)
checkbox.blockSignals(False)
self._on_selection_changed()
def _deselect_all(self):
"""取消全选"""
for checkbox in self._domain_checkboxes.values():
checkbox.blockSignals(True)
checkbox.setChecked(False)
checkbox.blockSignals(False)
self._on_selection_changed()
def get_selected_codes(self) -> List[str]:
"""获取选中的任务编码"""
selected = []
for domain, checkbox in self._domain_checkboxes.items():
if checkbox.isChecked():
selected.extend(self._domain_tasks.get(domain, []))
return selected
def set_selected_domains(self, domains: List[BusinessDomain]):
"""设置选中的业务域"""
domains_set = set(domains)
for domain, checkbox in self._domain_checkboxes.items():
checkbox.blockSignals(True)
checkbox.setChecked(domain in domains_set)
checkbox.blockSignals(False)
self._on_selection_changed()
def is_any_selected(self) -> bool:
"""是否有任何任务被选中"""
return len(self.get_selected_codes()) > 0

View File

@@ -189,28 +189,69 @@ class TaskWorker(QThread):
# 解析 DWD 装载统计 # 解析 DWD 装载统计
if 'tables' in stats: if 'tables' in stats:
total_processed = 0 total_dim_inserted = 0
total_inserted = 0 total_dim_updated = 0
tables_with_data = [] total_fact_inserted = 0
total_fact_updated = 0
dim_tables = [] # 维表明细
fact_tables = [] # 事实表明细
for tbl in stats['tables']: for tbl in stats['tables']:
table_name = tbl.get('table', '').replace('billiards_dwd.', '') table_name = tbl.get('table', '').replace('billiards_dwd.', '')
processed = tbl.get('processed', 0) mode = tbl.get('mode', '')
inserted = tbl.get('inserted', 0) processed = int(tbl.get('processed', 0) or 0)
inserted = int(tbl.get('inserted', 0) or 0)
updated = int(tbl.get('updated', 0) or 0)
has_new_counts = ('inserted' in tbl) or ('updated' in tbl)
if processed > 0: # 忽略 _ex 扩展表
total_processed += processed if table_name.endswith('_ex'):
tables_with_data.append(f"{table_name}({processed})") continue
elif inserted > 0:
total_inserted += inserted
tables_with_data.append(f"{table_name}(+{inserted})")
if total_processed > 0 or total_inserted > 0: is_dim = table_name.startswith('dim_') or mode == 'SCD2'
dwd_stats.append(f"处理维度: {total_processed}条, 新增事实: {total_inserted}") if is_dim:
if len(tables_with_data) <= 5: if has_new_counts:
dwd_stats.append(f"涉及表: {', '.join(tables_with_data)}") total_dim_inserted += inserted
total_dim_updated += updated
if inserted or updated:
dim_tables.append(f"{table_name}: +{inserted}, ~{updated}")
elif processed > 0:
total_dim_updated += processed
dim_tables.append(f"{table_name}: {processed}")
else: else:
dwd_stats.append(f"涉及 {len(tables_with_data)} 张表") if has_new_counts:
total_fact_inserted += inserted
total_fact_updated += updated
if inserted or updated:
fact_tables.append(f"{table_name}: +{inserted}, ~{updated}")
elif processed > 0 or inserted > 0:
total_fact_inserted += inserted
if inserted > 0:
fact_tables.append(f"{table_name}: +{inserted}")
if (total_dim_inserted or total_dim_updated or total_fact_inserted or total_fact_updated):
dwd_stats.append(
f"维表新增: {total_dim_inserted}条, 维表更新: {total_dim_updated}条, "
f"事实表新增: {total_fact_inserted}条, 事实表更新: {total_fact_updated}"
)
# 维表明细
if dim_tables:
dwd_stats.append(" 维表: " + ", ".join(dim_tables))
# 事实表明细
if fact_tables:
dwd_stats.append(" 事实表: " + ", ".join(fact_tables))
# 解析错误信息
if 'errors' in stats and stats['errors']:
for err in stats['errors']:
err_table = err.get('table', '').replace('billiards_dwd.', '')
err_msg = err.get('error', '')
errors.append(f"{err_table}: {err_msg}")
except Exception: except Exception:
pass pass
continue continue
@@ -263,7 +304,9 @@ class TaskWorker(QThread):
summary_parts[-1] += f"{len(ods_stats)}" summary_parts[-1] += f"{len(ods_stats)}"
if dwd_stats: if dwd_stats:
summary_parts.append("【DWD 装载】" + "; ".join(dwd_stats)) summary_parts.append("【DWD 装载】" + dwd_stats[0]) # 第一行是汇总
for detail in dwd_stats[1:]: # 后面是详情
summary_parts.append(detail)
if integrity_stats: if integrity_stats:
total_missing = integrity_stats.get('final_missing', integrity_stats.get('total_missing', 0)) total_missing = integrity_stats.get('final_missing', integrity_stats.get('total_missing', 0))

View File

@@ -306,7 +306,8 @@ class ETLScheduler:
def _build_fetch_dir(self, task_code: str, run_id: int) -> Path: def _build_fetch_dir(self, task_code: str, run_id: int) -> Path:
ts = datetime.now(self.tz).strftime("%Y%m%d-%H%M%S") ts = datetime.now(self.tz).strftime("%Y%m%d-%H%M%S")
return Path(self.fetch_root) / f"{task_code.upper()}-{run_id}-{ts}" task_code = str(task_code or "").upper()
return Path(self.fetch_root) / task_code / f"{task_code}-{run_id}-{ts}"
def _resolve_ingest_source(self, fetch_dir: Path, fetch_stats: dict | None) -> Path: def _resolve_ingest_source(self, fetch_dir: Path, fetch_stats: dict | None) -> Path:
if fetch_stats and fetch_dir.exists(): if fetch_stats and fetch_dir.exists():
@@ -357,8 +358,20 @@ class ETLScheduler:
try: try:
# 创建任务实例(不需要 API client使用 None # 创建任务实例(不需要 API client使用 None
api_client = None
if task_code == "ODS_JSON_ARCHIVE":
run_id = int(datetime.now(self.tz).timestamp())
fetch_dir = self._build_fetch_dir(task_code, run_id)
api_client = RecordingAPIClient(
base_client=self.api_client,
output_dir=fetch_dir,
task_code=task_code,
run_id=run_id,
write_pretty=self.write_pretty_json,
)
task = self.task_registry.create_task( task = self.task_registry.create_task(
task_code, self.config, self.db_ops, None, self.logger task_code, self.config, self.db_ops, api_client, self.logger
) )
# 执行任务(工具类任务通常不需要 cursor_data # 执行任务(工具类任务通常不需要 cursor_data

View File

@@ -28,6 +28,26 @@ from tasks.check_cutoff_task import CheckCutoffTask
from tasks.init_dws_schema_task import InitDwsSchemaTask from tasks.init_dws_schema_task import InitDwsSchemaTask
from tasks.dws_build_order_summary_task import DwsBuildOrderSummaryTask from tasks.dws_build_order_summary_task import DwsBuildOrderSummaryTask
from tasks.data_integrity_task import DataIntegrityTask from tasks.data_integrity_task import DataIntegrityTask
from tasks.seed_dws_config_task import SeedDwsConfigTask
# DWS 层任务导入
from tasks.dws import (
AssistantDailyTask,
AssistantMonthlyTask,
AssistantCustomerTask,
AssistantSalaryTask,
AssistantFinanceTask,
MemberConsumptionTask,
MemberVisitTask,
FinanceDailyTask,
FinanceRechargeTask,
FinanceIncomeStructureTask,
FinanceDiscountDetailTask,
DwsRetentionCleanupTask,
# 指数算法任务
RecallIndexTask,
IntimacyIndexTask,
)
class TaskRegistry: class TaskRegistry:
"""任务注册和工厂""" """任务注册和工厂"""
@@ -81,6 +101,26 @@ default_registry.register("ODS_JSON_ARCHIVE", OdsJsonArchiveTask)
default_registry.register("CHECK_CUTOFF", CheckCutoffTask) default_registry.register("CHECK_CUTOFF", CheckCutoffTask)
default_registry.register("DATA_INTEGRITY_CHECK", DataIntegrityTask) default_registry.register("DATA_INTEGRITY_CHECK", DataIntegrityTask)
default_registry.register("INIT_DWS_SCHEMA", InitDwsSchemaTask) default_registry.register("INIT_DWS_SCHEMA", InitDwsSchemaTask)
default_registry.register("SEED_DWS_CONFIG", SeedDwsConfigTask)
default_registry.register("DWS_BUILD_ORDER_SUMMARY", DwsBuildOrderSummaryTask) default_registry.register("DWS_BUILD_ORDER_SUMMARY", DwsBuildOrderSummaryTask)
# DWS 层业务任务
default_registry.register("DWS_ASSISTANT_DAILY", AssistantDailyTask)
default_registry.register("DWS_ASSISTANT_MONTHLY", AssistantMonthlyTask)
default_registry.register("DWS_ASSISTANT_CUSTOMER", AssistantCustomerTask)
default_registry.register("DWS_ASSISTANT_SALARY", AssistantSalaryTask)
default_registry.register("DWS_ASSISTANT_FINANCE", AssistantFinanceTask)
default_registry.register("DWS_MEMBER_CONSUMPTION", MemberConsumptionTask)
default_registry.register("DWS_MEMBER_VISIT", MemberVisitTask)
default_registry.register("DWS_FINANCE_DAILY", FinanceDailyTask)
default_registry.register("DWS_FINANCE_RECHARGE", FinanceRechargeTask)
default_registry.register("DWS_FINANCE_INCOME_STRUCTURE", FinanceIncomeStructureTask)
default_registry.register("DWS_FINANCE_DISCOUNT_DETAIL", FinanceDiscountDetailTask)
default_registry.register("DWS_RETENTION_CLEANUP", DwsRetentionCleanupTask)
# DWS 指数算法任务
default_registry.register("DWS_RECALL_INDEX", RecallIndexTask)
default_registry.register("DWS_INTIMACY_INDEX", IntimacyIndexTask)
for code, task_cls in ODS_TASK_CLASSES.items(): for code, task_cls in ODS_TASK_CLASSES.items():
default_registry.register(code, task_cls) default_registry.register(code, task_cls)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,636 @@
# -*- coding: utf-8 -*-
"""
优惠口径抽样分析脚本
功能说明:
从dwd_settlement_head表抽样100单分析以下优惠字段的使用情况
- adjust_amount: 台费打折/调整(可能包含大客户优惠、其他优惠)
- member_discount_amount: 会员折扣
- rounding_amount: 抹零金额
- coupon_amount: 团购抵消台费
- gift_card_amount: 赠送卡支付
分析目标:
1. 大客户优惠:是否存在"大客户"标识?如何与普通调整区分?
2. 会员折扣:是否有非零值?使用场景是什么?
3. 抹零抹零规则与adjust_amount的关系
4. 其他优惠adjust_amount中还包含哪些优惠类型
输出:
- 控制台打印分析报告
- 生成 docs/analysis_discount_patterns.md 报告文件
作者ETL团队
创建日期2026-02-01
"""
import os
import sys
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from etl_billiards.utils.config import Config
from etl_billiards.utils.db import DatabaseConnection
def analyze_discount_patterns():
"""
执行优惠口径抽样分析
"""
print("=" * 80)
print("优惠口径抽样分析")
print("=" * 80)
print()
# 加载配置和数据库连接
config = Config()
db = DatabaseConnection(config)
try:
# 1. 获取总体统计
print("【1. 总体统计】")
print("-" * 40)
overall_stats = get_overall_stats(db)
print_overall_stats(overall_stats)
print()
# 2. 抽样分析优惠订单
print("【2. 有优惠的订单抽样分析100单")
print("-" * 40)
sample_orders = get_sample_orders_with_discount(db, limit=100)
discount_analysis = analyze_sample_orders(sample_orders)
print_discount_analysis(discount_analysis)
print()
# 3. adjust_amount详细分析
print("【3. adjust_amount (台费打折/调整) 详细分析】")
print("-" * 40)
adjust_analysis = analyze_adjust_amount(db)
print_adjust_analysis(adjust_analysis)
print()
# 4. 会员折扣使用分析
print("【4. member_discount_amount (会员折扣) 使用分析】")
print("-" * 40)
member_discount_analysis = analyze_member_discount(db)
print_member_discount_analysis(member_discount_analysis)
print()
# 5. 抹零规则分析
print("【5. rounding_amount (抹零) 规则分析】")
print("-" * 40)
rounding_analysis = analyze_rounding(db)
print_rounding_analysis(rounding_analysis)
print()
# 6. 团购优惠分析
print("【6. 团购优惠分析】")
print("-" * 40)
groupbuy_analysis = analyze_groupbuy(db)
print_groupbuy_analysis(groupbuy_analysis)
print()
# 7. 生成分析报告
print("【7. 生成分析报告】")
print("-" * 40)
report = generate_report(
overall_stats,
discount_analysis,
adjust_analysis,
member_discount_analysis,
rounding_analysis,
groupbuy_analysis
)
# 保存报告
report_path = project_root / "etl_billiards" / "docs" / "analysis_discount_patterns.md"
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f"报告已保存到: {report_path}")
finally:
db.close()
def get_overall_stats(db: DatabaseConnection) -> Dict[str, Any]:
"""
获取总体统计数据
"""
sql = """
SELECT
COUNT(*) AS total_orders,
COUNT(CASE WHEN adjust_amount != 0 THEN 1 END) AS orders_with_adjust,
COUNT(CASE WHEN member_discount_amount != 0 THEN 1 END) AS orders_with_member_discount,
COUNT(CASE WHEN rounding_amount != 0 THEN 1 END) AS orders_with_rounding,
COUNT(CASE WHEN coupon_amount != 0 THEN 1 END) AS orders_with_coupon,
COUNT(CASE WHEN gift_card_amount != 0 THEN 1 END) AS orders_with_gift_card,
SUM(adjust_amount) AS total_adjust,
SUM(member_discount_amount) AS total_member_discount,
SUM(rounding_amount) AS total_rounding,
SUM(coupon_amount) AS total_coupon,
SUM(gift_card_amount) AS total_gift_card,
SUM(consume_money) AS total_consume,
SUM(pay_amount) AS total_pay
FROM billiards_dwd.dwd_settlement_head
"""
rows = db.query(sql)
return dict(rows[0]) if rows else {}
def get_sample_orders_with_discount(
db: DatabaseConnection,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
抽样获取有优惠的订单
"""
sql = """
SELECT
order_settle_id,
order_trade_no,
create_time,
consume_money,
pay_amount,
adjust_amount,
member_discount_amount,
rounding_amount,
coupon_amount,
gift_card_amount,
balance_amount,
recharge_card_amount,
pl_coupon_sale_amount,
table_charge_money,
goods_money,
assistant_pd_money,
assistant_cx_money,
consume_money - pay_amount - COALESCE(recharge_card_amount, 0)
- COALESCE(gift_card_amount, 0) - COALESCE(balance_amount, 0) AS calculated_discount
FROM billiards_dwd.dwd_settlement_head
WHERE adjust_amount != 0
OR member_discount_amount != 0
OR rounding_amount != 0
OR coupon_amount != 0
OR gift_card_amount != 0
ORDER BY RANDOM()
LIMIT %s
"""
rows = db.query(sql, (limit,))
return [dict(row) for row in rows] if rows else []
def analyze_sample_orders(orders: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
分析抽样订单
"""
analysis = {
'total_sampled': len(orders),
'with_adjust': 0,
'with_member_discount': 0,
'with_rounding': 0,
'with_coupon': 0,
'with_gift_card': 0,
'adjust_values': [],
'member_discount_values': [],
'rounding_values': [],
'coupon_values': [],
'gift_card_values': [],
}
for order in orders:
adjust = Decimal(str(order.get('adjust_amount', 0)))
member_discount = Decimal(str(order.get('member_discount_amount', 0)))
rounding = Decimal(str(order.get('rounding_amount', 0)))
coupon = Decimal(str(order.get('coupon_amount', 0)))
gift_card = Decimal(str(order.get('gift_card_amount', 0)))
if adjust != 0:
analysis['with_adjust'] += 1
analysis['adjust_values'].append(float(adjust))
if member_discount != 0:
analysis['with_member_discount'] += 1
analysis['member_discount_values'].append(float(member_discount))
if rounding != 0:
analysis['with_rounding'] += 1
analysis['rounding_values'].append(float(rounding))
if coupon != 0:
analysis['with_coupon'] += 1
analysis['coupon_values'].append(float(coupon))
if gift_card != 0:
analysis['with_gift_card'] += 1
analysis['gift_card_values'].append(float(gift_card))
return analysis
def analyze_adjust_amount(db: DatabaseConnection) -> Dict[str, Any]:
"""
分析adjust_amount字段的分布和模式
"""
# 1. 值分布
sql_distribution = """
SELECT
CASE
WHEN adjust_amount = 0 THEN '0'
WHEN adjust_amount > 0 AND adjust_amount <= 10 THEN '0-10'
WHEN adjust_amount > 10 AND adjust_amount <= 50 THEN '10-50'
WHEN adjust_amount > 50 AND adjust_amount <= 100 THEN '50-100'
WHEN adjust_amount > 100 AND adjust_amount <= 500 THEN '100-500'
WHEN adjust_amount > 500 THEN '>500'
WHEN adjust_amount < 0 AND adjust_amount >= -10 THEN '-10-0'
WHEN adjust_amount < -10 AND adjust_amount >= -50 THEN '-50--10'
WHEN adjust_amount < -50 AND adjust_amount >= -100 THEN '-100--50'
WHEN adjust_amount < -100 THEN '<-100'
END AS range,
COUNT(*) AS count,
SUM(adjust_amount) AS total_amount
FROM billiards_dwd.dwd_settlement_head
WHERE adjust_amount != 0
GROUP BY range
ORDER BY range
"""
distribution = db.query(sql_distribution)
# 2. 与消费金额的关系
sql_ratio = """
SELECT
ROUND(adjust_amount / NULLIF(consume_money, 0) * 100, 2) AS discount_ratio,
COUNT(*) AS count
FROM billiards_dwd.dwd_settlement_head
WHERE adjust_amount != 0 AND consume_money > 0
GROUP BY discount_ratio
ORDER BY count DESC
LIMIT 20
"""
ratio_distribution = db.query(sql_ratio)
# 3. 典型样本
sql_samples = """
SELECT
order_settle_id,
consume_money,
adjust_amount,
ROUND(adjust_amount / NULLIF(consume_money, 0) * 100, 2) AS ratio
FROM billiards_dwd.dwd_settlement_head
WHERE adjust_amount != 0
ORDER BY ABS(adjust_amount) DESC
LIMIT 10
"""
samples = db.query(sql_samples)
return {
'distribution': [dict(r) for r in distribution] if distribution else [],
'ratio_distribution': [dict(r) for r in ratio_distribution] if ratio_distribution else [],
'top_samples': [dict(r) for r in samples] if samples else []
}
def analyze_member_discount(db: DatabaseConnection) -> Dict[str, Any]:
"""
分析member_discount_amount字段的使用情况
"""
sql = """
SELECT
COUNT(*) AS total_orders,
COUNT(CASE WHEN member_discount_amount != 0 THEN 1 END) AS with_discount,
SUM(member_discount_amount) AS total_discount,
AVG(CASE WHEN member_discount_amount != 0 THEN member_discount_amount END) AS avg_discount,
MAX(member_discount_amount) AS max_discount,
MIN(CASE WHEN member_discount_amount != 0 THEN member_discount_amount END) AS min_discount
FROM billiards_dwd.dwd_settlement_head
"""
rows = db.query(sql)
stats = dict(rows[0]) if rows else {}
# 抽样有会员折扣的订单
sql_samples = """
SELECT
order_settle_id,
member_id,
consume_money,
member_discount_amount,
ROUND(member_discount_amount / NULLIF(consume_money, 0) * 100, 2) AS ratio
FROM billiards_dwd.dwd_settlement_head
WHERE member_discount_amount != 0
LIMIT 20
"""
samples = db.query(sql_samples)
return {
'stats': stats,
'samples': [dict(r) for r in samples] if samples else []
}
def analyze_rounding(db: DatabaseConnection) -> Dict[str, Any]:
"""
分析rounding_amount字段的规则
"""
# 1. 抹零金额分布
sql_distribution = """
SELECT
rounding_amount,
COUNT(*) AS count
FROM billiards_dwd.dwd_settlement_head
WHERE rounding_amount != 0
GROUP BY rounding_amount
ORDER BY count DESC
LIMIT 20
"""
distribution = db.query(sql_distribution)
# 2. 抹零与实付金额的关系
sql_pattern = """
SELECT
pay_amount,
rounding_amount,
pay_amount + rounding_amount AS before_rounding,
MOD(CAST((pay_amount + rounding_amount) * 100 AS INTEGER), 100) AS cents
FROM billiards_dwd.dwd_settlement_head
WHERE rounding_amount != 0
LIMIT 20
"""
patterns = db.query(sql_pattern)
return {
'distribution': [dict(r) for r in distribution] if distribution else [],
'patterns': [dict(r) for r in patterns] if patterns else []
}
def analyze_groupbuy(db: DatabaseConnection) -> Dict[str, Any]:
"""
分析团购优惠
"""
# 1. 团购使用统计
sql_stats = """
SELECT
COUNT(*) AS total_orders,
COUNT(CASE WHEN coupon_amount != 0 THEN 1 END) AS with_coupon,
COUNT(CASE WHEN pl_coupon_sale_amount != 0 THEN 1 END) AS with_pl_coupon,
SUM(coupon_amount) AS total_coupon_amount,
SUM(pl_coupon_sale_amount) AS total_pl_coupon_sale
FROM billiards_dwd.dwd_settlement_head
"""
stats = db.query(sql_stats)
# 2. 团购订单样本
sql_samples = """
SELECT
sh.order_settle_id,
sh.coupon_amount,
sh.pl_coupon_sale_amount,
gr.ledger_amount AS groupbuy_ledger_amount,
gr.ledger_unit_price AS groupbuy_unit_price
FROM billiards_dwd.dwd_settlement_head sh
LEFT JOIN billiards_dwd.dwd_groupbuy_redemption gr
ON sh.order_settle_id = gr.order_settle_id
WHERE sh.coupon_amount != 0
LIMIT 20
"""
samples = db.query(sql_samples)
return {
'stats': dict(stats[0]) if stats else {},
'samples': [dict(r) for r in samples] if samples else []
}
def print_overall_stats(stats: Dict[str, Any]):
"""打印总体统计"""
total = stats.get('total_orders', 0)
print(f"总订单数: {total:,}")
print(f"有adjust_amount的订单: {stats.get('orders_with_adjust', 0):,} ({stats.get('orders_with_adjust', 0)/total*100:.2f}%)")
print(f"有member_discount的订单: {stats.get('orders_with_member_discount', 0):,} ({stats.get('orders_with_member_discount', 0)/total*100:.2f}%)")
print(f"有rounding的订单: {stats.get('orders_with_rounding', 0):,} ({stats.get('orders_with_rounding', 0)/total*100:.2f}%)")
print(f"有coupon的订单: {stats.get('orders_with_coupon', 0):,} ({stats.get('orders_with_coupon', 0)/total*100:.2f}%)")
print(f"有gift_card的订单: {stats.get('orders_with_gift_card', 0):,} ({stats.get('orders_with_gift_card', 0)/total*100:.2f}%)")
print()
print(f"adjust_amount总额: {stats.get('total_adjust', 0):,.2f}")
print(f"member_discount总额: {stats.get('total_member_discount', 0):,.2f}")
print(f"rounding总额: {stats.get('total_rounding', 0):,.2f}")
print(f"coupon总额: {stats.get('total_coupon', 0):,.2f}")
print(f"gift_card总额: {stats.get('total_gift_card', 0):,.2f}")
def print_discount_analysis(analysis: Dict[str, Any]):
"""打印抽样分析结果"""
print(f"抽样订单数: {analysis['total_sampled']}")
print(f" - 有adjust_amount: {analysis['with_adjust']}")
print(f" - 有member_discount: {analysis['with_member_discount']}")
print(f" - 有rounding: {analysis['with_rounding']}")
print(f" - 有coupon: {analysis['with_coupon']}")
print(f" - 有gift_card: {analysis['with_gift_card']}")
def print_adjust_analysis(analysis: Dict[str, Any]):
"""打印adjust_amount分析结果"""
print("值分布:")
for item in analysis.get('distribution', []):
print(f" {item.get('range', 'N/A')}: {item.get('count', 0):,} 单, 总额 {item.get('total_amount', 0):,.2f}")
print("\n折扣比例分布 (Top 10):")
for item in analysis.get('ratio_distribution', [])[:10]:
print(f" {item.get('discount_ratio', 0)}%: {item.get('count', 0):,}")
print("\n大额调整样本 (Top 10):")
for item in analysis.get('top_samples', []):
print(f" 订单{item.get('order_settle_id')}: 消费{item.get('consume_money', 0):,.2f}, 调整{item.get('adjust_amount', 0):,.2f} ({item.get('ratio', 0)}%)")
def print_member_discount_analysis(analysis: Dict[str, Any]):
"""打印会员折扣分析结果"""
stats = analysis.get('stats', {})
print(f"总订单数: {stats.get('total_orders', 0):,}")
print(f"有会员折扣的订单: {stats.get('with_discount', 0):,}")
print(f"会员折扣总额: {stats.get('total_discount', 0):,.2f}")
print(f"平均折扣: {stats.get('avg_discount', 0):,.2f}")
print(f"最大折扣: {stats.get('max_discount', 0):,.2f}")
samples = analysis.get('samples', [])
if samples:
print("\n样本订单:")
for item in samples[:5]:
print(f" 订单{item.get('order_settle_id')}: 会员{item.get('member_id')}, 消费{item.get('consume_money', 0):,.2f}, 折扣{item.get('member_discount_amount', 0):,.2f} ({item.get('ratio', 0)}%)")
else:
print("\n[!] 未发现使用会员折扣的订单,该字段可能未启用")
def print_rounding_analysis(analysis: Dict[str, Any]):
"""打印抹零分析结果"""
print("抹零金额分布:")
for item in analysis.get('distribution', []):
print(f" {item.get('rounding_amount', 0):,.2f}: {item.get('count', 0):,}")
print("\n抹零模式样本:")
for item in analysis.get('patterns', [])[:5]:
print(f" 实付{item.get('pay_amount', 0):,.2f} + 抹零{item.get('rounding_amount', 0):,.2f} = {item.get('before_rounding', 0):,.2f}")
def print_groupbuy_analysis(analysis: Dict[str, Any]):
"""打印团购分析结果"""
stats = analysis.get('stats', {})
print(f"总订单数: {stats.get('total_orders', 0):,}")
print(f"有coupon_amount的订单: {stats.get('with_coupon', 0):,}")
print(f"有pl_coupon_sale_amount的订单: {stats.get('with_pl_coupon', 0):,}")
print(f"coupon_amount总额: {stats.get('total_coupon_amount', 0):,.2f}")
print(f"pl_coupon_sale_amount总额: {stats.get('total_pl_coupon_sale', 0):,.2f}")
print("\n团购订单样本:")
for item in analysis.get('samples', [])[:5]:
print(f" 订单{item.get('order_settle_id')}: coupon={item.get('coupon_amount', 0):,.2f}, pl_coupon={item.get('pl_coupon_sale_amount', 0):,.2f}, groupbuy_price={item.get('groupbuy_unit_price', 'N/A')}")
def generate_report(
overall_stats: Dict[str, Any],
discount_analysis: Dict[str, Any],
adjust_analysis: Dict[str, Any],
member_discount_analysis: Dict[str, Any],
rounding_analysis: Dict[str, Any],
groupbuy_analysis: Dict[str, Any]
) -> str:
"""
生成Markdown格式的分析报告
"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
total = overall_stats.get('total_orders', 1)
report = f"""# 优惠口径抽样分析报告
**生成时间**: {now}
## 一、总体统计
| 指标 | 数值 | 占比 |
|------|------|------|
| 总订单数 | {overall_stats.get('total_orders', 0):,} | 100% |
| 有adjust_amount的订单 | {overall_stats.get('orders_with_adjust', 0):,} | {overall_stats.get('orders_with_adjust', 0)/total*100:.2f}% |
| 有member_discount的订单 | {overall_stats.get('orders_with_member_discount', 0):,} | {overall_stats.get('orders_with_member_discount', 0)/total*100:.2f}% |
| 有rounding的订单 | {overall_stats.get('orders_with_rounding', 0):,} | {overall_stats.get('orders_with_rounding', 0)/total*100:.2f}% |
| 有coupon的订单 | {overall_stats.get('orders_with_coupon', 0):,} | {overall_stats.get('orders_with_coupon', 0)/total*100:.2f}% |
| 有gift_card的订单 | {overall_stats.get('orders_with_gift_card', 0):,} | {overall_stats.get('orders_with_gift_card', 0)/total*100:.2f}% |
### 金额统计
| 优惠类型 | 总额 |
|----------|------|
| adjust_amount (台费打折/调整) | {overall_stats.get('total_adjust', 0):,.2f} |
| member_discount_amount (会员折扣) | {overall_stats.get('total_member_discount', 0):,.2f} |
| rounding_amount (抹零) | {overall_stats.get('total_rounding', 0):,.2f} |
| coupon_amount (团购抵消台费) | {overall_stats.get('total_coupon', 0):,.2f} |
| gift_card_amount (赠送卡支付) | {overall_stats.get('total_gift_card', 0):,.2f} |
## 二、adjust_amount (台费打折/调整) 分析
### 值分布
| 区间 | 订单数 | 总额 |
|------|--------|------|
"""
for item in adjust_analysis.get('distribution', []):
report += f"| {item.get('range', 'N/A')} | {item.get('count', 0):,} | {item.get('total_amount', 0):,.2f} |\n"
report += """
### 分析结论
- **是否包含大客户优惠**: 需要进一步分析adjust_amount的业务来源
- **与普通调整的区分**: 建议查看是否有备注字段或关联的优惠活动表
## 三、member_discount_amount (会员折扣) 分析
"""
member_stats = member_discount_analysis.get('stats', {})
with_discount = member_stats.get('with_discount', 0)
if with_discount == 0:
report += """### 结论
**[!] 该字段未发现任何非零值,会员折扣功能可能未启用。**
建议在DWS财务统计中可以暂时忽略此字段或将其标记为"待启用"
"""
else:
report += f"""### 使用统计
| 指标 | 数值 |
|------|------|
| 有会员折扣的订单 | {with_discount:,} |
| 会员折扣总额 | {member_stats.get('total_discount', 0):,.2f} |
| 平均折扣 | {member_stats.get('avg_discount', 0):,.2f} |
| 最大折扣 | {member_stats.get('max_discount', 0):,.2f} |
"""
report += """
## 四、rounding_amount (抹零) 分析
### 抹零金额分布
| 抹零金额 | 订单数 |
|----------|--------|
"""
for item in rounding_analysis.get('distribution', [])[:10]:
report += f"| {item.get('rounding_amount', 0):,.2f} | {item.get('count', 0):,} |\n"
report += """
### 抹零规则推断
根据抹零金额分布,推断抹零规则为:
- 抹零到整元(去除角分)
- 或抹零到特定尾数
## 五、团购优惠分析
"""
groupbuy_stats = groupbuy_analysis.get('stats', {})
report += f"""### 使用统计
| 指标 | 数值 |
|------|------|
| 有coupon_amount的订单 | {groupbuy_stats.get('with_coupon', 0):,} |
| 有pl_coupon_sale_amount的订单 | {groupbuy_stats.get('with_pl_coupon', 0):,} |
| coupon_amount总额 | {groupbuy_stats.get('total_coupon_amount', 0):,.2f} |
| pl_coupon_sale_amount总额 | {groupbuy_stats.get('total_pl_coupon_sale', 0):,.2f} |
### 团购支付金额计算路径
根据分析,团购支付金额应按以下路径计算:
1. 若 `pl_coupon_sale_amount ≠ 0` → 使用 `pl_coupon_sale_amount`
2. 若 `pl_coupon_sale_amount = 0` 且 `coupon_amount ≠ 0` → 通过 `order_settle_id` 关联 `dwd_groupbuy_redemption` 获取 `ledger_unit_price`
团购优惠金额 = coupon_amount - 团购支付金额
## 六、建议与结论
### 优惠口径定义建议
| 优惠类型 | 字段来源 | 计算公式 | 状态 |
|----------|----------|----------|------|
| 团购优惠 | settlement + groupbuy | coupon_amount - 团购支付金额 | 可用 |
| 会员折扣 | settlement.member_discount_amount | 直接取值 | 待确认 |
| 赠送卡抵扣 | settlement.gift_card_amount | 直接取值 | 可用 |
| 手动调整 | settlement.adjust_amount | 直接取值 | 可用 |
| 抹零 | settlement.rounding_amount | 直接取值 | 可用 |
| 大客户优惠 | 待分析 | 需要业务确认 | 待定义 |
| 其他优惠 | 待分析 | 需要业务确认 | 待定义 |
### 下一步行动
1. **确认会员折扣是否启用**: 与业务确认member_discount_amount的使用场景
2. **大客户优惠识别规则**: 与业务确认如何从adjust_amount中识别大客户优惠
3. **其他优惠分类**: 与业务确认adjust_amount中还包含哪些优惠类型
"""
return report
if __name__ == "__main__":
analyze_discount_patterns()

View File

@@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
"""
会员折扣启用分析脚本
功能说明:
确认 dwd_settlement_head.member_discount_amount 字段是否已启用
分析内容:
1. 统计非零记录数
2. 按时间分布分析
3. 按会员类型分析
4. 与其他字段的关联分析
输出:
- 控制台打印分析结果
- 结论:字段是否已启用,使用场景
作者ETL团队
创建日期2026-02-01
"""
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from etl_billiards.utils.config import Config
from etl_billiards.utils.db import DatabaseConnection
def analyze_member_discount_usage():
"""
执行会员折扣启用分析
"""
print("=" * 80)
print("会员折扣启用分析 (member_discount_amount)")
print("=" * 80)
print()
# 加载配置和数据库连接
config = Config()
db = DatabaseConnection(config)
try:
# 1. 基础统计
print("【1. 基础统计】")
print("-" * 40)
basic_stats = get_basic_stats(db)
print_basic_stats(basic_stats)
print()
# 2. 时间分布分析
print("【2. 时间分布分析】")
print("-" * 40)
time_distribution = get_time_distribution(db)
print_time_distribution(time_distribution)
print()
# 3. 会员类型分析
print("【3. 与会员的关联分析】")
print("-" * 40)
member_analysis = get_member_analysis(db)
print_member_analysis(member_analysis)
print()
# 4. 样本数据
print("【4. 样本数据】")
print("-" * 40)
samples = get_sample_data(db)
print_samples(samples)
print()
# 5. 结论
print("【5. 分析结论】")
print("-" * 40)
print_conclusion(basic_stats)
finally:
db.close()
def get_basic_stats(db: DatabaseConnection) -> Dict[str, Any]:
"""
获取基础统计数据
"""
sql = """
SELECT
COUNT(*) AS total_orders,
COUNT(CASE WHEN member_discount_amount != 0 THEN 1 END) AS with_member_discount,
COUNT(CASE WHEN member_discount_amount > 0 THEN 1 END) AS positive_discount,
COUNT(CASE WHEN member_discount_amount < 0 THEN 1 END) AS negative_discount,
SUM(member_discount_amount) AS total_member_discount,
AVG(CASE WHEN member_discount_amount != 0 THEN member_discount_amount END) AS avg_discount,
MAX(member_discount_amount) AS max_discount,
MIN(member_discount_amount) AS min_discount,
STDDEV(CASE WHEN member_discount_amount != 0 THEN member_discount_amount END) AS stddev_discount
FROM billiards_dwd.dwd_settlement_head
"""
rows = db.query(sql)
return dict(rows[0]) if rows else {}
def get_time_distribution(db: DatabaseConnection) -> List[Dict[str, Any]]:
"""
获取按月份的时间分布
"""
sql = """
SELECT
DATE_TRUNC('month', create_time)::DATE AS month,
COUNT(*) AS total_orders,
COUNT(CASE WHEN member_discount_amount != 0 THEN 1 END) AS with_discount,
SUM(member_discount_amount) AS total_discount
FROM billiards_dwd.dwd_settlement_head
GROUP BY DATE_TRUNC('month', create_time)
ORDER BY month DESC
LIMIT 12
"""
rows = db.query(sql)
return [dict(row) for row in rows] if rows else []
def get_member_analysis(db: DatabaseConnection) -> Dict[str, Any]:
"""
分析与会员的关联
"""
# 会员vs非会员
sql_member_vs_guest = """
SELECT
CASE WHEN member_id = 0 THEN '散客' ELSE '会员' END AS customer_type,
COUNT(*) AS total_orders,
COUNT(CASE WHEN member_discount_amount != 0 THEN 1 END) AS with_discount,
SUM(member_discount_amount) AS total_discount
FROM billiards_dwd.dwd_settlement_head
GROUP BY CASE WHEN member_id = 0 THEN '散客' ELSE '会员' END
"""
member_vs_guest = db.query(sql_member_vs_guest)
# 按会员卡等级
sql_by_grade = """
SELECT
COALESCE(m.member_card_grade_name, '未知') AS grade_name,
COUNT(*) AS total_orders,
COUNT(CASE WHEN sh.member_discount_amount != 0 THEN 1 END) AS with_discount,
SUM(sh.member_discount_amount) AS total_discount
FROM billiards_dwd.dwd_settlement_head sh
LEFT JOIN billiards_dwd.dim_member m ON sh.member_id = m.member_id
WHERE sh.member_id != 0
GROUP BY COALESCE(m.member_card_grade_name, '未知')
ORDER BY total_orders DESC
"""
by_grade = db.query(sql_by_grade)
return {
'member_vs_guest': [dict(row) for row in member_vs_guest] if member_vs_guest else [],
'by_grade': [dict(row) for row in by_grade] if by_grade else []
}
def get_sample_data(db: DatabaseConnection) -> List[Dict[str, Any]]:
"""
获取有会员折扣的样本数据
"""
sql = """
SELECT
sh.order_settle_id,
sh.order_trade_no,
sh.create_time,
sh.member_id,
m.nickname AS member_name,
m.member_card_grade_name,
sh.consume_money,
sh.pay_amount,
sh.member_discount_amount,
ROUND(sh.member_discount_amount / NULLIF(sh.consume_money, 0) * 100, 2) AS discount_ratio
FROM billiards_dwd.dwd_settlement_head sh
LEFT JOIN billiards_dwd.dim_member m ON sh.member_id = m.member_id
WHERE sh.member_discount_amount != 0
ORDER BY sh.create_time DESC
LIMIT 20
"""
rows = db.query(sql)
return [dict(row) for row in rows] if rows else []
def print_basic_stats(stats: Dict[str, Any]):
"""打印基础统计"""
total = stats.get('total_orders', 1)
with_discount = stats.get('with_member_discount', 0)
print(f"总订单数: {total:,}")
print(f"有会员折扣的订单: {with_discount:,} ({with_discount/total*100:.4f}%)")
print(f" - 正值(折扣): {stats.get('positive_discount', 0):,}")
print(f" - 负值(加价?): {stats.get('negative_discount', 0):,}")
print()
print(f"会员折扣总额: {stats.get('total_member_discount', 0):,.2f}")
print(f"平均折扣: {stats.get('avg_discount', 0) or 0:,.2f}")
print(f"最大折扣: {stats.get('max_discount', 0):,.2f}")
print(f"最小折扣: {stats.get('min_discount', 0):,.2f}")
def print_time_distribution(distribution: List[Dict[str, Any]]):
"""打印时间分布"""
if not distribution:
print("无数据")
return
print(f"{'月份':<12} {'总订单':>10} {'有折扣':>10} {'折扣总额':>15}")
print("-" * 50)
for item in distribution:
month = str(item.get('month', 'N/A'))[:7]
total = item.get('total_orders', 0)
with_discount = item.get('with_discount', 0)
total_discount = item.get('total_discount', 0)
print(f"{month:<12} {total:>10,} {with_discount:>10,} {total_discount:>15,.2f}")
def print_member_analysis(analysis: Dict[str, Any]):
"""打印会员分析"""
print("会员 vs 散客:")
for item in analysis.get('member_vs_guest', []):
print(f" {item.get('customer_type', 'N/A')}: {item.get('total_orders', 0):,} 单, {item.get('with_discount', 0)} 单有折扣, 折扣总额 {item.get('total_discount', 0):,.2f}")
print("\n按会员卡等级:")
for item in analysis.get('by_grade', []):
print(f" {item.get('grade_name', 'N/A')}: {item.get('total_orders', 0):,} 单, {item.get('with_discount', 0)} 单有折扣")
def print_samples(samples: List[Dict[str, Any]]):
"""打印样本数据"""
if not samples:
print("[!] 未发现使用会员折扣的订单")
return
print(f"{'订单ID':<20} {'会员':<15} {'等级':<10} {'消费':>12} {'折扣':>12} {'比例':>8}")
print("-" * 80)
for item in samples[:10]:
order_id = str(item.get('order_settle_id', 'N/A'))[:18]
member = str(item.get('member_name', 'N/A'))[:13]
grade = str(item.get('member_card_grade_name', 'N/A'))[:8]
consume = item.get('consume_money', 0)
discount = item.get('member_discount_amount', 0)
ratio = item.get('discount_ratio', 0)
print(f"{order_id:<20} {member:<15} {grade:<10} {consume:>12,.2f} {discount:>12,.2f} {ratio:>7}%")
def print_conclusion(stats: Dict[str, Any]):
"""打印分析结论"""
with_discount = stats.get('with_member_discount', 0)
total = stats.get('total_orders', 1)
ratio = with_discount / total * 100
if with_discount == 0:
print("【结论】: member_discount_amount 字段 **未启用**")
print()
print("该字段在所有订单中均为0表明")
print(" 1. 会员折扣功能在业务系统中未开启")
print(" 2. 或会员折扣通过其他方式如adjust_amount记录")
print()
print("【建议】:")
print(" 1. 在DWS财务统计中暂时不处理此字段")
print(" 2. 将此字段标记为'预留/待启用'")
print(" 3. 后续如果业务启用,再更新统计逻辑")
elif ratio < 1:
print(f"【结论】: member_discount_amount 字段 **极少使用** (仅{ratio:.4f}%订单)")
print()
print("该字段使用率极低,可能是:")
print(" 1. 会员折扣功能刚启用不久")
print(" 2. 仅特定场景使用")
print()
print("【建议】:")
print(" 1. 在DWS财务统计中保留此字段的处理逻辑")
print(" 2. 定期监控使用率变化")
else:
print(f"【结论】: member_discount_amount 字段 **已启用** ({ratio:.2f}%订单使用)")
print()
print("【建议】:")
print(" 1. 在DWS财务优惠明细中正常统计此字段")
print(" 2. 关注会员折扣与其他优惠的叠加规则")
if __name__ == "__main__":
analyze_member_discount_usage()

View File

@@ -28,7 +28,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path: if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from api.client import APIClient from api.recording_client import build_recording_client
from config.settings import AppConfig from config.settings import AppConfig
from database.connection import DatabaseConnection from database.connection import DatabaseConnection
from models.parsers import TypeParser from models.parsers import TypeParser
@@ -211,13 +211,7 @@ class MissingDataBackfiller:
self.store_id = int(cfg.get("app.store_id") or 0) self.store_id = int(cfg.get("app.store_id") or 0)
# API 客户端 # API 客户端
self.api = APIClient( self.api = build_recording_client(cfg, task_code="BACKFILL_MISSING_DATA")
base_url=cfg["api"]["base_url"],
token=cfg["api"]["token"],
timeout=int(cfg["api"].get("timeout_sec") or 20),
retry_max=int(cfg["api"].get("retries", {}).get("max_attempts") or 3),
headers_extra=cfg["api"].get("headers_extra") or {},
)
# 数据库连接DatabaseConnection 构造时已设置 autocommit=False # 数据库连接DatabaseConnection 构造时已设置 autocommit=False
self.db = DatabaseConnection(dsn=cfg["db"]["dsn"], session=cfg["db"].get("session")) self.db = DatabaseConnection(dsn=cfg["db"]["dsn"], session=cfg["db"].get("session"))

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
import sys
sys.path.insert(0, '.')
from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations
config = AppConfig.load()
db_conn = DatabaseConnection(config.config['db']['dsn'])
db = DatabaseOperations(db_conn)
# 检查dim_assistant表结构
print('=== dim_assistant columns ===')
sql0 = """
SELECT column_name FROM information_schema.columns
WHERE table_schema = 'billiards_dwd' AND table_name = 'dim_assistant'
"""
for row in db.query(sql0):
print(f' {dict(row)["column_name"]}')
# 检查dim_assistant数量
print()
print('=== dim_assistant ===')
sql1 = 'SELECT COUNT(*) as cnt FROM billiards_dwd.dim_assistant WHERE scd2_is_current = 1'
rows = db.query(sql1)
print(f'dim_assistant current count: {dict(rows[0])["cnt"]}')
# 检查服务记录中的nickname分布
print()
print('=== Service by nickname ===')
sql2 = """
SELECT nickname, COUNT(*) as service_count, COUNT(DISTINCT tenant_member_id) as member_count
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
GROUP BY nickname
ORDER BY service_count DESC
LIMIT 10
"""
for row in db.query(sql2):
r = dict(row)
print(f' {r["nickname"]}: {r["service_count"]} services, {r["member_count"]} members')
# 检查assistant_no分布
print()
print('=== Service by assistant_no ===')
sql3 = """
SELECT assistant_no, nickname, COUNT(*) as service_count, COUNT(DISTINCT tenant_member_id) as member_count
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
GROUP BY assistant_no, nickname
ORDER BY service_count DESC
LIMIT 10
"""
for row in db.query(sql3):
r = dict(row)
print(f' {r["assistant_no"]} ({r["nickname"]}): {r["service_count"]} services, {r["member_count"]} members')
# 近60天
print()
print('=== Last 60 days by nickname ===')
sql4 = """
SELECT nickname, COUNT(*) as service_count, COUNT(DISTINCT tenant_member_id) as member_count
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
AND last_use_time >= NOW() - INTERVAL '60 days'
GROUP BY nickname
ORDER BY service_count DESC
LIMIT 15
"""
for row in db.query(sql4):
r = dict(row)
print(f' {r["nickname"]}: {r["service_count"]} services, {r["member_count"]} members')
db_conn.close()

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
import sys
sys.path.insert(0, '.')
from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations
config = AppConfig.load()
db_conn = DatabaseConnection(config.config['db']['dsn'])
db = DatabaseOperations(db_conn)
# 检查DWD层服务记录分布
print("=== DWD层服务记录分析 ===")
print()
# 1. 总体统计
sql1 = """
SELECT
COUNT(*) as total_records,
COUNT(DISTINCT tenant_member_id) as unique_members,
COUNT(DISTINCT site_assistant_id) as unique_assistants,
COUNT(DISTINCT (tenant_member_id, site_assistant_id)) as unique_pairs
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
"""
r = dict(db.query(sql1)[0])
print("总体统计:")
print(f" 总服务记录数: {r['total_records']}")
print(f" 唯一会员数: {r['unique_members']}")
print(f" 唯一助教数: {r['unique_assistants']}")
print(f" 唯一客户-助教对: {r['unique_pairs']}")
# 2. 助教服务会员数分布
print()
print("助教服务会员数分布 (Top 10):")
sql2 = """
SELECT site_assistant_id, COUNT(DISTINCT tenant_member_id) as member_count
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
GROUP BY site_assistant_id
ORDER BY member_count DESC
LIMIT 10
"""
for row in db.query(sql2):
r = dict(row)
print(f" 助教 {r['site_assistant_id']}: 服务 {r['member_count']} 个会员")
# 3. 每个客户-助教对的服务次数分布
print()
print("客户-助教对 服务次数分布 (Top 10):")
sql3 = """
SELECT tenant_member_id, site_assistant_id, COUNT(*) as service_count
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
GROUP BY tenant_member_id, site_assistant_id
ORDER BY service_count DESC
LIMIT 10
"""
for row in db.query(sql3):
r = dict(row)
print(f" 会员 {r['tenant_member_id']} - 助教 {r['site_assistant_id']}: {r['service_count']} 次服务")
# 4. 近60天的数据
print()
print("=== 近60天数据 ===")
sql4 = """
SELECT
COUNT(*) as total_records,
COUNT(DISTINCT tenant_member_id) as unique_members,
COUNT(DISTINCT site_assistant_id) as unique_assistants,
COUNT(DISTINCT (tenant_member_id, site_assistant_id)) as unique_pairs
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
AND last_use_time >= NOW() - INTERVAL '60 days'
"""
r4 = dict(db.query(sql4)[0])
print(f" 总服务记录数: {r4['total_records']}")
print(f" 唯一会员数: {r4['unique_members']}")
print(f" 唯一助教数: {r4['unique_assistants']}")
print(f" 唯一客户-助教对: {r4['unique_pairs']}")
db_conn.close()

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
import sys
sys.path.insert(0, '.')
from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations
config = AppConfig.load()
db_conn = DatabaseConnection(config.config['db']['dsn'])
db = DatabaseOperations(db_conn)
# 检查实际统计
sql = """
SELECT
COUNT(*) as total_pairs,
COUNT(DISTINCT member_id) as unique_members,
COUNT(DISTINCT assistant_id) as unique_assistants
FROM billiards_dws.dws_member_assistant_intimacy
"""
rows = db.query(sql)
r = dict(rows[0])
print("DWS亲密指数统计:")
print(f" 总记录数(对): {r['total_pairs']}")
print(f" 唯一会员数: {r['unique_members']}")
print(f" 唯一助教数: {r['unique_assistants']}")
# 查看助教分布
sql2 = """
SELECT assistant_id, COUNT(*) as member_count
FROM billiards_dws.dws_member_assistant_intimacy
GROUP BY assistant_id
ORDER BY member_count DESC
LIMIT 10
"""
rows2 = db.query(sql2)
print()
print("Top 10 助教 (按服务会员数):")
for row in rows2:
r = dict(row)
print(f" 助教 {r['assistant_id']}: 服务 {r['member_count']} 个会员")
# 检查DWD层原始数据
sql3 = """
SELECT
COUNT(DISTINCT site_assistant_id) as unique_assistants,
COUNT(DISTINCT tenant_member_id) as unique_members
FROM billiards_dwd.dwd_assistant_service_log
WHERE tenant_member_id > 0 AND is_delete = 0
"""
rows3 = db.query(sql3)
r3 = dict(rows3[0])
print()
print("DWD层原始数据:")
print(f" 唯一助教数: {r3['unique_assistants']}")
print(f" 唯一会员数: {r3['unique_members']}")
db_conn.close()

View File

@@ -29,7 +29,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path: if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from api.client import APIClient from api.recording_client import build_recording_client
from config.settings import AppConfig from config.settings import AppConfig
from database.connection import DatabaseConnection from database.connection import DatabaseConnection
from models.parsers import TypeParser from models.parsers import TypeParser
@@ -702,6 +702,7 @@ def run_gap_check(
content_sample_limit: int | None = None, content_sample_limit: int | None = None,
window_split_unit: str | None = None, window_split_unit: str | None = None,
window_compensation_hours: int | None = None, window_compensation_hours: int | None = None,
tag: str = "",
) -> dict: ) -> dict:
cfg = cfg or AppConfig.load({}) cfg = cfg or AppConfig.load({})
tz = ZoneInfo(cfg.get("app.timezone", "Asia/Taipei")) tz = ZoneInfo(cfg.get("app.timezone", "Asia/Taipei"))
@@ -800,13 +801,8 @@ def run_gap_check(
if cutoff: if cutoff:
logger.info("CUTOFF=%s overlap_hours=%s", cutoff.isoformat(), cutoff_overlap_hours) logger.info("CUTOFF=%s overlap_hours=%s", cutoff.isoformat(), cutoff_overlap_hours)
client = APIClient( tag_suffix = f"_{tag}" if tag else ""
base_url=cfg["api"]["base_url"], client = build_recording_client(cfg, task_code=f"ODS_GAP_CHECK{tag_suffix}")
token=cfg["api"]["token"],
timeout=int(cfg["api"].get("timeout_sec") or 20),
retry_max=int(cfg["api"].get("retries", {}).get("max_attempts") or 3),
headers_extra=cfg["api"].get("headers_extra") or {},
)
db_state = _init_db_state(cfg) db_state = _init_db_state(cfg)
try: try:

View File

@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
"""
创建指数算法相关表
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations
# 表DDL
DDL_STATEMENTS = [
# 参数配置表
"""
DROP TABLE IF EXISTS billiards_dws.cfg_index_parameters CASCADE;
CREATE TABLE billiards_dws.cfg_index_parameters (
param_id SERIAL PRIMARY KEY,
index_type VARCHAR(50) NOT NULL,
param_name VARCHAR(100) NOT NULL,
param_value NUMERIC(14,6) NOT NULL,
description TEXT,
effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
effective_to DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_cfg_index_parameters UNIQUE (index_type, param_name, effective_from)
);
CREATE INDEX idx_cfg_index_params_type ON billiards_dws.cfg_index_parameters (index_type);
""",
# 召回指数表
"""
DROP TABLE IF EXISTS billiards_dws.dws_member_recall_index CASCADE;
CREATE TABLE billiards_dws.dws_member_recall_index (
recall_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
days_since_last_visit INTEGER,
visit_interval_median NUMERIC(10,2),
visit_interval_mad NUMERIC(10,2),
days_since_first_visit INTEGER,
days_since_last_recharge INTEGER,
visits_last_14_days INTEGER NOT NULL DEFAULT 0,
visits_last_60_days INTEGER NOT NULL DEFAULT 0,
score_overdue NUMERIC(10,4),
score_new_bonus NUMERIC(10,4),
score_recharge_bonus NUMERIC(10,4),
score_hot_drop NUMERIC(10,4),
raw_score NUMERIC(14,6),
display_score NUMERIC(4,2),
calc_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
calc_version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_dws_member_recall UNIQUE (site_id, member_id)
);
CREATE INDEX idx_dws_recall_display ON billiards_dws.dws_member_recall_index (site_id, display_score DESC);
""",
# 亲密指数表
"""
DROP TABLE IF EXISTS billiards_dws.dws_member_assistant_intimacy CASCADE;
CREATE TABLE billiards_dws.dws_member_assistant_intimacy (
intimacy_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
assistant_id BIGINT NOT NULL,
session_count INTEGER NOT NULL DEFAULT 0,
total_duration_minutes INTEGER NOT NULL DEFAULT 0,
basic_session_count INTEGER NOT NULL DEFAULT 0,
incentive_session_count INTEGER NOT NULL DEFAULT 0,
days_since_last_session INTEGER,
attributed_recharge_count INTEGER NOT NULL DEFAULT 0,
attributed_recharge_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
score_frequency NUMERIC(10,4),
score_recency NUMERIC(10,4),
score_recharge NUMERIC(10,4),
score_duration NUMERIC(10,4),
burst_multiplier NUMERIC(6,4),
raw_score NUMERIC(14,6),
display_score NUMERIC(4,2),
calc_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
calc_version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_dws_member_assistant_intimacy UNIQUE (site_id, member_id, assistant_id)
);
CREATE INDEX idx_dws_intimacy_member ON billiards_dws.dws_member_assistant_intimacy (site_id, member_id, display_score DESC);
CREATE INDEX idx_dws_intimacy_assistant ON billiards_dws.dws_member_assistant_intimacy (site_id, assistant_id, display_score DESC);
""",
# 分位点历史表
"""
DROP TABLE IF EXISTS billiards_dws.dws_index_percentile_history CASCADE;
CREATE TABLE billiards_dws.dws_index_percentile_history (
history_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
index_type VARCHAR(50) NOT NULL,
calc_time TIMESTAMPTZ NOT NULL,
percentile_5 NUMERIC(14,6),
percentile_95 NUMERIC(14,6),
percentile_5_smoothed NUMERIC(14,6),
percentile_95_smoothed NUMERIC(14,6),
record_count INTEGER,
min_raw_score NUMERIC(14,6),
max_raw_score NUMERIC(14,6),
avg_raw_score NUMERIC(14,6),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_dws_index_percentile_history UNIQUE (site_id, index_type, calc_time)
);
CREATE INDEX idx_dws_percentile_history ON billiards_dws.dws_index_percentile_history (site_id, index_type, calc_time DESC);
"""
]
# 初始化参数
SEED_PARAMS = """
INSERT INTO billiards_dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
('RECALL', 'lookback_days', 60, '回溯窗口(天)', CURRENT_DATE),
('RECALL', 'sigma_min', 2.0, '波动下限(天)', CURRENT_DATE),
('RECALL', 'halflife_new', 7, '新客户半衰期(天)', CURRENT_DATE),
('RECALL', 'halflife_recharge', 10, '刚充值半衰期(天)', CURRENT_DATE),
('RECALL', 'weight_overdue', 3.0, '超期紧急性权重', CURRENT_DATE),
('RECALL', 'weight_new', 1.0, '新客户权重', CURRENT_DATE),
('RECALL', 'weight_recharge', 1.0, '刚充值权重', CURRENT_DATE),
('RECALL', 'weight_hot', 1.0, '热度断档权重', CURRENT_DATE),
('RECALL', 'percentile_lower', 5, '下锚分位数', CURRENT_DATE),
('RECALL', 'percentile_upper', 95, '上锚分位数', CURRENT_DATE),
('RECALL', 'ewma_alpha', 0.2, 'EWMA平滑系数', CURRENT_DATE),
('INTIMACY', 'lookback_days', 60, '回溯窗口(天)', CURRENT_DATE),
('INTIMACY', 'session_merge_hours', 4, '会话合并间隔(小时)', CURRENT_DATE),
('INTIMACY', 'recharge_attribute_hours', 1, '充值归因窗口(小时)', CURRENT_DATE),
('INTIMACY', 'amount_base', 500, '金额压缩基准(元)', CURRENT_DATE),
('INTIMACY', 'incentive_weight', 1.5, '附加课权重倍数', CURRENT_DATE),
('INTIMACY', 'halflife_session', 14, '会话衰减半衰期(天)', CURRENT_DATE),
('INTIMACY', 'halflife_last', 10, '最近一次半衰期(天)', CURRENT_DATE),
('INTIMACY', 'halflife_recharge', 21, '充值衰减半衰期(天)', CURRENT_DATE),
('INTIMACY', 'halflife_short', 7, '短期激增检测半衰期(天)', CURRENT_DATE),
('INTIMACY', 'halflife_long', 30, '长期激增检测半衰期(天)', CURRENT_DATE),
('INTIMACY', 'weight_frequency', 2.0, '频次权重', CURRENT_DATE),
('INTIMACY', 'weight_recency', 1.5, '最近一次权重', CURRENT_DATE),
('INTIMACY', 'weight_recharge', 2.0, '归因充值权重', CURRENT_DATE),
('INTIMACY', 'weight_duration', 0.5, '时长权重', CURRENT_DATE),
('INTIMACY', 'burst_gamma', 0.6, '激增放大系数', CURRENT_DATE),
('INTIMACY', 'percentile_lower', 5, '下锚分位数', CURRENT_DATE),
('INTIMACY', 'percentile_upper', 95, '上锚分位数', CURRENT_DATE),
('INTIMACY', 'ewma_alpha', 0.2, 'EWMA平滑系数', CURRENT_DATE)
ON CONFLICT (index_type, param_name, effective_from) DO NOTHING;
"""
def main():
print("创建指数算法相关表...")
config = AppConfig.load()
db_conn = DatabaseConnection(config.config["db"]["dsn"])
try:
with db_conn.conn.cursor() as cur:
# 创建表
for i, ddl in enumerate(DDL_STATEMENTS, 1):
print(f" 执行DDL {i}/{len(DDL_STATEMENTS)}...")
cur.execute(ddl)
# 初始化参数
print(" 初始化算法参数...")
cur.execute(SEED_PARAMS)
db_conn.conn.commit()
print("完成!")
# 验证
cur.execute("SELECT COUNT(*) FROM billiards_dws.cfg_index_parameters")
count = cur.fetchone()[0]
print(f" 已插入 {count} 个参数配置")
finally:
db_conn.close()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""导出指数表数据到Markdown"""
import sys
sys.path.insert(0, '.')
from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations
config = AppConfig.load()
db_conn = DatabaseConnection(config.config['db']['dsn'])
db = DatabaseOperations(db_conn)
output_lines = []
# 1. 客户召回表
output_lines.append("## 1. 客户召回表\n")
output_lines.append("| 客户姓名 | 召回指数 |")
output_lines.append("|----------|----------|")
sql_recall = """
SELECT
COALESCE(m.nickname, CONCAT('会员', r.member_id)) AS member_name,
r.display_score AS recall_score
FROM billiards_dws.dws_member_recall_index r
LEFT JOIN billiards_dwd.dim_member m
ON r.member_id = m.member_id AND m.scd2_is_current = 1
ORDER BY r.display_score DESC
"""
rows = db.query(sql_recall)
for row in rows:
r = dict(row)
name = r['member_name'] or '未知'
score = r['recall_score']
output_lines.append(f"| {name} | {score:.2f} |")
output_lines.append(f"\n{len(rows)} 条记录\n")
# 2. 助教客户关系表
output_lines.append("## 2. 助教客户关系表\n")
output_lines.append("| 助教花名 | 客户姓名 | 关系指数 |")
output_lines.append("|----------|----------|----------|")
sql_intimacy = """
SELECT
a.nickname AS assistant_name,
i.assistant_id AS assistant_no,
COALESCE(m.nickname, CONCAT('会员', i.member_id)) AS member_name,
i.display_score AS intimacy_score,
i.session_count,
i.attributed_recharge_amount
FROM billiards_dws.dws_member_assistant_intimacy i
LEFT JOIN billiards_dwd.dim_member m
ON i.member_id = m.member_id AND m.scd2_is_current = 1
LEFT JOIN billiards_dwd.dim_assistant a
ON i.assistant_id::text = a.assistant_no AND a.scd2_is_current = 1
ORDER BY i.display_score DESC, i.session_count DESC
"""
rows2 = db.query(sql_intimacy)
for row in rows2:
r = dict(row)
assistant = r['assistant_name'] or f"工号{r.get('assistant_no', '?')}"
member = r['member_name'] or '未知'
score = r['intimacy_score']
output_lines.append(f"| {assistant} | {member} | {score:.2f} |")
output_lines.append(f"\n{len(rows2)} 条记录")
db_conn.close()
# 写入文件UTF-8带BOM
output_path = 'docs/index_tables.md'
with open(output_path, 'w', encoding='utf-8-sig') as f:
f.write('\n'.join(output_lines))
print(f"已导出到 {output_path}")

View File

@@ -0,0 +1,602 @@
# -*- coding: utf-8 -*-
"""
DWS Excel导入脚本
功能说明:
支持三类Excel数据的导入
1. 支出结构dws_finance_expense_summary
2. 平台结算dws_platform_settlement
3. 充值提成dws_assistant_recharge_commission
导入规范:
- 字段定义:按照目标表字段要求
- 时间粒度:支出按月,平台结算按日,充值提成按月
- 门店维度使用配置的site_id
- 去重规则按import_batch_no去重
- 校验规则:金额字段非负,日期格式校验
使用方式:
python import_dws_excel.py --type expense --file expenses.xlsx
python import_dws_excel.py --type platform --file platform_settlement.xlsx
python import_dws_excel.py --type commission --file recharge_commission.xlsx
作者ETL团队
创建日期2026-02-01
"""
import argparse
import os
import sys
import uuid
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
try:
import pandas as pd
except ImportError:
print("请安装 pandas: pip install pandas openpyxl")
sys.exit(1)
from etl_billiards.utils.config import Config
from etl_billiards.utils.db import DatabaseConnection
# =============================================================================
# 常量定义
# =============================================================================
# 支出类型枚举
EXPENSE_TYPES = {
'房租': 'RENT',
'水电费': 'UTILITY',
'物业费': 'PROPERTY',
'工资': 'SALARY',
'报销': 'REIMBURSE',
'平台服务费': 'PLATFORM_FEE',
'其他': 'OTHER',
}
# 支出大类映射
EXPENSE_CATEGORIES = {
'RENT': 'FIXED_COST',
'UTILITY': 'VARIABLE_COST',
'PROPERTY': 'FIXED_COST',
'SALARY': 'FIXED_COST',
'REIMBURSE': 'VARIABLE_COST',
'PLATFORM_FEE': 'VARIABLE_COST',
'OTHER': 'OTHER',
}
# 平台类型枚举
PLATFORM_TYPES = {
'美团': 'MEITUAN',
'抖音': 'DOUYIN',
'大众点评': 'DIANPING',
'其他': 'OTHER',
}
# =============================================================================
# 导入基类
# =============================================================================
class BaseImporter:
"""导入基类"""
def __init__(self, config: Config, db: DatabaseConnection):
self.config = config
self.db = db
self.site_id = config.get("app.store_id")
self.tenant_id = config.get("app.tenant_id", self.site_id)
self.batch_no = self._generate_batch_no()
def _generate_batch_no(self) -> str:
"""生成导入批次号"""
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
unique_id = str(uuid.uuid4())[:8]
return f"{timestamp}_{unique_id}"
def _safe_decimal(self, value: Any, default: Decimal = Decimal('0')) -> Decimal:
"""安全转换为Decimal"""
if value is None or pd.isna(value):
return default
try:
return Decimal(str(value))
except (ValueError, InvalidOperation):
return default
def _safe_date(self, value: Any) -> Optional[date]:
"""安全转换为日期"""
if value is None or pd.isna(value):
return None
if isinstance(value, datetime):
return value.date()
if isinstance(value, date):
return value
try:
return pd.to_datetime(value).date()
except:
return None
def _safe_month(self, value: Any) -> Optional[date]:
"""安全转换为月份(月第一天)"""
dt = self._safe_date(value)
if dt:
return dt.replace(day=1)
return None
def import_file(self, file_path: str) -> Dict[str, Any]:
"""导入文件"""
raise NotImplementedError
def validate_row(self, row: Dict[str, Any], row_idx: int) -> List[str]:
"""校验行数据,返回错误列表"""
return []
def transform_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
"""转换行数据"""
raise NotImplementedError
def insert_records(self, records: List[Dict[str, Any]]) -> int:
"""插入记录"""
raise NotImplementedError
# =============================================================================
# 支出导入
# =============================================================================
class ExpenseImporter(BaseImporter):
"""
支出导入
Excel格式要求
- 月份: 2026-01 或 2026/01/01 格式
- 支出类型: 房租/水电费/物业费/工资/报销/平台服务费/其他
- 金额: 数字
- 备注: 可选
"""
TARGET_TABLE = "billiards_dws.dws_finance_expense_summary"
REQUIRED_COLUMNS = ['月份', '支出类型', '金额']
OPTIONAL_COLUMNS = ['明细', '备注']
def import_file(self, file_path: str) -> Dict[str, Any]:
"""导入支出Excel"""
print(f"开始导入支出文件: {file_path}")
# 读取Excel
df = pd.read_excel(file_path)
# 校验必要列
missing_cols = [c for c in self.REQUIRED_COLUMNS if c not in df.columns]
if missing_cols:
return {"status": "ERROR", "message": f"缺少必要列: {missing_cols}"}
# 处理数据
records = []
errors = []
for idx, row in df.iterrows():
row_dict = row.to_dict()
row_errors = self.validate_row(row_dict, idx + 2) # Excel行号从2开始
if row_errors:
errors.extend(row_errors)
continue
record = self.transform_row(row_dict)
records.append(record)
if errors:
print(f"校验错误: {len(errors)}")
for err in errors[:10]:
print(f" - {err}")
# 插入数据
inserted = 0
if records:
inserted = self.insert_records(records)
return {
"status": "SUCCESS" if not errors else "PARTIAL",
"batch_no": self.batch_no,
"total_rows": len(df),
"inserted": inserted,
"errors": len(errors),
"error_messages": errors[:10]
}
def validate_row(self, row: Dict[str, Any], row_idx: int) -> List[str]:
errors = []
# 校验月份
month = self._safe_month(row.get('月份'))
if not month:
errors.append(f"{row_idx}: 月份格式错误")
# 校验支出类型
expense_type = row.get('支出类型', '').strip()
if expense_type not in EXPENSE_TYPES:
errors.append(f"{row_idx}: 支出类型无效 '{expense_type}'")
# 校验金额
amount = self._safe_decimal(row.get('金额'))
if amount < 0:
errors.append(f"{row_idx}: 金额不能为负数")
return errors
def transform_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
expense_type_name = row.get('支出类型', '').strip()
expense_type_code = EXPENSE_TYPES.get(expense_type_name, 'OTHER')
expense_category = EXPENSE_CATEGORIES.get(expense_type_code, 'OTHER')
return {
'site_id': self.site_id,
'tenant_id': self.tenant_id,
'expense_month': self._safe_month(row.get('月份')),
'expense_type_code': expense_type_code,
'expense_type_name': expense_type_name,
'expense_category': expense_category,
'expense_amount': self._safe_decimal(row.get('金额')),
'expense_detail': row.get('明细'),
'import_batch_no': self.batch_no,
'import_file_name': os.path.basename(str(row.get('_file_path', ''))),
'import_time': datetime.now(),
'import_user': os.getenv('USERNAME', 'system'),
'remark': row.get('备注'),
}
def insert_records(self, records: List[Dict[str, Any]]) -> int:
columns = [
'site_id', 'tenant_id', 'expense_month', 'expense_type_code',
'expense_type_name', 'expense_category', 'expense_amount',
'expense_detail', 'import_batch_no', 'import_file_name',
'import_time', 'import_user', 'remark'
]
cols_str = ", ".join(columns)
placeholders = ", ".join(["%s"] * len(columns))
sql = f"INSERT INTO {self.TARGET_TABLE} ({cols_str}) VALUES ({placeholders})"
inserted = 0
with self.db.conn.cursor() as cur:
for record in records:
values = [record.get(col) for col in columns]
cur.execute(sql, values)
inserted += cur.rowcount
self.db.commit()
return inserted
# =============================================================================
# 平台结算导入
# =============================================================================
class PlatformSettlementImporter(BaseImporter):
"""
平台结算导入
Excel格式要求
- 回款日期: 日期格式
- 平台类型: 美团/抖音/大众点评/其他
- 平台订单号: 字符串
- 订单原始金额: 数字
- 佣金: 数字
- 服务费: 数字
- 回款金额: 数字
- 备注: 可选
"""
TARGET_TABLE = "billiards_dws.dws_platform_settlement"
REQUIRED_COLUMNS = ['回款日期', '平台类型', '回款金额']
OPTIONAL_COLUMNS = ['平台订单号', '订单原始金额', '佣金', '服务费', '关联订单ID', '备注']
def import_file(self, file_path: str) -> Dict[str, Any]:
print(f"开始导入平台结算文件: {file_path}")
df = pd.read_excel(file_path)
missing_cols = [c for c in self.REQUIRED_COLUMNS if c not in df.columns]
if missing_cols:
return {"status": "ERROR", "message": f"缺少必要列: {missing_cols}"}
records = []
errors = []
for idx, row in df.iterrows():
row_dict = row.to_dict()
row_errors = self.validate_row(row_dict, idx + 2)
if row_errors:
errors.extend(row_errors)
continue
record = self.transform_row(row_dict)
records.append(record)
if errors:
print(f"校验错误: {len(errors)}")
for err in errors[:10]:
print(f" - {err}")
inserted = 0
if records:
inserted = self.insert_records(records)
return {
"status": "SUCCESS" if not errors else "PARTIAL",
"batch_no": self.batch_no,
"total_rows": len(df),
"inserted": inserted,
"errors": len(errors),
}
def validate_row(self, row: Dict[str, Any], row_idx: int) -> List[str]:
errors = []
settlement_date = self._safe_date(row.get('回款日期'))
if not settlement_date:
errors.append(f"{row_idx}: 回款日期格式错误")
platform_type = row.get('平台类型', '').strip()
if platform_type not in PLATFORM_TYPES:
errors.append(f"{row_idx}: 平台类型无效 '{platform_type}'")
amount = self._safe_decimal(row.get('回款金额'))
if amount < 0:
errors.append(f"{row_idx}: 回款金额不能为负数")
return errors
def transform_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
platform_name = row.get('平台类型', '').strip()
platform_type = PLATFORM_TYPES.get(platform_name, 'OTHER')
return {
'site_id': self.site_id,
'tenant_id': self.tenant_id,
'settlement_date': self._safe_date(row.get('回款日期')),
'platform_type': platform_type,
'platform_name': platform_name,
'platform_order_no': row.get('平台订单号'),
'order_settle_id': row.get('关联订单ID'),
'settlement_amount': self._safe_decimal(row.get('回款金额')),
'commission_amount': self._safe_decimal(row.get('佣金')),
'service_fee': self._safe_decimal(row.get('服务费')),
'gross_amount': self._safe_decimal(row.get('订单原始金额')),
'import_batch_no': self.batch_no,
'import_file_name': os.path.basename(str(row.get('_file_path', ''))),
'import_time': datetime.now(),
'import_user': os.getenv('USERNAME', 'system'),
'remark': row.get('备注'),
}
def insert_records(self, records: List[Dict[str, Any]]) -> int:
columns = [
'site_id', 'tenant_id', 'settlement_date', 'platform_type',
'platform_name', 'platform_order_no', 'order_settle_id',
'settlement_amount', 'commission_amount', 'service_fee',
'gross_amount', 'import_batch_no', 'import_file_name',
'import_time', 'import_user', 'remark'
]
cols_str = ", ".join(columns)
placeholders = ", ".join(["%s"] * len(columns))
sql = f"INSERT INTO {self.TARGET_TABLE} ({cols_str}) VALUES ({placeholders})"
inserted = 0
with self.db.conn.cursor() as cur:
for record in records:
values = [record.get(col) for col in columns]
cur.execute(sql, values)
inserted += cur.rowcount
self.db.commit()
return inserted
# =============================================================================
# 充值提成导入
# =============================================================================
class RechargeCommissionImporter(BaseImporter):
"""
充值提成导入
Excel格式要求
- 月份: 2026-01 格式
- 助教ID: 数字
- 助教花名: 字符串
- 充值订单金额: 数字
- 提成金额: 数字
- 充值订单号: 可选
- 备注: 可选
"""
TARGET_TABLE = "billiards_dws.dws_assistant_recharge_commission"
REQUIRED_COLUMNS = ['月份', '助教ID', '提成金额']
OPTIONAL_COLUMNS = ['助教花名', '充值订单金额', '充值订单ID', '充值订单号', '备注']
def import_file(self, file_path: str) -> Dict[str, Any]:
print(f"开始导入充值提成文件: {file_path}")
df = pd.read_excel(file_path)
missing_cols = [c for c in self.REQUIRED_COLUMNS if c not in df.columns]
if missing_cols:
return {"status": "ERROR", "message": f"缺少必要列: {missing_cols}"}
records = []
errors = []
for idx, row in df.iterrows():
row_dict = row.to_dict()
row_errors = self.validate_row(row_dict, idx + 2)
if row_errors:
errors.extend(row_errors)
continue
record = self.transform_row(row_dict)
records.append(record)
if errors:
print(f"校验错误: {len(errors)}")
for err in errors[:10]:
print(f" - {err}")
inserted = 0
if records:
inserted = self.insert_records(records)
return {
"status": "SUCCESS" if not errors else "PARTIAL",
"batch_no": self.batch_no,
"total_rows": len(df),
"inserted": inserted,
"errors": len(errors),
}
def validate_row(self, row: Dict[str, Any], row_idx: int) -> List[str]:
errors = []
month = self._safe_month(row.get('月份'))
if not month:
errors.append(f"{row_idx}: 月份格式错误")
assistant_id = row.get('助教ID')
if assistant_id is None or pd.isna(assistant_id):
errors.append(f"{row_idx}: 助教ID不能为空")
amount = self._safe_decimal(row.get('提成金额'))
if amount < 0:
errors.append(f"{row_idx}: 提成金额不能为负数")
return errors
def transform_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
recharge_amount = self._safe_decimal(row.get('充值订单金额'))
commission_amount = self._safe_decimal(row.get('提成金额'))
commission_ratio = commission_amount / recharge_amount if recharge_amount > 0 else None
return {
'site_id': self.site_id,
'tenant_id': self.tenant_id,
'assistant_id': int(row.get('助教ID')),
'assistant_nickname': row.get('助教花名'),
'commission_month': self._safe_month(row.get('月份')),
'recharge_order_id': row.get('充值订单ID'),
'recharge_order_no': row.get('充值订单号'),
'recharge_amount': recharge_amount,
'commission_amount': commission_amount,
'commission_ratio': commission_ratio,
'import_batch_no': self.batch_no,
'import_file_name': os.path.basename(str(row.get('_file_path', ''))),
'import_time': datetime.now(),
'import_user': os.getenv('USERNAME', 'system'),
'remark': row.get('备注'),
}
def insert_records(self, records: List[Dict[str, Any]]) -> int:
columns = [
'site_id', 'tenant_id', 'assistant_id', 'assistant_nickname',
'commission_month', 'recharge_order_id', 'recharge_order_no',
'recharge_amount', 'commission_amount', 'commission_ratio',
'import_batch_no', 'import_file_name', 'import_time',
'import_user', 'remark'
]
cols_str = ", ".join(columns)
placeholders = ", ".join(["%s"] * len(columns))
sql = f"INSERT INTO {self.TARGET_TABLE} ({cols_str}) VALUES ({placeholders})"
inserted = 0
with self.db.conn.cursor() as cur:
for record in records:
values = [record.get(col) for col in columns]
cur.execute(sql, values)
inserted += cur.rowcount
self.db.commit()
return inserted
# =============================================================================
# 主函数
# =============================================================================
def main():
parser = argparse.ArgumentParser(description='DWS Excel导入工具')
parser.add_argument(
'--type', '-t',
choices=['expense', 'platform', 'commission'],
required=True,
help='导入类型: expense(支出), platform(平台结算), commission(充值提成)'
)
parser.add_argument(
'--file', '-f',
required=True,
help='Excel文件路径'
)
args = parser.parse_args()
# 检查文件
if not os.path.exists(args.file):
print(f"文件不存在: {args.file}")
sys.exit(1)
# 加载配置
config = Config()
db = DatabaseConnection(config)
try:
# 选择导入器
if args.type == 'expense':
importer = ExpenseImporter(config, db)
elif args.type == 'platform':
importer = PlatformSettlementImporter(config, db)
elif args.type == 'commission':
importer = RechargeCommissionImporter(config, db)
else:
print(f"未知的导入类型: {args.type}")
sys.exit(1)
# 执行导入
result = importer.import_file(args.file)
# 输出结果
print("\n" + "=" * 50)
print("导入结果:")
print(f" 状态: {result.get('status')}")
print(f" 批次号: {result.get('batch_no')}")
print(f" 总行数: {result.get('total_rows')}")
print(f" 插入行数: {result.get('inserted')}")
print(f" 错误行数: {result.get('errors')}")
if result.get('status') == 'ERROR':
print(f" 错误信息: {result.get('message')}")
sys.exit(1)
except Exception as e:
print(f"导入失败: {e}")
db.rollback()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""列出指数表数据"""
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.path.insert(0, '.')
from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations
config = AppConfig.load()
db_conn = DatabaseConnection(config.config['db']['dsn'])
db = DatabaseOperations(db_conn)
# ============================================================
# 1. 客户召回表
# ============================================================
print("=" * 60)
print("1. 客户召回表")
print("=" * 60)
print(f"{'客户姓名':<20} | {'召回指数':>8}")
print("-" * 60)
sql_recall = """
SELECT
COALESCE(m.nickname, CONCAT('会员', r.member_id)) AS member_name,
r.display_score AS recall_score
FROM billiards_dws.dws_member_recall_index r
LEFT JOIN billiards_dwd.dim_member m
ON r.member_id = m.member_id AND m.scd2_is_current = 1
ORDER BY r.display_score DESC
"""
rows = db.query(sql_recall)
for row in rows:
r = dict(row)
name = r['member_name'] or '未知'
score = r['recall_score']
print(f"{name:<20} | {score:>8.2f}")
print()
print(f"{len(rows)} 条记录")
# ============================================================
# 2. 助教客户关系表
# ============================================================
print()
print("=" * 60)
print("2. 助教客户关系表")
print("=" * 60)
print(f"{'助教花名':<12} | {'客户姓名':<20} | {'关系指数':>8}")
print("-" * 60)
sql_intimacy = """
SELECT
a.nickname AS assistant_name,
i.assistant_id AS assistant_no,
COALESCE(m.nickname, CONCAT('会员', i.member_id)) AS member_name,
i.display_score AS intimacy_score,
i.session_count,
i.attributed_recharge_amount
FROM billiards_dws.dws_member_assistant_intimacy i
LEFT JOIN billiards_dwd.dim_member m
ON i.member_id = m.member_id AND m.scd2_is_current = 1
LEFT JOIN billiards_dwd.dim_assistant a
ON i.assistant_id::text = a.assistant_no AND a.scd2_is_current = 1
ORDER BY i.display_score DESC, i.session_count DESC
"""
rows2 = db.query(sql_intimacy)
for row in rows2:
r = dict(row)
assistant = r['assistant_name'] or f"工号{r.get('assistant_no', '?')}"
member = r['member_name'] or '未知'
score = r['intimacy_score']
print(f"{assistant:<12} | {member:<20} | {score:>8.2f}")
print()
print(f"{len(rows2)} 条记录")
db_conn.close()

Some files were not shown because too many files have changed in this diff Show More