添加用户批量限额管理功能

This commit is contained in:
Your Name 2025-12-19 00:24:18 +08:00
parent 6646a7a099
commit ee2d5e90ea
3 changed files with 304 additions and 27 deletions

View File

@ -244,6 +244,12 @@ export function AdminSettingsClient() {
const [userSearch, setUserSearch] = useState("");
const [userPage, setUserPage] = useState(1);
const [userLimit] = useState(10);
// 批量设置出图限额状态
const [showBatchLimitDialog, setShowBatchLimitDialog] = useState(false);
const [batchLimitType, setBatchLimitType] = useState<"all" | "department" | "no_department">("all");
const [batchLimitValue, setBatchLimitValue] = useState<string>("");
const [batchLimitDepartment, setBatchLimitDepartment] = useState<string>("");
// 🆕 生图记录状态
const [imageSearch, setImageSearch] = useState("");
@ -270,6 +276,12 @@ export function AdminSettingsClient() {
`/api/admin/users?page=${userPage}&limit=${userLimit}&search=${encodeURIComponent(userSearch)}`,
fetcher
);
// 获取部门列表(用于批量设置)
const { data: departmentsData } = useSWR<{ departments: string[] }>(
"/api/admin/users/batch-limit",
fetcher
);
const { data: providersData, mutate: mutateProviders } = useSWR<{ providers: AiProvider[] }>("/api/admin/ai-providers", fetcher);
const { data: modelsData, mutate: mutateModels } = useSWR<{ models: AiModel[] }>("/api/admin/ai-models", fetcher);
const { data: storageData, mutate: mutateStorage } = useSWR<{ configs: StorageConfig[] }>("/api/admin/storage", fetcher);
@ -561,6 +573,35 @@ export function AdminSettingsClient() {
}
};
// 批量设置出图限额
const handleBatchSetLimit = async () => {
try {
const response = await fetch("/api/admin/users/batch-limit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: batchLimitType,
limit: batchLimitValue === "" ? null : parseInt(batchLimitValue, 10),
department: batchLimitDepartment,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "批量设置失败");
}
toast.success(data.message);
setShowBatchLimitDialog(false);
setBatchLimitValue("");
setBatchLimitDepartment("");
mutateUsers();
} catch (error) {
toast.error(error instanceof Error ? error.message : "批量设置失败");
}
};
// --- 网站设置逻辑 ---
const handleSaveSiteSettings = async () => {
try {
@ -940,33 +981,44 @@ export function AdminSettingsClient() {
onChange={(e) => handleUserSearch(e.target.value)}
className="max-w-md"
/>
<Button
variant="outline"
size="sm"
onClick={async () => {
try {
toast.info("正在从钉钉同步用户信息,可能需要几分钟...");
const res = await fetch("/api/admin/sync-dingtalk", { method: "POST" });
const data = await res.json();
if (res.ok) {
toast.success(
`同步完成!匹配 ${data.stats.matched}/${data.stats.total} 个用户,` +
`更新 ${data.stats.synced} 个,跳过 ${data.stats.skipped} 个` +
(data.stats.errors > 0 ? `,失败 ${data.stats.errors} 个` : "")
);
mutateUsers(); // 刷新用户列表
} else {
toast.error(data.error || "同步失败");
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowBatchLimitDialog(true)}
className="flex items-center gap-2"
>
<Settings className="h-4 w-4" />
批量设置限额
</Button>
<Button
variant="outline"
size="sm"
onClick={async () => {
try {
toast.info("正在从钉钉同步用户信息,可能需要几分钟...");
const res = await fetch("/api/admin/sync-dingtalk", { method: "POST" });
const data = await res.json();
if (res.ok) {
toast.success(
`同步完成!匹配 ${data.stats.matched}/${data.stats.total} 个用户,` +
`更新 ${data.stats.synced} 个,跳过 ${data.stats.skipped} 个` +
(data.stats.errors > 0 ? `,失败 ${data.stats.errors} 个` : "")
);
mutateUsers(); // 刷新用户列表
} else {
toast.error(data.error || "同步失败");
}
} catch (error) {
toast.error("同步请求失败");
}
} catch (error) {
toast.error("同步请求失败");
}
}}
className="flex items-center gap-2"
>
<RefreshCw className="h-4 w-4" />
同步钉钉用户
</Button>
}}
className="flex items-center gap-2"
>
<RefreshCw className="h-4 w-4" />
同步钉钉用户
</Button>
</div>
</div>
{/* 用户列表 */}
@ -2769,6 +2821,105 @@ export function AdminSettingsClient() {
</TabsContent>
</Tabs>
{/* 批量设置出图限额对话框 */}
<Dialog open={showBatchLimitDialog} onOpenChange={setShowBatchLimitDialog}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>批量设置出图限额</DialogTitle>
</DialogHeader>
<div className="space-y-4 pt-4">
{/* 设置类型选择 */}
<div className="space-y-2">
<Label>设置范围</Label>
<Select
value={batchLimitType}
onValueChange={(value: "all" | "department" | "no_department") => {
setBatchLimitType(value);
setBatchLimitDepartment("");
}}
>
<SelectTrigger>
<SelectValue placeholder="选择设置范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">所有用户</SelectItem>
<SelectItem value="department">按部门设置</SelectItem>
<SelectItem value="no_department">无部门用户</SelectItem>
</SelectContent>
</Select>
</div>
{/* 部门选择(仅在按部门设置时显示) */}
{batchLimitType === "department" && (
<div className="space-y-2">
<Label>选择部门</Label>
<Select
value={batchLimitDepartment}
onValueChange={setBatchLimitDepartment}
>
<SelectTrigger>
<SelectValue placeholder="选择部门" />
</SelectTrigger>
<SelectContent className="max-h-60 overflow-y-auto">
{departmentsData?.departments?.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
))}
</SelectContent>
</Select>
{departmentsData?.departments?.length === 0 && (
<p className="text-sm text-muted-foreground">暂无部门数据,请先同步钉钉用户</p>
)}
</div>
)}
{/* 限额输入 */}
<div className="space-y-2">
<Label>月度出图限额</Label>
<div className="flex items-center gap-2">
<Input
type="number"
min="0"
placeholder="留空表示无限制"
value={batchLimitValue}
onChange={(e) => setBatchLimitValue(e.target.value)}
className="flex-1"
/>
<span className="text-sm text-muted-foreground">张/月</span>
</div>
<p className="text-xs text-muted-foreground">
设置为空或0表示无限制
</p>
</div>
{/* 预览信息 */}
<div className="rounded-md bg-muted p-3 text-sm">
<p className="font-medium mb-1">即将执行:</p>
<p className="text-muted-foreground">
{batchLimitType === "all" && "将为所有用户设置出图限额"}
{batchLimitType === "department" && batchLimitDepartment && `将为部门「${batchLimitDepartment}」的用户设置出图限额`}
{batchLimitType === "department" && !batchLimitDepartment && "请先选择部门"}
{batchLimitType === "no_department" && "将为所有无部门用户设置出图限额"}
{" → "}
{batchLimitValue ? `${batchLimitValue} 张/月` : "无限制"}
</p>
</div>
</div>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={() => setShowBatchLimitDialog(false)}>
取消
</Button>
<Button
onClick={handleBatchSetLimit}
disabled={batchLimitType === "department" && !batchLimitDepartment}
>
确认设置
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -590,8 +590,14 @@ export async function POST(request: Request) {
const geminiStream = createUIMessageStream({
execute: async ({ writer }) => {
try {
// 🆕 只传递最后一条用户消息,避免上下文历史干扰生图效果
// 用户反馈:带上下文记录会影响生图效果
const lastUserMessage = uiMessages.filter((m) => m.role === "user").pop();
const messagesForGemini = lastUserMessage ? [lastUserMessage] : uiMessages;
console.log("[Chat API] Gemini: Using only last user message (no history context)");
// 转换消息,Gemini 需要 base64 格式的图片
const modelMessages = await convertMessagesWithImages(uiMessages, "gemini");
const modelMessages = await convertMessagesWithImages(messagesForGemini, "gemini");
// 调用 doGenerate 获取图片
const result = await (languageModel as any).doGenerate({

View File

@ -0,0 +1,120 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { user } from "@/lib/db/schema";
import { eq, isNull, or, sql } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
// 获取所有部门列表
export async function GET() {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 检查是否为管理员
const currentUser = await db.query.user.findFirst({
where: eq(user.id, session.user.id),
});
if (currentUser?.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// 获取所有不重复的部门列表
const departments = await db
.selectDistinct({ department: user.department })
.from(user)
.where(sql`${user.department} IS NOT NULL AND ${user.department} != ''`)
.orderBy(user.department);
return NextResponse.json({
departments: departments.map(d => d.department).filter(Boolean),
});
} catch (error) {
console.error("[Batch Limit API] Error:", error);
return NextResponse.json({ error: "获取部门列表失败" }, { status: 500 });
}
}
// 批量设置出图限制
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 检查是否为管理员
const currentUser = await db.query.user.findFirst({
where: eq(user.id, session.user.id),
});
if (currentUser?.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const body = await request.json();
const { type, limit, department } = body;
// limit 为 null 表示无限制,为数字表示限制张数
const limitValue = limit === null || limit === "" || limit === undefined ? null : parseInt(limit, 10);
let updatedCount = 0;
if (type === "all") {
// 批量设置所有用户
const result = await db
.update(user)
.set({ monthlyImageLimit: limitValue });
// 获取更新的用户数
const allUsers = await db.select({ id: user.id }).from(user);
updatedCount = allUsers.length;
console.log(`[Batch Limit] 批量设置所有用户限额: ${limitValue ?? "无限制"}, 影响 ${updatedCount} 个用户`);
} else if (type === "department" && department) {
// 按部门批量设置
await db
.update(user)
.set({ monthlyImageLimit: limitValue })
.where(eq(user.department, department));
// 获取更新的用户数
const deptUsers = await db.select({ id: user.id }).from(user).where(eq(user.department, department));
updatedCount = deptUsers.length;
console.log(`[Batch Limit] 批量设置部门 "${department}" 限额: ${limitValue ?? "无限制"}, 影响 ${updatedCount} 个用户`);
} else if (type === "no_department") {
// 设置无部门用户
await db
.update(user)
.set({ monthlyImageLimit: limitValue })
.where(or(isNull(user.department), eq(user.department, "")));
// 获取更新的用户数
const noDeptUsers = await db
.select({ id: user.id })
.from(user)
.where(or(isNull(user.department), eq(user.department, "")));
updatedCount = noDeptUsers.length;
console.log(`[Batch Limit] 批量设置无部门用户限额: ${limitValue ?? "无限制"}, 影响 ${updatedCount} 个用户`);
} else {
return NextResponse.json({ error: "无效的操作类型" }, { status: 400 });
}
return NextResponse.json({
success: true,
message: `成功更新 ${updatedCount} 个用户的出图限额`,
updatedCount,
limit: limitValue,
});
} catch (error) {
console.error("[Batch Limit API] Error:", error);
return NextResponse.json({ error: "批量设置失败" }, { status: 500 });
}
}