fix: 旧订单允许退款 + 移除自动退款 + 改为默认扣除余额/订阅开关

- providerInstanceId 为 null 的旧订单默认允许退款申请
- 移除 AUTO_REFUND_ENABLED 自动退款功能
- 改为 DEFAULT_DEDUCT_BALANCE:控制退款对话框默认勾选状态
This commit is contained in:
erio
2026-04-03 20:36:11 +08:00
parent 7622da5dfb
commit 1bb75b2baa
7 changed files with 31 additions and 25 deletions
@@ -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 () => {
+13
View File
@@ -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}
+11 -9
View File
@@ -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 */}
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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),
+3 -1
View File
@@ -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 = '¥';
-11
View File
@@ -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 };
}