修改前台动态获取支付是否开启
This commit is contained in:
@@ -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,15 +59,18 @@ 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="无效的套餐")
|
||||||
order = await create_recharge_order(
|
try:
|
||||||
db,
|
order = await create_recharge_order(
|
||||||
current_user.id,
|
db,
|
||||||
credits=pkg.credits,
|
current_user.id,
|
||||||
price=pkg.price,
|
credits=pkg.credits,
|
||||||
label=pkg.name,
|
price=pkg.price,
|
||||||
bonus_credits=pkg.bonus_credits,
|
label=pkg.name,
|
||||||
method=req.method,
|
bonus_credits=pkg.bonus_credits,
|
||||||
)
|
method=req.method,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,32 +566,44 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment method selection */}
|
{/* Payment method selection */}
|
||||||
<div style={{ marginTop: 20, marginBottom: 8 }}>
|
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
|
||||||
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}>选择支付方式</Typography.Text>
|
<div style={{ marginTop: 20, marginBottom: 8, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
|
||||||
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
|
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
|
||||||
style={{ display: 'flex', gap: 12 }}>
|
⚠️ 暂无可用的支付方式,请联系管理员开启支付功能
|
||||||
<Radio.Button value="alipay" style={{
|
</Typography.Text>
|
||||||
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
|
</div>
|
||||||
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
|
) : (
|
||||||
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
|
<div style={{ marginTop: 20, marginBottom: 8 }}>
|
||||||
}}>
|
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}>选择支付方式</Typography.Text>
|
||||||
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
|
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
|
||||||
支付宝
|
style={{ display: 'flex', gap: 12 }}>
|
||||||
</Radio.Button>
|
{enabledMethods.alipay && (
|
||||||
<Radio.Button value="wechat" 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 === 'wechat' ? '#07c160' : undefined,
|
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
|
||||||
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
|
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
|
||||||
}}>
|
}}>
|
||||||
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
|
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
|
||||||
微信支付
|
支付宝
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
</Radio.Group>
|
)}
|
||||||
</div>
|
{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' }}>
|
<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;
|
||||||
|
|||||||
Reference in New Issue
Block a user