feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系
## P1 数据库基础 - zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu - etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表 - 清理 assistant_abolish 残留数据 ## P2 ETL/DWS 扩展 - 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution) - 新增 assistant_order_contribution_task 任务及 RLS 视图 - member_consumption 增加充值字段、assistant_daily 增加处罚字段 - 更新 ODS/DWD/DWS 任务文档及业务规则文档 - 更新 consistency_checker、flow_runner、task_registry 等核心模块 ## P3 小程序鉴权系统 - 新增 xcx_auth 路由/schema(微信登录 + JWT) - 新增 wechat/role/matching/application 服务层 - zqyy_app 鉴权表迁移 + 角色权限种子数据 - auth/dependencies.py 支持小程序 JWT 鉴权 ## 文档与审计 - 新增 DOCUMENTATION-MAP 文档导航 - 新增 7 份 BD_Manual 数据库变更文档 - 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth) - 新增全栈集成审计记录、部署检查清单更新 - 新增 BACKLOG 路线图、FDW→Core 迁移计划 ## Kiro 工程化 - 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务) - 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan) - 新增 6 个 Hook(合规检查/会话日志/提交审计等) - 新增 doc-map steering 文件 ## 运维与测试 - 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告 - 新增属性测试:test_dws_contribution / test_auth_system - 清理过期 export 报告文件 - 更新 .gitignore 排除规则
This commit is contained in:
1
.kiro/specs/02-etl-dws-miniapp-extensions/.config.kiro
Normal file
1
.kiro/specs/02-etl-dws-miniapp-extensions/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
562
.kiro/specs/02-etl-dws-miniapp-extensions/design.md
Normal file
562
.kiro/specs/02-etl-dws-miniapp-extensions/design.md
Normal file
@@ -0,0 +1,562 @@
|
||||
# 设计文档:ETL DWS 层扩展 — 小程序数据支撑
|
||||
|
||||
## 概述
|
||||
|
||||
本设计覆盖三个独立但相关的 DWS 层扩展模块:
|
||||
|
||||
1. **助教订单流水四项统计**:新建 `AssistantOrderContributionTask`,计算每名助教每日的订单总流水、订单净流水、时效贡献流水、时效净贡献。算法核心在于"时效贡献流水"的台费分摊和酒水食品均分逻辑。
|
||||
2. **会员消费汇总扩展**:修改现有 `MemberConsumptionTask`,新增 30/60/90 天充值窗口统计和次均消费字段。
|
||||
3. **定档折算惩罚**:修改现有 `AssistantDailyTask`,新增时间重叠检测和惩罚计算逻辑。
|
||||
|
||||
三个模块共享同一套 RLS 视图 + FDW 映射基础设施。
|
||||
|
||||
### 设计决策
|
||||
|
||||
1. **助教订单流水独立建表**:四项统计粒度为 `(site_id, assistant_id, stat_date)`,与现有 `dws_assistant_daily_detail` 粒度相同但语义不同(daily_detail 聚焦服务时长/金额,contribution 聚焦订单级流水分摊),独立建表避免字段膨胀。
|
||||
2. **时效贡献流水计算为纯函数**:核心分摊算法(`compute_time_weighted_revenue`)设计为静态方法,输入为结构化的订单数据,输出为每名助教的贡献值。不依赖数据库,便于属性测试。
|
||||
3. **惩罚检测在 transform 阶段完成**:定档折算惩罚的时间重叠检测和计算在 `AssistantDailyTask.transform` 中完成,不新建独立任务,因为惩罚字段与日度明细同粒度。
|
||||
4. **充值统计复用现有 extract 模式**:在 `MemberConsumptionTask` 中新增一个 `_extract_recharge_stats` 方法,与现有的 `_extract_consumption_stats` 并行提取,在 transform 阶段合并。
|
||||
|
||||
## 架构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 数据来源(DWD)
|
||||
SH[dwd_settlement_head<br/>结算主表]
|
||||
TF[dwd_table_fee_log<br/>台费明细]
|
||||
ASL[dwd_assistant_service_log<br/>助教服务记录]
|
||||
RO[dwd_recharge_order<br/>充值订单]
|
||||
end
|
||||
|
||||
subgraph 新建任务
|
||||
AOC[AssistantOrderContributionTask<br/>助教订单流水统计]
|
||||
end
|
||||
|
||||
subgraph 修改任务
|
||||
MCT[MemberConsumptionTask<br/>+充值窗口 +次均消费]
|
||||
ADT[AssistantDailyTask<br/>+惩罚检测 +惩罚计算]
|
||||
end
|
||||
|
||||
subgraph 输出(DWS)
|
||||
T1[dws_assistant_order_contribution<br/>新建]
|
||||
T2[dws_member_consumption_summary<br/>扩展字段]
|
||||
T3[dws_assistant_daily_detail<br/>扩展字段]
|
||||
end
|
||||
|
||||
subgraph 基础设施
|
||||
RLS[app schema RLS 视图]
|
||||
FDW[fdw_etl 外部表映射]
|
||||
end
|
||||
|
||||
SH --> AOC
|
||||
TF --> AOC
|
||||
ASL --> AOC
|
||||
AOC --> T1
|
||||
|
||||
RO --> MCT
|
||||
SH --> MCT
|
||||
MCT --> T2
|
||||
|
||||
ASL --> ADT
|
||||
TF --> ADT
|
||||
ADT --> T3
|
||||
|
||||
T1 --> RLS
|
||||
T2 --> RLS
|
||||
T3 --> RLS
|
||||
RLS --> FDW
|
||||
```
|
||||
|
||||
### 任务依赖关系
|
||||
|
||||
```
|
||||
DWD_LOAD_FROM_ODS
|
||||
├── DWS_ASSISTANT_DAILY (扩展:+惩罚检测计算)
|
||||
├── DWS_MEMBER_CONSUMPTION (扩展:+充值窗口+次均消费)
|
||||
└── DWS_ASSISTANT_ORDER_CONTRIBUTION (新建:四项统计)
|
||||
```
|
||||
|
||||
`DWS_ASSISTANT_ORDER_CONTRIBUTION` 依赖 `DWD_LOAD_FROM_ODS`(需要最新的结算、台费、服务记录数据)。
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### AssistantOrderContributionTask(新建)
|
||||
|
||||
继承 `BaseDwsTask`,实现四项统计计算:
|
||||
|
||||
```python
|
||||
class AssistantOrderContributionTask(BaseDwsTask):
|
||||
DATE_COL = "stat_date"
|
||||
|
||||
def get_task_code(self) -> str:
|
||||
return "DWS_ASSISTANT_ORDER_CONTRIBUTION"
|
||||
|
||||
def get_target_table(self) -> str:
|
||||
return "dws_assistant_order_contribution"
|
||||
|
||||
def get_primary_keys(self) -> List[str]:
|
||||
return ["site_id", "assistant_id", "stat_date"]
|
||||
|
||||
# --- ETL 主流程 ---
|
||||
def extract(self, context: TaskContext) -> Dict[str, Any]: ...
|
||||
def transform(self, extracted, context) -> List[Dict[str, Any]]: ...
|
||||
# load() 使用 BaseDwsTask 默认实现
|
||||
|
||||
# --- 数据提取 ---
|
||||
def _extract_order_data(self, site_id, start_date, end_date) -> List[Dict]: ...
|
||||
|
||||
# --- 核心计算(纯函数,可独立测试) ---
|
||||
@staticmethod
|
||||
def compute_order_gross_revenue(order: OrderData) -> Decimal:
|
||||
"""订单总流水 = 台费 + 酒水食品 + 所有助教服务费"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def compute_order_net_revenue(order: OrderData) -> Decimal:
|
||||
"""订单净流水 = 订单总流水 - 所有助教服务分成"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def compute_time_weighted_revenue(
|
||||
order: OrderData, assistant_id: int
|
||||
) -> Decimal:
|
||||
"""时效贡献流水 = 台费按时长分摊 + 个人服务费 + 酒水食品按时长比例"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def compute_time_weighted_net_revenue(
|
||||
time_weighted_revenue: Decimal, assistant_commission: Decimal
|
||||
) -> Decimal:
|
||||
"""时效净贡献 = 时效贡献流水 - 个人服务分成"""
|
||||
...
|
||||
```
|
||||
|
||||
### 核心数据结构
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TableUsage:
|
||||
"""台桌使用信息"""
|
||||
table_id: int
|
||||
table_area: str # 区域名称(A/B/C/S/TV/M1-M7 等)
|
||||
usage_seconds: int # 台桌使用时长(秒)
|
||||
table_fee: Decimal # 台费/房费
|
||||
|
||||
@dataclass
|
||||
class AssistantService:
|
||||
"""助教服务记录"""
|
||||
assistant_id: int
|
||||
table_id: int
|
||||
service_seconds: int # 服务时长(秒)
|
||||
ledger_amount: Decimal # 服务流水(助教收费)
|
||||
commission: Decimal # 助教分成
|
||||
skill_id: int
|
||||
course_type: str # BASE / BONUS / ROOM
|
||||
|
||||
@dataclass
|
||||
class OrderData:
|
||||
"""订单聚合数据(一个结算单的完整信息)"""
|
||||
order_settle_id: int
|
||||
site_id: int
|
||||
total_table_fee: Decimal # 台费总额
|
||||
total_goods_amount: Decimal # 酒水食品总额
|
||||
tables: List[TableUsage] # 台桌列表
|
||||
assistants: List[AssistantService] # 助教服务列表
|
||||
```
|
||||
|
||||
### 四项统计算法详解
|
||||
|
||||
#### 1. 订单总流水(order_gross_revenue)
|
||||
|
||||
```
|
||||
order_gross_revenue = total_table_fee + total_goods_amount + SUM(所有助教的 ledger_amount)
|
||||
```
|
||||
|
||||
每个参与助教获得相同的 order_gross_revenue 值。
|
||||
|
||||
#### 2. 订单净流水(order_net_revenue)
|
||||
|
||||
```
|
||||
order_net_revenue = order_gross_revenue - SUM(所有助教的 commission)
|
||||
```
|
||||
|
||||
每个参与助教获得相同的 order_net_revenue 值。
|
||||
|
||||
#### 3. 时效贡献流水(time_weighted_revenue)— 核心算法
|
||||
|
||||
这是最复杂的计算,按以下步骤进行:
|
||||
|
||||
**步骤 1:确定每张台桌的有效计费时长**
|
||||
```
|
||||
对于每张台桌 t:
|
||||
助教总服务时长 = SUM(该台桌所有助教的 service_seconds)
|
||||
有效计费时长 = MAX(助教总服务时长, 台桌使用时长)
|
||||
每小时单价 = table_fee / (有效计费时长 / 3600)
|
||||
```
|
||||
|
||||
**步骤 2:按助教在台桌的服务时长分摊台费**
|
||||
```
|
||||
对于每个助教 a 在台桌 t:
|
||||
台费分摊 = 每小时单价 × (助教在该台桌的服务时长 / 3600)
|
||||
|
||||
特殊情况:当助教总服务时长 < 台桌使用时长时
|
||||
按比例缩放:台费分摊 = (table_fee × 台桌使用时长对应比例) / 该台桌助教人数中按时长占比分配
|
||||
即:台费分摊 = (table_fee / 台桌使用时长 × MIN(助教总服务时长, 台桌使用时长))
|
||||
× (助教个人时长 / 助教总服务时长)
|
||||
```
|
||||
|
||||
更精确的公式(统一处理两种情况):
|
||||
```
|
||||
对于台桌 t:
|
||||
billable_seconds = MAX(SUM(助教服务时长), 台桌使用时长)
|
||||
对于助教 a:
|
||||
台费分摊_a = table_fee_t × (service_seconds_a / billable_seconds)
|
||||
```
|
||||
|
||||
> 注意:当 `SUM(助教服务时长) > 台桌使用时长` 时,`billable_seconds = SUM(助教服务时长)`,
|
||||
> 此时各助教按自己的时长占比分摊台费,总和 = table_fee。
|
||||
> 当 `SUM(助教服务时长) < 台桌使用时长` 时,`billable_seconds = 台桌使用时长`,
|
||||
> 此时各助教分摊的台费总和 < table_fee(未被助教覆盖的时段不分配给任何人)。
|
||||
|
||||
**步骤 3:助教个人服务费直接计入**
|
||||
```
|
||||
个人服务费 = 助教的 ledger_amount
|
||||
```
|
||||
|
||||
**步骤 4:酒水食品按助教总时长比例均分**
|
||||
```
|
||||
助教总时长 = SUM(所有助教在所有台桌的 service_seconds)
|
||||
对于助教 a:
|
||||
酒水食品分摊 = total_goods_amount × (助教 a 的总服务时长 / 助教总时长)
|
||||
```
|
||||
|
||||
**合成:**
|
||||
```
|
||||
time_weighted_revenue_a = SUM(各台桌台费分摊_a) + 个人服务费_a + 酒水食品分摊_a
|
||||
```
|
||||
|
||||
#### 4. 时效净贡献(time_weighted_net_revenue)
|
||||
|
||||
```
|
||||
time_weighted_net_revenue_a = time_weighted_revenue_a - commission_a
|
||||
```
|
||||
|
||||
#### 5. 超休/打赏课特殊处理
|
||||
|
||||
当助教为超休/打赏课类型(`course_type = BONUS`)时,该助教不参与订单级分摊:
|
||||
```
|
||||
order_gross_revenue = ledger_amount(个人服务流水)
|
||||
order_net_revenue = ledger_amount - commission
|
||||
time_weighted_revenue = ledger_amount
|
||||
time_weighted_net_revenue = ledger_amount - commission
|
||||
```
|
||||
|
||||
### MemberConsumptionTask 扩展
|
||||
|
||||
在现有任务中新增:
|
||||
|
||||
```python
|
||||
# extract 阶段新增
|
||||
def _extract_recharge_stats(self, site_id: int, stat_date: date) -> Dict[int, Dict]:
|
||||
"""从 dwd_recharge_order 提取 30/60/90 天充值统计"""
|
||||
...
|
||||
|
||||
# transform 阶段新增字段
|
||||
record['recharge_count_30d'] = recharge.get('count_30d', 0)
|
||||
record['recharge_count_60d'] = recharge.get('count_60d', 0)
|
||||
record['recharge_count_90d'] = recharge.get('count_90d', 0)
|
||||
record['recharge_amount_30d'] = recharge.get('amount_30d', Decimal('0'))
|
||||
record['recharge_amount_60d'] = recharge.get('amount_60d', Decimal('0'))
|
||||
record['recharge_amount_90d'] = recharge.get('amount_90d', Decimal('0'))
|
||||
record['avg_ticket_amount'] = (
|
||||
record['total_consume_amount'] / max(record['total_visit_count'], 1)
|
||||
)
|
||||
```
|
||||
|
||||
### AssistantDailyTask 扩展 — 惩罚检测
|
||||
|
||||
在现有任务的 transform 阶段新增惩罚检测逻辑:
|
||||
|
||||
```python
|
||||
# 惩罚检测核心逻辑
|
||||
@staticmethod
|
||||
def detect_overlap_violations(
|
||||
service_records: List[Dict],
|
||||
penalty_areas: Set[str]
|
||||
) -> Dict[Tuple[int, date], List[Dict]]:
|
||||
"""
|
||||
检测同一台桌同一时间段超过 2 名助教挂台的违规。
|
||||
|
||||
penalty_areas: 指定区域集合,如 {'A','B','C','S','TV','M1','M2',...,'M7'}
|
||||
|
||||
返回: {(assistant_id, stat_date): [violation_info, ...]}
|
||||
"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def compute_penalty_minutes(
|
||||
actual_minutes: Decimal,
|
||||
per_hour_contribution: Decimal,
|
||||
threshold: Decimal = Decimal('24')
|
||||
) -> Decimal:
|
||||
"""
|
||||
计算惩罚分钟数。
|
||||
|
||||
per_hour_contribution >= threshold: 返回 0
|
||||
per_hour_contribution < threshold:
|
||||
返回 actual_minutes × (1 - per_hour_contribution / threshold)
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
**惩罚区域定义:**
|
||||
- 大厅:A、B、C、S、TV
|
||||
- 麻将房:M1、M2、M3、M4、M5、M6、M7
|
||||
|
||||
**时间重叠检测算法:**
|
||||
1. 按 `(台桌ID, 服务日期)` 分组所有服务记录
|
||||
2. 对每组内的服务记录,检查时间段是否有重叠(任意两个助教的 `[start_time, end_time]` 有交集)
|
||||
3. 若同一时间段内助教数 > 2,标记为违规
|
||||
4. 对违规记录计算 `per_hour_contribution = 台费每小时单价 / 该时段助教人数`
|
||||
5. 根据 `per_hour_contribution` 与 24 元阈值比较,计算 `penalty_minutes`
|
||||
|
||||
## 数据模型
|
||||
|
||||
### dws.dws_assistant_order_contribution(新建)
|
||||
|
||||
```sql
|
||||
CREATE TABLE dws.dws_assistant_order_contribution (
|
||||
contribution_id BIGSERIAL PRIMARY KEY,
|
||||
site_id INTEGER NOT NULL,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
assistant_id BIGINT NOT NULL,
|
||||
assistant_nickname VARCHAR(100),
|
||||
stat_date DATE NOT NULL,
|
||||
|
||||
-- 四项统计
|
||||
order_gross_revenue NUMERIC(14,2) DEFAULT 0,
|
||||
order_net_revenue NUMERIC(14,2) DEFAULT 0,
|
||||
time_weighted_revenue NUMERIC(14,2) DEFAULT 0,
|
||||
time_weighted_net_revenue NUMERIC(14,2) DEFAULT 0,
|
||||
|
||||
-- 辅助字段
|
||||
order_count INTEGER DEFAULT 0,
|
||||
total_service_seconds INTEGER DEFAULT 0,
|
||||
|
||||
-- 元数据
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_aoc_site_assistant_date
|
||||
ON dws.dws_assistant_order_contribution (site_id, assistant_id, stat_date);
|
||||
|
||||
CREATE INDEX idx_aoc_stat_date
|
||||
ON dws.dws_assistant_order_contribution (site_id, stat_date);
|
||||
```
|
||||
|
||||
### dws_member_consumption_summary 扩展字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE dws.dws_member_consumption_summary
|
||||
ADD COLUMN IF NOT EXISTS recharge_count_30d INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS recharge_count_60d INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS recharge_count_90d INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS recharge_amount_30d NUMERIC(14,2) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS recharge_amount_60d NUMERIC(14,2) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS recharge_amount_90d NUMERIC(14,2) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS avg_ticket_amount NUMERIC(14,2) DEFAULT 0;
|
||||
```
|
||||
|
||||
### dws_assistant_daily_detail 扩展字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE dws.dws_assistant_daily_detail
|
||||
ADD COLUMN IF NOT EXISTS penalty_minutes NUMERIC(10,2) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS penalty_reason TEXT,
|
||||
ADD COLUMN IF NOT EXISTS is_exempt BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS per_hour_contribution NUMERIC(14,2);
|
||||
```
|
||||
|
||||
### RLS 视图(app schema)
|
||||
|
||||
```sql
|
||||
-- 新建:助教订单流水统计
|
||||
CREATE OR REPLACE VIEW app.v_dws_assistant_order_contribution AS
|
||||
SELECT * FROM dws.dws_assistant_order_contribution
|
||||
WHERE site_id = current_setting('app.current_site_id')::bigint;
|
||||
|
||||
-- 已有视图无需修改:dws_member_consumption_summary 和 dws_assistant_daily_detail
|
||||
-- 的 RLS 视图使用 SELECT *,新增字段自动包含
|
||||
```
|
||||
|
||||
### FDW 映射(fdw_etl schema)
|
||||
|
||||
在 `test_zqyy_app.fdw_etl` 中新建外部表:
|
||||
|
||||
```sql
|
||||
CREATE FOREIGN TABLE fdw_etl.dws_assistant_order_contribution (
|
||||
contribution_id BIGINT,
|
||||
site_id INTEGER,
|
||||
tenant_id INTEGER,
|
||||
assistant_id BIGINT,
|
||||
assistant_nickname VARCHAR(100),
|
||||
stat_date DATE,
|
||||
order_gross_revenue NUMERIC(14,2),
|
||||
order_net_revenue NUMERIC(14,2),
|
||||
time_weighted_revenue NUMERIC(14,2),
|
||||
time_weighted_net_revenue NUMERIC(14,2),
|
||||
order_count INTEGER,
|
||||
total_service_seconds INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE
|
||||
) SERVER etl_server
|
||||
OPTIONS (schema_name 'app', table_name 'v_dws_assistant_order_contribution');
|
||||
```
|
||||
|
||||
对于扩展字段的表(`dws_member_consumption_summary`、`dws_assistant_daily_detail`),需要 `DROP` 并重建 FDW 外部表定义以包含新字段。
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*正确性属性(Correctness Property)是系统在所有合法执行路径上都应成立的行为特征——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
以下属性基于需求文档中的验收标准推导。四项统计的核心计算函数(`compute_order_gross_revenue`、`compute_time_weighted_revenue` 等)和惩罚计算函数(`compute_penalty_minutes`)设计为纯静态方法,不依赖数据库,可直接用于属性测试。
|
||||
|
||||
### Property 1: 订单级统计不变量 — gross/net 各助教相等
|
||||
|
||||
*For any* 订单数据(包含任意数量的台桌、助教服务和酒水食品),所有参与该订单的助教应获得相同的 `order_gross_revenue` 值,且获得相同的 `order_net_revenue` 值。
|
||||
|
||||
推导:`order_gross_revenue` 和 `order_net_revenue` 是订单级聚合值,不按助教个人拆分,因此所有参与助教共享同一个值。
|
||||
|
||||
**Validates: Requirements 2.2, 2.3, 10.1, 10.2**
|
||||
|
||||
### Property 2: 时效贡献流水之和约束
|
||||
|
||||
*For any* 订单数据,所有参与助教的 `time_weighted_revenue` 之和应满足:
|
||||
- 当所有台桌的助教总服务时长 ≥ 台桌使用时长时,之和 = `order_gross_revenue`
|
||||
- 当存在台桌的助教总服务时长 < 台桌使用时长时,之和 ≤ `order_gross_revenue`
|
||||
|
||||
且在所有情况下,之和 ≥ 0。
|
||||
|
||||
推导:台费按时长比例分摊,当助教完全覆盖台桌时长时分摊总和等于台费;酒水食品按时长比例均分总和等于酒水总额;助教服务费直接计入。因此总和 = 台费分摊总和 + 酒水分摊总和 + 服务费总和 ≤ order_gross_revenue。
|
||||
|
||||
**Validates: Requirements 2.4, 10.3**
|
||||
|
||||
### Property 3: 时效净贡献减法关系
|
||||
|
||||
*For any* 助教和订单数据,该助教的 `time_weighted_net_revenue` 应等于 `time_weighted_revenue - commission`(该助教个人的服务分成)。
|
||||
|
||||
推导:这是定义性等式,直接从需求 2.5 得出。
|
||||
|
||||
**Validates: Requirements 2.5, 10.4**
|
||||
|
||||
### Property 4: 惩罚分钟数分段公式
|
||||
|
||||
*For any* 非负的 `actual_minutes` 和非负的 `per_hour_contribution`:
|
||||
- 当 `per_hour_contribution >= 24` 时,`penalty_minutes = 0`
|
||||
- 当 `per_hour_contribution < 24` 时,`penalty_minutes = actual_minutes × (1 - per_hour_contribution / 24)`
|
||||
|
||||
且在所有情况下,`0 ≤ penalty_minutes ≤ actual_minutes`。
|
||||
|
||||
推导:直接从需求 6.3/6.4 的分段公式得出。上界 `actual_minutes` 在 `per_hour_contribution = 0` 时取到。
|
||||
|
||||
**Validates: Requirements 6.3, 6.4, 10.5, 10.6**
|
||||
|
||||
### Property 5: 次均消费公式
|
||||
|
||||
*For any* 非负的 `total_consume_amount` 和非负整数 `total_visit_count`,`avg_ticket_amount` 应等于 `total_consume_amount / MAX(total_visit_count, 1)`。
|
||||
|
||||
推导:直接从需求 3.4 得出。`MAX(total_visit_count, 1)` 防止除零。
|
||||
|
||||
**Validates: Requirements 3.4, 10.7**
|
||||
|
||||
### Property 6: 重叠检测正确性
|
||||
|
||||
*For any* 一组助教服务记录,若在指定区域的同一台桌上存在 3 名或以上助教的服务时间段有重叠,则 `detect_overlap_violations` 应返回非空的违规列表。
|
||||
|
||||
推导:需求 6.1 要求检测"同一台桌同一时间段超过 2 名助教挂台"。我们可以生成随机的服务记录(包含时间段重叠和不重叠的情况),验证检测函数的正确性。
|
||||
|
||||
**Validates: Requirements 6.1**
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| 订单无助教服务记录 | 跳过该订单,不生成统计记录 |
|
||||
| 台桌使用时长为 0 | 台费分摊设为 0,避免除零 |
|
||||
| 助教总服务时长为 0 | 酒水食品分摊设为 0,避免除零 |
|
||||
| 会员无充值记录 | 充值次数/金额设为 0 |
|
||||
| 会员无消费记录 | avg_ticket_amount 设为 0 |
|
||||
| 助教当日无违规 | penalty_minutes = 0,penalty_reason = NULL |
|
||||
| 服务记录缺少时间段信息 | 跳过该记录的重叠检测,日志 WARNING |
|
||||
| per_hour_contribution 为负数 | 视为 0 处理(防御性编程) |
|
||||
| FDW 映射创建失败 | 事务回滚,报错终止 |
|
||||
| 数据库写入失败 | 事务回滚,抛出异常由调度器处理 |
|
||||
|
||||
> **注意:所有数据库操作均在测试库(`test_etl_feiqiu` / `test_zqyy_app`)中进行。**
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 属性测试(hypothesis)
|
||||
|
||||
属性测试位于 `tests/` 目录(Monorepo 级),使用 `hypothesis` 库。
|
||||
|
||||
每个属性测试对应设计文档中的一个 Property,最少运行 100 次迭代。
|
||||
|
||||
测试文件:`tests/test_dws_contribution_properties.py`
|
||||
|
||||
```python
|
||||
# Feature: 02-etl-dws-miniapp-extensions, Property 1: 订单级统计不变量
|
||||
@given(order_data=order_data_strategy())
|
||||
@settings(max_examples=200)
|
||||
def test_gross_net_equal_across_assistants(order_data):
|
||||
"""所有参与助教的 order_gross_revenue 和 order_net_revenue 应分别相等"""
|
||||
gross = AssistantOrderContributionTask.compute_order_gross_revenue(order_data)
|
||||
net = AssistantOrderContributionTask.compute_order_net_revenue(order_data)
|
||||
# 每个助教获得相同的 gross 和 net
|
||||
for assistant in order_data.assistants:
|
||||
assert assistant_gross == gross
|
||||
assert assistant_net == net
|
||||
```
|
||||
|
||||
```python
|
||||
# Feature: 02-etl-dws-miniapp-extensions, Property 4: 惩罚分钟数分段公式
|
||||
@given(
|
||||
actual_minutes=st.decimals(min_value=0, max_value=600, places=2),
|
||||
per_hour_contribution=st.decimals(min_value=0, max_value=200, places=2),
|
||||
)
|
||||
@settings(max_examples=200)
|
||||
def test_penalty_minutes_formula(actual_minutes, per_hour_contribution):
|
||||
"""惩罚分钟数应符合分段公式且在 [0, actual_minutes] 范围内"""
|
||||
result = AssistantDailyTask.compute_penalty_minutes(
|
||||
actual_minutes, per_hour_contribution
|
||||
)
|
||||
if per_hour_contribution >= 24:
|
||||
assert result == 0
|
||||
else:
|
||||
expected = actual_minutes * (1 - per_hour_contribution / 24)
|
||||
assert abs(result - expected) < Decimal('0.01')
|
||||
assert 0 <= result <= actual_minutes
|
||||
```
|
||||
|
||||
属性测试库:`hypothesis`(已在项目依赖中)
|
||||
|
||||
### 单元测试
|
||||
|
||||
单元测试位于 `apps/etl/connectors/feiqiu/tests/unit/`,使用 FakeDB/FakeAPI 工具。
|
||||
|
||||
重点覆盖:
|
||||
- PRD 示例数据验算:使用 PRD 中的具体订单示例(3 名助教、2 张台桌、酒水 600 元)验证四项统计的精确数值
|
||||
- 超休/打赏课边界:验证超休助教的四项统计等于个人流水
|
||||
- 零值边界:无台费、无酒水、无助教服务的极端情况
|
||||
- 惩罚计算边界:per_hour_contribution 恰好等于 24 元的临界值
|
||||
- 充值窗口:验证 30/60/90 天窗口的正确切分
|
||||
- 豁免逻辑:is_exempt = TRUE 时跳过惩罚
|
||||
|
||||
### 测试配置
|
||||
|
||||
- 属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
|
||||
- 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_assistant_order_contribution.py -v`
|
||||
- 每个属性测试标注 `@settings(max_examples=200)`
|
||||
- 每个属性测试注释引用设计文档 Property 编号
|
||||
157
.kiro/specs/02-etl-dws-miniapp-extensions/requirements.md
Normal file
157
.kiro/specs/02-etl-dws-miniapp-extensions/requirements.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 需求文档:ETL DWS 层扩展 — 小程序数据支撑
|
||||
|
||||
## 简介
|
||||
|
||||
本 Spec 覆盖 P2 任务中 T4–T11 的 ETL DWS 层扩展,为小程序提供三类核心数据支撑:
|
||||
1. 助教订单流水四项统计(`dws_assistant_order_contribution`)
|
||||
2. 会员消费汇总扩展(充值窗口 + 次均消费)
|
||||
3. 定档折算惩罚检测与计算
|
||||
|
||||
同时包含新表的 RLS 视图创建、FDW 映射同步,以及影子跑数验证。
|
||||
|
||||
> SPI 消费力指数(T1–T3)已在独立 Spec `.kiro/specs/spi-spending-power-index/` 中完成,本文档不再重复。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **AssistantOrderContributionTask**:助教订单流水统计 ETL 任务,粒度 `(site_id, assistant_id, stat_date)`
|
||||
- **MemberConsumptionTask**:会员消费汇总 ETL 任务,粒度 `(site_id, member_id, stat_date)`
|
||||
- **AssistantDailyTask**:助教日度业绩明细 ETL 任务,粒度 `(site_id, assistant_id, stat_date)`
|
||||
- **dws_assistant_order_contribution**:助教订单流水四项统计结果表
|
||||
- **dws_member_consumption_summary**:会员消费汇总表(已有,需扩展字段)
|
||||
- **dws_assistant_daily_detail**:助教日度业绩明细表(已有,需扩展字段)
|
||||
- **order_gross_revenue**:订单总流水 — 助教参与订单的全部流水(台费 + 酒水食品 + 助教服务费)
|
||||
- **order_net_revenue**:订单净流水 — 订单总流水 - 该订单所有助教的服务分成总额
|
||||
- **time_weighted_revenue**:时效贡献流水 — 按助教个人服务时长折算的订单金额贡献
|
||||
- **time_weighted_net_revenue**:时效净贡献 — 时效贡献流水 - 该助教个人的服务分成
|
||||
- **penalty_minutes**:定档折算惩罚分钟数 — 因违规被扣减的定档业绩时长
|
||||
- **per_hour_contribution**:单人每小时贡献流水 — 台费/房费每小时实收单价 ÷ 本次基础课助教人数
|
||||
- **RLS 视图**:行级安全视图,位于 ETL 库 `app` schema,按 `site_id` 隔离数据
|
||||
- **FDW 映射**:外部数据包装器映射,将 ETL 库表映射到业务库 `fdw_etl` schema
|
||||
- **settle_type**:结算类型,1=台桌结账,3=商城订单,5=充值订单
|
||||
- **BaseDwsTask**:DWS 层任务基类,提供 delete-before-insert 幂等机制
|
||||
- **delete-before-insert**:幂等更新策略,先按条件删除旧记录再批量插入新记录
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:助教订单流水统计表创建(T4)
|
||||
|
||||
**用户故事:** 作为 ETL 开发者,我需要创建助教订单流水四项统计表,以便存储每名助教每日的订单流水贡献数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 开发者 SHALL 创建 `dws.dws_assistant_order_contribution` 表,主键为 `(site_id, assistant_id, stat_date)`
|
||||
2. THE dws_assistant_order_contribution 表 SHALL 包含四项统计字段:`order_gross_revenue`(订单总流水)、`order_net_revenue`(订单净流水)、`time_weighted_revenue`(时效贡献流水)、`time_weighted_net_revenue`(时效净贡献),精度为 `NUMERIC(14,2)`
|
||||
3. THE dws_assistant_order_contribution 表 SHALL 包含辅助字段:`order_count`(参与订单数)、`total_service_seconds`(总服务时长秒数)、`assistant_nickname`(助教昵称)
|
||||
4. THE dws_assistant_order_contribution 表 SHALL 包含元数据字段:`tenant_id`、`created_at`、`updated_at`
|
||||
5. THE 开发者 SHALL 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>__create_dws_assistant_order_contribution.sql`,在测试库 `test_etl_feiqiu` 中执行建表
|
||||
6. WHEN DDL 在测试库执行成功后,THE 开发者 SHALL 运行 `gen_consolidated_ddl.py` 导出最新 DDL
|
||||
|
||||
### 需求 2:助教订单流水四项统计计算(T5)
|
||||
|
||||
**用户故事:** 作为产品经理,我需要助教订单流水四项统计(订单总流水/订单净流水/时效贡献流水/时效净贡献),以便评估助教个人能力。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE AssistantOrderContributionTask SHALL 从 `dwd.dwd_settlement_head`、`dwd.dwd_table_fee_log`、`dwd.dwd_assistant_service_log` 提取订单、台费和助教服务数据
|
||||
2. WHEN 计算 order_gross_revenue 时,THE AssistantOrderContributionTask SHALL 将助教参与订单的全部流水(台费 + 酒水食品 + 所有助教服务费)累加,每个参与助教获得相同的订单总流水值
|
||||
3. WHEN 计算 order_net_revenue 时,THE AssistantOrderContributionTask SHALL 从订单总流水中减去该订单所有助教的服务分成总额,每个参与助教获得相同的订单净流水值
|
||||
4. WHEN 计算 time_weighted_revenue 时,THE AssistantOrderContributionTask SHALL 按以下步骤折算个人贡献:
|
||||
- 确定每张台桌的有效计费时长:取 MAX(该台桌所有助教服务时长之和, 台桌使用时长)
|
||||
- 按助教在该台桌的服务时长占比分摊台费
|
||||
- 助教个人服务费直接计入
|
||||
- 酒水食品按助教个人服务总时长占所有助教服务总时长的比例均分
|
||||
5. WHEN 计算 time_weighted_net_revenue 时,THE AssistantOrderContributionTask SHALL 从该助教的时效贡献流水中减去该助教个人的服务分成
|
||||
6. WHEN 助教为超休/打赏课类型时,THE AssistantOrderContributionTask SHALL 将四项统计均设为该助教个人的服务流水和分成(不参与订单级分摊)
|
||||
7. THE AssistantOrderContributionTask SHALL 以任务代码 `DWS_ASSISTANT_ORDER_CONTRIBUTION` 注册到 task_registry
|
||||
8. THE AssistantOrderContributionTask SHALL 采用 delete-before-insert 策略按日期窗口幂等更新
|
||||
|
||||
### 需求 3:会员消费汇总扩展(T6)
|
||||
|
||||
**用户故事:** 作为产品经理,我需要客户 30/60/90 天充值次数和金额、次均消费,以便在客户看板中展示。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 开发者 SHALL 在 `dws.dws_member_consumption_summary` 表中新增以下字段:`recharge_count_30d`、`recharge_count_60d`、`recharge_count_90d`(充值次数,INTEGER)、`recharge_amount_30d`、`recharge_amount_60d`、`recharge_amount_90d`(充值金额,NUMERIC(14,2))、`avg_ticket_amount`(次均消费额度,NUMERIC(14,2))
|
||||
2. THE 开发者 SHALL 编写 ALTER TABLE 迁移脚本在测试库 `test_etl_feiqiu` 中执行字段扩展
|
||||
3. THE 充值数据 SHALL 从 `dwd.dwd_recharge_order` 提取,按 `member_id` 和时间窗口聚合
|
||||
4. THE avg_ticket_amount SHALL 按公式 `total_consume_amount / MAX(total_visit_count, 1)` 计算
|
||||
|
||||
### 需求 4:会员消费汇总任务修改(T7)
|
||||
|
||||
**用户故事:** 作为 ETL 开发者,我需要修改 MemberConsumptionTask 以填充新增的充值窗口和次均消费字段。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE MemberConsumptionTask SHALL 在 extract 阶段新增充值订单提取逻辑,从 `dwd.dwd_recharge_order` 按 30/60/90 天窗口聚合充值次数和金额
|
||||
2. THE MemberConsumptionTask SHALL 在 transform 阶段将充值统计和次均消费填充到输出记录中
|
||||
3. WHEN 会员无充值记录时,THE MemberConsumptionTask SHALL 将充值次数设为 0、充值金额设为 0.00
|
||||
4. WHEN 会员无消费记录时,THE MemberConsumptionTask SHALL 将 avg_ticket_amount 设为 0.00
|
||||
|
||||
### 需求 5:助教日度明细表扩展 — 定档折算惩罚字段(T8)
|
||||
|
||||
**用户故事:** 作为 ETL 开发者,我需要在助教日度明细表中新增定档折算惩罚相关字段,以便存储惩罚检测和计算结果。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 开发者 SHALL 在 `dws.dws_assistant_daily_detail` 表中新增以下字段:`penalty_minutes` (NUMERIC(10,2))、`penalty_reason` (TEXT)、`is_exempt` (BOOLEAN DEFAULT FALSE)、`per_hour_contribution` (NUMERIC(14,2))
|
||||
2. THE 开发者 SHALL 编写 ALTER TABLE 迁移脚本在测试库 `test_etl_feiqiu` 中执行字段扩展
|
||||
3. WHEN 助教当日无惩罚时,THE penalty_minutes SHALL 为 0,penalty_reason SHALL 为 NULL
|
||||
|
||||
### 需求 6:定档折算惩罚检测与计算逻辑(T9)
|
||||
|
||||
**用户故事:** 作为产品经理,我需要定档折算惩罚数据,以便在绩效页面展示折算详情,防止助教利用低价订单冲档位。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE AssistantDailyTask SHALL 检测规则 2 违规:在指定区域(大厅 A/B/C/S/TV 和麻将房 M1–M7)内,同一台桌同一时间段超过 2 名助教挂台(课程时间段有重叠即算)
|
||||
2. WHEN 检测到违规时,THE AssistantDailyTask SHALL 计算单人每小时贡献流水:台费/房费每小时实收单价 ÷ 本次基础课助教人数
|
||||
3. WHEN per_hour_contribution >= 24 元时,THE AssistantDailyTask SHALL 按满额计入定档业绩时长(penalty_minutes = 0)
|
||||
4. WHEN per_hour_contribution < 24 元时,THE AssistantDailyTask SHALL 按比例折算:`penalty_minutes = 实际服务分钟数 × (1 - per_hour_contribution / 24)`
|
||||
5. WHEN 订单标记为 is_exempt = TRUE 时,THE AssistantDailyTask SHALL 跳过惩罚计算,penalty_minutes 设为 0
|
||||
6. THE 定档折算惩罚 SHALL 仅影响定档业绩时长统计,不影响实际工资时长
|
||||
7. THE AssistantDailyTask SHALL 每日自动计算惩罚,计算频率与现有日度任务一致
|
||||
|
||||
### 需求 7:RLS 视图创建(T10)
|
||||
|
||||
**用户故事:** 作为 ETL 开发者,我需要为新表创建 RLS 视图,以便通过 FDW 安全地向业务库暴露数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 开发者 SHALL 在 ETL 库 `app` schema 中为 `dws_assistant_order_contribution` 创建 RLS 视图,按 `site_id` 过滤
|
||||
2. THE 开发者 SHALL 更新已有 RLS 视图以包含 `dws_member_consumption_summary` 和 `dws_assistant_daily_detail` 的新增字段
|
||||
3. THE RLS 视图 SHALL 使用 `current_setting('app.current_site_id')::INTEGER` 进行行级过滤
|
||||
|
||||
### 需求 8:FDW 映射同步(T10)
|
||||
|
||||
**用户故事:** 作为后端开发者,我需要在业务库中通过 FDW 访问新建和扩展的 ETL 表,以便小程序后端读取数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 开发者 SHALL 在 `test_zqyy_app.fdw_etl` schema 中创建 `dws_assistant_order_contribution` 的外部表映射
|
||||
2. THE 开发者 SHALL 更新 `dws_member_consumption_summary` 和 `dws_assistant_daily_detail` 的 FDW 映射以包含新增字段
|
||||
3. THE FDW 映射 SHALL 通过 `app` schema 的 RLS 视图访问数据,而非直接访问 `dws` schema
|
||||
|
||||
### 需求 9:影子跑数验证(T11)
|
||||
|
||||
**用户故事:** 作为 ETL 开发者,我需要通过影子跑数验证新增统计的正确性,以便确保数据质量。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 开发者 SHALL 编写验证脚本,对照 PRD 示例数据验算四项统计的计算结果
|
||||
2. THE 验证脚本 SHALL 检查 `dws_assistant_order_contribution` 中四项统计数值的一致性:order_gross_revenue 各助教相等、order_net_revenue 各助教相等、time_weighted_revenue 各助教之和加上误差容忍度等于订单总流水
|
||||
3. THE 验证脚本 SHALL 检查 `dws_member_consumption_summary` 新增字段有值且充值金额与 `dwd_recharge_order` 源数据一致
|
||||
4. THE 验证脚本 SHALL 检查定档折算惩罚字段在符合惩罚条件的记录上正确填充
|
||||
|
||||
### 需求 10:算法正确性测试
|
||||
|
||||
**用户故事:** 作为 ETL 开发者,我需要通过属性测试(hypothesis)验证四项统计和惩罚计算的正确性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 属性测试 SHALL 验证:对于任意订单,所有参与助教的 order_gross_revenue 值相等
|
||||
2. THE 属性测试 SHALL 验证:对于任意订单,所有参与助教的 order_net_revenue 值相等
|
||||
3. THE 属性测试 SHALL 验证:对于任意订单,所有参与助教的 time_weighted_revenue 之和应在订单总流水的合理误差范围内(±0.01 元)
|
||||
4. THE 属性测试 SHALL 验证:对于任意助教,time_weighted_net_revenue = time_weighted_revenue - 该助教个人服务分成
|
||||
5. THE 属性测试 SHALL 验证:对于任意 per_hour_contribution >= 24 的记录,penalty_minutes 为 0
|
||||
6. THE 属性测试 SHALL 验证:对于任意 per_hour_contribution < 24 且 per_hour_contribution >= 0 的记录,penalty_minutes = 实际分钟数 × (1 - per_hour_contribution / 24)
|
||||
7. THE 属性测试 SHALL 验证:对于任意会员,avg_ticket_amount = total_consume_amount / MAX(total_visit_count, 1)
|
||||
152
.kiro/specs/02-etl-dws-miniapp-extensions/tasks.md
Normal file
152
.kiro/specs/02-etl-dws-miniapp-extensions/tasks.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 实现计划:ETL DWS 层扩展 — 小程序数据支撑
|
||||
|
||||
## 概述
|
||||
|
||||
基于设计文档,将实现拆分为:DDL 建表/扩展 → 助教订单流水统计任务 → 会员消费汇总扩展 → 定档折算惩罚 → RLS 视图 + FDW 映射 → 影子跑数验证 六个阶段。所有数据库操作在测试库(`test_etl_feiqiu` / `test_zqyy_app`)中进行。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. DDL 建表与字段扩展
|
||||
- [x] 1.1 编写迁移脚本创建 `dws.dws_assistant_order_contribution` 表
|
||||
- 新建 `db/etl_feiqiu/migrations/<日期>__create_dws_assistant_order_contribution.sql`
|
||||
- 包含表定义、唯一索引 `idx_aoc_site_assistant_date`、查询索引 `idx_aoc_stat_date`
|
||||
- 字段参照设计文档数据模型章节
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
|
||||
- [x] 1.2 编写迁移脚本扩展 `dws_member_consumption_summary` 字段
|
||||
- 新建 `db/etl_feiqiu/migrations/<日期>__alter_member_consumption_add_recharge_fields.sql`
|
||||
- ALTER TABLE 添加 `recharge_count_30d/60d/90d`、`recharge_amount_30d/60d/90d`、`avg_ticket_amount`
|
||||
- _Requirements: 3.1, 3.2_
|
||||
- [x] 1.3 编写迁移脚本扩展 `dws_assistant_daily_detail` 字段
|
||||
- 新建 `db/etl_feiqiu/migrations/<日期>__alter_assistant_daily_add_penalty_fields.sql`
|
||||
- ALTER TABLE 添加 `penalty_minutes`、`penalty_reason`、`is_exempt`、`per_hour_contribution`
|
||||
- _Requirements: 5.1, 5.2_
|
||||
- [x] 1.4 在测试库 `test_etl_feiqiu` 执行全部迁移脚本
|
||||
- 通过 `PG_DSN`(指向测试库)连接执行 SQL
|
||||
- _Requirements: 1.5, 3.2, 5.2_
|
||||
- [x] 1.5 运行 `gen_consolidated_ddl.py` 导出最新 DDL
|
||||
- 执行 `python scripts/ops/gen_consolidated_ddl.py`
|
||||
- 验证 `docs/database/ddl/etl_feiqiu__dws.sql` 已包含新表和扩展字段
|
||||
- _Requirements: 1.6_
|
||||
|
||||
- [x] 2. 实现助教订单流水统计任务
|
||||
- [x] 2.1 创建数据结构和 `AssistantOrderContributionTask` 骨架
|
||||
- 新建 `apps/etl/connectors/feiqiu/tasks/dws/assistant_order_contribution_task.py`
|
||||
- 定义 `TableUsage`、`AssistantService`、`OrderData` dataclass
|
||||
- 定义 `AssistantOrderContributionTask` 类继承 `BaseDwsTask`
|
||||
- 实现 `get_task_code`、`get_target_table`、`get_primary_keys`
|
||||
- _Requirements: 1.1, 2.7_
|
||||
- [x] 2.2 实现四项统计核心计算(纯函数)
|
||||
- 实现 `compute_order_gross_revenue` 静态方法
|
||||
- 实现 `compute_order_net_revenue` 静态方法
|
||||
- 实现 `compute_time_weighted_revenue` 静态方法(含台费分摊、酒水均分逻辑)
|
||||
- 实现 `compute_time_weighted_net_revenue` 静态方法
|
||||
- 处理超休/打赏课特殊情况
|
||||
- _Requirements: 2.2, 2.3, 2.4, 2.5, 2.6_
|
||||
- [x] 2.3 编写属性测试:订单级统计不变量
|
||||
- **Property 1: 订单级统计不变量 — gross/net 各助教相等**
|
||||
- **Validates: Requirements 2.2, 2.3, 10.1, 10.2**
|
||||
- [x] 2.4 编写属性测试:时效贡献流水之和约束
|
||||
- **Property 2: 时效贡献流水之和 ≤ order_gross_revenue**
|
||||
- **Validates: Requirements 2.4, 10.3**
|
||||
- [x] 2.5 编写属性测试:时效净贡献减法关系
|
||||
- **Property 3: time_weighted_net_revenue = time_weighted_revenue - commission**
|
||||
- **Validates: Requirements 2.5, 10.4**
|
||||
- [x] 2.6 实现 `extract` 方法
|
||||
- 从 `dwd_settlement_head`、`dwd_table_fee_log`、`dwd_assistant_service_log` 提取数据
|
||||
- 按 `order_settle_id` 聚合为 `OrderData` 结构
|
||||
- _Requirements: 2.1_
|
||||
- [x] 2.7 实现 `transform` 方法
|
||||
- 遍历订单,调用四项统计计算函数
|
||||
- 按 `(assistant_id, stat_date)` 聚合日度统计
|
||||
- _Requirements: 2.2, 2.3, 2.4, 2.5, 2.6_
|
||||
- [x] 2.8 注册任务到 task_registry 并导出模块
|
||||
- 在 `tasks/dws/__init__.py` 中导出 `AssistantOrderContributionTask`
|
||||
- 在 `orchestration/task_registry.py` 中注册 `DWS_ASSISTANT_ORDER_CONTRIBUTION`,`layer="DWS"`,`depends_on=["DWD_LOAD_FROM_ODS"]`
|
||||
- _Requirements: 2.7, 2.8_
|
||||
|
||||
- [x] 3. 检查点 — 确保助教订单流水统计测试通过
|
||||
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
|
||||
- 确保所有属性测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 4. 扩展会员消费汇总任务
|
||||
- [x] 4.1 在 `MemberConsumptionTask` 中新增充值统计提取
|
||||
- 新增 `_extract_recharge_stats` 方法,从 `dwd.dwd_recharge_order` 按 30/60/90 天窗口聚合
|
||||
- 在 `extract` 方法中调用并返回充值统计数据
|
||||
- _Requirements: 4.1, 3.3_
|
||||
- [x] 4.2 在 `MemberConsumptionTask.transform` 中填充新字段
|
||||
- 合并充值统计到输出记录
|
||||
- 计算 `avg_ticket_amount = total_consume_amount / MAX(total_visit_count, 1)`
|
||||
- 处理无充值/无消费的边界情况
|
||||
- _Requirements: 4.2, 4.3, 4.4, 3.4_
|
||||
- [x] 4.3 编写属性测试:次均消费公式
|
||||
- **Property 5: avg_ticket_amount = total_consume_amount / MAX(total_visit_count, 1)**
|
||||
- **Validates: Requirements 3.4, 10.7**
|
||||
|
||||
- [x] 5. 实现定档折算惩罚检测与计算
|
||||
- [x] 5.1 实现时间重叠检测逻辑
|
||||
- 在 `AssistantDailyTask` 中新增 `detect_overlap_violations` 静态方法
|
||||
- 定义惩罚区域集合(大厅 A/B/C/S/TV + 麻将房 M1–M7)
|
||||
- 按 `(table_id, service_date)` 分组,检测时间段重叠且助教数 > 2
|
||||
- _Requirements: 6.1_
|
||||
- [x] 5.2 实现惩罚分钟数计算
|
||||
- 在 `AssistantDailyTask` 中新增 `compute_penalty_minutes` 静态方法
|
||||
- 计算 `per_hour_contribution = 台费每小时单价 / 助教人数`
|
||||
- 按分段公式计算 `penalty_minutes`
|
||||
- 处理 `is_exempt = TRUE` 豁免逻辑
|
||||
- _Requirements: 6.2, 6.3, 6.4, 6.5_
|
||||
- [x] 5.3 集成惩罚逻辑到 `AssistantDailyTask.transform`
|
||||
- 在现有聚合逻辑后调用重叠检测和惩罚计算
|
||||
- 将 `penalty_minutes`、`penalty_reason`、`is_exempt`、`per_hour_contribution` 填充到输出记录
|
||||
- _Requirements: 6.6, 6.7_
|
||||
- [x] 5.4 编写属性测试:惩罚分钟数分段公式
|
||||
- **Property 4: 惩罚分钟数符合分段公式且在 [0, actual_minutes] 范围内**
|
||||
- **Validates: Requirements 6.3, 6.4, 10.5, 10.6**
|
||||
- [x] 5.5 编写属性测试:重叠检测正确性
|
||||
- **Property 6: 3+ 助教时间重叠时应检测到违规**
|
||||
- **Validates: Requirements 6.1**
|
||||
|
||||
- [x] 6. 检查点 — 确保惩罚计算和消费汇总测试通过
|
||||
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
|
||||
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/ -k "contribution or penalty or consumption" -v`
|
||||
- 确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 7. RLS 视图与 FDW 映射
|
||||
- [x] 7.1 创建 `dws_assistant_order_contribution` 的 RLS 视图
|
||||
- 在测试库 `test_etl_feiqiu` 的 `app` schema 中创建 `v_dws_assistant_order_contribution` 视图
|
||||
- 使用 `current_setting('app.current_site_id')::bigint` 过滤
|
||||
- 授予 `app_reader` 角色 SELECT 权限
|
||||
- _Requirements: 7.1, 7.3_
|
||||
- [x] 7.2 验证已有 RLS 视图自动包含新增字段
|
||||
- 确认 `app.v_dws_member_consumption_summary` 和 `app.v_dws_assistant_daily_detail` 使用 `SELECT *`,新增字段自动包含
|
||||
- _Requirements: 7.2_
|
||||
- [x] 7.3 创建/更新 FDW 外部表映射
|
||||
- 在测试库 `test_zqyy_app` 的 `fdw_etl` schema 中创建 `dws_assistant_order_contribution` 外部表
|
||||
- 重建 `dws_member_consumption_summary` 和 `dws_assistant_daily_detail` 的 FDW 外部表以包含新字段
|
||||
- FDW 映射通过 `app` schema RLS 视图访问
|
||||
- _Requirements: 8.1, 8.2, 8.3_
|
||||
|
||||
- [x] 8. 影子跑数验证
|
||||
- [x] 8.1 编写验证脚本
|
||||
- 新建 `apps/etl/connectors/feiqiu/scripts/verify_dws_extensions.py`
|
||||
- 验证四项统计:对照 PRD 示例数据验算,检查 gross/net 各助教相等
|
||||
- 验证充值窗口:检查新增字段有值,充值金额与源数据一致
|
||||
- 验证惩罚字段:检查符合条件的记录正确填充
|
||||
- _Requirements: 9.1, 9.2, 9.3, 9.4_
|
||||
- [x] 8.2 编写数据库手册文档
|
||||
- 新建 `docs/database/BD_Manual_dws_assistant_order_contribution.md`
|
||||
- 包含表结构、字段说明、索引、验证 SQL(至少 3 条)、兼容性说明、回滚策略
|
||||
- 更新 `docs/database/` 中 `dws_member_consumption_summary` 和 `dws_assistant_daily_detail` 的文档
|
||||
- _Requirements: 1.1_
|
||||
|
||||
- [x] 9. 最终检查点 — 确保所有测试通过
|
||||
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
|
||||
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/ -k "contribution or penalty or consumption" -v`
|
||||
- 确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP
|
||||
- 每个任务引用具体需求编号以确保可追溯
|
||||
- 所有数据库操作在测试库(`test_etl_feiqiu` / `test_zqyy_app`)中进行
|
||||
- 检查点确保增量验证
|
||||
- 属性测试验证全称正确性属性,单元测试验证具体示例和边界情况
|
||||
Reference in New Issue
Block a user