feat: 添加飞书用户模块和订阅功能支持

- 新增feishu_users模块用于处理飞书用户身份验证和权限管理
- 新增subscriptions模块用于处理订阅相关功能
- 新增personalization模块用于个性化服务
- 在alembic迁移配置中注册新的模型模块
- 在API路由器中添加feishu_users和subscriptions路由
- 实现事件调度服务的改进,包括错误处理和状态更新优化
- 添加飞书命令处理的权限检查机制
- 实现飞书应用票据事件处理
- 改进审计日志记录功能
```
This commit is contained in:
2026-07-27 08:02:17 +08:00
parent db751f03b4
commit d7db84571d
148 changed files with 17110 additions and 765 deletions

View File

@@ -0,0 +1,105 @@
from datetime import UTC, datetime, time
import pytest
from app.modules.subscriptions.constants import SubscriptionScheduleType
from app.modules.subscriptions.services.schedule import (
ScheduleParseError,
is_in_quiet_hours,
next_occurrence,
next_quiet_end,
parse_schedule,
)
NOW = datetime(2026, 7, 26, 0, 0, tzinfo=UTC)
@pytest.mark.parametrize(
("expression", "schedule_type", "next_run_at"),
[
("今天 09:00", SubscriptionScheduleType.ONCE, datetime(2026, 7, 26, 1, 0)),
("明天 9点半", SubscriptionScheduleType.ONCE, datetime(2026, 7, 27, 1, 30)),
(
"2026-08-01 10:15",
SubscriptionScheduleType.ONCE,
datetime(2026, 8, 1, 2, 15),
),
("每天 09:00", SubscriptionScheduleType.DAILY, datetime(2026, 7, 26, 1, 0)),
("工作日 09:00", SubscriptionScheduleType.WEEKDAY, datetime(2026, 7, 27, 1, 0)),
("每周一 09:00", SubscriptionScheduleType.WEEKLY, datetime(2026, 7, 27, 1, 0)),
("每月26号 09:00", SubscriptionScheduleType.MONTHLY, datetime(2026, 7, 26, 1, 0)),
(
"每隔 2 小时",
SubscriptionScheduleType.INTERVAL,
datetime(2026, 7, 26, 2, 0),
),
],
)
def test_parse_supported_chinese_schedules(
expression: str,
schedule_type: str,
next_run_at: datetime,
) -> None:
result = parse_schedule(expression, "Asia/Shanghai", now=NOW)
assert result.schedule_type == schedule_type
assert result.next_run_at == next_run_at
assert result.timezone == "Asia/Shanghai"
def test_monthly_schedule_skips_months_without_the_requested_day() -> None:
result = parse_schedule(
"每月31号 09:00",
"Asia/Shanghai",
now=datetime(2026, 4, 30, 2, 0, tzinfo=UTC),
)
assert result.next_run_at == datetime(2026, 5, 31, 1, 0)
assert (
next_occurrence(
result.schedule_type,
result.schedule_config,
result.timezone,
after=datetime(2026, 5, 31, 1, 0),
)
== datetime(2026, 7, 31, 1, 0)
)
@pytest.mark.parametrize(
"expression",
[
"每隔 14 分钟",
"有空时提醒我",
"今天 07:59",
"2026-02-30 09:00",
"每天 25:00",
],
)
def test_invalid_or_ambiguous_schedule_is_rejected(expression: str) -> None:
with pytest.raises(ScheduleParseError):
parse_schedule(expression, "Asia/Shanghai", now=NOW)
def test_invalid_iana_timezone_is_rejected() -> None:
with pytest.raises(ScheduleParseError, match="Invalid IANA timezone"):
parse_schedule("每天 09:00", "Mars/Olympus", now=NOW)
def test_quiet_hours_support_cross_midnight_windows() -> None:
current = datetime(2026, 7, 26, 15, 30, tzinfo=UTC)
assert is_in_quiet_hours(
current,
"Asia/Shanghai",
time(22, 0),
time(7, 0),
)
assert next_quiet_end(
current,
"Asia/Shanghai",
time(22, 0),
time(7, 0),
) == datetime(2026, 7, 26, 23, 0)