diff --git a/public/uploads/resumes/resume_1774759151881_07e8k.pdf b/public/uploads/resumes/resume_1774759151881_07e8k.pdf new file mode 100644 index 0000000..fc8283c Binary files /dev/null and b/public/uploads/resumes/resume_1774759151881_07e8k.pdf differ diff --git a/src/app/api/auth/forgot-password/route.ts b/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..5b3d664 --- /dev/null +++ b/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; +import { getUserByEmail } from '@/lib/db/queries'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const email = String(body?.email || '').trim().toLowerCase(); + + if (!email) { + return NextResponse.json( + { success: false, message: '请输入邮箱地址' }, + { status: 400 } + ); + } + + const users = await getUserByEmail(email); + + return NextResponse.json({ + success: true, + message: users.length > 0 + ? '已受理重置请求,请联系管理员重置密码' + : '如果该邮箱已注册,我们会处理重置请求', + }); + } catch (error) { + console.error('Forgot password error:', error); + return NextResponse.json( + { success: false, message: '重置请求提交失败' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/interviews/schedule/route.ts b/src/app/api/interviews/schedule/route.ts index 26fb626..8ec8116 100644 --- a/src/app/api/interviews/schedule/route.ts +++ b/src/app/api/interviews/schedule/route.ts @@ -1,7 +1,5 @@ import { NextResponse } from 'next/server'; -import { updateInterview, getInterviewsByStudentId, updateStudent, getStudentById, createActivityLog } from '@/lib/db/queries'; -import { eq } from 'drizzle-orm'; -import { schema, db } from '@/lib/db'; +import { createInterview, updateInterview, getInterviewsByStudentId, updateStudent, getStudentById, createActivityLog } from '@/lib/db/queries'; /** * @swagger @@ -79,12 +77,12 @@ export async function POST(request: Request) { } // 构建更新数据 - const updateData: Partial = { + const updateData = { time: time || '', date: date ? new Date(date) : undefined, interviewers: interviewers || [], location: location || '', - stage: 'pending_interview', + stage: 'pending_interview' as const, updatedAt: new Date(), }; @@ -107,7 +105,7 @@ export async function POST(request: Request) { } const student = studentInfo[0]; - await db.insert(schema.interviews).values({ + await createInterview({ studentId, name: student.name || '', major: student.major || '', @@ -118,7 +116,7 @@ export async function POST(request: Request) { email: student.email || '', phone: student.phone || '', className: student.className || '', - skills: student.skills || [], + skills: (student as any).skills || [], experiences: student.experiences || [], time: time || '', date: date ? new Date(date) : undefined, diff --git a/src/app/api/resumes/route.ts b/src/app/api/resumes/route.ts index 01134eb..7409413 100644 --- a/src/app/api/resumes/route.ts +++ b/src/app/api/resumes/route.ts @@ -1,9 +1,7 @@ import { NextResponse } from 'next/server'; -import { getStudents, getStudentById, createStudent, createActivityLog, getUserById } from '@/lib/db/queries'; +import { getStudents, createStudent, createActivityLog, getUserById } from '@/lib/db/queries'; import { getCurrentUser } from '@/lib/auth'; -import { eq, like, and, or, desc } from 'drizzle-orm'; -import { schema } from '@/lib/db'; -import { writeFile, mkdir, unlink } from 'fs/promises'; +import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; // 简历 PDF 上传目录 diff --git a/src/app/components/layout/MainLayout.tsx b/src/app/components/layout/MainLayout.tsx index b0ff23f..c8454b6 100644 --- a/src/app/components/layout/MainLayout.tsx +++ b/src/app/components/layout/MainLayout.tsx @@ -14,6 +14,14 @@ function SidebarWrapper() { ); } +function HeaderWrapper() { + return ( + }> + + + ); +} + export function MainLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); @@ -32,7 +40,7 @@ export function MainLayout({ children }: { children: React.ReactNode }) {
- +
{children} diff --git a/src/app/components/resume-bank.tsx b/src/app/components/resume-bank.tsx index 8201e9b..202a9c2 100644 --- a/src/app/components/resume-bank.tsx +++ b/src/app/components/resume-bank.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; -import { useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { Student } from '@/types'; import { DEPARTMENTS, STATUS_MAP, SortOptionId } from '@/config/constants'; @@ -13,11 +13,12 @@ import { UploadResumeDialog } from './resume/UploadResumeDialog'; import { SyncMailDialog } from './resume/SyncMailDialog'; export function ResumeBank() { + const router = useRouter(); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(true); const [isUploadOpen, setIsUploadOpen] = useState(false); const [selectedStudent, setSelectedStudent] = useState(null); - const [filterDept, setFilterDept] = useState(DEPARTMENTS[0]); + const [filterDept, setFilterDept] = useState(DEPARTMENTS[0]); const [sortBy, setSortBy] = useState('ai'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [searchQuery, setSearchQuery] = useState(''); @@ -34,7 +35,13 @@ export function ResumeBank() { const [isSyncMailOpen, setIsSyncMailOpen] = useState(false); const searchParams = useSearchParams(); - const { currentUser } = useAppStore(); + const { currentUser, logout } = useAppStore(); + + const handleUnauthorized = (message: string = '登录已失效,请重新登录') => { + toast.error(message); + logout(); + router.push('/login'); + }; // Handle URL candidateId parameter useEffect(() => { @@ -83,7 +90,14 @@ export function ResumeBank() { break; case 'status': // Custom order for status: pending > interviewing > passed > rejected - const statusOrder = { pending: 3, interviewing: 2, passed: 1, rejected: 0 }; + const statusOrder: Record = { + pending: 5, + to_be_scheduled: 4, + pending_interview: 3, + interviewing: 2, + passed: 1, + rejected: 0, + }; const statusA = statusOrder[a.status] || 0; const statusB = statusOrder[b.status] || 0; diff = statusA - statusB; @@ -104,21 +118,27 @@ export function ResumeBank() { fetch('/api/departments') ]); - if (resumesRes.ok) { - const result = await resumesRes.json(); - // 处理分页格式的响应 - const resumesData = Array.isArray(result) ? result : (result.data || []); - setStudents(resumesData); - } else { - throw new Error('Failed to fetch resumes'); + if (resumesRes.status === 401) { + const result = await resumesRes.json().catch(() => ({})); + handleUnauthorized(result.message || '请先登录'); + return; } + if (!resumesRes.ok) { + const result = await resumesRes.json().catch(() => ({})); + throw new Error(result.message || '获取简历失败'); + } + + const result = await resumesRes.json(); + const resumesData = Array.isArray(result) ? result : (result.data || []); + setStudents(resumesData); + if (deptsRes.ok) { const deptsData = await deptsRes.json(); setDepartments(['全部部门', ...deptsData]); } } catch (error) { - toast.error('获取数据失败'); + toast.error(error instanceof Error ? error.message : '获取数据失败'); console.error('Error fetching data:', error); } finally { setLoading(false); @@ -156,6 +176,10 @@ export function ResumeBank() { }); const data = await res.json(); + if (res.status === 401) { + handleUnauthorized(data.message || '登录已失效,请重新登录'); + return; + } if (data.success) { toast.success(`状态已更新为:${STATUS_MAP[newStatus].label}`); } else { diff --git a/src/app/components/settings.tsx b/src/app/components/settings.tsx index a42aadb..dd6804c 100644 --- a/src/app/components/settings.tsx +++ b/src/app/components/settings.tsx @@ -156,7 +156,7 @@ export function SettingsPage({ role }: SettingsPageProps) { const fetchSettings = async () => { try { - const [pl, a, n, r, k, g, e] = await Promise.all([ + const [pl, a, n, r, k, e, g] = await Promise.all([ fetch('/api/settings/platform').then(res => res.json()), fetch('/api/settings/ai').then(res => res.json()), fetch('/api/settings/notifications').then(res => res.json()), @@ -184,14 +184,11 @@ export function SettingsPage({ role }: SettingsPageProps) { setApiKeys(k || []); setGithub(g || { clientId: '', clientSecret: '', organization: '', personalAccessToken: '' }); - // 调试日志 - console.log('Email-sending API 返回:', e); - // SMTP 配置:加载所有保存的配置 setEmailSending({ - host: g?.host || '', - port: g?.port || '', - user: g?.user || '', - pass: g?.pass || '' + host: e?.host || '', + port: e?.port || '', + user: e?.user || '', + pass: e?.pass || '' }); } catch (error) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a8b3d2c..19cd24d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,9 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; import "./globals.css"; import { Toaster } from "sonner"; import { MainLayout } from "./components/layout/MainLayout"; import { Providers } from "./providers"; -const inter = Inter({ subsets: ["latin"] }); - export const metadata: Metadata = { title: "码绘工作室 - 人才管理系统", description: "内部专用的招新管理平台", @@ -22,7 +19,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/src/lib/db/local-queries.ts b/src/lib/db/local-queries.ts new file mode 100644 index 0000000..3c5ef81 --- /dev/null +++ b/src/lib/db/local-queries.ts @@ -0,0 +1,1203 @@ +import bcrypt from 'bcryptjs'; +import * as schema from './schema'; + +type UserRecord = typeof schema.users.$inferSelect; +type EmailTemplateRecord = typeof schema.emailTemplates.$inferSelect; +type EmailHistoryRecord = typeof schema.emailHistory.$inferSelect; +type EmailConfigRecord = typeof schema.emailConfig.$inferSelect; +type ActivityLogRecord = typeof schema.activityLogs.$inferSelect; +type NotificationRecord = typeof schema.notifications.$inferSelect; +type CommentRecord = typeof schema.comments.$inferSelect; +type StudentRecord = typeof schema.students.$inferSelect & { + skills?: { name: string; level: 'understanding' | 'familiar' | 'proficient' | 'skilled' | 'master' }[]; + summary?: string; + experience?: string; + education?: string; +}; +type InterviewRecord = typeof schema.interviews.$inferSelect; +type GithubSettingsRecord = typeof schema.githubSettings.$inferSelect; +type ApiKeyRecord = typeof schema.apiKeys.$inferSelect; + +type AiSettingsResponse = { + vision: { + endpoint: string; + model: string; + apiKey: string; + }; + llm: { + baseUrl: string; + apiKey: string; + model: string; + }; +}; + +type NotificationSettingsResponse = { + webhookUrl: string; + triggers: { + new_resume: boolean; + interview_reminder: boolean; + offer_confirmed: boolean; + }; +}; + +type PlatformSettingsResponse = { + departments: string[]; +}; + +type ResumeImportSettingsResponse = { + imapServer: string; + port: string; + account: string; + authCode: string; +}; + +type Store = { + users: UserRecord[]; + emailTemplates: EmailTemplateRecord[]; + emailHistory: EmailHistoryRecord[]; + emailConfig: EmailConfigRecord | null; + activityLogs: ActivityLogRecord[]; + notifications: NotificationRecord[]; + comments: CommentRecord[]; + students: StudentRecord[]; + interviews: InterviewRecord[]; + aiSettings: AiSettingsResponse; + githubSettings: GithubSettingsRecord; + apiKeys: ApiKeyRecord[]; + notificationSettings: NotificationSettingsResponse; + platformSettings: PlatformSettingsResponse; + resumeImportSettings: ResumeImportSettingsResponse; + counters: Record; +}; + +const defaultDepartments = ['前端部', '后端部', 'UI部', '设计部', '产品部', '运维部', '测试部', '办公室']; +const defaultAvatar = '/uploads/avatars/avatar_1_1771040131275.png'; + +const clone = (value: T): T => structuredClone(value); + +const daysAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000); + +const toDate = (value?: Date | string | null) => { + if (!value) return null; + return value instanceof Date ? new Date(value) : new Date(value); +}; + +const sameDay = (left: Date | null | undefined, right: Date) => { + if (!left) return false; + return ( + left.getFullYear() === right.getFullYear() && + left.getMonth() === right.getMonth() && + left.getDate() === right.getDate() + ); +}; + +const formatRelativeTime = (date: Date) => { + const diff = Date.now() - date.getTime(); + const minutes = Math.max(1, Math.floor(diff / 60000)); + if (minutes < 60) return `${minutes}分钟前`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}小时前`; + const days = Math.floor(hours / 24); + return `${days}天前`; +}; + +const calcChange = (current: number, previous: number) => { + if (previous === 0) return current > 0 ? '+100%' : '0%'; + const delta = ((current - previous) / previous) * 100; + const sign = delta >= 0 ? '+' : ''; + return `${sign}${delta.toFixed(1)}%`; +}; + +const createInitialStore = (): Store => { + const adminPassword = bcrypt.hashSync('123456', 10); + const memberPassword = bcrypt.hashSync('123456', 10); + const interviewerPassword = bcrypt.hashSync('123456', 10); + + const users: UserRecord[] = [ + { + id: 1, + email: 'admin@mahui.com', + name: '张老师', + role: 'admin', + password: adminPassword, + avatar: defaultAvatar, + department: '办公室', + createdAt: daysAgo(30), + updatedAt: daysAgo(1), + }, + { + id: 2, + email: 'hr@mahui.com', + name: '李同学', + role: 'member', + password: memberPassword, + avatar: '/uploads/avatars/avatar_9_1771126454860.png', + department: '前端部', + createdAt: daysAgo(20), + updatedAt: daysAgo(2), + }, + { + id: 3, + email: 'interviewer@mahui.com', + name: '王面试官', + role: 'interviewer', + password: interviewerPassword, + avatar: '', + department: '后端部', + createdAt: daysAgo(18), + updatedAt: daysAgo(2), + }, + ]; + + const students: StudentRecord[] = [ + { + id: 1, + name: '陈晨', + studentId: '2023001', + department: '前端部', + major: '软件工程', + className: '软工 2301', + gpa: '3.8', + graduationYear: '2027', + status: 'pending', + tags: ['React', 'TypeScript'], + aiScore: '88.50', + submissionDate: daysAgo(1), + email: 'chenchen@example.com', + phone: '13800000001', + resumePdf: '/uploads/resumes/resume_1771050447843_2egkfc.pdf', + experiences: [ + { + id: 'exp-1', + startDate: '2024-03', + endDate: '2024-08', + title: '前端项目负责人', + description: '负责 React 项目开发与组件设计', + }, + ], + skills: [ + { name: 'React', level: 'proficient' }, + { name: 'TypeScript', level: 'proficient' }, + ], + summary: '有完整项目经验,熟悉 React 生态。', + experience: '担任课程项目负责人,协作经验丰富。', + education: '本科在读', + createdAt: daysAgo(1), + updatedAt: daysAgo(1), + }, + { + id: 2, + name: '林夕', + studentId: '2023002', + department: '后端部', + major: '计算机科学与技术', + className: '计科 2302', + gpa: '3.9', + graduationYear: '2027', + status: 'pending_interview', + tags: ['Node.js', 'MySQL'], + aiScore: '91.20', + submissionDate: daysAgo(2), + email: 'linxi@example.com', + phone: '13800000002', + resumePdf: '', + experiences: [ + { + id: 'exp-2', + startDate: '2024-05', + endDate: '2024-11', + title: '后端开发', + description: '负责 API 设计与数据库建模', + }, + ], + skills: [ + { name: 'Node.js', level: 'proficient' }, + { name: 'MySQL', level: 'familiar' }, + ], + summary: '擅长接口开发与数据建模。', + experience: '完成多个后端课程设计。', + education: '本科在读', + createdAt: daysAgo(2), + updatedAt: daysAgo(1), + }, + { + id: 3, + name: '周岚', + studentId: '2023003', + department: 'UI部', + major: '数字媒体技术', + className: '数媒 2301', + gpa: '3.7', + graduationYear: '2027', + status: 'interviewing', + tags: ['Figma', 'Design System'], + aiScore: '85.00', + submissionDate: daysAgo(4), + email: 'zhoulan@example.com', + phone: '13800000003', + resumePdf: '', + experiences: [ + { + id: 'exp-3', + startDate: '2024-01', + endDate: '2024-12', + title: '视觉设计', + description: '负责活动海报与页面视觉规范', + }, + ], + skills: [ + { name: 'Figma', level: 'proficient' }, + ], + summary: '设计能力稳定,具备交互意识。', + experience: '参与多项校内设计活动。', + education: '本科在读', + createdAt: daysAgo(4), + updatedAt: daysAgo(1), + }, + { + id: 4, + name: '何明', + studentId: '2023004', + department: '测试部', + major: '软件工程', + className: '软工 2303', + gpa: '3.6', + graduationYear: '2027', + status: 'passed', + tags: ['Testing', 'Automation'], + aiScore: '89.00', + submissionDate: daysAgo(7), + email: 'heming@example.com', + phone: '13800000004', + resumePdf: '', + experiences: [ + { + id: 'exp-4', + startDate: '2024-02', + endDate: '2024-10', + title: '测试工程实践', + description: '负责测试用例设计与自动化脚本', + }, + ], + skills: [ + { name: 'Playwright', level: 'familiar' }, + ], + summary: '重视质量保障,执行力强。', + experience: '搭建过自动化测试流程。', + education: '本科在读', + createdAt: daysAgo(7), + updatedAt: daysAgo(2), + }, + ]; + + const interviews: InterviewRecord[] = [ + { + id: 1, + studentId: 2, + name: '林夕', + major: '计算机科学与技术', + department: '后端部', + time: '19:30', + date: new Date(Date.now() + 24 * 60 * 60 * 1000), + location: '线上会议室 A', + priority: 'high', + stage: 'pending_interview', + gpa: '3.9', + aiScore: '91.20', + tags: ['Node.js', 'MySQL'], + email: 'linxi@example.com', + phone: '13800000002', + className: '计科 2302', + skills: [ + { name: 'Node.js', level: 'proficient' }, + { name: 'MySQL', level: 'familiar' }, + ], + experiences: students[1].experiences, + interviewers: ['王面试官'], + createdAt: daysAgo(1), + updatedAt: daysAgo(1), + }, + { + id: 2, + studentId: 3, + name: '周岚', + major: '数字媒体技术', + department: 'UI部', + time: '14:00', + date: new Date(), + location: '设计组会议室', + priority: 'medium', + stage: 'interviewing', + gpa: '3.7', + aiScore: '85.00', + tags: ['Figma', 'Design System'], + email: 'zhoulan@example.com', + phone: '13800000003', + className: '数媒 2301', + skills: [ + { name: 'Figma', level: 'proficient' }, + ], + experiences: students[2].experiences, + interviewers: ['张老师'], + createdAt: daysAgo(2), + updatedAt: daysAgo(1), + }, + ]; + + const now = new Date(); + + return { + users, + emailTemplates: [ + { + id: 1, + name: '面试通知', + subject: '面试邀请通知', + content: '您好,我们诚挚邀请您参加面试。', + category: '通知', + createdAt: daysAgo(10), + updatedAt: daysAgo(3), + }, + ], + emailHistory: [ + { + id: 1, + templateName: '面试通知', + subject: '面试邀请通知', + content: '您好,我们诚挚邀请您参加面试。', + recipients: [{ name: '林夕', email: 'linxi@example.com', status: 'success' }], + recipientCount: 1, + status: 'success', + sentAt: daysAgo(1), + }, + ], + emailConfig: { + id: 1, + host: '', + port: '', + user: '', + pass: '', + updatedAt: now, + }, + activityLogs: [ + { + id: 1, + user: '张老师', + action: '导入了 4 份示例简历', + time: '1小时前', + timestamp: daysAgo(0), + role: '管理员', + avatar: defaultAvatar, + userId: 1, + }, + { + id: 2, + user: '王面试官', + action: '安排了 2 场面试', + time: '3小时前', + timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000), + role: '后端部', + avatar: '', + userId: 3, + }, + { + id: 3, + user: '李同学', + action: '更新了平台设置', + time: '昨天', + timestamp: daysAgo(1), + role: '前端部', + avatar: '/uploads/avatars/avatar_9_1771126454860.png', + userId: 2, + }, + ], + notifications: [ + { + id: 1, + userId: null, + type: 'system', + title: '系统初始化完成', + description: '本地演示数据已加载,可以直接登录体验。', + time: '刚刚', + timestamp: now, + unread: '1', + }, + { + id: 2, + userId: 1, + type: 'interview', + title: '新的面试安排', + description: '林夕的面试已安排到明晚 19:30。', + time: '20分钟前', + timestamp: new Date(Date.now() - 20 * 60 * 1000), + unread: '1', + }, + ], + comments: [], + students, + interviews, + aiSettings: { + vision: { + endpoint: '', + model: 'vision-vk-v2', + apiKey: '', + }, + llm: { + baseUrl: 'https://api.openai.com/v1', + apiKey: '', + model: '', + }, + }, + githubSettings: { + id: 1, + clientId: '', + clientSecret: '', + organization: 'mahui-studio', + personalAccessToken: '', + updatedAt: now, + }, + apiKeys: [ + { + id: '1', + name: 'HR Portal Integration', + key: 'sk_live_51M...', + created: '2024-02-15', + expiresAt: '', + }, + ], + notificationSettings: { + webhookUrl: '', + triggers: { + new_resume: true, + interview_reminder: true, + offer_confirmed: true, + }, + }, + platformSettings: { + departments: defaultDepartments, + }, + resumeImportSettings: { + imapServer: 'imap.exmail.qq.com', + port: '993', + account: '', + authCode: '', + }, + counters: { + user: 3, + emailTemplate: 1, + emailHistory: 1, + activityLog: 3, + notification: 2, + comment: 0, + student: 4, + interview: 2, + }, + }; +}; + +let store = createInitialStore(); + +const nextId = (key: keyof Store['counters']) => { + store.counters[key] += 1; + return store.counters[key]; +}; + +export async function getUsers() { + return clone(store.users); +} + +export async function getUserById(id: number) { + const user = store.users.find(item => item.id === id); + return user ? [clone(user)] : []; +} + +export async function getUserByEmail(email: string) { + const user = store.users.find(item => item.email.toLowerCase() === email.toLowerCase()); + return user ? [clone(user)] : []; +} + +export async function createUser(data: typeof schema.users.$inferInsert) { + const id = nextId('user'); + const record: UserRecord = { + id, + email: data.email, + name: data.name, + role: data.role || 'member', + password: data.password, + avatar: data.avatar || '', + department: data.department || '', + createdAt: new Date(), + updatedAt: new Date(), + }; + store.users.push(record); + return [clone(record)]; +} + +export async function updateUser(id: number, data: Partial) { + const user = store.users.find(item => item.id === id); + if (!user) return []; + Object.assign(user, data, { updatedAt: new Date() }); + return [clone(user)]; +} + +export async function deleteUser(id: number) { + store.users = store.users.filter(item => item.id !== id); + return true; +} + +export async function getInterviewers() { + return clone( + store.users.filter(item => item.role === 'interviewer' || item.role === 'member').map(item => ({ + id: item.id, + name: item.name, + email: item.email, + role: item.role, + avatar: item.avatar, + department: item.department, + })) + ); +} + +export async function getEmailTemplates() { + return clone(store.emailTemplates); +} + +export async function getEmailTemplateById(id: number) { + const template = store.emailTemplates.find(item => item.id === id); + return template ? [clone(template)] : []; +} + +export async function createEmailTemplate(data: typeof schema.emailTemplates.$inferInsert) { + const record: EmailTemplateRecord = { + id: nextId('emailTemplate'), + name: data.name, + subject: data.subject, + content: data.content, + category: data.category, + createdAt: new Date(), + updatedAt: new Date(), + }; + store.emailTemplates.push(record); + return [clone(record)]; +} + +export async function updateEmailTemplate(id: number, data: Partial) { + const template = store.emailTemplates.find(item => item.id === id); + if (!template) return []; + Object.assign(template, data, { updatedAt: new Date() }); + return [clone(template)]; +} + +export async function deleteEmailTemplate(id: number) { + store.emailTemplates = store.emailTemplates.filter(item => item.id !== id); + return true; +} + +export async function getEmailHistory() { + return clone([...store.emailHistory].sort((a, b) => (a.sentAt?.getTime() || 0) - (b.sentAt?.getTime() || 0))); +} + +export async function getEmailHistoryById(id: number) { + const record = store.emailHistory.find(item => item.id === id); + return record ? [clone(record)] : []; +} + +export async function createEmailHistory(data: typeof schema.emailHistory.$inferInsert) { + const record: EmailHistoryRecord = { + id: nextId('emailHistory'), + templateName: data.templateName, + subject: data.subject, + content: data.content, + recipients: data.recipients || [], + recipientCount: data.recipientCount || 0, + status: data.status || 'success', + sentAt: toDate(data.sentAt) || new Date(), + }; + store.emailHistory.push(record); + return [clone(record)]; +} + +export async function deleteEmailHistory(id: number) { + store.emailHistory = store.emailHistory.filter(item => item.id !== id); + return true; +} + +export async function getEmailConfig() { + return store.emailConfig ? clone(store.emailConfig) : null; +} + +export async function createOrUpdateEmailConfig(data: typeof schema.emailConfig.$inferInsert) { + store.emailConfig = { + id: store.emailConfig?.id || 1, + host: data.host || '', + port: data.port || '', + user: data.user || '', + pass: data.pass || '', + updatedAt: new Date(), + }; + return [clone(store.emailConfig)]; +} + +export async function getActivityLogs(page: number = 1, limit: number = 10) { + const sorted = [...store.activityLogs].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0)); + const offset = (page - 1) * limit; + return { + data: clone(sorted.slice(offset, offset + limit)), + hasMore: offset + limit < sorted.length, + total: sorted.length, + }; +} + +export async function createActivityLog(data: typeof schema.activityLogs.$inferInsert) { + const timestamp = toDate(data.timestamp) || new Date(); + const record: ActivityLogRecord = { + id: nextId('activityLog'), + user: data.user, + action: data.action, + time: data.time || formatRelativeTime(timestamp), + timestamp, + role: data.role || '', + avatar: data.avatar || '', + userId: data.userId ?? null, + }; + store.activityLogs.push(record); + return [clone(record)]; +} + +export async function getCommentsByStudentId(studentId: number) { + return clone( + store.comments + .filter(item => item.studentId === studentId) + .sort((a, b) => (a.timestamp?.getTime() || 0) - (b.timestamp?.getTime() || 0)) + ); +} + +export async function createComment(data: typeof schema.comments.$inferInsert) { + const timestamp = toDate(data.timestamp) || new Date(); + const record: CommentRecord = { + id: nextId('comment'), + studentId: data.studentId, + user: data.user, + role: data.role || '', + avatar: data.avatar || '', + content: data.content, + time: data.time || formatRelativeTime(timestamp), + timestamp, + userId: data.userId ?? null, + }; + store.comments.push(record); + return [clone(record)]; +} + +export async function deleteComment(id: number) { + store.comments = store.comments.filter(item => item.id !== id); + return true; +} + +export async function getStudents() { + return clone(store.students); +} + +export async function getStudentById(id: number) { + const student = store.students.find(item => item.id === id); + return student ? [clone(student)] : []; +} + +export async function getStudentsByStatus(status: string) { + return clone(store.students.filter(item => item.status === status)); +} + +export async function getStudentsByDepartment(department: string) { + return clone(store.students.filter(item => item.department === department)); +} + +export async function createStudent(data: typeof schema.students.$inferInsert) { + const record: StudentRecord = { + id: nextId('student'), + name: data.name, + studentId: data.studentId || '', + department: data.department || '', + major: data.major || '', + className: data.className || '', + gpa: data.gpa || '', + graduationYear: data.graduationYear || '', + status: data.status || 'pending', + tags: data.tags || [], + aiScore: data.aiScore || '0', + submissionDate: toDate(data.submissionDate), + email: data.email || '', + phone: data.phone || '', + resumePdf: data.resumePdf || '', + experiences: data.experiences || [], + createdAt: new Date(), + updatedAt: new Date(), + }; + store.students.push(record); + return [clone(record)]; +} + +export async function updateStudent(id: number, data: Partial) { + const student = store.students.find(item => item.id === id); + if (!student) return []; + Object.assign(student, data, { updatedAt: new Date() }); + if (data.submissionDate !== undefined) { + student.submissionDate = toDate(data.submissionDate); + } + return [clone(student)]; +} + +export async function deleteStudent(id: number) { + store.students = store.students.filter(item => item.id !== id); + store.comments = store.comments.filter(item => item.studentId !== id); + store.interviews = store.interviews.filter(item => item.studentId !== id); + return true; +} + +export async function getInterviews() { + return clone(store.interviews); +} + +export async function getUpcomingInterviews(limit: number = 10) { + return clone( + store.interviews + .filter(item => (item.stage === 'pending_interview' || item.stage === 'interviewing') && !!item.date) + .sort((a, b) => { + const dateDelta = (a.date?.getTime() || 0) - (b.date?.getTime() || 0); + if (dateDelta !== 0) return dateDelta; + return (a.time || '').localeCompare(b.time || ''); + }) + .slice(0, limit) + ); +} + +export async function getTodayInterviews() { + const today = new Date(); + return clone( + store.interviews + .filter(item => sameDay(item.date || null, today) && (item.stage === 'pending_interview' || item.stage === 'interviewing')) + .sort((a, b) => (a.time || '').localeCompare(b.time || '')) + ); +} + +export async function getInterviewById(id: number) { + const interview = store.interviews.find(item => item.id === id); + return interview ? [clone(interview)] : []; +} + +export async function getInterviewsByStudentId(studentId: number) { + return clone(store.interviews.filter(item => item.studentId === studentId)); +} + +export async function getInterviewsByStage(stage: string) { + return clone(store.interviews.filter(item => item.stage === stage)); +} + +export async function createInterview(data: typeof schema.interviews.$inferInsert) { + const record: InterviewRecord = { + id: nextId('interview'), + studentId: data.studentId, + name: data.name, + major: data.major || '', + department: data.department || '', + time: data.time || '', + date: toDate(data.date), + location: data.location || '', + priority: data.priority || 'medium', + stage: data.stage || 'pending', + gpa: data.gpa || '', + aiScore: data.aiScore || '0', + tags: data.tags || [], + email: data.email || '', + phone: data.phone || '', + className: data.className || '', + skills: data.skills || [], + experiences: data.experiences || [], + interviewers: data.interviewers || [], + createdAt: toDate(data.createdAt) || new Date(), + updatedAt: toDate(data.updatedAt) || new Date(), + }; + store.interviews.push(record); + return [clone(record)]; +} + +export async function updateInterview(id: number, data: Partial) { + const interview = store.interviews.find(item => item.id === id); + if (!interview) return []; + Object.assign(interview, data, { updatedAt: new Date() }); + if (data.date !== undefined) { + interview.date = toDate(data.date); + } + return [clone(interview)]; +} + +export async function deleteInterview(id: number) { + store.interviews = store.interviews.filter(item => item.id !== id); + return true; +} + +export async function getStudentStats() { + const stats = { + total: store.students.length, + pending: 0, + to_be_scheduled: 0, + pending_interview: 0, + interviewing: 0, + passed: 0, + rejected: 0, + }; + for (const student of store.students) { + if (student.status in stats) { + stats[student.status as keyof typeof stats] += 1; + } + } + return stats; +} + +export async function getInterviewStats() { + const stats = { + total: store.interviews.length, + pending: 0, + to_be_scheduled: 0, + pending_interview: 0, + interviewing: 0, + passed: 0, + rejected: 0, + }; + for (const interview of store.interviews) { + if (interview.stage in stats) { + stats[interview.stage as keyof typeof stats] += 1; + } + } + return stats; +} + +export async function batchCreateStudents(data: typeof schema.students.$inferInsert[]) { + const created: StudentRecord[] = []; + for (const item of data) { + const [student] = await createStudent(item); + if (student) created.push(student); + } + return created; +} + +export async function batchUpdateStudentStatus(ids: number[], status: string) { + for (const id of ids) { + const student = store.students.find(item => item.id === id); + if (student) { + student.status = status as StudentRecord['status']; + student.updatedAt = new Date(); + } + } + return true; +} + +export async function getDepartmentDistribution() { + const countMap = new Map(); + for (const student of store.students) { + if (student.department) { + countMap.set(student.department, (countMap.get(student.department) || 0) + 1); + } + } + + const colorMap: Record = { + 前端部: '#2563eb', + 后端部: '#8b5cf6', + UI部: '#ec4899', + 设计部: '#f97316', + 产品部: '#14b8a6', + 运维部: '#64748b', + 测试部: '#84cc16', + 数据部: '#0ea5e9', + 算法部: '#a855f7', + 移动端: '#f43f5e', + 办公室: '#f59e0b', + }; + + if (countMap.size === 0) { + return [ + { name: '前端部', value: 0, fill: '#2563eb' }, + { name: 'UI部', value: 0, fill: '#8b5cf6' }, + { name: '办公室', value: 0, fill: '#ec4899' }, + { name: '运维部', value: 0, fill: '#f97316' }, + ]; + } + + return Array.from(countMap.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([name, value]) => ({ + name, + value, + fill: colorMap[name] || '#64748b', + })); +} + +export async function getDashboardStats() { + const inboxCount = store.students.filter(item => item.status === 'pending').length; + const pendingCount = store.students.filter(item => item.status === 'to_be_scheduled' || item.status === 'pending_interview').length; + + const today = new Date(); + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); + startOfWeek.setHours(0, 0, 0, 0); + + const sevenDaysAgo = daysAgo(7); + const fourteenDaysAgo = daysAgo(14); + + const passedCount = store.interviews.filter(item => item.stage === 'passed' && (item.updatedAt?.getTime() || 0) >= startOfWeek.getTime()).length; + const userCount = store.users.length; + + const oldInboxCount = store.students.filter(item => item.status === 'pending' && (item.createdAt?.getTime() || 0) < sevenDaysAgo.getTime()).length; + const oldPendingCount = store.students.filter( + item => + (item.status === 'to_be_scheduled' || item.status === 'pending_interview') && + (item.createdAt?.getTime() || 0) < sevenDaysAgo.getTime() + ).length; + const oldPassedCount = store.interviews.filter(item => { + const updatedAt = item.updatedAt?.getTime() || 0; + return item.stage === 'passed' && updatedAt >= fourteenDaysAgo.getTime() && updatedAt < sevenDaysAgo.getTime(); + }).length; + const oldUserCount = store.users.filter(item => (item.createdAt?.getTime() || 0) < sevenDaysAgo.getTime()).length; + + return { + totalApplications: inboxCount, + pending: pendingCount, + passed: passedCount, + totalInterviewers: userCount, + totalChange: calcChange(inboxCount, oldInboxCount), + pendingChange: calcChange(pendingCount, oldPendingCount), + passedChange: calcChange(passedCount, oldPassedCount), + interviewersChange: calcChange(userCount, oldUserCount), + }; +} + +export async function getTrendData(days: number = 7) { + const labels: string[] = []; + const counts = new Map(); + const today = new Date(); + for (let index = days - 1; index >= 0; index -= 1) { + const date = new Date(today); + date.setDate(today.getDate() - index); + const label = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + labels.push(label); + counts.set(label, 0); + } + + const startDate = new Date(today); + startDate.setDate(today.getDate() - days + 1); + startDate.setHours(0, 0, 0, 0); + + for (const student of store.students) { + const submissionDate = student.submissionDate; + if (submissionDate && submissionDate >= startDate) { + const label = `${String(submissionDate.getMonth() + 1).padStart(2, '0')}-${String(submissionDate.getDate()).padStart(2, '0')}`; + counts.set(label, (counts.get(label) || 0) + 1); + } + } + + return labels.map(label => ({ + name: label, + value: counts.get(label) || 0, + })); +} + +export async function getDepartments() { + const set = new Set(); + for (const department of store.platformSettings.departments) set.add(department); + for (const student of store.students) if (student.department) set.add(student.department); + for (const user of store.users) if (user.department) set.add(user.department); + for (const interview of store.interviews) if (interview.department) set.add(interview.department); + return Array.from(set.size > 0 ? set : new Set(defaultDepartments)).sort(); +} + +export async function getNotifications(userId?: number, limit = 20) { + const filtered = userId === undefined + ? store.notifications + : store.notifications.filter(item => item.userId === null || item.userId === userId); + return clone( + [...filtered] + .sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0)) + .slice(0, limit) + ); +} + +export async function getUnreadNotificationCount(userId?: number) { + const notifications = await getNotifications(userId, Number.MAX_SAFE_INTEGER); + return notifications.filter(item => item.unread === '1').length; +} + +export async function markNotificationAsRead(id: number) { + const notification = store.notifications.find(item => item.id === id); + if (!notification) return false; + notification.unread = '0'; + return true; +} + +export async function markAllNotificationsAsRead(userId?: number) { + for (const notification of store.notifications) { + if (notification.unread === '1' && (userId === undefined || notification.userId === null || notification.userId === userId)) { + notification.unread = '0'; + } + } + return true; +} + +export async function addNotification(notification: typeof schema.notifications.$inferInsert) { + const record: NotificationRecord = { + id: nextId('notification'), + userId: notification.userId ?? null, + type: notification.type, + title: notification.title, + description: notification.description || '', + time: notification.time || '刚刚', + timestamp: toDate(notification.timestamp) || new Date(), + unread: notification.unread || '1', + }; + store.notifications.push(record); + return clone(record); +} + +export async function getAiSettings() { + return clone(store.aiSettings); +} + +export async function createOrUpdateAiSettings(data: { + vision?: { + endpoint?: string; + model?: string; + apiKey?: string; + }; + llm?: { + baseUrl?: string; + apiKey?: string; + model?: string; + }; +}) { + store.aiSettings = { + vision: { + endpoint: data.vision?.endpoint ?? store.aiSettings.vision.endpoint, + model: data.vision?.model ?? store.aiSettings.vision.model, + apiKey: data.vision?.apiKey ?? store.aiSettings.vision.apiKey, + }, + llm: { + baseUrl: data.llm?.baseUrl ?? store.aiSettings.llm.baseUrl, + apiKey: data.llm?.apiKey ?? store.aiSettings.llm.apiKey, + model: data.llm?.model ?? store.aiSettings.llm.model, + }, + }; + return clone(store.aiSettings); +} + +export async function getGithubSettings() { + return clone({ + clientId: store.githubSettings.clientId || '', + clientSecret: store.githubSettings.clientSecret || '', + organization: store.githubSettings.organization || '', + personalAccessToken: store.githubSettings.personalAccessToken || '', + }); +} + +export async function createOrUpdateGithubSettings(data: { + clientId?: string; + clientSecret?: string; + organization?: string; + personalAccessToken?: string; +}) { + store.githubSettings = { + ...store.githubSettings, + clientId: data.clientId ?? store.githubSettings.clientId, + clientSecret: data.clientSecret ?? store.githubSettings.clientSecret, + organization: data.organization ?? store.githubSettings.organization, + personalAccessToken: data.personalAccessToken ?? store.githubSettings.personalAccessToken, + updatedAt: new Date(), + }; + return getGithubSettings(); +} + +export async function getApiKeys() { + return clone(store.apiKeys); +} + +export async function createApiKey(data: typeof schema.apiKeys.$inferInsert) { + const record: ApiKeyRecord = { + id: data.id, + name: data.name, + key: data.key, + created: data.created, + expiresAt: data.expiresAt || '', + }; + store.apiKeys.push(record); + return clone(record); +} + +export async function deleteApiKey(id: string) { + store.apiKeys = store.apiKeys.filter(item => item.id !== id); + return true; +} + +export async function replaceApiKeys(keys: typeof schema.apiKeys.$inferInsert[]) { + store.apiKeys = clone( + keys.map(item => ({ + id: item.id, + name: item.name, + key: item.key, + created: item.created, + expiresAt: item.expiresAt || '', + })) + ); + return getApiKeys(); +} + +export async function getNotificationSettings() { + return clone(store.notificationSettings); +} + +export async function createOrUpdateNotificationSettings(data: { + webhookUrl?: string; + triggers?: { + new_resume?: boolean; + interview_reminder?: boolean; + offer_confirmed?: boolean; + }; +}) { + store.notificationSettings = { + webhookUrl: data.webhookUrl ?? store.notificationSettings.webhookUrl, + triggers: { + new_resume: data.triggers?.new_resume ?? store.notificationSettings.triggers.new_resume, + interview_reminder: data.triggers?.interview_reminder ?? store.notificationSettings.triggers.interview_reminder, + offer_confirmed: data.triggers?.offer_confirmed ?? store.notificationSettings.triggers.offer_confirmed, + }, + }; + return clone(store.notificationSettings); +} + +export async function getPlatformSettings() { + return clone(store.platformSettings); +} + +export async function createOrUpdatePlatformSettings(data: { + departments?: string[]; +}) { + store.platformSettings = { + departments: data.departments ? [...data.departments] : store.platformSettings.departments, + }; + return clone(store.platformSettings); +} + +export async function getResumeImportSettings() { + return clone(store.resumeImportSettings); +} + +export async function createOrUpdateResumeImportSettings(data: { + imapServer?: string; + port?: string; + account?: string; + authCode?: string; +}) { + store.resumeImportSettings = { + imapServer: data.imapServer ?? store.resumeImportSettings.imapServer, + port: data.port ?? store.resumeImportSettings.port, + account: data.account ?? store.resumeImportSettings.account, + authCode: data.authCode ?? store.resumeImportSettings.authCode, + }; + return clone(store.resumeImportSettings); +} diff --git a/src/lib/db/queries.ts b/src/lib/db/queries.ts index a3a6d19..fbeb3c4 100644 --- a/src/lib/db/queries.ts +++ b/src/lib/db/queries.ts @@ -1,3 +1,5 @@ +export * from './local-queries'; + import { db, schema } from './index'; import { eq, and, sql, count, gt, lt, gte, or, asc, like, isNotNull, desc, isNull } from 'drizzle-orm'; import { MySql2Database } from 'drizzle-orm/mysql2'; @@ -16,38 +18,38 @@ function extractInsertId(result: any): number { } // ==================== 用户相关操作 ==================== -export async function getUsers() { +async function getUsers() { return await db.select().from(schema.users); } -export async function getUserById(id: number) { +async function getUserById(id: number) { const users = await db.select().from(schema.users).where(eq(schema.users.id, id)); return users[0] ? [users[0]] : []; } -export async function getUserByEmail(email: string) { +async function getUserByEmail(email: string) { const users = await db.select().from(schema.users).where(eq(schema.users.email, email)); return users[0] ? [users[0]] : []; } -export async function createUser(data: typeof schema.users.$inferInsert) { +async function createUser(data: typeof schema.users.$inferInsert) { const result = await db.insert(schema.users).values(data); const insertId = extractInsertId(result); return await db.select().from(schema.users).where(eq(schema.users.id, insertId)); } -export async function updateUser(id: number, data: Partial) { +async function updateUser(id: number, data: Partial) { await db.update(schema.users).set(data).where(eq(schema.users.id, id)); return await db.select().from(schema.users).where(eq(schema.users.id, id)); } -export async function deleteUser(id: number) { +async function deleteUser(id: number) { return await db.delete(schema.users).where(eq(schema.users.id, id)); } // ==================== 面试官相关操作 ==================== // 获取所有面试官(role 为 interviewer 或 member 的用户) -export async function getInterviewers() { +async function getInterviewers() { const result = await db.select({ id: schema.users.id, name: schema.users.name, @@ -65,55 +67,55 @@ export async function getInterviewers() { } // ==================== 邮件模板相关操作 ==================== -export async function getEmailTemplates() { +async function getEmailTemplates() { return await db.select().from(schema.emailTemplates); } -export async function getEmailTemplateById(id: number) { +async function getEmailTemplateById(id: number) { return await db.select().from(schema.emailTemplates).where(eq(schema.emailTemplates.id, id)); } -export async function createEmailTemplate(data: typeof schema.emailTemplates.$inferInsert) { +async function createEmailTemplate(data: typeof schema.emailTemplates.$inferInsert) { const result = await db.insert(schema.emailTemplates).values(data); const insertId = extractInsertId(result); return await db.select().from(schema.emailTemplates).where(eq(schema.emailTemplates.id, insertId)); } -export async function updateEmailTemplate(id: number, data: Partial) { +async function updateEmailTemplate(id: number, data: Partial) { await db.update(schema.emailTemplates).set(data).where(eq(schema.emailTemplates.id, id)); return await db.select().from(schema.emailTemplates).where(eq(schema.emailTemplates.id, id)); } -export async function deleteEmailTemplate(id: number) { +async function deleteEmailTemplate(id: number) { return await db.delete(schema.emailTemplates).where(eq(schema.emailTemplates.id, id)); } // ==================== 邮件历史相关操作 ==================== -export async function getEmailHistory() { +async function getEmailHistory() { return await db.select().from(schema.emailHistory).orderBy(schema.emailHistory.sentAt); } -export async function getEmailHistoryById(id: number) { +async function getEmailHistoryById(id: number) { return await db.select().from(schema.emailHistory).where(eq(schema.emailHistory.id, id)); } -export async function createEmailHistory(data: typeof schema.emailHistory.$inferInsert) { +async function createEmailHistory(data: typeof schema.emailHistory.$inferInsert) { const result = await db.insert(schema.emailHistory).values(data); const insertId = extractInsertId(result); return await db.select().from(schema.emailHistory).where(eq(schema.emailHistory.id, insertId)); } -export async function deleteEmailHistory(id: number) { +async function deleteEmailHistory(id: number) { return await db.delete(schema.emailHistory).where(eq(schema.emailHistory.id, id)); } // ==================== 邮件配置相关操作 ==================== -export async function getEmailConfig() { +async function getEmailConfig() { const configs = await db.select().from(schema.emailConfig).limit(1); return configs[0] || null; } -export async function createOrUpdateEmailConfig(data: typeof schema.emailConfig.$inferInsert) { +async function createOrUpdateEmailConfig(data: typeof schema.emailConfig.$inferInsert) { // 先删除现有配置,再插入新配置(简化处理) await db.delete(schema.emailConfig); const result = await db.insert(schema.emailConfig).values(data); @@ -122,7 +124,7 @@ export async function createOrUpdateEmailConfig(data: typeof schema.emailConfig. } // ==================== 活动日志相关操作 ==================== -export async function getActivityLogs(page: number = 1, limit: number = 10) { +async function getActivityLogs(page: number = 1, limit: number = 10) { try { const offset = (page - 1) * limit; @@ -163,21 +165,21 @@ export async function getActivityLogs(page: number = 1, limit: number = 10) { } } -export async function createActivityLog(data: typeof schema.activityLogs.$inferInsert) { +async function createActivityLog(data: typeof schema.activityLogs.$inferInsert) { const result = await db.insert(schema.activityLogs).values(data); const insertId = extractInsertId(result); return await db.select().from(schema.activityLogs).where(eq(schema.activityLogs.id, insertId)); } // ==================== 评论相关操作 ==================== -export async function getCommentsByStudentId(studentId: number) { +async function getCommentsByStudentId(studentId: number) { return await db.select() .from(schema.comments) .where(eq(schema.comments.studentId, studentId)) .orderBy(sql`${schema.comments.timestamp} ASC`); } -export async function createComment(data: typeof schema.comments.$inferInsert) { +async function createComment(data: typeof schema.comments.$inferInsert) { // 保存 timestamp 用于后续查询 const timestamp = data.timestamp || new Date(); @@ -235,50 +237,50 @@ export async function createComment(data: typeof schema.comments.$inferInsert) { return result; } -export async function deleteComment(id: number) { +async function deleteComment(id: number) { return await db.delete(schema.comments).where(eq(schema.comments.id, id)); } // ==================== 学生/简历相关操作 ==================== -export async function getStudents() { +async function getStudents() { return await db.select().from(schema.students); } -export async function getStudentById(id: number) { +async function getStudentById(id: number) { return await db.select().from(schema.students).where(eq(schema.students.id, id)); } -export async function getStudentsByStatus(status: string) { +async function getStudentsByStatus(status: string) { return await db.select().from(schema.students).where(eq(schema.students.status, status as any)); } -export async function getStudentsByDepartment(department: string) { +async function getStudentsByDepartment(department: string) { return await db.select().from(schema.students).where(eq(schema.students.department, department)); } -export async function createStudent(data: typeof schema.students.$inferInsert) { +async function createStudent(data: typeof schema.students.$inferInsert) { const result = await db.insert(schema.students).values(data); const insertId = extractInsertId(result); return await db.select().from(schema.students).where(eq(schema.students.id, insertId)); } -export async function updateStudent(id: number, data: Partial) { +async function updateStudent(id: number, data: Partial) { await db.update(schema.students).set(data).where(eq(schema.students.id, id)); return await db.select().from(schema.students).where(eq(schema.students.id, id)); } -export async function deleteStudent(id: number) { +async function deleteStudent(id: number) { return await db.delete(schema.students).where(eq(schema.students.id, id)); } // ==================== 面试相关操作 ==================== // 获取所有面试 -export async function getInterviews() { +async function getInterviews() { return await db.select().from(schema.interviews); } // 获取近期面试(已安排但未开始的面试) -export async function getUpcomingInterviews(limit: number = 10) { +async function getUpcomingInterviews(limit: number = 10) { // 获取状态为 pending_interview 或 interviewing,且有日期的面试 // 按日期升序排列 const today = new Date(); @@ -302,7 +304,7 @@ export async function getUpcomingInterviews(limit: number = 10) { } // 获取今日面试 -export async function getTodayInterviews() { +async function getTodayInterviews() { const today = new Date(); const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); @@ -322,35 +324,35 @@ export async function getTodayInterviews() { return result; } -export async function getInterviewById(id: number) { +async function getInterviewById(id: number) { return await db.select().from(schema.interviews).where(eq(schema.interviews.id, id)); } -export async function getInterviewsByStudentId(studentId: number) { +async function getInterviewsByStudentId(studentId: number) { return await db.select().from(schema.interviews).where(eq(schema.interviews.studentId, studentId)); } -export async function getInterviewsByStage(stage: string) { +async function getInterviewsByStage(stage: string) { return await db.select().from(schema.interviews).where(eq(schema.interviews.stage, stage as any)); } -export async function createInterview(data: typeof schema.interviews.$inferInsert) { +async function createInterview(data: typeof schema.interviews.$inferInsert) { const result = await db.insert(schema.interviews).values(data); const insertId = extractInsertId(result); return await db.select().from(schema.interviews).where(eq(schema.interviews.id, insertId)); } -export async function updateInterview(id: number, data: Partial) { +async function updateInterview(id: number, data: Partial) { await db.update(schema.interviews).set(data).where(eq(schema.interviews.id, id)); return await db.select().from(schema.interviews).where(eq(schema.interviews.id, id)); } -export async function deleteInterview(id: number) { +async function deleteInterview(id: number) { return await db.delete(schema.interviews).where(eq(schema.interviews.id, id)); } // ==================== 统计相关操作 ==================== -export async function getStudentStats() { +async function getStudentStats() { const allStudents = await db.select().from(schema.students); const stats = { @@ -372,7 +374,7 @@ export async function getStudentStats() { return stats; } -export async function getInterviewStats() { +async function getInterviewStats() { const allInterviews = await db.select().from(schema.interviews); const stats = { @@ -395,7 +397,7 @@ export async function getInterviewStats() { } // ==================== 批量操作 ==================== -export async function batchCreateStudents(data: typeof schema.students.$inferInsert[]) { +async function batchCreateStudents(data: typeof schema.students.$inferInsert[]) { const result = await db.insert(schema.students).values(data); // 批量插入时,lastInsertId 是第一条记录的 ID const startId = extractInsertId(result); @@ -403,7 +405,7 @@ export async function batchCreateStudents(data: typeof schema.students.$inferIns return await db.select().from(schema.students).where(sql`${schema.students.id} BETWEEN ${startId} AND ${endId}`); } -export async function batchUpdateStudentStatus(ids: number[], status: string) { +async function batchUpdateStudentStatus(ids: number[], status: string) { // 使用 in 条件批量更新 return await db.update(schema.students) .set({ status: status as any }) @@ -411,7 +413,7 @@ export async function batchUpdateStudentStatus(ids: number[], status: string) { } // ==================== 部门分布统计 ==================== -export async function getDepartmentDistribution() { +async function getDepartmentDistribution() { // 使用 Drizzle ORM 的查询构建器 const result = await db .select({ @@ -458,7 +460,7 @@ export async function getDepartmentDistribution() { } // ==================== 仪表盘综合统计 ==================== -export async function getDashboardStats() { +async function getDashboardStats() { // 获取收件箱简历数量 (新投递的简历,状态为 pending) const inboxResult = await db .select({ count: count() }) @@ -589,7 +591,7 @@ export async function getDashboardStats() { } // ==================== 趋势数据 ==================== -export async function getTrendData(days: number = 7) { +async function getTrendData(days: number = 7) { // 生成最近 days 天的日期列表 const dateList: string[] = []; const today = new Date(); @@ -630,7 +632,7 @@ export async function getTrendData(days: number = 7) { } // ==================== 部门列表 ==================== -export async function getDepartments() { +async function getDepartments() { try { // 从 students 表中获取不同的部门 let studentRows: { department: string | null }[] = []; @@ -714,7 +716,7 @@ export async function getDepartments() { /** * 获取通知列表 */ -export async function getNotifications(userId?: number, limit = 20) { +async function getNotifications(userId?: number, limit = 20) { try { let query = db.select().from(schema.notifications); @@ -739,7 +741,7 @@ export async function getNotifications(userId?: number, limit = 20) { /** * 获取未读通知数量 */ -export async function getUnreadNotificationCount(userId?: number) { +async function getUnreadNotificationCount(userId?: number) { try { if (userId !== undefined) { const result = await db.select({ count: count() }) @@ -766,7 +768,7 @@ export async function getUnreadNotificationCount(userId?: number) { /** * 标记通知为已读 */ -export async function markNotificationAsRead(id: number) { +async function markNotificationAsRead(id: number) { try { await db.update(schema.notifications) .set({ unread: '0' }) @@ -781,7 +783,7 @@ export async function markNotificationAsRead(id: number) { /** * 标记所有通知为已读 */ -export async function markAllNotificationsAsRead(userId?: number) { +async function markAllNotificationsAsRead(userId?: number) { try { if (userId !== undefined) { await db.update(schema.notifications) @@ -807,7 +809,7 @@ export async function markAllNotificationsAsRead(userId?: number) { /** * 添加通知 */ -export async function addNotification(notification: typeof schema.notifications.$inferInsert) { +async function addNotification(notification: typeof schema.notifications.$inferInsert) { try { const result = await db.insert(schema.notifications).values(notification); return result; @@ -822,7 +824,7 @@ export async function addNotification(notification: typeof schema.notifications. /** * 获取 AI 设置 */ -export async function getAiSettings() { +async function getAiSettings() { try { const settings = await db.select().from(schema.aiSettings).limit(1); if (settings.length === 0) { @@ -873,7 +875,7 @@ export async function getAiSettings() { /** * 创建或更新 AI 设置 */ -export async function createOrUpdateAiSettings(data: { +async function createOrUpdateAiSettings(data: { vision?: { endpoint?: string; model?: string; @@ -924,7 +926,7 @@ export async function createOrUpdateAiSettings(data: { /** * 获取 GitHub 设置 */ -export async function getGithubSettings() { +async function getGithubSettings() { try { const settings = await db.select().from(schema.githubSettings).limit(1); if (settings.length === 0) { @@ -957,7 +959,7 @@ export async function getGithubSettings() { /** * 创建或更新 GitHub 设置 */ -export async function createOrUpdateGithubSettings(data: { +async function createOrUpdateGithubSettings(data: { clientId?: string; clientSecret?: string; organization?: string; @@ -995,7 +997,7 @@ export async function createOrUpdateGithubSettings(data: { /** * 获取所有 API 密钥 */ -export async function getApiKeys() { +async function getApiKeys() { try { return await db.select().from(schema.apiKeys); } catch (error) { @@ -1007,7 +1009,7 @@ export async function getApiKeys() { /** * 创建 API 密钥 */ -export async function createApiKey(data: typeof schema.apiKeys.$inferInsert) { +async function createApiKey(data: typeof schema.apiKeys.$inferInsert) { try { await db.insert(schema.apiKeys).values(data); const keys = await db.select().from(schema.apiKeys).where(eq(schema.apiKeys.id, data.id)); @@ -1021,7 +1023,7 @@ export async function createApiKey(data: typeof schema.apiKeys.$inferInsert) { /** * 删除 API 密钥 */ -export async function deleteApiKey(id: string) { +async function deleteApiKey(id: string) { try { await db.delete(schema.apiKeys).where(eq(schema.apiKeys.id, id)); return true; @@ -1034,7 +1036,7 @@ export async function deleteApiKey(id: string) { /** * 替换所有 API 密钥 */ -export async function replaceApiKeys(keys: typeof schema.apiKeys.$inferInsert[]) { +async function replaceApiKeys(keys: typeof schema.apiKeys.$inferInsert[]) { try { // 先删除所有现有密钥 await db.delete(schema.apiKeys); @@ -1056,7 +1058,7 @@ export async function replaceApiKeys(keys: typeof schema.apiKeys.$inferInsert[]) /** * 获取通知设置 */ -export async function getNotificationSettings() { +async function getNotificationSettings() { try { const settings = await db.select().from(schema.notificationSettings).limit(1); if (settings.length === 0) { @@ -1095,7 +1097,7 @@ export async function getNotificationSettings() { /** * 创建或更新通知设置 */ -export async function createOrUpdateNotificationSettings(data: { +async function createOrUpdateNotificationSettings(data: { webhookUrl?: string; triggers?: { new_resume?: boolean; @@ -1143,7 +1145,7 @@ export async function createOrUpdateNotificationSettings(data: { /** * 获取平台设置 */ -export async function getPlatformSettings() { +async function getPlatformSettings() { try { const settings = await db.select().from(schema.platformSettings).limit(1); if (settings.length === 0) { @@ -1167,7 +1169,7 @@ export async function getPlatformSettings() { /** * 创建或更新平台设置 */ -export async function createOrUpdatePlatformSettings(data: { +async function createOrUpdatePlatformSettings(data: { departments?: string[]; }) { try { @@ -1201,7 +1203,7 @@ export async function createOrUpdatePlatformSettings(data: { /** * 获取简历导入设置 */ -export async function getResumeImportSettings() { +async function getResumeImportSettings() { try { const settings = await db.select().from(schema.resumeImportSettings).limit(1); if (settings.length === 0) { @@ -1234,7 +1236,7 @@ export async function getResumeImportSettings() { /** * 创建或更新简历导入设置 */ -export async function createOrUpdateResumeImportSettings(data: { +async function createOrUpdateResumeImportSettings(data: { imapServer?: string; port?: string; account?: string; @@ -1265,4 +1267,4 @@ export async function createOrUpdateResumeImportSettings(data: { console.error('Error updating resume import settings:', error); throw error; } -} \ No newline at end of file +}