From ffc56aa51111cae17eff0019ed131595cdecc8d2 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Agent Date: Fri, 7 Aug 2026 14:57:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=85=8D=E9=A2=9D=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20pro=20=E5=A5=97=E9=A4=90=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=9C=AA=E7=9F=A5=E5=A5=97=E9=A4=90=20fallback=20?= =?UTF-8?q?=E5=88=B0=20free?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 QUOTA_TIERS 中添加 pro 套餐,配额与 premium 相同 - QuotaRegistry.get_tier() 对未知套餐 fallback 到 free 而非返回 None - QuotaRegistry.get_limit() 同步更新 fallback 逻辑 - 更新所有相关单元测试以匹配新行为 - 新增 TestProTier 测试类验证 pro 套餐正确性 修复问题:用户 subscription_plan='pro' 时所有配额 limit=0 导致创建操作被拦截 --- packages/domain/quota.py | 31 ++++++++++++++++++++-------- tests/unit/domain/test_quota.py | 22 +++++++++++--------- tests/unit/test_quota.py | 36 +++++++++++++++++++++++++++++---- tests/unit/test_quota_domain.py | 9 ++++++--- 4 files changed, 73 insertions(+), 25 deletions(-) diff --git a/packages/domain/quota.py b/packages/domain/quota.py index 3f2f79e9c..681183463 100644 --- a/packages/domain/quota.py +++ b/packages/domain/quota.py @@ -1,9 +1,10 @@ """Quota system with registry pattern. -Three subscription tiers with different limits: +Four subscription tiers with different limits: - free: 2GB storage, 5 videos/month, 3 concurrent, 3 templates, 50 titles, 10 voiceovers, no AI voice - basic: 20GB storage, 30 videos/month, 10 concurrent, 15 templates, 500 titles, 100 voiceovers, AI voice - premium: 100GB storage, 100 videos/month, 20 concurrent, unlimited templates, 500 titles, 100 voiceovers, AI voice +- pro: Same as premium (alias for premium tier) Quota dimensions are registered by modules via the ModuleRegistry, and checked against the user's subscription plan. @@ -99,6 +100,22 @@ QUOTA_TIERS: Dict[str, QuotaTier] = { QuotaDimension.DEDUP_REPORT_ENABLED: 1, }, ), + "pro": QuotaTier( + name="pro", + limits={ + QuotaDimension.STORAGE_GB: 100, + QuotaDimension.VIDEOS_PER_MONTH: 100, + QuotaDimension.MAX_CONCURRENT: 20, + QuotaDimension.MAX_TEMPLATES: float("inf"), # 不限量 + QuotaDimension.MAX_TITLES: 500, + QuotaDimension.MAX_VOICEOVERS: 100, + QuotaDimension.AI_VOICE_ENABLED: 1, + QuotaDimension.AI_VOICE_CREDITS: 500, + QuotaDimension.BATCH_EXPORT_ENABLED: 1, + QuotaDimension.MULTI_PLATFORM_ENABLED: 1, + QuotaDimension.DEDUP_REPORT_ENABLED: 1, + }, + ), } @@ -174,15 +191,13 @@ class QuotaRegistry: for tier in self._tiers.values(): tier.limits.setdefault(dimension, 0) - def get_tier(self, plan_name: str) -> Optional[QuotaTier]: - """获取套餐配额定义""" - return self._tiers.get(plan_name) + def get_tier(self, plan_name: str) -> QuotaTier: + """获取套餐配额定义,未知套餐 fallback 到 free""" + return self._tiers.get(plan_name, self._tiers["free"]) def get_limit(self, plan_name: str, dimension: str) -> float: - """获取指定套餐在指定维度的限制""" - tier = self._tiers.get(plan_name) - if tier is None: - return 0 + """获取指定套餐在指定维度的限制,未知套餐 fallback 到 free""" + tier = self._tiers.get(plan_name, self._tiers["free"]) return tier.get_limit(dimension) def list_dimensions(self) -> Dict[str, str]: diff --git a/tests/unit/domain/test_quota.py b/tests/unit/domain/test_quota.py index bda9c05ae..1c4660b7e 100755 --- a/tests/unit/domain/test_quota.py +++ b/tests/unit/domain/test_quota.py @@ -354,13 +354,13 @@ class TestQuotaRegistry: assert len(reg.list_dimensions()) == len(QuotaDimension) def test_list_tiers(self): - """三个套餐等级.""" + """四个套餐等级.""" reg = QuotaRegistry() tiers = reg.list_tiers() + assert "pro" in tiers assert "free" in tiers assert "basic" in tiers - assert "premium" in tiers - assert len(tiers) == 3 + assert len(tiers) == 4 def test_get_tier_existing(self): """获取已有的套餐.""" @@ -370,9 +370,11 @@ class TestQuotaRegistry: assert tier.name == "free" def test_get_tier_nonexistent(self): - """获取不存在的套餐返回 None.""" + """不存在的套餐 fallback 到 free""" reg = QuotaRegistry() - assert reg.get_tier("enterprise") is None + tier = reg.get_tier("enterprise") + assert tier is not None + assert tier.name == "free" def test_get_limit_existing(self): """获取已有限制.""" @@ -380,9 +382,9 @@ class TestQuotaRegistry: assert reg.get_limit("free", QuotaDimension.STORAGE_GB) == 2 def test_get_limit_nonexistent_plan(self): - """不存在的套餐返回 0.""" + """不存在的套餐 fallback 到 free 配额""" reg = QuotaRegistry() - assert reg.get_limit("enterprise", QuotaDimension.STORAGE_GB) == 0 + assert reg.get_limit("enterprise", QuotaDimension.STORAGE_GB) == 2 # fallback to free def test_get_limit_unknown_dimension(self): """未知维度返回 0.""" @@ -506,12 +508,12 @@ class TestQuotaChecker: assert result.usage_percent == 0.0 def test_check_unknown_plan(self): - """未知套餐,限制为0.""" + """未知套餐 fallback 到 free""" checker = QuotaChecker() result = checker.check("enterprise", QuotaDimension.STORAGE_GB, 0) - assert result.limit == 0 + assert result.limit == 2 # fallback to free tier, storage_gb=2 # used=0, limit=0 → 0 < 0 is False → allowed=False - assert result.allowed is False + assert result.allowed is True # used=0, limit=2, allowed assert result.warning_level == QuotaWarningLevel.NORMAL def test_check_unknown_dimension(self): diff --git a/tests/unit/test_quota.py b/tests/unit/test_quota.py index 76c4e18b1..ec9952186 100644 --- a/tests/unit/test_quota.py +++ b/tests/unit/test_quota.py @@ -222,7 +222,7 @@ class TestQuotaRegistry: def test_get_limit_unknown_plan_returns_zero(self): reg = QuotaRegistry() - assert reg.get_limit("unknown_plan", "storage_gb") == 0 + assert reg.get_limit("unknown_plan", "storage_gb") == 2 # fallback to free def test_get_tier_free(self): reg = QuotaRegistry() @@ -231,8 +231,11 @@ class TestQuotaRegistry: assert tier.name == "free" def test_get_tier_unknown_returns_none(self): + """未知套餐 fallback 到 free""" reg = QuotaRegistry() - assert reg.get_tier("nonexistent") is None + tier = reg.get_tier("nonexistent") + assert tier is not None + assert tier.name == "free" def test_register_new_dimension(self): reg = QuotaRegistry() @@ -300,8 +303,8 @@ class TestQuotaChecker: def test_check_unknown_plan(self): checker = QuotaChecker() result = checker.check("unknown", "storage_gb", 1.0) - assert result.allowed is False - assert result.limit == 0 + assert result.allowed is True # fallback to free, limit=2, used=1.0 < 2 + assert result.limit == 2 # fallback to free tier def test_check_multiple(self): checker = QuotaChecker() @@ -412,3 +415,28 @@ class TestGlobalSingletons: result = quota_checker.check("free", "storage_gb", 1.0) assert result.allowed is True assert result.limit == 2 + + +class TestProTier: + """Pro 套餐专项测试""" + + def test_pro_tier_exists(self): + """pro 套餐存在于 QUOTA_TIERS""" + from packages.domain.quota import QUOTA_TIERS + assert "pro" in QUOTA_TIERS + + def test_pro_tier_same_as_premium(self): + """pro 套餐配额与 premium 完全一致""" + from packages.domain.quota import QUOTA_TIERS + pro = QUOTA_TIERS["pro"] + premium = QUOTA_TIERS["premium"] + assert pro.limits == premium.limits + + def test_pro_tier_get_limit(self): + """pro 套餐各维度配额正确""" + reg = QuotaRegistry() + assert reg.get_limit("pro", "storage_gb") == 100 + assert reg.get_limit("pro", "videos_per_month") == 100 + assert reg.get_limit("pro", "max_concurrent") == 20 + assert reg.get_limit("pro", "max_titles") == 500 + assert reg.get_limit("pro", "ai_voice_enabled") == 1 diff --git a/tests/unit/test_quota_domain.py b/tests/unit/test_quota_domain.py index 912d12b2c..cf4b283b3 100755 --- a/tests/unit/test_quota_domain.py +++ b/tests/unit/test_quota_domain.py @@ -185,9 +185,10 @@ class TestQuotaRegistry: reg = QuotaRegistry() tiers = reg.list_tiers() assert "free" in tiers + assert "pro" in tiers assert "basic" in tiers assert "premium" in tiers - assert len(tiers) == 3 + assert len(tiers) == 4 def test_get_tier_existing(self): reg = QuotaRegistry() @@ -197,7 +198,9 @@ class TestQuotaRegistry: def test_get_tier_unknown(self): reg = QuotaRegistry() - assert reg.get_tier("unknown_plan") is None + tier = reg.get_tier("unknown_plan") + assert tier is not None + assert tier.name == "free" def test_get_limit_known(self): reg = QuotaRegistry() @@ -206,7 +209,7 @@ class TestQuotaRegistry: def test_get_limit_unknown_plan(self): reg = QuotaRegistry() - assert reg.get_limit("unknown", "storage_gb") == 0 + assert reg.get_limit("unknown", "storage_gb") == 2 # fallback to free tier def test_register_new_dimension(self): reg = QuotaRegistry()