diff --git a/server/app.mjs b/server/app.mjs index 6687479..990bfa3 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -1233,6 +1233,25 @@ export function createAdminApp(services) { }); }); + adminApi.put('/users/:userId/image-quota', requireAdmin, async (req, res) => { + if (!subscriptionService?.setImageQuota) { + return res.status(503).json({ message: '图片额度服务未启用' }); + } + const remaining = req.body?.remaining; + const total = req.body?.total; + const note = String(req.body?.note ?? '').trim(); + const result = await subscriptionService.setImageQuota( + req.params.userId, + { + remaining: remaining === undefined || remaining === null ? null : Math.floor(Number(remaining)), + total: total === undefined || total === null ? null : Math.floor(Number(total)), + }, + { operatorId: req.currentUser.id, note }, + ); + if (!result.ok) return res.status(400).json({ message: result.message }); + res.json(result); + }); + adminApi.post('/users/:userId/image-quota/grant', requireAdmin, async (req, res) => { if (!subscriptionService?.grantImageQuota) { return res.status(503).json({ message: '图片额度服务未启用' }); diff --git a/src/admin/pages/UserDetailPage.tsx b/src/admin/pages/UserDetailPage.tsx index 073b6a5..a7608c1 100644 --- a/src/admin/pages/UserDetailPage.tsx +++ b/src/admin/pages/UserDetailPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { Link, Navigate, useParams } from 'react-router-dom'; -import { getAdminUser, rechargeUser, updateAdminUser, fetchUserImageQuota, grantUserImageQuota } from '../../api/client'; +import { getAdminUser, rechargeUser, updateAdminUser, fetchUserImageQuota, setUserImageQuota } from '../../api/client'; import { CapabilitySettings } from '../../components/CapabilitySettings'; import { PolicySettings } from '../../components/PolicySettings'; import { SkillSettings } from '../../components/SkillSettings'; @@ -25,7 +25,7 @@ export function UserDetailPage() { const [spaceQuotaMb, setSpaceQuotaMb] = useState('5'); const [imageQuota, setImageQuota] = useState(null); const [imageQuotaLoading, setImageQuotaLoading] = useState(false); - const [imageTotal, setImageTotal] = useState(''); + const [imageRemaining, setImageRemaining] = useState(''); const [imageGrantNote, setImageGrantNote] = useState(''); useEffect(() => { @@ -84,12 +84,12 @@ export function UserDetailPage() { }, [user?.id, user?.role]); useEffect(() => { - if (!imageQuota || imageQuota.unlimited || imageQuota.total == null) { - setImageTotal(''); + if (!imageQuota || imageQuota.unlimited || imageQuota.remaining == null) { + setImageRemaining(''); return; } - setImageTotal(String(imageQuota.total)); - }, [imageQuota?.total, imageQuota?.unlimited]); + setImageRemaining(String(imageQuota.remaining)); + }, [imageQuota?.remaining, imageQuota?.unlimited]); const handleRecharge = async (e: React.FormEvent) => { e.preventDefault(); @@ -123,25 +123,19 @@ export function UserDetailPage() { setLocalError('当前为无限额度套餐,无法在此调整'); return; } - const targetTotal = Math.floor(Number(imageTotal)); - if (!Number.isFinite(targetTotal) || targetTotal < 0) { - setLocalError('请输入非负整数总额度'); + const targetRemaining = Math.floor(Number(imageRemaining)); + if (!Number.isFinite(targetRemaining) || targetRemaining < 0) { + setLocalError('请输入非负整数剩余额度'); return; } - if (targetTotal < imageQuota.used) { - setLocalError(`总额度不能低于已用量(${imageQuota.used} 张)`); - return; - } - const targetBonus = targetTotal - imageQuota.limit; - const delta = targetBonus - imageQuota.bonus; - if (delta === 0) { + if (targetRemaining === imageQuota.remaining) { setMessage('额度未变化'); return; } try { - const result = await grantUserImageQuota(user.id, delta, imageGrantNote.trim()); + const result = await setUserImageQuota(user.id, { remaining: targetRemaining }, imageGrantNote.trim()); setImageQuota(result.quota); - setMessage('图片额度已更新'); + setMessage(result.unchanged ? '额度未变化' : '图片额度已更新'); setImageGrantNote(''); } catch (err) { setLocalError(err instanceof Error ? err.message : '图片额度设置失败'); @@ -273,16 +267,16 @@ export function UserDetailPage() {

{imageQuotaSummary || '暂无额度信息(用户可能尚无订阅)'}

)}

- 直接设置本周期图片总额度(套餐额度 + 额外充值)。已用量不变,修改后会反映在「充值」部分。 + 直接设置本周期剩余可用张数。若目标低于当前套餐额度,会自动下调该用户的周期额度上限。

setImageTotal(e.target.value)} + value={imageRemaining} + onChange={(e) => setImageRemaining(e.target.value)} disabled={!imageQuota || imageQuota.unlimited} /> (`/admin-api/users/${userId}/image-quota`); } +export async function setUserImageQuota( + userId: string, + payload: { remaining?: number; total?: number }, + note = '', +) { + return portalFetch<{ + ok: boolean; + unchanged?: boolean; + subscription: AdminSubscription; + quota: ImageQuotaView; + }>(`/admin-api/users/${userId}/image-quota`, { + method: 'PUT', + body: JSON.stringify({ ...payload, note }), + }); +} + export async function grantUserImageQuota(userId: string, delta: number, note = '') { return portalFetch<{ ok: boolean;