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
+102
View File
@@ -0,0 +1,102 @@
import logging
from typing import List, Dict, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.search_provider import SearchProvider
logger = logging.getLogger(__name__)
IGNORE_DOMAINS = [
"google.com", "facebook.com", "twitter.com", "instagram.com",
"youtube.com", "reddit.com", "amazon.com", "ebay.com",
"wikipedia.org", "linkedin.com", "pinterest.com", "baidu.com",
"bing.com",
]
class SearchService:
def __init__(self, db: AsyncSession):
self.db = db
async def search(self, query: str, limit: int = 10) -> List[Dict[str, str]]:
providers = await self._get_enabled_providers()
for provider in providers:
try:
return await self._search_provider(provider, query, limit)
except Exception as e:
logger.warning(f"Search provider {provider.provider_type} failed: {e}")
return []
async def _get_enabled_providers(self) -> List[SearchProvider]:
result = await self.db.execute(
select(SearchProvider)
.where(SearchProvider.enabled == True)
.order_by(SearchProvider.priority)
)
return list(result.scalars().all())
async def _search_provider(self, provider: SearchProvider, query: str, limit: int) -> List[Dict[str, str]]:
pt = provider.provider_type
if pt == "searxng":
return await searxng_search(provider.api_endpoint, query, limit)
elif pt == "bing":
return await bing_search(provider.api_key, query, limit)
else:
raise ValueError(f"Unknown provider type: {pt}")
async def searxng_search(endpoint: Optional[str], query: str, limit: int) -> List[Dict[str, str]]:
if not endpoint:
raise ValueError("SearXNG endpoint not configured")
import httpx
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.get(
endpoint.rstrip("/") + "/search",
params={"q": query, "format": "json", "language": "zh-CN,en", "categories": "general"},
headers={"User-Agent": "TradeMate/1.0"},
)
if resp.status_code != 200:
raise ValueError(f"SearXNG returned {resp.status_code}")
data = resp.json()
results = []
for item in (data.get("results", []) if isinstance(data, dict) else data):
url = item.get("url", "")
if any(d in url for d in IGNORE_DOMAINS):
continue
results.append({
"title": (item.get("title") or url)[:100],
"url": url.rstrip("/"),
"snippet": (item.get("content") or item.get("snippet") or "")[:200],
})
if len(results) >= limit:
break
return results
async def bing_search(api_key: Optional[str], query: str, limit: int) -> List[Dict[str, str]]:
if not api_key:
raise ValueError("Bing API key not configured")
import httpx
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.get(
"https://api.bing.microsoft.com/v7.0/search",
params={"q": query, "count": min(limit, 50), "mkt": "en-US", "textFormat": "Raw"},
headers={"Ocp-Apim-Subscription-Key": api_key},
)
if resp.status_code != 200:
raise ValueError(f"Bing returned {resp.status_code}")
data = resp.json()
results = []
for item in data.get("webPages", {}).get("value", []):
url = item.get("url", "")
if any(d in url for d in IGNORE_DOMAINS):
continue
results.append({
"title": (item.get("name") or url)[:100],
"url": url.rstrip("/"),
"snippet": (item.get("snippet") or "")[:200],
})
if len(results) >= limit:
break
return results