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
+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