From ffc56aa51111cae17eff0019ed131595cdecc8d2 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Agent Date: Fri, 7 Aug 2026 14:57:30 +0800 Subject: [PATCH 1/3] =?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() -- 2.54.0 From 9ca7ae89b57be40d3f25da506ef4e9cc5a9c41e9 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 7 Aug 2026 06:59:50 +0000 Subject: [PATCH 2/3] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_quota.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_quota.py b/tests/unit/test_quota.py index ec9952186..e0313761c 100644 --- a/tests/unit/test_quota.py +++ b/tests/unit/test_quota.py @@ -423,11 +423,13 @@ class TestProTier: 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 -- 2.54.0 From b1f4b1d0345e7258cec6aff0210b6dbcc838c981 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Agent Date: Fri, 7 Aug 2026 15:08:47 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=20AI=20Code=20Rev?= =?UTF-8?q?iew=20=E9=98=BB=E5=A1=9E=E7=BA=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. pro 套餐改用别名引用 premium,避免硬编码重复(DRY 原则) 2. 恢复 get_tier() 返回 Optional[QuotaTier],未知套餐返回 None 3. 恢复 get_limit() 对未知套餐返回 0(默认拒绝策略) 4. 同步恢复所有单元测试的原始断言逻辑 --- packages/domain/quota.py | 30 +++++++++--------------------- tests/unit/domain/test_quota.py | 14 ++++++-------- tests/unit/test_quota.py | 12 +++++------- tests/unit/test_quota_domain.py | 6 ++---- 4 files changed, 22 insertions(+), 40 deletions(-) diff --git a/packages/domain/quota.py b/packages/domain/quota.py index 681183463..0ea9e573b 100644 --- a/packages/domain/quota.py +++ b/packages/domain/quota.py @@ -100,23 +100,9 @@ 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, - }, - ), } +# pro 套餐与 premium 配额相同,使用别名引用避免重复维护 +QUOTA_TIERS["pro"] = QUOTA_TIERS["premium"] class QuotaWarningLevel: @@ -191,13 +177,15 @@ class QuotaRegistry: for tier in self._tiers.values(): tier.limits.setdefault(dimension, 0) - def get_tier(self, plan_name: str) -> QuotaTier: - """获取套餐配额定义,未知套餐 fallback 到 free""" - return self._tiers.get(plan_name, self._tiers["free"]) + def get_tier(self, plan_name: str) -> Optional[QuotaTier]: + """获取套餐配额定义""" + return self._tiers.get(plan_name) def get_limit(self, plan_name: str, dimension: str) -> float: - """获取指定套餐在指定维度的限制,未知套餐 fallback 到 free""" - tier = self._tiers.get(plan_name, self._tiers["free"]) + """获取指定套餐在指定维度的限制""" + tier = self._tiers.get(plan_name) + if tier is None: + return 0 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 1c4660b7e..9a16e89f3 100755 --- a/tests/unit/domain/test_quota.py +++ b/tests/unit/domain/test_quota.py @@ -370,11 +370,9 @@ class TestQuotaRegistry: assert tier.name == "free" def test_get_tier_nonexistent(self): - """不存在的套餐 fallback 到 free""" + """不存在的套餐返回 None""" reg = QuotaRegistry() - tier = reg.get_tier("enterprise") - assert tier is not None - assert tier.name == "free" + assert reg.get_tier("enterprise") is None def test_get_limit_existing(self): """获取已有限制.""" @@ -384,7 +382,7 @@ class TestQuotaRegistry: def test_get_limit_nonexistent_plan(self): """不存在的套餐 fallback 到 free 配额""" reg = QuotaRegistry() - assert reg.get_limit("enterprise", QuotaDimension.STORAGE_GB) == 2 # fallback to free + assert reg.get_limit("enterprise", QuotaDimension.STORAGE_GB) == 0 def test_get_limit_unknown_dimension(self): """未知维度返回 0.""" @@ -508,12 +506,12 @@ class TestQuotaChecker: assert result.usage_percent == 0.0 def test_check_unknown_plan(self): - """未知套餐 fallback 到 free""" + """未知套餐,限制为0.""" checker = QuotaChecker() result = checker.check("enterprise", QuotaDimension.STORAGE_GB, 0) - assert result.limit == 2 # fallback to free tier, storage_gb=2 + assert result.limit == 0 # used=0, limit=0 → 0 < 0 is False → allowed=False - assert result.allowed is True # used=0, limit=2, allowed + assert result.allowed is False 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 e0313761c..e6e93cad8 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") == 2 # fallback to free + assert reg.get_limit("unknown_plan", "storage_gb") == 0 def test_get_tier_free(self): reg = QuotaRegistry() @@ -231,11 +231,9 @@ class TestQuotaRegistry: assert tier.name == "free" def test_get_tier_unknown_returns_none(self): - """未知套餐 fallback 到 free""" + """未知套餐返回 None""" reg = QuotaRegistry() - tier = reg.get_tier("nonexistent") - assert tier is not None - assert tier.name == "free" + assert reg.get_tier("nonexistent") is None def test_register_new_dimension(self): reg = QuotaRegistry() @@ -303,8 +301,8 @@ class TestQuotaChecker: def test_check_unknown_plan(self): checker = QuotaChecker() result = checker.check("unknown", "storage_gb", 1.0) - assert result.allowed is True # fallback to free, limit=2, used=1.0 < 2 - assert result.limit == 2 # fallback to free tier + assert result.limit == 0 + assert not result.allowed def test_check_multiple(self): checker = QuotaChecker() diff --git a/tests/unit/test_quota_domain.py b/tests/unit/test_quota_domain.py index cf4b283b3..d7ea39ff7 100755 --- a/tests/unit/test_quota_domain.py +++ b/tests/unit/test_quota_domain.py @@ -198,9 +198,7 @@ class TestQuotaRegistry: def test_get_tier_unknown(self): reg = QuotaRegistry() - tier = reg.get_tier("unknown_plan") - assert tier is not None - assert tier.name == "free" + assert reg.get_tier("unknown_plan") is None def test_get_limit_known(self): reg = QuotaRegistry() @@ -209,7 +207,7 @@ class TestQuotaRegistry: def test_get_limit_unknown_plan(self): reg = QuotaRegistry() - assert reg.get_limit("unknown", "storage_gb") == 2 # fallback to free tier + assert reg.get_limit("unknown", "storage_gb") == 0 def test_register_new_dimension(self): reg = QuotaRegistry() -- 2.54.0