mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 08:14:32 +00:00
Implement comprehensive topup billing system with user history viewing and admin management capabilities.
## Features Added
### Frontend
- Add topup history modal with paginated billing records
- Display order details: trade number, payment method, amount, money, status, create time
- Implement empty state with proper illustrations
- Add payment method column with localized display (Stripe, Alipay, WeChat)
- Add admin manual completion feature for pending orders
- Add Coins icon for recharge amount display
- Integrate "Bills" button in RechargeCard header
- Optimize code quality by using shared utility functions (isAdmin)
- Extract constants for status and payment method mappings
- Use React.useMemo for performance optimization
### Backend
- Create GET `/api/user/topup/self` endpoint for user topup history with pagination
- Create POST `/api/user/topup/complete` endpoint for admin manual order completion
- Add `payment_method` field to TopUp model for tracking payment types
- Implement `GetUserTopUps` method with proper pagination and ordering
- Implement `ManualCompleteTopUp` with transaction safety and row-level locking
- Add application-level mutex locks to prevent concurrent order processing
- Record payment method in Epay and Stripe payment flows
- Ensure idempotency and data consistency with proper error handling
### Internationalization
- Add i18n keys for Chinese (zh), English (en), and French (fr)
- Support for billing-related UI text and status messages
## Technical Improvements
- Use database transactions with FOR UPDATE row-level locking
- Implement sync.Map-based mutex for order-level concurrency control
- Proper error handling and user-friendly toast notifications
- Follow existing codebase patterns for empty states and modals
- Maintain code quality with extracted render functions and constants
## Files Changed
- Backend: controller/topup.go, controller/topup_stripe.go, model/topup.go, router/api-router.go
- Frontend: web/src/components/topup/modals/TopupHistoryModal.jsx (new), web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx
- i18n: web/src/i18n/locales/{zh,en,fr}.json
275 lines
7.4 KiB
JavaScript
275 lines
7.4 KiB
JavaScript
/*
|
||
Copyright (C) 2025 QuantumNous
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU Affero General Public License as
|
||
published by the Free Software Foundation, either version 3 of the
|
||
License, or (at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU Affero General Public License for more details.
|
||
|
||
You should have received a copy of the GNU Affero General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
For commercial licensing, please contact support@quantumnous.com
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { SecureVerificationService } from '../../services/secureVerification';
|
||
import { showError, showSuccess } from '../../helpers';
|
||
import { isVerificationRequiredError } from '../../helpers/secureApiCall';
|
||
|
||
/**
|
||
* 通用安全验证 Hook
|
||
* @param {Object} options - 配置选项
|
||
* @param {Function} options.onSuccess - 验证成功回调
|
||
* @param {Function} options.onError - 验证失败回调
|
||
* @param {string} options.successMessage - 成功提示消息
|
||
* @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true
|
||
*/
|
||
export const useSecureVerification = ({
|
||
onSuccess,
|
||
onError,
|
||
successMessage,
|
||
autoReset = true,
|
||
} = {}) => {
|
||
const { t } = useTranslation();
|
||
|
||
// 验证方式可用性状态
|
||
const [verificationMethods, setVerificationMethods] = useState({
|
||
has2FA: false,
|
||
hasPasskey: false,
|
||
passkeySupported: false,
|
||
});
|
||
|
||
// 模态框状态
|
||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||
|
||
// 当前验证状态
|
||
const [verificationState, setVerificationState] = useState({
|
||
method: null, // '2fa' | 'passkey'
|
||
loading: false,
|
||
code: '',
|
||
apiCall: null,
|
||
});
|
||
|
||
// 检查可用的验证方式
|
||
const checkVerificationMethods = useCallback(async () => {
|
||
const methods =
|
||
await SecureVerificationService.checkAvailableVerificationMethods();
|
||
setVerificationMethods(methods);
|
||
return methods;
|
||
}, []);
|
||
|
||
// 初始化时检查验证方式
|
||
useEffect(() => {
|
||
checkVerificationMethods();
|
||
}, [checkVerificationMethods]);
|
||
|
||
// 重置状态
|
||
const resetState = useCallback(() => {
|
||
setVerificationState({
|
||
method: null,
|
||
loading: false,
|
||
code: '',
|
||
apiCall: null,
|
||
});
|
||
setIsModalVisible(false);
|
||
}, []);
|
||
|
||
// 开始验证流程
|
||
const startVerification = useCallback(
|
||
async (apiCall, options = {}) => {
|
||
const { preferredMethod, title, description } = options;
|
||
|
||
// 检查验证方式
|
||
const methods = await checkVerificationMethods();
|
||
|
||
if (!methods.has2FA && !methods.hasPasskey) {
|
||
const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作');
|
||
showError(errorMessage);
|
||
onError?.(new Error(errorMessage));
|
||
return false;
|
||
}
|
||
|
||
// 设置默认验证方式
|
||
let defaultMethod = preferredMethod;
|
||
if (!defaultMethod) {
|
||
if (methods.hasPasskey && methods.passkeySupported) {
|
||
defaultMethod = 'passkey';
|
||
} else if (methods.has2FA) {
|
||
defaultMethod = '2fa';
|
||
}
|
||
}
|
||
|
||
setVerificationState((prev) => ({
|
||
...prev,
|
||
method: defaultMethod,
|
||
apiCall,
|
||
title,
|
||
description,
|
||
}));
|
||
setIsModalVisible(true);
|
||
|
||
return true;
|
||
},
|
||
[checkVerificationMethods, onError, t],
|
||
);
|
||
|
||
// 执行验证
|
||
const executeVerification = useCallback(
|
||
async (method, code = '') => {
|
||
if (!verificationState.apiCall) {
|
||
showError(t('验证配置错误'));
|
||
return;
|
||
}
|
||
|
||
setVerificationState((prev) => ({ ...prev, loading: true }));
|
||
|
||
try {
|
||
// 先调用验证 API,成功后后端会设置 session
|
||
await SecureVerificationService.verify(method, code);
|
||
|
||
// 验证成功,调用业务 API(此时中间件会通过)
|
||
const result = await verificationState.apiCall();
|
||
|
||
// 显示成功消息
|
||
if (successMessage) {
|
||
showSuccess(successMessage);
|
||
}
|
||
|
||
// 调用成功回调
|
||
onSuccess?.(result, method);
|
||
|
||
// 自动重置状态
|
||
if (autoReset) {
|
||
resetState();
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
showError(error.message || t('验证失败,请重试'));
|
||
onError?.(error);
|
||
throw error;
|
||
} finally {
|
||
setVerificationState((prev) => ({ ...prev, loading: false }));
|
||
}
|
||
},
|
||
[
|
||
verificationState.apiCall,
|
||
successMessage,
|
||
onSuccess,
|
||
onError,
|
||
autoReset,
|
||
resetState,
|
||
t,
|
||
],
|
||
);
|
||
|
||
// 设置验证码
|
||
const setVerificationCode = useCallback((code) => {
|
||
setVerificationState((prev) => ({ ...prev, code }));
|
||
}, []);
|
||
|
||
// 切换验证方式
|
||
const switchVerificationMethod = useCallback((method) => {
|
||
setVerificationState((prev) => ({ ...prev, method, code: '' }));
|
||
}, []);
|
||
|
||
// 取消验证
|
||
const cancelVerification = useCallback(() => {
|
||
resetState();
|
||
}, [resetState]);
|
||
|
||
// 检查是否可以使用某种验证方式
|
||
const canUseMethod = useCallback(
|
||
(method) => {
|
||
switch (method) {
|
||
case '2fa':
|
||
return verificationMethods.has2FA;
|
||
case 'passkey':
|
||
return (
|
||
verificationMethods.hasPasskey &&
|
||
verificationMethods.passkeySupported
|
||
);
|
||
default:
|
||
return false;
|
||
}
|
||
},
|
||
[verificationMethods],
|
||
);
|
||
|
||
// 获取推荐的验证方式
|
||
const getRecommendedMethod = useCallback(() => {
|
||
if (
|
||
verificationMethods.hasPasskey &&
|
||
verificationMethods.passkeySupported
|
||
) {
|
||
return 'passkey';
|
||
}
|
||
if (verificationMethods.has2FA) {
|
||
return '2fa';
|
||
}
|
||
return null;
|
||
}, [verificationMethods]);
|
||
|
||
/**
|
||
* 包装 API 调用,自动处理验证错误
|
||
* 当 API 返回需要验证的错误时,自动弹出验证模态框
|
||
* @param {Function} apiCall - API 调用函数
|
||
* @param {Object} options - 验证选项(同 startVerification)
|
||
* @returns {Promise<any>}
|
||
*/
|
||
const withVerification = useCallback(
|
||
async (apiCall, options = {}) => {
|
||
try {
|
||
// 直接尝试调用 API
|
||
return await apiCall();
|
||
} catch (error) {
|
||
// 检查是否是需要验证的错误
|
||
if (isVerificationRequiredError(error)) {
|
||
// 自动触发验证流程
|
||
await startVerification(apiCall, options);
|
||
// 不抛出错误,让验证模态框处理
|
||
return null;
|
||
}
|
||
// 其他错误继续抛出
|
||
throw error;
|
||
}
|
||
},
|
||
[startVerification],
|
||
);
|
||
|
||
return {
|
||
// 状态
|
||
isModalVisible,
|
||
verificationMethods,
|
||
verificationState,
|
||
|
||
// 方法
|
||
startVerification,
|
||
executeVerification,
|
||
cancelVerification,
|
||
resetState,
|
||
setVerificationCode,
|
||
switchVerificationMethod,
|
||
checkVerificationMethods,
|
||
|
||
// 辅助方法
|
||
canUseMethod,
|
||
getRecommendedMethod,
|
||
withVerification, // 新增:自动处理验证的包装函数
|
||
|
||
// 便捷属性
|
||
hasAnyVerificationMethod:
|
||
verificationMethods.has2FA || verificationMethods.hasPasskey,
|
||
isLoading: verificationState.loading,
|
||
currentMethod: verificationState.method,
|
||
code: verificationState.code,
|
||
};
|
||
};
|