修改前台动态获取支付是否开启

This commit is contained in:
2026-06-10 11:49:41 +08:00
parent 429a7cf561
commit de494060ad
4 changed files with 94 additions and 37 deletions
+31 -9
View File
@@ -21,6 +21,17 @@ from app.services.payment import (
router = APIRouter(prefix="/payments", tags=["payments"])
@router.get("/methods")
async def get_payment_methods(db: AsyncSession = Depends(get_db)):
"""Return which payment methods are enabled (from admin config)."""
from app.services.payment import _get_payment_configs
configs = await _get_payment_configs(db)
return {
"alipay": configs.get("payment_alipay_enabled", "").lower() == "true",
"wechat": configs.get("payment_wechat_enabled", "").lower() == "true",
}
@router.post("/recharge", response_model=PaymentOrderOut)
async def recharge(
req: RechargeRequest,
@@ -30,6 +41,14 @@ async def recharge(
if req.method not in ("wechat", "alipay"):
raise HTTPException(status_code=400, detail="不支持的支付方式")
# Check if the selected payment method is enabled in admin config
from app.services.payment import _get_payment_configs, _is_mock_mode
configs = await _get_payment_configs(db)
if not _is_mock_mode(configs):
enabled_key = f"payment_{req.method}_enabled"
if configs.get(enabled_key, "").lower() != "true":
raise HTTPException(status_code=400, detail="该支付方式未启用")
result = await db.execute(
select(RechargePackage).where(
RechargePackage.id == req.plan,
@@ -40,15 +59,18 @@ async def recharge(
pkg = result.scalar_one_or_none()
if not pkg:
raise HTTPException(status_code=400, detail="无效的套餐")
order = await create_recharge_order(
db,
current_user.id,
credits=pkg.credits,
price=pkg.price,
label=pkg.name,
bonus_credits=pkg.bonus_credits,
method=req.method,
)
try:
order = await create_recharge_order(
db,
current_user.id,
credits=pkg.credits,
price=pkg.price,
label=pkg.name,
bonus_credits=pkg.bonus_credits,
method=req.method,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return order
+16 -4
View File
@@ -97,6 +97,22 @@ async def create_recharge_order(
Returns the order; for Alipay the ``qr_url`` attribute will be populated
with the scan-to-pay URL.
"""
# Read config from database first
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
# In real mode, validate that the payment method is enabled and configured
if not mock_mode:
enabled_key = f"payment_{method}_enabled"
if db_configs.get(enabled_key, "").lower() != "true":
raise ValueError("该支付方式未启用,请联系管理员")
if method == "alipay":
if not db_configs.get("payment_alipay_app_id") or not db_configs.get("payment_alipay_private_key"):
raise ValueError("支付宝支付未完成配置,请联系管理员")
elif method == "wechat":
if not db_configs.get("payment_wechat_mch_id") or not db_configs.get("payment_wechat_api_key"):
raise ValueError("微信支付未完成配置,请联系管理员")
total_credits = credits + bonus_credits
order = PaymentOrder(
id=generate_id(),
@@ -110,10 +126,6 @@ async def create_recharge_order(
db.add(order)
await db.flush()
# Read config from database
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
# Mock: immediately complete payment
order.status = "paid"
+4
View File
@@ -319,6 +319,10 @@ export async function getRechargePackages(): Promise<any[]> {
return api.get('/recharge-packages');
}
export async function getPaymentMethods(): Promise<{ alipay: boolean; wechat: boolean }> {
return api.get('/payments/methods');
}
export async function createRechargeOrder(planId: string, method: string = 'wechat'): Promise<any> {
return api.post('/payments/recharge', { plan: planId, method });
}
@@ -27,7 +27,7 @@ import {
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, createRechargeOrder, getPaymentOrders, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrders, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import NotificationPopup from '../NotificationPopup';
interface MenuConfig {
@@ -93,6 +93,7 @@ const AppLayout: React.FC = () => {
const [paymentMethod, setPaymentMethod] = useState<string>('alipay');
const [paying, setPaying] = useState(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
// 监听预览弹窗状态,关闭浮动按钮
useEffect(() => {
@@ -140,6 +141,12 @@ const AppLayout: React.FC = () => {
getRechargePackages().then(data => {
setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false));
}).catch(() => {});
getPaymentMethods().then(data => {
setEnabledMethods(data);
// Auto-select the first enabled method
if (data.alipay) setPaymentMethod('alipay');
else if (data.wechat) setPaymentMethod('wechat');
}).catch(() => {});
loadNotifications();
}, [user]);
@@ -559,32 +566,44 @@ const AppLayout: React.FC = () => {
</div>
{/* Payment method selection */}
<div style={{ marginTop: 20, marginBottom: 8 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
</Radio.Group>
</div>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginTop: 20, marginBottom: 8, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
</Typography.Text>
</div>
) : (
<div style={{ marginTop: 20, marginBottom: 8 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
{enabledMethods.alipay && (
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
{enabledMethods.wechat && (
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
</Radio.Group>
</div>
)}
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan} loading={paying}
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying}
onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return;