refactor(modules): update classes, course-plans, diagnostic, questions, settings, student, layout

- Update classes data-access (invitations, main) for invitation management

- Update course-plans actions, data-access, and types

- Update diagnostic data-access for report queries

- Update questions data-access for question bank queries

- Update settings actions, ai-provider-settings-card, data-access, and types

- Update student course-filters, student-courses-view, student-schedule-filters, student-schedule-view

- Update layout app-sidebar, site-header, and navigation config
This commit is contained in:
SpecialX
2026-06-24 12:03:35 +08:00
parent c9e46f9f80
commit 8c2fe14c20
18 changed files with 712 additions and 189 deletions

View File

@@ -5,7 +5,11 @@ import { revalidatePath } from "next/cache"
import { createId } from "@paralleldrive/cuid2"
import type { ActionState } from "@/shared/types/action-state"
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
import {
requirePermission,
PermissionDeniedError,
getAuthContext,
} from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"
@@ -15,13 +19,15 @@ import {
deleteAiProvider as deleteAiProviderRecord,
getAiProviderForUpdate,
getAiProviderSummaries as fetchAiProviderSummaries,
getAiProviderSummariesForUser,
updateAiProvider,
} from "./data-access"
import type { AiProviderSummary } from "./types"
import type { AiProviderSummary, AiProviderVisibility } from "./types"
export type { AiProviderSummary } from "./types"
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"])
const VisibilitySchema = z.enum(["public", "private"])
const AiProviderFormSchema = z.object({
id: z.string().optional(),
@@ -30,6 +36,7 @@ const AiProviderFormSchema = z.object({
model: z.string().min(1),
apiKey: z.string().min(1).optional(),
isDefault: z.boolean().optional(),
visibility: VisibilitySchema.optional(),
})
const AiProviderTestSchema = AiProviderFormSchema.extend({
@@ -44,9 +51,15 @@ const AiProviderTestSchema = AiProviderFormSchema.extend({
}
})
const ensureUser = async (): Promise<{ id: string }> => {
const ctx = await requirePermission(Permissions.AI_CONFIGURE)
return { id: ctx.userId }
/**
* 校验当前用户身份,返回 { id, isAdmin }
*
* - 所有 AI_CHAT 用户均可访问(用于管理自己的 private provider
* - isAdmin 标识是否拥有 AI_CONFIGURE 权限(可管理 public provider 与他人 private
*/
const ensureUser = async (): Promise<{ id: string; isAdmin: boolean }> => {
const ctx = await requirePermission(Permissions.AI_CHAT)
return { id: ctx.userId, isAdmin: ctx.permissions.includes(Permissions.AI_CONFIGURE) }
}
const normalizeBaseUrl = (value: string | undefined): string | null => {
@@ -58,10 +71,18 @@ const normalizeBaseUrl = (value: string | undefined): string | null => {
.replace(/\/chat\/completions$/i, "")
}
/**
* 获取当前用户可见的 AI Provider 列表
*
* - 管理员:返回所有 public + private 记录
* - 普通用户:返回 public + 自己创建的 private 记录
*/
export async function getAiProviderSummaries(): Promise<ActionState<AiProviderSummary[]>> {
try {
await ensureUser()
const data = await fetchAiProviderSummaries()
const user = await ensureUser()
const data = user.isAdmin
? await fetchAiProviderSummaries()
: await getAiProviderSummariesForUser(user.id)
return { success: true, data }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
@@ -85,10 +106,17 @@ export async function upsertAiProviderAction(
return { success: false, message: "Base URL is required for this provider" }
}
// 可见性规则:
// - 管理员可创建/更新 public 或 private
// - 普通用户只能创建/更新 private
const requestedVisibility: AiProviderVisibility = payload.visibility ?? "private"
const visibility: AiProviderVisibility =
user.isAdmin ? requestedVisibility : "private"
// Parallelize default-count and existing-provider queries
const [defaultCount, existing] = await Promise.all([
countDefaultAiProviders(),
payload.id ? getAiProviderForUpdate(payload.id) : Promise.resolve(null),
payload.id ? getAiProviderForUpdate(payload.id, user.id) : Promise.resolve(null),
])
const hasDefault = defaultCount > 0
@@ -114,11 +142,13 @@ export async function upsertAiProviderAction(
apiKeyEncrypted: encrypted,
apiKeyLast4: last4,
isDefault: isNextDefault,
visibility,
updatedBy: user.id,
},
payload.isDefault === true
)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
return { success: true, message: "AI provider updated", data: id }
}
@@ -141,16 +171,19 @@ export async function upsertAiProviderAction(
apiKeyEncrypted: encrypted,
apiKeyLast4: last4,
isDefault: shouldMakeDefault,
visibility,
createdBy: user.id,
updatedBy: user.id,
},
shouldMakeDefault
)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
return { success: true, message: "AI provider created", data: id }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
console.error("[upsertAiProviderAction] Failed to save AI provider:", error)
return { success: false, message: "Failed to save AI provider" }
}
}
@@ -190,18 +223,26 @@ const DeleteAiProviderSchema = z.object({
/**
* 删除 AI Provider
*
* 权限规则:
* - 管理员AI_CONFIGURE可删除任意 Provider
* - 普通用户AI_CHAT仅可删除自己创建的 Provider
*
* 如果删除的是默认 Provider自动将最新的一条记录设为默认若存在
*/
export async function deleteAiProviderAction(
input: z.infer<typeof DeleteAiProviderSchema>
): Promise<ActionState<null>> {
try {
await ensureUser()
const user = await ensureUser()
const parsed = DeleteAiProviderSchema.safeParse(input)
if (!parsed.success) {
return { success: false, message: "Invalid provider id" }
}
await deleteAiProviderRecord(parsed.data.id)
// 管理员不传 userId可删除任意普通用户传 userId 做所有权校验
await deleteAiProviderRecord(
parsed.data.id,
user.isAdmin ? undefined : user.id
)
revalidatePath("/admin/ai-settings")
revalidatePath("/settings")
return { success: true, message: "AI provider deleted", data: null }
@@ -210,3 +251,18 @@ export async function deleteAiProviderAction(
return { success: false, message: "Failed to delete AI provider" }
}
}
/**
* 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员)
*
* 供 UI 层决定是否显示 public 可见性选项。
*/
export async function canConfigurePublicAiProvider(): Promise<ActionState<boolean>> {
try {
const ctx = await getAuthContext()
return { success: true, data: ctx.permissions.includes(Permissions.AI_CONFIGURE) }
} catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message, data: false }
return { success: false, message: "Failed to check permission", data: false }
}
}