Skip to content

Commit 84595c8

Browse files
committed
chore(merge): gradlew 可执行位修复合并到 main
2 parents ed72bf0 + e7b47cf commit 84595c8

4 files changed

Lines changed: 106 additions & 1 deletion

File tree

‎android/twa/gradlew‎

100644100755
File mode changed.

‎server/ai-gateway/middleware/auth.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ def extract_tiers(payload: dict) -> tuple[Optional[str], Optional[str]]:
123123
# 不需要认证的路径白名单
124124
# 注意:license_webhook 是支付平台服务端回调(无用户 token),
125125
# 防伪依赖 HMAC 签名 + order_id 查询确认(见 payment_adapter),而非 JWT。
126-
PUBLIC_PATHS = {"/health", "/health/quick", "/health/live", "/api/v1/license/webhook"}
126+
# license/plans 是公开套餐目录,供前端 PlanCompareModal 无需登录拉取。
127+
PUBLIC_PATHS = {"/health", "/health/quick", "/health/live", "/api/v1/license/webhook", "/api/v1/license/plans"}
127128

128129

129130
async def verify_token(request: Request) -> str:

‎server/ai-gateway/routers/license.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,58 @@ class StatusResponse(BaseModel):
6060
licenses: list[dict] = []
6161

6262

63+
class Plan(BaseModel):
64+
id: str
65+
name: str
66+
price: float
67+
period: str # "month" | "year" | "lifetime"
68+
duration_days: int
69+
daily_quota: int
70+
featured: bool = False
71+
badge: str | None = None
72+
models: list[str] = []
73+
multimodal: bool = False
74+
early_access: int = 0
75+
sync_devices: int = 1
76+
savings: str | None = None
77+
78+
79+
class PlanListResponse(BaseModel):
80+
plans: list[Plan]
81+
82+
83+
# ============================================================
84+
# 套餐目录(PlanCompareModal 数据源)
85+
# ============================================================
86+
# @ai-context: 公开套餐目录,供前端 PlanCompareModal 拉取并渲染价格/周期/
87+
# 配额对比;无需登录(auth.PUBLIC_PATHS 白名单)。
88+
89+
PLANS: list[Plan] = [
90+
Plan(
91+
id="pro_monthly", name="Pro 月卡", price=12.0, period="month",
92+
duration_days=30, daily_quota=80, featured=False,
93+
models=["基础模型", "DeepSeek"], multimodal=False, early_access=0, sync_devices=2,
94+
),
95+
Plan(
96+
id="pro_yearly", name="Pro 年卡", price=99.0, period="year",
97+
duration_days=365, daily_quota=80, featured=True, badge="最受欢迎",
98+
models=["基础模型", "DeepSeek"], multimodal=False, early_access=0, sync_devices=2,
99+
savings="省 ¥45",
100+
),
101+
Plan(
102+
id="lifetime", name="终身 Pro", price=199.0, period="lifetime",
103+
duration_days=36500, daily_quota=120, featured=False, badge="终身早鸟价",
104+
models=["全部模型"], multimodal=True, early_access=5, sync_devices=3,
105+
),
106+
]
107+
108+
109+
@router.get("/plans", response_model=PlanListResponse)
110+
async def get_plans():
111+
"""返回当前可购买套餐列表(公开接口,无需登录)。"""
112+
return PlanListResponse(plans=PLANS)
113+
114+
63115
# ============================================================
64116
# 激活码验证逻辑
65117
# ============================================================
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
测试套餐列表 API
3+
4+
覆盖:
5+
- 返回三个套餐(月卡/年卡/终身)
6+
- 各套餐字段正确(价格/周期/时长/推荐标记/多模态)
7+
- 无需认证即可访问
8+
"""
9+
10+
import sys
11+
from pathlib import Path
12+
13+
GATEWAY_ROOT = str(Path(__file__).resolve().parent.parent)
14+
if GATEWAY_ROOT not in sys.path:
15+
sys.path.insert(0, GATEWAY_ROOT)
16+
17+
from fastapi.testclient import TestClient
18+
19+
from main import app
20+
21+
client = TestClient(app)
22+
23+
24+
class TestGetPlans:
25+
"""套餐列表接口(无需认证)"""
26+
27+
def test_returns_three_plans(self):
28+
resp = client.get("/api/v1/license/plans")
29+
assert resp.status_code == 200
30+
data = resp.json()
31+
assert "plans" in data
32+
assert len(data["plans"]) == 3
33+
34+
def test_pro_monthly_fields(self):
35+
resp = client.get("/api/v1/license/plans")
36+
monthly = [p for p in resp.json()["plans"] if p["id"] == "pro_monthly"][0]
37+
assert monthly["price"] == 12.0
38+
assert monthly["period"] == "month"
39+
assert monthly["duration_days"] == 30
40+
41+
def test_pro_yearly_is_featured(self):
42+
resp = client.get("/api/v1/license/plans")
43+
yearly = [p for p in resp.json()["plans"] if p["id"] == "pro_yearly"][0]
44+
assert yearly["featured"] is True
45+
assert yearly["badge"] == "最受欢迎"
46+
assert yearly["savings"] == "省 ¥45"
47+
48+
def test_lifetime_multimodal(self):
49+
resp = client.get("/api/v1/license/plans")
50+
lifetime = [p for p in resp.json()["plans"] if p["id"] == "lifetime"][0]
51+
assert lifetime["multimodal"] is True
52+
assert lifetime["daily_quota"] == 120

0 commit comments

Comments
 (0)