Files

14 KiB
Raw Permalink Blame History

实现计划业务日分割点机制Business Day Cutoff

概述

将全系统统计时间口径从自然日切换为以可配置小时值(默认 08:00为分割点的营业日。实现顺序配置层 → 共享工具层 → ETL 层 → 后端 API 层 → 数据库层 → 前端适配 → 历史数据重算 → 运维脚本排查。

任务

  • 1. 配置层:环境变量与配置加载

    • 1.1 在根 .env.env.template 中新增 BUSINESS_DAY_START_HOUR=8

      • .env 新增 BUSINESS_DAY_START_HOUR=8
      • .env.template 新增带注释的定义,说明日/周/月统计分割语义
      • Requirements: 1.1, 1.2
    • 1.2 在 AppConfig._validate 中新增 business_day_start_hour 范围校验

      • apps/etl/connectors/feiqiu/config/settings.py_validate 方法中增加校验
      • 值不在 023 范围内或非整数时抛出 SystemExit
      • 环境变量缺失时使用默认值 8已由 defaults.py 保证)
      • Requirements: 1.3, 1.4, 3.1, 3.2, 3.3, 3.4
    • 1.3 在后端 apps/backend/app/config.py 中新增 BUSINESS_DAY_START_HOUR 常量

      • 新增 BUSINESS_DAY_START_HOUR: int = int(get("BUSINESS_DAY_START_HOUR", "8"))
      • Requirements: 6.1
  • 2. 共享时间工具层:新增 range 函数

    • 2.1 在 packages/shared/src/neozqyy_shared/datetime_utils.py 中实现三个 range 函数

      • 实现 business_day_range(biz_date, day_start_hour)tuple[datetime, datetime]
      • 实现 business_week_range(week_monday, day_start_hour)tuple[datetime, datetime]
      • 实现 business_month_range(month_first, day_start_hour)tuple[datetime, datetime]
      • 所有返回值带 Asia/Shanghai 时区(使用 SHANGHAI_TZ
      • 默认 day_start_hour=8
      • Requirements: 2.6, 2.7, 2.8, 2.5
    • 2.2 编写属性测试营业日归属往返一致性Property 1

      • Property 1: 营业日归属往返一致性Round-Trip
      • business_day_range(business_date(dt, h), h) 的范围 [start, end) 应满足 start <= dt < end
      • 使用 hypothesis@given(dt=st.datetimes(...), h=st.integers(0, 23))@settings(max_examples=200)
      • 测试文件:tests/test_property_business_day_cutoff.py
      • Validates: Requirements 2.9, 11.1
    • 2.3 编写属性测试营业月与营业日一致性Property 2

      • Property 2: 营业月与营业日一致性
      • business_month(dt, h) == business_date(dt, h).replace(day=1)
      • Validates: Requirements 2.10, 11.2
    • 2.4 编写属性测试营业周与营业日一致性Property 3

      • Property 3: 营业周与营业日一致性
      • business_week_monday(dt, h) == business_date(dt, h) - timedelta(days=business_date(dt, h).weekday()),且结果 weekday() == 0
      • Validates: Requirements 2.11, 11.3
    • 2.5 编写属性测试营业日归属单调性Property 4

      • Property 4: 营业日归属单调性
      • dt1 < dt2 且两者在同一 business_day_range 范围内,则 business_date(dt1, h) == business_date(dt2, h)
      • Validates: Requirements 11.9
    • 2.6 编写属性测试时间范围长度不变量Property 5

      • Property 5: 时间范围长度不变量
      • business_day_range(d, h)end - start == timedelta(hours=24)
      • business_week_range(monday, h)end - start == timedelta(days=7)
      • Validates: Requirements 11.6, 11.7
    • 2.7 编写属性测试SQL 表达式生成幂等性Property 6

      • Property 6: SQL 表达式生成幂等性
      • biz_date_sql_expr(col, h) 多次调用返回完全相同的字符串
      • Validates: Requirements 11.4
    • 2.8 编写属性测试边界条件验证Property 1 补充)

      • cutoff 时刻恰好在分割点上的时间戳归属当天,分割点前一秒归属前一天
      • 使用 hypothesis 生成 day_start_hour,构造边界时间戳验证
      • Validates: Requirements 11.5
    • 2.9 编写单元测试:共享时间工具函数

      • 测试 day_start_hour=8 时 07:59:59 归属前一天、08:00:00 归属当天
      • 测试 biz_date_sql_expr("pay_time", 8) 返回 DATE(pay_time - INTERVAL '8 hours')
      • 测试月末边界1月31日 07:00 归属1月30日business_month 返回1月1日
      • 测试默认值行为:不传 day_start_hour 时使用 8
      • Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
  • 3. Checkpoint — 共享工具层验证

    • 确保所有属性测试和单元测试通过,运行 pytest tests/test_property_business_day_cutoff.py -v,如有问题请向用户确认。
  • 4. ETL 配置层集成与 BaseDwsTask 基类改造

    • 4.1 在 AppConfig._validate 中新增 business_day_start_hour 范围校验(与 1.2 合并实现)

      • 确认 defaults.pyenv_parser.py 已有配置映射(设计文档标注"已完成"
      • 仅需在 settings.py_validate 中增加校验逻辑
      • Requirements: 3.1, 3.2, 3.3, 3.4
    • 4.2 编写属性测试非法配置值拒绝Property 7

      • Property 7: 非法配置值拒绝
      • 对任意不在 023 范围内的整数值,AppConfig.load() 应抛出 SystemExit
      • Validates: Requirements 1.3
    • 4.3 编写属性测试合法配置值正确加载Property 8

      • Property 8: 合法配置值正确加载
      • 对任意 023 范围内的整数值 vAppConfig.load()cfg.get("app.business_day_start_hour") 应返回 v
      • Validates: Requirements 3.4
    • 4.4 改造 BaseDwsTask.iter_dwd_rows,将 DATE() 替换为 biz_date_sql_expr

      • self.config.get("app.business_day_start_hour", 8) 读取 cutoff 值
      • 调用 biz_date_sql_expr(date_col, cutoff) 生成 SQL 表达式
      • 替换 WHERE 子句中的 DATE(col)biz_date_sql_expr 生成的表达式
      • Requirements: 4.1, 4.5, 4.7
    • 4.5 改造 BaseDwsTask.get_time_window_range,使用 business_date 计算营业日归属

      • base_date 为 None 时使用 business_date(now_shanghai(), cutoff) 计算
      • 确保返回的 TimeRange 基于营业日口径
      • Requirements: 4.6
  • 5. DWS 任务 SQL 改造(财务类)

    • 5.1 改造 FinanceBaseTaskDATE(pay_time)biz_date_sql_expr("pay_time", cutoff)

      • 从 config 读取 cutoff替换所有 DATE(pay_time) 为营业日表达式
      • 包括 SELECT、WHERE、GROUP BY 中的所有出现
      • Requirements: 5.1
    • 5.2 改造 FinanceDailyTask使用 Business_Date 口径

      • 替换结账单、团购核销、充值等数据的日期归属
      • Requirements: 5.2
    • 5.3 改造 FinanceRechargeTaskDATE(pay_time) → 营业日归属表达式

      • Requirements: 5.3
    • 5.4 改造 FinanceDiscountTask使用 Business_Date 口径聚合优惠明细

      • Requirements: 5.4
    • 5.5 改造 FinanceIncomeTask使用 Business_Date 口径聚合收入结构

      • Requirements: 5.5
  • 6. DWS 任务 SQL 改造(助教类)

    • 6.1 改造 AssistantDailyTaskDATE(start_use_time) → 营业日归属表达式

      • Requirements: 5.6
    • 6.2 改造 AssistantOrderContributionTaskDATE(pay_time) → 营业日归属表达式

      • Requirements: 5.7
    • 6.3 改造 AssistantCustomerTaskDATE(start_use_time) → 营业日归属表达式

      • Requirements: 5.8
    • 6.4 改造 AssistantMonthlyTask使用 Business_Month 口径聚合月度汇总

      • Requirements: 5.9
    • 6.5 改造 AssistantFinanceTask使用 Business_Date 口径聚合助教财务分析

      • Requirements: 5.10
  • 7. DWS 任务 SQL 改造(会员类 + 商品库存类 + 指标类)

    • 7.1 改造 MemberVisitTaskDATE(pay_time)DATE(start_use_time)DATE(ledger_end_time) → 营业日归属表达式

      • Requirements: 5.11
    • 7.2 改造 MemberConsumptionTaskDATE(pay_time)DATE(create_time) → 营业日归属表达式

      • Requirements: 5.12
    • 7.3 改造 GoodsStockDailyTaskDATE(fetched_at) → 营业日归属表达式

      • Requirements: 5.13
    • 7.4 改造 GoodsStockWeeklyTask使用 Business_Week 口径聚合库存周报

      • Requirements: 5.14
    • 7.5 改造 GoodsStockMonthlyTask使用 Business_Month 口径聚合库存月报

      • Requirements: 5.15
    • 7.6 改造 SpendingPowerIndexTaskDATE(pay_time) → 营业日归属表达式

      • Requirements: 5.16
    • 7.7 改造 MemberIndexBaseDATE(pay_time) → 营业日归属表达式

      • Requirements: 5.17
    • 7.8 改造 MvRefreshTask确保物化视图刷新的时间过滤条件使用 Business_Date 口径

      • Requirements: 5.18
  • 8. Checkpoint — ETL DWS 层改造验证

    • 确保所有 18 个 DWS 任务的 SQL 改造完成,DATE() 调用已全部替换为 biz_date_sql_expr 生成的表达式。运行 ETL 单元测试 cd apps/etl/connectors/feiqiu && pytest tests/unit -v,如有问题请向用户确认。
  • 9. 后端 API 层

    • 9.1 创建 apps/backend/app/routers/business_day.py,实现 /api/config/business-day 端点

      • 创建 APIRouter(prefix="/api/config", tags=["业务配置"])
      • 实现 GET /business-day 返回 {"business_day_start_hour": config.BUSINESS_DAY_START_HOUR}
      • 无需认证(公开配置)
      • Requirements: 8.1, 8.2, 8.3
    • 9.2 在后端主路由中注册 business_day 路由

      • apps/backend/app/main.py 或路由注册文件中 include_router
      • Requirements: 8.1
    • 9.3 后端时间范围查询统一使用 business_*_range 函数

      • 在后端需要计算"今日/本周/本月"时间范围的 API 中,导入并使用 business_day_rangebusiness_week_rangebusiness_month_range
      • Shared_DateTime_Utils 导入,禁止重复实现
      • Requirements: 6.2, 6.3, 6.4, 6.5
    • 9.4 编写单元测试:后端配置查询 API

      • 测试 /api/config/business-day 返回正确 JSON 格式
      • 测试返回值与 config.BUSINESS_DAY_START_HOUR 一致
      • Requirements: 8.1, 8.2, 8.3
  • 10. 数据库层PostgreSQL 函数与物化视图迁移

    • 10.1 创建迁移脚本:新增 dws.biz_date() PostgreSQL 函数

      • 文件:db/etl_feiqiu/migrations/2026-03-XX__add_biz_date_function.sql
      • CREATE OR REPLACE FUNCTION dws.biz_date(ts timestamptz, cutoff_hour int DEFAULT 8) RETURNS date
      • 标记为 IMMUTABLE PARALLEL SAFE
      • Requirements: 9.2, 9.4
    • 10.2 创建迁移脚本:重建物化视图使用 biz_date() 函数

      • 文件:db/etl_feiqiu/migrations/2026-03-XX__rebuild_mv_with_biz_date.sql
      • mv_dws_finance_daily_summary_l1..l4mv_dws_assistant_daily_detail_l1..l4CURRENT_DATE / date_trunc 替换为 dws.biz_date(NOW())
      • 使用 DROP MATERIALIZED VIEW IF EXISTS + CREATE MATERIALIZED VIEW 重建
      • Requirements: 9.1, 9.3
  • 11. 前端适配

    • 11.1 Admin_Web日期选择器旁标注营业日口径说明

      • 在日期选择器组件旁增加 Tooltip 或文字标注:营业日:{HH}:00 起
      • 通过 /api/config/business-day 获取 business_day_start_hour,启动时请求一次存入全局状态
      • 降级策略API 不可用时使用默认值 8console.warn 输出警告
      • Requirements: 7.1, 7.2, 7.4, 7.5
    • 11.2 Miniprogram确认后端 API 返回的数据已是营业日口径

      • 小程序不直接使用 cutoff 值,所有统计数据由后端 API 按营业日口径返回
      • 确认无需前端改造,仅验证后端 API 数据正确性
      • Requirements: 7.3
  • 12. Checkpoint — 后端 + 数据库 + 前端验证

    • 确保后端 API 端点可用、迁移脚本语法正确、前端标注正常显示。如有问题请向用户确认。
  • 13. 历史数据重算脚本

    • 13.1 创建 scripts/ops/rebuild_dws_biz_date.py 历史数据重算脚本
      • 复用正式 ETL 任务逻辑,使用相同的 Business_Day_Cutoff 配置
      • 按月分窗口执行,避免单次事务过大
      • 执行前后记录行数对比到日志
      • 支持 --dry-run 模式预览影响范围
      • 支持 --fail-fast 参数控制错误时中止或继续
      • 错误时回滚当前批次并记录错误日志
      • Requirements: 10.1, 10.2, 10.3, 10.4
  • 14. 运维脚本排查与改造

    • 14.1 排查并改造 scripts/ops/export_bug_report.py

      • DATE(trash_time)DATE(create_time)DATE(start_use_time) 替换为 biz_date_sql_expr 生成的表达式
      • Requirements: 12.1, 12.5
    • 14.2 排查并改造 scripts/ops/etl_consistency_check.py

      • 评估日期比较逻辑,按需替换为营业日归属表达式
      • Requirements: 12.2, 12.5
    • 14.3 排查并改造 apps/etl/connectors/feiqiu/scripts/debug/debug_blackbox.py

      • ::date 类型转换替换为 biz_date() 函数调用
      • Requirements: 12.3
    • 14.4 排查并改造 apps/etl/connectors/feiqiu/scripts/run_update.py

      • .date()datetime.combine 替换为 business_date() + business_day_range()
      • Requirements: 12.4, 12.5
  • 15. Final Checkpoint — 全量验证

    • 确保所有属性测试通过:cd C:\NeoZQYY && pytest tests/test_property_business_day_cutoff.py -v
    • 确保 ETL 单元测试通过:cd apps/etl/connectors/feiqiu && pytest tests/unit -v
    • 确认所有 12 项需求的验收标准均有对应任务覆盖
    • 如有问题请向用户确认。

备注

  • 标记 * 的子任务为可选,可跳过以加速 MVP 交付
  • 每个任务引用了具体的需求编号,确保可追溯性
  • 属性测试验证设计文档中的 8 个正确性属性
  • Checkpoint 任务确保增量验证,及时发现问题
  • 设计文档标注 defaults.pyenv_parser.py 已完成,任务 4.1 仅需增加校验逻辑