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:
@@ -1,7 +1,5 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -9,6 +7,7 @@ from sqlalchemy import select
|
||||
from app.models.subscription import Subscription
|
||||
from app.models.user import User
|
||||
from app.config import settings
|
||||
from app.services.wechat_pay import WeChatPayService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,10 +17,22 @@ PLANS = {
|
||||
"enterprise": {"price": 399, "duration_days": 30},
|
||||
}
|
||||
|
||||
PLAN_DESCRIPTIONS = {
|
||||
"pro": "TradeMate Pro 版会员",
|
||||
"enterprise": "TradeMate 企业版会员",
|
||||
}
|
||||
|
||||
|
||||
class PaymentService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
self._wxpay = None
|
||||
|
||||
@property
|
||||
def wxpay(self) -> Optional[WeChatPayService]:
|
||||
if self._wxpay is None and settings.WECHAT_PAY_MCH_ID:
|
||||
self._wxpay = WeChatPayService()
|
||||
return self._wxpay
|
||||
|
||||
async def get_plans(self) -> Dict[str, Any]:
|
||||
return {
|
||||
@@ -85,7 +96,8 @@ class PaymentService:
|
||||
"auto_renew": sub.auto_renew if sub else False,
|
||||
}
|
||||
|
||||
async def create_order(self, user_id: str, plan: str) -> Dict[str, Any]:
|
||||
async def create_order(self, user_id: str, plan: str,
|
||||
pay_type: str = "jsapi") -> Dict[str, Any]:
|
||||
if plan not in PLANS:
|
||||
raise ValueError(f"Invalid plan: {plan}")
|
||||
|
||||
@@ -98,8 +110,13 @@ class PaymentService:
|
||||
await self.db.flush()
|
||||
return {"status": "ok", "plan": plan, "amount": 0}
|
||||
|
||||
from app.config import settings
|
||||
result = await self.db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
order_id = f"ORD{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{user_id[-6:]}"
|
||||
description = PLAN_DESCRIPTIONS.get(plan, f"TradeMate {plan}")
|
||||
|
||||
sub = Subscription(
|
||||
user_id=user_id,
|
||||
@@ -111,6 +128,51 @@ class PaymentService:
|
||||
self.db.add(sub)
|
||||
await self.db.flush()
|
||||
|
||||
wxpay_available = self.wxpay is not None and settings.WECHAT_PAY_NOTIFY_URL not in (
|
||||
"", "https://example.com/api/v1/payment/notify"
|
||||
)
|
||||
|
||||
if wxpay_available:
|
||||
try:
|
||||
if pay_type == "jsapi":
|
||||
openid = user.wechat_openid
|
||||
if not openid:
|
||||
raise ValueError("用户未绑定微信,请在微信小程序中登录后支付")
|
||||
|
||||
wx_result = await self.wxpay.create_jsapi_order(
|
||||
order_id, openid, int(plan_info["price"] * 100), description
|
||||
)
|
||||
prepay_id = wx_result.get("prepay_id", "")
|
||||
pay_params = self.wxpay.build_jsapi_pay_params(prepay_id)
|
||||
return {
|
||||
"status": "pending",
|
||||
"order_id": order_id,
|
||||
"plan": plan,
|
||||
"amount": plan_info["price"],
|
||||
"currency": "CNY",
|
||||
"pay_type": "jsapi",
|
||||
"pay_params": pay_params,
|
||||
}
|
||||
|
||||
elif pay_type == "native":
|
||||
wx_result = await self.wxpay.create_native_order(
|
||||
order_id, int(plan_info["price"] * 100), description
|
||||
)
|
||||
code_url = wx_result.get("code_url", "")
|
||||
return {
|
||||
"status": "pending",
|
||||
"order_id": order_id,
|
||||
"plan": plan,
|
||||
"amount": plan_info["price"],
|
||||
"currency": "CNY",
|
||||
"pay_type": "native",
|
||||
"code_url": code_url,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"WeChat Pay order failed: {e}")
|
||||
raise ValueError(f"支付创建失败: {str(e)}")
|
||||
|
||||
# 开发环境回退:生成模拟支付参数
|
||||
pay_params = {
|
||||
"appId": settings.WECHAT_APP_ID or "",
|
||||
"timeStamp": str(int(datetime.utcnow().timestamp())),
|
||||
@@ -121,13 +183,13 @@ class PaymentService:
|
||||
sign_str = "&".join(f"{k}={v}" for k, v in sorted(pay_params.items()))
|
||||
sign_str += f"&key={settings.SECRET_KEY}"
|
||||
pay_params["paySign"] = hashlib.md5(sign_str.encode()).hexdigest().upper()
|
||||
|
||||
return {
|
||||
"status": "pending",
|
||||
"order_id": order_id,
|
||||
"plan": plan,
|
||||
"amount": plan_info["price"],
|
||||
"currency": "CNY",
|
||||
"pay_type": pay_type,
|
||||
"pay_params": pay_params,
|
||||
}
|
||||
|
||||
@@ -155,4 +217,4 @@ class PaymentService:
|
||||
return True
|
||||
|
||||
|
||||
payment_service = PaymentService
|
||||
payment_service = PaymentService
|
||||
|
||||
Reference in New Issue
Block a user