mirror of
https://github.com/touwaeriol/sub2apipay.git
synced 2026-09-21 05:12:04 +08:00
fix: 旧订单允许退款 + 移除自动退款 + 改为默认扣除余额/订阅开关
- providerInstanceId 为 null 的旧订单默认允许退款申请 - 移除 AUTO_REFUND_ENABLED 自动退款功能 - 改为 DEFAULT_DEDUCT_BALANCE:控制退款对话框默认勾选状态
This commit is contained in:
@@ -164,7 +164,7 @@ describe('GET /api/orders/my - canRefundRequest', () => {
|
||||
expect(data.orders[0].canRefundRequest).toBe(false);
|
||||
});
|
||||
|
||||
it('canRefundRequest = false when providerInstanceId is null', async () => {
|
||||
it('canRefundRequest = true when providerInstanceId is null (legacy orders)', async () => {
|
||||
mockOrderFindMany.mockResolvedValue([
|
||||
{
|
||||
id: 'order-005',
|
||||
@@ -184,7 +184,7 @@ describe('GET /api/orders/my - canRefundRequest', () => {
|
||||
const res = await GET(createRequest());
|
||||
const data = await res.json();
|
||||
|
||||
expect(data.orders[0].canRefundRequest).toBe(false);
|
||||
expect(data.orders[0].canRefundRequest).toBe(true);
|
||||
});
|
||||
|
||||
it('canRefundRequest = false when instance not found in DB', async () => {
|
||||
|
||||
@@ -162,6 +162,7 @@ function AdminContent() {
|
||||
const [refundUserBalance, setRefundUserBalance] = useState<number | undefined>(undefined);
|
||||
const [refundSubRemainingDays, setRefundSubRemainingDays] = useState<number | undefined>(undefined);
|
||||
const [refundWarning, setRefundWarning] = useState<string | undefined>(undefined);
|
||||
const [defaultDeductBalance, setDefaultDeductBalance] = useState(true);
|
||||
const [refundRequireForce, setRefundRequireForce] = useState(false);
|
||||
|
||||
const fetchOrders = useCallback(async () => {
|
||||
@@ -196,6 +197,17 @@ function AdminContent() {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
fetch(`/api/admin/config?token=${token}`)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
const val = data?.configs?.find((c: { key: string }) => c.key === 'DEFAULT_DEDUCT_BALANCE');
|
||||
if (val) setDefaultDeductBalance(val.value === 'true');
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [token]);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||
@@ -482,6 +494,7 @@ function AdminContent() {
|
||||
userBalance={refundUserBalance}
|
||||
subscriptionDays={refundOrder.subscriptionDays ?? undefined}
|
||||
subscriptionRemainingDays={refundSubRemainingDays}
|
||||
defaultDeductBalance={defaultDeductBalance}
|
||||
warning={refundWarning}
|
||||
requireForce={refundRequireForce}
|
||||
onConfirm={handleConfirmRefund}
|
||||
|
||||
@@ -69,8 +69,8 @@ function getTexts(locale: Locale) {
|
||||
allChannels: 'All Channels',
|
||||
sub2apiAdminApiKey: 'Sub2API Admin API Key',
|
||||
sub2apiAdminApiKeyHint: 'Leave empty to use environment variable',
|
||||
autoRefund: 'Auto Refund',
|
||||
autoRefundHint: 'When enabled, user refund requests are executed immediately without admin approval',
|
||||
defaultDeductBalance: 'Default Deduct Balance',
|
||||
defaultDeductBalanceHint: 'When enabled, refund approval defaults to deducting balance/subscription',
|
||||
}
|
||||
: {
|
||||
missingToken: '缺少管理员凭证',
|
||||
@@ -132,8 +132,8 @@ function getTexts(locale: Locale) {
|
||||
allChannels: '全部渠道',
|
||||
sub2apiAdminApiKey: 'Sub2API Admin API Key',
|
||||
sub2apiAdminApiKeyHint: '留空则使用环境变量',
|
||||
autoRefund: '自动退款',
|
||||
autoRefundHint: '开启后,用户提交退款申请会立即执行退款,无需管理员审核',
|
||||
defaultDeductBalance: '默认扣除余额/订阅',
|
||||
defaultDeductBalanceHint: '开启后,退款通过时默认扣除余额/订阅;关闭时默认不扣除',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ function PaymentConfigContent() {
|
||||
const [rcSaving, setRcSaving] = useState(false);
|
||||
const [rcLoadBalanceStrategy, setRcLoadBalanceStrategy] = useState('round-robin');
|
||||
const [rcSub2apiKey, setRcSub2apiKey] = useState('');
|
||||
const [rcAutoRefundEnabled, setRcAutoRefundEnabled] = useState(false);
|
||||
const [rcAutoRefundEnabled, setRcAutoRefundEnabled] = useState(true);
|
||||
|
||||
// Override env
|
||||
const [rcOverrideEnv, setRcOverrideEnv] = useState(false);
|
||||
@@ -327,7 +327,7 @@ function PaymentConfigContent() {
|
||||
if (c.key === 'ORDER_TIMEOUT_MINUTES') setRcOrderTimeout(c.value);
|
||||
if (c.key === 'LOAD_BALANCE_STRATEGY') setRcLoadBalanceStrategy(c.value || 'round-robin');
|
||||
if (c.key === 'SUB2API_ADMIN_API_KEY') setRcSub2apiKey(/\*{4,}/.test(c.value) ? '' : c.value);
|
||||
if (c.key === 'AUTO_REFUND_ENABLED') setRcAutoRefundEnabled(c.value === 'true');
|
||||
if (c.key === 'DEFAULT_DEDUCT_BALANCE') setRcAutoRefundEnabled(c.value === 'true');
|
||||
}
|
||||
setRcOverrideEnv(hasOverride);
|
||||
setRcOverrideSaved(hasOverride);
|
||||
@@ -577,7 +577,7 @@ function PaymentConfigContent() {
|
||||
label: '余额充值禁用',
|
||||
},
|
||||
{
|
||||
key: 'AUTO_REFUND_ENABLED',
|
||||
key: 'DEFAULT_DEDUCT_BALANCE',
|
||||
value: rcAutoRefundEnabled ? 'true' : 'false',
|
||||
group: 'payment',
|
||||
label: '自动退款开关',
|
||||
@@ -775,9 +775,11 @@ function PaymentConfigContent() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Toggle value={rcAutoRefundEnabled} onChange={() => setRcAutoRefundEnabled(!rcAutoRefundEnabled)} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-slate-700'}`}>{t.autoRefund}</span>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-slate-700'}`}>{t.defaultDeductBalance}</span>
|
||||
</div>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>{t.autoRefundHint}</span>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>
|
||||
{t.defaultDeductBalanceHint}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cancel rate limit */}
|
||||
|
||||
@@ -94,7 +94,7 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
||||
'ENABLED_PROVIDERS',
|
||||
'SUB2API_ADMIN_API_KEY',
|
||||
'OVERRIDE_ENV_ENABLED',
|
||||
'AUTO_REFUND_ENABLED',
|
||||
'DEFAULT_DEDUCT_BALANCE',
|
||||
]);
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function GET(request: NextRequest) {
|
||||
const derived = deriveOrderState(item);
|
||||
const instanceRefundEnabled = item.providerInstanceId
|
||||
? (refundEnabledMap.get(item.providerInstanceId) ?? false)
|
||||
: false;
|
||||
: true;
|
||||
return {
|
||||
id: item.id,
|
||||
amount: Number(item.amount),
|
||||
|
||||
@@ -11,6 +11,7 @@ interface RefundDialogProps {
|
||||
subscriptionDays?: number;
|
||||
subscriptionRemainingDays?: number;
|
||||
requestedAmount?: number | null;
|
||||
defaultDeductBalance?: boolean;
|
||||
onConfirm: (reason: string, force: boolean, deductBalance: boolean, amount?: number) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
warning?: string;
|
||||
@@ -27,6 +28,7 @@ export default function RefundDialog({
|
||||
subscriptionDays,
|
||||
subscriptionRemainingDays,
|
||||
requestedAmount,
|
||||
defaultDeductBalance = true,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
warning,
|
||||
@@ -37,7 +39,7 @@ export default function RefundDialog({
|
||||
const [reason, setReason] = useState('');
|
||||
const [refundAmount, setRefundAmount] = useState((requestedAmount ?? amount).toFixed(2));
|
||||
const [force, setForce] = useState(false);
|
||||
const [deductBalance, setDeductBalance] = useState(true);
|
||||
const [deductBalance, setDeductBalance] = useState(defaultDeductBalance);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const currency = '¥';
|
||||
|
||||
@@ -1173,7 +1173,6 @@ export async function requestRefund(input: RefundRequestInput): Promise<{ succes
|
||||
);
|
||||
}
|
||||
|
||||
const autoRefundEnabled = (await getSystemConfig('AUTO_REFUND_ENABLED')) === 'true';
|
||||
const normalizedReason = input.reason?.trim() || null;
|
||||
|
||||
const updated = await prisma.order.updateMany({
|
||||
@@ -1203,21 +1202,11 @@ export async function requestRefund(input: RefundRequestInput): Promise<{ succes
|
||||
amount: refundAmount,
|
||||
reason: normalizedReason,
|
||||
requestedBy: input.userId,
|
||||
autoRefundEnabled,
|
||||
}),
|
||||
operator: `user:${input.userId}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (autoRefundEnabled) {
|
||||
return processRefund({
|
||||
orderId: input.orderId,
|
||||
amount: refundAmount,
|
||||
reason: normalizedReason || undefined,
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user