14 KiB
实现计划:业务日分割点机制(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方法中增加校验 - 值不在 0–23 范围内或非整数时抛出
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.py和env_parser.py已有配置映射(设计文档标注"已完成") - 仅需在
settings.py的_validate中增加校验逻辑 - Requirements: 3.1, 3.2, 3.3, 3.4
- 确认
-
4.2 编写属性测试:非法配置值拒绝(Property 7)
- Property 7: 非法配置值拒绝
- 对任意不在 0–23 范围内的整数值,
AppConfig.load()应抛出SystemExit - Validates: Requirements 1.3
-
4.3 编写属性测试:合法配置值正确加载(Property 8)
- Property 8: 合法配置值正确加载
- 对任意 0–23 范围内的整数值
v,AppConfig.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 改造 FinanceBaseTask:
DATE(pay_time)→biz_date_sql_expr("pay_time", cutoff)- 从 config 读取 cutoff,替换所有
DATE(pay_time)为营业日表达式 - 包括 SELECT、WHERE、GROUP BY 中的所有出现
- Requirements: 5.1
- 从 config 读取 cutoff,替换所有
-
5.2 改造 FinanceDailyTask:使用 Business_Date 口径
- 替换结账单、团购核销、充值等数据的日期归属
- Requirements: 5.2
-
5.3 改造 FinanceRechargeTask:
DATE(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 改造 AssistantDailyTask:
DATE(start_use_time)→ 营业日归属表达式- Requirements: 5.6
-
6.2 改造 AssistantOrderContributionTask:
DATE(pay_time)→ 营业日归属表达式- Requirements: 5.7
-
6.3 改造 AssistantCustomerTask:
DATE(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 改造 MemberVisitTask:
DATE(pay_time)、DATE(start_use_time)、DATE(ledger_end_time)→ 营业日归属表达式- Requirements: 5.11
-
7.2 改造 MemberConsumptionTask:
DATE(pay_time)和DATE(create_time)→ 营业日归属表达式- Requirements: 5.12
-
7.3 改造 GoodsStockDailyTask:
DATE(fetched_at)→ 营业日归属表达式- Requirements: 5.13
-
7.4 改造 GoodsStockWeeklyTask:使用 Business_Week 口径聚合库存周报
- Requirements: 5.14
-
7.5 改造 GoodsStockMonthlyTask:使用 Business_Month 口径聚合库存月报
- Requirements: 5.15
-
7.6 改造 SpendingPowerIndexTask:
DATE(pay_time)→ 营业日归属表达式- Requirements: 5.16
-
7.7 改造 MemberIndexBase:
DATE(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,如有问题请向用户确认。
- 确保所有 18 个 DWS 任务的 SQL 改造完成,
-
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_range、business_week_range、business_month_range - 从
Shared_DateTime_Utils导入,禁止重复实现 - Requirements: 6.2, 6.3, 6.4, 6.5
- 在后端需要计算"今日/本周/本月"时间范围的 API 中,导入并使用
-
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..l4和mv_dws_assistant_daily_detail_l1..l4的CURRENT_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 不可用时使用默认值 8,
console.warn输出警告 - Requirements: 7.1, 7.2, 7.4, 7.5
- 在日期选择器组件旁增加 Tooltip 或文字标注:
-
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
- 复用正式 ETL 任务逻辑,使用相同的
- 13.1 创建
-
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.py和env_parser.py已完成,任务 4.1 仅需增加校验逻辑