diff --git a/backend/package-lock.json b/backend/package-lock.json
index 2acf6b9..98ad68f 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1378,7 +1378,10 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"@prisma/config": "6.19.3",
"@prisma/engines": "6.19.3"
diff --git a/backend/prisma/prisma/dev.db b/backend/prisma/prisma/dev.db
index 240560f..106cc27 100644
Binary files a/backend/prisma/prisma/dev.db and b/backend/prisma/prisma/dev.db differ
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index f33388f..2c39672 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -32,8 +32,11 @@ model User {
email String @unique
passwordHash String
studentId String? @unique
+ employeeId String? @unique // 新增:馆员工号
role Role @default(STUDENT)
createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
loans Loan[]
ratings Rating[]
holds Hold[]
@@ -43,14 +46,16 @@ model User {
}
model Book {
- id Int @id @default(autoincrement())
+ id Int @id @default(autoincrement())
title String
author String
- isbn String @unique
+ isbn String @unique
genre String
description String?
- language String @default("English")
- createdAt DateTime @default(now())
+ language String @default("English")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
copies Copy[]
ratings Rating[]
holds Hold[]
@@ -58,17 +63,19 @@ model Book {
}
model Copy {
- id Int @id @default(autoincrement())
- bookId Int
- book Book @relation(fields: [bookId], references: [id])
- barcode String @unique
- floor Int
- libraryArea String
- shelfNo String
- shelfLevel Int
- status CopyStatus @default(AVAILABLE)
- createdAt DateTime @default(now())
- loans Loan[]
+ id Int @id @default(autoincrement())
+ bookId Int
+ book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
+ barcode String @unique
+ floor Int?
+ libraryArea String?
+ shelfNo String?
+ shelfLevel Int?
+ status CopyStatus @default(AVAILABLE)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ loans Loan[]
}
model Loan {
@@ -78,10 +85,12 @@ model Loan {
checkoutDate DateTime @default(now())
dueDate DateTime
returnDate DateTime?
- fineAmount Float? @default(0)
- finePaid Boolean? @default(false)
- fineForgiven Boolean? @default(false)
+ fineAmount Float @default(0)
+ finePaid Boolean @default(false)
+ fineForgiven Boolean @default(false)
createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
copy Copy @relation(fields: [copyId], references: [id])
user User @relation(fields: [userId], references: [id])
}
@@ -92,7 +101,8 @@ model Rating {
userId Int
stars Int @default(1)
createdAt DateTime @default(now())
- book Book @relation(fields: [bookId], references: [id])
+
+ book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
@@unique([bookId, userId])
@@ -104,7 +114,8 @@ model Hold {
userId Int
status HoldStatus @default(WAITING)
createdAt DateTime @default(now())
- book Book @relation(fields: [bookId], references: [id])
+
+ book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
}
@@ -113,16 +124,19 @@ model Wishlist {
bookId Int
userId Int
createdAt DateTime @default(now())
- book Book @relation(fields: [bookId], references: [id])
+
+ book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
@@unique([bookId, userId])
}
model Config {
- id Int @id @default(autoincrement())
- key String @unique
- value String
+ id Int @id @default(autoincrement())
+ key String @unique
+ value String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
}
model AuditLog {
@@ -133,20 +147,10 @@ model AuditLog {
entityId Int?
detail String?
createdAt DateTime @default(now())
+
user User? @relation(fields: [userId], references: [id])
}
-model Librarian {
- id Int @id @default(autoincrement())
- employeeId String @unique @map("employee_id")
- name String
- password String
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("librarians")
-}
-
model Announcement {
id Int @id @default(autoincrement())
title String
@@ -156,6 +160,7 @@ model Announcement {
expiryDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+
publishers AnnouncementPublisher[]
}
@@ -163,6 +168,7 @@ model AnnouncementPublisher {
id Int @id @default(autoincrement())
userId Int
announcementId Int
+
user User @relation(fields: [userId], references: [id])
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js
index c397c37..dbdd17f 100644
--- a/backend/prisma/seed.js
+++ b/backend/prisma/seed.js
@@ -2,119 +2,576 @@ const { PrismaClient } = require('@prisma/client');
const bcrypt = require('bcrypt');
const prisma = new PrismaClient();
+// 配置常量
+const CONFIG = {
+ PASSWORD_HASH_ROUNDS: 10,
+ DEFAULT_PASSWORD: 'password123',
+ ADMIN_PASSWORD: 'admin123',
+ LIBRARIAN_PASSWORD: 'lib123',
+ STUDENT_PASSWORD: 'student123',
+};
+
+// 预定义的测试账号
+const TEST_ACCOUNTS = {
+ admin: {
+ email: 'admin@library.com',
+ password: CONFIG.ADMIN_PASSWORD,
+ name: '系统管理员',
+ },
+ librarians: [
+ {
+ employeeId: 'LIB001',
+ password: CONFIG.LIBRARIAN_PASSWORD,
+ name: '张明',
+ },
+ {
+ employeeId: 'LIB002',
+ password: CONFIG.LIBRARIAN_PASSWORD,
+ name: '李华',
+ },
+ {
+ employeeId: 'LIB003',
+ password: CONFIG.LIBRARIAN_PASSWORD,
+ name: '王芳',
+ },
+ ],
+ students: [
+ {
+ studentId: 'S2021001',
+ email: 'student1@university.edu',
+ password: CONFIG.STUDENT_PASSWORD,
+ name: '张三',
+ },
+ {
+ studentId: 'S2021002',
+ email: 'student2@university.edu',
+ password: CONFIG.STUDENT_PASSWORD,
+ name: '李四',
+ },
+ {
+ studentId: 'S2021003',
+ email: 'student3@university.edu',
+ password: CONFIG.STUDENT_PASSWORD,
+ name: '王五',
+ },
+ {
+ studentId: 'S2021004',
+ email: 'student4@university.edu',
+ password: CONFIG.STUDENT_PASSWORD,
+ name: '赵六',
+ },
+ {
+ studentId: 'S2021005',
+ email: 'student5@university.edu',
+ password: CONFIG.STUDENT_PASSWORD,
+ name: '孙七',
+ },
+ ],
+};
+
+// 图书数据
+const BOOKS_DATA = [
+ // Technology
+ {
+ title: 'The Pragmatic Programmer',
+ author: 'David Thomas & Andrew Hunt',
+ isbn: '978-0201616224',
+ genre: 'Technology',
+ description: 'A must-read for any programmer, filled with practical advice and best practices.',
+ language: 'English',
+ },
+ {
+ title: 'Clean Code',
+ author: 'Robert C. Martin',
+ isbn: '978-0132350884',
+ genre: 'Technology',
+ description: 'A handbook of agile software craftsmanship.',
+ language: 'English',
+ },
+ {
+ title: 'Designing Data-Intensive Applications',
+ author: 'Martin Kleppmann',
+ isbn: '978-1449373320',
+ genre: 'Technology',
+ description: 'The big ideas behind reliable, scalable, and maintainable systems.',
+ language: 'English',
+ },
+ {
+ title: "You Don't Know JS",
+ author: 'Kyle Simpson',
+ isbn: '978-1491904244',
+ genre: 'Technology',
+ description: 'Deep dive into JavaScript language features.',
+ language: 'English',
+ },
+ // Fiction
+ {
+ title: 'The Great Gatsby',
+ author: 'F. Scott Fitzgerald',
+ isbn: '978-0743273565',
+ genre: 'Fiction',
+ description: 'A story of decadence and excess in Jazz Age America.',
+ language: 'English',
+ },
+ {
+ title: 'To Kill a Mockingbird',
+ author: 'Harper Lee',
+ isbn: '978-0446310789',
+ genre: 'Fiction',
+ description: 'A powerful story of racial injustice in the American South.',
+ language: 'English',
+ },
+ {
+ title: '1984',
+ author: 'George Orwell',
+ isbn: '978-0451524935',
+ genre: 'Fiction',
+ description: 'A dystopian novel about totalitarianism and surveillance.',
+ language: 'English',
+ },
+ {
+ title: 'Pride and Prejudice',
+ author: 'Jane Austen',
+ isbn: '978-0141439518',
+ genre: 'Fiction',
+ description: 'A classic romance novel about manners and marriage.',
+ language: 'English',
+ },
+ // Science
+ {
+ title: 'A Brief History of Time',
+ author: 'Stephen Hawking',
+ isbn: '978-0553380163',
+ genre: 'Science',
+ description: 'From the Big Bang to black holes.',
+ language: 'English',
+ },
+ {
+ title: 'The Selfish Gene',
+ author: 'Richard Dawkins',
+ isbn: '978-0199291151',
+ genre: 'Science',
+ description: 'A gene-centered view of evolution.',
+ language: 'English',
+ },
+ {
+ title: 'Cosmos',
+ author: 'Carl Sagan',
+ isbn: '978-0345539434',
+ genre: 'Science',
+ description: 'A journey through space and time.',
+ language: 'English',
+ },
+ {
+ title: 'The Double Helix',
+ author: 'James Watson',
+ isbn: '978-0743216302',
+ genre: 'Science',
+ description: 'The story of the discovery of DNA structure.',
+ language: 'English',
+ },
+ // History
+ {
+ title: 'Sapiens',
+ author: 'Yuval Noah Harari',
+ isbn: '978-0062316097',
+ genre: 'History',
+ description: 'A brief history of humankind.',
+ language: 'English',
+ },
+ {
+ title: 'Guns, Germs, and Steel',
+ author: 'Jared Diamond',
+ isbn: '978-0393317558',
+ genre: 'History',
+ description: 'The fates of human societies.',
+ language: 'English',
+ },
+ {
+ title: 'The Silk Roads',
+ author: 'Peter Frankopan',
+ isbn: '978-1101912379',
+ genre: 'History',
+ description: 'A new history of the world.',
+ language: 'English',
+ },
+ // Management
+ {
+ title: 'The Lean Startup',
+ author: 'Eric Ries',
+ isbn: '978-0307887894',
+ genre: 'Management',
+ description: 'How today\'s entrepreneurs use continuous innovation.',
+ language: 'English',
+ },
+ {
+ title: 'Good to Great',
+ author: 'Jim Collins',
+ isbn: '978-0066620992',
+ genre: 'Management',
+ description: 'Why some companies make the leap and others don\'t.',
+ language: 'English',
+ },
+ {
+ title: 'Drive',
+ author: 'Daniel H. Pink',
+ isbn: '978-1594484803',
+ genre: 'Management',
+ description: 'The surprising truth about what motivates us.',
+ language: 'English',
+ },
+ // Chinese Books
+ {
+ title: '三体',
+ author: '刘慈欣',
+ isbn: '978-7536692930',
+ genre: 'Science Fiction',
+ description: '中国科幻文学的里程碑之作。',
+ language: 'Chinese',
+ },
+ {
+ title: '活着',
+ author: '余华',
+ isbn: '978-7506365437',
+ genre: 'Fiction',
+ description: '讲述了一个人历尽世间沧桑和磨难的一生。',
+ language: 'Chinese',
+ },
+];
+
+// 系统配置
+const SYSTEM_CONFIGS = [
+ { key: 'FINE_RATE_PER_DAY', value: '0.50', description: '每日逾期罚款金额(元)' },
+ { key: 'MAX_BORROW_STUDENT', value: '3', description: '学生最大借阅数量' },
+ { key: 'LOAN_DURATION_DAYS', value: '30', description: '默认借阅天数' },
+ { key: 'MAX_RENEW_TIMES', value: '2', description: '最大续借次数' },
+ { key: 'RENEW_DURATION_DAYS', value: '15', description: '续借天数' },
+ { key: 'LIBRARY_NAME', value: '大学图书馆管理系统', description: '图书馆名称' },
+ { key: 'LIBRARY_HOURS', value: '周一至周五 8:00-22:00,周末 9:00-21:00', description: '开放时间' },
+ { key: 'CONTACT_EMAIL', value: 'library@university.edu', description: '联系邮箱' },
+ { key: 'CONTACT_PHONE', value: '123-4567-8901', description: '联系电话' },
+];
+
+// 辅助函数
+function generateISBN(index) {
+ return `978-${String(index).padStart(10, '0')}`;
+}
+
+function generateBarcode(bookId, copyNumber) {
+ return `BC-${String(bookId).padStart(6, '0')}-${String(copyNumber).padStart(3, '0')}`;
+}
+
+function getRandomInt(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+function getRandomElement(array) {
+ return array[Math.floor(Math.random() * array.length)];
+}
+
+// 主函数
async function main() {
- // 清空现有数据(避免重复键冲突)
- await prisma.auditLog.deleteMany();
- await prisma.hold.deleteMany();
- await prisma.wishlist.deleteMany();
- await prisma.rating.deleteMany();
- await prisma.loan.deleteMany();
- await prisma.book.deleteMany();
- await prisma.user.deleteMany();
- await prisma.config.deleteMany();
-
- // 创建用户
- const adminPassword = await bcrypt.hash('admin123', 10);
- const librarianPassword = await bcrypt.hash('lib123', 10);
- const studentPassword = await bcrypt.hash('student123', 10);
-
- await prisma.user.create({
+ console.log('🚀 开始初始化数据库...\n');
+
+ // ==================== 清空现有数据 ====================
+ console.log('📦 清空现有数据...');
+
+ const deleteOperations = [
+ { name: '审计日志', model: prisma.auditLog },
+ { name: '公告发布者', model: prisma.announcementPublisher },
+ { name: '公告', model: prisma.announcement },
+ { name: '预约', model: prisma.hold },
+ { name: '心愿单', model: prisma.wishlist },
+ { name: '评分', model: prisma.rating },
+ { name: '借阅记录', model: prisma.loan },
+ { name: '副本', model: prisma.copy },
+ { name: '图书', model: prisma.book },
+ { name: '用户', model: prisma.user },
+ { name: '馆员', model: prisma.librarian },
+ { name: '配置', model: prisma.config },
+ ];
+
+ for (const op of deleteOperations) {
+ try {
+ await op.model.deleteMany();
+ console.log(` ✓ 清空${op.name}`);
+ } catch (error) {
+ // 某些表可能不存在,忽略错误
+ }
+ }
+
+ console.log('\n✅ 数据清空完成\n');
+
+ // ==================== 创建用户 ====================
+ console.log('👥 创建用户账号...');
+
+ // 创建管理员
+ const adminPasswordHash = await bcrypt.hash(TEST_ACCOUNTS.admin.password, CONFIG.PASSWORD_HASH_ROUNDS);
+ const admin = await prisma.user.create({
data: {
- name: 'Admin User',
- email: 'admin@library.com',
- passwordHash: adminPassword,
+ name: TEST_ACCOUNTS.admin.name,
+ email: TEST_ACCOUNTS.admin.email,
+ passwordHash: adminPasswordHash,
role: 'ADMIN',
},
});
+ console.log(` ✓ 管理员: ${admin.email} / ${TEST_ACCOUNTS.admin.password}`);
- await prisma.user.create({
- data: {
- name: 'Librarian User',
- email: 'librarian@library.com',
- passwordHash: librarianPassword,
- role: 'LIBRARIAN',
- },
- });
+ // 创建学生
+ const students = [];
+ const studentPasswordHash = await bcrypt.hash(CONFIG.STUDENT_PASSWORD, CONFIG.PASSWORD_HASH_ROUNDS);
+
+ for (const studentData of TEST_ACCOUNTS.students) {
+ const student = await prisma.user.create({
+ data: {
+ name: studentData.name,
+ email: studentData.email,
+ studentId: studentData.studentId,
+ passwordHash: studentPasswordHash,
+ role: 'STUDENT',
+ },
+ });
+ students.push(student);
+ console.log(` ✓ 学生: ${student.studentId} - ${student.name} (${student.email})`);
+ }
- await prisma.user.create({
- data: {
- name: 'Student One',
- email: 'student1@university.edu',
- passwordHash: studentPassword,
- studentId: 'S12345',
- role: 'STUDENT',
- },
- });
+ // 创建馆员(使用 User 表,role='LIBRARIAN')
+const librarianPasswordHash = await bcrypt.hash(CONFIG.LIBRARIAN_PASSWORD, CONFIG.PASSWORD_HASH_ROUNDS);
- await prisma.user.create({
+for (const librarianData of TEST_ACCOUNTS.librarians) {
+ const librarian = await prisma.user.create({
data: {
- name: 'Student Two',
- email: 'student2@university.edu',
- passwordHash: studentPassword,
- studentId: 'S67890',
- role: 'STUDENT',
+ email: `${librarianData.employeeId.toLowerCase()}@library.com`, // 根据工号生成一个登录邮箱
+ name: librarianData.name,
+ employeeId: librarianData.employeeId, // 确保你的 User 模型有这个字段
+ passwordHash: librarianPasswordHash,
+ role: 'LIBRARIAN', // 角色设置为馆员
},
});
+ console.log(` ✓ 馆员: ${librarian.employeeId} - ${librarian.name} / ${CONFIG.LIBRARIAN_PASSWORD}`);
+}
- // 图书数据
- const booksData = [
- // Technology
- { title: 'The Pragmatic Programmer', author: 'David Thomas', genre: 'Technology', available: true },
- { title: 'Clean Code', author: 'Robert C. Martin', genre: 'Technology', available: true },
- { title: 'Designing Data-Intensive Applications', author: 'Martin Kleppmann', genre: 'Technology', available: true },
- { title: "You Don't Know JS", author: 'Kyle Simpson', genre: 'Technology', available: false },
- // Fiction
- { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Fiction', available: true },
- { title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', available: true },
- { title: '1984', author: 'George Orwell', genre: 'Fiction', available: true },
- { title: 'Pride and Prejudice', author: 'Jane Austen', genre: 'Fiction', available: false },
- // Science
- { title: 'A Brief History of Time', author: 'Stephen Hawking', genre: 'Science', available: true },
- { title: 'The Selfish Gene', author: 'Richard Dawkins', genre: 'Science', available: true },
- { title: 'Cosmos', author: 'Carl Sagan', genre: 'Science', available: true },
- { title: 'The Double Helix', author: 'James Watson', genre: 'Science', available: false },
- // History
- { title: 'Sapiens', author: 'Yuval Noah Harari', genre: 'History', available: true },
- { title: 'Guns, Germs, and Steel', author: 'Jared Diamond', genre: 'History', available: true },
- { title: 'The Silk Roads', author: 'Peter Frankopan', genre: 'History', available: true },
- { title: "A People's History of the United States", author: 'Howard Zinn', genre: 'History', available: false },
- // Management
- { title: 'The Lean Startup', author: 'Eric Ries', genre: 'Management', available: true },
- { title: 'Good to Great', author: 'Jim Collins', genre: 'Management', available: true },
- { title: 'Drive', author: 'Daniel H. Pink', genre: 'Management', available: true },
- { title: 'The Five Dysfunctions of a Team', author: 'Patrick Lencioni', genre: 'Management', available: false },
- ];
+ console.log('\n✅ 用户创建完成\n');
- for (const book of booksData) {
- await prisma.book.create({
- data: {
- title: book.title,
- author: book.author,
- isbn: `ISBN-${Math.random().toString(36).substring(2, 10)}`,
- genre: book.genre,
- description: `${book.title} is a great read.`,
- language: 'English',
- shelfLocation: `${book.genre}-${Math.floor(Math.random() * 100)}`,
- available: book.available, // 保留旧字段
- totalCopies: 1, // 新增
- availableCopies: book.available ? 1 : 0 // 新增:根据 available 初始化
+ // ==================== 创建系统配置 ====================
+ console.log('⚙️ 创建系统配置...');
+
+ for (const config of SYSTEM_CONFIGS) {
+ await prisma.config.create({
+ data: {
+ key: config.key,
+ value: config.value,
+ },
+ });
+ console.log(` ✓ ${config.key} = ${config.value} (${config.description})`);
+ }
+
+ console.log('\n✅ 系统配置创建完成\n');
+
+ // ==================== 创建图书和副本 ====================
+ console.log('📚 创建图书和副本...');
+
+ const floorOptions = [1, 2, 3, 4, 5];
+ const areaOptions = {
+ 'Technology': '科技图书区',
+ 'Fiction': '文学小说区',
+ 'Science': '自然科学区',
+ 'History': '历史地理区',
+ 'Management': '管理科学区',
+ 'Science Fiction': '科幻小说区',
+ };
+ const shelfOptions = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
+
+ let bookCount = 0;
+ let copyCount = 0;
+
+ for (const bookData of BOOKS_DATA) {
+ // 创建图书
+ const book = await prisma.book.create({
+ data: {
+ title: bookData.title,
+ author: bookData.author,
+ isbn: bookData.isbn,
+ genre: bookData.genre,
+ description: bookData.description,
+ language: bookData.language,
+ },
+ });
+
+ // 为每本书创建 1-5 个副本
+ const numberOfCopies = getRandomInt(1, 5);
+ const floor = getRandomElement(floorOptions);
+ const area = areaOptions[bookData.genre] || '综合图书区';
+ const shelf = getRandomElement(shelfOptions);
+
+ for (let i = 0; i < numberOfCopies; i++) {
+ const status = Math.random() > 0.3 ? 'AVAILABLE' : getRandomElement(['BORROWED', 'AVAILABLE']);
+
+ await prisma.copy.create({
+ data: {
+ bookId: book.id,
+ barcode: generateBarcode(book.id, i + 1),
+ floor: floor,
+ libraryArea: area,
+ shelfNo: shelf,
+ shelfLevel: getRandomInt(1, 5),
+ status: status,
+ },
+ });
+ copyCount++;
}
- });
+
+ bookCount++;
}
- // 添加配置项
- await prisma.config.create({
+ console.log(` ✓ 创建了 ${bookCount} 本图书,共 ${copyCount} 个副本`);
+
+ console.log('\n✅ 图书创建完成\n');
+
+ // ==================== 创建示例借阅记录 ====================
+ console.log('📋 创建示例借阅记录...');
+
+ if (students.length > 0) {
+ const allCopies = await prisma.copy.findMany({
+ where: { status: 'AVAILABLE' },
+ take: 10,
+ });
+
+ const loanCount = Math.min(5, allCopies.length);
+
+ for (let i = 0; i < loanCount; i++) {
+ const student = students[i % students.length];
+ const copy = allCopies[i];
+
+ const checkoutDate = new Date();
+ checkoutDate.setDate(checkoutDate.getDate() - getRandomInt(1, 60));
+
+ const dueDate = new Date(checkoutDate);
+ dueDate.setDate(dueDate.getDate() + 30);
+
+ const isReturned = Math.random() > 0.5;
+ const returnDate = isReturned ? new Date(dueDate.getTime() + getRandomInt(-5, 10) * 24 * 60 * 60 * 1000) : null;
+
+ let fineAmount = 0;
+ if (returnDate && returnDate > dueDate) {
+ const diffDays = Math.ceil((returnDate - dueDate) / (1000 * 60 * 60 * 24));
+ fineAmount = diffDays * 0.5;
+ }
+
+ await prisma.loan.create({
+ data: {
+ copyId: copy.id,
+ userId: student.id,
+ checkoutDate: checkoutDate,
+ dueDate: dueDate,
+ returnDate: returnDate,
+ fineAmount: fineAmount,
+ finePaid: fineAmount > 0 && Math.random() > 0.5,
+ },
+ });
+
+ // 更新副本状态
+ await prisma.copy.update({
+ where: { id: copy.id },
+ data: { status: isReturned ? 'AVAILABLE' : 'BORROWED' },
+ });
+ }
+
+ console.log(` ✓ 创建了 ${loanCount} 条借阅记录`);
+ }
+
+ console.log('\n✅ 示例数据创建完成\n');
+
+ // ==================== 创建示例评分 ====================
+ console.log('⭐ 创建示例评分...');
+
+ const allBooks = await prisma.book.findMany({ take: 10 });
+ let ratingCount = 0;
+
+ for (const book of allBooks) {
+ if (students.length > 0 && Math.random() > 0.5) {
+ const ratingStudents = students.slice(0, getRandomInt(1, 3));
+
+ for (const student of ratingStudents) {
+ try {
+ await prisma.rating.create({
+ data: {
+ bookId: book.id,
+ userId: student.id,
+ stars: getRandomInt(3, 5),
+ },
+ });
+ ratingCount++;
+ } catch (error) {
+ // 忽略重复评分
+ }
+ }
+ }
+ }
+
+ console.log(` ✓ 创建了 ${ratingCount} 条评分`);
+
+ console.log('\n✅ 评分创建完成\n');
+
+ // ==================== 创建审计日志 ====================
+ console.log('📝 创建审计日志...');
+
+ await prisma.auditLog.create({
data: {
- key: 'FINE_RATE_PER_DAY',
- value: '0.50',
+ userId: admin.id,
+ action: 'SYSTEM_INIT',
+ entity: 'System',
+ detail: '系统初始化完成,种子数据已加载',
},
});
+
+ console.log(' ✓ 审计日志创建完成');
- console.log('Seed data inserted successfully');
+ // ==================== 输出测试账号信息 ====================
+ console.log('\n' + '='.repeat(60));
+ console.log('🎉 数据库初始化完成!');
+ console.log('='.repeat(60));
+
+ console.log('\n📋 测试账号信息:');
+ console.log('-'.repeat(40));
+
+ console.log('\n👑 管理员 (Admin):');
+ console.log(` 邮箱: ${TEST_ACCOUNTS.admin.email}`);
+ console.log(` 密码: ${TEST_ACCOUNTS.admin.password}`);
+ console.log(` 姓名: ${TEST_ACCOUNTS.admin.name}`);
+
+ console.log('\n📚 馆员 (Librarian):');
+ TEST_ACCOUNTS.librarians.forEach((lib, index) => {
+ console.log(` ${index + 1}. 工号: ${lib.employeeId} / 密码: ${lib.password} (${lib.name})`);
+ });
+
+ console.log('\n🎓 学生 (Student):');
+ TEST_ACCOUNTS.students.slice(0, 3).forEach((student, index) => {
+ console.log(` ${index + 1}. 学号: ${student.studentId} / 密码: ${student.password} (${student.name})`);
+ console.log(` 邮箱: ${student.email}`);
+ });
+ if (TEST_ACCOUNTS.students.length > 3) {
+ console.log(` ... 还有 ${TEST_ACCOUNTS.students.length - 3} 个学生账号`);
+ }
+
+ console.log('\n📖 统计数据:');
+ console.log(` 图书总数: ${bookCount}`);
+ console.log(` 副本总数: ${copyCount}`);
+ console.log(` 学生总数: ${students.length}`);
+ console.log(` 馆员总数: ${librarians.length}`);
+
+ console.log('\n💡 提示:');
+ console.log(' 1. 馆员登录请使用统一登录接口,选择 "librarian" 类型');
+ console.log(' 2. 学生和管理员登录使用邮箱');
+ console.log(' 3. 馆员登录使用工号');
+
+ console.log('\n' + '='.repeat(60) + '\n');
}
+// 执行主函数
main()
.catch((e) => {
+ console.error('\n❌ 种子数据初始化失败:');
console.error(e);
process.exit(1);
})
diff --git a/backend/src/app.js b/backend/src/app.js
index fe2f16b..299b5f3 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -1,13 +1,57 @@
const express = require('express');
const cors = require('cors');
+<<<<<<< HEAD
+// 导入所有路由
const readersRouter = require('./routes/readers');
const authRouter = require('./routes/auth');
const loansRouter = require('./routes/loans');
+const booksRouter = require('./routes/books'); // ✅ 添加图书路由
+=======
+const readersRouter = require('./routes/readers');
+const authRouter = require('./routes/auth');
+const loansRouter = require('./routes/loans');
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
const announcementsRouter = require('./routes/announcements');
const app = express();
+<<<<<<< HEAD
+// CORS 配置
+app.use(cors({
+ origin: ['http://localhost:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'],
+ credentials: true
+}));
+app.use(express.json());
+
+// 健康检查
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'ok',
+ message: 'Library API is running',
+ timestamp: new Date().toISOString()
+ });
+});
+
+// API 路由挂载
+app.use('/api/readers', readersRouter);
+app.use('/api/auth', authRouter); // 认证路由(登录、注册)
+app.use('/api/books', booksRouter); // ✅ 图书管理路由
+app.use('/api/loans', loansRouter); // 借阅管理路由
+app.use('/api/announcements', announcementsRouter);
+
+// 404 处理
+app.use((req, res) => {
+ res.status(404).json({
+ success: false,
+ message: `Route ${req.method} ${req.path} not found`
+ });
+});
+
+// 全局错误处理
+app.use((error, req, res, next) => {
+ // Prisma 唯一约束冲突
+=======
app.use(cors());
app.use(express.json());
@@ -26,19 +70,54 @@ app.use((req, res) => {
});
app.use((error, req, res, next) => {
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
if (error && error.code === 'P2002') {
const target = Array.isArray(error.meta?.target)
? error.meta.target.join(', ')
: 'field';
return res.status(409).json({
+<<<<<<< HEAD
+ success: false,
+ message: `A record with that ${target} already exists.`,
+ error: error.code
+ });
+ }
+
+ // Prisma 记录不存在
+ if (error && error.code === 'P2025') {
+ return res.status(404).json({
+ success: false,
+ message: 'Record not found',
+ error: error.code
+ });
+ }
+
+ // JWT 错误
+ if (error && error.name === 'JsonWebTokenError') {
+ return res.status(401).json({
+ success: false,
+ message: 'Invalid token',
+ });
+ }
+
+ if (error && error.name === 'TokenExpiredError') {
+ return res.status(401).json({
+ success: false,
+ message: 'Token expired',
+=======
message: `A record with that ${target} already exists.`,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
});
}
console.error('Unhandled error:', error);
res.status(error?.statusCode || 500).json({
+<<<<<<< HEAD
+ success: false,
+=======
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
message: error?.message || 'Internal server error',
});
});
diff --git a/backend/src/lib/prisma.js b/backend/src/lib/prisma.js
index 0dbc802..b2bbc98 100644
--- a/backend/src/lib/prisma.js
+++ b/backend/src/lib/prisma.js
@@ -1,5 +1,18 @@
+<<<<<<< HEAD
+// lib/prisma.js
+const { PrismaClient } = require('@prisma/client');
+
+const prisma = new PrismaClient({
+ log: process.env.NODE_ENV === 'development'
+ ? ['query', 'info', 'warn', 'error']
+ : ['error']
+});
+
+module.exports = prisma;
+=======
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
module.exports = prisma;
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js
index f1f105d..6c8a68d 100644
--- a/backend/src/middleware/auth.js
+++ b/backend/src/middleware/auth.js
@@ -1,6 +1,9 @@
const prisma = require('../lib/prisma');
const { verifyToken } = require('../lib/token');
+<<<<<<< HEAD
+=======
const { toPublicUser } = require('../lib/user');
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
function extractBearerToken(authorizationHeader) {
if (!authorizationHeader) {
@@ -27,7 +30,11 @@ async function requireAuth(req, res, next) {
try {
const payload = verifyToken(token);
+<<<<<<< HEAD
+ const userId = Number(payload.sub || payload.id);
+=======
const userId = Number(payload.sub);
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
if (!userId) {
return res.status(401).json({ message: 'Token payload is invalid.' });
@@ -35,22 +42,76 @@ async function requireAuth(req, res, next) {
const user = await prisma.user.findUnique({
where: { id: userId },
+<<<<<<< HEAD
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ studentId: true,
+ employeeId: true,
+ role: true,
+ }
+=======
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
});
if (!user) {
return res.status(401).json({ message: 'User no longer exists.' });
}
+<<<<<<< HEAD
+ req.user = user;
+ req.auth = payload;
+ next();
+ } catch (error) {
+ return res.status(401).json({
+ message: error.message || 'Invalid or expired token.',
+=======
req.auth = payload;
req.user = toPublicUser(user);
next();
} catch (error) {
return res.status(401).json({
message: 'Invalid or expired token.',
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
});
}
}
+<<<<<<< HEAD
+// 馆员权限检查(包括管理员)
+function requireLibrarian(req, res, next) {
+ if (!req.user) {
+ return res.status(401).json({ message: 'Authentication required' });
+ }
+
+ if (req.user.role !== 'LIBRARIAN' && req.user.role !== 'ADMIN') {
+ return res.status(403).json({ message: 'Librarian or Admin access required' });
+ }
+
+ next();
+}
+
+// 仅管理员权限
+function requireAdmin(req, res, next) {
+ if (!req.user) {
+ return res.status(401).json({ message: 'Authentication required' });
+ }
+
+ if (req.user.role !== 'ADMIN') {
+ return res.status(403).json({ message: 'Admin access required' });
+ }
+
+ next();
+}
+
+module.exports = {
+ requireAuth,
+ requireLibrarian,
+ requireAdmin,
+};
+=======
module.exports = {
requireAuth,
};
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index df4f56f..6cc16e1 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -8,6 +8,665 @@ const { signLibrarianToken } = require('../lib/librarianToken');
const router = express.Router();
const prisma = new PrismaClient();
+<<<<<<< HEAD
+// ==================== 辅助函数 ====================
+
+function validateEmail(email) {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+}
+
+function validatePassword(password) {
+ return password && password.length >= 6;
+}
+
+// ==================== 统一登录接口 ====================
+
+router.post('/login', async (req, res) => {
+ const { email, password, type } = req.body;
+
+ // 基本验证
+ if (!email || !password) {
+ return res.status(400).json({ error: '邮箱和密码都是必需的' });
+ }
+
+ try {
+ // 学生登录
+ if (type === 'student' || !type) {
+ const user = await prisma.user.findUnique({
+ where: { email },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ passwordHash: true,
+ role: true,
+ studentId: true,
+ }
+ });
+
+ if (!user) {
+ return res.status(401).json({ error: '用户不存在', type: 'student' });
+ }
+
+ if (user.role === 'LIBRARIAN' || user.role === 'ADMIN') {
+ return res.status(401).json({
+ error: user.role === 'ADMIN' ? '请使用管理员入口登录' : '请使用馆员入口登录',
+ type: user.role.toLowerCase()
+ });
+ }
+
+ const isValid = await bcrypt.compare(password, user.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '密码错误', type: 'student' });
+ }
+
+ const token = signToken({
+ sub: String(user.id),
+ id: user.id,
+ role: user.role,
+ email: user.email
+ });
+
+ const { passwordHash, ...userWithoutPassword } = user;
+
+ return res.json({
+ success: true,
+ message: '学生登录成功',
+ token,
+ user: userWithoutPassword
+ });
+ }
+
+ // 馆员登录
+if (type === 'librarian') {
+ // 从 User 表查询馆员
+ const librarian = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { email: email },
+ { employeeId: email }
+ ],
+ role: 'LIBRARIAN'
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ employeeId: true,
+ passwordHash: true,
+ role: true
+ }
+ });
+
+ if (!librarian) {
+ return res.status(401).json({ error: '工号不存在', type: 'librarian' });
+ }
+
+ const isValid = await bcrypt.compare(password, librarian.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '密码错误', type: 'librarian' });
+ }
+
+ const token = signToken({
+ sub: String(librarian.id),
+ id: librarian.id,
+ role: librarian.role,
+ email: librarian.email
+ });
+
+ const { passwordHash, ...librarianWithoutPassword } = librarian;
+
+ return res.json({
+ success: true,
+ message: '馆员登录成功',
+ token,
+ librarian: librarianWithoutPassword
+ });
+}
+
+ // 管理员登录
+ if (type === 'admin') {
+ const user = await prisma.user.findUnique({
+ where: { email },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ passwordHash: true,
+ role: true,
+ }
+ });
+
+ if (!user) {
+ return res.status(401).json({ error: '用户不存在', type: 'admin' });
+ }
+
+ if (user.role !== 'ADMIN') {
+ return res.status(401).json({ error: '非管理员账号', type: 'admin' });
+ }
+
+ const isValid = await bcrypt.compare(password, user.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '密码错误', type: 'admin' });
+ }
+
+ const token = signToken({
+ sub: String(user.id),
+ id: user.id,
+ role: user.role,
+ email: user.email
+ });
+
+ const { passwordHash, ...userWithoutPassword } = user;
+
+ return res.json({
+ success: true,
+ message: '管理员登录成功',
+ token,
+ user: userWithoutPassword
+ });
+ }
+
+ return res.status(400).json({ error: '无效的登录类型' });
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({ error: '登录过程中发生错误,请稍后重试' });
+ }
+});
+
+// ==================== 学生登录接口 ====================
+
+router.post('/login-student', async (req, res) => {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({ error: '邮箱和密码都是必需的' });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { email },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ passwordHash: true,
+ role: true,
+ studentId: true,
+ }
+ });
+
+ if (!user) {
+ return res.status(401).json({ error: '用户不存在' });
+ }
+
+ if (user.role !== 'STUDENT') {
+ return res.status(401).json({ error: '该账号不是学生账号' });
+ }
+
+ const isValid = await bcrypt.compare(password, user.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '密码错误' });
+ }
+
+ const token = signToken({
+ sub: String(user.id),
+ id: user.id,
+ role: user.role,
+ email: user.email
+ });
+
+ const { passwordHash, ...userWithoutPassword } = user;
+
+ return res.json({
+ success: true,
+ message: '学生登录成功',
+ token,
+ user: userWithoutPassword
+ });
+ } catch (error) {
+ console.error('Student login error:', error);
+ res.status(500).json({ error: '登录过程中发生错误' });
+ }
+});
+
+// ==================== 馆员登录接口 ====================
+
+router.post('/login-librarian', async (req, res) => {
+ const { employeeId, password } = req.body;
+
+ if (!employeeId || !password) {
+ return res.status(400).json({ error: '工号和密码都是必需的' });
+ }
+
+ try {
+ // 从 User 表查询馆员
+ const librarian = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { email: employeeId },
+ { employeeId: employeeId }
+ ],
+ role: 'LIBRARIAN'
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ employeeId: true,
+ passwordHash: true,
+ role: true
+ }
+ });
+
+ if (!librarian) {
+ return res.status(401).json({ error: '工号不存在' });
+ }
+
+ const isValid = await bcrypt.compare(password, librarian.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '密码错误' });
+ }
+
+ const token = signToken({
+ sub: String(librarian.id),
+ id: librarian.id,
+ role: librarian.role,
+ email: librarian.email
+ });
+
+ const { passwordHash, ...librarianWithoutPassword } = librarian;
+
+ return res.json({
+ success: true,
+ message: '馆员登录成功',
+ token,
+ librarian: librarianWithoutPassword
+ });
+ } catch (error) {
+ console.error('Librarian login error:', error);
+ res.status(500).json({ error: '登录过程中发生错误' });
+ }
+});
+
+// ==================== 管理员登录接口 ====================
+
+router.post('/login-admin', async (req, res) => {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({ error: '邮箱和密码都是必需的' });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { email },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ passwordHash: true,
+ role: true,
+ }
+ });
+
+ if (!user) {
+ return res.status(401).json({ error: '用户不存在' });
+ }
+
+ if (user.role !== 'ADMIN') {
+ return res.status(401).json({ error: '非管理员账号' });
+ }
+
+ const isValid = await bcrypt.compare(password, user.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '密码错误' });
+ }
+
+ const token = signToken({
+ sub: String(user.id),
+ id: user.id,
+ role: user.role,
+ email: user.email
+ });
+
+ const { passwordHash, ...userWithoutPassword } = user;
+
+ return res.json({
+ success: true,
+ message: '管理员登录成功',
+ token,
+ user: userWithoutPassword
+ });
+ } catch (error) {
+ console.error('Admin login error:', error);
+ res.status(500).json({ error: '登录过程中发生错误' });
+ }
+});
+
+// ==================== 馆员注册接口 ====================
+
+router.post('/register', async (req, res) => {
+ const { employeeId, name, password } = req.body;
+
+ // 验证输入
+ if (!employeeId || !name || !password) {
+ return res.status(400).json({
+ error: '工号、姓名和密码都是必需的',
+ fields: {
+ employeeId: !employeeId,
+ name: !name,
+ password: !password
+ }
+ });
+ }
+
+ if (employeeId.length < 3) {
+ return res.status(400).json({ error: '工号长度不能少于3位' });
+ }
+
+ if (name.length < 2) {
+ return res.status(400).json({ error: '姓名长度不能少于2位' });
+ }
+
+ if (password.length < 6) {
+ return res.status(400).json({ error: '密码长度不能少于6位' });
+ }
+
+ try {
+ // 检查工号是否已存在(在 User 表中)
+ const existing = await prisma.user.findFirst({
+ where: { employeeId }
+ });
+
+ if (existing) {
+ return res.status(409).json({ error: '该工号已被注册' });
+ }
+
+ // 加密密码
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // 创建馆员到 User 表
+ const librarian = await prisma.user.create({
+ data: {
+ email: `${employeeId.toLowerCase()}@librarian.com`, // 生成默认邮箱
+ employeeId,
+ name,
+ passwordHash: hashedPassword,
+ role: 'LIBRARIAN'
+ },
+ select: {
+ id: true,
+ employeeId: true,
+ name: true,
+ role: true,
+ createdAt: true,
+ }
+ });
+
+ return res.status(201).json({
+ success: true,
+ message: '注册成功',
+ librarian
+ });
+ } catch (error) {
+ console.error('Librarian registration error:', error);
+ res.status(500).json({ error: '注册失败,请稍后重试' });
+ }
+});
+
+// ==================== 学生注册接口 ====================
+
+router.post('/register-student', async (req, res) => {
+ const { studentId, name, email, password } = req.body;
+
+ // 验证输入
+ if (!studentId || !name || !email || !password) {
+ return res.status(400).json({
+ error: '学号、姓名、邮箱和密码都是必需的'
+ });
+ }
+
+ if (studentId.length < 5) {
+ return res.status(400).json({ error: '学号格式不正确' });
+ }
+
+ if (name.length < 2) {
+ return res.status(400).json({ error: '姓名长度不能少于2位' });
+ }
+
+ if (!validateEmail(email)) {
+ return res.status(400).json({ error: '邮箱格式不正确' });
+ }
+
+ if (!validatePassword(password)) {
+ return res.status(400).json({ error: '密码长度不能少于6位' });
+ }
+
+ try {
+ // 检查邮箱或学号是否已存在
+ const existing = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { email },
+ { studentId }
+ ]
+ }
+ });
+
+ if (existing) {
+ if (existing.email === email) {
+ return res.status(409).json({ error: '该邮箱已被注册' });
+ }
+ if (existing.studentId === studentId) {
+ return res.status(409).json({ error: '该学号已被注册' });
+ }
+ }
+
+ // 加密密码
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // 创建学生
+ const student = await prisma.user.create({
+ data: {
+ email,
+ name,
+ studentId,
+ passwordHash: hashedPassword,
+ role: 'STUDENT',
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ studentId: true,
+ role: true,
+ createdAt: true,
+ }
+ });
+
+ // 记录审计日志
+ try {
+ await prisma.auditLog.create({
+ data: {
+ userId: student.id,
+ action: 'STUDENT_REGISTER',
+ entity: 'User',
+ entityId: student.id,
+ detail: `Student ${studentId} (${name}) registered`
+ }
+ });
+ } catch (logError) {
+ console.error('Failed to create audit log:', logError);
+ }
+
+ return res.status(201).json({
+ success: true,
+ message: '注册成功',
+ student
+ });
+ } catch (error) {
+ console.error('Student registration error:', error);
+ res.status(500).json({ error: '注册失败,请稍后重试' });
+ }
+});
+
+// ==================== 验证 Token 接口 ====================
+
+router.get('/verify', async (req, res) => {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader) {
+ return res.status(401).json({ error: '未提供认证令牌' });
+ }
+
+ const token = authHeader.replace('Bearer ', '');
+
+ try {
+ // 尝试作为用户 token 验证
+ try {
+ const { verifyToken } = require('../lib/token');
+ const payload = verifyToken(token);
+
+ const user = await prisma.user.findUnique({
+ where: { id: payload.id },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ role: true,
+ studentId: true,
+ }
+ });
+
+ if (user) {
+ return res.json({
+ valid: true,
+ type: 'user',
+ user
+ });
+ }
+ } catch (userError) {
+ // 用户 token 验证失败,尝试馆员 token
+ }
+
+ // 尝试作为馆员 token 验证
+ try {
+ const { verifyLibrarianToken } = require('../lib/librarianToken');
+ const payload = verifyLibrarianToken(token);
+
+ const librarian = await prisma.librarian.findUnique({
+ where: { id: payload.id },
+ select: {
+ id: true,
+ employeeId: true,
+ name: true,
+ }
+ });
+
+ if (librarian) {
+ return res.json({
+ valid: true,
+ type: 'librarian',
+ librarian
+ });
+ }
+ } catch (librarianError) {
+ // 馆员 token 验证失败
+ }
+
+ return res.status(401).json({ valid: false, error: '无效的令牌' });
+ } catch (error) {
+ console.error('Token verification error:', error);
+ res.status(500).json({ error: '令牌验证失败' });
+ }
+});
+
+// ==================== 修改密码接口 ====================
+
+router.post('/change-password', async (req, res) => {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader) {
+ return res.status(401).json({ error: '未提供认证令牌' });
+ }
+
+ const token = authHeader.replace('Bearer ', '');
+ const { oldPassword, newPassword } = req.body;
+
+ if (!oldPassword || !newPassword) {
+ return res.status(400).json({ error: '旧密码和新密码都是必需的' });
+ }
+
+ if (!validatePassword(newPassword)) {
+ return res.status(400).json({ error: '新密码长度不能少于6位' });
+ }
+
+ try {
+ // 尝试作为用户修改密码
+ try {
+ const { verifyToken } = require('../lib/token');
+ const payload = verifyToken(token);
+
+ const user = await prisma.user.findUnique({
+ where: { id: payload.id }
+ });
+
+ if (user) {
+ const isValid = await bcrypt.compare(oldPassword, user.passwordHash);
+ if (!isValid) {
+ return res.status(401).json({ error: '旧密码错误' });
+ }
+
+ const hashedPassword = await bcrypt.hash(newPassword, 10);
+ await prisma.user.update({
+ where: { id: user.id },
+ data: { passwordHash: hashedPassword }
+ });
+
+ return res.json({ success: true, message: '密码修改成功' });
+ }
+ } catch (userError) {
+ // 用户 token 验证失败,尝试馆员 token
+ }
+
+ // 尝试作为馆员修改密码
+ try {
+ const { verifyLibrarianToken } = require('../lib/librarianToken');
+ const payload = verifyLibrarianToken(token);
+
+ const librarian = await prisma.librarian.findUnique({
+ where: { id: payload.id }
+ });
+
+ if (librarian) {
+ const isValid = await bcrypt.compare(oldPassword, librarian.password);
+ if (!isValid) {
+ return res.status(401).json({ error: '旧密码错误' });
+ }
+
+ const hashedPassword = await bcrypt.hash(newPassword, 10);
+ await prisma.librarian.update({
+ where: { id: librarian.id },
+ data: { password: hashedPassword }
+ });
+
+ return res.json({ success: true, message: '密码修改成功' });
+ }
+ } catch (librarianError) {
+ // 馆员 token 验证失败
+ }
+
+ return res.status(401).json({ error: '无效的认证令牌' });
+ } catch (error) {
+ console.error('Password change error:', error);
+ res.status(500).json({ error: '密码修改失败' });
+ }
+});
+
+module.exports = router;
+=======
// --- ͳһ¼ӿ (ѧͼԱԱ) ---
router.post('/login', async (req, res) => {
const { email, password, type } = req.body;
@@ -179,3 +838,4 @@ router.post('/login-admin', async (req, res) => {
});
module.exports = router;
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
diff --git a/backend/src/routes/books.js b/backend/src/routes/books.js
index d527a0d..bf83a84 100644
--- a/backend/src/routes/books.js
+++ b/backend/src/routes/books.js
@@ -1,7 +1,11 @@
const express = require('express');
const prisma = require('../lib/prisma');
+<<<<<<< HEAD
+const { requireAuth } = require('../middleware/auth');
+=======
const { requireLibrarianAuth } = require('../middleware/librarianAuth');
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
const router = express.Router();
@@ -14,6 +18,23 @@ const BOOK_SELECT = {
description: true,
language: true,
createdAt: true,
+<<<<<<< HEAD
+ updatedAt: true,
+};
+
+// ==================== 权限检查中间件 ====================
+function checkLibrarianOrAdmin(req, res, next) {
+ if (!req.user) {
+ return res.status(401).json({ error: '未认证' });
+ }
+ if (req.user.role !== 'LIBRARIAN' && req.user.role !== 'ADMIN') {
+ return res.status(403).json({ error: '权限不足,需要馆员或管理员权限' });
+ }
+ next();
+}
+
+// ==================== 公开接口(无需认证) ====================
+=======
};
const BOOK_DETAIL_INCLUDE = {
@@ -106,6 +127,7 @@ async function writeAuditLog(action, entityId, detail) {
console.error('Failed to write audit log:', error);
}
}
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
// 获取所有图书
router.get('/', async (req, res) => {
@@ -114,22 +136,66 @@ router.get('/', async (req, res) => {
orderBy: { id: 'asc' },
include: {
copies: {
+<<<<<<< HEAD
+ select: {
+ id: true,
+ barcode: true,
+ status: true,
+ floor: true,
+ libraryArea: true,
+ shelfNo: true,
+ shelfLevel: true,
+ }
+=======
select: { status: true }
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
}
});
const booksWithCount = books.map(book => {
const availableCopies = book.copies.filter(c => c.status === 'AVAILABLE').length;
+<<<<<<< HEAD
+ const firstCopy = book.copies[0] || {};
+ return {
+ id: book.id,
+ title: book.title,
+ author: book.author,
+ isbn: book.isbn,
+ genre: book.genre,
+ description: book.description,
+ language: book.language,
+ createdAt: book.createdAt,
+ updatedAt: book.updatedAt,
+ availableCopies: availableCopies,
+ totalCopies: book.copies.length,
+ floor: firstCopy.floor || 1,
+ libraryArea: firstCopy.libraryArea || '',
+ shelfNo: firstCopy.shelfNo || 'A',
+ shelfLevel: firstCopy.shelfLevel || 1,
+ copies: book.copies
+=======
return {
...book,
availableCopies: availableCopies,
totalCopies: book.copies.length
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
};
});
res.json({ data: booksWithCount });
} catch (error) {
+<<<<<<< HEAD
+ console.error('Failed to fetch books:', error);
+ res.status(500).json({ error: 'Failed to fetch books', detail: error.message });
+ }
+});
+
+// 图书搜索功能
+router.get('/search', async (req, res) => {
+ try {
+ const { title, author, keyword } = req.query;
+=======
res.status(500).json({
error: 'Failed to fetch books',
detail: error.message,
@@ -142,10 +208,15 @@ router.get('/search', async (req, res) => {
try {
const { title, author, keyword } = req.query;
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
const whereCondition = {};
if (title || author || keyword) {
whereCondition.OR = [];
+<<<<<<< HEAD
+ if (title) whereCondition.OR.push({ title: { contains: title } });
+ if (author) whereCondition.OR.push({ author: { contains: author } });
+=======
if (title) {
whereCondition.OR.push({ title: { contains: title } });
@@ -155,6 +226,7 @@ router.get('/search', async (req, res) => {
whereCondition.OR.push({ author: { contains: author } });
}
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
if (keyword) {
whereCondition.OR.push(
{ title: { contains: keyword } },
@@ -189,6 +261,11 @@ router.get('/search', async (req, res) => {
};
});
+<<<<<<< HEAD
+ res.json({ success: true, data: booksWithCount, count: booksWithCount.length });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Failed to search books', detail: error.message });
+=======
res.json({
success: true,
data: booksWithCount,
@@ -200,14 +277,20 @@ router.get('/search', async (req, res) => {
error: 'Failed to search books',
detail: error.message,
});
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
});
// 获取单本图书详情
router.get('/:id', async (req, res) => {
+<<<<<<< HEAD
+ const bookId = Number(req.params.id);
+ if (isNaN(bookId)) {
+=======
const bookId = Number.parseInt(req.params.id, 10);
if (Number.isNaN(bookId)) {
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
return res.status(400).json({ error: 'Invalid book id' });
}
@@ -215,9 +298,17 @@ router.get('/:id', async (req, res) => {
const book = await prisma.book.findUnique({
where: { id: bookId },
include: {
+<<<<<<< HEAD
+ copies: {
+ select: { id: true, barcode: true, floor: true, libraryArea: true, shelfNo: true, shelfLevel: true, status: true }
+ },
+ ratings: {
+ include: { user: { select: { id: true, name: true } } }
+=======
...BOOK_DETAIL_INCLUDE,
copies: {
select: { id: true, barcode: true, floor: true, libraryArea: true, shelfNo: true, shelfLevel: true, status: true }
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
}
});
@@ -226,12 +317,15 @@ router.get('/:id', async (req, res) => {
return res.status(404).json({ error: 'Book not found' });
}
+<<<<<<< HEAD
+=======
const ratingCount = book.ratings.length;
const averageRating =
ratingCount === 0
? null
: Number((book.ratings.reduce((sum, rating) => sum + rating.stars, 0) / ratingCount).toFixed(2));
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
const availableCopies = book.copies.filter(c => c.status === 'AVAILABLE').length;
res.json({
@@ -240,6 +334,44 @@ router.get('/:id', async (req, res) => {
...book,
availableCopies: availableCopies,
totalCopies: book.copies.length,
+<<<<<<< HEAD
+ }
+ });
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to fetch book detail', detail: error.message });
+ }
+});
+
+// ==================== 馆员/管理员接口(需要认证) ====================
+
+// 添加图书
+router.post('/', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const {
+ title, author, isbn, genre, description, language,
+ floor, libraryArea, shelfNo, shelfLevel
+ } = req.body;
+
+ if (!title || !author || !isbn || !genre) {
+ return res.status(400).json({ error: '书名、作者、ISBN和分类是必填项' });
+ }
+
+ // 检查 ISBN 是否已存在
+ const existingBook = await prisma.book.findUnique({ where: { isbn: isbn.trim() } });
+ if (existingBook) {
+ return res.status(409).json({ error: '该 ISBN 已存在' });
+ }
+
+ // 创建图书
+ const book = await prisma.book.create({
+ data: {
+ title: title.trim(),
+ author: author.trim(),
+ isbn: isbn.trim(),
+ genre: genre.trim(),
+ description: description?.trim() || null,
+ language: language?.trim() || 'English',
+=======
stats: {
averageRating,
activeLoans: book.loans.filter((loan) => !loan.returnDate).length,
@@ -278,10 +410,178 @@ router.post('/', requireLibrarianAuth, async (req, res) => {
genre,
description,
language,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
},
select: BOOK_SELECT,
});
+<<<<<<< HEAD
+ // 创建默认副本(使用前端传来的位置信息)
+ await prisma.copy.create({
+ data: {
+ bookId: book.id,
+ barcode: `BC-${book.id}-1`,
+ floor: floor || 1,
+ libraryArea: libraryArea || `${genre}区`,
+ shelfNo: shelfNo || 'A',
+ shelfLevel: shelfLevel || 1,
+ status: 'AVAILABLE'
+ }
+ });
+
+ // 记录审计日志
+ await prisma.auditLog.create({
+ data: {
+ userId: req.user.role === 'ADMIN' ? req.user.id : null,
+ action: 'CREATE_BOOK',
+ entity: 'Book',
+ entityId: book.id,
+ detail: `${req.user.role === 'LIBRARIAN' ? '馆员' : '管理员'} ${req.user.name || req.user.email} 添加了图书《${book.title}》`
+ }
+ });
+
+ // 返回完整的图书信息(包含副本)
+ const fullBook = await prisma.book.findUnique({
+ where: { id: book.id },
+ include: {
+ copies: {
+ select: { id: true, barcode: true, status: true, floor: true, libraryArea: true, shelfNo: true, shelfLevel: true }
+ }
+ }
+ });
+
+ const availableCopies = fullBook.copies.filter(c => c.status === 'AVAILABLE').length;
+ const firstCopy = fullBook.copies[0] || {};
+
+ res.status(201).json({
+ success: true,
+ message: '图书添加成功',
+ book: {
+ ...fullBook,
+ availableCopies,
+ totalCopies: fullBook.copies.length,
+ floor: firstCopy.floor,
+ libraryArea: firstCopy.libraryArea,
+ shelfNo: firstCopy.shelfNo,
+ shelfLevel: firstCopy.shelfLevel,
+ }
+ });
+ } catch (error) {
+ console.error('Create book error:', error);
+ res.status(500).json({ error: '添加图书失败' });
+ }
+});
+
+// 更新图书信息
+router.put('/:id', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const bookId = Number(req.params.id);
+ const {
+ title, author, isbn, genre, description, language,
+ floor, libraryArea, shelfNo, shelfLevel
+ } = req.body;
+
+ if (isNaN(bookId)) {
+ return res.status(400).json({ error: '无效的图书ID' });
+ }
+
+ if (!title || !author || !isbn || !genre) {
+ return res.status(400).json({ error: '书名、作者、ISBN和分类是必填项' });
+ }
+
+ // 检查图书是否存在
+ const existingBook = await prisma.book.findUnique({ where: { id: bookId } });
+ if (!existingBook) {
+ return res.status(404).json({ error: '图书不存在' });
+ }
+
+ // 检查 ISBN 是否被其他图书使用
+ if (isbn.trim() !== existingBook.isbn) {
+ const isbnConflict = await prisma.book.findUnique({ where: { isbn: isbn.trim() } });
+ if (isbnConflict) {
+ return res.status(409).json({ error: '该 ISBN 已被其他图书使用' });
+ }
+ }
+
+ // 更新图书
+ const updatedBook = await prisma.book.update({
+ where: { id: bookId },
+ data: {
+ title: title.trim(),
+ author: author.trim(),
+ isbn: isbn.trim(),
+ genre: genre.trim(),
+ description: description?.trim() || null,
+ language: language?.trim() || 'English',
+ },
+ select: BOOK_SELECT,
+ });
+
+ // 如果传入了位置信息,更新第一个副本的位置
+ if (floor || libraryArea || shelfNo || shelfLevel) {
+ const firstCopy = await prisma.copy.findFirst({ where: { bookId: bookId } });
+ if (firstCopy) {
+ await prisma.copy.update({
+ where: { id: firstCopy.id },
+ data: {
+ floor: floor !== undefined ? floor : firstCopy.floor,
+ libraryArea: libraryArea !== undefined ? libraryArea : firstCopy.libraryArea,
+ shelfNo: shelfNo !== undefined ? shelfNo : firstCopy.shelfNo,
+ shelfLevel: shelfLevel !== undefined ? shelfLevel : firstCopy.shelfLevel,
+ }
+ });
+ }
+ }
+
+ // 记录审计日志
+ await prisma.auditLog.create({
+ data: {
+ userId: req.user.role === 'ADMIN' ? req.user.id : null,
+ action: 'UPDATE_BOOK',
+ entity: 'Book',
+ entityId: bookId,
+ detail: `${req.user.role === 'LIBRARIAN' ? '馆员' : '管理员'} ${req.user.name || req.user.email} 更新了图书《${title}》`
+ }
+ });
+
+ // 返回完整的图书信息
+ const fullBook = await prisma.book.findUnique({
+ where: { id: bookId },
+ include: {
+ copies: {
+ select: { id: true, barcode: true, status: true, floor: true, libraryArea: true, shelfNo: true, shelfLevel: true }
+ }
+ }
+ });
+
+ const availableCopies = fullBook.copies.filter(c => c.status === 'AVAILABLE').length;
+ const firstCopy = fullBook.copies[0] || {};
+
+ res.json({
+ success: true,
+ message: '图书更新成功',
+ book: {
+ ...fullBook,
+ availableCopies,
+ totalCopies: fullBook.copies.length,
+ floor: firstCopy.floor,
+ libraryArea: firstCopy.libraryArea,
+ shelfNo: firstCopy.shelfNo,
+ shelfLevel: firstCopy.shelfLevel,
+ }
+ });
+ } catch (error) {
+ console.error('Update book error:', error);
+ res.status(500).json({ error: '更新图书失败' });
+ }
+});
+
+// 删除图书
+router.delete('/:id', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ const bookId = Number(req.params.id);
+ if (isNaN(bookId)) {
+ return res.status(400).json({ error: '无效的图书ID' });
+=======
await writeAuditLog(
'CREATE_BOOK',
book.id,
@@ -311,11 +611,49 @@ router.delete('/:id', requireLibrarianAuth, async (req, res) => {
if (Number.isNaN(bookId)) {
return res.status(400).json({ error: 'Invalid book id' });
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
try {
const book = await prisma.book.findUnique({
where: { id: bookId },
+<<<<<<< HEAD
+ include: {
+ copies: {
+ include: {
+ loans: { where: { returnDate: null } }
+ }
+ }
+ }
+ });
+
+ if (!book) {
+ return res.status(404).json({ error: '图书不存在' });
+ }
+
+ // 检查是否有未归还的借阅
+ const hasActiveLoans = book.copies.some(copy => copy.loans.length > 0);
+ if (hasActiveLoans) {
+ return res.status(400).json({ error: '该图书有未归还的借阅记录,无法删除' });
+ }
+
+ await prisma.book.delete({ where: { id: bookId } });
+
+ await prisma.auditLog.create({
+ data: {
+ userId: req.user.role === 'ADMIN' ? req.user.id : null,
+ action: 'DELETE_BOOK',
+ entity: 'Book',
+ entityId: bookId,
+ detail: `${req.user.role === 'LIBRARIAN' ? '馆员' : '管理员'} ${req.user.name || req.user.email} 删除了图书《${book.title}》`
+ }
+ });
+
+ res.json({ success: true, message: '图书删除成功' });
+ } catch (error) {
+ console.error('Delete book error:', error);
+ res.status(500).json({ error: '删除图书失败' });
+=======
select: {
id: true,
title: true,
@@ -365,6 +703,7 @@ router.delete('/:id', requireLibrarianAuth, async (req, res) => {
error: 'Failed to delete book',
detail: error.message,
});
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
});
diff --git a/backend/src/routes/loans.js b/backend/src/routes/loans.js
index 26b51e9..0cc56eb 100644
--- a/backend/src/routes/loans.js
+++ b/backend/src/routes/loans.js
@@ -1,9 +1,279 @@
+<<<<<<< HEAD
+// routes/loans.js - 馆员借书给学生 (完善版)
+
+=======
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
const express = require('express');
const prisma = require('../lib/prisma');
const { requireAuth } = require('../middleware/auth');
const router = express.Router();
+<<<<<<< HEAD
+// 配置
+const LOAN_DURATION_DAYS = 30;
+
+// ==================== 权限检查中间件 ====================
+function checkLibrarianOrAdmin(req, res, next) {
+ if (!req.user) {
+ return res.status(401).json({ message: '未认证' });
+ }
+ if (req.user.role !== 'LIBRARIAN' && req.user.role !== 'ADMIN') {
+ return res.status(403).json({ message: '权限不足,需要馆员或管理员权限' });
+ }
+ next();
+}
+
+// ==================== 辅助函数 ====================
+
+async function calculateDueDate(checkoutDate) {
+ const dueDate = new Date(checkoutDate);
+ dueDate.setDate(dueDate.getDate() + LOAN_DURATION_DAYS);
+ return dueDate;
+}
+
+// ==================== 馆员专用 API ====================
+
+// 馆员搜索学生
+router.get('/users/search', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const keyword = (req.query.keyword || '').trim();
+
+ if (!keyword) {
+ return res.status(400).json({ message: '请输入搜索关键词' });
+ }
+
+ const students = await prisma.user.findMany({
+ where: {
+ role: 'STUDENT',
+ OR: [
+ { studentId: { contains: keyword } },
+ { email: { contains: keyword } },
+ { name: { contains: keyword } }
+ ]
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ studentId: true,
+ role: true,
+ createdAt: true,
+ }
+ });
+
+ // 获取每个学生的借阅统计
+ const usersWithStats = await Promise.all(students.map(async (student) => {
+ const currentBorrowCount = await prisma.loan.count({
+ where: {
+ userId: student.id,
+ returnDate: null
+ }
+ });
+
+ const overdueLoans = await prisma.loan.count({
+ where: {
+ userId: student.id,
+ returnDate: null,
+ dueDate: { lt: new Date() }
+ }
+ });
+
+ const totalBorrowed = await prisma.loan.count({
+ where: { userId: student.id }
+ });
+
+ return {
+ ...student,
+ stats: {
+ currentBorrowCount,
+ hasOverdue: overdueLoans > 0,
+ overdueCount: overdueLoans,
+ totalBorrowed,
+ }
+ };
+ }));
+
+ res.json({
+ success: true,
+ users: usersWithStats
+ });
+ } catch (error) {
+ console.error('Search students error:', error);
+ res.status(500).json({ message: '搜索学生失败' });
+ }
+});
+
+// 馆员搜索图书
+router.get('/books/search', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const keyword = (req.query.keyword || '').trim();
+
+ if (!keyword) {
+ return res.status(400).json({ message: '请输入搜索关键词' });
+ }
+
+ const books = await prisma.book.findMany({
+ where: {
+ OR: [
+ { title: { contains: keyword } },
+ { isbn: { contains: keyword } },
+ { author: { contains: keyword } }
+ ]
+ },
+ include: {
+ copies: {
+ select: {
+ id: true,
+ barcode: true,
+ status: true,
+ floor: true,
+ libraryArea: true,
+ }
+ }
+ }
+ });
+
+ const booksWithAvailability = books.map(book => {
+ const availableCopies = book.copies.filter(c => c.status === 'AVAILABLE');
+ const borrowedCopies = book.copies.filter(c => c.status === 'BORROWED');
+
+ return {
+ id: book.id,
+ title: book.title,
+ author: book.author,
+ isbn: book.isbn,
+ genre: book.genre,
+ description: book.description,
+ language: book.language,
+ availableCopies: availableCopies.length,
+ totalCopies: book.copies.length,
+ copies: book.copies.map(c => ({
+ id: c.id,
+ barcode: c.barcode,
+ status: c.status,
+ location: `${c.libraryArea || ''} ${c.floor || ''}楼`.trim()
+ }))
+ };
+ });
+
+ res.json({
+ success: true,
+ books: booksWithAvailability
+ });
+ } catch (error) {
+ console.error('Search books error:', error);
+ res.status(500).json({ message: '搜索图书失败' });
+ }
+});
+
+// 馆员借书给学生 (R1.1.12)
+router.post('/lend', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const { userId, bookId, copyId } = req.body;
+
+ if (!userId || !bookId) {
+ return res.status(400).json({
+ success: false,
+ message: '请选择学生和图书'
+ });
+ }
+
+ const studentId = Number(userId);
+ const targetBookId = Number(bookId);
+
+ // 验证学生
+ const student = await prisma.user.findUnique({
+ where: { id: studentId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ studentId: true,
+ role: true,
+ }
+ });
+
+ if (!student || student.role !== 'STUDENT') {
+ return res.status(404).json({
+ success: false,
+ message: '学生不存在或不是学生账号'
+ });
+ }
+
+ // 验证图书
+ const book = await prisma.book.findUnique({
+ where: { id: targetBookId },
+ include: {
+ copies: {
+ where: copyId ? { id: Number(copyId) } : { status: 'AVAILABLE' },
+ take: 1
+ }
+ }
+ });
+
+ if (!book) {
+ return res.status(404).json({
+ success: false,
+ message: '图书不存在'
+ });
+ }
+
+ // 检查可用副本
+ const availableCopies = copyId
+ ? book.copies
+ : book.copies.filter(copy => copy.status === 'AVAILABLE');
+
+ if (availableCopies.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: '该图书没有可用副本'
+ });
+ }
+
+ // 检查学生是否已借阅此书(未归还)
+ const existingLoan = await prisma.loan.findFirst({
+ where: {
+ userId: studentId,
+ copy: { bookId: targetBookId },
+ returnDate: null
+ }
+ });
+
+ if (existingLoan) {
+ return res.status(400).json({
+ success: false,
+ message: '该学生已经借阅了这本书且未归还'
+ });
+ }
+
+ // 检查学生是否有逾期图书
+ const overdueLoans = await prisma.loan.findMany({
+ where: {
+ userId: studentId,
+ returnDate: null,
+ dueDate: { lt: new Date() }
+ }
+ });
+
+ if (overdueLoans.length > 0) {
+ return res.status(400).json({
+ success: false,
+ message: `该学生有 ${overdueLoans.length} 本逾期图书,请先归还后再借阅`,
+ overdueCount: overdueLoans.length
+ });
+ }
+
+ // 创建借阅记录
+ const selectedCopy = availableCopies[0];
+ const checkoutDate = new Date();
+ const dueDate = await calculateDueDate(checkoutDate);
+
+ const loan = await prisma.loan.create({
+ data: {
+ userId: studentId,
+ copyId: selectedCopy.id,
+=======
// 配置:学生最大借阅数量(可以从 Config 表读取)
const MAX_BORROW_STUDENT = 3;
const LOAN_DURATION_DAYS = 30;
@@ -99,11 +369,44 @@ router.post('/borrow/:bookId', requireAuth, async (req, res, next) => {
data: {
bookId: Number(bookId),
userId,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
checkoutDate,
dueDate,
fineAmount: 0,
finePaid: false,
fineForgiven: false
+<<<<<<< HEAD
+ },
+ include: {
+ copy: {
+ include: { book: true }
+ },
+ user: {
+ select: {
+ id: true,
+ name: true,
+ studentId: true,
+ email: true
+ }
+ }
+ }
+ });
+
+ // 更新副本状态
+ await prisma.copy.update({
+ where: { id: selectedCopy.id },
+ data: { status: 'BORROWED' }
+ });
+
+ // 记录审计日志
+ await prisma.auditLog.create({
+ data: {
+ userId: req.user.role === 'ADMIN' ? req.user.id : null,
+ action: 'LIBRARIAN_LEND',
+ entity: 'Loan',
+ entityId: loan.id,
+ detail: `${req.user.role === 'LIBRARIAN' ? '馆员' : '管理员'} ${req.user.name || req.user.email} 将《${book.title}》借给学生 ${student.name} (${student.studentId}),应还日期: ${dueDate.toLocaleDateString()}`
+=======
}
});
@@ -121,10 +424,332 @@ router.post('/borrow/:bookId', requireAuth, async (req, res, next) => {
entity: 'Loan',
entityId: loan.id,
detail: `User ${userId} borrowed book ${bookId}`
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
});
res.status(201).json({
+<<<<<<< HEAD
+ success: true,
+ message: `借书成功!《${book.title}》已借给 ${student.name}`,
+ loan: {
+ id: loan.id,
+ bookTitle: book.title,
+ bookAuthor: book.author,
+ studentName: student.name,
+ studentId: student.studentId,
+ checkoutDate: checkoutDate.toISOString(),
+ dueDate: dueDate.toISOString(),
+ copyBarcode: selectedCopy.barcode
+ }
+ });
+ } catch (error) {
+ console.error('Lend book error:', error);
+ res.status(500).json({
+ success: false,
+ message: '借书失败,请稍后重试'
+ });
+ }
+});
+
+// ==================== 4. 馆员还书 (R1.1.13) ====================
+
+// 获取当前在借记录
+router.get('/records', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const { status } = req.query; // active, overdue, all
+
+ let whereCondition = {};
+
+ if (status === 'active') {
+ whereCondition.returnDate = null;
+ } else if (status === 'overdue') {
+ whereCondition.returnDate = null;
+ whereCondition.dueDate = { lt: new Date() };
+ }
+
+ const loans = await prisma.loan.findMany({
+ where: whereCondition,
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ studentId: true
+ }
+ },
+ copy: {
+ include: {
+ book: {
+ select: {
+ id: true,
+ title: true,
+ author: true,
+ isbn: true,
+ genre: true
+ }
+ }
+ }
+ }
+ },
+ orderBy: [
+ { returnDate: 'asc' },
+ { dueDate: 'asc' }
+ ]
+ });
+
+ const loansWithStatus = loans.map(loan => {
+ const now = new Date();
+ const isOverdue = !loan.returnDate && loan.dueDate < now;
+ const daysOverdue = isOverdue
+ ? Math.ceil((now - loan.dueDate) / (1000 * 60 * 60 * 24))
+ : 0;
+
+ return {
+ ...loan,
+ status: loan.returnDate ? 'returned' : (isOverdue ? 'overdue' : 'active'),
+ daysOverdue,
+ estimatedFine: isOverdue ? daysOverdue * 0.5 : 0
+ };
+ });
+
+ res.json({
+ success: true,
+ loans: loansWithStatus,
+ stats: {
+ total: loans.length,
+ active: loansWithStatus.filter(l => l.status === 'active').length,
+ overdue: loansWithStatus.filter(l => l.status === 'overdue').length,
+ returned: loansWithStatus.filter(l => l.status === 'returned').length
+ }
+ });
+ } catch (error) {
+ console.error('Fetch loan records error:', error);
+ res.status(500).json({ message: '获取借阅记录失败' });
+ }
+});
+
+// 馆员还书 (R1.1.13)
+router.post('/return', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const { loanId, waiveFine } = req.body;
+
+ if (!loanId) {
+ return res.status(400).json({
+ success: false,
+ message: '请选择要归还的借阅记录'
+ });
+ }
+
+ // 查找借阅记录
+ const loan = await prisma.loan.findUnique({
+ where: { id: Number(loanId) },
+ include: {
+ copy: {
+ include: { book: true }
+ },
+ user: {
+ select: {
+ id: true,
+ name: true,
+ studentId: true
+ }
+ }
+ }
+ });
+
+ if (!loan) {
+ return res.status(404).json({
+ success: false,
+ message: '借阅记录不存在'
+ });
+ }
+
+ if (loan.returnDate !== null) {
+ return res.status(400).json({
+ success: false,
+ message: '该图书已经归还过了'
+ });
+ }
+
+ const returnDate = new Date();
+ let fineAmount = 0;
+
+ // 计算罚款
+ if (returnDate > loan.dueDate) {
+ const diffDays = Math.ceil((returnDate - loan.dueDate) / (1000 * 60 * 60 * 24));
+ fineAmount = diffDays * 0.5; // 每天 0.5 元
+ }
+
+ // 如果免除了罚款
+ const finalFine = waiveFine ? 0 : fineAmount;
+ const fineForgiven = waiveFine && fineAmount > 0;
+
+ // 更新借阅记录
+ await prisma.loan.update({
+ where: { id: Number(loanId) },
+ data: {
+ returnDate,
+ fineAmount: finalFine,
+ finePaid: finalFine === 0,
+ fineForgiven
+ }
+ });
+
+ // 更新副本状态为可用
+ await prisma.copy.update({
+ where: { id: loan.copyId },
+ data: { status: 'AVAILABLE' }
+ });
+
+ // 记录审计日志
+ let logDetail = `${req.user.role === 'LIBRARIAN' ? '馆员' : '管理员'} ${req.user.name || req.user.email} 接收学生 ${loan.user?.name} 归还《${loan.copy?.book?.title}》`;
+
+ if (finalFine > 0) {
+ logDetail += `,罚款 ${finalFine} 元`;
+ }
+ if (fineForgiven) {
+ logDetail += `(已免除原罚款 ${fineAmount} 元)`;
+ }
+
+ await prisma.auditLog.create({
+ data: {
+ userId: req.user.role === 'ADMIN' ? req.user.id : null,
+ action: 'LIBRARIAN_RETURN',
+ entity: 'Loan',
+ entityId: loan.id,
+ detail: logDetail
+ }
+ });
+
+ // 构建响应消息
+ let message = `《${loan.copy?.book?.title}》已成功归还`;
+ if (finalFine > 0) {
+ message += `,逾期罚款 ${finalFine} 元`;
+ }
+ if (fineForgiven) {
+ message += `(已免除罚款)`;
+ }
+
+ res.json({
+ success: true,
+ message,
+ returnInfo: {
+ loanId: loan.id,
+ bookTitle: loan.copy?.book?.title,
+ studentName: loan.user?.name,
+ studentId: loan.user?.studentId,
+ checkoutDate: loan.checkoutDate,
+ dueDate: loan.dueDate,
+ returnDate: returnDate,
+ daysLate: fineAmount > 0 ? Math.ceil((returnDate - loan.dueDate) / (1000 * 60 * 60 * 24)) : 0,
+ fineAmount: finalFine,
+ originalFine: fineAmount,
+ fineForgiven
+ }
+ });
+ } catch (error) {
+ console.error('Return book error:', error);
+ res.status(500).json({
+ success: false,
+ message: '还书失败,请稍后重试'
+ });
+ }
+});
+
+// 获取单个借阅记录详情
+router.get('/records/:id', requireAuth, checkLibrarianOrAdmin, async (req, res) => {
+ try {
+ const loanId = Number(req.params.id);
+
+ const loan = await prisma.loan.findUnique({
+ where: { id: loanId },
+ include: {
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ studentId: true
+ }
+ },
+ copy: {
+ include: {
+ book: {
+ select: {
+ id: true,
+ title: true,
+ author: true,
+ isbn: true,
+ genre: true,
+ description: true
+ }
+ }
+ }
+ }
+ }
+ });
+
+ if (!loan) {
+ return res.status(404).json({ message: '借阅记录不存在' });
+ }
+
+ const now = new Date();
+ const isOverdue = !loan.returnDate && loan.dueDate < now;
+ const daysOverdue = isOverdue
+ ? Math.ceil((now - loan.dueDate) / (1000 * 60 * 60 * 24))
+ : 0;
+
+ res.json({
+ success: true,
+ loan: {
+ ...loan,
+ status: loan.returnDate ? 'returned' : (isOverdue ? 'overdue' : 'active'),
+ daysOverdue,
+ estimatedFine: isOverdue ? daysOverdue * 0.5 : 0
+ }
+ });
+ } catch (error) {
+ console.error('Fetch loan detail error:', error);
+ res.status(500).json({ message: '获取借阅详情失败' });
+ }
+});
+
+// ==================== 学生借还书接口 ====================
+
+// 学生获取自己的借阅记录
+router.get('/me', requireAuth, async (req, res) => {
+ try {
+ const userId = req.user.id;
+
+ const loans = await prisma.loan.findMany({
+ where: { userId },
+ include: {
+ copy: {
+ include: {
+ book: {
+ select: {
+ id: true,
+ title: true,
+ author: true,
+ isbn: true
+ }
+ }
+ }
+ }
+ },
+ orderBy: { checkoutDate: 'desc' }
+ });
+
+ res.json({
+ success: true,
+ loans
+ });
+ } catch (error) {
+ console.error('Fetch my loans error:', error);
+ res.status(500).json({ message: '获取借阅记录失败' });
+=======
message: 'Book borrowed successfully',
loan: {
id: loan.id,
@@ -282,6 +907,7 @@ router.post('/admin/force-borrow/:bookId/:userId', requireAuth, async (req, res,
res.status(201).json({ message: 'Force borrow successful', loan });
} catch (error) {
next(error);
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
});
diff --git a/backend/src/server.js b/backend/src/server.js
new file mode 100644
index 0000000..2d03ce7
--- /dev/null
+++ b/backend/src/server.js
@@ -0,0 +1,40 @@
+// server.js
+const app = require('./app');
+const prisma = require('./lib/prisma');
+
+const PORT = process.env.PORT || 3001;
+
+async function startServer() {
+ try {
+ // 测试数据库连接
+ await prisma.$connect();
+ console.log('✅ Database connected successfully');
+
+ // 启动服务器
+ app.listen(PORT, () => {
+ console.log(`
+╔═══════════════════════════════════════════════════════╗
+║ 📚 Library Management System API Server ║
+╠═══════════════════════════════════════════════════════╣
+║ 🚀 Server running on: http://localhost:${PORT} ║
+║ 📖 API Documentation: http://localhost:${PORT}/health ║
+║ 🔑 Auth endpoints: /api/auth/* ║
+║ 📕 Books endpoints: /api/books/* ║
+║ 📋 Loans endpoints: /api/loans/* ║
+╚═══════════════════════════════════════════════════════╝
+ `);
+ });
+ } catch (error) {
+ console.error('❌ Failed to start server:', error);
+ process.exit(1);
+ }
+}
+
+// 优雅关闭
+process.on('SIGINT', async () => {
+ console.log('\n👋 Shutting down gracefully...');
+ await prisma.$disconnect();
+ process.exit(0);
+});
+
+startServer();
\ No newline at end of file
diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json
index babf8fb..3773ad4 100644
--- a/frontend/jsconfig.json
+++ b/frontend/jsconfig.json
@@ -3,6 +3,19 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
+<<<<<<< HEAD
+ },
+ "target": "ES6",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "build"]
+=======
}
}
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 5ab7077..2d52fd2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -65,7 +65,10 @@
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -612,6 +615,30 @@
"@noble/ciphers": "^1.0.0"
}
},
+<<<<<<< HEAD
+ "node_modules/@emnapi/core": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+=======
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -1120,7 +1147,10 @@
"resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -3320,7 +3350,10 @@
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"devOptional": true,
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"undici-types": "~7.19.0"
}
@@ -3331,7 +3364,10 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3342,7 +3378,10 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3404,7 +3443,10 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"bin": {
"acorn": "bin/acorn"
},
@@ -3666,7 +3708,10 @@
}
],
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -4369,7 +4414,10 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5205,7 +5253,10 @@
"resolved": "https://registry.npmmirror.com/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"engines": {
"node": ">=16.9.0"
}
@@ -6613,7 +6664,10 @@
}
],
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -6866,7 +6920,10 @@
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"engines": {
"node": ">=0.10.0"
}
@@ -6876,7 +6933,10 @@
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -7912,7 +7972,10 @@
"resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -8236,7 +8299,10 @@
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
+<<<<<<< HEAD
+=======
"peer": true,
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/frontend/src/librarian/LibrarianApp.jsx b/frontend/src/librarian/LibrarianApp.jsx
index 985db14..a72ee7d 100644
--- a/frontend/src/librarian/LibrarianApp.jsx
+++ b/frontend/src/librarian/LibrarianApp.jsx
@@ -2,11 +2,33 @@ import { useState, useEffect } from 'react'
import LibrarianLogin from './LibrarianLogin'
import LibrarianRegister from './LibrarianRegister'
import LibrarianDashboard from './LibrarianDashboard'
+<<<<<<< HEAD
+import { isLibrarianAuthenticated, librarianLogout } from './api'
+=======
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
function LibrarianApp() {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [librarian, setLibrarian] = useState(null)
const [showRegister, setShowRegister] = useState(false)
+<<<<<<< HEAD
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ // 检查登录状态
+ if (isLibrarianAuthenticated()) {
+ const savedLibrarian = localStorage.getItem('librarianInfo')
+ if (savedLibrarian) {
+ try {
+ setLibrarian(JSON.parse(savedLibrarian))
+ setIsLoggedIn(true)
+ } catch (e) {
+ librarianLogout()
+ }
+ }
+ }
+ setLoading(false)
+=======
useEffect(() => {
const token = localStorage.getItem('librarianToken')
@@ -16,6 +38,7 @@ function LibrarianApp() {
setIsLoggedIn(true)
setLibrarian(JSON.parse(savedLibrarian))
}
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
}, [])
const handleLogin = (user, token) => {
@@ -27,8 +50,12 @@ function LibrarianApp() {
}
const handleLogout = () => {
+<<<<<<< HEAD
+ librarianLogout()
+=======
localStorage.removeItem('librarianToken')
localStorage.removeItem('librarianInfo')
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
setIsLoggedIn(false)
setLibrarian(null)
}
@@ -37,7 +64,19 @@ function LibrarianApp() {
setShowRegister(false)
}
+<<<<<<< HEAD
+ if (loading) {
+ return (
+
+ )
+ }
+
+ if (isLoggedIn && librarian) {
+=======
if (isLoggedIn) {
+>>>>>>> ddb6f928a0a4d415de4bcd19023920f056be6972
return
}
diff --git a/frontend/src/librarian/LibrarianBookManager.jsx b/frontend/src/librarian/LibrarianBookManager.jsx
new file mode 100644
index 0000000..9d823d4
--- /dev/null
+++ b/frontend/src/librarian/LibrarianBookManager.jsx
@@ -0,0 +1,480 @@
+import { useEffect, useState } from 'react'
+import { LIBRARIAN_API_URL } from './api'
+
+const initialForm = {
+ title: '',
+ author: '',
+ isbn: '',
+ genre: '',
+ description: '',
+ language: 'English',
+ floor: 1,
+ libraryArea: '',
+ shelfNo: 'A',
+ shelfLevel: 1,
+};
+
+function normalizeBookToForm(book) {
+ return {
+ title: book.title || '',
+ author: book.author || '',
+ isbn: book.isbn || '',
+ genre: book.genre || '',
+ description: book.description || '',
+ language: book.language || 'English',
+ floor: book.floor || 1,
+ libraryArea: book.libraryArea || '',
+ shelfNo: book.shelfNo || 'A',
+ shelfLevel: book.shelfLevel || 1,
+ };
+}
+
+function formatDate(value) {
+ if (!value) return '暂无'
+ return new Date(value).toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+}
+
+export default function LibrarianBookManager({ librarian, onBack, onLogout }) {
+ const [books, setBooks] = useState([])
+ const [form, setForm] = useState(initialForm)
+ const [editingBookId, setEditingBookId] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [deletingId, setDeletingId] = useState(null)
+ const [error, setError] = useState('')
+ const [success, setSuccess] = useState('')
+ const isEditing = editingBookId !== null
+
+ const fetchBooks = async () => {
+ setLoading(true)
+ setError('')
+ try {
+ const response = await fetch(`${LIBRARIAN_API_URL}/books`)
+ const data = await response.json()
+ if (!response.ok) throw new Error(data.error || '获取图书列表失败')
+ setBooks(Array.isArray(data.data) ? data.data : [])
+ } catch (fetchError) {
+ setError(fetchError.message || '获取图书列表失败')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ void fetchBooks()
+ }, [])
+
+ const handleChange = (event) => {
+ const { name, value } = event.target
+ setForm((current) => ({ ...current, [name]: value }))
+ }
+
+ const handleUnauthorized = () => {
+ setError('登录状态已失效,请重新登录')
+ if (onLogout) onLogout()
+ }
+
+ const handleSubmit = async (event) => {
+ event.preventDefault()
+ setError('')
+ setSuccess('')
+
+ if (!form.title.trim() || !form.author.trim() || !form.isbn.trim() || !form.genre.trim()) {
+ setError('请填写完整的图书基础信息')
+ return
+ }
+
+ setSaving(true)
+
+ try {
+ const token = localStorage.getItem('librarianToken')
+ const response = await fetch(
+ isEditing
+ ? `${LIBRARIAN_API_URL}/books/${editingBookId}`
+ : `${LIBRARIAN_API_URL}/books`,
+ {
+ method: isEditing ? 'PUT' : 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(form),
+ }
+ )
+
+ const data = await response.json()
+
+ if (response.status === 401) {
+ handleUnauthorized()
+ return
+ }
+
+ if (!response.ok) {
+ throw new Error(data.error || (isEditing ? '更新图书失败' : '新增图书失败'))
+ }
+
+ setBooks((current) => {
+ if (isEditing) {
+ return current.map((book) => (book.id === data.book.id ? data.book : book))
+ }
+ return [...current, data.book].sort((left, right) => left.id - right.id)
+ })
+ setForm(initialForm)
+ setEditingBookId(null)
+ setSuccess(isEditing ? `已更新《${data.book.title}》` : '图书新增成功')
+ } catch (submitError) {
+ setError(submitError.message || (isEditing ? '更新图书失败' : '新增图书失败'))
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const handleEdit = (book) => {
+ setEditingBookId(book.id)
+ setForm(normalizeBookToForm(book))
+ setError('')
+ setSuccess('')
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }
+
+ const handleDelete = async (book) => {
+ const confirmed = window.confirm(`确定删除《${book.title}》吗?`)
+ if (!confirmed) return
+
+ setDeletingId(book.id)
+ setError('')
+ setSuccess('')
+
+ try {
+ const token = localStorage.getItem('librarianToken')
+ const response = await fetch(`${LIBRARIAN_API_URL}/books/${book.id}`, {
+ method: 'DELETE',
+ headers: { Authorization: `Bearer ${token}` },
+ })
+
+ const data = await response.json()
+
+ if (response.status === 401) {
+ handleUnauthorized()
+ return
+ }
+
+ if (!response.ok) {
+ throw new Error(data.error || '删除图书失败')
+ }
+
+ setBooks((current) => current.filter((item) => item.id !== book.id))
+ if (editingBookId === book.id) {
+ setEditingBookId(null)
+ setForm(initialForm)
+ }
+ setSuccess(`已删除《${book.title}》`)
+ } catch (deleteError) {
+ setError(deleteError.message || '删除图书失败')
+ } finally {
+ setDeletingId(null)
+ }
+ }
+
+ const handleReset = () => {
+ setEditingBookId(null)
+ setForm(initialForm)
+ setError('')
+ setSuccess('')
+ }
+
+ return (
+
+
+
+
+
+
+
+ 当前管理员:
+
+ {librarian?.name}({librarian?.employeeId})
+
+
+
+
+
+
+
+
+
+
+
+ {isEditing ? '编辑图书' : '新增图书'}
+
+
+ {isEditing ? '可在这里修正图书信息和书架位置' : '带 * 的字段为必填项'}
+
+
+
+ {isEditing && (
+
+ 正在编辑已有馆藏记录。修改完成后点击"保存修改",或点"取消编辑"返回新增模式。
+
+ )}
+
+
+
+
+
+
+
+
馆藏列表
+
当前共 {books.length} 本图书记录
+
+
+
+
+ {loading ? (
+ 正在加载图书列表...
+ ) : books.length === 0 ? (
+ 还没有图书记录,先在左侧新增一本吧。
+ ) : (
+
+ {books.map((book) => (
+
+
+
+
+
{book.title}
+
+ {book.genre}
+
+ 0 ? 'bg-green-50 text-green-600' : 'bg-red-50 text-red-600'
+ }`}
+ >
+ {book.availableCopies > 0 ? '可借' : '无可借副本'}
+
+
+
+
+ 作者:{book.author} | ISBN:{book.isbn}
+
+
+ 语言:{book.language || '暂无'}
+
+
+ 位置:{book.floor || 1}F {book.libraryArea || '未设置'} {book.shelfNo || 'A'}架 {book.shelfLevel || 1}层
+
+
+ 副本数:{book.totalCopies || 1} / 可借:{book.availableCopies || 0}
+
+
+ 创建时间:{formatDate(book.createdAt)}
+
+
+ {book.description && (
+
+ {book.description}
+
+ )}
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/librarian/LibrarianBorrow.jsx b/frontend/src/librarian/LibrarianBorrow.jsx
index 323b765..684b998 100644
--- a/frontend/src/librarian/LibrarianBorrow.jsx
+++ b/frontend/src/librarian/LibrarianBorrow.jsx
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react'
-
-const API_URL = 'http://localhost:3001/api'
+import { API_URL, getAuthHeaders } from './api'
export default function LibrarianBorrow() {
const [studentKeyword, setStudentKeyword] = useState('')
@@ -13,40 +12,34 @@ export default function LibrarianBorrow() {
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
-
- const token = localStorage.getItem('librarianToken')
-
- const buildHeaders = () => ({
- 'Content-Type': 'application/json',
- Authorization: token ? `Bearer ${token}` : '',
- })
+ const [stats, setStats] = useState({ total: 0, active: 0, overdue: 0 })
const searchStudents = async () => {
setError('')
setMessage('')
if (!studentKeyword.trim()) {
- setError('请输入学生学号或邮箱后再搜索。')
+ setError('请输入学生学号、姓名或邮箱')
return
}
setLoading(true)
try {
const response = await fetch(`${API_URL}/loans/users/search?keyword=${encodeURIComponent(studentKeyword)}`, {
- headers: buildHeaders(),
+ headers: getAuthHeaders('librarian'),
})
const data = await response.json()
if (!response.ok) {
- throw new Error(data.message || '学生搜索失败')
+ throw new Error(data.message || '搜索失败')
}
setStudents(data.users || [])
setSelectedStudent(null)
if ((data.users || []).length === 0) {
- setMessage('未找到匹配的学生。')
+ setMessage('未找到匹配的学生')
}
} catch (err) {
- setError(err.message || '学生搜索失败')
+ setError(err.message)
} finally {
setLoading(false)
}
@@ -56,75 +49,45 @@ export default function LibrarianBorrow() {
setError('')
setMessage('')
if (!bookKeyword.trim()) {
- setError('请输入图书名称或 ISBN 后再搜索。')
+ setError('请输入书名、作者或ISBN')
return
}
setLoading(true)
try {
const response = await fetch(`${API_URL}/loans/books/search?keyword=${encodeURIComponent(bookKeyword)}`, {
- headers: buildHeaders(),
+ headers: getAuthHeaders('librarian'),
})
const data = await response.json()
if (!response.ok) {
- throw new Error(data.message || '图书搜索失败')
+ throw new Error(data.message || '搜索失败')
}
setBooks(data.books || [])
setSelectedBook(null)
if ((data.books || []).length === 0) {
- setMessage('未找到匹配的图书。')
+ setMessage('未找到匹配的图书')
}
} catch (err) {
- setError(err.message || '图书搜索失败')
+ setError(err.message)
} finally {
setLoading(false)
}
}
const fetchLoanRecords = async () => {
- setError('')
- setMessage('')
- setLoading(true)
-
try {
const response = await fetch(`${API_URL}/loans/records`, {
- headers: buildHeaders(),
+ headers: getAuthHeaders('librarian'),
})
const data = await response.json()
- if (!response.ok) {
- throw new Error(data.message || '获取借阅记录失败')
+ if (response.ok) {
+ setLoanRecords(data.loans || [])
+ setStats(data.stats || { total: 0, active: 0, overdue: 0 })
}
- setLoanRecords(data.loans || [])
} catch (err) {
- setError(err.message || '获取借阅记录失败')
- } finally {
- setLoading(false)
- }
- }
-
- const handleReturn = async (loanId) => {
- setError('')
- setMessage('')
- setLoading(true)
-
- try {
- const response = await fetch(`${API_URL}/loans/return`, {
- method: 'POST',
- headers: buildHeaders(),
- body: JSON.stringify({ loanId }),
- })
- const data = await response.json()
- if (!response.ok) {
- throw new Error(data.message || '还书失败')
- }
- setMessage(`还书成功,归还日期:${new Date(data.returnDate).toLocaleDateString()}`)
- await fetchLoanRecords()
- } catch (err) {
- setError(err.message || '还书失败')
- } finally {
- setLoading(false)
+ console.error('获取借阅记录失败:', err)
}
}
@@ -133,7 +96,12 @@ export default function LibrarianBorrow() {
setMessage('')
if (!selectedStudent || !selectedBook) {
- setError('请先选择学生和图书。')
+ setError('请先选择学生和图书')
+ return
+ }
+
+ if (selectedBook.availableCopies === 0) {
+ setError('该图书没有可用副本')
return
}
@@ -141,8 +109,11 @@ export default function LibrarianBorrow() {
try {
const response = await fetch(`${API_URL}/loans/lend`, {
method: 'POST',
- headers: buildHeaders(),
- body: JSON.stringify({ userId: selectedStudent.id, bookId: selectedBook.id }),
+ headers: getAuthHeaders('librarian'),
+ body: JSON.stringify({
+ userId: selectedStudent.id,
+ bookId: selectedBook.id
+ }),
})
const data = await response.json()
@@ -150,7 +121,7 @@ export default function LibrarianBorrow() {
throw new Error(data.message || '借书失败')
}
- setMessage(`借书成功,归还日期:${new Date(data.loan.dueDate).toLocaleDateString()}`)
+ setMessage(`✅ ${data.message}`)
setSelectedStudent(null)
setSelectedBook(null)
setStudents([])
@@ -159,7 +130,7 @@ export default function LibrarianBorrow() {
setBookKeyword('')
fetchLoanRecords()
} catch (err) {
- setError(err.message || '借书失败')
+ setError(err.message)
} finally {
setLoading(false)
}
@@ -169,172 +140,197 @@ export default function LibrarianBorrow() {
fetchLoanRecords()
}, [])
- const renderLoanRecords = () => (
-
-
-
-
借阅记录管理
-
查看当前借阅记录并处理还书。
+ return (
+
+
+
📚 借出图书
+
搜索学生和图书,为学生办理借阅
+
+
+
+ {/* 学生搜索 */}
+
+
+
+ setStudentKeyword(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && searchStudents()}
+ className="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-200"
+ placeholder="学号 / 姓名 / 邮箱"
+ />
+
+
+ {students.length > 0 && (
+
+ {students.map((student) => (
+
setSelectedStudent(student)}
+ className={`p-3 rounded-lg cursor-pointer transition ${
+ selectedStudent?.id === student.id
+ ? 'bg-blue-50 border-2 border-blue-500'
+ : 'bg-gray-50 hover:bg-gray-100 border-2 border-transparent'
+ }`}
+ >
+
{student.name}
+
学号: {student.studentId}
+
{student.email}
+
+
+ 借阅: {student.stats?.currentBorrowCount || 0} 本
+
+ {student.stats?.hasOverdue && (
+ ⚠️ 有逾期
+ )}
+
+
+ ))}
+
+ )}
+
+
+ {/* 图书搜索 */}
+
+
+
+ setBookKeyword(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && searchBooks()}
+ className="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-200"
+ placeholder="书名 / 作者 / ISBN"
+ />
+
+
+ {books.length > 0 && (
+
+ {books.map((book) => (
+
setSelectedBook(book)}
+ className={`p-3 rounded-lg cursor-pointer transition ${
+ selectedBook?.id === book.id
+ ? 'bg-blue-50 border-2 border-blue-500'
+ : 'bg-gray-50 hover:bg-gray-100 border-2 border-transparent'
+ }`}
+ >
+
{book.title}
+
{book.author}
+
ISBN: {book.isbn}
+
+ 0
+ ? 'bg-green-100 text-green-700'
+ : 'bg-red-100 text-red-700'
+ }`}>
+ 可借: {book.availableCopies} / {book.totalCopies}
+
+
+
+ ))}
+
+ )}
-
- {loanRecords.length === 0 && (
-
当前没有在借记录。
+ {/* 已选信息 */}
+
+
+
+ 已选学生:
+
+ {selectedStudent ? `${selectedStudent.name} (${selectedStudent.studentId})` : '未选择'}
+
+
+
+ 已选图书:
+
+ {selectedBook ? `${selectedBook.title} (可借: ${selectedBook.availableCopies})` : '未选择'}
+
+
+
+
+
+ {/* 消息 */}
+ {error && (
+
+ ❌ {error}
+
+ )}
+ {message && (
+
+ {message}
+
)}
- {loanRecords.length > 0 && (
+ {/* 借书按钮 */}
+
+
+ {/* 当前借阅记录 */}
+
+
+
📋 当前借阅记录
+
+ 在借: {stats.active}
+ 逾期: {stats.overdue}
+
+
-
-
-
- | 学生 |
- 图书 |
- 借出日期 |
- 应还日期 |
- 操作 |
+
+
+
+ | 学生 |
+ 图书 |
+ 借出日期 |
+ 应还日期 |
+ 状态 |
- {loanRecords.map((loan) => (
-
- |
- {loan.user?.name || '未知'} ({loan.user?.studentId || '无'})
- |
-
- {loan.book?.title || '未知'}
- |
- {new Date(loan.checkoutDate).toLocaleDateString()} |
- {new Date(loan.dueDate).toLocaleDateString()} |
-
-
+ {loanRecords.slice(0, 10).map((loan) => (
+ |
+ | {loan.user?.name} |
+ {loan.copy?.book?.title} |
+ {new Date(loan.checkoutDate).toLocaleDateString()} |
+ {new Date(loan.dueDate).toLocaleDateString()} |
+
+
+ {loan.status === 'overdue' ? '逾期' : '在借'}
+
|
))}
- )}
-
- )
-
- return (
-
-
-
-
借出图书给学生
-
搜索学生和图书,然后为学生创建借阅记录,并自动记录应还日期。
-
-
-
-
-
-
- setStudentKeyword(e.target.value)}
- className="w-full border rounded-lg px-3 py-2"
- placeholder="按学号或邮箱搜索"
- />
-
-
- {students.length > 0 && (
-
- {students.map((student) => (
-
- ))}
-
- )}
-
-
-
-
-
- setBookKeyword(e.target.value)}
- className="w-full border rounded-lg px-3 py-2"
- placeholder="按书名或 ISBN 搜索"
- />
-
-
- {books.length > 0 && (
-
- {books.map((book) => (
-
- ))}
-
- )}
-
-
-
-
-
已选学生:{selectedStudent ? `${selectedStudent.name} (${selectedStudent.studentId})` : '未选择'}
-
已选图书:{selectedBook ? selectedBook.title : '未选择'}
-
-
- {error &&
{error}
}
- {message &&
{message}
}
-
-
-
- {renderLoanRecords()}
-
+
)
-}
+}
\ No newline at end of file
diff --git a/frontend/src/librarian/LibrarianDashboard.jsx b/frontend/src/librarian/LibrarianDashboard.jsx
index b9c6c85..8035d27 100644
--- a/frontend/src/librarian/LibrarianDashboard.jsx
+++ b/frontend/src/librarian/LibrarianDashboard.jsx
@@ -1,9 +1,51 @@
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
+import LibrarianBookManager from './LibrarianBookManager'
import LibrarianBorrow from './LibrarianBorrow'
+import LibrarianReturnBooks from './LibrarianReturnBooks'
+import { API_URL, getAuthHeaders } from './api'
export default function LibrarianDashboard({ librarian, onLogout }) {
const [showConfirm, setShowConfirm] = useState(false)
const [activeTab, setActiveTab] = useState('home')
+ const [stats, setStats] = useState({
+ totalBooks: 0,
+ activeLoans: 0,
+ overdueLoans: 0,
+ totalStudents: 0
+ })
+ const [loading, setLoading] = useState(true)
+
+ // 获取统计数据
+ useEffect(() => {
+ const fetchStats = async () => {
+ try {
+ // 获取图书数量
+ const booksRes = await fetch(`${API_URL}/books`, {
+ headers: getAuthHeaders('librarian')
+ })
+ const booksData = await booksRes.json()
+
+ // 获取借阅记录
+ const loansRes = await fetch(`${API_URL}/loans/records`, {
+ headers: getAuthHeaders('librarian')
+ })
+ const loansData = await loansRes.json()
+
+ setStats({
+ totalBooks: booksData.data?.length || 0,
+ activeLoans: loansData.stats?.active || 0,
+ overdueLoans: loansData.stats?.overdue || 0,
+ totalStudents: 0 // 可以从其他接口获取
+ })
+ } catch (error) {
+ console.error('获取统计数据失败:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchStats()
+ }, [])
const handleLogout = () => {
localStorage.removeItem('librarianToken')
@@ -17,114 +59,298 @@ export default function LibrarianDashboard({ librarian, onLogout }) {
// 获取当前时间问候语
const getGreeting = () => {
const hour = new Date().getHours()
- if (hour < 12) return '早上好'
+ if (hour < 6) return '夜深了'
+ if (hour < 9) return '早上好'
+ if (hour < 12) return '上午好'
+ if (hour < 14) return '中午好'
if (hour < 18) return '下午好'
- return '晚上好'
+ if (hour < 22) return '晚上好'
+ return '夜深了'
}
- return (
-
- {/* 顶部导航栏 */}
-
-
-
-
📚
-
图书馆管理系统
-
图书管理员
+ // 获取当前日期
+ const getCurrentDate = () => {
+ const now = new Date()
+ const year = now.getFullYear()
+ const month = String(now.getMonth() + 1).padStart(2, '0')
+ const day = String(now.getDate()).padStart(2, '0')
+ const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+ const weekday = weekdays[now.getDay()]
+ return `${year}年${month}月${day}日 ${weekday}`
+ }
+
+ // 渲染不同内容
+ const renderContent = () => {
+ // 图书管理页面
+ if (activeTab === 'books') {
+ return (
+
setActiveTab('home')}
+ onLogout={handleLogout}
+ />
+ )
+ }
+
+ // 借阅管理页面(借书)
+ if (activeTab === 'borrow') {
+ return (
+
+
+
+
+ )
+ }
+
+ // 还书管理页面
+ if (activeTab === 'return') {
+ return (
+ setActiveTab('home')} />
+ )
+ }
+
+ // 首页仪表盘
+ return (
+ <>
+ {/* 欢迎卡片 */}
+
+
+
+
{getCurrentDate()}
+
+ {getGreeting()},{librarian?.name}!
+
+
欢迎回到图书馆管理系统
+
+
📚
+
+
+
+ {/* 统计卡片 */}
+
+
setActiveTab('books')}>
+
+
+
馆藏图书
+
+ {loading ? '...' : stats.totalBooks}
+
+
本
+
+
📖
+
+
+
+
setActiveTab('borrow')}>
+
+
+
在借图书
+
+ {loading ? '...' : stats.activeLoans}
+
+
本
+
+
📋
+
+
+
+
setActiveTab('return')}>
+
+
+
逾期图书
+
0 ? 'text-red-600' : 'text-gray-800'}`}>
+ {loading ? '...' : stats.overdueLoans}
+
+
本待处理
+
+
⚠️
+
-
-
-
{getGreeting()}
-
{librarian?.name}
-
工号:{librarian?.employeeId}
+
+
+
-
-
- {/* 主要内容 */}
-
- {/* 欢迎卡片 */}
-
-
- {getGreeting()},{librarian?.name}!
-
-
欢迎回来,您可以通过下方功能管理图书馆系统。
+ {/* 功能卡片 */}
+
⚡ 快速操作
+
+ {/* 图书管理卡片 */}
+
setActiveTab('books')}
+ >
+
📖
+
图书管理
+
添加新书、编辑信息、管理副本、删除图书记录
+
+ 进入图书管理 →
+
+
+
+ {/* 借书管理卡片 */}
+
setActiveTab('borrow')}
+ >
+
📤
+
借出图书
+
搜索学生、查找图书、办理借阅、记录应还日期
+
+ 进入借书管理 →
+
+
+
+ {/* 还书管理卡片 */}
+
setActiveTab('return')}
+ >
+
📥
+
归还图书
+
查看在借记录、接收归还、处理逾期罚款
+
+ 进入还书管理 →
+
+
- {activeTab === 'home' ? (
- <>
- {/* 功能卡片 */}
-
-
-
📖
-
图书管理
-
添加、编辑、删除图书信息
-
-
+ {/* 快捷提示 */}
+
+
+
💡
+
+
操作提示
+
+ - • 借书前请确认学生是否有逾期未还的图书
+ - • 还书时系统会自动计算逾期罚款金额
+ - • 删除图书前请确保没有未归还的借阅记录
+
+
+
+
+ >
+ )
+ }
-
-
📋
-
借阅管理
-
管理借阅记录、处理还书
+ return (
+
+ {/* 顶部导航栏 */}
+
+
+
+
+
+
+ {/* 快捷导航 */}
+
+
+
+
-
-
👥
-
读者管理
-
查看读者信息、借阅历史
-
- >
- ) : (
-
- setActiveTab('home')}
- className="bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition"
- >
- ← 返回仪表盘
-
-
- )}
+
+
+
+ {/* 主要内容 */}
+
+ {renderContent()}
{/* 退出确认弹窗 */}
{showConfirm && (
-
-
确认退出
-
确定要退出登录吗?
+
+
setShowConfirm(false)}
+ className="flex-1 bg-gray-200 text-gray-700 py-2.5 rounded-lg hover:bg-gray-300 transition font-semibold"
>
- 确定
+ 取消
setShowConfirm(false)}
- className="flex-1 bg-gray-300 text-gray-700 py-2 rounded-lg hover:bg-gray-400 transition"
+ onClick={handleLogout}
+ className="flex-1 bg-red-500 text-white py-2.5 rounded-lg hover:bg-red-600 transition font-semibold"
>
- 取消
+ 确定退出
diff --git a/frontend/src/librarian/LibrarianLogin.jsx b/frontend/src/librarian/LibrarianLogin.jsx
index e088f65..a50dfee 100644
--- a/frontend/src/librarian/LibrarianLogin.jsx
+++ b/frontend/src/librarian/LibrarianLogin.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
-const API_URL = 'http://localhost:3001/api'
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api'
export default function LibrarianLogin({ onLogin, onSwitchToRegister }) {
const [employeeId, setEmployeeId] = useState('')
@@ -20,7 +20,7 @@ export default function LibrarianLogin({ onLogin, onSwitchToRegister }) {
}
}, [])
- // 实时验证工号
+ // 实时验证
const validateEmployeeId = (value) => {
if (!value.trim()) {
setFieldErrors(prev => ({ ...prev, employeeId: '请输入工号' }))
@@ -30,7 +30,6 @@ export default function LibrarianLogin({ onLogin, onSwitchToRegister }) {
return true
}
- // 实时验证密码
const validatePassword = (value) => {
if (!value) {
setFieldErrors(prev => ({ ...prev, password: '请输入密码' }))
@@ -40,18 +39,6 @@ export default function LibrarianLogin({ onLogin, onSwitchToRegister }) {
return true
}
- const handleEmployeeIdChange = (e) => {
- const value = e.target.value
- setEmployeeId(value)
- validateEmployeeId(value)
- }
-
- const handlePasswordChange = (e) => {
- const value = e.target.value
- setPassword(value)
- validatePassword(value)
- }
-
const handleSubmit = async (e) => {
e.preventDefault()
@@ -67,18 +54,20 @@ export default function LibrarianLogin({ onLogin, onSwitchToRegister }) {
setLoading(true)
try {
- const res = await fetch('http://localhost:3001/api/auth/login', {
+ const res = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email: employeeId, password, type: 'librarian' })
+ body: JSON.stringify({
+ email: employeeId, // 馆员登录使用工号作为 email 字段
+ password,
+ type: 'librarian'
+ })
})
const data = await res.json()
if (!res.ok) {
- setError(data.error || '登录失败')
- setLoading(false)
- return
+ throw new Error(data.error || '登录失败')
}
// 记住工号
@@ -88,36 +77,45 @@ export default function LibrarianLogin({ onLogin, onSwitchToRegister }) {
localStorage.removeItem('savedEmployeeId')
}
+ // 保存 token 和用户信息
+ localStorage.setItem('librarianToken', data.token)
+ localStorage.setItem('librarianInfo', JSON.stringify(data.librarian))
+
onLogin(data.librarian, data.token)
} catch (err) {
- setError('网络错误,请确保后端已启动')
+ setError(err.message || '网络错误,请确保后端已启动')
+ } finally {
setLoading(false)
}
}
return (
-
-
+
+
-
图书管理员登录
+
📚
+
图书管理员登录
欢迎回来!请登录您的账号