微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# packages/shared — 跨项目共享包
|
||||
|
||||
`neozqyy-shared`:NeoZQYY Monorepo 的共享工具包,提供业务枚举、金额精度处理和时间工具。
|
||||
`neozqyy-shared`:NeoZQYY Monorepo 的共享工具包,提供业务枚举、金额精度处理、时间工具和其他通用功能。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -57,20 +57,26 @@ total = round_cny(Decimal("123.456")) # Decimal('123.46')
|
||||
|
||||
### `datetime_utils.py` — 时间工具
|
||||
|
||||
默认时区 `Asia/Shanghai`(UTC+8),与业务数据库 `timestamptz` 对齐。
|
||||
默认时区 `Asia/Shanghai`(UTC+8),与业务数据库 `timestamptz` 对齐。支持营业日计算和日期范围生成。
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `now_shanghai() -> datetime` | 获取上海时区当前时间 |
|
||||
| `date_range(start, end) -> list[date]` | 生成日期范围列表(含首尾) |
|
||||
| `get_business_date(dt, start_hour=8) -> date` | 根据营业日分割点计算业务日期 |
|
||||
| `format_business_date(dt, start_hour=8) -> str` | 格式化业务日期为 YYYY-MM-DD |
|
||||
|
||||
用法:
|
||||
```python
|
||||
from neozqyy_shared.datetime_utils import now_shanghai, date_range
|
||||
from datetime import date
|
||||
from neozqyy_shared.datetime_utils import now_shanghai, date_range, get_business_date
|
||||
from datetime import date, datetime
|
||||
|
||||
now = now_shanghai()
|
||||
dates = date_range(date(2026, 1, 1), date(2026, 1, 7)) # 7 天
|
||||
|
||||
# 营业日计算(凌晨 2:00 算前一天,上午 10:00 算当天)
|
||||
biz_date = get_business_date(datetime(2026, 1, 2, 2, 0)) # 2026-01-01
|
||||
biz_date = get_business_date(datetime(2026, 1, 2, 10, 0)) # 2026-01-02
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
@@ -20,6 +20,9 @@ from neozqyy_shared.datetime_utils import (
|
||||
SHANGHAI_TZ,
|
||||
now_shanghai,
|
||||
date_range,
|
||||
business_day_range,
|
||||
business_week_range,
|
||||
business_month_range,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -38,4 +41,7 @@ __all__ = [
|
||||
"SHANGHAI_TZ",
|
||||
"now_shanghai",
|
||||
"date_range",
|
||||
"business_day_range",
|
||||
"business_week_range",
|
||||
"business_month_range",
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
默认时区:Asia/Shanghai(UTC+8),与业务数据库 timestamptz 对齐。
|
||||
"""
|
||||
import calendar
|
||||
from datetime import datetime, date, timedelta
|
||||
from dateutil import tz
|
||||
|
||||
@@ -23,3 +24,94 @@ def date_range(start: date, end: date) -> list[date]:
|
||||
return []
|
||||
days = (end - start).days + 1
|
||||
return [start + timedelta(days=i) for i in range(days)]
|
||||
|
||||
|
||||
# 营业日切点默认值(小时),与 .env BUSINESS_DAY_START_HOUR 对齐
|
||||
DEFAULT_BUSINESS_DAY_START_HOUR = 8
|
||||
|
||||
|
||||
def business_date(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date:
|
||||
"""将时间戳归属到营业日。
|
||||
|
||||
day_start_hour 之前的记录归属前一天。
|
||||
例:day_start_hour=8 时,07:59 归属前一天,08:00 归属当天。
|
||||
"""
|
||||
if dt.hour < day_start_hour:
|
||||
return (dt - timedelta(days=1)).date()
|
||||
return dt.date()
|
||||
|
||||
|
||||
def business_month(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date:
|
||||
"""将时间戳归属到营业月(返回该月第一天)。
|
||||
|
||||
当月1日 day_start_hour 之前的记录归属上月。
|
||||
"""
|
||||
biz_d = business_date(dt, day_start_hour)
|
||||
return biz_d.replace(day=1)
|
||||
|
||||
|
||||
def business_week_monday(dt: datetime, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> date:
|
||||
"""将时间戳归属到营业周(返回该周周一日期)。
|
||||
|
||||
周一 day_start_hour 之前的记录归属上周。
|
||||
"""
|
||||
biz_d = business_date(dt, day_start_hour)
|
||||
return biz_d - timedelta(days=biz_d.weekday())
|
||||
|
||||
|
||||
def biz_date_sql_expr(col: str, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR) -> str:
|
||||
"""生成 PostgreSQL 营业日归属 SQL 表达式。
|
||||
|
||||
返回形如 DATE(col - INTERVAL '8 hours') 的字符串,
|
||||
用于替换原来的 DATE(col)。
|
||||
"""
|
||||
return f"DATE({col} - INTERVAL '{day_start_hour} hours')"
|
||||
|
||||
|
||||
def business_day_range(
|
||||
biz_date: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""返回给定营业日的精确时间戳范围 [start, end)。
|
||||
|
||||
start = biz_date 当天 day_start_hour:00(Asia/Shanghai)
|
||||
end = biz_date 次日 day_start_hour:00(Asia/Shanghai)
|
||||
"""
|
||||
start = datetime(biz_date.year, biz_date.month, biz_date.day,
|
||||
day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ)
|
||||
end = start + timedelta(days=1)
|
||||
return (start, end)
|
||||
|
||||
|
||||
def business_week_range(
|
||||
week_monday: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""返回给定营业周(周一)的精确时间戳范围 [start, end)。
|
||||
|
||||
start = week_monday 当天 day_start_hour:00(Asia/Shanghai)
|
||||
end = week_monday + 7天 day_start_hour:00(Asia/Shanghai)
|
||||
"""
|
||||
start = datetime(week_monday.year, week_monday.month, week_monday.day,
|
||||
day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ)
|
||||
end = start + timedelta(days=7)
|
||||
return (start, end)
|
||||
|
||||
|
||||
def business_month_range(
|
||||
month_first: date, day_start_hour: int = DEFAULT_BUSINESS_DAY_START_HOUR
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""返回给定营业月(首日)的精确时间戳范围 [start, end)。
|
||||
|
||||
start = month_first 当天 day_start_hour:00(Asia/Shanghai)
|
||||
end = 次月1日 day_start_hour:00(Asia/Shanghai)
|
||||
"""
|
||||
start = datetime(month_first.year, month_first.month, month_first.day,
|
||||
day_start_hour, 0, 0, tzinfo=SHANGHAI_TZ)
|
||||
# 计算次月1日
|
||||
if month_first.month == 12:
|
||||
next_month_first = date(month_first.year + 1, 1, 1)
|
||||
else:
|
||||
next_month_first = date(month_first.year, month_first.month + 1, 1)
|
||||
end = datetime(next_month_first.year, next_month_first.month,
|
||||
next_month_first.day, day_start_hour, 0, 0,
|
||||
tzinfo=SHANGHAI_TZ)
|
||||
return (start, end)
|
||||
|
||||
82
packages/shared/src/neozqyy_shared/repo_root.py
Normal file
82
packages/shared/src/neozqyy_shared/repo_root.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""仓库根目录校验工具 — 全仓共享。
|
||||
|
||||
所有使用相对路径(docs/、export/、.kiro/ 等)的脚本,
|
||||
应在入口处调用 ensure_repo_root() 确保 cwd 正确。
|
||||
|
||||
用法:
|
||||
from neozqyy_shared.repo_root import ensure_repo_root
|
||||
ensure_repo_root()
|
||||
|
||||
行为:
|
||||
1. cwd 已是仓库根 → 直接返回
|
||||
2. cwd 不是根但能通过 __main__.__file__ 或调用栈推断 → 自动 chdir + 警告
|
||||
3. 无法定位 → 抛出 RuntimeError
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
# 仓库根的标志文件组合(同时存在才算)
|
||||
_MARKERS = ("pyproject.toml", ".kiro")
|
||||
|
||||
|
||||
def _is_repo_root(p: Path) -> bool:
|
||||
return (p / "pyproject.toml").is_file() and (p / ".kiro").is_dir()
|
||||
|
||||
|
||||
def _find_root_from_file(anchor: Path, max_depth: int = 8) -> Path | None:
|
||||
"""从给定文件路径向上搜索仓库根。"""
|
||||
current = anchor.resolve().parent
|
||||
for _ in range(max_depth):
|
||||
if _is_repo_root(current):
|
||||
return current
|
||||
parent = current.parent
|
||||
if parent == current:
|
||||
break
|
||||
current = parent
|
||||
return None
|
||||
|
||||
|
||||
def ensure_repo_root() -> Path:
|
||||
"""校验并确保 cwd 为仓库根目录。
|
||||
|
||||
Returns:
|
||||
仓库根目录的 Path 对象。
|
||||
|
||||
Raises:
|
||||
RuntimeError: 无法定位仓库根目录。
|
||||
"""
|
||||
cwd = Path.cwd()
|
||||
if _is_repo_root(cwd):
|
||||
return cwd
|
||||
|
||||
# 策略 1:通过 __main__.__file__ 推断(脚本直接运行时可用)
|
||||
import __main__
|
||||
main_file = getattr(__main__, "__file__", None)
|
||||
if main_file:
|
||||
root = _find_root_from_file(Path(main_file))
|
||||
if root:
|
||||
os.chdir(root)
|
||||
warnings.warn(
|
||||
f"cwd 不是仓库根目录,已自动切换: {cwd} → {root}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return root
|
||||
|
||||
# 策略 2:通过本文件位置推断(packages/shared/src/neozqyy_shared/repo_root.py)
|
||||
root = _find_root_from_file(Path(__file__))
|
||||
if root:
|
||||
os.chdir(root)
|
||||
warnings.warn(
|
||||
f"cwd 不是仓库根目录,已自动切换: {cwd} → {root}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return root
|
||||
|
||||
raise RuntimeError(
|
||||
f"无法定位仓库根目录。当前 cwd={cwd}。"
|
||||
f"请在仓库根目录下运行脚本。"
|
||||
)
|
||||
Reference in New Issue
Block a user