添加用户批量限额管理功能
This commit is contained in:
parent
6646a7a099
commit
ee2d5e90ea
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
120
app/api/admin/users/batch-limit/route.ts
Normal file
120
app/api/admin/users/batch-limit/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user