Add landing page, referral system, usage quotas, search API management, and yearly pricing

- Separate workspace landing from login for better UX
- Referral system rewards both parties with Pro days
- Quota enforcement prevents abuse without breaking endpoints
- 7-day free trial with auto-downgrade on expiry
- Admin-managed search provider config (SearXNG, Bing)
- 15% discount on annual subscriptions
- MCP search server wrapping opencode search
- Fix discovery module field name mismatch causing 422
This commit is contained in:
TradeMate Dev
2026-05-26 11:40:13 +08:00
parent 52dba37f22
commit bed5c7abef
39 changed files with 1988 additions and 152 deletions
+53 -1
View File
@@ -10,6 +10,11 @@ from app.core.security import hash_password, verify_password, create_access_toke
from pydantic import BaseModel, EmailStr
from datetime import datetime, timedelta
from app.services.admin import AdminService
from app.models.subscription import Subscription
from app.api.v1.referral import apply_referral
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -18,6 +23,7 @@ class RegisterRequest(BaseModel):
phone: str
password: str
username: str = ""
ref_code: str = ""
class LoginResponse(BaseModel):
@@ -47,11 +53,28 @@ async def register(data: RegisterRequest, request: Request, db: AsyncSession = D
phone=data.phone,
username=data.username or data.phone,
password_hash=hash_password(data.password),
tier="free",
tier="pro",
)
db.add(user)
await db.flush()
trial_end = datetime.utcnow() + timedelta(days=settings.TRIAL_DAYS)
sub = Subscription(
user_id=user.id,
plan="pro_trial",
status="active",
started_at=datetime.utcnow(),
expires_at=trial_end,
)
db.add(sub)
if data.ref_code:
try:
from app.api.v1.referral import do_claim_referral
await do_claim_referral(data.ref_code, str(user.id), db)
except Exception as e:
logger.warning(f"Referral claim failed: {e}")
client_ip = request.client.host if request.client else None
await AdminService(db).log_usage(str(user.id), "user.register", {"phone": data.phone}, ip=client_ip)
@@ -89,6 +112,20 @@ async def login(
client_ip = request.client.host if request.client else None
await AdminService(db).log_usage(str(user.id), "user.login", {"login_id": login_id}, ip=client_ip)
if user.tier == "pro":
sub_result = await db.execute(
select(Subscription).where(
Subscription.user_id == user.id,
Subscription.plan == "pro_trial",
Subscription.status == "active",
)
)
trial_sub = sub_result.scalar_one_or_none()
if trial_sub and trial_sub.expires_at and trial_sub.expires_at < datetime.utcnow():
trial_sub.status = "expired"
user.tier = "free"
await db.flush()
return LoginResponse(
access_token=create_access_token({"sub": str(user.id), "tier": user.tier, "role": user.role}),
refresh_token=create_refresh_token({"sub": str(user.id)}),
@@ -178,6 +215,20 @@ async def get_me(
if not user:
raise HTTPException(status_code=404, detail="User not found")
trial_days_left = 0
if user.tier == "pro":
sub_result = await db.execute(
select(Subscription).where(
Subscription.user_id == user.id,
Subscription.plan == "pro_trial",
Subscription.status == "active",
)
)
trial_sub = sub_result.scalar_one_or_none()
if trial_sub and trial_sub.expires_at:
remaining = (trial_sub.expires_at - datetime.utcnow()).days
trial_days_left = max(0, remaining)
return {
"id": str(user.id),
"phone": user.phone,
@@ -186,6 +237,7 @@ async def get_me(
"role": user.role,
"settings": user.settings,
"created_at": user.created_at.isoformat() if user.created_at else None,
"trial_days_left": trial_days_left,
}