14 KiB
需求文档:业务日分割点机制(Business Day Cutoff)
简介
引入"业务日分割点"机制,将全系统的统计时间口径从自然日/自然周/自然月切换为以可配置的小时值(默认 08:00)为分割点的营业日/营业周/营业月。影响范围覆盖配置层、共享包、ETL 层(ODS→DWD→DWS)、后端 API 层、前端展示层(管理后台、小程序)及数据库层。
术语表
- Business_Day_Cutoff:营业日分割点,一个整数小时值(0–23),定义一个"业务日"的起始时刻。默认值为 8(即 08:00)。
- Business_Date:营业日,从当天
Business_Day_Cutoff时刻到次日Business_Day_Cutoff时刻的时间段所归属的日期。Business_Day_Cutoff之前的时间戳归属前一天。 - Business_Week:营业周,从周一
Business_Day_Cutoff到次周一Business_Day_Cutoff的时间段。 - Business_Month:营业月,从当月1日
Business_Day_Cutoff到次月1日Business_Day_Cutoff的时间段。 - Shared_DateTime_Utils:
packages/shared/src/neozqyy_shared/datetime_utils.py,跨子系统共享的时间工具模块。 - AppConfig:ETL 配置管理器(
apps/etl/connectors/feiqiu/config/settings.py),通过AppConfig.load()加载配置。 - Backend_Config:后端配置模块(
apps/backend/app/config.py),从.env加载环境变量。 - DWS_Task:DWS 层聚合任务,从 DWD 事实表读取数据并按时间维度聚合写入 DWS 汇总表。
- biz_date_sql_expr:
Shared_DateTime_Utils中生成 PostgreSQL 营业日归属 SQL 表达式的函数。 - stat_date:DWS 汇总表中的统计日期字段,存储的是 Business_Date 而非自然日期。
- Admin_Web:管理后台前端(
apps/admin-web/,React + Vite + Ant Design)。 - Miniprogram:微信小程序前端(
apps/miniprogram/)。
需求
需求 1:环境变量配置
用户故事: 作为运维人员,我希望通过 .env 环境变量配置营业日分割点小时值,以便在不修改代码的情况下调整统计时间口径。
验收标准
- THE Root_Env SHALL 定义
BUSINESS_DAY_START_HOUR环境变量,值为 0–23 的整数,默认值为 8 - THE Env_Template SHALL 同步包含
BUSINESS_DAY_START_HOUR的定义及注释说明(日/周/月统计的分割语义) - WHEN
BUSINESS_DAY_START_HOUR的值不在 0–23 范围内时, THEN THE AppConfig SHALL 在加载阶段抛出SystemExit错误并给出明确提示 - WHEN
BUSINESS_DAY_START_HOUR环境变量缺失时, THE AppConfig SHALL 使用默认值 8
需求 2:共享时间工具函数
用户故事: 作为开发者,我希望有一组经过充分测试的共享时间工具函数,以便所有子系统使用统一的营业日归属逻辑。
验收标准
- THE Shared_DateTime_Utils SHALL 提供
business_date(dt, day_start_hour)函数,将任意时间戳归属到对应的 Business_Date - THE Shared_DateTime_Utils SHALL 提供
business_month(dt, day_start_hour)函数,将任意时间戳归属到对应的 Business_Month 首日 - THE Shared_DateTime_Utils SHALL 提供
business_week_monday(dt, day_start_hour)函数,将任意时间戳归属到对应的 Business_Week 的周一日期 - THE Shared_DateTime_Utils SHALL 提供
biz_date_sql_expr(col, day_start_hour)函数,生成 PostgreSQL 营业日归属 SQL 表达式(形如DATE(col - INTERVAL 'N hours')) - WHEN
day_start_hour参数未传入时, THE Shared_DateTime_Utils SHALL 使用DEFAULT_BUSINESS_DAY_START_HOUR(值为 8)作为默认值 - THE Shared_DateTime_Utils SHALL 提供
business_day_range(biz_date, day_start_hour)函数,返回给定 Business_Date 对应的精确时间戳范围(start_dt, end_dt),即(biz_date 当天 day_start_hour:00, biz_date 次日 day_start_hour:00) - THE Shared_DateTime_Utils SHALL 提供
business_week_range(week_monday, day_start_hour)函数,返回给定 Business_Week 周一对应的精确时间戳范围 - THE Shared_DateTime_Utils SHALL 提供
business_month_range(month_first, day_start_hour)函数,返回给定 Business_Month 首日对应的精确时间戳范围 - FOR ALL 合法的 datetime 输入,
business_date的输出 SHALL 满足:business_day_range(business_date(dt, h), h)[0] <= dt < business_day_range(business_date(dt, h), h)[1](往返一致性) - FOR ALL 合法的 datetime 输入,
business_month(dt, h)SHALL 等于business_date(dt, h).replace(day=1)(月归属与日归属一致性) - FOR ALL 合法的 datetime 输入,
business_week_monday(dt, h)SHALL 等于business_date(dt, h) - timedelta(days=business_date(dt, h).weekday())(周归属与日归属一致性)
需求 3:ETL 配置层集成
用户故事: 作为 ETL 开发者,我希望 AppConfig 正确加载并传播 BUSINESS_DAY_START_HOUR,以便 ETL 任务能获取到配置的分割点值。
验收标准
- THE AppConfig SHALL 在
app.business_day_start_hour路径下存储BUSINESS_DAY_START_HOUR的整数值 - THE Env_Parser SHALL 将环境变量
BUSINESS_DAY_START_HOUR映射到app.business_day_start_hour配置路径 - THE AppConfig_Defaults SHALL 将
app.business_day_start_hour的默认值设为 8 - WHEN AppConfig 加载完成后, THE AppConfig SHALL 通过
cfg.get("app.business_day_start_hour")返回正确的整数值
需求 4:ETL DWS 层聚合逻辑
用户故事: 作为数据分析师,我希望 DWS 层的所有日度/周度/月度聚合统计都基于营业日口径,以便统计结果与门店实际营业周期一致。
验收标准
- WHEN DWS_Task 从 DWD 表提取数据时, THE DWS_Task SHALL 使用
biz_date_sql_expr替代DATE()进行日期归属计算 - WHEN DWS_Task 按日聚合时, THE DWS_Task SHALL 使用
DATE(timestamp_col - INTERVAL 'N hours')作为stat_date的分组依据,其中 N 为Business_Day_Cutoff - WHEN DWS_Task 按月聚合时, THE DWS_Task SHALL 使用 Business_Month 口径(当月1日 cutoff 到次月1日 cutoff)
- WHEN DWS_Task 按周聚合时, THE DWS_Task SHALL 使用 Business_Week 口径(周一 cutoff 到次周一 cutoff)
- THE BaseDwsTask.iter_dwd_rows SHALL 使用
biz_date_sql_expr替代DATE()进行日期过滤 - THE BaseDwsTask.get_time_window_range SHALL 返回基于 Business_Date 口径的时间范围
- WHILE ETL 任务运行期间, THE DWS_Task SHALL 从
AppConfig读取app.business_day_start_hour值,禁止硬编码
需求 5:受影响的 DWS 任务全面排查
用户故事: 作为项目负责人,我希望所有使用 DATE() 进行时间归属的 DWS 任务都被排查并改造,确保无遗漏。
验收标准
- THE FinanceBaseTask SHALL 将所有
DATE(pay_time)替换为biz_date_sql_expr("pay_time", cutoff_hour)生成的表达式 - THE FinanceDailyTask SHALL 使用 Business_Date 口径提取和聚合结账单、团购核销、充值等数据
- THE FinanceRechargeTask SHALL 将
DATE(pay_time)替换为营业日归属表达式 - THE FinanceDiscountTask SHALL 使用 Business_Date 口径聚合优惠明细
- THE FinanceIncomeTask SHALL 使用 Business_Date 口径聚合收入结构
- THE AssistantDailyTask SHALL 使用 Business_Date 口径聚合助教日度明细
- THE AssistantOrderContributionTask SHALL 将
DATE(pay_time)替换为营业日归属表达式 - THE AssistantCustomerTask SHALL 将
DATE(start_use_time)替换为营业日归属表达式 - THE AssistantMonthlyTask SHALL 使用 Business_Month 口径聚合助教月度汇总
- THE AssistantFinanceTask SHALL 使用 Business_Date 口径聚合助教财务分析
- THE MemberVisitTask SHALL 将
DATE(pay_time)、DATE(start_use_time)、DATE(ledger_end_time)替换为营业日归属表达式 - THE MemberConsumptionTask SHALL 将
DATE(pay_time)和DATE(create_time)替换为营业日归属表达式 - THE GoodsStockDailyTask SHALL 将
DATE(fetched_at)替换为营业日归属表达式 - THE GoodsStockWeeklyTask SHALL 使用 Business_Week 口径聚合库存周报
- THE GoodsStockMonthlyTask SHALL 使用 Business_Month 口径聚合库存月报
- THE SpendingPowerIndexTask SHALL 将
DATE(pay_time)替换为营业日归属表达式 - THE MemberIndexBase SHALL 将
DATE(pay_time)替换为营业日归属表达式 - THE MvRefreshTask SHALL 确保物化视图刷新的时间过滤条件使用 Business_Date 口径
需求 6:后端 API 层时间范围计算
用户故事: 作为后端开发者,我希望后端 API 在处理"今日/本周/本月"等时间范围查询时使用营业日口径,以便前端展示的数据与 DWS 统计一致。
验收标准
- THE Backend_Config SHALL 加载
BUSINESS_DAY_START_HOUR环境变量并暴露为模块级常量 - WHEN 后端 API 需要计算"今日"时间范围时, THE Backend SHALL 使用
business_day_range函数计算从当天 cutoff 到次日 cutoff 的时间戳范围 - WHEN 后端 API 需要计算"本周"时间范围时, THE Backend SHALL 使用
business_week_range函数计算从本周一 cutoff 到次周一 cutoff 的时间戳范围 - WHEN 后端 API 需要计算"本月"时间范围时, THE Backend SHALL 使用
business_month_range函数计算从本月1日 cutoff 到次月1日 cutoff 的时间戳范围 - THE Backend SHALL 从
Shared_DateTime_Utils导入时间工具函数,禁止在后端重复实现营业日逻辑
需求 7:前端展示层适配
用户故事: 作为前端开发者,我希望管理后台和小程序在展示日期选择器和统计数据时,能正确反映营业日口径,避免用户困惑。
验收标准
- WHEN Admin_Web 展示日期选择器时, THE Admin_Web SHALL 在日期选择器旁标注营业日口径说明(如"营业日:08:00 起")
- WHEN Admin_Web 展示"今日统计"时, THE Admin_Web SHALL 显示的时间范围为当天 cutoff 到次日 cutoff
- WHEN Miniprogram 展示统计数据时, THE Miniprogram SHALL 使用后端 API 返回的基于营业日口径的数据
- THE Admin_Web SHALL 通过后端 API 获取
BUSINESS_DAY_START_HOUR配置值,禁止前端硬编码 - IF 后端 API 返回的
BUSINESS_DAY_START_HOUR配置值不可用, THEN THE Admin_Web SHALL 使用默认值 8 并在控制台输出警告
需求 8:后端配置查询 API
用户故事: 作为前端开发者,我希望有一个 API 端点能返回当前的营业日分割点配置,以便前端动态获取并展示。
验收标准
- THE Backend SHALL 提供一个 API 端点返回当前
BUSINESS_DAY_START_HOUR的值 - WHEN 前端请求该端点时, THE Backend SHALL 返回包含
business_day_start_hour字段的 JSON 响应 - THE Backend SHALL 确保该端点的响应值与 ETL 层使用的
BUSINESS_DAY_START_HOUR值一致(均来源于同一.env配置)
需求 9:数据库层适配
用户故事: 作为 DBA,我希望数据库中的物化视图和 SQL 函数使用营业日口径,以便直接查询数据库时也能获得正确的统计结果。
验收标准
- WHEN 物化视图使用
date_trunc或CURRENT_DATE进行时间过滤时, THE Migration_Script SHALL 将其替换为基于Business_Day_Cutoff的表达式 - THE Migration_Script SHALL 提供一个 PostgreSQL 函数
biz_date(timestamptz, int)用于在 SQL 中直接计算营业日归属 - WHEN 迁移脚本执行后, THE Database SHALL 确保所有物化视图的时间过滤条件使用营业日口径
- THE Migration_Script SHALL 使用日期前缀命名(如
2026-03-XX__add_biz_date_function.sql),遵循项目迁移脚本规范
需求 10:数据迁移与历史数据兼容
用户故事: 作为运维人员,我希望引入营业日机制后,历史数据能被正确重算,确保统计连续性。
验收标准
- THE Migration_Plan SHALL 提供 DWS 历史数据重算脚本,按营业日口径重新聚合所有受影响的 DWS 表
- WHEN 历史数据重算执行时, THE Rebuild_Script SHALL 使用与正式 ETL 任务相同的
Business_Day_Cutoff配置值 - THE Migration_Plan SHALL 记录重算前后的数据行数对比,用于验证重算正确性
- IF 重算过程中发生错误, THEN THE Rebuild_Script SHALL 回滚到重算前的状态并记录错误日志
需求 11:属性测试覆盖
用户故事: 作为测试工程师,我希望营业日归属逻辑有完整的属性测试覆盖,确保边界条件和不变量得到验证。
验收标准
- THE Property_Test SHALL 验证
business_date的往返一致性:对任意 datetime dt,business_day_range(business_date(dt, h), h)的范围包含 dt - THE Property_Test SHALL 验证
business_month与business_date的一致性:business_month(dt, h) == business_date(dt, h).replace(day=1) - THE Property_Test SHALL 验证
business_week_monday与business_date的一致性:business_week_monday(dt, h).weekday() == 0(结果始终为周一) - THE Property_Test SHALL 验证
biz_date_sql_expr的幂等性:对同一输入参数多次调用返回相同结果 - THE Property_Test SHALL 验证边界条件:cutoff 时刻恰好在分割点上的时间戳归属当天,分割点前一秒归属前一天
- THE Property_Test SHALL 验证
business_day_range返回的范围恰好为 24 小时 - THE Property_Test SHALL 验证
business_week_range返回的范围恰好为 7 天(168 小时) - THE Property_Test SHALL 使用 hypothesis 库生成随机 datetime 和 day_start_hour(0–23)进行测试
- FOR ALL
day_start_hour值(0–23), THE Property_Test SHALL 验证business_date函数的单调性:若 dt1 < dt2 且两者在同一 Business_Date 范围内,则business_date(dt1, h) == business_date(dt2, h)
需求 12:运维脚本中的 DATE() 排查
用户故事: 作为运维人员,我希望 scripts/ops/ 和 ETL scripts/ 中使用 DATE() 的运维脚本也被排查,确保调试和排查工具与正式统计口径一致。
验收标准
- THE Ops_Scripts SHALL 排查
scripts/ops/export_bug_report.py中的DATE(trash_time)、DATE(create_time)、DATE(start_use_time)调用,评估是否需要替换为营业日归属表达式 - THE Ops_Scripts SHALL 排查
scripts/ops/etl_consistency_check.py中的日期比较逻辑 - THE ETL_Scripts SHALL 排查
apps/etl/connectors/feiqiu/scripts/debug/debug_blackbox.py中的::date类型转换 - THE ETL_Scripts SHALL 排查
apps/etl/connectors/feiqiu/scripts/run_update.py中的.date()调用和datetime.combine逻辑 - WHEN 运维脚本用于与 DWS 数据对比验证时, THE Ops_Scripts SHALL 使用与 DWS 任务相同的营业日归属逻辑