feat: WeChat Pay integration, translation quota management, login UX fixes

- WeChat Pay APIv3 integration (JSAPI + Native) with cert-based auth
- TranslationQuota model + admin management UI (配额 tab)
- Alibaba MT provider now checks quota before translation
- Fix: admin tabs scrollable on mobile, remove header-card
- Fix: profile/login navigation - logout stays on profile, login returns to profile
- Fix: login form now visible by default (no extra click to show)
- Fix: home page translate link uses navigateTo (was switchTab to non-tabBar page)
- Add .coverage and apiclient_key.pem to gitignore
This commit is contained in:
TradeMate Dev
2026-05-20 18:30:12 +08:00
parent a60aac4638
commit c397740748
22 changed files with 828 additions and 35 deletions
+2 -1
View File
@@ -6,5 +6,6 @@ from .spark import SparkProvider
from .sensenova import SensenovaProvider
from .opencode_go import OpencodeGoProvider
from .nvidia import NvidiaProvider
from .alibaba import AlibabaMTProvider
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider"]
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider", "AlibabaMTProvider"]
+98
View File
@@ -0,0 +1,98 @@
from typing import Dict, Any, Optional
from aliyunsdkcore.client import AcsClient
from aliyunsdkalimt.request.v20181012 import TranslateGeneralRequest, TranslateECommerceRequest
from app.services.translation_quota import TranslationQuotaService
from app.database import AsyncSessionLocal
import asyncio
import json
import logging
logger = logging.getLogger(__name__)
ALIBABA_LANG_MAP = {
"zh": "zh", "en": "en", "ja": "ja", "ko": "ko",
"fr": "fr", "de": "de", "es": "es", "pt": "pt",
"ru": "ru", "ar": "ar", "th": "th", "vi": "vi",
"id": "id", "ms": "ms", "tl": "tl", "hi": "hi",
}
class AlibabaMTProvider:
def __init__(self, access_key_id: str, access_key_secret: str,
region_id: str = "cn-hangzhou"):
self.client = AcsClient(access_key_id, access_key_secret, region_id)
self._name = "alibaba-mt"
async def translate(self, text: str, source_lang: Optional[str],
target_lang: str, context: Optional[str] = None) -> Dict[str, Any]:
src = source_lang if source_lang and source_lang != "auto" else "auto"
tgt = ALIBABA_LANG_MAP.get(target_lang[:2].lower(), "en")
async with AsyncSessionLocal() as db:
quota_svc = TranslationQuotaService(db)
for version in ("ecommerce", "general"):
if not await quota_svc.check_quota(version):
logger.info(f"Quota [{version}] exhausted or disabled, trying next")
continue
result = await self._do_translate(version, text, src, tgt)
if result and result.get("translated_text"):
await quota_svc.consume(version, len(text))
await db.commit()
result["provider"] = f"{self.name}-{version}"
return result
raise Exception("Alibaba MT: both versions quota exhausted or API failed")
async def _do_translate(self, version: str, text: str, src: str,
tgt: str) -> Optional[Dict[str, Any]]:
try:
if version == "ecommerce":
req = TranslateECommerceRequest.TranslateECommerceRequest()
else:
req = TranslateGeneralRequest.TranslateGeneralRequest()
req.set_FormatType("text")
req.set_Scene(version)
req.set_SourceLanguage(src)
req.set_TargetLanguage(tgt)
req.set_SourceText(text)
loop = asyncio.get_event_loop()
body = await loop.run_in_executor(None, self.client.do_action_with_exception, req)
resp = json.loads(body)
data = resp.get("Data", {})
translated = data.get("Translated", "")
detected = data.get("DetectedLanguage", src)
if translated:
logger.info(f"Alibaba MT [{version}] ok: {text[:20]}... -> {translated[:20]}...")
return {
"translated_text": translated,
"provider": f"{self.name}-{version}",
"detected_source_lang": detected,
"char_count": len(text),
"cost": 0,
}
except Exception as e:
logger.warning(f"Alibaba MT [{version}] failed: {e}")
return None
async def reply(self, *args, **kwargs) -> Dict[str, Any]:
raise NotImplementedError("Alibaba MT does not support reply generation")
async def generate_marketing(self, *args, **kwargs) -> Dict[str, Any]:
raise NotImplementedError("Alibaba MT does not support marketing generation")
async def extract_info(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError("Alibaba MT does not support info extraction")
@property
def name(self) -> str:
return self._name
@property
def cost_per_1k_tokens(self) -> float:
return 0
+11 -1
View File
@@ -1,6 +1,6 @@
from typing import Dict, Any, Optional, List
from app.ai.base import AIProvider
from app.ai.providers import OpenAIProvider, ClaudeProvider, DeepLProvider, LocalProvider, SparkProvider, SensenovaProvider, OpencodeGoProvider, NvidiaProvider
from app.ai.providers import OpenAIProvider, ClaudeProvider, DeepLProvider, LocalProvider, SparkProvider, SensenovaProvider, OpencodeGoProvider, NvidiaProvider, AlibabaMTProvider
from app.config import settings
from app.ai.trade_corpus import TradeCorpus
import logging
@@ -81,6 +81,16 @@ class AIRouter:
except Exception as e:
logger.warning(f"Spark init failed: {e}")
if settings.ALIBABA_ACCESS_KEY_ID and settings.ALIBABA_ACCESS_KEY_SECRET:
try:
self.providers["alibaba-mt"] = AlibabaMTProvider(
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET,
)
logger.info("Alibaba MT provider ready")
except Exception as e:
logger.warning(f"Alibaba MT init failed: {e}")
if settings.LOCAL_MODEL_ENABLED:
try:
self.providers["local"] = LocalProvider(model_url=settings.LOCAL_MODEL_URL)