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

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
+22
View File
@@ -21,6 +21,17 @@ from app.services.payment import (
router = APIRouter(prefix="/payments", tags=["payments"]) 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) @router.post("/recharge", response_model=PaymentOrderOut)
async def recharge( async def recharge(
req: RechargeRequest, req: RechargeRequest,
@@ -30,6 +41,14 @@ async def recharge(
if req.method not in ("wechat", "alipay"): if req.method not in ("wechat", "alipay"):
raise HTTPException(status_code=400, detail="不支持的支付方式") 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( result = await db.execute(
select(RechargePackage).where( select(RechargePackage).where(
RechargePackage.id == req.plan, RechargePackage.id == req.plan,
@@ -40,6 +59,7 @@ async def recharge(
pkg = result.scalar_one_or_none() pkg = result.scalar_one_or_none()
if not pkg: if not pkg:
raise HTTPException(status_code=400, detail="无效的套餐") raise HTTPException(status_code=400, detail="无效的套餐")
try:
order = await create_recharge_order( order = await create_recharge_order(
db, db,
current_user.id, current_user.id,
@@ -49,6 +69,8 @@ async def recharge(
bonus_credits=pkg.bonus_credits, bonus_credits=pkg.bonus_credits,
method=req.method, method=req.method,
) )
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return order 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 Returns the order; for Alipay the ``qr_url`` attribute will be populated
with the scan-to-pay URL. 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 total_credits = credits + bonus_credits
order = PaymentOrder( order = PaymentOrder(
id=generate_id(), id=generate_id(),
@@ -110,10 +126,6 @@ async def create_recharge_order(
db.add(order) db.add(order)
await db.flush() await db.flush()
# Read config from database
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode: if mock_mode:
# Mock: immediately complete payment # Mock: immediately complete payment
order.status = "paid" order.status = "paid"
+4
View File
@@ -319,6 +319,10 @@ export async function getRechargePackages(): Promise<any[]> {
return api.get('/recharge-packages'); 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> { export async function createRechargeOrder(planId: string, method: string = 'wechat'): Promise<any> {
return api.post('/payments/recharge', { plan: planId, method }); return api.post('/payments/recharge', { plan: planId, method });
} }
@@ -27,7 +27,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore'; 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'; import NotificationPopup from '../NotificationPopup';
interface MenuConfig { interface MenuConfig {
@@ -93,6 +93,7 @@ const AppLayout: React.FC = () => {
const [paymentMethod, setPaymentMethod] = useState<string>('alipay'); const [paymentMethod, setPaymentMethod] = useState<string>('alipay');
const [paying, setPaying] = useState(false); const [paying, setPaying] = useState(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
// 监听预览弹窗状态,关闭浮动按钮 // 监听预览弹窗状态,关闭浮动按钮
useEffect(() => { useEffect(() => {
@@ -140,6 +141,12 @@ const AppLayout: React.FC = () => {
getRechargePackages().then(data => { getRechargePackages().then(data => {
setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false)); setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false));
}).catch(() => {}); }).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(); loadNotifications();
}, [user]); }, [user]);
@@ -559,10 +566,18 @@ const AppLayout: React.FC = () => {
</div> </div>
{/* Payment method selection */} {/* Payment method selection */}
{(!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 }}> <div style={{ marginTop: 20, marginBottom: 8 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text> <Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)} <Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}> style={{ display: 'flex', gap: 12 }}>
{enabledMethods.alipay && (
<Radio.Button value="alipay" style={{ <Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px', flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined, borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
@@ -571,6 +586,8 @@ const AppLayout: React.FC = () => {
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} /> <AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button> </Radio.Button>
)}
{enabledMethods.wechat && (
<Radio.Button value="wechat" style={{ <Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px', flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined, borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
@@ -579,12 +596,14 @@ const AppLayout: React.FC = () => {
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} /> <WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button> </Radio.Button>
)}
</Radio.Group> </Radio.Group>
</div> </div>
)}
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}> <div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button> <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 () => { onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan); const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return; if (!plan) return;