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:
+108
-22
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user