ODS 完成
This commit is contained in:
1360
20251121-task.txt
Normal file
1360
20251121-task.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
DWD层设计建议.docx
Normal file
BIN
DWD层设计建议.docx
Normal file
Binary file not shown.
79
README.md
79
README.md
@@ -1,5 +1,78 @@
|
||||
# 台球场 ETL 系统(模块化版本)合并文档
|
||||
# 台球场 ETL 系统
|
||||
|
||||
本文为原多份文档(如 `INDEX.md`、`QUICK_START.md`、`ARCHITECTURE.md`、`MIGRATION_GUIDE.md`、`PROJECT_STRUCTURE.md`、`README.md` 等)的合并版,只保留与**当前项目本身**相关的内容:项目说明、目录结构、架构设计、数据与控制流程、迁移与扩展指南等,不包含修改历史和重构过程描述。
|
||||
用于台球门店业务的数据采集与入湖:从上游 API 拉取订单、支付、会员、库存等数据,先落地 ODS,再清洗写入事实/维度表,并提供运行追踪、增量游标、数据质量检查与测试脚手架。
|
||||
|
||||
## 核心特性
|
||||
- **两阶段链路**:ODS 原始留痕 + DWD/事实表清洗,支持回放与重跑。
|
||||
- **任务注册与调度**:`TaskRegistry` 统一管理任务代码,`ETLScheduler` 负责游标、运行记录和失败隔离。
|
||||
- **统一底座**:配置(默认值 + `.env` + CLI 覆盖)、分页/重试的 API 客户端、批量 Upsert 的数据库封装、SCD2 维度处理、质量检查。
|
||||
- **测试与回放**:ONLINE/OFFLINE 模式切换,`run_tests.py`/`test_presets.py` 支持参数化测试;`MANUAL_INGEST` 可将归档 JSON 重灌入 ODS。
|
||||
- **可安装**:`setup.py` / `entry_point` 提供 `etl-billiards` 命令,或直接 `python -m cli.main` 运行。
|
||||
|
||||
## 仓库结构(摘录)
|
||||
- `etl_billiards/config`:默认配置、环境变量解析、配置加载。
|
||||
- `etl_billiards/api`:HTTP 客户端,内置重试/分页。
|
||||
- `etl_billiards/database`:连接管理、批量 Upsert。
|
||||
- `etl_billiards/tasks`:业务任务(ORDERS、PAYMENTS…)、ODS 任务、DWD 任务、人工回放;`base_task.py`/`base_dwd_task.py` 提供模板。
|
||||
- `etl_billiards/loaders`:事实/维度/ODS Loader;`scd/` 为 SCD2。
|
||||
- `etl_billiards/orchestration`:调度器、任务注册表、游标与运行追踪。
|
||||
- `etl_billiards/scripts`:测试执行器、数据库连通性检测、预置测试指令。
|
||||
- `etl_billiards/tests`:单元/集成测试与离线 JSON 归档。
|
||||
|
||||
## 支持的任务代码
|
||||
- **事实/维度**:`ORDERS`、`PAYMENTS`、`REFUNDS`、`INVENTORY_CHANGE`、`COUPON_USAGE`、`MEMBERS`、`ASSISTANTS`、`PRODUCTS`、`TABLES`、`PACKAGES_DEF`、`TOPUPS`、`TABLE_DISCOUNT`、`ASSISTANT_ABOLISH`、`LEDGER`、`TICKET_DWD`、`PAYMENTS_DWD`、`MEMBERS_DWD`。
|
||||
- **ODS 原始采集**:`ODS_ORDER_SETTLE`、`ODS_TABLE_USE`、`ODS_ASSISTANT_LEDGER`、`ODS_ASSISTANT_ABOLISH`、`ODS_GOODS_LEDGER`、`ODS_PAYMENT`、`ODS_REFUND`、`ODS_COUPON_VERIFY`、`ODS_MEMBER`、`ODS_MEMBER_CARD`、`ODS_PACKAGE`、`ODS_INVENTORY_STOCK`、`ODS_INVENTORY_CHANGE`。
|
||||
- **辅助**:`MANUAL_INGEST`(将归档 JSON 回放到 ODS)。
|
||||
|
||||
## 快速开始
|
||||
1. **环境要求**:Python 3.10+、PostgreSQL。推荐在 `etl_billiards/` 目录下执行命令。
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
cd etl_billiards
|
||||
pip install -r requirements.txt
|
||||
# 开发模式:pip install -e .
|
||||
```
|
||||
3. **配置 `.env`**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 核心项
|
||||
PG_DSN=postgresql://user:pwd@host:5432/LLZQ
|
||||
API_BASE=https://api.example.com
|
||||
API_TOKEN=your_token
|
||||
STORE_ID=2790685415443269
|
||||
EXPORT_ROOT=/path/to/export
|
||||
LOG_ROOT=/path/to/logs
|
||||
```
|
||||
配置的生效顺序为 “默认值” < “环境变量/.env” < “CLI 参数”。
|
||||
4. **运行任务**
|
||||
```bash
|
||||
# 运行默认任务集
|
||||
python -m cli.main
|
||||
|
||||
# 按需选择任务(逗号分隔)
|
||||
python -m cli.main --tasks ODS_ORDER_SETTLE,ORDERS,PAYMENTS
|
||||
|
||||
# Dry-run 示例(不提交事务)
|
||||
python -m cli.main --tasks ORDERS --dry-run
|
||||
|
||||
# Windows 批处理
|
||||
..\\run_etl.bat --tasks PAYMENTS
|
||||
```
|
||||
5. **查看输出**:日志目录与导出目录分别由 `LOG_ROOT`、`EXPORT_ROOT` 控制;运行追踪与游标记录写入数据库 `etl_admin.*` 表。
|
||||
|
||||
## 数据与运行流转
|
||||
- CLI 解析参数 → `AppConfig.load()` 组装配置 → `ETLScheduler` 创建 DB/API/游标/运行追踪器。
|
||||
- 调度器按任务代码实例化任务,读取/推进游标,落盘运行记录。
|
||||
- 任务模板:确定时间窗口 → 调用 API/ODS 数据 → 解析校验 → Loader 批量 Upsert/SCD2 → 质量检查 → 提交事务并回写游标。
|
||||
|
||||
## 测试与回放
|
||||
- 单元/集成测试:`pytest` 或 `python scripts/run_tests.py --suite online`。
|
||||
- 预置组合:`python scripts/run_tests.py --preset offline_realdb`(见 `scripts/test_presets.py`)。
|
||||
- 离线模式:`TEST_MODE=OFFLINE TEST_JSON_ARCHIVE_DIR=... pytest tests/unit/test_etl_tasks_offline.py`。
|
||||
- 数据库连通性:`python scripts/test_db_connection.py --dsn postgresql://... --query "SELECT 1"`。
|
||||
|
||||
## 其他提示
|
||||
- `.env.example` 列出了所有常用配置;`config/defaults.py` 记录默认值与任务窗口配置。
|
||||
- `loaders/ods/generic.py` 支持定义主键/列名即可落 ODS;`tasks/manual_ingest_task.py` 可将归档 JSON 快速灌入对应 ODS 表。
|
||||
- 需要新增任务时,在 `tasks/` 中实现并在 `orchestration/task_registry.py` 注册即可复用调度能力。
|
||||
|
||||
## 见 etl_billiards
|
||||
@@ -1,14 +1,11 @@
|
||||
# 数据库配置
|
||||
PG_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/LLZQ
|
||||
# PG_HOST=localhost
|
||||
# PG_PORT=5432
|
||||
# PG_NAME=LLZQ
|
||||
# PG_USER=local-Python
|
||||
# PG_PASSWORD=your_password_here
|
||||
# 数据库配置(真实库)
|
||||
PG_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/LLZQ-test
|
||||
PG_CONNECT_TIMEOUT=10
|
||||
# 如需拆分配置:PG_HOST=... PG_PORT=... PG_NAME=... PG_USER=... PG_PASSWORD=...
|
||||
|
||||
# API配置(抓取时,非球的一些配置)
|
||||
API_BASE=https://api.example.com # 非球URL前缀
|
||||
API_TOKEN=your_token_here # 登录Token
|
||||
# API配置(如需走真实接口再填写)
|
||||
API_BASE=https://api.example.com
|
||||
API_TOKEN=your_token_here
|
||||
# API_TIMEOUT=20
|
||||
# API_PAGE_SIZE=200
|
||||
# API_RETRY_MAX=3
|
||||
@@ -20,17 +17,20 @@ STORE_ID=2790685415443269
|
||||
# SCHEMA_ETL=etl_admin
|
||||
|
||||
# 路径配置
|
||||
EXPORT_ROOT=r"D:\LLZQ\DB\export",
|
||||
LOG_ROOT=r"D:\LLZQ\DB\logs",
|
||||
EXPORT_ROOT=C:\dev\LLTQ\export\JSON
|
||||
LOG_ROOT=C:\dev\LLTQ\export\LOG
|
||||
FETCH_ROOT=
|
||||
INGEST_SOURCE_DIR=
|
||||
WRITE_PRETTY_JSON=false
|
||||
PGCLIENTENCODING=utf8
|
||||
|
||||
# ETL配置
|
||||
OVERLAP_SECONDS=120 # 为了防止边界遗漏,会往前“回拨”一点的冗余秒数
|
||||
OVERLAP_SECONDS=120
|
||||
WINDOW_BUSY_MIN=30
|
||||
WINDOW_IDLE_MIN=180
|
||||
IDLE_START=04:00
|
||||
IDLE_END=16:00
|
||||
ALLOW_EMPTY_ADVANCE=true
|
||||
ALLOW_EMPTY_RESULT_ADVANCE=true
|
||||
|
||||
# 清洗配置
|
||||
LOG_UNKNOWN_FIELDS=true
|
||||
@@ -38,10 +38,16 @@ HASH_ALGO=sha1
|
||||
STRICT_NUMERIC=true
|
||||
ROUND_MONEY_SCALE=2
|
||||
|
||||
# 测试/离线模式
|
||||
TEST_MODE=OFFLINE
|
||||
TEST_JSON_ARCHIVE_DIR=tests/testdata_json #指定离线模式(OFFLINE)要读取的 JSON 归档目录。测试或回放任务时,会从这个目录中找到各个任务预先保存的 API 响应文件,直接做 Transform + Load,不再访问真实接口。
|
||||
TEST_JSON_TEMP_DIR=/tmp/etl_billiards_json_tmp #指定测试运行时临时生成或复制 JSON 文件的目录。在线/离线联动测试会把输出、中间文件等写到这个路径,既避免污染真实导出目录,也让 CI 可以在一次运行中隔离不同任务产生的临时数据。
|
||||
# 测试/离线模式(真实库联调建议 ONLINE)
|
||||
TEST_MODE=ONLINE
|
||||
TEST_JSON_ARCHIVE_DIR=tests/source-data-doc
|
||||
TEST_JSON_TEMP_DIR=/tmp/etl_billiards_json_tmp
|
||||
|
||||
# 测试数据库(留空则单元测试使用伪库)
|
||||
# 测试数据库
|
||||
TEST_DB_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/LLZQ-test
|
||||
|
||||
# ODS <20>ؽ<EFBFBD><D8BD>ű<EFBFBD><C5B1><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>
|
||||
JSON_DOC_DIR=C:\dev\LLTQ\export\test-json-doc
|
||||
ODS_INCLUDE_FILES=
|
||||
ODS_DROP_SCHEMA_FIRST=true
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# 数据库配置
|
||||
PG_DSN=postgresql://user:password@localhost:5432/LLZQ
|
||||
PG_DSN=postgresql://user:password@localhost:5432/....
|
||||
PG_HOST=localhost
|
||||
PG_PORT=5432
|
||||
PG_NAME=LLZQ
|
||||
PG_USER=local-Python
|
||||
PG_PASSWORD=your_password_here
|
||||
PG_CONNECT_TIMEOUT=10
|
||||
|
||||
# API配置
|
||||
API_BASE=https://api.example.com
|
||||
@@ -12,6 +13,7 @@ API_TOKEN=your_token_here
|
||||
API_TIMEOUT=20
|
||||
API_PAGE_SIZE=200
|
||||
API_RETRY_MAX=3
|
||||
API_RETRY_BACKOFF=[1,2,4]
|
||||
|
||||
# 应用配置
|
||||
STORE_ID=2790685415443269
|
||||
@@ -22,6 +24,11 @@ SCHEMA_ETL=etl_admin
|
||||
# 路径配置
|
||||
EXPORT_ROOT=/path/to/export
|
||||
LOG_ROOT=/path/to/logs
|
||||
FETCH_ROOT=/path/to/json_fetch
|
||||
INGEST_SOURCE_DIR=
|
||||
WRITE_PRETTY_JSON=false
|
||||
MANIFEST_NAME=manifest.json
|
||||
INGEST_REPORT_NAME=ingest_report.json
|
||||
|
||||
# ETL配置
|
||||
OVERLAP_SECONDS=120
|
||||
@@ -29,7 +36,7 @@ WINDOW_BUSY_MIN=30
|
||||
WINDOW_IDLE_MIN=180
|
||||
IDLE_START=04:00
|
||||
IDLE_END=16:00
|
||||
ALLOW_EMPTY_ADVANCE=true
|
||||
ALLOW_EMPTY_RESULT_ADVANCE=true
|
||||
|
||||
# 清洗配置
|
||||
LOG_UNKNOWN_FIELDS=true
|
||||
@@ -39,8 +46,14 @@ ROUND_MONEY_SCALE=2
|
||||
|
||||
# 测试/离线模式
|
||||
TEST_MODE=ONLINE
|
||||
TEST_JSON_ARCHIVE_DIR=tests/testdata_json
|
||||
TEST_JSON_ARCHIVE_DIR=tests/source-data-doc
|
||||
TEST_JSON_TEMP_DIR=/tmp/etl_billiards_json_tmp
|
||||
|
||||
# 测试数据库(可选:若设置则单元测试连入此 DSN)
|
||||
TEST_DB_DSN=
|
||||
|
||||
# ODS <20>ؽ<EFBFBD><D8BD>ű<EFBFBD><C5B1><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>
|
||||
JSON_DOC_DIR=C:\dev\LLTQ\export\test-json-doc
|
||||
ODS_INCLUDE_FILES=
|
||||
ODS_DROP_SCHEMA_FIRST=true
|
||||
|
||||
|
||||
@@ -1,7 +1,40 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""Simple PostgreSQL connectivity smoke-checker."""
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
from psycopg2 import OperationalError
|
||||
|
||||
# Filename : helloworld.py
|
||||
# author by : www.runoob.com
|
||||
|
||||
# 该实例输出 Hello World!
|
||||
print('Hello World!')
|
||||
DEFAULT_DSN = os.environ.get(
|
||||
"PG_DSN", "postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/LLZQ-test"
|
||||
)
|
||||
DEFAULT_TIMEOUT = max(1, min(int(os.environ.get("PG_CONNECT_TIMEOUT", 10)), 20))
|
||||
|
||||
|
||||
def check_postgres_connection(dsn: str, timeout: int = DEFAULT_TIMEOUT) -> bool:
|
||||
"""Return True if connection succeeds; print diagnostics otherwise."""
|
||||
try:
|
||||
conn = psycopg2.connect(dsn, connect_timeout=timeout)
|
||||
with conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT 1;")
|
||||
_ = cur.fetchone()
|
||||
print(f"PostgreSQL 连接成功 (timeout={timeout}s)")
|
||||
return True
|
||||
except OperationalError as exc:
|
||||
print("PostgreSQL 连接失败(OperationalError):", exc)
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
print("PostgreSQL 连接失败(其他异常):", exc)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dsn = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_DSN
|
||||
if not dsn:
|
||||
print("缺少 DSN,请传入参数或设置 PG_DSN 环境变量。")
|
||||
sys.exit(2)
|
||||
|
||||
ok = check_postgres_connection(dsn)
|
||||
if not ok:
|
||||
sys.exit(1)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ cp .env.example .env
|
||||
|
||||
```bash
|
||||
# 数据库
|
||||
PG_DSN=postgresql://user:password@localhost:5432/LLZQ
|
||||
PG_DSN=postgresql://user:password@localhost:5432/....
|
||||
|
||||
# API
|
||||
API_BASE=https://api.example.com
|
||||
@@ -114,13 +114,13 @@ run_etl.bat --tasks ORDERS
|
||||
- 日志目录:使用 `LOG_ROOT` 指定,例如
|
||||
|
||||
```bash
|
||||
ls -la D:\LLZQ\DB\logs/
|
||||
ls -la C:\dev\LLTQ\export\LOG/
|
||||
```
|
||||
|
||||
- 导出目录:使用 `EXPORT_ROOT` 指定,例如
|
||||
|
||||
```bash
|
||||
ls -la D:\LLZQ\DB\export/
|
||||
ls -la C:\dev\LLTQ\export\JSON/
|
||||
```
|
||||
|
||||
---
|
||||
@@ -195,7 +195,7 @@ pytest --cov=. --cov-report=html
|
||||
|
||||
- `TEST_MODE=ONLINE`(默认)时,测试会模拟实时 API,完整执行 E/T/L。
|
||||
- `TEST_MODE=OFFLINE` 时,测试改为从 `TEST_JSON_ARCHIVE_DIR` 指定的归档 JSON 中读取数据,仅做 Transform + Load,适合验证本地归档数据是否仍可回放。
|
||||
- `TEST_JSON_ARCHIVE_DIR`:离线 JSON 归档目录(示例:`tests/testdata_json` 或 CI 产出的快照)。
|
||||
- `TEST_JSON_ARCHIVE_DIR`:离线 JSON 归档目录(示例:`tests/source-data-doc` 或 CI 产出的快照)。
|
||||
- `TEST_JSON_TEMP_DIR`:测试生成的临时 JSON 输出目录,便于隔离每次运行的数据。
|
||||
- `TEST_DB_DSN`:可选,若设置则单元测试会连接到此 PostgreSQL DSN,实打实执行写库;留空时测试使用内存伪库,避免依赖数据库。
|
||||
|
||||
@@ -206,7 +206,7 @@ pytest --cov=. --cov-report=html
|
||||
TEST_MODE=ONLINE pytest tests/unit/test_etl_tasks_online.py
|
||||
|
||||
# 离线模式使用归档 JSON 覆盖所有任务
|
||||
TEST_MODE=OFFLINE TEST_JSON_ARCHIVE_DIR=tests/testdata_json pytest tests/unit/test_etl_tasks_offline.py
|
||||
TEST_MODE=OFFLINE TEST_JSON_ARCHIVE_DIR=tests/source-data-doc pytest tests/unit/test_etl_tasks_offline.py
|
||||
|
||||
# 使用脚本按需组合参数(示例:在线 + 仅订单用例)
|
||||
python scripts/run_tests.py --suite online --mode ONLINE --keyword ORDERS
|
||||
@@ -228,7 +228,7 @@ python scripts/run_tests.py --list-presets # 查看或自定义 scripts/test_p
|
||||
`test_presets.py` 充当“指令仓库”。每个预置都是一个字典,常用字段解释如下:
|
||||
|
||||
| 字段 | 作用 |
|
||||
| ---- | ---- |
|
||||
| ---------------------------- | ------------------------------------------------------------------ |
|
||||
| `suite` | 复用 `run_tests.py` 内置套件(online/offline/integration,可多选) |
|
||||
| `tests` | 追加任意 pytest 路径,例如 `tests/unit/test_config.py` |
|
||||
| `mode` | 覆盖 `TEST_MODE`(ONLINE / OFFLINE) |
|
||||
@@ -239,7 +239,7 @@ python scripts/run_tests.py --list-presets # 查看或自定义 scripts/test_p
|
||||
| `env` | 额外环境变量列表,如 `["STORE_ID=123"]` |
|
||||
| `preset_meta` | 说明性文字,便于描述场景 |
|
||||
|
||||
示例:`offline_realdb` 预置会设置 `TEST_MODE=OFFLINE`、指定 `tests/testdata_json` 为归档目录,并通过 `db_dsn` 连到测试库。执行 `python scripts/run_tests.py --preset offline_realdb` 或 `python scripts/test_presets.py --preset offline_realdb` 即可复用该组合,保证本地、CI 与生产回放脚本一致。
|
||||
示例:`offline_realdb` 预置会设置 `TEST_MODE=OFFLINE`、指定 `tests/source-data-doc` 为归档目录,并通过 `db_dsn` 连到测试库。执行 `python scripts/run_tests.py --preset offline_realdb` 或 `python scripts/test_presets.py --preset offline_realdb` 即可复用该组合,保证本地、CI 与生产回放脚本一致。
|
||||
|
||||
#### 3.3.3 数据库连通性快速检查
|
||||
|
||||
@@ -250,7 +250,7 @@ python scripts/run_tests.py --list-presets # 查看或自定义 scripts/test_p
|
||||
python scripts/test_db_connection.py
|
||||
|
||||
# 临时指定 DSN,并检查任务配置表
|
||||
python scripts/test_db_connection.py --dsn postgresql://user:pwd@host:5432/LLZQ-test --query "SELECT count(*) FROM etl_admin.etl_task"
|
||||
python scripts/test_db_connection.py --dsn postgresql://user:pwd@host:5432/.... --query "SELECT count(*) FROM etl_admin.etl_task"
|
||||
```
|
||||
|
||||
脚本返回 0 代表连接与查询成功;若返回非 0,可结合第 8 章“常见问题排查”的数据库章节(网络、防火墙、账号权限等)先定位问题,再运行完整 ETL。
|
||||
@@ -424,32 +424,39 @@ etl_billiards/
|
||||
### 5.2 各层职责(当前设计)
|
||||
|
||||
- **CLI 层 (`cli/`)**
|
||||
|
||||
- 解析命令行参数(指定任务列表、Dry-run、覆盖配置项等)。
|
||||
- 初始化配置与日志后交由编排层执行。
|
||||
|
||||
- **编排层 (`orchestration/`)**
|
||||
|
||||
- `scheduler.py`:根据配置与 CLI 参数选择需要执行的任务,控制执行顺序和并行策略。
|
||||
- `task_registry.py`:提供任务注册表,按任务代码创建任务实例(工厂模式)。
|
||||
- `cursor_manager.py`:管理增量游标(时间窗口 / ID 游标)。
|
||||
- `run_tracker.py`:记录每次任务运行的状态、统计信息和错误信息。
|
||||
|
||||
- **任务层 (`tasks/`)**
|
||||
|
||||
- `base_task.py`:定义任务执行模板流程(模板方法模式),包括获取窗口、调用上游、解析 / 校验、写库、更新游标等。
|
||||
- `orders_task.py` / `payments_task.py` / `members_task.py`:实现具体任务逻辑(订单、支付、会员)。
|
||||
|
||||
- **加载器 / SCD / 质量层**
|
||||
|
||||
- `loaders/`:根据目标表封装 Upsert / Insert / Update 逻辑。
|
||||
- `scd/scd2_handler.py`:为维度表提供 SCD2 历史管理能力。
|
||||
- `quality/`:执行数据质量检查,如余额对账。
|
||||
|
||||
- **模型层 (`models/`)**
|
||||
|
||||
- `parsers.py`:负责数据类型转换(字符串 → 时间戳、Decimal、int 等)。
|
||||
- `validators.py`:执行字段级和记录级的数据校验。
|
||||
|
||||
- **API 层 (`api/client.py`)**
|
||||
|
||||
- 封装 HTTP 调用,处理重试、超时及分页。
|
||||
|
||||
- **数据库层 (`database/`)**
|
||||
|
||||
- 管理数据库连接及上下文。
|
||||
- 提供批量插入 / 更新 / Upsert 操作接口。
|
||||
|
||||
@@ -510,7 +517,7 @@ etl_billiards/
|
||||
### 6.1 核心功能映射示意
|
||||
|
||||
| 旧版本函数 / 类 | 新版本位置 | 说明 |
|
||||
|---------------------------|--------------------------------------------------------|----------------|
|
||||
| --------------------- | ----------------------------------------------------- | ---------- |
|
||||
| `DEFAULTS` 字典 | `config/defaults.py` | 配置默认值 |
|
||||
| `build_config()` | `config/settings.py::AppConfig.load()` | 配置加载 |
|
||||
| `Pg` 类 | `database/connection.py::DatabaseConnection` | 数据库连接 |
|
||||
@@ -525,6 +532,7 @@ etl_billiards/
|
||||
### 6.2 典型迁移步骤
|
||||
|
||||
1. **配置迁移**
|
||||
|
||||
- 原来在 `DEFAULTS` 或脚本内硬编码的配置,迁移到 `.env` 与 `config/defaults.py`。
|
||||
- 使用 `AppConfig.load()` 统一获取配置。
|
||||
|
||||
@@ -541,6 +549,7 @@ etl_billiards/
|
||||
对比新旧版本导出的数据表和日志,确认一致性。
|
||||
|
||||
3. **自定义逻辑迁移**
|
||||
|
||||
- 原脚本中的自定义清洗逻辑 → 放入相应 `loaders/` 或任务类中。
|
||||
- 自定义任务 → 在 `tasks/` 中实现并在 `task_registry` 中注册。
|
||||
- 自定义 API 调用 → 扩展 `api/client.py` 或单独封装服务类。
|
||||
@@ -692,3 +701,137 @@ class MyLoader(BaseLoader):
|
||||
|
||||
- 本文已合并原有的快速开始、项目结构、架构说明、迁移指南等内容,可作为当前项目的统一说明文档。
|
||||
- 如需在此基础上拆分多份文档,可按章节拆出,例如「快速开始」「架构设计」「迁移指南」「开发扩展」等。
|
||||
|
||||
## 11. 运行/调试模式说明
|
||||
|
||||
- 生产环境仅保留“任务模式”:通过调度/CLI 执行注册的任务(ETL/ODS),不使用调试脚本。
|
||||
- 开发/调试可使用的辅助脚本(上线前可删除或禁用):
|
||||
- `python -m etl_billiards.scripts.rebuild_ods_from_json`:从本地 JSON 目录重建 `billiards_ods`,用于离线初始化/验证。环境变量:`PG_DSN`(必填)、`JSON_DOC_DIR`(可选,默认 `C:\dev\LLTQ\export\test-json-doc`)、`INCLUDE_FILES`(逗号分隔文件名)、`DROP_SCHEMA_FIRST`(默认 true)。
|
||||
- 如需在生产环境保留脚本,请在运维手册中明确用途和禁用条件,避免误用。
|
||||
|
||||
## 12. ODS 任务上线指引
|
||||
|
||||
- 任务注册:`etl_billiards/database/seed_ods_tasks.sql` 列出了当前启用的 ODS 任务。将其中的 `store_id` 替换为实际门店后执行:
|
||||
```
|
||||
psql "$PG_DSN" -f etl_billiards/database/seed_ods_tasks.sql
|
||||
```
|
||||
`ON CONFLICT` 会保持 enabled=true,避免重复。
|
||||
- 调度:确认 `etl_admin.etl_task` 中已启用所需的 ODS 任务(任务代码见 seed 脚本),调度器或 CLI `--tasks` 即可调用。
|
||||
- 离线回灌:开发环境可用 `rebuild_ods_from_json` 以样例 JSON 初始化 ODS,生产慎用;默认按 `(source_file, record_index)` 去重。
|
||||
- 测试:`pytest etl_billiards/tests/unit/test_ods_tasks.py` 覆盖核心 ODS 任务;测试时可设置 `ETL_SKIP_DOTENV=1` 跳过本地 .env 读取。
|
||||
|
||||
## 13. ODS 表映射总览
|
||||
|
||||
| ODS 表名 | 接口 Path | 数据列表路径 |
|
||||
| ------------------------------------ | ---------------------------------------------------- | ----------------------------- |
|
||||
| `assistant_accounts_master` | `/PersonnelManagement/SearchAssistantInfo` | data.assistantInfos |
|
||||
| `assistant_service_records` | `/AssistantPerformance/GetOrderAssistantDetails` | data.orderAssistantDetails |
|
||||
| `assistant_cancellation_records` | `/AssistantPerformance/GetAbolitionAssistant` | data.abolitionAssistants |
|
||||
| `goods_stock_movements` | `/GoodsStockManage/QueryGoodsOutboundReceipt` | data.queryDeliveryRecordsList |
|
||||
| `goods_stock_summary` | `/TenantGoods/GetGoodsStockReport` | data |
|
||||
| `group_buy_packages` | `/PackageCoupon/QueryPackageCouponList` | data.packageCouponList |
|
||||
| `group_buy_redemption_records` | `/Site/GetSiteTableUseDetails` | data.siteTableUseDetailsList |
|
||||
| `member_profiles` | `/MemberProfile/GetTenantMemberList` | data.tenantMemberInfos |
|
||||
| `member_balance_changes` | `/MemberProfile/GetMemberCardBalanceChange` | data.tenantMemberCardLogs |
|
||||
| `member_stored_value_cards` | `/MemberProfile/GetTenantMemberCardList` | data.tenantMemberCards |
|
||||
| `payment_transactions` | `/PayLog/GetPayLogListPage` | data |
|
||||
| `platform_coupon_redemption_records` | `/Promotion/GetOfflineCouponConsumePageList` | data |
|
||||
| `recharge_settlements` | `/Site/GetRechargeSettleList` | data.settleList |
|
||||
| `refund_transactions` | `/Order/GetRefundPayLogList` | data |
|
||||
| `settlement_records` | `/Site/GetAllOrderSettleList` | data.settleList |
|
||||
| `settlement_ticket_details` | `/Order/GetOrderSettleTicketNew` | (整包原始 JSON) |
|
||||
| `site_tables_master` | `/Table/GetSiteTables` | data.siteTables |
|
||||
| `stock_goods_category_tree` | `/TenantGoodsCategory/QueryPrimarySecondaryCategory` | data.goodsCategoryList |
|
||||
| `store_goods_master` | `/TenantGoods/GetGoodsInventoryList` | data.orderGoodsList |
|
||||
| `store_goods_sales_records` | `/TenantGoods/GetGoodsSalesList` | data.orderGoodsLedgers |
|
||||
| `table_fee_discount_records` | `/Site/GetTaiFeeAdjustList` | data.taiFeeAdjustInfos |
|
||||
| `table_fee_transactions` | `/Site/GetSiteTableOrderDetails` | data.siteTableUseDetailsList |
|
||||
| `tenant_goods_master` | `/TenantGoods/QueryTenantGoods` | data.tenantGoodsList |
|
||||
|
||||
## 14. ODS 相关环境变量/默认值
|
||||
|
||||
- `.env` / 环境变量:
|
||||
- `JSON_DOC_DIR`:ODS 样例 JSON 目录(开发/回灌用)
|
||||
- `ODS_INCLUDE_FILES`:限定导入的文件名(逗号分隔,不含 .json)
|
||||
- `ODS_DROP_SCHEMA_FIRST`:true/false,是否重建 schema
|
||||
- `ETL_SKIP_DOTENV`:测试/CI 时设为 1 跳过本地 .env 读取
|
||||
- `config/defaults.py` 中 `ods` 默认值:
|
||||
- `json_doc_dir`: `C:\dev\LLTQ\export\test-json-doc`
|
||||
- `include_files`: `""`
|
||||
- `drop_schema_first`: `True`
|
||||
|
||||
---
|
||||
|
||||
## 15. DWD 维度 “业务事件”
|
||||
|
||||
1. 粒度唯一、原子
|
||||
|
||||
- 一张 DWD 表只能有一种业务粒度,比如:
|
||||
- 一条记录 = 一次结账;
|
||||
- 一条记录 = 一段台费流水;
|
||||
- 一条记录 = 一次助教服务;
|
||||
- 一条记录 = 一次会员余额变动。
|
||||
- 表里面不能又混“订单头”又混“订单行”,不能一部分是“汇总”,一部分是“明细”。
|
||||
- 一旦粒度确定,所有字段都要跟这个粒度匹配:
|
||||
- 比如“结账头表”就不要塞每一行商品明细;
|
||||
- 商品明细就不要塞整单级别的总金额。
|
||||
- 这是 DWD 层最重要的一条。
|
||||
|
||||
2. 以业务过程建模,不以 JSON 列表建模
|
||||
|
||||
- 先画清楚你真实的业务链路:
|
||||
- 开台 / 换台 / 关台 → 台费流水
|
||||
- 助教上桌 → 助教服务流水 / 废除事件
|
||||
- 点单 → 商品销售流水
|
||||
- 充值 / 消费 → 余额变更 / 充值单
|
||||
- 结账 → 结账头表 + 支付流水 / 退款流水
|
||||
- 团购 / 平台券 → 核销流水
|
||||
|
||||
3. 主键明确、外键统一
|
||||
|
||||
- 每张 DWD 表必须有业务主键(哪怕是接口给的 id),不要依赖数据库自增。
|
||||
- 所有“同一概念”的字段必须统一命名、统一含义:
|
||||
- 门店:统一叫 site_id,都对应 siteProfile.id;
|
||||
- 会员:统一叫 member_id 对应 member_profiles.id,system_member_id 单独一列;
|
||||
- 台桌:统一 table_id 对应 site_tables_master.id;
|
||||
- 结账:统一 order_settle_id;
|
||||
- 订单:统一 order_trade_no 等。
|
||||
- 否则后面 DWS、AI 要把表拼起来会非常痛苦。
|
||||
|
||||
4. 保留明细,不做过度汇总
|
||||
|
||||
- DWD 层的事实表原则上只做“明细级”的数据:
|
||||
- 不要在 DWD 就把“日汇总、周汇总、月汇总”算出来,那是 DWS 的事;
|
||||
- 也不要把多个事件折成一行(例如一张表同时放日汇总+单笔流水)。
|
||||
- 需要聚合时,再在 DWS 做主题宽表:
|
||||
- dws_member_day_profile、dws_site_day_summary 等。
|
||||
- DWD 只负责细颗粒度的真相。
|
||||
|
||||
5. 统一清洗、标准化,但保持可追溯
|
||||
|
||||
- 在 DWD 层一定要做的清洗:
|
||||
- 类型转换:字符串时间 → 时间类型,金额统一为 decimal,布尔统一为 0/1;
|
||||
- 单位统一:秒 / 分钟、元 / 分都统一;
|
||||
- 枚举标准化:状态码、类型码在 DWD 里就定死含义,必要时建枚举维表。
|
||||
- 同时要保证:
|
||||
- 每条 DWD 记录都能追溯回 ODS:
|
||||
- 保留源系统主键;
|
||||
- 保留原始时间 / 原始金额字段(不要覆盖掉)。
|
||||
|
||||
6. 扁平化、去嵌套
|
||||
|
||||
- JSON 里常见结构是:分页壳 + 头 + 明细数组 + 各种嵌套对象(siteProfile、tableProfile、goodsLedgers…)。
|
||||
- DWD 的原则是:
|
||||
- 去掉分页壳;
|
||||
- 把“数组”拆成子表(头表 / 行表);
|
||||
- 把重复出现的 profile 抽出去做维度表(门店、台、商品、会员……)。
|
||||
- 目标是:DWD 表都是二维表结构,不存复杂嵌套 JSON。
|
||||
|
||||
7. 模型长期稳定,可扩展
|
||||
|
||||
- DWD 的表结构要尽可能稳定,新增需求尽量通过:
|
||||
- 加字段;
|
||||
- 新建事实表 / 维度表;
|
||||
- 在 DWS 做派生指标;
|
||||
- 而不是频繁重构已有 DWD 表结构。
|
||||
- 这点跟你后面要喂给 LLM 也很相关:AI 配的 prompt、schema 理解都要尽量少改。
|
||||
|
||||
@@ -15,10 +15,10 @@ DEFAULT_BROWSER_HEADERS = {
|
||||
"Referer": "https://pc.ficoo.vip/",
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
"(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"sec-ch-ua": '"Google Chrome";v="120", "Not?A_Brand";v="8", "Chromium";v="120"',
|
||||
"sec-ch-ua": '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-fetch-site": "same-origin",
|
||||
@@ -26,6 +26,7 @@ DEFAULT_BROWSER_HEADERS = {
|
||||
"sec-fetch-dest": "empty",
|
||||
"priority": "u=1, i",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"DNT": "1",
|
||||
}
|
||||
|
||||
DEFAULT_LIST_KEYS: Tuple[str, ...] = (
|
||||
|
||||
@@ -39,11 +39,23 @@ def parse_args():
|
||||
parser.add_argument("--api-token", "--token", dest="api_token", help="API令牌(Bearer Token)")
|
||||
parser.add_argument("--api-timeout", type=int, help="API超时(秒)")
|
||||
parser.add_argument("--api-page-size", type=int, help="分页大小")
|
||||
parser.add_argument("--api-retry-max", type=int, help="API重试最大次数")
|
||||
|
||||
# 目录参数
|
||||
parser.add_argument("--export-root", help="导出根目录")
|
||||
parser.add_argument("--log-root", help="日志根目录")
|
||||
|
||||
# 抓取/清洗管线
|
||||
parser.add_argument("--pipeline-flow", choices=["FULL", "FETCH_ONLY", "INGEST_ONLY"], help="流水线模式")
|
||||
parser.add_argument("--fetch-root", help="抓取JSON输出根目录")
|
||||
parser.add_argument("--ingest-source", help="本地清洗入库源目录")
|
||||
parser.add_argument("--write-pretty-json", action="store_true", help="抓取JSON美化输出")
|
||||
|
||||
# 运行窗口
|
||||
parser.add_argument("--idle-start", help="闲时窗口开始(HH:MM)")
|
||||
parser.add_argument("--idle-end", help="闲时窗口结束(HH:MM)")
|
||||
parser.add_argument("--allow-empty-advance", action="store_true", help="允许空结果推进窗口")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def build_cli_overrides(args) -> dict:
|
||||
@@ -77,6 +89,8 @@ def build_cli_overrides(args) -> dict:
|
||||
overrides.setdefault("api", {})["timeout_sec"] = args.api_timeout
|
||||
if args.api_page_size:
|
||||
overrides.setdefault("api", {})["page_size"] = args.api_page_size
|
||||
if args.api_retry_max:
|
||||
overrides.setdefault("api", {}).setdefault("retries", {})["max_attempts"] = args.api_retry_max
|
||||
|
||||
# 目录
|
||||
if args.export_root:
|
||||
@@ -84,6 +98,24 @@ def build_cli_overrides(args) -> dict:
|
||||
if args.log_root:
|
||||
overrides.setdefault("io", {})["log_root"] = args.log_root
|
||||
|
||||
# 抓取/清洗管线
|
||||
if args.pipeline_flow:
|
||||
overrides.setdefault("pipeline", {})["flow"] = args.pipeline_flow.upper()
|
||||
if args.fetch_root:
|
||||
overrides.setdefault("pipeline", {})["fetch_root"] = args.fetch_root
|
||||
if args.ingest_source:
|
||||
overrides.setdefault("pipeline", {})["ingest_source_dir"] = args.ingest_source
|
||||
if args.write_pretty_json:
|
||||
overrides.setdefault("io", {})["write_pretty_json"] = True
|
||||
|
||||
# 运行窗口
|
||||
if args.idle_start:
|
||||
overrides.setdefault("run", {}).setdefault("idle_window", {})["start"] = args.idle_start
|
||||
if args.idle_end:
|
||||
overrides.setdefault("run", {}).setdefault("idle_window", {})["end"] = args.idle_end
|
||||
if args.allow_empty_advance:
|
||||
overrides.setdefault("run", {})["allow_empty_result_advance"] = True
|
||||
|
||||
# 任务
|
||||
if args.tasks:
|
||||
tasks = [t.strip().upper() for t in args.tasks.split(",") if t.strip()]
|
||||
|
||||
@@ -65,18 +65,18 @@ DEFAULTS = {
|
||||
"allow_empty_result_advance": True,
|
||||
},
|
||||
"io": {
|
||||
"export_root": r"D:\LLZQ\DB\export",
|
||||
"log_root": r"D:\LLZQ\DB\logs",
|
||||
"export_root": r"C:\dev\LLTQ\export\JSON",
|
||||
"log_root": r"C:\dev\LLTQ\export\LOG",
|
||||
"manifest_name": "manifest.json",
|
||||
"ingest_report_name": "ingest_report.json",
|
||||
"write_pretty_json": False,
|
||||
"write_pretty_json": True,
|
||||
"max_file_bytes": 50 * 1024 * 1024,
|
||||
},
|
||||
"pipeline": {
|
||||
# 运行流程:FETCH_ONLY(仅在线抓取落盘)、INGEST_ONLY(本地清洗入库)、FULL(抓取 + 清洗入库)
|
||||
"flow": "FULL",
|
||||
# 在线抓取 JSON 输出根目录(按任务、run_id 与时间自动创建子目录)
|
||||
"fetch_root": r"D:\LLZQ\DB\json_fetch",
|
||||
"fetch_root": r"C:\dev\LLTQ\export\JSON",
|
||||
# 本地清洗入库时的 JSON 输入目录(为空则默认使用本次抓取目录)
|
||||
"ingest_source_dir": "",
|
||||
},
|
||||
@@ -95,6 +95,12 @@ DEFAULTS = {
|
||||
"redact_keys": ["token", "password", "Authorization"],
|
||||
"echo_token_in_logs": False,
|
||||
},
|
||||
"ods": {
|
||||
# ODS 离线重建/回放相关(仅开发/运维使用)
|
||||
"json_doc_dir": r"C:\dev\LLTQ\export\test-json-doc",
|
||||
"include_files": "",
|
||||
"drop_schema_first": True,
|
||||
},
|
||||
}
|
||||
|
||||
# 任务代码常量
|
||||
|
||||
@@ -22,24 +22,39 @@ ENV_MAP = {
|
||||
"FICOO_TOKEN": ("api.token",),
|
||||
"API_TIMEOUT": ("api.timeout_sec",),
|
||||
"API_PAGE_SIZE": ("api.page_size",),
|
||||
"API_RETRY_MAX": ("api.retries.max_attempts",),
|
||||
"API_RETRY_BACKOFF": ("api.retries.backoff_sec",),
|
||||
"API_PARAMS": ("api.params",),
|
||||
"EXPORT_ROOT": ("io.export_root",),
|
||||
"LOG_ROOT": ("io.log_root",),
|
||||
"MANIFEST_NAME": ("io.manifest_name",),
|
||||
"INGEST_REPORT_NAME": ("io.ingest_report_name",),
|
||||
"WRITE_PRETTY_JSON": ("io.write_pretty_json",),
|
||||
"RUN_TASKS": ("run.tasks",),
|
||||
"OVERLAP_SECONDS": ("run.overlap_seconds",),
|
||||
"WINDOW_BUSY_MIN": ("run.window_minutes.default_busy",),
|
||||
"WINDOW_IDLE_MIN": ("run.window_minutes.default_idle",),
|
||||
"IDLE_START": ("run.idle_window.start",),
|
||||
"IDLE_END": ("run.idle_window.end",),
|
||||
"IDLE_WINDOW_START": ("run.idle_window.start",),
|
||||
"IDLE_WINDOW_END": ("run.idle_window.end",),
|
||||
"ALLOW_EMPTY_RESULT_ADVANCE": ("run.allow_empty_result_advance",),
|
||||
"ALLOW_EMPTY_ADVANCE": ("run.allow_empty_result_advance",),
|
||||
"PIPELINE_FLOW": ("pipeline.flow",),
|
||||
"JSON_FETCH_ROOT": ("pipeline.fetch_root",),
|
||||
"JSON_SOURCE_DIR": ("pipeline.ingest_source_dir",),
|
||||
"FETCH_ROOT": ("pipeline.fetch_root",),
|
||||
"INGEST_SOURCE_DIR": ("pipeline.ingest_source_dir",),
|
||||
}
|
||||
|
||||
|
||||
def _deep_set(d, dotted_keys, value):
|
||||
cur = d
|
||||
for k in dotted_keys[:-1]:
|
||||
cur = cur.setdefault(k, {})
|
||||
cur[dotted_keys[-1]] = value
|
||||
|
||||
|
||||
def _coerce_env(v: str):
|
||||
if v is None:
|
||||
return None
|
||||
@@ -58,6 +73,7 @@ def _coerce_env(v: str):
|
||||
return s
|
||||
return s
|
||||
|
||||
|
||||
def _strip_inline_comment(value: str) -> str:
|
||||
"""去掉未被引号包裹的内联注释"""
|
||||
result = []
|
||||
@@ -121,20 +137,24 @@ def _parse_dotenv_line(line: str) -> tuple[str, str] | None:
|
||||
value = _unquote_value(value)
|
||||
return key, value
|
||||
|
||||
|
||||
def _load_dotenv_values() -> dict:
|
||||
"""从项目根目录的 .env 文件读取键值"""
|
||||
"""从项目根目录读取 .env 文件键值"""
|
||||
if os.environ.get("ETL_SKIP_DOTENV") in ("1", "true", "TRUE", "True"):
|
||||
return {}
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
dotenv_path = root / ".env"
|
||||
if not dotenv_path.exists():
|
||||
return {}
|
||||
values: dict[str, str] = {}
|
||||
for line in dotenv_path.read_text(encoding="utf-8").splitlines():
|
||||
for line in dotenv_path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
parsed = _parse_dotenv_line(line)
|
||||
if parsed:
|
||||
key, value = parsed
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
def _apply_env_values(cfg: dict, source: dict):
|
||||
for env_key, dotted in ENV_MAP.items():
|
||||
val = source.get(env_key)
|
||||
@@ -146,6 +166,7 @@ def _apply_env_values(cfg: dict, source: dict):
|
||||
v2 = [item.strip() for item in v2.split(",") if item.strip()]
|
||||
_deep_set(cfg, path.split("."), v2)
|
||||
|
||||
|
||||
def load_env_overrides(defaults: dict) -> dict:
|
||||
cfg = deepcopy(defaults)
|
||||
# 先读取 .env,再读取真实环境变量,确保 CLI 仍然最高优先级
|
||||
|
||||
@@ -49,6 +49,13 @@ class AppConfig:
|
||||
f"@{cfg['db']['host']}:{cfg['db']['port']}/{cfg['db']['name']}"
|
||||
)
|
||||
|
||||
# connect_timeout 限定 1-20 秒
|
||||
try:
|
||||
timeout_sec = int(cfg["db"].get("connect_timeout_sec") or 5)
|
||||
except Exception:
|
||||
raise SystemExit("db.connect_timeout_sec 必须为整数")
|
||||
cfg["db"]["connect_timeout_sec"] = max(1, min(timeout_sec, 20))
|
||||
|
||||
# 会话参数
|
||||
cfg["db"].setdefault("session", {})
|
||||
sess = cfg["db"]["session"]
|
||||
|
||||
@@ -1,49 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接管理"""
|
||||
"""Database connection manager with capped connect_timeout."""
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
|
||||
class DatabaseConnection:
|
||||
"""数据库连接管理器"""
|
||||
"""Wrap psycopg2 connection with session parameters and timeout guard."""
|
||||
|
||||
def __init__(self, dsn: str, session: dict = None, connect_timeout: int = None):
|
||||
self.conn = psycopg2.connect(dsn, connect_timeout=(connect_timeout or 5))
|
||||
timeout_val = connect_timeout if connect_timeout is not None else 5
|
||||
# PRD: database connect_timeout must not exceed 20 seconds.
|
||||
timeout_val = max(1, min(int(timeout_val), 20))
|
||||
|
||||
self.conn = psycopg2.connect(dsn, connect_timeout=timeout_val)
|
||||
self.conn.autocommit = False
|
||||
|
||||
# 设置会话参数
|
||||
# Session parameters (timezone, statement timeout, etc.)
|
||||
if session:
|
||||
with self.conn.cursor() as c:
|
||||
if session.get("timezone"):
|
||||
c.execute("SET TIME ZONE %s", (session["timezone"],))
|
||||
if session.get("statement_timeout_ms") is not None:
|
||||
c.execute("SET statement_timeout = %s", (int(session["statement_timeout_ms"]),))
|
||||
c.execute(
|
||||
"SET statement_timeout = %s",
|
||||
(int(session["statement_timeout_ms"]),),
|
||||
)
|
||||
if session.get("lock_timeout_ms") is not None:
|
||||
c.execute("SET lock_timeout = %s", (int(session["lock_timeout_ms"]),))
|
||||
c.execute(
|
||||
"SET lock_timeout = %s", (int(session["lock_timeout_ms"]),)
|
||||
)
|
||||
if session.get("idle_in_tx_timeout_ms") is not None:
|
||||
c.execute("SET idle_in_transaction_session_timeout = %s",
|
||||
(int(session["idle_in_tx_timeout_ms"]),))
|
||||
c.execute(
|
||||
"SET idle_in_transaction_session_timeout = %s",
|
||||
(int(session["idle_in_tx_timeout_ms"]),),
|
||||
)
|
||||
|
||||
def query(self, sql: str, args=None):
|
||||
"""执行查询并返回结果"""
|
||||
"""Execute a query and fetch all rows."""
|
||||
with self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as c:
|
||||
c.execute(sql, args)
|
||||
return c.fetchall()
|
||||
|
||||
def execute(self, sql: str, args=None):
|
||||
"""执行SQL语句"""
|
||||
"""Execute a SQL statement without returning rows."""
|
||||
with self.conn.cursor() as c:
|
||||
c.execute(sql, args)
|
||||
|
||||
def commit(self):
|
||||
"""提交事务"""
|
||||
"""Commit current transaction."""
|
||||
self.conn.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""回滚事务"""
|
||||
"""Rollback current transaction."""
|
||||
self.conn.rollback()
|
||||
|
||||
def close(self):
|
||||
"""关闭连接"""
|
||||
"""Safely close the connection."""
|
||||
try:
|
||||
self.conn.close()
|
||||
except Exception:
|
||||
|
||||
@@ -24,8 +24,8 @@ CREATE TABLE IF NOT EXISTS dim_site_Ex (
|
||||
site_id BIGINT,
|
||||
avatar TEXT,
|
||||
address TEXT,
|
||||
longitude NUMERIC(18,2),
|
||||
latitude NUMERIC(18,2),
|
||||
longitude NUMERIC(9,6),
|
||||
latitude NUMERIC(9,6),
|
||||
tenant_site_region_id BIGINT,
|
||||
auto_light INTEGER,
|
||||
light_status INTEGER,
|
||||
@@ -85,7 +85,7 @@ COMMENT ON COLUMN dim_table.table_name IS '台桌名称/编号,如 A17、888
|
||||
COMMENT ON COLUMN dim_table.site_table_area_id IS '门店区 ID,用于区分 A区/B区/补时区等。 | 来源: siteTableAreaId | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_table.site_table_area_name IS '区域名称,如 “A区”“补时长”。 | 来源: siteTableAreaName';
|
||||
COMMENT ON COLUMN dim_table.tenant_table_area_id IS '租户级区域 ID。 | 来源: tenantTableAreaId | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_table.table_price IS '???????? table_fee_transactions ??????? id ?? table_fee_transactions.site_table_id';
|
||||
COMMENT ON COLUMN dim_table.table_price IS '台桌基础单价,从table_fee_transactions取值。方法:对应本表id,table_fee_transactions表的site_table_id。';
|
||||
|
||||
-- dim_table_Ex
|
||||
CREATE TABLE IF NOT EXISTS dim_table_Ex (
|
||||
@@ -151,8 +151,8 @@ CREATE TABLE IF NOT EXISTS dim_assistant_Ex (
|
||||
avatar TEXT,
|
||||
introduce TEXT,
|
||||
video_introduction_url TEXT,
|
||||
height DOUBLE PRECISION,
|
||||
weight DOUBLE PRECISION,
|
||||
height NUMERIC(5,2),
|
||||
weight NUMERIC(5,2),
|
||||
shop_name TEXT,
|
||||
group_id BIGINT,
|
||||
group_name TEXT,
|
||||
@@ -244,7 +244,7 @@ CREATE TABLE IF NOT EXISTS dim_member (
|
||||
register_site_id BIGINT,
|
||||
mobile TEXT,
|
||||
nickname TEXT,
|
||||
member_card_grade_code INTEGER,
|
||||
member_card_grade_code BIGINT,
|
||||
member_card_grade_name TEXT,
|
||||
create_time TIMESTAMPTZ,
|
||||
update_time TIMESTAMPTZ,
|
||||
@@ -307,7 +307,7 @@ COMMENT ON COLUMN dim_member_card_account.register_site_id IS '开卡门店 ID
|
||||
COMMENT ON COLUMN dim_member_card_account.tenant_member_id IS '对应会员档案中的 member_id(本租户内)。0 表示未绑定会员。 | 来源: tenant_member_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_member_card_account.system_member_id IS '全局会员 ID,用于跨租户统一会员身份。0 表示未绑定会员。 | 来源: system_member_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_member_card_account.card_type_id IS '卡种 ID,指向卡种配置表。与下面的 grade_code 共同定义卡类别。 | 来源: card_type_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_member_card_account.member_card_grade_code IS '?????/?????????????2790683528022853=????2790683528022856=??????2790683528022855=????2790683528022858=????2790683528022857=??';
|
||||
COMMENT ON COLUMN dim_member_card_account.member_card_grade_code IS '卡等级/卡类代码,区别不同类别卡。2790683528022853=储值卡,2790683528022856=活动抵用券,2790683528022855=台费卡,2790683528022858=酒水卡,2790683528022857=月卡';
|
||||
COMMENT ON COLUMN dim_member_card_account.member_card_grade_code_name IS '卡等级中文名称,与 member_card_grade_code 一一对应。 | 来源: member_card_grade_code_name';
|
||||
COMMENT ON COLUMN dim_member_card_account.member_card_type_name IS '卡类型名称,通常与 grade_code_name 相同,纯展示字段。 | 来源: member_card_type_name';
|
||||
COMMENT ON COLUMN dim_member_card_account.member_name IS '持卡会员姓名快照,部分为空表示未绑定。 | 来源: member_name';
|
||||
@@ -375,58 +375,58 @@ CREATE TABLE IF NOT EXISTS dim_member_card_account_Ex (
|
||||
cxAssisnatLevel TEXT,
|
||||
PRIMARY KEY (member_card_id)
|
||||
);
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.member_card_id IS 'id | 来源: bigint | 角色: 会员卡账户主键,唯一标识一张具体卡。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.site_name IS '门店名称展示字段(全部相同)。 | 来源: site_name | 角色: 门店名称展示字段(全部相同)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tenant_name IS 'tenant_name | 来源: string | 角色: 租户名称(当前导出为空)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tenantAvatar IS 'tenantAvatar | 来源: string | 角色: 租户头像 URL(当前导出为空)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.effect_site_id IS 'effect_site_id | 来源: bigint | 角色: 卡片限定生效门店 ID。0 表示不限门店,配合 able_cross_site=1 表示全店通用。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.able_cross_site IS 'able_cross_site | 来源: int | 角色: 是否允许跨门店使用该卡:1=允许跨店;0=仅限开卡门店。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.card_physics_type IS 'card_physics_type | 来源: int | 角色: 物理卡类型:1=实体/标准卡;其他值未出现,含义未知。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.card_no IS 'card_no | 来源: string | 角色: 物理卡号或条码(当前全部为空)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.bind_password IS 'bind_password | 来源: string | 角色: 卡绑定密码(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.use_scene IS 'use_scene | 来源: string | 角色: 使用场景说明(当前为空)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.denomination IS 'denomination | 来源: decimal | 角色: 面额或初始储值额度(当前均为0.0,未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.create_time IS 'create_time | 来源: datetime | 角色: 卡片创建时间。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.disable_start_time IS 'disable_start_time | 来源: datetime | 角色: 卡片禁用开始时间,当前为默认值表示未禁用。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.disable_end_time IS 'disable_end_time | 来源: datetime | 角色: 卡片禁用结束时间,当前为默认值表示未禁用。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.is_allow_give IS 'is_allow_give | 来源: int | 角色: 是否允许转赠给他人:0=不允许;1=允许。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.is_allow_order_deduct IS 'is_allow_order_deduct | 来源: int | 角色: 是否允许在订单层面统一扣款:0=不允许;1=允许。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.sort IS 'sort | 来源: int | 角色: 前端排序序号。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_discount IS 'table_discount | 来源: float | 角色: 台费折扣率(折扣百分比,10.0=不打折,9.0=九折等)。当前全部 10.0。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_discount IS 'goods_discount | 来源: float | 角色: 商品折扣率,当前为 10.0 表示无折扣。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_discount IS 'assistant_discount | 来源: float | 角色: 助教服务折扣率,当前为 10.0。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_reward_discount IS 'assistant_reward_discount | 来源: float | 角色: 助教奖励折扣率,当前为 10.0(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_service_discount IS 'table_service_discount | 来源: float | 角色: 台费服务类折扣率,当前为 10.0。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_service_discount IS 'goods_service_discount | 来源: float | 角色: 商品服务折扣率,当前为 10.0。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_service_discount IS 'assistant_service_discount | 来源: float | 角色: 助教服务类折扣率,当前为 10.0。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.coupon_discount IS 'coupon_discount | 来源: float | 角色: 使用券的折扣比例(全部 10.0,未使用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_discount_sub_switch IS 'table_discount_sub_switch | 来源: int | 角色: 台费折扣叠加开关:1=叠加其他折扣;2=不叠加,仅用卡折扣。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_discount_sub_switch IS 'goods_discount_sub_switch | 来源: int | 角色: 商品折扣叠加开关,意义同上。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_discount_sub_switch IS 'assistant_discount_sub_switch | 来源: int | 角色: 助教折扣叠加开关,意义同上。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_reward_discount_sub_switch IS 'assistant_reward_discount_sub_switch | 来源: int | 角色: 助教奖励折扣叠加开关(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_discount_range_type IS 'goods_discount_range_type | 来源: int | 角色: 商品折扣范围类型,未在文档说明具体含义。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_deduct_radio IS 'table_deduct_radio | 来源: float | 角色: 台费抵扣比例(百分比)。100.0 表示允许全额抵扣;0=不允许。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_deduct_radio IS 'goods_deduct_radio | 来源: float | 角色: 商品抵扣比例,意义同上。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_deduct_radio IS 'assistant_deduct_radio | 来源: float | 角色: 助教抵扣比例,意义同上。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_service_deduct_radio IS 'table_service_deduct_radio | 来源: float | 角色: 台费服务金抵扣比例。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_service_deduct_radio IS 'goods_service_deduct_radio | 来源: float | 角色: 商品服务金抵扣比例。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_service_deduct_radio IS 'assistant_service_deduct_radio | 来源: float | 角色: 助教服务金抵扣比例。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_reward_deduct_radio IS 'assistant_reward_deduct_radio | 来源: float | 角色: 助教奖励金抵扣比例(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.coupon_deduct_radio IS 'coupon_deduct_radio | 来源: float | 角色: 券抵扣比例(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.cardSettleDeduct IS 'cardSettleDeduct | 来源: float | 角色: 结算时统一扣卡金额配置(当前为 0.0,未使用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tableCardDeduct IS 'tableCardDeduct | 来源: float | 角色: 台费扣卡金额配置,当前 0.0。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tableServiceCardDeduct IS 'tableServiceCardDeduct | 来源: float | 角色: 台费服务金扣卡金额配置。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goodsCarDeduct IS 'goodsCarDeduct | 来源: float | 角色: 商品扣卡金额配置。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goodsServiceCardDeduct IS 'goodsServiceCardDeduct | 来源: float | 角色: 商品服务金扣卡金额配置。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistantCardDeduct IS 'assistantCardDeduct | 来源: float | 角色: 助教扣卡金额配置。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistantServiceCardDeduct IS 'assistantServiceCardDeduct | 来源: float | 角色: 助教服务金扣卡金额配置。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistantRewardCardDeduct IS 'assistantRewardCardDeduct | 来源: float | 角色: 助教奖励金扣卡金额配置(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.couponCardDeduct IS 'couponCardDeduct | 来源: float | 角色: 使用券扣卡金额配置。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.deliveryFeeDeduct IS 'deliveryFeeDeduct | 来源: float | 角色: 配送费扣卡金额配置(未启用)。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tableAreaId IS 'tableAreaId | 来源: list | 角色: 可用台区 ID 列表,空表示不限台区。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goodsCategoryId IS 'goodsCategoryId | 来源: list | 角色: 可用商品分类 ID 列表,空表示不限制商品类别。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.pdAssisnatLevel IS 'pdAssisnatLevel | 来源: list | 角色: 允许的陪打助教等级列表,空表示不限。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.cxAssisnatLevel IS 'cxAssisnatLevel | 来源: list | 角色: 允许的促销助教等级列表,空表示不限。';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.member_card_id IS '会员卡账户主键,唯一标识一张具体卡。 | 来源: id | 角色: 主键';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.site_name IS '门店名称展示字段(全部相同)。 | 来源: site_name';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tenant_name IS '租户名称(当前导出为空)。 | 来源: tenantName';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tenantAvatar IS '租户头像 URL(当前导出为空)。 | 来源: tenantAvatar';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.effect_site_id IS '卡片限定生效门店 ID。0 表示不限门店,配合 able_cross_site=1 表示全店通用。 | 来源: effect_site_id';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.able_cross_site IS '是否允许跨门店使用该卡:1=允许跨店;0=仅限开卡门店。 | 来源: able_cross_site';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.card_physics_type IS '物理卡类型:1=实体/标准卡;其他值未出现,含义未知。 | 来源: card_physics_type';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.card_no IS '物理卡号或条码(当前全部为空)。 | 来源: card_no';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.bind_password IS '卡绑定密码(未启用)。 | 来源: bind_password';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.use_scene IS '使用场景说明(当前为空)。 | 来源: use_scene';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.denomination IS '面额或初始储值额度(当前均为 0.0,未启用)。 | 来源: denomination';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.create_time IS '卡片创建时间。 | 来源: create_time';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.disable_start_time IS '卡片禁用开始时间,当前为默认值表示未禁用。 | 来源: disable_start_time';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.disable_end_time IS '卡片禁用结束时间,当前为默认值表示未禁用。 | 来源: disable_end_time';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.is_allow_give IS '是否允许转赠给他人:0=不允许;1=允许。 | 来源: is_allow_give';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.is_allow_order_deduct IS '是否允许在订单层面统一扣款:0=不允许;1=允许。 | 来源: is_allow_order_deduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.sort IS '前端排序序号。 | 来源: sort';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_discount IS '台费折扣率(折扣百分比,10.0=不打折,9.0=九折等)。当前全部 10.0。 | 来源: table_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_discount IS '商品折扣率,当前为 10.0 表示无折扣。 | 来源: goods_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_discount IS '助教服务折扣率,当前为 10.0。 | 来源: assistant_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_reward_discount IS '助教奖励折扣率,当前为 10.0(未启用)。 | 来源: assistant_reward_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_service_discount IS '台费服务类折扣率,当前为 10.0。 | 来源: table_service_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_service_discount IS '商品服务折扣率,当前为 10.0。 | 来源: goods_service_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_service_discount IS '助教服务类折扣率,当前为 10.0。 | 来源: assistant_service_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.coupon_discount IS '使用券的折扣比例(全部 10.0,未使用)。 | 来源: coupon_discount';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_discount_sub_switch IS '台费折扣叠加开关:1=叠加其他折扣;2=不叠加,仅用卡折扣。 | 来源: table_discount_sub_switch';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_discount_sub_switch IS '商品折扣叠加开关,意义同上。 | 来源: goods_discount_sub_switch';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_discount_sub_switch IS '助教折扣叠加开关,意义同上。 | 来源: assistant_discount_sub_switch';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_reward_discount_sub_switch IS '助教奖励折扣叠加开关(未启用)。 | 来源: assistant_reward_discount_sub_switch';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_discount_range_type IS '商品折扣范围类型,未在文档说明具体含义。 | 来源: goods_discount_range_type';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_deduct_radio IS '台费抵扣比例(百分比)。100.0 表示允许全额抵扣;0=不允许。 | 来源: table_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_deduct_radio IS '商品抵扣比例,意义同上。 | 来源: goods_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_deduct_radio IS '助教抵扣比例,意义同上。 | 来源: assistant_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.table_service_deduct_radio IS '台费服务金抵扣比例。 | 来源: table_service_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goods_service_deduct_radio IS '商品服务金抵扣比例。 | 来源: goods_service_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_service_deduct_radio IS '助教服务金抵扣比例。 | 来源: assistant_service_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistant_reward_deduct_radio IS '助教奖励金抵扣比例(未启用)。 | 来源: assistant_reward_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.coupon_deduct_radio IS '券抵扣比例(未启用)。 | 来源: coupon_deduct_radio';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.cardSettleDeduct IS '结算时统一扣卡金额配置(当前为 0.0,未使用)。 | 来源: cardSettleDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tableCardDeduct IS '台费扣卡金额配置,当前 0.0。 | 来源: tableCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tableServiceCardDeduct IS '台费服务金扣卡金额配置。 | 来源: tableServiceCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goodsCarDeduct IS '商品扣卡金额配置。 | 来源: goodsCarDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goodsServiceCardDeduct IS '商品服务金扣卡金额配置。 | 来源: goodsServiceCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistantCardDeduct IS '助教扣卡金额配置。 | 来源: assistantCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistantServiceCardDeduct IS '助教服务金扣卡金额配置。 | 来源: assistantServiceCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.assistantRewardCardDeduct IS '助教奖励金扣卡金额配置(未启用)。 | 来源: assistantRewardCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.couponCardDeduct IS '使用券扣卡金额配置。 | 来源: couponCardDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.deliveryFeeDeduct IS '配送费扣卡金额配置(未启用)。 | 来源: deliveryFeeDeduct';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.tableAreaId IS '可用台区 ID 列表,空表示不限台区。 | 来源: tableAreaId';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.goodsCategoryId IS '可用商品分类 ID 列表,空表示不限制商品类别。 | 来源: goodsCategoryId';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.pdAssisnatLevel IS '允许的陪打助教等级列表,空表示不限。 | 来源: pdAssisnatLevel';
|
||||
COMMENT ON COLUMN dim_member_card_account_Ex.cxAssisnatLevel IS '允许的促销助教等级列表,空表示不限。 | 来源: cxAssisnatLevel';
|
||||
|
||||
-- dim_tenant_goods
|
||||
CREATE TABLE IF NOT EXISTS dim_tenant_goods (
|
||||
@@ -523,7 +523,7 @@ CREATE TABLE IF NOT EXISTS dim_store_goods (
|
||||
goods_state INTEGER,
|
||||
enable_status INTEGER,
|
||||
send_state INTEGER,
|
||||
is_deleted INTEGER,
|
||||
is_delete INTEGER,
|
||||
PRIMARY KEY (site_goods_id)
|
||||
);
|
||||
COMMENT ON COLUMN dim_store_goods.site_goods_id IS '门店级商品 ID,本表主键;其它业务表中的 site_goods_id 与此对应,用于库存、销售等关联。 | 来源: id | 角色: 主键';
|
||||
@@ -545,7 +545,7 @@ COMMENT ON COLUMN dim_store_goods.avg_monthly_sales IS '平均月销量(件/
|
||||
COMMENT ON COLUMN dim_store_goods.goods_state IS '商品基础状态枚举:1=正常状态(主流值),2=特殊状态(如新建未完全启用或停售但未彻底下架,通常伴随 stock=0、days_on_shelf=0)。 | 来源: goods_state';
|
||||
COMMENT ON COLUMN dim_store_goods.enable_status IS '档案启用状态:1=启用;2=停用(推测,样本中未出现);控制商品档案是否参与业务处理。 | 来源: enable_status';
|
||||
COMMENT ON COLUMN dim_store_goods.send_state IS '销售端可售状态:1=可销售/可下单;其他值可能代表停售或仅内部使用(当前样本全部为 1)。 | 来源: send_state';
|
||||
COMMENT ON COLUMN dim_store_goods.is_deleted IS '逻辑删除标志:0=未删除(有效档案);1=已删除(逻辑删除,不再参与业务但保留历史引用)。 | 来源: is_delete';
|
||||
COMMENT ON COLUMN dim_store_goods.is_delete IS '逻辑删除标志:0=未删除(有效档案);1=已删除(逻辑删除,不再参与业务但保留历史引用)。 | 来源: is_delete';
|
||||
|
||||
-- dim_store_goods_Ex
|
||||
CREATE TABLE IF NOT EXISTS dim_store_goods_Ex (
|
||||
@@ -633,35 +633,6 @@ COMMENT ON COLUMN dim_goods_category.open_salesman IS '营业员开关控制。
|
||||
COMMENT ON COLUMN dim_goods_category.sort_order IS '分类排序序号。来自 sort 字段,用于前端展示顺序控制,数值越小越靠前。当前大部分分类为 0,仅少数为 1,说明排序配置较为粗略。对指标统计无实质影响。 | 来源: sort';
|
||||
COMMENT ON COLUMN dim_goods_category.is_warehousing IS '是否参与库存管理。枚举:1 表示参与库存管理,0 表示不参与(如服务类商品、手工费用)。当前文件中所有分类取值为 1,表示这一份分类树只包含“走库存”的商品分类。可在库存报表中用作过滤条件。 | 来源: is_warehousing';
|
||||
|
||||
-- dim_goods_category_Ex
|
||||
CREATE TABLE IF NOT EXISTS dim_goods_category_Ex (
|
||||
category_id BIGINT,
|
||||
tenant_id BIGINT,
|
||||
category_name VARCHAR(50),
|
||||
alias_name VARCHAR(50),
|
||||
parent_category_id BIGINT,
|
||||
business_name VARCHAR(50),
|
||||
tenant_goods_business_id BIGINT,
|
||||
category_level INTEGER,
|
||||
is_leaf INTEGER,
|
||||
open_salesman INTEGER,
|
||||
sort_order INTEGER,
|
||||
is_warehousing INTEGER,
|
||||
PRIMARY KEY (category_id)
|
||||
);
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.category_id IS '分类节点主键。来自分类树节点的 id,在整个商品分类维度内唯一。用于在事实表中作为商品分类外键引用。 | 来源: id | 角色: 主键';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.tenant_id IS '租户 ID(商户/品牌 ID)。当前所有节点取值相同,表示同一个租户下的分类树。事实表可通过该字段与租户维度或门店维度间接关联。 | 来源: tenant_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.category_name IS '分类名称。一级大类示例:槟榔、器材、酒水、水果、零食、雪糕、香烟、其他、小吃。二级子类示例:槟榔、皮头、球杆、其他、饮料、酒水、茶水、咖啡、加料、洋酒、果盘、面、小吃等。用于前台展示和报表按细分类统计。 | 来源: category_name';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.alias_name IS '分类别名。当前样例数据全部为空字符串,预留给业务方做简称或别名展示。对现阶段经营分析无影响。 | 来源: alias_name';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.parent_category_id IS '父级分类 ID。根节点取值为 0,表示没有父分类;子节点取值为父分类的 id。与 category_id 共同形成树形层级关系。 | 来源: pid | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.business_name IS '业务大类名称。将多个细分类归入同一业务线。观测值与一级大类相同:槟榔、器材、酒水、水果、零食、雪糕、香烟、其他、小吃。子类的 business_name 继承所属根节点的大类名称。用于按业务线汇总库存和销售。 | 来源: business_name';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.tenant_goods_business_id IS '业务大类 ID。每个 business_name 对应唯一一个 tenant_goods_business_id,根节点和其下所有子节点共享同一取值。例如“酒水”大类及其子类饮料、茶水、咖啡、加料、洋酒拥有相同的业务 ID。可作为外键连接“业务线维度表”。 | 来源: tenant_goods_business_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.category_level IS '分类层级:1 表示一级大类(pid = 0),2 表示二级子类(pid ≠ 0)。方便在报表中区分大类与子类进行分组和展示层级控制。 | 来源: 由 pid 推导';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.is_leaf IS '是否叶子节点:1 表示叶子分类(categoryBoxes 为空列表),0 表示非叶子分类(存在子分类)。当前样例数据中,一级大类是非叶子节点,二级分类是叶子节点。用于树状导航或限制只能在叶子分类建商品。 | 来源: 由 categoryBoxes 推导';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.open_salesman IS '营业员开关控制。枚举含义根据业务系统定义,一般设计为:1 表示启用营业员/导购相关功能,2 表示关闭或不启用。当前样例所有分类取值为 2,说明这一套分类在库存模块中统一未启用营业员逻辑。对目前的经营分析影响较小。 | 来源: open_salesman';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.sort_order IS '分类排序序号。来自 sort 字段,用于前端展示顺序控制,数值越小越靠前。当前大部分分类为 0,仅少数为 1,说明排序配置较为粗略。对指标统计无实质影响。 | 来源: sort';
|
||||
COMMENT ON COLUMN dim_goods_category_Ex.is_warehousing IS '是否参与库存管理。枚举:1 表示参与库存管理,0 表示不参与(如服务类商品、手工费用)。当前文件中所有分类取值为 1,表示这一份分类树只包含“走库存”的商品分类。可在库存报表中用作过滤条件。 | 来源: is_warehousing';
|
||||
|
||||
-- dim_groupbuy_package
|
||||
CREATE TABLE IF NOT EXISTS dim_groupbuy_package (
|
||||
groupbuy_package_id BIGINT,
|
||||
@@ -676,7 +647,7 @@ CREATE TABLE IF NOT EXISTS dim_groupbuy_package (
|
||||
end_time TIMESTAMPTZ,
|
||||
table_area_name VARCHAR(100),
|
||||
is_enabled INTEGER,
|
||||
is_deleted INTEGER,
|
||||
is_delete INTEGER,
|
||||
create_time TIMESTAMPTZ,
|
||||
tenant_table_area_id_list VARCHAR(512),
|
||||
card_type_ids VARCHAR(255),
|
||||
@@ -694,7 +665,7 @@ COMMENT ON COLUMN dim_groupbuy_package.start_time IS '套餐整体生效开始
|
||||
COMMENT ON COLUMN dim_groupbuy_package.end_time IS '套餐整体生效结束时间。在该时间点之后不可使用。极大日期(如 9999-12-31 23:59:59)可视为长期有效。 | 来源: end_time';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.table_area_name IS '套餐适用的门店台区名称,例如“A区中八”“B区中八”“斯诺克”“包厢”“KTV”等。主要用于展示和过滤,配合区域 ID 列实现人类可读的说明。 | 来源: table_area_name';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.is_enabled IS '启用状态枚举。1 表示启用或上架;2 表示停用或下架。侧重表示“配置是否上架”,与 effective_status 区分。 | 来源: is_enabled';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.is_deleted IS '逻辑删除标志。0 表示正常;1 表示逻辑删除(数据仍保留但不再使用)。当前样本全部为 0。 | 来源: is_delete';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.is_delete IS '逻辑删除标志。0 表示正常;1 表示逻辑删除(数据仍保留但不再使用)。当前样本全部为 0。 | 来源: is_delete';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.create_time IS '套餐配置在系统中的创建时间,用于审计和版本追踪。 | 来源: create_time';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.tenant_table_area_id_list IS '租户级台区分组 ID 列表。当前每条记录为一个大整数(例如 2791960001957765)字符串,表示“台区分组”主键。系统通过此分组再关联到具体多个台区。 | 来源: tenant_table_area_id_list | 角色: 外键(指向台区分组维,后续可建)';
|
||||
COMMENT ON COLUMN dim_groupbuy_package.card_type_ids IS '允许使用本套餐的会员卡类型 ID 列表。当前样本统一为字符串“0”,表示未限制卡种,任意顾客或任意会员卡都可使用。若未来启用,将以分隔的 ID 列表形式记录限定卡种。 | 来源: card_type_ids | 角色: 外键(潜在指向卡种维)';
|
||||
@@ -1082,7 +1053,7 @@ CREATE TABLE IF NOT EXISTS dwd_store_goods_sale_Ex (
|
||||
salesman_user_id BIGINT,
|
||||
salesman_name TEXT,
|
||||
salesman_role_id BIGINT,
|
||||
sales_man_org_id BIGINT,
|
||||
salesman_org_id BIGINT,
|
||||
discount_money NUMERIC(18,2),
|
||||
returns_number INTEGER,
|
||||
coupon_deduct_money NUMERIC(18,2),
|
||||
@@ -1111,7 +1082,7 @@ COMMENT ON COLUMN dwd_store_goods_sale_Ex.open_salesman_flag IS '是否启用营
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.salesman_user_id IS '营业员用户 ID(系统账号 ID);当前样本全部为 0,说明门店未启用营业员业绩统计。 | 来源: salesman_user_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.salesman_name IS '营业员姓名;当前样本全部为空字符串。 | 来源: salesman_name';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.salesman_role_id IS '营业员角色 ID(例如某角色代码对应“销售员”角色);当前样本全部为 0。 | 来源: salesman_role_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.sales_man_org_id IS '营业员所属组织/部门 ID;当前样本全部为 0,未启用按组织分组统计。 | 来源: sales_man_org_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.salesman_org_id IS '营业员所属组织/部门 ID;当前样本全部为 0,未启用按组织分组统计。 | 来源: sales_man_org_id | 角色: 外键';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.discount_money IS '本行商品的直接价格优惠金额(打折让利部分);在简单场景下满足:ledger_amount − discount_money ≈ real_goods_money(不含积分、券抵扣)。 | 来源: discount_money';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.returns_number IS '退货数量;当前样本全部为 0,如发生退货则记录退回的件数。 | 来源: returns_number';
|
||||
COMMENT ON COLUMN dwd_store_goods_sale_Ex.coupon_deduct_money IS '优惠券/团购券直接抵扣到本条商品明细上的金额;当前样本为 0,说明券更多在订单级处理。 | 来源: coupon_deduct_money';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- Data warehouse schema for the entertainment chain (ODS -> DWD -> DWS)
|
||||
-- ASCII only to keep cross-platform friendly.
|
||||
|
||||
-- ---------- Schemas ----------
|
||||
DROP SCHEMA IF EXISTS billiards_ods CASCADE;
|
||||
CREATE SCHEMA IF NOT EXISTS billiards_ods;
|
||||
CREATE SCHEMA IF NOT EXISTS billiards_dwd;
|
||||
CREATE SCHEMA IF NOT EXISTS billiards_dws;
|
||||
@@ -13,6 +13,9 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_member_profile (
|
||||
tenant_id BIGINT,
|
||||
site_id BIGINT NOT NULL,
|
||||
member_id BIGINT NOT NULL,
|
||||
system_member_id BIGINT,
|
||||
register_site_id BIGINT,
|
||||
site_name TEXT,
|
||||
member_name TEXT,
|
||||
nickname TEXT,
|
||||
mobile TEXT,
|
||||
@@ -21,13 +24,17 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_member_profile (
|
||||
register_time TIMESTAMPTZ,
|
||||
member_type_id BIGINT,
|
||||
member_type_name TEXT,
|
||||
member_card_grade_code TEXT,
|
||||
status TEXT,
|
||||
user_status TEXT,
|
||||
balance NUMERIC(18,2),
|
||||
points NUMERIC(18,2),
|
||||
growth_value NUMERIC(18,2),
|
||||
last_visit_time TIMESTAMPTZ,
|
||||
wechat_id TEXT,
|
||||
alipay_id TEXT,
|
||||
member_card_no TEXT,
|
||||
referrer_member_id BIGINT,
|
||||
remarks TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
@@ -41,16 +48,28 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_member_card (
|
||||
site_id BIGINT NOT NULL,
|
||||
card_id BIGINT NOT NULL,
|
||||
member_id BIGINT,
|
||||
tenant_member_id BIGINT,
|
||||
card_type_id BIGINT,
|
||||
card_type_name TEXT,
|
||||
card_no TEXT,
|
||||
card_physics_type TEXT,
|
||||
card_balance NUMERIC(18,2),
|
||||
denomination NUMERIC(18,2),
|
||||
discount_rate NUMERIC(8,4),
|
||||
table_discount NUMERIC(18,2),
|
||||
goods_discount NUMERIC(18,2),
|
||||
assistant_discount NUMERIC(18,2),
|
||||
assistant_reward_discount NUMERIC(18,2),
|
||||
valid_start_date DATE,
|
||||
valid_end_date DATE,
|
||||
disable_start_date DATE,
|
||||
disable_end_date DATE,
|
||||
last_consume_time TIMESTAMPTZ,
|
||||
status TEXT,
|
||||
is_delete BOOLEAN,
|
||||
activate_time TIMESTAMPTZ,
|
||||
deactivate_time TIMESTAMPTZ,
|
||||
register_site_id BIGINT,
|
||||
issuer_id BIGINT,
|
||||
issuer_name TEXT,
|
||||
source_file TEXT,
|
||||
@@ -65,12 +84,20 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_balance_change (
|
||||
site_id BIGINT NOT NULL,
|
||||
change_id BIGINT NOT NULL,
|
||||
member_id BIGINT,
|
||||
tenant_member_id BIGINT,
|
||||
tenant_member_card_id BIGINT,
|
||||
member_name TEXT,
|
||||
member_mobile TEXT,
|
||||
change_amount NUMERIC(18,2),
|
||||
balance_before NUMERIC(18,2),
|
||||
balance_after NUMERIC(18,2),
|
||||
change_type INT,
|
||||
payment_method INT,
|
||||
refund_amount NUMERIC(18,2),
|
||||
relate_id BIGINT,
|
||||
pay_method INT,
|
||||
register_site_id BIGINT,
|
||||
register_site_name TEXT,
|
||||
pay_site_name TEXT,
|
||||
remark TEXT,
|
||||
operator_id BIGINT,
|
||||
operator_name TEXT,
|
||||
@@ -112,8 +139,16 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_product (
|
||||
goods_code TEXT,
|
||||
category_id BIGINT,
|
||||
category_name TEXT,
|
||||
goods_second_category_id BIGINT,
|
||||
unit TEXT,
|
||||
price NUMERIC(18,2),
|
||||
cost_price NUMERIC(18,2),
|
||||
market_price NUMERIC(18,2),
|
||||
goods_state TEXT,
|
||||
goods_cover TEXT,
|
||||
goods_bar_code TEXT,
|
||||
able_discount BOOLEAN,
|
||||
is_delete BOOLEAN,
|
||||
status TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
@@ -130,8 +165,16 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_store_product (
|
||||
goods_name TEXT,
|
||||
category_id BIGINT,
|
||||
category_name TEXT,
|
||||
unit TEXT,
|
||||
sale_price NUMERIC(18,2),
|
||||
cost_price NUMERIC(18,2),
|
||||
sale_num NUMERIC(18,2),
|
||||
stock_a NUMERIC(18,2),
|
||||
stock NUMERIC(18,2),
|
||||
provisional_total_cost NUMERIC(18,2),
|
||||
total_purchase_cost NUMERIC(18,2),
|
||||
batch_stock_quantity NUMERIC(18,2),
|
||||
goods_state TEXT,
|
||||
status TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
@@ -144,17 +187,32 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_store_sale_item (
|
||||
tenant_id BIGINT,
|
||||
site_id BIGINT NOT NULL,
|
||||
sale_item_id BIGINT NOT NULL,
|
||||
order_goods_id BIGINT,
|
||||
order_trade_no TEXT,
|
||||
order_settle_id BIGINT,
|
||||
site_goods_id BIGINT,
|
||||
goods_id BIGINT,
|
||||
goods_name TEXT,
|
||||
category_id BIGINT,
|
||||
quantity NUMERIC(18,4),
|
||||
unit_price NUMERIC(18,2),
|
||||
original_amount NUMERIC(18,2),
|
||||
discount_amount NUMERIC(18,2),
|
||||
final_amount NUMERIC(18,2),
|
||||
is_gift BOOLEAN DEFAULT FALSE,
|
||||
sale_time TIMESTAMPTZ,
|
||||
member_id BIGINT,
|
||||
salesman_id BIGINT,
|
||||
operator_id BIGINT,
|
||||
is_refunded BOOLEAN DEFAULT FALSE,
|
||||
discount_price NUMERIC(18,2),
|
||||
cost_money NUMERIC(18,2),
|
||||
coupon_deduct_amount NUMERIC(18,2),
|
||||
member_discount_amount NUMERIC(18,2),
|
||||
point_discount_money NUMERIC(18,2),
|
||||
point_discount_cost NUMERIC(18,2),
|
||||
sales_type TEXT,
|
||||
goods_remark TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
fetched_at TIMESTAMPTZ DEFAULT now(),
|
||||
@@ -170,6 +228,18 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_table_info (
|
||||
table_name TEXT,
|
||||
table_type TEXT,
|
||||
area_name TEXT,
|
||||
site_table_area_id BIGINT,
|
||||
tenant_table_area_id BIGINT,
|
||||
table_price NUMERIC(18,2),
|
||||
table_status TEXT,
|
||||
audit_status INT,
|
||||
show_status INT,
|
||||
light_status INT,
|
||||
virtual_table BOOLEAN,
|
||||
is_rest_area BOOLEAN,
|
||||
charge_free BOOLEAN,
|
||||
table_cloth_use_time INT,
|
||||
table_cloth_use_cycle INT,
|
||||
status TEXT,
|
||||
created_time TIMESTAMPTZ,
|
||||
updated_time TIMESTAMPTZ,
|
||||
@@ -185,15 +255,42 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_table_use_log (
|
||||
site_id BIGINT NOT NULL,
|
||||
ledger_id BIGINT NOT NULL,
|
||||
table_id BIGINT,
|
||||
table_name TEXT,
|
||||
order_trade_no TEXT,
|
||||
order_settle_id BIGINT,
|
||||
start_time TIMESTAMPTZ,
|
||||
end_time TIMESTAMPTZ,
|
||||
duration_minutes INT,
|
||||
duration_seconds INT,
|
||||
billing_unit_price NUMERIC(18,4),
|
||||
billing_count NUMERIC(18,4),
|
||||
original_table_fee NUMERIC(18,2),
|
||||
discount_amount NUMERIC(18,2),
|
||||
member_discount_amount NUMERIC(18,2),
|
||||
coupon_discount_amount NUMERIC(18,2),
|
||||
manual_discount_amount NUMERIC(18,2),
|
||||
service_fee NUMERIC(18,2),
|
||||
final_table_fee NUMERIC(18,2),
|
||||
member_id BIGINT,
|
||||
operator_id BIGINT,
|
||||
salesman_id BIGINT,
|
||||
is_canceled BOOLEAN DEFAULT FALSE,
|
||||
cancel_time TIMESTAMPTZ,
|
||||
site_table_area_id BIGINT,
|
||||
tenant_table_area_id BIGINT,
|
||||
site_table_area_name TEXT,
|
||||
is_single_order BOOLEAN,
|
||||
used_card_amount NUMERIC(18,2),
|
||||
adjust_amount NUMERIC(18,2),
|
||||
coupon_promotion_amount NUMERIC(18,2),
|
||||
service_money NUMERIC(18,2),
|
||||
mgmt_fee NUMERIC(18,2),
|
||||
fee_total NUMERIC(18,2),
|
||||
real_table_use_seconds INT,
|
||||
last_use_time TIMESTAMPTZ,
|
||||
ledger_start_time TIMESTAMPTZ,
|
||||
ledger_end_time TIMESTAMPTZ,
|
||||
ledger_status INT,
|
||||
start_use_time TIMESTAMPTZ,
|
||||
add_clock_seconds INT,
|
||||
status TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
@@ -226,8 +323,16 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_assistant_account (
|
||||
assistant_id BIGINT NOT NULL,
|
||||
assistant_name TEXT,
|
||||
mobile TEXT,
|
||||
assistant_no INT,
|
||||
team_id BIGINT,
|
||||
team_name TEXT,
|
||||
group_id BIGINT,
|
||||
group_name TEXT,
|
||||
job_num TEXT,
|
||||
entry_type TEXT,
|
||||
leave_status TEXT,
|
||||
assistant_status TEXT,
|
||||
allow_cx BOOLEAN,
|
||||
status TEXT,
|
||||
hired_date DATE,
|
||||
left_date DATE,
|
||||
@@ -243,16 +348,27 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_assistant_service_log (
|
||||
site_id BIGINT NOT NULL,
|
||||
ledger_id BIGINT NOT NULL,
|
||||
assistant_id BIGINT,
|
||||
assistant_name TEXT,
|
||||
service_type TEXT,
|
||||
order_trade_no TEXT,
|
||||
order_settle_id BIGINT,
|
||||
start_time TIMESTAMPTZ,
|
||||
end_time TIMESTAMPTZ,
|
||||
duration_minutes INT,
|
||||
duration_seconds INT,
|
||||
original_fee NUMERIC(18,2),
|
||||
discount_amount NUMERIC(18,2),
|
||||
member_discount_amount NUMERIC(18,2),
|
||||
manual_discount_amount NUMERIC(18,2),
|
||||
coupon_discount_amount NUMERIC(18,2),
|
||||
final_fee NUMERIC(18,2),
|
||||
member_id BIGINT,
|
||||
operator_id BIGINT,
|
||||
salesman_id BIGINT,
|
||||
is_canceled BOOLEAN DEFAULT FALSE,
|
||||
cancel_time TIMESTAMPTZ,
|
||||
skill_grade NUMERIC(10,2),
|
||||
service_grade NUMERIC(10,2),
|
||||
composite_grade NUMERIC(10,2),
|
||||
overall_score NUMERIC(10,2),
|
||||
status TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
@@ -268,6 +384,13 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_assistant_cancel_log (
|
||||
ledger_id BIGINT,
|
||||
assistant_id BIGINT,
|
||||
order_trade_no TEXT,
|
||||
table_id BIGINT,
|
||||
table_area_id BIGINT,
|
||||
table_area_name TEXT,
|
||||
table_name TEXT,
|
||||
assistant_on INT,
|
||||
pd_charge_minutes INT,
|
||||
assistant_abolish_amount NUMERIC(18,2),
|
||||
reason TEXT,
|
||||
cancel_time TIMESTAMPTZ,
|
||||
operator_id BIGINT,
|
||||
@@ -284,12 +407,24 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_group_package (
|
||||
site_id BIGINT NOT NULL,
|
||||
package_id BIGINT NOT NULL,
|
||||
package_name TEXT,
|
||||
table_area_id BIGINT,
|
||||
table_area_name TEXT,
|
||||
platform_code TEXT,
|
||||
status TEXT,
|
||||
face_price NUMERIC(18,2),
|
||||
settle_price NUMERIC(18,2),
|
||||
selling_price NUMERIC(18,2),
|
||||
duration INT,
|
||||
valid_from DATE,
|
||||
valid_to DATE,
|
||||
start_time TIMESTAMPTZ,
|
||||
end_time TIMESTAMPTZ,
|
||||
is_enabled BOOLEAN,
|
||||
is_delete BOOLEAN,
|
||||
package_type TEXT,
|
||||
usable_count INT,
|
||||
creator_name TEXT,
|
||||
tenant_table_area_id BIGINT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
fetched_at TIMESTAMPTZ DEFAULT now(),
|
||||
@@ -310,6 +445,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_group_package_log (
|
||||
used_time TIMESTAMPTZ,
|
||||
deduct_amount NUMERIC(18,2),
|
||||
settle_price NUMERIC(18,2),
|
||||
coupon_code TEXT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
fetched_at TIMESTAMPTZ DEFAULT now(),
|
||||
@@ -323,6 +459,8 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_platform_coupon_log (
|
||||
coupon_id BIGINT NOT NULL,
|
||||
platform_code TEXT,
|
||||
verify_code TEXT,
|
||||
coupon_code TEXT,
|
||||
coupon_channel TEXT,
|
||||
order_trade_no TEXT,
|
||||
order_settle_id BIGINT,
|
||||
member_id BIGINT,
|
||||
@@ -330,6 +468,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_platform_coupon_log (
|
||||
used_time TIMESTAMPTZ,
|
||||
deduct_amount NUMERIC(18,2),
|
||||
settle_price NUMERIC(18,2),
|
||||
coupon_money NUMERIC(18,2),
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
fetched_at TIMESTAMPTZ DEFAULT now(),
|
||||
@@ -343,11 +482,19 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_inventory_change (
|
||||
change_id BIGINT NOT NULL,
|
||||
site_goods_id BIGINT,
|
||||
goods_id BIGINT,
|
||||
stock_type TEXT,
|
||||
change_amount NUMERIC(18,2),
|
||||
before_stock NUMERIC(18,2),
|
||||
after_stock NUMERIC(18,2),
|
||||
change_amount_alt NUMERIC(18,2),
|
||||
before_stock_alt NUMERIC(18,2),
|
||||
after_stock_alt NUMERIC(18,2),
|
||||
change_type TEXT,
|
||||
relate_id BIGINT,
|
||||
unit TEXT,
|
||||
price NUMERIC(18,2),
|
||||
goods_category_id BIGINT,
|
||||
goods_second_category_id BIGINT,
|
||||
remark TEXT,
|
||||
operator_id BIGINT,
|
||||
operator_name TEXT,
|
||||
@@ -364,8 +511,20 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_inventory_stock (
|
||||
site_id BIGINT NOT NULL,
|
||||
site_goods_id BIGINT NOT NULL,
|
||||
goods_id BIGINT,
|
||||
goods_name TEXT,
|
||||
goods_unit TEXT,
|
||||
goods_category_id BIGINT,
|
||||
goods_second_category_id BIGINT,
|
||||
range_start_stock NUMERIC(18,2),
|
||||
range_end_stock NUMERIC(18,2),
|
||||
range_in NUMERIC(18,2),
|
||||
range_out NUMERIC(18,2),
|
||||
range_inventory NUMERIC(18,2),
|
||||
range_sale NUMERIC(18,2),
|
||||
range_sale_money NUMERIC(18,2),
|
||||
current_stock NUMERIC(18,2),
|
||||
cost_price NUMERIC(18,2),
|
||||
category_name TEXT,
|
||||
snapshot_key TEXT NOT NULL DEFAULT 'default',
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
@@ -383,6 +542,7 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_order_settle (
|
||||
settle_type INT,
|
||||
settle_status INT,
|
||||
member_id BIGINT,
|
||||
member_name TEXT,
|
||||
member_phone TEXT,
|
||||
table_id BIGINT,
|
||||
consume_money NUMERIC(18,2),
|
||||
@@ -392,6 +552,9 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_order_settle (
|
||||
assistant_pd_money NUMERIC(18,2),
|
||||
assistant_cx_money NUMERIC(18,2),
|
||||
pay_amount NUMERIC(18,2),
|
||||
cash_amount NUMERIC(18,2),
|
||||
online_amount NUMERIC(18,2),
|
||||
point_amount NUMERIC(18,2),
|
||||
coupon_amount NUMERIC(18,2),
|
||||
card_amount NUMERIC(18,2),
|
||||
balance_amount NUMERIC(18,2),
|
||||
@@ -399,9 +562,33 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_order_settle (
|
||||
prepay_money NUMERIC(18,2),
|
||||
adjust_amount NUMERIC(18,2),
|
||||
rounding_amount NUMERIC(18,2),
|
||||
member_discount_amount NUMERIC(18,2),
|
||||
coupon_sale_amount NUMERIC(18,2),
|
||||
goods_promotion_money NUMERIC(18,2),
|
||||
assistant_promotion_money NUMERIC(18,2),
|
||||
point_discount_price NUMERIC(18,2),
|
||||
point_discount_cost NUMERIC(18,2),
|
||||
real_goods_money NUMERIC(18,2),
|
||||
assistant_manual_discount NUMERIC(18,2),
|
||||
all_coupon_discount NUMERIC(18,2),
|
||||
is_use_coupon BOOLEAN,
|
||||
is_use_discount BOOLEAN,
|
||||
is_activity BOOLEAN,
|
||||
is_bind_member BOOLEAN,
|
||||
is_first BOOLEAN,
|
||||
recharge_card_amount NUMERIC(18,2),
|
||||
gift_card_amount NUMERIC(18,2),
|
||||
payment_method INT,
|
||||
create_time TIMESTAMPTZ,
|
||||
pay_time TIMESTAMPTZ,
|
||||
revoke_order_id BIGINT,
|
||||
revoke_order_name TEXT,
|
||||
revoke_time TIMESTAMPTZ,
|
||||
can_be_revoked BOOLEAN,
|
||||
serial_number TEXT,
|
||||
sales_man_name TEXT,
|
||||
sales_man_user_id BIGINT,
|
||||
order_remark TEXT,
|
||||
operator_id BIGINT,
|
||||
operator_name TEXT,
|
||||
source_file TEXT,
|
||||
@@ -436,8 +623,13 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_payment_record (
|
||||
member_id BIGINT,
|
||||
pay_method_code TEXT,
|
||||
pay_method_name TEXT,
|
||||
pay_status INT,
|
||||
pay_amount NUMERIC(18,2),
|
||||
pay_time TIMESTAMPTZ,
|
||||
online_pay_channel TEXT,
|
||||
transaction_id TEXT,
|
||||
operator_id BIGINT,
|
||||
remark TEXT,
|
||||
relate_type TEXT,
|
||||
relate_id BIGINT,
|
||||
source_file TEXT,
|
||||
@@ -454,10 +646,34 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_refund_record (
|
||||
order_trade_no TEXT,
|
||||
order_settle_id BIGINT,
|
||||
member_id BIGINT,
|
||||
pay_sn TEXT,
|
||||
pay_amount NUMERIC(18,2),
|
||||
pay_status INT,
|
||||
is_revoke BOOLEAN,
|
||||
is_delete BOOLEAN,
|
||||
online_pay_channel TEXT,
|
||||
pay_method_code TEXT,
|
||||
refund_amount NUMERIC(18,2),
|
||||
refund_time TIMESTAMPTZ,
|
||||
action_type INT,
|
||||
pay_terminal INT,
|
||||
pay_config_id BIGINT,
|
||||
cashier_point_id BIGINT,
|
||||
operator_id BIGINT,
|
||||
member_card_id BIGINT,
|
||||
balance_frozen_amount NUMERIC(18,2),
|
||||
card_frozen_amount NUMERIC(18,2),
|
||||
round_amount NUMERIC(18,2),
|
||||
online_pay_type INT,
|
||||
channel_payer_id TEXT,
|
||||
channel_pay_no TEXT,
|
||||
check_status INT,
|
||||
channel_fee NUMERIC(18,2),
|
||||
relate_type TEXT,
|
||||
relate_id BIGINT,
|
||||
status TEXT,
|
||||
reason TEXT,
|
||||
related_payment_id BIGINT,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
fetched_at TIMESTAMPTZ DEFAULT now(),
|
||||
@@ -470,13 +686,36 @@ CREATE TABLE IF NOT EXISTS billiards_ods.ods_order_receipt_detail (
|
||||
site_id BIGINT NOT NULL,
|
||||
order_settle_id BIGINT NOT NULL,
|
||||
order_trade_no TEXT,
|
||||
order_settle_number TEXT,
|
||||
settle_type INT,
|
||||
receipt_no TEXT,
|
||||
receipt_time TIMESTAMPTZ,
|
||||
total_amount NUMERIC(18,2),
|
||||
discount_amount NUMERIC(18,2),
|
||||
final_amount NUMERIC(18,2),
|
||||
actual_payment NUMERIC(18,2),
|
||||
ledger_amount NUMERIC(18,2),
|
||||
member_offer_amount NUMERIC(18,2),
|
||||
delivery_fee NUMERIC(18,2),
|
||||
adjust_amount NUMERIC(18,2),
|
||||
payment_method INT,
|
||||
pay_time TIMESTAMPTZ,
|
||||
member_id BIGINT,
|
||||
order_remark TEXT,
|
||||
cashier_name TEXT,
|
||||
ticket_remark TEXT,
|
||||
ticket_custom_content TEXT,
|
||||
voucher_money NUMERIC(18,2),
|
||||
reward_name TEXT,
|
||||
consume_money NUMERIC(18,2),
|
||||
refund_amount NUMERIC(18,2),
|
||||
balance_amount NUMERIC(18,2),
|
||||
coupon_amount NUMERIC(18,2),
|
||||
member_deduct_amount NUMERIC(18,2),
|
||||
prepay_money NUMERIC(18,2),
|
||||
delivery_address TEXT,
|
||||
snapshot_raw JSONB,
|
||||
member_snapshot JSONB,
|
||||
source_file TEXT,
|
||||
source_endpoint TEXT,
|
||||
fetched_at TIMESTAMPTZ DEFAULT now(),
|
||||
|
||||
39
etl_billiards/database/seed_ods_tasks.sql
Normal file
39
etl_billiards/database/seed_ods_tasks.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- 将新的 ODS 任务注册到 etl_admin.etl_task(根据需要替换 store_id)
|
||||
-- 使用方式(示例):
|
||||
-- psql "$PG_DSN" -f etl_billiards/database/seed_ods_tasks.sql
|
||||
-- 或者在 psql 中执行本文件内容。
|
||||
|
||||
WITH target_store AS (
|
||||
SELECT 2790685415443269::bigint AS store_id -- TODO: 替换为实际 store_id
|
||||
),
|
||||
task_codes AS (
|
||||
SELECT unnest(ARRAY[
|
||||
'ODS_ASSISTANT_ACCOUNTS',
|
||||
'ODS_ASSISTANT_LEDGER',
|
||||
'ODS_ASSISTANT_ABOLISH',
|
||||
'ODS_INVENTORY_CHANGE',
|
||||
'ODS_INVENTORY_STOCK',
|
||||
'ODS_PACKAGE',
|
||||
'ODS_GROUP_BUY_REDEMPTION',
|
||||
'ODS_MEMBER',
|
||||
'ODS_MEMBER_BALANCE',
|
||||
'ODS_MEMBER_CARD',
|
||||
'ODS_PAYMENT',
|
||||
'ODS_REFUND',
|
||||
'ODS_COUPON_VERIFY',
|
||||
'ODS_RECHARGE_SETTLE',
|
||||
'ODS_TABLES',
|
||||
'ODS_GOODS_CATEGORY',
|
||||
'ODS_STORE_GOODS',
|
||||
'ODS_TABLE_DISCOUNT',
|
||||
'ODS_TENANT_GOODS',
|
||||
'ODS_SETTLEMENT_TICKET',
|
||||
'ODS_ORDER_SETTLE'
|
||||
]) AS task_code
|
||||
)
|
||||
INSERT INTO etl_admin.etl_task (task_code, store_id, enabled)
|
||||
SELECT t.task_code, s.store_id, TRUE
|
||||
FROM task_codes t CROSS JOIN target_store s
|
||||
ON CONFLICT (task_code, store_id) DO UPDATE
|
||||
SET enabled = EXCLUDED.enabled;
|
||||
|
||||
16
etl_billiards/feiqiu-ETL.code-workspace
Normal file
16
etl_billiards/feiqiu-ETL.code-workspace
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"name": "LLZQ-server",
|
||||
"path": "../../../LLZQ-server"
|
||||
},
|
||||
{
|
||||
"name": "feiqiu-ETL-reload",
|
||||
"path": "../../feiqiu-ETL-reload"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据加载器基类"""
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class BaseLoader:
|
||||
"""数据加载器基类"""
|
||||
|
||||
def __init__(self, db_ops):
|
||||
def __init__(self, db_ops, logger=None):
|
||||
self.db = db_ops
|
||||
self.logger = logger or logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def upsert(self, records: list) -> tuple:
|
||||
"""
|
||||
|
||||
@@ -12,28 +12,33 @@ class PaymentLoader(BaseLoader):
|
||||
|
||||
sql = """
|
||||
INSERT INTO billiards.fact_payment (
|
||||
store_id, pay_id, site_id, tenant_id,
|
||||
store_id, pay_id, order_id,
|
||||
site_id, tenant_id,
|
||||
order_settle_id, order_trade_no,
|
||||
relate_type, relate_id,
|
||||
create_time, pay_time,
|
||||
pay_amount, fee_amount, discount_amount,
|
||||
payment_method, online_pay_channel, pay_terminal,
|
||||
pay_status, raw_data
|
||||
payment_method, pay_type,
|
||||
online_pay_channel, pay_terminal,
|
||||
pay_status, remark, raw_data
|
||||
)
|
||||
VALUES (
|
||||
%(store_id)s, %(pay_id)s, %(site_id)s, %(tenant_id)s,
|
||||
%(store_id)s, %(pay_id)s, %(order_id)s,
|
||||
%(site_id)s, %(tenant_id)s,
|
||||
%(order_settle_id)s, %(order_trade_no)s,
|
||||
%(relate_type)s, %(relate_id)s,
|
||||
%(create_time)s, %(pay_time)s,
|
||||
%(pay_amount)s, %(fee_amount)s, %(discount_amount)s,
|
||||
%(payment_method)s, %(online_pay_channel)s, %(pay_terminal)s,
|
||||
%(pay_status)s, %(raw_data)s
|
||||
%(payment_method)s, %(pay_type)s,
|
||||
%(online_pay_channel)s, %(pay_terminal)s,
|
||||
%(pay_status)s, %(remark)s, %(raw_data)s
|
||||
)
|
||||
ON CONFLICT (store_id, pay_id) DO UPDATE SET
|
||||
order_settle_id = EXCLUDED.order_settle_id,
|
||||
order_trade_no = EXCLUDED.order_trade_no,
|
||||
relate_type = EXCLUDED.relate_type,
|
||||
relate_id = EXCLUDED.relate_id,
|
||||
order_id = EXCLUDED.order_id,
|
||||
site_id = EXCLUDED.site_id,
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
create_time = EXCLUDED.create_time,
|
||||
@@ -42,9 +47,11 @@ class PaymentLoader(BaseLoader):
|
||||
fee_amount = EXCLUDED.fee_amount,
|
||||
discount_amount = EXCLUDED.discount_amount,
|
||||
payment_method = EXCLUDED.payment_method,
|
||||
pay_type = EXCLUDED.pay_type,
|
||||
online_pay_channel = EXCLUDED.online_pay_channel,
|
||||
pay_terminal = EXCLUDED.pay_terminal,
|
||||
pay_status = EXCLUDED.pay_status,
|
||||
remark = EXCLUDED.remark,
|
||||
raw_data = EXCLUDED.raw_data,
|
||||
updated_at = now()
|
||||
RETURNING (xmax = 0) AS inserted
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""ETL调度器"""
|
||||
"""ETL 调度:支持在线抓取、离线清洗入库、全流程三种模式。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from api.client import APIClient
|
||||
from api.local_json_client import LocalJsonClient
|
||||
from api.recording_client import RecordingAPIClient
|
||||
from database.connection import DatabaseConnection
|
||||
from database.operations import DatabaseOperations
|
||||
from api.client import APIClient
|
||||
from orchestration.cursor_manager import CursorManager
|
||||
from orchestration.run_tracker import RunTracker
|
||||
from orchestration.task_registry import default_registry
|
||||
|
||||
|
||||
class ETLScheduler:
|
||||
"""ETL任务调度器"""
|
||||
"""调度多个任务,按 pipeline.flow 执行抓取/清洗入库。"""
|
||||
|
||||
def __init__(self, config, logger):
|
||||
self.config = config
|
||||
self.logger = logger
|
||||
self.tz = ZoneInfo(config.get("app.timezone", "Asia/Taipei"))
|
||||
|
||||
# 初始化组件
|
||||
self.pipeline_flow = str(config.get("pipeline.flow", "FULL") or "FULL").upper()
|
||||
self.fetch_root = Path(config.get("pipeline.fetch_root") or config["io"]["export_root"])
|
||||
self.ingest_source_dir = config.get("pipeline.ingest_source_dir") or ""
|
||||
self.write_pretty_json = bool(config.get("io.write_pretty_json", False))
|
||||
|
||||
# 组件
|
||||
self.db_conn = DatabaseConnection(
|
||||
dsn=config["db"]["dsn"],
|
||||
session=config["db"].get("session"),
|
||||
connect_timeout=config["db"].get("connect_timeout_sec")
|
||||
connect_timeout=config["db"].get("connect_timeout_sec"),
|
||||
)
|
||||
self.db_ops = DatabaseOperations(self.db_conn)
|
||||
|
||||
@@ -33,89 +43,166 @@ class ETLScheduler:
|
||||
token=config["api"]["token"],
|
||||
timeout=config["api"]["timeout_sec"],
|
||||
retry_max=config["api"]["retries"]["max_attempts"],
|
||||
headers_extra=config["api"].get("headers_extra")
|
||||
headers_extra=config["api"].get("headers_extra"),
|
||||
)
|
||||
|
||||
self.cursor_mgr = CursorManager(self.db_conn)
|
||||
self.run_tracker = RunTracker(self.db_conn)
|
||||
self.task_registry = default_registry
|
||||
|
||||
def run_tasks(self, task_codes: list = None):
|
||||
"""运行任务列表"""
|
||||
# ------------------------------------------------------------------ public
|
||||
def run_tasks(self, task_codes: list | None = None):
|
||||
"""按配置或传入列表执行任务。"""
|
||||
run_uuid = uuid.uuid4().hex
|
||||
store_id = self.config.get("app.store_id")
|
||||
|
||||
if not task_codes:
|
||||
task_codes = self.config.get("run.tasks", [])
|
||||
|
||||
self.logger.info(f"开始运行任务: {task_codes}, run_uuid={run_uuid}")
|
||||
self.logger.info("开始运行任务: %s, run_uuid=%s", task_codes, run_uuid)
|
||||
|
||||
for task_code in task_codes:
|
||||
try:
|
||||
self._run_single_task(task_code, run_uuid, store_id)
|
||||
except Exception as e:
|
||||
self.logger.error(f"任务 {task_code} 失败: {e}", exc_info=True)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self.logger.error("任务 %s 失败: %s", task_code, exc, exc_info=True)
|
||||
continue
|
||||
|
||||
self.logger.info("所有任务执行完成")
|
||||
|
||||
# ------------------------------------------------------------------ internals
|
||||
def _run_single_task(self, task_code: str, run_uuid: str, store_id: int):
|
||||
"""运行单个任务"""
|
||||
# 创建任务实例
|
||||
task = self.task_registry.create_task(
|
||||
task_code, self.config, self.db_ops, self.api_client, self.logger
|
||||
)
|
||||
|
||||
# 获取任务配置(从数据库)
|
||||
"""单个任务的抓取/清洗编排。"""
|
||||
task_cfg = self._load_task_config(task_code, store_id)
|
||||
if not task_cfg:
|
||||
self.logger.warning(f"任务 {task_code} 未启用或不存在")
|
||||
self.logger.warning("任务 %s 未启用或不存在", task_code)
|
||||
return
|
||||
|
||||
task_id = task_cfg["task_id"]
|
||||
cursor_data = self.cursor_mgr.get_or_create(task_id, store_id)
|
||||
|
||||
# 创建运行记录
|
||||
# run 记录
|
||||
export_dir = Path(self.config["io"]["export_root"]) / datetime.now(self.tz).strftime("%Y%m%d")
|
||||
log_path = str(Path(self.config["io"]["log_root"]) / f"{run_uuid}.log")
|
||||
|
||||
run_id = self.run_tracker.create_run(
|
||||
task_id=task_id,
|
||||
store_id=store_id,
|
||||
run_uuid=run_uuid,
|
||||
export_dir=str(export_dir),
|
||||
log_path=log_path,
|
||||
status="RUNNING"
|
||||
status=self._map_run_status("RUNNING"),
|
||||
)
|
||||
|
||||
# 执行任务
|
||||
try:
|
||||
result = task.execute()
|
||||
# 为抓取阶段准备目录
|
||||
fetch_dir = self._build_fetch_dir(task_code, run_id)
|
||||
fetch_stats = None
|
||||
|
||||
try:
|
||||
if self._flow_includes_fetch():
|
||||
fetch_stats = self._execute_fetch(task_code, cursor_data, fetch_dir, run_id)
|
||||
if self.pipeline_flow == "FETCH_ONLY":
|
||||
counts = self._counts_from_fetch(fetch_stats)
|
||||
self.run_tracker.update_run(
|
||||
run_id=run_id,
|
||||
counts=counts,
|
||||
status=self._map_run_status("SUCCESS"),
|
||||
ended_at=datetime.now(self.tz),
|
||||
)
|
||||
return
|
||||
|
||||
if self._flow_includes_ingest():
|
||||
source_dir = self._resolve_ingest_source(fetch_dir, fetch_stats)
|
||||
result = self._execute_ingest(task_code, cursor_data, source_dir)
|
||||
|
||||
# 更新运行记录
|
||||
self.run_tracker.update_run(
|
||||
run_id=run_id,
|
||||
counts=result["counts"],
|
||||
status=result["status"],
|
||||
ended_at=datetime.now(self.tz)
|
||||
status=self._map_run_status(result["status"]),
|
||||
ended_at=datetime.now(self.tz),
|
||||
)
|
||||
|
||||
# 推进游标
|
||||
if result["status"] == "SUCCESS":
|
||||
# TODO: 从任务结果中获取窗口信息
|
||||
pass
|
||||
if (result.get("status") or "").upper() == "SUCCESS":
|
||||
window = result.get("window")
|
||||
if window:
|
||||
self.cursor_mgr.advance(
|
||||
task_id=task_id,
|
||||
store_id=store_id,
|
||||
window_start=window.get("start"),
|
||||
window_end=window.get("end"),
|
||||
run_id=run_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self.run_tracker.update_run(
|
||||
run_id=run_id,
|
||||
counts={},
|
||||
status="FAIL",
|
||||
status=self._map_run_status("FAIL"),
|
||||
ended_at=datetime.now(self.tz),
|
||||
error_message=str(e)
|
||||
error_message=str(exc),
|
||||
)
|
||||
raise
|
||||
|
||||
def _load_task_config(self, task_code: str, store_id: int) -> dict:
|
||||
"""从数据库加载任务配置"""
|
||||
def _execute_fetch(self, task_code: str, cursor_data: dict | None, fetch_dir: Path, run_id: int):
|
||||
"""在线抓取阶段:用 RecordingAPIClient 拉取并落盘,不做 Transform/Load。"""
|
||||
recording_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_code, self.config, self.db_ops, recording_client, self.logger)
|
||||
context = task._build_context(cursor_data) # type: ignore[attr-defined]
|
||||
self.logger.info("%s: 抓取阶段开始,目录=%s", task_code, fetch_dir)
|
||||
|
||||
extracted = task.extract(context)
|
||||
# 抓取结束,不执行 transform/load
|
||||
stats = recording_client.last_dump or {}
|
||||
fetched_count = stats.get("records") or len(extracted.get("records", [])) if isinstance(extracted, dict) else 0
|
||||
self.logger.info(
|
||||
"%s: 抓取完成,文件=%s,记录数=%s",
|
||||
task_code,
|
||||
stats.get("file"),
|
||||
fetched_count,
|
||||
)
|
||||
return {"file": stats.get("file"), "records": fetched_count, "pages": stats.get("pages")}
|
||||
|
||||
def _execute_ingest(self, task_code: str, cursor_data: dict | None, source_dir: Path):
|
||||
"""本地清洗入库:使用 LocalJsonClient 回放 JSON,走原有任务 ETL。"""
|
||||
local_client = LocalJsonClient(source_dir)
|
||||
task = self.task_registry.create_task(task_code, self.config, self.db_ops, local_client, self.logger)
|
||||
self.logger.info("%s: 本地清洗入库开始,源目录=%s", task_code, source_dir)
|
||||
return task.execute(cursor_data)
|
||||
|
||||
def _build_fetch_dir(self, task_code: str, run_id: int) -> Path:
|
||||
ts = datetime.now(self.tz).strftime("%Y%m%d-%H%M%S")
|
||||
return Path(self.fetch_root) / f"{task_code.upper()}-{run_id}-{ts}"
|
||||
|
||||
def _resolve_ingest_source(self, fetch_dir: Path, fetch_stats: dict | None) -> Path:
|
||||
if fetch_stats and fetch_dir.exists():
|
||||
return fetch_dir
|
||||
if self.ingest_source_dir:
|
||||
return Path(self.ingest_source_dir)
|
||||
raise FileNotFoundError("未提供本地清洗入库所需的 JSON 目录")
|
||||
|
||||
def _counts_from_fetch(self, stats: dict | None) -> dict:
|
||||
fetched = (stats or {}).get("records") or 0
|
||||
return {
|
||||
"fetched": fetched,
|
||||
"inserted": 0,
|
||||
"updated": 0,
|
||||
"skipped": 0,
|
||||
"errors": 0,
|
||||
}
|
||||
|
||||
def _flow_includes_fetch(self) -> bool:
|
||||
return self.pipeline_flow in {"FETCH_ONLY", "FULL"}
|
||||
|
||||
def _flow_includes_ingest(self) -> bool:
|
||||
return self.pipeline_flow in {"INGEST_ONLY", "FULL"}
|
||||
|
||||
def _load_task_config(self, task_code: str, store_id: int) -> dict | None:
|
||||
"""从数据库加载任务配置。"""
|
||||
sql = """
|
||||
SELECT task_id, task_code, store_id, enabled, cursor_field,
|
||||
window_minutes_default, overlap_seconds, page_size, retry_max, params
|
||||
@@ -127,5 +214,21 @@ class ETLScheduler:
|
||||
return rows[0] if rows else None
|
||||
|
||||
def close(self):
|
||||
"""关闭连接"""
|
||||
"""关闭连接。"""
|
||||
self.db_conn.close()
|
||||
|
||||
@staticmethod
|
||||
def _map_run_status(status: str) -> str:
|
||||
"""
|
||||
将任务返回的状态转换为 etl_admin.run_status_enum
|
||||
(SUCC / FAIL / PARTIAL)
|
||||
"""
|
||||
normalized = (status or "").upper()
|
||||
if normalized in {"SUCCESS", "SUCC"}:
|
||||
return "SUCC"
|
||||
if normalized in {"FAIL", "FAILED", "ERROR"}:
|
||||
return "FAIL"
|
||||
if normalized in {"RUNNING", "PARTIAL", "PENDING", "IN_PROGRESS"}:
|
||||
return "PARTIAL"
|
||||
# 未知状态默认标记为 FAIL,便于排查
|
||||
return "FAIL"
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SCD2 (Slowly Changing Dimension Type 2) 处理器"""
|
||||
"""SCD2 (Slowly Changing Dimension Type 2) 处理逻辑"""
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _row_to_dict(cursor, row):
|
||||
if row is None:
|
||||
return None
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
return {col: row[idx] for idx, col in enumerate(columns)}
|
||||
|
||||
|
||||
class SCD2Handler:
|
||||
"""SCD2历史记录处理器"""
|
||||
"""SCD2历史记录处理"""
|
||||
|
||||
def __init__(self, db_ops):
|
||||
self.db = db_ops
|
||||
|
||||
def upsert(self, table_name: str, natural_key: list, tracked_fields: list,
|
||||
record: dict, effective_date: datetime = None) -> str:
|
||||
def upsert(
|
||||
self,
|
||||
table_name: str,
|
||||
natural_key: list,
|
||||
tracked_fields: list,
|
||||
record: dict,
|
||||
effective_date: datetime = None,
|
||||
) -> str:
|
||||
"""
|
||||
处理SCD2更新
|
||||
|
||||
Args:
|
||||
table_name: 表名
|
||||
natural_key: 自然键字段列表
|
||||
tracked_fields: 需要跟踪变化的字段列表
|
||||
record: 记录数据
|
||||
effective_date: 生效日期
|
||||
|
||||
Returns:
|
||||
操作类型: 'INSERT', 'UPDATE', 'UNCHANGED'
|
||||
"""
|
||||
effective_date = effective_date or datetime.now()
|
||||
|
||||
# 查找当前有效记录
|
||||
where_clause = " AND ".join([f"{k} = %({k})s" for k in natural_key])
|
||||
sql_select = f"""
|
||||
SELECT * FROM {table_name}
|
||||
@@ -33,13 +39,11 @@ class SCD2Handler:
|
||||
AND valid_to IS NULL
|
||||
"""
|
||||
|
||||
# 使用 db 的 connection
|
||||
current = self.db.conn.cursor()
|
||||
with self.db.conn.cursor() as current:
|
||||
current.execute(sql_select, record)
|
||||
existing = current.fetchone()
|
||||
existing = _row_to_dict(current, current.fetchone())
|
||||
|
||||
if not existing:
|
||||
# 新记录:直接插入
|
||||
record["valid_from"] = effective_date
|
||||
record["valid_to"] = None
|
||||
record["is_current"] = True
|
||||
@@ -51,18 +55,12 @@ class SCD2Handler:
|
||||
VALUES ({placeholders})
|
||||
"""
|
||||
current.execute(sql_insert, record)
|
||||
return 'INSERT'
|
||||
|
||||
# 检查是否有变化
|
||||
has_changes = any(
|
||||
existing.get(field) != record.get(field)
|
||||
for field in tracked_fields
|
||||
)
|
||||
return "INSERT"
|
||||
|
||||
has_changes = any(existing.get(field) != record.get(field) for field in tracked_fields)
|
||||
if not has_changes:
|
||||
return 'UNCHANGED'
|
||||
return "UNCHANGED"
|
||||
|
||||
# 有变化:关闭旧记录,插入新记录
|
||||
update_where = " AND ".join([f"{k} = %({k})s" for k in natural_key])
|
||||
sql_close = f"""
|
||||
UPDATE {table_name}
|
||||
@@ -74,7 +72,6 @@ class SCD2Handler:
|
||||
record["effective_date"] = effective_date
|
||||
current.execute(sql_close, record)
|
||||
|
||||
# 插入新记录
|
||||
record["valid_from"] = effective_date
|
||||
record["valid_to"] = None
|
||||
record["is_current"] = True
|
||||
@@ -89,4 +86,4 @@ class SCD2Handler:
|
||||
"""
|
||||
current.execute(sql_insert, record)
|
||||
|
||||
return 'UPDATE'
|
||||
return "UPDATE"
|
||||
|
||||
@@ -1,663 +0,0 @@
|
||||
-- -*- coding: utf-8 -*-
|
||||
-- Feiqiu-ETL schema (JSON-first alignment)
|
||||
-- Updated: 2025-11-19 (Refactored for Manual Import & AI-Friendly DWS)
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS billiards;
|
||||
CREATE SCHEMA IF NOT EXISTS billiards_ods;
|
||||
CREATE SCHEMA IF NOT EXISTS billiards_dws;
|
||||
CREATE SCHEMA IF NOT EXISTS etl_admin;
|
||||
|
||||
COMMENT ON SCHEMA billiards IS '门店业务数据 Schema,存放维度/事实层(与 JSON 字段对应)';
|
||||
COMMENT ON SCHEMA billiards_ods IS '原始数据层 (ODS),存放原始 JSON,支持 API 抓取和手工导入';
|
||||
COMMENT ON SCHEMA billiards_dws IS '数据汇总层 (DWS),存放面向 AI 分析的宽表视图';
|
||||
COMMENT ON SCHEMA etl_admin IS 'ETL 调度、游标与运行记录 Schema';
|
||||
|
||||
-- =========================
|
||||
-- 1. Billiards ODS tables
|
||||
-- =========================
|
||||
|
||||
-- 1.1 Order & Settlement ODS
|
||||
-- Corresponds to /order/list or manual export
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_order_settle (
|
||||
store_id bigint NOT NULL,
|
||||
order_settle_id bigint NOT NULL,
|
||||
order_trade_no bigint,
|
||||
page_no integer, -- Nullable for manual import
|
||||
page_size integer, -- Nullable for manual import
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, order_settle_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE billiards_ods.ods_order_settle IS '订单/结算 ODS(/order/list、ticket 接口原始 JSON)';
|
||||
|
||||
-- 1.2 Ticket Detail ODS (NEW)
|
||||
-- Corresponds to "小票详情.json" - Contains full nested details
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_ticket_detail (
|
||||
store_id bigint NOT NULL,
|
||||
order_settle_id bigint NOT NULL,
|
||||
source_file varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, order_settle_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE billiards_ods.ods_ticket_detail IS '小票详情 ODS(包含台费、商品、助教明细的完整 JSON)';
|
||||
|
||||
-- 1.3 Table Usage ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_table_use_detail (
|
||||
store_id bigint NOT NULL,
|
||||
ledger_id bigint NOT NULL,
|
||||
order_trade_no bigint,
|
||||
order_settle_id bigint,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, ledger_id)
|
||||
);
|
||||
|
||||
-- 1.4 Assistant Ledger ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_assistant_ledger (
|
||||
store_id bigint NOT NULL,
|
||||
ledger_id bigint NOT NULL,
|
||||
order_trade_no bigint,
|
||||
order_settle_id bigint,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, ledger_id)
|
||||
);
|
||||
|
||||
-- 1.5 Assistant Abolish ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_assistant_abolish (
|
||||
store_id bigint NOT NULL,
|
||||
abolish_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, abolish_id)
|
||||
);
|
||||
|
||||
-- 1.6 Goods Ledger ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_goods_ledger (
|
||||
store_id bigint NOT NULL,
|
||||
order_goods_id bigint NOT NULL,
|
||||
order_trade_no bigint,
|
||||
order_settle_id bigint,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, order_goods_id)
|
||||
);
|
||||
|
||||
-- 1.7 Payment ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_payment (
|
||||
store_id bigint NOT NULL,
|
||||
pay_id bigint NOT NULL,
|
||||
relate_type varchar(50),
|
||||
relate_id bigint,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, pay_id)
|
||||
);
|
||||
|
||||
-- 1.8 Refund ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_refund (
|
||||
store_id bigint NOT NULL,
|
||||
refund_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, refund_id)
|
||||
);
|
||||
|
||||
-- 1.9 Coupon Verify ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_coupon_verify (
|
||||
store_id bigint NOT NULL,
|
||||
coupon_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, coupon_id)
|
||||
);
|
||||
|
||||
-- 1.10 Member ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_member (
|
||||
store_id bigint NOT NULL,
|
||||
member_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, member_id)
|
||||
);
|
||||
|
||||
-- 1.11 Member Card ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_member_card (
|
||||
store_id bigint NOT NULL,
|
||||
card_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, card_id)
|
||||
);
|
||||
|
||||
-- 1.12 Package Coupon ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_package_coupon (
|
||||
store_id bigint NOT NULL,
|
||||
package_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, package_id)
|
||||
);
|
||||
|
||||
-- 1.13 Inventory Stock ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_inventory_stock (
|
||||
store_id bigint NOT NULL,
|
||||
site_goods_id bigint NOT NULL,
|
||||
snapshot_key varchar(100) NOT NULL DEFAULT 'default',
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, site_goods_id, snapshot_key)
|
||||
);
|
||||
|
||||
-- 1.14 Inventory Change ODS
|
||||
CREATE TABLE IF NOT EXISTS billiards_ods.ods_inventory_change (
|
||||
store_id bigint NOT NULL,
|
||||
change_id bigint NOT NULL,
|
||||
page_no integer,
|
||||
source_file varchar(255),
|
||||
source_endpoint varchar(255),
|
||||
fetched_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload jsonb NOT NULL,
|
||||
PRIMARY KEY (store_id, change_id)
|
||||
);
|
||||
|
||||
-- =========================
|
||||
-- 2. Billiards Dimension Tables
|
||||
-- =========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_store (
|
||||
store_id bigint PRIMARY KEY,
|
||||
store_name varchar(200),
|
||||
tenant_id bigint,
|
||||
region_code varchar(30),
|
||||
address varchar(500),
|
||||
contact_name varchar(100),
|
||||
contact_phone varchar(30),
|
||||
created_time timestamptz,
|
||||
updated_time timestamptz,
|
||||
remark text,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_assistant (
|
||||
store_id bigint NOT NULL,
|
||||
assistant_id bigint NOT NULL,
|
||||
assistant_no varchar(64),
|
||||
nickname varchar(100),
|
||||
real_name varchar(100),
|
||||
gender varchar(20),
|
||||
mobile varchar(30),
|
||||
level varchar(50),
|
||||
team_id bigint,
|
||||
team_name varchar(100),
|
||||
assistant_status varchar(30),
|
||||
work_status varchar(30),
|
||||
entry_time timestamptz,
|
||||
resign_time timestamptz,
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
create_time timestamptz,
|
||||
update_time timestamptz,
|
||||
system_role_id bigint,
|
||||
online_status varchar(30),
|
||||
allow_cx integer,
|
||||
charge_way varchar(30),
|
||||
pd_unit_price numeric(14,2),
|
||||
cx_unit_price numeric(14,2),
|
||||
is_guaranteed integer,
|
||||
is_team_leader integer,
|
||||
serial_number varchar(64),
|
||||
show_sort integer,
|
||||
is_delete integer,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, assistant_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_member (
|
||||
store_id bigint NOT NULL,
|
||||
member_id bigint NOT NULL,
|
||||
member_name varchar(100),
|
||||
phone varchar(30),
|
||||
balance numeric(18,4),
|
||||
status varchar(30),
|
||||
register_time timestamptz,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, member_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_package_coupon (
|
||||
store_id bigint NOT NULL,
|
||||
package_id bigint NOT NULL,
|
||||
package_code varchar(100),
|
||||
package_name varchar(200),
|
||||
table_area_id bigint,
|
||||
table_area_name varchar(100),
|
||||
selling_price numeric(14,2),
|
||||
duration_seconds integer,
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
type varchar(50),
|
||||
is_enabled integer,
|
||||
is_delete integer,
|
||||
usable_count integer,
|
||||
creator_name varchar(100),
|
||||
date_type varchar(50),
|
||||
group_type varchar(50),
|
||||
coupon_money numeric(14,2),
|
||||
area_tag_type varchar(50),
|
||||
system_group_type varchar(50),
|
||||
card_type_ids text,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, package_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_product (
|
||||
store_id bigint NOT NULL,
|
||||
product_id bigint NOT NULL,
|
||||
site_product_id bigint,
|
||||
product_name varchar(200) NOT NULL,
|
||||
category_id bigint,
|
||||
category_name varchar(100),
|
||||
second_category_id bigint,
|
||||
unit varchar(20),
|
||||
cost_price numeric(14,4),
|
||||
sale_price numeric(14,4),
|
||||
allow_discount boolean,
|
||||
status varchar(30),
|
||||
supplier_id bigint,
|
||||
barcode varchar(128),
|
||||
is_combo boolean,
|
||||
created_time timestamptz,
|
||||
updated_time timestamptz,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, product_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_product_price_scd (
|
||||
product_scd_id bigserial PRIMARY KEY,
|
||||
store_id bigint NOT NULL,
|
||||
product_id bigint NOT NULL,
|
||||
product_name varchar(200),
|
||||
category_id bigint,
|
||||
category_name varchar(100),
|
||||
second_category_id bigint,
|
||||
cost_price numeric(14,4),
|
||||
sale_price numeric(14,4),
|
||||
allow_discount boolean,
|
||||
status varchar(30),
|
||||
valid_from timestamptz NOT NULL DEFAULT now(),
|
||||
valid_to timestamptz,
|
||||
is_current boolean NOT NULL DEFAULT true,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
CONSTRAINT fk_dpps_product FOREIGN KEY (store_id, product_id)
|
||||
REFERENCES billiards.dim_product(store_id, product_id) ON DELETE CASCADE,
|
||||
CONSTRAINT ck_dpps_range CHECK (
|
||||
valid_from < COALESCE(valid_to, '9999-12-31 00:00:00+00'::timestamptz)
|
||||
)
|
||||
);
|
||||
|
||||
-- Create partial unique index for current records only
|
||||
CREATE UNIQUE INDEX uq_dpps_current ON billiards.dim_product_price_scd (store_id, product_id) WHERE is_current;
|
||||
|
||||
CREATE VIEW billiards.dim_product_price_current AS
|
||||
SELECT product_scd_id,
|
||||
store_id,
|
||||
product_id,
|
||||
product_name,
|
||||
category_id,
|
||||
category_name,
|
||||
second_category_id,
|
||||
cost_price,
|
||||
sale_price,
|
||||
allow_discount,
|
||||
status,
|
||||
valid_from,
|
||||
valid_to,
|
||||
raw_data
|
||||
FROM billiards.dim_product_price_scd
|
||||
WHERE is_current;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.dim_table (
|
||||
store_id bigint NOT NULL,
|
||||
table_id bigint NOT NULL,
|
||||
site_id bigint,
|
||||
area_id bigint,
|
||||
area_name varchar(100),
|
||||
table_name varchar(100) NOT NULL,
|
||||
table_price numeric(14,4),
|
||||
table_status varchar(30),
|
||||
table_status_name varchar(50),
|
||||
light_status integer,
|
||||
is_rest_area integer,
|
||||
show_status integer,
|
||||
virtual_table integer,
|
||||
charge_free integer,
|
||||
only_allow_groupon integer,
|
||||
is_online_reservation integer,
|
||||
created_time timestamptz,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, table_id)
|
||||
);
|
||||
|
||||
-- =========================
|
||||
-- 3. Billiards Fact Tables
|
||||
-- =========================
|
||||
|
||||
-- 3.1 Order Fact (Header)
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_order (
|
||||
store_id bigint NOT NULL,
|
||||
order_settle_id bigint NOT NULL, -- Settle ID is the main key for payment
|
||||
order_trade_no bigint, -- Can be one of many if merged, but usually 1-1 main
|
||||
order_no varchar(100),
|
||||
member_id bigint,
|
||||
pay_time timestamptz,
|
||||
total_amount numeric(14,4), -- Original price
|
||||
pay_amount numeric(14,4), -- Actual paid
|
||||
discount_amount numeric(14,4),
|
||||
coupon_amount numeric(14,4),
|
||||
status varchar(50),
|
||||
cashier_name varchar(100),
|
||||
remark text,
|
||||
raw_data jsonb,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
PRIMARY KEY (store_id, order_settle_id)
|
||||
);
|
||||
|
||||
-- 3.2 Order Items (Goods)
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_order_goods (
|
||||
store_id bigint NOT NULL,
|
||||
order_goods_id bigint NOT NULL, -- orderGoodsLedgerId
|
||||
order_settle_id bigint NOT NULL,
|
||||
order_trade_no bigint,
|
||||
goods_id bigint,
|
||||
goods_name varchar(200),
|
||||
quantity numeric(10,2),
|
||||
unit_price numeric(14,4),
|
||||
total_amount numeric(14,4), -- quantity * price
|
||||
pay_amount numeric(14,4), -- After discount
|
||||
created_at timestamptz DEFAULT now(),
|
||||
PRIMARY KEY (store_id, order_goods_id)
|
||||
);
|
||||
|
||||
-- 3.3 Table Usage Fact
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_table_usage (
|
||||
store_id bigint NOT NULL,
|
||||
order_ledger_id bigint NOT NULL, -- orderTableLedgerId
|
||||
order_settle_id bigint NOT NULL,
|
||||
table_id bigint,
|
||||
table_name varchar(100),
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
duration_minutes integer,
|
||||
total_amount numeric(14,4),
|
||||
pay_amount numeric(14,4),
|
||||
created_at timestamptz DEFAULT now(),
|
||||
PRIMARY KEY (store_id, order_ledger_id)
|
||||
);
|
||||
|
||||
-- 3.4 Assistant Service Fact
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_assistant_service (
|
||||
store_id bigint NOT NULL,
|
||||
ledger_id bigint NOT NULL, -- orderAssistantLedgerId
|
||||
order_settle_id bigint NOT NULL,
|
||||
assistant_id bigint,
|
||||
assistant_name varchar(100),
|
||||
service_type varchar(50), -- e.g., "Play with"
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
duration_minutes integer,
|
||||
total_amount numeric(14,4),
|
||||
pay_amount numeric(14,4),
|
||||
created_at timestamptz DEFAULT now(),
|
||||
PRIMARY KEY (store_id, ledger_id)
|
||||
);
|
||||
|
||||
-- 3.5 Payment Fact
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_payment (
|
||||
store_id bigint NOT NULL,
|
||||
pay_id bigint NOT NULL,
|
||||
site_id bigint,
|
||||
tenant_id bigint,
|
||||
order_settle_id bigint,
|
||||
order_trade_no bigint,
|
||||
relate_type varchar(50),
|
||||
relate_id bigint,
|
||||
create_time timestamptz,
|
||||
pay_time timestamptz,
|
||||
pay_amount numeric(14,4),
|
||||
fee_amount numeric(14,4),
|
||||
discount_amount numeric(14,4),
|
||||
payment_method varchar(50), -- e.g., 'WeChat', 'Cash', 'Balance'
|
||||
online_pay_channel varchar(50),
|
||||
pay_terminal varchar(30),
|
||||
pay_status varchar(30),
|
||||
raw_data jsonb,
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
PRIMARY KEY (store_id, pay_id)
|
||||
);
|
||||
|
||||
-- 3.6 Legacy/Other Facts (Preserved)
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_assistant_abolish (
|
||||
store_id bigint NOT NULL,
|
||||
abolish_id bigint NOT NULL,
|
||||
table_id bigint,
|
||||
table_name varchar(100),
|
||||
table_area_id bigint,
|
||||
table_area varchar(100),
|
||||
assistant_no varchar(64),
|
||||
assistant_name varchar(100),
|
||||
charge_minutes integer,
|
||||
abolish_amount numeric(14,4),
|
||||
create_time timestamptz,
|
||||
trash_reason text,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, abolish_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_assistant_ledger (
|
||||
store_id bigint NOT NULL,
|
||||
ledger_id bigint NOT NULL,
|
||||
assistant_no varchar(64),
|
||||
assistant_name varchar(100),
|
||||
nickname varchar(100),
|
||||
level_name varchar(50),
|
||||
table_name varchar(100),
|
||||
ledger_unit_price numeric(14,4),
|
||||
ledger_count numeric(14,4),
|
||||
ledger_amount numeric(14,4),
|
||||
projected_income numeric(14,4),
|
||||
service_money numeric(14,4),
|
||||
member_discount_amount numeric(14,4),
|
||||
manual_discount_amount numeric(14,4),
|
||||
coupon_deduct_money numeric(14,4),
|
||||
order_trade_no bigint,
|
||||
order_settle_id bigint,
|
||||
operator_id bigint,
|
||||
operator_name varchar(100),
|
||||
assistant_team_id bigint,
|
||||
assistant_level varchar(50),
|
||||
site_table_id bigint,
|
||||
order_assistant_id bigint,
|
||||
site_assistant_id bigint,
|
||||
user_id bigint,
|
||||
ledger_start_time timestamptz,
|
||||
ledger_end_time timestamptz,
|
||||
start_use_time timestamptz,
|
||||
last_use_time timestamptz,
|
||||
income_seconds integer,
|
||||
real_use_seconds integer,
|
||||
is_trash integer,
|
||||
trash_reason text,
|
||||
is_confirm integer,
|
||||
ledger_status varchar(30),
|
||||
create_time timestamptz,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, ledger_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_inventory_change (
|
||||
store_id bigint NOT NULL,
|
||||
change_id bigint NOT NULL,
|
||||
site_goods_id bigint,
|
||||
stock_type varchar(50),
|
||||
goods_name varchar(200),
|
||||
change_time timestamptz,
|
||||
start_qty numeric(18,4),
|
||||
end_qty numeric(18,4),
|
||||
change_qty numeric(18,4),
|
||||
unit varchar(20),
|
||||
price numeric(14,4),
|
||||
operator_name varchar(100),
|
||||
remark text,
|
||||
goods_category_id bigint,
|
||||
goods_second_category_id bigint,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, change_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billiards.fact_refund (
|
||||
store_id bigint NOT NULL,
|
||||
refund_id bigint NOT NULL,
|
||||
site_id bigint,
|
||||
tenant_id bigint,
|
||||
pay_amount numeric(14,4),
|
||||
pay_status varchar(30),
|
||||
pay_time timestamptz,
|
||||
create_time timestamptz,
|
||||
relate_type varchar(50),
|
||||
relate_id bigint,
|
||||
payment_method varchar(50),
|
||||
refund_amount numeric(14,4),
|
||||
refund_reason text,
|
||||
raw_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (store_id, refund_id)
|
||||
);
|
||||
|
||||
-- =========================
|
||||
-- 4. DWS Layer (Data Warehouse Summary)
|
||||
-- Views for Analysis & AI
|
||||
-- =========================
|
||||
|
||||
-- 4.1 Sales Detail View (The "AI-Friendly" Wide Table)
|
||||
-- Unifies Goods, Table Fees, and Assistant Services into a single stream of "Sales Items"
|
||||
CREATE OR REPLACE VIEW billiards_dws.dws_sales_detail AS
|
||||
SELECT
|
||||
'GOODS' as item_type,
|
||||
g.store_id,
|
||||
g.order_settle_id,
|
||||
o.pay_time,
|
||||
g.goods_name as item_name,
|
||||
g.quantity,
|
||||
g.pay_amount as amount,
|
||||
o.cashier_name
|
||||
FROM billiards.fact_order_goods g
|
||||
JOIN billiards.fact_order o ON g.store_id = o.store_id AND g.order_settle_id = o.order_settle_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'TABLE' as item_type,
|
||||
t.store_id,
|
||||
t.order_settle_id,
|
||||
o.pay_time,
|
||||
t.table_name || ' (' || t.duration_minutes || ' mins)' as item_name,
|
||||
1 as quantity,
|
||||
t.pay_amount as amount,
|
||||
o.cashier_name
|
||||
FROM billiards.fact_table_usage t
|
||||
JOIN billiards.fact_order o ON t.store_id = o.store_id AND t.order_settle_id = o.order_settle_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'ASSISTANT' as item_type,
|
||||
a.store_id,
|
||||
a.order_settle_id,
|
||||
o.pay_time,
|
||||
a.assistant_name || ' (' || a.duration_minutes || ' mins)' as item_name,
|
||||
1 as quantity,
|
||||
a.pay_amount as amount,
|
||||
o.cashier_name
|
||||
FROM billiards.fact_assistant_service a
|
||||
JOIN billiards.fact_order o ON a.store_id = o.store_id AND a.order_settle_id = o.order_settle_id;
|
||||
|
||||
-- 4.2 Daily Revenue View
|
||||
CREATE OR REPLACE VIEW billiards_dws.dws_daily_revenue AS
|
||||
SELECT
|
||||
store_id,
|
||||
DATE(pay_time) as report_date,
|
||||
COUNT(DISTINCT order_settle_id) as total_orders,
|
||||
SUM(amount) as total_revenue,
|
||||
SUM(amount) FILTER (WHERE item_type = 'GOODS') as goods_revenue,
|
||||
SUM(amount) FILTER (WHERE item_type = 'TABLE') as table_revenue,
|
||||
SUM(amount) FILTER (WHERE item_type = 'ASSISTANT') as assistant_revenue
|
||||
FROM billiards_dws.dws_sales_detail
|
||||
GROUP BY store_id, DATE(pay_time);
|
||||
|
||||
-- 4.3 Order Detail Wide View (For detailed inspection)
|
||||
CREATE OR REPLACE VIEW billiards_dws.dws_order_detail AS
|
||||
SELECT
|
||||
o.store_id,
|
||||
o.order_settle_id,
|
||||
o.order_no,
|
||||
o.pay_time,
|
||||
o.total_amount,
|
||||
o.pay_amount,
|
||||
o.cashier_name,
|
||||
-- Payment pivot (approximate, assumes simple mapping)
|
||||
COALESCE(SUM(p.pay_amount) FILTER (WHERE p.payment_method = '1'), 0) AS pay_cash,
|
||||
COALESCE(SUM(p.pay_amount) FILTER (WHERE p.payment_method = '2'), 0) AS pay_balance,
|
||||
COALESCE(SUM(p.pay_amount) FILTER (WHERE p.payment_method = '4'), 0) AS pay_wechat,
|
||||
-- Content summary
|
||||
(SELECT string_agg(goods_name || 'x' || quantity, '; ') FROM billiards.fact_order_goods WHERE order_settle_id = o.order_settle_id) AS goods_summary,
|
||||
(SELECT string_agg(assistant_name, '; ') FROM billiards.fact_assistant_service WHERE order_settle_id = o.order_settle_id) AS assistant_summary
|
||||
FROM billiards.fact_order o
|
||||
LEFT JOIN billiards.fact_payment p ON o.order_settle_id = p.order_settle_id
|
||||
GROUP BY o.store_id, o.order_settle_id, o.order_no, o.pay_time, o.total_amount, o.pay_amount, o.cashier_name;
|
||||
0
etl_billiards/scripts/Temp1.py
Normal file
0
etl_billiards/scripts/Temp1.py
Normal file
76
etl_billiards/scripts/bootstrap_schema.py
Normal file
76
etl_billiards/scripts/bootstrap_schema.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Apply the PRD-aligned warehouse schema (ODS/DWD/DWS) to PostgreSQL."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from database.connection import DatabaseConnection # noqa: E402
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create/upgrade warehouse schemas using schema_v2.sql"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dsn",
|
||||
help="PostgreSQL DSN (fallback to PG_DSN env)",
|
||||
default=os.environ.get("PG_DSN"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
help="Path to schema SQL",
|
||||
default=str(PROJECT_ROOT / "database" / "schema_v2.sql"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=int(os.environ.get("PG_CONNECT_TIMEOUT", 10) or 10),
|
||||
help="connect_timeout seconds (capped at 20, default 10)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def apply_schema(dsn: str, sql_path: Path, timeout: int) -> None:
|
||||
if not sql_path.exists():
|
||||
raise FileNotFoundError(f"Schema file not found: {sql_path}")
|
||||
|
||||
sql_text = sql_path.read_text(encoding="utf-8")
|
||||
timeout_val = max(1, min(timeout, 20))
|
||||
|
||||
conn = DatabaseConnection(dsn, connect_timeout=timeout_val)
|
||||
try:
|
||||
with conn.conn.cursor() as cur:
|
||||
cur.execute(sql_text)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if not args.dsn:
|
||||
print("Missing DSN. Set PG_DSN or pass --dsn.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
apply_schema(args.dsn, Path(args.file), args.timeout)
|
||||
except Exception as exc: # pragma: no cover - utility script
|
||||
print(f"Schema apply failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("Schema applied successfully.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
425
etl_billiards/scripts/build_dwd_from_ods.py
Normal file
425
etl_billiards/scripts/build_dwd_from_ods.py
Normal file
@@ -0,0 +1,425 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Populate PRD DWD tables from ODS payload snapshots."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import psycopg2
|
||||
|
||||
|
||||
SQL_STEPS: list[tuple[str, str]] = [
|
||||
(
|
||||
"dim_tenant",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_tenant (tenant_id, tenant_name, status)
|
||||
SELECT DISTINCT tenant_id, 'default' AS tenant_name, 'active' AS status
|
||||
FROM (
|
||||
SELECT tenant_id FROM billiards_ods.ods_order_settle
|
||||
UNION SELECT tenant_id FROM billiards_ods.ods_order_receipt_detail
|
||||
UNION SELECT tenant_id FROM billiards_ods.ods_member_profile
|
||||
) s
|
||||
WHERE tenant_id IS NOT NULL
|
||||
ON CONFLICT (tenant_id) DO UPDATE SET updated_at = now();
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_site",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_site (site_id, tenant_id, site_name, status)
|
||||
SELECT DISTINCT site_id, MAX(tenant_id) AS tenant_id, 'default' AS site_name, 'active' AS status
|
||||
FROM (
|
||||
SELECT site_id, tenant_id FROM billiards_ods.ods_order_settle
|
||||
UNION SELECT site_id, tenant_id FROM billiards_ods.ods_order_receipt_detail
|
||||
UNION SELECT site_id, tenant_id FROM billiards_ods.ods_table_info
|
||||
) s
|
||||
WHERE site_id IS NOT NULL
|
||||
GROUP BY site_id
|
||||
ON CONFLICT (site_id) DO UPDATE SET updated_at = now();
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_product_category",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_product_category (category_id, category_name, parent_id, level_no, status)
|
||||
SELECT DISTINCT category_id, category_name, parent_id, level_no, status
|
||||
FROM billiards_ods.ods_goods_category
|
||||
WHERE category_id IS NOT NULL
|
||||
ON CONFLICT (category_id) DO UPDATE SET
|
||||
category_name = EXCLUDED.category_name,
|
||||
parent_id = EXCLUDED.parent_id,
|
||||
level_no = EXCLUDED.level_no,
|
||||
status = EXCLUDED.status;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_product",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_product (goods_id, goods_name, goods_code, category_id, category_name, unit, default_price, status)
|
||||
SELECT DISTINCT goods_id, goods_name, NULL::TEXT AS goods_code, category_id, category_name, NULL::TEXT AS unit, sale_price AS default_price, status
|
||||
FROM billiards_ods.ods_store_product
|
||||
WHERE goods_id IS NOT NULL
|
||||
ON CONFLICT (goods_id) DO UPDATE SET
|
||||
goods_name = EXCLUDED.goods_name,
|
||||
category_id = EXCLUDED.category_id,
|
||||
category_name = EXCLUDED.category_name,
|
||||
default_price = EXCLUDED.default_price,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = now();
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_product_from_sales",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_product (goods_id, goods_name)
|
||||
SELECT DISTINCT goods_id, goods_name
|
||||
FROM billiards_ods.ods_store_sale_item
|
||||
WHERE goods_id IS NOT NULL
|
||||
ON CONFLICT (goods_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_member_card_type",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_member_card_type (card_type_id, card_type_name, discount_rate)
|
||||
SELECT DISTINCT card_type_id, card_type_name, discount_rate
|
||||
FROM billiards_ods.ods_member_card
|
||||
WHERE card_type_id IS NOT NULL
|
||||
ON CONFLICT (card_type_id) DO UPDATE SET
|
||||
card_type_name = EXCLUDED.card_type_name,
|
||||
discount_rate = EXCLUDED.discount_rate;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_member",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_member (
|
||||
site_id, member_id, tenant_id, member_name, nickname, gender, birthday, mobile,
|
||||
member_type_id, member_type_name, status, register_time, last_visit_time,
|
||||
balance, total_recharge_amount, total_consumed_amount, wechat_id, alipay_id, remark
|
||||
)
|
||||
SELECT DISTINCT
|
||||
prof.site_id,
|
||||
prof.member_id,
|
||||
prof.tenant_id,
|
||||
prof.member_name,
|
||||
prof.nickname,
|
||||
prof.gender,
|
||||
prof.birthday,
|
||||
prof.mobile,
|
||||
card.member_type_id,
|
||||
card.member_type_name,
|
||||
prof.status,
|
||||
prof.register_time,
|
||||
prof.last_visit_time,
|
||||
prof.balance,
|
||||
NULL::NUMERIC AS total_recharge_amount,
|
||||
NULL::NUMERIC AS total_consumed_amount,
|
||||
prof.wechat_id,
|
||||
prof.alipay_id,
|
||||
prof.remarks
|
||||
FROM billiards_ods.ods_member_profile prof
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT site_id, member_id, card_type_id AS member_type_id, card_type_name AS member_type_name
|
||||
FROM billiards_ods.ods_member_card
|
||||
) card
|
||||
ON prof.site_id = card.site_id AND prof.member_id = card.member_id
|
||||
WHERE prof.member_id IS NOT NULL
|
||||
ON CONFLICT (site_id, member_id) DO UPDATE SET
|
||||
member_name = EXCLUDED.member_name,
|
||||
nickname = EXCLUDED.nickname,
|
||||
gender = EXCLUDED.gender,
|
||||
birthday = EXCLUDED.birthday,
|
||||
mobile = EXCLUDED.mobile,
|
||||
member_type_id = EXCLUDED.member_type_id,
|
||||
member_type_name = EXCLUDED.member_type_name,
|
||||
status = EXCLUDED.status,
|
||||
register_time = EXCLUDED.register_time,
|
||||
last_visit_time = EXCLUDED.last_visit_time,
|
||||
balance = EXCLUDED.balance,
|
||||
wechat_id = EXCLUDED.wechat_id,
|
||||
alipay_id = EXCLUDED.alipay_id,
|
||||
remark = EXCLUDED.remark,
|
||||
updated_at = now();
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_table",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_table (table_id, site_id, table_code, table_name, table_type, area_name, status, created_time, updated_time)
|
||||
SELECT DISTINCT table_id, site_id, table_code, table_name, table_type, area_name, status, created_time, updated_time
|
||||
FROM billiards_ods.ods_table_info
|
||||
WHERE table_id IS NOT NULL
|
||||
ON CONFLICT (table_id) DO UPDATE SET
|
||||
site_id = EXCLUDED.site_id,
|
||||
table_code = EXCLUDED.table_code,
|
||||
table_name = EXCLUDED.table_name,
|
||||
table_type = EXCLUDED.table_type,
|
||||
area_name = EXCLUDED.area_name,
|
||||
status = EXCLUDED.status,
|
||||
created_time = EXCLUDED.created_time,
|
||||
updated_time = EXCLUDED.updated_time;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_assistant",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_assistant (assistant_id, assistant_name, mobile, status)
|
||||
SELECT DISTINCT assistant_id, assistant_name, mobile, status
|
||||
FROM billiards_ods.ods_assistant_account
|
||||
WHERE assistant_id IS NOT NULL
|
||||
ON CONFLICT (assistant_id) DO UPDATE SET
|
||||
assistant_name = EXCLUDED.assistant_name,
|
||||
mobile = EXCLUDED.mobile,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = now();
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_pay_method",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_pay_method (pay_method_code, pay_method_name, is_stored_value, status)
|
||||
SELECT DISTINCT pay_method_code, pay_method_name, FALSE AS is_stored_value, 'active' AS status
|
||||
FROM billiards_ods.ods_payment_record
|
||||
WHERE pay_method_code IS NOT NULL
|
||||
ON CONFLICT (pay_method_code) DO UPDATE SET
|
||||
pay_method_name = EXCLUDED.pay_method_name,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = now();
|
||||
""",
|
||||
),
|
||||
(
|
||||
"dim_coupon_platform",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.dim_coupon_platform (platform_code, platform_name)
|
||||
SELECT DISTINCT platform_code, platform_code AS platform_name
|
||||
FROM billiards_ods.ods_platform_coupon_log
|
||||
WHERE platform_code IS NOT NULL
|
||||
ON CONFLICT (platform_code) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_sale_item",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_sale_item (
|
||||
site_id, sale_item_id, order_trade_no, order_settle_id, member_id,
|
||||
goods_id, category_id, quantity, original_amount, discount_amount,
|
||||
final_amount, is_gift, sale_time
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
sale_item_id,
|
||||
order_trade_no,
|
||||
order_settle_id,
|
||||
NULL::BIGINT AS member_id,
|
||||
goods_id,
|
||||
category_id,
|
||||
quantity,
|
||||
original_amount,
|
||||
discount_amount,
|
||||
final_amount,
|
||||
COALESCE(is_gift, FALSE),
|
||||
sale_time
|
||||
FROM billiards_ods.ods_store_sale_item
|
||||
ON CONFLICT (site_id, sale_item_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_table_usage",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_table_usage (
|
||||
site_id, ledger_id, order_trade_no, order_settle_id, table_id,
|
||||
member_id, start_time, end_time, duration_minutes,
|
||||
original_table_fee, member_discount_amount, manual_discount_amount,
|
||||
final_table_fee, is_canceled, cancel_time
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
ledger_id,
|
||||
order_trade_no,
|
||||
order_settle_id,
|
||||
table_id,
|
||||
member_id,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_minutes,
|
||||
original_table_fee,
|
||||
0::NUMERIC AS member_discount_amount,
|
||||
discount_amount AS manual_discount_amount,
|
||||
final_table_fee,
|
||||
FALSE AS is_canceled,
|
||||
NULL::TIMESTAMPTZ AS cancel_time
|
||||
FROM billiards_ods.ods_table_use_log
|
||||
ON CONFLICT (site_id, ledger_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_assistant_service",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_assistant_service (
|
||||
site_id, ledger_id, order_trade_no, order_settle_id, assistant_id,
|
||||
assist_type_code, member_id, start_time, end_time, duration_minutes,
|
||||
original_fee, member_discount_amount, manual_discount_amount,
|
||||
final_fee, is_canceled, cancel_time
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
ledger_id,
|
||||
order_trade_no,
|
||||
order_settle_id,
|
||||
assistant_id,
|
||||
NULL::TEXT AS assist_type_code,
|
||||
member_id,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_minutes,
|
||||
original_fee,
|
||||
0::NUMERIC AS member_discount_amount,
|
||||
discount_amount AS manual_discount_amount,
|
||||
final_fee,
|
||||
FALSE AS is_canceled,
|
||||
NULL::TIMESTAMPTZ AS cancel_time
|
||||
FROM billiards_ods.ods_assistant_service_log
|
||||
ON CONFLICT (site_id, ledger_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_coupon_usage",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_coupon_usage (
|
||||
site_id, coupon_id, package_id, order_trade_no, order_settle_id,
|
||||
member_id, platform_code, status, deduct_amount, settle_price, used_time
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
coupon_id,
|
||||
NULL::BIGINT AS package_id,
|
||||
order_trade_no,
|
||||
order_settle_id,
|
||||
member_id,
|
||||
platform_code,
|
||||
status,
|
||||
deduct_amount,
|
||||
settle_price,
|
||||
used_time
|
||||
FROM billiards_ods.ods_platform_coupon_log
|
||||
ON CONFLICT (site_id, coupon_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_payment",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_payment (
|
||||
site_id, pay_id, order_trade_no, order_settle_id, member_id,
|
||||
pay_method_code, pay_amount, pay_time, relate_type, relate_id
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
pay_id,
|
||||
order_trade_no,
|
||||
order_settle_id,
|
||||
member_id,
|
||||
pay_method_code,
|
||||
pay_amount,
|
||||
pay_time,
|
||||
relate_type,
|
||||
relate_id
|
||||
FROM billiards_ods.ods_payment_record
|
||||
ON CONFLICT (site_id, pay_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_refund",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_refund (
|
||||
site_id, refund_id, order_trade_no, order_settle_id, member_id,
|
||||
pay_method_code, refund_amount, refund_time, status
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
refund_id,
|
||||
order_trade_no,
|
||||
order_settle_id,
|
||||
member_id,
|
||||
pay_method_code,
|
||||
refund_amount,
|
||||
refund_time,
|
||||
status
|
||||
FROM billiards_ods.ods_refund_record
|
||||
ON CONFLICT (site_id, refund_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
(
|
||||
"fact_balance_change",
|
||||
"""
|
||||
INSERT INTO billiards_dwd.fact_balance_change (
|
||||
site_id, change_id, member_id, change_type, relate_type, relate_id,
|
||||
pay_method_code, change_amount, balance_before, balance_after, change_time
|
||||
)
|
||||
SELECT
|
||||
site_id,
|
||||
change_id,
|
||||
member_id,
|
||||
change_type,
|
||||
NULL::TEXT AS relate_type,
|
||||
relate_id,
|
||||
NULL::TEXT AS pay_method_code,
|
||||
change_amount,
|
||||
balance_before,
|
||||
balance_after,
|
||||
change_time
|
||||
FROM billiards_ods.ods_balance_change
|
||||
ON CONFLICT (site_id, change_id) DO NOTHING;
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Build DWD tables from ODS payloads (PRD schema).")
|
||||
parser.add_argument(
|
||||
"--dsn",
|
||||
default=os.environ.get("PG_DSN"),
|
||||
help="PostgreSQL DSN (fallback PG_DSN env)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=int(os.environ.get("PG_CONNECT_TIMEOUT", 10) or 10),
|
||||
help="connect_timeout seconds (capped at 20, default 10)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if not args.dsn:
|
||||
print("Missing DSN. Use --dsn or PG_DSN.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
timeout_val = max(1, min(args.timeout, 20))
|
||||
conn = psycopg2.connect(args.dsn, connect_timeout=timeout_val)
|
||||
conn.autocommit = False
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
for name, sql in SQL_STEPS:
|
||||
cur.execute(sql)
|
||||
print(f"[OK] {name}")
|
||||
conn.commit()
|
||||
except Exception as exc: # pragma: no cover - operational script
|
||||
conn.rollback()
|
||||
print(f"[FAIL] {exc}", file=sys.stderr)
|
||||
return 1
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("DWD build complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
322
etl_billiards/scripts/build_dws_order_summary.py
Normal file
322
etl_billiards/scripts/build_dws_order_summary.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Recompute billiards_dws.dws_order_summary from DWD fact tables."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from database.connection import DatabaseConnection # noqa: E402
|
||||
|
||||
|
||||
SQL_BUILD_SUMMARY = r"""
|
||||
WITH table_fee AS (
|
||||
SELECT
|
||||
site_id,
|
||||
order_settle_id,
|
||||
order_trade_no,
|
||||
MIN(member_id) AS member_id,
|
||||
SUM(COALESCE(final_table_fee, 0)) AS table_fee_amount,
|
||||
SUM(COALESCE(member_discount_amount, 0)) AS member_discount_amount,
|
||||
SUM(COALESCE(manual_discount_amount, 0)) AS manual_discount_amount,
|
||||
SUM(COALESCE(original_table_fee, 0)) AS original_table_fee,
|
||||
MIN(start_time) AS first_time
|
||||
FROM billiards_dwd.fact_table_usage
|
||||
WHERE (%(site_id)s IS NULL OR site_id = %(site_id)s)
|
||||
AND (%(start_date)s IS NULL OR start_time::date >= %(start_date)s)
|
||||
AND (%(end_date)s IS NULL OR start_time::date <= %(end_date)s)
|
||||
AND COALESCE(is_canceled, FALSE) = FALSE
|
||||
GROUP BY site_id, order_settle_id, order_trade_no
|
||||
),
|
||||
assistant_fee AS (
|
||||
SELECT
|
||||
site_id,
|
||||
order_settle_id,
|
||||
order_trade_no,
|
||||
MIN(member_id) AS member_id,
|
||||
SUM(COALESCE(final_fee, 0)) AS assistant_service_amount,
|
||||
SUM(COALESCE(member_discount_amount, 0)) AS member_discount_amount,
|
||||
SUM(COALESCE(manual_discount_amount, 0)) AS manual_discount_amount,
|
||||
SUM(COALESCE(original_fee, 0)) AS original_fee,
|
||||
MIN(start_time) AS first_time
|
||||
FROM billiards_dwd.fact_assistant_service
|
||||
WHERE (%(site_id)s IS NULL OR site_id = %(site_id)s)
|
||||
AND (%(start_date)s IS NULL OR start_time::date >= %(start_date)s)
|
||||
AND (%(end_date)s IS NULL OR start_time::date <= %(end_date)s)
|
||||
AND COALESCE(is_canceled, FALSE) = FALSE
|
||||
GROUP BY site_id, order_settle_id, order_trade_no
|
||||
),
|
||||
goods_fee AS (
|
||||
SELECT
|
||||
site_id,
|
||||
order_settle_id,
|
||||
order_trade_no,
|
||||
MIN(member_id) AS member_id,
|
||||
SUM(COALESCE(final_amount, 0)) FILTER (WHERE COALESCE(is_gift, FALSE) = FALSE) AS goods_amount,
|
||||
SUM(COALESCE(discount_amount, 0)) FILTER (WHERE COALESCE(is_gift, FALSE) = FALSE) AS goods_discount_amount,
|
||||
SUM(COALESCE(original_amount, 0)) FILTER (WHERE COALESCE(is_gift, FALSE) = FALSE) AS goods_original_amount,
|
||||
COUNT(*) FILTER (WHERE COALESCE(is_gift, FALSE) = FALSE) AS item_count,
|
||||
SUM(COALESCE(quantity, 0)) FILTER (WHERE COALESCE(is_gift, FALSE) = FALSE) AS total_item_quantity,
|
||||
MIN(sale_time) AS first_time
|
||||
FROM billiards_dwd.fact_sale_item
|
||||
WHERE (%(site_id)s IS NULL OR site_id = %(site_id)s)
|
||||
AND (%(start_date)s IS NULL OR sale_time::date >= %(start_date)s)
|
||||
AND (%(end_date)s IS NULL OR sale_time::date <= %(end_date)s)
|
||||
GROUP BY site_id, order_settle_id, order_trade_no
|
||||
),
|
||||
coupon_usage AS (
|
||||
SELECT
|
||||
site_id,
|
||||
order_settle_id,
|
||||
order_trade_no,
|
||||
MIN(member_id) AS member_id,
|
||||
SUM(COALESCE(deduct_amount, 0)) AS coupon_deduction,
|
||||
SUM(COALESCE(settle_price, 0)) AS settle_price,
|
||||
MIN(used_time) AS first_time
|
||||
FROM billiards_dwd.fact_coupon_usage
|
||||
WHERE (%(site_id)s IS NULL OR site_id = %(site_id)s)
|
||||
AND (%(start_date)s IS NULL OR used_time::date >= %(start_date)s)
|
||||
AND (%(end_date)s IS NULL OR used_time::date <= %(end_date)s)
|
||||
GROUP BY site_id, order_settle_id, order_trade_no
|
||||
),
|
||||
payments AS (
|
||||
SELECT
|
||||
fp.site_id,
|
||||
fp.order_settle_id,
|
||||
fp.order_trade_no,
|
||||
MIN(fp.member_id) AS member_id,
|
||||
SUM(COALESCE(fp.pay_amount, 0)) AS total_paid_amount,
|
||||
SUM(COALESCE(fp.pay_amount, 0)) FILTER (WHERE COALESCE(pm.is_stored_value, FALSE)) AS stored_card_deduct,
|
||||
SUM(COALESCE(fp.pay_amount, 0)) FILTER (WHERE NOT COALESCE(pm.is_stored_value, FALSE)) AS external_paid_amount,
|
||||
MIN(fp.pay_time) AS first_time
|
||||
FROM billiards_dwd.fact_payment fp
|
||||
LEFT JOIN billiards_dwd.dim_pay_method pm ON fp.pay_method_code = pm.pay_method_code
|
||||
WHERE (%(site_id)s IS NULL OR fp.site_id = %(site_id)s)
|
||||
AND (%(start_date)s IS NULL OR fp.pay_time::date >= %(start_date)s)
|
||||
AND (%(end_date)s IS NULL OR fp.pay_time::date <= %(end_date)s)
|
||||
GROUP BY fp.site_id, fp.order_settle_id, fp.order_trade_no
|
||||
),
|
||||
refunds AS (
|
||||
SELECT
|
||||
site_id,
|
||||
order_settle_id,
|
||||
order_trade_no,
|
||||
SUM(COALESCE(refund_amount, 0)) AS refund_amount
|
||||
FROM billiards_dwd.fact_refund
|
||||
WHERE (%(site_id)s IS NULL OR site_id = %(site_id)s)
|
||||
AND (%(start_date)s IS NULL OR refund_time::date >= %(start_date)s)
|
||||
AND (%(end_date)s IS NULL OR refund_time::date <= %(end_date)s)
|
||||
GROUP BY site_id, order_settle_id, order_trade_no
|
||||
),
|
||||
combined_ids AS (
|
||||
SELECT site_id, order_settle_id, order_trade_no FROM table_fee
|
||||
UNION
|
||||
SELECT site_id, order_settle_id, order_trade_no FROM assistant_fee
|
||||
UNION
|
||||
SELECT site_id, order_settle_id, order_trade_no FROM goods_fee
|
||||
UNION
|
||||
SELECT site_id, order_settle_id, order_trade_no FROM coupon_usage
|
||||
UNION
|
||||
SELECT site_id, order_settle_id, order_trade_no FROM payments
|
||||
UNION
|
||||
SELECT site_id, order_settle_id, order_trade_no FROM refunds
|
||||
),
|
||||
site_dim AS (
|
||||
SELECT site_id, tenant_id FROM billiards_dwd.dim_site
|
||||
)
|
||||
INSERT INTO billiards_dws.dws_order_summary (
|
||||
site_id,
|
||||
order_settle_id,
|
||||
order_trade_no,
|
||||
order_date,
|
||||
tenant_id,
|
||||
member_id,
|
||||
member_flag,
|
||||
recharge_order_flag,
|
||||
item_count,
|
||||
total_item_quantity,
|
||||
table_fee_amount,
|
||||
assistant_service_amount,
|
||||
goods_amount,
|
||||
group_amount,
|
||||
total_coupon_deduction,
|
||||
member_discount_amount,
|
||||
manual_discount_amount,
|
||||
order_original_amount,
|
||||
order_final_amount,
|
||||
stored_card_deduct,
|
||||
external_paid_amount,
|
||||
total_paid_amount,
|
||||
book_table_flow,
|
||||
book_assistant_flow,
|
||||
book_goods_flow,
|
||||
book_group_flow,
|
||||
book_order_flow,
|
||||
order_effective_consume_cash,
|
||||
order_effective_recharge_cash,
|
||||
order_effective_flow,
|
||||
refund_amount,
|
||||
net_income,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
c.site_id,
|
||||
c.order_settle_id,
|
||||
c.order_trade_no,
|
||||
COALESCE(tf.first_time, af.first_time, gf.first_time, pay.first_time, cu.first_time)::date AS order_date,
|
||||
sd.tenant_id,
|
||||
COALESCE(tf.member_id, af.member_id, gf.member_id, cu.member_id, pay.member_id) AS member_id,
|
||||
COALESCE(tf.member_id, af.member_id, gf.member_id, cu.member_id, pay.member_id) IS NOT NULL AS member_flag,
|
||||
-- recharge flag: no consumption side but has payments
|
||||
(COALESCE(tf.table_fee_amount, 0) + COALESCE(af.assistant_service_amount, 0) + COALESCE(gf.goods_amount, 0) + COALESCE(cu.settle_price, 0) = 0)
|
||||
AND COALESCE(pay.total_paid_amount, 0) > 0 AS recharge_order_flag,
|
||||
COALESCE(gf.item_count, 0) AS item_count,
|
||||
COALESCE(gf.total_item_quantity, 0) AS total_item_quantity,
|
||||
COALESCE(tf.table_fee_amount, 0) AS table_fee_amount,
|
||||
COALESCE(af.assistant_service_amount, 0) AS assistant_service_amount,
|
||||
COALESCE(gf.goods_amount, 0) AS goods_amount,
|
||||
COALESCE(cu.settle_price, 0) AS group_amount,
|
||||
COALESCE(cu.coupon_deduction, 0) AS total_coupon_deduction,
|
||||
COALESCE(tf.member_discount_amount, 0) + COALESCE(af.member_discount_amount, 0) + COALESCE(gf.goods_discount_amount, 0) AS member_discount_amount,
|
||||
COALESCE(tf.manual_discount_amount, 0) + COALESCE(af.manual_discount_amount, 0) AS manual_discount_amount,
|
||||
COALESCE(tf.original_table_fee, 0) + COALESCE(af.original_fee, 0) + COALESCE(gf.goods_original_amount, 0) AS order_original_amount,
|
||||
COALESCE(tf.table_fee_amount, 0) + COALESCE(af.assistant_service_amount, 0) + COALESCE(gf.goods_amount, 0) + COALESCE(cu.settle_price, 0) - COALESCE(cu.coupon_deduction, 0) AS order_final_amount,
|
||||
COALESCE(pay.stored_card_deduct, 0) AS stored_card_deduct,
|
||||
COALESCE(pay.external_paid_amount, 0) AS external_paid_amount,
|
||||
COALESCE(pay.total_paid_amount, 0) AS total_paid_amount,
|
||||
COALESCE(tf.table_fee_amount, 0) AS book_table_flow,
|
||||
COALESCE(af.assistant_service_amount, 0) AS book_assistant_flow,
|
||||
COALESCE(gf.goods_amount, 0) AS book_goods_flow,
|
||||
COALESCE(cu.settle_price, 0) AS book_group_flow,
|
||||
COALESCE(tf.table_fee_amount, 0) + COALESCE(af.assistant_service_amount, 0) + COALESCE(gf.goods_amount, 0) + COALESCE(cu.settle_price, 0) AS book_order_flow,
|
||||
CASE
|
||||
WHEN (COALESCE(tf.table_fee_amount, 0) + COALESCE(af.assistant_service_amount, 0) + COALESCE(gf.goods_amount, 0) + COALESCE(cu.settle_price, 0) = 0)
|
||||
THEN 0
|
||||
ELSE COALESCE(pay.external_paid_amount, 0)
|
||||
END AS order_effective_consume_cash,
|
||||
CASE
|
||||
WHEN (COALESCE(tf.table_fee_amount, 0) + COALESCE(af.assistant_service_amount, 0) + COALESCE(gf.goods_amount, 0) + COALESCE(cu.settle_price, 0) = 0)
|
||||
THEN COALESCE(pay.external_paid_amount, 0)
|
||||
ELSE 0
|
||||
END AS order_effective_recharge_cash,
|
||||
COALESCE(pay.external_paid_amount, 0) + COALESCE(cu.settle_price, 0) AS order_effective_flow,
|
||||
COALESCE(rf.refund_amount, 0) AS refund_amount,
|
||||
(COALESCE(pay.external_paid_amount, 0) + COALESCE(cu.settle_price, 0)) - COALESCE(rf.refund_amount, 0) AS net_income,
|
||||
now() AS created_at,
|
||||
now() AS updated_at
|
||||
FROM combined_ids c
|
||||
LEFT JOIN table_fee tf ON c.site_id = tf.site_id AND c.order_settle_id = tf.order_settle_id
|
||||
LEFT JOIN assistant_fee af ON c.site_id = af.site_id AND c.order_settle_id = af.order_settle_id
|
||||
LEFT JOIN goods_fee gf ON c.site_id = gf.site_id AND c.order_settle_id = gf.order_settle_id
|
||||
LEFT JOIN coupon_usage cu ON c.site_id = cu.site_id AND c.order_settle_id = cu.order_settle_id
|
||||
LEFT JOIN payments pay ON c.site_id = pay.site_id AND c.order_settle_id = pay.order_settle_id
|
||||
LEFT JOIN refunds rf ON c.site_id = rf.site_id AND c.order_settle_id = rf.order_settle_id
|
||||
LEFT JOIN site_dim sd ON c.site_id = sd.site_id
|
||||
ON CONFLICT (site_id, order_settle_id) DO UPDATE SET
|
||||
order_trade_no = EXCLUDED.order_trade_no,
|
||||
order_date = EXCLUDED.order_date,
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
member_id = EXCLUDED.member_id,
|
||||
member_flag = EXCLUDED.member_flag,
|
||||
recharge_order_flag = EXCLUDED.recharge_order_flag,
|
||||
item_count = EXCLUDED.item_count,
|
||||
total_item_quantity = EXCLUDED.total_item_quantity,
|
||||
table_fee_amount = EXCLUDED.table_fee_amount,
|
||||
assistant_service_amount = EXCLUDED.assistant_service_amount,
|
||||
goods_amount = EXCLUDED.goods_amount,
|
||||
group_amount = EXCLUDED.group_amount,
|
||||
total_coupon_deduction = EXCLUDED.total_coupon_deduction,
|
||||
member_discount_amount = EXCLUDED.member_discount_amount,
|
||||
manual_discount_amount = EXCLUDED.manual_discount_amount,
|
||||
order_original_amount = EXCLUDED.order_original_amount,
|
||||
order_final_amount = EXCLUDED.order_final_amount,
|
||||
stored_card_deduct = EXCLUDED.stored_card_deduct,
|
||||
external_paid_amount = EXCLUDED.external_paid_amount,
|
||||
total_paid_amount = EXCLUDED.total_paid_amount,
|
||||
book_table_flow = EXCLUDED.book_table_flow,
|
||||
book_assistant_flow = EXCLUDED.book_assistant_flow,
|
||||
book_goods_flow = EXCLUDED.book_goods_flow,
|
||||
book_group_flow = EXCLUDED.book_group_flow,
|
||||
book_order_flow = EXCLUDED.book_order_flow,
|
||||
order_effective_consume_cash = EXCLUDED.order_effective_consume_cash,
|
||||
order_effective_recharge_cash = EXCLUDED.order_effective_recharge_cash,
|
||||
order_effective_flow = EXCLUDED.order_effective_flow,
|
||||
refund_amount = EXCLUDED.refund_amount,
|
||||
net_income = EXCLUDED.net_income,
|
||||
updated_at = now();
|
||||
"""
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build/update dws_order_summary from DWD fact tables."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dsn",
|
||||
default=os.environ.get("PG_DSN"),
|
||||
help="PostgreSQL DSN (fallback: PG_DSN env)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--site-id",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Filter by site_id (optional, default all sites)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-date",
|
||||
dest="start_date",
|
||||
default=None,
|
||||
help="Filter facts from this date (YYYY-MM-DD, optional)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--end-date",
|
||||
dest="end_date",
|
||||
default=None,
|
||||
help="Filter facts until this date (YYYY-MM-DD, optional)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=int(os.environ.get("PG_CONNECT_TIMEOUT", 10) or 10),
|
||||
help="connect_timeout seconds (capped at 20, default 10)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if not args.dsn:
|
||||
print("Missing DSN. Set PG_DSN or pass --dsn.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
params = {
|
||||
"site_id": args.site_id,
|
||||
"start_date": args.start_date,
|
||||
"end_date": args.end_date,
|
||||
}
|
||||
timeout_val = max(1, min(args.timeout, 20))
|
||||
|
||||
conn = DatabaseConnection(args.dsn, connect_timeout=timeout_val)
|
||||
try:
|
||||
with conn.conn.cursor() as cur:
|
||||
cur.execute(SQL_BUILD_SUMMARY, params)
|
||||
conn.commit()
|
||||
except Exception as exc: # pragma: no cover - operational script
|
||||
conn.rollback()
|
||||
print(f"DWS build failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print("dws_order_summary refreshed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
258
etl_billiards/scripts/rebuild_ods_from_json.py
Normal file
258
etl_billiards/scripts/rebuild_ods_from_json.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
从本地 JSON 示例目录重建 billiards_ods.* 表,并导入样例数据。
|
||||
用法:
|
||||
PYTHONPATH=. python -m etl_billiards.scripts.rebuild_ods_from_json [--dsn ...] [--json-dir ...] [--include ...] [--drop-schema-first]
|
||||
|
||||
依赖环境变量:
|
||||
PG_DSN PostgreSQL 连接串(必填)
|
||||
PG_CONNECT_TIMEOUT 可选,秒,默认 10
|
||||
JSON_DOC_DIR 可选,JSON 目录,默认 C:\\dev\\LLTQ\\export\\test-json-doc
|
||||
ODS_INCLUDE_FILES 可选,逗号分隔文件名(不含 .json)
|
||||
ODS_DROP_SCHEMA_FIRST 可选,true/false,默认 true
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
import psycopg2
|
||||
from psycopg2 import sql
|
||||
from psycopg2.extras import Json, execute_values
|
||||
|
||||
|
||||
DEFAULT_JSON_DIR = r"C:\dev\LLTQ\export\test-json-doc"
|
||||
SPECIAL_LIST_PATHS: dict[str, tuple[str, ...]] = {
|
||||
"assistant_accounts_master": ("data", "assistantInfos"),
|
||||
"assistant_cancellation_records": ("data", "abolitionAssistants"),
|
||||
"assistant_service_records": ("data", "orderAssistantDetails"),
|
||||
"goods_stock_movements": ("data", "queryDeliveryRecordsList"),
|
||||
"goods_stock_summary": ("data",),
|
||||
"group_buy_packages": ("data", "packageCouponList"),
|
||||
"group_buy_redemption_records": ("data", "siteTableUseDetailsList"),
|
||||
"member_balance_changes": ("data", "tenantMemberCardLogs"),
|
||||
"member_profiles": ("data", "tenantMemberInfos"),
|
||||
"member_stored_value_cards": ("data", "tenantMemberCards"),
|
||||
"recharge_settlements": ("data", "settleList"),
|
||||
"settlement_records": ("data", "settleList"),
|
||||
"site_tables_master": ("data", "siteTables"),
|
||||
"stock_goods_category_tree": ("data", "goodsCategoryList"),
|
||||
"store_goods_master": ("data", "orderGoodsList"),
|
||||
"store_goods_sales_records": ("data", "orderGoodsLedgers"),
|
||||
"table_fee_discount_records": ("data", "taiFeeAdjustInfos"),
|
||||
"table_fee_transactions": ("data", "siteTableUseDetailsList"),
|
||||
"tenant_goods_master": ("data", "tenantGoodsList"),
|
||||
}
|
||||
|
||||
|
||||
def sanitize_identifier(name: str) -> str:
|
||||
"""将任意字符串转为可用的 SQL identifier(小写、非字母数字转下划线)。"""
|
||||
cleaned = re.sub(r"[^0-9a-zA-Z_]", "_", name.strip())
|
||||
if not cleaned:
|
||||
cleaned = "col"
|
||||
if cleaned[0].isdigit():
|
||||
cleaned = f"_{cleaned}"
|
||||
return cleaned.lower()
|
||||
|
||||
|
||||
def _extract_list_via_path(node, path: tuple[str, ...]):
|
||||
cur = node
|
||||
for key in path:
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(key)
|
||||
else:
|
||||
return []
|
||||
return cur if isinstance(cur, list) else []
|
||||
|
||||
|
||||
def load_records(payload, list_path: tuple[str, ...] | None = None) -> list:
|
||||
"""
|
||||
尝试从 JSON 结构中提取记录列表:
|
||||
- 直接是 list -> 返回
|
||||
- dict 中 data 是 list -> 返回
|
||||
- dict 中 data 是 dict,取第一个 list 字段
|
||||
- dict 中任意值是 list -> 返回
|
||||
- 其余情况,包装为单条记录
|
||||
"""
|
||||
if list_path:
|
||||
if isinstance(payload, list):
|
||||
merged: list = []
|
||||
for item in payload:
|
||||
merged.extend(_extract_list_via_path(item, list_path))
|
||||
if merged:
|
||||
return merged
|
||||
elif isinstance(payload, dict):
|
||||
lst = _extract_list_via_path(payload, list_path)
|
||||
if lst:
|
||||
return lst
|
||||
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
if isinstance(payload, dict):
|
||||
data_node = payload.get("data")
|
||||
if isinstance(data_node, list):
|
||||
return data_node
|
||||
if isinstance(data_node, dict):
|
||||
for v in data_node.values():
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
for v in payload.values():
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [payload]
|
||||
|
||||
|
||||
def collect_columns(records: Iterable[dict]) -> List[str]:
|
||||
"""汇总所有顶层键,作为表字段;仅处理 dict 记录。"""
|
||||
cols: set[str] = set()
|
||||
for rec in records:
|
||||
if isinstance(rec, dict):
|
||||
cols.update(rec.keys())
|
||||
return sorted(cols)
|
||||
|
||||
|
||||
def create_table(cur, schema: str, table: str, columns: List[Tuple[str, str]]):
|
||||
"""
|
||||
创建表:字段全部 jsonb,外加 source_file、record_index、payload、ingested_at。
|
||||
columns: [(col_name, original_key)]
|
||||
"""
|
||||
fields = [sql.SQL("{} jsonb").format(sql.Identifier(col)) for col, _ in columns]
|
||||
constraint_name = f"uq_{table}_source_record"
|
||||
ddl = sql.SQL(
|
||||
"CREATE TABLE IF NOT EXISTS {schema}.{table} ("
|
||||
"source_file text,"
|
||||
"record_index integer,"
|
||||
"{cols},"
|
||||
"payload jsonb,"
|
||||
"ingested_at timestamptz default now(),"
|
||||
"CONSTRAINT {constraint} UNIQUE (source_file, record_index)"
|
||||
");"
|
||||
).format(
|
||||
schema=sql.Identifier(schema),
|
||||
table=sql.Identifier(table),
|
||||
cols=sql.SQL(",").join(fields),
|
||||
constraint=sql.Identifier(constraint_name),
|
||||
)
|
||||
cur.execute(ddl)
|
||||
|
||||
|
||||
def insert_records(cur, schema: str, table: str, columns: List[Tuple[str, str]], records: list, source_file: str):
|
||||
"""批量插入记录。"""
|
||||
col_idents = [sql.Identifier(col) for col, _ in columns]
|
||||
col_names = [col for col, _ in columns]
|
||||
orig_keys = [orig for _, orig in columns]
|
||||
all_cols = [sql.Identifier("source_file"), sql.Identifier("record_index")] + col_idents + [
|
||||
sql.Identifier("payload")
|
||||
]
|
||||
|
||||
rows = []
|
||||
for idx, rec in enumerate(records):
|
||||
if not isinstance(rec, dict):
|
||||
rec = {"value": rec}
|
||||
row_values = [source_file, idx]
|
||||
for key in orig_keys:
|
||||
row_values.append(Json(rec.get(key)))
|
||||
row_values.append(Json(rec))
|
||||
rows.append(row_values)
|
||||
|
||||
insert_sql = sql.SQL("INSERT INTO {}.{} ({}) VALUES %s ON CONFLICT DO NOTHING").format(
|
||||
sql.Identifier(schema),
|
||||
sql.Identifier(table),
|
||||
sql.SQL(",").join(all_cols),
|
||||
)
|
||||
execute_values(cur, insert_sql, rows, page_size=500)
|
||||
|
||||
|
||||
def rebuild(schema: str = "billiards_ods", data_dir: str | Path = DEFAULT_JSON_DIR):
|
||||
parser = argparse.ArgumentParser(description="重建 billiards_ods.* 表并导入 JSON 样例")
|
||||
parser.add_argument("--dsn", dest="dsn", help="PostgreSQL DSN(默认读取环境变量 PG_DSN)")
|
||||
parser.add_argument("--json-dir", dest="json_dir", help=f"JSON 目录,默认 {DEFAULT_JSON_DIR}")
|
||||
parser.add_argument(
|
||||
"--include",
|
||||
dest="include_files",
|
||||
help="限定导入的文件名(逗号分隔,不含 .json),默认全部",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--drop-schema-first",
|
||||
dest="drop_schema_first",
|
||||
action="store_true",
|
||||
help="先删除并重建 schema(默认 true)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-drop-schema-first",
|
||||
dest="drop_schema_first",
|
||||
action="store_false",
|
||||
help="保留现有 schema,仅按冲突去重导入",
|
||||
)
|
||||
parser.set_defaults(drop_schema_first=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
dsn = args.dsn or os.environ.get("PG_DSN")
|
||||
if not dsn:
|
||||
print("缺少参数/环境变量 PG_DSN,无法连接数据库。")
|
||||
sys.exit(1)
|
||||
timeout = max(1, min(int(os.environ.get("PG_CONNECT_TIMEOUT", 10)), 60))
|
||||
env_drop = os.environ.get("ODS_DROP_SCHEMA_FIRST") or os.environ.get("DROP_SCHEMA_FIRST")
|
||||
drop_schema_first = (
|
||||
args.drop_schema_first
|
||||
if args.drop_schema_first is not None
|
||||
else str(env_drop or "true").lower() in ("1", "true", "yes")
|
||||
)
|
||||
include_files_env = args.include_files or os.environ.get("ODS_INCLUDE_FILES") or os.environ.get("INCLUDE_FILES")
|
||||
include_files = set()
|
||||
if include_files_env:
|
||||
include_files = {p.strip().lower() for p in include_files_env.split(",") if p.strip()}
|
||||
|
||||
base_dir = Path(args.json_dir or data_dir or DEFAULT_JSON_DIR)
|
||||
if not base_dir.exists():
|
||||
print(f"JSON 目录不存在: {base_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
conn = psycopg2.connect(dsn, connect_timeout=timeout)
|
||||
conn.autocommit = False
|
||||
cur = conn.cursor()
|
||||
|
||||
if drop_schema_first:
|
||||
print(f"Dropping schema {schema} ...")
|
||||
cur.execute(sql.SQL("DROP SCHEMA IF EXISTS {} CASCADE;").format(sql.Identifier(schema)))
|
||||
cur.execute(sql.SQL("CREATE SCHEMA {};").format(sql.Identifier(schema)))
|
||||
else:
|
||||
cur.execute(
|
||||
sql.SQL("SELECT schema_name FROM information_schema.schemata WHERE schema_name=%s"),
|
||||
(schema,),
|
||||
)
|
||||
if not cur.fetchone():
|
||||
cur.execute(sql.SQL("CREATE SCHEMA {};").format(sql.Identifier(schema)))
|
||||
|
||||
json_files = sorted(base_dir.glob("*.json"))
|
||||
for path in json_files:
|
||||
stem_lower = path.stem.lower()
|
||||
if include_files and stem_lower not in include_files:
|
||||
continue
|
||||
|
||||
print(f"Processing {path.name} ...")
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
list_path = SPECIAL_LIST_PATHS.get(stem_lower)
|
||||
records = load_records(payload, list_path=list_path)
|
||||
columns_raw = collect_columns(records)
|
||||
columns = [(sanitize_identifier(c), c) for c in columns_raw]
|
||||
|
||||
table_name = sanitize_identifier(path.stem)
|
||||
create_table(cur, schema, table_name, columns)
|
||||
if records:
|
||||
insert_records(cur, schema, table_name, columns, records, path.name)
|
||||
print(f" -> rows: {len(records)}, columns: {len(columns)}")
|
||||
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
print("Rebuild done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rebuild()
|
||||
@@ -4,9 +4,9 @@
|
||||
直接运行本文件即可触发 pytest。
|
||||
|
||||
示例:
|
||||
python scripts/run_tests.py --suite online --mode ONLINE --keyword ORDERS
|
||||
python scripts/run_tests.py --preset offline_realdb
|
||||
python scripts/run_tests.py --suite online offline --db-dsn ... --json-archive tmp/archives
|
||||
python scripts/run_tests.py --suite online --flow FULL --keyword ORDERS
|
||||
python scripts/run_tests.py --preset fetch_only
|
||||
python scripts/run_tests.py --suite online --json-source tmp/archives
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -27,7 +27,6 @@ if PROJECT_ROOT not in sys.path:
|
||||
|
||||
SUITE_MAP: Dict[str, str] = {
|
||||
"online": "tests/unit/test_etl_tasks_online.py",
|
||||
"offline": "tests/unit/test_etl_tasks_offline.py",
|
||||
"integration": "tests/integration/test_database.py",
|
||||
}
|
||||
|
||||
@@ -64,13 +63,12 @@ def parse_args() -> argparse.Namespace:
|
||||
help="自定义测试路径(可与 --suite 混用),例如 tests/unit/test_config.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["ONLINE", "OFFLINE"],
|
||||
help="覆盖 TEST_MODE(默认沿用 .env / 环境变量)",
|
||||
"--flow",
|
||||
choices=["FETCH_ONLY", "INGEST_ONLY", "FULL"],
|
||||
help="覆盖 PIPELINE_FLOW(在线抓取/本地清洗/全流程)",
|
||||
)
|
||||
parser.add_argument("--db-dsn", help="设置 TEST_DB_DSN,连接真实数据库进行测试")
|
||||
parser.add_argument("--json-archive", help="设置 TEST_JSON_ARCHIVE_DIR(离线档案目录)")
|
||||
parser.add_argument("--json-temp", help="设置 TEST_JSON_TEMP_DIR(临时 JSON 路径)")
|
||||
parser.add_argument("--json-source", help="设置 JSON_SOURCE_DIR(本地清洗入库使用的 JSON 目录)")
|
||||
parser.add_argument("--json-fetch-root", help="设置 JSON_FETCH_ROOT(在线抓取输出根目录)")
|
||||
parser.add_argument(
|
||||
"--keyword",
|
||||
"-k",
|
||||
@@ -123,14 +121,12 @@ def apply_presets_to_args(args: argparse.Namespace):
|
||||
|
||||
def apply_env(args: argparse.Namespace) -> Dict[str, str]:
|
||||
env_updates = {}
|
||||
if args.mode:
|
||||
env_updates["TEST_MODE"] = args.mode
|
||||
if args.db_dsn:
|
||||
env_updates["TEST_DB_DSN"] = args.db_dsn
|
||||
if args.json_archive:
|
||||
env_updates["TEST_JSON_ARCHIVE_DIR"] = args.json_archive
|
||||
if args.json_temp:
|
||||
env_updates["TEST_JSON_TEMP_DIR"] = args.json_temp
|
||||
if args.flow:
|
||||
env_updates["PIPELINE_FLOW"] = args.flow
|
||||
if args.json_source:
|
||||
env_updates["JSON_SOURCE_DIR"] = args.json_source
|
||||
if args.json_fetch_root:
|
||||
env_updates["JSON_FETCH_ROOT"] = args.json_fetch_root
|
||||
if args.env:
|
||||
for item in args.env:
|
||||
if "=" not in item:
|
||||
@@ -151,8 +147,7 @@ def build_pytest_args(args: argparse.Namespace) -> List[str]:
|
||||
if args.tests:
|
||||
targets.extend(args.tests)
|
||||
if not targets:
|
||||
# 默认跑 online + offline 套件
|
||||
targets = [SUITE_MAP["online"], SUITE_MAP["offline"]]
|
||||
targets = list(SUITE_MAP.values())
|
||||
|
||||
pytest_args: List[str] = targets
|
||||
if args.keyword:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Quick utility for validating PostgreSQL connectivity."""
|
||||
"""Quick utility for validating PostgreSQL connectivity (ASCII-only output)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -24,8 +24,8 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=5,
|
||||
help="connect_timeout seconds passed to psycopg2 (default: 5)",
|
||||
default=10,
|
||||
help="connect_timeout seconds passed to psycopg2 (capped at 20, default: 10)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -34,25 +34,26 @@ def main() -> int:
|
||||
args = parse_args()
|
||||
dsn = args.dsn or os.environ.get("TEST_DB_DSN")
|
||||
if not dsn:
|
||||
print("❌ 未提供 DSN,请通过 --dsn 或 TEST_DB_DSN 指定连接串", file=sys.stderr)
|
||||
print("Missing DSN. Use --dsn or TEST_DB_DSN.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(f"尝试连接: {dsn}")
|
||||
print(f"Trying connection: {dsn}")
|
||||
try:
|
||||
conn = DatabaseConnection(dsn, connect_timeout=args.timeout)
|
||||
timeout = max(1, min(args.timeout, 20))
|
||||
conn = DatabaseConnection(dsn, connect_timeout=timeout)
|
||||
except Exception as exc: # pragma: no cover - diagnostic output
|
||||
print("❌ 连接失败:", exc, file=sys.stderr)
|
||||
print("Connection failed:", exc, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
result = conn.query(args.query)
|
||||
print("✅ 连接成功,查询结果:")
|
||||
print("Connection OK, query result:")
|
||||
for row in result:
|
||||
print(row)
|
||||
conn.close()
|
||||
return 0
|
||||
except Exception as exc: # pragma: no cover - diagnostic output
|
||||
print("⚠️ 连接成功但执行查询失败:", exc, file=sys.stderr)
|
||||
print("Connection succeeded but query failed:", exc, file=sys.stderr)
|
||||
try:
|
||||
conn.close()
|
||||
finally:
|
||||
|
||||
@@ -1,50 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""测试命令仓库:集中维护 run_tests.py 的常用组合,并支持一键执行。
|
||||
|
||||
参数键说明(可在 PRESETS 中任意叠加):
|
||||
|
||||
1. suite
|
||||
类型:列表;值:["online"], ["offline"], ["integration"] 等。
|
||||
含义:引用 run_tests 内置测试套件。online=在线模式;offline=离线模式;integration=数据库集成测试。
|
||||
用法:["online","offline"] 表示一次执行两套;["integration"] 仅跑数据库相关用例。
|
||||
|
||||
2. tests
|
||||
类型:列表;示例:["tests/unit/test_config.py"]。
|
||||
含义:自定义的 pytest 目标路径,适合补充临时/个别测试。
|
||||
|
||||
3. mode
|
||||
类型:字符串;取值:"ONLINE" 或 "OFFLINE"。
|
||||
含义:覆盖 TEST_MODE;ONLINE 走 API 全流程,OFFLINE 读取 JSON 归档执行 Transform + Load。
|
||||
|
||||
4. db_dsn
|
||||
类型:字符串;示例:postgresql://user:pwd@host:5432/testdb。
|
||||
含义:设置 TEST_DB_DSN,使用真实 PostgreSQL 连接;不设置则使用伪 DB(仅记录操作,不落库)。
|
||||
|
||||
5. json_archive / json_temp
|
||||
类型:字符串;示例:"tests/testdata_json"、"C:/tmp/json"。
|
||||
含义:离线模式所需的归档输入目录 / 临时输出目录。未设置时沿用 .env 或默认配置。
|
||||
|
||||
6. keyword
|
||||
类型:字符串;示例:"ORDERS"。
|
||||
含义:等价 pytest -k,可筛选测试名/节点,只运行包含该关键字的用例。
|
||||
|
||||
7. pytest_args
|
||||
类型:字符串;示例:"-vv --maxfail=1"。
|
||||
含义:追加 pytest 命令行参数,用于控制日志、失败策略等。
|
||||
|
||||
8. env
|
||||
类型:列表;示例:["STORE_ID=123","API_TOKEN=xxx"]。
|
||||
含义:额外的环境变量,在调用 run_tests 前注入到 os.environ。
|
||||
|
||||
9. preset_meta
|
||||
类型:字符串;仅用于描述场景,不会传给 run_tests(纯注释)。
|
||||
|
||||
使用方式:
|
||||
- 直接 F5 或 `python scripts/test_presets.py`:读取 AUTO_RUN_PRESETS 的预置并顺序执行。
|
||||
- `python scripts/test_presets.py --preset offline_realdb`:临时指定要运行的组合。
|
||||
- `python scripts/test_presets.py --list`:查看参数说明及所有预置详情。
|
||||
"""
|
||||
|
||||
"""测试命令仓库:集中维护 run_tests.py 的常用组合,支持一键执行。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -55,46 +10,42 @@ from typing import List
|
||||
|
||||
RUN_TESTS_SCRIPT = os.path.join(os.path.dirname(__file__), "run_tests.py")
|
||||
|
||||
# 默认自动运行的预置(可自定义顺序)
|
||||
|
||||
AUTO_RUN_PRESETS = ["offline_realdb"]
|
||||
# 默认自动运行的预置(可根据需要修改顺序/条目)
|
||||
AUTO_RUN_PRESETS = ["fetch_only"]
|
||||
|
||||
PRESETS = {
|
||||
"online_orders": {
|
||||
"fetch_only": {
|
||||
"suite": ["online"],
|
||||
"mode": "ONLINE",
|
||||
"flow": "FETCH_ONLY",
|
||||
"json_fetch_root": "tmp/json_fetch",
|
||||
"keyword": "ORDERS",
|
||||
"pytest_args": "-vv",
|
||||
"preset_meta": "在线模式,仅跑订单任务并输出详细日志",
|
||||
"preset_meta": "仅在线抓取阶段,输出到本地目录",
|
||||
},
|
||||
|
||||
"dbrun": {
|
||||
"suite": ["integration"],
|
||||
# "mode": "OFFLINE",
|
||||
# "keyword": "ORDERS",
|
||||
# "pytest_args": "-vv",
|
||||
"preset_meta": "在线模式,仅跑订单任务并输出详细日志",
|
||||
},
|
||||
|
||||
"offline_realdb": {
|
||||
"suite": ["offline"],
|
||||
"mode": "OFFLINE",
|
||||
"db_dsn": "postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/LLZQ-test",
|
||||
"json_archive": "tests/testdata_json",
|
||||
"ingest_local": {
|
||||
"suite": ["online"],
|
||||
"flow": "INGEST_ONLY",
|
||||
"json_source": "tests/source-data-doc",
|
||||
"keyword": "ORDERS",
|
||||
"preset_meta": "离线模式 + 真实测试库,用预置 JSON 回放并写入测试库",
|
||||
"preset_meta": "从指定 JSON 目录做本地清洗入库",
|
||||
},
|
||||
"full_pipeline": {
|
||||
"suite": ["online"],
|
||||
"flow": "FULL",
|
||||
"json_fetch_root": "tmp/json_fetch",
|
||||
"keyword": "ORDERS",
|
||||
"preset_meta": "先抓取再清洗入库的全流程",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def print_parameter_help() -> None:
|
||||
print("=== 参数键说明 ===")
|
||||
print("suite : 预置套件列表,如 ['online','offline']")
|
||||
print("suite : 预置套件列表,如 ['online','integration']")
|
||||
print("tests : 自定义 pytest 路径列表")
|
||||
print("mode : TEST_MODE(ONLINE/ OFFLINE)")
|
||||
print("db_dsn : TEST_DB_DSN,连接真实 PostgreSQL")
|
||||
print("json_archive : TEST_JSON_ARCHIVE_DIR,离线模式输入目录")
|
||||
print("json_temp : TEST_JSON_TEMP_DIR,离线模式临时目录")
|
||||
print("flow : PIPELINE_FLOW(FETCH_ONLY / INGEST_ONLY / FULL)")
|
||||
print("json_source : JSON_SOURCE_DIR,本地清洗入库使用的 JSON 目录")
|
||||
print("json_fetch_root : JSON_FETCH_ROOT,在线抓取输出根目录")
|
||||
print("keyword : pytest -k 过滤关键字")
|
||||
print("pytest_args : 额外 pytest 参数(字符串)")
|
||||
print("env : 附加环境变量,例如 ['KEY=VALUE']")
|
||||
@@ -120,7 +71,7 @@ def print_presets() -> None:
|
||||
|
||||
def resolve_targets(requested: List[str] | None) -> List[str]:
|
||||
if not PRESETS:
|
||||
raise SystemExit("Pre-sets 为空,请先在 PRESETS 中定义测试组合。")
|
||||
raise SystemExit("预置为空,请先在 PRESETS 中定义测试组合。")
|
||||
|
||||
def valid(names: List[str]) -> List[str]:
|
||||
return [name for name in names if name in PRESETS]
|
||||
@@ -137,7 +88,6 @@ def resolve_targets(requested: List[str] | None) -> List[str]:
|
||||
if auto:
|
||||
return auto
|
||||
|
||||
# 兜底:全部预置
|
||||
return list(PRESETS.keys())
|
||||
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ class BaseDwdTask(BaseTask):
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
|
||||
rows = self.db.fetch_all(sql, (start_time, end_time, batch_size, offset))
|
||||
rows = self.db.query(sql, (start_time, end_time, batch_size, offset))
|
||||
|
||||
if not rows:
|
||||
break
|
||||
|
||||
yield [dict(row) for row in rows]
|
||||
yield rows
|
||||
|
||||
if len(rows) < batch_size:
|
||||
break
|
||||
|
||||
@@ -16,29 +16,31 @@ class ManualIngestTask(BaseTask):
|
||||
Used when upstream API is unavailable and we need to replay captured payloads.
|
||||
"""
|
||||
|
||||
# 仅保留已确认的新表映射,其余表待逐一校准后再添加
|
||||
FILE_MAPPING: list[tuple[tuple[str, ...], str]] = [
|
||||
(("会员档案",), "billiards_ods.ods_member_profile"),
|
||||
(("储值卡列表", "储值卡"), "billiards_ods.ods_member_card"),
|
||||
(("充值记录",), "billiards_ods.ods_recharge_record"),
|
||||
(("余额变动",), "billiards_ods.ods_balance_change"),
|
||||
(("助教账号",), "billiards_ods.ods_assistant_account"),
|
||||
(("助教流水",), "billiards_ods.ods_assistant_service_log"),
|
||||
(("助教废除", "助教作废"), "billiards_ods.ods_assistant_cancel_log"),
|
||||
(("台桌列表",), "billiards_ods.ods_table_info"),
|
||||
(("台费流水",), "billiards_ods.ods_table_use_log"),
|
||||
(("台费打折",), "billiards_ods.ods_table_fee_adjust"),
|
||||
(("商品档案",), "billiards_ods.ods_store_product"),
|
||||
(("门店商品销售", "销售记录"), "billiards_ods.ods_store_sale_item"),
|
||||
(("团购套餐定义", "套餐定义"), "billiards_ods.ods_group_package"),
|
||||
(("团购套餐使用", "套餐使用"), "billiards_ods.ods_group_package_log"),
|
||||
(("平台验券", "验券记录"), "billiards_ods.ods_platform_coupon_log"),
|
||||
(("库存汇总",), "billiards_ods.ods_inventory_stock"),
|
||||
(("库存变化记录1",), "billiards_ods.ods_inventory_change"),
|
||||
(("库存变化记录2", "分类配置"), "billiards_ods.ods_goods_category"),
|
||||
(("结账记录",), "billiards_ods.ods_order_settle"),
|
||||
(("小票详情", "小票明细", "票详"), "billiards_ods.ods_order_receipt_detail"),
|
||||
(("支付记录",), "billiards_ods.ods_payment_record"),
|
||||
(("退款记录",), "billiards_ods.ods_refund_record"),
|
||||
(("助教账号",), "billiards_ods.assistant_accounts_master"),
|
||||
(("助教流水",), "billiards_ods.assistant_service_records"),
|
||||
(("助教废除", "助教作废"), "billiards_ods.assistant_cancellation_records"),
|
||||
(("库存变化记录1",), "billiards_ods.goods_stock_movements"),
|
||||
(("库存汇总", "库存汇总记录"), "billiards_ods.goods_stock_summary"),
|
||||
(("团购套餐", "套餐定义"), "billiards_ods.group_buy_packages"),
|
||||
(("团购核销", "套餐使用", "团购使用"), "billiards_ods.group_buy_redemption_records"),
|
||||
(("余额变动", "余额变更"), "billiards_ods.member_balance_changes"),
|
||||
(("会员档案", "会员列表"), "billiards_ods.member_profiles"),
|
||||
(("储值卡", "会员储值卡"), "billiards_ods.member_stored_value_cards"),
|
||||
(("支付记录", "支付流水"), "billiards_ods.payment_transactions"),
|
||||
(("平台验券", "团购验券", "平台券核销"), "billiards_ods.platform_coupon_redemption_records"),
|
||||
(("充值结算", "充值记录"), "billiards_ods.recharge_settlements"),
|
||||
(("退款流水", "退款记录"), "billiards_ods.refund_transactions"),
|
||||
(("结账记录", "结算记录"), "billiards_ods.settlement_records"),
|
||||
(("小票详情", "结账小票"), "billiards_ods.settlement_ticket_details"),
|
||||
(("台桌列表", "台桌维表"), "billiards_ods.site_tables_master"),
|
||||
(("商品分类树", "库存分类"), "billiards_ods.stock_goods_category_tree"),
|
||||
(("门店商品档案",), "billiards_ods.store_goods_master"),
|
||||
(("门店商品销售", "商品销售流水"), "billiards_ods.store_goods_sales_records"),
|
||||
(("台费折扣", "台费调账"), "billiards_ods.table_fee_discount_records"),
|
||||
(("台费流水", "台费计费"), "billiards_ods.table_fee_transactions"),
|
||||
(("租户商品档案", "商品档案"), "billiards_ods.tenant_goods_master"),
|
||||
]
|
||||
WRAPPER_META_KEYS = {"code", "message", "msg", "success", "error", "status"}
|
||||
|
||||
@@ -253,7 +255,7 @@ class ManualIngestTask(BaseTask):
|
||||
if isinstance(data_node, dict):
|
||||
return data_node.get("siteId") or data_node.get("site_id")
|
||||
|
||||
return None
|
||||
return self.config.get("app.store_id")
|
||||
|
||||
def _extract_pk(self, item: dict, table: str):
|
||||
if "ods_order_receipt_detail" in table:
|
||||
@@ -390,9 +392,28 @@ class ManualIngestTask(BaseTask):
|
||||
if isinstance(obj, dict) and obj.get(k) not in (None, ""):
|
||||
return obj.get(k)
|
||||
return None
|
||||
def to_bool(value):
|
||||
if value in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return bool(int(value))
|
||||
except Exception:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return None
|
||||
def to_int(value):
|
||||
if value in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if "ods_member_profile" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["system_member_id"] = record.get("system_member_id")
|
||||
row["register_site_id"] = record.get("register_site_id")
|
||||
row["site_name"] = record.get("site_name")
|
||||
row["member_name"] = pick(record, "name", "memberName")
|
||||
row["nickname"] = record.get("nickname")
|
||||
row["mobile"] = record.get("mobile")
|
||||
@@ -401,28 +422,44 @@ class ManualIngestTask(BaseTask):
|
||||
row["register_time"] = record.get("register_time") or record.get("registerTime")
|
||||
row["member_type_id"] = pick(record, "cardTypeId", "member_type_id")
|
||||
row["member_type_name"] = record.get("cardTypeName")
|
||||
row["member_card_grade_code"] = record.get("member_card_grade_code")
|
||||
row["status"] = pick(record, "status", "state")
|
||||
row["user_status"] = record.get("user_status")
|
||||
row["balance"] = record.get("balance")
|
||||
row["points"] = record.get("points") or record.get("point")
|
||||
row["growth_value"] = record.get("growth_value")
|
||||
row["last_visit_time"] = record.get("lastVisitTime")
|
||||
row["wechat_id"] = record.get("wechatId")
|
||||
row["alipay_id"] = record.get("alipayId")
|
||||
row["member_card_no"] = record.get("cardNo")
|
||||
row["referrer_member_id"] = record.get("referrer_member_id")
|
||||
row["remarks"] = record.get("remark")
|
||||
|
||||
if "ods_member_card" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["member_id"] = pick(record, "memberId", "member_id")
|
||||
row["tenant_member_id"] = record.get("tenant_member_id")
|
||||
row["card_type_id"] = record.get("cardTypeId")
|
||||
row["card_type_name"] = record.get("cardTypeName")
|
||||
row["card_no"] = record.get("card_no") or record.get("cardNo")
|
||||
row["card_physics_type"] = record.get("card_physics_type")
|
||||
row["card_balance"] = record.get("balance")
|
||||
row["denomination"] = record.get("denomination")
|
||||
row["discount_rate"] = record.get("discount") or record.get("discount_rate")
|
||||
row["valid_start_date"] = record.get("validStart")
|
||||
row["valid_end_date"] = record.get("validEnd")
|
||||
row["table_discount"] = record.get("table_discount")
|
||||
row["goods_discount"] = record.get("goods_discount")
|
||||
row["assistant_discount"] = record.get("assistant_discount")
|
||||
row["assistant_reward_discount"] = record.get("assistant_reward_discount")
|
||||
row["valid_start_date"] = record.get("validStart") or record.get("start_time")
|
||||
row["valid_end_date"] = record.get("validEnd") or record.get("end_time")
|
||||
row["disable_start_date"] = record.get("disable_start_time")
|
||||
row["disable_end_date"] = record.get("disable_end_time")
|
||||
row["last_consume_time"] = record.get("lastConsumeTime")
|
||||
row["status"] = record.get("status")
|
||||
row["is_delete"] = to_bool(record.get("is_delete"))
|
||||
row["activate_time"] = record.get("activateTime")
|
||||
row["deactivate_time"] = record.get("cancelTime")
|
||||
row["register_site_id"] = record.get("register_site_id")
|
||||
row["issuer_id"] = record.get("issuerId")
|
||||
row["issuer_name"] = record.get("issuerName")
|
||||
|
||||
@@ -443,17 +480,25 @@ class ManualIngestTask(BaseTask):
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["site_id"] = row.get("site_id") or pick(record, "siteId", "site_id")
|
||||
row["member_id"] = pick(record, "memberId", "member_id")
|
||||
row["change_amount"] = record.get("change_amount")
|
||||
row["balance_before"] = record.get("before_balance")
|
||||
row["balance_after"] = record.get("after_balance")
|
||||
row["tenant_member_id"] = record.get("tenant_member_id")
|
||||
row["tenant_member_card_id"] = record.get("tenant_member_card_id")
|
||||
row["member_name"] = record.get("memberName")
|
||||
row["member_mobile"] = record.get("memberMobile")
|
||||
row["change_amount"] = record.get("change_amount") or record.get("account_data")
|
||||
row["balance_before"] = record.get("before_balance") or record.get("before")
|
||||
row["balance_after"] = record.get("after_balance") or record.get("after")
|
||||
row["change_type"] = record.get("from_type") or record.get("type")
|
||||
row["payment_method"] = record.get("payment_method")
|
||||
row["refund_amount"] = record.get("refund_amount")
|
||||
row["relate_id"] = record.get("relate_id")
|
||||
row["pay_method"] = record.get("pay_type")
|
||||
row["register_site_id"] = record.get("register_site_id")
|
||||
row["register_site_name"] = record.get("registerSiteName")
|
||||
row["pay_site_name"] = record.get("paySiteName")
|
||||
row["remark"] = record.get("remark")
|
||||
row["operator_id"] = record.get("operatorId")
|
||||
row["operator_name"] = record.get("operatorName")
|
||||
row["change_time"] = record.get("create_time") or record.get("changeTime")
|
||||
row["is_deleted"] = record.get("is_delete") or record.get("is_deleted")
|
||||
row["is_deleted"] = to_bool(record.get("is_delete") or record.get("is_deleted"))
|
||||
row["source_file"] = row.get("source_file")
|
||||
row["fetched_at"] = row.get("fetched_at")
|
||||
|
||||
@@ -461,25 +506,44 @@ class ManualIngestTask(BaseTask):
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["assistant_name"] = record.get("assistantName") or record.get("name")
|
||||
row["mobile"] = record.get("mobile")
|
||||
row["assistant_no"] = record.get("assistant_no") or record.get("assistantNo")
|
||||
row["team_id"] = record.get("teamId")
|
||||
row["team_name"] = record.get("teamName")
|
||||
row["group_id"] = record.get("group_id")
|
||||
row["group_name"] = record.get("group_name")
|
||||
row["job_num"] = record.get("job_num")
|
||||
row["entry_type"] = record.get("entry_type")
|
||||
row["leave_status"] = record.get("leave_status")
|
||||
row["assistant_status"] = record.get("assistant_status")
|
||||
row["allow_cx"] = to_bool(record.get("allow_cx"))
|
||||
row["status"] = record.get("status")
|
||||
row["hired_date"] = record.get("hireDate")
|
||||
row["left_date"] = record.get("leaveDate")
|
||||
|
||||
if "ods_assistant_service_log" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["assistant_id"] = record.get("assistantId")
|
||||
row["service_type"] = record.get("serviceType")
|
||||
row["assistant_id"] = record.get("assistantId") or record.get("site_assistant_id")
|
||||
row["assistant_name"] = record.get("assistantName") or record.get("ledger_name")
|
||||
row["service_type"] = record.get("serviceType") or record.get("order_assistant_type")
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["order_settle_id"] = record.get("orderSettleId")
|
||||
row["start_time"] = record.get("startTime")
|
||||
row["end_time"] = record.get("endTime")
|
||||
row["duration_minutes"] = record.get("duration")
|
||||
row["original_fee"] = record.get("originFee") or record.get("original_fee")
|
||||
row["discount_amount"] = record.get("discountAmount")
|
||||
row["final_fee"] = record.get("finalFee") or record.get("final_fee")
|
||||
row["member_id"] = record.get("memberId")
|
||||
row["start_time"] = record.get("start_use_time") or record.get("startTime") or record.get("ledger_start_time")
|
||||
row["end_time"] = record.get("end_time") or record.get("last_use_time") or record.get("ledger_end_time")
|
||||
row["duration_seconds"] = record.get("real_use_seconds") or record.get("duration")
|
||||
row["original_fee"] = record.get("originFee") or record.get("original_fee") or record.get("ledger_amount")
|
||||
row["member_discount_amount"] = record.get("member_discount_amount")
|
||||
row["manual_discount_amount"] = record.get("manual_discount_amount")
|
||||
row["coupon_discount_amount"] = record.get("coupon_deduct_money")
|
||||
row["final_fee"] = record.get("finalFee") or record.get("final_fee") or record.get("service_money")
|
||||
row["member_id"] = record.get("memberId") or record.get("tenant_member_id")
|
||||
row["operator_id"] = record.get("operator_id")
|
||||
row["salesman_id"] = record.get("salesman_user_id")
|
||||
row["is_canceled"] = bool(record.get("is_trash"))
|
||||
row["cancel_time"] = record.get("trash_time")
|
||||
row["skill_grade"] = record.get("skill_grade")
|
||||
row["service_grade"] = record.get("service_grade")
|
||||
row["composite_grade"] = record.get("composite_grade")
|
||||
row["overall_score"] = record.get("sum_grade")
|
||||
row["status"] = record.get("status")
|
||||
|
||||
if "ods_assistant_cancel_log" in table:
|
||||
@@ -487,6 +551,13 @@ class ManualIngestTask(BaseTask):
|
||||
row["ledger_id"] = record.get("ledgerId")
|
||||
row["assistant_id"] = record.get("assistantId")
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["table_id"] = record.get("tableId")
|
||||
row["table_area_id"] = record.get("tableAreaId")
|
||||
row["table_area_name"] = record.get("tableArea")
|
||||
row["table_name"] = record.get("tableName")
|
||||
row["assistant_on"] = record.get("assistantOn")
|
||||
row["pd_charge_minutes"] = record.get("pdChargeMinutes")
|
||||
row["assistant_abolish_amount"] = record.get("assistantAbolishAmount")
|
||||
row["reason"] = record.get("reason")
|
||||
row["cancel_time"] = record.get("cancel_time") or record.get("cancelTime")
|
||||
row["operator_id"] = record.get("operatorId")
|
||||
@@ -498,22 +569,64 @@ class ManualIngestTask(BaseTask):
|
||||
row["table_name"] = record.get("tableName")
|
||||
row["table_type"] = record.get("tableType")
|
||||
row["area_name"] = record.get("areaName")
|
||||
row["site_table_area_id"] = record.get("site_table_area_id") or record.get("siteTableAreaId")
|
||||
row["tenant_table_area_id"] = record.get("tenant_table_area_id") or record.get("tenantTableAreaId")
|
||||
row["table_price"] = record.get("table_price") or record.get("tablePrice")
|
||||
row["table_status"] = record.get("table_status") or record.get("tableStatusName") or record.get("table_status")
|
||||
row["audit_status"] = record.get("audit_status")
|
||||
row["show_status"] = record.get("show_status")
|
||||
row["light_status"] = record.get("light_status")
|
||||
row["virtual_table"] = to_bool(record.get("virtual_table"))
|
||||
row["is_rest_area"] = to_bool(record.get("is_rest_area"))
|
||||
row["charge_free"] = to_bool(record.get("charge_free"))
|
||||
row["table_cloth_use_time"] = record.get("table_cloth_use_time")
|
||||
row["table_cloth_use_cycle"] = record.get("table_cloth_use_Cycle")
|
||||
row["status"] = record.get("status")
|
||||
row["created_time"] = record.get("createTime")
|
||||
row["updated_time"] = record.get("updateTime")
|
||||
|
||||
if "ods_table_use_log" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["table_id"] = record.get("tableId")
|
||||
row["table_id"] = record.get("tableId") or record.get("site_table_id")
|
||||
row["table_name"] = record.get("tableName") or record.get("ledger_name")
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["order_settle_id"] = record.get("orderSettleId")
|
||||
row["start_time"] = record.get("startTime")
|
||||
row["end_time"] = record.get("endTime")
|
||||
row["duration_minutes"] = record.get("duration")
|
||||
row["original_table_fee"] = record.get("originFee") or record.get("original_table_fee")
|
||||
row["discount_amount"] = record.get("discountAmount")
|
||||
row["final_table_fee"] = record.get("finalFee") or record.get("final_table_fee")
|
||||
row["start_time"] = record.get("start_use_time") or record.get("startTime") or record.get("ledger_start_time")
|
||||
row["end_time"] = record.get("end_time") or record.get("ledger_end_time")
|
||||
row["duration_seconds"] = record.get("real_table_use_seconds") or record.get("duration")
|
||||
row["billing_unit_price"] = record.get("ledger_unit_price")
|
||||
row["billing_count"] = record.get("ledger_count")
|
||||
row["original_table_fee"] = record.get("originFee") or record.get("original_table_fee") or record.get("ledger_amount")
|
||||
row["member_discount_amount"] = record.get("member_discount_amount")
|
||||
row["coupon_discount_amount"] = record.get("coupon_promotion_amount")
|
||||
row["manual_discount_amount"] = record.get("adjust_amount") or record.get("discountAmount")
|
||||
row["service_fee"] = record.get("mgmt_fee") or record.get("service_money")
|
||||
row["final_table_fee"] = (
|
||||
record.get("finalFee")
|
||||
or record.get("final_table_fee")
|
||||
or record.get("real_table_charge_money")
|
||||
)
|
||||
row["member_id"] = record.get("memberId")
|
||||
row["operator_id"] = record.get("operator_id")
|
||||
row["salesman_id"] = record.get("salesman_user_id")
|
||||
row["is_canceled"] = bool(record.get("is_delete"))
|
||||
row["cancel_time"] = record.get("cancel_time")
|
||||
row["site_table_area_id"] = record.get("site_table_area_id")
|
||||
row["tenant_table_area_id"] = record.get("tenant_table_area_id")
|
||||
row["site_table_area_name"] = record.get("site_table_area_name")
|
||||
row["is_single_order"] = to_bool(record.get("is_single_order"))
|
||||
row["used_card_amount"] = record.get("used_card_amount")
|
||||
row["adjust_amount"] = record.get("adjust_amount")
|
||||
row["coupon_promotion_amount"] = record.get("coupon_promotion_amount")
|
||||
row["service_money"] = record.get("service_money")
|
||||
row["mgmt_fee"] = record.get("mgmt_fee")
|
||||
row["fee_total"] = record.get("fee_total")
|
||||
row["last_use_time"] = record.get("last_use_time")
|
||||
row["ledger_start_time"] = record.get("ledger_start_time")
|
||||
row["ledger_end_time"] = record.get("ledger_end_time")
|
||||
row["ledger_status"] = record.get("ledger_status")
|
||||
row["start_use_time"] = record.get("start_use_time")
|
||||
row["add_clock_seconds"] = record.get("add_clock_seconds")
|
||||
row["status"] = record.get("status")
|
||||
|
||||
if "ods_table_fee_adjust" in table:
|
||||
@@ -532,23 +645,45 @@ class ManualIngestTask(BaseTask):
|
||||
row["goods_name"] = record.get("goodsName")
|
||||
row["category_id"] = record.get("categoryId")
|
||||
row["category_name"] = record.get("categoryName")
|
||||
row["unit"] = record.get("unit")
|
||||
row["sale_price"] = record.get("salePrice")
|
||||
row["cost_price"] = record.get("costPrice")
|
||||
row["sale_num"] = record.get("sale_num")
|
||||
row["stock_a"] = record.get("stock_A")
|
||||
row["stock"] = record.get("stock")
|
||||
row["provisional_total_cost"] = record.get("provisional_total_cost")
|
||||
row["total_purchase_cost"] = record.get("total_purchase_cost")
|
||||
row["batch_stock_quantity"] = record.get("batch_stock_quantity")
|
||||
row["goods_state"] = record.get("goods_state")
|
||||
row["status"] = record.get("status")
|
||||
|
||||
if "ods_store_sale_item" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["order_settle_id"] = record.get("orderSettleId")
|
||||
row["goods_id"] = record.get("goodsId")
|
||||
row["goods_name"] = record.get("goodsName")
|
||||
row["category_id"] = record.get("categoryId")
|
||||
row["quantity"] = record.get("quantity")
|
||||
row["original_amount"] = record.get("originalAmount")
|
||||
row["discount_amount"] = record.get("discountAmount")
|
||||
row["final_amount"] = record.get("finalAmount")
|
||||
row["is_gift"] = record.get("isGift")
|
||||
row["sale_time"] = record.get("saleTime")
|
||||
row["order_goods_id"] = record.get("order_goods_id") or record.get("orderGoodsId")
|
||||
row["site_goods_id"] = record.get("site_goods_id") or record.get("siteGoodsId")
|
||||
row["goods_id"] = record.get("tenant_goods_id") or record.get("goodsId")
|
||||
row["goods_name"] = record.get("goodsName") or record.get("ledger_name")
|
||||
row["category_id"] = record.get("categoryId") or record.get("tenant_goods_category_id")
|
||||
row["quantity"] = record.get("quantity") or record.get("ledger_count")
|
||||
row["unit_price"] = record.get("unitPrice") or record.get("ledger_unit_price")
|
||||
row["original_amount"] = record.get("originalAmount") or record.get("ledger_amount")
|
||||
row["discount_amount"] = record.get("discountAmount") or record.get("discount_money")
|
||||
row["final_amount"] = record.get("finalAmount") or record.get("real_goods_money")
|
||||
row["discount_price"] = record.get("discount_price")
|
||||
row["cost_money"] = record.get("cost_money")
|
||||
row["coupon_deduct_amount"] = record.get("coupon_deduct_money")
|
||||
row["member_discount_amount"] = record.get("member_discount_amount")
|
||||
row["point_discount_money"] = record.get("point_discount_money")
|
||||
row["point_discount_cost"] = record.get("point_discount_money_cost")
|
||||
row["sales_type"] = record.get("sales_type")
|
||||
row["goods_remark"] = record.get("goods_remark")
|
||||
row["is_gift"] = to_bool(record.get("isGift"))
|
||||
row["sale_time"] = record.get("saleTime") or record.get("create_time")
|
||||
row["salesman_id"] = record.get("salesman_user_id")
|
||||
row["operator_id"] = record.get("operator_id")
|
||||
row["is_refunded"] = to_bool(record.get("is_delete")) or bool(record.get("returns_number"))
|
||||
|
||||
if "ods_group_package_log" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
@@ -561,16 +696,29 @@ class ManualIngestTask(BaseTask):
|
||||
row["used_time"] = record.get("usedTime")
|
||||
row["deduct_amount"] = record.get("deductAmount")
|
||||
row["settle_price"] = record.get("settlePrice")
|
||||
row["coupon_code"] = record.get("coupon_code") or record.get("couponCode")
|
||||
|
||||
if "ods_group_package" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["package_name"] = record.get("packageName")
|
||||
row["table_area_id"] = record.get("table_area_id") or record.get("tableAreaId")
|
||||
row["table_area_name"] = record.get("tableAreaName")
|
||||
row["platform_code"] = record.get("platformCode")
|
||||
row["status"] = record.get("status")
|
||||
row["face_price"] = record.get("facePrice")
|
||||
row["settle_price"] = record.get("settlePrice")
|
||||
row["valid_from"] = record.get("validFrom")
|
||||
row["valid_to"] = record.get("validTo")
|
||||
row["selling_price"] = record.get("selling_price")
|
||||
row["duration"] = record.get("duration")
|
||||
row["valid_from"] = record.get("validFrom") or record.get("start_time")
|
||||
row["valid_to"] = record.get("validTo") or record.get("end_time")
|
||||
row["start_time"] = record.get("start_time")
|
||||
row["end_time"] = record.get("end_time")
|
||||
row["is_enabled"] = to_bool(record.get("is_enabled"))
|
||||
row["is_delete"] = to_bool(record.get("is_delete"))
|
||||
row["package_type"] = record.get("type")
|
||||
row["usable_count"] = record.get("usable_count")
|
||||
row["creator_name"] = record.get("creator_name")
|
||||
row["tenant_table_area_id"] = record.get("tenant_table_area_id")
|
||||
|
||||
if "ods_platform_coupon_log" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
@@ -589,10 +737,15 @@ class ManualIngestTask(BaseTask):
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["order_settle_id"] = record.get("orderSettleId")
|
||||
row["member_id"] = record.get("memberId")
|
||||
row["pay_method_code"] = record.get("payMethodCode") or record.get("pay_type")
|
||||
row["pay_method_code"] = record.get("payMethodCode") or record.get("pay_type") or record.get("payment_method")
|
||||
row["pay_method_name"] = record.get("payMethodName")
|
||||
row["pay_status"] = record.get("pay_status")
|
||||
row["pay_amount"] = record.get("payAmount")
|
||||
row["pay_time"] = record.get("payTime")
|
||||
row["online_pay_channel"] = record.get("online_pay_channel")
|
||||
row["transaction_id"] = record.get("transaction_id")
|
||||
row["operator_id"] = record.get("operator_id")
|
||||
row["remark"] = record.get("remark")
|
||||
row["relate_type"] = record.get("relateType")
|
||||
row["relate_id"] = record.get("relateId")
|
||||
|
||||
@@ -601,30 +754,74 @@ class ManualIngestTask(BaseTask):
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["order_settle_id"] = record.get("orderSettleId")
|
||||
row["member_id"] = record.get("memberId")
|
||||
row["pay_sn"] = record.get("pay_sn")
|
||||
row["pay_amount"] = record.get("pay_amount")
|
||||
row["pay_status"] = record.get("pay_status")
|
||||
row["is_revoke"] = to_bool(record.get("is_revoke"))
|
||||
row["is_delete"] = to_bool(record.get("is_delete"))
|
||||
row["online_pay_channel"] = record.get("online_pay_channel")
|
||||
row["pay_method_code"] = record.get("payMethodCode")
|
||||
row["refund_amount"] = record.get("refundAmount")
|
||||
row["refund_time"] = record.get("refundTime")
|
||||
row["action_type"] = record.get("action_type")
|
||||
row["pay_terminal"] = record.get("pay_terminal")
|
||||
row["pay_config_id"] = record.get("pay_config_id")
|
||||
row["cashier_point_id"] = record.get("cashier_point_id")
|
||||
row["operator_id"] = record.get("operator_id")
|
||||
row["member_card_id"] = record.get("member_card_id")
|
||||
row["balance_frozen_amount"] = record.get("balance_frozen_amount")
|
||||
row["card_frozen_amount"] = record.get("card_frozen_amount")
|
||||
row["round_amount"] = record.get("round_amount")
|
||||
row["online_pay_type"] = record.get("online_pay_type")
|
||||
row["channel_payer_id"] = record.get("channel_payer_id")
|
||||
row["channel_pay_no"] = record.get("channel_pay_no")
|
||||
row["check_status"] = record.get("check_status")
|
||||
row["channel_fee"] = record.get("channel_fee")
|
||||
row["relate_type"] = record.get("relate_type")
|
||||
row["relate_id"] = record.get("relate_id")
|
||||
row["status"] = record.get("status")
|
||||
row["reason"] = record.get("reason")
|
||||
row["related_payment_id"] = record.get("related_payment_id") or record.get("payment_id")
|
||||
|
||||
if "ods_inventory_change" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["site_goods_id"] = record.get("siteGoodsId")
|
||||
row["goods_id"] = record.get("goodsId")
|
||||
row["change_amount"] = record.get("changeAmount")
|
||||
row["before_stock"] = record.get("beforeStock")
|
||||
row["after_stock"] = record.get("afterStock")
|
||||
row["stock_type"] = record.get("stockType")
|
||||
row["change_amount"] = record.get("changeAmount") or record.get("changeNum")
|
||||
row["before_stock"] = record.get("beforeStock") or record.get("startNum")
|
||||
row["after_stock"] = record.get("afterStock") or record.get("endNum")
|
||||
row["change_amount_alt"] = record.get("changeNumA")
|
||||
row["before_stock_alt"] = record.get("startNumA")
|
||||
row["after_stock_alt"] = record.get("endNumA")
|
||||
row["change_type"] = record.get("changeType")
|
||||
row["relate_id"] = record.get("relateId")
|
||||
row["unit"] = record.get("unit")
|
||||
row["price"] = record.get("price")
|
||||
row["goods_category_id"] = record.get("goodsCategoryId")
|
||||
row["goods_second_category_id"] = record.get("goodsSecondCategoryId")
|
||||
row["remark"] = record.get("remark")
|
||||
row["operator_id"] = record.get("operatorId")
|
||||
row["operator_name"] = record.get("operatorName")
|
||||
row["change_time"] = record.get("changeTime")
|
||||
row["change_time"] = record.get("changeTime") or record.get("createTime")
|
||||
|
||||
if "ods_inventory_stock" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["goods_id"] = record.get("goodsId")
|
||||
row["goods_name"] = record.get("goodsName")
|
||||
row["goods_unit"] = record.get("goodsUnit")
|
||||
row["goods_category_id"] = record.get("goodsCategoryId")
|
||||
row["goods_second_category_id"] = record.get("goodsCategorySecondId")
|
||||
row["range_start_stock"] = record.get("rangeStartStock")
|
||||
row["range_end_stock"] = record.get("rangeEndStock")
|
||||
row["range_in"] = record.get("rangeIn")
|
||||
row["range_out"] = record.get("rangeOut")
|
||||
row["range_inventory"] = record.get("rangeInventory")
|
||||
row["range_sale"] = record.get("rangeSale")
|
||||
row["range_sale_money"] = record.get("rangeSaleMoney")
|
||||
row["current_stock"] = record.get("currentStock")
|
||||
row["cost_price"] = record.get("costPrice")
|
||||
row["category_name"] = record.get("categoryName")
|
||||
|
||||
if "ods_goods_category" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
@@ -635,25 +832,63 @@ class ManualIngestTask(BaseTask):
|
||||
row["remark"] = record.get("remark")
|
||||
|
||||
if "ods_order_receipt_detail" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["receipt_no"] = record.get("receiptNo")
|
||||
row["receipt_time"] = record.get("receiptTime")
|
||||
row["total_amount"] = record.get("totalAmount")
|
||||
row["discount_amount"] = record.get("discountAmount")
|
||||
row["final_amount"] = record.get("finalAmount")
|
||||
row["member_id"] = record.get("memberId")
|
||||
row["snapshot_raw"] = record.get("siteProfile") or record.get("site_profile")
|
||||
detail = record
|
||||
if isinstance(record.get("data"), dict):
|
||||
detail = record["data"].get("data", record["data"])
|
||||
row["tenant_id"] = pick(detail, "tenantId", "tenant_id")
|
||||
row["order_trade_no"] = detail.get("orderTradeNo")
|
||||
row["order_settle_number"] = detail.get("orderSettleNumber")
|
||||
row["settle_type"] = to_int(detail.get("settleType"))
|
||||
row["receipt_no"] = detail.get("receiptNo")
|
||||
row["receipt_time"] = detail.get("receiptTime")
|
||||
row["total_amount"] = detail.get("totalAmount") or detail.get("ledgerAmount")
|
||||
row["discount_amount"] = detail.get("discountAmount")
|
||||
row["final_amount"] = detail.get("finalAmount")
|
||||
row["actual_payment"] = detail.get("actualPayment")
|
||||
row["ledger_amount"] = detail.get("ledgerAmount")
|
||||
row["member_offer_amount"] = detail.get("memberOfferAmount")
|
||||
row["delivery_fee"] = detail.get("deliveryFee")
|
||||
row["adjust_amount"] = detail.get("adjustAmount")
|
||||
row["payment_method"] = detail.get("paymentMethod")
|
||||
row["pay_time"] = detail.get("payTime")
|
||||
row["member_id"] = detail.get("memberId")
|
||||
row["order_remark"] = detail.get("orderRemark")
|
||||
row["cashier_name"] = detail.get("cashierName")
|
||||
row["ticket_remark"] = detail.get("ticketRemark")
|
||||
row["ticket_custom_content"] = detail.get("ticketCustomContent")
|
||||
row["voucher_money"] = detail.get("voucherMoney")
|
||||
row["reward_name"] = detail.get("rewardName")
|
||||
row["consume_money"] = detail.get("consumeMoney")
|
||||
row["refund_amount"] = detail.get("refundAmount")
|
||||
row["balance_amount"] = detail.get("balanceAmount")
|
||||
row["coupon_amount"] = detail.get("couponAmount")
|
||||
row["member_deduct_amount"] = detail.get("memberDeductAmount")
|
||||
row["prepay_money"] = detail.get("prepayMoney")
|
||||
row["delivery_address"] = detail.get("deliveryAddress")
|
||||
row["snapshot_raw"] = detail.get("siteProfile") or record.get("site_profile")
|
||||
row["member_snapshot"] = detail.get("memberProfile")
|
||||
if isinstance(row.get("snapshot_raw"), (dict, list)):
|
||||
row["snapshot_raw"] = json.dumps(row["snapshot_raw"], ensure_ascii=False)
|
||||
if isinstance(row.get("member_snapshot"), (dict, list)):
|
||||
row["member_snapshot"] = json.dumps(row["member_snapshot"], ensure_ascii=False)
|
||||
|
||||
if "ods_order_settle" in table:
|
||||
settle = record.get("settleList") if isinstance(record.get("settleList"), dict) else record
|
||||
settle_node = record.get("settleList") or record.get("settle")
|
||||
if not settle_node and isinstance(record.get("data"), dict):
|
||||
settle_node = record["data"].get("settleList") or record["data"].get("settle")
|
||||
if isinstance(settle_node, list):
|
||||
settle = settle_node[0] if settle_node else {}
|
||||
else:
|
||||
settle = settle_node or record
|
||||
if isinstance(settle, dict):
|
||||
row["site_id"] = row.get("site_id") or settle.get("siteId")
|
||||
row["tenant_id"] = pick(settle, "tenantId", "tenant_id")
|
||||
row["settle_relate_id"] = settle.get("settleRelateId")
|
||||
row["settle_name"] = settle.get("settleName")
|
||||
row["settle_type"] = settle.get("settleType")
|
||||
row["settle_status"] = settle.get("settleStatus")
|
||||
row["member_id"] = settle.get("memberId")
|
||||
row["member_name"] = settle.get("memberName")
|
||||
row["member_phone"] = settle.get("memberPhone")
|
||||
row["table_id"] = settle.get("tableId")
|
||||
row["consume_money"] = settle.get("consumeMoney")
|
||||
@@ -663,6 +898,9 @@ class ManualIngestTask(BaseTask):
|
||||
row["assistant_pd_money"] = settle.get("assistantPdMoney")
|
||||
row["assistant_cx_money"] = settle.get("assistantCxMoney")
|
||||
row["pay_amount"] = settle.get("payAmount")
|
||||
row["cash_amount"] = settle.get("cashAmount")
|
||||
row["online_amount"] = settle.get("onlineAmount")
|
||||
row["point_amount"] = settle.get("pointAmount")
|
||||
row["coupon_amount"] = settle.get("couponAmount")
|
||||
row["card_amount"] = settle.get("cardAmount")
|
||||
row["balance_amount"] = settle.get("balanceAmount")
|
||||
@@ -670,9 +908,33 @@ class ManualIngestTask(BaseTask):
|
||||
row["prepay_money"] = settle.get("prepayMoney")
|
||||
row["adjust_amount"] = settle.get("adjustAmount")
|
||||
row["rounding_amount"] = settle.get("roundingAmount")
|
||||
row["member_discount_amount"] = settle.get("memberDiscountAmount")
|
||||
row["coupon_sale_amount"] = settle.get("couponSaleAmount")
|
||||
row["goods_promotion_money"] = settle.get("goodsPromotionMoney")
|
||||
row["assistant_promotion_money"] = settle.get("assistantPromotionMoney")
|
||||
row["point_discount_price"] = settle.get("pointDiscountPrice")
|
||||
row["point_discount_cost"] = settle.get("pointDiscountCost")
|
||||
row["real_goods_money"] = settle.get("realGoodsMoney")
|
||||
row["assistant_manual_discount"] = settle.get("assistantManualDiscount")
|
||||
row["all_coupon_discount"] = settle.get("allCouponDiscount")
|
||||
row["is_use_coupon"] = to_bool(settle.get("isUseCoupon"))
|
||||
row["is_use_discount"] = to_bool(settle.get("isUseDiscount"))
|
||||
row["is_activity"] = to_bool(settle.get("isActivity"))
|
||||
row["is_bind_member"] = to_bool(settle.get("isBindMember"))
|
||||
row["is_first"] = to_bool(settle.get("isFirst"))
|
||||
row["recharge_card_amount"] = settle.get("rechargeCardAmount")
|
||||
row["gift_card_amount"] = settle.get("giftCardAmount")
|
||||
row["payment_method"] = settle.get("paymentMethod")
|
||||
row["create_time"] = settle.get("createTime")
|
||||
row["pay_time"] = settle.get("payTime")
|
||||
row["revoke_order_id"] = settle.get("revokeOrderId")
|
||||
row["revoke_order_name"] = settle.get("revokeOrderName")
|
||||
row["revoke_time"] = settle.get("revokeTime")
|
||||
row["can_be_revoked"] = settle.get("canBeRevoked")
|
||||
row["serial_number"] = settle.get("serialNumber")
|
||||
row["sales_man_name"] = settle.get("salesManName")
|
||||
row["sales_man_user_id"] = settle.get("salesManUserId")
|
||||
row["order_remark"] = settle.get("orderRemark")
|
||||
row["operator_id"] = settle.get("operatorId")
|
||||
row["operator_name"] = settle.get("operatorName")
|
||||
|
||||
@@ -683,12 +945,32 @@ class ManualIngestTask(BaseTask):
|
||||
row["goods_code"] = record.get("goodsCode")
|
||||
row["category_id"] = record.get("categoryId")
|
||||
row["category_name"] = record.get("categoryName")
|
||||
row["goods_second_category_id"] = record.get("goods_second_category_id") or record.get("goodsSecondCategoryId")
|
||||
row["unit"] = record.get("unit")
|
||||
row["price"] = record.get("price")
|
||||
row["cost_price"] = record.get("cost_price")
|
||||
row["market_price"] = record.get("market_price")
|
||||
row["goods_state"] = record.get("goods_state")
|
||||
row["goods_cover"] = record.get("goods_cover")
|
||||
row["goods_bar_code"] = record.get("goods_bar_code")
|
||||
row["able_discount"] = record.get("able_discount")
|
||||
row["is_delete"] = record.get("is_delete")
|
||||
row["status"] = record.get("status")
|
||||
|
||||
if "ods_platform_coupon_log" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
row["platform_code"] = record.get("platformCode")
|
||||
row["verify_code"] = record.get("verifyCode")
|
||||
row["coupon_code"] = record.get("coupon_code")
|
||||
row["coupon_channel"] = record.get("coupon_channel")
|
||||
row["order_trade_no"] = record.get("orderTradeNo")
|
||||
row["order_settle_id"] = record.get("orderSettleId")
|
||||
row["member_id"] = record.get("memberId")
|
||||
row["status"] = record.get("use_status") or record.get("status")
|
||||
row["used_time"] = record.get("consume_time") or record.get("usedTime")
|
||||
row["deduct_amount"] = record.get("deductAmount")
|
||||
row["settle_price"] = record.get("sale_price") or record.get("settlePrice")
|
||||
row["coupon_money"] = record.get("coupon_money")
|
||||
|
||||
if "ods_table_use_log" in table:
|
||||
row["tenant_id"] = pick(record, "tenantId", "tenant_id")
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
class MembersDwdTask(BaseDwdTask):
|
||||
"""
|
||||
DWD Task: Process Member Records from ODS to Dimension Table
|
||||
Source: billiards_ods.ods_member
|
||||
Source: billiards_ods.ods_member_profile
|
||||
Target: billiards.dim_member
|
||||
"""
|
||||
|
||||
@@ -29,8 +29,8 @@ class MembersDwdTask(BaseDwdTask):
|
||||
|
||||
# Iterate ODS Data
|
||||
batches = self.iter_ods_rows(
|
||||
table_name="billiards_ods.ods_member",
|
||||
columns=["store_id", "member_id", "payload", "fetched_at"],
|
||||
table_name="billiards_ods.ods_member_profile",
|
||||
columns=["site_id", "member_id", "payload", "fetched_at"],
|
||||
start_time=window_start,
|
||||
end_time=window_end
|
||||
)
|
||||
|
||||
@@ -41,11 +41,15 @@ class OdsTaskSpec:
|
||||
include_page_no: bool = False
|
||||
include_source_file: bool = True
|
||||
include_source_endpoint: bool = True
|
||||
include_record_index: bool = False
|
||||
include_site_column: bool = True
|
||||
include_fetched_at: bool = True
|
||||
requires_window: bool = True
|
||||
time_fields: Tuple[str, str] | None = ("startTime", "endTime")
|
||||
include_site_id: bool = True
|
||||
description: str = ""
|
||||
extra_params: Dict[str, Any] = field(default_factory=dict)
|
||||
conflict_columns_override: Tuple[str, ...] | None = None
|
||||
|
||||
|
||||
class BaseOdsTask(BaseTask):
|
||||
@@ -67,7 +71,13 @@ class BaseOdsTask(BaseTask):
|
||||
page_size = self.config.get("api.page_size", 200)
|
||||
params = self._build_params(spec, store_id)
|
||||
columns = self._resolve_columns(spec)
|
||||
conflict_columns = ["site_id"] + [col.column for col in spec.pk_columns]
|
||||
if spec.conflict_columns_override:
|
||||
conflict_columns = list(spec.conflict_columns_override)
|
||||
else:
|
||||
conflict_columns = []
|
||||
if spec.include_site_column:
|
||||
conflict_columns.append("site_id")
|
||||
conflict_columns += [col.column for col in spec.pk_columns]
|
||||
loader = GenericODSLoader(
|
||||
self.db,
|
||||
spec.table_name,
|
||||
@@ -79,6 +89,7 @@ class BaseOdsTask(BaseTask):
|
||||
source_file = self._resolve_source_file_hint(spec)
|
||||
|
||||
try:
|
||||
global_index = 0
|
||||
for page_no, page_records, _, _ in self.api.iter_paginated(
|
||||
endpoint=spec.endpoint,
|
||||
params=params,
|
||||
@@ -97,11 +108,13 @@ class BaseOdsTask(BaseTask):
|
||||
if spec.include_page_size
|
||||
else None,
|
||||
source_file=source_file,
|
||||
record_index=global_index if spec.include_record_index else None,
|
||||
)
|
||||
if row is None:
|
||||
counts["skipped"] += 1
|
||||
continue
|
||||
rows.append(row)
|
||||
global_index += 1
|
||||
|
||||
inserted, updated, _ = loader.upsert_rows(rows)
|
||||
counts["inserted"] += inserted
|
||||
@@ -133,13 +146,19 @@ class BaseOdsTask(BaseTask):
|
||||
return params
|
||||
|
||||
def _resolve_columns(self, spec: OdsTaskSpec) -> List[str]:
|
||||
columns: List[str] = ["site_id"]
|
||||
columns: List[str] = []
|
||||
if spec.include_site_column:
|
||||
columns.append("site_id")
|
||||
seen = set(columns)
|
||||
for col_spec in list(spec.pk_columns) + list(spec.extra_columns):
|
||||
if col_spec.column not in seen:
|
||||
columns.append(col_spec.column)
|
||||
seen.add(col_spec.column)
|
||||
|
||||
if spec.include_record_index and "record_index" not in seen:
|
||||
columns.append("record_index")
|
||||
seen.add("record_index")
|
||||
|
||||
if spec.include_page_no and "page_no" not in seen:
|
||||
columns.append("page_no")
|
||||
seen.add("page_no")
|
||||
@@ -156,7 +175,7 @@ class BaseOdsTask(BaseTask):
|
||||
columns.append("source_endpoint")
|
||||
seen.add("source_endpoint")
|
||||
|
||||
if "fetched_at" not in seen:
|
||||
if spec.include_fetched_at and "fetched_at" not in seen:
|
||||
columns.append("fetched_at")
|
||||
seen.add("fetched_at")
|
||||
if "payload" not in seen:
|
||||
@@ -172,8 +191,11 @@ class BaseOdsTask(BaseTask):
|
||||
page_no: int | None,
|
||||
page_size_value: int | None,
|
||||
source_file: str | None,
|
||||
record_index: int | None = None,
|
||||
) -> dict | None:
|
||||
row: dict[str, Any] = {"site_id": store_id}
|
||||
row: dict[str, Any] = {}
|
||||
if spec.include_site_column:
|
||||
row["site_id"] = store_id
|
||||
|
||||
for col_spec in spec.pk_columns + spec.extra_columns:
|
||||
value = self._extract_value(record, col_spec)
|
||||
@@ -191,11 +213,14 @@ class BaseOdsTask(BaseTask):
|
||||
row["page_no"] = page_no
|
||||
if spec.include_page_size:
|
||||
row["page_size"] = page_size_value
|
||||
if spec.include_record_index:
|
||||
row["record_index"] = record_index
|
||||
if spec.include_source_file:
|
||||
row["source_file"] = source_file
|
||||
if spec.include_source_endpoint:
|
||||
row["source_endpoint"] = spec.endpoint
|
||||
|
||||
if spec.include_fetched_at:
|
||||
row["fetched_at"] = datetime.now(self.tz)
|
||||
row["payload"] = record
|
||||
return row
|
||||
@@ -241,178 +266,629 @@ def _int_col(name: str, *sources: str, required: bool = False) -> ColumnSpec:
|
||||
|
||||
|
||||
ODS_TASK_SPECS: Tuple[OdsTaskSpec, ...] = (
|
||||
OdsTaskSpec(
|
||||
code="ODS_ASSISTANT_ACCOUNTS",
|
||||
class_name="OdsAssistantAccountsTask",
|
||||
table_name="billiards_ods.assistant_accounts_master",
|
||||
endpoint="/PersonnelManagement/SearchAssistantInfo",
|
||||
data_path=("data",),
|
||||
list_key="assistantInfos",
|
||||
pk_columns=(_int_col("id", "id", required=True),),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
description="助教账号档案 ODS:SearchAssistantInfo -> assistantInfos 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_ORDER_SETTLE",
|
||||
class_name="OdsOrderSettleTask",
|
||||
table_name="billiards_ods.ods_order_settle",
|
||||
table_name="billiards_ods.settlement_records",
|
||||
endpoint="/Site/GetAllOrderSettleList",
|
||||
data_path=("data",),
|
||||
list_key="settleList",
|
||||
pk_columns=(
|
||||
_int_col(
|
||||
"order_settle_id",
|
||||
"orderSettleId",
|
||||
"order_settle_id",
|
||||
"settleList.id",
|
||||
"id",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
extra_columns=(
|
||||
_int_col("order_trade_no", "orderTradeNo", "order_trade_no", "settleList.orderTradeNo"),
|
||||
),
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
time_fields=("rangeStartTime", "rangeEndTime"),
|
||||
description="订单/结算 ODS 原始记录",
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="结账记录 ODS:GetAllOrderSettleList -> settleList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_TABLE_USE",
|
||||
class_name="OdsTableUseTask",
|
||||
table_name="billiards_ods.ods_table_use_log",
|
||||
table_name="billiards_ods.table_fee_transactions",
|
||||
endpoint="/Site/GetSiteTableOrderDetails",
|
||||
data_path=("data",),
|
||||
list_key="siteTableUseDetailsList",
|
||||
pk_columns=(_int_col("ledger_id", "id", required=True),),
|
||||
extra_columns=(
|
||||
_int_col("order_trade_no", "order_trade_no", "orderTradeNo"),
|
||||
_int_col("order_settle_id", "order_settle_id", "orderSettleId"),
|
||||
),
|
||||
description="台费/开台流水 ODS",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="台费计费流水 ODS:GetSiteTableOrderDetails -> siteTableUseDetailsList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_ASSISTANT_LEDGER",
|
||||
class_name="OdsAssistantLedgerTask",
|
||||
table_name="billiards_ods.ods_assistant_service_log",
|
||||
table_name="billiards_ods.assistant_service_records",
|
||||
endpoint="/AssistantPerformance/GetOrderAssistantDetails",
|
||||
data_path=("data",),
|
||||
list_key="orderAssistantDetails",
|
||||
pk_columns=(_int_col("ledger_id", "id", required=True),),
|
||||
extra_columns=(
|
||||
_int_col("order_trade_no", "order_trade_no", "orderTradeNo"),
|
||||
_int_col("order_settle_id", "order_settle_id", "orderSettleId"),
|
||||
),
|
||||
description="助教流水 ODS",
|
||||
pk_columns=(_int_col("id", "id", required=True),),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
description="助教服务流水 ODS:GetOrderAssistantDetails -> orderAssistantDetails 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_ASSISTANT_ABOLISH",
|
||||
class_name="OdsAssistantAbolishTask",
|
||||
table_name="billiards_ods.ods_assistant_cancel_log",
|
||||
table_name="billiards_ods.assistant_cancellation_records",
|
||||
endpoint="/AssistantPerformance/GetAbolitionAssistant",
|
||||
data_path=("data",),
|
||||
list_key="abolitionAssistants",
|
||||
pk_columns=(_int_col("abolish_id", "id", required=True),),
|
||||
description="助教作废记录 ODS",
|
||||
pk_columns=(_int_col("id", "id", required=True),),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
description="助教废除记录 ODS:GetAbolitionAssistant -> abolitionAssistants 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_GOODS_LEDGER",
|
||||
class_name="OdsGoodsLedgerTask",
|
||||
table_name="billiards_ods.ods_store_sale_item",
|
||||
table_name="billiards_ods.store_goods_sales_records",
|
||||
endpoint="/TenantGoods/GetGoodsSalesList",
|
||||
data_path=("data",),
|
||||
list_key="orderGoodsLedgers",
|
||||
pk_columns=(_int_col("order_goods_id", "orderGoodsId", "id", required=True),),
|
||||
extra_columns=(
|
||||
_int_col("order_trade_no", "order_trade_no", "orderTradeNo"),
|
||||
_int_col("order_settle_id", "order_settle_id", "orderSettleId"),
|
||||
),
|
||||
description="商品销售流水 ODS",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="门店商品销售流水 ODS:GetGoodsSalesList -> orderGoodsLedgers 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_PAYMENT",
|
||||
class_name="OdsPaymentTask",
|
||||
table_name="billiards_ods.ods_payment_record",
|
||||
table_name="billiards_ods.payment_transactions",
|
||||
endpoint="/PayLog/GetPayLogListPage",
|
||||
data_path=("data",),
|
||||
pk_columns=(_int_col("pay_id", "payId", "id", required=True),),
|
||||
extra_columns=(
|
||||
ColumnSpec(column="relate_type", sources=("relate_type", "relateType")),
|
||||
_int_col("relate_id", "relate_id", "relateId"),
|
||||
),
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
time_fields=("StartPayTime", "EndPayTime"),
|
||||
description="支付流水 ODS",
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="支付流水 ODS:GetPayLogListPage 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_REFUND",
|
||||
class_name="OdsRefundTask",
|
||||
table_name="billiards_ods.ods_refund_record",
|
||||
table_name="billiards_ods.refund_transactions",
|
||||
endpoint="/Order/GetRefundPayLogList",
|
||||
data_path=("data",),
|
||||
pk_columns=(_int_col("refund_id", "id", required=True),),
|
||||
extra_columns=(
|
||||
ColumnSpec(column="relate_type", sources=("relate_type", "relateType")),
|
||||
_int_col("relate_id", "relate_id", "relateId"),
|
||||
),
|
||||
description="退款流水 ODS",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="退款流水 ODS:GetRefundPayLogList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_COUPON_VERIFY",
|
||||
class_name="OdsCouponVerifyTask",
|
||||
table_name="billiards_ods.ods_platform_coupon_log",
|
||||
table_name="billiards_ods.platform_coupon_redemption_records",
|
||||
endpoint="/Promotion/GetOfflineCouponConsumePageList",
|
||||
data_path=("data",),
|
||||
pk_columns=(_int_col("coupon_id", "id", "couponId", required=True),),
|
||||
description="平台验券/团购流水 ODS",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="平台/团购券核销 ODS:GetOfflineCouponConsumePageList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_MEMBER",
|
||||
class_name="OdsMemberTask",
|
||||
table_name="billiards_ods.ods_member_profile",
|
||||
table_name="billiards_ods.member_profiles",
|
||||
endpoint="/MemberProfile/GetTenantMemberList",
|
||||
data_path=("data",),
|
||||
list_key="tenantMemberInfos",
|
||||
pk_columns=(_int_col("member_id", "memberId", required=True),),
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="会员档案 ODS",
|
||||
description="会员档案 ODS:GetTenantMemberList -> tenantMemberInfos 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_MEMBER_CARD",
|
||||
class_name="OdsMemberCardTask",
|
||||
table_name="billiards_ods.ods_member_card",
|
||||
table_name="billiards_ods.member_stored_value_cards",
|
||||
endpoint="/MemberProfile/GetTenantMemberCardList",
|
||||
data_path=("data",),
|
||||
list_key="tenantMemberCards",
|
||||
pk_columns=(_int_col("card_id", "tenantMemberCardId", "cardId", required=True),),
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="会员卡/储值卡 ODS",
|
||||
description="会员储值卡 ODS:GetTenantMemberCardList -> tenantMemberCards 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_MEMBER_BALANCE",
|
||||
class_name="OdsMemberBalanceTask",
|
||||
table_name="billiards_ods.member_balance_changes",
|
||||
endpoint="/MemberProfile/GetMemberCardBalanceChange",
|
||||
data_path=("data",),
|
||||
list_key="tenantMemberCardLogs",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="会员余额变动 ODS:GetMemberCardBalanceChange -> tenantMemberCardLogs 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_RECHARGE_SETTLE",
|
||||
class_name="OdsRechargeSettleTask",
|
||||
table_name="billiards_ods.recharge_settlements",
|
||||
endpoint="/Site/GetRechargeSettleList",
|
||||
data_path=("data",),
|
||||
list_key="settleList",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="会员充值结算 ODS:GetRechargeSettleList -> settleList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_PACKAGE",
|
||||
class_name="OdsPackageTask",
|
||||
table_name="billiards_ods.ods_group_package",
|
||||
table_name="billiards_ods.group_buy_packages",
|
||||
endpoint="/PackageCoupon/QueryPackageCouponList",
|
||||
data_path=("data",),
|
||||
list_key="packageCouponList",
|
||||
pk_columns=(_int_col("package_id", "id", "packageId", required=True),),
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="团购/套餐定义 ODS",
|
||||
description="团购套餐定义 ODS:QueryPackageCouponList -> packageCouponList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_GROUP_BUY_REDEMPTION",
|
||||
class_name="OdsGroupBuyRedemptionTask",
|
||||
table_name="billiards_ods.group_buy_redemption_records",
|
||||
endpoint="/Site/GetSiteTableUseDetails",
|
||||
data_path=("data",),
|
||||
list_key="siteTableUseDetailsList",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="团购套餐核销 ODS:GetSiteTableUseDetails -> siteTableUseDetailsList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_INVENTORY_STOCK",
|
||||
class_name="OdsInventoryStockTask",
|
||||
table_name="billiards_ods.ods_inventory_stock",
|
||||
table_name="billiards_ods.goods_stock_summary",
|
||||
endpoint="/TenantGoods/GetGoodsStockReport",
|
||||
data_path=("data",),
|
||||
pk_columns=(
|
||||
_int_col("site_goods_id", "siteGoodsId", required=True),
|
||||
ColumnSpec(column="snapshot_key", default="default", required=True),
|
||||
),
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="库存汇总 ODS",
|
||||
description="库存汇总 ODS:GetGoodsStockReport 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_INVENTORY_CHANGE",
|
||||
class_name="OdsInventoryChangeTask",
|
||||
table_name="billiards_ods.ods_inventory_change",
|
||||
table_name="billiards_ods.goods_stock_movements",
|
||||
endpoint="/GoodsStockManage/QueryGoodsOutboundReceipt",
|
||||
data_path=("data",),
|
||||
list_key="queryDeliveryRecordsList",
|
||||
pk_columns=(_int_col("change_id", "siteGoodsStockId", "id", required=True),),
|
||||
description="库存变动 ODS",
|
||||
pk_columns=(_int_col("sitegoodsstockid", "siteGoodsStockId", required=True),),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
description="库存变化记录 ODS:QueryGoodsOutboundReceipt -> queryDeliveryRecordsList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_TABLES",
|
||||
class_name="OdsTablesTask",
|
||||
table_name="billiards_ods.site_tables_master",
|
||||
endpoint="/Table/GetSiteTables",
|
||||
data_path=("data",),
|
||||
list_key="siteTables",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="台桌维表 ODS:GetSiteTables -> siteTables 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_GOODS_CATEGORY",
|
||||
class_name="OdsGoodsCategoryTask",
|
||||
table_name="billiards_ods.stock_goods_category_tree",
|
||||
endpoint="/TenantGoodsCategory/QueryPrimarySecondaryCategory",
|
||||
data_path=("data",),
|
||||
list_key="goodsCategoryList",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="库存商品分类树 ODS:QueryPrimarySecondaryCategory -> goodsCategoryList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_STORE_GOODS",
|
||||
class_name="OdsStoreGoodsTask",
|
||||
table_name="billiards_ods.store_goods_master",
|
||||
endpoint="/TenantGoods/GetGoodsInventoryList",
|
||||
data_path=("data",),
|
||||
list_key="orderGoodsList",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="门店商品档案 ODS:GetGoodsInventoryList -> orderGoodsList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_TABLE_DISCOUNT",
|
||||
class_name="OdsTableDiscountTask",
|
||||
table_name="billiards_ods.table_fee_discount_records",
|
||||
endpoint="/Site/GetTaiFeeAdjustList",
|
||||
data_path=("data",),
|
||||
list_key="taiFeeAdjustInfos",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="台费折扣/调账 ODS:GetTaiFeeAdjustList -> taiFeeAdjustInfos 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_TENANT_GOODS",
|
||||
class_name="OdsTenantGoodsTask",
|
||||
table_name="billiards_ods.tenant_goods_master",
|
||||
endpoint="/TenantGoods/QueryTenantGoods",
|
||||
data_path=("data",),
|
||||
list_key="tenantGoodsList",
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=False,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
description="租户商品档案 ODS:QueryTenantGoods -> tenantGoodsList 原始 JSON",
|
||||
),
|
||||
OdsTaskSpec(
|
||||
code="ODS_SETTLEMENT_TICKET",
|
||||
class_name="OdsSettlementTicketTask",
|
||||
table_name="billiards_ods.settlement_ticket_details",
|
||||
endpoint="/Order/GetOrderSettleTicketNew",
|
||||
data_path=(),
|
||||
list_key=None,
|
||||
pk_columns=(),
|
||||
include_site_column=False,
|
||||
include_source_endpoint=False,
|
||||
include_page_no=False,
|
||||
include_page_size=False,
|
||||
include_fetched_at=True,
|
||||
include_record_index=True,
|
||||
conflict_columns_override=("source_file", "record_index"),
|
||||
requires_window=False,
|
||||
include_site_id=False,
|
||||
description="结账小票详情 ODS:GetOrderSettleTicketNew 原始 JSON",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_spec(code: str) -> OdsTaskSpec:
|
||||
for spec in ODS_TASK_SPECS:
|
||||
if spec.code == code:
|
||||
return spec
|
||||
raise KeyError(f"Spec not found for code {code}")
|
||||
|
||||
|
||||
_SETTLEMENT_TICKET_SPEC = _get_spec("ODS_SETTLEMENT_TICKET")
|
||||
|
||||
|
||||
class OdsSettlementTicketTask(BaseOdsTask):
|
||||
"""Special handling: fetch ticket details per payment relate_id/orderSettleId."""
|
||||
|
||||
SPEC = _SETTLEMENT_TICKET_SPEC
|
||||
|
||||
def extract(self, context) -> dict:
|
||||
"""Fetch ticket payloads only (used by fetch-only pipeline)."""
|
||||
existing_ids = self._fetch_existing_ticket_ids()
|
||||
candidates = self._collect_settlement_ids(
|
||||
context.store_id or 0, existing_ids, context.window_start, context.window_end
|
||||
)
|
||||
candidates = [cid for cid in candidates if cid and cid not in existing_ids]
|
||||
payloads, skipped = self._fetch_ticket_payloads(candidates)
|
||||
return {"records": payloads, "skipped": skipped, "fetched": len(candidates)}
|
||||
|
||||
def execute(self, cursor_data: dict | None = None) -> dict:
|
||||
spec = self.SPEC
|
||||
context = self._build_context(cursor_data)
|
||||
store_id = TypeParser.parse_int(self.config.get("app.store_id")) or 0
|
||||
|
||||
counts = {"fetched": 0, "inserted": 0, "updated": 0, "skipped": 0, "errors": 0}
|
||||
loader = GenericODSLoader(
|
||||
self.db,
|
||||
spec.table_name,
|
||||
self._resolve_columns(spec),
|
||||
list(spec.conflict_columns_override or ("source_file", "record_index")),
|
||||
)
|
||||
source_file = self._resolve_source_file_hint(spec)
|
||||
|
||||
try:
|
||||
existing_ids = self._fetch_existing_ticket_ids()
|
||||
candidates = self._collect_settlement_ids(
|
||||
store_id, existing_ids, context.window_start, context.window_end
|
||||
)
|
||||
candidates = [cid for cid in candidates if cid and cid not in existing_ids]
|
||||
counts["fetched"] = len(candidates)
|
||||
|
||||
if not candidates:
|
||||
self.logger.info(
|
||||
"%s: 窗口[%s ~ %s] 未发现需要抓取的小票",
|
||||
spec.code,
|
||||
context.window_start,
|
||||
context.window_end,
|
||||
)
|
||||
return self._build_result("SUCCESS", counts)
|
||||
|
||||
payloads, skipped = self._fetch_ticket_payloads(candidates)
|
||||
counts["skipped"] += skipped
|
||||
rows: list[dict] = []
|
||||
for idx, payload in enumerate(payloads):
|
||||
row = self._build_row(
|
||||
spec=spec,
|
||||
store_id=store_id,
|
||||
record=payload,
|
||||
page_no=None,
|
||||
page_size_value=None,
|
||||
source_file=source_file,
|
||||
record_index=idx if spec.include_record_index else None,
|
||||
)
|
||||
if row is None:
|
||||
counts["skipped"] += 1
|
||||
continue
|
||||
rows.append(row)
|
||||
|
||||
inserted, updated, _ = loader.upsert_rows(rows)
|
||||
counts["inserted"] += inserted
|
||||
counts["updated"] += updated
|
||||
self.db.commit()
|
||||
self.logger.info(
|
||||
"%s: 小票抓取完成,候选=%s 插入=%s 更新=%s 跳过=%s",
|
||||
spec.code,
|
||||
len(candidates),
|
||||
inserted,
|
||||
updated,
|
||||
counts["skipped"],
|
||||
)
|
||||
return self._build_result("SUCCESS", counts)
|
||||
|
||||
except Exception:
|
||||
counts["errors"] += 1
|
||||
self.db.rollback()
|
||||
self.logger.error("%s: 小票抓取失败", spec.code, exc_info=True)
|
||||
raise
|
||||
|
||||
# ------------------------------------------------------------------ helpers
|
||||
def _fetch_existing_ticket_ids(self) -> set[int]:
|
||||
sql = """
|
||||
SELECT DISTINCT
|
||||
CASE WHEN (payload ->> 'orderSettleId') ~ '^[0-9]+$'
|
||||
THEN (payload ->> 'orderSettleId')::bigint
|
||||
END AS order_settle_id
|
||||
FROM billiards_ods.settlement_ticket_details
|
||||
"""
|
||||
try:
|
||||
rows = self.db.query(sql)
|
||||
except Exception:
|
||||
self.logger.warning("查询已有小票失败,按空集处理", exc_info=True)
|
||||
return set()
|
||||
|
||||
return {
|
||||
TypeParser.parse_int(row.get("order_settle_id"))
|
||||
for row in rows
|
||||
if row.get("order_settle_id") is not None
|
||||
}
|
||||
|
||||
def _collect_settlement_ids(
|
||||
self, store_id: int, existing_ids: set[int], window_start, window_end
|
||||
) -> list[int]:
|
||||
ids = self._fetch_from_payment_table(store_id)
|
||||
if not ids:
|
||||
ids = self._fetch_from_payment_api(store_id, window_start, window_end)
|
||||
return sorted(i for i in ids if i is not None and i not in existing_ids)
|
||||
|
||||
def _fetch_from_payment_table(self, store_id: int) -> set[int]:
|
||||
sql = """
|
||||
SELECT DISTINCT COALESCE(
|
||||
CASE WHEN (payload ->> 'orderSettleId') ~ '^[0-9]+$'
|
||||
THEN (payload ->> 'orderSettleId')::bigint END,
|
||||
CASE WHEN (payload ->> 'relateId') ~ '^[0-9]+$'
|
||||
THEN (payload ->> 'relateId')::bigint END
|
||||
) AS order_settle_id
|
||||
FROM billiards_ods.payment_transactions
|
||||
WHERE (payload ->> 'orderSettleId') ~ '^[0-9]+$'
|
||||
OR (payload ->> 'relateId') ~ '^[0-9]+$'
|
||||
"""
|
||||
params = None
|
||||
if store_id:
|
||||
sql += " AND COALESCE((payload ->> 'siteId')::bigint, %s) = %s"
|
||||
params = (store_id, store_id)
|
||||
|
||||
try:
|
||||
rows = self.db.query(sql, params)
|
||||
except Exception:
|
||||
self.logger.warning("读取支付流水以获取结算单ID失败,将尝试调用支付接口回退", exc_info=True)
|
||||
return set()
|
||||
|
||||
return {
|
||||
TypeParser.parse_int(row.get("order_settle_id"))
|
||||
for row in rows
|
||||
if row.get("order_settle_id") is not None
|
||||
}
|
||||
|
||||
def _fetch_from_payment_api(self, store_id: int, window_start, window_end) -> set[int]:
|
||||
params = self._merge_common_params(
|
||||
{
|
||||
"siteId": store_id,
|
||||
"StartPayTime": TypeParser.format_timestamp(window_start, self.tz),
|
||||
"EndPayTime": TypeParser.format_timestamp(window_end, self.tz),
|
||||
}
|
||||
)
|
||||
candidate_ids: set[int] = set()
|
||||
try:
|
||||
for _, records, _, _ in self.api.iter_paginated(
|
||||
endpoint="/PayLog/GetPayLogListPage",
|
||||
params=params,
|
||||
page_size=self.config.get("api.page_size", 200),
|
||||
data_path=("data",),
|
||||
):
|
||||
for rec in records:
|
||||
relate_id = TypeParser.parse_int(
|
||||
(rec or {}).get("relateId")
|
||||
or (rec or {}).get("orderSettleId")
|
||||
or (rec or {}).get("order_settle_id")
|
||||
)
|
||||
if relate_id:
|
||||
candidate_ids.add(relate_id)
|
||||
except Exception:
|
||||
self.logger.warning("调用支付接口获取结算单ID失败,当前批次将跳过回退来源", exc_info=True)
|
||||
return candidate_ids
|
||||
|
||||
def _fetch_ticket_payload(self, order_settle_id: int):
|
||||
payload = None
|
||||
try:
|
||||
for _, _, _, response in self.api.iter_paginated(
|
||||
endpoint=self.SPEC.endpoint,
|
||||
params={"orderSettleId": order_settle_id},
|
||||
page_size=None,
|
||||
data_path=self.SPEC.data_path,
|
||||
list_key=self.SPEC.list_key,
|
||||
):
|
||||
payload = response
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
"调用小票接口失败 orderSettleId=%s", order_settle_id, exc_info=True
|
||||
)
|
||||
if isinstance(payload, dict) and isinstance(payload.get("data"), list) and len(payload["data"]) == 1:
|
||||
# 本地桩/回放可能把响应包装成单元素 list,这里展开以贴近真实结构
|
||||
payload = payload["data"][0]
|
||||
return payload
|
||||
|
||||
def _fetch_ticket_payloads(self, candidates: list[int]) -> tuple[list, int]:
|
||||
"""Fetch ticket payloads for a set of orderSettleIds; returns (payloads, skipped)."""
|
||||
payloads: list = []
|
||||
skipped = 0
|
||||
for order_settle_id in candidates:
|
||||
payload = self._fetch_ticket_payload(order_settle_id)
|
||||
if payload:
|
||||
payloads.append(payload)
|
||||
else:
|
||||
skipped += 1
|
||||
return payloads, skipped
|
||||
|
||||
|
||||
def _build_task_class(spec: OdsTaskSpec) -> Type[BaseOdsTask]:
|
||||
attrs = {
|
||||
"SPEC": spec,
|
||||
@@ -422,8 +898,36 @@ def _build_task_class(spec: OdsTaskSpec) -> Type[BaseOdsTask]:
|
||||
return type(spec.class_name, (BaseOdsTask,), attrs)
|
||||
|
||||
|
||||
ODS_TASK_CLASSES: Dict[str, Type[BaseOdsTask]] = {
|
||||
spec.code: _build_task_class(spec) for spec in ODS_TASK_SPECS
|
||||
ENABLED_ODS_CODES = {
|
||||
"ODS_ASSISTANT_ACCOUNTS",
|
||||
"ODS_ASSISTANT_LEDGER",
|
||||
"ODS_ASSISTANT_ABOLISH",
|
||||
"ODS_INVENTORY_CHANGE",
|
||||
"ODS_INVENTORY_STOCK",
|
||||
"ODS_PACKAGE",
|
||||
"ODS_GROUP_BUY_REDEMPTION",
|
||||
"ODS_MEMBER",
|
||||
"ODS_MEMBER_BALANCE",
|
||||
"ODS_MEMBER_CARD",
|
||||
"ODS_PAYMENT",
|
||||
"ODS_REFUND",
|
||||
"ODS_COUPON_VERIFY",
|
||||
"ODS_RECHARGE_SETTLE",
|
||||
"ODS_TABLES",
|
||||
"ODS_GOODS_CATEGORY",
|
||||
"ODS_STORE_GOODS",
|
||||
"ODS_TABLE_DISCOUNT",
|
||||
"ODS_TENANT_GOODS",
|
||||
"ODS_SETTLEMENT_TICKET",
|
||||
"ODS_ORDER_SETTLE",
|
||||
}
|
||||
|
||||
__all__ = ["ODS_TASK_CLASSES", "ODS_TASK_SPECS", "BaseOdsTask"]
|
||||
ODS_TASK_CLASSES: Dict[str, Type[BaseOdsTask]] = {
|
||||
spec.code: _build_task_class(spec)
|
||||
for spec in ODS_TASK_SPECS
|
||||
if spec.code in ENABLED_ODS_CODES
|
||||
}
|
||||
# Override with specialized settlement ticket implementation
|
||||
ODS_TASK_CLASSES["ODS_SETTLEMENT_TICKET"] = OdsSettlementTicketTask
|
||||
|
||||
__all__ = ["ODS_TASK_CLASSES", "ODS_TASK_SPECS", "BaseOdsTask", "ENABLED_ODS_CODES"]
|
||||
|
||||
@@ -20,16 +20,17 @@ class PaymentsDwdTask(BaseDwdTask):
|
||||
window_start, window_end, _ = self._get_time_window()
|
||||
self.logger.info(f"Processing window: {window_start} to {window_end}")
|
||||
|
||||
loader = PaymentLoader(self.db)
|
||||
loader = PaymentLoader(self.db, logger=self.logger)
|
||||
store_id = self.config.get("app.store_id")
|
||||
|
||||
total_inserted = 0
|
||||
total_errors = 0
|
||||
total_updated = 0
|
||||
total_skipped = 0
|
||||
|
||||
# Iterate ODS Data
|
||||
batches = self.iter_ods_rows(
|
||||
table_name="billiards_ods.ods_payment",
|
||||
columns=["store_id", "pay_id", "payload", "fetched_at"],
|
||||
table_name="billiards_ods.ods_payment_record",
|
||||
columns=["site_id", "pay_id", "payload", "fetched_at"],
|
||||
start_time=window_start,
|
||||
end_time=window_end
|
||||
)
|
||||
@@ -49,20 +50,30 @@ class PaymentsDwdTask(BaseDwdTask):
|
||||
parsed_rows.append(parsed)
|
||||
|
||||
if parsed_rows:
|
||||
inserted, errors = loader.upsert_payments(parsed_rows, store_id)
|
||||
inserted, updated, skipped = loader.upsert_payments(parsed_rows, store_id)
|
||||
total_inserted += inserted
|
||||
total_errors += errors
|
||||
total_updated += updated
|
||||
total_skipped += skipped
|
||||
|
||||
self.db.commit()
|
||||
|
||||
self.logger.info(f"Task {self.get_task_code()} completed. Inserted: {total_inserted}, Errors: {total_errors}")
|
||||
self.logger.info(
|
||||
"Task %s completed. inserted=%s updated=%s skipped=%s",
|
||||
self.get_task_code(),
|
||||
total_inserted,
|
||||
total_updated,
|
||||
total_skipped,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"status": "SUCCESS",
|
||||
"counts": {
|
||||
"inserted": total_inserted,
|
||||
"errors": total_errors,
|
||||
"window_start": window_start.isoformat(),
|
||||
"window_end": window_end.isoformat()
|
||||
"updated": total_updated,
|
||||
"skipped": total_skipped,
|
||||
},
|
||||
"window_start": window_start,
|
||||
"window_end": window_end,
|
||||
}
|
||||
|
||||
def _parse_payment(self, raw: dict, store_id: int) -> dict:
|
||||
@@ -89,6 +100,7 @@ class PaymentsDwdTask(BaseDwdTask):
|
||||
return {
|
||||
"store_id": store_id,
|
||||
"pay_id": pay_id,
|
||||
"order_id": TypeParser.parse_int(raw.get("orderId") or raw.get("order_id")),
|
||||
"order_settle_id": order_settle_id,
|
||||
"order_trade_no": order_trade_no,
|
||||
"relate_type": relate_type,
|
||||
@@ -114,9 +126,11 @@ class PaymentsDwdTask(BaseDwdTask):
|
||||
or raw.get("discount_amount")
|
||||
),
|
||||
"payment_method": str(raw.get("paymentMethod") or raw.get("payment_method") or ""),
|
||||
"pay_type": raw.get("payType") or raw.get("pay_type"),
|
||||
"online_pay_channel": raw.get("onlinePayChannel") or raw.get("online_pay_channel"),
|
||||
"pay_terminal": raw.get("payTerminal") or raw.get("pay_terminal"),
|
||||
"pay_status": str(raw.get("payStatus") or raw.get("pay_status") or ""),
|
||||
"remark": raw.get("remark"),
|
||||
"raw_data": json.dumps(raw, ensure_ascii=False)
|
||||
}
|
||||
except Exception as e:
|
||||
|
||||
@@ -24,7 +24,7 @@ class TicketDwdTask(BaseDwdTask):
|
||||
self.logger.info(f"Processing window: {window_start} to {window_end}")
|
||||
|
||||
# 2. Initialize Loader
|
||||
loader = TicketLoader(self.db)
|
||||
loader = TicketLoader(self.db, logger=self.logger)
|
||||
store_id = self.config.get("app.store_id")
|
||||
|
||||
total_inserted = 0
|
||||
@@ -33,8 +33,8 @@ class TicketDwdTask(BaseDwdTask):
|
||||
# 3. Iterate ODS Data
|
||||
# We query ods_ticket_detail based on fetched_at
|
||||
batches = self.iter_ods_rows(
|
||||
table_name="billiards_ods.ods_ticket_detail",
|
||||
columns=["store_id", "order_settle_id", "payload", "fetched_at"],
|
||||
table_name="billiards_ods.settlement_ticket_details",
|
||||
columns=["payload", "fetched_at", "source_file", "record_index"],
|
||||
start_time=window_start,
|
||||
end_time=window_end
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,647 +0,0 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"total": 15,
|
||||
"abolitionAssistants": [
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-11-09 19:23:29",
|
||||
"id": 2957675849518789,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963816579205,
|
||||
"tableId": 2793016660660357,
|
||||
"tableArea": "C区",
|
||||
"tableName": "C1",
|
||||
"assistantOn": "27",
|
||||
"assistantName": "泡芙",
|
||||
"pdChargeMinutes": 214,
|
||||
"assistantAbolishAmount": 5.83,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-11-06 17:42:09",
|
||||
"id": 2953329501898373,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2802006170324037,
|
||||
"tableId": 2851642357976581,
|
||||
"tableArea": "补时长",
|
||||
"tableName": "补时长5",
|
||||
"assistantOn": "23",
|
||||
"assistantName": "婉婉",
|
||||
"pdChargeMinutes": 10800,
|
||||
"assistantAbolishAmount": 570.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-11-06 17:42:09",
|
||||
"id": 2953329502357125,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2802006170324037,
|
||||
"tableId": 2851642357976581,
|
||||
"tableArea": "补时长",
|
||||
"tableName": "补时长5",
|
||||
"assistantOn": "52",
|
||||
"assistantName": "小柔",
|
||||
"pdChargeMinutes": 10800,
|
||||
"assistantAbolishAmount": 570.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-30 01:17:22",
|
||||
"id": 2942452375932869,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963825803397,
|
||||
"tableId": 2793018776604805,
|
||||
"tableArea": "VIP包厢",
|
||||
"tableName": "VIP1",
|
||||
"assistantOn": "2",
|
||||
"assistantName": "佳怡",
|
||||
"pdChargeMinutes": 0,
|
||||
"assistantAbolishAmount": 0.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-29 06:57:22",
|
||||
"id": 2941371032964997,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963848527941,
|
||||
"tableId": 2793021451292741,
|
||||
"tableArea": "666",
|
||||
"tableName": "董事办",
|
||||
"assistantOn": "4",
|
||||
"assistantName": "璇子",
|
||||
"pdChargeMinutes": 0,
|
||||
"assistantAbolishAmount": 0.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-28 02:13:18",
|
||||
"id": 2939676194180229,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963887030341,
|
||||
"tableId": 2793023960551493,
|
||||
"tableArea": "麻将房",
|
||||
"tableName": "M1",
|
||||
"assistantOn": "2",
|
||||
"assistantName": "佳怡",
|
||||
"pdChargeMinutes": 3602,
|
||||
"assistantAbolishAmount": 108.06,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-26 21:06:37",
|
||||
"id": 2937959143262725,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963855982661,
|
||||
"tableId": 2793022145302597,
|
||||
"tableArea": "K包",
|
||||
"tableName": "888",
|
||||
"assistantOn": "16",
|
||||
"assistantName": "周周",
|
||||
"pdChargeMinutes": 0,
|
||||
"assistantAbolishAmount": 0.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-25 21:36:22",
|
||||
"id": 2936572806285765,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963816579205,
|
||||
"tableId": 2793017278451845,
|
||||
"tableArea": "C区",
|
||||
"tableName": "C2",
|
||||
"assistantOn": "4",
|
||||
"assistantName": "璇子",
|
||||
"pdChargeMinutes": 0,
|
||||
"assistantAbolishAmount": 0.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-23 19:05:48",
|
||||
"id": 2933593641256581,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963807682693,
|
||||
"tableId": 2793012902318213,
|
||||
"tableArea": "B区",
|
||||
"tableName": "B9",
|
||||
"assistantOn": "16",
|
||||
"assistantName": "周周",
|
||||
"pdChargeMinutes": 3600,
|
||||
"assistantAbolishAmount": 190.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-18 20:25:50",
|
||||
"id": 2926594431305093,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963794329671,
|
||||
"tableId": 2793001904918661,
|
||||
"tableArea": "A区",
|
||||
"tableName": "A4",
|
||||
"assistantOn": "15",
|
||||
"assistantName": "七七",
|
||||
"pdChargeMinutes": 2379,
|
||||
"assistantAbolishAmount": 71.37,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-14 14:20:32",
|
||||
"id": 2920573007709573,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963855982661,
|
||||
"tableId": 2793022145302597,
|
||||
"tableArea": "K包",
|
||||
"tableName": "888",
|
||||
"assistantOn": "9",
|
||||
"assistantName": "球球",
|
||||
"pdChargeMinutes": 14400,
|
||||
"assistantAbolishAmount": 392.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-03 01:21:59",
|
||||
"id": 2904236313234373,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963848527941,
|
||||
"tableId": 2793020955840645,
|
||||
"tableArea": "666",
|
||||
"tableName": "666",
|
||||
"assistantOn": "9",
|
||||
"assistantName": "球球",
|
||||
"pdChargeMinutes": 0,
|
||||
"assistantAbolishAmount": 0.0,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-01 00:27:29",
|
||||
"id": 2901351579143365,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963855982661,
|
||||
"tableId": 2793022145302597,
|
||||
"tableArea": "K包",
|
||||
"tableName": "888",
|
||||
"assistantOn": "99",
|
||||
"assistantName": "Amy",
|
||||
"pdChargeMinutes": 10605,
|
||||
"assistantAbolishAmount": 465.44,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-01 00:27:29",
|
||||
"id": 2901351578864837,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963855982661,
|
||||
"tableId": 2793022145302597,
|
||||
"tableArea": "K包",
|
||||
"tableName": "888",
|
||||
"assistantOn": "4",
|
||||
"assistantName": "璇子",
|
||||
"pdChargeMinutes": 10608,
|
||||
"assistantAbolishAmount": 318.24,
|
||||
"trashReason": ""
|
||||
},
|
||||
{
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 1,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"createTime": "2025-10-01 00:27:29",
|
||||
"id": 2901351578602693,
|
||||
"siteId": 2790685415443269,
|
||||
"tableAreaId": 2791963855982661,
|
||||
"tableId": 2793022145302597,
|
||||
"tableArea": "K包",
|
||||
"tableName": "888",
|
||||
"assistantOn": "2",
|
||||
"assistantName": "佳怡",
|
||||
"pdChargeMinutes": 10611,
|
||||
"assistantAbolishAmount": 318.33,
|
||||
"trashReason": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"code": 0
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"total": 15,
|
||||
"abolitionAssistants": []
|
||||
},
|
||||
"code": 0
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,673 +0,0 @@
|
||||
[
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2955202296416389,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -5000.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-11-08 01:27:16",
|
||||
"create_time": "2025-11-08 01:27:16",
|
||||
"relate_type": 5,
|
||||
"relate_id": 2955078219057349,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2955171790194821,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -10000.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-11-08 00:56:14",
|
||||
"create_time": "2025-11-08 00:56:14",
|
||||
"relate_type": 5,
|
||||
"relate_id": 2955153380001861,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2951883030513413,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -12.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-11-05 17:10:44",
|
||||
"create_time": "2025-11-05 17:10:44",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2951881496577861,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2948959062542597,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -65.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-11-03 15:36:19",
|
||||
"create_time": "2025-11-03 15:36:19",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2948934289967557,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2948630468005509,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -88.33,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-11-03 10:02:03",
|
||||
"create_time": "2025-11-03 10:02:03",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2948246513454661,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2948269239095045,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -0.67,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-11-03 03:54:36",
|
||||
"create_time": "2025-11-03 03:54:36",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2948246513454661,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2944743812581445,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -44000.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-10-31 16:08:21",
|
||||
"create_time": "2025-10-31 16:08:21",
|
||||
"relate_type": 5,
|
||||
"relate_id": 2944743413958789,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2931109065131653,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -10.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-10-22 00:58:22",
|
||||
"create_time": "2025-10-22 00:58:22",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2931108522378885,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2921195994465669,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -2.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-10-15 00:54:16",
|
||||
"create_time": "2025-10-15 00:54:16",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2920440691344901,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2919690732146181,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -3000.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-10-13 23:23:02",
|
||||
"create_time": "2025-10-13 23:23:02",
|
||||
"relate_type": 5,
|
||||
"relate_id": 2919519811440261,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
},
|
||||
{
|
||||
"tenantName": "朗朗桌球",
|
||||
"siteProfile": {
|
||||
"id": 2790685415443269,
|
||||
"org_id": 2790684179467077,
|
||||
"shop_name": "朗朗桌球",
|
||||
"avatar": "https://oss.ficoo.vip/admin/hXcE4E_1752495052016.jpg",
|
||||
"business_tel": "13316068642",
|
||||
"full_address": "广东省广州市天河区丽阳街12号",
|
||||
"address": "广东省广州市天河区天园街道朗朗桌球",
|
||||
"longitude": 113.360321,
|
||||
"latitude": 23.133629,
|
||||
"tenant_site_region_id": 156440100,
|
||||
"tenant_id": 2790683160709957,
|
||||
"auto_light": 1,
|
||||
"attendance_distance": 0,
|
||||
"wifi_name": "",
|
||||
"wifi_password": "",
|
||||
"customer_service_qrcode": "",
|
||||
"customer_service_wechat": "",
|
||||
"fixed_pay_qrCode": "",
|
||||
"prod_env": 1,
|
||||
"light_status": 2,
|
||||
"light_type": 0,
|
||||
"site_type": 1,
|
||||
"light_token": "",
|
||||
"site_label": "A",
|
||||
"attendance_enabled": 1,
|
||||
"shop_status": 1
|
||||
},
|
||||
"id": 2914039374956165,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"pay_sn": 0,
|
||||
"pay_amount": -8.0,
|
||||
"pay_status": 2,
|
||||
"pay_time": "2025-10-09 23:34:11",
|
||||
"create_time": "2025-10-09 23:34:11",
|
||||
"relate_type": 2,
|
||||
"relate_id": 2914030720124357,
|
||||
"is_revoke": 0,
|
||||
"is_delete": 0,
|
||||
"online_pay_channel": 0,
|
||||
"payment_method": 4,
|
||||
"balance_frozen_amount": 0.0,
|
||||
"card_frozen_amount": 0.0,
|
||||
"member_id": 0,
|
||||
"member_card_id": 0,
|
||||
"round_amount": 0.0,
|
||||
"online_pay_type": 0,
|
||||
"action_type": 2,
|
||||
"refund_amount": 0.0,
|
||||
"cashier_point_id": 0,
|
||||
"operator_id": 0,
|
||||
"pay_terminal": 1,
|
||||
"pay_config_id": 0,
|
||||
"channel_payer_id": "",
|
||||
"channel_pay_no": "",
|
||||
"check_status": 1,
|
||||
"channel_fee": 0.0
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,712 +0,0 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"total": 0,
|
||||
"goodsCategoryList": [
|
||||
{
|
||||
"id": 2790683528350533,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "槟榔",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "槟榔",
|
||||
"tenant_goods_business_id": 2790683528317766,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2790683528350534,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "槟榔",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350533,
|
||||
"business_name": "槟榔",
|
||||
"tenant_goods_business_id": 2790683528317766,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 1,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350535,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "器材",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2790683528350536,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "皮头",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350535,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350537,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "球杆",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350535,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350538,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "其他",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350535,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350539,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "酒水",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2790683528350540,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "饮料",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350541,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "酒水",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350542,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "茶水",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350543,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "咖啡",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350544,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "加料",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793221553489733,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "洋酒",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350545,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "果盘",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "水果",
|
||||
"tenant_goods_business_id": 2790683528317769,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2792050275864453,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "果盘",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350545,
|
||||
"business_name": "水果",
|
||||
"tenant_goods_business_id": 2790683528317769,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2791941988405125,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "零食",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "零食",
|
||||
"tenant_goods_business_id": 2791932037238661,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2791948300259205,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "零食",
|
||||
"alias_name": "",
|
||||
"pid": 2791941988405125,
|
||||
"business_name": "零食",
|
||||
"tenant_goods_business_id": 2791932037238661,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793236829620037,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "面",
|
||||
"alias_name": "",
|
||||
"pid": 2791941988405125,
|
||||
"business_name": "零食",
|
||||
"tenant_goods_business_id": 2791932037238661,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2791942087561093,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "雪糕",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "雪糕",
|
||||
"tenant_goods_business_id": 2791931866402693,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2792035069284229,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "雪糕",
|
||||
"alias_name": "",
|
||||
"pid": 2791942087561093,
|
||||
"business_name": "雪糕",
|
||||
"tenant_goods_business_id": 2791931866402693,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2792062778003333,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "香烟",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "香烟",
|
||||
"tenant_goods_business_id": 2790683528317765,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2792063209623429,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "香烟",
|
||||
"alias_name": "",
|
||||
"pid": 2792062778003333,
|
||||
"business_name": "香烟",
|
||||
"tenant_goods_business_id": 2790683528317765,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 1,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 1,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793217944864581,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "其他",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "其他",
|
||||
"tenant_goods_business_id": 2793217599407941,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2793218343257925,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "其他2",
|
||||
"alias_name": "",
|
||||
"pid": 2793217944864581,
|
||||
"business_name": "其他",
|
||||
"tenant_goods_business_id": 2793217599407941,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793220945250117,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "小吃",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "小吃",
|
||||
"tenant_goods_business_id": 2793220268902213,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2793221283104581,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "小吃",
|
||||
"alias_name": "",
|
||||
"pid": 2793220945250117,
|
||||
"business_name": "小吃",
|
||||
"tenant_goods_business_id": 2793220268902213,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"code": 0
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"total": 0,
|
||||
"goodsCategoryList": [
|
||||
{
|
||||
"id": 2790683528350533,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "槟榔",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "槟榔",
|
||||
"tenant_goods_business_id": 2790683528317766,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2790683528350534,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "槟榔",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350533,
|
||||
"business_name": "槟榔",
|
||||
"tenant_goods_business_id": 2790683528317766,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 1,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350535,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "器材",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2790683528350536,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "皮头",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350535,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350537,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "球杆",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350535,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350538,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "其他",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350535,
|
||||
"business_name": "器材",
|
||||
"tenant_goods_business_id": 2790683528317767,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350539,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "酒水",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2790683528350540,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "饮料",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350541,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "酒水",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350542,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "茶水",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350543,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "咖啡",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350544,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "加料",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793221553489733,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "洋酒",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350539,
|
||||
"business_name": "酒水",
|
||||
"tenant_goods_business_id": 2790683528317768,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2790683528350545,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "果盘",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "水果",
|
||||
"tenant_goods_business_id": 2790683528317769,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2792050275864453,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "果盘",
|
||||
"alias_name": "",
|
||||
"pid": 2790683528350545,
|
||||
"business_name": "水果",
|
||||
"tenant_goods_business_id": 2790683528317769,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2791941988405125,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "零食",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "零食",
|
||||
"tenant_goods_business_id": 2791932037238661,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2791948300259205,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "零食",
|
||||
"alias_name": "",
|
||||
"pid": 2791941988405125,
|
||||
"business_name": "零食",
|
||||
"tenant_goods_business_id": 2791932037238661,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793236829620037,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "面",
|
||||
"alias_name": "",
|
||||
"pid": 2791941988405125,
|
||||
"business_name": "零食",
|
||||
"tenant_goods_business_id": 2791932037238661,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2791942087561093,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "雪糕",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "雪糕",
|
||||
"tenant_goods_business_id": 2791931866402693,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2792035069284229,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "雪糕",
|
||||
"alias_name": "",
|
||||
"pid": 2791942087561093,
|
||||
"business_name": "雪糕",
|
||||
"tenant_goods_business_id": 2791931866402693,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2792062778003333,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "香烟",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "香烟",
|
||||
"tenant_goods_business_id": 2790683528317765,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2792063209623429,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "香烟",
|
||||
"alias_name": "",
|
||||
"pid": 2792062778003333,
|
||||
"business_name": "香烟",
|
||||
"tenant_goods_business_id": 2790683528317765,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 1,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 1,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793217944864581,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "其他",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "其他",
|
||||
"tenant_goods_business_id": 2793217599407941,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2793218343257925,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "其他2",
|
||||
"alias_name": "",
|
||||
"pid": 2793217944864581,
|
||||
"business_name": "其他",
|
||||
"tenant_goods_business_id": 2793217599407941,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
},
|
||||
{
|
||||
"id": 2793220945250117,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "小吃",
|
||||
"alias_name": "",
|
||||
"pid": 0,
|
||||
"business_name": "小吃",
|
||||
"tenant_goods_business_id": 2793220268902213,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [
|
||||
{
|
||||
"id": 2793221283104581,
|
||||
"tenant_id": 2790683160709957,
|
||||
"category_name": "小吃",
|
||||
"alias_name": "",
|
||||
"pid": 2793220945250117,
|
||||
"business_name": "小吃",
|
||||
"tenant_goods_business_id": 2793220268902213,
|
||||
"open_salesman": 2,
|
||||
"categoryBoxes": [],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
],
|
||||
"sort": 0,
|
||||
"is_warehousing": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"code": 0
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,646 +0,0 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"total": 17,
|
||||
"packageCouponList": [
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2939215004469573,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "早场特惠一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-10-27 00:00:00",
|
||||
"end_time": "2026-10-28 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 2,
|
||||
"package_id": 1814707240811572,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-10-27 18:24:09",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2861343275830405,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "B区桌球一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "B区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-09-02 00:00:00",
|
||||
"end_time": "2026-09-03 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1370841337,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-09-02 18:08:56",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960521691013",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 3,
|
||||
"id": 2836713896429317,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "午夜一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-08-16 00:00:00",
|
||||
"end_time": "2026-08-17 00:00:00",
|
||||
"is_enabled": 2,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1370841337,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-08-16 08:34:38",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2801876691340293,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "中八、斯诺克包厢两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "VIP包厢",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-22 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1126976372,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-22 17:56:24",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791961060364165",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 3,
|
||||
"id": 2801875268668357,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "中八、斯诺克包厢两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "VIP包厢",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-22 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 2,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1126976372,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-22 17:54:57",
|
||||
"creator_name": "管理员:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791961060364165",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "0",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 3,
|
||||
"id": 2800772613934149,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "午夜场一小时A区",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 2,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1370841337,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-21 23:13:16",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798905767676933,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "中八、斯诺克包厢两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "VIP包厢",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 2,
|
||||
"package_id": 1812429097416714,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 15:34:13",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791961060364165",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 3,
|
||||
"id": 2798901295615045,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "新人特惠A区中八一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 2,
|
||||
"is_delete": 0,
|
||||
"type": 2,
|
||||
"package_id": 1814707240811572,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 15:29:40",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798898826300485,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "斯诺克两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "斯诺克区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 2,
|
||||
"package_id": 1814983609169019,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 15:27:09",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791961347968901",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798734170983493,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "助理教练竞技教学两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1173128804,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:39:39",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798732571167749,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "全天斯诺克一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "斯诺克区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-30 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1147633733,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:38:02",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791961347968901",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798731703045189,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "KTV欢唱四小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "888",
|
||||
"selling_price": 0.0,
|
||||
"duration": 14400,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1137882866,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:37:09",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791961709907845",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "10:00:00",
|
||||
"add_end_clock": "18:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798729978514501,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "全天A区中八两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1130465371,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:35:24",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798728823213061,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "全天B区中八两小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "B区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 7200,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1137872168,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:34:13",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960521691013",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798727423528005,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "全天A区中八一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1128411555,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:32:48",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798723640069125,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "中八A区新人特惠一小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "A区",
|
||||
"selling_price": 0.0,
|
||||
"duration": 3600,
|
||||
"start_time": "2025-07-20 00:00:00",
|
||||
"end_time": "2025-12-31 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1203035334,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:28:57",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791960001957765",
|
||||
"start_clock": "00:00:00",
|
||||
"end_clock": "1.00:00:00",
|
||||
"add_start_clock": "00:00:00",
|
||||
"add_end_clock": "1.00:00:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
},
|
||||
{
|
||||
"site_name": "朗朗桌球",
|
||||
"effective_status": 1,
|
||||
"id": 2798713926290437,
|
||||
"site_id": 2790685415443269,
|
||||
"tenant_id": 2790683160709957,
|
||||
"package_name": "麻将 、掼蛋包厢四小时",
|
||||
"table_area_id": "0",
|
||||
"table_area_name": "麻将房",
|
||||
"selling_price": 0.0,
|
||||
"duration": 14400,
|
||||
"start_time": "2025-07-21 00:00:00",
|
||||
"end_time": "2025-12-30 00:00:00",
|
||||
"is_enabled": 1,
|
||||
"is_delete": 0,
|
||||
"type": 1,
|
||||
"package_id": 1134269810,
|
||||
"usable_count": 9999999,
|
||||
"create_time": "2025-07-20 12:19:04",
|
||||
"creator_name": "店长:郑丽珊",
|
||||
"tenant_table_area_id": "0",
|
||||
"table_area_id_list": "",
|
||||
"tenant_table_area_id_list": "2791962314215301",
|
||||
"start_clock": "10:00:00",
|
||||
"end_clock": "1.02:00:00",
|
||||
"add_start_clock": "10:00:00",
|
||||
"add_end_clock": "23:59:00",
|
||||
"date_info": "",
|
||||
"date_type": 1,
|
||||
"group_type": 1,
|
||||
"usable_range": "",
|
||||
"coupon_money": 0.0,
|
||||
"area_tag_type": 1,
|
||||
"system_group_type": 1,
|
||||
"max_selectable_categories": 0,
|
||||
"card_type_ids": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"code": 0
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"total": 17,
|
||||
"packageCouponList": []
|
||||
},
|
||||
"code": 0
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"goodsStockA": 0,
|
||||
"goodsStockB": 6252,
|
||||
"goodsSaleNum": 210.29,
|
||||
"stockSumMoney": 1461.28
|
||||
},
|
||||
"code": 0
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"goodsStockA": 0,
|
||||
"goodsStockB": 6252,
|
||||
"goodsSaleNum": 210.29,
|
||||
"stockSumMoney": 1461.28
|
||||
},
|
||||
"code": 0
|
||||
}
|
||||
]
|
||||
@@ -1,144 +0,0 @@
|
||||
==========================================================================================
|
||||
20251110_034959_助教流水.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'orderAssistantDetails']
|
||||
list orderAssistantDetails len 100, elem type dict, keys ['assistantNo', 'nickname', 'levelName', 'assistantName', 'tableName', 'siteProfile', 'skillName', 'id', 'order_trade_no', 'site_id']
|
||||
==========================================================================================
|
||||
20251110_035004_助教废除.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'abolitionAssistants']
|
||||
list abolitionAssistants len 15, elem type dict, keys ['siteProfile', 'createTime', 'id', 'siteId', 'tableAreaId', 'tableId', 'tableArea', 'tableName', 'assistantOn', 'assistantName']
|
||||
==========================================================================================
|
||||
20251110_035011_台费流水.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'siteTableUseDetailsList']
|
||||
list siteTableUseDetailsList len 100, elem type dict, keys ['siteProfile', 'id', 'order_trade_no', 'site_id', 'tenant_id', 'member_id', 'operator_id', 'operator_name', 'order_settle_id', 'ledger_unit_price']
|
||||
==========================================================================================
|
||||
20251110_035904_小票详情.json
|
||||
root list len 193
|
||||
sample keys ['orderSettleId', 'data']
|
||||
data keys ['data', 'code']
|
||||
dict data keys ['tenantId', 'siteId', 'orderSettleId', 'orderSettleNumber', 'assistantManualDiscount', 'siteName', 'tenantName', 'siteAddress', 'siteBusinessTel', 'ticketRemark']
|
||||
==========================================================================================
|
||||
20251110_035908_台费打折.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'taiFeeAdjustInfos']
|
||||
list taiFeeAdjustInfos len 100, elem type dict, keys ['tableProfile', 'siteProfile', 'id', 'adjust_type', 'applicant_id', 'applicant_name', 'create_time', 'is_delete', 'ledger_amount', 'ledger_count']
|
||||
==========================================================================================
|
||||
20251110_035916_结账记录.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'settleList']
|
||||
list settleList len 100, elem type dict, keys ['siteProfile', 'settleList']
|
||||
==========================================================================================
|
||||
20251110_035923_支付记录.json
|
||||
root list len 200
|
||||
sample keys ['siteProfile', 'create_time', 'pay_amount', 'pay_status', 'pay_time', 'online_pay_channel', 'relate_type', 'relate_id', 'site_id', 'id', 'payment_method']
|
||||
dict siteProfile keys ['id', 'org_id', 'shop_name', 'avatar', 'business_tel', 'full_address', 'address', 'longitude', 'latitude', 'tenant_site_region_id']
|
||||
==========================================================================================
|
||||
20251110_035929_退款记录.json
|
||||
root list len 11
|
||||
sample keys ['tenantName', 'siteProfile', 'id', 'site_id', 'tenant_id', 'pay_sn', 'pay_amount', 'pay_status', 'pay_time', 'create_time', 'relate_type', 'relate_id', 'is_revoke', 'is_delete', 'online_pay_channel', 'payment_method', 'balance_frozen_amount', 'card_frozen_amount', 'member_id', 'member_card_id']
|
||||
dict siteProfile keys ['id', 'org_id', 'shop_name', 'avatar', 'business_tel', 'full_address', 'address', 'longitude', 'latitude', 'tenant_site_region_id']
|
||||
==========================================================================================
|
||||
20251110_035934_平台验券记录.json
|
||||
root list len 200
|
||||
sample keys ['siteProfile', 'id', 'tenant_id', 'site_id', 'sale_price', 'coupon_code', 'coupon_channel', 'site_order_id', 'coupon_free_time', 'use_status', 'create_time', 'is_delete', 'coupon_name', 'coupon_cover', 'coupon_remark', 'channel_deal_id', 'group_package_id', 'consume_time', 'groupon_type', 'coupon_money']
|
||||
dict siteProfile keys ['id', 'org_id', 'shop_name', 'avatar', 'business_tel', 'full_address', 'address', 'longitude', 'latitude', 'tenant_site_region_id']
|
||||
==========================================================================================
|
||||
20251110_035941_商品档案.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'tenantGoodsList']
|
||||
list tenantGoodsList len 100, elem type dict, keys ['categoryName', 'isInSite', 'commodityCode', 'id', 'tenant_id', 'goods_name', 'goods_cover', 'goods_state', 'goods_category_id', 'unit']
|
||||
==========================================================================================
|
||||
20251110_035948_门店销售记录.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'orderGoodsLedgers']
|
||||
list orderGoodsLedgers len 100, elem type dict, keys ['siteId', 'siteName', 'orderGoodsId', 'openSalesman', 'id', 'order_trade_no', 'site_id', 'tenant_id', 'operator_id', 'operator_name']
|
||||
==========================================================================================
|
||||
20251110_043159_库存变化记录1.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'queryDeliveryRecordsList']
|
||||
list queryDeliveryRecordsList len 100, elem type dict, keys ['siteGoodsStockId', 'siteGoodsId', 'siteId', 'tenantId', 'stockType', 'goodsName', 'createTime', 'startNum', 'endNum', 'changeNum']
|
||||
==========================================================================================
|
||||
20251110_043204_库存变化记录2.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'goodsCategoryList']
|
||||
list goodsCategoryList len 9, elem type dict, keys ['id', 'tenant_id', 'category_name', 'alias_name', 'pid', 'business_name', 'tenant_goods_business_id', 'open_salesman', 'categoryBoxes', 'sort']
|
||||
==========================================================================================
|
||||
20251110_043209_会员档案.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'tenantMemberInfos']
|
||||
list tenantMemberInfos len 100, elem type dict, keys ['id', 'create_time', 'member_card_grade_code', 'mobile', 'nickname', 'register_site_id', 'site_name', 'member_card_grade_name', 'system_member_id', 'tenant_id']
|
||||
==========================================================================================
|
||||
20251110_043217_余额变更记录.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'tenantMemberCardLogs']
|
||||
list tenantMemberCardLogs len 100, elem type dict, keys ['memberCardTypeName', 'paySiteName', 'registerSiteName', 'memberName', 'memberMobile', 'id', 'account_data', 'after', 'before', 'card_type_id']
|
||||
==========================================================================================
|
||||
20251110_043223_储值卡列表.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'totalOther', 'tenantMemberCards']
|
||||
list tenantMemberCards len 100, elem type dict, keys ['site_name', 'member_name', 'member_mobile', 'member_card_type_name', 'table_service_discount', 'assistant_service_discount', 'coupon_discount', 'goods_service_discount', 'is_allow_give', 'able_cross_site']
|
||||
==========================================================================================
|
||||
20251110_043231_充值记录.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'settleList']
|
||||
list settleList len 74, elem type dict, keys ['siteProfile', 'settleList']
|
||||
==========================================================================================
|
||||
20251110_043237_助教账号1.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'assistantInfos']
|
||||
list assistantInfos len 50, elem type dict, keys ['job_num', 'shop_name', 'group_id', 'group_name', 'staff_profile_id', 'ding_talk_synced', 'entry_type', 'team_name', 'entry_sign_status', 'resign_sign_status']
|
||||
==========================================================================================
|
||||
20251110_043243_助教账号2.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'assistantInfos']
|
||||
list assistantInfos len 50, elem type dict, keys ['job_num', 'shop_name', 'group_id', 'group_name', 'staff_profile_id', 'ding_talk_synced', 'entry_type', 'team_name', 'entry_sign_status', 'resign_sign_status']
|
||||
==========================================================================================
|
||||
20251110_043250_台桌列表.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'siteTables']
|
||||
list siteTables len 71, elem type dict, keys ['id', 'audit_status', 'charge_free', 'self_table', 'create_time', 'is_rest_area', 'light_status', 'show_status', 'site_id', 'site_table_area_id']
|
||||
==========================================================================================
|
||||
20251110_043255_团购套餐.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'packageCouponList']
|
||||
list packageCouponList len 17, elem type dict, keys ['site_name', 'effective_status', 'id', 'site_id', 'tenant_id', 'package_name', 'table_area_id', 'table_area_name', 'selling_price', 'duration']
|
||||
==========================================================================================
|
||||
20251110_043302_团购套餐流水.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'couponAmountSum', 'siteTableUseDetailsList']
|
||||
list siteTableUseDetailsList len 100, elem type dict, keys ['tableName', 'tableAreaName', 'siteName', 'goodsOptionPrice', 'id', 'order_trade_no', 'table_id', 'site_id', 'tenant_id', 'operator_id']
|
||||
==========================================================================================
|
||||
20251110_043308_库存汇总.json
|
||||
root list len 161
|
||||
sample keys ['siteGoodsId', 'goodsName', 'goodsUnit', 'goodsCategoryId', 'goodsCategorySecondId', 'rangeStartStock', 'rangeEndStock', 'rangeIn', 'rangeOut', 'rangeInventory', 'rangeSale', 'rangeSaleMoney', 'currentStock', 'categoryName']
|
||||
==========================================================================================
|
||||
20251110_051132_门店商品档案1.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['total', 'orderGoodsList']
|
||||
list orderGoodsList len 100, elem type dict, keys ['siteName', 'oneCategoryName', 'twoCategoryName', 'id', 'tenant_goods_id', 'site_id', 'tenant_id', 'goods_name', 'goods_cover', 'goods_state']
|
||||
==========================================================================================
|
||||
20251110_051138_门店商品档案2.json
|
||||
root list len 2
|
||||
sample keys ['data', 'code']
|
||||
data keys ['goodsStockA', 'goodsStockB', 'goodsSaleNum', 'stockSumMoney']
|
||||
@@ -26,6 +26,7 @@ from tasks.refunds_task import RefundsTask
|
||||
from tasks.table_discount_task import TableDiscountTask
|
||||
from tasks.tables_task import TablesTask
|
||||
from tasks.topups_task import TopupsTask
|
||||
from utils.json_store import endpoint_to_filename
|
||||
|
||||
DEFAULT_STORE_ID = 2790685415443269
|
||||
BASE_TS = "2025-01-01 10:00:00"
|
||||
@@ -47,12 +48,6 @@ class TaskSpec:
|
||||
return endpoint_to_filename(self.endpoint)
|
||||
|
||||
|
||||
def endpoint_to_filename(endpoint: str) -> str:
|
||||
"""根据 API endpoint 生成稳定可复用的文件名,便于离线模式在目录中直接定位归档 JSON。"""
|
||||
normalized = endpoint.strip("/").replace("/", "__").replace(" ", "_").lower()
|
||||
return f"{normalized or 'root'}.json"
|
||||
|
||||
|
||||
def wrap_records(records: List[Dict], data_path: Sequence[str]):
|
||||
"""按照 data_path 逐层包裹记录列表,使其结构与真实 API 返回体一致,方便离线回放。"""
|
||||
payload = records
|
||||
@@ -140,6 +135,8 @@ class FakeDBOperations:
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
self.conn = FakeConnection()
|
||||
# Pre-seeded query results (FIFO) to let tests control DB-returned rows
|
||||
self.query_results: List[List[Dict]] = []
|
||||
|
||||
def batch_upsert_with_returning(self, sql: str, rows: List[Dict], page_size: int = 1000):
|
||||
self.upserts.append(
|
||||
@@ -167,6 +164,8 @@ class FakeDBOperations:
|
||||
|
||||
def query(self, sql: str, params=None):
|
||||
self.executes.append({"sql": sql.strip(), "params": params, "type": "query"})
|
||||
if self.query_results:
|
||||
return self.query_results.pop(0)
|
||||
return []
|
||||
|
||||
def cursor(self):
|
||||
|
||||
59
etl_billiards/tests/unit/test_etl_tasks_stages.py
Normal file
59
etl_billiards/tests/unit/test_etl_tasks_stages.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""验证 14 个任务的 E/T/L 分阶段调用(FakeDB/FakeAPI,不访问真实接口或数据库)。"""
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from tasks.base_task import TaskContext
|
||||
from tests.unit.task_test_utils import (
|
||||
TASK_SPECS,
|
||||
create_test_config,
|
||||
get_db_operations,
|
||||
FakeAPIClient,
|
||||
)
|
||||
|
||||
|
||||
def _build_context(store_id: int) -> TaskContext:
|
||||
now = datetime.now(ZoneInfo("Asia/Taipei"))
|
||||
return TaskContext(
|
||||
store_id=store_id,
|
||||
window_start=now - timedelta(minutes=30),
|
||||
window_end=now,
|
||||
window_minutes=30,
|
||||
cursor=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spec", TASK_SPECS)
|
||||
def test_etl_stage_flow(spec, tmp_path):
|
||||
"""对每个任务,单独调用 transform/load,验证 counts 结构与 FakeDB 写入。"""
|
||||
config = create_test_config("ONLINE", tmp_path / "archive", tmp_path / "temp")
|
||||
api = FakeAPIClient({spec.endpoint: spec.sample_records})
|
||||
logger = logging.getLogger(f"test_{spec.code.lower()}")
|
||||
|
||||
task_cls = spec.task_cls
|
||||
with get_db_operations() as db_ops:
|
||||
task = task_cls(config, db_ops, api, logger)
|
||||
ctx = _build_context(config.get("app.store_id"))
|
||||
|
||||
# 跳过 extract,直接验证 transform + load
|
||||
extracted = {"records": spec.sample_records}
|
||||
transformed = task.transform(extracted, ctx)
|
||||
counts = task.load(transformed, ctx)
|
||||
|
||||
assert set(counts.keys()) == {"fetched", "inserted", "updated", "skipped", "errors"}
|
||||
assert counts["fetched"] == len(spec.sample_records)
|
||||
assert counts["errors"] == 0
|
||||
|
||||
# FakeDB 记录(upserts/executes)至少有一条
|
||||
upserts = getattr(db_ops, "upserts", [])
|
||||
executes = getattr(db_ops, "executes", [])
|
||||
assert upserts or executes, "expected db operations to be recorded"
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests for the new ODS ingestion tasks."""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,6 +10,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
os.environ.setdefault("ETL_SKIP_DOTENV", "1")
|
||||
|
||||
from tasks.ods_tasks import ODS_TASK_CLASSES
|
||||
from .task_test_utils import create_test_config, get_db_operations, FakeAPIClient
|
||||
|
||||
@@ -19,44 +22,80 @@ def _build_config(tmp_path):
|
||||
return create_test_config("ONLINE", archive_dir, temp_dir)
|
||||
|
||||
|
||||
def test_ods_order_settle_ingest(tmp_path):
|
||||
"""Ensure ODS_ORDER_SETTLE task writes raw payload + metadata."""
|
||||
def test_ods_assistant_accounts_ingest(tmp_path):
|
||||
"""Ensure ODS_ASSISTANT_ACCOUNTS task stores raw payload with record_index dedup keys."""
|
||||
config = _build_config(tmp_path)
|
||||
sample = [
|
||||
{
|
||||
"orderSettleId": 701,
|
||||
"orderTradeNo": 8001,
|
||||
"anyField": "value",
|
||||
"id": 5001,
|
||||
"assistant_no": "A01",
|
||||
"nickname": "小张",
|
||||
}
|
||||
]
|
||||
api = FakeAPIClient({"/Site/GetAllOrderSettleList": sample})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_ORDER_SETTLE"]
|
||||
api = FakeAPIClient({"/PersonnelManagement/SearchAssistantInfo": sample})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_ASSISTANT_ACCOUNTS"]
|
||||
|
||||
with get_db_operations() as db_ops:
|
||||
task = task_cls(config, db_ops, api, logging.getLogger("test_ods_order"))
|
||||
task = task_cls(config, db_ops, api, logging.getLogger("test_ods_assistant_accounts"))
|
||||
result = task.execute()
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["counts"]["fetched"] == 1
|
||||
assert db_ops.commits == 1
|
||||
row = db_ops.upserts[0]["rows"][0]
|
||||
assert row["order_settle_id"] == 701
|
||||
assert row["order_trade_no"] == 8001
|
||||
assert row["source_endpoint"] == "/Site/GetAllOrderSettleList"
|
||||
assert '"orderSettleId": 701' in row["payload"]
|
||||
assert row["id"] == 5001
|
||||
assert row["record_index"] == 0
|
||||
assert row["source_file"] is None or row["source_file"] == ""
|
||||
assert '"id": 5001' in row["payload"]
|
||||
|
||||
|
||||
def test_ods_payment_ingest(tmp_path):
|
||||
"""Ensure ODS_PAYMENT task stores relate fields and payload."""
|
||||
def test_ods_inventory_change_ingest(tmp_path):
|
||||
"""Ensure ODS_INVENTORY_CHANGE task stores raw payload with record_index dedup keys."""
|
||||
config = _build_config(tmp_path)
|
||||
sample = [
|
||||
{
|
||||
"payId": 901,
|
||||
"relateType": "ORDER",
|
||||
"relateId": 123,
|
||||
"payAmount": "100.00",
|
||||
"siteGoodsStockId": 123456,
|
||||
"stockType": 1,
|
||||
"goodsName": "测试商品",
|
||||
}
|
||||
]
|
||||
api = FakeAPIClient({"/GoodsStockManage/QueryGoodsOutboundReceipt": sample})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_INVENTORY_CHANGE"]
|
||||
|
||||
with get_db_operations() as db_ops:
|
||||
task = task_cls(config, db_ops, api, logging.getLogger("test_ods_inventory_change"))
|
||||
result = task.execute()
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["counts"]["fetched"] == 1
|
||||
assert db_ops.commits == 1
|
||||
row = db_ops.upserts[0]["rows"][0]
|
||||
assert row["sitegoodsstockid"] == 123456
|
||||
assert row["record_index"] == 0
|
||||
assert '"siteGoodsStockId": 123456' in row["payload"]
|
||||
|
||||
|
||||
def test_ods_member_profiles_ingest(tmp_path):
|
||||
"""Ensure ODS_MEMBER task stores tenantMemberInfos raw JSON."""
|
||||
config = _build_config(tmp_path)
|
||||
sample = [{"tenantMemberInfos": [{"id": 101, "mobile": "13800000000"}]}]
|
||||
api = FakeAPIClient({"/MemberProfile/GetTenantMemberList": sample})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_MEMBER"]
|
||||
|
||||
with get_db_operations() as db_ops:
|
||||
task = task_cls(config, db_ops, api, logging.getLogger("test_ods_member"))
|
||||
result = task.execute()
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
row = db_ops.upserts[0]["rows"][0]
|
||||
assert row["record_index"] == 0
|
||||
assert '"id": 101' in row["payload"]
|
||||
|
||||
|
||||
def test_ods_payment_ingest(tmp_path):
|
||||
"""Ensure ODS_PAYMENT task stores payment_transactions raw JSON."""
|
||||
config = _build_config(tmp_path)
|
||||
sample = [{"payId": 901, "payAmount": "100.00"}]
|
||||
api = FakeAPIClient({"/PayLog/GetPayLogListPage": sample})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_PAYMENT"]
|
||||
|
||||
@@ -65,10 +104,57 @@ def test_ods_payment_ingest(tmp_path):
|
||||
result = task.execute()
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["counts"]["fetched"] == 1
|
||||
assert db_ops.commits == 1
|
||||
row = db_ops.upserts[0]["rows"][0]
|
||||
assert row["pay_id"] == 901
|
||||
assert row["relate_type"] == "ORDER"
|
||||
assert row["relate_id"] == 123
|
||||
assert row["record_index"] == 0
|
||||
assert '"payId": 901' in row["payload"]
|
||||
|
||||
|
||||
def test_ods_settlement_records_ingest(tmp_path):
|
||||
"""Ensure ODS_ORDER_SETTLE task stores settleList raw JSON."""
|
||||
config = _build_config(tmp_path)
|
||||
sample = [{"data": {"settleList": [{"id": 701, "orderTradeNo": 8001}]}}]
|
||||
api = FakeAPIClient({"/Site/GetAllOrderSettleList": sample})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_ORDER_SETTLE"]
|
||||
|
||||
with get_db_operations() as db_ops:
|
||||
task = task_cls(config, db_ops, api, logging.getLogger("test_ods_order_settle"))
|
||||
result = task.execute()
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
row = db_ops.upserts[0]["rows"][0]
|
||||
assert row["record_index"] == 0
|
||||
assert '"orderTradeNo": 8001' in row["payload"]
|
||||
|
||||
|
||||
def test_ods_settlement_ticket_by_payment_relate_ids(tmp_path):
|
||||
"""Ensure settlement tickets are fetched per payment relate_id and skip existing ones."""
|
||||
config = _build_config(tmp_path)
|
||||
ticket_payload = {"data": {"data": {"orderSettleId": 9001, "orderSettleNumber": "T001"}}}
|
||||
api = FakeAPIClient({"/Order/GetOrderSettleTicketNew": [ticket_payload]})
|
||||
task_cls = ODS_TASK_CLASSES["ODS_SETTLEMENT_TICKET"]
|
||||
|
||||
with get_db_operations() as db_ops:
|
||||
# First query: existing ticket ids; Second query: payment relate_ids
|
||||
db_ops.query_results = [
|
||||
[{"order_settle_id": 9002}],
|
||||
[
|
||||
{"order_settle_id": 9001},
|
||||
{"order_settle_id": 9002},
|
||||
{"order_settle_id": None},
|
||||
],
|
||||
]
|
||||
task = task_cls(config, db_ops, api, logging.getLogger("test_ods_settlement_ticket"))
|
||||
result = task.execute()
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
counts = result["counts"]
|
||||
assert counts["fetched"] == 1
|
||||
assert counts["inserted"] == 1
|
||||
assert counts["updated"] == 0
|
||||
assert counts["skipped"] == 0
|
||||
assert '"orderSettleId": 9001' in db_ops.upserts[0]["rows"][0]["payload"]
|
||||
assert any(
|
||||
call["endpoint"] == "/Order/GetOrderSettleTicketNew"
|
||||
and call.get("params", {}).get("orderSettleId") == 9001
|
||||
for call in api.calls
|
||||
)
|
||||
|
||||
22
etl_billiards/tests/unit/test_reporting.py
Normal file
22
etl_billiards/tests/unit/test_reporting.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""汇总与报告工具的单测。"""
|
||||
from utils.reporting import summarize_counts, format_report
|
||||
|
||||
|
||||
def test_summarize_counts_and_format():
|
||||
task_results = [
|
||||
{"task_code": "ORDERS", "counts": {"fetched": 2, "inserted": 2, "updated": 0, "skipped": 0, "errors": 0}},
|
||||
{"task_code": "PAYMENTS", "counts": {"fetched": 3, "inserted": 2, "updated": 1, "skipped": 0, "errors": 0}},
|
||||
]
|
||||
|
||||
summary = summarize_counts(task_results)
|
||||
assert summary["total"]["fetched"] == 5
|
||||
assert summary["total"]["inserted"] == 4
|
||||
assert summary["total"]["updated"] == 1
|
||||
assert summary["total"]["errors"] == 0
|
||||
assert len(summary["details"]) == 2
|
||||
|
||||
report = format_report(summary)
|
||||
assert "TOTAL fetched=5" in report
|
||||
assert "ORDERS:" in report
|
||||
assert "PAYMENTS:" in report
|
||||
819
etl_billiards/tmp/dwd_schema_columns.txt
Normal file
819
etl_billiards/tmp/dwd_schema_columns.txt
Normal file
@@ -0,0 +1,819 @@
|
||||
-- dim_assistant
|
||||
assistant_id bigint 助教账号 ID,关联助教服务流水表。 | 来源: id | 角色: 主键
|
||||
user_id bigint 系统用户 ID,用于统一跨模块身份。 | 来源: user_id | 角色: 外键
|
||||
assistant_no text 助教工号/编号,业务识别用。 | 来源: assistant_no
|
||||
real_name text 助教真实姓名。 | 来源: real_name
|
||||
nickname text 前台展示昵称。 | 来源: nickname
|
||||
mobile text 手机号码。 | 来源: mobile
|
||||
tenant_id bigint 租户 ID。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID。 | 来源: site_id | 角色: 外键
|
||||
team_id bigint 助教团队 ID。 | 来源: team_id | 角色: 外键
|
||||
team_name text 团队名称。 | 来源: team_name
|
||||
level integer 助教等级:8=管理员、10=初级、20=中级、30=高级、40=专家。 | 来源: level
|
||||
entry_time timestamp with time zone 入职时间。 | 来源: entry_time
|
||||
resign_time timestamp with time zone 离职时间;远未来日期表示在职。 | 来源: resign_time
|
||||
leave_status integer 在职状态:0=在职,1=已离职。 | 来源: leave_status
|
||||
assistant_status integer 账号启用状态:1=启用,2=停用/冻结。 | 来源: assistant_status
|
||||
-- dim_assistant_ex
|
||||
assistant_id bigint 助教账号 ID,关联助教服务流水表。 | 来源: id | 角色: 主键
|
||||
gender integer 性别枚举:0=未填/保密,1=男,2=女。 | 来源: gender
|
||||
birth_date timestamp with time zone 出生日期,默认为 0001-01-01 表示未设置。 | 来源: birth_date
|
||||
avatar text 头像 URL。 | 来源: avatar
|
||||
introduce text 个人简介文案(目前为空)。 | 来源: introduce
|
||||
video_introduction_url text 视频介绍 URL。 | 来源: video_introduction_url
|
||||
height double precision 身高(厘米),0 表示未填。 | 来源: height
|
||||
weight double precision 体重(公斤),0 表示未填。 | 来源: weight
|
||||
shop_name text 门店名称。 | 来源: shop_name
|
||||
group_id bigint 上级分组 ID,未使用。 | 来源: group_id
|
||||
group_name text 上级分组名称,空。 | 来源: group_name
|
||||
person_org_id bigint 人事组织 ID,用于权限和报表分组。 | 来源: person_org_id
|
||||
staff_id bigint 预留员工 ID(全部为0)。 | 来源: staff_id
|
||||
staff_profile_id bigint 外部人事档案 ID(全部为0)。 | 来源: staff_profile_id
|
||||
assistant_grade double precision 平均评分(0 表示暂无)。 | 来源: assistant_grade
|
||||
sum_grade double precision 总评分累加值。 | 来源: sum_grade
|
||||
get_grade_times integer 累计评分次数。 | 来源: get_grade_times
|
||||
charge_way integer 计费方式:2=计时,其他未出现。 | 来源: charge_way
|
||||
allow_cx integer 是否允许促销计费:1=允许。 | 来源: allow_cx
|
||||
is_guaranteed integer 是否有保底:1=是。 | 来源: is_guaranteed
|
||||
salary_grant_enabled integer 薪资发放开关(值2,具体含义未知)。 | 来源: salary_grant_enabled
|
||||
entry_type integer 入职类型:1=正式;其他未出现。 | 来源: entry_type
|
||||
entry_sign_status integer 入职签约状态:0=未签约,1=已签约(未出现)。 | 来源: entry_sign_status
|
||||
resign_sign_status integer 离职签约状态,未出现非 0。 | 来源: resign_sign_status
|
||||
work_status integer 工作状态:1=在岗,2=离岗。与 leave_status 呼应。 | 来源: work_status
|
||||
show_status integer 前台展示状态:1=显示;其他值未出现。 | 来源: show_status
|
||||
show_sort integer 前端排序序号。 | 来源: show_sort
|
||||
online_status integer 在线状态:1=在线。 | 来源: online_status
|
||||
is_delete integer 逻辑删除标记:0=未删除,1=已删除。 | 来源: is_delete
|
||||
criticism_status integer 投诉状态:1=正常,2=有投诉。 | 来源: criticism_status
|
||||
create_time timestamp with time zone 账号创建时间。 | 来源: create_time
|
||||
update_time timestamp with time zone 账号最近修改时间。 | 来源: update_time
|
||||
start_time timestamp with time zone 配置生效开始时间。 | 来源: start_time
|
||||
end_time timestamp with time zone 配置生效结束时间。 | 来源: end_time
|
||||
last_table_id bigint 最近服务的台桌 ID(未必存在)。 | 来源: last_table_id
|
||||
last_table_name text 最近服务球台名称。 | 来源: last_table_name
|
||||
last_update_name text 最近更新该账号的管理员。 | 来源: last_update_name
|
||||
order_trade_no bigint 最近关联的订单号(非外键,仅做展示)。 | 来源: order_trade_no
|
||||
ding_talk_synced integer 是否同步钉钉:1=已同步。 | 来源: ding_talk_synced
|
||||
site_light_cfg_id bigint 灯控配置 ID(未启用)。 | 来源: site_light_cfg_id
|
||||
light_equipment_id text 灯控设备 ID(未启用)。 | 来源: light_equipment_id
|
||||
light_status integer 灯控状态(值2,具体含义未知)。 | 来源: light_status
|
||||
is_team_leader integer 是否团队长:0=否,1=是。 | 来源: is_team_leader
|
||||
serial_number bigint 来源: serial_number
|
||||
-- dim_goods_category
|
||||
category_id bigint 分类节点主键。来自分类树节点的 id,在整个商品分类维度内唯一。用于在事实表中作为商品分类外键引用。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户 ID(商户/品牌 ID)。当前所有节点取值相同,表示同一个租户下的分类树。事实表可通过该字段与租户维度或门店维度间接关联。 | 来源: tenant_id | 角色: 外键
|
||||
category_name character varying(50) 分类名称。一级大类示例:槟榔、器材、酒水、水果、零食、雪糕、香烟、其他、小吃。二级子类示例:槟榔、皮头、球杆、其他、饮料、酒水、茶水、咖啡、加料、洋酒、果盘、面、小吃等。用于前台展示和报表按细分类统计。 | 来源: category_name
|
||||
alias_name character varying(50) 分类别名。当前样例数据全部为空字符串,预留给业务方做简称或别名展示。对现阶段经营分析无影响。 | 来源: alias_name
|
||||
parent_category_id bigint 父级分类 ID。根节点取值为 0,表示没有父分类;子节点取值为父分类的 id。与 category_id 共同形成树形层级关系。 | 来源: pid | 角色: 外键
|
||||
business_name character varying(50) 业务大类名称。将多个细分类归入同一业务线。观测值与一级大类相同:槟榔、器材、酒水、水果、零食、雪糕、香烟、其他、小吃。子类的 business_name 继承所属根节点的大类名称。用于按业务线汇总库存和销售。 | 来源: business_name
|
||||
tenant_goods_business_id bigint 业务大类 ID。每个 business_name 对应唯一一个 tenant_goods_business_id,根节点和其下所有子节点共享同一取值。例如“酒水”大类及其子类饮料、茶水、咖啡、加料、洋酒拥有相同的业务 ID。可作为外键连接“业务线维度表”。 | 来源: tenant_goods_business_id | 角色: 外键
|
||||
category_level integer 分类层级:1 表示一级大类(pid = 0),2 表示二级子类(pid ≠ 0)。方便在报表中区分大类与子类进行分组和展示层级控制。 | 来源: 由 pid 推导
|
||||
is_leaf integer 是否叶子节点:1 表示叶子分类(categoryBoxes 为空列表),0 表示非叶子分类(存在子分类)。当前样例数据中,一级大类是非叶子节点,二级分类是叶子节点。用于树状导航或限制只能在叶子分类建商品。 | 来源: 由 categoryBoxes 推导
|
||||
open_salesman integer 营业员开关控制。枚举含义根据业务系统定义,一般设计为:1 表示启用营业员/导购相关功能,2 表示关闭或不启用。当前样例所有分类取值为 2,说明这一套分类在库存模块中统一未启用营业员逻辑。对目前的经营分析影响较小。 | 来源: open_salesman
|
||||
sort_order integer 分类排序序号。来自 sort 字段,用于前端展示顺序控制,数值越小越靠前。当前大部分分类为 0,仅少数为 1,说明排序配置较为粗略。对指标统计无实质影响。 | 来源: sort
|
||||
is_warehousing integer 是否参与库存管理。枚举:1 表示参与库存管理,0 表示不参与(如服务类商品、手工费用)。当前文件中所有分类取值为 1,表示这一份分类树只包含“走库存”的商品分类。可在库存报表中用作过滤条件。 | 来源: is_warehousing
|
||||
-- dim_goods_category_ex
|
||||
category_id bigint 分类节点主键。来自分类树节点的 id,在整个商品分类维度内唯一。用于在事实表中作为商品分类外键引用。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户 ID(商户/品牌 ID)。当前所有节点取值相同,表示同一个租户下的分类树。事实表可通过该字段与租户维度或门店维度间接关联。 | 来源: tenant_id | 角色: 外键
|
||||
category_name character varying(50) 分类名称。一级大类示例:槟榔、器材、酒水、水果、零食、雪糕、香烟、其他、小吃。二级子类示例:槟榔、皮头、球杆、其他、饮料、酒水、茶水、咖啡、加料、洋酒、果盘、面、小吃等。用于前台展示和报表按细分类统计。 | 来源: category_name
|
||||
alias_name character varying(50) 分类别名。当前样例数据全部为空字符串,预留给业务方做简称或别名展示。对现阶段经营分析无影响。 | 来源: alias_name
|
||||
parent_category_id bigint 父级分类 ID。根节点取值为 0,表示没有父分类;子节点取值为父分类的 id。与 category_id 共同形成树形层级关系。 | 来源: pid | 角色: 外键
|
||||
business_name character varying(50) 业务大类名称。将多个细分类归入同一业务线。观测值与一级大类相同:槟榔、器材、酒水、水果、零食、雪糕、香烟、其他、小吃。子类的 business_name 继承所属根节点的大类名称。用于按业务线汇总库存和销售。 | 来源: business_name
|
||||
tenant_goods_business_id bigint 业务大类 ID。每个 business_name 对应唯一一个 tenant_goods_business_id,根节点和其下所有子节点共享同一取值。例如“酒水”大类及其子类饮料、茶水、咖啡、加料、洋酒拥有相同的业务 ID。可作为外键连接“业务线维度表”。 | 来源: tenant_goods_business_id | 角色: 外键
|
||||
category_level integer 分类层级:1 表示一级大类(pid = 0),2 表示二级子类(pid ≠ 0)。方便在报表中区分大类与子类进行分组和展示层级控制。 | 来源: 由 pid 推导
|
||||
is_leaf integer 是否叶子节点:1 表示叶子分类(categoryBoxes 为空列表),0 表示非叶子分类(存在子分类)。当前样例数据中,一级大类是非叶子节点,二级分类是叶子节点。用于树状导航或限制只能在叶子分类建商品。 | 来源: 由 categoryBoxes 推导
|
||||
open_salesman integer 营业员开关控制。枚举含义根据业务系统定义,一般设计为:1 表示启用营业员/导购相关功能,2 表示关闭或不启用。当前样例所有分类取值为 2,说明这一套分类在库存模块中统一未启用营业员逻辑。对目前的经营分析影响较小。 | 来源: open_salesman
|
||||
sort_order integer 分类排序序号。来自 sort 字段,用于前端展示顺序控制,数值越小越靠前。当前大部分分类为 0,仅少数为 1,说明排序配置较为粗略。对指标统计无实质影响。 | 来源: sort
|
||||
is_warehousing integer 是否参与库存管理。枚举:1 表示参与库存管理,0 表示不参与(如服务类商品、手工费用)。当前文件中所有分类取值为 1,表示这一份分类树只包含“走库存”的商品分类。可在库存报表中用作过滤条件。 | 来源: is_warehousing
|
||||
-- dim_groupbuy_package
|
||||
groupbuy_package_id bigint 门店侧团购套餐主键。每条记录一个套餐定义,供团购券核销记录指向。平台验券记录中的 group_package_id 通常指向这里。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户(品牌/商户)ID。本数据集中全表相同,表示同一品牌。 | 来源: tenant_id | 角色: 外键(指向租户维)
|
||||
site_id bigint 门店 ID,本表所有记录属于同一门店。与其他 JSON 的 site_id 一致。 | 来源: site_id | 角色: 外键(指向门店维)
|
||||
package_name character varying(200) 团购套餐名称,用于前台展示及核销界面,例如“早场特惠一小时”“KTV欢唱四小时”等。 | 来源: package_name
|
||||
package_template_id bigint 上层套餐 ID 或总部/系统级套餐 ID。多个 groupbuy_package_id 可能共享同一个 package_template_id,表示同一业务套餐在不同门店或不同版本下的配置。 | 来源: package_id | 角色: 外键(指向套餐模板维,后续可建)
|
||||
selling_price numeric(10,2) 团购售卖价,面向顾客在外部平台的成交价格。当前样本全部为 0,实际平台售价可能在外部系统,不在本地落地。 | 来源: selling_price
|
||||
coupon_face_value numeric(10,2) 券面值或内部结算面值。表示此套餐在门店侧可以抵扣的金额,用于验券或套餐流水时记账。例如“早场特惠一小时”可配置为 40.00,“KTV欢唱四小时”可配置为 200.00。当前样本为 0 但字段设计上非常关键。 | 来源: coupon_money
|
||||
duration_seconds integer 套餐包含的时长,单位为秒。常见取值:3600 表示 1 小时,7200 表示 2 小时,14400 表示 4 小时。核销时可用于换算可用台费时长。 | 来源: duration
|
||||
start_time timestamp with time zone 套餐整体生效开始时间。例如“2025-07-20 00:00:00”。通常从某日零点开始。 | 来源: start_time
|
||||
end_time timestamp with time zone 套餐整体生效结束时间。在该时间点之后不可使用。极大日期(如 9999-12-31 23:59:59)可视为长期有效。 | 来源: end_time
|
||||
table_area_name character varying(100) 套餐适用的门店台区名称,例如“A区中八”“B区中八”“斯诺克”“包厢”“KTV”等。主要用于展示和过滤,配合区域 ID 列实现人类可读的说明。 | 来源: table_area_name
|
||||
is_enabled integer 启用状态枚举。1 表示启用或上架;2 表示停用或下架。侧重表示“配置是否上架”,与 effective_status 区分。 | 来源: is_enabled
|
||||
is_deleted integer 逻辑删除标志。0 表示正常;1 表示逻辑删除(数据仍保留但不再使用)。当前样本全部为 0。 | 来源: is_delete
|
||||
create_time timestamp with time zone 套餐配置在系统中的创建时间,用于审计和版本追踪。 | 来源: create_time
|
||||
tenant_table_area_id_list character varying(512) 租户级台区分组 ID 列表。当前每条记录为一个大整数(例如 2791960001957765)字符串,表示“台区分组”主键。系统通过此分组再关联到具体多个台区。 | 来源: tenant_table_area_id_list | 角色: 外键(指向台区分组维,后续可建)
|
||||
card_type_ids character varying(255) 允许使用本套餐的会员卡类型 ID 列表。当前样本统一为字符串“0”,表示未限制卡种,任意顾客或任意会员卡都可使用。若未来启用,将以分隔的 ID 列表形式记录限定卡种。 | 来源: card_type_ids | 角色: 外键(潜在指向卡种维)
|
||||
-- dim_groupbuy_package_ex
|
||||
groupbuy_package_id bigint 门店侧团购套餐主键。每条记录一个套餐定义,供团购券核销记录指向。平台验券记录中的 group_package_id 通常指向这里。 | 来源: id | 角色: 主键
|
||||
site_name character varying(100) 门店名称,当前均为“朗朗桌球”。属于冗余展示字段,可用于报表标题。 | 来源: site_name
|
||||
usable_count integer 可使用次数上限。当前全部为 9999999,用作“无限次使用”的哨兵值。若未来限制次数,只需配置为具体次数。 | 来源: usable_count
|
||||
date_type integer 日期限制类型枚举。当前样本全部为 1。推测常见含义:1 表示“全部日期可用”;其他值可用于区分工作日、周末或指定日期等模式。 | 来源: date_type
|
||||
usable_range character varying(255) 日期范围说明的文本,例如“周一至周五”等。当前全部为空字符串,实际规则由 date_type 与时间段字段控制。 | 来源: usable_range
|
||||
date_info character varying(255) 更细粒度的日期信息,可能用于存储具体日期列表或节假日规则,形式可能是编码或 JSON 字符串。当前几乎全部为空,仅有极少记录为“0”。 | 来源: date_info
|
||||
start_clock character varying(16) 每日可用时间段的起始时间(第一段),字符串格式 HH:MM:SS,例如“10:00:00”“00:00:00”。与 end_clock 组合定义日内时段。 | 来源: start_clock
|
||||
end_clock character varying(16) 每日可用时间段的结束时间(第一段),字符串格式 HH:MM:SS。与 start_clock 共同描述第一段可用时段。 | 来源: end_clock
|
||||
add_start_clock character varying(16) 附加可用时段的起始时间(第二段),格式 HH:MM:SS。当前样本常见值为“00:00:00”或“10:00:00”。用于配置早场加夜场等双时段场景。 | 来源: add_start_clock
|
||||
add_end_clock character varying(16) 附加可用时段的结束时间(第二段)。常见值如“1.00:00:00”“18:00:00”“23:59:00”。其中“1.00:00:00”表示跨至次日零点,用于表示“可用到第二天凌晨”的场景。 | 来源: add_end_clock
|
||||
area_tag_type integer 区域标记类型枚举。当前样本全部为 1。推测 1 表示“按台区标签限制”(如 A 区、B 区、中八、斯诺克、包厢、KTV 等)。其他取值可能对应按具体台桌或其它规则限用。 | 来源: area_tag_type
|
||||
table_area_id bigint 单一台区 ID。当前样本全部为 0。原始设计用于限定只能在一个具体区域使用,但由于已引入多选逻辑,实际使用已迁移到 tenant_table_area_id_list。 | 来源: table_area_id
|
||||
tenant_table_area_id bigint 租户级台区 ID,单值版本。当前样本全部为 0。与 table_area_id 类似,已被多选列表字段取代。 | 来源: tenant_table_area_id
|
||||
table_area_id_list character varying(512) 门店级台区 ID 列表。当前样本全部为空字符串。根据命名推测原本用于存储多个 table_area_id,实际实现已转向租户维度列表字段。 | 来源: table_area_id_list
|
||||
group_type integer 团购类型枚举。当前样本全部为 1。推测 1 表示“计时类/台费类套餐”。其他取值可能用于商品类套餐、代金券类等,需结合系统配置进一步确认。 | 来源: group_type
|
||||
system_group_type integer 系统级团购类型枚举。当前样本全部为 1。推测 1 表示“券码类团购”,即通过券码核销。其他取值可能为卡内套餐、内部套餐等,具体含义有待业务确认。 | 来源: system_group_type
|
||||
package_type integer 内部业务子类型枚举。样本中取值有 1 与 2,各占比不同。具体含义不明,可能区分不同产品线或套餐来源,例如“平台合作套餐”与“自定义套餐”等,需要参考业务文档。 | 来源: type
|
||||
effective_status integer 当前有效状态枚举,由系统根据时间及配置动态计算。观测值:1 表示当前有效,可正常核销;3 表示失效或已过期(即使 is_enabled 仍为 1,也不可使用)。可用于分析时过滤失效套餐。 | 来源: effective_status
|
||||
max_selectable_categories integer 最大可选择分类数或子项数,具体含义未在样本和说明中体现,当前值全部为 0。可能用于“组合型套餐”中限制可选项目数量。 | 来源: max_selectable_categories
|
||||
creator_name character varying(100) 创建人名称,例如“店长:郑丽珊”。主要用于审计追踪和后台展示。 | 来源: creator_name
|
||||
-- dim_member
|
||||
member_id bigint 租户内会员主键。 | 来源: id | 角色: 主键
|
||||
system_member_id bigint 跨租户全局会员 ID。 | 来源: system_member_id | 角色: 外键
|
||||
tenant_id bigint 租户 ID。 | 来源: tenant_id | 角色: 外键
|
||||
register_site_id bigint 注册门店 ID。 | 来源: register_site_id | 角色: 外键
|
||||
mobile text 会员手机号。 | 来源: mobile
|
||||
nickname text 昵称(未必是真实姓名)。 | 来源: nickname
|
||||
member_card_grade_code integer 会员等级代码:1=金卡?2=银卡?3=钻石卡?4=黑卡?(按照 MD 文档枚举)。 | 来源: member_card_grade_code
|
||||
member_card_grade_name text 等级名称,中文描述。 | 来源: member_card_grade_name
|
||||
create_time timestamp with time zone 会员档案创建时间。 | 来源: create_time
|
||||
update_time timestamp with time zone 最近更新时间。 | 来源: update_time
|
||||
-- dim_member_card_account
|
||||
member_card_id bigint 会员卡账户主键,唯一标识一张具体卡。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID,用于分隔不同业务主体。 | 来源: tenant_id | 角色: 外键
|
||||
register_site_id bigint 开卡门店 ID,对应 dim_site.site_id。 | 来源: register_site_id | 角色: 外键
|
||||
tenant_member_id bigint 对应会员档案中的 member_id(本租户内)。0 表示未绑定会员。 | 来源: tenant_member_id | 角色: 外键
|
||||
system_member_id bigint 全局会员 ID,用于跨租户统一会员身份。0 表示未绑定会员。 | 来源: system_member_id | 角色: 外键
|
||||
card_type_id bigint 卡种 ID,指向卡种配置表。与下面的 grade_code 共同定义卡类别。 | 来源: card_type_id | 角色: 外键
|
||||
member_card_grade_code bigint ?????/?????????????2790683528022853=????2790683528022856=??????2790683528022855=????2790683528022858=????2790683528022857=??
|
||||
member_card_grade_code_name text 卡等级中文名称,与 member_card_grade_code 一一对应。 | 来源: member_card_grade_code_name
|
||||
member_card_type_name text 卡类型名称,通常与 grade_code_name 相同,纯展示字段。 | 来源: member_card_type_name
|
||||
member_name text 持卡会员姓名快照,部分为空表示未绑定。 | 来源: member_name
|
||||
member_mobile text 持卡会员手机号快照。 | 来源: member_mobile
|
||||
balance numeric(18,2) 当前余额或额度。对储值卡表示余额,对其他卡表示剩余金额或次数。 | 来源: balance
|
||||
start_time timestamp with time zone 卡片有效期开始时间。 | 来源: start_time
|
||||
end_time timestamp with time zone 卡片有效期结束时间。 | 来源: end_time
|
||||
last_consume_time timestamp with time zone 最近一次消费时间;若为 "1970-01-01" 表示未消费过。 | 来源: last_consume_time
|
||||
status integer 卡状态:1=正常可用;4=过期/停用。其他值在数据中未出现。 | 来源: status
|
||||
is_delete integer 逻辑删除标记:0=未删除;1=已删除。 | 来源: is_delete
|
||||
-- dim_member_card_account_ex
|
||||
member_card_id bigint id | 来源: bigint | 角色: 会员卡账户主键,唯一标识一张具体卡。
|
||||
site_name text 门店名称展示字段(全部相同)。 | 来源: site_name | 角色: 门店名称展示字段(全部相同)。
|
||||
tenant_name character varying(64) tenant_name | 来源: string | 角色: 租户名称(当前导出为空)。
|
||||
tenantavatar text tenantAvatar | 来源: string | 角色: 租户头像 URL(当前导出为空)。
|
||||
effect_site_id bigint effect_site_id | 来源: bigint | 角色: 卡片限定生效门店 ID。0 表示不限门店,配合 able_cross_site=1 表示全店通用。
|
||||
able_cross_site integer able_cross_site | 来源: int | 角色: 是否允许跨门店使用该卡:1=允许跨店;0=仅限开卡门店。
|
||||
card_physics_type integer card_physics_type | 来源: int | 角色: 物理卡类型:1=实体/标准卡;其他值未出现,含义未知。
|
||||
card_no text card_no | 来源: string | 角色: 物理卡号或条码(当前全部为空)。
|
||||
bind_password text bind_password | 来源: string | 角色: 卡绑定密码(未启用)。
|
||||
use_scene text use_scene | 来源: string | 角色: 使用场景说明(当前为空)。
|
||||
denomination numeric(18,2) denomination | 来源: decimal | 角色: 面额或初始储值额度(当前均为0.0,未启用)。
|
||||
create_time timestamp with time zone create_time | 来源: datetime | 角色: 卡片创建时间。
|
||||
disable_start_time timestamp with time zone disable_start_time | 来源: datetime | 角色: 卡片禁用开始时间,当前为默认值表示未禁用。
|
||||
disable_end_time timestamp with time zone disable_end_time | 来源: datetime | 角色: 卡片禁用结束时间,当前为默认值表示未禁用。
|
||||
is_allow_give integer is_allow_give | 来源: int | 角色: 是否允许转赠给他人:0=不允许;1=允许。
|
||||
is_allow_order_deduct integer is_allow_order_deduct | 来源: int | 角色: 是否允许在订单层面统一扣款:0=不允许;1=允许。
|
||||
sort integer sort | 来源: int | 角色: 前端排序序号。
|
||||
table_discount numeric(10,2) table_discount | 来源: float | 角色: 台费折扣率(折扣百分比,10.0=不打折,9.0=九折等)。当前全部 10.0。
|
||||
goods_discount numeric(10,2) goods_discount | 来源: float | 角色: 商品折扣率,当前为 10.0 表示无折扣。
|
||||
assistant_discount numeric(10,2) assistant_discount | 来源: float | 角色: 助教服务折扣率,当前为 10.0。
|
||||
assistant_reward_discount numeric(10,2) assistant_reward_discount | 来源: float | 角色: 助教奖励折扣率,当前为 10.0(未启用)。
|
||||
table_service_discount numeric(10,2) table_service_discount | 来源: float | 角色: 台费服务类折扣率,当前为 10.0。
|
||||
goods_service_discount numeric(10,2) goods_service_discount | 来源: float | 角色: 商品服务折扣率,当前为 10.0。
|
||||
assistant_service_discount numeric(10,2) assistant_service_discount | 来源: float | 角色: 助教服务类折扣率,当前为 10.0。
|
||||
coupon_discount numeric(10,2) coupon_discount | 来源: float | 角色: 使用券的折扣比例(全部 10.0,未使用)。
|
||||
table_discount_sub_switch integer table_discount_sub_switch | 来源: int | 角色: 台费折扣叠加开关:1=叠加其他折扣;2=不叠加,仅用卡折扣。
|
||||
goods_discount_sub_switch integer goods_discount_sub_switch | 来源: int | 角色: 商品折扣叠加开关,意义同上。
|
||||
assistant_discount_sub_switch integer assistant_discount_sub_switch | 来源: int | 角色: 助教折扣叠加开关,意义同上。
|
||||
assistant_reward_discount_sub_switch integer assistant_reward_discount_sub_switch | 来源: int | 角色: 助教奖励折扣叠加开关(未启用)。
|
||||
goods_discount_range_type integer goods_discount_range_type | 来源: int | 角色: 商品折扣范围类型,未在文档说明具体含义。
|
||||
table_deduct_radio numeric(10,2) table_deduct_radio | 来源: float | 角色: 台费抵扣比例(百分比)。100.0 表示允许全额抵扣;0=不允许。
|
||||
goods_deduct_radio numeric(10,2) goods_deduct_radio | 来源: float | 角色: 商品抵扣比例,意义同上。
|
||||
assistant_deduct_radio numeric(10,2) assistant_deduct_radio | 来源: float | 角色: 助教抵扣比例,意义同上。
|
||||
table_service_deduct_radio numeric(10,2) table_service_deduct_radio | 来源: float | 角色: 台费服务金抵扣比例。
|
||||
goods_service_deduct_radio numeric(10,2) goods_service_deduct_radio | 来源: float | 角色: 商品服务金抵扣比例。
|
||||
assistant_service_deduct_radio numeric(10,2) assistant_service_deduct_radio | 来源: float | 角色: 助教服务金抵扣比例。
|
||||
assistant_reward_deduct_radio numeric(10,2) assistant_reward_deduct_radio | 来源: float | 角色: 助教奖励金抵扣比例(未启用)。
|
||||
coupon_deduct_radio numeric(10,2) coupon_deduct_radio | 来源: float | 角色: 券抵扣比例(未启用)。
|
||||
cardsettlededuct numeric(18,2) cardSettleDeduct | 来源: float | 角色: 结算时统一扣卡金额配置(当前为 0.0,未使用)。
|
||||
tablecarddeduct numeric(18,2) tableCardDeduct | 来源: float | 角色: 台费扣卡金额配置,当前 0.0。
|
||||
tableservicecarddeduct numeric(18,2) tableServiceCardDeduct | 来源: float | 角色: 台费服务金扣卡金额配置。
|
||||
goodscardeduct numeric(18,2) goodsCarDeduct | 来源: float | 角色: 商品扣卡金额配置。
|
||||
goodsservicecarddeduct numeric(18,2) goodsServiceCardDeduct | 来源: float | 角色: 商品服务金扣卡金额配置。
|
||||
assistantcarddeduct numeric(18,2) assistantCardDeduct | 来源: float | 角色: 助教扣卡金额配置。
|
||||
assistantservicecarddeduct numeric(18,2) assistantServiceCardDeduct | 来源: float | 角色: 助教服务金扣卡金额配置。
|
||||
assistantrewardcarddeduct numeric(18,2) assistantRewardCardDeduct | 来源: float | 角色: 助教奖励金扣卡金额配置(未启用)。
|
||||
couponcarddeduct numeric(18,2) couponCardDeduct | 来源: float | 角色: 使用券扣卡金额配置。
|
||||
deliveryfeededuct numeric(18,2) deliveryFeeDeduct | 来源: float | 角色: 配送费扣卡金额配置(未启用)。
|
||||
tableareaid text tableAreaId | 来源: list | 角色: 可用台区 ID 列表,空表示不限台区。
|
||||
goodscategoryid text goodsCategoryId | 来源: list | 角色: 可用商品分类 ID 列表,空表示不限制商品类别。
|
||||
pdassisnatlevel text pdAssisnatLevel | 来源: list | 角色: 允许的陪打助教等级列表,空表示不限。
|
||||
cxassisnatlevel text cxAssisnatLevel | 来源: list | 角色: 允许的促销助教等级列表,空表示不限。
|
||||
-- dim_member_ex
|
||||
member_id bigint 租户内会员主键。 | 来源: id | 角色: 主键
|
||||
referrer_member_id bigint 推荐人会员 ID,营销分析用。 | 来源: referrer_member_id
|
||||
point numeric(18,2) 积分余额(暂未启用)。 | 来源: point
|
||||
register_site_name text 注册门店名称。 | 来源: site_name
|
||||
growth_value numeric(18,2) 成长值,暂未启用。 | 来源: growth_value
|
||||
user_status integer 会员状态枚举:1=正常,其它值未出现。 | 来源: user_status
|
||||
status integer 帐户状态:1=正常;其它值未出现。 | 来源: status
|
||||
-- dim_site
|
||||
site_id bigint 门店主键 ID,唯一标识一家门店。与所有事实表中的 site_id 对应。 | 来源: siteProfile.id | 角色: 主键
|
||||
org_id bigint 上级组织 ID,用于区域组织划分。 | 来源: siteProfile.org_id | 角色: 外键
|
||||
shop_name text 门店名称,展示用。 | 来源: siteProfile.shop_name
|
||||
business_tel text 门店电话。 | 来源: siteProfile.business_tel
|
||||
full_address text 门店完整地址。 | 来源: siteProfile.full_address
|
||||
tenant_id bigint 租户 ID。与其它表 tenant_id 对应。 | 来源: siteProfile.tenant_id | 角色: 外键
|
||||
-- dim_site_ex
|
||||
site_id bigint 门店主键 ID,唯一标识一家门店。与所有事实表中的 site_id 对应。 | 来源: siteProfile.id | 角色: 主键
|
||||
avatar text 门店头像 URL。 | 来源: siteProfile.avatar
|
||||
address text 地址简写。 | 来源: siteProfile.address
|
||||
longitude numeric(18,2) 经度。 | 来源: siteProfile.longitude
|
||||
latitude numeric(18,2) 纬度。 | 来源: siteProfile.latitude
|
||||
tenant_site_region_id bigint 地区编码。 | 来源: siteProfile.tenant_site_region_id
|
||||
auto_light integer 是否自动控制灯光:1=是,2=否(根据系统约定)。 | 来源: siteProfile.auto_light
|
||||
light_status integer 灯光状态,系统预留字段。 | 来源: siteProfile.light_status
|
||||
light_type integer 灯光类型,预留字段。 | 来源: siteProfile.light_type
|
||||
light_token text 灯光控制令牌。 | 来源: siteProfile.light_token
|
||||
site_type integer 门店类型枚举(未在导出中说明,视系统配置)。 | 来源: siteProfile.site_type
|
||||
site_label text 门店标签,展示用。 | 来源: siteProfile.site_label
|
||||
attendance_enabled integer 门店是否启用考勤功能:1=启用,2=不启用。 | 来源: siteProfile.attendance_enabled
|
||||
attendance_distance integer 考勤打卡距离限制(米)。 | 来源: siteProfile.attendance_distance
|
||||
customer_service_qrcode text 客服二维码 URL。 | 来源: siteProfile.customer_service_qrcode
|
||||
customer_service_wechat text 客服微信号。 | 来源: siteProfile.customer_service_wechat
|
||||
fixed_pay_qrcode text 固定收款二维码。 | 来源: siteProfile.fixed_pay_qrCode
|
||||
prod_env text 环境标记(生产/测试)。 | 来源: siteProfile.prod_env
|
||||
shop_status integer 门店状态,未在文档解释。 | 来源: siteProfile.shop_status
|
||||
create_time timestamp with time zone 门店创建时间。 | 来源: siteProfile.create_time
|
||||
update_time timestamp with time zone 门店最近更新时间。 | 来源: siteProfile.update_time
|
||||
-- dim_store_goods
|
||||
site_goods_id bigint 门店级商品 ID,本表主键;其它业务表中的 site_goods_id 与此对应,用于库存、销售等关联。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID,同一品牌下多个门店共享,用于跨门店汇总分析。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID,对应门店维度表主键。 | 来源: site_id | 角色: 外键
|
||||
tenant_goods_id bigint 租户级(品牌级)商品 ID,用于关联 dim_tenant_goods,实现跨门店统一商品档案。 | 来源: tenant_goods_id | 角色: 外键
|
||||
goods_name text 商品名称,例如“合味道泡面”“地道肠”“茶位费”。 | 来源: goods_name
|
||||
goods_category_id bigint 商品一级分类 ID,对应商品分类维表主键,与 category_level1_name 一一对应。 | 来源: goods_category_id | 角色: 外键
|
||||
goods_second_category_id bigint 商品二级分类 ID,其父分类为 goods_category_id。 | 来源: goods_second_category_id | 角色: 外键
|
||||
category_level1_name text 一级分类名称,如“零食”“酒水”“服务费”,用于报表维度展示。 | 来源: oneCategoryName
|
||||
category_level2_name text 二级分类名称,如“面”“洋酒”“纸巾”,用于更细粒度分类分析。 | 来源: twoCategoryName
|
||||
batch_stock_qty integer 当前成本批次的库存数量,用于按 cost_price 估算库存价值。 | 来源: batch_stock_quantity
|
||||
sale_qty integer 截至导出时的销售数量(件),当前数据中与 total_sales_qty 相同。 | 来源: sale_num
|
||||
total_sales_qty integer 累计销售数量;当前导出周期下与 sale_qty 一致,为历史全量口径。 | 来源: total_sales
|
||||
sale_price numeric(18,2) 商品标准销售价(挂牌价),单位为元。实际结算可能有折扣或券抵扣。 | 来源: sale_price
|
||||
created_at timestamp with time zone 门店商品档案创建时间(在门店建立该商品档案时的时间点)。 | 来源: create_time
|
||||
updated_at timestamp with time zone 最近一次修改商品档案的时间(包括价格调整、状态变更等)。 | 来源: update_time
|
||||
avg_monthly_sales numeric(18,4) 平均月销量(件/月),由某个统计周期内销售数据折算而来,用于补货和品类管理分析。 | 来源: average_monthly_sales
|
||||
goods_state integer 商品基础状态枚举:1=正常状态(主流值),2=特殊状态(如新建未完全启用或停售但未彻底下架,通常伴随 stock=0、days_on_shelf=0)。 | 来源: goods_state
|
||||
enable_status integer 档案启用状态:1=启用;2=停用(推测,样本中未出现);控制商品档案是否参与业务处理。 | 来源: enable_status
|
||||
send_state integer 销售端可售状态:1=可销售/可下单;其他值可能代表停售或仅内部使用(当前样本全部为 1)。 | 来源: send_state
|
||||
is_deleted integer 逻辑删除标志:0=未删除(有效档案);1=已删除(逻辑删除,不再参与业务但保留历史引用)。 | 来源: is_delete
|
||||
-- dim_store_goods_ex
|
||||
site_goods_id bigint 门店级商品 ID,本表主键;其它业务表中的 site_goods_id 与此对应,用于库存、销售等关联。 | 来源: id | 角色: 主键
|
||||
site_name text 门店名称,例如“朗朗桌球”,是对 site_id 的冗余展示,方便直接阅读。 | 来源: siteName
|
||||
unit text 销售计量单位,如“包”“瓶”“个”“份”“杯”等。 | 来源: unit
|
||||
goods_barcode text 商品条形码,用于扫码销售;当前样本多为空。 | 来源: goods_bar_code
|
||||
goods_cover_url text 商品图片 URL,用于前端展示商品图片。 | 来源: goods_cover
|
||||
pinyin_initial text 商品名称拼音首字母缩写,有时多个别名用逗号分隔,用于按字母快速检索和排序。 | 来源: pinyin_initial
|
||||
stock_qty integer 当前主单位可用库存数量,以 unit 为单位。 | 来源: stock
|
||||
stock_secondary_qty integer 副单位库存数量;若商品存在双单位(如箱/瓶),用于记录副单位库存;当前门店未启用双单位库存,样本中为 0。 | 来源: stock_A
|
||||
safety_stock_qty integer 安全库存阈值,低于该值时系统可提示补货;当前门店尚未配置,样本中为 0。 | 来源: safe_stock
|
||||
cost_price numeric(18,4) 商品单件成本价,单位元;部分商品为 0,表示未录入或由其它模块结转成本。 | 来源: cost_price
|
||||
cost_price_type integer 成本类型枚举:1=固定成本价(按 cost_price 计),2=动态成本价(按采购单等方式结转,当前多数仍为暂估)。 | 来源: cost_price_type
|
||||
provisional_total_cost numeric(18,2) 当前库存暂估总成本,单位元;通常约等于 batch_stock_qty × cost_price。 | 来源: provisional_total_cost
|
||||
total_purchase_cost numeric(18,2) 当前库存总采购成本,单位元;当前样本中与 provisional_total_cost 相等,为后续精算成本预留。 | 来源: total_purchase_cost
|
||||
min_discount_price numeric(18,2) 最低允许成交价(限价),单位元;收银改价时需保证成交价 ≥ 此值,为 0 时表示未设置限价或由其它规则控制。 | 来源: min_discount_price
|
||||
is_discountable integer 是否允许参与折扣的标志:1=允许参与折扣;0=不参与任何折扣策略。当前样本全部为 1。 | 来源: able_discount
|
||||
days_on_shelf integer 商品在架天数或可售天数,大致等于当前时间减去首次上架时间;0 通常表示刚建档或刚启用。 | 来源: days_available
|
||||
audit_status integer 审核状态枚举:2=审核通过(当前唯一值);其他值可能代表待提交、待审核、审核不通过等。 | 来源: audit_status
|
||||
sale_channel integer 销售渠道枚举:当前样本全部为 1 表示线下门店渠道;其他值可用于区分外卖、线上商城等渠道。 | 来源: sale_channel
|
||||
is_warehousing integer 是否纳入库存管理:1=参与库存管理(有出入库流水);0 或其他值可能表示不计库存(样本中全部为 1)。 | 来源: is_warehousing
|
||||
freeze_status integer 冻结状态:0=未冻结;非 0 可能表示锁定库存或禁止出库,具体业务规则需系统确认。 | 来源: freeze
|
||||
forbid_sell_status integer 禁止销售状态:1=未禁止,允许销售;2=被禁止销售,即使上架也不能卖(含义基于命名和行业惯例推测)。 | 来源: forbid_sell_status
|
||||
able_site_transfer integer 是否允许跨门店调拨或跨站点共享库存:2=不允许跨店调拨(当前主流值);0=未配置(个别记录),含义为是否参与跨店调拨功能。 | 来源: able_site_transfer
|
||||
custom_label_type integer 自定义标签类型(基于字段名和取值推测):2=使用自定义标签;1 可能表示使用系统默认标签。具体影响哪些标签功能需业务确认。 | 来源: custom_label_type
|
||||
option_required integer 是否需要额外选项或规格(基于字段名和取值推测):1=不需要额外选项,按单规格销售;其他值可能表示必须选择配料或口味。当前样本全部为 1。 | 来源: option_required
|
||||
remark text 商品备注,可填写口味说明、供应商信息、注意事项等;当前样本全部为空。 | 来源: remark
|
||||
sort_order integer 前端展示排序权重,控制商品在列表中的显示顺序,具体规则(数值越大还是越小排前)由业务配置决定。 | 来源: sort
|
||||
-- dim_table
|
||||
table_id bigint 台桌主键,唯一标识一张台或包厢。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户 ID。 | 来源: tenantId | 角色: 外键
|
||||
site_id bigint 门店 ID。 | 来源: siteId | 角色: 外键
|
||||
table_name text 台桌名称/编号,如 A17、888。 | 来源: tableName
|
||||
site_table_area_id bigint 门店区 ID,用于区分 A区/B区/补时区等。 | 来源: siteTableAreaId | 角色: 外键
|
||||
site_table_area_name text 区域名称,如 “A区”“补时长”。 | 来源: siteTableAreaName
|
||||
tenant_table_area_id bigint 租户级区域 ID。 | 来源: tenantTableAreaId | 角色: 外键
|
||||
table_price numeric(18,2) ???????? table_fee_transactions ??????? id ?? table_fee_transactions.site_table_id
|
||||
-- dim_table_ex
|
||||
table_id bigint 台桌主键,唯一标识一张台或包厢。 | 来源: id | 角色: 主键
|
||||
show_status integer 显示状态:1=正常台;其他值=特殊用途(包厢、补时长等)。 | 来源: showStatus
|
||||
is_online_reservation integer 是否可线上预约:1=是,2=否。 | 来源: isOnlineReservation
|
||||
table_cloth_use_time integer 已使用台呢时长(秒)。 | 来源: tableClothUseTime
|
||||
table_cloth_use_cycle integer 台呢更换周期阈值(秒)。 | 来源: tableClothUseCycle
|
||||
table_status integer 当前台桌状态:1=空闲,2=使用中,3=暂停中,4=锁定。 | 来源: tableStatus
|
||||
last_maintenance_time timestamp with time zone 最近保养时间(未在 JSON 中出现)。 | 来源: lastMaintenanceTime
|
||||
remark text 备注信息。 | 来源: remark
|
||||
-- dim_tenant_goods
|
||||
tenant_goods_id bigint 租户级商品档案主键 ID,唯一标识一条商品档案。所有业务事实表(销售、库存等)中引用租户级商品时应指向此字段。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID,用于区分不同商户。当前样本中全表同一值,但模型上应作为维表外键,用于关联租户维度。 | 来源: tenant_id | 角色: 外键
|
||||
supplier_id bigint 供应商 ID,用于关联供应商档案维度。当前样本全部为 0,说明门店尚未维护供应商信息或导出视图未包含真实供应商关联,但字段含义明确。 | 来源: supplier_id | 角色: 外键
|
||||
category_name character varying(64) 商品一级分类名称(可读名称),例如:零食、饮料、香烟、雪糕、小吃、酒水、面、槟榔等。真实分类关联通过 goods_category_id 与 goods_second_category_id 实现,此字段主要用于展示和直观分析。 | 来源: categoryName
|
||||
goods_category_id bigint 商品一级分类 ID。与分类维表(例如 dim_goods_category)关联,构成商品分类的第一层。一个 goods_category_id 对应一个 category_name。 | 来源: goods_category_id | 角色: 外键
|
||||
goods_second_category_id bigint 商品二级分类 ID。与分类维表的二级节点关联,用于更细粒度的品类统计。取值数目约十四种,每个值属于某个一级分类之下。 | 来源: goods_second_category_id | 角色: 外键
|
||||
goods_name character varying(128) 商品名称(前台展示名),如 “东方树叶”“红烧牛肉面”“百威 235 毫升”等。当前样本中基本唯一。作为用户认知的主显示名称,用于报表、前台展示、小票打印。 | 来源: goods_name
|
||||
goods_number character varying(64) 商品内部编号或自定义货号。当前样本中各记录不重复,如 “1”“2”“10”“11” 等。可用于与其他系统对接或人工查找,有一定对账和排错价值。 | 来源: goods_number
|
||||
unit character varying(16) 商品计量单位,例如:瓶、包、个、份、根、盒、杯、桶、盘、支等。用于解释数量含义,是销售数量与库存数量的度量单位。 | 来源: unit
|
||||
market_price numeric(18,2) 商品标价或标准销售单价。例如 2、5、6、8、10、12、15、18、20、28 元。POS 默认销售价格,结算时的基础金额字段。 | 来源: market_price
|
||||
goods_state integer 商品状态枚举。当前样本全部为 1,推测含义为“正常”“已上架”或“有效”。其他值(数据中未出现)通常表示下架、停用或草稿状态。用于控制商品是否可销售。 | 来源: goods_state
|
||||
create_time timestamp with time zone 商品档案创建时间,格式为 “YYYY-MM-DD HH:MM:SS”。每条记录唯一。用于增量抽取和审计,也可用于分析商品生命周期。 | 来源: create_time
|
||||
update_time timestamp with time zone 商品档案最近一次修改时间,可为空(表示自创建后未修改)。用于增量同步、变化跟踪和审计分析。 | 来源: update_time
|
||||
is_delete integer 逻辑删除标志。枚举:0 表示未删除(有效商品);1 表示已逻辑删除(在前台不再展示)。当前样本全部为 0。用于软删除控制和历史数据保留。 | 来源: is_delete
|
||||
-- dim_tenant_goods_ex
|
||||
tenant_goods_id bigint 租户级商品档案主键 ID,唯一标识一条商品档案。所有业务事实表(销售、库存等)中引用租户级商品时应指向此字段。 | 来源: id | 角色: 主键
|
||||
remark_name character varying(128) 商品备注名或别名,目前样本中均为空。设计用途为简写名、特殊展示名或内部备注,在当前门店尚未启用。 | 来源: remark_name
|
||||
pinyin_initial character varying(128) 商品拼音首字母或助记码,用于前台按拼音检索,如 “DFSY,DFSX”“HSNRM,GSNRM”“SP” 等。主要为操作便利,对经营分析影响较小。 | 来源: pinyin_initial
|
||||
goods_cover character varying(512) 商品封面图片 URL,用于前端展示商品图片。多个商品可能共用同一图片。对经营和结算逻辑无直接影响。 | 来源: goods_cover
|
||||
goods_bar_code character varying(64) 商品条码(如 EAN 码)。当前样本全部为空。含义明确但尚未使用,未来可用于扫码收银或与第三方商品库对接。 | 来源: goods_bar_code
|
||||
commodity_code character varying(64) 对外商品编码或系列编码,用于与外部系统或其他内部模块对接。例如 “10000”“100000”“10000028”等。一个编码在多条商品上复用,说明它不是主键而是“系列标识”或“外部编码”。具体业务含义依赖上游系统定义。 | 来源: commodity_code
|
||||
commodity_code_list character varying(256) 商品编码列表的序列化形式,对应源 JSON 的数组字段(当前每条记录仅一个元素)。设计上支持 “一个商品多个编码” 场景,目前仅为 commodity_code 的冗余表现形式。 | 来源: commodityCode
|
||||
min_discount_price numeric(18,2) 商品可售最低价(底价)。部分记录为 0.00,表示未设置底价或沿用系统默认规则。用于限制打折或手动改价的下限,防止亏损销售。 | 来源: min_discount_price
|
||||
cost_price numeric(18,2) 商品成本价,当前大多数为 0.00,仅少数录入 2.0、2.5、3.0 等。用于成本核算与毛利分析。虽当前门店未完整维护,但字段含义清晰,属于成本分析必备结构。 | 来源: cost_price
|
||||
cost_price_type integer 成本价格类型枚举,用于标识成本价的来源或计算方式。已知取值:1 和 2。常见推测:1 表示手工录入成本;2 表示按最近进货价或加权平均价生成。具体含义需结合系统枚举字典确认。 | 来源: cost_price_type
|
||||
able_discount integer 是否允许该商品参与折扣的标志。已知取值:1。按命名推断枚举约定为:1 表示允许参与打折;0 表示不允许参与打折(当前样本未出现)。配合活动、整单折扣等控制哪些商品可享优惠。 | 来源: able_discount
|
||||
sale_channel integer 销售渠道类型枚举。当前样本全部为 1,推测为“线下门店正常销售渠道”。理论上可扩展为不同渠道值,例如外卖、小程序、电商等,用于渠道维度分析。具体枚举说明依赖系统配置。 | 来源: sale_channel
|
||||
is_warehousing integer 是否纳入库存管理的标志。已知取值:1,表示纳入库存管理;0 则表示不纳入库存管理(虚拟商品等,当前未出现)。本门店所有商品均启用库存管理。 | 来源: is_warehousing
|
||||
is_in_site boolean 是否在当前门店启用或上架。当前样本全部为 false。由于该文件是租户级商品档案视图,且 isInSite 全为 false,该字段在本视图的实际含义存在不确定性,可能仅在门店级商品表中才有明确业务意义。 | 来源: isInSite
|
||||
able_site_transfer integer 是否允许门店间调拨或门店级操作的枚举。已知取值:2 为绝大多数,0 为少数一条。按命名推测大致含义为:2 表示允许调拨或默认允许;0 表示禁止调拨。实际枚举定义需查阅系统配置,当前无法完全确定具体业务规则。 | 来源: able_site_transfer
|
||||
common_sale_royalty integer 普通销售提成或佣金配置字段,单位和含义需结合上游系统(可能为金额或比例)。当前样本全部为 0,说明未启用商品级提成配置。 | 来源: common_sale_royalty
|
||||
point_sale_royalty integer 积分销售相关的提成或赠送规则配置字段。当前样本全部为 0,同样未启用该功能。具体数值含义(百分比或固定值)需结合系统定义。 | 来源: point_sale_royalty
|
||||
out_goods_id bigint 外部系统商品 ID,用于对接第三方平台或统一商品库时作为映射主键。目前样本全部为 0,说明尚未配置外部商品映射,具体对接规则依赖上游系统。 | 来源: out_goods_id
|
||||
-- dwd_assistant_service_log
|
||||
assistant_service_id bigint 助教服务流水主键,系统内唯一标识一次助教服务记录。 | 来源: id | 角色: 主键
|
||||
order_trade_no bigint 订单交易号,用于与台费、商品、支付等同一订单下的其他明细串联。 | 来源: order_trade_no | 角色: 外键
|
||||
order_settle_id bigint 结算单号,对应结账记录、小票中的结算主键。 | 来源: order_settle_id | 角色: 外键
|
||||
order_pay_id bigint 支付记录主键 ID,用于关联支付流水。 | 来源: order_pay_id | 角色: 外键
|
||||
order_assistant_id bigint 订单中“助教项目明细”的内部 ID,一笔订单中多段助教服务时用于区分。 | 来源: order_assistant_id | 角色: 外键
|
||||
order_assistant_type integer 助教服务类型枚举:1 表示常规助教服务(如基础课);2 表示附加类助教服务(如附加课);其他值预留。 | 来源: order_assistant_type
|
||||
tenant_id bigint 租户/品牌 ID,用于区分商户。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID,对应门店维表中的门店主键。 | 来源: site_id | 角色: 外键
|
||||
site_table_id bigint 球台/包厢 ID,对应台桌维表主键。 | 来源: site_table_id | 角色: 外键
|
||||
tenant_member_id bigint 商户维度会员 ID,对应会员档案主键;0 表示非会员或散客。 | 来源: tenant_member_id | 角色: 外键
|
||||
system_member_id bigint 系统级会员 ID,用于跨门店识别同一会员。 | 来源: system_member_id | 角色: 外键
|
||||
assistant_no character varying(64) 助教编号/工号,如 “27”;与助教档案中的工号一致。 | 来源: assistantNo
|
||||
nickname character varying(64) 助教对外昵称,如“佳怡”“周周”;用于展示,不参与业务逻辑。 | 来源: nickname
|
||||
site_assistant_id bigint 门店维度助教 ID,对应助教账号维表主键。 | 来源: site_assistant_id | 角色: 外键
|
||||
user_id bigint 助教对应的系统用户 ID,对应账号体系中的 user_id。 | 来源: user_id | 角色: 外键
|
||||
assistant_team_id bigint 助教团队 ID,用于分组统计团队业绩。 | 来源: assistant_team_id | 角色: 外键
|
||||
person_org_id bigint 助教所属人事组织/部门 ID,如“助教部”;用于组织维度分析。 | 来源: person_org_id | 角色: 外键
|
||||
assistant_level integer 助教等级编码:8=助教管理;10=初级;20=中级;30=高级;用于薪酬/评价分层。 | 来源: assistant_level | 角色: 外键
|
||||
level_name character varying(64) 助教等级名称,与 assistant_level 对应,如“初级”“中级”“高级”“助教管理”。 | 来源: levelName
|
||||
skill_id bigint 助教服务课程/技能 ID,应对应课程/技能配置表。 | 来源: skill_id | 角色: 外键
|
||||
skill_name character varying(64) 助教服务课程/技能名称,如“基础课”“附加课”。 | 来源: skillName
|
||||
ledger_unit_price numeric(10,2) 助教服务标准单价(原价),例如按小时或按节课的标价。 | 来源: ledger_unit_price
|
||||
ledger_amount numeric(10,2) 按标准单价计算的应收金额,近似等于 ledger_unit_price×计费时长换算后的金额。 | 来源: ledger_amount
|
||||
projected_income numeric(10,2) 实际计入门店收入的金额,已经考虑会员权益、券抵扣等后的结果。 | 来源: projected_income
|
||||
coupon_deduct_money numeric(10,2) 由优惠券、团购券等直接抵扣到本次助教服务上的金额;0 表示未使用券。 | 来源: coupon_deduct_money
|
||||
income_seconds integer 计费秒数(用于计算应收收入的时间长度),通常为按分钟取整的秒数。 | 来源: income_seconds
|
||||
real_use_seconds integer 实际服务时长(秒),真实消耗的时间,用于分析助教工作量。 | 来源: real_use_seconds
|
||||
add_clock integer 加钟秒数,在原有预约基础上临时增加的服务时间,数值为 60 的倍数。 | 来源: add_clock
|
||||
create_time timestamp with time zone 助教流水记录创建时间,接近下单/结算时间。 | 来源: create_time
|
||||
start_use_time timestamp with time zone 助教实际开始服务时间,通常与 ledger_start_time 一致。 | 来源: start_use_time
|
||||
last_use_time timestamp with time zone 助教最后一次服务时间,通常与 ledger_end_time 一致。 | 来源: last_use_time
|
||||
is_delete integer 逻辑删除标志:0 未删除;1 已逻辑删除,用于保留历史数据。 | 来源: is_delete
|
||||
-- dwd_assistant_service_log_ex
|
||||
assistant_service_id bigint 助教服务流水主键,系统内唯一标识一次助教服务记录。 | 来源: id | 角色: 主键
|
||||
table_name character varying(64) 球台名称,如 “A17”“S1”,与 site_table_id 冗余,用于展示。 | 来源: tableName
|
||||
assistant_name character varying(64) 助教姓名,如“何海婷”;与助教档案中的真实姓名一致。 | 来源: assistantName
|
||||
ledger_name character varying(128) 助教计费项目名称,如“2-佳怡”等,通常为展示用组合字段。 | 来源: ledger_name
|
||||
ledger_group_name character varying(128) 助教项目所属的计费分组/套餐分组名称,目前导出数据中为空,未看到实际使用场景。 | 来源: ledger_group_name
|
||||
ledger_count integer 台账计费时长(秒),通常与 real_use_seconds 接近或相等。取income_seconds TEXT
|
||||
member_discount_amount numeric(10,2) 由会员卡折扣产生的优惠金额,当前样本中为 0,但字段语义明确。 | 来源: member_discount_amount
|
||||
manual_discount_amount numeric(10,2) 收银员手动减免金额(人工改价);当前导出数据中为 0。 | 来源: manual_discount_amount
|
||||
service_money numeric(10,2) 与助教结算的金额或服务成本金额,当前数据全部为 0,具体结算规则未见启用。 | 来源: service_money
|
||||
returns_clock integer 退钟秒数(取消加钟或提前结束退回的时间),当前样本中全部为 0,未见业务使用。 | 来源: returns_clock
|
||||
ledger_start_time timestamp with time zone 台账计费起始时间。 | 来源: ledger_start_time
|
||||
ledger_end_time timestamp with time zone 台账计费结束时间,可作为本次服务结束时间。 | 来源: ledger_end_time
|
||||
ledger_status integer 助教流水状态:当前数据为 1,表示正常有效;其他值预留给已作废、未结算等状态。 | 来源: ledger_status
|
||||
is_confirm integer 确认状态:当前样本为 2,一般含义为 1=待确认,2=已确认/已完成(含义基于字段名和现有值推断)。 | 来源: is_confirm
|
||||
is_single_order integer 是否单独订单:1 表示助教服务作为单独订单结算;0 表示与其他项目合单结算。当前样本全部为 1。 | 来源: is_single_order
|
||||
is_not_responding integer 是否存在“未响应/爽约”等异常:0 表示正常;1 表示未响应或爽约(基于字段名推断,当前数据均为 0)。 | 来源: is_not_responding
|
||||
is_trash integer 是否已废除:0 表示正常有效;1 表示已废除,与助教废除记录表(assistant_cancellation_records)对应。 | 来源: is_trash
|
||||
trash_applicant_id bigint 提出废除申请的员工 ID,用于追溯谁发起了废除操作。 | 来源: trash_applicant_id | 角色: 外键
|
||||
trash_applicant_name character varying(64) 废除申请人姓名,仅用于展示,与 trash_applicant_id 冗余。 | 来源: trash_applicant_name
|
||||
trash_reason character varying(255) 废除原因文案,如“顾客取消”“录入错误”,便于分析异常原因。 | 来源: trash_reason
|
||||
salesman_user_id bigint 营业员/销售员用户 ID,大多为 0,当前门店未明显使用此维度。 | 来源: salesman_user_id | 角色: 外键
|
||||
salesman_name character varying(64) 营业员/销售员姓名,多数为空字符串。 | 来源: salesman_name
|
||||
salesman_org_id bigint 营业员所属组织/部门 ID,多数为 0,尚未看到实际业务使用。 | 来源: salesman_org_id | 角色: 外键
|
||||
skill_grade integer 课程技能评分(整数),当前样本全为 0,评价功能尚未启用。 | 来源: skill_grade
|
||||
service_grade integer 服务态度评分(整数),当前样本全为 0。 | 来源: service_grade
|
||||
composite_grade numeric(5,2) 综合评分(技能+服务等加权结果),当前样本为 0。 | 来源: composite_grade
|
||||
sum_grade numeric(10,2) 累计评分总和,用于计算平均分,当前样本为 0。 | 来源: sum_grade
|
||||
get_grade_times integer 获得评价的次数,当前样本为 0。 | 来源: get_grade_times
|
||||
grade_status integer 评价状态枚举:当前样本为 1,一般含义为“未评价/正常”,其他状态未见实际值。 | 来源: grade_status
|
||||
composite_grade_time timestamp with time zone 最近一次综合评分时间或评价更新时间,当前为默认时间 “0001-01-01 00:00:00”。 | 来源: composite_grade_time
|
||||
-- dwd_assistant_trash_event
|
||||
assistant_trash_event_id bigint 助教废除事件主键。与源 JSON 中 id 一一对应,单表内唯一。没有业务含义,只作为技术主键使用。 | 来源: id | 角色: 主键
|
||||
site_id bigint 门店 ID。与其他 JSON 中的 siteId / site_id 含义一致。用来关联 dim_site。当前样例全部为同一门店(朗朗桌球),但设计上支持多门店。 | 来源: siteId | 角色: 外键(指向 dim_site)
|
||||
table_id bigint 台桌 ID。对应 site_tables_master.json 中的 id。用于定位哪一张球台发生了助教废除,用于后续软关联台费流水、助教流水时的重要条件。 | 来源: tableId | 角色: 外键(指向 dim_table)
|
||||
table_area_id bigint 台桌区域 ID。应与台桌维或区域维中的 area_id 一致,用于按区域统计(A 区/B 区/VIP 包厢等)。 | 来源: tableAreaId | 角色: 外键(潜在指向 dim_table_area)
|
||||
assistant_no character varying(32) 助教编号(工号/序号),如 '2'、'4'、'27' 等。与助教档案表 assistant_accounts_master.assistant_no、助教流水中的 assistantNo 一致,用于标识哪位助教。枚举:在门店内是有限编号集合,但并非硬编码含义。 | 来源: assistantOn | 角色: 外键(指向 dim_assistant)
|
||||
assistant_name character varying(64) 助教姓名/昵称,如 “泡芙”“佳怡”等。为冗余展示字段,真实姓名以助教档案为准。当前数据中与档案一致。 | 来源: assistantName
|
||||
charge_minutes_raw integer 助教被废除前“已计费时长(分钟)”的原始值。单位为分钟。示例:214、3600、10800 等。0 表示尚未发生有效计费就被废除。当前数据中存在异常大值(例如 10800),这一业务含义需结合实际规则理解,但本字段原样保留。枚举:数值型,无固定枚举。 | 来源: pdChargeMinutes
|
||||
abolish_amount numeric(18,2) 与本次助教废除操作关联的金额,单位元。字面含义为“助教废除金额”。当前样例均为非负数,如 5.83、570.00、0.00 等。正负方向:按照 ODS/JSON 原样保留,暂不在数仓层赋予“收入/支出”的方向含义,后续在 DWS 层按业务规则解释(例如是退还顾客、扣除收益等)。 | 来源: assistantAbolishAmount
|
||||
trash_reason character varying(255) 废除原因的文本说明,例如可以写“顾客临时取消”“误操作”等。当前样例中全部为空字符串,说明前台并未使用该字段。但从结构上看,是一个自由文本字段,不是枚举。 | 来源: trashReason
|
||||
create_time timestamp with time zone 这条废除记录创建的时间,格式 YYYY-MM-DD HH:MM:SS。代表系统正式记录“废除操作”的时刻,用于和助教服务流水按时间窗口做软关联(结合 site、table、assistant 等条件)。 | 来源: createTime
|
||||
-- dwd_assistant_trash_event_ex
|
||||
assistant_trash_event_id bigint 助教废除事件主键。与源 JSON 中 id 一一对应,单表内唯一。没有业务含义,只作为技术主键使用。 | 来源: id | 角色: 主键
|
||||
table_name character varying(64) 台桌名称/编号,便于直观看报表,如 “C1”“B9”“VIP1”等。文案冗余自台桌维度。枚举:在门店范围内是有限集合,但不是固定编码表。 | 来源: tableName
|
||||
table_area_name character varying(64) 台桌区域名称(中文),如 “A区”“B区”“C区”“VIP包厢”“补时长”等。展示用文本,具体层级信息由区域维表提供。 | 来源: tableArea
|
||||
-- dwd_groupbuy_redemption
|
||||
redemption_id bigint 团购券核销流水主键。一条记录代表一次团购券使用在某次台费上的一条核销明细。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID。与其他表统一的租户标识,用于品牌维度聚合。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID。与门店维度及其他业务事实中的 site_id 一致。 | 来源: site_id | 角色: 外键
|
||||
table_id bigint 球台 ID。与台桌维度表中的 id 对应,用于确定具体哪一张球台。 | 来源: table_id | 角色: 外键
|
||||
tenant_table_area_id bigint 租户级台区组合 ID。与团购套餐定义中的 tenant_table_area_id_list 元素对应,表示该券实际使用时所在的台区组合。用于校验券的适用台区是否匹配实际台桌。 | 来源: tenant_table_area_id | 角色: 外键
|
||||
table_charge_seconds integer 本次结算中该球台总计计费的秒数(整台计费时间)。当台上除了券覆盖时长之外还有额外计费时间时,该值会大于券核销时长。 | 来源: table_charge_seconds
|
||||
order_trade_no bigint 订单交易号。与台费流水、商品销售、助教服务、小票详情等表的 order_trade_no 一致,用于将同一笔结账中的所有明细串联起来。 | 来源: order_trade_no | 角色: 外键
|
||||
order_settle_id bigint 结算单 ID。与结账记录和小票详情中的结算主键对应,用于从团购券核销记录跳转到整单结算。 | 来源: order_settle_id | 角色: 外键
|
||||
order_coupon_id bigint 订单中的“券使用记录 ID”。与平台或内部券核销表中的主键一致,用于在订单内部定位这条券使用记录。当前与 coupon_origin_id 数值相等。 | 来源: order_coupon_id | 角色: 外键
|
||||
coupon_origin_id bigint 上游系统或第三方平台中该券记录的主键 ID。可在平台验券记录中查到券的来源平台、原订单等。当前与 order_coupon_id 数值一致,但语义是“券来源 ID”。 | 来源: coupon_origin_id | 角色: 外键
|
||||
promotion_activity_id bigint 促销活动 ID。每条记录对应一个活动主键,用于识别券所属的促销活动或团购活动。 | 来源: promotion_activity_id | 角色: 外键
|
||||
promotion_coupon_id bigint 团购套餐定义 ID。与 group_buy_packages.json 中的 id 一一对应,表示当前使用的是哪一种团购套餐(例如某款“一小时套餐”“两小时套餐”)。 | 来源: promotion_coupon_id | 角色: 外键
|
||||
order_coupon_channel integer 券渠道类型枚举。观测值:1(大量记录)、2(少量记录)。用于区分券的来源渠道,例如不同团购平台或内部券。具体数值与渠道名称的映射由业务配置决定。 | 来源: order_coupon_channel | 角色: 外键
|
||||
ledger_unit_price numeric(18,2) 本次券在台费侧对应的标准单价,单位元/小时。典型值如 29.9、39.9、59.9、69.9 等。与门店台费计费规则中的单价相对应,用于计算券对应的金额。 | 来源: ledger_unit_price
|
||||
ledger_count integer 本次券实际核销的计费秒数。大部分记录等于 promotion_seconds,少数略小于标准时长,表示这张券只覆盖了本次台费的一部分时长。 | 来源: ledger_count
|
||||
ledger_amount numeric(18,2) 本次团购券实际冲抵台费的金额。大部分记录中该值与 coupon_money 相等,少数存在小数差异,来源于按单价与秒数换算的结果。 | 来源: ledger_amount
|
||||
coupon_money numeric(18,2) 本次核销时,团购券在门店侧对应的金额额度(可抵扣金额)。同一种 promotion_coupon_id 下,该值固定,例如某套餐固定为 48.00 元、某套餐固定为 96.00 元等。 | 来源: coupon_money
|
||||
promotion_seconds integer 团购套餐定义的标准时长权益,单位秒。观测枚举值为 3600、7200、14400,分别对应 1 小时、2 小时、4 小时。与团购套餐定义表中的 duration 字段一致。 | 来源: promotion_seconds
|
||||
coupon_code character varying(64) 团购券券码字符串。每条记录一个唯一券码,例如“0107892475999”。用于与平台验券记录、券购买记录等做一一对应,追踪券的全生命周期。 | 来源: coupon_code | 角色: 外键
|
||||
is_single_order integer 是否作为单独订单行。观测值:1 为主,表示以独立条目方式结算;0 为个别记录,表示嵌入某种组合结算结构。具体业务含义依赖上层订单结构设计。 | 来源: is_single_order
|
||||
is_delete integer 逻辑删除标记。0 表示正常记录,1 表示逻辑删除但数据仍保留用于追溯。当前样本全部为 0,用于过滤有效记录。 | 来源: is_delete
|
||||
ledger_name character varying(128) 团购项目记账名称,如“全天A区中八一小时”“B区桌球一小时”“中八、斯诺克包厢两小时”等。通常与团购套餐名称相近,用于报表展示和套餐维度分析。 | 来源: ledger_name
|
||||
create_time timestamp with time zone 本条团购券核销流水的创建时间,通常即核销时间,格式为“YYYY-MM-DD HH:MM:SS”。用于时间维度分析和数据分区。 | 来源: create_time
|
||||
-- dwd_groupbuy_redemption_ex
|
||||
redemption_id bigint 团购券核销流水主键。一条记录代表一次团购券使用在某次台费上的一条核销明细。 | 来源: id | 角色: 主键
|
||||
site_name character varying(64) 门店名称。当前样本全部为同一门店,仅作冗余展示。 | 来源: siteName
|
||||
table_name character varying(64) 球台名称或台号。如 A7、A11、B1 等。用于业务报表展示与人工识别。 | 来源: tableName
|
||||
table_area_name character varying(64) 台区名称。观测枚举值包括 A区、B区、斯诺克区、麻将房。实际取值随门店台区配置变化。 | 来源: tableAreaName
|
||||
order_pay_id bigint 支付流水 ID。部分记录为 0,表示未在当前导出范围内关联到具体支付记录。真实含义为“指向支付记录表中的支付流水”。 | 来源: order_pay_id | 角色: 外键
|
||||
goods_option_price numeric(18,2) 商品规格价格,用于商品类促销分摊时使用。当前在团购券核销场景中全部为 0,仅作为结构预留。 | 来源: goodsOptionPrice
|
||||
goods_promotion_money numeric(18,2) 本次券使用中分摊到“商品”部分的促销金额。当前所有记录为 0,说明本门店的团购券未用于商品抵扣。 | 来源: goods_promotion_money
|
||||
table_service_promotion_money numeric(18,2) 本次券使用中分摊到“台费服务费”部分的促销金额。当前样本全部为 0,结构上用于支持更细粒度的费用拆分。 | 来源: table_service_promotion_money
|
||||
assistant_promotion_money numeric(18,2) 本次券使用中分摊到“助教服务”的促销金额。当前全部为 0,说明团购券尚未用于助教服务的抵扣。 | 来源: assistant_promotion_money
|
||||
assistant_service_promotion_money numeric(18,2) 进一步细分助教服务对应的促销金额。当前为 0,仅预留结构以支持复杂场景。 | 来源: assistant_service_promotion_money
|
||||
reward_promotion_money numeric(18,2) 本次促销中属于“奖励金、积分”等来源的促销金额分摊。当前为 0,预留用于积分或奖励金同时参与活动时的金额拆分。 | 来源: reward_promotion_money
|
||||
recharge_promotion_money numeric(18,2) 来自“充值赠送”等储值优惠的促销金额分摊。当前为 0,预留用于将来区分“券优惠”和“充值赠送优惠”的场景。 | 来源: recharge_promotion_money
|
||||
offer_type integer 优惠类型枚举。当前样本值全部为 1,表示本门店使用的团购券均为同一类型(例如“套餐券”)。其他取值可能对应满减、折扣、代金券等优惠类型,在本数据中未出现。 | 来源: offer_type
|
||||
ledger_status integer 流水状态。观测值全部为 1。常规含义为:1 表示正常有效记录;其他值预留用于表示作废、撤销、未生效等状态。当前导出仅包含正常状态记录。 | 来源: ledger_status
|
||||
operator_id bigint 执行本次券核销操作的操作员 ID。可与员工维度表对接,用于分析不同操作员的核销行为与绩效。 | 来源: operator_id | 角色: 外键
|
||||
operator_name character varying(64) 操作员名称及角色说明,例如“收银员:郑丽珊”。与 operator_id 冗余,对报表展示友好,但不参与模型关联。 | 来源: operator_name
|
||||
salesman_user_id bigint 营业员用户 ID。当前全部为 0,表示本门店在团购券场景未单独记录营业员信息。 | 来源: salesman_user_id | 角色: 外键
|
||||
salesman_name character varying(64) 营业员姓名。当前为空字符串。结构上用于记录拉单或促销的业务员信息。 | 来源: salesman_name
|
||||
salesman_role_id bigint 营业员角色 ID。当前为 0,预留用于标识营业员在组织中的角色类型。 | 来源: salesman_role_id
|
||||
salesman_org_id bigint 营业员所属组织 ID。来源字段为 sales_man_org_id,DWD 层统一命名为 salesman_org_id。当前为 0,用于将来对接组织架构维度。 | 来源: sales_man_org_id
|
||||
ledger_group_name character varying(128) 团购项目的记账分组名称。当前全部为空,预留给将来按团购项目大类分组(例如“团购台费”“团购包厢”)使用。 | 来源: ledger_group_name
|
||||
-- dwd_member_balance_change
|
||||
balance_change_id bigint 余额变动记录主键 ID,来源于源系统的余额变更流水 ID,唯一标识一条余额变动事件。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID,在整体系统中唯一标识一家商户。当前样本中为同一值。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 本次余额变动发生的门店 ID。通常对应具体门店;样本中:非 0 为“朗朗桌球”,0 代表平台级/虚拟门店场景(如活动抵用券结算)。 | 来源: site_id | 角色: 外键
|
||||
register_site_id bigint 办卡门店 ID(卡注册门店)。与 site_id 区分:register_site_id=当初办卡门店,site_id=本次余额变动实际发生门店。当前样本中全部相同。 | 来源: register_site_id | 角色: 外键
|
||||
tenant_member_id bigint 租户维度会员 ID(同一租户内的会员主键),用于关联会员档案。 | 来源: tenant_member_id | 角色: 外键
|
||||
system_member_id bigint 系统全局会员 ID(跨租户统一的会员标识)。当前只有一个门店,但结构上允许同一人跨租户共享该 ID。 | 来源: system_member_id | 角色: 外键
|
||||
tenant_member_card_id bigint 会员卡账户 ID(租户内唯一的一张具体卡,例如某人的储值卡/酒水卡/台费卡/活动抵用券等)。本次余额变动作用于这张卡。 | 来源: tenant_member_card_id | 角色: 外键
|
||||
card_type_id bigint 卡种类型 ID。与 card_type_name 一一对应,用于区分不同卡种(储值卡/活动抵用券/酒水卡/台费卡)。 | 来源: card_type_id | 角色: 外键
|
||||
card_type_name character varying(32) 卡种名称(中文):• 储值卡:通用储值卡;• 活动抵用券:活动送券型卡;• 酒水卡:指定用于酒水类消费;• 台费卡:指定用于台费消费。 | 来源: memberCardTypeName
|
||||
member_name character varying(64) 会员姓名/称呼(如“曾丹烨”“葛先生”“胡先生”),主要用于运营、客服和人工识别。 | 来源: memberName
|
||||
member_mobile character varying(20) 会员手机号(完整号码字符串),是会员识别、营销触达的重要字段。 | 来源: memberMobile
|
||||
balance_before numeric(18,2) 本次变动前的卡内余额,单位:元。可为 0、数百、数千等。 | 来源: before
|
||||
change_amount numeric(18,2) 本次余额变动金额,单位:元:• 正数:余额增加(充值、赠送、调整加款等);• 负数:余额减少(消费扣款、退款冲减、活动抵扣等)。所有记录严格满足:balance_after = balance_before + change_amount(浮点精度内)。 | 来源: account_data
|
||||
balance_after numeric(18,2) 本次变动后的卡内余额,单位:元。由 before + account_data 计算而得,在源数据中已给出。 | 来源: after
|
||||
from_type integer 余额变动来源类型枚举(控制业务含义与方向):• 1:日常消费扣款 —— change_amount 为负数,payment_method=0,表示用卡支付消费被扣余额;• 3:充值增加 —— change_amount 为正数,payment_method=4,表示顾客通过外部支付为卡充值(扫码、银行卡等);• 4:调整/赠送增加 —— change_amount 为正数,payment_method=3,通常为后台赠送或手工加款;• 7:充值退款 —— change_amount 为负数,remark='充值退款',表示对历史充值做退款,以减少卡内余额方式体现;• 9:活动抵用券相关余额冲减 —— change_amount 为负数,卡种为“活动抵用券”,site_id=0,表示活动券额度被扣回或结算;• 2:其他增加 —— 当前仅 1 条正数样本(+1865.80),具体业务类型不明,但可确定为余额增加类。总体上:1/7/9 为减余额类,2/3/4 为加余额类。 | 来源: from_type
|
||||
payment_method integer 支付/变动渠道枚举(与源系统支付方式枚举一致):• 0:内部结算/无外部支付 —— 日常消费扣款、内部扣减、退款冲减等场景,新资金流不在本记录中产生;• 3:赠送/后台调整渠道 —— 与 from_type=4 搭配出现,表示此余额增加不是顾客付钱,而是后台发放或内部调账;• 4:充值外部支付渠道 —— 与 from_type=3 搭配出现,代表顾客通过某外部渠道完成充值(具体是微信/支付宝/银行卡等需要结合支付枚举表进一步映射)。 | 来源: payment_method
|
||||
change_time timestamp with time zone 余额变动时间(记录创建时间),格式 YYYY-MM-DD HH:MM:SS。通常紧邻实际交易发生时间,用于按时间线分析资金变动。 | 来源: create_time
|
||||
is_delete integer 逻辑删除标记:• 0:正常记录(当前样本全部为 0);• 1:逻辑删除(标记为删除但数据库保留,用于追溯)。分析时通常需要过滤掉 is_delete=1 的记录。 | 来源: is_delete
|
||||
remark character varying(255) 余额变动备注信息。当前样本中主要为:• 空字符串:无额外说明;• 充值退款:明确标记该条记录为“充值退款”业务,与 from_type=7 完全对应。后续可能出现其他业务备注。 | 来源: remark
|
||||
-- dwd_member_balance_change_ex
|
||||
balance_change_id bigint 余额变动记录主键 ID,来源于源系统的余额变更流水 ID,唯一标识一条余额变动事件。 | 来源: id | 角色: 主键
|
||||
pay_site_name character varying(64) 余额变动发生门店名称,对应 site_id 的中文名。示例:朗朗桌球;当 site_id = 0 时通常为空字符串。纯展示冗余。 | 来源: paySiteName
|
||||
register_site_name character varying(64) 办卡门店名称,对应 register_site_id 的中文名。当前样本全部为 朗朗桌球,属于冗余展示。 | 来源: registerSiteName
|
||||
refund_amount numeric(18,2) 退款金额字段。在当前样本数据中全部为 0.00,推测用于区分“退回卡内余额”和“原路退回”等更细的退款模式,但目前未启用。 | 来源: refund_amount
|
||||
operator_id bigint 操作员 ID,执行本次余额变动操作的员工账号主键。可关联员工/账号维度。 | 来源: operator_id | 角色: 外键
|
||||
operator_name character varying(64) 操作员名称及职位说明,例如:收银员:郑丽珊、店长:谢晓洪 等,是对 operator_id 的可读冗余。 | 来源: operator_name
|
||||
-- dwd_payment
|
||||
payment_id bigint 支付流水主键ID。与源系统 id 一致。每条支付流水唯一标识一条支付行为(包括金额为 0 的记录)。 | 来源: payment_transactions.id | 角色: 主键
|
||||
site_id bigint 门店ID。当前样本中全部为同一门店 2790685415443269。在数仓中外键关联 dim_site.site_id。 | 来源: payment_transactions.site_id | 角色: 外键
|
||||
relate_type integer 业务关联类型枚举,用来区分这条支付流水对应哪一类业务单据:• 2:结账单支付,对应结账记录 settlement_records 中的结账单;• 5:会员卡充值/账户变动类支付,对应会员余额/充值业务单号,在会员余额变更或充值结算中复用;• 1:其他业务类型,目前样本中仅有 1 条记录,具体业务含义待业务侧补充。 | 来源: payment_transactions.relate_type | 角色: −
|
||||
relate_id bigint 关联业务记录ID,配合 relate_type 使用,是一个“多态外键”:• 当 relate_type = 2 时:relate_id = settlement_records.settleList.id(结账记录主键,对应 dwd_settlement_head_di.order_settle_id);• 当 relate_type = 5 时:relate_id = 会员卡余额变动/充值业务单号,在会员余额变更流水中同名字段使用;• 当 relate_type = 1 时:关联具体业务尚未确认,仅可视为预留类型。 | 来源: payment_transactions.relate_id | 角色: 外键(多业务类型)
|
||||
pay_amount numeric(18,2) 本次支付金额,单位元。为收入类字段,当前样本全部为非负数:• 正数:实际通过该支付方式收取的金额;• 0:仍生成支付流水,但实收金额为 0(例如整单由会员优惠、团购券、余额等抵扣),当前样本中有 140 条记录金额为 0。 | 来源: payment_transactions.pay_amount | 角色: −
|
||||
pay_status integer 支付状态枚举。当前样本中仅出现:• 2:支付成功。其它可能的状态(未支付、支付中、失败、已退款等)在本次导出中未出现,需以后按系统支付状态配置补充。由于本 JSON 仅导出成功记录,可以视作“成功支付流水视图”。 | 来源: payment_transactions.pay_status | 角色: −
|
||||
payment_method integer 支付方式枚举。当前样本中出现的取值:• 2:共 140 条记录;• 4:共 60 条记录。具体取值与“支付方式配置表”对应,例如可能代表现金、扫码支付、银行卡等。由于配置表未导出,在数仓中应作为枚举码字段,通过后续 dim_payment_method 进行解码。不要在数仓层擅自写死“2=微信、4=支付宝”等含义。 | 来源: payment_transactions.payment_method | 角色: 外键(预期关联支付方式维度)
|
||||
online_pay_channel integer 线上支付通道枚举。用于进一步细分线上渠道,例如:• 0:无线上通道/线下,或未区分具体线上通道(当前样本全部为 0);• 1:微信(推测,未在样本中出现);• 2:支付宝(推测,未在样本中出现)。目前门店在当前时间段内尚未使用该字段进行实际区分,业务含义需结合正式配置确认。 | 来源: payment_transactions.online_pay_channel | 角色: −
|
||||
create_time timestamp with time zone 支付流水创建时间,格式 YYYY-MM-DD HH:MM:SS。通常是发起支付请求的时间。当前样本中 create_time 与 pay_time 多数相同,但模型上允许两者不同(例如异步支付)。 | 来源: payment_transactions.create_time | 角色: −
|
||||
pay_time timestamp with time zone 支付完成时间(支付成功时间戳),格式 YYYY-MM-DD HH:MM:SS。用于统计资金实际入账时间,以及与结账时间进行对齐分析。 | 来源: payment_transactions.pay_time | 角色: −
|
||||
pay_date date 支付日期分区字段,从 pay_time 截取 YYYY-MM-DD 得到。例如 pay_time = '2025-11-09 23:35:57' 时,pay_date = '2025-11-09'。用于 DWD 表按天分区和日粒度汇总。 | 来源: 由 payment_transactions.pay_time 派生 | 角色: −
|
||||
-- dwd_platform_coupon_redemption
|
||||
platform_coupon_redemption_id bigint 平台券核销记录在本系统内的主键 ID。长整型分布式 ID,用于唯一标识本次核销流水。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户 ID,品牌级别标识。例如整套系统中的“朗朗桌球”品牌。与其他表的 tenant_id 一致,用于划分租户数据域。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID。与门店维度 dim_site.site_id 对应,用于区分不同门店。siteProfile.id 与此字段相同,本表不再冗余门店快照。 | 来源: site_id | 角色: 外键
|
||||
coupon_code character varying(64) 第三方团购券券码,顾客出示的核销码。当前样本中全表唯一,可视为业务自然主键,用于验券、对账、查询。 | 来源: coupon_code
|
||||
coupon_channel integer 券来源渠道枚举,表示第三方平台渠道编号。观测值:1,2。具体含义需结合系统配置,一般可理解为:1 表示平台渠道 1(主平台),2 表示平台渠道 2(其他入口或子平台)。 | 来源: coupon_channel
|
||||
coupon_name character varying(200) 第三方团购券产品名称,例如“【全天可用】中八桌球一小时(A区)”“1小时中八台球【11月特惠】(A区)”等。用于报表展示和区分不同团购产品。 | 来源: coupon_name
|
||||
sale_price numeric(10,2) 顾客在第三方平台实际支付的团购售价,例如 11.11、29.90、39.90 等。始终小于 coupon_money,体现“折后价”。 | 来源: sale_price
|
||||
coupon_money numeric(10,2) 券面值或套餐价值,即系统认为该券可抵扣的金额,例如 48.00、58.00、68.00、96.00、116.00、288.00。与 coupon_name 有固定对应关系。 | 来源: coupon_money
|
||||
coupon_free_time integer 券附带的赠送时长,单位:秒。当前样本全部为 0,表示无独立赠送时长。若未来有赠送时间型券,则该字段存储赠送的秒数。 | 来源: coupon_free_time
|
||||
channel_deal_id bigint 渠道侧团购商品 ID(第三方平台 dealId)。值域有限(约 9 个值),与 coupon_name 一一对应。用于对接第三方接口和按平台商品维度统计。 | 来源: channel_deal_id | 角色: - 或 外键(预留)
|
||||
deal_id bigint 平台/系统侧团购商品 ID。多数记录为非 0 整数,也有部分为 0。与 coupon_name 存在稳定对应关系,0 表示内部未配置或未同步。未来可作为内部团购商品维度外键。 | 来源: deal_id | 角色: 外键(预留)
|
||||
group_package_id bigint 内部“团购套餐”定义表主键 ID。当前样本中全部为 0,表示平台券尚未映射到自有团购套餐;从设计上是预留的外键字段。 | 来源: group_package_id | 角色: 外键
|
||||
site_order_id bigint 门店内部订单 ID。本次平台券核销所挂靠的店内订单号。用于与结账记录、台费流水、商品销售等事实表通过订单维度关联。 | 来源: site_order_id | 角色: 外键
|
||||
table_id bigint 使用团购券的球台 ID。与 dim_site_table.table_id 对应,用于统计每张球台的第三方平台引流情况。 | 来源: table_id | 角色: 外键
|
||||
certificate_id character varying(64) 第三方平台侧券实例 ID(凭证 ID),通常为 16–19 位数字字符串。用于与外部平台对账与查询核销结果。存在重复值,不能单独作为唯一键。 | 来源: certificate_id
|
||||
verify_id character varying(64) 第三方平台核销记录 ID。大部分记录为空,少量有值。存在时可用于精确反查平台侧核销记录。 | 来源: verify_id
|
||||
use_status integer 券使用状态枚举。观测值:1、2。1 表示已使用/已核销(正常消耗);2 表示已退款/已撤销或使用后反冲的状态。是判断券生命周期状态的核心字段。 | 来源: use_status
|
||||
is_delete integer 逻辑删除标志。0 表示未删除;1 表示已逻辑删除。与 use_status 独立:即便业务状态异常(如 use_status=2),也可能 is_delete 仍为 0 以保留记录。 | 来源: is_delete
|
||||
create_time timestamp with time zone 系统记录创建时间,即核销记录写入本系统的时间。格式为 YYYY-MM-DD HH:MM:SS。通常与 consume_time 相差约 1 秒。 | 来源: create_time
|
||||
consume_time timestamp with time zone 券被核销/使用的业务时间,代表实际团购券使用发生的时间点。后续按核销日期统计核销量时以该字段为准。 | 来源: consume_time
|
||||
-- dwd_platform_coupon_redemption_ex
|
||||
platform_coupon_redemption_id bigint 平台券核销记录在本系统内的主键 ID。长整型分布式 ID,用于唯一标识本次核销流水。 | 来源: id | 角色: 主键
|
||||
coupon_cover character varying(255) 券封面图片地址 URL,用于前端展示团购券图片。对经营分析和结算逻辑无影响。 | 来源: coupon_cover
|
||||
coupon_remark character varying(255) 券描述或备注信息,用于展示券规则、说明文字。未参与计算和关联逻辑。 | 来源: coupon_remark
|
||||
groupon_type integer 团购券类型枚举。当前样本全部为 1。推断含义:1 表示标准团购券,其他值预留为次卡、套餐券、权益券等类型。 | 来源: groupon_type
|
||||
operator_id bigint 执行验券操作的员工/收银员账号 ID。当前样本中多数为同一值。可与员工/账号维度表关联。 | 来源: operator_id | 角色: 外键
|
||||
operator_name character varying(50) 操作员姓名或显示名,例如“收银员:郑丽珊”。是 operator_id 的冗余展示字段,用于报表展示。 | 来源: operator_name
|
||||
-- dwd_recharge_order
|
||||
recharge_order_id bigint 充值结算记录主键;唯一标识一条充值/撤销记录。 | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID;与其他 JSON 中 tenantId 含义一致。 | 来源: tenantId | 角色: 外键(dim_tenant)
|
||||
site_id bigint 门店 ID;与 siteProfile.id 一致,用于关联门店维度。 | 来源: siteId | 角色: 外键(dim_site)
|
||||
member_id bigint 会员 ID;对应会员档案中的 tenantMemberInfos.id。标识给哪个会员充值。 | 来源: memberId | 角色: 外键(dim_member)
|
||||
member_name_snapshot text 会员姓名/昵称快照,可能是名称或手机号字符串。 | 来源: memberName
|
||||
member_phone_snapshot text 会员手机号快照。 | 来源: memberPhone
|
||||
tenant_member_card_id bigint 会员卡实例 ID(某张具体卡);可关联 dim_member_card_account。 | 来源: tenantMemberCardId | 角色: 外键(dim_member_card_account)
|
||||
member_card_type_name text 会员卡类型名称;当前样例主要有:“储值卡”、“月卡”。 | 来源: memberCardTypeName
|
||||
settle_relate_id bigint 结算关联 ID;用于与支付记录等跨表关联(类似业务单 ID)。 | 来源: settleRelateId | 角色: 外键(与支付/结算域软关联)
|
||||
settle_type integer 结算类型枚举:5=“充值订单”(正常充值);7=“充值撤销”(冲销记录)。 | 来源: settleType
|
||||
settle_name text 结算类型名称:"充值订单"、"充值撤销";前端展示用。 | 来源: settleName
|
||||
is_first integer 是否首充标记。取值:1 或 2。理论含义类似“是否首充”,但 1/2 的精确定义需系统字典确认。 | 来源: isFirst
|
||||
pay_amount numeric(18,2) 本条记录的充值金额(可为正/负):正数=实际充值金额;负数=撤销流水金额(settleType=7)。 | 来源: payAmount
|
||||
refund_amount numeric(18,2) 对该充值订单的退款金额(通常为正数);原始充值单上为已退款金额,对应会有一条负数的“充值撤销”记录。 | 来源: refundAmount
|
||||
point_amount numeric(18,2) 计入会员账户的储值/积分金额;多数情况下等于 pay_amount 的绝对值;撤销记录一般为 0。 | 来源: pointAmount
|
||||
cash_amount numeric(18,2) 现金收款金额;样本中少数为 3000/5000,其余为 0。 | 来源: cashAmount
|
||||
payment_method integer 支付方式编码。样本取值:1、2、4;具体对应渠道(现金/微信/支付宝/银行卡等)需系统“支付方式字典”确认。 | 来源: paymentMethod
|
||||
create_time timestamp with time zone 充值记录创建时间(收银完成时间);用于时间分区、统计。 | 来源: createTime
|
||||
pay_time timestamp with time zone 支付完成时间;通常与 create_time 接近或相同。 | 来源: payTime
|
||||
-- dwd_recharge_order_ex
|
||||
recharge_order_id bigint 充值结算记录主键;唯一标识一条充值/撤销记录。 | 来源: id | 角色: 主键
|
||||
site_name_snapshot text 门店名称快照,如“朗朗桌球”;仅用于展示,门店改名后本字段不变。 | 来源: siteName
|
||||
settle_status integer 结算状态;当前数据全部为 2,推测 2=已完成。其它状态未出现在样本中。 | 来源: settleStatus
|
||||
is_bind_member boolean 是否绑定为会员/其他绑定标记;当前所有记录为 False,而又都有 memberId,实际业务含义不清。 | 来源: isBindMember
|
||||
is_activity boolean 是否关联营销活动;当前全部为 False,表示样本周期内充值未绑定活动。 | 来源: isActivity
|
||||
is_use_coupon boolean 本次充值是否使用优惠券;当前样本全部为 False。结构上预留“充值用券”的能力。 | 来源: isUseCoupon
|
||||
is_use_discount boolean 是否使用折扣(如充值折扣);样本中全为 False。 | 来源: isUseDiscount
|
||||
can_be_revoked boolean 当前记录是否仍可撤销;样本中全为 False(导出时均不可撤销)。 | 来源: canBeRevoked
|
||||
online_amount numeric(18,2) 线上支付金额(如微信、支付宝等);当前样本为 0,但字段为支付渠道拆分预留。 | 来源: onlineAmount
|
||||
balance_amount numeric(18,2) 从账户余额中支付的金额;充值场景通常为 0(用余额充值没有实际意义)。 | 来源: balanceAmount
|
||||
card_amount numeric(18,2) 从其他储值卡或某种卡余额支付的金额;当前样本全为 0。 | 来源: cardAmount
|
||||
coupon_amount numeric(18,2) 使用券直接支付的金额(如储值券);当前样本为 0。 | 来源: couponAmount
|
||||
recharge_card_amount numeric(18,2) 充值到卡上的金额(与 point_amount 区分不同资金属性);样本为 0,结构预留。 | 来源: rechargeCardAmount
|
||||
gift_card_amount numeric(18,2) 赠送卡金额(如买 1000 送 100 的赠送部分);当前样本为 0。 | 来源: giftCardAmount
|
||||
prepay_money numeric(18,2) 预付款金额(订金);当前样本为 0,充值未启用此场景。 | 来源: prepayMoney
|
||||
consume_money numeric(18,2) 消费总金额;在充值文件中全部为 0,实际用于消费场景(台费/商品)的结算模型复用字段。 | 来源: consumeMoney
|
||||
goods_money numeric(18,2) 商品消费金额(充值记录中为 0)。 | 来源: goodsMoney
|
||||
real_goods_money numeric(18,2) 实际应计商品金额(扣除折扣后);充值记录中为 0。 | 来源: realGoodsMoney
|
||||
table_charge_money numeric(18,2) 台费金额;充值记录中为 0,来自通用结算模型。 | 来源: tableChargeMoney
|
||||
service_money numeric(18,2) 服务项目金额(如助教、其他服务);充值中为 0。 | 来源: serviceMoney
|
||||
activity_discount numeric(18,2) 营销活动折扣金额;当前样本为 0。 | 来源: activityDiscount
|
||||
all_coupon_discount numeric(18,2) 各类优惠券、团购券综合折扣金额;样本为 0。 | 来源: allCouponDiscount
|
||||
goods_promotion_money numeric(18,2) 商品促销优惠金额;样本为 0。 | 来源: goodsPromotionMoney
|
||||
assistant_promotion_money numeric(18,2) 助教相关促销优惠金额;样本为 0。 | 来源: assistantPromotionMoney
|
||||
assistant_pd_money numeric(18,2) 助教配单金额/相关费用;样本为 0。 | 来源: assistantPdMoney
|
||||
assistant_cx_money numeric(18,2) 助教冲销/促销相关金额;样本为 0。 | 来源: assistantCxMoney
|
||||
assistant_manual_discount numeric(18,2) 助教手工减免金额;样本为 0。 | 来源: assistantManualDiscount
|
||||
coupon_sale_amount numeric(18,2) 券/套餐销售金额(售卖券时使用);充值场景中为 0。 | 来源: couponSaleAmount
|
||||
member_discount_amount numeric(18,2) 因会员折扣产生的优惠金额;在充值样本中为 0。 | 来源: memberDiscountAmount
|
||||
point_discount_price numeric(18,2) 积分抵扣产生的价差(价格部分);样本为 0。 | 来源: pointDiscountPrice
|
||||
point_discount_cost numeric(18,2) 积分抵扣对应的成本金额;样本为 0。 | 来源: pointDiscountCost
|
||||
adjust_amount numeric(18,2) 手工调整金额(非抹零);样本为 0。 | 来源: adjustAmount
|
||||
rounding_amount numeric(18,2) 抹零金额(四舍五入产生的差额);样本为 0。 | 来源: roundingAmount
|
||||
operator_id bigint 操作该笔充值的收银员/员工 ID。 | 来源: operatorId | 角色: 外键(将来可关联 dim_staff)
|
||||
operator_name_snapshot text 操作员姓名快照,便于直接阅读;与 operator_id 对应。 | 来源: operatorName
|
||||
salesman_user_id bigint 营业员用户 ID;当前样本全部为 0。 | 来源: salesManUserId | 角色: 外键(潜在 dim_staff)
|
||||
salesman_name text 营业员/销售员姓名;样本为空字符串。 | 来源: salesManName
|
||||
order_remark text 充值订单备注,如手工说明;当前样本为空。 | 来源: orderRemark
|
||||
table_id integer 台桌 ID;充值场景下全部是 0,表示该订单不挂具体球台。 | 来源: tableId
|
||||
serial_number integer 流水/小票序号;当前样本全部为 0,本门店未启用。 | 来源: serialNumber
|
||||
revoke_order_id bigint 撤销相关订单 ID(被撤销原单或撤销单指针);样本中存在值但逻辑未完全启用。 | 来源: revokeOrderId
|
||||
revoke_order_name text 撤销单名称/说明;样本全为空。 | 来源: revokeOrderName
|
||||
revoke_time timestamp with time zone 撤销时间;当前样本为空,撤销信息主要通过负数流水与 refund_amount 表达。 | 来源: revokeTime
|
||||
-- dwd_refund
|
||||
refund_id bigint 退款流水主键。每条退款记录唯一 ID(雪花ID风格长整型) | 来源: id | 角色: 主键
|
||||
tenant_id bigint 租户/品牌 ID,标识所属商户。与其他表中的 tenant_id 一致。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID。与门店维度 dim_site.site_id 对应,用于分门店分析。 | 来源: site_id | 角色: 外键
|
||||
relate_type integer 业务类型枚举,指示本退款对应哪类业务主单:当前样本值 {2, 5}:2=消费/结账类业务;5=充值/储值类业务(具体定义以业务字典为准)。与 relate_id 组合使用。 | 来源: relate_type
|
||||
relate_id bigint 关联的业务主键 ID,含义依赖 relate_type:relate_type=2 时通常指结账主单 ID;relate_type=5 时通常指充值/储值业务单 ID。同一个 relate_id 可能有多笔退款(分批退款)。 | 来源: relate_id | 角色: 外键
|
||||
pay_amount numeric(18,2) 本次退款的资金金额,统一为负数。绝对值即退款金额,例如 -5000.00 表示退款 5000 元。 | 来源: pay_amount
|
||||
channel_fee numeric(18,2) 第三方支付渠道对本次退款收取的手续费。当前样本为 0.00,用于通道成本核算。 | 来源: channel_fee
|
||||
pay_time timestamp with time zone 退款在支付渠道/系统中发生的时间(退款完成时间),用于对账及按时间统计。 | 来源: pay_time
|
||||
create_time timestamp with time zone 本条退款流水在系统内创建时间,用于区分记录生成时间与渠道时间。 | 来源: create_time
|
||||
payment_method integer 支付方式枚举,指本次退款对应的原支付方式(如通道)。样本出现值 4,支付记录中有 {2,4},具体含义需参照支付方式配置表。 | 来源: payment_method
|
||||
member_id bigint 关联的会员 ID,对应会员档案表主键。样本为 0,表示未绑定会员。 | 来源: member_id | 角色: 外键
|
||||
member_card_id bigint 关联的会员卡账户 ID,对应会员卡账户/储值卡维度主键。样本为 0,当前没有“退到会员卡”的记录。 | 来源: member_card_id | 角色: 外键
|
||||
-- dwd_refund_ex
|
||||
refund_id bigint 退款流水主键。每条退款记录唯一 ID(雪花ID风格长整型)。 | 来源: id | 角色: 主键
|
||||
tenant_name character varying(64) 租户名称,例如“朗朗桌球”。与租户维度中的名称冗余。 | 来源: tenantName
|
||||
pay_sn bigint 支付流水内部序号。退款记录中样本全部为 0,未看到实际使用场景,含义未说明。 | 来源: pay_sn
|
||||
refund_amount numeric(18,2) 本次退款金额(正数)设计字段,样本为 0.00,实际使用 pay_amount。 | 来源: refund_amount
|
||||
round_amount numeric(18,2) 退款过程中的舍入/抹零金额。 | 来源: round_amount
|
||||
balance_frozen_amount numeric(18,2) 与会员余额相关的冻结金额,样本为 0。 | 来源: balance_frozen_amount
|
||||
card_frozen_amount numeric(18,2) 与某张会员卡余额相关的冻结金额,样本为 0。 | 来源: card_frozen_amount
|
||||
pay_status integer 退款状态枚举。样本中全部为 2(成功/已完成)。 | 来源: pay_status
|
||||
action_type integer 资金动作类型枚举。样本全部为 2(退款)。 | 来源: action_type
|
||||
is_revoke integer 是否为撤销型退款:0=正常退款;1=撤销原支付。 | 来源: is_revoke
|
||||
is_delete integer 逻辑删除标志:0=未删除;1=已逻辑删除。 | 来源: is_delete
|
||||
check_status integer 审核状态。样本全部为 1(已审核/通过)。 | 来源: check_status
|
||||
online_pay_channel integer 线上支付渠道枚举。样本中全部为 0(线下或默认)。 | 来源: online_pay_channel
|
||||
online_pay_type integer 在线退款类型。样本全部为 0(原路退回)。 | 来源: online_pay_type
|
||||
pay_terminal integer 退款终端类型。样本全部为 1(前台收银端)。 | 来源: pay_terminal
|
||||
pay_config_id integer 支付配置 ID,例如某个具体支付通道(微信商户号、银联通道等)的配置主键。样本全部为 0。 | 来源: pay_config_id | 角色: 外键
|
||||
cashier_point_id integer 收银点编号,例如前台1、前台2、自助机等。样本全部为 0。 | 来源: cashier_point_id
|
||||
operator_id bigint 执行退款操作的操作员 ID。样本全部为 0。 | 来源: operator_id | 角色: 外键
|
||||
channel_payer_id character varying(128) 支付渠道侧的付款人标识,如 openid、银行卡号掩码等。样本全部为空字符串。 | 来源: channel_payer_id
|
||||
channel_pay_no character varying(128) 第三方支付平台交易号(如微信支付单号、支付宝交易号等)。当前样本全部为空。 | 来源: channel_pay_no
|
||||
-- dwd_refund_ex_pkey
|
||||
refund_id bigint
|
||||
-- dwd_refund_pkey
|
||||
refund_id bigint
|
||||
-- dwd_settlement_head
|
||||
order_settle_id bigint 结账记录主键 ID(订单结算 ID),全系统统一的结账单号,用于关联台费流水、助教流水、小票等明细表。 | 来源: settleList.id | 角色: 主键
|
||||
tenant_id bigint 租户/商户 ID(品牌维度),与各业务 JSON 中的 tenantId 一致。 | 来源: settleList.tenantId | 角色: 外键
|
||||
site_id bigint 门店 ID,用于关联门店维表 dim_site。 | 来源: settleList.siteId | 角色: 外键
|
||||
site_name character varying(100) 门店名称快照,冗余展示字段,推荐通过 site_id 关联维表获取标准名称。 | 来源: settleList.siteName
|
||||
table_id bigint 结账关联的桌台 ID,对应台桌维表 dim_site_table 的主键。 | 来源: settleList.tableId | 角色: 外键
|
||||
settle_name character varying(100) 结账对象名称,一般为 “区域 + 桌号”,如 “A区 A17”,便于报表展示。 | 来源: settleList.settleName
|
||||
order_trade_no bigint 交易号 / 订单流水号,与台费、助教等明细中的 order_trade_no 一致,用于按“交易维度”串联各业务明细。 | 来源: settleList.settleRelateId
|
||||
create_time timestamp with time zone 结账创建时间(收银端点击“确认结账”的时间),格式:YYYY-MM-DD HH:MM:SS。 | 来源: settleList.createTime
|
||||
pay_time timestamp with time zone 实际支付完成时间,通常晚于 create_time,用于资金结算及对账分析。 | 来源: settleList.payTime
|
||||
settle_type integer 结账类型枚举。样本中主要有:1=正常结账;3=特殊类型结账(如挂账、补单、调整单等,具体需业务确认)。 | 来源: settleList.settleType
|
||||
revoke_order_id bigint 若当前记录属于撤销链路,记录对应的撤销单或原单的结账 ID,形成自关联关系。样本中为 0。 | 来源: settleList.revokeOrderId | 角色: 外键
|
||||
member_id bigint 会员主键 ID,一般对应租户维度的会员 ID,用于关联 dim_member。 | 来源: settleList.memberId | 角色: 外键
|
||||
member_name character varying(100) 会员姓名快照,冗余展示字段;当前样本多为空,推荐通过关联会员维表获取标准姓名。 | 来源: settleList.memberName
|
||||
member_phone character varying(50) 会员手机号快照,冗余展示字段,通常通过会员维表获取更可靠。 | 来源: settleList.memberPhone
|
||||
member_card_account_id bigint 会员卡账户 ID,对应 dim_member_card_account 主键;当前样本多为 0,但结构上是“结账 → 具体卡账户”的外键。 | 来源: settleList.tenantMemberCardId | 角色: 外键
|
||||
member_card_type_name character varying(100) 会员卡类型名称快照,如“储值卡”“次卡”等,便于前端展示和报表查看。 | 来源: settleList.memberCardTypeName
|
||||
is_bind_member boolean 本单是否绑定会员。0=否(散客);1=是(存在 member_id)。样本中多为 0。 | 来源: settleList.isBindMember
|
||||
member_discount_amount numeric(18,2) 会员折扣产生的优惠金额(元),例如会员卡折扣减免的台费/商品金额,参与整单优惠拆分。 | 来源: settleList.memberDiscountAmount
|
||||
consume_money numeric(18,2) 本次结账消费总额(原价小计),约等于台费 + 商品 + 助教 + 服务等项目原价金额之和,未扣除任何优惠。 | 来源: settleList.consumeMoney
|
||||
table_charge_money numeric(18,2) 本单台费(桌台计费部分)的金额(原价侧)。 | 来源: settleList.tableChargeMoney
|
||||
goods_money numeric(18,2) 本单商品销售原价金额,对应酒水、小吃等商品类消费。 | 来源: settleList.goodsMoney
|
||||
real_goods_money numeric(18,2) 商品实际计入金额,通常为 goods_money 扣除部分促销/折扣之后的金额。 | 来源: settleList.realGoodsMoney
|
||||
assistant_pd_money numeric(18,2) 助教“排钟 / 点钟 / 按时长服务”等项目的应计金额(原价侧),与助教流水中的 ledger_amount 汇总对应。 | 来源: settleList.assistantPdMoney
|
||||
assistant_cx_money numeric(18,2) 助教“超休”类助教项目金额(原价侧),是对助教收入的补充拆分维度,具体业务定义需结合助教模块确认。 | 来源: settleList.assistantCxMoney
|
||||
adjust_amount numeric(18,2) 手动减免,人工调价金额汇总(整单减免或特殊价格调整),通常正值表示减免额度。 | 来源: settleList.adjustAmount
|
||||
pay_amount numeric(18,2) 本单顾客“实付金额”(不含券面值这类虚拟抵扣),等于各支付渠道金额之和减去退款等调整。 | 来源: settleList.payAmount
|
||||
balance_amount numeric(18,2) 从会员储值余额账户中扣除的金额(储值卡消费部分)。 | 来源: settleList.balanceAmount
|
||||
recharge_card_amount numeric(18,2) 充值卡支付金额(使用充值类卡片余额支付的金额),与储值/充值型卡资金来源相关。 | 来源: settleList.rechargeCardAmount
|
||||
gift_card_amount numeric(18,2) 礼品卡或代金卡支付金额。 | 来源: settleList.giftCardAmount
|
||||
coupon_amount numeric(18,2) 本单由优惠券(团购券、代金券等)实际抵扣的金额。 | 来源: settleList.couponAmount
|
||||
rounding_amount numeric(18,2) 抹零 / 四舍五入产生的金额差值,例如按角、分抹零。 | 来源: settleList.roundingAmount
|
||||
point_amount numeric(18,2) 积分相关金额或数量。根据系统配置可能表示“使用积分抵扣的金额”或“本单获得的积分折算金额”,文档未给出唯一定义。 | 来源: settleList.pointAmount
|
||||
-- dwd_settlement_head_ex
|
||||
order_settle_id bigint 结账记录主键 ID(订单结算 ID),全系统统一的结账单号,用于关联台费流水、助教流水、小票等明细表。 | 来源: settleList.id | 角色: 主键
|
||||
serial_number integer 结账序列号或打印序号,当前样本全部为 0,具体业务用途未在文档中明确。 | 来源: settleList.serialNumber
|
||||
settle_status integer 结账状态枚举。当前样本值均为 2,表示“已结算/已完成”;其他取值及含义未在样本和文档中出现,需后续补充。 | 来源: settleList.settleStatus
|
||||
can_be_revoked boolean 本单是否仍允许撤销/冲正。0=否;1=是。样本中均为 0。主要用于运维控制,分析价值有限。 | 来源: settleList.canBeRevoked
|
||||
revoke_order_name character varying(100) 撤销单名称/标识,用于人工识别撤销关系;当前样本为空。 | 来源: settleList.revokeOrderName
|
||||
revoke_time timestamp with time zone 撤销时间。无撤销时通常为系统默认值(如 0001-01-01 00:00:00)。 | 来源: settleList.revokeTime
|
||||
is_first_order boolean 是否首单(新客首单)标记。0=否;1=是。当前样本全部为 0,且文档中说明为“推测用途”,具体业务定义需确认。 | 来源: settleList.isFirst
|
||||
service_money numeric(18,2) 其他服务费金额(如包间服务费等),用于与台费、商品、助教金额区分。 | 来源: settleList.serviceMoney
|
||||
cash_amount numeric(18,2) 现金支付金额。 | 来源: settleList.cashAmount
|
||||
card_amount numeric(18,2) 刷卡类支付金额(如银行卡/信用卡等),具体包含哪些通道需结合支付模块确认。 | 来源: settleList.cardAmount
|
||||
online_amount numeric(18,2) 线上支付金额汇总(如微信、支付宝、云闪付等),不区分具体通道。 | 来源: settleList.onlineAmount
|
||||
refund_amount numeric(18,2) 本单涉及的退款金额(元)。普通正常结账为 0,退单或部分退款时为正数。 | 来源: settleList.refundAmount
|
||||
prepay_money numeric(18,2) 本单使用的预付金/定金金额。 | 来源: settleList.prepayMoney
|
||||
payment_method integer 支付方式整体标记(枚举)。当前样本值统一为 0,具体各枚举值对应的支付方式未在文档中说明,需业务确认。 | 来源: settleList.paymentMethod
|
||||
coupon_sale_amount numeric(18,2) 优惠券本身的售卖金额/成本金额(例如顾客为购买套餐券支付的金额),当前样本多为 0。 | 来源: settleList.couponSaleAmount
|
||||
all_coupon_discount numeric(18,2) 所有券类优惠折扣的汇总金额,用于统计“券优惠总额”。 | 来源: settleList.allCouponDiscount
|
||||
goods_promotion_money numeric(18,2) 商品促销产生的优惠金额(如满减、买赠均摊到商品部分)。 | 来源: settleList.goodsPromotionMoney
|
||||
assistant_promotion_money numeric(18,2) 助教项目参与活动/促销产生的优惠金额。 | 来源: settleList.assistantPromotionMoney
|
||||
activity_discount numeric(18,2) 整单活动折扣金额(如整单打折、满减活动产生的优惠),不区分具体项目类别。 | 来源: settleList.activityDiscount
|
||||
assistant_manual_discount numeric(18,2) 针对助教服务的人工减免金额,与一般商品/台费折扣区分开。 | 来源: settleList.assistantManualDiscount
|
||||
point_discount_price numeric(18,2) 积分抵扣对应的金额(售价侧),记录因积分使用而减少的应收金额。 | 来源: settleList.pointDiscountPrice
|
||||
point_discount_cost numeric(18,2) 积分抵扣对应的成本金额(成本侧),用于毛利和利润分析。 | 来源: settleList.pointDiscountCost
|
||||
is_use_coupon boolean 是否使用优惠券。0=未使用;1=使用。当前样本均为 0。 | 来源: settleList.isUseCoupon
|
||||
is_use_discount boolean 是否使用折扣(包括会员折扣或其他整单折扣)。0=未使用;1=使用。当前样本多为 0。 | 来源: settleList.isUseDiscount
|
||||
is_activity boolean 是否参与营销活动。0=未参与;1=参与。 | 来源: settleList.isActivity
|
||||
operator_name character varying(100) 结账操作员名称快照(通常带角色前缀,如“收银员:张三”),用于报表展示。 | 来源: settleList.operatorName
|
||||
salesman_name character varying(100) 营业员/业务员名称,用于业绩归属及提成分析;样本中多为空。 | 来源: settleList.salesManName
|
||||
order_remark character varying(255) 订单备注,由收银员手工填写的文字说明,如特殊情况、赠送原因等,主要用于人工复盘。 | 来源: settleList.orderRemark
|
||||
operator_id bigint 结账操作员用户 ID,用于关联员工/账号维度(如 dim_staff)。 | 来源: settleList.operatorId | 角色: 外键
|
||||
salesman_user_id bigint 营业员用户 ID,可关联员工维度,用于业绩分析和提成计算。 | 来源: settleList.salesManUserId | 角色: 外键
|
||||
-- dwd_store_goods_sale
|
||||
store_goods_sale_id bigint 商品销售明细主键;每条记录代表一次订单中的一个商品行流水。 | 来源: id | 角色: 主键
|
||||
order_trade_no bigint 订单交易号(业务单号);与台费、助教、团购等表的 order_trade_no 一致,用于把同一订单下各类消费串联起来。 | 来源: order_trade_no | 角色: 外键
|
||||
order_settle_id bigint 结账记录主键 ID;连接结账记录 / 结算头事实表。 | 来源: order_settle_id | 角色: 外键
|
||||
order_pay_id bigint 支付记录 ID;连接支付流水事实表,用于还原本条销售对应的收款信息。 | 来源: order_pay_id | 角色: 外键
|
||||
order_goods_id bigint 当前版本的订单内商品明细 ID;可在订单范围内唯一定位该商品行,用于与小票明细等做行级关联。 | 来源: order_goods_id | 角色: 外键
|
||||
site_id bigint 门店 ID(系统主键);与其它流水表中的 site_id 一致。 | 来源: site_id | 角色: 外键
|
||||
tenant_id bigint 租户/品牌 ID;同一品牌下多门店共享同一个 tenant_id。 | 来源: tenant_id | 角色: 外键
|
||||
site_goods_id bigint 门店级商品 ID;连接门店商品档案 dim_store_goods,与库存变动记录中的 siteGoodsId 一致。 | 来源: site_goods_id | 角色: 外键
|
||||
tenant_goods_id bigint 租户级(品牌级)商品 ID;连接租户级商品档案维度表,一个 tenant_goods_id 在不同门店可对应多个 site_goods_id。 | 来源: tenant_goods_id | 角色: 外键
|
||||
tenant_goods_category_id bigint 租户级商品一级分类 ID;连接商品分类维度(如酒水、零食等)。 | 来源: tenant_goods_category_id | 角色: 外键
|
||||
tenant_goods_business_id bigint 租户级商品业务大类 ID(更高一层的业务分类,如“零食类”“酒水类”等)。 | 来源: tenant_goods_business_id | 角色: 外键
|
||||
site_table_id bigint 球台 ID;非 0 表示该商品在某张台桌上点单,0 表示前台售卖或与台桌无关。连接台桌维度 dim_table。 | 来源: site_table_id | 角色: 外键
|
||||
ledger_name character varying(200) 销售项目名称(商品名),如“哇哈哈矿泉水”“地道肠”等;为当时销售时刻的名称快照。 | 来源: ledger_name
|
||||
ledger_group_name character varying(100) 门店前台菜单分组名称,如“酒水”“零食”“小吃”等;与品牌统一分类是两套维度。 | 来源: ledger_group_name
|
||||
ledger_unit_price numeric(18,2) 结算单价(元/单位);本次销售实际使用的单价。 | 来源: ledger_unit_price
|
||||
ledger_count integer 销售数量(以商品单位计),如 1、2、6、36 等。 | 来源: ledger_count
|
||||
ledger_amount numeric(18,2) 原始应收金额(未考虑任何折扣/抵扣),通常接近 ledger_unit_price × ledger_count。 | 来源: ledger_amount
|
||||
discount_price numeric(18,2) 折后单价(元/单位);无折扣时等于 ledger_unit_price,有折扣时小于 ledger_unit_price。 | 来源: discount_price
|
||||
real_goods_money numeric(18,2) 本行商品实际入账金额(已考虑折扣及其他抵扣后,实际计入营业额的金额);一定不大于 ledger_amount。 | 来源: real_goods_money
|
||||
cost_money numeric(18,2) 本行商品对应的成本金额,用于毛利和利润分析;源自商品档案成本价及成本核算逻辑。 | 来源: cost_money
|
||||
ledger_status integer 销售流水状态:1=正常有效;其他数值(当前数据未出现)一般表示“待结算”“作废”等。 | 来源: ledger_status
|
||||
is_delete integer 逻辑删除标志:0=正常有效;1=已删除(仅保留历史,不再参与前端展示及统计)(本批数据全部为 0)。 | 来源: is_delete
|
||||
create_time timestamp with time zone 销售记录创建时间,通常为结账时间或录入时间;用于时间维度分析,与订单层时间字段对齐。 | 来源: create_time
|
||||
-- dwd_store_goods_sale_ex
|
||||
store_goods_sale_id bigint bigint | 来源: store_goods_sale_id | 角色: id
|
||||
legacy_order_goods_id bigint bigint | 来源: legacy_order_goods_id | 角色: orderGoodsId
|
||||
site_name text varchar(100) | 来源: site_name | 角色: siteName
|
||||
legacy_site_id bigint bigint | 来源: legacy_site_id | 角色: siteId
|
||||
goods_remark text varchar(255) | 来源: goods_remark | 角色: goods_remark
|
||||
option_value_name text varchar(200) | 来源: option_value_name | 角色: option_value_name
|
||||
operator_name text varchar(100) | 来源: operator_name | 角色: operator_name
|
||||
open_salesman_flag integer tinyint | 来源: open_salesman_flag | 角色: openSalesman
|
||||
salesman_user_id bigint bigint | 来源: salesman_user_id | 角色: salesman_user_id
|
||||
salesman_name text varchar(100) | 来源: salesman_name | 角色: salesman_name
|
||||
salesman_role_id bigint bigint | 来源: salesman_role_id | 角色: salesman_role_id
|
||||
sales_man_org_id bigint bigint | 来源: sales_man_org_id | 角色: sales_man_org_id
|
||||
discount_money numeric(18,2) decimal(18,2) | 来源: discount_money | 角色: discount_money
|
||||
returns_number integer int | 来源: returns_number | 角色: returns_number
|
||||
coupon_deduct_money numeric(18,2) decimal(18,2) | 来源: coupon_deduct_money | 角色: coupon_deduct_money
|
||||
member_discount_amount numeric(18,2) decimal(18,2) | 来源: member_discount_amount | 角色: member_discount_amount
|
||||
point_discount_money numeric(18,2) decimal(18,2) | 来源: point_discount_money | 角色: point_discount_money
|
||||
point_discount_money_cost numeric(18,2) decimal(18,2) | 来源: point_discount_money_cost | 角色: point_discount_money_cost
|
||||
package_coupon_id bigint bigint | 来源: package_coupon_id | 角色: package_coupon_id
|
||||
order_coupon_id bigint bigint | 来源: order_coupon_id | 角色: order_coupon_id
|
||||
member_coupon_id bigint bigint | 来源: member_coupon_id | 角色: member_coupon_id
|
||||
option_price numeric(18,2) decimal(18,2) | 来源: option_price | 角色: option_price
|
||||
option_member_discount_money numeric(18,2) decimal(18,2) | 来源: option_member_discount_money | 角色: option_member_discount_money
|
||||
option_coupon_deduct_money numeric(18,2) decimal(18,2) | 来源: option_coupon_deduct_money | 角色: option_coupon_deduct_money
|
||||
push_money numeric(18,2) decimal(18,2) | 来源: push_money | 角色: push_money
|
||||
is_single_order integer tinyint | 来源: is_single_order | 角色: is_single_order
|
||||
sales_type integer tinyint | 来源: sales_type | 角色: sales_type
|
||||
operator_id bigint operator_id | 来源: operator_id | 角色: operator_id
|
||||
-- dwd_table_fee_adjust
|
||||
table_fee_adjust_id bigint 台费折扣 / 调整流水主键;一条台费打折或调账操作对应一条记录。 | 来源: id | 角色: 主键
|
||||
order_trade_no bigint 订单交易号;与台费流水、结账记录等表中的同名字段一致,用于把本次台费调整挂到具体订单上。 | 来源: order_trade_no | 角色: 外键
|
||||
order_settle_id bigint 结算单 / 小票 ID;与小票、结账头表中的 order_settle_id 对应,用于关联同一次结账。 | 来源: order_settle_id | 角色: 外键
|
||||
tenant_id bigint 租户 / 品牌 ID;标识该记录属于哪一个商户。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID;与 dim_site、其它业务事实表中的 site_id 一致。 | 来源: site_id | 角色: 外键
|
||||
table_id bigint 台桌 ID;与 dim_table(site_tables_master.id)以及各类台费、助教流水中的 site_table_id 对应,标识哪一张台发生了折扣/调账。 | 来源: site_table_id | 角色: 外键
|
||||
table_area_id bigint 门店维度的台桌区域 ID;与 dim_table 中的 site_table_area_id 对应,例如 “斯诺克区”“VIP包厢”等区域。 | 来源: tableProfile.site_table_area_id | 角色: 外键
|
||||
table_area_name character varying(64) 台桌区域名称快照,例如 “斯诺克区”“A区”“VIP包厢”;冗余展示字段,可从 dim_table 通过 table_area_id 获取。 | 来源: tableProfile.site_table_area_name
|
||||
tenant_table_area_id bigint 租户维度的“台桌区域 ID”;同一租户下跨门店复用的区域标识,用于在租户级别统计各区域的折扣分布。 | 来源: tenant_table_area_id | 角色: 外键
|
||||
ledger_amount numeric(18,2) 台费调整金额;等于台费流水中对应记录的 adjust_amount。正数表示被减免/调账掉的台费金额(本批数据全部为正);是衡量台费折扣规模的核心度量。 | 来源: ledger_amount
|
||||
ledger_status integer 调整记录状态(枚举)。1:生效调整(当前有效的折扣/调账记录);0:失效/被覆盖的历史记录(同一订单有多次调整时,旧记录会标记为 0,仅最新一条为 1)。 | 来源: ledger_status
|
||||
is_delete integer 逻辑删除标记(枚举)。0:未删除(有效记录);1:已逻辑删除(后台标记删除,不再参与业务统计)。当前数据全部为 0,但字段需保留以适配长期数据。 | 来源: is_delete
|
||||
adjust_time timestamp with time zone 台费调整记录创建时间,即打折/调账操作被系统写入的时间戳,用于时间分析和与结账时间对比(判断是事前折扣还是事后调账)。 | 来源: create_time
|
||||
-- dwd_table_fee_adjust_ex
|
||||
table_fee_adjust_id bigint 台费折扣 / 调整流水主键;一条台费打折或调账操作对应一条记录。 | 来源: id | 角色: 主键
|
||||
adjust_type integer 调整类型(枚举)。当前数据全部为 1。取值示例:1:台费打折 / 台费减免(本门店实际使用的唯一类型);其他值:预留给台费转移、误操作恢复等其他类型(当前未出现,仅推测)。 | 来源: adjust_type
|
||||
ledger_count integer 调整次数计数,本数据中恒为 1,表示“一次调整事件”;与台费流水中的 ledger_count(计时长)含义不同。 | 来源: ledger_count
|
||||
ledger_name character varying(128) 调账项目名称或打折原因名称(设计意图);当前门店所有记录值为空字符串,未实际使用。作用暂不明确,保留以备后续业务启用。 | 来源: ledger_name
|
||||
applicant_name character varying(64) 申请人姓名快照,通常包含角色前缀(如 “收银员:张三”);是 applicant_id 的冗余展示字段,实际名称应以员工维表为准。 | 来源: applicant_name
|
||||
operator_name character varying(64) 操作员姓名快照;与 operator_id 对应的姓名冗余字段。 | 来源: operator_name
|
||||
applicant_id bigint 申请人 ID;发起本次台费折扣/调账的员工账号 ID,用于按员工维度统计折扣行为。 | 来源: applicant_id | 角色: 外键
|
||||
operator_id bigint 实际执行调账操作的操作员 ID;当前样本中与 applicant_id 相同,但模型上允许“申请人 ≠ 操作人”。 | 来源: operator_id | 角色: 外键
|
||||
-- dwd_table_fee_log
|
||||
table_fee_log_id bigint 台费流水记录主键。每一条台费使用记录唯一一条。对应一次“台费计费单元”。 | 来源: id | 角色: 主键
|
||||
order_trade_no bigint 订单交易号。整笔订单的主编号,用于把同一订单下的台费、商品、助教等多种明细串联在一起。可与支付记录中的交易号对应。 | 来源: order_trade_no | 角色: 外键
|
||||
order_settle_id bigint 结算单号 / 结账 ID。对应一次完整的结账操作。与 dwd_settlement_head 的主键关联。 | 来源: order_settle_id | 角色: 外键
|
||||
order_pay_id bigint 订单支付记录 ID。对应支付记录中的 id 或 relate_id(视具体模型)。用于追踪这条台费最终对应哪一条支付流水。 | 来源: order_pay_id | 角色: 外键
|
||||
tenant_id bigint 租户 / 品牌 ID。本文件内所有记录属于同一租户。与其他表的 tenant_id 一致,用于品牌级过滤。 | 来源: tenant_id | 角色: 外键
|
||||
site_id bigint 门店 ID。当前样本为同一门店。与嵌套的 siteProfile.id 以及其他 JSON 中的 site_id 对应,用于门店维度关联。 | 来源: site_id | 角色: 外键
|
||||
site_table_id bigint 桌台 ID。对应“台桌基础表”的主键。用于确定具体哪一张台或包厢。 | 来源: site_table_id | 角色: 外键
|
||||
site_table_area_id bigint 门店内“台桌区域” ID(门店视角的区域,如 A 区、B 区、斯诺克区、包厢区)。与门店内部的区域维度对应。 | 来源: site_table_area_id | 角色: 外键
|
||||
site_table_area_name character varying(64) 台桌区域名称,如 “A区”“B区”“斯诺克区”“VIP包厢” 等。主要用于报表展示和人工阅读。 | 来源: site_table_area_name | 角色: 无
|
||||
tenant_table_area_id bigint 租户层面的台桌区域 ID。用于品牌层统一定义的区域配置(一个区域可在多门店复用)。对应租户级区域维度。 | 来源: tenant_table_area_id | 角色: 外键
|
||||
member_id bigint 会员 ID。多数为 0 表示散客。非 0 时表示关联会员:0 表示散客或未使用会员;>0 对应会员档案中的 id。用于将台费流水关联到 dim_member。 | 来源: member_id | 角色: 外键
|
||||
ledger_name character varying(64) 台号名称,例如 “A1”“A2”“S1”“VIP包厢” 等。等价于桌台维表中的展示名称,冗余在流水中作为快照。 | 来源: ledger_name | 角色: 无
|
||||
ledger_unit_price numeric(18,2) 台费结算时的计费单价(元/小时或元/单位时长)。与 ledger_count 配合计算原始应收台费。常见值如 48.0、58.0、68.0、88.0、98.0、116.0 等。 | 来源: ledger_unit_price | 角色: 无
|
||||
ledger_count integer 计费时长单位数。与 ledger_unit_price 共同决定原始应收额。可与 real_table_use_seconds 换算关系约为:时长秒数 ≈ ledger_count × 计费粒度(例如 30 分钟、60 分钟)。 | 来源: ledger_count | 角色: 无
|
||||
ledger_amount numeric(18,2) 原始应收台费金额,按单价与计费时长计算出来的台费金额,尚未考虑会员、券、调账等各类优惠拆分。 | 来源: ledger_amount | 角色: 无
|
||||
real_table_charge_money numeric(18,2) 实际向顾客收取的台费金额(现金 / 实付维度),不含券方承担、会员承担和内部调账部分。若为 0,则该笔台费完全由券、会员或内部调账承担。 | 来源: real_table_charge_money | 角色: 无
|
||||
coupon_promotion_amount numeric(18,2) 由优惠券、活动、团购等促销承担的优惠金额,直接抵扣在台费上。常见值为与单价或整倍数相同,例如 48.0、96.0、136.0、144.0 等。若 real_table_charge_money 为 0 且该字段等于 ledger_amount,说明台费完全由促销承担。 | 来源: coupon_promotion_amount | 角色: 无
|
||||
member_discount_amount numeric(18,2) 由会员权益产生的优惠金额,例如会员折扣、会员免费台等。若 ledger_amount = real_table_charge_money = member_discount_amount,表示这笔台费由会员权益承担,但仍作为台费收入进行记录。 | 来源: member_discount_amount | 角色: 无
|
||||
adjust_amount numeric(18,2) 手动减免,调整金额 / 调账金额。用于将台费金额转移或冲减到其他项目(例如套餐、包厢统一计费)或做手工调整。若 ledger_amount 完全被 adjust_amount 抵消,则说明该笔台费被整体调出当前台费科目。 | 来源: adjust_amount | 角色: 无
|
||||
real_table_use_seconds integer 台费实际计费时长(秒)。用于计算台费单价与费率分析。内部统一以秒为单位。 | 来源: real_table_use_seconds | 角色: 无
|
||||
add_clock_seconds integer 加钟时长(秒)。在原有使用基础上追加的累计加钟时长,常见为 2400、4200 等 60 的倍数(对应 40 分钟、70 分钟等)。 | 来源: add_clock_seconds | 角色: 无
|
||||
start_use_time timestamp with time zone 台开始使用时间,即实际开台时间。与 ledger_start_time 相同,表示计费起算点。 | 来源: start_use_time | 角色: 无
|
||||
ledger_end_time timestamp with time zone 台账计费结束时间。通常与 last_use_time 相差 1 秒。可理解为系统为计费进行的截断时刻。 | 来源: ledger_end_time | 角色: 无
|
||||
create_time timestamp with time zone 台费流水记录创建时间,通常接近结账时间。用于区分计费期间与结账时间。 | 来源: create_time | 角色: 无
|
||||
ledger_status integer 台费状态。样本中全部为 1。含义:1 表示正常已结算台费。按命名推断,0 可能表示未结算,2 可能表示作废或撤销,需要结合后续数据确认。 | 来源: ledger_status | 角色: 无
|
||||
is_single_order integer 是否独立计费单元。枚举:1 表示该记录是独立结算的桌费;0 表示非独立结算条目(可能是合并结账、转台过程中的占位记录)。is_single_order = 0 的记录通常 ledger_count 与 real_table_use_seconds 为 0。 | 来源: is_single_order | 角色: 无
|
||||
is_delete integer 逻辑删除标志。0 表示未删除(有效记录);1 表示已逻辑删除(一般不参与统计)。当前样本全部为 0。 | 来源: is_delete | 角色: 无
|
||||
-- dwd_table_fee_log_ex
|
||||
table_fee_log_id bigint 台费流水记录主键。每一条台费使用记录唯一一条。对应一次“台费计费单元”。 | 来源: id | 角色: 主键
|
||||
operator_name character varying(64) 操作员姓名。为冗余展示字段,便于直接阅读而不必联表员工档案。 | 来源: operator_name | 角色: 无
|
||||
salesman_name character varying(64) 营业员姓名。当前样本为空。用于需要对营业员维度做业绩统计时作为冗余展示。 | 来源: salesman_name | 角色: 无
|
||||
used_card_amount numeric(18,2) 由储值卡、次卡等“卡内余额”直接抵扣到台费的金额。当前样本为 0,但语义明确,用于区分“卡扣款”与“现金收款”。 | 来源: used_card_amount | 角色: 无
|
||||
service_money numeric(18,2) 服务费 / 成本 /分成金额字段,类似助教流水里的 service_money。当前样本全为 0,门店未启用该字段,未来可能用于台费附加服务费或分成计算。 | 来源: service_money | 角色: 无
|
||||
mgmt_fee numeric(18,2) 管理费字段。当前样本为 0。推测用于未来支持“台费附加管理费”功能。尚未实际启用。 | 来源: mgmt_fee | 角色: 无
|
||||
fee_total numeric(18,2) 附加费用合计值字段。当前样本为 0。设计上用于汇总管理费、服务费等附加费用。尚未实际启用。 | 来源: fee_total | 角色: 无
|
||||
ledger_start_time timestamp with time zone 台账计费起始时间。当前样本与 start_use_time 相同,表示计费与开台同时开始。 | 来源: ledger_start_time | 角色: 无
|
||||
last_use_time timestamp with time zone 最后使用 / 操作时间,通常略晚于 ledger_end_time。可视为客人最后一次用台或最后一次加钟/操作的时间点。 | 来源: last_use_time | 角色: 无
|
||||
operator_id bigint 操作员 ID。负责开台 / 结账的员工账号 ID。与员工 / 账号体系中的用户 ID 对应。 | 来源: operator_id | 角色: 外键
|
||||
salesman_user_id bigint 营业员用户 ID。目前样本值为 0,表示门店暂未使用此字段做提成员工归属,但语义清晰。 | 来源: salesman_user_id | 角色: 外键
|
||||
salesman_org_id bigint 营业员所属机构 / 部门 ID。目前样本为 0。用于员工组织结构统计时的归属。 | 来源: salesman_org_id | 角色: 外键
|
||||
78
etl_billiards/utils/json_store.py
Normal file
78
etl_billiards/utils/json_store.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""JSON 归档/读取的通用工具。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
ENDPOINT_FILENAME_MAP: dict[str, str] = {
|
||||
"/memberprofile/gettenantmemberlist": "member_profiles.json",
|
||||
"/memberprofile/getmembercardbalancechange": "member_balance_changes.json",
|
||||
"/memberprofile/gettenantmembercardlist": "member_stored_value_cards.json",
|
||||
"/site/getrechargesettlelist": "recharge_settlements.json",
|
||||
"/assistantperformance/getabolitionassistant": "assistant_cancellation_records.json",
|
||||
"/assistantperformance/getorderassistantdetails": "assistant_service_records.json",
|
||||
"/personnelmanagement/searchassistantinfo": "assistant_accounts_master.json",
|
||||
"/table/getsitetables": "site_tables_master.json",
|
||||
"/site/gettaifeeadjustlist": "table_fee_discount_records.json",
|
||||
"/site/getsitetableorderdetails": "table_fee_transactions.json",
|
||||
"/tenantgoods/querytenantgoods": "tenant_goods_master.json",
|
||||
"/packagecoupon/querypackagecouponlist": "group_buy_packages.json",
|
||||
"/site/getsitetableusedetails": "group_buy_redemption_records.json",
|
||||
"/order/getordersettleticketnew": "settlement_ticket_details.json",
|
||||
"/promotion/getofflinecouponconsumepagelist": "platform_coupon_redemption_records.json",
|
||||
"/goodsstockmanage/querygoodsoutboundreceipt": "goods_stock_movements.json",
|
||||
"/tenantgoodscategory/queryprimarysecondarycategory": "stock_goods_category_tree.json",
|
||||
"/tenantgoods/getgoodsstockreport": "goods_stock_summary.json",
|
||||
"/paylog/getpayloglistpage": "payment_transactions.json",
|
||||
"/site/getallordersettlelist": "settlement_records.json",
|
||||
"/order/getrefundpayloglist": "refund_transactions.json",
|
||||
"/tenantgoods/getgoodsinventorylist": "store_goods_master.json",
|
||||
"/tenantgoods/getgoodssaleslist": "store_goods_sales_records.json",
|
||||
}
|
||||
|
||||
def endpoint_to_filename(endpoint: str) -> str:
|
||||
"""
|
||||
将 API endpoint 转换为规范化的文件名,优先使用 非球接口API.md 中约定的名称。
|
||||
未覆盖的路径会回退到“去掉开头斜杠 -> 用双下划线替换斜杠 -> 小写”的规则。
|
||||
"""
|
||||
normalized = _normalize_endpoint(endpoint)
|
||||
if normalized in ENDPOINT_FILENAME_MAP:
|
||||
return ENDPOINT_FILENAME_MAP[normalized]
|
||||
|
||||
fallback = normalized.strip("/").replace("/", "__").replace(" ", "_")
|
||||
return f"{fallback or 'root'}.json"
|
||||
|
||||
|
||||
def dump_json(path: Path, payload: Any, pretty: bool = False):
|
||||
"""将 JSON 对象写入文件,默认紧凑,可选美化。"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as fp:
|
||||
json.dump(payload, fp, ensure_ascii=False, indent=2 if pretty else None)
|
||||
|
||||
|
||||
def _normalize_endpoint(endpoint: str) -> str:
|
||||
"""标准化 endpoint,提取路径部分并统一小写、去除 base 前缀。"""
|
||||
raw = str(endpoint or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
|
||||
parsed = urlparse(raw)
|
||||
path = parsed.path or raw
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
path = path.rstrip("/") or "/"
|
||||
lowered = path.lower()
|
||||
for prefix in ("/apiprod/admin/v1", "apiprod/admin/v1"):
|
||||
if lowered.startswith(prefix):
|
||||
path = path[len(prefix) :]
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
path = path.rstrip("/") or "/"
|
||||
lowered = path.lower()
|
||||
break
|
||||
|
||||
return lowered
|
||||
53
etl_billiards/utils/reporting.py
Normal file
53
etl_billiards/utils/reporting.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""简单的任务结果汇总与格式化工具。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def summarize_counts(task_results: Iterable[dict]) -> dict:
|
||||
"""
|
||||
汇总多个任务的 counts,返回总计与逐任务明细。
|
||||
task_results: 形如 {"task_code": str, "counts": {...}} 的字典序列。
|
||||
"""
|
||||
totals = {"fetched": 0, "inserted": 0, "updated": 0, "skipped": 0, "errors": 0}
|
||||
details = []
|
||||
|
||||
for res in task_results:
|
||||
code = res.get("task_code") or res.get("code") or "UNKNOWN"
|
||||
counts = res.get("counts") or {}
|
||||
row = {"task_code": code}
|
||||
for key in totals.keys():
|
||||
val = int(counts.get(key, 0) or 0)
|
||||
row[key] = val
|
||||
totals[key] += val
|
||||
details.append(row)
|
||||
|
||||
return {"total": totals, "details": details}
|
||||
|
||||
|
||||
def format_report(summary: dict) -> str:
|
||||
"""将 summarize_counts 的输出格式化为可读文案。"""
|
||||
lines = []
|
||||
totals = summary.get("total", {})
|
||||
lines.append(
|
||||
"TOTAL fetched={fetched} inserted={inserted} updated={updated} skipped={skipped} errors={errors}".format(
|
||||
fetched=totals.get("fetched", 0),
|
||||
inserted=totals.get("inserted", 0),
|
||||
updated=totals.get("updated", 0),
|
||||
skipped=totals.get("skipped", 0),
|
||||
errors=totals.get("errors", 0),
|
||||
)
|
||||
)
|
||||
for row in summary.get("details", []):
|
||||
lines.append(
|
||||
"{task_code}: fetched={fetched} inserted={inserted} updated={updated} skipped={skipped} errors={errors}".format(
|
||||
task_code=row.get("task_code", "UNKNOWN"),
|
||||
fetched=row.get("fetched", 0),
|
||||
inserted=row.get("inserted", 0),
|
||||
updated=row.get("updated", 0),
|
||||
skipped=row.get("skipped", 0),
|
||||
errors=row.get("errors", 0),
|
||||
)
|
||||
)
|
||||
return "\n".join(lines)
|
||||
33
etl_billiards/草稿.txt
Normal file
33
etl_billiards/草稿.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
一个用于开发的示例目录:C:\dev\LLTQ\export\test-json-doc
|
||||
|
||||
我建议完全删除LLZQ-test数据库中名称是billiards_ods的Schema下全部表和内容。根据非球接口API.md文档重建表,表名
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
对feiqiu-ETL 项目进行修改:
|
||||
1 清空LLZQ-test数据库billiards_ods的schame全部内容。
|
||||
2 对照etl_billiards\tests\source-data-doc\下的json数据和md字段分析文档,重建ODS层表结构。
|
||||
3 对应Python文件也需要对应修改
|
||||
4 严禁修改source-data-doc中的任意文件,允许新建临时文件。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
下面开始重建数据库的ODS。
|
||||
一个用于开发的示例目录:C:\dev\LLTQ\export\test-json-doc。是测试用的json数据,并且已经按照现行逻辑进行了重命名。对应.md是Json数据的分析。
|
||||
用于数据库ODS层的构建和Python文件的编写。
|
||||
我建议完全删除LLZQ-test数据库中名称是billiards_ods的Schema下全部表和内容。根据上述示例目录的json和md文档重建表,表名就按对应json文件名来,字段名就是json字段名。
|
||||
我理解ODS层本来就说Json文件的数据库留档,我这么理解对么?
|
||||
|
||||
|
||||
|
||||
|
||||
使用中文沟通。
|
||||
先通读整个项目,然后我们聚焦DWD的数据库表的建立和 从ODS到DWD的 Python处理(含当前的测试,任务,)代码
|
||||
|
||||
|
||||
|
||||
115
hebing.py
Normal file
115
hebing.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 分隔线定义,保持与你示例中的长度一致
|
||||
HEADER_LINE_SHORT = "=" * 22 # ======================
|
||||
HEADER_LINE_LONG = "=" * 26 # ==========================
|
||||
MIDDLE_LINE = "-" * 31 # -------------------------------
|
||||
|
||||
|
||||
def merge_md_and_json(directory: str, output_file: str = "merged_output.txt") -> None:
|
||||
"""
|
||||
在指定目录下,将 .md 和 .json 文件按规则合并输出到一个文件中。
|
||||
|
||||
规则:
|
||||
- 遍历所有 .md 文件;
|
||||
- 对每个 .md 文件,取“文件名(不含扩展名)中第一个空格前的字符串”作为 key;
|
||||
- 在同目录下按 key 精确匹配 .json 的“文件名(不含扩展名)”;
|
||||
- 只有存在匹配的 json 文件时才合并;找不到 json 的 md 文件丢弃。
|
||||
"""
|
||||
base_dir = Path(directory).resolve()
|
||||
if not base_dir.is_dir():
|
||||
raise NotADirectoryError(f"指定路径不是目录: {base_dir}")
|
||||
|
||||
# 收集所有 json 文件,以“文件名(不含扩展名)”为 key
|
||||
json_map = {}
|
||||
for json_path in base_dir.glob("*.json"):
|
||||
key = json_path.stem # 文件名不含扩展名
|
||||
json_map[key] = json_path
|
||||
|
||||
# 收集所有 md 文件
|
||||
md_files = list(base_dir.glob("*.md"))
|
||||
# 为了输出顺序稳定,按文件名排序
|
||||
md_files.sort(key=lambda p: p.name)
|
||||
|
||||
output_path = base_dir / output_file
|
||||
|
||||
with output_path.open("w", encoding="utf-8") as out_f:
|
||||
first_section_written = False
|
||||
|
||||
for md_path in md_files:
|
||||
# 取 md 文件名(不含扩展名)第一个空格前部分作为 key
|
||||
md_stem = md_path.stem
|
||||
key = md_stem.split(" ", 1)[0] # 仅按第一个空格切分
|
||||
|
||||
json_path = json_map.get(key)
|
||||
if json_path is None:
|
||||
# 没有匹配到 json,丢弃此 md
|
||||
continue
|
||||
|
||||
# 读 md 内容
|
||||
with md_path.open("r", encoding="utf-8") as f_md:
|
||||
md_content = f_md.read()
|
||||
|
||||
# 读 json 内容
|
||||
with json_path.open("r", encoding="utf-8") as f_json:
|
||||
json_content = f_json.read()
|
||||
|
||||
# 如果不是第一段,可以视需要在前面插入一个空行,避免段落粘连
|
||||
if first_section_written:
|
||||
out_f.write("\n")
|
||||
first_section_written = True
|
||||
|
||||
# 写入合并内容
|
||||
# 结构:
|
||||
# ======================
|
||||
# XXX.md
|
||||
# ======================
|
||||
# <md 内容>
|
||||
#
|
||||
# -------------------------------
|
||||
# 示例数据:
|
||||
# <json 内容>
|
||||
#
|
||||
# ==========================
|
||||
# <下一个 md 文件名>
|
||||
# ==========================
|
||||
|
||||
# 头部(短等号)
|
||||
out_f.write(f"{HEADER_LINE_SHORT}\n")
|
||||
out_f.write(f"{md_path.name}\n")
|
||||
out_f.write(f"{HEADER_LINE_SHORT}\n")
|
||||
out_f.write(md_content.rstrip() + "\n") # 去掉尾部多余换行,统一在后面加一个
|
||||
|
||||
out_f.write("\n")
|
||||
out_f.write(f"{MIDDLE_LINE}\n")
|
||||
out_f.write("示例数据:\n")
|
||||
out_f.write(json_content.rstrip() + "\n")
|
||||
|
||||
# 底部(长等号)
|
||||
out_f.write("\n")
|
||||
out_f.write(f"{HEADER_LINE_LONG}\n")
|
||||
out_f.write(f"{md_path.name}\n")
|
||||
out_f.write(f"{HEADER_LINE_LONG}\n")
|
||||
|
||||
print(f"合并完成,输出文件:{output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
用法示例:
|
||||
python merge_md_json.py /path/to/dir
|
||||
python merge_md_json.py /path/to/dir result.txt
|
||||
"""
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python merge_md_json.py <目录路径> [输出文件名]")
|
||||
sys.exit(1)
|
||||
|
||||
input_dir = sys.argv[1]
|
||||
if len(sys.argv) >= 3:
|
||||
output_name = sys.argv[2]
|
||||
else:
|
||||
output_name = "merged_output.txt"
|
||||
|
||||
merge_md_and_json(input_dir, output_name)
|
||||
@@ -1,2 +1,4 @@
|
||||
requests
|
||||
requests
|
||||
psycopg2-binary
|
||||
python-dateutil
|
||||
tzdata
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
2
tmp/doc_extracted.txt
Normal file
2
tmp/doc_extracted.txt
Normal file
File diff suppressed because one or more lines are too long
286
tmp/doc_lines.txt
Normal file
286
tmp/doc_lines.txt
Normal file
@@ -0,0 +1,286 @@
|
||||
台球厅数仓 DWD 层数据库说明书
|
||||
本说明书详细列出了台球厅经营系统的 DWD 层表结构。
|
||||
每张表都包含字段名称、数据类型、来源、含义、是否属于主键/外键、业务重要性、未知作用标记,以及枚举值解释。说明书依据《*-Analysis.md》中提供的字段说明整理完成,未出现省略号,确保字段信息完整可追溯。
|
||||
因业务需求,将一个表拆成主数据表和扩展数据表(Ex为后缀),如:维度表的门店数据表分为主表dim_site 和扩展表dim_site_Ex,主键相同,作为唯一关联标识。在业务代码处理的读和写时,使用统一处理方式,将数据视为一个表格。注意,极少数表,没有扩展表。
|
||||
注意:考虑到后期分布式部署,以及测试的便利性。所有的“外键”的处理,使用业务处理,不在数据库中强制约束。
|
||||
维度表(DIM)
|
||||
dim_site
|
||||
门店维度表,提取自各 ODS 中的 siteProfile 对象,如table_fee_transactions。记录门店的基本信息和配置,是其他事实表的外键。
|
||||
dim_site_Ex
|
||||
dim_table
|
||||
台桌维度表,来自 site_tables_master。每行代表一张球台或包厢,包含区域和业务角色信息。
|
||||
dim_table_Ex
|
||||
dim_assistant
|
||||
助教档案维表,对应 assistant_accounts_master。每行代表一位助教账号及其人事/账号状态。
|
||||
dim_assistant_Ex
|
||||
dim_member
|
||||
会员档案维表,对应 member_profiles。每行记录租户内某会员的主档信息,包括等级、状态、注册信息等。
|
||||
dim_member_Ex
|
||||
dim_member_card_account
|
||||
已开通的会员卡账户视图,来自 member_stored_value_cards。每行代表一张会员卡账户的快照,记录卡种、持卡人、余额、有效期及各种折扣/扣款配置。
|
||||
重要说明:本视图不仅包含储值卡,还囊括活动抵用券、台费卡、酒水卡、月卡等多种卡种。
|
||||
大多数折扣/扣款字段在当前数据中保持默认值(如 10.0 表示不打折、100.0 表示全额抵扣、0 表示不启用),业务上暂未使用,但为系统预留能力。
|
||||
dim_member_card_account_Ex
|
||||
dim_tenant_goods
|
||||
租户级商品档案,来自 tenant_goods_master。每行代表一款商品标准定义。
|
||||
dim_tenant_goods_Ex
|
||||
dim_store_goods
|
||||
门店级商品档案,来自 store_goods_master.json。每行代表门店自定义的商品 SKU,包括售价和折扣。关联到 dim_tenant_goods 和分类维度。
|
||||
dim_store_goods_Ex
|
||||
dim_goods_category
|
||||
商品分类索引树,来自 stock_goods_category_tree.json。每行是一个分类节点。
|
||||
categoryBoxes 是“某个分类节点下面的子分类列表”,整个文件里只有两层:根节点 + 子节点两级,不存在孙节点。
|
||||
每个 categoryBoxes 里的元素结构与根节点完全一致(同样的 11 个字段),只是 pid 指向父节点的 id,categoryBoxes 为空。
|
||||
同一个分类树在 JSON 里分页返回了两次,goodsCategoryList 和每个 categoryBoxes 在两个 page 中完全重复,真正的不同分类节点一共只有 26 个。
|
||||
从数仓角度,树结构的“真实关系”完全由 id 和 pid 就可以表达,categoryBoxes 更像是前端为了直接画树而准备的冗余展开结果,在 DWD 里不需要原样存这一坨结构,只需要被“打散”成一行一个节点。
|
||||
下面我把完整的 categoryBoxes 结构按业务和数据的视角展开给你看。
|
||||
一、整体结构:categoryBoxes 是子分类数组,深度只有两层
|
||||
stock_goods_category_tree.json 顶层是一个分页数组 pages,每个元素形如:
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": 9,
|
||||
"goodsCategoryList": [ 根分类1, 根分类2, ... 共9个 ]
|
||||
}
|
||||
}
|
||||
每个“根分类”对象都有这些字段:
|
||||
id
|
||||
tenant_id
|
||||
category_name
|
||||
alias_name
|
||||
pid
|
||||
business_name
|
||||
tenant_goods_business_id
|
||||
open_salesman
|
||||
categoryBoxes
|
||||
sort
|
||||
is_warehousing
|
||||
其中:
|
||||
pid = 0 表示根分类。
|
||||
categoryBoxes 是一个数组,里面放的是子分类节点对象。
|
||||
子分类对象和根分类字段完全一样,只是:
|
||||
pid = 父节点 id
|
||||
categoryBoxes = []
|
||||
两个 page 的 goodsCategoryList 完全相同,所以你看到的 18 个“根”其实是相同的 9 个重复了两次,categoryBoxes 里的子节点也重复了两次。按照去重后的真实结构:
|
||||
根节点 9 个。
|
||||
子节点 17 个。
|
||||
总共 26 个不同的 id。
|
||||
所以“完整的 categoryBoxes”其实就是:9 个根节点各自带着若干子节点。
|
||||
二、逐个根节点把 categoryBoxes 展开给你看
|
||||
下面按“根分类 → categoryBoxes 子分类”的树形方式列一遍,便于你直观看到完整结构。
|
||||
1. 根:槟榔
|
||||
根节点:
|
||||
id = 2790683528350533
|
||||
category_name = 槟榔
|
||||
business_name = 槟榔
|
||||
pid = 0
|
||||
categoryBoxes 只有一个子分类:
|
||||
子分类 1:
|
||||
id = 2790683528350534
|
||||
category_name = 槟榔
|
||||
business_name = 槟榔
|
||||
pid = 2790683528350533
|
||||
categoryBoxes = []
|
||||
其它字段:tenant_id、tenant_goods_business_id、is_warehousing、open_salesman、sort 与父节点一致。
|
||||
可以理解为:业务线“槟榔”,下面只有一个细分类“槟榔”。
|
||||
2. 根:器材
|
||||
根节点:
|
||||
id = 2790683528350535
|
||||
category_name = 器材
|
||||
business_name = 器材
|
||||
pid = 0
|
||||
categoryBoxes 子分类 3 个:
|
||||
子分类 1:
|
||||
id = 2790683528350536
|
||||
category_name = 皮头
|
||||
pid = 2790683528350535
|
||||
子分类 2:
|
||||
id = 2790683528350537
|
||||
category_name = 球杆
|
||||
pid = 2790683528350535
|
||||
子分类 3:
|
||||
id = 2790683528350538
|
||||
category_name = 其他
|
||||
pid = 2790683528350535
|
||||
这条业务线代表所有“器材相关商品”,细分为皮头、球杆、器材其他。
|
||||
3. 根:酒水
|
||||
根节点:
|
||||
id = 2790683528350539
|
||||
category_name = 酒水
|
||||
business_name = 酒水
|
||||
pid = 0
|
||||
categoryBoxes 子分类 6 个:
|
||||
子分类 1:饮料
|
||||
id = 2790683528350540
|
||||
category_name = 饮料
|
||||
pid = 2790683528350539
|
||||
子分类 2:酒水
|
||||
id = 2790683528350541
|
||||
category_name = 酒水
|
||||
pid = 2790683528350539
|
||||
子分类 3:茶水
|
||||
id = 2790683528350542
|
||||
category_name = 茶水
|
||||
pid = 2790683528350539
|
||||
子分类 4:咖啡
|
||||
id = 2790683528350543
|
||||
category_name = 咖啡
|
||||
pid = 2790683528350539
|
||||
子分类 5:加料
|
||||
id = 2790683528350544
|
||||
category_name = 加料
|
||||
pid = 2790683528350539
|
||||
子分类 6:洋酒
|
||||
id = 2793221553489733
|
||||
category_name = 洋酒
|
||||
pid = 2790683528350539
|
||||
这里是最典型的一棵分类树:业务线“酒水”,细分成饮料、普通酒水、茶水、咖啡、加料、洋酒。
|
||||
4. 根:果盘(业务线:水果)
|
||||
根节点:
|
||||
id = 2790683528350545
|
||||
category_name = 果盘
|
||||
business_name = 水果
|
||||
pid = 0
|
||||
categoryBoxes 子分类 1 个:
|
||||
子分类:
|
||||
id = 2792050275864453
|
||||
category_name = 果盘
|
||||
business_name = 水果
|
||||
pid = 2790683528350545
|
||||
这里有个有意思的点:
|
||||
分类名称用的是“果盘”,而业务大类 business_name 是“水果”,说明业务线从“水果”角度管理,这个店真正卖的具体品类是“果盘”。
|
||||
5. 根:零食
|
||||
根节点:
|
||||
id = 2791941988405125
|
||||
category_name = 零食
|
||||
business_name = 零食
|
||||
pid = 0
|
||||
categoryBoxes 子分类 2 个:
|
||||
子分类 1:
|
||||
id = 2791948300259205
|
||||
category_name = 零食
|
||||
pid = 2791941988405125
|
||||
子分类 2:
|
||||
id = 2793236829620037
|
||||
category_name = 面
|
||||
pid = 2791941988405125
|
||||
这说明“面”类商品也被算在零食这条业务线里(这完全是你们的门店本地习惯)。
|
||||
6. 根:雪糕
|
||||
根节点:
|
||||
id = 2791942087561093
|
||||
category_name = 雪糕
|
||||
business_name = 雪糕
|
||||
pid = 0
|
||||
categoryBoxes 子分类 1 个:
|
||||
子分类:
|
||||
id = 2792035069284229
|
||||
category_name = 雪糕
|
||||
pid = 2791942087561093
|
||||
7. 根:香烟
|
||||
根节点:
|
||||
id = 2792062778003333
|
||||
category_name = 香烟
|
||||
business_name = 香烟
|
||||
pid = 0
|
||||
categoryBoxes 子分类 1 个:
|
||||
子分类:
|
||||
id = 2792063209623429
|
||||
category_name = 香烟
|
||||
pid = 2792062778003333
|
||||
8. 根:其他
|
||||
根节点:
|
||||
id = 2793217944864581
|
||||
category_name = 其他
|
||||
business_name = 其他
|
||||
pid = 0
|
||||
categoryBoxes 子分类 1 个:
|
||||
子分类:
|
||||
id = 2793218343257925
|
||||
category_name = 其他2
|
||||
pid = 2793217944864581
|
||||
可以理解为“杂项类商品”的一级和二级拆分。
|
||||
9. 根:小吃
|
||||
根节点:
|
||||
id = 2793220945250117
|
||||
category_name = 小吃
|
||||
business_name = 小吃
|
||||
pid = 0
|
||||
categoryBoxes 子分类 1 个:
|
||||
子分类:
|
||||
id = 2793221283104581
|
||||
category_name = 小吃
|
||||
pid = 2793220945250117
|
||||
三、categoryBoxes 元素的字段与含义
|
||||
无论是在 goodsCategoryList 还是 categoryBoxes 里,每个分类节点的字段集合完全一致:
|
||||
id:分类节点主键,唯一。
|
||||
tenant_id:租户 ID,本文件所有节点相同。
|
||||
category_name:分类名,见上面的各种名称。
|
||||
alias_name:分类别名,当前全部为空字符串。
|
||||
pid:父级分类 ID,根节点为 0,子节点为父节点 id。
|
||||
business_name:业务大类名,用于业务线聚合。
|
||||
tenant_goods_business_id:业务大类 ID,对应 business_name,根节点与子节点在同一业务线中取值相同。
|
||||
open_salesman:营业员开关,当前所有值为 2,表示没启用分类级差异。
|
||||
categoryBoxes:子分类数组,只有根节点非空,子节点都是空数组。
|
||||
sort:排序,小部分为 1,大部分为 0,目前排序未精细化使用。
|
||||
is_warehousing:是否走库存,本文件全部为 1。
|
||||
这一点很重要:categoryBoxes 里的元素不是“别的结构”,就是一套完整分类节点,只是挂在父节点下面而已。
|
||||
四、从 DWD 建模角度怎么看 categoryBoxes
|
||||
结合上面的完整展开,可以得出几个明确结论和建议:
|
||||
真实树关系靠的是 id 和 pid
|
||||
子节点的 pid 永远等于父节点的 id。
|
||||
即使没有 categoryBoxes,你也完全可以自下而上拼出整棵树。
|
||||
categoryBoxes 是前端友好的结构,不是建模必要字段。
|
||||
当前树只有两层
|
||||
根节点的 categoryBoxes 非空,子节点的 categoryBoxes 全为空。
|
||||
对应 DWD 可以直接用一个 category_level 字段区分 1、2 层,再配一个 is_leaf 字段。
|
||||
JSON 有分页重复
|
||||
同一套分类树出现在两个 page 中,goodsCategoryList 与 categoryBoxes 内容重复。
|
||||
ETL 时必须按 id 去重,否则维表会重复插入相同分类。
|
||||
在 DWD 的 dim_goods_category 里,categoryBoxes 本身不需要落列
|
||||
只保留每个节点一行:category_id、category_name、parent_category_id、category_level、is_leaf、tenant_goods_business_id、business_name 等即可。
|
||||
如果你特别想保留源结构,可以另开一个 raw_json 字段存原始节点 JSON,日后排错用,但不建议在分析建模中依赖它。
|
||||
dim_goods_category_Ex
|
||||
dim_groupbuy_package
|
||||
团购套餐定义,来自 group_buy_packages。每行代表一种团购套餐及其使用规则。
|
||||
dim_groupbuy_package_Ex
|
||||
事实表(DWD)
|
||||
以下事实表均以“业务事件”为粒度,不做聚合。字段来源包括原始 JSON(或ODS) 中的明细数组以及对象属性。时间单位均统一为秒,并保留原始字段以备检查。金额按照源系统保持符号规则,不做符号转换。
|
||||
dwd_settlement_head(结账记录)
|
||||
来自 settlement_records的内层 settleList 对象,每行代表一次结账。该表在业务上是其他明细事实表的汇总头,用于串联台费、商品、助教、券等明细。
|
||||
dwd_settlement_head_Ex(结账记录扩展)
|
||||
dwd_table_fee_log(台费流水)
|
||||
来自 table_fee_transactions的 siteTableUseDetailsList,忽略siteProfile(已在dim_site实现)。粒度为一次台费使用记录(包括包厢)。该表连结订单结账头、桌台、会员等维度。
|
||||
dwd_table_fee_log_Ex(台费流水扩展)
|
||||
dwd_table_fee_adjust(台费折扣/调整)
|
||||
来自 table_fee_discount_records的table_fee_discount_records.data.taiFeeAdjustInfos. 路径下字段路径。每行代表一次台费打折或减免操作。由于结构相对简单,字段说明如下:
|
||||
dwd_table_fee_adjust_Ex(台费折扣/调整扩展)
|
||||
dwd_store_goods_sale(商品销售明细)
|
||||
来自 store_goods_sales_records的 orderGoodsLedgers。每行代表订单中的一条商品销售明细。字段较多,以下列出关键字段及其作用。
|
||||
dwd_store_goods_sale_Ex(商品销售明细扩展)
|
||||
dwd_assistant_service_log(助教服务流水)
|
||||
来自 assistant_service_records的 assistant_service_records.data.orderAssistantDetails.。每行表示一次助教提供服务的记录,包括服务时长、金额、助教与会员关联等。
|
||||
dwd_assistant_service_log_Ex(助教服务流水扩展)
|
||||
dwd_assistant_trash_event(助教废除事件)
|
||||
来自 assistant_cancellation_records 的 abolitionAssistants。每行代表一次助教服务被废除的事件,无法直接与结算记录或助教流水关联,只能通过门店+台桌+助教+时间窗口软关联。
|
||||
dwd_assistant_trash_event_Ex(助教废除事件扩展)
|
||||
dwd_member_balance_change(会员余额变动)
|
||||
来自 member_balance_changes.json,粒度为一次储值卡账户余额变动。此表是分析会员资金往来的核心事实表。
|
||||
dwd_member_balance_change_EX(会员余额变动扩展)
|
||||
dwd_groupbuy_redemption(团购券核销)
|
||||
来自 group_buy_redemption_records.json 中各条记录。每行代表一次团购券使用/核销事件。
|
||||
dwd_groupbuy_redemption_Ex(团购券核销扩展)
|
||||
来自 group_buy_redemption_records.json 中各条记录。每行代表一次团购券使用/核销事件。
|
||||
dwd_platform_coupon_redemption(第三方平台券核销)
|
||||
来自 platform_coupon_redemption_records.json。每条记录代表一次第三方团购券的核销,用于追踪渠道引流和兑换。
|
||||
dwd_platform_coupon_redemption_Ex(第三方平台券核销扩展)
|
||||
dwd_recharge_order(充值结算)
|
||||
来自 recharge_settlements.json的settleList.settleList.。每行是一条充值订单,记录充值金额、赠送金额及是否首充。
|
||||
dwd_recharge_order_Ex(充值结算扩展)
|
||||
dwd_payment(支付流水)
|
||||
来自 payment_transactions.json。每行代表一笔支付或收款流水,与结算单、充值单等关联。只有 pay_status=2 的支付成功记录被导出。
|
||||
dwd_refund(退款流水)
|
||||
来自 refund_transactions.json。每行代表一笔退款,对应原支付流水。退款金额以负数存储在 pay_amount 字段;字段 refund_amount 全部为 0,实际退款金额需取 pay_amount 的绝对值。
|
||||
dwd_refund(退款流水)
|
||||
来自 refund_transactions.json。每行代表一笔退款,对应原支付流水。退款金额以负数存储在 pay_amount 字段;字段 refund_amount 全部为 0,实际退款金额需取 pay_amount 的绝对值。
|
||||
总结
|
||||
本说明书列出了经营数据仓库 DWD 层的主要维度表和事实表的字段结构及说明,尽可能在每个字段上标注其来源、含义及业务重要性。对于未在 MD 文档中解释的字段标记为 作用未知,在建模时建议保留字段但谨慎使用;对业务逻辑影响较小的展示类字段标记为 不重要。枚举字段均列出了观测到的取值和推断的含义,便于后续 ETL 做值域映射和数据清洗。随着业务扩展和数据补充,可继续完善枚举信息、用途说明和字段分类。
|
||||
2610
tmp/dwd_tables.json
Normal file
2610
tmp/dwd_tables.json
Normal file
File diff suppressed because it is too large
Load Diff
6188
tmp/dwd_tables_full.json
Normal file
6188
tmp/dwd_tables_full.json
Normal file
File diff suppressed because it is too large
Load Diff
13
开发笔记/记录.md
13
开发笔记/记录.md
@@ -1,13 +0,0 @@
|
||||
原来在 task_merged.py 里配置的 14 个任务中,有 11 个目前还没有在新项目里实现,对应的 loader / task 类也不存在。
|
||||
|
||||
|
||||
|
||||
原脚本里“导出请求/响应 JSON 到本地目录、生成 manifest.json / ingest_report.json 并支持 offline 模式”的那一块逻辑,在新代码里还没有真正落地,只保留了配置字段和数据库字段,但没有实际写文件和离线装载的实现。
|
||||
|
||||
|
||||
丰富Pytest,进行分模块.分任务测试
|
||||
|
||||
|
||||
质量检查目前没有被“接入主流程”,内容也待完善,入库等问题?
|
||||
|
||||
|
||||
751
非球接口API.md
Normal file
751
非球接口API.md
Normal file
@@ -0,0 +1,751 @@
|
||||
## 1. 总体概览
|
||||
|
||||
### 1.1 服务地址与版本
|
||||
|
||||
* Base URL(统一前缀):
|
||||
`https://pc.ficoo.vip/apiprod/admin/v1`
|
||||
* 所有接口路径均在该前缀下,例如:
|
||||
|
||||
* 助教流水:`/AssistantPerformance/GetOrderAssistantDetails`
|
||||
* 门店商品档案1:`/TenantGoods/GetGoodsInventoryList`
|
||||
|
||||
统一完整 URL 形式:
|
||||
|
||||
```text
|
||||
https://pc.ficoo.vip/apiprod/admin/v1{path}
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
https://pc.ficoo.vip/apiprod/admin/v1/AssistantPerformance/GetOrderAssistantDetails
|
||||
```
|
||||
|
||||
### 1.2 认证方式
|
||||
|
||||
* 认证方式:Bearer Token(JWT)
|
||||
|
||||
* HTTP Header:
|
||||
|
||||
```http
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
* Token 获取方式:由 Ficoo 系统颁发(脚本目前通过环境变量 `FICOO_TOKEN` 或命令行 `--token` 传入)。
|
||||
|
||||
* 注意:
|
||||
|
||||
* Token 为敏感信息,不要写死在代码仓库或文档中。通过CLI或.env配置。
|
||||
* 示例中出现的实际 JWT 请在内部全部替换成占位符。
|
||||
|
||||
### 1.3 请求方式与编码
|
||||
|
||||
* HTTP Method:目前脚本全部使用 `POST`。
|
||||
* 请求体:`Content-Type: application/json`,JSON 编码,UTF-8。
|
||||
* 响应体:JSON,UTF-8。
|
||||
|
||||
示例(来自现有 curl,经脱敏后):
|
||||
|
||||
```bash
|
||||
curl "https://pc.ficoo.vip/apiprod/admin/v1/AssistantPerformance/GetOrderAssistantDetails" \
|
||||
-H "Authorization: Bearer <your_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/plain, */*" \
|
||||
--data-raw '{
|
||||
"siteId": 2790685415443269,
|
||||
"startTime": "2025-11-01 08:00:00",
|
||||
"endTime": "2025-11-08 08:00:00",
|
||||
"IsConfirm": 2,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
}'
|
||||
```
|
||||
|
||||
### 1.4 通用 Header 建议(必须)
|
||||
|
||||
所有接口均为内部管理后台接口,调用时必须携带以下核心请求头:
|
||||
|
||||
Header 名称 示例值 说明
|
||||
Authorization Bearer <access_token> 鉴权,使用 Bearer Token
|
||||
Content-Type application/json 请求体为 JSON 编码
|
||||
Accept application/json, text/plain, */* 客户端期望接收 JSON 响应
|
||||
|
||||
注意:<access_token> 为系统颁发的 JWT,文档和示例中一律使用占位符,不要写实际 token。
|
||||
|
||||
#### 1.4.2 浏览器兼容性请求头(建议统一保留)
|
||||
|
||||
为降低前置网关 / 安全策略的拦截风险,系统当前前端及采集脚本在调用接口时,统一携带一组“浏览器兼容性请求头”,用于模拟真实浏览器环境。这些头部对业务语义无强制要求,但推荐所有客户端统一保留:
|
||||
|
||||
Header 名称 示例值(仅示意) 说明
|
||||
Origin https://pc.ficoo.vip 请求来源域名,前端浏览器自动携带
|
||||
Referer https://pc.ficoo.vip/ 页面来源地址
|
||||
User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64)... Chrome/141.0.0.0 Safari/537.36 模拟 Chrome 浏览器 UA
|
||||
Accept-Language zh-CN,zh;q=0.9 语言偏好
|
||||
sec-ch-ua "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141" 浏览器 User-Agent Client Hints
|
||||
sec-ch-ua-platform "Windows" 同上,标识操作系统平台
|
||||
sec-ch-ua-mobile ?0 同上,标识是否移动端
|
||||
sec-fetch-site same-origin Fetch Metadata,标识请求来源站点关系
|
||||
sec-fetch-mode cors Fetch Metadata,标识跨域模式
|
||||
sec-fetch-dest empty Fetch Metadata,标识资源类型
|
||||
priority u=1, i HTTP 优先级提示,用于底层网络调度
|
||||
X-Requested-With XMLHttpRequest 传统 AJAX 头部,部分后端可能用来识别异步请求
|
||||
DNT(可选) 1 Do Not Track,浏览器隐私偏好,业务逻辑不依赖
|
||||
|
||||
#### 1.4.3 示例请求头(推荐组合)
|
||||
|
||||
下面是一个推荐的完整请求头示例,供客户端实现参考(已脱敏):
|
||||
|
||||
POST /apiprod/admin/v1/AssistantPerformance/GetOrderAssistantDetails HTTP/1.1
|
||||
Host: pc.ficoo.vip
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
Accept: application/json, text/plain, */*
|
||||
Origin: https://pc.ficoo.vip
|
||||
Referer: https://pc.ficoo.vip/
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)... Chrome/141.0.0.0 Safari/537.36
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"
|
||||
sec-ch-ua-platform: "Windows"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-fetch-site: same-origin
|
||||
sec-fetch-mode: cors
|
||||
sec-fetch-dest: empty
|
||||
priority: u=1, i
|
||||
X-Requested-With: XMLHttpRequest
|
||||
DNT: 1
|
||||
|
||||
|
||||
## 2. 通用协议约定
|
||||
|
||||
### 2.1 时间格式与时区
|
||||
|
||||
脚本中所有时间字段使用统一字符串格式:
|
||||
|
||||
```text
|
||||
"YYYY-MM-DD HH:MM:SS"
|
||||
例如:"2025-10-01 00:00:00"
|
||||
```
|
||||
|
||||
常见时间字段:
|
||||
|
||||
* `startTime` / `endTime`:一般为创建时间或业务发生时间段
|
||||
* `rangeStartTime` / `rangeEndTime`:多用于结账记录等“区间查询”
|
||||
* `StartPayTime` / `EndPayTime`:支付时间范围(支付流水)
|
||||
|
||||
时区未在响应中显式标识,默认可按北京时间(Asia/Shanghai)处理。
|
||||
|
||||
### 2.2 分页约定
|
||||
|
||||
* 通用分页参数:
|
||||
|
||||
* `page`:页码,从 1 开始
|
||||
* `limit`:每页条数,例如 20 或 100
|
||||
* Python 脚本支持配置:
|
||||
|
||||
````python
|
||||
PAGE_START = 1
|
||||
PAGE_END = 2
|
||||
PAGE_LIMIT = 100
|
||||
``` :contentReference[oaicite:7]{index=7}
|
||||
|
||||
````
|
||||
* 采集策略:遍历 page 从 `PAGE_START` 到 `PAGE_END`,每页将列表数据合并写入一个 JSON 文件。
|
||||
|
||||
**特殊情况:**
|
||||
|
||||
* `小票详情`(/Order/GetOrderSettleTicketNew)本身是“单据级”明细接口,从业务上看不分页,通过 `orderSettleId` 或 `orderSettleIdList` 批量请求。脚本中单独针对该接口做了循环调用设计。
|
||||
|
||||
### 2.3 通用响应结构
|
||||
|
||||
从样本 JSON 看,多数接口返回结构类似:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"...": "...",
|
||||
"total": 438,
|
||||
"<listField>": [ ... ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
示例(会员档案,字段名略):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": 438,
|
||||
"tenantMemberInfos": [
|
||||
{
|
||||
"id": 2799...,
|
||||
"create_time": "2025-11-08 01:29:33",
|
||||
"member_card_grade_code": 1,
|
||||
"mobile": "138****1234",
|
||||
"nickname": "xxx",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
* 列表字段名称并不统一:可能是 `list` / `rows` / `records` / `items` / `dataList`,也可能是业务专有名(例如 `tenantMemberInfos`)。脚本中专门写了一个通用提取函数 `extract_list` 来处理上述几种常见命名。
|
||||
* 部分接口可能完全不分页,只返回单条对象或嵌套结构,需按实际响应处理。
|
||||
|
||||
---
|
||||
|
||||
## 3. 通用参数说明
|
||||
|
||||
脚本顶部有一个公共参数池 `PARAMS`,各个接口在构造请求时会从中按需取值。这些字段可以视为“通用过滤条件”和“业务枚举”。
|
||||
|
||||
### 3.1 门店与组织
|
||||
|
||||
* `siteId`:门店 ID(int64)
|
||||
|
||||
* 单门店场景为一个长整型;
|
||||
* 在门店商品档案 1/2 中,脚本会自动转成数组形式:`[siteId]`。
|
||||
* `siteTableAreaIdList`:台桌区域 ID 列表(array<int>),用于结账记录筛选具体区域。
|
||||
|
||||
### 3.2 时间区间
|
||||
|
||||
* `startTime` / `endTime`:通用时间范围
|
||||
* `rangeStartTime` / `rangeEndTime`:结账类接口使用
|
||||
* `StartPayTime` / `EndPayTime`:支付流水接口使用
|
||||
|
||||
全部为字符串格式 `"YYYY-MM-DD HH:MM:SS"`。
|
||||
|
||||
### 3.3 业务枚举类字段(常见)
|
||||
|
||||
以下均为整数枚举,具体含义需结合业务系统配置或你提供的 .md 报告来确定:
|
||||
|
||||
* `IsConfirm`:助教业绩是否确认(示例值:2)
|
||||
* `OnlinePayChannel`:线上支付渠道(0 表示全部 / 默认)
|
||||
* `paymentMethod`:支付方式(现金 / 微信 / 支付宝 / …)
|
||||
* `relateType`:关联类型(支付记录)
|
||||
* `isSaleManUser`:是否按业务员维度汇总台费流水
|
||||
* `isSalesBind`:门店销售记录中,是否按销售绑定过滤
|
||||
* `goodsSalesType`:商品销售维度(按单品 / 分类 / 时间等)
|
||||
* `costPriceType`:成本价类型
|
||||
* `ableDiscount`:是否可打折(-1 代表全部)
|
||||
* `tenantGoodsStatus`:商品状态(启用 / 停用)
|
||||
* `goodsSecondCategoryId`:二级分类 ID 列表
|
||||
* `goodsState`:商品状态(在售 / 已停售)
|
||||
* `enableStatus`:启用状态
|
||||
* `existsGoodsStock`:是否仅查有库存商品
|
||||
* `couponChannel`:券来源渠道
|
||||
* `couponUseStatus`:券使用状态
|
||||
* `settleType`:结账类型 / 账单类型
|
||||
* `stockType`:库存变动类型(出库 / 入库 / 盘点等)
|
||||
|
||||
### 3.4 小票详情专用字段
|
||||
|
||||
* `orderSettleId`:单张结账小票对应的结算 ID
|
||||
* `orderSettleIdList`:结算 ID 列表(脚本中大量示例 ID),支持批量拉取小票详情。
|
||||
|
||||
---
|
||||
|
||||
## 4. 批量采集脚本使用说明
|
||||
|
||||
### 4.2 Token 配置
|
||||
|
||||
支持两种方式(优先级从高到低):
|
||||
|
||||
1. 命令行参数:
|
||||
|
||||
```bash
|
||||
python fetch_feiqiu_endpoints.py --token "Bearer x.y.z"
|
||||
# 或仅传 bare token,脚本会自动补上 Bearer 前缀
|
||||
```
|
||||
|
||||
2. 也支持在同目录 `.env` 文件中设置 `FICOO_TOKEN`(依赖 `python-dotenv`)。
|
||||
|
||||
若两者都缺失,脚本会抛出异常并退出。
|
||||
|
||||
### 4.3 配置要调用的接口
|
||||
|
||||
脚本中通过 `API_NAME` 控制要调用的接口,可以是单个字符串或字符串列表:
|
||||
|
||||
```python
|
||||
# 示例:只跑门店商品档案 1 和 2
|
||||
API_NAME = ["门店商品档案1", "门店商品档案2"]
|
||||
```
|
||||
|
||||
可选值即 `ENDPOINTS` 字典中的所有键(见后文接口清单)。
|
||||
|
||||
### 4.4 分页与时间范围配置
|
||||
|
||||
在 `PARAMS` 和分页配置区集中调整:
|
||||
|
||||
````python
|
||||
PAGE_START = 1
|
||||
PAGE_END = 2
|
||||
PAGE_LIMIT = 100
|
||||
|
||||
PARAMS = {
|
||||
"startTime": "2025-10-01 00:00:00",
|
||||
"endTime": "2025-11-10 00:00:00",
|
||||
"rangeStartTime": "...",
|
||||
"rangeEndTime": "...",
|
||||
"StartPayTime": "...",
|
||||
"EndPayTime": "...",
|
||||
"siteId": 2790685415443269,
|
||||
...
|
||||
}
|
||||
``` :contentReference[oaicite:17]{index=17}
|
||||
|
||||
### 4.5 输出位置与命名
|
||||
|
||||
- 输出目录:可配置
|
||||
- 文件命名列表:
|
||||
接口 Path 与建议 JSON 文件名对照表
|
||||
|
||||
| Request Path | New JSON Filename | Brief Description |
|
||||
| ---------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------- |
|
||||
| `/MemberProfile/GetTenantMemberList` | `member_profiles.json` | 会员主档案数据,每条记录对应租户下的一位会员/账户,包括会员基本信息、等级、联系方式等。 |
|
||||
| `/MemberProfile/GetMemberCardBalanceChange` | `member_balance_changes.json` | 会员账户余额变动流水,每条记录对应一次余额增减(充值、消费、调账等)。 |
|
||||
| `/MemberProfile/GetTenantMemberCardList` | `member_stored_value_cards.json` | 会员储值类卡片列表,每条记录是一张已开通的储值卡及其规则配置(卡种、折扣、适用范围等)。 |
|
||||
| `/Site/GetRechargeSettleList` | `recharge_settlements.json` | 会员充值结算流水,每条记录是一笔充值订单的结算信息(金额、支付方式、时间等)。 |
|
||||
| `/AssistantPerformance/GetAbolitionAssistant` | `assistant_cancellation_records.json` | 助教废除/取消流水,每条记录表示某张台桌上一次助教被移除/废除的操作,含门店与台桌信息。 |
|
||||
| `/AssistantPerformance/GetOrderAssistantDetails` | `assistant_service_records.json` | 助教服务流水明细,每条记录对应一次助教服务(单次上台/服务时段),可关联订单、结账、小票等。 |
|
||||
| `/PersonnelManagement/SearchAssistantInfo` | `assistant_accounts_master.json` | 助教账号/人事档案维表,每条记录对应一名助教账号,包含人员基本信息、在职状态、计费与权限配置等。 |
|
||||
| `/Table/GetSiteTables` | `site_tables_master.json` | 门店台桌维表,每条记录对应一张球台/包厢,包含台号、区域、状态、收费策略等基础配置。 |
|
||||
| `/Site/GetTaiFeeAdjustList` | `table_fee_discount_records.json` | 台费打折/调账流水,每条记录是一笔针对台费的手工折扣或金额调整,关联订单与台桌。 |
|
||||
| `/Site/GetSiteTableOrderDetails` | `table_fee_transactions.json` | 台费计费流水明细,每条记录对应一次台费收费明细,可按订单、台桌、时长等维度分析。 |
|
||||
| `/TenantGoods/QueryTenantGoods` | `tenant_goods_master.json` | 品牌维度的商品档案,每条记录是一个商品在租户层的主数据(名称、分类、定价等),供各门店复用。 |
|
||||
| `/PackageCoupon/QueryPackageCouponList` | `group_buy_packages.json` | 团购套餐定义列表,每条记录是一种团购券/套餐的规则配置(名称、面值、有效期、适用时段等)。 |
|
||||
| `/Site/GetSiteTableUseDetails` | `group_buy_redemption_records.json` | 团购套餐使用流水,每条记录对应一次团购券在门店被核销/使用的明细,含台桌与订单信息。 |
|
||||
| `/Order/GetOrderSettleTicketNew` | `settlement_ticket_details.json` | 结账小票打印详情,每条记录对应一张结算小票的完整快照,包括整单金额、会员、支付方式及台费/商品分项明细。 |
|
||||
| `/Promotion/GetOfflineCouponConsumePageList` | `platform_coupon_redemption_records.json` | 平台券(如美团等)验券流水,每条记录对应一次第三方团购券在门店的核销事件。 |
|
||||
| `/GoodsStockManage/QueryGoodsOutboundReceipt` | `goods_stock_movements.json` | 商品库存变动流水,每条记录是某个门店商品的一次出入库/调整事件,可追溯至库存汇总与销售记录。 |
|
||||
| `/TenantGoodsCategory/QueryPrimarySecondaryCategory` | `stock_goods_category_tree.json` | 库存模块使用的商品分类树,每条记录是一个商品分类节点,形成一级/二级分类层级结构。 |
|
||||
| `/TenantGoods/GetGoodsStockReport` | `goods_stock_summary.json` | 门店商品库存汇总,每条记录对应一个门店商品在查询区间内的库存现状与汇总指标。 |
|
||||
| `/PayLog/GetPayLogListPage` | `payment_transactions.json` | 门店支付流水,每条记录是一笔支付交易(含支付方式、渠道、金额、关联订单等)。 |
|
||||
| `/Site/GetAllOrderSettleList` | `settlement_records.json` | 门店结账记录,每条记录是一笔结算单的汇总信息,可与小票详情、支付记录等联查。 |
|
||||
| `/Order/GetRefundPayLogList` | `refund_transactions.json` | 退款支付流水,每条记录是一笔已发生的退款交易,包含退款金额、业务来源、支付通道等。 |
|
||||
| `/TenantGoods/GetGoodsSalesList` | `store_goods_sales_records.json` | 门店商品销售流水,每条记录对应一条商品销售明细,可关联支付记录、结账记录与库存变动。 |
|
||||
|
||||
|
||||
* 日志目录:可配置,按接口名单独记录一份日志,包含每次调用的 curl 命令和原始请求/响应快照(Authorization 可选择打码)。
|
||||
|
||||
### 4.6 调用重试与限流策略
|
||||
|
||||
脚本中使用了 `requests.adapters.HTTPAdapter` + `urllib3.Retry`:
|
||||
|
||||
* 重试总次数:3
|
||||
* 指定的可重试状态码:`429, 500, 502, 503, 504`
|
||||
* 退避因子:0.5(指数退避)
|
||||
* 每次请求后随机 `1–4` 秒延迟,减轻服务器压力。
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口清单总览
|
||||
|
||||
以下均为 `POST` 接口,路径相对于 Base URL。
|
||||
|
||||
| 模块 | 接口名称 | Path | 是否分页(按脚本设计) |
|
||||
| ------- | ------- | ---------------------------------------------------- | ----------------- |
|
||||
| 会员 | 会员档案 | `/MemberProfile/GetTenantMemberList` | 是(`page`,`limit`) |
|
||||
| 会员 | 余额变更记录 | `/MemberProfile/GetMemberCardBalanceChange` | 是 |
|
||||
| 会员 | 储值卡列表 | `/MemberProfile/GetTenantMemberCardList` | 是 |
|
||||
| 会员 / 充值 | 充值记录 | `/Site/GetRechargeSettleList` | 是 |
|
||||
| 助教 | 助教废除 | `/AssistantPerformance/GetAbolitionAssistant` | 是 |
|
||||
| 助教 | 助教流水 | `/AssistantPerformance/GetOrderAssistantDetails` | 是 |
|
||||
| 助教 | 助教账号1 | `/PersonnelManagement/SearchAssistantInfo` | 是 |
|
||||
| 助教 | 助教账号2 | `/PersonnelManagement/SearchAssistantInfo` | 是 |
|
||||
| 台桌 | 台桌列表 | `/Table/GetSiteTables` | 是 |
|
||||
| 台费 | 台费打折 | `/Site/GetTaiFeeAdjustList` | 是 |
|
||||
| 台费 | 台费流水 | `/Site/GetSiteTableOrderDetails` | 是 |
|
||||
| 商品 | 商品档案 | `/TenantGoods/QueryTenantGoods` | 是 |
|
||||
| 团购 | 团购套餐 | `/PackageCoupon/QueryPackageCouponList` | 是 |
|
||||
| 团购 | 团购套餐流水 | `/Site/GetSiteTableUseDetails` | 是 |
|
||||
| 订单 | 小票详情 | `/Order/GetOrderSettleTicketNew` | 否(单笔/批量按 ID) |
|
||||
| 营销 / 券 | 平台验券记录 | `/Promotion/GetOfflineCouponConsumePageList` | 是 |
|
||||
| 库存 | 库存变化记录1 | `/GoodsStockManage/QueryGoodsOutboundReceipt` | 是 |
|
||||
| 库存 | 库存变化记录2 | `/TenantGoodsCategory/QueryPrimarySecondaryCategory` | 是 |
|
||||
| 库存 | 库存汇总 | `/TenantGoods/GetGoodsStockReport` | 是 |
|
||||
| 支付 | 支付记录 | `/PayLog/GetPayLogListPage` | 是 |
|
||||
| 结账 | 结账记录 | `/Site/GetAllOrderSettleList` | 是 |
|
||||
| 订单 | 退款记录 | `/Order/GetRefundPayLogList` | 是 |
|
||||
| 商品 / 门店 | 门店商品档案1 | `/TenantGoods/GetGoodsInventoryList` | 是 |
|
||||
| 商品 / 销售 | 门店销售记录 | `/TenantGoods/GetGoodsSalesList` | 是 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 接口详情(参数级,按模块整理)
|
||||
|
||||
下面只写“请求参数层面”的反推说明;响应字段请结合etl_billiards/tests/source-data-doc目录下的 `.json` + `.md` 报告使用。
|
||||
|
||||
### 6.1 助教相关接口
|
||||
|
||||
#### 6.1.1 助教流水 – GetOrderAssistantDetails
|
||||
|
||||
* 中文名称:助教流水
|
||||
* Path:`/AssistantPerformance/GetOrderAssistantDetails`
|
||||
* Method:POST
|
||||
* 是否分页:是
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ----------------------------- |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `startTime` | string | 是 | 起始时间(含),`YYYY-MM-DD HH:MM:SS` |
|
||||
| `endTime` | string | 是 | 结束时间(含) |
|
||||
| `IsConfirm` | int | 否 | 业绩确认状态(枚举,具体含义待确认) |
|
||||
| `page` | int | 是 | 页码,从 1 开始 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
|
||||
#### 6.1.2 助教废除 – GetAbolitionAssistant
|
||||
|
||||
* Path:`/AssistantPerformance/GetAbolitionAssistant`
|
||||
* Method:POST
|
||||
* 是否分页:是
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ----- |
|
||||
| `startTime` | string | 是 | 起始时间 |
|
||||
| `endTime` | string | 是 | 结束时间 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.1.3 助教账号1 / 2 – SearchAssistantInfo
|
||||
|
||||
* Path:`/PersonnelManagement/SearchAssistantInfo`
|
||||
* 两个名称(助教账号1/2)仅是脚本中拆分;实际路径一致,可以视为不同筛选组合。
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ----- | -- | ------------ |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `workStatusEnum` | int | 否 | 在职 / 离职状态 |
|
||||
| `dingTalkSynced` | int | 否 | 是否同步钉钉 |
|
||||
| `leaveId` | int | 否 | 请假状态/ID(待确认) |
|
||||
| `criticismStatus` | int | 否 | 处分/批评状态(待确认) |
|
||||
| `signStatus` | int | 否 | 签到状态(待确认) |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
### 6.2 台桌 / 台费接口
|
||||
|
||||
#### 6.2.1 台桌列表 – GetSiteTables
|
||||
|
||||
* Path:`/Table/GetSiteTables`
|
||||
* Method:POST
|
||||
* 是否分页:是
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------------------ | --- | -- | ------------ |
|
||||
| `showStatus` | int | 否 | 展示状态(启用/停用等) |
|
||||
| `virtualTableType` | int | 否 | 虚拟台桌类型 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.2.2 台费流水 – GetSiteTableOrderDetails
|
||||
|
||||
* Path:`/Site/GetSiteTableOrderDetails`
|
||||
* Method:POST
|
||||
* 是否分页:是
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --------------- | ------ | -- | ----------- |
|
||||
| `startTime` | string | 是 | 起始时间 |
|
||||
| `endTime` | string | 是 | 结束时间 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `isSaleManUser` | int | 否 | 是否按销售人员维度过滤 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.2.3 台费打折 – GetTaiFeeAdjustList
|
||||
|
||||
* Path:`/Site/GetTaiFeeAdjustList`
|
||||
* Method:POST
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ----- |
|
||||
| `startTime` | string | 是 | 起始时间 |
|
||||
| `endTime` | string | 是 | 结束时间 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
### 6.3 会员相关接口
|
||||
|
||||
#### 6.3.1 会员档案 – GetTenantMemberList
|
||||
|
||||
* Path:`/MemberProfile/GetTenantMemberList`
|
||||
* Method:POST
|
||||
* 是否分页:是
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --------------------- | --- | -- | --------- |
|
||||
| `isMemberInBlackList` | int | 否 | 是否黑名单 |
|
||||
| `status_Revoked` | int | 否 | 会员状态(注销等) |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
> 备注:样本 JSON 显示列表字段为 `tenantMemberInfos`,包含 `id`、`system_member_id`、`member_card_grade_code` 等字段,具体字段含义详见对应 `.md` 报告。
|
||||
|
||||
#### 6.3.2 余额变更记录 – GetMemberCardBalanceChange
|
||||
|
||||
* Path:`/MemberProfile/GetMemberCardBalanceChange`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ------ |
|
||||
| `startTime` | string | 是 | 变更起始时间 |
|
||||
| `endTime` | string | 是 | 变更结束时间 |
|
||||
| `fromType` | int | 否 | 变更来源类型 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.3.3 储值卡列表 – GetTenantMemberCardList
|
||||
|
||||
* Path:`/MemberProfile/GetTenantMemberCardList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ----- | -- | ----- |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `cardPhysicsType` | int | 否 | 卡实体类型 |
|
||||
| `status` | int | 否 | 卡状态 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.3.4 充值记录 – GetRechargeSettleList
|
||||
|
||||
* Path:`/Site/GetRechargeSettleList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | -- | ------------ |
|
||||
| `settleType` | int | 否 | 结账/充值类型 |
|
||||
| `paymentMethod` | int | 否 | 支付方式 |
|
||||
| `rangeStartTime` | string | 是 | 充值时间起 |
|
||||
| `rangeEndTime` | string | 是 | 充值时间止 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `isFirst` | int | 否 | 是否首单/首充(待确认) |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
### 6.4 支付 / 结账 / 订单接口
|
||||
|
||||
#### 6.4.1 支付记录 – GetPayLogListPage
|
||||
|
||||
* Path:`/PayLog/GetPayLogListPage`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------------------ | ------ | -- | ----- |
|
||||
| `StartPayTime` | string | 是 | 支付时间起 |
|
||||
| `EndPayTime` | string | 是 | 支付时间止 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `OnlinePayChannel` | int | 否 | 支付渠道 |
|
||||
| `paymentMethod` | int | 否 | 支付方式 |
|
||||
| `relateType` | int | 否 | 关联类型 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.4.2 结账记录 – GetAllOrderSettleList
|
||||
|
||||
* Path:`/Site/GetAllOrderSettleList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --------------------- | ---------- | -- | -------- |
|
||||
| `settleType` | int | 否 | 结账类型 |
|
||||
| `rangeStartTime` | string | 是 | 结账时间起 |
|
||||
| `rangeEndTime` | string | 是 | 结账时间止 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `siteTableAreaIdList` | array<int> | 否 | 台桌区域ID列表 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.4.3 退款记录 – GetRefundPayLogList
|
||||
|
||||
* Path:`/Order/GetRefundPayLogList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ----- |
|
||||
| `startTime` | string | 是 | 退款时间起 |
|
||||
| `endTime` | string | 是 | 退款时间止 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.4.4 小票详情 – GetOrderSettleTicketNew
|
||||
|
||||
* Path:`/Order/GetOrderSettleTicketNew`
|
||||
* Method:POST
|
||||
* 设计上非分页接口,每次查询一个 `orderSettleId`,但可以在客户端循环批量调用。
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| --------------- | ----- | -- | ------------ |
|
||||
| `orderSettleId` | int64 | 是 | 结算单 ID(单个小票) |
|
||||
|
||||
脚本级批量用法:遍历 `orderSettleIdList`,每个 ID 调用一次该接口,将结果追加到数组中再保存。
|
||||
|
||||
### 6.5 商品 / 库存 / 销售接口
|
||||
|
||||
#### 6.5.1 商品档案 – QueryTenantGoods
|
||||
|
||||
* Path:`/TenantGoods/QueryTenantGoods`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | --- | -- | -------------- |
|
||||
| `costPriceType` | int | 否 | 成本价类型 |
|
||||
| `ableDiscount` | int | 否 | 是否可打折(-1 表示全部) |
|
||||
| `tenantGoodsStatus` | int | 否 | 商品状态 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.5.2 门店商品档案1 – GetGoodsInventoryList
|
||||
|
||||
* Path:`/TenantGoods/GetGoodsInventoryList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------------ | -- | ------------------------- |
|
||||
| `goodsSecondCategoryId` | array<int> | 否 | 二级分类 ID |
|
||||
| `goodsState` | int | 否 | 商品状态 |
|
||||
| `enableStatus` | int | 否 | 启用状态 |
|
||||
| `siteId` | array<int64> | 是 | 门店 ID 列表(脚本会把单个 ID 包装为数组) |
|
||||
| `existsGoodsStock` | int | 否 | 是否仅查有库存 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
|
||||
#### 6.5.4 门店销售记录 – GetGoodsSalesList
|
||||
|
||||
* Path:`/TenantGoods/GetGoodsSalesList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------ | -- | -------------- |
|
||||
| `isSalesBind` | int | 否 | 是否按销售绑定过滤 |
|
||||
| `startTime` | string | 是 | 销售时间起 |
|
||||
| `endTime` | string | 是 | 销售时间止 |
|
||||
| `goodsSalesType` | int | 否 | 销售维度(按单品 / 分类) |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.5.5 库存变化记录1 – QueryGoodsOutboundReceipt
|
||||
|
||||
* Path:`/GoodsStockManage/QueryGoodsOutboundReceipt`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ------ |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `stockType` | int | 否 | 库存变动类型 |
|
||||
| `startTime` | string | 是 | 变动时间起 |
|
||||
| `endTime` | string | 是 | 变动时间止 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.5.6 库存变化记录2 – QueryPrimarySecondaryCategory
|
||||
|
||||
* Path:`/TenantGoodsCategory/QueryPrimarySecondaryCategory`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ------- |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `startTime` | string | 否 | 时间起(如有) |
|
||||
| `endTime` | string | 否 | 时间止(如有) |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.5.7 库存汇总 – GetGoodsStockReport
|
||||
|
||||
* Path:`/TenantGoods/GetGoodsStockReport`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | -- | ----- |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `startTime` | string | 否 | 时间过滤起 |
|
||||
| `endTime` | string | 否 | 时间过滤止 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
### 6.6 团购 / 验券接口
|
||||
|
||||
#### 6.6.1 团购套餐 – QueryPackageCouponList
|
||||
|
||||
* Path:`/PackageCoupon/QueryPackageCouponList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | --- | -- | ----- |
|
||||
| `areaId` | int | 否 | 区域 ID |
|
||||
| `commonShowStatus` | int | 否 | 展示状态 |
|
||||
| `offlineCouponChannel` | int | 否 | 券渠道 |
|
||||
| `systemGroupType` | int | 否 | 套餐类型 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.6.2 团购套餐流水 – GetSiteTableUseDetails
|
||||
|
||||
* Path:`/Site/GetSiteTableUseDetails`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | -- | ----- |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `offlineCouponChannel` | int | 否 | 券渠道 |
|
||||
| `startTime` | string | 是 | 使用时间起 |
|
||||
| `endTime` | string | 是 | 使用时间止 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
#### 6.6.3 平台验券记录 – GetOfflineCouponConsumePageList
|
||||
|
||||
* Path:`/Promotion/GetOfflineCouponConsumePageList`
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | -- | ----- |
|
||||
| `couponChannel` | int | 否 | 券渠道 |
|
||||
| `startTime` | string | 是 | 验券时间起 |
|
||||
| `endTime` | string | 是 | 验券时间止 |
|
||||
| `siteId` | int64 | 是 | 门店 ID |
|
||||
| `couponUseStatus` | int | 否 | 券使用状态 |
|
||||
| `page` | int | 是 | 页码 |
|
||||
| `limit` | int | 是 | 每页条数 |
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user