feat: AI routing DB-driven, payment gateway full integration, WeChat mini-program CI/CD

- AI routing rules now stored in system_configs DB table instead of hardcoded config
- Multi-model support via name|model composite key for same-provider routing
- UnifiedPayService with HMAC-SHA256 gateway integration (alipay/wechat)
- Admin payment panel: list, stats, search, filter, refund
- WeChat mini-program CI/CD via miniprogram-ci (v1.0.9)
- Translation quota extended to LLM provider tier
- SearchService with DB-driven provider config (bing/google_cse/searxng)
- Footer cleanup across admin/workspace/uni-app
- Private key excluded from git tracking
This commit is contained in:
TradeMate Dev
2026-06-09 17:19:45 +08:00
parent f17a6ccbac
commit d2736d1ef6
28 changed files with 12368 additions and 267 deletions
+108 -22
View File
@@ -7,6 +7,7 @@ from app.models.analytics import UsageLog
from app.models.customer import Customer
from app.models.quotation import Quotation
from app.models.system_config import SystemConfig
from app.models.search_provider import SearchProvider
from datetime import datetime, timedelta
import logging
@@ -289,13 +290,13 @@ class AdminService:
async def _seed_default_configs(self):
defaults = [
SystemConfig(key="ai_routing", value={
"translate": {"primary": "sensenova", "fallback": ["alibaba-mt", "nvidia"]},
"reply": {"primary": "sensenova", "fallback": ["nvidia"]},
"marketing": {"primary": "sensenova", "fallback": ["nvidia"]},
"extract": {"primary": "sensenova", "fallback": ["nvidia"]},
"quotation": {"primary": "sensenova", "fallback": ["nvidia"]},
"chat": {"primary": "sensenova", "fallback": ["nvidia"]},
}, description="AI 路由规则:各任务的主选/备用供应商"),
"translate": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["阿里翻译|alibaba-mt", "NVIDIA|stepfun-ai/step-3.7-flash"]},
"reply": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"marketing": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"extract": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"quotation": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"chat": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
}, description="AI 路由规则:各任务的主选/备用供应商(按模型名称)"),
SystemConfig(key="feature_guest_mode", value={"enabled": True}, description="游客模式开关"),
SystemConfig(key="feature_wechat_login", value={"enabled": False}, description="微信登录开关"),
SystemConfig(key="feature_registration", value={"enabled": True}, description="新用户注册开关"),
@@ -334,21 +335,13 @@ class AdminService:
result = await self.db.execute(
select(SystemConfig).where(SystemConfig.key == "ai_routing")
)
if not result.scalar_one_or_none():
self.db.add(SystemConfig(
key="ai_routing",
value={
"translate": {"primary": "sensenova", "fallback": ["alibaba-mt", "nvidia"]},
"reply": {"primary": "sensenova", "fallback": ["nvidia"]},
"marketing": {"primary": "sensenova", "fallback": ["nvidia"]},
"extract": {"primary": "sensenova", "fallback": ["nvidia"]},
"quotation": {"primary": "sensenova", "fallback": ["nvidia"]},
"chat": {"primary": "sensenova", "fallback": ["nvidia"]},
},
description="AI 路由规则:各任务的主选/备用供应商",
))
await self.db.flush()
logger.info("Seeded ai_routing config")
existing = result.scalar_one_or_none()
if not existing:
await self._seed_ai_routing()
else:
await self._migrate_routing_names(existing)
await self._seed_search_providers()
result = await self.db.execute(
select(SystemConfig).order_by(SystemConfig.key)
@@ -364,6 +357,99 @@ class AdminService:
for c in configs
]
async def _seed_ai_routing(self):
self.db.add(SystemConfig(
key="ai_routing",
value={
"translate": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["阿里翻译|alibaba-mt", "NVIDIA|stepfun-ai/step-3.7-flash"]},
"reply": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"marketing": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"extract": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"quotation": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
"chat": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
},
description="AI 路由规则:各任务的主选/备用供应商(按模型名称)",
))
await self.db.flush()
logger.info("Seeded ai_routing config")
async def _migrate_routing_names(self, cfg):
"""Migrate routing rules from provider_type to provider name, and from name-only to name|model composite."""
type_to_name = {"sensenova": "Sensenova (商汤)", "nvidia": "NVIDIA",
"alibaba-mt": "阿里翻译", "opencode_go": "Sensenova (商汤)",
"spark": "NVIDIA", "openai": "NVIDIA",
"anthropic": "NVIDIA", "local": "NVIDIA"}
# Build name→model lookup from DB
result = await self.db.execute(
select(SearchProvider.id).limit(1) # dummy check — actually AIProvider
)
from app.models.ai_provider import AIProvider
prov_result = await self.db.execute(
select(AIProvider).where(AIProvider.enabled == True).order_by(AIProvider.priority)
)
name_to_model = {}
for p in prov_result.scalars().all():
key = p.name
if key not in name_to_model:
name_to_model[key] = p.model_name
updated = False
for task, rules in cfg.value.items():
if not isinstance(rules, dict):
continue
primary = rules.get("primary", "")
# Step 1: type → name
if primary in type_to_name:
primary = type_to_name[primary]
updated = True
# Step 2: name → name|model
if "|" not in primary and primary in name_to_model:
primary = f"{primary}|{name_to_model[primary]}"
updated = True
rules["primary"] = primary
fallback = rules.get("fallback", [])
new_fb = []
for fb in fallback:
# Step 1: type → name
if fb in type_to_name:
fb = type_to_name[fb]
updated = True
# Step 2: name → name|model
if "|" not in fb and fb in name_to_model:
fb = f"{fb}|{name_to_model[fb]}"
updated = True
new_fb.append(fb)
rules["fallback"] = new_fb
if updated:
cfg.value = dict(cfg.value)
cfg.updated_at = datetime.utcnow()
await self.db.flush()
logger.info("Migrated ai_routing to composite name|model keys")
async def _seed_search_providers(self):
result = await self.db.execute(
select(func.count(SearchProvider.id))
)
if result.scalar() > 0:
return
import uuid
defaults = [
SearchProvider(id=uuid.uuid4(), name="Bing Search", provider_type="bing",
api_key="", api_endpoint=None, extra_config={},
priority=0, enabled=True),
SearchProvider(id=uuid.uuid4(), name="Google CSE", provider_type="google_cse",
api_key="", api_endpoint=None,
extra_config={"cx": ""},
priority=1, enabled=False),
]
for p in defaults:
self.db.add(p)
await self.db.flush()
logger.info("Seeded %d default search providers", len(defaults))
async def update_config(self, key: str, value: Any) -> Optional[Dict[str, Any]]:
result = await self.db.execute(
select(SystemConfig).where(SystemConfig.key == key)